diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml deleted file mode 100644 index 827cc9c64eb..00000000000 --- a/.github/workflows/benchmarks.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Benchmarks -on: - workflow_dispatch: - schedule: - - cron: '0 5 * * *' - push: - tags: - - 'v*' - -concurrency: benchmarks - -jobs: - benchmark: - name: Run Benchmarks - runs-on: ubuntu-latest - steps: - - name: Run script on centcomm - uses: appleboy/ssh-action@master - with: - host: centcomm.spacestation14.io - username: robust-benchmark-runner - key: ${{ secrets.CENTCOMM_ROBUST_BENCHMARK_RUNNER_KEY }} - command_timeout: 100000m - script: | - mkdir benchmark_run_${{ github.sha }} - cd benchmark_run_${{ github.sha }} - git clone https://github.com/space-wizards/RobustToolbox.git repo_dir --recursive - cd repo_dir - git checkout ${{ github.sha }} - cd Robust.Benchmarks - dotnet restore - export ROBUST_BENCHMARKS_ENABLE_SQL=1 - export ROBUST_BENCHMARKS_SQL_ADDRESS="${{ secrets.BENCHMARKS_WRITE_ADDRESS }}" - export ROBUST_BENCHMARKS_SQL_PORT="${{ secrets.BENCHMARKS_WRITE_PORT }}" - export ROBUST_BENCHMARKS_SQL_USER="${{ secrets.BENCHMARKS_WRITE_USER }}" - export ROBUST_BENCHMARKS_SQL_PASSWORD="${{ secrets.BENCHMARKS_WRITE_PASSWORD }}" - export ROBUST_BENCHMARKS_SQL_DATABASE="benchmarks" - export GITHUB_SHA="${{ github.sha }}" - dotnet run --filter '*' --configuration Release - cd ../../.. - rm -rf benchmark_run_${{ github.sha }} diff --git a/.github/workflows/build-all-configurations.yml b/.github/workflows/build-all-configurations.yml index 2f209bda4ef..fc969e44672 100644 --- a/.github/workflows/build-all-configurations.yml +++ b/.github/workflows/build-all-configurations.yml @@ -4,8 +4,13 @@ on: push: branches: [master] pull_request: + types: [ opened, reopened, synchronize, ready_for_review ] branches: [master] +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name != 'merge_group' }} + jobs: build: strategy: diff --git a/.github/workflows/build-docfx.yml b/.github/workflows/build-docfx.yml deleted file mode 100644 index 46f443ac23b..00000000000 --- a/.github/workflows/build-docfx.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Build & Publish DocFX - -on: - schedule: - - cron: "0 0 * * 0" -jobs: - docfx: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4.2.2 - with: - submodules: true - - - name: Setup .NET Core - uses: actions/setup-dotnet@v4.1.0 - with: - dotnet-version: 10.0.x - - - name: Install dependencies - run: dotnet restore - - - name: Build Project - run: dotnet build --no-restore /p:WarningsAsErrors=nullable - - - name: Build DocFX - uses: nikeee/docfx-action@v1.0.0 - with: - args: Robust.Docfx/docfx.json - - - name: Publish Docfx Documentation on GitHub Pages - uses: maxheld83/ghpages@master - env: - BUILD_DIR: Robust.Docfx/_robust-site - GH_PAT: ${{ secrets.GH_PAT }} diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 87cc7573d36..3a0d8a41cd0 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -4,8 +4,13 @@ on: push: branches: [master] pull_request: + types: [ opened, reopened, synchronize, ready_for_review ] branches: [master] +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name != 'merge_group' }} + jobs: build: strategy: @@ -13,6 +18,7 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} + timeout-minutes: 30 steps: - uses: actions/checkout@v4.2.2 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index ae600a3be6d..00000000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,71 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - workflow_dispatch - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - language: ["csharp"] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed - - steps: - - name: Checkout repository - uses: actions/checkout@v4.2.2 - with: - submodules: true - - - name: Setup .NET Core - uses: actions/setup-dotnet@v4.1.0 - with: - dotnet-version: 7.0.x - - - name: Build - run: dotnet build - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/publish-client.yml b/.github/workflows/publish-client.yml index 4767740a2ff..5319e4bec30 100644 --- a/.github/workflows/publish-client.yml +++ b/.github/workflows/publish-client.yml @@ -33,10 +33,10 @@ jobs: mkdir "release/${{ steps.parse_version.outputs.version }}" mv release/*.zip "release/${{ steps.parse_version.outputs.version }}" - - name: Upload files to Suns + - name: Upload files to Dropwig uses: appleboy/scp-action@master with: - host: suns.spacestation14.com + host: dropwig.playss14.com username: robust-build-push key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }} source: "release/${{ steps.parse_version.outputs.version }}" @@ -46,7 +46,7 @@ jobs: - name: Update manifest JSON uses: appleboy/ssh-action@master with: - host: suns.spacestation14.com + host: dropwig.playss14.com username: robust-build-push key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }} script: /home/robust-build-push/push.ps1 ${{ steps.parse_version.outputs.version }} diff --git a/.github/workflows/test-content.yml b/.github/workflows/test-content.yml index f8014cc945a..2bdd0c06291 100644 --- a/.github/workflows/test-content.yml +++ b/.github/workflows/test-content.yml @@ -4,11 +4,17 @@ on: push: branches: [master] pull_request: + types: [ opened, reopened, synchronize, ready_for_review ] branches: [master] +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name != 'merge_group' }} + jobs: build: runs-on: ubuntu-latest + timeout-minutes: 30 steps: - name: Check out content @@ -34,7 +40,9 @@ jobs: run: cp RobustToolbox/global.json . - name: Install dependencies run: dotnet restore - - name: Build + - name: Build (Release) + run: dotnet build --configuration Release --no-restore /m + - name: Build (Debug) run: dotnet build --configuration DebugOpt --no-restore /m - name: Content.Tests shell: pwsh diff --git a/.gitmodules b/.gitmodules index ced7b604ef7..4f7f4889a01 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,7 +3,7 @@ url = https://github.com/space-wizards/netserializer [submodule "Lidgren.Network"] path = Lidgren.Network/Lidgren.Network - url = https://github.com/ss14Starlight/SpaceWizards.Lidgren.Network.git + url = https://github.com/space-wizards/SpaceWizards.Lidgren.Network.git [submodule "XamlX"] path = XamlX url = https://github.com/space-wizards/XamlX diff --git a/Directory.Build.props b/Directory.Build.props index 446a43ae83c..8d998165911 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -24,4 +24,10 @@ --> $(NoWarn);NU1510 + + + + false + diff --git a/Directory.Packages.props b/Directory.Packages.props index 69000ce2613..e740d3a3e38 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -48,7 +48,7 @@ - + @@ -69,6 +69,7 @@ + diff --git a/Lidgren.Network/Lidgren.Network b/Lidgren.Network/Lidgren.Network index b060f75c6bd..68a5b883d5c 160000 --- a/Lidgren.Network/Lidgren.Network +++ b/Lidgren.Network/Lidgren.Network @@ -1 +1 @@ -Subproject commit b060f75c6bde4a0bd0623da15b7eecf9f4eca5cb +Subproject commit 68a5b883d5c3f5d4eabda8f04a2f4fd1afce12f4 diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props index bac96a1d378..befe6da3636 100644 --- a/MSBuild/Robust.Engine.Version.props +++ b/MSBuild/Robust.Engine.Version.props @@ -1,4 +1,4 @@ - 277.0.0 + 283.0.0 diff --git a/MSBuild/Robust.EntitySystemSubscriptionsGenerator.targets b/MSBuild/Robust.EntitySystemSubscriptionsGenerator.targets new file mode 100644 index 00000000000..dd74af58764 --- /dev/null +++ b/MSBuild/Robust.EntitySystemSubscriptionsGenerator.targets @@ -0,0 +1,5 @@ + + + + + diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index e60a7b1ed48..5eb382e8722 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -54,6 +54,251 @@ END TEMPLATE--> *None yet* +## 283.0.0 + +### Breaking changes + +* The CompNetworkGenerator now .Clears and .Adds for collections where possible on the client. +* Defer UI state application until the Update loop. +* Expose the file name for selected file in dialog. +* Removed unused GridEventHandler. + +### New features + +* Added a NetMessage.EstimateBufferSize() to provide an estimate of the initialCapacity required for NetMessages. This will make NetMessage take an existing adequately sized pooled buffer for your message. This is opt-in and will default to the old 4-byte size if not specified. +* Add AudioParams.AddVariation. +* Made AudioAuxiliaryComponent public. +* Added API for OwnedTexture to load a texture without caching it. +* Make TableContainer virtual and public. +* Try to make connection failure message more useful by not defaulting to the first message (on IPV4 or IPV6). +* Add methods for generating Color palettes. +* Expose FovRenderTarget for the viewport. +* Add TryComp(EntityUid, Type, IComponent?) proxy methods to EntitySystem. + +### Bugfixes + +* Static + StaticSundries will now also be considered for grid-traversal when a grid moves over these entities. +* Fix field deltas not supporting 1-field states. +* Fix interface cast in serialization generator. +* Dispose PVS session states on disconnect. +* Fix BaseWindow jittering on resize. + +### Internal + +* Improve clean and incremental build times around serialization generators. +* Cleanup engine warnings around transform handling. +* Made several internal performance improvements on debug and release. + + +## 282.0.0 + +### Breaking changes + +* Serv5 has been merged, re-doing the internals of DataDefinition serialization. + * It can now write into readonly fields. + * DataFields defined on objects that don't have DataDefinitions will cause errors in the analyzer and require the attribute. + * Value types will now be copied directly where possible rather than round-tripping through TryCustomCopy if no custom serializer is specified. + * It no longer uses as many expression trees so more tests should be able to run concurrently + + +## 281.0.0 + +### Breaking changes + +* Updated Lidgren.Network to `04678d057cc503f14f49801a725e61cfe27790a0` with additional fixes around MTU handling, NAT handling, and malformed packets. + +### New features + +* Exposed new Lidgren properties/CVARs for the above-mentioned fixes and previous updates around rate-limit settings. + +### Bugfixes + +* Fixed `EntitySystemSubscriptionsGenerator` not targeting server-side `SubscribeLocalEvent`/`SubscribeNetworkEvent` attributes. + + +## 280.0.1 + +### Bugfixes + +* Fix DynamicTree.Clear not removing node references. +* Reverted validation for `UiBox2i` `ctor`s as it was causing regressions in debug UIs. +* Fix command completions not being ordered. The list will still populate by any commands that contain the supplied arg. +* Lidgren rate-limit settings were tweaked to make it less likely that players will unintentionally trigger it + +### Other + +* `EyeComponent.DrawLight` is now serialized. + + +## 280.0.0 + +### Breaking changes + +* Validate UIBox2i inputs +* IMapManager has been completely nuked from the codebase. Almost all of its content-facing functionality was ported to `SharedMapSystem` in https://github.com/space-wizards/RobustToolbox/pull/6579 beforehand. + +### New features + +* Lidgren now rate-limits logging. You can control this via the `net.lidgren_log_rate_...` CVars. + +### Bugfixes + +* Fixed Lidgren.ChatClient/ChatServer not building properly. +* Fixed multiple sources of memory exhaustion/DOS attack surfaces in Lidgren. + + +## 279.0.1 + +### Bugfixes + +* Fix SubscribeLocalEvent name and added `MeansImplicitUse` attribute to the sourcegenned eventbus methods. + + +## 279.0.0 + +### Breaking changes + +* Validate Box2i inputs to ensure no negative-sized boxes. +* Removed QuadTree due to lack of maintenance, test coverage, and usage. +* Partivally reverted the additional mouse cursors on button hovers. +* Changed SpawnAtPosition EntityCoordinates overload to use the attached entity's rotation, and also added a rotation override argument. +* Reduced the default ReallyBeIdle tick count from 25 to 5. + +### New features + +* Added DictionaryEquals helper method to compare elements to determine if 2 dictionaries are identical. +* Tracy integration is now supported for profiling. +* SubscribeLocalEvent and SubscribeNetworkEvent can now be replaced with the similarly named attributes on methods. +* Adds `AttachedAudioDespawnedEvent`, which is raised against the parent of a despawning `AudioComponent`. +* Added a DictionaryEquals extension method to check equality between two dictionaries. +* Added support for uploading .ftl files. Format is: //.ftl - Example: /TestUpload/en-US.ftl , you can have multiple files, so long they are on different subfolders, they will all be loaded. + +### Bugfixes + +* Fix IsHardCollidable mask check. +* Fix Robust.Benchmarks not compiling in some instances. +* Fix ApplyLinearImpulse not correctly using world-space. +* Fix OnClientRequestFull throwing an error when logging deleted entities. + +### Internal + +* Unnecessary prototypemanager dependencies were removed from engine systems. +* Added a 30m timeout to engine test workflows. +* Cleaned up ContainerSystems and PlayerManagers code-files. + + +## 278.0.0 + +### Breaking changes + +* Remove the duplicate serialization copy of components kept on ComponentRegistryEntry; now it only stores the deserialized component. To get the raw MappingDataNode for EntityPrototypes use PrototypeManager. This is expected to significantly reduce memory usage. +* Obsolete LocalRotation in favor of the system method. The angle is now also normalized to 2PI and no longer grows indefinitely. +* Obsoleted IMapManager methods in lieu of SharedMapSystem. + +### New features + +* Add a `OnlyRotation` property to MoveEvent where the EntityCoordinates remain the same. +* ValueList now implements IList and not just IEnumerable. +* Add a BoundUserInterfaceMessageReceivedEvent that will be raised whenever a BoundUserInterfaceMessage is received regardless of validation. +* Added `IsHardCollidable` to `SharedPhysicsSystem`. +* Added `GetFilledTileCount` to `SharedMapSystem`. +* Changed the cursors on interactive controls. +* Added new `StyleProperty` `track` to `ScrollBar` that takes a `StyleBox` and displays it as a backing track for the whole height of the `ScrollBar`. +* Scroll Lock is now a bindable key. +* The obsoleted TryIndex methods on PrototypeManager have now been removed. +* Add batched Box2 / Box2Rotated drawing methods to Clyde WorldHandle. +* Completion filter now works by Contains instead of StartsWith +* Add SwitchAudioDevice API to AudioManager. +* Added a PhysicsBodyStatusChangedEvent (self-descriptive). +* Allow enumeration on EntityQuery. + +### Bugfixes + +* Fix ValueList TryPop not clearing element references. +* Fix ValueList Peek always throwing by referencing the wrong index. +* Windows will stay at relative position not absolute pixel position on window resize. +* Fixed override properties in `WrapContainer` not actually overriding the Style Properties. +* Fixed `BoxContainer`'s `SeparationOverride` not overriding the Style Properties. +* Fixed `SeparationOverride` not invalidating measure. +* Fixed swapped parameters in `MapManager`'s `FindGridsIntersecting` methods. +* Fix DataRecord serialization. + +### Other + +* Optimise Direction and DirectionFlag methods. +* Added Pure attributes to the `EntityLookup` bounds methods. +* Improved performance of collision filter test. +* Removed an outdated xmldoc comment regarding dependency injection. +* Audio resources now use `AsSpan` when checking signatures. + +### Internal + +* Reduce TryParseEnum string allocations. +* Reduce TryRelativeTo string allocations. +* Reduce OpenGL logging string allocations on debug for the client. +* Optimise sprite sorting slightly. +* Simplify and optimise Box2.Contains(Vector2) +* Optimise ComponentRegistry deserialization slightly. +* Optimise Box2Rotated.TransformBox slightly. +* Added several test helpers to avoid boilerplate in integration tests around client connection / disconnection. +* Simplifed AccessAnalyzer check to speed it up. + +## 277.2.1 + +### Bugfixes + +* Fixed a bug in Lidgren.Network HandleReleasedFragment that could cause out of memory errors. + + +## 277.2.0 + +### New features + +* Local and guest trust scores are now assigned via the `auth.localtrust` and `auth.guesttrust` CVars. + +### Bugfixes + +* Fixed exceptions related to WebSocket transfer system. + + +## 277.1.0 + +### New features + +* Console commands can now be "hidden" by prefixing them with `_`. +* Add `ESCAPE()` and `ESCAPE-PARAM()` localization functions, for escaping text for markup formatting. +* The `[cmdlink /]` tag can now have a tooltip specified with the optional "title" attribute. +* Added `CommandParsing.EscapeCommand()` for *formatting* command strings easily. +* Added `IUserInterfaceManager.GetRootForMouse()`. +* Added `Popup.OpenAtCursor()`. +* Added `FormattedStringBuilder` for safely constructing markup with code. +* Added `VBox` and `HBox` convenience types for more concisely construct `BoxContainer`s. +* Added `IsLocal` to `NetUserData`. +* Add `SharedMapSystem.GetFilledTileCount()` +* Add a `track` style property for scroll bars. +* Added `ScrollLock` key. + +### Bugfixes + +* Fixes override properties in `WrapContainer` not being respected properly. +* Fix `BoxContainer.SeparationOverride` not being respected properly and not invalidating layout. +* Fixed swapped arguments being passed through in various `FindGridsIntersecting` overloads. +* Fixed a doc comment in `LocalizationManager`. + +### Other + +* Add Pure attributes to the EntityLookup bounds methods +* Bump `Robust.Natives` to `0.2.5`. +* Minor performance improvement in `IsHardCollidable()`. +* Remove an outdated paragraph from `[DependencyAttribute]` documentation related to `readonly` fields. +* More stock controls use alternative cursor shapes where appropriate. +* Minor performance improvement to audio loading. + +### Internal + +* Added `.lscache` to `.gitignore`. + + ## 277.0.0 ### New features diff --git a/Resources/Locale/en-US/commands.ftl b/Resources/Locale/en-US/commands.ftl index 4d4e3264618..fcafc42eedf 100644 --- a/Resources/Locale/en-US/commands.ftl +++ b/Resources/Locale/en-US/commands.ftl @@ -306,6 +306,15 @@ cmd-addmap-help = Usage: {$command} [pre-init] cmd-rmmap-desc = Removes a map from the world. You cannot remove nullspace. cmd-rmmap-help = Usage: {$command} +cmd-pausemap-desc = Pauses a map, pausing all simulation processing on it. +cmd-pausemap-help = Usage: pausemap + +cmd-unpausemap-desc = Unpauses a map, resuming all simulation processing on it. +cmd-unpausemap-help = Usage: unpausemap + +cmd-querymappaused-desc = Check whether a map is paused or not. +cmd-querymappaused-help = Usage: querymappaused + cmd-savegrid-desc = Serializes a grid to disk. cmd-savegrid-help = Usage: {$command} diff --git a/Robust.Analyzers.Tests/DataDefinitionAnalyzerTest.cs b/Robust.Analyzers.Tests/DataDefinitionAnalyzerTest.cs index 7e49abc0131..cf417d53502 100644 --- a/Robust.Analyzers.Tests/DataDefinitionAnalyzerTest.cs +++ b/Robust.Analyzers.Tests/DataDefinitionAnalyzerTest.cs @@ -94,29 +94,6 @@ await Verifier(code, ); } - [Test] - public async Task ReadOnlyFieldTest() - { - const string code = """ - using Robust.Shared.Serialization.Manager.Attributes; - - [DataDefinition] - public sealed partial class Foo - { - [DataField] - public readonly int Bad; - - [DataField] - public int Good; - } - """; - - await Verifier(code, - // /0/Test0.cs(7,12): error RA0019: Data field Bad in data definition Foo is readonly - VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldWritableRule).WithSpan(7, 12, 7, 20).WithArguments("Bad", "Foo") - ); - } - [Test] public async Task PartialDataDefinitionTest() { diff --git a/Robust.Analyzers/AccessAnalyzer.cs b/Robust.Analyzers/AccessAnalyzer.cs index 3068d806dd5..7a37281617e 100644 --- a/Robust.Analyzers/AccessAnalyzer.cs +++ b/Robust.Analyzers/AccessAnalyzer.cs @@ -1,7 +1,5 @@ -using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; @@ -14,7 +12,6 @@ namespace Robust.Analyzers public class AccessAnalyzer : DiagnosticAnalyzer { private const string AccessAttributeType = "Robust.Shared.Analyzers.AccessAttribute"; - private const string RobustAutoGeneratedAttributeType = "Robust.Shared.Analyzers.RobustAutoGeneratedAttribute"; private const string PureAttributeType = "System.Diagnostics.Contracts.PureAttribute"; [SuppressMessage("ReSharper", "RS2008")] @@ -32,7 +29,7 @@ public class AccessAnalyzer : DiagnosticAnalyzer public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterOperationAction(CheckFriendship, OperationKind.FieldReference, @@ -76,20 +73,11 @@ private void CheckFriendship(OperationAnalysisContext context) // Get the attributes var friendAttribute = context.Compilation.GetTypeByMetadataName(AccessAttributeType); - var autoGenAttribute = context.Compilation.GetTypeByMetadataName(RobustAutoGeneratedAttributeType); // Get the type that is containing this expression, or, the type where this is happening. if (context.ContainingSymbol?.ContainingType is not {} accessingType) return; - // Should we ignore the access attempt due to the accessing type being auto-generated? - if (accessingType.GetAttributes().FirstOrDefault(a => - a.AttributeClass != null && - a.AttributeClass.Equals(autoGenAttribute, SymbolEqualityComparer.Default)) is { } attr) - { - return; - } - // Determine which type of access is happening here... Read, write or execute? var accessAttempt = DetermineAccess(context, targetAccess, operation); diff --git a/Robust.Analyzers/DataDefinitionAnalyzer.cs b/Robust.Analyzers/DataDefinitionAnalyzer.cs index d5f3fd62267..40959273d6e 100644 --- a/Robust.Analyzers/DataDefinitionAnalyzer.cs +++ b/Robust.Analyzers/DataDefinitionAnalyzer.cs @@ -1,11 +1,13 @@ #nullable enable using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Robust.Roslyn.Shared; +using Robust.Roslyn.Shared.Helpers; using Robust.Shared.Serialization.Manager.Definition; using Robust.Shared.ViewVariables; @@ -14,9 +16,6 @@ namespace Robust.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] 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"; @@ -43,16 +42,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer "Make sure to mark any type containing a nested data definition as partial." ); - public static readonly DiagnosticDescriptor DataFieldWritableRule = new( - Diagnostics.IdDataFieldWritable, - "Data field must not be readonly", - "Data field {0} in data definition {1} is readonly", - "Usage", - DiagnosticSeverity.Error, - true, - "Make sure to remove the readonly modifier." - ); - public static readonly DiagnosticDescriptor DataFieldPropertyWritableRule = new( Diagnostics.IdDataFieldPropertyWritable, "Data field property must have a setter", @@ -93,9 +82,24 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer "Make sure to use a type that is YAML serializable." ); + public static readonly DiagnosticDescriptor DataFieldOutsideDefinition = new( + Diagnostics.IdDataFieldOutsideDefinition, + "Data field defined in a type that is not marked as a data definition or data record", + "Data field {0} is defined in type {1} which is not a data definition or data record", + "Usage", + DiagnosticSeverity.Error, + true, + "Make sure to add a data definition or data record attribute, or inherit a type or add an attribute that implicitly makes its inheritors data definitions or data records." + ); + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( - DataDefinitionPartialRule, NestedDataDefinitionPartialRule, DataFieldWritableRule, DataFieldPropertyWritableRule, - DataFieldRedundantTagRule, DataFieldNoVVReadWriteRule, DataFieldYamlSerializableRule + DataDefinitionPartialRule, + NestedDataDefinitionPartialRule, + DataFieldPropertyWritableRule, + DataFieldRedundantTagRule, + DataFieldNoVVReadWriteRule, + DataFieldYamlSerializableRule, + DataFieldOutsideDefinition ); public override void Initialize(AnalysisContext context) @@ -108,6 +112,9 @@ public override void Initialize(AnalysisContext context) if (symbolContext.Symbol is not INamedTypeSymbol typeSymbol) return; + symbolContext.RegisterSyntaxNodeAction(AnalyzeDataField, SyntaxKind.FieldDeclaration); + symbolContext.RegisterSyntaxNodeAction(AnalyzeDataFieldProperty, SyntaxKind.PropertyDeclaration); + if (!IsDataDefinition(typeSymbol)) return; @@ -116,9 +123,6 @@ public override void Initialize(AnalysisContext context) symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordDeclaration); symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordStructDeclaration); symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.InterfaceDeclaration); - - symbolContext.RegisterSyntaxNodeAction(AnalyzeDataField, SyntaxKind.FieldDeclaration); - symbolContext.RegisterSyntaxNodeAction(AnalyzeDataFieldProperty, SyntaxKind.PropertyDeclaration); }, SymbolKind.NamedType); } @@ -156,6 +160,7 @@ private static void AnalyzeDataField(SyntaxNodeAnalysisContext context) if (context.ContainingSymbol?.ContainingType is not INamedTypeSymbol type) return; + var isDataDefinition = DataDefinitionHelper.IsDataDefinition(type, out var isDataRecord); foreach (var variable in field.Declaration.Variables) { var fieldSymbol = context.SemanticModel.GetDeclaredSymbol(variable); @@ -163,13 +168,13 @@ private static void AnalyzeDataField(SyntaxNodeAnalysisContext context) if (fieldSymbol == null) continue; - if (!IsDataField(fieldSymbol, out _, out var datafieldAttribute)) + if (!DataDefinitionHelper.IsDataField(fieldSymbol, isDataRecord, out _, out var datafieldAttribute)) continue; - if (IsReadOnlyDataField(type, fieldSymbol)) + if (!isDataDefinition) { - TryGetModifierLocation(field, SyntaxKind.ReadOnlyKeyword, out var location); - context.ReportDiagnostic(Diagnostic.Create(DataFieldWritableRule, location, fieldSymbol.Name, type.Name)); + var location = type.DeclaringSyntaxReferences[0].GetSyntax().GetLocation(); + context.ReportDiagnostic(Diagnostic.Create(DataFieldOutsideDefinition, location, fieldSymbol.Name, type.Name)); } if (HasRedundantTag(fieldSymbol, datafieldAttribute)) @@ -212,19 +217,30 @@ private static void AnalyzeDataFieldProperty(SyntaxNodeAnalysisContext context) if (propertySymbol.ContainingType is not INamedTypeSymbol type) return; - if (type.IsRecord || type.IsValueType) + if (propertySymbol == null) return; - if (propertySymbol == null) + var isDataDefinition = DataDefinitionHelper.IsDataDefinition(type, out var isDataRecord); + if (isDataDefinition && + !isDataRecord && + propertySymbol.SetMethod == null && + HasDataFieldAttribute(propertySymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + DataFieldPropertyWritableRule, + property.AccessorList?.GetLocation() ?? property.GetLocation(), + propertySymbol.Name, + type.Name)); return; + } - if (!IsDataField(propertySymbol, out _, out var datafieldAttribute)) + if (!DataDefinitionHelper.IsDataField(propertySymbol, isDataRecord, out _, out var datafieldAttribute)) return; - if (IsReadOnlyDataField(type, propertySymbol)) + if (!isDataDefinition) { - var location = property.AccessorList != null ? property.AccessorList.GetLocation() : property.GetLocation(); - context.ReportDiagnostic(Diagnostic.Create(DataFieldPropertyWritableRule, location, propertySymbol.Name, type.Name)); + var location = type.DeclaringSyntaxReferences[0].GetSyntax().GetLocation(); + context.ReportDiagnostic(Diagnostic.Create(DataFieldOutsideDefinition, location, propertySymbol.Name, type.Name)); } if (HasRedundantTag(propertySymbol, datafieldAttribute)) @@ -255,9 +271,16 @@ private static void AnalyzeDataFieldProperty(SyntaxNodeAnalysisContext context) } } - private static bool IsReadOnlyDataField(ITypeSymbol type, ISymbol field) + private static bool HasDataFieldAttribute(ISymbol symbol) { - return IsReadOnlyMember(type, field); + foreach (var attribute in symbol.GetAttributes()) + { + if (attribute.AttributeClass is { } attributeClass && + TypeSymbolHelper.Inherits(attributeClass, DataFieldBaseNamespace)) + return true; + } + + return false; } private static bool IsPartial(TypeDeclarationSyntax type) @@ -270,53 +293,7 @@ private static bool IsDataDefinition(ITypeSymbol? type) if (type == null) return false; - return HasAttribute(type, DataDefinitionNamespace) || - MeansDataDefinition(type) || - IsImplicitDataDefinition(type); - } - - private static bool IsDataField(ISymbol member, out ITypeSymbol type, out AttributeData attribute) - { - // TODO data records and other attributes - if (member is IFieldSymbol field) - { - foreach (var attr in field.GetAttributes()) - { - if (attr.AttributeClass != null && Inherits(attr.AttributeClass, DataFieldBaseNamespace)) - { - type = field.Type; - attribute = attr; - return true; - } - } - } - else if (member is IPropertySymbol property) - { - foreach (var attr in property.GetAttributes()) - { - if (attr.AttributeClass != null && Inherits(attr.AttributeClass, DataFieldBaseNamespace)) - { - type = property.Type; - attribute = attr; - return true; - } - } - } - - type = null!; - attribute = null!; - return false; - } - - private static bool Inherits(ITypeSymbol type, string parent) - { - foreach (var baseType in GetBaseTypes(type)) - { - if (baseType.ToDisplayString() == parent) - return true; - } - - return false; + return DataDefinitionHelper.IsDataDefinition(type, out _); } private static bool TryGetAttributeLocation(MemberDeclarationSyntax syntax, string attributeName, out Location location) @@ -382,14 +359,14 @@ private static bool HasAttribute(ITypeSymbol type, string attributeName) return false; } - private static bool HasRedundantTag(ISymbol symbol, AttributeData datafieldAttribute) + private static bool HasRedundantTag(ISymbol symbol, DataFieldAttribute datafieldAttribute) { // No args, no problem - if (datafieldAttribute.ConstructorArguments.Length == 0) + if (datafieldAttribute.Data is not { ConstructorArguments.Length: > 0 }) return false; // If a tag is explicitly specified, it will be the first argument... - var tagArgument = datafieldAttribute.ConstructorArguments[0]; + var tagArgument = datafieldAttribute.Data.ConstructorArguments[0]; // ...but the first arg could also something else, since tag is optional // so we make sure that it's a string if (tagArgument.Value is not string explicitName) @@ -427,65 +404,8 @@ private static bool HasVVReadWrite(ISymbol symbol) 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); } - - private static bool IsImplicitDataDefinition(ITypeSymbol type) - { - if (HasAttribute(type, ImplicitDataDefinitionNamespace)) - return true; - - foreach (var baseType in GetBaseTypes(type)) - { - if (HasAttribute(baseType, ImplicitDataDefinitionNamespace)) - return true; - } - - foreach (var @interface in type.AllInterfaces) - { - if (IsImplicitDataDefinitionInterface(@interface)) - return true; - } - - return false; - } - - private static bool IsImplicitDataDefinitionInterface(ITypeSymbol @interface) - { - if (HasAttribute(@interface, ImplicitDataDefinitionNamespace)) - return true; - - foreach (var subInterface in @interface.AllInterfaces) - { - if (HasAttribute(subInterface, ImplicitDataDefinitionNamespace)) - return true; - } - - return false; - } - - private static IEnumerable GetBaseTypes(ITypeSymbol type) - { - var baseType = type.BaseType; - while (baseType != null) - { - yield return baseType; - baseType = baseType.BaseType; - } - } } diff --git a/Robust.Analyzers/DataDefinitionFixer.cs b/Robust.Analyzers/DataDefinitionFixer.cs index d474d28528e..218b346b340 100644 --- a/Robust.Analyzers/DataDefinitionFixer.cs +++ b/Robust.Analyzers/DataDefinitionFixer.cs @@ -18,7 +18,7 @@ public sealed class DefinitionFixer : CodeFixProvider private const string ViewVariablesAttributeName = "ViewVariables"; public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( - IdDataDefinitionPartial, IdNestedDataDefinitionPartial, IdDataFieldWritable, IdDataFieldPropertyWritable, + IdDataDefinitionPartial, IdNestedDataDefinitionPartial, IdDataFieldPropertyWritable, IdDataFieldRedundantTag, IdDataFieldNoVVReadWrite ); @@ -32,8 +32,6 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context) return RegisterPartialTypeFix(context, diagnostic); case IdNestedDataDefinitionPartial: return RegisterPartialTypeFix(context, diagnostic); - case IdDataFieldWritable: - return RegisterDataFieldFix(context, diagnostic); case IdDataFieldPropertyWritable: return RegisterDataFieldPropertyFix(context, diagnostic); case IdDataFieldRedundantTag: @@ -182,22 +180,6 @@ private static async Task RemoveVVAttribute(Document document, MemberD return document.WithSyntaxRoot(root); } - private static async Task RegisterDataFieldFix(CodeFixContext context, Diagnostic diagnostic) - { - var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken); - var span = diagnostic.Location.SourceSpan; - var field = root?.FindToken(span.Start).Parent?.AncestorsAndSelf().OfType().FirstOrDefault(); - - if (field == null) - return; - - context.RegisterCodeFix(CodeAction.Create( - "Make data field writable", - c => MakeFieldWritable(context.Document, field, c), - "Make data field writable" - ), diagnostic); - } - private static async Task RegisterDataFieldPropertyFix(CodeFixContext context, Diagnostic diagnostic) { var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken); @@ -214,17 +196,6 @@ private static async Task RegisterDataFieldPropertyFix(CodeFixContext context, D ), diagnostic); } - private static async Task MakeFieldWritable(Document document, FieldDeclarationSyntax declaration, CancellationToken cancellation) - { - var root = (CompilationUnitSyntax?) await document.GetSyntaxRootAsync(cancellation); - var token = declaration.Modifiers.First(t => t.IsKind(ReadOnlyKeyword)); - var newDeclaration = declaration.WithModifiers(declaration.Modifiers.Remove(token)); - - root = root!.ReplaceNode(declaration, newDeclaration); - - return document.WithSyntaxRoot(root); - } - private static async Task MakePropertyWritable(Document document, PropertyDeclarationSyntax declaration, CancellationToken cancellation) { var root = (CompilationUnitSyntax?) await document.GetSyntaxRootAsync(cancellation); diff --git a/Robust.Benchmarks/EntityManager/RaiseEventBenchmark.cs b/Robust.Benchmarks/EntityManager/RaiseEventBenchmark.cs new file mode 100644 index 00000000000..847e28f1dd1 --- /dev/null +++ b/Robust.Benchmarks/EntityManager/RaiseEventBenchmark.cs @@ -0,0 +1,318 @@ +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Robust.Shared; +using Robust.Shared.Analyzers; +using Robust.Shared.GameObjects; +using Robust.UnitTesting.Server; + +namespace Robust.Benchmarks.EntityManager; + +[Virtual] +public partial class RaiseEventBenchmark +{ + private ISimulation _simulation = default!; + private IEntityManager _entityManager = default!; + private BenchSystem _sys = default!; + + [GlobalSetup] + public async Task Setup() + { + ProgramShared.PathOffset = "../../../../"; + _simulation = RobustServerSimulation + .NewSimulation() + .RegisterComponents(f => + { + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + }) + .RegisterEntitySystems(f => f.LoadExtraSystemType()) + .InitializeInstance(); + + _entityManager = _simulation.Resolve(); + var bus = (EntityEventBus)_entityManager.EventBus; + _entityManager.EntitySysManager.Resolve(ref _sys); + + var uid = _entityManager.Spawn(); + _sys.Ent = new(uid, _entityManager.GetComponent(uid)); + _sys.Ent2 = new(_sys.Ent.Owner, _sys.Ent.Comp); + _sys.NetId = _entityManager.ComponentFactory.GetRegistration().NetID!.Value; + _sys.EvSubs = bus.GetNetCompEventHandlers(); + + var id2 = _entityManager.Spawn(); + _entityManager.AddComponent(id2); + _entityManager.AddComponent(id2); + _sys.Ent2Comps = id2; + + var id4 = _entityManager.Spawn(); + _entityManager.AddComponent(id4); + _entityManager.AddComponent(id4); + _entityManager.AddComponent(id4); + _entityManager.AddComponent(id4); + _sys.Ent4Comps = id4; + + var id8 = _entityManager.Spawn(); + _entityManager.AddComponent(id8); + _entityManager.AddComponent(id8); + _entityManager.AddComponent(id8); + _entityManager.AddComponent(id8); + _entityManager.AddComponent(id8); + _entityManager.AddComponent(id8); + _entityManager.AddComponent(id8); + _entityManager.AddComponent(id8); + _sys.Ent8Comps = id8; + + var id16 = _entityManager.Spawn(); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _sys.Ent16Comps = id16; + } + + [Benchmark(Baseline = true)] + public int RaiseEvent1() + { + return _sys.RaiseEvent1(); + } + + [Benchmark] + public int RaiseEvent2() + { + return _sys.RaiseEvent2(); + } + + [Benchmark] + public int RaiseEvent4() + { + return _sys.RaiseEvent4(); + } + + [Benchmark] + public int RaiseEvent8() + { + return _sys.RaiseEvent8(); + } + + [Benchmark] + public int RaiseEvent16() + { + return _sys.RaiseEvent16(); + } + + [Benchmark] + public int RaiseCompEvent() + { + return _sys.RaiseCompEvent(); + } + + [Benchmark] + public int RaiseICompEvent() + { + return _sys.RaiseICompEvent(); + } + + [Benchmark] + public int RaiseNetEvent() + { + return _sys.RaiseNetIdEvent(); + } + + [Benchmark] + public int RaiseCSharpEvent() + { + return _sys.CSharpEvent(); + } + + public sealed partial class BenchSystem : EntitySystem + { + public Entity Ent; + public Entity Ent2; + public EntityUid Ent2Comps; + public EntityUid Ent4Comps; + public EntityUid Ent8Comps; + public EntityUid Ent16Comps; + + public delegate void EntityEventHandler(EntityUid uid, TransformComponent comp, ref BenchEv ev); + + public event EntityEventHandler? OnCSharpEvent; + public ushort NetId; + internal EntityEventBus.DirectedEventHandler?[] EvSubs = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + OnCSharpEvent += OnEvent; + } + + public int RaiseEvent1() + { + var ev = new BenchEv(); + RaiseLocalEvent(Ent.Owner, ref ev); + return ev.N; + } + + public int RaiseEvent2() + { + var ev = new BenchEv(); + RaiseLocalEvent(Ent2Comps, ref ev); + return ev.N; + } + + public int RaiseEvent4() + { + var ev = new BenchEv(); + RaiseLocalEvent(Ent4Comps, ref ev); + return ev.N; + } + + public int RaiseEvent8() + { + var ev = new BenchEv(); + RaiseLocalEvent(Ent8Comps, ref ev); + return ev.N; + } + + public int RaiseEvent16() + { + var ev = new BenchEv(); + RaiseLocalEvent(Ent16Comps, ref ev); + return ev.N; + } + + public int RaiseCompEvent() + { + var ev = new BenchEv(); + RaiseComponentEvent(Ent.Owner, Ent.Comp, ref ev); + return ev.N; + } + + public int RaiseICompEvent() + { + // Raise with an IComponent instead of concrete type + var ev = new BenchEv(); + RaiseComponentEvent(Ent2.Owner, Ent2.Comp, ref ev); + return ev.N; + } + + public int RaiseNetIdEvent() + { + // Raise a "IComponent" event using a net-id index delegate array (for PVS & client game-state events) + var ev = new BenchEv(); + ref var unitEv = ref Unsafe.As(ref ev); + EvSubs[NetId]?.Invoke(Ent2.Owner, Ent2.Comp, ref unitEv); + return ev.N; + } + + public int CSharpEvent() + { + var ev = new BenchEv(); + OnCSharpEvent?.Invoke(Ent.Owner, Ent.Comp, ref ev); + return ev.N; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void OnEvent(EntityUid uid, T component, ref BenchEv args) + { + args.N += uid.Id; + } + + [ByRefEvent] + [ComponentEvent(Exclusive = false)] + public struct BenchEv + { + public int N; + } + + [RegisterComponent] + public sealed partial class Bench1Component : Component; + + [RegisterComponent] + public sealed partial class Bench2Component : Component; + + [RegisterComponent] + public sealed partial class Bench3Component : Component; + + [RegisterComponent] + public sealed partial class Bench4Component : Component; + + [RegisterComponent] + public sealed partial class Bench5Component : Component; + + [RegisterComponent] + public sealed partial class Bench6Component : Component; + + [RegisterComponent] + public sealed partial class Bench7Component : Component; + + [RegisterComponent] + public sealed partial class Bench8Component : Component; + + [RegisterComponent] + public sealed partial class Bench9Component : Component; + + [RegisterComponent] + public sealed partial class Bench10Component : Component; + + [RegisterComponent] + public sealed partial class Bench11Component : Component; + + [RegisterComponent] + public sealed partial class Bench12Component : Component; + + [RegisterComponent] + public sealed partial class Bench13Component : Component; + + [RegisterComponent] + public sealed partial class Bench14Component : Component; + + [RegisterComponent] + public sealed partial class Bench15Component : Component; + + [RegisterComponent] + public sealed partial class Bench16Component : Component; + } +} diff --git a/Robust.Benchmarks/NumericsHelpers/Box2Benchmark.cs b/Robust.Benchmarks/NumericsHelpers/Box2Benchmark.cs index 4fc4f9f6157..e40cb2c9ca3 100644 --- a/Robust.Benchmarks/NumericsHelpers/Box2Benchmark.cs +++ b/Robust.Benchmarks/NumericsHelpers/Box2Benchmark.cs @@ -8,12 +8,28 @@ namespace Robust.Benchmarks.NumericsHelpers; [Virtual, DisassemblyDiagnoser] public class Box2Benchmark { - public Box2 Box = new(); - public Matrix3x2 Matrix = new(); + public Box2 Box = new(-1, -2, 3, 4); + public Vector2 Point = new(0.75f, 1.25f); + public Matrix3x2 Matrix = Matrix3x2.CreateRotation(0.5f) * Matrix3x2.CreateTranslation(3, -1); + private Box2 _boxResult; [Benchmark] - public Box2 Transform() + public void Transform() { - return Matrix.TransformBox(Box); + _boxResult = Matrix.TransformBox(Box); + } + + [Benchmark(Baseline = true)] + public bool ContainsOld() + { + var xOk = Point.X >= Box.Left ^ Point.X > Box.Right; + var yOk = Point.Y >= Box.Bottom ^ Point.Y > Box.Top; + return xOk && yOk; + } + + [Benchmark] + public bool ContainsCurrent() + { + return Box.Contains(Point); } } diff --git a/Robust.Benchmarks/NumericsHelpers/Box2RotatedBenchmark.cs b/Robust.Benchmarks/NumericsHelpers/Box2RotatedBenchmark.cs index 3515af67032..09d6d0bf302 100644 --- a/Robust.Benchmarks/NumericsHelpers/Box2RotatedBenchmark.cs +++ b/Robust.Benchmarks/NumericsHelpers/Box2RotatedBenchmark.cs @@ -8,11 +8,26 @@ namespace Robust.Benchmarks.NumericsHelpers; [Virtual, DisassemblyDiagnoser] public class Box2RotatedBenchmark { - public Box2Rotated Box = new(); + public Box2Rotated Box = new(Box2.UnitCentered.Translated(new Vector2(1, 2)), Angle.FromDegrees(37), new Vector2(1, 2)); + public Matrix3x2 Matrix = Matrix3x2.CreateScale(1.5f, 0.5f) * Matrix3x2.CreateRotation(0.5f) * Matrix3x2.CreateTranslation(3, -1); + private Matrix3x2 _matrixResult; + private Box2 _boxResult; [Benchmark] - public Matrix3x2 GetTransform() + public void GetTransform() { - return Box.Transform; + _matrixResult = Box.Transform; + } + + [Benchmark(Baseline = true)] + public void TransformBoxOld() + { + _boxResult = (Box.Transform * Matrix).TransformBox(Box.Box); + } + + [Benchmark] + public void TransformBox() + { + _boxResult = Matrix.TransformBox(Box); } } diff --git a/Robust.Benchmarks/Serialization/Read/SerializationReadBenchmark.cs b/Robust.Benchmarks/Serialization/Read/SerializationReadBenchmark.cs index e1fc2d5c3ca..b2ad198a640 100644 --- a/Robust.Benchmarks/Serialization/Read/SerializationReadBenchmark.cs +++ b/Robust.Benchmarks/Serialization/Read/SerializationReadBenchmark.cs @@ -20,7 +20,7 @@ public SerializationReadBenchmark() InitializeSerialization(); StringDataDefNode = new MappingDataNode(); - StringDataDefNode.Add(new ValueDataNode("string"), new ValueDataNode("ABC")); + StringDataDefNode.Add("string", new ValueDataNode("ABC")); var yamlStream = new YamlStream(); yamlStream.Load(new StringReader(SeedDataDefinition.Prototype)); diff --git a/Robust.Benchmarks/Transform/RecursiveMoveBenchmark.cs b/Robust.Benchmarks/Transform/RecursiveMoveBenchmark.cs index 15acf565239..1f67c1a5483 100644 --- a/Robust.Benchmarks/Transform/RecursiveMoveBenchmark.cs +++ b/Robust.Benchmarks/Transform/RecursiveMoveBenchmark.cs @@ -47,7 +47,6 @@ public void GlobalSetup() Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()).Wait(); - var mapMan = server.ResolveDependency(); _entMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); var sPlayerMan = server.ResolveDependency(); @@ -92,7 +91,7 @@ public void GlobalSetup() server.WaitPost(() => { var map = server.ResolveDependency().CreateMap(out var mapId); - var gridComp = mapMan.CreateGridEntity(mapId); + var gridComp = mapSys.CreateGridEntity(mapId); var grid = gridComp.Owner; mapSys.SetTile(grid, gridComp, Vector2i.Zero, new Tile(1)); _gridCoords = new EntityCoordinates(grid, .5f, .5f); diff --git a/Robust.Client.IntegrationTests/Audio/AudioManagerTest.cs b/Robust.Client.IntegrationTests/Audio/AudioManagerTest.cs new file mode 100644 index 00000000000..a6df0dfa80f --- /dev/null +++ b/Robust.Client.IntegrationTests/Audio/AudioManagerTest.cs @@ -0,0 +1,48 @@ +using NUnit.Framework; +using Robust.Client.Audio; +using Robust.Shared; +using Robust.Shared.IoC; +using Robust.UnitTesting; + +namespace Robust.Client.IntegrationTests.Audio; + +[TestFixture] +[TestOf(typeof(AudioManager))] +[Explicit("Server test-runners don't typically have the means of running OpenAL.")] +public sealed class AudioManagerTest : RobustIntegrationTest +{ + [Test] + public async Task SwitchesAudioDevice() + { + var client = StartClient(new ClientIntegrationOptions + { + Pool = false, + InitIoC = () => + { + IoCManager.Register(overwrite: true); + IoCManager.Register(overwrite: true); + }, + }); + + await client.WaitIdleAsync(); + + var audio = client.ResolveDependency(); + Assert.That(audio, Is.TypeOf()); + + var defaultDevice = audio.GetDefaultAudioDevice(); + var devices = audio.GetAudioDevices(); + var testDevice = devices.FirstOrDefault(device => device != defaultDevice) ?? defaultDevice; + + if (testDevice == null) + Assert.Ignore("OpenAL did not expose any audio output devices."); + + await client.WaitAssertion(() => + { + client.CfgMan.SetCVar(CVars.AudioDevice, testDevice); + Assert.That(client.CfgMan.GetCVar(CVars.AudioDevice), Is.EqualTo(testDevice)); + + client.CfgMan.SetCVar(CVars.AudioDevice, string.Empty); + Assert.That(client.CfgMan.GetCVar(CVars.AudioDevice), Is.Empty); + }); + } +} diff --git a/Robust.Client.IntegrationTests/GameObjects/Components/TransformComponentTests.cs b/Robust.Client.IntegrationTests/GameObjects/Components/TransformComponentTests.cs index 0f5a3e3c289..1b774095a6f 100644 --- a/Robust.Client.IntegrationTests/GameObjects/Components/TransformComponentTests.cs +++ b/Robust.Client.IntegrationTests/GameObjects/Components/TransformComponentTests.cs @@ -19,12 +19,13 @@ private static (ISimulation, EntityUid gridA, EntityUid gridB) SimulationFactor .NewSimulation() .InitializeInstance(); - var mapId = sim.Resolve().System().CreateMap(); - var mapManager = sim.Resolve(); + var entMan = sim.Resolve(); + var mapSys = entMan.System(); + var mapId = mapSys.CreateMap(); // Adds two grids to use in tests. - var gridA = mapManager.CreateGridEntity(mapId); - var gridB = mapManager.CreateGridEntity(mapId); + var gridA = mapSys.CreateGridEntity(mapId); + var gridB = mapSys.CreateGridEntity(mapId); return (sim, gridA, gridB); } diff --git a/Robust.Client.IntegrationTests/UserInterface/UserInterfaceManagerTest.cs b/Robust.Client.IntegrationTests/UserInterface/UserInterfaceManagerTest.cs index ce5e891d6cb..4faef41172f 100644 --- a/Robust.Client.IntegrationTests/UserInterface/UserInterfaceManagerTest.cs +++ b/Robust.Client.IntegrationTests/UserInterface/UserInterfaceManagerTest.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; using Robust.Shared.Input; using Robust.Shared.IoC; using Robust.Shared.Map; @@ -242,5 +243,42 @@ public void TestNotGrabKeyboardFocusOnClick() control.Orphan(); } + + /// + /// Assert that windows correctly re-position themselves if their parent is re-sized. + /// + [Test] + public void TestWindowTracksRelativePositionOnResize() + { + _userInterfaceManager.RootControl.Arrange(new UIBox2(0, 0, 100, 100)); + + var window = new TestWindow + { + SetSize = new Vector2(20, 20), + }; + + _userInterfaceManager.WindowRoot.AddChild(window); + + var initialPos = new Vector2(40, 40); + + LayoutContainer.SetPosition(window, initialPos); + Assert.That(window.Position, Is.EqualTo(initialPos)); + + _userInterfaceManager.WindowRoot.InvalidateArrange(); + _userInterfaceManager.RootControl.Arrange(new UIBox2(0, 0, 200, 200)); + + // 40,40 x 2 + 10 + Assert.That(window.Position, Is.EqualTo(new Vector2(90, 90))); + + window.Orphan(); + } + + private sealed class TestWindow : BaseWindow + { + protected override DragMode GetDragModeFor(Vector2 relativeMousePos) + { + return DragMode.None; + } + } } } diff --git a/Robust.Client.WebView/Cef/WebViewManagerCef.Control.cs b/Robust.Client.WebView/Cef/WebViewManagerCef.Control.cs index 9af640113a6..ebf1782aae8 100644 --- a/Robust.Client.WebView/Cef/WebViewManagerCef.Control.cs +++ b/Robust.Client.WebView/Cef/WebViewManagerCef.Control.cs @@ -7,6 +7,7 @@ using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Maths; +using Robust.Shared.Prototypes; using Robust.Shared.Utility; using SixLabors.ImageSharp.PixelFormats; using Xilium.CefGlue; @@ -19,16 +20,18 @@ internal partial class WebViewManagerCef { private readonly List _activeControls = new(); + private static readonly ProtoId _bgraShader = "bgra"; + public IWebViewControlImpl MakeControlImpl(WebViewControl owner) { - var shader = _prototypeManager.Index("bgra"); + var shader = _prototypeManager.Index(_bgraShader); var shaderInstance = shader.Instance(); var impl = new ControlImpl(this, owner, shaderInstance); _dependencyCollection.InjectDependencies(impl); return impl; } - private sealed class ControlImpl : IWebViewControlImpl + private sealed partial class ControlImpl : IWebViewControlImpl { private static readonly Dictionary KeyMap = new() { diff --git a/Robust.Client/Audio/AudioManager.cs b/Robust.Client/Audio/AudioManager.cs index 63fc5fdc0da..f70f3b8d365 100644 --- a/Robust.Client/Audio/AudioManager.cs +++ b/Robust.Client/Audio/AudioManager.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using OpenTK.Audio.OpenAL; @@ -36,6 +38,7 @@ internal sealed partial class AudioManager : IAudioInternal private readonly HashSet _alcDeviceExtensions = new(); private readonly HashSet _alContextExtensions = new(); private Attenuation _attenuation; + private bool _audioInitialized; public bool HasAlDeviceExtension(string extension) => _alcDeviceExtensions.Contains(extension); public bool HasAlContextExtension(string extension) => _alContextExtensions.Contains(extension); @@ -92,6 +95,28 @@ private void _audioCreateContext() OpenALSawmill.Debug("HRTF status: {0}", hrtfEnabled == 1 ? "Enabled" : "Disabled"); } + public IReadOnlyList GetAudioDevices() + { + if (ALC.EnumerateAll.IsExtensionPresent()) + return ALC.EnumerateAll.GetStringList(GetEnumerateAllContextStringList.AllDevicesSpecifier).ToList(); + + if (ALC.IsExtensionPresent(ALDevice.Null, "ALC_ENUMERATION_EXT")) + return ALC.GetStringList(GetEnumerationStringList.DeviceSpecifier).ToList(); + + return Array.Empty(); + } + + public string? GetDefaultAudioDevice() + { + if (ALC.EnumerateAll.IsExtensionPresent()) + return ALC.EnumerateAll.GetString(ALDevice.Null, GetEnumerateAllContextString.DefaultAllDevicesSpecifier); + + if (ALC.IsExtensionPresent(ALDevice.Null, "ALC_ENUMERATION_EXT")) + return ALC.GetString(ALDevice.Null, AlcGetString.DefaultDeviceSpecifier); + + return null; + } + private bool _audioOpenDevice() { var preferredDevice = _cfg.GetCVar(CVars.AudioDevice); @@ -143,11 +168,81 @@ private void InitializeAudio() IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX"); _cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterGain, true); + _cfg.OnValueChanged(CVars.AudioDevice, OnAudioDeviceChanged); _reload.Register("/Audio", "*.ogg"); _reload.Register("/Audio", "*.wav"); _reload.OnChanged += OnReload; + _audioInitialized = true; + } + + private void OnAudioDeviceChanged(string deviceSpecifier) + { + if (!_audioInitialized) + return; + + SwitchAudioDevice(deviceSpecifier); + } + + private void SwitchAudioDevice(string requestedDevice) + { + OpenALSawmill.Info("Switching OpenAL output device to {0}.", + string.IsNullOrEmpty(requestedDevice) ? "" : requestedDevice); + + if (TryReopenAudioDevice(requestedDevice)) + { + ReloadDeviceExtensions(); + IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX"); + SetMasterGain(_cfg.GetCVar(CVars.AudioMasterVolume)); + return; + } + + // The skrunkly path that's hopefully never needed. + OpenALSawmill.Warning( + "ALC_SOFT_reopen_device is unavailable or failed. Falling back to full audio device rebuild."); + + DisposeAllAudio(); + FlushALDisposeQueues(); + + if (!_audioOpenDevice()) + { + OpenALSawmill.Error("Failed to reopen OpenAL device after device switch."); + return; + } + + _audioCreateContext(); + IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX"); + SetMasterGain(_cfg.GetCVar(CVars.AudioMasterVolume)); + } + + private bool TryReopenAudioDevice(string requestedDevice) + { + if (_openALDevice == ALDevice.Null || + !ALC.IsExtensionPresent(_openALDevice, "ALC_SOFT_reopen_device")) + { + return false; + } + + var reopen = LoadAlcDelegate("alcReopenDeviceSOFT"); + if (reopen == null) + return false; + + var reopenTarget = string.IsNullOrEmpty(requestedDevice) ? null : requestedDevice; + var reopened = reopen(_openALDevice, reopenTarget, IntPtr.Zero); + _checkAlcError(_openALDevice); + return reopened; + } + + private void ReloadDeviceExtensions() + { + _alcDeviceExtensions.Clear(); + + var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? ""; + foreach (var extension in s.Split(' ', StringSplitOptions.RemoveEmptyEntries)) + { + _alcDeviceExtensions.Add(extension); + } } private void OnReload(ResPath args) @@ -188,6 +283,31 @@ internal void LogError(string message) OpenALSawmill.Error(message); } + /* + * Evil hack because OpenTK doesn't expose the device switch call. + */ + + private delegate bool AlcReopenDeviceSoftDelegate(ALDevice device, string? deviceName, IntPtr attribs); + + private static TDelegate? LoadAlcDelegate(string name) + where TDelegate : Delegate + { + var type = typeof(ALC); + while (type != null) + { + var method = type.GetMethod( + "LoadDelegate", + BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy); + + if (method != null) + return (TDelegate?) method.MakeGenericMethod(typeof(TDelegate)).Invoke(null, [name]); + + type = type.BaseType; + } + + return null; + } + /// /// Like _checkAlError but allows custom data to be passed in as relevant. /// diff --git a/Robust.Client/Audio/AudioSystem.cs b/Robust.Client/Audio/AudioSystem.cs index e7944446cfb..0eb1f65bdc3 100644 --- a/Robust.Client/Audio/AudioSystem.cs +++ b/Robust.Client/Audio/AudioSystem.cs @@ -118,16 +118,6 @@ public override void Initialize() _physicsQuery = GetEntityQuery(); - SubscribeLocalEvent(OnAudioStartup); - SubscribeLocalEvent(OnAudioShutdown); - SubscribeLocalEvent(OnAudioPaused); - SubscribeLocalEvent(OnAudioState); - - // Replay stuff - SubscribeNetworkEvent(OnGlobalAudio); - SubscribeNetworkEvent(OnEntityAudio); - SubscribeNetworkEvent(OnEntityCoordinates); - Subs.CVar(CfgManager, CVars.AudioEndBuffer, OnAudioBuffer, true); Subs.CVar(CfgManager, CVars.AudioAttenuation, OnAudioAttenuation, true); Subs.CVar(CfgManager, CVars.AudioRaycastLength, OnRaycastLengthChanged, true); @@ -146,6 +136,7 @@ private void OnAudioTickRate(int obj) _audioFrameTimeRemaining = MathF.Min(_audioFrameTimeRemaining, _audioFrameTime); } + [SubscribeLocalEvent] private void OnAudioState(Entity entity, ref AfterAutoHandleStateEvent args) { var component = entity.Comp; @@ -211,6 +202,7 @@ public void SetMasterVolume(float value) _audio.SetMasterGain(value); } + [SubscribeLocalEvent] private void OnAudioPaused(EntityUid uid, AudioComponent component, ref EntityPausedEvent args) { component.Pause(); @@ -222,6 +214,7 @@ protected override void OnAudioUnpaused(EntityUid uid, AudioComponent component, component.StartPlaying(); } + [SubscribeLocalEvent] private void OnAudioStartup(EntityUid uid, AudioComponent component, ComponentStartup args) { if (!Timing.ApplyingState && !Timing.IsFirstTimePredicted) @@ -293,6 +286,7 @@ private void SetupSource(Entity entity, AudioResource audioResou } } + [SubscribeLocalEvent] private void OnAudioShutdown(EntityUid uid, AudioComponent component, ComponentShutdown args) { // Breaks with prediction? @@ -775,16 +769,19 @@ private void ApplyAudioParams(AudioParams audioParams, IAudioSource source) source.Looping = audioParams.Loop; } + [SubscribeNetworkEvent] private void OnEntityCoordinates(PlayAudioPositionalMessage ev) { PlayStatic(ev.Specifier, GetCoordinates(ev.Coordinates), ev.AudioParams, false); } + [SubscribeNetworkEvent] private void OnEntityAudio(PlayAudioEntityMessage ev) { PlayEntity(ev.Specifier, GetEntity(ev.NetEntity), ev.AudioParams, false); } + [SubscribeNetworkEvent] private void OnGlobalAudio(PlayAudioGlobalMessage ev) { PlayGlobal(ev.Specifier, ev.AudioParams, false); diff --git a/Robust.Client/Audio/HeadlessAudioManager.cs b/Robust.Client/Audio/HeadlessAudioManager.cs index 46c2cc8e442..b38449adc6e 100644 --- a/Robust.Client/Audio/HeadlessAudioManager.cs +++ b/Robust.Client/Audio/HeadlessAudioManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Numerics; using Robust.Shared.Audio; @@ -13,6 +14,8 @@ namespace Robust.Client.Audio; /// internal sealed class HeadlessAudioManager : IAudioInternal { + + private readonly IReadOnlyList _emptyDevices = Array.Empty(); private int _audioBuffer; /// @@ -36,6 +39,16 @@ public IAudioSource CreateAudioSource(AudioStream stream) return DummyAudioSource.Instance; } + public IReadOnlyList GetAudioDevices() + { + return _emptyDevices; + } + + public string? GetDefaultAudioDevice() + { + return null; + } + /// public IBufferedAudioSource? CreateBufferedAudioSource(int buffers, bool floatAudio = false) { diff --git a/Robust.Client/Audio/IAudioManager.cs b/Robust.Client/Audio/IAudioManager.cs index ce65d1d0cb1..9396346b056 100644 --- a/Robust.Client/Audio/IAudioManager.cs +++ b/Robust.Client/Audio/IAudioManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using Robust.Shared.Audio.Sources; @@ -10,6 +11,10 @@ namespace Robust.Client.Audio; [NotContentImplementable] public interface IAudioManager { + IReadOnlyList GetAudioDevices(); + + string? GetDefaultAudioDevice(); + IAudioSource? CreateAudioSource(AudioStream stream); AudioStream LoadAudioOggVorbis(Stream stream, string? name = null); diff --git a/Robust.Client/BaseClient.cs b/Robust.Client/BaseClient.cs index 428ab9859ce..4c8dbf89e2e 100644 --- a/Robust.Client/BaseClient.cs +++ b/Robust.Client/BaseClient.cs @@ -26,7 +26,6 @@ public sealed partial class BaseClient : IBaseClient, IPostInjectInit [Dependency] private IPlayerManager _playMan = default!; [Dependency] private IClientNetConfigurationManager _configManager = default!; [Dependency] private IClientEntityManager _entityManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IDiscordRichPresence _discord = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private IClientGameStateManager _gameStates = default!; @@ -247,7 +246,6 @@ private void OnNetDisconnect(object? sender, NetDisconnectedArgs args) private void GameStartedSetup() { _entityManager.Startup(); - _mapManager.Startup(); _timing.ResetSimTime(_timeBase); _timing.Paused = false; @@ -259,7 +257,6 @@ private void GameStoppedReset() _gameStates.Reset(); _playMan.Shutdown(); _entityManager.Shutdown(); - _mapManager.Shutdown(); _discord.ClearPresence(); Reset(); } diff --git a/Robust.Client/ClientIoC.cs b/Robust.Client/ClientIoC.cs index 3df84845048..7a2ca8e14fe 100644 --- a/Robust.Client/ClientIoC.cs +++ b/Robust.Client/ClientIoC.cs @@ -66,9 +66,6 @@ public static void RegisterIoC(GameController.DisplayMode mode, IDependencyColle deps.Register(); deps.Register(); deps.Register(); - deps.Register(); - deps.Register(); - deps.Register(); deps.Register(); deps.Register(); deps.Register(); diff --git a/Robust.Client/Console/ClientConsoleHost.Completions.cs b/Robust.Client/Console/ClientConsoleHost.Completions.cs index 7bda1199f44..6195a952b2e 100644 --- a/Robust.Client/Console/ClientConsoleHost.Completions.cs +++ b/Robust.Client/Console/ClientConsoleHost.Completions.cs @@ -34,7 +34,7 @@ private Task CalcCompletions(List args, string argStr, // Typing out command name, handle this ourselves. var cmdOptions = CompletionResult.FromOptions( AvailableCommands.Values - .Where(c => CanExecute(c.Command)) + .Where(c => CanExecute(c.Command) && !IsCommandHidden(c)) .OrderBy(c => c.Command) .Select(c => new CompletionOption(c.Command, c.Description))); diff --git a/Robust.Client/Console/Commands/Debug.cs b/Robust.Client/Console/Commands/Debug.cs index 3d8c0ed0a0a..1d21498e123 100644 --- a/Robust.Client/Console/Commands/Debug.cs +++ b/Robust.Client/Console/Commands/Debug.cs @@ -718,7 +718,6 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) internal sealed partial class ChunkInfoCommand : LocalizedEntityCommands { - [Dependency] private IMapManager _map = default!; [Dependency] private IEyeManager _eye = default!; [Dependency] private IInputManager _input = default!; [Dependency] private SharedMapSystem _mapSystem = default!; @@ -729,7 +728,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) { var mousePos = _eye.PixelToMap(_input.MouseScreenPosition); - if (!_map.TryFindGridAt(mousePos, out var gridUid, out var grid)) + if (!_mapSystem.TryFindGridAt(mousePos, out var gridUid, out var grid)) { shell.WriteLine("No grid under your mouse cursor."); return; diff --git a/Robust.Client/Console/Commands/UITestCommand.TabSpriteView.cs b/Robust.Client/Console/Commands/UITestCommand.TabSpriteView.cs index 1ae4557d708..cd44fe6b0d3 100644 --- a/Robust.Client/Console/Commands/UITestCommand.TabSpriteView.cs +++ b/Robust.Client/Console/Commands/UITestCommand.TabSpriteView.cs @@ -21,6 +21,8 @@ private sealed class TabSpriteView : Control private readonly IEntityManager _entMan; private readonly IGameTiming _timing; + private readonly SpriteSystem _sprite; + private readonly SharedTransformSystem _xform; private readonly BoxContainer _box; private record Entry(EntityUid Uid, SpriteComponent Sprite, TransformComponent Transform, @@ -33,6 +35,8 @@ private record Entry(EntityUid Uid, SpriteComponent Sprite, TransformComponent T public TabSpriteView() { IoCManager.Resolve(ref _entMan, ref _timing); + _sprite = _entMan.System(); + _xform = _entMan.System(); SetValue(TabContainer.TabTitleProperty, nameof(SpriteView)); _box = new BoxContainer { @@ -178,14 +182,14 @@ private List AddEntries() entry = AddEntry("Local Rotation", (e, time) => { - e.Transform.LocalRotation = Angle.FromDegrees(time * _degreesPerSecond); + _xform.SetLocalRotation(e.Uid, Angle.FromDegrees(time * _degreesPerSecond), e.Transform); e.View.InvalidateMeasure(); }); added.Add(entry); entry = AddEntry("Local Rotation (NoRot)", (e, time) => { - e.Transform.LocalRotation = Angle.FromDegrees(time * _degreesPerSecond); + _xform.SetLocalRotation(e.Uid, Angle.FromDegrees(time * _degreesPerSecond), e.Transform); e.View.InvalidateMeasure(); }); entry.Sprite.NoRotation = true; @@ -193,7 +197,7 @@ private List AddEntries() entry = AddEntry("Offset", (e, time) => { - e.Sprite.Offset = new Vector2(MathF.Sin((float) Angle.FromDegrees(time * _degreesPerSecond)), 0); + _sprite.SetOffset((e.Uid, e.Sprite), new Vector2(MathF.Sin((float) Angle.FromDegrees(time * _degreesPerSecond)), 0)); e.View.InvalidateMeasure(); }); added.Add(entry); @@ -201,24 +205,24 @@ private List AddEntries() entry = AddEntry("Scaled", (e, time) => { var theta = (float) Angle.FromDegrees(_degreesPerSecond * time).Theta; - e.Sprite.Scale = Vector2.One + new Vector2(0.5f * MathF.Sin(theta), 0.5f * MathF.Cos(theta)); + _sprite.SetScale((e.Uid, e.Sprite), Vector2.One + new Vector2(0.5f * MathF.Sin(theta), 0.5f * MathF.Cos(theta))); e.View.InvalidateMeasure(); }); added.Add(entry); entry = AddEntry("Sprite Rotation", (e, time) => { - e.Sprite.Rotation = Angle.FromDegrees(time * _degreesPerSecond); + _sprite.SetRotation((e.Uid, e.Sprite), Angle.FromDegrees(time * _degreesPerSecond)); }); added.Add(entry); entry = AddEntry("Combination", (e, time) => { var theta = (float) Angle.FromDegrees(_degreesPerSecond * time * 2).Theta; - e.Sprite.Scale = Vector2.One + new Vector2(0.5f * MathF.Sin(theta), 0.5f * MathF.Cos(theta)); - e.Sprite.Offset = new(MathF.Sin((float) Angle.FromDegrees(time * _degreesPerSecond)), 0); - e.Sprite.Rotation = Angle.FromDegrees(0.5 * time * _degreesPerSecond); - e.Transform.LocalRotation = Angle.FromDegrees(0.25 * time * _degreesPerSecond); + _sprite.SetScale((e.Uid, e.Sprite), Vector2.One + new Vector2(0.5f * MathF.Sin(theta), 0.5f * MathF.Cos(theta))); + _sprite.SetOffset((e.Uid, e.Sprite), new(MathF.Sin((float) Angle.FromDegrees(time * _degreesPerSecond)), 0)); + _sprite.SetRotation((e.Uid, e.Sprite), Angle.FromDegrees(0.5 * time * _degreesPerSecond)); + _xform.SetLocalRotationNoLerp(e.Uid, Angle.FromDegrees(0.25 * time * _degreesPerSecond), e.Transform); e.View.InvalidateMeasure(); }); added.Add(entry); diff --git a/Robust.Client/Debugging/DebugAnchoringSystem.cs b/Robust.Client/Debugging/DebugAnchoringSystem.cs index 0446aa7b676..75c6d07b7e7 100644 --- a/Robust.Client/Debugging/DebugAnchoringSystem.cs +++ b/Robust.Client/Debugging/DebugAnchoringSystem.cs @@ -17,7 +17,6 @@ public sealed partial class DebugAnchoringSystem : EntitySystem { [Dependency] private IEyeManager _eyeManager = default!; [Dependency] private IInputManager _inputManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IUserInterfaceManager _userInterface = default!; [Dependency] private MapSystem _mapSystem = default!; @@ -64,7 +63,7 @@ public override void FrameUpdate(float frameTime) var mouseSpot = _inputManager.MouseScreenPosition; var spot = _eyeManager.PixelToMap(mouseSpot); - if (!_mapManager.TryFindGridAt(spot, out var gridUid, out var grid)) + if (!_mapSystem.TryFindGridAt(spot, out var gridUid, out var grid)) { _label.Text = string.Empty; _hovered = null; diff --git a/Robust.Client/Debugging/DebugPhysicsSystem.cs b/Robust.Client/Debugging/DebugPhysicsSystem.cs index d34944a415d..df1e357be7c 100644 --- a/Robust.Client/Debugging/DebugPhysicsSystem.cs +++ b/Robust.Client/Debugging/DebugPhysicsSystem.cs @@ -84,7 +84,7 @@ public sealed partial class DebugPhysicsSystem : SharedDebugPhysicsSystem [Dependency] private IOverlayManager _overlay = default!; [Dependency] private IEyeManager _eye = default!; [Dependency] private IInputManager _input = default!; - [Dependency] private IMapManager _map = default!; + [Dependency] private SharedMapSystem _map = default!; [Dependency] private IPlayerManager _player = default!; [Dependency] private IResourceCache _resourceCache = default!; @@ -103,13 +103,13 @@ public PhysicsDebugFlags Flags EntityManager, _eye, _input, - _map, _player, _resourceCache, this, _entityLookup, _physics, - _transform)); + _transform, + _map)); if (value == PhysicsDebugFlags.None) _overlay.RemoveOverlay(typeof(PhysicsDebugOverlay)); @@ -203,12 +203,12 @@ internal sealed class PhysicsDebugOverlay : Overlay private readonly IEntityManager _entityManager; private readonly IEyeManager _eyeManager; private readonly IInputManager _inputManager; - private readonly IMapManager _mapManager; private readonly IPlayerManager _playerManager; private readonly DebugPhysicsSystem _debugPhysicsSystem; private readonly EntityLookupSystem _lookup; private readonly SharedPhysicsSystem _physicsSystem; private readonly SharedTransformSystem _transformSystem; + private readonly SharedMapSystem _mapSystem; public override OverlaySpace Space => OverlaySpace.WorldSpace | OverlaySpace.ScreenSpace; @@ -219,17 +219,17 @@ internal sealed class PhysicsDebugOverlay : Overlay private HashSet _drawnJoints = new(); private List> _grids = new(); - public PhysicsDebugOverlay(IEntityManager entityManager, IEyeManager eyeManager, IInputManager inputManager, IMapManager mapManager, IPlayerManager playerManager, IResourceCache cache, DebugPhysicsSystem system, EntityLookupSystem lookup, SharedPhysicsSystem physicsSystem, SharedTransformSystem transformSystem) + public PhysicsDebugOverlay(IEntityManager entityManager, IEyeManager eyeManager, IInputManager inputManager, IPlayerManager playerManager, IResourceCache cache, DebugPhysicsSystem system, EntityLookupSystem lookup, SharedPhysicsSystem physicsSystem, SharedTransformSystem transformSystem, SharedMapSystem mapSystem) { _entityManager = entityManager; _eyeManager = eyeManager; _inputManager = inputManager; - _mapManager = mapManager; _playerManager = playerManager; _debugPhysicsSystem = system; _lookup = lookup; _physicsSystem = physicsSystem; _transformSystem = transformSystem; + _mapSystem = mapSystem; _font = new VectorFont(cache.GetResource("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 10); } @@ -293,7 +293,7 @@ private void DrawWorld(DrawingHandleWorld worldHandle, OverlayDrawArgs args) } _grids.Clear(); - _mapManager.FindGridsIntersecting(mapId, viewBounds, ref _grids); + _mapSystem.FindGridsIntersecting(mapId, viewBounds, ref _grids); foreach (var grid in _grids) { diff --git a/Robust.Client/Debugging/Overlays/TileDebugOverlay.cs b/Robust.Client/Debugging/Overlays/TileDebugOverlay.cs index 74779c8eccc..d3f1df1639c 100644 --- a/Robust.Client/Debugging/Overlays/TileDebugOverlay.cs +++ b/Robust.Client/Debugging/Overlays/TileDebugOverlay.cs @@ -25,7 +25,6 @@ public abstract partial class TileDebugOverlay : Overlay, IPostInjectInit { [Dependency] protected IEntityManager Entity = default!; [Dependency] protected IEyeManager Eye = default!; - [Dependency] protected IMapManager MapMan = default!; [Dependency] protected IInputManager Input = default!; [Dependency] protected IUserInterfaceManager Ui = default!; [Dependency] protected IResourceCache Cache = default!; @@ -59,7 +58,7 @@ protected internal override void Draw(in OverlayDrawArgs args) if (args.Viewport.Eye?.Position.MapId is not {} map || map == MapId.Nullspace) return; - MapMan.FindGridsIntersecting(map, args.WorldBounds, ref Grids); + Map.FindGridsIntersecting(map, args.WorldBounds, ref Grids); foreach (var grid in Grids) { @@ -108,7 +107,7 @@ protected virtual void DrawTooltip(DrawingHandleScreen handle) var coords = viewport.PixelToMap(mousePos.Position); - if (!MapMan.TryFindGridAt(coords, out var grid, out var comp)) + if (!Map.TryFindGridAt(coords, out var grid, out var comp)) return; var local = Map.WorldToLocal(grid, comp, coords.Position); diff --git a/Robust.Client/GameController/GameController.cs b/Robust.Client/GameController/GameController.cs index 1071ea0df48..52f29e12bcd 100644 --- a/Robust.Client/GameController/GameController.cs +++ b/Robust.Client/GameController/GameController.cs @@ -61,7 +61,6 @@ internal sealed partial class GameController : IGameControllerInternal [Dependency] private IXamlHotReloadManager _xamlHotReloadManager = default!; [Dependency] private IPrototypeManager _prototypeManager = default!; [Dependency] private IClientNetManager _networkManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IStateManager _stateManager = default!; [Dependency] private IUserInterfaceManagerInternal _userInterfaceManager = default!; [Dependency] private IBaseClient _client = default!; @@ -148,7 +147,7 @@ internal bool StartupContinue(DisplayMode displayMode) { DebugTools.AssertNotNull(_resourceManifest); - _loadscr.Initialize(42); + _loadscr.Initialize(41); _loadscr.BeginLoadingSection("Init graphics", dontRender: true); _clyde.InitializePostWindowing(); @@ -236,7 +235,6 @@ internal bool StartupContinue(DisplayMode displayMode) _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); diff --git a/Robust.Client/GameObjects/ClientEntityManager.Spawn.cs b/Robust.Client/GameObjects/ClientEntityManager.Spawn.cs index a83ef38a9a2..46a97463b6b 100644 --- a/Robust.Client/GameObjects/ClientEntityManager.Spawn.cs +++ b/Robust.Client/GameObjects/ClientEntityManager.Spawn.cs @@ -40,6 +40,13 @@ public override EntityUid PredictedSpawnAtPosition(string? protoName, EntityCoor return ent; } + public override EntityUid PredictedSpawnAtPosition(string? protoName, EntityCoordinates coordinates, Angle rotation, ComponentRegistry? overrides = null) + { + var ent = SpawnAtPosition(protoName, coordinates, rotation, overrides); + FlagPredicted(ent); + return ent; + } + public override bool PredictedTrySpawnNextTo( string? protoName, EntityUid target, diff --git a/Robust.Client/GameObjects/ClientEntityManager.cs b/Robust.Client/GameObjects/ClientEntityManager.cs index ff9c38ba3de..4d65c2efefc 100644 --- a/Robust.Client/GameObjects/ClientEntityManager.cs +++ b/Robust.Client/GameObjects/ClientEntityManager.cs @@ -31,6 +31,8 @@ public sealed partial class ClientEntityManager : EntityManager, IClientEntityMa private readonly Queue _queuedPredictedDeletions = new(); private readonly HashSet _queuedPredictedDeletionsSet = new(); + private Histogram? _tickUpdateHistogram; + private Histogram.Child? _entityNetHistogram; public override void Initialize() { @@ -206,7 +208,9 @@ public void SetupNetworking() public override void TickUpdate(float frameTime, bool noPredictions, Histogram? histogram) { - using (histogram?.WithLabels("EntityNet").NewTimer()) + UpdateTickHistogram(histogram); + + using (_entityNetHistogram?.NewTimer()) { while (_queue.Count != 0 && _queue.Peek().msg.SourceTick <= _gameTiming.LastRealTick) { @@ -219,6 +223,15 @@ public override void TickUpdate(float frameTime, bool noPredictions, Histogram? base.TickUpdate(frameTime, noPredictions, histogram); } + private void UpdateTickHistogram(Histogram? histogram) + { + if (ReferenceEquals(_tickUpdateHistogram, histogram)) + return; + + _tickUpdateHistogram = histogram; + _entityNetHistogram = histogram?.WithLabels("EntityNet"); + } + internal override void ProcessQueueudDeletions() { base.ProcessQueueudDeletions(); diff --git a/Robust.Client/GameObjects/EntitySystems/AnimationPlayerSystem.cs b/Robust.Client/GameObjects/EntitySystems/AnimationPlayerSystem.cs index 5bb934f2a88..14d15a4abe5 100644 --- a/Robust.Client/GameObjects/EntitySystems/AnimationPlayerSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/AnimationPlayerSystem.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using Robust.Client.Animations; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; using Robust.Shared.Utility; namespace Robust.Client.GameObjects @@ -14,10 +13,6 @@ public sealed partial class AnimationPlayerSystem : EntitySystem private EntityQuery _playerQuery; private EntityQuery _metaQuery; -#if DEBUG - [Dependency] private IComponentFactory _compFact = default!; -#endif - public override void Initialize() { base.Initialize(); @@ -129,7 +124,7 @@ public void Play(Entity ent, Animation animation, stri if (IsClientSide(ent) || !animatedComp.NetSyncEnabled) continue; - var reg = _compFact.GetRegistration(animatedComp); + var reg = Factory.GetRegistration(animatedComp); // In principle there is nothing wrong with this, as long as the property of the component being // animated is not part of the networked state and setting it does not dirty the component. Hence only a diff --git a/Robust.Client/GameObjects/EntitySystems/DebugLightTreeSystem.cs b/Robust.Client/GameObjects/EntitySystems/DebugLightTreeSystem.cs index cc41a5482ee..c50fa091e60 100644 --- a/Robust.Client/GameObjects/EntitySystems/DebugLightTreeSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/DebugLightTreeSystem.cs @@ -27,8 +27,6 @@ public bool Enabled { _lightOverlay = new DebugLightOverlay( EntityManager.System(), - IoCManager.Resolve(), - IoCManager.Resolve(), EntityManager.System()); overlayManager.AddOverlay(_lightOverlay); @@ -46,18 +44,14 @@ public bool Enabled private sealed class DebugLightOverlay : Overlay { private EntityLookupSystem _lookup; - private IEyeManager _eyeManager; - private IMapManager _mapManager; private LightTreeSystem _trees; public override OverlaySpace Space => OverlaySpace.WorldSpace; - public DebugLightOverlay(EntityLookupSystem lookup, IEyeManager eyeManager, IMapManager mapManager, LightTreeSystem trees) + public DebugLightOverlay(EntityLookupSystem lookup, LightTreeSystem trees) { _lookup = lookup; - _eyeManager = eyeManager; - _mapManager = mapManager; _trees = trees; } diff --git a/Robust.Client/GameObjects/EntitySystems/GridChunkBoundsDebugSystem.cs b/Robust.Client/GameObjects/EntitySystems/GridChunkBoundsDebugSystem.cs index 1a60f7bd9e7..761244b9b5b 100644 --- a/Robust.Client/GameObjects/EntitySystems/GridChunkBoundsDebugSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/GridChunkBoundsDebugSystem.cs @@ -4,7 +4,6 @@ using Robust.Shared.Enums; using Robust.Shared.GameObjects; using Robust.Shared.IoC; -using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Maths; using Robust.Shared.Physics; @@ -15,8 +14,6 @@ namespace Robust.Client.GameObjects { public sealed partial class GridChunkBoundsDebugSystem : EntitySystem { - [Dependency] private IEyeManager _eyeManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IOverlayManager _overlayManager = default!; [Dependency] private TransformSystem _transform = default!; [Dependency] private SharedMapSystem _map = default!; @@ -37,8 +34,6 @@ public bool Enabled DebugTools.Assert(_overlay == null); _overlay = new GridChunkBoundsOverlay( EntityManager, - _eyeManager, - _mapManager, _transform, _map); @@ -58,8 +53,6 @@ public bool Enabled internal sealed class GridChunkBoundsOverlay : Overlay { private readonly IEntityManager _entityManager; - private readonly IEyeManager _eyeManager; - private readonly IMapManager _mapManager; private readonly SharedTransformSystem _transformSystem; private readonly SharedMapSystem _mapSystem; @@ -67,11 +60,9 @@ internal sealed class GridChunkBoundsOverlay : Overlay private List> _grids = new(); - public GridChunkBoundsOverlay(IEntityManager entManager, IEyeManager eyeManager, IMapManager mapManager, SharedTransformSystem transformSystem, SharedMapSystem mapSystem) + public GridChunkBoundsOverlay(IEntityManager entManager, SharedTransformSystem transformSystem, SharedMapSystem mapSystem) { _entityManager = entManager; - _eyeManager = eyeManager; - _mapManager = mapManager; _transformSystem = transformSystem; _mapSystem = mapSystem; } @@ -84,7 +75,7 @@ protected internal override void Draw(in OverlayDrawArgs args) var fixturesQuery = _entityManager.GetEntityQuery(); _grids.Clear(); - _mapManager.FindGridsIntersecting(currentMap, viewport, ref _grids); + _mapSystem.FindGridsIntersecting(currentMap, viewport, ref _grids); foreach (var grid in _grids) { var worldMatrix = _transformSystem.GetWorldMatrix(grid); diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Helpers.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Helpers.cs index dec67c6cec2..c544b9b3dc8 100644 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Helpers.cs +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Helpers.cs @@ -56,7 +56,7 @@ public Texture GetIcon(IconComponent icon) /// public IRsiStateLike GetPrototypeIcon(string prototype) { - if (!_proto.TryIndex(prototype, out var entityPrototype)) + if (!ProtoMan.TryIndex(prototype, out var entityPrototype)) { // The specified prototype doesn't exist, return the fallback "error" sprite. _sawmill.Error("Failed to load PrototypeIcon {0}", prototype); @@ -82,7 +82,7 @@ public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype) private IRsiStateLike GetPrototypeIconInternal(EntityPrototype prototype) { // IconComponent takes precedence. If it has a valid icon, return that. Otherwise, continue as normal. - if (prototype.TryGetComponent(out IconComponent? icon, _factory)) + if (prototype.TryComp(out IconComponent? icon, Factory)) return GetIcon(icon); // If the prototype doesn't have a SpriteComponent, then there's nothing we can do but return the fallback. @@ -108,7 +108,7 @@ public IEnumerable GetPrototypeTextures(EntityProto var results = new List(); noRot = false; - if (proto.TryGetComponent(out IconComponent? icon, _factory)) + if (proto.TryComp(out IconComponent? icon, Factory)) { results.Add(GetIcon(icon)); return results; diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs index 0780a670f15..34b2153a11b 100644 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Numerics; using JetBrains.Annotations; using Robust.Client.ComponentTrees; using Robust.Client.Graphics; @@ -13,7 +12,6 @@ using Robust.Shared.Graphics.RSI; using Robust.Shared.IoC; using Robust.Shared.Log; -using Robust.Shared.Maths; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations; using Robust.Shared.Timing; @@ -31,10 +29,7 @@ public sealed partial class SpriteSystem : EntitySystem [Dependency] private IConfigurationManager _cfg = default!; [Dependency] private IEyeManager _eye = default!; [Dependency] private IGameTiming _timing = default!; - [Dependency] private IPrototypeManager _proto = default!; [Dependency] private IResourceCache _resourceCache = default!; - [Dependency] private ILogManager _logManager = default!; - [Dependency] private IComponentFactory _factory = default!; // Note that any new system dependencies have to be added to RobustUnitTest.BaseSetup() [Dependency] private SharedTransformSystem _xforms = default!; @@ -64,7 +59,7 @@ public override void Initialize() SubscribeLocalEvent(OnInit); Subs.CVar(_cfg, CVars.RenderSpriteDirectionBias, OnBiasChanged, true); - _sawmill = _logManager.GetSawmill("sprite"); + _sawmill = LogManager.GetSawmill("sprite"); _query = GetEntityQuery(); } @@ -76,7 +71,7 @@ public bool IsVisible(Layer layer) private void OnInit(EntityUid uid, SpriteComponent component, ComponentInit args) { // I'm not 100% this is needed, but I CBF with this ATM. Somebody kill server sprite component please. - QueueUpdateInert(uid, component); + QueueUpdateIsInert((uid, component)); } private void OnBiasChanged(double value) @@ -220,7 +215,7 @@ public Texture GetFrame(SpriteSpecifier spriteSpec, TimeSpan curTime, bool loop sprite ??= Frame0(spriteSpec); break; case SpriteSpecifier.Texture texture: - sprite = texture.GetTexture(_resourceCache); + sprite = GetTexture(texture); break; default: throw new NotImplementedException(); diff --git a/Robust.Client/GameObjects/EntitySystems/UserInterfaceSystem.cs b/Robust.Client/GameObjects/EntitySystems/UserInterfaceSystem.cs index 289164db9ad..924a4085f7b 100644 --- a/Robust.Client/GameObjects/EntitySystems/UserInterfaceSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/UserInterfaceSystem.cs @@ -16,13 +16,13 @@ public sealed class UserInterfaceSystem : SharedUserInterfaceSystem public override void Initialize() { base.Initialize(); - ProtoManager.PrototypesReloaded += OnProtoReload; + ProtoMan.PrototypesReloaded += OnProtoReload; } public override void Shutdown() { base.Shutdown(); - ProtoManager.PrototypesReloaded -= OnProtoReload; + ProtoMan.PrototypesReloaded -= OnProtoReload; } /// diff --git a/Robust.Client/GameStates/ClientGameStateManager.cs b/Robust.Client/GameStates/ClientGameStateManager.cs index d9edd7a1e5e..9ef5161534e 100644 --- a/Robust.Client/GameStates/ClientGameStateManager.cs +++ b/Robust.Client/GameStates/ClientGameStateManager.cs @@ -321,6 +321,7 @@ public void ApplyGameState() _prof.WriteValue($"State buffer size", curBufSize); _prof.WriteValue($"State apply count", targetProcessedTick.Value - _timing.LastProcessedTick.Value); + _prof.EmitEntities(_entities.EntityCount); bool processedAny = false; @@ -1037,6 +1038,9 @@ private void CreateNewEntity(EntityState state, GameTick toTick) if (metaState == null) throw new MissingMetadataException(state.NetEntity); + // record the entity we created to the profiler + using var group = _prof.Group($"Create entity {metaState.PrototypeId}"); + var uid = _entities.CreateEntity(metaState.PrototypeId, out var newMeta); _toApply.Add(uid, new(uid, state.NetEntity, newMeta, true, false, GameTick.Zero, state, null, null)); _created.Add(state.NetEntity); diff --git a/Robust.Client/Graphics/Clyde/Clyde.GLFeatures.cs b/Robust.Client/Graphics/Clyde/Clyde.GLFeatures.cs index 1f474590539..ee3addaa154 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.GLFeatures.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.GLFeatures.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Text; using OpenToolkit.Graphics.OpenGL4; namespace Robust.Client.Graphics.Clyde @@ -201,17 +202,17 @@ private HashSet GetGLExtensions() if (!_isGLES) { var extensions = new HashSet(); - var extensionsText = ""; + var extensionsText = new StringBuilder(); // Desktop OpenGL uses this API to discourage static buffers var count = GL.GetInteger(GetPName.NumExtensions); for (var i = 0; i < count; i++) { if (i != 0) { - extensionsText += " "; + extensionsText.Append(' '); } var extension = GL.GetString(StringNameIndexed.Extensions, i); - extensionsText += extension; + extensionsText.Append(extension); extensions.Add(extension); } _sawmillOgl.Debug("OpenGL Extensions: {0}", extensionsText); diff --git a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs index 933eb05b774..d47ba808ec6 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs @@ -52,19 +52,18 @@ private void RenderTileEdgesChanges(bool value) private void _drawGrids(Viewport viewport, Box2 worldAABB, Box2Rotated worldBounds, IEye eye) { var mapId = eye.Position.MapId; - if (!_mapManager.MapExists(mapId)) + if (!_mapSystem.MapExists(mapId)) { // fall back to nullspace map mapId = MapId.Nullspace; } _grids.Clear(); - _mapManager.FindGridsIntersecting(mapId, worldBounds, ref _grids); + _mapSystem.FindGridsIntersecting(mapId, worldBounds, ref _grids); var requiresFlush = true; GLShaderProgram gridProgram = default!; var gridOverlays = GetOverlaysForSpace(OverlaySpace.WorldSpaceGrids); - var mapSystem = _entityManager.System(); foreach (var mapGrid in _grids) { @@ -86,7 +85,7 @@ private void _drawGrids(Viewport viewport, Box2 worldAABB, Box2Rotated worldBoun } gridProgram.SetUniform(UniIModelMatrix, _transformSystem.GetWorldMatrix(mapGrid)); - var enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds); + var enumerator = _mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds); // Handle base texture updates. while (enumerator.MoveNext(out var chunk)) @@ -123,7 +122,7 @@ private void _drawGrids(Viewport viewport, Box2 worldAABB, Box2Rotated worldBoun // Handle edge sprites. if (_drawTileEdges) { - enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds); + enumerator = _mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds); while (enumerator.MoveNext(out var chunk)) { var datum = data[chunk.Indices]; @@ -132,7 +131,7 @@ private void _drawGrids(Viewport viewport, Box2 worldAABB, Box2Rotated worldBoun } } - enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds); + enumerator = _mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds); // Draw chunks while (enumerator.MoveNext(out var chunk)) diff --git a/Robust.Client/Graphics/Clyde/Clyde.Helpers.cs b/Robust.Client/Graphics/Clyde/Clyde.Helpers.cs index a71b0e827ee..7c2ea83413c 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Helpers.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Helpers.cs @@ -12,6 +12,8 @@ namespace Robust.Client.Graphics.Clyde { internal sealed partial class Clyde { + private unsafe delegate* unmanaged _glUniformMatrix3fv; + private void GLClearColor(Color color) { GL.ClearColor(color.R, color.G, color.B, color.A); @@ -290,5 +292,37 @@ private nint LoadGLProc(string name) return proc; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void UniformMatrix3fv(int location, in Matrix3x2 value) + { + // OpenTK's pointer overload allocates a RuntimePtr on every call in debug builds. + // Bypass the binding for this hot path until OpenTK is upgraded. + var func = _glUniformMatrix3fv; + if (func == null) + { + func = (delegate* unmanaged) LoadGLProc("glUniformMatrix3fv"); + _glUniformMatrix3fv = func; + } + + // We put the rows of the input matrix into the columns of our GPU matrices. + // This transpose is required, as in C#, we premultiply vectors with matrices + // (vM) while GL postmultiplies vectors with matrices (Mv); however, since + // Matrix3x2 is stored row-major and GL uses column-major, the memory layout + // is the same apart from Matrix3x2's implicit column. + // Assign these individually instead of using a stackalloc initializer because in debug it is allocating yipee. + float* matrix = stackalloc float[9]; + matrix[0] = value.M11; + matrix[1] = value.M12; + matrix[2] = 0; + matrix[3] = value.M21; + matrix[4] = value.M22; + matrix[5] = 0; + matrix[6] = value.M31; + matrix[7] = value.M32; + matrix[8] = 1; + + func(location, 1, 0, matrix); + } } } diff --git a/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs b/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs index 57b4c038eac..82efebf4bac 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs @@ -19,6 +19,8 @@ internal sealed class RenderHandle : IRenderHandle { private readonly Clyde _clyde; private readonly IEntityManager _entities; + private readonly ClydeTexture _whiteClydeTexture; + private readonly Box2 _whiteUv; public DrawingHandleScreen DrawingHandleScreen { get; } public DrawingHandleWorld DrawingHandleWorld { get; } @@ -29,6 +31,8 @@ public RenderHandle(Clyde clyde, IEntityManager entities) _entities = entities; var white = _clyde.GetStockTexture(ClydeStockTexture.White); + _whiteClydeTexture = ExtractTexture(white, null, out var whiteBounds); + _whiteUv = WorldTextureBoundsToUV(_whiteClydeTexture, whiteBounds); DrawingHandleScreen = new DrawingHandleScreenImpl(white, this); DrawingHandleWorld = new DrawingHandleWorldImpl(white, this); } @@ -93,6 +97,30 @@ public void DrawTextureWorld(Texture texture, Vector2 bl, Vector2 br, Vector2 tl _clyde.DrawTexture(clydeTexture.TextureId, bl, br, tl, tr, in modulate, in sr); } + public void DrawTextureWorldBatch(Texture texture, ReadOnlySpan rects, Color modulate) + { + var clydeTexture = ExtractTexture(texture, null, out var csr); + var sr = WorldTextureBoundsToUV(clydeTexture, csr); + _clyde.DrawTextureBatch(clydeTexture.TextureId, rects, modulate, in sr); + } + + public void DrawTextureWorldBatchUnmodulated(Texture texture, ReadOnlySpan rects) + { + var clydeTexture = ExtractTexture(texture, null, out var csr); + var sr = WorldTextureBoundsToUV(clydeTexture, csr); + _clyde.DrawTextureBatchUnmodulated(clydeTexture.TextureId, rects, in sr); + } + + public void DrawRectWorldBatch(ReadOnlySpan rects, Color modulate) + { + _clyde.DrawRectBatch(_whiteClydeTexture.TextureId, rects, modulate, in _whiteUv); + } + + public void DrawRectWorldBatchUnmodulated(ReadOnlySpan rects) + { + _clyde.DrawRectBatchUnmodulated(_whiteClydeTexture.TextureId, rects, in _whiteUv); + } + internal static Box2 WorldTextureBoundsToUV(ClydeTexture texture, UIBox2 csr) { var (w, h) = texture.Size; @@ -488,6 +516,16 @@ public override void DrawRect(Box2 rect, Color color, bool filled = true) } } + public override void DrawRects(ReadOnlySpan rects) + { + _renderHandle.DrawRectWorldBatch(rects, Modulate); + } + + public override void DrawRectsUnmodulated(ReadOnlySpan rects) + { + _renderHandle.DrawRectWorldBatchUnmodulated(rects); + } + public override void DrawRect(in Box2Rotated rect, Color color, bool filled = true) { if (filled) @@ -539,6 +577,18 @@ public override void DrawTextureRectRegion(Texture texture, in Box2Rotated quad, quad.TopLeft, quad.TopRight, color, in subRegion); } + /// + public override void DrawTextureRects(Texture texture, ReadOnlySpan rects) + { + _renderHandle.DrawTextureWorldBatch(texture, rects, Modulate); + } + + /// + public override void DrawTextureRectsUnmodulated(Texture texture, ReadOnlySpan rects) + { + _renderHandle.DrawTextureWorldBatchUnmodulated(texture, rects); + } + public override void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture, ReadOnlySpan vertices) { diff --git a/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs b/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs index f7d419161d9..7d6f67220c2 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs @@ -620,16 +620,191 @@ private void DrawTexture(ClydeHandle texture, Vector2 bl, Vector2 br, Vector2 tl // TODO: split batch if necessary. var vIdx = BatchVertexIndex; - BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, new Vector2(0, 0), modulate); - BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, new Vector2(1, 0), modulate); - BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, new Vector2(1, 1), modulate); - BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, new Vector2(0, 1), modulate); + BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, Vector2.Zero, modulate); + BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, Vector2.UnitX, modulate); + BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, Vector2.One, modulate); + BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, Vector2.UnitY, modulate); BatchVertexIndex += 4; QuadBatchIndexWrite(BatchIndexData, ref BatchIndexIndex, (ushort) vIdx); _debugStats.LastClydeDrawCalls += 1; } + private void DrawTextureBatch( + ClydeHandle texture, + ReadOnlySpan rects, + Color modulate, + in Box2 texCoords) + { + DrawTextureBatch(texture, rects, modulate, in texCoords, true); + } + + private void DrawTextureBatchUnmodulated( + ClydeHandle texture, + ReadOnlySpan rects, + in Box2 texCoords) + { + DrawTextureBatch(texture, rects, Color.White, in texCoords, false); + } + + private void DrawTextureBatch( + ClydeHandle texture, + ReadOnlySpan rects, + Color modulate, + in Box2 texCoords, + bool applyModulation) + { + if (rects.Length == 0) + return; + + // Avoids calling EnsureBatchSpaceAvailable / EnsureBatchState for each individual quad. + var primitiveType = GetQuadBatchPrimitiveType(); + var indexCount = GetQuadBatchIndexCount(); + var rectIndex = 0; + + while (rectIndex < rects.Length) + { + var availableQuads = GetAvailableBatchQuads(indexCount); + + if (availableQuads <= 0) + { + FlushBatchQueue(); + continue; + } + + EnsureBatchState(texture, true, primitiveType, _queuedShader); + + availableQuads = GetAvailableBatchQuads(indexCount); + + if (availableQuads <= 0) + { + FlushBatchQueue(); + continue; + } + + var count = Math.Min(availableQuads, rects.Length - rectIndex); + + for (var i = 0; i < count; i++) + { + ref readonly var rect = ref rects[rectIndex + i]; + var color = rect.Modulate ?? Color.White; + + if (applyModulation) + color *= modulate; + + // Can probably SIMD this more somehow but future concern. + var quad = rect.Quad; + var transform = quad.Transform * _currentMatrixModel; + + var bl = Vector2.Transform(quad.Box.BottomLeft, transform); + var br = Vector2.Transform(quad.Box.BottomRight, transform); + var tr = Vector2.Transform(quad.Box.TopRight, transform); + var tl = tr + bl - br; + + var vIdx = BatchVertexIndex; + BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, Vector2.Zero, color); + BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, Vector2.UnitX, color); + BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, Vector2.One, color); + BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, Vector2.UnitY, color); + BatchVertexIndex += 4; + QuadBatchIndexWrite(BatchIndexData, ref BatchIndexIndex, (ushort) vIdx); + } + + rectIndex += count; + _debugStats.LastClydeDrawCalls += count; + } + } + + private void DrawRectBatch( + ClydeHandle texture, + ReadOnlySpan rects, + Color modulate, + in Box2 texCoords) + { + DrawRectBatch(texture, rects, modulate, in texCoords, true); + } + + private void DrawRectBatchUnmodulated( + ClydeHandle texture, + ReadOnlySpan rects, + in Box2 texCoords) + { + DrawRectBatch(texture, rects, Color.White, in texCoords, false); + } + + private void DrawRectBatch( + ClydeHandle texture, + ReadOnlySpan rects, + Color modulate, + in Box2 texCoords, + bool applyModulation) + { + if (rects.Length == 0) + return; + + var primitiveType = GetQuadBatchPrimitiveType(); + var indexCount = GetQuadBatchIndexCount(); + var rectIndex = 0; + + while (rectIndex < rects.Length) + { + var availableQuads = GetAvailableBatchQuads(indexCount); + + if (availableQuads <= 0) + { + FlushBatchQueue(); + continue; + } + + EnsureBatchState(texture, true, primitiveType, _queuedShader); + + availableQuads = GetAvailableBatchQuads(indexCount); + + if (availableQuads <= 0) + { + FlushBatchQueue(); + continue; + } + + var count = Math.Min(availableQuads, rects.Length - rectIndex); + + for (var i = 0; i < count; i++) + { + ref readonly var rect = ref rects[rectIndex + i]; + var color = rect.Color; + + if (applyModulation) + color *= modulate; + + var box = rect.Rect; + + var bl = Vector2.Transform(box.BottomLeft, _currentMatrixModel); + var br = Vector2.Transform(box.BottomRight, _currentMatrixModel); + var tr = Vector2.Transform(box.TopRight, _currentMatrixModel); + var tl = tr + bl - br; + + var vIdx = BatchVertexIndex; + BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, Vector2.Zero, color); + BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, Vector2.UnitX, color); + BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, Vector2.One, color); + BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, Vector2.UnitY, color); + BatchVertexIndex += 4; + QuadBatchIndexWrite(BatchIndexData, ref BatchIndexIndex, (ushort) vIdx); + } + + rectIndex += count; + _debugStats.LastClydeDrawCalls += count; + } + } + + private int GetAvailableBatchQuads(int indexCount) + { + // The vertex size is so comically high you're probably never hitting it. + var availableVertices = Math.Max(0, BatchVertexData.Length - BatchVertexIndex - 1); + var availableIndices = Math.Max(0, BatchIndexData.Length - BatchIndexIndex); + return Math.Min(availableVertices / 4, availableIndices / indexCount); + } + private void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, ClydeHandle textureId, ReadOnlySpan indices, ReadOnlySpan vertices) { diff --git a/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs b/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs index c30b639b8be..439e23ebdeb 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs @@ -1,12 +1,9 @@ using System; using System.Buffers; -using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.X86; using System.Threading.Tasks; -using Robust.Client.ComponentTrees; using Robust.Client.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.Graphics; @@ -29,16 +26,31 @@ private void GetSprites(MapId map, Viewport view, IEye eye, Box2Rotated worldBou { ProcessSpriteEntities(map, view, eye, worldBounds, _drawingSpriteList); - // We use a separate list for indexing sprites so that the sort is faster. - indexList = ArrayPool.Shared.Rent(_drawingSpriteList.Count); + var count = _drawingSpriteList.Count; - // populate index list - for (var i = 0; i < _drawingSpriteList.Count; i++) - indexList[i] = i; + indexList = ArrayPool.Shared.Rent(count); + var sortItems = ArrayPool.Shared.Rent(count); + + for (var i = 0; i < count; i++) + { + ref var data = ref _drawingSpriteList[i]; + sortItems[i] = new SpriteSortItem( + i, + data.Sprite.DrawDepth, + data.Sprite.RenderOrder, + data.SpriteScreenBB.Top, + data.Uid); + } - // sort index list // TODO better sorting? parallel merge sort? - Array.Sort(indexList, 0, _drawingSpriteList.Count, new SpriteDrawingOrderComparer(_drawingSpriteList)); + Array.Sort(sortItems, 0, count); + + for (var i = 0; i < count; i++) + { + indexList[i] = sortItems[i].Index; + } + + ArrayPool.Shared.Return(sortItems); } [MethodImpl(MethodImplOptions.NoInlining)] @@ -124,11 +136,11 @@ private void ProcessSprites( // To help explain the remainder of this function, it should be functionally equivalent to the following // three lines of code, but has been expanded & simplified to speed up the calculation: // - // (data.WorldPos, data.WorldRot) = batch.Sys.GetWorldPositionRotation(data.Xform, batch.Query); + // (data.WorldPos, data.WorldRot) = batch.Sys.GetWorldPositionRotation(data.Xform); // var spriteWorldBB = data.Sprite.CalculateRotatedBoundingBox(data.WorldPos, data.WorldRot, batch.ViewRotation); // data.SpriteScreenBB = Viewport.GetWorldToLocalMatrix().TransformBox(spriteWorldBB); - var (pos, rot) = batch.Sys.GetRelativePositionRotation(data.Xform, batch.TreeOwner, batch.Query); + var (pos, rot) = batch.Sys.GetRelativePositionRotation(data.Xform, batch.TreeOwner); pos = new Vector2( batch.TreePos.X + batch.Cos * pos.X - batch.Sin * pos.Y, batch.TreePos.Y + batch.Sin * pos.X + batch.Cos * pos.Y); @@ -216,36 +228,41 @@ private readonly struct BatchData public float Cos { get; init; } } - private sealed class SpriteDrawingOrderComparer : IComparer + private readonly struct SpriteSortItem : IComparable { - private readonly RefList _drawList; + public readonly int Index; + private readonly int _drawDepth; + private readonly uint _renderOrder; + private readonly float _ySort; + private readonly EntityUid _uid; - public SpriteDrawingOrderComparer(RefList drawList) + public SpriteSortItem(int index, int drawDepth, uint renderOrder, float ySort, EntityUid uid) { - _drawList = drawList; + Index = index; + _drawDepth = drawDepth; + _renderOrder = renderOrder; + _ySort = ySort; + _uid = uid; } - public int Compare(int x, int y) + public int CompareTo(SpriteSortItem other) { - var a = _drawList[x]; - var b = _drawList[y]; - - var cmp = a.Sprite.DrawDepth.CompareTo(b.Sprite.DrawDepth); + var cmp = _drawDepth.CompareTo(other._drawDepth); if (cmp != 0) return cmp; - cmp = a.Sprite.RenderOrder.CompareTo(b.Sprite.RenderOrder); + cmp = _renderOrder.CompareTo(other._renderOrder); if (cmp != 0) return cmp; // compare the top of the sprite's BB for y-sorting. Because screen coordinates are flipped, the "top" of the BB is actually the "bottom". - cmp = a.SpriteScreenBB.Top.CompareTo(b.SpriteScreenBB.Top); + cmp = _ySort.CompareTo(other._ySort); if (cmp != 0) return cmp; - return a.Uid.CompareTo(b.Uid); + return _uid.CompareTo(other._uid); } } } diff --git a/Robust.Client/Graphics/Clyde/Clyde.Viewport.cs b/Robust.Client/Graphics/Clyde/Clyde.Viewport.cs index efbc3451502..a474f28bfbf 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Viewport.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Viewport.cs @@ -252,6 +252,7 @@ public void FireClear() IRenderTexture IClydeViewport.RenderTarget => RenderTarget; IRenderTexture IClydeViewport.LightRenderTarget => LightRenderTarget; + IRenderTexture IClydeViewport.FovRenderTarget => _clyde._fovRenderTarget; public IEye? Eye { get; set; } } diff --git a/Robust.Client/Graphics/Clyde/Clyde.cs b/Robust.Client/Graphics/Clyde/Clyde.cs index 03f929c536d..9cad353b87b 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.cs @@ -37,7 +37,6 @@ internal sealed partial class Clyde : IClydeInternal, IPostInjectInit, IEntityEv [Dependency] private IClydeTileDefinitionManager _tileDefinitionManager = default!; [Dependency] private ILightManager _lightManager = default!; [Dependency] private ILogManager _logManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IOverlayManager _overlayManager = default!; [Dependency] private IResourceCache _resourceCache = default!; [Dependency] private IResourceManager _resManager = default!; @@ -476,60 +475,38 @@ private unsafe void SetupDebugCallback() private void DebugMessageCallback(DebugSource source, DebugType type, int id, DebugSeverity severity, int length, IntPtr message, IntPtr userParam) { - var contents = $"{source}: " + Marshal.PtrToStringAnsi(message, length); - - var category = "ogl.debug"; - switch (type) + var category = type switch { - case DebugType.DebugTypePerformance: - category += ".performance"; - break; - case DebugType.DebugTypeOther: - category += ".other"; - break; - case DebugType.DebugTypeError: - category += ".error"; - break; - case DebugType.DebugTypeDeprecatedBehavior: - category += ".deprecated"; - break; - case DebugType.DebugTypeUndefinedBehavior: - category += ".ub"; - break; - case DebugType.DebugTypePortability: - category += ".portability"; - break; - case DebugType.DebugTypeMarker: - case DebugType.DebugTypePushGroup: - case DebugType.DebugTypePopGroup: + DebugType.DebugTypePerformance => "ogl.debug.performance", + DebugType.DebugTypeOther => "ogl.debug.other", + DebugType.DebugTypeError => "ogl.debug.error", + DebugType.DebugTypeDeprecatedBehavior => "ogl.debug.deprecated", + DebugType.DebugTypeUndefinedBehavior => "ogl.debug.ub", + DebugType.DebugTypePortability => "ogl.debug.portability", + DebugType.DebugTypeMarker or DebugType.DebugTypePushGroup or DebugType.DebugTypePopGroup => // These are inserted by our own code so I imagine they're not necessary to log? - return; - default: - throw new ArgumentOutOfRangeException(nameof(type), type, null); - } + null, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; - var sawmill = _logManager.GetSawmill(category); + if (category == null) + return; - switch (severity) + var sawmill = _logManager.GetSawmill(category); + var level = severity switch { - case DebugSeverity.DontCare: - sawmill.Info(contents); - break; - case DebugSeverity.DebugSeverityNotification: - sawmill.Info(contents); - break; - case DebugSeverity.DebugSeverityHigh: - sawmill.Error(contents); - break; - case DebugSeverity.DebugSeverityMedium: - sawmill.Error(contents); - break; - case DebugSeverity.DebugSeverityLow: - sawmill.Warning(contents); - break; - default: - throw new ArgumentOutOfRangeException(nameof(severity), severity, null); - } + DebugSeverity.DontCare => LogLevel.Info, + DebugSeverity.DebugSeverityNotification => LogLevel.Info, + DebugSeverity.DebugSeverityHigh => LogLevel.Error, + DebugSeverity.DebugSeverityMedium => LogLevel.Error, + DebugSeverity.DebugSeverityLow => LogLevel.Warning, + _ => throw new ArgumentOutOfRangeException(nameof(severity), severity, null) + }; + + if (!sawmill.IsLogLevelEnabled(level)) + return; + + sawmill.Log(level, "{0}: {1}", source, Marshal.PtrToStringAnsi(message, length)); } private static DebugProc? _debugMessageCallbackInstance; diff --git a/Robust.Client/Graphics/Clyde/ClydeHeadless.cs b/Robust.Client/Graphics/Clyde/ClydeHeadless.cs index 093527e6b99..c8c708c5d6c 100644 --- a/Robust.Client/Graphics/Clyde/ClydeHeadless.cs +++ b/Robust.Client/Graphics/Clyde/ClydeHeadless.cs @@ -522,6 +522,9 @@ public void Dispose() public IRenderTexture LightRenderTarget { get; } = new DummyRenderTexture(Vector2i.One, new DummyTexture(Vector2i.One)); + public IRenderTexture FovRenderTarget { get; } = + new DummyRenderTexture(Vector2i.One, new DummyTexture(Vector2i.One)); + public IEye? Eye { get; set; } public Vector2i Size { get; } public event Action? ClearCachedResources; diff --git a/Robust.Client/Graphics/Clyde/GLObjects/Clyde.ShaderProgram.cs b/Robust.Client/Graphics/Clyde/GLObjects/Clyde.ShaderProgram.cs index c70ec3734f1..2d7042981d8 100644 --- a/Robust.Client/Graphics/Clyde/GLObjects/Clyde.ShaderProgram.cs +++ b/Robust.Client/Graphics/Clyde/GLObjects/Clyde.ShaderProgram.cs @@ -258,20 +258,9 @@ public void SetUniform(int uniformName, in Matrix3x2 matrix) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private unsafe void SetUniformDirect(int slot, in Matrix3x2 value) - { - // We put the rows of the input matrix into the columns of our GPU matrices - // this transpose is required, as in C#, we premultiply vectors with matrices - // (vM) while GL postmultiplies vectors with matrices (Mv); however, since - // the Matrix3x2 data is stored row-major, and GL uses column-major, the - // memory layout is the same (or would be, if Matrix3x2 didn't have an - // implicit column) - var buf = stackalloc float[9]{ - value.M11, value.M12, 0, - value.M21, value.M22, 0, - value.M31, value.M32, 1 - }; - GL.UniformMatrix3(slot, 1, false, (float*)buf); + private void SetUniformDirect(int slot, in Matrix3x2 value) + { + _clyde.UniformMatrix3fv(slot, value); _clyde.CheckGlError(); } diff --git a/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs b/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs index 9fba50a0506..af7d88d03da 100644 --- a/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs +++ b/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using Robust.Shared.Graphics; using Robust.Shared.Maths; @@ -22,6 +23,19 @@ protected DrawingHandleWorld(Texture white) : base(white) /// Is it filled with color, or just the border lines? public abstract void DrawRect(Box2 rect, Color color, bool filled = true); + /// + /// Draws multiple filled, untextured colored rectangles to the world. All rectangles use the current transform. + /// + /// + /// This is the batched equivalent of repeated filled calls. + /// + public abstract void DrawRects(ReadOnlySpan rects); + + /// + /// Draws multiple filled, untextured colored rectangles without multiplying by the handle modulation color. + /// + public abstract void DrawRectsUnmodulated(ReadOnlySpan rects); + /// /// Draws an untextured colored rectangle to the world.The coordinate system is right handed. /// Make sure to set @@ -58,6 +72,16 @@ public abstract void DrawTextureRectRegion(Texture texture, Box2 quad, public abstract void DrawTextureRectRegion(Texture texture, in Box2Rotated quad, Color? modulate = null, UIBox2? subRegion = null); + /// + /// Draws multiple rotated rectangles for the same texture. + /// + public abstract void DrawTextureRects(Texture texture, ReadOnlySpan rects); + + /// + /// Draws multiple rotated rectangles for the same texture without multiplying by the handle modulation color. + /// + public abstract void DrawTextureRectsUnmodulated(Texture texture, ReadOnlySpan rects); + private Box2 GetQuad(Texture texture, Vector2 position) { return Box2.FromDimensions(position, texture.Size / (float)Ppm); @@ -133,4 +157,14 @@ public void DrawTextureRect(Texture texture, in Box2Rotated quad, Color? modulat DrawTextureRectRegion(texture, in quad, modulate); } } + + /// + /// A rotated rectangle for batched world texture drawing. + /// + public readonly record struct WorldTextureRect(Box2Rotated Quad, Color? Modulate = null); + + /// + /// An axis-aligned rectangle for batched world rectangle drawing. + /// + public readonly record struct WorldRect(Box2 Rect, Color Color); } diff --git a/Robust.Client/Graphics/IClydeViewport.cs b/Robust.Client/Graphics/IClydeViewport.cs index e4001978298..0e5525965b8 100644 --- a/Robust.Client/Graphics/IClydeViewport.cs +++ b/Robust.Client/Graphics/IClydeViewport.cs @@ -25,6 +25,13 @@ public interface IClydeViewport : IDisposable IRenderTexture RenderTarget { get; } IRenderTexture LightRenderTarget { get; } + /// + /// The render target holding the FOV shadow-depth map for this viewport's eye. + /// Can be sampled by world-space shaders (together with the eye position) to + /// determine per-pixel whether a world point is occluded by FOV. + /// + IRenderTexture FovRenderTarget { get; } + IEye? Eye { get; set; } Vector2i Size { get; } diff --git a/Robust.Client/Network/Transfer/ClientTransferManager.cs b/Robust.Client/Network/Transfer/ClientTransferManager.cs index 781679d9e63..4f3af82a39b 100644 --- a/Robust.Client/Network/Transfer/ClientTransferManager.cs +++ b/Robust.Client/Network/Transfer/ClientTransferManager.cs @@ -46,6 +46,13 @@ public void Initialize() _netManager.RegisterNetMessage(RxTransferInit, NetMessageAccept.Client | NetMessageAccept.Handshake); _netManager.RegisterNetMessage(); _netManager.RegisterNetMessage(RxTransferData, NetMessageAccept.Client | NetMessageAccept.Handshake); + _netManager.Disconnect += OnNetDisconnect; + } + + private void OnNetDisconnect(object? sender, NetDisconnectedArgs e) + { + _transferImpl?.Dispose(); + _transferImpl = null; } private async void RxTransferInit(MsgTransferInit message) diff --git a/Robust.Client/Physics/GridFixtureSystem.cs b/Robust.Client/Physics/GridFixtureSystem.cs index 9907f81ef08..ac42b077369 100644 --- a/Robust.Client/Physics/GridFixtureSystem.cs +++ b/Robust.Client/Physics/GridFixtureSystem.cs @@ -13,7 +13,6 @@ namespace Robust.Client.Physics internal sealed partial class GridFixtureSystem : SharedGridFixtureSystem { [Dependency] private IOverlayManager _overlay = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private SharedMapSystem _map = default!; @@ -29,7 +28,7 @@ public bool EnableDebug if (_enableDebug) { - var overlay = new GridSplitNodeOverlay(_mapManager, this, _transform, _map); + var overlay = new GridSplitNodeOverlay(this, _transform, _map); _overlay.AddOverlay(overlay); RaiseNetworkEvent(new RequestGridNodesMessage()); } @@ -72,14 +71,12 @@ private sealed class GridSplitNodeOverlay : Overlay { public override OverlaySpace Space => OverlaySpace.WorldSpace; - private readonly IMapManager _mapManager; private readonly GridFixtureSystem _system; private readonly SharedTransformSystem _transform; private readonly SharedMapSystem _map; - public GridSplitNodeOverlay(IMapManager mapManager, GridFixtureSystem system, SharedTransformSystem transform, SharedMapSystem map) + public GridSplitNodeOverlay(GridFixtureSystem system, SharedTransformSystem transform, SharedMapSystem map) { - _mapManager = mapManager; _system = system; _transform = transform; _map = map; @@ -91,7 +88,7 @@ protected internal override void Draw(in OverlayDrawArgs args) var state = (_system, _transform, args.WorldBounds, worldHandle); - _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref state, + _map.FindGridsIntersecting(args.MapId, args.WorldBounds, ref state, (EntityUid uid, MapGridComponent grid, ref (GridFixtureSystem system, SharedTransformSystem transform, Box2Rotated worldBounds, DrawingHandleWorld worldHandle) tuple) => { diff --git a/Robust.Client/Placement/IPlacementManager.cs b/Robust.Client/Placement/IPlacementManager.cs index 15b8f09167d..756521b2e1c 100644 --- a/Robust.Client/Placement/IPlacementManager.cs +++ b/Robust.Client/Placement/IPlacementManager.cs @@ -24,7 +24,6 @@ public interface IPlacementManager IEntityManager EntityManager { get; } IEyeManager EyeManager { get; } - IMapManager MapManager { get; } /// /// The direction to spawn the entity in (presently exposed for EntitySpawnWindow UI) diff --git a/Robust.Client/Placement/Modes/AlignTileAny.cs b/Robust.Client/Placement/Modes/AlignTileAny.cs index ff51aa89eb0..9523b229514 100644 --- a/Robust.Client/Placement/Modes/AlignTileAny.cs +++ b/Robust.Client/Placement/Modes/AlignTileAny.cs @@ -17,7 +17,7 @@ public override void AlignPlacementMode(ScreenCoordinates mouseScreen) // Go over diagonal size so when placing in a line it doesn't stop snapping. const float searchBoxSize = 2f; // size of search box in meters - MouseCoords = ScreenToCursorGrid(mouseScreen).AlignWithClosestGridTile(searchBoxSize, pManager.EntityManager, pManager.MapManager); + MouseCoords = ScreenToCursorGrid(mouseScreen).AlignWithClosestGridTile(searchBoxSize, pManager.EntityManager); var gridId = pManager.EntityManager.System().GetGrid(MouseCoords); diff --git a/Robust.Client/Placement/PlacementManager.cs b/Robust.Client/Placement/PlacementManager.cs index eda141df12e..b5ec4a6c9a3 100644 --- a/Robust.Client/Placement/PlacementManager.cs +++ b/Robust.Client/Placement/PlacementManager.cs @@ -33,13 +33,13 @@ public sealed partial class PlacementManager : IPlacementManager, IDisposable, I [Dependency] internal IPlayerManager PlayerManager = default!; [Dependency] internal IResourceCache ResourceCache = default!; [Dependency] private IReflectionManager _reflectionManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IGameTiming _time = default!; [Dependency] private IEyeManager _eyeManager = default!; [Dependency] internal IInputManager InputManager = default!; [Dependency] private IEntitySystemManager _entitySystemManager = default!; [Dependency] private IEntityManager _entityManager = default!; [Dependency] private IPrototypeManager _prototypeManager = default!; + [Dependency] private IComponentFactory _factory = default!; [Dependency] private IBaseClient _baseClient = default!; [Dependency] private IOverlayManager _overlayManager = default!; [Dependency] internal IClyde Clyde = default!; @@ -48,7 +48,6 @@ public sealed partial class PlacementManager : IPlacementManager, IDisposable, I public IEntityManager EntityManager => _entityManager; public IEyeManager EyeManager => _eyeManager; - public IMapManager MapManager => _mapManager; private ISawmill _sawmill = default!; @@ -787,7 +786,7 @@ public void PreparePlacementTexList(List? texs, boo sc.Comp.NoRotation = noRot; - if (prototype != null && prototype.TryGetComponent("Sprite", out var spriteComp)) + if (prototype != null && prototype.TryComp(out var spriteComp, _factory)) { Sprite.SetScale(sc.AsNullable(), spriteComp.Scale); } diff --git a/Robust.Client/Placement/PlacementMode.cs b/Robust.Client/Placement/PlacementMode.cs index 8ca0de2bd48..810ec1f0220 100644 --- a/Robust.Client/Placement/PlacementMode.cs +++ b/Robust.Client/Placement/PlacementMode.cs @@ -201,7 +201,7 @@ public TileRef GetTileRef(EntityCoordinates coordinates) return gridUidOpt is { } gridUid && gridUid.IsValid() ? pManager.EntityManager.System().GetTileRef(gridUid, pManager.EntityManager.GetComponent(gridUid), MouseCoords) : new TileRef(gridUidOpt ?? EntityUid.Invalid, - MouseCoords.ToVector2i(pManager.EntityManager, pManager.MapManager, pManager.EntityManager.System()), Tile.Empty); + MouseCoords.ToVector2i(pManager.EntityManager, pManager.EntityManager.System()), Tile.Empty); } public TextureResource GetSprite(string key) @@ -267,7 +267,8 @@ protected EntityCoordinates ScreenToCursorGrid(ScreenCoordinates coords) { var mapCoords = pManager.EyeManager.PixelToMap(coords.Position); var transformSys = pManager.EntityManager.System(); - if (!pManager.MapManager.TryFindGridAt(mapCoords, out var gridUid, out _)) + var mapSys = pManager.EntityManager.System(); + if (!mapSys.TryFindGridAt(mapCoords, out var gridUid, out _)) { return transformSys.ToCoordinates(mapCoords); } diff --git a/Robust.Client/Player/PlayerManager.cs b/Robust.Client/Player/PlayerManager.cs index 0bc24f35cce..16d248ea5e6 100644 --- a/Robust.Client/Player/PlayerManager.cs +++ b/Robust.Client/Player/PlayerManager.cs @@ -10,312 +10,311 @@ using Robust.Shared.Player; using Robust.Shared.Utility; -namespace Robust.Client.Player +namespace Robust.Client.Player; + +/// +/// Here's the player controller. This will handle attaching GUIs and input to controllable things. +/// Why not just attach the inputs directly? It's messy! This makes the whole thing nicely encapsulated. +/// This class also communicates with the server to let the server control what entity it is attached to. +/// +internal sealed partial class PlayerManager : SharedPlayerManager, IPlayerManager { + [Dependency] private IClientNetManager _network = default!; + [Dependency] private IBaseClient _client = default!; + /// - /// Here's the player controller. This will handle attaching GUIs and input to controllable things. - /// Why not just attach the inputs directly? It's messy! This makes the whole thing nicely encapsulated. - /// This class also communicates with the server to let the server control what entity it is attached to. + /// Received player states that had an unknown . /// - internal sealed partial class PlayerManager : SharedPlayerManager, IPlayerManager - { - [Dependency] private IClientNetManager _network = default!; - [Dependency] private IBaseClient _client = default!; + private Dictionary _pendingStates = new(); + private List _pending = new(); - /// - /// Received player states that had an unknown . - /// - private Dictionary _pendingStates = new(); - private List _pending = new(); - - /// - public override ICommonSession[] NetworkedSessions + /// + public override ICommonSession[] NetworkedSessions + { + get { - get - { - return LocalSession != null - ? new[] { LocalSession } - : Array.Empty(); - } + return LocalSession != null + ? new[] { LocalSession } + : Array.Empty(); } + } - /// - public override int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? -1; + /// + public override int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? -1; - public LocalPlayer? LocalPlayer { get; private set; } + public LocalPlayer? LocalPlayer { get; private set; } - public event Action? LocalStatusChanged; - public event Action? PlayerListUpdated; - public event Action? LocalPlayerDetached; - public event Action? LocalPlayerAttached; - public event Action<(ICommonSession? Old, ICommonSession? New)>? LocalSessionChanged; + public event Action? LocalStatusChanged; + public event Action? PlayerListUpdated; + public event Action? LocalPlayerDetached; + public event Action? LocalPlayerAttached; + public event Action<(ICommonSession? Old, ICommonSession? New)>? LocalSessionChanged; - /// - public override void Initialize(int maxPlayers) + /// + public override void Initialize(int maxPlayers) + { + base.Initialize(maxPlayers); + _network.RegisterNetMessage(); + _network.RegisterNetMessage(HandlePlayerList); + PlayerStatusChanged += StatusChanged; + } + + private void StatusChanged(object? sender, SessionStatusEventArgs e) + { + if (e.Session == LocalPlayer?.Session) + LocalStatusChanged?.Invoke(e); + } + + public void SetupSinglePlayer(string name) + { + if (LocalSession != null) + throw new InvalidOperationException($"Player manager already running?"); + + var session = CreateAndAddSession(default, name); + session.ClientSide = true; + SetLocalSession(session); + Startup(); + PlayerListUpdated?.Invoke(); + } + + public void SetupMultiplayer(INetChannel channel) + { + if (LocalSession != null) + throw new InvalidOperationException($"Player manager already running?"); + + SetLocalSession(CreateAndAddSession(channel)); + Startup(); + _network.ClientSendMessage(new MsgPlayerListReq()); + } + + public void SetLocalSession(ICommonSession? session) + { + if (session == LocalSession) + return; + + var old = LocalSession; + + if (old?.AttachedEntity is { } oldUid) { - base.Initialize(maxPlayers); - _network.RegisterNetMessage(); - _network.RegisterNetMessage(HandlePlayerList); - PlayerStatusChanged += StatusChanged; + LocalSession = null; + LocalPlayer = null; + Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(oldUid)}."); + EntManager.EventBus.RaiseLocalEvent(oldUid, new LocalPlayerDetachedEvent(oldUid), true); + LocalPlayerDetached?.Invoke(oldUid); } - private void StatusChanged(object? sender, SessionStatusEventArgs e) + LocalSession = session; + LocalPlayer = session == null ? null : new LocalPlayer(session); + Sawmill.Info($"Changing local session from {old?.ToString() ?? "null"} to {session?.ToString() ?? "null"}."); + LocalSessionChanged?.Invoke((old, LocalSession)); + + if (session?.AttachedEntity is { } newUid) { - if (e.Session == LocalPlayer?.Session) - LocalStatusChanged?.Invoke(e); + Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(newUid)}."); + EntManager.EventBus.RaiseLocalEvent(newUid, new LocalPlayerAttachedEvent(newUid), true); + LocalPlayerAttached?.Invoke(newUid); } + } - public void SetupSinglePlayer(string name) - { - if (LocalSession != null) - throw new InvalidOperationException($"Player manager already running?"); + /// + public override void Shutdown() + { + SetAttachedEntity(LocalSession, null, out _); + LocalPlayer = null; + LocalSession = null; + _pendingStates.Clear(); + base.Shutdown(); + PlayerListUpdated?.Invoke(); + } - var session = CreateAndAddSession(default, name); - session.ClientSide = true; - SetLocalSession(session); - Startup(); - PlayerListUpdated?.Invoke(); - } + public override bool SetAttachedEntity(ICommonSession? session, EntityUid? uid, out ICommonSession? kicked, bool force = false) + { + kicked = null; + if (session == null) + return false; - public void SetupMultiplayer(INetChannel channel) - { - if (LocalSession != null) - throw new InvalidOperationException($"Player manager already running?"); + if (session.AttachedEntity == uid) + return true; + + var old = session.AttachedEntity; + if (!base.SetAttachedEntity(session, uid, out kicked, force)) + return false; + + if (session != LocalSession) + return true; - SetLocalSession(CreateAndAddSession(channel)); - Startup(); - _network.ClientSendMessage(new MsgPlayerListReq()); + if (old.HasValue) + { + Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(old)}."); + EntManager.EventBus.RaiseLocalEvent(old.Value, new LocalPlayerDetachedEvent(old.Value), true); + LocalPlayerDetached?.Invoke(old.Value); } - public void SetLocalSession(ICommonSession? session) + if (uid == null) { - if (session == LocalSession) - return; + Sawmill.Info($"Local player is no longer attached to any entity."); + return true; + } - var old = LocalSession; + if (!EntManager.EntityExists(uid)) + { + Sawmill.Error($"Attempted to attach player to non-existent entity {uid}!"); + return true; + } - if (old?.AttachedEntity is { } oldUid) - { - LocalSession = null; - LocalPlayer = null; - Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(oldUid)}."); - EntManager.EventBus.RaiseLocalEvent(oldUid, new LocalPlayerDetachedEvent(oldUid), true); - LocalPlayerDetached?.Invoke(oldUid); - } + if (!EntManager.HasComponent(uid.Value)) + { + if (_client.RunLevel != ClientRunLevel.SinglePlayerGame) + Sawmill.Warning($"Attaching local player to an entity {EntManager.ToPrettyString(uid)} without an eye. This eye will not be netsynced and may cause issues."); + var eye = Factory.GetComponent(); + eye.NetSyncEnabled = false; + EntManager.AddComponent(uid.Value, eye); + } - LocalSession = session; - LocalPlayer = session == null ? null : new LocalPlayer(session); - Sawmill.Info($"Changing local session from {old?.ToString() ?? "null"} to {session?.ToString() ?? "null"}."); - LocalSessionChanged?.Invoke((old, LocalSession)); + Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(uid)}."); + EntManager.EventBus.RaiseLocalEvent(uid.Value, new LocalPlayerAttachedEvent(uid.Value), true); + LocalPlayerAttached?.Invoke(uid.Value); + return true; + } - if (session?.AttachedEntity is { } newUid) - { - Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(newUid)}."); - EntManager.EventBus.RaiseLocalEvent(newUid, new LocalPlayerAttachedEvent(newUid), true); - LocalPlayerAttached?.Invoke(newUid); - } - } + public void ApplyPlayerStates(IReadOnlyCollection list) + { + var dirty = ApplyStates(list, true); - /// - public override void Shutdown() + if (_pendingStates.Count == 0) { - SetAttachedEntity(LocalSession, null, out _); - LocalPlayer = null; - LocalSession = null; + // This is somewhat inefficient as it might try to re-apply states that failed just a moment ago. + _pending.Clear(); + _pending.AddRange(_pendingStates.Values); _pendingStates.Clear(); - base.Shutdown(); - PlayerListUpdated?.Invoke(); + dirty |= ApplyStates(_pending, false); } - public override bool SetAttachedEntity(ICommonSession? session, EntityUid? uid, out ICommonSession? kicked, bool force = false) - { - kicked = null; - if (session == null) - return false; + if (dirty) + PlayerListUpdated?.Invoke(); + } - if (session.AttachedEntity == uid) - return true; + private bool ApplyStates(IReadOnlyCollection list, bool fullList) + { + if (list.Count == 0) + return false; - var old = session.AttachedEntity; - if (!base.SetAttachedEntity(session, uid, out kicked, force)) - return false; + DebugTools.Assert(_network.IsConnected || _client.RunLevel == ClientRunLevel.SinglePlayerGame // replays use state application. + , "Received player state without being connected?"); + DebugTools.Assert(LocalSession != null, "Received player state before Session finished setup."); - if (session != LocalSession) - return true; + var state = list.FirstOrDefault(s => s.UserId == LocalSession.UserId); - if (old.HasValue) + bool dirty = false; + if (state != null) + { + dirty = true; + if (!EntManager.TryGetEntity(state.ControlledEntity, out var uid) + && state.ControlledEntity is { Valid: true }) { - Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(old)}."); - EntManager.EventBus.RaiseLocalEvent(old.Value, new LocalPlayerDetachedEvent(old.Value), true); - LocalPlayerDetached?.Invoke(old.Value); + Sawmill.Error($"Received player state for local player with an unknown net entity!"); + _pendingStates[state.UserId] = state; } - - if (uid == null) + else { - Sawmill.Info($"Local player is no longer attached to any entity."); - return true; + _pendingStates.Remove(state.UserId); } - if (!EntManager.EntityExists(uid)) - { - Sawmill.Error($"Attempted to attach player to non-existent entity {uid}!"); - return true; - } + SetAttachedEntity(LocalSession, uid, out _, true); + SetStatus(LocalSession, state.Status); + } - if (!EntManager.HasComponent(uid.Value)) - { - if (_client.RunLevel != ClientRunLevel.SinglePlayerGame) - Sawmill.Warning($"Attaching local player to an entity {EntManager.ToPrettyString(uid)} without an eye. This eye will not be netsynced and may cause issues."); - var eye = Factory.GetComponent(); - eye.NetSyncEnabled = false; - EntManager.AddComponent(uid.Value, eye); - } + return UpdatePlayerList(list, fullList) || dirty; + } - Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(uid)}."); - EntManager.EventBus.RaiseLocalEvent(uid.Value, new LocalPlayerAttachedEvent(uid.Value), true); - LocalPlayerAttached?.Invoke(uid.Value); - return true; - } + /// + /// Handles the incoming PlayerList message from the server. + /// + private void HandlePlayerList(MsgPlayerList msg) + { + ApplyPlayerStates(msg.Plyrs); + } - public void ApplyPlayerStates(IReadOnlyCollection list) + /// + /// Compares the server player list to the client one, and updates if needed. + /// + private bool UpdatePlayerList(IEnumerable remotePlayers, bool fullList) + { + var dirty = false; + var users = new List(); + foreach (var state in remotePlayers) { - var dirty = ApplyStates(list, true); + users.Add(state.UserId); - if (_pendingStates.Count == 0) + if (!EntManager.TryGetEntity(state.ControlledEntity, out var controlled) + && state.ControlledEntity is { Valid: true }) { - // This is somewhat inefficient as it might try to re-apply states that failed just a moment ago. - _pending.Clear(); - _pending.AddRange(_pendingStates.Values); - _pendingStates.Clear(); - dirty |= ApplyStates(_pending, false); + _pendingStates[state.UserId] = state; + } + else + { + _pendingStates.Remove(state.UserId); } - if (dirty) - PlayerListUpdated?.Invoke(); - } - - private bool ApplyStates(IReadOnlyCollection list, bool fullList) - { - if (list.Count == 0) - return false; - - DebugTools.Assert(_network.IsConnected || _client.RunLevel == ClientRunLevel.SinglePlayerGame // replays use state application. - , "Received player state without being connected?"); - DebugTools.Assert(LocalSession != null, "Received player state before Session finished setup."); - - var state = list.FirstOrDefault(s => s.UserId == LocalSession.UserId); - - bool dirty = false; - if (state != null) + if (!InternalSessions.TryGetValue(state.UserId, out var session)) { + // This is a new userid, so we create a new session. + DebugTools.Assert(state.UserId != LocalPlayer?.UserId); + var newSession = (ICommonSessionInternal)CreateAndAddSession(state.UserId, state.Name); + SetStatus(newSession, state.Status); + SetAttachedEntity(newSession, controlled, out _, true); dirty = true; - if (!EntManager.TryGetEntity(state.ControlledEntity, out var uid) - && state.ControlledEntity is { Valid: true }) - { - Sawmill.Error($"Received player state for local player with an unknown net entity!"); - _pendingStates[state.UserId] = state; - } - else - { - _pendingStates.Remove(state.UserId); - } - - SetAttachedEntity(LocalSession, uid, out _, true); - SetStatus(LocalSession, state.Status); + continue; } - return UpdatePlayerList(list, fullList) || dirty; - } + // Check if the data is actually different + if (session.Name == state.Name + && session.Status == state.Status + && session.AttachedEntity == controlled) + { + continue; + } - /// - /// Handles the incoming PlayerList message from the server. - /// - private void HandlePlayerList(MsgPlayerList msg) - { - ApplyPlayerStates(msg.Plyrs); + dirty = true; + var local = (ICommonSessionInternal)session; + local.SetName(state.Name); + SetStatus(local, state.Status); + SetAttachedEntity(local, controlled, out _, true); } - /// - /// Compares the server player list to the client one, and updates if needed. - /// - private bool UpdatePlayerList(IEnumerable remotePlayers, bool fullList) + // Remove old users. This only works if the provided state is a list of all players + if (fullList) { - var dirty = false; - var users = new List(); - foreach (var state in remotePlayers) + foreach (var oldUser in InternalSessions.Keys.ToArray()) { - users.Add(state.UserId); - - if (!EntManager.TryGetEntity(state.ControlledEntity, out var controlled) - && state.ControlledEntity is { Valid: true }) - { - _pendingStates[state.UserId] = state; - } - else - { - _pendingStates.Remove(state.UserId); - } - - if (!InternalSessions.TryGetValue(state.UserId, out var session)) - { - // This is a new userid, so we create a new session. - DebugTools.Assert(state.UserId != LocalPlayer?.UserId); - var newSession = (ICommonSessionInternal)CreateAndAddSession(state.UserId, state.Name); - SetStatus(newSession, state.Status); - SetAttachedEntity(newSession, controlled, out _, true); - dirty = true; + if (users.Contains(oldUser)) continue; - } - // Check if the data is actually different - if (session.Name == state.Name - && session.Status == state.Status - && session.AttachedEntity == controlled) - { + if (InternalSessions[oldUser].ClientSide) continue; - } + DebugTools.Assert(oldUser != LocalUser + || LocalUser == null + || LocalUser == default(NetUserId), + "Client is still connected to the server but not in the list of players?"); + RemoveSession(oldUser); + _pendingStates.Remove(oldUser); dirty = true; - var local = (ICommonSessionInternal)session; - local.SetName(state.Name); - SetStatus(local, state.Status); - SetAttachedEntity(local, controlled, out _, true); } - - // Remove old users. This only works if the provided state is a list of all players - if (fullList) - { - foreach (var oldUser in InternalSessions.Keys.ToArray()) - { - if (users.Contains(oldUser)) - continue; - - if (InternalSessions[oldUser].ClientSide) - continue; - - DebugTools.Assert(oldUser != LocalUser - || LocalUser == null - || LocalUser == default(NetUserId), - "Client is still connected to the server but not in the list of players?"); - RemoveSession(oldUser); - _pendingStates.Remove(oldUser); - dirty = true; - } - } - - return dirty; } - public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session) - { - if (LocalEntity == uid) - { - session = LocalSession!; - return true; - } + return dirty; + } - session = null; - return false; + public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session) + { + if (LocalEntity == uid) + { + session = LocalSession!; + return true; } + + session = null; + return false; } } diff --git a/Robust.Client/ResourceManagement/ResourceCache.Preload.cs b/Robust.Client/ResourceManagement/ResourceCache.Preload.cs index 5e35b74de95..7d1e5a693c2 100644 --- a/Robust.Client/ResourceManagement/ResourceCache.Preload.cs +++ b/Robust.Client/ResourceManagement/ResourceCache.Preload.cs @@ -325,10 +325,18 @@ private void PreloadRsis(ISawmill sawmill) // Finalize the atlases. for (var i = 0; i < imageAtlases.Count; i++) { - var atlasTexture = Clyde.LoadTextureFromImage(imageAtlases[i], $"Meta atlas {i}"); - finalAtlases.Add(atlasTexture); + var imageAtlas = imageAtlases[i]; + try + { + var atlasTexture = Clyde.LoadTextureFromImage(imageAtlas, $"Meta atlas {i}"); + finalAtlases.Add(atlasTexture); - sawmill.Debug($"(Meta atlas {i}) - cropped utilization: {(float)finalPixels[i] / (maxSize * imageAtlases[i].Height):P2}, fill percentage: {(float)imageAtlases[i].Height / maxSize:P2}"); + sawmill.Debug($"(Meta atlas {i}) - cropped utilization: {(float)finalPixels[i] / (maxSize * imageAtlas.Height):P2}, fill percentage: {(float)imageAtlas.Height / maxSize:P2}"); + } + finally + { + imageAtlas.Dispose(); + } } // Finally, reference the actual atlas from the RSIs. @@ -359,23 +367,30 @@ private void PreloadRsis(ISawmill sawmill) var errors = 0; foreach (var data in rsiList) { - if (data.Bad) - { - errors += 1; - continue; - } - try { - var rsiRes = new RSIResource(); - rsiRes.LoadFinish(this, data); - resList[data.Path] = rsiRes; + if (data.Bad) + { + errors += 1; + continue; + } + + try + { + var rsiRes = new RSIResource(); + rsiRes.LoadFinish(this, data); + resList[data.Path] = rsiRes; + } + catch (Exception e) + { + sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}"); + data.Bad = true; + errors += 1; + } } - catch (Exception e) + finally { - sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}"); - data.Bad = true; - errors += 1; + data.AtlasSheet?.Dispose(); } } diff --git a/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs b/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs index 0880f8e9518..494834e5490 100644 --- a/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs +++ b/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs @@ -83,27 +83,44 @@ private static void LoadPreTextureFolder(IResourceManager manager, LoadStepData metadata = RsiLoading.LoadRsiMetadata(manifestFile); } - data.FrameCounts = RsiLoading.CalculateFrameCounts(metadata); - data.Images = RsiLoading.LoadImages( - metadata, - SixLabors.ImageSharp.Configuration.Default, - name => - { - var texPath = data.Path / (name + ".png"); - return manager.ContentFileRead(texPath); - }); + Image[]? images = null; + Image sheet; - var sheet = RsiLoading.GenerateAtlas( - metadata, - data.FrameCounts, - data.Images, - SixLabors.ImageSharp.Configuration.Default, - out var dimensionX); + try + { + data.FrameCounts = RsiLoading.CalculateFrameCounts(metadata); + images = RsiLoading.LoadImages( + metadata, + SixLabors.ImageSharp.Configuration.Default, + name => + { + var texPath = data.Path / (name + ".png"); + return manager.ContentFileRead(texPath); + }); + + sheet = RsiLoading.GenerateAtlas( + metadata, + data.FrameCounts, + images, + SixLabors.ImageSharp.Configuration.Default, + out var dimensionX); + + data.AtlasSheet = sheet; + data.DimX = dimensionX; + } + finally + { + if (images != null) + { + foreach (var image in images) + { + image.Dispose(); + } + } + } LoadPreTextureCommon(metadata, data); - data.AtlasSheet = sheet; - data.DimX = dimensionX; data.LoadParameters = metadata.LoadParameters; data.MetaAtlas = metadata.MetaAtlas; } @@ -392,7 +409,6 @@ internal sealed class LoadStepData public int DimX; public StateReg[] AtlasList = default!; public int[] FrameCounts = default!; - public Image[] Images = default!; public Vector2i FrameSize; public Dictionary CallbackOffsets = default!; public Texture AtlasTexture = default!; diff --git a/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs b/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs index d0ae97c601f..c450c166e24 100644 --- a/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs +++ b/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs @@ -88,6 +88,18 @@ internal void LoadFinish(IResourceCache cache, LoadStepData data) data.Image.Dispose(); } + /// + /// Loads a texture from path as an . + /// Texture parameters are taken from a matching .yml file if present. + /// + public static OwnedTexture LoadOwnedTexture(IResourceManager resManager, IClyde clyde, ResPath path) + { + var loadParams = TryLoadTextureParameters(resManager, path) ?? TextureLoadParameters.Default; + using var stream = resManager.ContentFileRead(path); + using var image = Image.Load(stream); + return clyde.LoadTextureFromImage(image, path.ToString(), loadParams); + } + private static TextureLoadParameters? TryLoadTextureParameters(IResourceManager cache, ResPath path) { var metaPath = path.WithName(path.Filename + ".yml"); diff --git a/Robust.Client/Robust.Client.csproj b/Robust.Client/Robust.Client.csproj index c99c8a4f4a9..6ee0278b6c4 100644 --- a/Robust.Client/Robust.Client.csproj +++ b/Robust.Client/Robust.Client.csproj @@ -31,6 +31,7 @@ + @@ -73,6 +74,7 @@ + diff --git a/Robust.Client/UserInterface/Controls/BaseButton.cs b/Robust.Client/UserInterface/Controls/BaseButton.cs index e8a6856166f..d590b9619c7 100644 --- a/Robust.Client/UserInterface/Controls/BaseButton.cs +++ b/Robust.Client/UserInterface/Controls/BaseButton.cs @@ -90,7 +90,6 @@ public bool Disabled if (old != value) { - DefaultCursorShape = Disabled ? CursorShape.NotAllowed : CursorShape.Pointer; DrawModeChanged(); } } @@ -235,7 +234,6 @@ public bool MuteSounds protected BaseButton() { MouseFilter = MouseFilterMode.Stop; - DefaultCursorShape = Disabled ? CursorShape.NotAllowed : CursorShape.Pointer; } protected virtual void DrawModeChanged() diff --git a/Robust.Client/UserInterface/Controls/HBox.cs b/Robust.Client/UserInterface/Controls/HBox.cs new file mode 100644 index 00000000000..c9ce4110c07 --- /dev/null +++ b/Robust.Client/UserInterface/Controls/HBox.cs @@ -0,0 +1,6 @@ +namespace Robust.Client.UserInterface.Controls; + +/// +/// Convenience type to describe a horizontal . +/// +public sealed class HBox : BoxContainer; diff --git a/Robust.Client/UserInterface/Controls/LayoutContainer.cs b/Robust.Client/UserInterface/Controls/LayoutContainer.cs index f161c3b6cce..01f35d48c1f 100644 --- a/Robust.Client/UserInterface/Controls/LayoutContainer.cs +++ b/Robust.Client/UserInterface/Controls/LayoutContainer.cs @@ -462,20 +462,19 @@ public static void SetMarginsPreset(Control control, LayoutPreset preset, protected override Vector2 MeasureOverride(Vector2 availableSize) { var min = Vector2.Zero; - var uiScale = UIScale; foreach (var child in Children) { var growH = child.GetValue(GrowHorizontalProperty); var growV = child.GetValue(GrowVerticalProperty); - var anchorMargins = CalcAnchorMargins(availableSize, uiScale, child); + var anchorMargins = CalcAnchorMargins(availableSize, child); var size = availableSize; if (growH == GrowDirection.Constrain) - size.X = anchorMargins.Width / uiScale; + size.X = anchorMargins.Width; if (growV == GrowDirection.Constrain) - size.Y = anchorMargins.Height / uiScale; + size.Y = anchorMargins.Height; child.Measure(size); min = Vector2.Max(min, child.DesiredSize); @@ -488,7 +487,7 @@ protected override Vector2 ArrangeOverride(Vector2 finalSize) { foreach (var child in Children) { - child.Arrange(CalcChildRect(finalSize, UIScale, child, out _)); + child.Arrange(CalcChildRect(finalSize, child, out _)); } return finalSize; @@ -509,7 +508,7 @@ protected internal override void Draw(DrawingHandleScreen handle) continue; } - var rect = CalcChildRect(Size, UIScale, child, out var anchorSize); + var rect = CalcChildRect(Size, child, out var anchorSize); var left = rect.Left * UIScale; var right = rect.Right * UIScale; @@ -542,24 +541,22 @@ void DrawHLine(float y, Color color) } } - private static UIBox2 CalcAnchorMargins(Vector2 ourSize, float uiScale, Control child) + private static UIBox2 CalcAnchorMargins(Vector2 ourSize, Control child) { - var (pSizeX, pSizeY) = ourSize * uiScale; - var anchorLeft = child.GetValue(AnchorLeftProperty); var anchorTop = child.GetValue(AnchorTopProperty); var anchorRight = child.GetValue(AnchorRightProperty); var anchorBottom = child.GetValue(AnchorBottomProperty); - var marginLeft = child.GetValue(MarginLeftProperty) * uiScale; - var marginTop = child.GetValue(MarginTopProperty) * uiScale; - var marginRight = child.GetValue(MarginRightProperty) * uiScale; - var marginBottom = child.GetValue(MarginBottomProperty) * uiScale; + var marginLeft = child.GetValue(MarginLeftProperty); + var marginTop = child.GetValue(MarginTopProperty); + var marginRight = child.GetValue(MarginRightProperty); + var marginBottom = child.GetValue(MarginBottomProperty); - var left = anchorLeft * pSizeX + marginLeft; - var top = anchorTop * pSizeY + marginTop; - var right = anchorRight * pSizeX + marginRight; - var bottom = anchorBottom * pSizeY + marginBottom; + var left = anchorLeft * ourSize.X + marginLeft; + var top = anchorTop * ourSize.Y + marginTop; + var right = anchorRight * ourSize.X + marginRight; + var bottom = anchorBottom * ourSize.Y + marginBottom; // Yes, this can return boxes with left > right (and top > bottom). // This is "intentional", see comment in CalcChildRect. @@ -567,23 +564,23 @@ private static UIBox2 CalcAnchorMargins(Vector2 ourSize, float uiScale, Control return new UIBox2(left, top, right, bottom); } - private static UIBox2 CalcChildRect(Vector2 ourSize, float uiScale, Control child, out UIBox2 anchorSize) + private static UIBox2 CalcChildRect(Vector2 ourSize, Control child, out UIBox2 anchorSize) { // Calculate where the control "wants" to be by its anchors/margins. var growHorizontal = child.GetValue(GrowHorizontalProperty); var growVertical = child.GetValue(GrowVerticalProperty); - anchorSize = CalcAnchorMargins(ourSize, uiScale, child); + anchorSize = CalcAnchorMargins(ourSize, child); // This intentionally results in negatives if the right bound is < the left bound. // Which then causes HandleLayoutOverflow to CORRECTLY work from the right bound instead. var (wSizeX, wSizeY) = (anchorSize.Right - anchorSize.Left, anchorSize.Bottom - anchorSize.Top); - var (minSizeX, minSizeY) = child.DesiredPixelSize; + var (minSizeX, minSizeY) = child.DesiredSize; HandleLayoutOverflow(growHorizontal, minSizeX, anchorSize.Left, wSizeX, out var posX, out var sizeX); HandleLayoutOverflow(growVertical, minSizeY, anchorSize.Top, wSizeY, out var posY, out var sizeY); - return UIBox2.FromDimensions(posX / uiScale, posY / uiScale, sizeX / uiScale, sizeY / uiScale); + return UIBox2.FromDimensions(posX, posY, sizeX, sizeY); } private static void HandleLayoutOverflow(GrowDirection direction, float minSize, float wPos, float wSize, diff --git a/Robust.Client/UserInterface/Controls/Popup.cs b/Robust.Client/UserInterface/Controls/Popup.cs index 2b4f363ea7e..a2223b3d40d 100644 --- a/Robust.Client/UserInterface/Controls/Popup.cs +++ b/Robust.Client/UserInterface/Controls/Popup.cs @@ -30,6 +30,29 @@ public Popup() public bool CloseOnEscape { get; set; } = true; + private bool _autoOrphan; + + /// + /// Opens the popup at the location of the mouse. + /// + /// + /// + /// The popup is placed in the modal root, and is automatically sized. + /// + /// + /// The popup *automatically* gets removed from the popup root when it is hidden again. + /// Do not remove it manually! + /// + /// + public void OpenAtMouse() + { + _autoOrphan = true; + var root = UserInterfaceManager.GetRootForMouse(); + root.ModalRoot.AddChild(this); + + Open(UIBox2.FromDimensions(UserInterfaceManager.MousePositionScaled.Position, Vector2.One)); + } + public virtual void Open(UIBox2? box = null, Vector2? altPos = null, Vector2? altPosUp = null) { if (Visible) @@ -72,6 +95,12 @@ protected internal override void ModalRemoved() Visible = false; OnPopupHide?.Invoke(); + + if (_autoOrphan) + { + Orphan(); + _autoOrphan = false; + } } protected override Vector2 MeasureOverride(Vector2 availableSize) diff --git a/Robust.Client/UserInterface/Controls/ScrollBar.cs b/Robust.Client/UserInterface/Controls/ScrollBar.cs index 0aa25ed45a6..7d5d1b2efd0 100644 --- a/Robust.Client/UserInterface/Controls/ScrollBar.cs +++ b/Robust.Client/UserInterface/Controls/ScrollBar.cs @@ -46,7 +46,6 @@ protected ScrollBar(OrientationMode orientation) ReservesSpace = true; _orientation = orientation; - DefaultCursorShape = CursorShape.Pointer; } public bool IsAtEnd @@ -132,19 +131,11 @@ protected internal override void KeyBindUp(GUIBoundKeyEventArgs args) protected internal override void MouseMove(GUIMouseMoveEventArgs args) { - DefaultCursorShape = CursorShape.Arrow; - - if (_isHovered || _grabData != null) - { - DefaultCursorShape = CursorShape.Pointer; - } - if (_grabData == null) { var box = _getGrabberBox(); _isHovered = box.Contains(args.RelativePixelPosition); _updatePseudoClass(); - return; } diff --git a/Robust.Client/UserInterface/Controls/TableContainer.cs b/Robust.Client/UserInterface/Controls/TableContainer.cs index 4d0625f7653..6474c82f5b4 100644 --- a/Robust.Client/UserInterface/Controls/TableContainer.cs +++ b/Robust.Client/UserInterface/Controls/TableContainer.cs @@ -4,7 +4,8 @@ namespace Robust.Client.UserInterface.Controls; -internal sealed class TableContainer : Container +[Virtual] +public class TableContainer : Container { private int _columns = 1; diff --git a/Robust.Client/UserInterface/Controls/VBox.cs b/Robust.Client/UserInterface/Controls/VBox.cs new file mode 100644 index 00000000000..1476f749b65 --- /dev/null +++ b/Robust.Client/UserInterface/Controls/VBox.cs @@ -0,0 +1,12 @@ +namespace Robust.Client.UserInterface.Controls; + +/// +/// Convenience type to describe a vertical . +/// +public sealed class VBox : BoxContainer +{ + public VBox() + { + Orientation = LayoutOrientation.Vertical; + } +} diff --git a/Robust.Client/UserInterface/CustomControls/BaseWindow.cs b/Robust.Client/UserInterface/CustomControls/BaseWindow.cs index 9154374bb50..6d9d24cee87 100644 --- a/Robust.Client/UserInterface/CustomControls/BaseWindow.cs +++ b/Robust.Client/UserInterface/CustomControls/BaseWindow.cs @@ -19,6 +19,8 @@ public abstract class BaseWindow : Control private DragMode CurrentDrag = DragMode.None; private Vector2 DragOffsetTopLeft; private Vector2 DragOffsetBottomRight; + private Control? _trackedParent; + private Vector2? _lastParentSize; public bool Resizable { get; set; } = true; public bool IsOpen => Parent != null; @@ -41,6 +43,18 @@ public virtual void Close() OnClose?.Invoke(); } + protected override void EnteredTree() + { + base.EnteredTree(); + TrackParentResize(); + } + + protected override void ExitedTree() + { + UntrackParentResize(); + base.ExitedTree(); + } + protected internal override void KeyBindDown(GUIBoundKeyEventArgs args) { base.KeyBindDown(args); @@ -291,6 +305,55 @@ protected virtual DragMode GetDragModeFor(Vector2 relativeMousePos) return DragMode.None; } + private void TrackParentResize() + { + if (Parent == null) + return; + + _trackedParent = Parent; + _trackedParent.OnResized += ParentResized; + _lastParentSize = _trackedParent.Size; + } + + private void UntrackParentResize() + { + if (_trackedParent != null) + _trackedParent.OnResized -= ParentResized; + + _trackedParent = null; + _lastParentSize = null; + } + + /// + /// Re-locates this window if the parent re-sizes. Useful if display size changes. + /// + private void ParentResized() + { + // Sanity check + if (_trackedParent == null || Parent != _trackedParent) + return; + + var newParentSize = _trackedParent.Size; + + if (_lastParentSize is not { } oldParentSize || + oldParentSize.X <= 0 || oldParentSize.Y <= 0 || + newParentSize.X <= 0 || newParentSize.Y <= 0) + { + _lastParentSize = newParentSize; + return; + } + + if (oldParentSize.EqualsApprox(newParentSize)) + return; + + var relativeCenter = (Position + Size / 2) / oldParentSize; + var newPosition = relativeCenter * newParentSize - Size / 2; + var maxPosition = Vector2.Max(newParentSize - Size, Vector2.Zero); + + _lastParentSize = newParentSize; + LayoutContainer.SetPosition(this, Vector2.Clamp(newPosition, Vector2.Zero, maxPosition)); + } + [Flags] protected enum DragMode : byte { diff --git a/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.Completions.cs b/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.Completions.cs index 45f58301e77..25f15c6909e 100644 --- a/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.Completions.cs +++ b/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.Completions.cs @@ -262,7 +262,8 @@ private void UpdateCompletionsPopup() private CompletionOption[] FilterCompletions(IEnumerable completions, string curTyping) { return completions - .Where(c => c.Value.StartsWith(curTyping, StringComparison.CurrentCultureIgnoreCase)) + .Where(c => c.Value.Contains(curTyping, StringComparison.CurrentCultureIgnoreCase)) + .OrderByDescending(c => c.Value.StartsWith(curTyping, StringComparison.CurrentCultureIgnoreCase)) .ToArray(); } diff --git a/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugCoordsPanel.cs b/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugCoordsPanel.cs index 86ff546fb2f..f4d643d564d 100644 --- a/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugCoordsPanel.cs +++ b/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugCoordsPanel.cs @@ -19,7 +19,6 @@ internal sealed partial class DebugCoordsPanel : PanelContainer [Dependency] private IInputManager _inputManager = default!; [Dependency] private IEntityManager _entityManager = default!; [Dependency] private IClyde _displayManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IBaseClient _baseClient = default!; private readonly StringBuilder _textBuilder = new(); @@ -76,7 +75,7 @@ protected override void FrameUpdate(FrameEventArgs args) var mapSystem = _entityManager.System(); var xformSystem = _entityManager.System(); - if (_mapManager.TryFindGridAt(mouseWorldMap, out var mouseGridUid, out var mouseGrid)) + if (mapSystem.TryFindGridAt(mouseWorldMap, out var mouseGridUid, out var mouseGrid)) { mouseGridPos = mapSystem.MapToGrid(mouseGridUid, mouseWorldMap); tile = mapSystem.GetTileRef(mouseGridUid, mouseGrid, mouseGridPos); @@ -86,7 +85,7 @@ protected override void FrameUpdate(FrameEventArgs args) mouseGridPos = new EntityCoordinates(mapSystem.GetMapOrInvalid(mouseWorldMap.MapId), mouseWorldMap.Position); tile = new TileRef(EntityUid.Invalid, - mouseGridPos.ToVector2i(_entityManager, _mapManager, xformSystem), Tile.Empty); + mouseGridPos.ToVector2i(_entityManager, xformSystem), Tile.Empty); } } } diff --git a/Robust.Client/UserInterface/FileDialogManager.cs b/Robust.Client/UserInterface/FileDialogManager.cs index 7921c38f8cd..27342c8755b 100644 --- a/Robust.Client/UserInterface/FileDialogManager.cs +++ b/Robust.Client/UserInterface/FileDialogManager.cs @@ -9,28 +9,60 @@ namespace Robust.Client.UserInterface; internal sealed class FileDialogManager(IClydeInternal clyde) : IFileDialogManager { - public async Task OpenFile( - FileDialogFilters? filters = null, - FileAccess access = FileAccess.ReadWrite, - FileShare? share = null) + private static FileShare Validate(FileAccess access, FileShare? share) { if ((access & FileAccess.ReadWrite) != access) throw new ArgumentException("Invalid file access specified"); 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 - return null; + return realShare; + } + + private async Task Prompt(bool isSave, FileDialogFilters? filters) + { + if (clyde.FileDialogImpl is { } impl) + return isSave ? await impl.SaveFile(filters) : await impl.OpenFile(filters); + + return null; + } - if (name == null) - return null; + public async Task<(Stream?, string?)> GetFileAndName( + FileDialogFilters? filters = null, + FileAccess access = FileAccess.ReadWrite, + FileShare? share = null) + { + var name = await Prompt(false, filters); - return File.Open(name, FileMode.Open, access, realShare); + if (name == null) return (null, null); + + var path = Path.GetFileName(name); + return (File.Open(name, FileMode.Open, access, Validate(access, share)), path); + } + + public async Task GetName( + FileDialogFilters? filters = null, + FileAccess access = FileAccess.ReadWrite, + FileShare? share = null) + { + Validate(access, share); + var name = await Prompt(false, filters); + + return name == null ? null : Path.GetFileName(name); + } + + public async Task OpenFile( + FileDialogFilters? filters = null, + FileAccess access = FileAccess.ReadWrite, + FileShare? share = null) + { + var realShare = Validate(access, share); + var name = await Prompt(false, filters); + + return name == null ? null : File.Open(name, FileMode.Open, access, realShare); } public async Task<(Stream, bool)?> SaveFile( @@ -39,20 +71,10 @@ internal sealed class FileDialogManager(IClydeInternal clyde) : IFileDialogManag 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 - return null; + Validate(access, share); + var name = await Prompt(true, filters); - if (name == null) - return null; + if (name == null) return null; try { diff --git a/Robust.Client/UserInterface/IFileDialogManager.cs b/Robust.Client/UserInterface/IFileDialogManager.cs index ab0ef06eada..9dcdc9a85ea 100644 --- a/Robust.Client/UserInterface/IFileDialogManager.cs +++ b/Robust.Client/UserInterface/IFileDialogManager.cs @@ -2,60 +2,84 @@ using System.Threading.Tasks; using Robust.Client.Graphics; -namespace Robust.Client.UserInterface +namespace Robust.Client.UserInterface; + +/// +/// Manager for opening of file dialogs. +/// +/// +/// 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 { /// - /// Manager for opening of file dialogs. + /// Open a file dialog used for opening a single file and getting its filename. /// - /// - /// 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 - { - /// - /// Open a file dialog used for opening a single file. - /// - /// - /// The file stream for the file the user opened. - /// if the user cancelled the action. - /// - /// Filters for file types that the user can select. - /// What access is desired from the file operation. - /// - /// What sharing mode is desired from the file operation. - /// If null is provided and is , - /// is selected, otherwise . - /// - Task OpenFile( - FileDialogFilters? filters = null, - FileAccess access = FileAccess.ReadWrite, - FileShare? share = null); + /// + /// The file stream and name for the file the user opened. + /// if the user canceled the action. + /// + /// Filters for file types that the user can select. + /// What access is desired from the file operation. + /// + /// What sharing mode is desired from the file operation. + /// If null is provided and is , + /// is selected, otherwise . + /// + Task<(Stream?, string?)> GetFileAndName( + FileDialogFilters? filters = null, + FileAccess access = FileAccess.ReadWrite, + FileShare? share = null); - /// - /// Open a file dialog used for saving a single file. - /// - /// - /// The file stream the user chose to save to, and whether the file already existed. - /// Null if the user cancelled the action. - /// - /// Should we truncate an existing file to 0-size then write or append. - /// What access is desired from the file operation. - /// Sharing mode for the opened file. - Task<(Stream fileStream, bool alreadyExisted)?> SaveFile( - FileDialogFilters? filters = null, - bool truncate = true, - FileAccess access = FileAccess.ReadWrite, - FileShare share = FileShare.None); - } + /// + /// Open a file dialog used for getting the file name of the selected file. + /// + /// + /// The file name for the file the user opened. + /// if the user canceled the action. + /// + /// + Task GetName( + FileDialogFilters? filters = null, + FileAccess access = FileAccess.ReadWrite, + FileShare? share = null); /// - /// Internal implementation interface used to connect and . + /// Open a file dialog used for opening the selected file. /// - internal interface IFileDialogManagerImplementation - { - Task OpenFile(FileDialogFilters? filters); - Task SaveFile(FileDialogFilters? filters); - } + /// + /// The file stream for the file the user opened. + /// if the user cancelled the action. + /// + /// + Task OpenFile( + FileDialogFilters? filters = null, + FileAccess access = FileAccess.ReadWrite, + FileShare? share = null); + + /// + /// Open a file dialog used for saving a single file. + /// + /// + /// The file stream the user chose to save to, and whether the file already existed. + /// Null if the user canceled the action. + /// + /// Should we truncate an existing file to 0-size then write or append. + /// + Task<(Stream fileStream, bool alreadyExisted)?> SaveFile( + FileDialogFilters? filters = null, + bool truncate = true, + FileAccess access = FileAccess.ReadWrite, + FileShare share = FileShare.None); +} + +/// +/// Internal implementation interface used to connect and . +/// +internal interface IFileDialogManagerImplementation +{ + Task OpenFile(FileDialogFilters? filters); + Task SaveFile(FileDialogFilters? filters); } diff --git a/Robust.Client/UserInterface/IUserInterfaceManager.cs b/Robust.Client/UserInterface/IUserInterfaceManager.cs index 0ad4eb29997..271b2df7485 100644 --- a/Robust.Client/UserInterface/IUserInterfaceManager.cs +++ b/Robust.Client/UserInterface/IUserInterfaceManager.cs @@ -167,6 +167,14 @@ public partial interface IUserInterfaceManager /// Exists so that control don't have to inject dependencies or otherwise obtain an instance just to log errors. /// ISawmill ControlSawmill { get; } + + /// + /// Get the UI root responsible for the current mouse position. + /// + /// + /// This is useful to open popups or similar on the current active window. + /// + UIRoot GetRootForMouse(); } public readonly struct PostDrawUIRootEventArgs diff --git a/Robust.Client/UserInterface/RichText/CommandLinkTag.cs b/Robust.Client/UserInterface/RichText/CommandLinkTag.cs index 743ab15f889..78c213f3fad 100644 --- a/Robust.Client/UserInterface/RichText/CommandLinkTag.cs +++ b/Robust.Client/UserInterface/RichText/CommandLinkTag.cs @@ -36,6 +36,9 @@ public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? c label.OnMouseExited += _ => label.FontColorOverride = Color.LightBlue; label.OnKeyBindDown += args => OnKeybindDown(args, command); + if (node.Attributes.TryGetValue("title", out var titleArg)) + label.ToolTip = titleArg.StringValue; + control = label; return true; } diff --git a/Robust.Client/UserInterface/UserInterfaceManager.cs b/Robust.Client/UserInterface/UserInterfaceManager.cs index 942e2f5ffef..57c86c35185 100644 --- a/Robust.Client/UserInterface/UserInterfaceManager.cs +++ b/Robust.Client/UserInterface/UserInterfaceManager.cs @@ -492,5 +492,20 @@ public void HoverSound() { ClearWindows(); } + + public UIRoot GetRootForMouse() + { + var pos = _inputManager.MouseScreenPosition; + + foreach (var root in _roots) + { + if (root.Window.Id == pos.Window) + { + return root; + } + } + + return RootControl; + } } } diff --git a/Robust.Client/Utility/UserDataDir.cs b/Robust.Client/Utility/UserDataDir.cs index 9ebcaad46c7..893a8435014 100644 --- a/Robust.Client/Utility/UserDataDir.cs +++ b/Robust.Client/Utility/UserDataDir.cs @@ -1,6 +1,8 @@ using System; using System.IO; using JetBrains.Annotations; +using Robust.Shared.ContentPack; +using Robust.Shared.Utility; namespace Robust.Client.Utility { @@ -35,7 +37,8 @@ public static string GetRootUserDataDir(IGameControllerInternal gameController) appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); #endif - return Path.Combine(appDataDir, gameController.Options.UserDataDirectoryName); + + return PathHelpers.SafeGetResourcePath(appDataDir, new ResPath(gameController.Options.UserDataDirectoryName)); } } } diff --git a/Robust.Client/ViewVariables/Editors/VVPropEditorEntityCoordinates.cs b/Robust.Client/ViewVariables/Editors/VVPropEditorEntityCoordinates.cs index 91620f5b544..6898fdd5974 100644 --- a/Robust.Client/ViewVariables/Editors/VVPropEditorEntityCoordinates.cs +++ b/Robust.Client/ViewVariables/Editors/VVPropEditorEntityCoordinates.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Numerics; +using Robust.Client.GameObjects; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.GameObjects; @@ -24,6 +25,7 @@ protected override Control MakeUI(object? value) hBoxContainer.AddChild(new Label {Text = "grid: "}); var entityManager = IoCManager.Resolve(); + var xformSystem = entityManager.System(); var gridId = new LineEdit { @@ -31,7 +33,7 @@ protected override Control MakeUI(object? value) HorizontalExpand = true, PlaceHolder = "Grid ID", ToolTip = "Grid ID", - Text = coords.GetGridUid(entityManager)?.ToString() ?? "" + Text = xformSystem.GetGrid(coords)?.ToString() ?? "" }; hBoxContainer.AddChild(gridId); @@ -63,7 +65,6 @@ protected override Control MakeUI(object? value) void OnEntered(LineEdit.LineEditEventArgs e) { var gridVal = EntityUid.Parse(gridId.Text); - var mapManager = IoCManager.Resolve(); var xVal = float.Parse(x.Text, CultureInfo.InvariantCulture); var yVal = float.Parse(y.Text, CultureInfo.InvariantCulture); diff --git a/Robust.Roslyn.Shared/AttributeHelper.cs b/Robust.Roslyn.Shared/AttributeHelper.cs index ac8101aac69..d7983629684 100644 --- a/Robust.Roslyn.Shared/AttributeHelper.cs +++ b/Robust.Roslyn.Shared/AttributeHelper.cs @@ -25,6 +25,11 @@ public static bool HasAttribute(ISymbol symbol, string attributeMetadataName, [N return false; } + public static bool HasAttribute(ISymbol symbol, string attributeMetadataName) + { + return HasAttribute(symbol, attributeMetadataName, out _); + } + public static bool GetNamedArgumentBool(AttributeData data, string name, bool defaultValue) { foreach (var kv in data.NamedArguments) diff --git a/Robust.Roslyn.Shared/DataDefinitionHelper.cs b/Robust.Roslyn.Shared/DataDefinitionHelper.cs new file mode 100644 index 00000000000..1401a3278bb --- /dev/null +++ b/Robust.Roslyn.Shared/DataDefinitionHelper.cs @@ -0,0 +1,349 @@ +#nullable enable +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Robust.Roslyn.Shared.Helpers; + +namespace Robust.Roslyn.Shared; + +public sealed class DataDefinitionHelper +{ + private const string DataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataDefinitionAttribute"; + private const string DataRecordNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataRecordAttribute"; + private const string ImplicitDataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.ImplicitDataDefinitionForInheritorsAttribute"; + private const string ImplicitDataRecordNamespace = "Robust.Shared.Serialization.Manager.Attributes.ImplicitDataRecordAttribute"; + private const string MeansDataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.MeansDataDefinitionAttribute"; + private const string MeansDataRecordNamespace = "Robust.Shared.Serialization.Manager.Attributes.MeansDataRecordAttribute"; + private const string DataFieldBaseNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataFieldBaseAttribute"; + private const string DataFieldAttributeName = "Robust.Shared.Serialization.Manager.Attributes.DataFieldAttribute"; + private const string IdDataFieldAttributeName = "Robust.Shared.Prototypes.IdDataFieldAttribute"; + private const string ParentDataFieldAttributeName = "Robust.Shared.Prototypes.ParentDataFieldAttribute"; + private const string AbstractDataFieldAttributeName = "Robust.Shared.Prototypes.AbstractDataFieldAttribute"; + private const string IncludeDataFieldAttributeName = "Robust.Shared.Serialization.Manager.Attributes.IncludeDataFieldAttribute"; + private const string AlwaysPushInheritanceAttributeName = "Robust.Shared.Serialization.Manager.Attributes.AlwaysPushInheritanceAttribute"; + private const string NeverPushInheritanceAttributeName = "Robust.Shared.Serialization.Manager.Attributes.NeverPushInheritanceAttribute"; + + public static (bool Definition, bool Record) IsImplicitDataDefinitionInterface(ITypeSymbol @interface) + { + if (AttributeHelper.HasAttribute(@interface, ImplicitDataRecordNamespace)) + return (true, true); + + var isDefinition = false; + foreach (var subInterface in @interface.AllInterfaces) + { + if (AttributeHelper.HasAttribute(subInterface, ImplicitDataRecordNamespace)) + return (true, true); + + if (AttributeHelper.HasAttribute(subInterface, ImplicitDataDefinitionNamespace)) + isDefinition = true; + } + + return (isDefinition || AttributeHelper.HasAttribute(@interface, ImplicitDataDefinitionNamespace), false); + } + + public static (bool Definition, bool Record) IsImplicitDataDefinition(ITypeSymbol type) + { + var isDefinition = false; + foreach (var attribute in type.GetAttributes()) + { + if (attribute.AttributeClass is not { } attributeClass) + continue; + + var str = attributeClass.ToDisplayString(); + switch (str) + { + case ImplicitDataRecordNamespace: + return (true, true); + case ImplicitDataDefinitionNamespace: + isDefinition = true; + break; + } + + foreach (var subAttribute in attributeClass.GetAttributes()) + { + if (subAttribute.AttributeClass is not { } subAttributeClass) + continue; + + var subStr = subAttributeClass.ToDisplayString(); + if (subStr is MeansDataRecordNamespace) + return (true, true); + + if (subStr is MeansDataDefinitionNamespace) + isDefinition = true; + } + } + + foreach (var baseType in TypeSymbolHelper.GetBaseTypes(type)) + { + if (AttributeHelper.HasAttribute(baseType, ImplicitDataRecordNamespace)) + return (true, true); + + if (AttributeHelper.HasAttribute(baseType, ImplicitDataDefinitionNamespace)) + isDefinition = true; + } + + foreach (var @interface in type.AllInterfaces) + { + var impl = IsImplicitDataDefinitionInterface(@interface); + if (impl.Record) + return (true, true); + + if (impl.Definition) + isDefinition = true; + } + + return (isDefinition, false); + } + + public static bool IsDataDefinition([NotNullWhen(true)] ITypeSymbol? type, out bool isDataRecord) + { + isDataRecord = false; + if (type == null) + return false; + + isDataRecord = AttributeHelper.HasAttribute(type, DataRecordNamespace); + if (isDataRecord) + return true; + + var (isImplicitDefinition, isImplicitRecord) = IsImplicitDataDefinition(type); + if (isImplicitRecord) + isDataRecord = true; + + return isImplicitDefinition || + isImplicitRecord || + AttributeHelper.HasAttribute(type, DataDefinitionNamespace); + } + + + public static DataFieldAttribute? GetDataFieldAttribute(AttributeData data, string fieldName) + { + if (data.AttributeClass == null) + return null; + + string? name = null; + var readOnly = false; + var priority = 1; + var include = false; + var isDataFieldAttribute = false; + var required = false; + var serverOnly = false; + + // (string? tag = null, bool readOnly = false, int priority = 1, bool required = false, bool serverOnly = false, Type? customTypeSerializer = null) + if (data.AttributeClass.ToDisplayString().Contains(DataFieldAttributeName)) + { + name = GetAttributeArgument(data, 0, "tag", name); + readOnly = GetAttributeArgument(data, 1, "readOnly", readOnly); + priority = GetAttributeArgument(data, 2, "priority", priority); + isDataFieldAttribute = true; + required = GetAttributeArgument(data, 3, "required", required); + serverOnly = GetAttributeArgument(data, 4, "serverOnly", serverOnly); + } + // (int priority = 1, Type? customTypeSerializer = null) + else if (data.AttributeClass.ToDisplayString().Contains(IdDataFieldAttributeName)) + { + name = "id"; + priority = GetAttributeArgument(data, 0, "priority", priority); + isDataFieldAttribute = true; + } + // (Type prototypeIdSerializer, int priority = 1) + else if (data.AttributeClass.ToDisplayString().Contains(ParentDataFieldAttributeName)) + { + name = "parent"; + priority = GetAttributeArgument(data, 1, "priority", priority); + isDataFieldAttribute = true; + } + // (int priority = 1) + else if (data.AttributeClass.ToDisplayString().Contains(AbstractDataFieldAttributeName)) + { + name = "abstract"; + priority = GetAttributeArgument(data, 0, "priority", priority); + isDataFieldAttribute = true; + } + // (bool readOnly = false, int priority = 1, bool serverOnly = false, Type? customTypeSerializer = null) + else if (data.AttributeClass.ToDisplayString().Contains(IncludeDataFieldAttributeName)) + { + var span = fieldName.AsSpan(); + name = $"{char.ToLowerInvariant(span[0])}{span.Slice(1).ToString()}"; + readOnly = GetAttributeArgument(data, 0, "readOnly", readOnly); + priority = GetAttributeArgument(data, 1, "priority", priority); + include = true; + serverOnly = GetAttributeArgument(data, 2, "serverOnly", serverOnly); + } + + var camelCasedName = ToCamelCase(fieldName); + if (string.IsNullOrWhiteSpace(name)) + name = ToCamelCase(camelCasedName); + + return new DataFieldAttribute( + data, + name!, + readOnly, + priority, + include, + isDataFieldAttribute, + required, + serverOnly, + camelCasedName + ); + } + + private static T GetAttributeArgument(AttributeData data, int constructorIndex, string namedArgument, T defaultValue) + { + if (constructorIndex < data.ConstructorArguments.Length) + { + var argument = data.ConstructorArguments[constructorIndex]; + if (!argument.IsNull && argument.Value is T value) + return value; + } + + foreach (var named in data.NamedArguments) + { + if (named.Key == namedArgument && !named.Value.IsNull && named.Value.Value is T value) + return value; + } + + return defaultValue; + } + + public static bool IsDataField( + ImmutableArray attributes, + string name, + bool isImplicitlyDeclared, + bool isStatic, + bool hasSetter, + bool isRecord, + out DataFieldAttribute? attribute, + out int inheritanceBehavior) + { + attribute = null; + inheritanceBehavior = 0; + if (isStatic) + return false; + + foreach (var attr in attributes) + { + if (attr.AttributeClass is not { } attributeClass) + continue; + + if (TypeSymbolHelper.Inherits(attributeClass, DataFieldBaseNamespace)) + attribute = GetDataFieldAttribute(attr, name); + + if (attributeClass.ToDisplayString() == AlwaysPushInheritanceAttributeName) + inheritanceBehavior = 1; + else if (attributeClass.ToDisplayString() == NeverPushInheritanceAttributeName) + inheritanceBehavior = 2; + } + + if (attribute != null) + attribute = attribute with { InheritanceBehavior = inheritanceBehavior }; + + if (isImplicitlyDeclared || !hasSetter || !isRecord) + return attribute != null; + + name = ToCamelCase(name); + attribute = new DataFieldAttribute( + null, + name, + false, + 1, + false, + true, + false, + false, + name, + inheritanceBehavior + ); + + return true; + } + + public static bool IsDataField( + ISymbol member, + bool isDataRecord, + out ITypeSymbol type, + [NotNullWhen(true)] out DataFieldAttribute? attribute) + { + // TODO data records and other attributes + type = null!; + attribute = null; + var inheritanceBehavior = 0; + switch (member) + { + case IFieldSymbol field: + { + var associatedProperty = field.AssociatedSymbol as IPropertySymbol; + var isDataRecordBackingField = isDataRecord && + field.IsImplicitlyDeclared && + associatedProperty != null; + var fieldName = isDataRecordBackingField + ? associatedProperty!.Name + : field.Name; + + if (IsDataField(field.GetAttributes(), + fieldName, + isDataRecordBackingField ? false : field.IsImplicitlyDeclared, + field.IsStatic, + true, + isDataRecord, + out attribute, + out inheritanceBehavior)) + type = field.Type; + + break; + } + case IPropertySymbol property: + { + var hasSetter = property.SetMethod != null; + if (IsDataField(property.GetAttributes(), + property.Name, + property.IsImplicitlyDeclared, + property.IsStatic, + hasSetter, + isDataRecord, + out attribute, + out inheritanceBehavior)) + type = property.Type; + + break; + } + } + + if (attribute != null) + attribute = attribute with { InheritanceBehavior = inheritanceBehavior }; + + return attribute != null; + } + + private static bool IsAutoProperty(IPropertySymbol property) + { + foreach (var reference in property.DeclaringSyntaxReferences) + { + if (reference.GetSyntax() is not PropertyDeclarationSyntax syntax) + continue; + + if (syntax.ExpressionBody != null) + return false; + + if (syntax.AccessorList == null) + return false; + + return syntax.AccessorList.Accessors.All(accessor => + accessor.Body == null && + accessor.ExpressionBody == null && + accessor.SemicolonToken.RawKind != 0); + } + + return false; + } + + public static string ToCamelCase(string name) + { + if (name == "ID") + return "id"; + + var span = name.AsSpan(); + return $"{char.ToLowerInvariant(span[0])}{span.Slice(1).ToString()}"; + } +} diff --git a/Robust.Roslyn.Shared/Diagnostics.cs b/Robust.Roslyn.Shared/Diagnostics.cs index 3a58ebc3c06..03b44dab2a5 100644 --- a/Robust.Roslyn.Shared/Diagnostics.cs +++ b/Robust.Roslyn.Shared/Diagnostics.cs @@ -22,7 +22,7 @@ public static class Diagnostics public const string IdValueEventRaisedByRef = "RA0016"; public const string IdDataDefinitionPartial = "RA0017"; public const string IdNestedDataDefinitionPartial = "RA0018"; - public const string IdDataFieldWritable = "RA0019"; + public const string IdDataFieldWritable = "RA0019"; // No longer used. public const string IdDataFieldPropertyWritable = "RA0020"; public const string IdComponentPauseNotComponent = "RA0021"; public const string IdComponentPauseNoFields = "RA0022"; @@ -57,6 +57,10 @@ public static class Diagnostics public const string IdHasDependenciesReadOnly = "RA0051"; public const string IdHasDependenciesPropertyField = "RA0052"; public const string IdExclusiveVirtual = "RA0053"; + public const string IdInvalidAMethodSignatureForGeneratedSubscription = "RA0054"; + public const string IdInvalidContainingTypeForGeneratedSubscription = "RA0055"; + public const string IdNonPartialContainingTypeForGeneratedSubscription = "RA0056"; + public const string IdDataFieldOutsideDefinition = "RA0057"; public static SuppressionDescriptor MeansImplicitAssignment => new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned."); diff --git a/Robust.Roslyn.Shared/Helpers/DataFieldAttribute.cs b/Robust.Roslyn.Shared/Helpers/DataFieldAttribute.cs new file mode 100644 index 00000000000..771223ccc38 --- /dev/null +++ b/Robust.Roslyn.Shared/Helpers/DataFieldAttribute.cs @@ -0,0 +1,18 @@ +using Microsoft.CodeAnalysis; + +#nullable enable + +namespace Robust.Roslyn.Shared.Helpers; + +public sealed record DataFieldAttribute( + AttributeData? Data, + string Tag, + bool ReadOnly, + int Priority, + bool Include, + bool IsDataFieldAttribute, + bool Required, + bool ServerOnly, + string CamelCasedName, + int InheritanceBehavior = 0 +); diff --git a/Robust.Roslyn.Shared/PartialTypeHelper.cs b/Robust.Roslyn.Shared/PartialTypeHelper.cs index 119c4619fb8..d47e3bd34ee 100644 --- a/Robust.Roslyn.Shared/PartialTypeHelper.cs +++ b/Robust.Roslyn.Shared/PartialTypeHelper.cs @@ -77,6 +77,11 @@ public bool CheckPartialDiagnostic(SourceProductionContext context, DiagnosticDe return false; } + public string GetQualifiedName() + { + return Namespace == null ? Name : $"{Namespace}.{Name}"; + } + public string GetGeneratedFileName() { var name = Namespace == null ? "" : $"{Namespace}."; @@ -253,4 +258,33 @@ public int GetHashCode(PartialTypeInfo obj) } } } + + /// + /// An for s which considers all fields EXCEPT + /// . This comparer, therefore, considers s constructed from + /// different syntactic parts of one partial to be equal. + /// + public static readonly IEqualityComparer WithoutLocationEqualityComparer = + new WithoutLocationEqualityComparerImpl(); + + private class WithoutLocationEqualityComparerImpl : IEqualityComparer + { + public bool Equals(PartialTypeInfo t, PartialTypeInfo other) + { + return t.Namespace == other.Namespace && + t.Parts.Equals(other.Parts) && + t.IsValid == other.IsValid && + t.IsSealed == other.IsSealed; + } + + public int GetHashCode(PartialTypeInfo t) + { + var hash = new HashCode(); + hash.Add(t.Namespace); + hash.Add(t.Parts); + hash.Add(t.IsValid); + hash.Add(t.IsSealed); + return hash.ToHashCode(); + } + } } diff --git a/Robust.Roslyn.Shared/TypeSymbolHelper.cs b/Robust.Roslyn.Shared/TypeSymbolHelper.cs index 878c98af2e3..d2702dc9bfb 100644 --- a/Robust.Roslyn.Shared/TypeSymbolHelper.cs +++ b/Robust.Roslyn.Shared/TypeSymbolHelper.cs @@ -96,4 +96,18 @@ public static bool Inherits(ITypeSymbol type, ITypeSymbol other) } return false; } + + /// + /// Checks if the given inherits from a type with the given metadata name. + /// + public static bool Inherits(ITypeSymbol type, string otherTypeName) + { + foreach (var baseType in GetBaseTypes(type)) + { + if (ShittyTypeMatch(baseType, otherTypeName)) + return true; + } + + return false; + } } diff --git a/Robust.Serialization.Generator/DataDefinition.cs b/Robust.Serialization.Generator/DataDefinition.cs index e33fbc6c0a3..8f02c82a46d 100644 --- a/Robust.Serialization.Generator/DataDefinition.cs +++ b/Robust.Serialization.Generator/DataDefinition.cs @@ -1,6 +1,54 @@ -using System.Collections.Generic; -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; + +using Robust.Roslyn.Shared; namespace Robust.Serialization.Generator; -public sealed record DataDefinition(ITypeSymbol Type, string GenericTypeName, List Fields, bool HasHooks, bool InvalidFields); +public sealed record DataDefinition( + ITypeSymbol Type, + string GenericTypeName, + List Fields, + bool HasHooks, + bool InvalidFields, + bool IsRecord) +{ + private readonly Dictionary _classificationCache = + new(SymbolEqualityComparer.Default); + + private bool _firstDataDefinitionBaseTypeResolved; + private ITypeSymbol? _firstDataDefinitionBaseType; + + internal bool IsDataDefinition(ITypeSymbol? type, out bool isDataRecord) + { + if (type == null) + { + isDataRecord = false; + return false; + } + + if (!_classificationCache.TryGetValue(type, out var result)) + { + result = (DataDefinitionHelper.IsDataDefinition(type, out var record), record); + _classificationCache.Add(type, result); + } + + isDataRecord = result.Record; + return result.Definition; + } + + internal ITypeSymbol? GetFirstDataDefinitionBaseType() + { + if (_firstDataDefinitionBaseTypeResolved) + return _firstDataDefinitionBaseType; + + _firstDataDefinitionBaseTypeResolved = true; + var parent = Type; + while ((parent = parent.BaseType) != null) + { + if (IsDataDefinition(parent, out _)) + return _firstDataDefinitionBaseType = parent; + } + + return null; + } +} diff --git a/Robust.Serialization.Generator/DataField.cs b/Robust.Serialization.Generator/DataField.cs index 820901eaee4..ff5e44dd6a9 100644 --- a/Robust.Serialization.Generator/DataField.cs +++ b/Robust.Serialization.Generator/DataField.cs @@ -1,14 +1,26 @@ using Microsoft.CodeAnalysis; +using Robust.Roslyn.Shared.Helpers; namespace Robust.Serialization.Generator; public sealed record DataField( ISymbol Symbol, ITypeSymbol Type, - (INamedTypeSymbol Serializer, CustomSerializerType Type)? CustomSerializer); + DataFieldAttribute Attribute, + (INamedTypeSymbol Serializer, CustomSerializerType Type)? CustomSerializer +); +[Flags] public enum CustomSerializerType { - Copier, - CopyCreator + None = 0, + Copier = 1 << 0, + CopyCreator = 1 << 1, + MappingValidator = 1 << 2, + SequenceValidator = 1 << 3, + ValueValidator = 1 << 4, + MappingReader = 1 << 5, + SequenceReader = 1 << 6, + ValueReader = 1 << 7, + Writer = 1 << 8, } diff --git a/Robust.Serialization.Generator/Generator.cs b/Robust.Serialization.Generator/Generator.cs index 357ee629cfe..bdf83318013 100644 --- a/Robust.Serialization.Generator/Generator.cs +++ b/Robust.Serialization.Generator/Generator.cs @@ -1,8 +1,12 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using static Robust.Roslyn.Shared.DataDefinitionHelper; using static Robust.Serialization.Generator.CustomSerializerType; using static Robust.Serialization.Generator.Types; @@ -11,51 +15,107 @@ namespace Robust.Serialization.Generator; [Generator] public class Generator : IIncrementalGenerator { - private const string TypeCopierInterfaceNamespace = "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeCopier"; - private const string TypeCopyCreatorInterfaceNamespace = "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeCopyCreator"; + private const string TypeCopierInterfaceNamespace = + "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeCopier"; + + private const string TypeCopyCreatorInterfaceNamespace = + "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeCopyCreator"; + + private const string TypeValidatorInterfaceNamespace = + "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeValidator"; + + private const string TypeReaderInterfaceNamespace = + "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeReader"; + + private const string TypeWriterInterfaceNamespace = + "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeWriter"; + private const string SerializationHooksNamespace = "Robust.Shared.Serialization.ISerializationHooks"; private const string AutoStateAttributeName = "Robust.Shared.Analyzers.AutoGenerateComponentStateAttribute"; private const string ComponentDeltaInterfaceName = "Robust.Shared.GameObjects.IComponentDelta"; + private const string MappingDataNodeName = "Robust.Shared.Serialization.Markdown.Mapping.MappingDataNode"; + private const string SequenceDataNodeName = "Robust.Shared.Serialization.Markdown.Sequence.SequenceDataNode"; + private const string ValueDataNodeName = "Robust.Shared.Serialization.Markdown.Value.ValueDataNode"; + private const string EntityUidName = "Robust.Shared.GameObjects.EntityUid"; public void Initialize(IncrementalGeneratorInitializationContext initContext) { IncrementalValuesProvider<(string name, string code)?> dataDefinitions = initContext.SyntaxProvider .CreateSyntaxProvider( - static (node, _) => node is TypeDeclarationSyntax, - static (context, _) => + static (node, _) => IsCandidateTypeDeclaration(node), + static (context, cancellationToken) => { var type = (TypeDeclarationSyntax)context.Node; - var symbol = (ITypeSymbol)context.SemanticModel.GetDeclaredSymbol(type)!; - if (!IsDataDefinition(symbol)) + if (context.SemanticModel.GetDeclaredSymbol(type, cancellationToken) is not INamedTypeSymbol symbol) + return null; + + if (symbol.TypeKind == TypeKind.Interface || + !IsCanonicalCandidateDeclaration(type, symbol, cancellationToken) || + !IsDataDefinition(symbol, out var isDataRecord)) + { return null; + } - return GenerateForDataDefinition(type, symbol); + return GenerateForDataDefinition(type, symbol, isDataRecord, cancellationToken); } ) .Where(static type => type != null); initContext.RegisterSourceOutput( - dataDefinitions.Collect(), - static (sourceContext, sources) => + dataDefinitions, + static (sourceContext, source) => { - var done = new HashSet(); + var (name, code) = source!.Value; + sourceContext.AddSource(name, SourceText.From(code, Encoding.UTF8)); + }); + } - foreach (var source in sources) - { - var (name, code) = source!.Value; + private static bool IsCandidateTypeDeclaration(SyntaxNode node) + { + return node is TypeDeclarationSyntax { AttributeLists.Count: > 0 } and not InterfaceDeclarationSyntax || + node is TypeDeclarationSyntax { BaseList: not null } and not InterfaceDeclarationSyntax; + } - if (!done.Add(name)) - continue; + private static bool IsCanonicalCandidateDeclaration( + TypeDeclarationSyntax declaration, + INamedTypeSymbol symbol, + CancellationToken cancellationToken) + { + TypeDeclarationSyntax? canonical = null; - sourceContext.AddSource(name, code); - } + foreach (var reference in symbol.DeclaringSyntaxReferences) + { + if (reference.GetSyntax(cancellationToken) is not TypeDeclarationSyntax syntax || + !IsCandidateTypeDeclaration(syntax)) + { + continue; } - ); + + if (canonical == null || CompareDeclarations(syntax, canonical) < 0) + canonical = syntax; + } + + return canonical == declaration; + } + + private static int CompareDeclarations(TypeDeclarationSyntax left, TypeDeclarationSyntax right) + { + var pathComparison = string.Compare( + left.SyntaxTree.FilePath, + right.SyntaxTree.FilePath, + StringComparison.Ordinal); + + if (pathComparison != 0) + return pathComparison; + + return left.SpanStart.CompareTo(right.SpanStart); } private static (string, string)? GenerateForDataDefinition( TypeDeclarationSyntax declaration, - ITypeSymbol type) + INamedTypeSymbol type, + bool isDataRecord, + CancellationToken cancellationToken) { var builder = new StringBuilder(); var containingTypes = new Stack(); @@ -83,7 +143,7 @@ private static (string, string)? GenerateForDataDefinition( var containingTypesEnd = new StringBuilder(); foreach (var parent in containingTypes) { - var syntax = (ClassDeclarationSyntax)parent.DeclaringSyntaxReferences[0].GetSyntax(); + var syntax = (TypeDeclarationSyntax)parent.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); if (!IsPartial(syntax)) { nonPartial = true; @@ -94,19 +154,28 @@ private static (string, string)? GenerateForDataDefinition( containingTypesEnd.AppendLine("}"); } - var definition = GetDataDefinition(type); + var definition = GetDataDefinition(type, isDataRecord); if (nonPartial || definition.InvalidFields) return null; builder.AppendLine($$""" #nullable enable using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Diagnostics.CodeAnalysis; using Robust.Shared.Analyzers; using Robust.Shared.IoC; using Robust.Shared.GameObjects; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager; + using Robust.Shared.Serialization.Manager.Definition; using Robust.Shared.Serialization.Manager.Exceptions; + using Robust.Shared.Serialization.Markdown; + using Robust.Shared.Serialization.Markdown.Mapping; + using Robust.Shared.Serialization.Markdown.Sequence; + using Robust.Shared.Serialization.Markdown.Validation; + using Robust.Shared.Serialization.Markdown.Value; using Robust.Shared.Serialization.TypeSerializers.Interfaces; #pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable CS0612 // Type or member is obsolete @@ -119,11 +188,19 @@ private static (string, string)? GenerateForDataDefinition( {{GetPartialTypeDefinitionLine(type)}} : ISerializationGenerated<{{definition.GenericTypeName}}> { - {{GetConstructor(definition)}} - - {{GetCopyMethods(definition)}} + {{GetConstructors(definition)}} {{GetInstantiators(definition)}} + + {{GetCopiers(definition)}} + + {{GetReader(definition)}} + + {{GetWriter(definition)}} + + {{GetValidator(definition)}} + + {{GetFieldDefinitions(definition)}} } {{containingTypesEnd}} @@ -132,175 +209,403 @@ private static (string, string)? GenerateForDataDefinition( return ($"{symbolName}.g.cs", builder.ToString()); } - private static DataDefinition GetDataDefinition(ITypeSymbol definition) + private static void GetDataFields( + ITypeSymbol definition, + bool isDataRecord, + List fields, + List symbols, + ref bool invalidFields) { - var fields = new List(); - var invalidFields = false; - - foreach (var member in definition.GetMembers()) + foreach (var (field, fieldType, attribute) in GetAllDataFields(definition, isDataRecord)) { - if (member is not IFieldSymbol && member is not IPropertySymbol) - continue; + var existingIndex = fields.FindIndex(existing => + SymbolEqualityComparer.Default.Equals(existing.Symbol, field)); + if (existingIndex != -1) + { + if (fields[existingIndex].Attribute.Data != null || attribute.Data == null) + continue; - if (member.IsStatic) - continue; + fields.RemoveAt(existingIndex); + } + + if (!IsDataDefinition(field.ContainingType, out _)) + invalidFields = true; - if (IsDataField(member, out var type, out var attribute)) + if (attribute.Data?.ConstructorArguments.FirstOrDefault(arg => arg.Kind == TypedConstantKind.Type).Value is + INamedTypeSymbol customSerializer) { - if (attribute.ConstructorArguments.FirstOrDefault(arg => arg.Kind == TypedConstantKind.Type).Value is INamedTypeSymbol customSerializer) + var serializerType = None; + if (ImplementsInterface(customSerializer, TypeCopierInterfaceNamespace)) + serializerType |= Copier; + else if (ImplementsInterface(customSerializer, TypeCopyCreatorInterfaceNamespace)) + serializerType |= CopyCreator; + + if (ImplementsInterface(customSerializer, TypeValidatorInterfaceNamespace, symbols)) { - if (ImplementsInterface(customSerializer, TypeCopierInterfaceNamespace)) + foreach (var symbol in symbols) { - fields.Add(new DataField(member, type, (customSerializer, Copier))); - continue; + if (symbol.IsGenericType && + symbol.TypeArguments is { Length: >= 2 } arguments) + { + var nodeType = arguments[1]; + if (nodeType.ToDisplayString().Contains(MappingDataNodeName)) + serializerType |= MappingValidator; + + if (nodeType.ToDisplayString().Contains(SequenceDataNodeName)) + serializerType |= SequenceValidator; + + if (nodeType.ToDisplayString().Contains(ValueDataNodeName)) + serializerType |= ValueValidator; + } } - else if (ImplementsInterface(customSerializer, TypeCopyCreatorInterfaceNamespace)) + } + + if (ImplementsInterface(customSerializer, TypeReaderInterfaceNamespace, symbols)) + { + foreach (var symbol in symbols) { - fields.Add(new DataField(member, type, (customSerializer, CopyCreator))); - continue; + if (symbol.IsGenericType && + symbol.TypeArguments is { Length: >= 2 } arguments) + { + var nodeType = arguments[1]; + if (nodeType.ToDisplayString().Contains(MappingDataNodeName)) + serializerType |= MappingReader; + + if (nodeType.ToDisplayString().Contains(SequenceDataNodeName)) + serializerType |= SequenceReader; + + if (nodeType.ToDisplayString().Contains(ValueDataNodeName)) + serializerType |= ValueReader; + } } } - fields.Add(new DataField(member, type, null)); + if (ImplementsInterface(customSerializer, TypeWriterInterfaceNamespace, symbols)) + serializerType |= Writer; - if (IsReadOnlyMember(definition, type)) + if (serializerType != None) { - invalidFields = true; + fields.Add(new DataField(field, fieldType, attribute, (customSerializer, serializerType))); + continue; } } + + fields.Add(new DataField(field, fieldType, attribute, null)); + + if (IsReadOnlyMember(definition, fieldType)) + invalidFields = true; } + } + + private static DataDefinition GetDataDefinition(ITypeSymbol definition, bool isDataRecord) + { + var fields = new List(); + var symbols = new List(); + var invalidFields = false; + + GetDataFields(definition, isDataRecord, fields, symbols, ref invalidFields); var typeName = GetGenericTypeName(definition); var hasHooks = ImplementsInterface(definition, SerializationHooksNamespace); - return new DataDefinition(definition, typeName, fields, hasHooks, invalidFields); + // Same as DataDefinition.cs + fields.Sort((a, b) => + { + var priority = b.Attribute.Priority.CompareTo(a.Attribute.Priority); + if (priority != 0) + return priority; + + return string.Compare(b.Symbol.Name, a.Symbol.Name, StringComparison.OrdinalIgnoreCase); + }); + + return new DataDefinition(definition, typeName, fields, hasHooks, invalidFields, isDataRecord); } - private static string GetConstructor(DataDefinition definition) - { - if (definition.Type.TypeKind == TypeKind.Interface) - return string.Empty; - - var builder = new StringBuilder(); - - if (NeedsEmptyConstructor(definition.Type)) - { - builder.AppendLine($$""" - // Implicit constructor - #pragma warning disable CS8618 - [RobustAutoGenerated] - public {{definition.Type.Name}}() - #pragma warning restore CS8618 - { - } - """); - } - - return builder.ToString(); - } - - private static string GetCopyMethods(DataDefinition definition) + private static string GetConstructors(DataDefinition definition) { + if (definition.Type.TypeKind == TypeKind.Interface) + return string.Empty; + var builder = new StringBuilder(); + var thisCall = new StringBuilder(); + var (needsEmpty, mustCall) = NeedsEmptyConstructor(definition.Type); + if (mustCall != null) + { + thisCall.Append(" : this("); + foreach (var parameter in mustCall.Parameters) + { + thisCall.Append($"{GetParameterDefaultExpression(parameter)},"); + } - var modifiers = IsVirtualClass(definition.Type) ? "virtual " : string.Empty; - var baseCall = string.Empty; - string baseCopy; - var baseType = definition.Type.BaseType; + if (thisCall[thisCall.Length - 1] == ',') + thisCall = thisCall.Remove(thisCall.Length - 1, 1); - if (baseType != null && IsDataDefinition(definition.Type.BaseType)) - { - var baseName = baseType.ToDisplayString(); - baseCall = $""" - var definitionCast = ({baseName}) target; - base.InternalCopy(ref definitionCast, serialization, hookCtx, context); - target = ({definition.GenericTypeName}) definitionCast; - """; - - baseCopy = $$""" - /// - [RobustAutoGenerated] - [Obsolete("Use ISerializationManager.CopyTo instead")] - public override void Copy(ref {{baseName}} target, ISerializationManager serialization, SerializationHookContext hookCtx, ISerializationContext? context = null) - { - var cast = ({{definition.GenericTypeName}}) target; - Copy(ref cast, serialization, hookCtx, context); - target = cast!; - } - - /// - [RobustAutoGenerated] - [Obsolete("Use ISerializationManager.CopyTo instead")] - public override void Copy(ref object target, ISerializationManager serialization, SerializationHookContext hookCtx, ISerializationContext? context = null) - { - var cast = ({{definition.GenericTypeName}}) target; - Copy(ref cast, serialization, hookCtx, context); - target = cast!; - } - """; + thisCall.Append(')'); } - else + + var setsRequired = GetSetsRequiredAttributeOrEmpty(definition.Type); + if (needsEmpty) { - baseCopy = $$""" - /// - [RobustAutoGenerated] - [Obsolete("Use ISerializationManager.CopyTo instead")] - public {{modifiers}} void Copy(ref object target, ISerializationManager serialization, SerializationHookContext hookCtx, ISerializationContext? context = null) - { - var cast = ({{definition.GenericTypeName}}) target; - Copy(ref cast, serialization, hookCtx, context); - target = cast!; - } - """; + // There was one case in content of a content-defined constructor calling the source-generated + // empty constructor, which would then call it again. + // Instead of attempting to find loops like this, I changed content. + // Because that's fucking stupid. + builder.AppendLine($$""" + // Implicit constructor + #pragma warning disable CS8618 + {{setsRequired}} + public {{definition.Type.Name}}(){{thisCall}} + #pragma warning restore CS8618 + { + } + """); } + var accessibility = definition.Type.IsValueType + ? "public" + : definition.Type.IsSealed + ? "private" + : "protected"; + + var copyBaseCall = definition.IsDataDefinition(definition.Type.BaseType, out _) + ? "base(ISerializationGeneratedCopy, source, serialization, hookCtx, context)" + : "this()"; + + var readBaseCall = definition.IsDataDefinition(definition.Type.BaseType, out _) + ? "base(ISerializationGeneratedRead, mappingDataNode, serialization, hookCtx, context)" + : "this()"; + builder.AppendLine($$""" - /// - [RobustAutoGenerated] - [Obsolete("Use ISerializationManager.CopyTo instead")] - public {{modifiers}} void InternalCopy(ref {{definition.GenericTypeName}} target, ISerializationManager serialization, SerializationHookContext hookCtx, ISerializationContext? context = null) - { - {{baseCall}} - {{CopyDataFields(definition)}} - } - - /// - [RobustAutoGenerated] - [Obsolete("Use ISerializationManager.CopyTo instead")] - public {{modifiers}} void Copy(ref {{definition.GenericTypeName}} target, ISerializationManager serialization, SerializationHookContext hookCtx, ISerializationContext? context = null) - { - InternalCopy(ref target, serialization, hookCtx, context); - } - - {{baseCopy}} - """); - - foreach (var interfaceName in InternalGetImplicitDataDefinitionInterfaces(definition.Type, true)) + [Obsolete("Used only in serialization source generation internally")] + #pragma warning disable CS8618 + {{setsRequired}} + {{accessibility}} {{definition.Type.Name}}( + ISerializationGeneratedCopy ISerializationGeneratedCopy, + {{definition.GenericTypeName}} source, + ISerializationManager serialization, + SerializationHookContext hookCtx, + ISerializationContext? context + ) : {{copyBaseCall}} + #pragma warning restore CS8618 + { + {{GetCopyBody(definition)}} + } + + [Obsolete("Used only in serialization source generation internally")] + #pragma warning disable CS8618 + {{setsRequired}} + {{accessibility}} {{definition.Type.Name}}( + ISerializationGeneratedRead ISerializationGeneratedRead, + MappingDataNode mappingDataNode, + ISerializationManager serialization, + SerializationHookContext hookCtx, + ISerializationContext? context + ) : {{readBaseCall}} + #pragma warning restore CS8618 + { + {{GetReadBody(definition)}} + } + """); + + return builder.ToString(); + } + + private static string GetParameterDefaultExpression(IParameterSymbol parameter) + { + var typeName = parameter.Type.ToDisplayString(); + if (!parameter.HasExplicitDefaultValue) + return $"({typeName}) default!"; + + var value = parameter.ExplicitDefaultValue; + if (value == null) + return "null!"; + + var literal = parameter.Type.SpecialType switch + { + SpecialType.System_Boolean => (bool)value ? "true" : "false", + SpecialType.System_Char => SyntaxFactory.LiteralExpression( + SyntaxKind.CharacterLiteralExpression, + SyntaxFactory.Literal((char)value)).ToFullString(), + SpecialType.System_String => SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal((string)value)).ToFullString(), + SpecialType.System_Single => Convert.ToString(value, CultureInfo.InvariantCulture) + "f", + SpecialType.System_Double => Convert.ToString(value, CultureInfo.InvariantCulture) + "d", + SpecialType.System_Decimal => Convert.ToString(value, CultureInfo.InvariantCulture) + "m", + SpecialType.System_UInt32 => Convert.ToString(value, CultureInfo.InvariantCulture) + "U", + SpecialType.System_Int64 => Convert.ToString(value, CultureInfo.InvariantCulture) + "L", + SpecialType.System_UInt64 => Convert.ToString(value, CultureInfo.InvariantCulture) + "UL", + _ => Convert.ToString(value, CultureInfo.InvariantCulture) ?? "default!" + }; + + if (literal.StartsWith("-", StringComparison.Ordinal)) + literal = $"({literal})"; + + return $"({typeName}) {literal}"; + } + + private static string GetReadBody(DataDefinition definition, string targetPrefix = "this") + { + var builder = new StringBuilder(); + for (var i = 0; i < definition.Fields.Count; i++) { - var interfaceModifiers = baseType != null && - baseType.AllInterfaces.Any(i => i.ToDisplayString() == interfaceName) - ? "override " - : modifiers; + var field = definition.Fields[i]; + if (!definition.Type.Equals(field.Symbol.ContainingType, SymbolEqualityComparer.Default)) + continue; + + if (field.Attribute.ServerOnly) + { + builder.AppendLine(""" + if (serialization.IsServer) + { + """); + } + + var fieldName = field.Symbol.Name; + var targetName = $"{targetPrefix}.{fieldName}"; + if (field.Attribute.IsDataFieldAttribute) + { + builder.AppendLine($$""" + if (mappingDataNode.TryGet("{{field.Attribute.Tag}}", out var node{{i}})) + { + """); + } + else + { + builder.AppendLine($$""" + { + var node{{i}} = mappingDataNode; + """); + } + + var (fieldTypeName, nonNullableFieldTypeName) = GetCleanNameForGenericType(field.Type, out _); + var tagName = field.Attribute.Tag; + var reader = field.CustomSerializer; + var readerName = reader?.Serializer.ToDisplayString(); + var nullable = field.Type.NullableAnnotation == NullableAnnotation.Annotated || + field.Type.ToDisplayString().EndsWith("?"); + var nullableString = string.Empty; + if (!field.Type.IsValueType) + { + nullableString = $", {(!nullable).ToString().ToLowerInvariant()}"; + if (fieldTypeName.EndsWith("?")) + fieldTypeName = fieldTypeName.Substring(0, fieldTypeName.Length - 1); + } + + var nullExpression = + field.Type.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString().Equals(EntityUidName) + ? $"{targetName} = EntityUid.Invalid;" + : nullable + ? $"{targetName} = default!;" + : "throw new NullNotAllowedException();"; builder.AppendLine($$""" - /// - [RobustAutoGenerated] - [Obsolete("Use ISerializationManager.CopyTo instead")] - public {{interfaceModifiers}} void InternalCopy(ref {{interfaceName}} target, ISerializationManager serialization, SerializationHookContext hookCtx, ISerializationContext? context = null) + if (node{{i}}.IsNull) { - var def = ({{definition.GenericTypeName}}) target; - Copy(ref def, serialization, hookCtx, context); - target = def; + {{nullExpression}} } + else + { + """); + + var method = $"Read<{fieldTypeName}>"; + if (field.Type.TypeKind == TypeKind.Enum) + { + method = $"ReadEnum<{fieldTypeName}>"; + nullableString = string.Empty; + } + else if (field.Type.IsValueType && definition.IsDataDefinition(field.Type, out _)) + { + method = $"ReadStructDefinition<{fieldTypeName}>"; + nullableString = string.Empty; + } + else if (field.Type.TypeKind == TypeKind.Array && + field.Type is IArrayTypeSymbol { Rank: 1 } arrayTypeSymbol) // [*,*] goes the regular way + { + var elementType = arrayTypeSymbol.ElementType; + method = $"ReadArray<{elementType}>"; - /// - [RobustAutoGenerated] - [Obsolete("Use ISerializationManager.CopyTo instead")] - public {{interfaceModifiers}} void Copy(ref {{interfaceName}} target, ISerializationManager serialization, SerializationHookContext hookCtx, ISerializationContext? context = null) + if (elementType.NullableAnnotation != NullableAnnotation.Annotated && + !elementType.ToDisplayString().EndsWith("?") && + !elementType.IsValueType) { - InternalCopy(ref target, serialization, hookCtx, context); + nullableString = $", {(!nullable).ToString().ToLowerInvariant()}"; } - """); + else + { + nullableString = string.Empty; + } + } + else if (definition.IsDataDefinition(field.Type, out _) && field.Type.TypeKind != TypeKind.Interface) + { + method = $"ReadDefinition<{fieldTypeName}>"; + } + + if (reader is { Type: var type } && + (type & (MappingReader | SequenceReader | ValueReader)) != 0) + { + builder.AppendLine($$""" + switch (node{{i}}) + { + """); + + if ((reader.Value.Type & MappingReader) != 0) + { + builder.AppendLine($""" + case MappingDataNode mapping: + {targetName} = serialization.Read<{nonNullableFieldTypeName}, MappingDataNode, {readerName}>(mapping, hookCtx, context, null{nullableString}); + break; + """); + } + + if ((reader.Value.Type & SequenceReader) != 0) + { + builder.AppendLine($""" + case SequenceDataNode sequence: + {targetName} = serialization.Read<{nonNullableFieldTypeName}, SequenceDataNode, {readerName}>(sequence, hookCtx, context, null{nullableString}); + break; + """); + } + + if ((reader.Value.Type & ValueReader) != 0) + { + builder.AppendLine($""" + case ValueDataNode value: + {targetName} = serialization.Read<{nonNullableFieldTypeName}, ValueDataNode, {readerName}>(value, hookCtx, context, null{nullableString}); + break; + """); + } + + builder.AppendLine($$""" + default: + throw new InvalidOperationException($"Unable to read node for {{field.Symbol.Name}}({{field.Attribute.Data?.AttributeClass?.Name}}) as valid."); + } + """); + } + else + { + builder.AppendLine( + $"{targetName} = serialization.{method}(node{i}, hookCtx, context, null{nullableString});"); + } + + builder.AppendLine("}"); + builder.AppendLine("}"); + + if (field.Attribute is { IsDataFieldAttribute: true, Required: true }) + { + if (field.Type.IsReferenceType && fieldTypeName.EndsWith("?")) + fieldTypeName = fieldTypeName.Substring(0, fieldTypeName.Length - 1); + + builder.AppendLine($$""" + else + { + throw new RequiredFieldNotMappedException(typeof({{fieldTypeName}}), "{{tagName}}", typeof({{definition.Type.ToDisplayString()}})); + } + """); + } + + if (field.Attribute.ServerOnly) + builder.AppendLine("}"); } return builder.ToString(); @@ -311,7 +616,7 @@ private static string GetInstantiators(DataDefinition definition) var builder = new StringBuilder(); var modifiers = string.Empty; - if (definition.Type.BaseType is { } baseType && IsDataDefinition(baseType)) + if (definition.GetFirstDataDefinitionBaseType() != null) modifiers = "override "; else if (IsVirtualClass(definition.Type)) modifiers = "virtual "; @@ -320,297 +625,816 @@ private static string GetInstantiators(DataDefinition definition) { // TODO make abstract once data definitions are forced to be partial builder.AppendLine($$""" - /// - [RobustAutoGenerated] - [Obsolete("Use ISerializationManager.CreateCopy instead")] - public {{modifiers}} {{definition.GenericTypeName}} Instantiate() - { - throw new NotImplementedException(); - } - """); + /// + [Obsolete("Use ISerializationManager.CreateCopy instead")] + public {{modifiers}} {{definition.GenericTypeName}} Instantiate() + { + throw new NotImplementedException(); + } + """); } else { + var requiredFields = GetRequiredFieldsPropertiesAssigners(definition.Type, string.Empty); builder.AppendLine($$""" - /// - [RobustAutoGenerated] - [Obsolete("Use ISerializationManager.CreateCopy instead")] - public {{modifiers}} {{definition.GenericTypeName}} Instantiate() - { - return new {{definition.GenericTypeName}}(); - } - """); + /// + [Obsolete("Use ISerializationManager.CreateCopy instead")] + public {{modifiers}} {{definition.GenericTypeName}} Instantiate() + { + return new {{definition.GenericTypeName}}(){{requiredFields}}; + } + + public static {{definition.GenericTypeName}} StaticInstantiate() + { + return new {{definition.GenericTypeName}}(); + } + + public static object StaticInstantiateObject() + { + return (object) {{definition.GenericTypeName}}.StaticInstantiate(); + } + """); } - foreach (var interfaceName in InternalGetImplicitDataDefinitionInterfaces(definition.Type, false)) + return builder.ToString(); + } + + private static string GetValidator(DataDefinition definition) + { + var builder = new StringBuilder(); + var validateBuilder = new StringBuilder(); + + for (var i = 0; i < definition.Fields.Count; i++) { + validateBuilder.Clear(); + + var field = definition.Fields[i]; + if (!definition.Type.Equals(field.Symbol.ContainingType, SymbolEqualityComparer.Default)) + continue; + + var fieldTypeName = GetNonNullableNameForGenericParameter(field.Type); + var tagName = field.Attribute.Tag; + if (field.Attribute.Include) + { + builder.AppendLine($"var node{i} = node;"); + } + else + { + builder.AppendLine($$""" + if (node.TryGetValue("{{tagName}}", out var node{{i}})) + { + """); + } + + var validator = field.CustomSerializer; + var validatorName = validator?.Serializer.ToDisplayString(); + if (validator != null && (validator.Value.Type & MappingValidator) != 0) + { + validateBuilder.AppendLine($""" + case MappingDataNode mapping: + nodes["{tagName}"] = serialization.ValidateNode<{fieldTypeName}, MappingDataNode, {validatorName}>(mapping, context); + break; + """); + } + + if (validator != null && (validator.Value.Type & SequenceValidator) != 0) + { + validateBuilder.AppendLine($""" + case SequenceDataNode sequence: + nodes["{tagName}"] = serialization.ValidateNode<{fieldTypeName}, SequenceDataNode, {validatorName}>(sequence, context); + break; + """); + } + + if (validator != null && (validator.Value.Type & ValueValidator) != 0) + { + validateBuilder.AppendLine($""" + case ValueDataNode value: + nodes["{tagName}"] = serialization.ValidateNode<{fieldTypeName}, ValueDataNode, {validatorName}>(value, context); + break; + """); + } + builder.AppendLine($$""" - [RobustAutoGenerated] - {{interfaceName}} {{interfaceName}}.Instantiate() + switch (node{{i}}) { - return Instantiate(); + {{validateBuilder}} + default: + nodes["{{tagName}}"] = serialization.ValidateNode<{{fieldTypeName}}>(node{{i}}, context); + break; } + """); + + if (!field.Attribute.Include) + builder.AppendLine("}"); + } - [RobustAutoGenerated] - {{interfaceName}} ISerializationGenerated<{{interfaceName}}>.Instantiate() + if (definition.GetFirstDataDefinitionBaseType() is { } baseType) + builder.AppendLine($"{baseType.ToDisplayString()}.Validate(nodes, node, serialization, context);"); + + return $$""" + public static void Validate(Dictionary nodes, MappingDataNode node, ISerializationManager serialization, ISerializationContext? context = null) + { + {{builder}} + } + """; + } + + private static string GetCopiers(DataDefinition definition) + { + var builder = new StringBuilder(); + var requiredFields = GetRequiredFieldsPropertiesAssigners(definition.Type, string.Empty); + var type = definition.Type; + var baseType = type.BaseType; + var baseDefinition = false; + while (baseType != null) + { + if (!baseDefinition && definition.IsDataDefinition(baseType, out _)) + baseDefinition = true; + + GetCopierMethod(definition, baseType, baseType.ToDisplayString(), true, builder, requiredFields); + baseType = baseType.BaseType; + } + + GetCopierMethod(definition, definition.Type, "object", baseDefinition, builder, requiredFields); + GetCopierMethod(definition, definition.Type, GetGenericTypeName(type), false, builder, requiredFields); + return builder.ToString(); + } + + private static void GetCopierMethod( + DataDefinition definition, + ITypeSymbol type, + string targetType, + bool forceOverride, + StringBuilder builder, + string requiredFields) + { + if (!definition.IsDataDefinition(type, out _)) + return; + + var sameType = definition.Type.Equals(type, SymbolEqualityComparer.Default) && + targetType == definition.GenericTypeName && + targetType != "object"; + var isSealedOrStruct = definition.Type.IsSealed || definition.Type.IsValueType; + var isAbstract = definition.Type.IsAbstract; + var isInterface = definition.Type.TypeKind == TypeKind.Interface; + var modifier = (sameType, targetType == "object", isSealedOrStruct, isInterface) switch + { + (true, _, true, _) => string.Empty, + (true, _, false, _) => "virtual ", + (false, true, true, _) => string.Empty, + (false, true, false, _) => "virtual ", + (false, false, _, true) => string.Empty, + (false, false, _, false) => "override ", + }; + + if (!sameType && targetType == "object" && forceOverride) + modifier = "override "; + + if (forceOverride && modifier is "" or "virtual ") + { + if (modifier is "") + modifier += "override "; + else if (modifier == "virtual ") + modifier = "override "; + } + + builder.AppendLine($""" + public {modifier}void Copy( + ref {targetType} target, + ISerializationManager serialization, + SerializationHookContext hookCtx, + ISerializationContext? context = null) + """); + + if (!sameType) + { + builder.AppendLine($$""" { - return Instantiate(); + var def = ({{definition.GenericTypeName}})target; + Copy(ref def, serialization, hookCtx, context); + target = def; + } + """); + } + else if (definition.Type.IsValueType || definition.IsRecord) + { + builder.AppendLine($$""" + { + target = new {{definition.GenericTypeName}}( + ISerializationGeneratedCopy.Default, + this, + serialization, + hookCtx, + context + ){{requiredFields}}; } """); } + else + { + var baseCopy = definition.IsDataDefinition(definition.Type.BaseType, out _) + ? $$""" + var definitionCast = ({{definition.Type.BaseType!.ToDisplayString()}})target; + base.Copy(ref definitionCast, serialization, hookCtx, context); + target = ({{definition.GenericTypeName}})definitionCast; + """ + : string.Empty; + + var instantiate = isAbstract + ? $$""" + if (target is null) + throw new NullReferenceException("Cannot copy into a null abstract data definition target."); + """ + : $$""" + if (target is null) + { + target = new {{definition.GenericTypeName}}( + ISerializationGeneratedCopy.Default, + this, + serialization, + hookCtx, + context + ){{requiredFields}}; + return; + } + """; - return builder.ToString(); + builder.AppendLine($$""" + { + {{instantiate}} + var source = this; + {{baseCopy}} + if (serialization.TryCustomCopy(this, ref target, hookCtx, {{definition.HasHooks.ToString().ToLower()}}, context)) + return; + + {{GetCopyBody(definition, "target")}} + } + """); + } } - [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] - private static IEnumerable InternalGetImplicitDataDefinitionInterfaces( - ITypeSymbol type, - bool all) + private static string GetReader(DataDefinition definition) { - var symbols = GetImplicitDataDefinitionInterfaces(type, all); - - // TODO SOURCE GEN - // fix this jank - // The comp-state source generator will add an "IComponentDelta" interface to classes with the auto state - // attribute, and this generator creates methods that those classes then have to implement because - // IComponentDelta a DataDefinition via the ImplicitDataDefinitionForInheritorsAttribute on IComponent. - if (!TryGetAttribute(type, AutoStateAttributeName, out var data)) - return symbols; - - // If it doesn't have field deltas then ignore - if (data.ConstructorArguments[1] is not { Value: bool fields and true }) + string body; + if (definition.Type.IsAbstract) + { + var baseRead = definition.IsDataDefinition(definition.Type.BaseType, out _) + ? $$""" + var definitionCast = ({{definition.Type.BaseType!.ToDisplayString()}})target; + {{definition.Type.BaseType!.ToDisplayString()}}.Read(ref definitionCast, mappingDataNode, serialization, hookCtx, context); + target = ({{definition.GenericTypeName}})definitionCast; + """ + : string.Empty; + + body = $$""" + if (target is null) + throw new NullReferenceException("Cannot read into a null abstract data definition target."); + + {{baseRead}} + {{GetReadBody(definition, "target")}} + """; + } + else if (definition.Type.IsValueType || definition.IsRecord) { - return symbols; + body = $$""" + target = new {{definition.GenericTypeName}}( + ISerializationGeneratedRead.Default, + mappingDataNode, + serialization, + hookCtx, + context + ); + """; } + else + { + var baseRead = definition.IsDataDefinition(definition.Type.BaseType, out _) + ? $$""" + var definitionCast = ({{definition.Type.BaseType!.ToDisplayString()}})target; + {{definition.Type.BaseType!.ToDisplayString()}}.Read(ref definitionCast, mappingDataNode, serialization, hookCtx, context); + target = ({{definition.GenericTypeName}})definitionCast; + """ + : string.Empty; + + body = $$""" + if (target is null) + { + target = new {{definition.GenericTypeName}}( + ISerializationGeneratedRead.Default, + mappingDataNode, + serialization, + hookCtx, + context + ); + return; + } - if (symbols.Any(x => x == ComponentDeltaInterfaceName)) - return symbols; + {{baseRead}} + {{GetReadBody(definition, "target")}} + """; + } - return symbols.Append(ComponentDeltaInterfaceName); + return $$""" + public static void Read( + ref {{definition.GenericTypeName}} target, + MappingDataNode mappingDataNode, + ISerializationManager serialization, + SerializationHookContext hookCtx, + ISerializationContext? context) + { + {{body}} + } + """; } - // TODO serveronly? do we care? who knows!! - private static StringBuilder CopyDataFields(DataDefinition definition) + private static string GetWriter(DataDefinition definition) { var builder = new StringBuilder(); + for (var i = 0; i < definition.Fields.Count; i++) + { + var field = definition.Fields[i]; + if (!definition.Type.Equals(field.Symbol.ContainingType, SymbolEqualityComparer.Default)) + continue; - builder.AppendLine($""" -if (serialization.TryCustomCopy(this, ref target, hookCtx, {definition.HasHooks.ToString().ToLower()}, context)) - return; -"""); + if (field.Attribute.ReadOnly) + continue; - var structCopier = new StringBuilder(); - foreach (var field in definition.Fields) + var fieldType = field.Type.ToDisplayString(); + if (IsMultidimensionalArray(field.Type)) + fieldType = fieldType.Replace("*", ""); + + if (field.Type.NullableAnnotation == NullableAnnotation.Annotated && + !fieldType.EndsWith("?")) + { + fieldType += "?"; + } + + var nonNullableFieldType = GetNonNullableNameForGenericParameter(field.Type); + var nullable = fieldType.EndsWith("?"); + var nullableString = string.Empty; + if (!field.Type.IsValueType) + { + if (!nullable) + nullableString = ", true"; + + if (nonNullableFieldType.EndsWith("?")) + nonNullableFieldType = nonNullableFieldType.Substring(0, nonNullableFieldType.Length - 1); + } + + if (field.Attribute.ServerOnly) + { + builder.AppendLine(""" + if (serialization.IsServer) + { + """); + } + + if (!field.Attribute.IsDataFieldAttribute || !field.Attribute.Required) + { + builder.AppendLine($$""" + if (alwaysWrite || !EqualityComparer<{{fieldType}}>.Default.Equals(obj.{{field.Symbol.Name}}, ({{fieldType}}) defaultValues["{{field.Attribute.Tag}}"]!)) + { + """); + } + + builder.AppendLine($"""DataNode node{i};"""); + + if (field.Attribute.IsDataFieldAttribute) + { + builder.AppendLine($$""" + if (!mapping.Has("{{field.Attribute.Tag}}")) + { + """); + } + + if (field.CustomSerializer is { } serializer && + (serializer.Type & Writer) != 0) + { + var nullableValueType = field.Type.IsValueType && nullable; + if (nullableValueType) + { + builder.AppendLine($$""" + if (obj.{{field.Symbol.Name}} == null) + { + node{{i}} = ValueDataNode.Null(); + } + else + { + """); + } + + var nullableValueTypeString = nullableValueType ? ".Value" : string.Empty; + var writerName = serializer.Serializer.ToDisplayString(); + builder.AppendLine($""" + #pragma warning disable RA0008 + node{i} = serialization.WriteValue<{nonNullableFieldType}, {writerName}>(obj.{field.Symbol.Name}{nullableValueTypeString}!, alwaysWrite, context{nullableString}); + #pragma warning restore RA0008 + """); + + if (nullableValueType) + builder.Append("}"); + } + else + { + builder.AppendLine( + $"node{i} = serialization.WriteValue<{fieldType}>(obj.{field.Symbol.Name}, alwaysWrite, context{nullableString});"); + } + + if (field.Attribute.IsDataFieldAttribute) + { + builder.AppendLine($$""" + mapping.Add("{{field.Attribute.Tag}}", node{{i}}); + } + """); + } + else + { + builder.AppendLine($$""" + if (node{{i}} is MappingDataNode mapping{{i}}) + { + mapping.Insert(mapping{{i}}, true); + } + else + { + throw new InvalidOperationException($"Writing field {{field.Symbol.Name}} for type {typeof({{definition.GenericTypeName}})} did not return a {nameof(MappingDataNode)} but was annotated to be included."); + } + """); + } + + if (!field.Attribute.IsDataFieldAttribute || !field.Attribute.Required) + builder.AppendLine("}"); + + if (field.Attribute.ServerOnly) + builder.AppendLine("}"); + + } + + if (definition.GetFirstDataDefinitionBaseType() is { } baseType) { - var type = field.Type; - var typeName = type.ToDisplayString(); - if (IsMultidimensionalArray(type)) + var baseTypeName = baseType.ToDisplayString(); + builder.AppendLine( + $"{baseTypeName}.Write(obj, mapping, serialization, context, alwaysWrite, defaultValues);"); + } + + return $$""" + public static void Write( + {{definition.GenericTypeName}} obj, + MappingDataNode mapping, + ISerializationManager serialization, + ISerializationContext? context, + bool alwaysWrite, + ImmutableDictionary defaultValues) { - typeName = typeName.Replace("*", ""); + {{builder}} } + """; + } + + private static string GetFieldDefinitions(DataDefinition definition) + { + var builder = new StringBuilder(); + var nullConditional = definition.Type.IsValueType ? string.Empty : "?"; + var fieldTags = new List(definition.Fields.Count); + foreach (var field in definition.Fields) + { + if (!definition.Type.Equals(field.Symbol.ContainingType, SymbolEqualityComparer.Default)) + continue; + + var (fieldType, _) = GetCleanNameForGenericType(field.Type, out var isNullableValueType); + var nullable = field.Type.NullableAnnotation == NullableAnnotation.Annotated || + field.Type.ToDisplayString().EndsWith("?"); - var isNullableValueType = IsNullableValueType(type); - var nonNullableTypeName = type.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString(); - if (isNullableValueType) + if (!isNullableValueType && fieldType.EndsWith("?")) + fieldType = fieldType.Substring(0, fieldType.Length - 1); + + builder.AppendLine($$""" + if (fieldsParsed == null || !fieldsParsed.Contains("{{field.Attribute.Tag}}")) + { + fields.Add(new DataFieldDefinition( + "{{field.Attribute.Tag}}", + {{field.Attribute.Priority}}, + {{field.Attribute.IsDataFieldAttribute.ToString().ToLowerInvariant()}}, + {{field.Attribute.Include.ToString().ToLowerInvariant()}}, + instance{{nullConditional}}.{{field.Symbol.Name}}, + (InheritanceBehavior) {{field.Attribute.InheritanceBehavior}}, + "{{field.Symbol.Name}}", + typeof({{fieldType}}), + {{nullable.ToString().ToLowerInvariant()}}, + "{{field.Attribute.CamelCasedName}}", + {{(field.CustomSerializer == null ? "null" : $"typeof({field.CustomSerializer.Value.Serializer.ToDisplayString()})")}} + )); + } + """); + + fieldTags.Add($"\"{field.Attribute.Tag}\""); + } + + if (definition.GetFirstDataDefinitionBaseType() is { } baseType) + builder.AppendLine( + $"{baseType.ToDisplayString()}.GetFieldDefinitions(instance, fields, [{string.Join(", ", fieldTags)}]);"); + + var instance = definition.Type.IsAbstract + ? string.Empty + : $"instance = {definition.GenericTypeName}.StaticInstantiate();"; + + return $$""" + public static void GetFieldDefinitions({{definition.GenericTypeName}}{{nullConditional}} instance, List fields, string[]? fieldsParsed = null) { - nonNullableTypeName = typeName.Substring(0, typeName.Length - 1); + {{instance}} + {{builder}} } + """; + } + + // TODO serveronly? do we care? who knows!! + private static StringBuilder GetCopyBody(DataDefinition definition, string targetPrefix = "") + { + var builder = new StringBuilder(); + foreach (var field in definition.Fields) + { + if (!definition.Type.Equals(field.Symbol.ContainingType, SymbolEqualityComparer.Default)) + continue; + + var type = field.Type; + var (typeName, nonNullableTypeName) = GetCleanNameForGenericType(type, out var isNullableValueType); var isClass = type.IsReferenceType || type.SpecialType == SpecialType.System_String; - var isNullable = type.NullableAnnotation == NullableAnnotation.Annotated; + var isNullable = type.NullableAnnotation == NullableAnnotation.Annotated || + field.Type.ToDisplayString().EndsWith("?"); var nullableOverride = isClass && !isNullable ? ", true" : string.Empty; var name = field.Symbol.Name; - var tempVarName = $"{name}Temp"; + var targetName = string.IsNullOrEmpty(targetPrefix) ? name : $"{targetPrefix}.{name}"; var nullableValue = isNullableValueType ? ".Value" : string.Empty; var nullNotAllowed = isClass && !isNullable; - if (field.CustomSerializer is { Serializer: var serializer, Type: var serializerType }) + if (field.CustomSerializer is { Serializer: var serializer, Type: var serializerType } && + ((serializerType & Copier) != 0 || (serializerType & CopyCreator) != 0)) { if (nullNotAllowed) { builder.AppendLine($$""" - if ({{name}} == null) - { - throw new NullNotAllowedException(); - } - """); + if (source.{{name}} == null) + { + throw new NullNotAllowedException(); + } + """); } - builder.AppendLine($$""" - {{typeName}} {{tempVarName}} = default!; - """); - if (isNullable || isNullableValueType) { builder.AppendLine($$""" - if ({{name}} == null) - { - {{tempVarName}} = null!; - } - else - { - """); + if (source.{{name}} == null) + { + {{targetName}} = null!; + } + else + { + """); } var serializerName = serializer.ToDisplayString(); - switch (serializerType) - { - case Copier: - CopyToCustom( - builder, - nonNullableTypeName, - serializerName, - tempVarName, - name, - isNullable, - isClass, - isNullableValueType - ); - break; - case CopyCreator: - CreateCopyCustom( - builder, - name, - tempVarName, - nonNullableTypeName, - serializerName, - nullableValue, - nullableOverride - ); - break; - } - if (isNullable || isNullableValueType) + // TODO ROBUST should these both be created if both are present? + if ((serializerType & Copier) != 0) { - builder.AppendLine("}"); + builder.AppendLine($""" + #pragma warning disable RA0008 + {nonNullableTypeName} {name}GeneratedTemp = default!; + serialization.CopyTo<{nonNullableTypeName}, {serializerName}>(source.{name}{nullableValue}, ref {name}GeneratedTemp, hookCtx, context{nullableOverride}); + #pragma warning restore RA0008 + {targetName} = {name}GeneratedTemp; + """); } - - if (definition.Type.IsValueType) + else if ((serializerType & CopyCreator) != 0) { - structCopier.AppendLine($"{name} = {tempVarName}!,"); + builder.AppendLine( + $"{targetName} = serialization.CreateCopy<{nonNullableTypeName}, {serializerName}>(source.{name}{nullableValue}, hookCtx, context{nullableOverride});"); } - else + + if (isNullable || isNullableValueType) + builder.AppendLine("}"); + } + else if (CanBeCopiedByValue(field.Symbol, field.Type)) + { + if (nullNotAllowed) { - builder.AppendLine($"target.{name} = {tempVarName}!;"); + builder.AppendLine($$""" + if (source.{{name}} == null) + { + throw new NullNotAllowedException(); + } + """); } + + builder.AppendLine($"{targetName} = source.{name};"); } else { - builder.AppendLine($$""" - {{typeName}} {{tempVarName}} = default!; - """); - if (nullNotAllowed) { builder.AppendLine($$""" - if ({{name}} == null) - { - throw new NullNotAllowedException(); - } - """); + if (source.{{name}} == null) + { + throw new NullNotAllowedException(); + } + """); } - var hasHooks = ImplementsInterface(type, SerializationHooksNamespace) || !type.IsSealed; - builder.AppendLine($$""" - if (!serialization.TryCustomCopy(this.{{name}}, ref {{tempVarName}}, hookCtx, {{hasHooks.ToString().ToLower()}}, context)) - { - """); - - if (CanBeCopiedByValue(field.Symbol, field.Type)) + if (TryGetFastCollectionCopy(field.Type, $"source.{name}", targetName, isNullable, out var collectionCopy)) { - builder.AppendLine($"{tempVarName} = {name};"); + builder.Append(collectionCopy); } - else if (IsDataDefinition(type) && !type.IsAbstract && - type is not INamedTypeSymbol { TypeKind: TypeKind.Interface }) + else { - var nullable = !type.IsValueType || IsNullableType(type); + var hasHooks = ImplementsInterface(type, SerializationHooksNamespace) || !type.IsSealed; + builder.AppendLine($$""" + {{typeName}} {{name}}GeneratedTemp = default!; + if (serialization.TryCustomCopy(source.{{name}}, ref {{name}}GeneratedTemp, hookCtx, {{hasHooks.ToString().ToLower()}}, context)) + { + {{targetName}} = {{name}}GeneratedTemp; + } + else + { + """); + + if (definition.IsDataDefinition(type, out _) && !type.IsAbstract && + type is not INamedTypeSymbol { TypeKind: TypeKind.Interface }) + { + var nullable = !type.IsValueType || IsNullableType(type); - if (nullable) + if (nullable) + { + builder.AppendLine($$""" + if (source.{{name}} == null) + { + {{targetName}} = null!; + } + else + { + """); + } + + builder.AppendLine($""" + serialization.CopyTo(source.{name}, ref {name}GeneratedTemp, hookCtx, context{nullableOverride}); + {targetName} = {name}GeneratedTemp; + """); + + if (nullable) + builder.AppendLine("}"); + } + else { - builder.AppendLine($$""" - if ({{name}} == null) - { - {{tempVarName}} = null!; - } - else - { - """); + builder.AppendLine($"{targetName} = serialization.CreateCopy(source.{name}, hookCtx, context);"); } - builder.AppendLine($$""" - serialization.CopyTo({{name}}, ref {{tempVarName}}, hookCtx, context{{nullableOverride}}); - """); + builder.AppendLine("}"); + } + } + } + + return builder; + } + + private static bool TryGetFastCollectionCopy( + ITypeSymbol type, + string sourceName, + string targetName, + bool nullable, + [NotNullWhen(true)] out string? copy) + { + copy = null; + var builder = new StringBuilder(); + var nullPrefix = string.Empty; + var nullSuffix = string.Empty; - if (nullable) + if (!type.IsValueType) + { + if (nullable) + { + nullPrefix = $$""" + if ({{sourceName}} == null) { - builder.AppendLine("}"); + {{targetName}} = null!; } - } + else + { + """; + nullSuffix = "}\n"; + } + } + + var sourceAccess = nullable ? $"{sourceName}!" : sourceName; + + if (type is IArrayTypeSymbol { Rank: 1 } arrayType && + CanTypeBeCopiedByValue(arrayType.ElementType)) + { + var arrayTypeName = type.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString(); + builder.Append(nullPrefix); + builder.AppendLine($"{targetName} = ({arrayTypeName}) {sourceAccess}.Clone();"); + builder.Append(nullSuffix); + copy = builder.ToString(); + return true; + } + + if (type is not INamedTypeSymbol { IsGenericType: true } namedType) + return false; + + var nonNullableTypeName = type.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString(); + var typeArgs = namedType.TypeArguments; + var targetAccess = $"{targetName}!"; + + if (IsGenericCollectionType(namedType, "List", 1) && + CanTypeBeCopiedByValue(typeArgs[0])) + { + builder.Append(nullPrefix); + builder.AppendLine($$""" + if ({{targetName}} == null) + {{targetName}} = new {{nonNullableTypeName}}({{sourceAccess}}.Count); else { - builder.AppendLine($"{tempVarName} = serialization.CreateCopy({name}, hookCtx, context);"); + {{targetAccess}}.Clear(); + {{targetAccess}}.EnsureCapacity({{sourceAccess}}.Count); } - builder.AppendLine("}"); + {{targetAccess}}.AddRange({{sourceAccess}}); + """); + builder.Append(nullSuffix); + copy = builder.ToString(); + return true; + } - if (definition.Type.IsValueType) + if (IsGenericCollectionType(namedType, "HashSet", 1) && + CanTypeBeCopiedByValue(typeArgs[0])) + { + builder.Append(nullPrefix); + builder.AppendLine($$""" + if ({{targetName}} == null) + {{targetName}} = new {{nonNullableTypeName}}({{sourceAccess}}.Count, {{sourceAccess}}.Comparer); + else { - structCopier.AppendLine($"{name} = {tempVarName}!,"); + {{targetAccess}}.Clear(); + {{targetAccess}}.EnsureCapacity({{sourceAccess}}.Count); } + + foreach (var value in {{sourceAccess}}) + {{targetAccess}}.Add(value); + """); + builder.Append(nullSuffix); + copy = builder.ToString(); + return true; + } + + if (IsGenericCollectionType(namedType, "Dictionary", 2) && + CanTypeBeCopiedByValue(typeArgs[0]) && + CanTypeBeCopiedByValue(typeArgs[1])) + { + builder.Append(nullPrefix); + builder.AppendLine($$""" + if ({{targetName}} == null) + {{targetName}} = new {{nonNullableTypeName}}({{sourceAccess}}.Count, {{sourceAccess}}.Comparer); else { - builder.AppendLine($"target.{name} = {tempVarName}!;"); + {{targetAccess}}.Clear(); + {{targetAccess}}.EnsureCapacity({{sourceAccess}}.Count); } - } + + foreach (var (key, value) in {{sourceAccess}}) + {{targetAccess}}.Add(key, value); + """); + builder.Append(nullSuffix); + copy = builder.ToString(); + return true; } - if (definition.Type.IsValueType) + if (IsGenericCollectionType(namedType, "SortedDictionary", 2) && + CanTypeBeCopiedByValue(typeArgs[0]) && + CanTypeBeCopiedByValue(typeArgs[1])) { + builder.Append(nullPrefix); builder.AppendLine($$""" - target = target with - { - {{structCopier}} - }; - """); + if ({{targetName}} == null) + {{targetName}} = new {{nonNullableTypeName}}({{sourceAccess}}.Comparer); + else + {{targetAccess}}.Clear(); + + foreach (var (key, value) in {{sourceAccess}}) + {{targetAccess}}.Add(key, value); + """); + builder.Append(nullSuffix); + copy = builder.ToString(); + return true; } - return builder; + return false; } - private static void CopyToCustom( - StringBuilder builder, - string typeName, - string serializerName, - string tempVarName, - string varName, - bool isNullable, - bool isClass, - bool isNullableValueType) + private static bool IsGenericCollectionType(INamedTypeSymbol type, string name, int typeArgumentCount) { - var newTemp = isNullable && isClass ? $"{tempVarName} ??= new();" : string.Empty; - var nullableOverride = isClass ? ", true" : string.Empty; - var nullableValue = isNullableValueType ? ".Value" : string.Empty; - var nonNullableTypeName = typeName.EndsWith("?") ? typeName.Substring(0, typeName.Length - 1) : typeName; + var definition = type.ConstructedFrom; - builder.AppendLine($$""" - {{nonNullableTypeName}} {{tempVarName}}CopyTo = default!; - {{newTemp}} - serialization.CopyTo<{{typeName}}, {{serializerName}}>(this.{{varName}}{{nullableValue}}, ref {{tempVarName}}CopyTo, hookCtx, context{{nullableOverride}}); - {{tempVarName}} = {{tempVarName}}CopyTo; - """); - } - - private static void CreateCopyCustom( - StringBuilder builder, - string varName, - string tempVarName, - string nonNullableTypeName, - string serializerName, - string nullableValue, - string nullableOverride) - { - builder.AppendLine($$""" - {{tempVarName}} = serialization.CreateCopy<{{nonNullableTypeName}}, {{serializerName}}>(this.{{varName}}{{nullableValue}}, hookCtx, context{{nullableOverride}}); - """); + return definition.Name == name && + definition.TypeArguments.Length == typeArgumentCount && + definition.ContainingNamespace.ToDisplayString() == "System.Collections.Generic"; } } diff --git a/Robust.Serialization.Generator/Types.cs b/Robust.Serialization.Generator/Types.cs index c07e3530d56..2a0da9c32c0 100644 --- a/Robust.Serialization.Generator/Types.cs +++ b/Robust.Serialization.Generator/Types.cs @@ -1,105 +1,31 @@ -using System.Collections.Generic; +using System.Diagnostics; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Robust.Roslyn.Shared.Helpers; +using static Robust.Roslyn.Shared.DataDefinitionHelper; namespace Robust.Serialization.Generator; internal static class Types { - private const string DataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataDefinitionAttribute"; - private const string ImplicitDataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.ImplicitDataDefinitionForInheritorsAttribute"; - private const string DataFieldBaseNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataFieldBaseAttribute"; private const string CopyByRefNamespace = "Robust.Shared.Serialization.Manager.Attributes.CopyByRefAttribute"; + private const string SerializationHooksNamespace = "Robust.Shared.Serialization.ISerializationHooks"; internal static bool IsPartial(TypeDeclarationSyntax type) { return type.Modifiers.IndexOf(SyntaxKind.PartialKeyword) != -1; } - internal static bool IsDataDefinition(ITypeSymbol? type) - { - if (type == null) - return false; - - return HasAttribute(type, DataDefinitionNamespace) || - IsImplicitDataDefinition(type); - } - - internal static bool IsDataField(ISymbol member, out ITypeSymbol type, out AttributeData attribute) - { - // TODO data records and other attributes - if (member is IFieldSymbol field) - { - foreach (var attr in field.GetAttributes()) - { - if (attr.AttributeClass != null && Inherits(attr.AttributeClass, DataFieldBaseNamespace)) - { - type = field.Type; - attribute = attr; - return true; - } - } - } - else if (member is IPropertySymbol property) - { - foreach (var attr in property.GetAttributes()) - { - if (attr.AttributeClass != null && Inherits(attr.AttributeClass, DataFieldBaseNamespace)) - { - type = property.Type; - attribute = attr; - return true; - } - } - } - - type = null!; - attribute = null!; - return false; - } - - internal static bool IsImplicitDataDefinition(ITypeSymbol type) - { - if (HasAttribute(type, ImplicitDataDefinitionNamespace)) - return true; - - foreach (var baseType in GetBaseTypes(type)) - { - if (HasAttribute(baseType, ImplicitDataDefinitionNamespace)) - return true; - } - - foreach (var @interface in type.AllInterfaces) - { - if (IsImplicitDataDefinitionInterface(@interface)) - return true; - } - - return false; - } - - internal static bool IsImplicitDataDefinitionInterface(ITypeSymbol @interface) - { - if (HasAttribute(@interface, ImplicitDataDefinitionNamespace)) - return true; - - foreach (var subInterface in @interface.AllInterfaces) - { - if (HasAttribute(subInterface, ImplicitDataDefinitionNamespace)) - return true; - } - - return false; - } - internal static IEnumerable GetImplicitDataDefinitionInterfaces(ITypeSymbol type, bool all) { var interfaces = all ? type.AllInterfaces : type.Interfaces; foreach (var @interface in interfaces) { - if (IsImplicitDataDefinitionInterface(@interface)) + if (IsImplicitDataDefinitionInterface(@interface) is { Definition: true }) yield return @interface.ToDisplayString(); } } @@ -130,6 +56,34 @@ internal static bool CanBeCopiedByValue(ISymbol member, ITypeSymbol type) if (type.OriginalDefinition.ToDisplayString() == "System.Nullable") return CanBeCopiedByValue(member, ((INamedTypeSymbol) type).TypeArguments[0]); + if (CanTypeBeCopiedByValue(type)) + return true; + + if (HasAttribute(member, CopyByRefNamespace)) + return true; + + return false; + } + + internal static bool CanTypeBeCopiedByValue(ITypeSymbol type) + { + return CanTypeBeCopiedByValue(type, []); + } + + private static bool CanTypeBeCopiedByValue(ITypeSymbol type, HashSet visited) + { + if (type.OriginalDefinition.ToDisplayString() == "System.Nullable") + return CanTypeBeCopiedByValue(((INamedTypeSymbol) type).TypeArguments[0], visited); + + if (HasAttribute(type, CopyByRefNamespace)) + return true; + + if (type is INamedTypeSymbol named && + HasAttribute(named.OriginalDefinition, CopyByRefNamespace)) + { + return true; + } + if (type.TypeKind == TypeKind.Enum) return true; @@ -149,15 +103,39 @@ internal static bool CanBeCopiedByValue(ISymbol member, ITypeSymbol type) case SpecialType.System_Decimal: case SpecialType.System_Single: case SpecialType.System_Double: + case SpecialType.System_IntPtr: + case SpecialType.System_UIntPtr: case SpecialType.System_String: case SpecialType.System_DateTime: return true; } - if (HasAttribute(member, CopyByRefNamespace)) + if (type is not INamedTypeSymbol { TypeKind: TypeKind.Struct } namedType) + return false; + + if (namedType.IsGenericType || namedType.IsUnboundGenericType) + return false; + + if (IsDataDefinition(namedType, out _) && + ImplementsInterface(namedType, SerializationHooksNamespace)) + { + return false; + } + + var typeName = namedType.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString(); + if (!visited.Add(typeName)) return true; - return false; + foreach (var member in namedType.GetMembers()) + { + if (member is not IFieldSymbol field || field.IsStatic) + continue; + + if (!CanTypeBeCopiedByValue(field.Type, visited)) + return false; + } + + return true; } internal static string GetGenericTypeName(ITypeSymbol symbol) @@ -223,28 +201,28 @@ internal static string GetPartialTypeDefinitionLine(ITypeSymbol symbol) return $"{access} {typeKeyword} {typeName}"; } - internal static bool Inherits(ITypeSymbol type, string parent) - { - foreach (var baseType in GetBaseTypes(type)) - { - if (baseType.ToDisplayString() == parent) - return true; - } - - return false; - } - - internal static bool ImplementsInterface(ITypeSymbol type, string interfaceName) + internal static bool ImplementsInterface(ITypeSymbol type, string interfaceName, List symbols) { + symbols.Clear(); foreach (var interfaceType in type.AllInterfaces) { if (interfaceType.ToDisplayString().Contains(interfaceName)) + symbols.Add(interfaceType); + + if (interfaceType.BaseType is { } baseInterface && + ImplementsInterface(baseInterface, interfaceName, symbols)) { return true; } } - return false; + return symbols.Count > 0; + } + + internal static bool ImplementsInterface(ITypeSymbol type, string interfaceName) + { + var symbols = new List(); + return ImplementsInterface(type, interfaceName, symbols); } internal static bool IsReadOnlyMember(ITypeSymbol type, ISymbol member) @@ -253,38 +231,62 @@ internal static bool IsReadOnlyMember(ITypeSymbol type, ISymbol member) { return field.IsReadOnly; } - else if (member is IPropertySymbol property) + + if (member is IPropertySymbol property) { if (property.SetMethod == null) return true; if (property.SetMethod.IsInitOnly) return type.IsReferenceType; - - return false; } return false; } - internal static bool NeedsEmptyConstructor(ITypeSymbol type) + internal static (bool NeedsEmpty, IMethodSymbol? MustCall) NeedsEmptyConstructor(ITypeSymbol type) { if (type is not INamedTypeSymbol named) - return false; + return (false, null); - if (named.InstanceConstructors.Length == 0) - return true; + if (named.InstanceConstructors.Length == 0 || + named.InstanceConstructors.All(c => c.IsImplicitlyDeclared)) + { + return (true, null); + } + var needsEmpty = true; + IMethodSymbol? mustCall = null; foreach (var constructor in named.InstanceConstructors) { - if (constructor.Parameters.Length == 0 && - !constructor.IsImplicitlyDeclared) + if (constructor.IsImplicitlyDeclared) + continue; + + if (constructor.Parameters.Length == 0) + needsEmpty = false; + + if (mustCall != null) + continue; + + // Is there a better way to find a primary constructor? I don't know! The docs don't tell you! + // Neither does Google, because all the results are useless SEO-optimized AI garbage! + // I don't think you can even access the underlying symbol directly! Hooray! + // So we get the syntax's nodes and find out + foreach (var syntax in constructor.DeclaringSyntaxReferences) { - return false; + var nodes = syntax.GetSyntax().DescendantNodesAndSelf().ToArray(); + if (nodes.Length == 0 || nodes[0] is not TypeDeclarationSyntax) + continue; + + if (nodes.Any(n => n is ParameterListSyntax)) + { + mustCall = constructor; + break; + } } } - return true; + return (needsEmpty, mustCall); } internal static bool IsVirtualClass(ITypeSymbol type) @@ -327,4 +329,139 @@ internal static IEnumerable GetBaseTypes(ITypeSymbol type) baseType = baseType.BaseType; } } + + internal static (string Flat, string NonNullable) GetCleanNameForGenericType(ITypeSymbol type, out bool isNullableValueType) + { + var typeName = type.ToDisplayString(); + if (IsMultidimensionalArray(type)) + typeName = typeName.Replace("*", ""); + + isNullableValueType = IsNullableValueType(type); + var nonNullableTypeName = type.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString(); + if (isNullableValueType) + nonNullableTypeName = typeName.Substring(0, typeName.Length - 1); + + return (typeName, nonNullableTypeName); + } + + internal static string GetNonNullableNameForGenericParameter(ITypeSymbol type) + { + var typeName = type.ToDisplayString(); + if (IsMultidimensionalArray(type)) + typeName = typeName.Replace("*", ""); + + if (typeName.EndsWith("?")) + typeName = typeName.Substring(0, typeName.Length - 1); + + return typeName; + } + + internal static IEnumerable GetRequiredFieldsProperties(ITypeSymbol type) + { + foreach (var member in type.GetMembers()) + { + if (member.IsImplicitlyDeclared) + continue; + + if (member is not IFieldSymbol { IsRequired: true } && + member is not IPropertySymbol { IsRequired: true }) + { + continue; + } + + yield return member; + } + } + + internal static string GetRequiredFieldsPropertiesAssigners(ITypeSymbol type, string accessor) + { + var requiredFields = new StringBuilder(); + foreach (var member in GetRequiredFieldsProperties(type)) + { + // Yes you can just set the field to itself to bypass the compiler's required check + // I don't know man + // Old dynamic method serialization did not change their values + // So we just do this + requiredFields.AppendLine($"{member.Name} = {accessor}{member.Name}!,"); + } + + if (requiredFields.Length > 0) + { + requiredFields.Insert(0, '{'); + requiredFields.Append('}'); + } + + return requiredFields.ToString(); + } + + internal static string GetSetsRequiredAttributeOrEmpty(ITypeSymbol type) + { + var setsRequired = string.Empty; + if (GetRequiredFieldsProperties(type).Any()) + setsRequired = "[SetsRequiredMembers]"; + + return setsRequired; + } + + internal static IEnumerable<(ISymbol Field, ITypeSymbol Type, DataFieldAttribute Attribute)> GetAllDataFields(ITypeSymbol? definition, bool isDataRecord) + { + while (definition != null) + { + foreach (var member in definition.GetMembers()) + { + if (member is not IFieldSymbol && member is not IPropertySymbol) + continue; + + if (member.IsStatic) + continue; + + if (member is IPropertySymbol dataRecordProperty && isDataRecord) + { + var backingField = definition.GetMembers() + .OfType() + .FirstOrDefault(field => + field.IsImplicitlyDeclared && + (SymbolEqualityComparer.Default.Equals(field.AssociatedSymbol, dataRecordProperty) || + field.Name == $"<{dataRecordProperty.Name}>k__BackingField")); + + if (backingField != null && + IsDataField(backingField.GetAttributes(), + dataRecordProperty.Name, + false, + false, + true, + true, + out var backingAttribute, + out _)) + { + yield return (dataRecordProperty, dataRecordProperty.Type, backingAttribute!); + continue; + } + } + + if (!IsDataField(member, isDataRecord, out var type, out var attribute)) + continue; + + var outputMember = member; + if (member is IFieldSymbol { IsImplicitlyDeclared: true } implicitField) + { + var property = implicitField.AssociatedSymbol as IPropertySymbol ?? + definition.GetMembers() + .OfType() + .FirstOrDefault(propertySymbol => + implicitField.Name == $"<{propertySymbol.Name}>k__BackingField"); + + if (property != null) + { + outputMember = property; + type = property.Type; + } + } + + yield return (outputMember, type, attribute); + } + + definition = definition.BaseType; + } + } } diff --git a/Robust.Server.IntegrationTests/GameObjects/Components/Container_Test.cs b/Robust.Server.IntegrationTests/GameObjects/Components/Container_Test.cs index 87f39f7a05c..175551d45d5 100644 --- a/Robust.Server.IntegrationTests/GameObjects/Components/Container_Test.cs +++ b/Robust.Server.IntegrationTests/GameObjects/Components/Container_Test.cs @@ -182,9 +182,11 @@ public void BaseContainer_InsertMap_False() public void BaseContainer_InsertGrid_False() { var sim = SimulationFactory(); - var containerSys = sim.Resolve().GetEntitySystem(); + var entMan = sim.Resolve(); + var mapSys = entMan.System(); + var containerSys = entMan.System(); - var grid = sim.Resolve().CreateGridEntity(new MapId(1)).Owner; + var grid = mapSys.CreateGridEntity(new MapId(1)).Owner; var entity = sim.SpawnEntity(null,_coords); var container = containerSys.MakeContainer(entity, "dummy"); diff --git a/Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs b/Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs index 95b857bb95e..dc28c8ca57e 100644 --- a/Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs +++ b/Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs @@ -19,7 +19,6 @@ internal sealed class Transform_Test : RobustUnitTest public override UnitTestProject Project => UnitTestProject.Server; private IEntityManager EntityManager = default!; - private IMapManager MapManager = default!; private SharedTransformSystem XformSystem => EntityManager.System(); const string Prototypes = @" @@ -47,7 +46,6 @@ public void Setup() IoCManager.Resolve().GenerateNetIds(); EntityManager = IoCManager.Resolve(); - MapManager = IoCManager.Resolve(); IoCManager.Resolve().Initialize(); var manager = IoCManager.Resolve(); @@ -60,8 +58,8 @@ public void Setup() mapSys.CreateMap(out MapA); mapSys.CreateMap(out MapB); - GridA = MapManager.CreateGridEntity(MapA); - GridB = MapManager.CreateGridEntity(MapB); + GridA = mapSys.CreateGridEntity(MapA); + GridB = mapSys.CreateGridEntity(MapB); //NOTE: The grids have not moved, so we can assert worldpos == localpos for the test } @@ -192,7 +190,7 @@ public void ParentRotateTest() XformSystem.SetParent(child, childTrans, parent, parentXform: parentTrans); //Act - parentTrans.LocalRotation = new Angle(MathHelper.Pi / 2); + XformSystem.SetLocalRotationNoLerp(parent, new Angle(MathHelper.Pi / 2), parentTrans); //Assert var result = XformSystem.GetWorldPosition(childTrans); @@ -219,7 +217,7 @@ public void ParentTransRotateTest() XformSystem.SetParent(child, childTrans, parent, parentXform: parentTrans); //Act - parentTrans.LocalRotation = new Angle(MathHelper.Pi / 2); + XformSystem.SetLocalRotationNoLerp(parent, new Angle(MathHelper.Pi / 2), parentTrans); //Assert var result = XformSystem.GetWorldPosition(childTrans); @@ -257,7 +255,7 @@ public void PositionCompositionTest() XformSystem.SetParent(node4, node4Trans, node3, parentXform: node3Trans); //Act - node1Trans.LocalRotation = new Angle(MathHelper.Pi / 2); + XformSystem.SetLocalRotationNoLerp(node1, new Angle(MathHelper.Pi / 2), node1Trans); //Assert var result = XformSystem.GetWorldPosition(node4Trans); @@ -339,11 +337,12 @@ public void ParentRotationRoundingErrorTest() // Act var oldWpos = XformSystem.GetWorldPosition(node3Trans); + var angle180 = new Angle(MathHelper.Pi); for (var i = 0; i < 100; i++) { - node1Trans.LocalRotation += new Angle(MathHelper.Pi); - node2Trans.LocalRotation += new Angle(MathHelper.Pi); - node3Trans.LocalRotation += new Angle(MathHelper.Pi); + XformSystem.SetLocalRotationNoLerp(node1, node1Trans.LocalRotation + angle180, node1Trans); + XformSystem.SetLocalRotationNoLerp(node2, node2Trans.LocalRotation + angle180, node2Trans); + XformSystem.SetLocalRotationNoLerp(node3, node3Trans.LocalRotation + angle180, node3Trans); } var newWpos = XformSystem.GetWorldPosition(node3Trans); @@ -388,7 +387,7 @@ public void TreeComposeWorldMatricesTest() XformSystem.SetParent(node4, node4Trans, node3, parentXform: node3Trans); //Act - node1Trans.LocalRotation = new Angle(MathHelper.Pi / 6.37); + XformSystem.SetLocalRotation(node1, new Angle(MathHelper.Pi / 6.37), node1Trans); XformSystem.SetWorldPosition(node1, new Vector2(1, 1)); var worldMat = XformSystem.GetWorldMatrix(node4Trans); @@ -427,18 +426,33 @@ public void WorldRotationTest() XformSystem.SetParent(node2, node2Trans, node1, parentXform: node1Trans); XformSystem.SetParent(node3, node3Trans, node2, parentXform: node2Trans); - node1Trans.LocalRotation = Angle.FromDegrees(0); - node2Trans.LocalRotation = Angle.FromDegrees(45); - node3Trans.LocalRotation = Angle.FromDegrees(45); + XformSystem.SetLocalRotationNoLerp(node1, Angle.Zero, node1Trans); + XformSystem.SetLocalRotationNoLerp(node2, Angle.FromDegrees(45), node2Trans); + XformSystem.SetLocalRotationNoLerp(node3, Angle.FromDegrees(45), node3Trans); // Act - node1Trans.LocalRotation = Angle.FromDegrees(135); + XformSystem.SetLocalRotationNoLerp(node1, Angle.FromDegrees(135), node1Trans); // Assert (135 + 45 + 45 = 225) var result = XformSystem.GetWorldRotation(node3Trans); Assert.That(result, new ApproxEqualityConstraint(Angle.FromDegrees(225))); } + [Test] + public void LocalRotationNormalizesTest() + { + var entity = EntityManager.SpawnEntity(null, InitialPos); + var transform = EntityManager.GetComponent(entity); + + XformSystem.SetLocalRotation(entity, Angle.FromDegrees(90), transform); + + Assert.That(transform.LocalRotation, NUnit.Framework.Is.EqualTo(Angle.FromDegrees(90))); + + XformSystem.SetWorldRotation(transform, Angle.FromDegrees(810)); + + Assert.That(transform.LocalRotation, NUnit.Framework.Is.EqualTo(Angle.FromDegrees(90))); + } + /// /// Test that, in a chain A -> B -> C, if A is moved C's world position correctly updates. /// diff --git a/Robust.Server.IntegrationTests/GameStates/DefaultEntityTest.cs b/Robust.Server.IntegrationTests/GameStates/DefaultEntityTest.cs index 60f2a63eee5..92e454506f6 100644 --- a/Robust.Server.IntegrationTests/GameStates/DefaultEntityTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/DefaultEntityTest.cs @@ -6,7 +6,6 @@ using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.Map; -using Robust.Shared.Network; namespace Robust.UnitTesting.Server.GameStates; @@ -19,35 +18,22 @@ public sealed class DefaultEntityTest : RobustIntegrationTest [Test] public async Task TestSpawnDefaultEntity() { - var server = StartServer(); - var client = StartClient(); - - await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); + await using var pair = await StartConnectedPair(); + var (client, server) = pair; var sEntMan = server.ResolveDependency(); var cEntMan = client.ResolveDependency(); - var netMan = client.ResolveDependency(); var playerMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); - client.SetConnectTarget(server); - client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => confMan.SetCVar(CVars.NetPVS, false)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); var session = playerMan.Sessions.First(); await server.WaitPost(() => playerMan.JoinGame(session)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Spawn a default unmodified entity. NetEntity ent = default; @@ -56,11 +42,7 @@ await server.WaitPost(() => ent = sEntMan.GetNetEntity(sEntMan.Spawn()); }); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Check that server & client both think the entity exists. Assert.That(sEntMan.EntityExists(sEntMan.GetEntity(ent))); @@ -81,11 +63,7 @@ await server.WaitPost(() => server.PlayerMan.SetAttachedEntity(session, playerUid); }); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); Assert.That(sEntMan.EntityExists(sEntMan.GetEntity(player))); Assert.That(cEntMan.EntityExists(cEntMan.GetEntity(player))); @@ -95,18 +73,11 @@ await server.WaitPost(() => ent = sEntMan.GetNetEntity(sEntMan.SpawnAtPosition(null, coords)); }); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); Assert.That(sEntMan.EntityExists(sEntMan.GetEntity(ent))); Assert.That(cEntMan.EntityExists(cEntMan.GetEntity(ent))); - await client.WaitPost(() => netMan.ClientDisconnect("")); - await server.WaitRunTicks(5); - await client.WaitRunTicks(5); } } diff --git a/Robust.Server.IntegrationTests/GameStates/DetachedParentTest.cs b/Robust.Server.IntegrationTests/GameStates/DetachedParentTest.cs index 4ea2a2dd9e8..4ca55516e0c 100644 --- a/Robust.Server.IntegrationTests/GameStates/DetachedParentTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/DetachedParentTest.cs @@ -7,7 +7,6 @@ using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; -using Robust.Shared.Network; using Robust.Shared.Player; namespace Robust.UnitTesting.Server.GameStates; @@ -22,28 +21,17 @@ public sealed class DetachedParentTest : RobustIntegrationTest [Test] public async Task TestDetachedParent() { - var server = StartServer(new() {Pool = false}); - var client = StartClient(new() {Pool = false}); - - await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); + await using var pair = await StartConnectedPair(new() {Pool = false}, new() {Pool = false}); + var (client, server) = pair; var mapSys = server.System(); var xformSys = server.System(); - var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); var sPlayerMan = server.ResolveDependency(); - var netMan = client.ResolveDependency(); - - Assert.DoesNotThrow(() => client.SetConnectTarget(server)); - client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => confMan.SetCVar(CVars.NetPVS, true)); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Ensure client & server ticks are synced. // Client runs 1 tick ahead @@ -83,7 +71,7 @@ await server.WaitPost(() => map = mapSys.CreateMap(out mapId); - var gridEnt = mapMan.CreateGridEntity(mapId); + var gridEnt = mapSys.CreateGridEntity(mapId); mapSys.SetTile(gridEnt.Owner, gridEnt.Comp, Vector2i.Zero, new Tile(1)); gridCoords = new EntityCoordinates(gridEnt, .5f, .5f); mapCoords = new EntityCoordinates(map, 200, 200); @@ -99,11 +87,7 @@ await server.WaitPost(() => sPlayerMan.JoinGame(session); }); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Check that transforms are as expected. var childX = server.Transform(child); @@ -160,11 +144,7 @@ await server.WaitPost(() => // Move the player into pvs range of the child, which will move them outside of the grid & parent's PVS range. await server.WaitPost(() => xformSys.SetCoordinates(player, mapCoords)); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // the client now knows about the child. cChild = client.EntMan.GetEntity(server.EntMan.GetNetEntity(child)); @@ -192,11 +172,7 @@ await server.WaitPost(() => await server.WaitPost(() => xformSys.SetCoordinates(player, parentCoords)); await server.WaitPost(() => xformSys.SetCoordinates(child, parentCoords)); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Check that server-side transforms are as expected Assert.That(childX.ParentUid, Is.EqualTo(parent)); @@ -227,11 +203,7 @@ await server.WaitPost(() => await server.WaitPost(() => xformSys.SetCoordinates(player, mapCoords)); await server.WaitPost(() => xformSys.SetCoordinates(child, mapCoords)); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Child transform has updated. Assert.That(childX.MapID, Is.EqualTo(mapId)); @@ -253,11 +225,7 @@ await server.WaitPost(() => EntityUid parent2 = default; await server.WaitPost(() => parent2 = sEntMan.SpawnEntity(null, gridCoords)); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); var parent2X = server.Transform(parent2); Assert.That(parent2X.MapID, Is.EqualTo(mapId)); @@ -273,11 +241,7 @@ await server.WaitPost(() => await server.WaitPost(() => xformSys.SetCoordinates(player, parent2Coords)); await server.WaitPost(() => xformSys.SetCoordinates(child, parent2Coords)); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Check all the transforms cParent2 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(parent2)); @@ -303,18 +267,14 @@ await server.WaitPost(() => await server.WaitPost(() => { map2 = mapSys.CreateMap(out mapId2); - var gridEnt = mapMan.CreateGridEntity(mapId2); + var gridEnt = mapSys.CreateGridEntity(mapId2); mapSys.SetTile(gridEnt.Owner, gridEnt.Comp, Vector2i.Zero, new Tile(1)); var grid2Coords = new EntityCoordinates(gridEnt, .5f, .5f); grid2 = gridEnt.Owner; parent3 = sEntMan.SpawnEntity(null, grid2Coords); }); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Check server-side transforms var grid2X = server.Transform(grid2); @@ -342,11 +302,7 @@ await server.WaitPost(() => await server.WaitPost(() => xformSys.SetCoordinates(player, parent3Coords)); await server.WaitPost(() => xformSys.SetCoordinates(child, parent3Coords)); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Check all the transforms cParent3 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(parent3)); @@ -384,7 +340,7 @@ await server.WaitPost(() => await server.WaitPost(() => { map3 = mapSys.CreateMap(out mapId3); - var gridEnt = mapMan.CreateGridEntity(mapId3); + var gridEnt = mapSys.CreateGridEntity(mapId3); mapSys.SetTile(gridEnt.Owner, gridEnt.Comp, Vector2i.Zero, new Tile(1)); var grid3Coords = new EntityCoordinates(gridEnt, .5f, .5f); grid3 = gridEnt.Owner; @@ -400,11 +356,7 @@ await server.WaitPost(() => }); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Check all the transforms var cParent4 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(parent4)); @@ -442,9 +394,6 @@ await server.WaitPost(() => Assert.That(cParent4X.MapUid, Is.EqualTo(cMap3)); Assert.That(cGrid3X.MapUid, Is.EqualTo(cMap3)); - await client.WaitPost(() => netMan.ClientDisconnect("")); - await server.WaitRunTicks(5); - await client.WaitRunTicks(5); } } diff --git a/Robust.Server.IntegrationTests/GameStates/MissingParentTest.cs b/Robust.Server.IntegrationTests/GameStates/MissingParentTest.cs index 470ee32d37e..afaf366fc5e 100644 --- a/Robust.Server.IntegrationTests/GameStates/MissingParentTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/MissingParentTest.cs @@ -5,7 +5,6 @@ using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.Map; -using Robust.Shared.Network; using Robust.Shared.Player; namespace Robust.UnitTesting.Server.GameStates; @@ -18,39 +17,25 @@ public sealed class MissingParentTest : RobustIntegrationTest [Test] public async Task TestMissingParent() { - var server = StartServer(); - var client = StartClient(); + await using var pair = await StartConnectedPair(); + var (client, server) = pair; - await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); - - var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); var sPlayerMan = server.ResolveDependency(); var cEntMan = client.ResolveDependency(); - var netMan = client.ResolveDependency(); var cPlayerMan = client.ResolveDependency(); var cConfMan = client.ResolveDependency(); - Assert.DoesNotThrow(() => client.SetConnectTarget(server)); - client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => confMan.SetCVar(CVars.NetPVS, true)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Limit client to receiving at most 1 entity per tick. cConfMan.SetCVar(CVars.NetPVSEntityBudget, 1); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Ensure client & server ticks are synced. // Client runs 1 tick ahead @@ -92,11 +77,7 @@ await server.WaitPost(() => sPlayerMan.JoinGame(session); }); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); Assert.That(player, Is.Not.EqualTo(NetEntity.Invalid)); Assert.That(entity, Is.Not.EqualTo(NetEntity.Invalid)); @@ -124,11 +105,7 @@ await server.WaitPost(() => }); // Wait for the client to receive some, but not all, of the entities - for (int i = 0; i < 8; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 8); Assert.That(cEntMan.TryGetEntity(first, out _), Is.True); Assert.That(cEntMan.TryGetEntity(last, out _), Is.False); @@ -140,11 +117,7 @@ await server.WaitPost(() => }); // Wait a few more ticks - for (int i = 0; i < 8; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 8); // Client should still not have received the new parent, however this shouldn't cause any issues. // The already known entity should just have been moved to nullspace. @@ -153,11 +126,7 @@ await server.WaitPost(() => Assert.That(client.MetaData(entity).Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.None)); // Wait untill the client receives the parent entity - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // now that the parent was received the entity should no longer be in nullspace. Assert.That(cEntMan.TryGetEntity(last, out var newParent), Is.True); @@ -165,9 +134,6 @@ await server.WaitPost(() => Assert.That(client.Transform(entity).ParentUid, Is.EqualTo(newParent)); Assert.That(client.MetaData(entity).Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.None)); - await client.WaitPost(() => netMan.ClientDisconnect("")); - await server.WaitRunTicks(5); - await client.WaitRunTicks(5); } } diff --git a/Robust.Server.IntegrationTests/GameStates/PvsChunkTest.cs b/Robust.Server.IntegrationTests/GameStates/PvsChunkTest.cs index 6e7cb201f78..e4e92f9e779 100644 --- a/Robust.Server.IntegrationTests/GameStates/PvsChunkTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/PvsChunkTest.cs @@ -7,7 +7,6 @@ using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; -using Robust.Shared.Network; using Robust.Shared.Player; namespace Robust.UnitTesting.Server.GameStates; @@ -17,12 +16,9 @@ public sealed class PvsChunkTest : RobustIntegrationTest [Test] public async Task TestGridMapChange() { - var server = StartServer(); - var client = StartClient(); + await using var pair = await StartConnectedPair(); + var (client, server) = pair; - await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); - - var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); var sPlayerMan = server.ResolveDependency(); @@ -30,17 +26,10 @@ public async Task TestGridMapChange() var mapSys = sEntMan.System(); var cEntMan = client.ResolveDependency(); - var netMan = client.ResolveDependency(); - Assert.DoesNotThrow(() => client.SetConnectTarget(server)); - client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => confMan.SetCVar(CVars.NetPVS, true)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Ensure client & server ticks are synced. // Client runs 1 tick ahead @@ -73,7 +62,7 @@ await server.WaitPost(() => mapCoords = new(map1, default); map2 = server.System().CreateMap(); - var gridComp = mapMan.CreateGridEntity(map2); + var gridComp = mapSys.CreateGridEntity(map2); grid = gridComp.Owner; mapSys.SetTile(grid, gridComp, Vector2i.Zero, new Tile(1)); var gridCoords = new EntityCoordinates(grid, .5f, .5f); @@ -87,11 +76,7 @@ await server.WaitPost(() => sPlayerMan.JoinGame(session); }); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); var nEntity = sEntMan.GetNetEntity(entity); var nGrid = sEntMan.GetNetEntity(grid); @@ -110,11 +95,7 @@ await server.WaitPost(() => // Teleport grid to new map await server.WaitPost(() => xforms.SetCoordinates(grid, mapCoords)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); Assert.That(xform.ParentUid, Is.EqualTo(grid)); Assert.That(xform.GridUid, Is.EqualTo(grid)); @@ -127,11 +108,7 @@ await server.WaitPost(() => // Delete the original map. await server.WaitPost(() => sEntMan.DeleteEntity(map2)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); Assert.That(xform.ParentUid, Is.EqualTo(grid)); Assert.That(xform.GridUid, Is.EqualTo(grid)); @@ -142,9 +119,6 @@ await server.WaitPost(() => Assert.That(!cEntMan.TryGetEntity(nMap2, out _)); Assert.That(cEntMan.TryGetEntity(nGrid, out _)); - await client.WaitPost(() => netMan.ClientDisconnect("")); - await server.WaitRunTicks(5); - await client.WaitRunTicks(5); } } diff --git a/Robust.Server.IntegrationTests/GameStates/PvsPauseTest.cs b/Robust.Server.IntegrationTests/GameStates/PvsPauseTest.cs index 12e4d6759ad..3ba8f3a214a 100644 --- a/Robust.Server.IntegrationTests/GameStates/PvsPauseTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/PvsPauseTest.cs @@ -7,7 +7,6 @@ using Robust.Shared; using Robust.Shared.GameObjects; using Robust.Shared.Map; -using Robust.Shared.Network; namespace Robust.UnitTesting.Server.GameStates; @@ -19,10 +18,8 @@ public sealed class PvsPauseTest : RobustIntegrationTest [Test] public async Task PauseTest() { - var server = StartServer(); - var client = StartClient(); - - await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); + await using var pair = await StartConnectedPair(); + var (client, server) = pair; var sEntMan = server.EntMan; var confMan = server.CfgMan; @@ -33,19 +30,12 @@ public async Task PauseTest() var cEntMan = client.EntMan; var cPlayerMan = client.PlayerMan; - var netMan = client.ResolveDependency(); - Assert.DoesNotThrow(() => client.SetConnectTarget(server)); - client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => confMan.SetCVar(CVars.NetPVS, true)); async Task RunTicks() { - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); } await RunTicks(); @@ -166,8 +156,6 @@ void AssertEnt(bool paused, bool detached, bool clientPaused) AssertEnt(paused: true, detached: false, clientPaused: true); } - client.Post(() => netMan.ClientDisconnect("")); - await RunTicks(); } } diff --git a/Robust.Server.IntegrationTests/GameStates/PvsReEntryTest.cs b/Robust.Server.IntegrationTests/GameStates/PvsReEntryTest.cs index 9ab6e1fa025..6344a734db0 100644 --- a/Robust.Server.IntegrationTests/GameStates/PvsReEntryTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/PvsReEntryTest.cs @@ -8,7 +8,6 @@ using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.Map; -using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Timing; @@ -23,12 +22,9 @@ public sealed class PvsReEntryTest : RobustIntegrationTest [Test] public async Task TestLossyReEntry() { - var server = StartServer(); - var client = StartClient(); + await using var pair = await StartConnectedPair(); + var (client, server) = pair; - await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); - - var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); var sPlayerMan = server.ResolveDependency(); @@ -36,18 +32,11 @@ public async Task TestLossyReEntry() var stateMan = (ClientGameStateManager) client.ResolveDependency(); var cEntMan = client.ResolveDependency(); - var netMan = client.ResolveDependency(); var cPlayerMan = client.ResolveDependency(); - Assert.DoesNotThrow(() => client.SetConnectTarget(server)); - client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => confMan.SetCVar(CVars.NetPVS, true)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Ensure client & server ticks are synced. // Client runs 1 tick ahead @@ -88,11 +77,7 @@ await server.WaitPost(() => sPlayerMan.JoinGame(session); }); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); Assert.That(player, Is.Not.EqualTo(NetEntity.Invalid)); Assert.That(entity, Is.Not.EqualTo(NetEntity.Invalid)); @@ -115,11 +100,7 @@ await client.WaitPost(() => // note that we move the PLAYER not the entity, as we don't want to dirty the entity. var farAway = new EntityCoordinates(map, new Vector2(100, 100)); await server.WaitPost( () => xforms.SetCoordinates(sEntMan.GetEntity(player), farAway)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Client should have detached the entity to null space. Assert.That(meta!.Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.Detached)); @@ -127,11 +108,7 @@ await client.WaitPost(() => // Move the player back into range await server.WaitPost( () => xforms.SetCoordinates(sEntMan.GetEntity(player), coords)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Entity is back in pvs range Assert.That(meta!.Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.None)); @@ -142,11 +119,7 @@ await client.WaitPost(() => var timing = client.ResolveDependency(); var lastRealTick = timing.LastRealTick; - for (int i = 0; i < 5; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 5); // Even though the client is receiving no new states, it will still have applied some from the state buffer. Assert.That(timing.LastRealTick, Is.GreaterThan(lastRealTick)); @@ -156,11 +129,7 @@ await client.WaitPost(() => Assert.That(meta!.Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.None)); await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), farAway)); - for (int i = 0; i < 5; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 5); // Client should have exhausted the buffer -- client has not been applying any states. Assert.That(timing.LastRealTick, Is.EqualTo(lastRealTick)); @@ -173,11 +142,7 @@ await client.WaitPost(() => // Move the entity back into range await server.WaitPost( () => xforms.SetCoordinates(sEntMan.GetEntity(player), coords)); - for (int i = 0; i < 5; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 5); // Still hasn't been applying states. Assert.That(timing.LastRealTick, Is.EqualTo(lastRealTick)); @@ -189,11 +154,7 @@ await client.WaitPost(() => // Client clears the tunnel, starts receiving states again. stateMan.DropStates = false; - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Entity should be in PVS range, client should know about it: Assert.That(timing.LastRealTick, Is.GreaterThan(lastRealTick)); @@ -204,9 +165,6 @@ await client.WaitPost(() => // If the test moves the entity instead of the player, then the test doesn't actually work. Assert.That(meta.LastModifiedTick, Is.EqualTo(lastDirty)); - await client.WaitPost(() => netMan.ClientDisconnect("")); - await server.WaitRunTicks(5); - await client.WaitRunTicks(5); } #endif } diff --git a/Robust.Server.IntegrationTests/GameStates/PvsResetTest.cs b/Robust.Server.IntegrationTests/GameStates/PvsResetTest.cs index 868994e6e46..f7e458632bc 100644 --- a/Robust.Server.IntegrationTests/GameStates/PvsResetTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/PvsResetTest.cs @@ -5,7 +5,6 @@ using Robust.Shared; using Robust.Shared.GameObjects; using Robust.Shared.Map; -using Robust.Shared.Network; namespace Robust.UnitTesting.Server.GameStates; @@ -17,10 +16,8 @@ public sealed class PvsResetTest : RobustIntegrationTest [Test] public async Task ResetTest() { - var server = StartServer(); - var client = StartClient(); - - await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); + await using var pair = await StartConnectedPair(); + var (client, server) = pair; var sEntMan = server.EntMan; var confMan = server.CfgMan; @@ -29,22 +26,10 @@ public async Task ResetTest() var cEntMan = client.EntMan; var cPlayerMan = client.PlayerMan; - var netMan = client.ResolveDependency(); - Assert.DoesNotThrow(() => client.SetConnectTarget(server)); - client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => confMan.SetCVar(CVars.NetPVS, true)); - async Task RunTicks() - { - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } - } - - await RunTicks(); + await RunTicksSync(server, client, 10); // Set up map and spawn player EntityUid sMap = default; @@ -64,7 +49,7 @@ await server.WaitPost(() => sPlayerMan.JoinGame(session); }); - await RunTicks(); + await RunTicksSync(server, client, 10); var farAway = new EntityCoordinates(sMap, new Vector2(100, 100)); var netEnt = sEntMan.GetNetEntity(sEnt); var player = sEntMan.GetNetEntity(playerUid); @@ -106,29 +91,26 @@ void AssertDetached(bool detached) // Move the player out of the entity's PVS range await server.WaitPost(() => xforms.SetCoordinates(playerUid, farAway)); - await RunTicks(); + await RunTicksSync(server, client, 10); // Client should now have detached the entity, moving it into nullspace AssertDetached(true); // Marking the entity as dirty due to client-side prediction should have effect await client.WaitPost(() => client.EntMan.Dirty(cEnt, client.Transform(cEnt))); - await RunTicks(); + await RunTicksSync(server, client, 10); AssertDetached(true); // Move the player back into range await server.WaitPost( () => xforms.SetCoordinates(playerUid, coords)); - await RunTicks(); + await RunTicksSync(server, client, 10); AssertDetached(false); // Marking the entity as dirty due to client-side prediction should have no real effect await client.WaitPost(() => client.EntMan.Dirty(cEnt, client.Transform(cEnt))); - await RunTicks(); + await RunTicksSync(server, client, 10); AssertDetached(false); - await client.WaitPost(() => netMan.ClientDisconnect("")); - await server.WaitRunTicks(5); - await client.WaitRunTicks(5); } } diff --git a/Robust.Server.IntegrationTests/GameStates/PvsSystemTests.cs b/Robust.Server.IntegrationTests/GameStates/PvsSystemTests.cs index 6e4dc30273e..d592ab07821 100644 --- a/Robust.Server.IntegrationTests/GameStates/PvsSystemTests.cs +++ b/Robust.Server.IntegrationTests/GameStates/PvsSystemTests.cs @@ -7,7 +7,6 @@ using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; -using Robust.Shared.Network; using Robust.Shared.Player; namespace Robust.UnitTesting.Server.GameStates; @@ -20,12 +19,9 @@ public sealed class PvsSystemTests : RobustIntegrationTest [Test] public async Task TestMultipleIndexChange() { - var server = StartServer(); - var client = StartClient(); + await using var pair = await StartConnectedPair(); + var (client, server) = pair; - await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); - - var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); var sPlayerMan = server.ResolveDependency(); @@ -33,18 +29,11 @@ public async Task TestMultipleIndexChange() var maps = sEntMan.System(); var cEntMan = client.ResolveDependency(); - var netMan = client.ResolveDependency(); var cPlayerMan = client.ResolveDependency(); - Assert.DoesNotThrow(() => client.SetConnectTarget(server)); - client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => confMan.SetCVar(CVars.NetPVS, true)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Set up map and grid EntityUid grid = default; @@ -52,7 +41,7 @@ public async Task TestMultipleIndexChange() await server.WaitPost(() => { map = server.System().CreateMap(out var mapId); - var gridComp = mapMan.CreateGridEntity(mapId); + var gridComp = maps.CreateGridEntity(mapId); maps.SetTile(gridComp, Vector2i.Zero, new Tile(1)); grid = gridComp.Owner; }); @@ -78,11 +67,7 @@ await server.WaitPost(() => sPlayerMan.JoinGame(session); }); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Check player got properly attached await client.WaitPost(() => @@ -96,35 +81,20 @@ await client.WaitPost(() => xforms.SetCoordinates(other, otherXform, gridCoords); // Run for a few ticks. The test just checks that no PVS asserts/errors happen. - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Repeat but in the opposite direction ( map -> grid -> map ) // first move to map and wait a bit. xforms.SetCoordinates(other, otherXform, mapCoords); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Move to and off grid in the same tick xforms.SetCoordinates(other, otherXform, gridCoords); xforms.SetCoordinates(other, otherXform, mapCoords); // wait for errors. - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); - await client.WaitPost(() => netMan.ClientDisconnect("")); - await server.WaitRunTicks(5); - await client.WaitRunTicks(5); } } diff --git a/Robust.Server.Testing/RobustServerSimulation.cs b/Robust.Server.Testing/RobustServerSimulation.cs index 66eb92d861d..5b6591eabd4 100644 --- a/Robust.Server.Testing/RobustServerSimulation.cs +++ b/Robust.Server.Testing/RobustServerSimulation.cs @@ -248,9 +248,6 @@ public ISimulation InitializeInstance() container.Register(); container.Register(); container.Register(); - container.Register(); - container.Register(); - container.Register(); container.Register(); container.Register(); container.Register(); @@ -341,11 +338,7 @@ public ISimulation InitializeInstance() _systemDelegate?.Invoke(entitySystemMan); - var mapManager = container.Resolve(); - mapManager.Initialize(); - entityMan.Startup(); - mapManager.Startup(); container.Resolve().Initialize(true); container.Resolve().Initialize(); diff --git a/Robust.Server/BaseServer.cs b/Robust.Server/BaseServer.cs index 7275de37265..65c1d2b221e 100644 --- a/Robust.Server/BaseServer.cs +++ b/Robust.Server/BaseServer.cs @@ -80,7 +80,6 @@ internal sealed partial class BaseServer : IBaseServerInternal, IPostInjectInit [Dependency] private IRobustSerializer _serializer = default!; [Dependency] private IGameTiming _time = default!; [Dependency] private IResourceManagerInternal _resources = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private ITimerManager _timerManager = default!; [Dependency] private IServerGameStateManager _stateManager = default!; [Dependency] private IServerNetManager _network = default!; @@ -379,7 +378,6 @@ public bool Start(ServerOptions options, Func? logHandlerFactory = _log.GetSawmill("res")); _entityManager.Initialize(); - _mapManager.Initialize(); _serialization.Initialize(); @@ -397,7 +395,6 @@ public bool Start(ServerOptions options, Func? logHandlerFactory = IoCManager.Resolve().Initialize(); _consoleHost.Initialize(); _entityManager.Startup(); - _mapManager.Startup(); _stateManager.Initialize(); _replay.Initialize(); diff --git a/Robust.Server/Containers/ContainerSystem.cs b/Robust.Server/Containers/ContainerSystem.cs index d298fc2b481..4122bd7369d 100644 --- a/Robust.Server/Containers/ContainerSystem.cs +++ b/Robust.Server/Containers/ContainerSystem.cs @@ -1,15 +1,13 @@ using Robust.Shared.Containers; using Robust.Shared.GameObjects; -using Robust.Shared.Log; -namespace Robust.Server.Containers +namespace Robust.Server.Containers; + +public sealed class ContainerSystem : SharedContainerSystem { - public sealed class ContainerSystem : SharedContainerSystem + protected override void ValidateMissingEntity(EntityUid uid, BaseContainer cont, EntityUid missing) { - protected override void ValidateMissingEntity(EntityUid uid, BaseContainer cont, EntityUid missing) - { - Log.Error($"Missing entity for container {ToPrettyString(uid)}. Missing uid: {missing}"); - //cont.InternalRemove(ent); - } + Log.Error($"Missing entity for container {ToPrettyString(uid)}. Missing uid: {missing}"); + //cont.InternalRemove(ent); } } diff --git a/Robust.Server/GameObjects/EntitySystems/InputSystem.cs b/Robust.Server/GameObjects/EntitySystems/InputSystem.cs index 57a5ab2a15a..5c844ba1c18 100644 --- a/Robust.Server/GameObjects/EntitySystems/InputSystem.cs +++ b/Robust.Server/GameObjects/EntitySystems/InputSystem.cs @@ -38,7 +38,7 @@ public override void Shutdown() private void InputMessageHandler(InputCmdMessage message, EntitySessionEventArgs eventArgs) { - if (!(message is FullInputCmdMessage msg)) + if (message is not FullInputCmdMessage msg) return; //Client Sanitization: out of bounds functionID @@ -46,7 +46,7 @@ private void InputMessageHandler(InputCmdMessage message, EntitySessionEventArgs return; //Client Sanitization: bad enum key state value - if (!Enum.IsDefined(typeof(BoundKeyState), msg.State)) + if (!Enum.IsDefined(msg.State)) return; var session = eventArgs.SenderSession; diff --git a/Robust.Server/GameObjects/ServerEntityManager.cs b/Robust.Server/GameObjects/ServerEntityManager.cs index 9721093413d..ad138596d8d 100644 --- a/Robust.Server/GameObjects/ServerEntityManager.cs +++ b/Robust.Server/GameObjects/ServerEntityManager.cs @@ -44,6 +44,8 @@ public sealed partial class ServerEntityManager : EntityManager, IServerEntityMa private ISawmill _netEntSawmill = default!; private PvsSystem _pvs = default!; + private Histogram? _tickUpdateHistogram; + private Histogram.Child? _entityNetHistogram; public override void Initialize() { @@ -159,7 +161,9 @@ public void SetupNetworking() /// public override void TickUpdate(float frameTime, bool noPredictions, Histogram? histogram) { - using (histogram?.WithLabels("EntityNet").NewTimer()) + UpdateTickHistogram(histogram); + + using (_entityNetHistogram?.NewTimer()) { while (_queue.Count != 0 && _queue.Peek().SourceTick <= _gameTiming.CurTick) { @@ -172,6 +176,15 @@ public override void TickUpdate(float frameTime, bool noPredictions, Histogram? EntitiesCount.Set(Entities.Count); } + private void UpdateTickHistogram(Histogram? histogram) + { + if (ReferenceEquals(_tickUpdateHistogram, histogram)) + return; + + _tickUpdateHistogram = histogram; + _entityNetHistogram = histogram?.WithLabels("EntityNet"); + } + public uint GetLastMessageSequence(ICommonSession? session) { return session == null ? default : _lastProcessedSequencesCmd.GetValueOrDefault(session); diff --git a/Robust.Server/GameStates/PvsSystem.Chunks.cs b/Robust.Server/GameStates/PvsSystem.Chunks.cs index 99466b1f6b7..a270c7410b7 100644 --- a/Robust.Server/GameStates/PvsSystem.Chunks.cs +++ b/Robust.Server/GameStates/PvsSystem.Chunks.cs @@ -133,7 +133,7 @@ private void GetVisibleChunks(Entity eye, _grids.Clear(); var rangeVec = new Vector2(range, range); var box = new Box2(viewPos - rangeVec, viewPos + rangeVec); - _mapManager.FindGridsIntersecting(map, box, ref _grids, approx: true, includeMap: false); + _maps.FindGridsIntersecting(map, box, ref _grids, approx: true, includeMap: false); foreach (var (grid, _) in _grids) { diff --git a/Robust.Server/GameStates/PvsSystem.Entity.cs b/Robust.Server/GameStates/PvsSystem.Entity.cs index 70748a196cf..bca88aac336 100644 --- a/Robust.Server/GameStates/PvsSystem.Entity.cs +++ b/Robust.Server/GameStates/PvsSystem.Entity.cs @@ -11,6 +11,9 @@ internal sealed partial class PvsSystem { private void OnEntityMove(ref MoveEvent ev) { + if (ev.OnlyRotation) + return; + UpdatePosition(ev.Entity.Owner, ev.Entity.Comp1, ev.Entity.Comp2, ev.OldPosition.EntityId); } diff --git a/Robust.Server/GameStates/PvsSystem.GetStates.cs b/Robust.Server/GameStates/PvsSystem.GetStates.cs index 87786ceacef..b2247c32c60 100644 --- a/Robust.Server/GameStates/PvsSystem.GetStates.cs +++ b/Robust.Server/GameStates/PvsSystem.GetStates.cs @@ -22,10 +22,10 @@ internal sealed partial class PvsSystem /// New entity State for the given entity. private EntityState GetEntityState(ICommonSession? player, EntityUid entityUid, GameTick fromTick, MetaDataComponent meta) { - var changed = new List(); + var changed = GetComponentChangeList(meta.NetComponents.Count); bool sendCompList = meta.LastComponentRemoved > fromTick; - HashSet? netComps = sendCompList ? new() : null; + HashSet? netComps = sendCompList ? GetNetComponentSet() : null; var stateEv = new ComponentGetState(player, fromTick); foreach (var (netId, component) in meta.NetComponents) @@ -83,10 +83,10 @@ private EntityState GetEntityState(ICommonSession? player, EntityUid entityUid, private EntityState GetFullEntityState(ICommonSession player, EntityUid entityUid, MetaDataComponent meta) { var bus = EntityManager.EventBusInternal; - var changed = new List(); + var changed = GetComponentChangeList(meta.NetComponents.Count); var stateEv = new ComponentGetState(player, GameTick.Zero); - HashSet netComps = new(); + HashSet netComps = GetNetComponentSet(); foreach (var (netId, component) in meta.NetComponents) { @@ -197,6 +197,7 @@ private void GetAllEntityStates(PvsSession pvsSession) Last modified: {md.EntityLastModifiedTick} Metadata last modified: {md.LastModifiedTick} Transform last modified: {Transform(uid).LastModifiedTick}"); + ReturnEntityState(state); continue; } @@ -217,6 +218,8 @@ private void GetAllEntityStates(PvsSession pvsSession) var state = GetEntityState(session, uid, fromTick, md); if (!state.Empty) pvsSession.States.Add(state); + else + ReturnEntityState(state); } } } diff --git a/Robust.Server/GameStates/PvsSystem.Pooling.cs b/Robust.Server/GameStates/PvsSystem.Pooling.cs index 871bd8f69a6..49c5735a915 100644 --- a/Robust.Server/GameStates/PvsSystem.Pooling.cs +++ b/Robust.Server/GameStates/PvsSystem.Pooling.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Microsoft.Extensions.ObjectPool; using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; using Robust.Shared.Utility; using SharpZstd.Interop; @@ -19,9 +20,15 @@ internal sealed partial class PvsSystem private readonly ObjectPool> _entDataListPool = new DefaultObjectPool>(new ListPolicy(), MaxVisPoolSize); + private readonly ObjectPool> _componentChangeListPool + = new DefaultObjectPool>(new ListPolicy(), MaxVisPoolSize); + private readonly ObjectPool> _uidSetPool = new DefaultObjectPool>(new SetPolicy(), MaxVisPoolSize); + private readonly ObjectPool> _netComponentSetPool + = new DefaultObjectPool>(new SetPolicy(), MaxVisPoolSize); + private readonly ObjectPool _chunkPool = new DefaultObjectPool(new PvsChunkPolicy(), 256); @@ -63,4 +70,38 @@ private sealed class PvsThreadResources CompressionContext.Dispose(); } } + + private List GetComponentChangeList(int capacity) + { + var list = _componentChangeListPool.Get(); + list.EnsureCapacity(capacity); + return list; + } + + private HashSet GetNetComponentSet() + { + return _netComponentSetPool.Get(); + } + + private void ReturnEntityState(EntityState state) + { + if (state.ComponentChanges.Value is List changes) + _componentChangeListPool.Return(changes); + + if (state.NetComponents is { } netComps) + { + _netComponentSetPool.Return(netComps); + state.NetComponents = null; + } + } + + internal void ClearSessionState(PvsSession session) + { + foreach (var state in session.States) + { + ReturnEntityState(state); + } + + session.ClearState(); + } } diff --git a/Robust.Server/GameStates/PvsSystem.Serialize.cs b/Robust.Server/GameStates/PvsSystem.Serialize.cs index 25a72ec864f..7ffb6e79ec3 100644 --- a/Robust.Server/GameStates/PvsSystem.Serialize.cs +++ b/Robust.Server/GameStates/PvsSystem.Serialize.cs @@ -71,6 +71,6 @@ private void SerializeSessionState(PvsSession data) _serializer.SerializeDirect(data.StateStream, data.State); } - data.ClearState(); + ClearSessionState(data); } } diff --git a/Robust.Server/GameStates/PvsSystem.Session.cs b/Robust.Server/GameStates/PvsSystem.Session.cs index 9e3a87dee40..c724dba6bde 100644 --- a/Robust.Server/GameStates/PvsSystem.Session.cs +++ b/Robust.Server/GameStates/PvsSystem.Session.cs @@ -149,6 +149,9 @@ private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) private void ClearSendHistory(PvsSession session) { + session.StateStream?.Dispose(); + session.StateStream = null; + if (session.Overflow != null) _entDataListPool.Return(session.Overflow.Value.SentEnts); session.Overflow = null; diff --git a/Robust.Server/GameStates/PvsSystem.ToSendSet.cs b/Robust.Server/GameStates/PvsSystem.ToSendSet.cs index 99e3e86b5c0..00aaa7185bc 100644 --- a/Robust.Server/GameStates/PvsSystem.ToSendSet.cs +++ b/Robust.Server/GameStates/PvsSystem.ToSendSet.cs @@ -112,6 +112,8 @@ private void AddEntity(PvsSession session, ref PvsChunk.ChunkEntity ent, ref Pvs if (!entState.Empty) session.States.Add(entState); + else + ReturnEntityState(entState); } /// @@ -189,6 +191,8 @@ private bool AddEntity(PvsSession session, Entity entity, Gam if (!entState.Empty) session.States.Add(entState); + else + ReturnEntityState(entState); return true; } diff --git a/Robust.Server/GameStates/PvsSystem.cs b/Robust.Server/GameStates/PvsSystem.cs index 2095023f220..1f93d1e54be 100644 --- a/Robust.Server/GameStates/PvsSystem.cs +++ b/Robust.Server/GameStates/PvsSystem.cs @@ -28,7 +28,6 @@ namespace Robust.Server.GameStates; internal sealed partial class PvsSystem : EntitySystem { [Dependency] private IConfigurationManager _configManager = default!; - [Dependency] private INetworkedMapManager _mapManager = default!; [Dependency] private IServerEntityNetworkManager _netEntMan = default!; [Dependency] private IPlayerManager _playerManager = default!; [Dependency] private IParallelManager _parallelManager = default!; @@ -40,6 +39,7 @@ internal sealed partial class PvsSystem : EntitySystem [Dependency] private IParallelManagerInternal _parallelMgr = default!; [Dependency] private PvsOverrideSystem _pvsOverride = default!; [Dependency] private IServerReplayRecordingManager _replay = default!; + [Dependency] private SharedMapSystem _maps = default!; // TODO make this a cvar. Make it in terms of seconds and tie it to tick rate? // Main issue is that I CBF figuring out the logic for handling it changing mid-game. @@ -243,8 +243,15 @@ private void OnClientRequestFull(ICommonSession session, GameTick tick, NetEntit if (missingEntity != null) { - var (entity, meta) = GetEntityData(missingEntity.Value); - sb.Append($" Apparently they received an entity without metadata: {ToPrettyString(entity)}."); + if (TryGetEntityData(missingEntity.Value, out var uid, out _)) + { + sb.Append($" Apparently they received an entity without metadata: {ToPrettyString(uid)}."); + } + else + { + sb.Append($" Apparently they received an entity without metadata (No entity found)."); + } + //sb.Append($" Entity last seen: {meta.PvsData[sessionData.Index].EntityLastAcked}"); } @@ -288,7 +295,7 @@ private void CullDeletionHistory(GameTick oldestAck) { using var _ = Histogram.WithLabels("Cull History").NewTimer(); CullDeletionHistoryUntil(oldestAck); - _mapManager.CullDeletionHistory(oldestAck); + _maps.CullDeletionHistory(oldestAck); } private void GetEntityStates(PvsSession session) diff --git a/Robust.Server/Network/Transfer/ServerTransferImplWebSocket.cs b/Robust.Server/Network/Transfer/ServerTransferImplWebSocket.cs index 641b960f3a0..6541aa15374 100644 --- a/Robust.Server/Network/Transfer/ServerTransferImplWebSocket.cs +++ b/Robust.Server/Network/Transfer/ServerTransferImplWebSocket.cs @@ -118,5 +118,7 @@ public async Task HandleApiRequest(NetUserId userId, IStatusHandlerContext conte public override void Dispose() { _connectTcs.TrySetCanceled(); + + base.Dispose(); } } diff --git a/Robust.Server/Physics/GridFixtureSystem.cs b/Robust.Server/Physics/GridFixtureSystem.cs index 873e2e7c31e..159a4ca15c1 100644 --- a/Robust.Server/Physics/GridFixtureSystem.cs +++ b/Robust.Server/Physics/GridFixtureSystem.cs @@ -25,7 +25,6 @@ namespace Robust.Server.Physics /// public sealed partial class GridFixtureSystem : SharedGridFixtureSystem { - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IConfigurationManager _cfg = default!; [Dependency] private IConGroupController _conGroup = default!; [Dependency] private EntityLookupSystem _lookup = default!; @@ -262,7 +261,7 @@ private void CheckSplits(EntityUid uid, HashSet dirtyNodes) for (var i = 0; i < grids.Count - 1; i++) { var group = grids[i]; - var newGrid = _mapManager.CreateGridEntity(mapId); + var newGrid = _maps.CreateGridEntity(mapId); var newGridUid = newGrid.Owner; var newGridXform = _xformQuery.GetComponent(newGridUid); newGrids[i] = newGridUid; diff --git a/Robust.Server/Placement/PlacementManager.cs b/Robust.Server/Placement/PlacementManager.cs index a7868c8730a..f4d426f669b 100644 --- a/Robust.Server/Placement/PlacementManager.cs +++ b/Robust.Server/Placement/PlacementManager.cs @@ -28,7 +28,6 @@ public sealed partial class PlacementManager : IPlacementManager [Dependency] private IPlayerManager _playerManager = default!; [Dependency] private IPrototypeManager _prototype = default!; [Dependency] private IServerEntityManager _entityManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private ILogManager _logManager = default!; private EntityLookupSystem _lookup => _entityManager.System(); @@ -190,27 +189,29 @@ private void PlaceNewTile(int tileType, EntityCoordinates coordinates, NetUserId { if (!coordinates.IsValid(_entityManager)) return; + var mapSystem = _maps; + MapGridComponent? grid; EntityUid gridId = coordinates.EntityId; if (_entityManager.TryGetComponent(coordinates.EntityId, out grid) - || _mapManager.TryFindGridAt(_xformSystem.ToMapCoordinates(coordinates), out gridId, out grid)) + || mapSystem.TryFindGridAt(_xformSystem.ToMapCoordinates(coordinates), out gridId, out grid)) { - _maps.SetTile(gridId, grid, coordinates, new Tile(tileType, rotationMirroring: (byte)(direction + (mirrored ? 4 : 0)))); + mapSystem.SetTile(gridId, grid, coordinates, new Tile(tileType, rotationMirroring: (byte)(direction + (mirrored ? 4 : 0)))); var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId); _entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent); } else if (tileType != 0) // create a new grid { - var newGrid = _mapManager.CreateGridEntity(_xformSystem.GetMapId(coordinates)); + var newGrid = mapSystem.CreateGridEntity(_xformSystem.GetMapId(coordinates)); var newGridXform = new Entity( newGrid.Owner, _entityManager.GetComponent(newGrid)); _xformSystem.SetWorldPosition(newGridXform, coordinates.Position - newGrid.Comp.TileSizeHalfVector); // assume bottom left tile origin - var tilePos = _maps.WorldToTile(newGrid.Owner, newGrid.Comp, coordinates.Position); - _maps.SetTile(newGrid.Owner, newGrid.Comp, tilePos, new Tile(tileType, rotationMirroring: (byte)(direction + (mirrored ? 4 : 0)))); + var tilePos = mapSystem.WorldToTile(newGrid.Owner, newGrid.Comp, coordinates.Position); + mapSystem.SetTile(newGrid.Owner, newGrid.Comp, tilePos, new Tile(tileType, rotationMirroring: (byte)(direction + (mirrored ? 4 : 0)))); var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId); _entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent); diff --git a/Robust.Server/Player/FilterSystem.cs b/Robust.Server/Player/FilterSystem.cs index f89ce972912..207e9f6abbe 100644 --- a/Robust.Server/Player/FilterSystem.cs +++ b/Robust.Server/Player/FilterSystem.cs @@ -1,20 +1,18 @@ -using Robust.Server.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.Player; -namespace Robust.Server.Player +namespace Robust.Server.Player; + +internal sealed class FilterSystem : SharedFilterSystem { - internal sealed class FilterSystem : SharedFilterSystem + public override Filter FromEntities(Filter filter, params EntityUid[] entities) { - public override Filter FromEntities(Filter filter, params EntityUid[] entities) + foreach (var uid in entities) { - foreach (var uid in entities) - { - if (TryComp(uid, out ActorComponent? actor)) - filter.AddPlayer(actor.PlayerSession); - } - - return filter; + if (TryComp(uid, out ActorComponent? actor)) + filter.AddPlayer(actor.PlayerSession); } + + return filter; } } diff --git a/Robust.Server/Player/PlayerManager.cs b/Robust.Server/Player/PlayerManager.cs index b46f18a4bc7..07323cfc950 100644 --- a/Robust.Server/Player/PlayerManager.cs +++ b/Robust.Server/Player/PlayerManager.cs @@ -16,194 +16,192 @@ using Robust.Shared.Player; using Robust.Shared.Reflection; using Robust.Shared.Timing; -using Robust.Shared.Utility; -namespace Robust.Server.Player +namespace Robust.Server.Player; + +/// +/// This class will manage connected player sessions. +/// +internal sealed partial class PlayerManager : SharedPlayerManager, IPlayerManager { - /// - /// This class will manage connected player sessions. - /// - internal sealed partial class PlayerManager : SharedPlayerManager, IPlayerManager - { - private static readonly Gauge PlayerCountMetric = Metrics - .CreateGauge("robust_player_count", "Number of players on the server."); + private static readonly Gauge PlayerCountMetric = Metrics + .CreateGauge("robust_player_count", "Number of players on the server."); - [Dependency] private IBaseServer _baseServer = default!; - [Dependency] private IGameTiming _timing = default!; - [Dependency] private IServerNetManager _network = default!; - [Dependency] private IReflectionManager _reflectionManager = default!; - [Dependency] private IEntityManager _entityManager = default!; - [Dependency] private IServerNetConfigurationManager _cfg = default!; + [Dependency] private IBaseServer _baseServer = default!; + [Dependency] private IGameTiming _timing = default!; + [Dependency] private IServerNetManager _network = default!; + [Dependency] private IReflectionManager _reflectionManager = default!; + [Dependency] private IEntityManager _entityManager = default!; + [Dependency] private IServerNetConfigurationManager _cfg = default!; - public BoundKeyMap KeyMap { get; private set; } = default!; + public BoundKeyMap KeyMap { get; private set; } = default!; - /// - public override void Initialize(int maxPlayers) - { - base.Initialize(maxPlayers); - KeyMap = new BoundKeyMap(_reflectionManager); - KeyMap.PopulateKeyFunctionsMap(); + /// + public override void Initialize(int maxPlayers) + { + base.Initialize(maxPlayers); + KeyMap = new BoundKeyMap(_reflectionManager); + KeyMap.PopulateKeyFunctionsMap(); - _network.RegisterNetMessage(HandlePlayerListReq); - _network.RegisterNetMessage(); - _network.RegisterNetMessage(); + _network.RegisterNetMessage(HandlePlayerListReq); + _network.RegisterNetMessage(); + _network.RegisterNetMessage(); - _network.Connecting += OnConnecting; - _network.Connected += NewSession; - _network.Disconnect += EndSession; - } + _network.Connecting += OnConnecting; + _network.Connected += NewSession; + _network.Disconnect += EndSession; + } - public override void Shutdown() - { - base.Shutdown(); - KeyMap = default!; + public override void Shutdown() + { + base.Shutdown(); + KeyMap = default!; - _network.Connecting -= OnConnecting; - _network.Connected -= NewSession; - _network.Disconnect -= EndSession; - } + _network.Connecting -= OnConnecting; + _network.Connected -= NewSession; + _network.Disconnect -= EndSession; + } - private Task OnConnecting(NetConnectingArgs args) + private Task OnConnecting(NetConnectingArgs args) + { + if (PlayerCount >= _baseServer.MaxPlayers) { - if (PlayerCount >= _baseServer.MaxPlayers) - { - args.Deny("The server is full."); - } - - return Task.CompletedTask; + args.Deny("The server is full."); } - /// - /// Creates a new session for a client. - /// - /// - /// - private void NewSession(object? sender, NetChannelArgs args) - { - CreateAndAddSession(args.Channel); - PlayerCountMetric.Set(PlayerCount); - // Synchronize base time. - var msgTimeBase = new MsgSyncTimeBase(); - (msgTimeBase.Time, msgTimeBase.Tick) = _timing.TimeBase; - _network.ServerSendMessage(msgTimeBase, args.Channel); - - _cfg.SyncConnectingClient(args.Channel); - } + return Task.CompletedTask; + } - private void EndSession(object? sender, NetChannelArgs args) - { - EndSession(args.Channel.UserId); - } + /// + /// Creates a new session for a client. + /// + /// + /// + private void NewSession(object? sender, NetChannelArgs args) + { + CreateAndAddSession(args.Channel); + PlayerCountMetric.Set(PlayerCount); + // Synchronize base time. + var msgTimeBase = new MsgSyncTimeBase(); + (msgTimeBase.Time, msgTimeBase.Tick) = _timing.TimeBase; + _network.ServerSendMessage(msgTimeBase, args.Channel); + + _cfg.SyncConnectingClient(args.Channel); + } - /// - /// Ends a clients session, and disconnects them. - /// - internal void EndSession(NetUserId user) - { - if (!TryGetSessionById(user, out var session)) - return; + private void EndSession(object? sender, NetChannelArgs args) + { + EndSession(args.Channel.UserId); + } - RemoveSession(session.UserId); - SetStatus(session, SessionStatus.Disconnected); - SetAttachedEntity(session, null, out _, true); + /// + /// Ends a clients session, and disconnects them. + /// + internal void EndSession(NetUserId user) + { + if (!TryGetSessionById(user, out var session)) + return; - var viewSys = EntManager.System(); - foreach (var eye in session.ViewSubscriptions.ToArray()) - { - viewSys.RemoveViewSubscriber(eye, session); - } + RemoveSession(session.UserId); + SetStatus(session, SessionStatus.Disconnected); + SetAttachedEntity(session, null, out _, true); - PlayerCountMetric.Set(PlayerCount); - Dirty(); + var viewSys = EntManager.System(); + foreach (var eye in session.ViewSubscriptions.ToArray()) + { + viewSys.RemoveViewSubscriber(eye, session); } - private void HandlePlayerListReq(MsgPlayerListReq message) - { - var channel = message.MsgChannel; - var session = (CommonSession)GetSessionByChannel(channel); - session.InitialPlayerListReqDone = true; + PlayerCountMetric.Set(PlayerCount); + Dirty(); + } - if (!session.InitialResourcesDone) - return; + private void HandlePlayerListReq(MsgPlayerListReq message) + { + var channel = message.MsgChannel; + var session = (CommonSession)GetSessionByChannel(channel); + session.InitialPlayerListReqDone = true; - SendPlayerList(channel, session); - } + if (!session.InitialResourcesDone) + return; - public void MarkPlayerResourcesSent(INetChannel channel) - { - var session = (CommonSession)GetSessionByChannel(channel); - session.InitialResourcesDone = true; + SendPlayerList(channel, session); + } - if (!session.InitialPlayerListReqDone) - return; + public void MarkPlayerResourcesSent(INetChannel channel) + { + var session = (CommonSession)GetSessionByChannel(channel); + session.InitialResourcesDone = true; - SendPlayerList(channel, session); - } + if (!session.InitialPlayerListReqDone) + return; - private void SendPlayerList(INetChannel channel, CommonSession session) - { - var players = Sessions; - var netMsg = new MsgPlayerList(); + SendPlayerList(channel, session); + } - // client session is complete, set their status accordingly. - // This is done before the packet is built, so that the client - // can see themselves Connected. - session.ConnectedTime = DateTime.UtcNow; - SetStatus(session, SessionStatus.Connected); + private void SendPlayerList(INetChannel channel, CommonSession session) + { + var players = Sessions; + var netMsg = new MsgPlayerList(); - var list = new List(); - foreach (var client in players) - { - var info = new SessionState - { - UserId = client.UserId, - Name = client.Name, - Status = client.Status - }; - list.Add(info); - } - netMsg.Plyrs = list; - - channel.SendMessage(netMsg); - } + // client session is complete, set their status accordingly. + // This is done before the packet is built, so that the client + // can see themselves Connected. + session.ConnectedTime = DateTime.UtcNow; + SetStatus(session, SessionStatus.Connected); - public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session) + var list = new List(); + foreach (var client in players) { - if (!_entityManager.TryGetComponent(uid, out ActorComponent? actor)) + var info = new SessionState { - session = null; - return false; - } - - session = actor.PlayerSession; - return true; + UserId = client.UserId, + Name = client.Name, + Status = client.Status + }; + list.Add(info); } + netMsg.Plyrs = list; - internal ICommonSession AddDummySession(NetUserId user, string name) + channel.SendMessage(netMsg); + } + + public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session) + { + if (!_entityManager.TryGetComponent(uid, out ActorComponent? actor)) { + session = null; + return false; + } + + session = actor.PlayerSession; + return true; + } + + internal ICommonSession AddDummySession(NetUserId user, string name) + { #if FULL_RELEASE // Lets not make it completely trivial to fake player counts. throw new NotSupportedException(); #endif - Lock.EnterWriteLock(); - DummySession session; - try - { - UserIdMap[name] = user; - if (!PlayerData.TryGetValue(user, out var data)) - PlayerData[user] = data = new(user, name); - - session = new DummySession(user, name, data); - InternalSessions.Add(user, session); - } - finally - { - Lock.ExitWriteLock(); - } - - UpdateState(session); + Lock.EnterWriteLock(); + DummySession session; + try + { + UserIdMap[name] = user; + if (!PlayerData.TryGetValue(user, out var data)) + PlayerData[user] = data = new(user, name); - return session; + session = new DummySession(user, name, data); + InternalSessions.Add(user, session); } + finally + { + Lock.ExitWriteLock(); + } + + UpdateState(session); + + return session; } } diff --git a/Robust.Server/Replays/ReplayRecordingManager.cs b/Robust.Server/Replays/ReplayRecordingManager.cs index 47c7dc74a15..96549589298 100644 --- a/Robust.Server/Replays/ReplayRecordingManager.cs +++ b/Robust.Server/Replays/ReplayRecordingManager.cs @@ -47,7 +47,7 @@ public void Update() _pvs.ComputeSessionState(_pvsSession); Update(_pvsSession.State); - _pvsSession.ClearState(); + _pvs.ClearSessionState(_pvsSession); _pvsSession.LastReceivedAck = Timing.CurTick; } @@ -55,6 +55,6 @@ protected override void Reset() { base.Reset(); _pvsSession.LastReceivedAck = GameTick.Zero; - _pvsSession.ClearState(); + _pvs.ClearSessionState(_pvsSession); } } diff --git a/Robust.Server/Robust.Server.csproj b/Robust.Server/Robust.Server.csproj index 11ed9746def..21caba7222d 100644 --- a/Robust.Server/Robust.Server.csproj +++ b/Robust.Server/Robust.Server.csproj @@ -40,6 +40,7 @@ + @@ -69,6 +70,7 @@ + diff --git a/Robust.Server/ServerIoC.cs b/Robust.Server/ServerIoC.cs index 2c26ca5df06..9bda629e414 100644 --- a/Robust.Server/ServerIoC.cs +++ b/Robust.Server/ServerIoC.cs @@ -56,9 +56,6 @@ internal static void RegisterIoC(IDependencyCollection deps) deps.Register(); deps.Register(); deps.Register(); - deps.Register(); - deps.Register(); - deps.Register(); deps.Register(); deps.Register(); deps.Register(); diff --git a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs index 6f31dc6d4c7..faf9d5f1a27 100644 --- a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs +++ b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs @@ -142,6 +142,7 @@ public class ComponentNetworkGenerator : ISourceGenerator // Name = component.Name, // Count = component.Count, var getStateInit = new StringBuilder(); + var clientGetStateInit = new StringBuilder(); // eg: // component.Name = state.Name; @@ -149,13 +150,12 @@ public class ComponentNetworkGenerator : ISourceGenerator var handleStateSetters = new StringBuilder(); // Builds the string for duplicating a full component state, in preparation for applying a delta state state - // without modifying the original. Note that this will not do a proper clone of any collections, under the - // assumption that nothing should ever try to modify them. Applying the delta state should just override the - // referenced collection, not modify it. + // without modifying the original. var shallowClone = new StringBuilder(); // Delta field states var deltaGetFields = new StringBuilder(); + var clientDeltaGetFields = new StringBuilder(); var deltaHandleFields = new StringBuilder(); @@ -168,6 +168,43 @@ public class ComponentNetworkGenerator : ISourceGenerator var fieldStates = new StringBuilder(); var networkedTypes = new List(); + var usesClientCollectionCopy = false; + + void AppendShallowClone(string fieldName) + { + shallowClone.Append($@" + {fieldName} = this.{fieldName},"); + } + + void AppendCollectionClone(string fieldName, bool nullable) + { + var value = nullable + ? $"this.{fieldName} == null ? null! : new(this.{fieldName})" + : $"new(this.{fieldName})"; + shallowClone.Append($@" + {fieldName} = {value},"); + } + + string GetClientCollectionField(string fieldName, bool nullable) + { + usesClientCollectionCopy = true; + return nullable + ? $"component.{fieldName} == null ? null! : new(component.{fieldName})" + : $"new(component.{fieldName})"; + } + + string GetCollectionRefill(ITypeSymbol type, string target, string source, string indentation) + { + var named = (INamedTypeSymbol) type; + return named.ConstructedFrom.ToDisplayString(FullyQualifiedFormat) switch + { + GlobalDictionaryName => $@"foreach (var (key, value) in {source}) +{indentation} {target}.Add(key, value);", + GlobalHashSetName => $"{target}.UnionWith({source});", + GlobalListName => $"{target}.AddRange({source});", + _ => throw new InvalidOperationException($"Unsupported collection type {type}") + }; + } foreach (var (type, name) in fields) { @@ -192,6 +229,7 @@ public class ComponentNetworkGenerator : ISourceGenerator string networkedType; string getField; + string? clientGetField = null; string? cast; // TODO: Uhh I just need casts or something. var castString = typeDisplayStr.Substring(8); @@ -202,6 +240,12 @@ public class ComponentNetworkGenerator : ISourceGenerator {{ "); + clientDeltaGetFields.Append(@$" + case {Math.Pow(2, index)}: + args.State = new {deltaStateName}() + {{ + "); + deltaHandleFields.Append(@$" case {deltaStateName} {deltaStateName}_State: {{"); @@ -226,8 +270,7 @@ public class ComponentNetworkGenerator : ISourceGenerator deltaHandleFields.Append($@" component.{name} = EnsureEntity<{componentName}>({cast} {fieldHandleValue}, uid);"); - shallowClone.Append($@" - {name} = this.{name},"); + AppendShallowClone(name); deltaApply.Add($"fullState.{name} = {name};"); @@ -248,8 +291,7 @@ public class ComponentNetworkGenerator : ISourceGenerator deltaHandleFields.Append($@" component.{name} = EnsureCoordinates<{componentName}>({cast} {fieldHandleValue}, uid);"); - shallowClone.Append($@" - {name} = this.{name},"); + AppendShallowClone(name); deltaApply.Add($@"fullState.{name} = {name};"); @@ -269,8 +311,7 @@ public class ComponentNetworkGenerator : ISourceGenerator deltaHandleFields.Append($@" EnsureEntitySet<{componentName}>({cast} {fieldHandleValue}, uid, component.{name});"); - shallowClone.Append($@" - {name} = this.{name},"); + AppendCollectionClone(name, nullable); deltaApply.Add($@"fullState.{name} = {name};"); @@ -279,7 +320,7 @@ public class ComponentNetworkGenerator : ISourceGenerator networkedType = $"{GlobalNetEntityUidListName}"; stateFields.Append($@" - public {networkedType} {name} = default!;"); + public {networkedType} {name} = default!;"); getField = $"GetNetEntityList(component.{name})"; cast = $"({GlobalNetEntityUidListName})"; @@ -290,8 +331,7 @@ public class ComponentNetworkGenerator : ISourceGenerator deltaHandleFields.Append($@" EnsureEntityList<{componentName}>({cast} {fieldHandleValue}, uid, component.{name});"); - shallowClone.Append($@" - {name} = this.{name},"); + AppendCollectionClone(name, nullable); deltaApply.Add($@"fullState.{name} = {name};"); @@ -345,8 +385,7 @@ public class ComponentNetworkGenerator : ISourceGenerator EnsureEntityDictionary<{ensureGeneric}>({cast} {fieldHandleValue}, uid, component.{name});"); } - shallowClone.Append($@" - {name} = this.{name},"); + AppendCollectionClone(name, nullable); deltaApply.Add($@"fullState.{name} = {name};"); @@ -370,8 +409,7 @@ public class ComponentNetworkGenerator : ISourceGenerator deltaHandleFields.Append($@" EnsureEntityDictionary<{componentName}, {key}>({cast} {fieldHandleValue}, uid, component.{name});"); - shallowClone.Append($@" - {name} = this.{name},"); + AppendCollectionClone(name, nullable); deltaApply.Add($@"fullState.{name} = {name};"); @@ -401,8 +439,7 @@ public class ComponentNetworkGenerator : ISourceGenerator component.{name} = null!; else component.{name} = ({nullCast})({name}Value.Clone());"); - shallowClone.Append($@" - {name} = this.{name},"); + AppendShallowClone(name); deltaApply.Add($"fullState.{name} = {name} == null ? null! : {name}.Clone();"); } else @@ -411,32 +448,68 @@ public class ComponentNetworkGenerator : ISourceGenerator component.{name} = state.{name}.Clone();"); deltaHandleFields.Append($@" component.{name} = {cast}({fieldHandleValue}.Clone());"); - shallowClone.Append($@" - {name} = this.{name},"); + AppendShallowClone(name); deltaApply.Add($"fullState.{name} = {name}.Clone();"); } } else if (IsCloneType(type)) { getField = $"component.{name}"; + clientGetField = GetClientCollectionField(name, nullable); cast = $"({castString})"; var nullCast = nullable ? castString.Substring(0, castString.Length - 1) : castString; + var handleRefill = GetCollectionRefill(type, $"component.{name}", $"state.{name}", " "); + var deltaRefill = GetCollectionRefill(type, $"component.{name}", $"{name}Value", " "); - handleStateSetters.Append($@" - component.{name} = state.{name} == null ? null! : new(state.{name});"); + if (nullable) + { + handleStateSetters.Append($@" + if (state.{name} == null) + component.{name} = null!; + else if (component.{name} == null) + component.{name} = new(state.{name}); + else if (!ReferenceEquals(component.{name}, state.{name})) + {{ + component.{name}.Clear(); + {handleRefill} + }}"); - deltaHandleFields.Append($@" + deltaHandleFields.Append($@" var {name}Value = {cast} {fieldHandleValue}; if ({name}Value == null) component.{name} = null!; - else - component.{name} = new {nullCast}({name}Value);"); + else if (component.{name} == null) + component.{name} = new {nullCast}({name}Value); + else if (!ReferenceEquals(component.{name}, {name}Value)) + {{ + component.{name}.Clear(); + {deltaRefill} + }}"); + + deltaApply.Add($"fullState.{name} = {name} == null ? null! : new({name});"); + } + else + { + handleStateSetters.Append($@" + if (!ReferenceEquals(component.{name}, state.{name})) + {{ + component.{name}.Clear(); + {handleRefill} + }}"); + + deltaHandleFields.Append($@" + var {name}Value = {cast} {fieldHandleValue}; + if (!ReferenceEquals(component.{name}, {name}Value)) + {{ + component.{name}.Clear(); + {deltaRefill} + }}"); - shallowClone.Append($@" - {name} = this.{name},"); + deltaApply.Add($"fullState.{name} = new({name});"); + } - deltaApply.Add($"fullState.{name} = {name} == null ? null! : new({name});"); + AppendCollectionClone(name, nullable); } else { @@ -449,8 +522,7 @@ public class ComponentNetworkGenerator : ISourceGenerator deltaHandleFields.Append($@" component.{name} = {cast} {fieldHandleValue};"); - shallowClone.Append($@" - {name} = this.{name},"); + AppendShallowClone(name); deltaApply.Add($"fullState.{name} = {name};"); } @@ -463,14 +535,22 @@ public class ComponentNetworkGenerator : ISourceGenerator */ networkedTypes.Add(networkedType); + clientGetField ??= getField; getStateInit.Append($@" {name} = {getField},"); + clientGetStateInit.Append($@" + {name} = {clientGetField},"); + deltaGetFields.Append(@$" {name} = {getField} }}; return;"); + clientDeltaGetFields.Append(@$" {name} = {clientGetField} + }}; + return;"); + deltaHandleFields.Append(@" break; } @@ -478,6 +558,7 @@ public class ComponentNetworkGenerator : ISourceGenerator } var deltaGetState = ""; + var clientDeltaGetState = ""; var deltaInterface = ""; var deltaCompFields = ""; var deltaNetRegister = ""; @@ -526,13 +607,30 @@ public void ApplyToFullState({stateName} fullState) deltaNetRegister = $@"EntityManager.ComponentFactory.RegisterNetworkedFields<{classSymbol}>({fieldsStr});"; deltaGetState = @$"// Delta state - if (component is IComponentDelta delta && args.FromTick > component.CreationTick && delta.LastFieldUpdate >= args.FromTick) + if (component is IComponentDelta delta) + {{ + var aspects = EntityManager.GetModifiedAspects(component, args.FromTick); + + // Try and get a matching delta state for the relevant dirty fields, otherwise fall back to full state. + switch (aspects) + {{ + case >= DeltaAspect.Unclassified: + break;{deltaGetFields} + default: + break; + }} + }}"; + + clientDeltaGetState = @$"// Delta state + if (component is IComponentDelta delta && args.FromTick > component.CreationTick) {{ - var fields = EntityManager.GetModifiedFields(component, args.FromTick); + var aspects = EntityManager.GetModifiedAspects(component, args.FromTick); // Try and get a matching delta state for the relevant dirty fields, otherwise fall back to full state. - switch (fields) - {{{deltaGetFields} + switch (aspects) + {{ + case >= DeltaAspect.Unclassified: + break;{clientDeltaGetFields} default: break; }} @@ -541,16 +639,16 @@ public void ApplyToFullState({stateName} fullState) deltaInterface = " : IComponentDelta"; deltaCompFields = @$"/// - public GameTick LastFieldUpdate {{ get; set; }} = GameTick.Zero; - + public GameTick LastUnclassifiedDirty {{ get; set; }} /// - public GameTick[] LastModifiedFields {{ get; set; }} = Array.Empty();"; + public GameTick[] LastModifiedFields {{ get; set; }}"; } string handleState; if (!fieldDeltas) { var eventRaise = ""; + var stateSetters = TrimNewLines(handleStateSetters); if (raiseAfterAutoHandle) { eventRaise = @" @@ -562,12 +660,13 @@ public void ApplyToFullState({stateName} fullState) handleState = $@" if (args.Current is not {stateName} state) return; -{handleStateSetters}{eventRaise}"; + +{stateSetters}{eventRaise}"; } else { // Re-indent handleStateSetters so it aligns with the switch block - var stateSetters = handleStateSetters.ToString(); + var stateSetters = TrimNewLines(handleStateSetters); stateSetters = stateSetters.Replace(" ", " "); @@ -597,6 +696,24 @@ public void ApplyToFullState({stateName} fullState) } var outSb = new StringBuilder(); + var stateFieldsText = TrimNewLines(stateFields); + var getStateInitText = TrimNewLines(getStateInit); + var clientGetStateInitText = TrimNewLines(clientGetStateInit); + var cloneMethodText = TrimNewLines(cloneMethod); + var deltaGetStateText = TrimNewLines(deltaGetState); + var clientDeltaGetStateText = TrimNewLines(clientDeltaGetState); + var deltaCompFieldsText = TrimNewLines(deltaCompFields); + var fieldStatesText = TrimNewLines(fieldStates); + + var netManagerDependency = usesClientCollectionCopy + ? "[global::Robust.Shared.IoC.Dependency] private global::Robust.Shared.Network.INetManager _net = default!;" + : string.Empty; + var getStateSubscription = usesClientCollectionCopy + ? $@" if (_net.IsClient) + SubscribeLocalEvent<{componentName}, ComponentGetState>(OnGetStateClient); + else + SubscribeLocalEvent<{componentName}, ComponentGetState>(OnGetState);" + : $@" SubscribeLocalEvent<{componentName}, ComponentGetState>(OnGetState);"; outSb.Append(""" // @@ -616,57 +733,125 @@ public void ApplyToFullState({stateName} fullState) partialInfo.WriteHeader(outSb); - outSb.Append($$""" - {{deltaInterface}} - { - {{deltaCompFields}} + outSb.AppendLine(deltaInterface); + outSb.AppendLine("{"); - [System.Serializable, NetSerializable] - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - [RobustAutoGenerated] - public sealed class {{stateName}} : IComponentState - { - {{stateFields}} - {{cloneMethod}} - } + if (deltaCompFieldsText.Length != 0) + { + outSb.AppendLine(deltaCompFieldsText); + outSb.AppendLine(); + } - [RobustAutoGenerated] - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - public sealed class {{componentName}}_AutoNetworkSystem : EntitySystem - { - public override void Initialize() - { - {{deltaNetRegister}} - SubscribeLocalEvent<{{componentName}}, ComponentGetState>(OnGetState); - SubscribeLocalEvent<{{componentName}}, ComponentHandleState>(OnHandleState); - } + outSb.AppendLine(" [System.Serializable, NetSerializable]"); + outSb.AppendLine(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]"); + outSb.AppendLine(" [RobustAutoGenerated]"); + outSb.AppendLine($" public sealed class {stateName} : IComponentState"); + outSb.AppendLine(" {"); + outSb.AppendLine(stateFieldsText); - private void OnGetState(EntityUid uid, {{componentName}} component, ref ComponentGetState args) - { - {{deltaGetState}} + if (cloneMethodText.Length != 0) + { + outSb.AppendLine(); + outSb.AppendLine(cloneMethodText); + } - // Get full state - args.State = new {{stateName}} - { - {{getStateInit}} - }; - } + outSb.AppendLine(" }"); + outSb.AppendLine(); + outSb.AppendLine(" [RobustAutoGenerated]"); + outSb.AppendLine(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]"); + outSb.AppendLine($" public sealed class {componentName}_AutoNetworkSystem : EntitySystem"); + outSb.AppendLine(" {"); - private void OnHandleState(EntityUid uid, {{componentName}} component, ref ComponentHandleState args) - { - {{handleState}} - } - } + if (netManagerDependency.Length != 0) + { + outSb.AppendLine($" {netManagerDependency}"); + outSb.AppendLine(); + } + + outSb.AppendLine(" public override void Initialize()"); + outSb.AppendLine(" {"); + + if (deltaNetRegister.Length != 0) + outSb.AppendLine($" {deltaNetRegister}"); + + outSb.AppendLine(getStateSubscription); + outSb.AppendLine($" SubscribeLocalEvent<{componentName}, ComponentHandleState>(OnHandleState);"); + outSb.AppendLine(" }"); + outSb.AppendLine(); + outSb.AppendLine($" private void OnGetState(EntityUid uid, {componentName} component, ref ComponentGetState args)"); + outSb.AppendLine(" {"); + + if (deltaGetStateText.Length != 0) + { + outSb.AppendLine(IndentFirstLine(deltaGetStateText, 12)); + outSb.AppendLine(); + } + + outSb.AppendLine(" // Get full state"); + outSb.AppendLine($" args.State = new {stateName}"); + outSb.AppendLine(" {"); + outSb.AppendLine(getStateInitText); + outSb.AppendLine(" };"); + outSb.AppendLine(" }"); - {{fieldStates}} + if (usesClientCollectionCopy) + { + outSb.AppendLine(); + outSb.AppendLine($" private void OnGetStateClient(EntityUid uid, {componentName} component, ref ComponentGetState args)"); + outSb.AppendLine(" {"); + + if (clientDeltaGetStateText.Length != 0) + { + outSb.AppendLine(IndentFirstLine(clientDeltaGetStateText, 12)); + outSb.AppendLine(); } - """); + + outSb.AppendLine(" // Get full state"); + outSb.AppendLine($" args.State = new {stateName}"); + outSb.AppendLine(" {"); + outSb.AppendLine(clientGetStateInitText); + outSb.AppendLine(" };"); + outSb.AppendLine(" }"); + } + + outSb.AppendLine(); + outSb.AppendLine($" private void OnHandleState(EntityUid uid, {componentName} component, ref ComponentHandleState args)"); + outSb.AppendLine(" {"); + outSb.AppendLine(TrimNewLines(handleState)); + outSb.AppendLine(" }"); + outSb.AppendLine(" }"); + + if (fieldStatesText.Length != 0) + { + outSb.AppendLine(); + outSb.AppendLine(fieldStatesText); + } + + outSb.AppendLine("}"); partialInfo.WriteFooter(outSb); return outSb.ToString(); } + private static string TrimNewLines(StringBuilder source) + { + return source.ToString().Trim('\r', '\n'); + } + + private static string TrimNewLines(string source) + { + return source.Trim('\r', '\n'); + } + + private static string IndentFirstLine(string source, int spaces) + { + if (source.Length == 0) + return source; + + return new string(' ', spaces) + source; + } + public void Execute(GeneratorExecutionContext context) { var comp = (CSharpCompilation) context.Compilation; diff --git a/Robust.Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGenerator.cs b/Robust.Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGenerator.cs new file mode 100644 index 00000000000..5f751673c40 --- /dev/null +++ b/Robust.Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGenerator.cs @@ -0,0 +1,246 @@ +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Robust.Roslyn.Shared; +using Robust.Roslyn.Shared.Helpers; +using static Robust.Shared.EntitySystemSubscriptionsGenerator.KnownTypes; + +namespace Robust.Shared.EntitySystemSubscriptionsGenerator; + +/// +/// This generator implements EntitySystem.AutoSubscriptions() for all EntitySystems with methods +/// annotated by auto-subscription attributes. In case any attributes are applied to methods incorrectly, this generator +/// just silently ignores them and expects will complain +/// on its behalf (except in the case of attempting to add generated code to a non-partial type -- we complain about +/// that here). +/// +/// +[Generator(LanguageNames.CSharp)] +public class EntitySystemSubscriptionGenerator : IIncrementalGenerator +{ + private static readonly DiagnosticDescriptor NotPartial = new( + Diagnostics.IdNonPartialContainingTypeForGeneratedSubscription, + "Containing class must be declared as Partial", + "Method is declared in type \"{0}\" which is not Partial", + "Usage", + DiagnosticSeverity.Error, + true + ); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var annotatedEntitySystems = Aggregate( + GetEntityTypeCandidatesContainingAnnotatedMethods(context, AllSubscriptionMemberAttributeName), + GetEntityTypeCandidatesContainingAnnotatedMethods(context, NetworkSubscriptionMemberAttributeName), + GetEntityTypeCandidatesContainingAnnotatedMethods(context, LocalSubscriptionMemberAttributeName) + ) // Get all candidate types containing subscription annotated methods + .SelectMany((array, _) => + array.ToImmutableHashSet(PartialTypeInfo.WithoutLocationEqualityComparer)) // Dedupe + .Combine(context.CompilationProvider) + .Select((inputs, cancel) => + { + // For each EntitySystem we've identified as containing subscriptions... + + var (partialTypeInfo, compilation) = inputs; + if (compilation.GetTypeByMetadataName(partialTypeInfo.GetMetadataName()) is not + { } entitySystemType) + return new EntitySystemInfo(partialTypeInfo, []); + + // ... check all methods in the type to see if it's a subscription, assembling subscriptions into an array. + var subs = entitySystemType.GetMembers() + .OfType() + .Select(method => + { + cancel.ThrowIfCancellationRequested(); + return TryParseSubscriptions(method); + }) + .OfType() + .ToImmutableArray(); + + return new EntitySystemInfo(partialTypeInfo, subs); + } + ); + + context.RegisterImplementationSourceOutput( + // Only deal with types that have subscriptions. + annotatedEntitySystems.Where(it => !it.Subscriptions.IsEmpty), + (productionContext, info) => + { + var (partialTypeInfo, subscriptions) = info; + if (partialTypeInfo.CheckPartialDiagnostic(productionContext, NotPartial)) + return; + + var subscriptionsSyntax = new StringBuilder(); + foreach (var method in subscriptions) + { + productionContext.CancellationToken.ThrowIfCancellationRequested(); + var subscriptionMethod = method.Type.ToSubscriptionMethod(); + var typeArgs = string.Join(", ", method.TypeArgs); + subscriptionsSyntax.AppendLine($" {subscriptionMethod}<{typeArgs}>({method.MethodName});"); + } + + var builder = new StringBuilder(@" +// + +using Robust.Shared.GameObjects; +using JetBrains.Annotations; + +"); + partialTypeInfo.WriteHeader(builder); + builder.AppendLine($@" +{{ + /// + [MustCallBase] + public override void AutoSubscriptions() + {{ + base.AutoSubscriptions(); + +{subscriptionsSyntax} + }} +}} +"); + partialTypeInfo.WriteFooter(builder); + + productionContext.AddSource(partialTypeInfo.GetGeneratedFileName(), builder.ToString()); + } + ); + } + + /// Returns s for all types in the compilation that contain methods with the given + /// attribute. + private static IncrementalValuesProvider GetEntityTypeCandidatesContainingAnnotatedMethods( + IncrementalGeneratorInitializationContext context, + string attributeName + ) + { + return context.SyntaxProvider.ForAttributeWithMetadataName( + attributeName, + (node, _) => node is MethodDeclarationSyntax, + (ctx, _) => + { + if (ctx.TargetSymbol is not IMethodSymbol symbol || + ctx.TargetNode is not MethodDeclarationSyntax { Parent: TypeDeclarationSyntax parentSyntax }) + return null; + + return PartialTypeInfo.FromSymbol(symbol.ContainingType, parentSyntax); + }) + .Where(it => it is not null) + .Select((it, _) => it ?? throw new("Unreachable")); + } + + /// Tries to parse 's signature as an even subscription, returning the information required + /// to make the subscription function call in the generated code. Returns null if the method is not a + /// subscription, or is a subscription and its signature is invalid. + private static SubscriptionInfo? TryParseSubscriptions(IMethodSymbol method) + { + return TryParseSubscription( + method, + AllSubscriptionMemberAttributeName, + m => TryParseEntityEventHandler(m) ?? TryParseEntitySessionEventHandler(m) + ) ?? TryParseSubscription( + method, + NetworkSubscriptionMemberAttributeName, + m => TryParseEntityEventHandler(m) ?? TryParseEntitySessionEventHandler(m) + ) ?? TryParseSubscription( + method, + LocalSubscriptionMemberAttributeName, + m => TryParseEntityEventHandler(m) ?? + TryParseEntitySessionEventHandler(m) ?? + TryParseComponentEventHandler(m) ?? + TryParseEntityEventRefHandler(m) + ); + } + + /// Tries to parse 's signature as Robust.Shared.GameObjects.EntityEventHandler. + /// The type argument syntax to include in the subscription function call. + public static ImmutableArray? TryParseEntityEventHandler(IMethodSymbol method) + { + if (method.Parameters.Length != 1 || + method.Parameters[0].Type is not INamedTypeSymbol eventType) + return null; + + return [eventType.ToString()]; + } + + /// Tries to parse 's signature as Robust.Shared.GameObjects.EntitySessionEventHandler. + /// The type argument syntax to include in the subscription function call. + public static ImmutableArray? TryParseEntitySessionEventHandler(IMethodSymbol method) + { + if (method.Parameters.Length != 2 || + method.Parameters[0].Type is not INamedTypeSymbol eventType || + !TypeSymbolHelper.ShittyTypeMatch( + method.Parameters[1].Type, + EntitySessionEventArgsTypeName + )) + return null; + + return [eventType.ToString()]; + } + + /// Tries to parse 's signature as Robust.Shared.GameObjects.EntityEventRefHandler. + /// The type argument syntax to include in the subscription function call. + public static ImmutableArray? TryParseEntityEventRefHandler(IMethodSymbol method) + { + if (method.Parameters.Length != 2 || + method.Parameters[0].Type is not INamedTypeSymbol entityType || + method.Parameters[1].Type is not INamedTypeSymbol eventType || + method.Parameters[1].RefKind != RefKind.Ref) + return null; + + if (entityType.OriginalDefinition.ToDisplayString() != EntityTypeName || + entityType.TypeArguments is not [INamedTypeSymbol componentType] || + !TypeSymbolHelper.ImplementsInterface(componentType, IComponentTypeName)) + return null; + + return [componentType.ToString(), eventType.ToString()]; + } + + /// Tries to parse 's signature as Robust.Shared.GameObjects.ComponentEventHandler. + /// The type argument syntax to include in the subscription function call. + public static ImmutableArray? TryParseComponentEventHandler(IMethodSymbol method) + { + if (method.Parameters.Length != 3 || + method.Parameters[0].Type is not INamedTypeSymbol entityUidType || + method.Parameters[1].Type is not INamedTypeSymbol componentType || + method.Parameters[2].Type is not INamedTypeSymbol eventType || + !TypeSymbolHelper.ShittyTypeMatch(entityUidType, EntityUidTypeName) || + !TypeSymbolHelper.ImplementsInterface(componentType, IComponentTypeName)) + return null; + + return [componentType.ToString(), eventType.ToString()]; + } + + private static SubscriptionInfo? TryParseSubscription( + IMethodSymbol method, + string annotationName, + Func?> parseFunc + ) + { + if (annotationName.ToSubscriptionType() is not { } subType || + !AttributeHelper.HasAttribute(method, annotationName, out _) || + parseFunc(method) is not { } parameters) + return null; + + return new SubscriptionInfo(method.Name, subType, parameters); + } + + /// Aggregates all of the s across all the given providers into a single array value + /// provided by the returned provider. + private static IncrementalValueProvider> Aggregate( + IncrementalValuesProvider first, + params IncrementalValuesProvider[] more + ) + { + return more.Aggregate( + first.Collect(), + (acc, valuesProvider) => + acc.Combine(valuesProvider.Collect()) + .Select((values, _) => values.Left.AddRange(values.Right)) + ); + } + + private record struct EntitySystemInfo(PartialTypeInfo Type, EquatableArray Subscriptions); + + private record struct SubscriptionInfo(string MethodName, SubscriptionType Type, EquatableArray TypeArgs); +} diff --git a/Robust.Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGeneratorErrorAnalyzer.cs b/Robust.Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGeneratorErrorAnalyzer.cs new file mode 100644 index 00000000000..42b65ec3efc --- /dev/null +++ b/Robust.Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGeneratorErrorAnalyzer.cs @@ -0,0 +1,166 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Robust.Roslyn.Shared; + +namespace Robust.Shared.EntitySystemSubscriptionsGenerator; + +/// +/// This analyzer ensures that all methods annotated with the relevant subscription attributes are:
    +///
  • In an EntitySystem
  • +///
  • Have the correct signature for their subscription type
  • +///
+/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class EntitySystemSubscriptionGeneratorErrorAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor BadMethodSignature = new( + Diagnostics.IdInvalidAMethodSignatureForGeneratedSubscription, + "Invalid method signature", + "Method signature is incompatible with required delegate type(s) for \"{0}\". Compatible types are: {1}.", + "Usage", + DiagnosticSeverity.Error, + true + ); + + private static readonly DiagnosticDescriptor NotEntitySystem = new( + Diagnostics.IdInvalidContainingTypeForGeneratedSubscription, + $"Method not in {KnownTypes.EntitySystemTypeName}", + $"Method is declared in type \"{{0}}\" which does not extend {KnownTypes.EntitySystemTypeName}", + "Usage", + DiagnosticSeverity.Error, + true + ); + + public override ImmutableArray SupportedDiagnostics { get; } = + [BadMethodSignature, NotEntitySystem]; + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis( + GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics + ); + + EnsureAnnotatedSubscriptionMethodsAreInAnEntitySystem(context); + EnsureAnnotatedSubscriptionMethodsHaveCorrectSignatures(context); + } + + private static void EnsureAnnotatedSubscriptionMethodsAreInAnEntitySystem(AnalysisContext context) + { + List attributeNames = + [ + KnownTypes.AllSubscriptionMemberAttributeName, + KnownTypes.NetworkSubscriptionMemberAttributeName, + KnownTypes.LocalSubscriptionMemberAttributeName, + ]; + + context.RegisterCompilationStartAction(c => + { + if (c.Compilation.GetTypeByMetadataName(KnownTypes.EntitySystemTypeName) is not { } entitySystemType) + return; + + var attributeSymbols = attributeNames + .Select(attributeName => c.Compilation.GetTypeByMetadataName(attributeName)) + .OfType() + .ToList(); + + c.RegisterSymbolStartAction( + c => + { + if (!c.Symbol.GetAttributes() + .Select(it => it.AttributeClass) + .Intersect(attributeSymbols, SymbolEqualityComparer.IncludeNullability) + .Any() || + IsSubtypeOf(c.Symbol.ContainingType, entitySystemType)) + return; + + c.RegisterSymbolEndAction(c => c.ReportDiagnostic(Diagnostic.Create( + NotEntitySystem, + c.Symbol.Locations[0], + c.Symbol.ContainingType?.Name ?? "" + ))); + }, + SymbolKind.Method + ); + }); + } + + private static bool IsSubtypeOf(ITypeSymbol subtype, INamedTypeSymbol supertype) + { + return SymbolEqualityComparer.Default.Equals(subtype.BaseType, supertype) || + (subtype.BaseType is not null && IsSubtypeOf(subtype.BaseType, supertype)); + } + + private static void EnsureAnnotatedSubscriptionMethodsHaveCorrectSignatures(AnalysisContext context) + { + EnsureAnnotatedSubscriptionMethodHasCorrectSignature( + context, + KnownTypes.AllSubscriptionMemberAttributeName, + m => (EntitySystemSubscriptionGenerator.TryParseEntityEventHandler(m) ?? + EntitySystemSubscriptionGenerator.TryParseEntitySessionEventHandler(m)) is not null, + KnownTypes.NonComponentSubscriptionHandlerTypes + ); + EnsureAnnotatedSubscriptionMethodHasCorrectSignature( + context, + KnownTypes.NetworkSubscriptionMemberAttributeName, + m => (EntitySystemSubscriptionGenerator.TryParseEntityEventHandler(m) ?? + EntitySystemSubscriptionGenerator.TryParseEntitySessionEventHandler(m)) is not null, + KnownTypes.NonComponentSubscriptionHandlerTypes + ); + EnsureAnnotatedSubscriptionMethodHasCorrectSignature( + context, + KnownTypes.LocalSubscriptionMemberAttributeName, + m => ( + EntitySystemSubscriptionGenerator.TryParseEntityEventHandler(m) ?? + EntitySystemSubscriptionGenerator.TryParseEntitySessionEventHandler(m) ?? + EntitySystemSubscriptionGenerator.TryParseComponentEventHandler(m) ?? + EntitySystemSubscriptionGenerator.TryParseEntityEventRefHandler(m) + ) is not null, + string.Join(", ", + KnownTypes.NonComponentSubscriptionHandlerTypes, + KnownTypes.ComponentSubscriptionHandlerTypes) + ); + } + + /// Checks that any methods annotated with have the correct signature as + /// determined by . If not, a + /// diagnostic is emitted, describing how the signature should instead conform to + /// . + private static void EnsureAnnotatedSubscriptionMethodHasCorrectSignature( + AnalysisContext context, + string annotationName, + Func hasCorrectParameters, + string acceptableHandlerTypes + ) + { + context.RegisterCompilationStartAction(c => + { + if (c.Compilation.GetTypeByMetadataName(annotationName) is not { } annotationSymbol) + return; + + c.RegisterSymbolStartAction( + c => + { + // The `symbolKind` arg to `RegisterSymbolStartAction` should make this never fail. + if (c.Symbol is not IMethodSymbol symbol) + throw new Exception($"Expected {nameof(IMethodSymbol)} but got {c.Symbol.GetType().FullName}"); + + if (!symbol.GetAttributes() + .Select(it => it.AttributeClass) + .Contains(annotationSymbol, SymbolEqualityComparer.IncludeNullability) || + hasCorrectParameters(symbol)) + return; + + c.RegisterSymbolEndAction(c => c.ReportDiagnostic(Diagnostic.Create( + BadMethodSignature, + symbol.Locations[0], + annotationName, + acceptableHandlerTypes + ))); + }, + SymbolKind.Method + ); + }); + } +} diff --git a/Robust.Shared.EntitySystemSubscriptionsGenerator/KnownTypes.cs b/Robust.Shared.EntitySystemSubscriptionsGenerator/KnownTypes.cs new file mode 100644 index 00000000000..ad6606d6cbd --- /dev/null +++ b/Robust.Shared.EntitySystemSubscriptionsGenerator/KnownTypes.cs @@ -0,0 +1,58 @@ +namespace Robust.Shared.EntitySystemSubscriptionsGenerator; + +public static class KnownTypes +{ + public const string EntitySystemTypeName = "Robust.Shared.GameObjects.EntitySystem"; + public const string EntityUidTypeName = "Robust.Shared.GameObjects.EntityUid"; + public const string EntityTypeName = "Robust.Shared.GameObjects.Entity"; + public const string EntitySessionEventArgsTypeName = "Robust.Shared.GameObjects.EntitySessionEventArgs"; + public const string IComponentTypeName = "Robust.Shared.GameObjects.IComponent"; + + public const string LocalSubscriptionMemberAttributeName = + "Robust.Shared.Analyzers.SubscribeLocalEventAttribute"; + + public const string NetworkSubscriptionMemberAttributeName = + "Robust.Shared.Analyzers.SubscribeNetworkEventAttribute"; + + public const string AllSubscriptionMemberAttributeName = "Robust.Shared.Analyzers.EventSubscriptionAttribute"; + + public static readonly string ComponentSubscriptionHandlerTypes = string.Join( + ", ", + "Robust.Shared.GameObjects.ComponentEventHandler", + "Robust.Shared.GameObjects.ComponentEventRefHandler", + "Robust.Shared.GameObjects.EntityEventRefHandler" + ); + + public static readonly string NonComponentSubscriptionHandlerTypes = string.Join( + ", ", + "Robust.Shared.GameObjects.EntityEventHandler", + "Robust.Shared.GameObjects.EntityEventRefHandler", + "Robust.Shared.GameObjects.EntitySessionEventHandler" + ); + + public static SubscriptionType? ToSubscriptionType(this string annotation) + { + return annotation switch + { + AllSubscriptionMemberAttributeName => SubscriptionType.All, + NetworkSubscriptionMemberAttributeName => SubscriptionType.Network, + LocalSubscriptionMemberAttributeName => SubscriptionType.Local, + _ => null + }; + } + + public static string ToSubscriptionMethod(this SubscriptionType type) => type switch + { + SubscriptionType.All => "SubscribeAllEvent", + SubscriptionType.Network => "SubscribeNetworkEvent", + SubscriptionType.Local => "SubscribeLocalEvent", + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; +} + +public enum SubscriptionType +{ + All, + Network, + Local, +} diff --git a/Robust.Shared.EntitySystemSubscriptionsGenerator/Properties/launchSettings.json b/Robust.Shared.EntitySystemSubscriptionsGenerator/Properties/launchSettings.json new file mode 100644 index 00000000000..91bdf53a218 --- /dev/null +++ b/Robust.Shared.EntitySystemSubscriptionsGenerator/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Entity System Subscriptions Generator": { + "commandName": "DebugRoslynComponent", + "targetProject": "../../Content.Shared/Content.Shared.csproj" + } + } +} diff --git a/Robust.Shared.EntitySystemSubscriptionsGenerator/Robust.Shared.EntitySystemSubscriptionsGenerator.csproj b/Robust.Shared.EntitySystemSubscriptionsGenerator/Robust.Shared.EntitySystemSubscriptionsGenerator.csproj new file mode 100644 index 00000000000..076e7a492d3 --- /dev/null +++ b/Robust.Shared.EntitySystemSubscriptionsGenerator/Robust.Shared.EntitySystemSubscriptionsGenerator.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Robust.Shared.IntegrationTests/EntityLookup_Test.cs b/Robust.Shared.IntegrationTests/EntityLookup_Test.cs index cd06309a60f..a6e4b60aa33 100644 --- a/Robust.Shared.IntegrationTests/EntityLookup_Test.cs +++ b/Robust.Shared.IntegrationTests/EntityLookup_Test.cs @@ -65,9 +65,9 @@ private EntityUid GetPhysicsEntity(IEntityManager entManager, MapCoordinates spa return ent; } - private Entity SetupGrid(MapId mapId, SharedMapSystem mapSystem, IEntityManager entManager, IMapManager mapManager) + private Entity SetupGrid(MapId mapId, SharedMapSystem mapSystem, IEntityManager entManager) { - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); entManager.System().SetLocalPosition(grid.Owner, new Vector2(10f, 10f)); mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1)); return grid; @@ -88,11 +88,10 @@ public void TestEntityAnyIntersecting(bool physics, MapCoordinates spawnPos, Box var lookup = server.Resolve().GetEntitySystem(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); mapSystem.CreateMap(spawnPos.MapId); - var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager); + var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager); if (physics) GetPhysicsEntity(entManager, spawnPos); @@ -113,11 +112,10 @@ public void TestEntityAnyLocalIntersecting(bool physics, MapCoordinates spawnPos var lookup = server.Resolve().GetEntitySystem(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); mapSystem.CreateMap(spawnPos.MapId); - var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager); + var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager); if (physics) GetPhysicsEntity(entManager, spawnPos); @@ -141,11 +139,10 @@ public void TestEntityGridLocalIntersecting(bool physics, MapCoordinates spawnPo var lookup = server.Resolve().GetEntitySystem(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); mapSystem.CreateMap(spawnPos.MapId); - var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager); + var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager); if (physics) GetPhysicsEntity(entManager, spawnPos); @@ -170,11 +167,10 @@ public void TestEntityGridTileIntersecting(bool physics, MapCoordinates spawnPos var lookup = server.Resolve().GetEntitySystem(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); mapSystem.CreateMap(spawnPos.MapId); - var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager); + var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager); if (physics) GetPhysicsEntity(entManager, spawnPos); @@ -221,11 +217,10 @@ public void TestGridIntersecting(bool physics, MapCoordinates spawnPos, MapCoord var lookup = server.Resolve().GetEntitySystem(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); mapSystem.CreateMap(spawnPos.MapId); - var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager); + var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager); if (physics) GetPhysicsEntity(entManager, spawnPos); @@ -247,11 +242,10 @@ public void TestGridInRange(bool physics, MapCoordinates spawnPos, MapCoordinate var lookup = server.Resolve().GetEntitySystem(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); mapSystem.CreateMap(spawnPos.MapId); - var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager); + var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager); if (physics) GetPhysicsEntity(entManager, spawnPos); @@ -295,11 +289,10 @@ public void TestGridAnyIntersecting(bool physics, MapCoordinates spawnPos, Box2 var lookup = server.Resolve().GetEntitySystem(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); mapSystem.CreateMap(spawnPos.MapId); - var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager); + var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager); if (physics) GetPhysicsEntity(entManager, spawnPos); @@ -323,11 +316,10 @@ public void TestGridLocalIntersecting(bool physics, MapCoordinates spawnPos, Box var lookup = server.Resolve().GetEntitySystem(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); mapSystem.CreateMap(spawnPos.MapId); - var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager); + var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager); if (physics) GetPhysicsEntity(entManager, spawnPos); @@ -354,12 +346,11 @@ public void TestAnchoring() var lookup = server.Resolve().GetEntitySystem(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); var transformSystem = entManager.System(); var mapId = server.CreateMap().MapId; - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var theMapSpotBeingUsed = new Box2(Vector2.Zero, Vector2.One); mapSystem.SetTile(grid, new Vector2i(), new Tile(1)); diff --git a/Robust.Shared.IntegrationTests/EntitySerialization/AutoIncludeSerializationTest.cs b/Robust.Shared.IntegrationTests/EntitySerialization/AutoIncludeSerializationTest.cs index 3e4b12c25d4..f049e1dfd37 100644 --- a/Robust.Shared.IntegrationTests/EntitySerialization/AutoIncludeSerializationTest.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/AutoIncludeSerializationTest.cs @@ -34,7 +34,6 @@ public async Task TestAutoIncludeSerialization() var entMan = server.EntMan; var mapSys = server.System(); var loader = server.System(); - var mapMan = server.ResolveDependency(); var tileMan = server.ResolveDependency(); var mapPath = new ResPath($"{nameof(AutoIncludeSerializationTest)}_map.yml"); var gridPath = new ResPath($"{nameof(AutoIncludeSerializationTest)}_grid.yml"); @@ -55,7 +54,7 @@ public async Task TestAutoIncludeSerialization() await server.WaitPost(() => { var mapUid = mapSys.CreateMap(out mapId); - var gridUid = mapMan.CreateGridEntity(mapId); + var gridUid = mapSys.CreateGridEntity(mapId); mapSys.SetTile(gridUid, Vector2i.Zero, new Tile(tDef.TileId)); var onGridUid = entMan.SpawnEntity(null, new EntityCoordinates(gridUid, 0.5f, 0.5f)); diff --git a/Robust.Shared.IntegrationTests/EntitySerialization/CategorizationTest.cs b/Robust.Shared.IntegrationTests/EntitySerialization/CategorizationTest.cs index 5b48ec6c1e6..aee367cbd32 100644 --- a/Robust.Shared.IntegrationTests/EntitySerialization/CategorizationTest.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/CategorizationTest.cs @@ -35,7 +35,6 @@ public async Task TestCategorization() var meta = server.System(); var mapSys = server.System(); var loader = server.System(); - var mapMan = server.ResolveDependency(); var tileMan = server.ResolveDependency(); var path = new ResPath($"{nameof(TestCategorization)}.yml"); @@ -56,8 +55,8 @@ await server.WaitPost(() => { mapA = mapSys.CreateMap(out var mapIdA); mapB = mapSys.CreateMap(out var mapIdB); - var gridEntA = mapMan.CreateGridEntity(mapIdA); - var gridEntB = mapMan.CreateGridEntity(mapIdB); + var gridEntA = mapSys.CreateGridEntity(mapIdA); + var gridEntB = mapSys.CreateGridEntity(mapIdB); mapSys.SetTile(gridEntA, Vector2i.Zero, new Tile(tDef.TileId)); mapSys.SetTile(gridEntB, Vector2i.Zero, new Tile(tDef.TileId)); gridA = gridEntA.Owner; diff --git a/Robust.Shared.IntegrationTests/EntitySerialization/MapMergeTest.cs b/Robust.Shared.IntegrationTests/EntitySerialization/MapMergeTest.cs index 97f2d94202f..aad38625cf4 100644 --- a/Robust.Shared.IntegrationTests/EntitySerialization/MapMergeTest.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/MapMergeTest.cs @@ -35,7 +35,6 @@ public async Task TestMapMerge() var entMan = server.EntMan; var mapSys = server.System(); var loader = server.System(); - var mapMan = server.ResolveDependency(); var tileMan = server.ResolveDependency(); var mapPath = new ResPath($"{nameof(TestMapMerge)}_map.yml"); @@ -52,7 +51,7 @@ public async Task TestMapMerge() await server.WaitPost(() => { var mapUid = mapSys.CreateMap(out mapId, runMapInit: false); - var gridEnt = mapMan.CreateGridEntity(mapId); + var gridEnt = mapSys.CreateGridEntity(mapId); mapSys.SetTile(gridEnt, Vector2i.Zero, new Tile(tDef.TileId)); var entUid = entMan.SpawnEntity(null, new MapCoordinates(10, 10, mapId)); map = Get(mapUid, entMan); diff --git a/Robust.Shared.IntegrationTests/EntitySerialization/OrphanSerializationTest.cs b/Robust.Shared.IntegrationTests/EntitySerialization/OrphanSerializationTest.cs index 2cbb7f1db82..518abaf559f 100644 --- a/Robust.Shared.IntegrationTests/EntitySerialization/OrphanSerializationTest.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/OrphanSerializationTest.cs @@ -129,7 +129,6 @@ public async Task TestOrphanedGridSerialization() var mapSys = server.System(); var loader = server.System(); var xform = server.System(); - var mapMan = server.ResolveDependency(); var tileMan = server.ResolveDependency(); var pathA = new ResPath($"{nameof(TestOrphanedGridSerialization)}_A.yml"); var pathB = new ResPath($"{nameof(TestOrphanedGridSerialization)}_B.yml"); @@ -150,12 +149,12 @@ await server.WaitPost(() => var mapUid = mapSys.CreateMap(out mapId); map = Get(mapUid, entMan); - var gridAUid = mapMan.CreateGridEntity(mapId); + var gridAUid = mapSys.CreateGridEntity(mapId); mapSys.SetTile(gridAUid, Vector2i.Zero, new Tile(tDef.TileId)); gridA = Get(gridAUid, entMan); xform.SetLocalPosition(gridA.Owner, new(100, 100)); - var gridBUid = mapMan.CreateGridEntity(mapId); + var gridBUid = mapSys.CreateGridEntity(mapId); mapSys.SetTile(gridBUid, Vector2i.Zero, new Tile(tDef.TileId)); gridB = Get(gridBUid, entMan); diff --git a/Robust.Shared.IntegrationTests/GameObjects/ContainerTests.cs b/Robust.Shared.IntegrationTests/GameObjects/ContainerTests.cs index 28b38f3f78b..8b52fba8d78 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/ContainerTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/ContainerTests.cs @@ -161,7 +161,6 @@ public async Task TestContainerExpectedEntityDeleted() var clientTime = client.ResolveDependency(); var clientNetManager = client.ResolveDependency(); - var sMapManager = server.ResolveDependency(); var sEntManager = server.ResolveDependency(); var sPlayerManager = server.ResolveDependency(); var serverTime = server.ResolveDependency(); diff --git a/Robust.Shared.IntegrationTests/GameObjects/DeferredEntityDeletionTest.cs b/Robust.Shared.IntegrationTests/GameObjects/DeferredEntityDeletionTest.cs index 5bb02e61d2c..1b354c9d81c 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/DeferredEntityDeletionTest.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/DeferredEntityDeletionTest.cs @@ -39,7 +39,6 @@ public async Task TestDeferredEntityDeletion() await server.WaitAssertion(() => { - var mapMan = IoCManager.Resolve(); entMan = IoCManager.Resolve(); var sys = entMan.EntitySysManager.GetEntitySystem(); diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityQueryTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityQueryTests.cs new file mode 100644 index 00000000000..73601506b2e --- /dev/null +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityQueryTests.cs @@ -0,0 +1,68 @@ +using NUnit.Framework; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.UnitTesting; + +namespace Robust.Shared.IntegrationTests.GameObjects; + +public sealed partial class EntityQueryTests : RobustIntegrationTest +{ + [Test] + public async Task TestEntityQueryForEach() + { + var server = StartServer(); + + await server.WaitIdleAsync(); + await server.WaitPost(() => + { + var sEntManager = server.ResolveDependency(); + for (var i = 0; i < 5; i++) + { + var ent = sEntManager.Spawn(null, MapCoordinates.Nullspace); + sEntManager.EnsureComponent(ent); + } + + for (var i = 0; i < 10; i++) + { + var ent = sEntManager.Spawn(null, MapCoordinates.Nullspace); + sEntManager.EnsureComponent(ent); + } + }); + + await server.WaitAssertion(() => + { + var sEntManager = server.ResolveDependency(); + var a = new List>(); + foreach (var ent in sEntManager.EntityQueryEnumerator()) + { + Assert.That(a, Does.Not.Contain(ent)); + a.Add(ent); + } + + Assert.That(a, Has.Count.EqualTo(5)); + + var b = new List>(); + foreach (var ent in sEntManager.EntityQueryEnumerator()) + { + Assert.That(b, Does.Not.Contain(ent)); + b.Add(ent); + } + + Assert.That(b, Has.Count.EqualTo(10)); + + var c = new List>(); + foreach (var ent in sEntManager.EntityQueryEnumerator()) + { + c.Add(ent); + } + + Assert.That(c, Is.Empty); + }); + } + + [RegisterComponent] + public sealed partial class EntityQueryTestsAComponent : Component; + + [RegisterComponent] + public sealed partial class EntityQueryTestsBComponent : Component; +} diff --git a/Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs b/Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs index 9f04c1f2d55..e4f8e53b330 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs @@ -1,4 +1,3 @@ -using System.Linq; using System.Numerics; using NUnit.Framework; using Robust.Shared.Containers; @@ -38,14 +37,14 @@ private static (ISimulation, Entity grid, MapCoordinates, Shar }) .InitializeInstance(); - var mapManager = sim.Resolve(); + var mapSystem = sim.System(); var testMapId = sim.CreateMap().MapId; var coords = new MapCoordinates(new Vector2(7, 7), testMapId); // Add grid 1, as the default grid to anchor things to. - var grid = mapManager.CreateGridEntity(testMapId); + var grid = mapSystem.CreateGridEntity(testMapId); - return (sim, grid, coords, sim.System(), sim.System()); + return (sim, grid, coords, sim.System(), mapSystem); } // An entity is anchored to the tile it is over on the target grid. @@ -83,12 +82,14 @@ public void OnAnchored_WorldPosition_TileCenter() private sealed partial class AnchorOnInitComponent : Component; [Reflect(false)] - private sealed class AnchorOnInitTestSystem : EntitySystem + private sealed partial class AnchorOnInitTestSystem : EntitySystem { + [Dependency] private SharedTransformSystem _transform = default!; + public override void Initialize() { base.Initialize(); - SubscribeLocalEvent((e, _, _) => Transform(e).Anchored = true); + SubscribeLocalEvent((e, _, _) => _transform.AnchorEntity(e)); } } @@ -160,9 +161,8 @@ public void OnInitAnchored_AddedToLookup() var mapSys = sim.System(); var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); var mapId = sim.CreateMap().MapId; - var grid = mapMan.CreateGridEntity(mapId); + var grid = mapSys.CreateGridEntity(mapId); var coordinates = new MapCoordinates(new Vector2(7, 7), mapId); var pos = mapSys.TileIndicesFor(grid, coordinates); mapSys.SetTile(grid, pos, new Tile(1)); @@ -264,8 +264,10 @@ public void Anchored_SetPosition_Nop() sim.System().FailOnMove = true; // Act +#pragma warning disable CS0618 // Checking property setters. sim.Transform(ent1).WorldPosition = new Vector2(99, 99); sim.Transform(ent1).LocalPosition = new Vector2(99, 99); +#pragma warning restore CS0618 Assert.That(xformSys.GetMapCoordinates(ent1), Is.EqualTo(coordinates)); sim.System().FailOnMove = false; diff --git a/Robust.Shared.IntegrationTests/GameObjects/TransformComponent_Tests.cs b/Robust.Shared.IntegrationTests/GameObjects/TransformComponent_Tests.cs index aeb951a1155..4a95e37a6a7 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/TransformComponent_Tests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/TransformComponent_Tests.cs @@ -1,4 +1,3 @@ -using System; using System.Numerics; using NUnit.Framework; using Robust.Server.GameObjects; @@ -32,7 +31,7 @@ public void TestGetWorldMatches() xform.SetParent(ent2, ent1); - xform1.LocalRotation = MathF.PI; + xform.SetLocalRotationNoLerp(ent1, MathF.PI, xform1); var (worldPos, worldRot, worldMatrix) = xform.GetWorldPositionRotationMatrix(xform2); @@ -54,12 +53,11 @@ public void AttachToGridOrMap() var server = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); var xformSystem = entManager.System(); mapSystem.CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); mapSystem.SetTile(grid, new Vector2i(0, 0), new Tile(1)); xformSystem.SetLocalPosition(grid, new Vector2(0f, 100f)); diff --git a/Robust.Shared.IntegrationTests/GameState/AutoNetworkingTest.cs b/Robust.Shared.IntegrationTests/GameState/AutoNetworkingTest.cs index 56845abec38..43622177381 100644 --- a/Robust.Shared.IntegrationTests/GameState/AutoNetworkingTest.cs +++ b/Robust.Shared.IntegrationTests/GameState/AutoNetworkingTest.cs @@ -1,5 +1,3 @@ -using System.Linq; -using System.Threading.Tasks; using NUnit.Framework; using Robust.Shared; using Robust.Shared.Analyzers; @@ -8,6 +6,7 @@ using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Timing; namespace Robust.UnitTesting.Shared.GameState; @@ -16,7 +15,6 @@ internal sealed partial class AutoNetworkingTests : RobustIntegrationTest /// /// Does basic testing for AutoNetworkedFieldAttribute and AutoGenerateComponentStateAttribute /// to make sure the datafields are correctly networked to the client when dirtied. - /// TODO: Add test for field deltas. /// [Test] public async Task AutoNetworkingTest() @@ -42,16 +40,28 @@ public async Task AutoNetworkingTest() await RunTicks(); - // Spawn entities + // Spawn entities. var coords = new EntityCoordinates(map, default); EntityUid player = default; EntityUid cPlayer = default; + EntityUid clientEnt1 = default; + EntityUid clientEnt2 = default; + EntityUid clientEnt3 = default; + EntityUid clientEnt4 = default; + EntityUid clientEnt5 = default; + EntityUid clientEnt6 = default; EntityUid serverEnt1 = default; EntityUid serverEnt2 = default; EntityUid serverEnt3 = default; - NetEntity serverNet1 = default; - NetEntity serverNet2 = default; - NetEntity serverNet3 = default; + EntityUid serverEnt4 = default; + EntityUid serverEnt5 = default; + EntityUid serverEnt6 = default; + NetEntity netEnt1 = default; + NetEntity netEnt2 = default; + NetEntity netEnt3 = default; + NetEntity netEnt4 = default; + NetEntity netEnt5 = default; + NetEntity netEnt6 = default; await server.WaitPost(() => { @@ -65,101 +75,161 @@ await server.WaitPost(() => serverEnt1 = server.EntMan.SpawnAttachedTo(null, coords); serverEnt2 = server.EntMan.SpawnAttachedTo(null, coords); serverEnt3 = server.EntMan.SpawnAttachedTo(null, coords); - serverNet1 = server.EntMan.GetNetEntity(serverEnt1); - serverNet2 = server.EntMan.GetNetEntity(serverEnt2); - serverNet3 = server.EntMan.GetNetEntity(serverEnt3); - - // Setup components + serverEnt4 = server.EntMan.SpawnAttachedTo(null, coords); + serverEnt5 = server.EntMan.SpawnAttachedTo(null, coords); + serverEnt6 = server.EntMan.SpawnAttachedTo(null, coords); + netEnt1 = server.EntMan.GetNetEntity(serverEnt1); + netEnt2 = server.EntMan.GetNetEntity(serverEnt2); + netEnt3 = server.EntMan.GetNetEntity(serverEnt3); + netEnt4 = server.EntMan.GetNetEntity(serverEnt4); + netEnt5 = server.EntMan.GetNetEntity(serverEnt5); + netEnt6 = server.EntMan.GetNetEntity(serverEnt6); + + // Setup components. server.EntMan.EnsureComponent(serverEnt1); server.EntMan.EnsureComponent(serverEnt2); server.EntMan.EnsureComponent(serverEnt3); + server.EntMan.EnsureComponent(serverEnt4); + server.EntMan.EnsureComponent(serverEnt5); + server.EntMan.EnsureComponent(serverEnt6); }); await RunTicks(); - // check client + // Check client. await client.WaitPost(() => { - // Get the client-side entities + // Get the client-side entities. cPlayer = client.EntMan.GetEntity(server.EntMan.GetNetEntity(player)); - var clientEnt1 = client.EntMan.GetEntity(serverNet1); - var clientEnt2 = client.EntMan.GetEntity(serverNet2); - var clientEnt3 = client.EntMan.GetEntity(serverNet3); - - // Check player got properly attached + clientEnt1 = client.EntMan.GetEntity(netEnt1); + clientEnt2 = client.EntMan.GetEntity(netEnt2); + clientEnt3 = client.EntMan.GetEntity(netEnt3); + clientEnt4 = client.EntMan.GetEntity(netEnt4); + clientEnt5 = client.EntMan.GetEntity(netEnt5); + clientEnt6 = client.EntMan.GetEntity(netEnt6); + + // Check that the player got properly attached. Assert.That(client.AttachedEntity, Is.EqualTo(cPlayer)); Assert.That(client.EntMan.EntityExists(cPlayer)); - // Get the client-side components - Assert.That(client.EntMan.TryGetComponent(clientEnt1, out AutoNetworkingTestComponent? cmpClient1)); - Assert.That(client.EntMan.TryGetComponent(clientEnt2, out AutoNetworkingTestChildComponent? cmpClient2)); - Assert.That(client.EntMan.TryGetComponent(clientEnt3, out AutoNetworkingTestEmptyChildComponent? cmpClient3)); - - // All datafields should be the default value - Assert.That(cmpClient1?.IsNetworked, Is.EqualTo(1)); - Assert.That(cmpClient1?.NotNetworked, Is.EqualTo(2)); - - Assert.That(cmpClient2?.ChildNetworked, Is.EqualTo(1)); - Assert.That(cmpClient2?.Child, Is.EqualTo(2)); - Assert.That(cmpClient2?.ParentNetworked, Is.EqualTo(3)); - Assert.That(cmpClient2?.Parent, Is.EqualTo(4)); - - Assert.That(cmpClient3?.ParentNetworked, Is.EqualTo(3)); - Assert.That(cmpClient3?.Parent, Is.EqualTo(4)); + // Get the client-side components. + Assert.That(client.EntMan.TryGetComponent(clientEnt1, out AutoNetworkingTestComponent? clientComp1)); + Assert.That(client.EntMan.TryGetComponent(clientEnt2, out AutoNetworkingTestChildComponent? clientComp2)); + Assert.That(client.EntMan.TryGetComponent(clientEnt3, out AutoNetworkingTestEmptyChildComponent? clientComp3)); + Assert.That(client.EntMan.TryGetComponent(clientEnt4, out AutoNetworkingTestFieldDeltaComponent? clientComp4)); + Assert.That(client.EntMan.TryGetComponent(clientEnt5, out AutoNetworkingTestFieldDeltaComponent? clientComp5)); + Assert.That(client.EntMan.TryGetComponent(clientEnt6, out AutoNetworkingTestFieldDeltaComponent? clientComp6)); + + // All datafields should be the default value. + Assert.That(clientComp1?.IsNetworked, Is.EqualTo(1)); + Assert.That(clientComp1?.NotNetworked, Is.EqualTo(2)); + + Assert.That(clientComp2?.ChildNetworked, Is.EqualTo(1)); + Assert.That(clientComp2?.Child, Is.EqualTo(2)); + Assert.That(clientComp2?.ParentNetworked, Is.EqualTo(3)); + Assert.That(clientComp2?.Parent, Is.EqualTo(4)); + + Assert.That(clientComp3?.ParentNetworked, Is.EqualTo(3)); + Assert.That(clientComp3?.Parent, Is.EqualTo(4)); + + Assert.That(clientComp4?.Field1, Is.EqualTo(1)); + Assert.That(clientComp4?.Field2, Is.EqualTo(2)); + Assert.That(clientComp4?.Field3, Is.EqualTo(3)); + + Assert.That(clientComp5?.Field1, Is.EqualTo(1)); + Assert.That(clientComp5?.Field2, Is.EqualTo(2)); + Assert.That(clientComp5?.Field3, Is.EqualTo(3)); + + Assert.That(clientComp6?.Field1, Is.EqualTo(1)); + Assert.That(clientComp6?.Field2, Is.EqualTo(2)); + Assert.That(clientComp6?.Field3, Is.EqualTo(3)); }); - // make changes on the server + // Make changes on the server. await server.WaitPost(() => { - // Get the server-side components - var cmpServer1 = server.EntMan.GetComponent(serverEnt1); - var cmpServer2 = server.EntMan.GetComponent(serverEnt2); - var cmpServer3 = server.EntMan.GetComponent(serverEnt3); + // Get the server-side components. + var serverComp1 = server.EntMan.GetComponent(serverEnt1); + var serverComp2 = server.EntMan.GetComponent(serverEnt2); + var serverComp3 = server.EntMan.GetComponent(serverEnt3); + var serverComp4 = server.EntMan.GetComponent(serverEnt4); + var serverComp5 = server.EntMan.GetComponent(serverEnt5); + var serverComp6 = server.EntMan.GetComponent(serverEnt6); // All datafields should be the default value - Assert.That(cmpServer1.IsNetworked, Is.EqualTo(1)); - Assert.That(cmpServer1.NotNetworked, Is.EqualTo(2)); - - Assert.That(cmpServer2.ChildNetworked, Is.EqualTo(1)); - Assert.That(cmpServer2.Child, Is.EqualTo(2)); - Assert.That(cmpServer2.ParentNetworked, Is.EqualTo(3)); - Assert.That(cmpServer2.Parent, Is.EqualTo(4)); - - Assert.That(cmpServer3.ParentNetworked, Is.EqualTo(3)); - Assert.That(cmpServer3.Parent, Is.EqualTo(4)); - - // change the datafields and dirty them - cmpServer1.IsNetworked = 101; - cmpServer1.NotNetworked = 102; - cmpServer2.ChildNetworked = 101; - cmpServer2.Child = 102; - cmpServer2.ParentNetworked = 103; - cmpServer2.Parent = 104; - cmpServer3.ParentNetworked = 103; - cmpServer3.Parent = 104; - - server.EntMan.Dirty(serverEnt1, cmpServer1); - server.EntMan.Dirty(serverEnt2, cmpServer2); - server.EntMan.Dirty(serverEnt3, cmpServer3); + Assert.That(serverComp1.IsNetworked, Is.EqualTo(1)); + Assert.That(serverComp1.NotNetworked, Is.EqualTo(2)); + + Assert.That(serverComp2.ChildNetworked, Is.EqualTo(1)); + Assert.That(serverComp2.Child, Is.EqualTo(2)); + Assert.That(serverComp2.ParentNetworked, Is.EqualTo(3)); + Assert.That(serverComp2.Parent, Is.EqualTo(4)); + + Assert.That(serverComp3.ParentNetworked, Is.EqualTo(3)); + Assert.That(serverComp3.Parent, Is.EqualTo(4)); + + Assert.That(serverComp4?.Field1, Is.EqualTo(1)); + Assert.That(serverComp4?.Field2, Is.EqualTo(2)); + Assert.That(serverComp4?.Field3, Is.EqualTo(3)); + + Assert.That(serverComp5?.Field1, Is.EqualTo(1)); + Assert.That(serverComp5?.Field2, Is.EqualTo(2)); + Assert.That(serverComp5?.Field3, Is.EqualTo(3)); + + Assert.That(serverComp6?.Field1, Is.EqualTo(1)); + Assert.That(serverComp6?.Field2, Is.EqualTo(2)); + Assert.That(serverComp6?.Field3, Is.EqualTo(3)); + + // Test that a field with AutoNetworkedField gets networked. + serverComp1.IsNetworked = 101; + // Test that a field without AutoNetworkedField does not get networked. + serverComp1.NotNetworked = 102; + server.EntMan.Dirty(serverEnt1, serverComp1); + // Test that inherited autonetworked fields get networked. + serverComp2.ChildNetworked = 101; + serverComp2.Child = 102; + serverComp2.ParentNetworked = 103; + serverComp2.Parent = 104; + server.EntMan.Dirty(serverEnt2, serverComp2); + serverComp3.ParentNetworked = 103; + serverComp3.Parent = 104; + server.EntMan.Dirty(serverEnt3, serverComp3); + + // Use an autogenerated delta state to only network a single field. + serverComp4.Field1 = 101; + serverComp4.Field2 = 102; + serverComp4.Field3 = 103; + server.EntMan.DirtyField(serverEnt4, serverComp4, nameof(AutoNetworkingTestFieldDeltaComponent.Field3)); + + // Check that calling both Dirty and then DirtyField will send a full state. + serverComp5.Field1 = 101; + serverComp5.Field2 = 102; + serverComp5.Field3 = 103; + server.EntMan.DirtyField(serverEnt5, serverComp5, nameof(AutoNetworkingTestFieldDeltaComponent.Field3)); + server.EntMan.Dirty(serverEnt5, serverComp5); + + // Check that calling both DirtyField and then Dirty will send a full state. + serverComp6.Field1 = 101; + serverComp6.Field2 = 102; + serverComp6.Field3 = 103; + server.EntMan.Dirty(serverEnt6, serverComp6); + server.EntMan.DirtyField(serverEnt6, serverComp6, nameof(AutoNetworkingTestFieldDeltaComponent.Field3)); }); await RunTicks(); - // check the client again + // Check the client again. await client.WaitPost(() => { - // Get the client-side entities - cPlayer = client.EntMan.GetEntity(server.EntMan.GetNetEntity(player)); - var clientEnt1 = client.EntMan.GetEntity(serverNet1); - var clientEnt2 = client.EntMan.GetEntity(serverNet2); - var clientEnt3 = client.EntMan.GetEntity(serverNet3); - - // Get the client-side components + // Get the client-side components. Assert.That(client.EntMan.TryGetComponent(clientEnt1, out AutoNetworkingTestComponent? cmpClient1)); Assert.That(client.EntMan.TryGetComponent(clientEnt2, out AutoNetworkingTestChildComponent? cmpClient2)); Assert.That(client.EntMan.TryGetComponent(clientEnt3, out AutoNetworkingTestEmptyChildComponent? cmpClient3)); + Assert.That(client.EntMan.TryGetComponent(clientEnt4, out AutoNetworkingTestFieldDeltaComponent? cmpClient4)); + Assert.That(client.EntMan.TryGetComponent(clientEnt5, out AutoNetworkingTestFieldDeltaComponent? cmpClient5)); + Assert.That(client.EntMan.TryGetComponent(clientEnt6, out AutoNetworkingTestFieldDeltaComponent? cmpClient6)); - // All datafields should be the default value + // Check that the networked datafields have changed. Assert.That(cmpClient1?.IsNetworked, Is.EqualTo(101)); Assert.That(cmpClient1?.NotNetworked, Is.EqualTo(2)); // unchanged @@ -170,6 +240,18 @@ await client.WaitPost(() => Assert.That(cmpClient3?.ParentNetworked, Is.EqualTo(103)); Assert.That(cmpClient3?.Parent, Is.EqualTo(4)); // unchanged + + Assert.That(cmpClient4?.Field1, Is.EqualTo(1)); // not dirtied + Assert.That(cmpClient4?.Field2, Is.EqualTo(2)); // not networked + Assert.That(cmpClient4?.Field3, Is.EqualTo(103)); // changed from delta state + + Assert.That(cmpClient5?.Field1, Is.EqualTo(101)); // changed from full state + Assert.That(cmpClient5?.Field2, Is.EqualTo(2)); // not networked + Assert.That(cmpClient5?.Field3, Is.EqualTo(103)); // changed from full state + + Assert.That(cmpClient6?.Field1, Is.EqualTo(101)); // changed from full state + Assert.That(cmpClient6?.Field2, Is.EqualTo(2)); // not networked + Assert.That(cmpClient6?.Field3, Is.EqualTo(103)); // changed from full state }); async Task RunTicks() @@ -185,6 +267,50 @@ async Task RunTicks() await server.WaitRunTicks(5); await client.WaitRunTicks(5); } + + /// + /// Tests that field-delta generation treats the from-tick as an already acknowledged state boundary. + /// A field dirtied exactly on the from-tick should not be included in the next delta mask. + /// + [Test] + public async Task AutoNetworkingFieldDeltaFromTickBoundaryTest() + { + using var server = StartServer(); + + await server.WaitIdleAsync(); + + EntityUid entity = default; + await server.WaitPost(() => + { + entity = server.EntMan.Spawn(); + server.EntMan.EnsureComponent(entity); + }); + + await server.WaitRunTicks(1); + + var acknowledgedTick = GameTick.Zero; + await server.WaitPost(() => + { + var component = server.EntMan.GetComponent(entity); + component.Field1 = 101; + server.EntMan.DirtyField(entity, component, nameof(AutoNetworkingTestFieldDeltaComponent.Field1)); + acknowledgedTick = server.Timing.CurTick; + }); + + await server.WaitRunTicks(1); + + IComponentState? state = null; + await server.WaitPost(() => + { + var component = server.EntMan.GetComponent(entity); + component.Field3 = 303; + server.EntMan.DirtyField(entity, component, nameof(AutoNetworkingTestFieldDeltaComponent.Field3)); + state = server.EntMan.GetComponentState(server.EntMan.EventBus, component, null, acknowledgedTick); + }); + + Assert.That(state?.GetType().Name, Is.EqualTo("Field3_FieldComponentState")); + } + } [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] @@ -220,3 +346,16 @@ public abstract partial class AutoNetworkingTestParentComponent : Component [DataField] public int Parent = 4; } + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] +public sealed partial class AutoNetworkingTestFieldDeltaComponent : Component +{ + [DataField, AutoNetworkedField] + public int Field1 = 1; + + [DataField] // Add one field that is not networked to see if the next one get indexed correctly. + public int Field2 = 2; + + [DataField, AutoNetworkedField] + public int Field3 = 3; +} diff --git a/Robust.Shared.IntegrationTests/GameState/DeletionNetworkingTests.cs b/Robust.Shared.IntegrationTests/GameState/DeletionNetworkingTests.cs index 48b050625d0..86c4de18a61 100644 --- a/Robust.Shared.IntegrationTests/GameState/DeletionNetworkingTests.cs +++ b/Robust.Shared.IntegrationTests/GameState/DeletionNetworkingTests.cs @@ -28,7 +28,6 @@ public async Task DeletionNetworkingTest() await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); - var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var cEntMan = client.ResolveDependency(); var netMan = client.ResolveDependency(); @@ -61,13 +60,13 @@ async Task RunTicks() await server.WaitPost(() => { mapSys.CreateMap(out var mapId); - var gridComp = mapMan.CreateGridEntity(mapId); + var gridComp = mapSys.CreateGridEntity(mapId); mapSys.SetTile(gridComp, Vector2i.Zero, new Tile(1)); grid1 = gridComp.Owner; xformSys.SetLocalPosition(grid1, new Vector2(-2,0)); grid1Net = sEntMan.GetNetEntity(grid1); - gridComp = mapMan.CreateGridEntity(mapId); + gridComp = mapSys.CreateGridEntity(mapId); mapSys.SetTile(gridComp, Vector2i.Zero, new Tile(1)); grid2 = gridComp.Owner; xformSys.SetLocalPosition(grid2, new Vector2(2,0)); diff --git a/Robust.Shared.IntegrationTests/GameState/NoSharedReferencesTest.cs b/Robust.Shared.IntegrationTests/GameState/NoSharedReferencesTest.cs index 9313bede30e..5c17b888c14 100644 --- a/Robust.Shared.IntegrationTests/GameState/NoSharedReferencesTest.cs +++ b/Robust.Shared.IntegrationTests/GameState/NoSharedReferencesTest.cs @@ -1,10 +1,11 @@ using System; +using System.Collections.Generic; using NUnit.Framework; using Robust.Client.GameStates; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; -using Robust.Shared.Network; using Robust.Shared.Serialization; +using Robust.Shared.Timing; using System.Linq; using System.Threading.Tasks; using Robust.Shared.Analyzers; @@ -27,16 +28,11 @@ public async Task ReferencesAreNotShared() { var serverOpts = new ServerIntegrationOptions { Pool = false }; var clientOpts = new ClientIntegrationOptions { Pool = false }; - var server = StartServer(serverOpts); - var client = StartClient(clientOpts); - - await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); - var netMan = client.ResolveDependency(); + await using var pair = await StartConnectedPair(serverOpts, clientOpts); + var server = pair.Server; + var client = pair.Client; var clientGameStateManager = client.ResolveDependency(); - Assert.DoesNotThrow(() => client.SetConnectTarget(server)); - client.Post(() => netMan.ClientConnect(null!, 0, null!)); - // Set up map. server.Post(() => { @@ -92,18 +88,123 @@ await server.WaitPost(() => // wait for errors. await RunTicks(); - async Task RunTicks() + Task RunTicks() => RunTicksSync(server, client, 10); + } + + [Test] + public async Task DeltaStateCollectionReferencesAreNotShared() + { + var serverOpts = new ServerIntegrationOptions { Pool = false }; + var clientOpts = new ClientIntegrationOptions { Pool = false }; + await using var pair = await StartConnectedPair(serverOpts, clientOpts); + var server = pair.Server; + var client = pair.Client; + var clientGameStateManager = client.ResolveDependency(); + + server.Post(() => + { + server.System().CreateMap(); + }); + + await RunTicks(); + + EntityUid sPlayer = default; + EntityUid cPlayer = default; + List clientValues = default!; + DeltaCollectionAutogeneratedComponent.DeltaCollectionAutogeneratedComponent_AutoState oldState = default!; + var sEntMan = server.EntMan; + + await server.WaitPost(() => + { + sPlayer = sEntMan.Spawn(); + sEntMan.AddComponent(sPlayer, new DeltaCollectionAutogeneratedComponent()); + sEntMan.AddComponent(sPlayer, new SingleDeltaCollectionAutogeneratedComponent()); + + var session = server.PlayerMan.Sessions.First(); + server.PlayerMan.SetAttachedEntity(session, sPlayer); + server.PlayerMan.JoinGame(session); + }); + + await server.WaitPost(() => + { + var comp = sEntMan.GetComponent(sPlayer); + var state = (DeltaCollectionAutogeneratedComponent.DeltaCollectionAutogeneratedComponent_AutoState) sEntMan + .GetComponentState(sEntMan.EventBus, comp, null, GameTick.Zero)!; + + Assert.That(state.Values, Is.EqualTo(comp.Values)); + Assert.That(ReferenceEquals(state.Values, comp.Values), Is.True); + }); + + await RunTicks(); + + await client.WaitPost(() => + { + var cEntMan = client.EntMan; + cPlayer = cEntMan.GetEntity(server.EntMan.GetNetEntity(sPlayer)); + Assert.That(cEntMan.TryGetComponent(cPlayer, out DeltaCollectionAutogeneratedComponent? comp)); + + var componentStates = clientGameStateManager.GetFullRep()[cEntMan.GetNetEntity(cPlayer)]; + oldState = (DeltaCollectionAutogeneratedComponent.DeltaCollectionAutogeneratedComponent_AutoState) componentStates + .First(x => x.Value is DeltaCollectionAutogeneratedComponent.DeltaCollectionAutogeneratedComponent_AutoState) + .Value!; + + Assert.That(comp!.Values, Is.EqualTo(oldState.Values)); + Assert.That(ReferenceEquals(comp.Values, oldState.Values), Is.False); + clientValues = comp.Values; + + var localState = (DeltaCollectionAutogeneratedComponent.DeltaCollectionAutogeneratedComponent_AutoState) cEntMan + .GetComponentState(cEntMan.EventBus, comp, null, GameTick.Zero)!; + + Assert.That(localState.Values, Is.EqualTo(comp.Values)); + Assert.That(ReferenceEquals(localState.Values, comp.Values), Is.False); + + Assert.That(cEntMan.TryGetComponent(cPlayer, out SingleDeltaCollectionAutogeneratedComponent? singleComp)); + var singleState = (SingleDeltaCollectionAutogeneratedComponent.SingleDeltaCollectionAutogeneratedComponent_AutoState) componentStates + .First(x => x.Value is SingleDeltaCollectionAutogeneratedComponent.SingleDeltaCollectionAutogeneratedComponent_AutoState) + .Value!; + + Assert.That(singleComp!.Values, Is.EqualTo(singleState.Values)); + Assert.That(ReferenceEquals(singleComp.Values, singleState.Values), Is.False); + + var singleLocalState = (SingleDeltaCollectionAutogeneratedComponent.SingleDeltaCollectionAutogeneratedComponent_AutoState) cEntMan + .GetComponentState(cEntMan.EventBus, singleComp, null, GameTick.Zero)!; + + Assert.That(singleLocalState.Values, Is.EqualTo(singleComp.Values)); + Assert.That(ReferenceEquals(singleLocalState.Values, singleComp.Values), Is.False); + }); + + await client.WaitPost(() => { - for (int i = 0; i < 10; i++) + var deltaState = new DeltaCollectionAutogeneratedComponent.Counter_FieldComponentState { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } - } - - await client.WaitPost(() => netMan.ClientDisconnect("")); - await server.WaitRunTicks(5); - await client.WaitRunTicks(5); + Counter = 1 + }; + var newState = deltaState.CreateNewFullState(oldState); + + Assert.That(newState, Is.Not.SameAs(oldState)); + Assert.That(newState.Counter, Is.EqualTo(1)); + Assert.That(newState.Values, Is.EqualTo(oldState.Values)); + Assert.That(ReferenceEquals(newState.Values, oldState.Values), Is.False); + }); + + await server.WaitPost(() => + { + var comp = sEntMan.GetComponent(sPlayer); + comp.Values.Clear(); + comp.Values.Add(4); + sEntMan.Dirty(sPlayer, comp); + }); + + await RunTicks(); + + await client.WaitPost(() => + { + var comp = client.EntMan.GetComponent(cPlayer); + Assert.That(comp.Values, Is.SameAs(clientValues)); + Assert.That(comp.Values, Is.EqualTo(new[] { 4 })); + }); + + Task RunTicks() => RunTicksSync(server, client, 10); } } @@ -116,3 +217,20 @@ public sealed partial class ExampleAutogeneratedComponent : Component [Serializable, NetSerializable] public sealed record ExampleObject(int Value); } + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] +public sealed partial class DeltaCollectionAutogeneratedComponent : Component +{ + [AutoNetworkedField] + public List Values = new() { 1, 2, 3 }; + + [AutoNetworkedField] + public int Counter; +} + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] +public sealed partial class SingleDeltaCollectionAutogeneratedComponent : Component +{ + [AutoNetworkedField] + public List Values = new() { 1, 2, 3 }; +} diff --git a/Robust.Shared.IntegrationTests/GlobalUsings.cs b/Robust.Shared.IntegrationTests/GlobalUsings.cs index 316ff0ec590..da7c7fa4c32 100644 --- a/Robust.Shared.IntegrationTests/GlobalUsings.cs +++ b/Robust.Shared.IntegrationTests/GlobalUsings.cs @@ -1 +1,3 @@ -global using Is = NUnit.Framework.Is; +global using Is = NUnit.Framework.Is; + +[assembly: NUnit.Framework.Parallelizable(NUnit.Framework.ParallelScope.Fixtures)] diff --git a/Robust.Shared.IntegrationTests/Map/EntityCoordinates_Tests.cs b/Robust.Shared.IntegrationTests/Map/EntityCoordinates_Tests.cs index f43ccb44fcd..c2c3035dab4 100644 --- a/Robust.Shared.IntegrationTests/Map/EntityCoordinates_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/EntityCoordinates_Tests.cs @@ -46,7 +46,6 @@ public void IsValid_InvalidEntId_False() public void IsValid_EntityDeleted_False() { var entityManager = IoCManager.Resolve(); - var mapManager = IoCManager.Resolve(); var mapEntity = entityManager.System().CreateMap(out var mapId); var newEnt = entityManager.CreateEntityUninitialized(null, new MapCoordinates(Vector2.Zero, mapId)); @@ -122,11 +121,11 @@ public void GetGridId_Map() public void GetGridId_Grid() { var entityManager = IoCManager.Resolve(); - var mapManager = IoCManager.Resolve(); + var mapSystem = entityManager.System(); var xformSys = entityManager.System(); entityManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridEnt = grid.Owner; var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(gridEnt, Vector2.Zero)); @@ -152,11 +151,11 @@ public void GetMapId_Map() public void GetMapId_Grid() { var entityManager = IoCManager.Resolve(); - var mapManager = IoCManager.Resolve(); + var mapSystem = entityManager.System(); var xformSys = entityManager.System(); entityManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridEnt = grid.Owner; var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(gridEnt, Vector2.Zero)); @@ -168,11 +167,11 @@ public void GetMapId_Grid() public void GetParent() { var entityManager = IoCManager.Resolve(); - var mapManager = IoCManager.Resolve(); + var mapSystem = entityManager.System(); var xformSys = entityManager.System(); var mapEnt = entityManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridEnt = grid.Owner; var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(grid, Vector2.Zero)); @@ -190,11 +189,11 @@ public void GetParent() public void TryGetParent() { var entityManager = IoCManager.Resolve(); - var mapManager = IoCManager.Resolve(); + var mapSystem = entityManager.System(); var xformSys = entityManager.System(); var mapEnt = entityManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridEnt = grid.Owner; var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(grid, Vector2.Zero)); @@ -241,11 +240,11 @@ public void ToMap_MoveGrid(float x1, float y1, float x2, float y2) var entPos = new Vector2(x2, y2); var entityManager = IoCManager.Resolve(); - var mapManager = IoCManager.Resolve(); + var mapSystem = entityManager.System(); var xformSys = entityManager.System(); entityManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridEnt = grid.Owner; var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(grid, entPos)); var newXform = entityManager.GetComponent(newEnt); @@ -263,11 +262,11 @@ public void ToMap_MoveGrid(float x1, float y1, float x2, float y2) public void WithEntityId() { var entityManager = IoCManager.Resolve(); - var mapManager = IoCManager.Resolve(); + var mapSystem = entityManager.System(); var xformSys = entityManager.System(); var mapEnt = entityManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridEnt = grid.Owner; var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(grid, Vector2.Zero)); var newEntXform = entityManager.GetComponent(newEnt); diff --git a/Robust.Shared.IntegrationTests/Map/GridCollision_Test.cs b/Robust.Shared.IntegrationTests/Map/GridCollision_Test.cs index b3726f19964..f2c6294ef17 100644 --- a/Robust.Shared.IntegrationTests/Map/GridCollision_Test.cs +++ b/Robust.Shared.IntegrationTests/Map/GridCollision_Test.cs @@ -19,7 +19,6 @@ public async Task TestGridsCollide() await server.WaitIdleAsync(); - var mapManager = server.ResolveDependency(); var entManager = server.ResolveDependency(); var physSystem = server.ResolveDependency().GetEntitySystem(); var mapSystem = server.ResolveDependency().GetEntitySystem(); @@ -35,8 +34,8 @@ public async Task TestGridsCollide() await server.WaitPost(() => { entManager.System().CreateMap(out mapId); - gridId1 = mapManager.CreateGridEntity(mapId); - gridId2 = mapManager.CreateGridEntity(mapId); + gridId1 = mapSystem.CreateGridEntity(mapId); + gridId2 = mapSystem.CreateGridEntity(mapId); gridEnt1 = gridId1.Value.Owner; gridEnt2 = gridId2.Value.Owner; physics1 = entManager.GetComponent(gridEnt1.Value); diff --git a/Robust.Shared.IntegrationTests/Map/GridContraction_Test.cs b/Robust.Shared.IntegrationTests/Map/GridContraction_Test.cs index a2eb87253f6..3256bd6abd6 100644 --- a/Robust.Shared.IntegrationTests/Map/GridContraction_Test.cs +++ b/Robust.Shared.IntegrationTests/Map/GridContraction_Test.cs @@ -17,13 +17,12 @@ public async Task TestGridDeletes() await server.WaitIdleAsync(); var entManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); var mapSystem = entManager.EntitySysManager.GetEntitySystem(); await server.WaitAssertion(() => { entManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridEntity = grid.Owner; for (var i = 0; i < 10; i++) @@ -56,13 +55,12 @@ public async Task TestGridNoDeletes() await server.WaitIdleAsync(); var entManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); var mapSystem = entManager.System(); await server.WaitAssertion(() => { entManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); for (var i = 0; i < 10; i++) { diff --git a/Robust.Shared.IntegrationTests/Map/GridFixtures_Tests.cs b/Robust.Shared.IntegrationTests/Map/GridFixtures_Tests.cs index f7374c83b6e..34c12cd20b1 100644 --- a/Robust.Shared.IntegrationTests/Map/GridFixtures_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/GridFixtures_Tests.cs @@ -29,8 +29,8 @@ public void TestGridFixtureDeletion() var server = RobustServerSimulation.NewSimulation().InitializeInstance(); var map = server.CreateMap(); var entManager = server.Resolve(); - var grid = server.Resolve().CreateGridEntity(map.MapId); var mapSystem = entManager.System(); + var grid = mapSystem.CreateGridEntity(map.MapId); var fixtures = entManager.GetComponent(grid); mapSystem.SetTiles(grid, new List<(Vector2i GridIndices, Tile Tile)>() @@ -57,14 +57,13 @@ public async Task TestGridFixtures() await server.WaitIdleAsync(); var entManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); var physSystem = server.ResolveDependency().GetEntitySystem(); var mapSystem = entManager.EntitySysManager.GetEntitySystem(); await server.WaitAssertion(() => { entManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); // Should be nothing if grid empty Assert.That(entManager.TryGetComponent(grid, out PhysicsComponent? gridBody)); diff --git a/Robust.Shared.IntegrationTests/Map/GridMerge_Tests.cs b/Robust.Shared.IntegrationTests/Map/GridMerge_Tests.cs index 0ff7aa9c4c9..fc70be68010 100644 --- a/Robust.Shared.IntegrationTests/Map/GridMerge_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/GridMerge_Tests.cs @@ -40,14 +40,13 @@ private ISimulation GetSim() public void Merge(Vector2i offset, Angle angle, Box2 bounds) { var sim = GetSim(); - var mapManager = sim.Resolve(); var entMan = sim.Resolve(); var mapSystem = entMan.System(); var gridFixtures = entMan.System(); var mapId = sim.CreateMap().MapId; - var grid1 = mapManager.CreateGridEntity(mapId); - var grid2 = mapManager.CreateGridEntity(mapId); + var grid1 = mapSystem.CreateGridEntity(mapId); + var grid2 = mapSystem.CreateGridEntity(mapId); var tiles = new List<(Vector2i, Tile)>(); for (var y = 0; y < 3; y++) @@ -58,11 +57,11 @@ public void Merge(Vector2i offset, Angle angle, Box2 bounds) mapSystem.SetTiles(grid1, tiles); mapSystem.SetTiles(grid2, tiles); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(2)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(2)); gridFixtures.Merge(grid1.Owner, grid2.Owner, offset, angle, grid1.Comp, grid2.Comp); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(1)); Assert.That(grid1.Comp.LocalAABB, Is.EqualTo(bounds)); } diff --git a/Robust.Shared.IntegrationTests/Map/GridRotation_Tests.cs b/Robust.Shared.IntegrationTests/Map/GridRotation_Tests.cs index d73e86ff9b2..4cefd9fcb75 100644 --- a/Robust.Shared.IntegrationTests/Map/GridRotation_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/GridRotation_Tests.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; using System.Numerics; -using System.Threading.Tasks; using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.Map; @@ -24,14 +21,13 @@ public async Task TestLocalWorldConversions() await server.WaitIdleAsync(); var entMan = server.ResolveDependency(); - var mapMan = server.ResolveDependency(); var mapSystem = entMan.System(); var transformSystem = entMan.System(); await server.WaitAssertion(() => { mapSystem.CreateMap(out var mapId); - var grid = mapMan.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridEnt = grid.Owner; var coordinates = new EntityCoordinates(gridEnt, new Vector2(10, 0)); @@ -65,13 +61,13 @@ public async Task TestChunkRotations() await server.WaitIdleAsync(); var entMan = server.ResolveDependency(); - var mapMan = server.ResolveDependency(); var mapSystem = entMan.System(); + var xformSystem = entMan.System(); await server.WaitAssertion(() => { mapSystem.CreateMap(out var mapId); - var grid = mapMan.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridEnt = grid.Owner; /* Test for map chunk rotations */ @@ -95,19 +91,20 @@ await server.WaitAssertion(() => // With all cardinal directions these should align. Assert.That(aabb, Is.EqualTo(bounds)); - entMan.GetComponent(gridEnt).LocalRotation = new Angle(Math.PI); + var gridXform = entMan.GetComponent(gridEnt); + xformSystem.SetLocalRotationNoLerp(gridEnt, new Angle(Math.PI), gridXform); aabb = mapSystem.CalcWorldAABB(gridEnt, grid, chunk); bounds = new Box2(new Vector2(-2, -10), new Vector2(0, 0)); Assert.That(aabb.EqualsApprox(bounds), $"Expected bounds of {aabb} and got {bounds}"); - entMan.GetComponent(gridEnt).LocalRotation = new Angle(-Math.PI / 2); + xformSystem.SetLocalRotationNoLerp(gridEnt, new Angle(-Math.PI / 2), gridXform); aabb = mapSystem.CalcWorldAABB(gridEnt, grid, chunk); bounds = new Box2(new Vector2(0, -2), new Vector2(10, 0)); Assert.That(aabb.EqualsApprox(bounds), $"Expected bounds of {aabb} and got {bounds}"); - entMan.GetComponent(gridEnt).LocalRotation = new Angle(-Math.PI / 4); + xformSystem.SetLocalRotationNoLerp(gridEnt, new Angle(-Math.PI / 4), gridXform); aabb = mapSystem.CalcWorldAABB(gridEnt, grid, chunk); bounds = new Box2(new Vector2(0, -1.4142135f), new Vector2(8.485281f, 7.071068f)); diff --git a/Robust.Shared.IntegrationTests/Map/GridSplit_Tests.cs b/Robust.Shared.IntegrationTests/Map/GridSplit_Tests.cs index 447c4ea9473..beca059b306 100644 --- a/Robust.Shared.IntegrationTests/Map/GridSplit_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/GridSplit_Tests.cs @@ -31,11 +31,10 @@ private ISimulation GetSim() public void NoSplit() { var sim = GetSim(); - var mapManager = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; - var gridEnt = mapManager.CreateGridEntity(mapId); + var gridEnt = mapSystem.CreateGridEntity(mapId); var grid = gridEnt.Comp; grid.CanSplit = false; @@ -44,14 +43,14 @@ public void NoSplit() mapSystem.SetTile(gridEnt, new Vector2i(x, 0), new Tile(1)); } - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(1)); mapSystem.SetTile(gridEnt, new Vector2i(1, 0), Tile.Empty); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(1)); grid.CanSplit = true; mapSystem.SetTile(gridEnt, new Vector2i(2, 0), Tile.Empty); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(2)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(2)); mapSystem.DeleteMap(mapId); } @@ -60,20 +59,19 @@ public void NoSplit() public void SimpleSplit() { var sim = GetSim(); - var mapManager = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; - var gridEnt = mapManager.CreateGridEntity(mapId); + var gridEnt = mapSystem.CreateGridEntity(mapId); for (var x = 0; x < 3; x++) { mapSystem.SetTile(gridEnt, new Vector2i(x, 0), new Tile(1)); } - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(1)); mapSystem.SetTile(gridEnt, new Vector2i(1, 0), Tile.Empty); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(2)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(2)); mapSystem.DeleteMap(mapId); } @@ -82,10 +80,9 @@ public void SimpleSplit() public void DonutSplit() { var sim = GetSim(); - var mapManager = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; - var gridEnt = mapManager.CreateGridEntity(mapId); + var gridEnt = mapSystem.CreateGridEntity(mapId); for (var x = 0; x < 3; x++) { @@ -95,16 +92,16 @@ public void DonutSplit() } } - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(1)); mapSystem.SetTile(gridEnt, Vector2i.One, Tile.Empty); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(1)); mapSystem.SetTile(gridEnt, new Vector2i(1, 2), Tile.Empty); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(1)); mapSystem.SetTile(gridEnt, new Vector2i(1, 0), Tile.Empty); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(2)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(2)); mapSystem.DeleteMap(mapId); } @@ -113,10 +110,9 @@ public void DonutSplit() public void TriSplit() { var sim = GetSim(); - var mapManager = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; - var gridEnt = mapManager.CreateGridEntity(mapId); + var gridEnt = mapSystem.CreateGridEntity(mapId); for (var x = 0; x < 3; x++) { @@ -125,10 +121,10 @@ public void TriSplit() mapSystem.SetTile(gridEnt, Vector2i.One, new Tile(1)); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(1)); mapSystem.SetTile(gridEnt, new Vector2i(1, 0), Tile.Empty); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(3)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(3)); mapSystem.DeleteMap(mapId); } @@ -141,11 +137,10 @@ public void ReparentSplit() { var sim = GetSim(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var mapSystem = sim.Resolve().System(); var transformSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; - var gridEnt = mapManager.CreateGridEntity(mapId); + var gridEnt = mapSystem.CreateGridEntity(mapId); var grid = gridEnt.Comp; for (var x = 0; x < 4; x++) @@ -153,7 +148,7 @@ public void ReparentSplit() mapSystem.SetTile(gridEnt, new Vector2i(x, 0), new Tile(1)); } - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(1)); var dummy = entManager.SpawnEntity(null, new EntityCoordinates(gridEnt, new Vector2(3.5f, 0.5f))); var dummyXform = entManager.GetComponent(dummy); @@ -164,9 +159,9 @@ public void ReparentSplit() Assert.That(anchoredXform.Anchored); mapSystem.SetTile(gridEnt, new Vector2i(2, 0), Tile.Empty); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(2)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(2)); - var newGrid = mapManager.GetAllGrids(mapId).First(x => x.Comp != grid); + var newGrid = mapSystem.GetAllGrids(mapId).First(x => x.Comp != grid); var newGridXform = entManager.GetComponent(newGrid.Owner); Assert.Multiple(() => diff --git a/Robust.Shared.IntegrationTests/Map/MapGridMap_Tests.cs b/Robust.Shared.IntegrationTests/Map/MapGridMap_Tests.cs index 7c6fef1c58f..782cb987de2 100644 --- a/Robust.Shared.IntegrationTests/Map/MapGridMap_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/MapGridMap_Tests.cs @@ -21,16 +21,15 @@ public void FindGrids() var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var mapSystem = entManager.System(); var mapId = sim.CreateMap().MapId; List> grids = []; - mapManager.FindGridsIntersecting(mapId, Box2.UnitCentered, ref grids); + mapSystem.FindGridsIntersecting(mapId, Box2.UnitCentered, ref grids); Assert.That(grids, Is.Empty); entManager.AddComponent(mapSystem.GetMapOrInvalid(mapId)); - mapManager.FindGridsIntersecting(mapId, Box2.UnitCentered, ref grids); + mapSystem.FindGridsIntersecting(mapId, Box2.UnitCentered, ref grids); Assert.That(grids, Has.Count.EqualTo(1)); } @@ -43,11 +42,10 @@ public void AddGridCompToMap() var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var mapSystem = entManager.System(); var mapId = sim.CreateMap().MapId; - mapManager.CreateGridEntity(mapId); + mapSystem.CreateGridEntity(mapId); Assert.DoesNotThrow(() => { diff --git a/Robust.Shared.IntegrationTests/Map/MapGrid_Tests.cs b/Robust.Shared.IntegrationTests/Map/MapGrid_Tests.cs index 1ba57f5825e..0aea19113e2 100644 --- a/Robust.Shared.IntegrationTests/Map/MapGrid_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/MapGrid_Tests.cs @@ -30,12 +30,11 @@ private static ISimulation SimulationFactory() public void GetTileRefCoords() { var sim = SimulationFactory(); - var mapMan = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; var gridOptions = new GridCreateOptions(); gridOptions.ChunkSize = 8; - var grid = mapMan.CreateGridEntity(mapId, gridOptions); + var grid = mapSystem.CreateGridEntity(mapId, gridOptions); mapSystem.SetTile(grid, new Vector2i(-9, -1), new Tile(typeId: 1, flags: 1, variant: 1)); var result = mapSystem.GetTileRef(grid.Owner, grid.Comp, new Vector2i(-9, -1)); @@ -52,13 +51,12 @@ public void GetTileRefCoords() public void BoundsExpansion() { var sim = SimulationFactory(); - var mapMan = sim.Resolve(); var mapSystem = sim.Resolve().System(); var transformSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; var gridOptions = new GridCreateOptions(); gridOptions.ChunkSize = 8; - var grid = mapMan.CreateGridEntity(mapId, gridOptions); + var grid = mapSystem.CreateGridEntity(mapId, gridOptions); transformSystem.SetWorldPosition(grid, new Vector2(3, 5)); mapSystem.SetTile(grid, new Vector2i(-1, -2), new Tile(1)); @@ -80,13 +78,12 @@ public void BoundsExpansion() public void BoundsContract() { var sim = SimulationFactory(); - var mapMan = sim.Resolve(); var mapSystem = sim.Resolve().System(); var transformSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; var gridOptions = new GridCreateOptions(); gridOptions.ChunkSize = 8; - var grid = mapMan.CreateGridEntity(mapId, gridOptions); + var grid = mapSystem.CreateGridEntity(mapId, gridOptions); transformSystem.SetWorldPosition(grid, new Vector2(3, 5)); @@ -108,12 +105,11 @@ public void BoundsContract() public void GridTileToChunkIndices() { var sim = SimulationFactory(); - var mapMan = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; var gridOptions = new GridCreateOptions(); gridOptions.ChunkSize = 8; - var grid = mapMan.CreateGridEntity(mapId, gridOptions); + var grid = mapSystem.CreateGridEntity(mapId, gridOptions); var result = mapSystem.GridTileToChunkIndices(grid.Comp, new Vector2i(-9, -1)); @@ -127,12 +123,11 @@ public void GridTileToChunkIndices() public void ToLocalCentered() { var sim = SimulationFactory(); - var mapMan = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; var gridOptions = new GridCreateOptions(); gridOptions.ChunkSize = 8; - var grid = mapMan.CreateGridEntity(mapId, gridOptions); + var grid = mapSystem.CreateGridEntity(mapId, gridOptions); var result = mapSystem.GridTileToLocal(grid.Owner, grid.Comp, new Vector2i(0, 0)).Position; @@ -144,12 +139,11 @@ public void ToLocalCentered() public void TryGetTileRefNoTile() { var sim = SimulationFactory(); - var mapMan = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; var gridOptions = new GridCreateOptions(); gridOptions.ChunkSize = 8; - var grid = mapMan.CreateGridEntity(mapId, gridOptions); + var grid = mapSystem.CreateGridEntity(mapId, gridOptions); var foundTile = mapSystem.TryGetTileRef(grid.Owner, grid.Comp, new Vector2i(-9, -1), out var tileRef) ; @@ -162,12 +156,11 @@ public void TryGetTileRefNoTile() public void TryGetTileRefTileExists() { var sim = SimulationFactory(); - var mapMan = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; var gridOptions = new GridCreateOptions(); gridOptions.ChunkSize = 8; - var grid = mapMan.CreateGridEntity(mapId, gridOptions); + var grid = mapSystem.CreateGridEntity(mapId, gridOptions); mapSystem.SetTile(grid, new Vector2i(-9, -1), new Tile(typeId: 1, flags: 1, variant: 1)); @@ -183,12 +176,11 @@ public void TryGetTileRefTileExists() public void PointCollidesWithGrid() { var sim = SimulationFactory(); - var mapMan = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; var gridOptions = new GridCreateOptions(); gridOptions.ChunkSize = 8; - var grid = mapMan.CreateGridEntity(mapId, gridOptions); + var grid = mapSystem.CreateGridEntity(mapId, gridOptions); mapSystem.SetTile(grid, new Vector2i(19, 23), new Tile(1)); @@ -201,12 +193,11 @@ public void PointCollidesWithGrid() public void PointNotCollideWithGrid() { var sim = SimulationFactory(); - var mapMan = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; var gridOptions = new GridCreateOptions(); gridOptions.ChunkSize = 8; - var grid = mapMan.CreateGridEntity(mapId, gridOptions); + var grid = mapSystem.CreateGridEntity(mapId, gridOptions); mapSystem.SetTile(grid, new Vector2i(19, 23), new Tile(1)); diff --git a/Robust.Shared.IntegrationTests/Map/MapManager_Tests.cs b/Robust.Shared.IntegrationTests/Map/MapManager_Tests.cs deleted file mode 100644 index 12d4a4604e4..00000000000 --- a/Robust.Shared.IntegrationTests/Map/MapManager_Tests.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Numerics; -using NUnit.Framework; -using Robust.Shared.GameObjects; -using Robust.Shared.Map; -using Robust.Shared.Map.Components; -using Robust.UnitTesting.Server; - -namespace Robust.UnitTesting.Shared.Map -{ - [TestFixture, TestOf(typeof(MapManager))] - internal sealed class MapManagerTests - { - private static ISimulation SimulationFactory() - { - var sim = RobustServerSimulation - .NewSimulation() - .InitializeInstance(); - - return sim; - } - - /// - /// When the map manager is restarted, the maps are deleted. - /// - [Test] - public void Restart_ExistingMap_IsRemoved() - { - var sim = SimulationFactory(); - var mapMan = sim.Resolve(); - var entMan = sim.Resolve(); - var mapSys = entMan.System(); - - var mapID = sim.CreateMap().MapId; - - mapMan.Restart(); - - Assert.That(mapSys.MapExists(mapID), Is.False); - } - - /// - /// When the map manager is restarted, the grids are removed. - /// - [Test] - public void Restart_ExistingGrid_IsRemoved() - { - var sim = SimulationFactory(); - var mapMan = sim.Resolve(); - var entMan = sim.Resolve(); - - var mapID = sim.CreateMap().MapId; - var grid = mapMan.CreateGridEntity(mapID); - - mapMan.Restart(); - - Assert.That(entMan.HasComponent(grid), Is.False); - } - - /// - /// When entities are flushed check nullsapce is also culled. - /// - [Test] - public void Restart_NullspaceMap_IsEmptied() - { - var sim = SimulationFactory(); - var entMan = sim.Resolve(); - var oldEntity = entMan.CreateEntityUninitialized(null, MapCoordinates.Nullspace); - entMan.InitializeEntity(oldEntity); - entMan.FlushEntities(); - Assert.That(entMan.Deleted(oldEntity), Is.True); - } - - [Test] - public void Restart_MapEntity_IsRemoved() - { - var sim = SimulationFactory(); - var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); - var entity = entMan.System().CreateMap(); - mapMan.Restart(); - Assert.That((!entMan.EntityExists(entity) ? EntityLifeStage.Deleted : entMan.GetComponent(entity).EntityLifeStage) >= EntityLifeStage.Deleted, Is.True); - } - } -} diff --git a/Robust.Shared.IntegrationTests/Map/MapPauseTests.cs b/Robust.Shared.IntegrationTests/Map/MapPauseTests.cs index 3627df9311b..55effd21968 100644 --- a/Robust.Shared.IntegrationTests/Map/MapPauseTests.cs +++ b/Robust.Shared.IntegrationTests/Map/MapPauseTests.cs @@ -26,7 +26,6 @@ public void Paused_NotIncluded_NotInQuery() { var sim = SimulationFactory(); var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); // arrange var mapId = sim.CreateMap().Uid; @@ -47,7 +46,6 @@ public void UnPaused_NotIncluded_InQuery() { var sim = SimulationFactory(); var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); // arrange var mapId = sim.CreateMap().Uid; @@ -68,7 +66,6 @@ public void Paused_Included_InQuery() { var sim = SimulationFactory(); var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); // arrange var mapId = sim.CreateMap().Uid; @@ -89,7 +86,6 @@ public void Paused_AddEntity_IsPaused() { var sim = SimulationFactory(); var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); // arrange var mapId = sim.CreateMap().Uid; @@ -108,7 +104,6 @@ public void UnPaused_AddEntity_IsNotPaused() { var sim = SimulationFactory(); var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); // arrange var mapId = sim.CreateMap().Uid; @@ -127,14 +122,14 @@ public void Paused_AddGrid_GridPaused() { var sim = SimulationFactory(); var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); + var mapSys = entMan.System(); // arrange var mapId = sim.CreateMap().MapId; - entMan.System().SetPaused(mapId, true); + mapSys.SetPaused(mapId, true); // act - var newGrid = mapMan.CreateGridEntity(mapId); + var newGrid = mapSys.CreateGridEntity(mapId); // assert var metaData = entMan.GetComponent(newGrid); @@ -199,7 +194,6 @@ public void Paused_UnpauseMap_UnpausedEntities() { var sim = SimulationFactory(); var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); var mapId = sim.CreateMap().Uid; entMan.System().SetPaused(mapId, true); @@ -219,7 +213,6 @@ public void Unpaused_PauseMap_PausedEntities() { var sim = SimulationFactory(); var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); var mapId = sim.CreateMap().Uid; entMan.System().SetPaused(mapId, false); diff --git a/Robust.Shared.IntegrationTests/Map/Query_Tests.cs b/Robust.Shared.IntegrationTests/Map/Query_Tests.cs index ed6b1f369ea..1686c5ceb54 100644 --- a/Robust.Shared.IntegrationTests/Map/Query_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/Query_Tests.cs @@ -57,12 +57,11 @@ public void TestBox2GridIntersection(Vector2 position, float radians, Box2 world var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var mapSystem = entManager.System(); var xformSystem = entManager.System(); var map = mapSystem.CreateMap(); - var grid = mapManager.CreateGridEntity(map); + var grid = mapSystem.CreateGridEntity(map); for (var i = 0; i < 10; i++) { @@ -72,7 +71,7 @@ public void TestBox2GridIntersection(Vector2 position, float radians, Box2 world xformSystem.SetWorldRotation(grid.Owner, radians); var grids = new List>(); - mapManager.FindGridsIntersecting(map, worldAABB, ref grids); + mapSystem.FindGridsIntersecting(map, worldAABB, ref grids); Assert.That(grids.Count > 0, Is.EqualTo(result)); } diff --git a/Robust.Shared.IntegrationTests/Map/SingleTileRemoveTest.cs b/Robust.Shared.IntegrationTests/Map/SingleTileRemoveTest.cs index ada8ffa7d49..6ceda8406f9 100644 --- a/Robust.Shared.IntegrationTests/Map/SingleTileRemoveTest.cs +++ b/Robust.Shared.IntegrationTests/Map/SingleTileRemoveTest.cs @@ -28,7 +28,6 @@ public async Task TestRemoveSingleTile() await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); - var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); var sPlayerMan = server.ResolveDependency(); @@ -74,7 +73,7 @@ public async Task TestRemoveSingleTile() await server.WaitPost(() => { sMap = sys.CreateMap(out var mapId); - var comp = mapMan.CreateGridEntity(mapId); + var comp = sys.CreateGridEntity(mapId); grid = (comp.Owner, comp); sys.SetTile(grid, grid, new Vector2i(0, 0), new Tile(typeId: 1, flags: 1, variant: 1)); var coords = new EntityCoordinates(grid, 0.5f, 0.5f); diff --git a/Robust.Shared.IntegrationTests/Physics/BroadphaseNetworkingTest.cs b/Robust.Shared.IntegrationTests/Physics/BroadphaseNetworkingTest.cs index db24213e235..026d64066a1 100644 --- a/Robust.Shared.IntegrationTests/Physics/BroadphaseNetworkingTest.cs +++ b/Robust.Shared.IntegrationTests/Physics/BroadphaseNetworkingTest.cs @@ -33,7 +33,6 @@ public async Task TestBroadphaseNetworking() await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); - var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var cEntMan = client.ResolveDependency(); var netMan = client.ResolveDependency(); @@ -60,7 +59,7 @@ public async Task TestBroadphaseNetworking() await server.WaitPost(() => { map1 = mapSystem.CreateMap(out var mapId); - var gridEnt = mapMan.CreateGridEntity(mapId); + var gridEnt = mapSystem.CreateGridEntity(mapId); mapSystem.SetTile(gridEnt, Vector2i.Zero, new Tile(1)); grid1 = gridEnt.Owner; }); @@ -130,7 +129,7 @@ await server.WaitPost(() => { // Create grid map2 = mapSystem.CreateMap(out var mapId); - var gridEnt = mapMan.CreateGridEntity(mapId); + var gridEnt = mapSystem.CreateGridEntity(mapId); mapSystem.SetTile(gridEnt, Vector2i.Zero, new Tile(1)); grid2 = gridEnt.Owner; diff --git a/Robust.Shared.IntegrationTests/Physics/Broadphase_Test.cs b/Robust.Shared.IntegrationTests/Physics/Broadphase_Test.cs index cb87f2dbaab..2f90b231d25 100644 --- a/Robust.Shared.IntegrationTests/Physics/Broadphase_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/Broadphase_Test.cs @@ -77,12 +77,11 @@ public void ReparentSundries() { var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var mapSys = entManager.System(); var xformSys = entManager.System(); var (mapEnt, mapId) = sim.CreateMap(); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSys.CreateGridEntity(mapId); mapSys.SetTile(grid, Vector2i.Zero, new Tile(1)); Assert.That(entManager.HasComponent(grid)); @@ -111,14 +110,13 @@ public void ReparentBroadphase() { var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var fixturesSystem = entManager.EntitySysManager.GetEntitySystem(); var physicsSystem = entManager.EntitySysManager.GetEntitySystem(); var mapSys = entManager.System(); var xformSys = entManager.System(); var (mapEnt, mapId) = sim.CreateMap(); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSys.CreateGridEntity(mapId); var gridUid = grid.Owner; mapSys.SetTile(grid, Vector2i.Zero, new Tile(1)); @@ -163,13 +161,12 @@ public void GridMapUpdate() { var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var mapSys = entManager.System(); var xformSys = entManager.System(); var (map1, mapId1) = sim.CreateMap(); var (map2, _) = sim.CreateMap(); - var grid = mapManager.CreateGridEntity(mapId1); + var grid = mapSys.CreateGridEntity(mapId1); mapSys.SetTile(grid, Vector2i.Zero, new Tile(1)); var mapBroadphase1 = entManager.GetComponent(map1); @@ -194,14 +191,13 @@ public void BroadphaseRecursiveUpdate() { var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var system = entManager.EntitySysManager; var physicsSystem = system.GetEntitySystem(); var lookup = system.GetEntitySystem(); var mapSys = entManager.System(); var (map, mapId) = sim.CreateMap(); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSys.CreateGridEntity(mapId); mapSys.SetTile(grid, Vector2i.Zero, new Tile(1)); var gridBroadphase = entManager.GetComponent(grid); @@ -244,7 +240,6 @@ public void EntMapChangeRecursiveUpdate() { var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var system = entManager.EntitySysManager; var lookup = system.GetEntitySystem(); var xforms = system.GetEntitySystem(); @@ -257,9 +252,9 @@ public void EntMapChangeRecursiveUpdate() var (mapB, mapBId) = sim.CreateMap(); // setup grids - var gridAComp = mapManager.CreateGridEntity(mapAId); - var gridBComp = mapManager.CreateGridEntity(mapBId); - var gridCComp = mapManager.CreateGridEntity(mapAId); + var gridAComp = mapSys.CreateGridEntity(mapAId); + var gridBComp = mapSys.CreateGridEntity(mapBId); + var gridCComp = mapSys.CreateGridEntity(mapAId); var gridA = gridAComp.Owner; var gridB = gridBComp.Owner; var gridC = gridCComp.Owner; diff --git a/Robust.Shared.IntegrationTests/Physics/CollisionWake_Test.cs b/Robust.Shared.IntegrationTests/Physics/CollisionWake_Test.cs index a4109827a8d..65b824afc7f 100644 --- a/Robust.Shared.IntegrationTests/Physics/CollisionWake_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/CollisionWake_Test.cs @@ -41,7 +41,6 @@ public async Task TestCollisionWakeGrid() await server.WaitIdleAsync(); var entManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); var mapSystem = entManager.System(); var transformSystem = entManager.System(); @@ -56,7 +55,7 @@ public async Task TestCollisionWakeGrid() await server.WaitPost(() => { mapSystem.CreateMap(out mapId); - grid = mapManager.CreateGridEntity(mapId); + grid = mapSystem.CreateGridEntity(mapId); mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1)); entityOne = entManager.SpawnEntity("CollisionWakeTestItem", new MapCoordinates(Vector2.One * 2f, mapId)); diff --git a/Robust.Shared.IntegrationTests/Physics/GridDeletion_Test.cs b/Robust.Shared.IntegrationTests/Physics/GridDeletion_Test.cs index 20b35cdf73c..54214964a33 100644 --- a/Robust.Shared.IntegrationTests/Physics/GridDeletion_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/GridDeletion_Test.cs @@ -27,8 +27,8 @@ public async Task GridDeletionTest() await server.WaitIdleAsync(); var entManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); - var physSystem = server.ResolveDependency().GetEntitySystem(); + var mapSystem = entManager.System(); + var physSystem = entManager.System(); PhysicsComponent physics = default!; @@ -38,7 +38,7 @@ public async Task GridDeletionTest() await server.WaitAssertion(() => { entManager.System().CreateMap(out mapId); - grid = mapManager.CreateGridEntity(mapId); + grid = mapSystem.CreateGridEntity(mapId); physics = entManager.GetComponent(grid); physSystem.SetBodyType(grid, BodyType.Dynamic, body: physics); @@ -55,7 +55,7 @@ await server.WaitAssertion(() => List> grids = []; // So if gridtree is fucky then this SHOULD throw. - mapManager.FindGridsIntersecting(mapId, + mapSystem.FindGridsIntersecting(mapId, new Box2(new Vector2(float.MinValue, float.MinValue), new Vector2(float.MaxValue, float.MaxValue)), ref grids); }); diff --git a/Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs b/Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs index 692fad565df..bb7d23fbbe4 100644 --- a/Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs @@ -25,7 +25,6 @@ public async Task TestFindGridContacts() // Checks that FindGridContacts succesfully overlaps a grid + map broadphase physics body var systems = server.ResolveDependency(); var fixtureSystem = systems.GetEntitySystem(); - var mapManager = server.ResolveDependency(); var entManager = server.ResolveDependency(); var physSystem = systems.GetEntitySystem(); var transformSystem = entManager.EntitySysManager.GetEntitySystem(); @@ -34,7 +33,7 @@ public async Task TestFindGridContacts() await server.WaitAssertion(() => { entManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); // Setup 1 body on grid, 1 body off grid, and assert that it's all gucci. mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1)); @@ -75,4 +74,45 @@ await server.WaitAssertion(() => Assert.That(onGridBody.ContactCount, Is.EqualTo(1)); }); } + + [TestCase(true)] + [TestCase(false)] + public async Task TestMovingGridAttachesMapEntity(bool canCollide) + { + var server = StartServer(); + + await server.WaitIdleAsync(); + + var systems = server.ResolveDependency(); + var fixtureSystem = systems.GetEntitySystem(); + var entManager = server.ResolveDependency(); + var physSystem = systems.GetEntitySystem(); + var transformSystem = entManager.EntitySysManager.GetEntitySystem(); + var mapSystem = entManager.EntitySysManager.GetEntitySystem(); + + await server.WaitAssertion(() => + { + mapSystem.CreateMap(out var mapId); + var grid = mapSystem.CreateGridEntity(mapId); + mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1)); + + var entity = entManager.SpawnEntity(null, new MapCoordinates(new Vector2(10.5f, 10.5f), mapId)); + var body = entManager.AddComponent(entity); + physSystem.SetBodyType(entity, BodyType.Dynamic, body: body); + + var shape = new PolygonShape(); + shape.SetAsBox(0.25f, 0.25f); + fixtureSystem.CreateFixture(entity, "fix1", new Fixture(shape, 0, 0, false), body: body); + physSystem.SetCanCollide(entity, canCollide, body: body); + + var xform = entManager.GetComponent(entity); + Assert.That(xform.ParentUid, Is.Not.EqualTo(grid.Owner)); + + physSystem.Update(0.001f); + transformSystem.SetLocalPosition(grid.Owner, new Vector2(10f, 10f)); + physSystem.Update(0.001f); + + Assert.That(xform.ParentUid, Is.EqualTo(grid.Owner)); + }); + } } diff --git a/Robust.Shared.IntegrationTests/Physics/GridReparentVelocity_Test.cs b/Robust.Shared.IntegrationTests/Physics/GridReparentVelocity_Test.cs index a8c2d11f2c2..7c8bcf9cbde 100644 --- a/Robust.Shared.IntegrationTests/Physics/GridReparentVelocity_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/GridReparentVelocity_Test.cs @@ -19,7 +19,6 @@ internal sealed class GridReparentVelocity_Test private ISimulation _sim = default!; private IEntitySystemManager _systems = default!; private IEntityManager _entManager = default!; - private IMapManager _mapManager = default!; private FixtureSystem _fixtureSystem = default!; private SharedMapSystem _mapSystem = default!; private SharedPhysicsSystem _physSystem = default!; @@ -38,7 +37,6 @@ public void FixtureSetup() _systems = _sim.Resolve(); _entManager = _sim.Resolve(); - _mapManager = _sim.Resolve(); _fixtureSystem = _systems.GetEntitySystem(); _mapSystem = _systems.GetEntitySystem(); _physSystem = _systems.GetEntitySystem(); @@ -50,7 +48,7 @@ public void Setup() _mapUid = _mapSystem.CreateMap(out _mapId); // Spawn a 1x1 grid centered at (0.5, 0.5), ensure it's movable and its velocity has no damping. - var gridEnt = _mapManager.CreateGridEntity(_mapId); + var gridEnt = _mapSystem.CreateGridEntity(_mapId); var gridPhys = _entManager.GetComponent(gridEnt); _physSystem.SetSleepingAllowed(gridEnt, gridPhys, false); _physSystem.SetBodyType(gridEnt, BodyType.Dynamic, body: gridPhys); diff --git a/Robust.Shared.IntegrationTests/Physics/JointDeletion_Test.cs b/Robust.Shared.IntegrationTests/Physics/JointDeletion_Test.cs index e7ebeb642dd..97631aa532f 100644 --- a/Robust.Shared.IntegrationTests/Physics/JointDeletion_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/JointDeletion_Test.cs @@ -23,7 +23,6 @@ public async Task JointDeletionTest() await server.WaitIdleAsync(); var entManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); var susManager = server.ResolveDependency(); var jointSystem = susManager.GetEntitySystem(); var broadphase = susManager.GetEntitySystem(); diff --git a/Robust.Shared.IntegrationTests/Physics/MapVelocity_Test.cs b/Robust.Shared.IntegrationTests/Physics/MapVelocity_Test.cs index 4067f53d130..23434da3b01 100644 --- a/Robust.Shared.IntegrationTests/Physics/MapVelocity_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/MapVelocity_Test.cs @@ -34,8 +34,8 @@ public async Task TestMapVelocities() await server.WaitIdleAsync(); var entityManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); var system = entityManager.EntitySysManager; + var mapSystem = entityManager.System(); var physicsSys = system.GetEntitySystem(); var xformSystem = system.GetEntitySystem(); var traversal = entityManager.System(); @@ -44,8 +44,8 @@ public async Task TestMapVelocities() await server.WaitAssertion(() => { entityManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); - var grid2 = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); + var grid2 = mapSystem.CreateGridEntity(mapId); var gridUidA = grid.Owner; Assert.That(entityManager.TryGetComponent(gridUidA, out var gridPhysics)); @@ -106,8 +106,8 @@ public async Task TestNestedParentVelocities() await server.WaitIdleAsync(); var entityManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); var system = entityManager.EntitySysManager; + var mapSystem = entityManager.System(); var physicsSys = system.GetEntitySystem(); var xformSystem = system.GetEntitySystem(); var traversal = entityManager.System(); @@ -116,7 +116,7 @@ public async Task TestNestedParentVelocities() await server.WaitAssertion(() => { entityManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridUid = grid.Owner; Assert.That(entityManager.TryGetComponent(gridUid, out var gridPhysics)); diff --git a/Robust.Shared.IntegrationTests/Physics/RayCast_Test.cs b/Robust.Shared.IntegrationTests/Physics/RayCast_Test.cs index 1672d94c3ec..9aafdab131d 100644 --- a/Robust.Shared.IntegrationTests/Physics/RayCast_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/RayCast_Test.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Numerics; using NUnit.Framework; @@ -17,6 +18,13 @@ namespace Robust.UnitTesting.Shared.Physics; [TestFixture] internal sealed class RayCast_Test { + public enum SensorCastKind + { + RayAll, + RayClosest, + Shape, + } + private static TestCaseData[] _rayCases = { // Ray goes through @@ -55,6 +63,34 @@ internal sealed class RayCast_Test new(new SlimPolygon(Box2.UnitCentered), new Transform(Vector2.Zero, Angle.Zero), -Vector2.UnitY, null), }; + private static TestCaseData[] _sensorCases = + { + new TestCaseData(SensorCastKind.RayAll, true, false) + .SetName("RayCast all returns hard fixtures without sensor query flag"), + new TestCaseData(SensorCastKind.RayAll, true, true) + .SetName("RayCast all returns hard fixtures with sensor query flag"), + new TestCaseData(SensorCastKind.RayAll, false, false) + .SetName("RayCast all filters sensor fixtures without sensor query flag"), + new TestCaseData(SensorCastKind.RayAll, false, true) + .SetName("RayCast all returns sensor fixtures with sensor query flag"), + new TestCaseData(SensorCastKind.RayClosest, true, false) + .SetName("RayCast closest returns hard fixtures without sensor query flag"), + new TestCaseData(SensorCastKind.RayClosest, true, true) + .SetName("RayCast closest returns hard fixtures with sensor query flag"), + new TestCaseData(SensorCastKind.RayClosest, false, false) + .SetName("RayCast closest filters sensor fixtures without sensor query flag"), + new TestCaseData(SensorCastKind.RayClosest, false, true) + .SetName("RayCast closest returns sensor fixtures with sensor query flag"), + new TestCaseData(SensorCastKind.Shape, true, false) + .SetName("ShapeCast returns hard fixtures without sensor query flag"), + new TestCaseData(SensorCastKind.Shape, true, true) + .SetName("ShapeCast returns hard fixtures with sensor query flag"), + new TestCaseData(SensorCastKind.Shape, false, false) + .SetName("ShapeCast filters sensor fixtures without sensor query flag"), + new TestCaseData(SensorCastKind.Shape, false, true) + .SetName("ShapeCast returns sensor fixtures with sensor query flag"), + }; + [Test, TestCaseSource(nameof(_rayCases))] public void RayCast(Vector2 origin, Vector2 direction, Vector2? point) { @@ -113,6 +149,55 @@ public void ShapeCast(IPhysShape shape, Transform origin, Vector2 direction, Vec } } + [Test, TestCaseSource(nameof(_sensorCases))] + public void SensorFixtureCasts(SensorCastKind kind, bool hardFixture, bool includeSensors) + { + var sim = RobustServerSimulation.NewSimulation().RegisterEntitySystems(f => + { + f.LoadExtraSystemType(); + }).InitializeInstance(); + SetupSensorFixture(sim, out var mapId, out var target, hardFixture); + var raycast = sim.System(); + + var flags = QueryFlags.Dynamic | QueryFlags.Static; + if (includeSensors) + flags |= QueryFlags.Sensors; + + var filter = new QueryFilter + { + LayerBits = 1, + Flags = flags, + }; + + var hits = kind switch + { + SensorCastKind.RayAll => raycast.CastRay( + mapId, + Vector2.UnitX / 2f, + Vector2.UnitY * 3f, + filter), + SensorCastKind.RayClosest => raycast.CastRayClosest( + mapId, + Vector2.UnitX / 2f, + Vector2.UnitY * 3f, + filter), + SensorCastKind.Shape => raycast.CastShape( + mapId, + new PhysShapeCircle(0.1f), + new Transform(Vector2.UnitX / 2f, Angle.Zero), + Vector2.UnitY * 3f, + filter, + RayCastSystem.RayCastAllCallback), + _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), + }; + + var expected = hardFixture || includeSensors + ? new[] { target } + : Array.Empty(); + + Assert.That(hits.Results.Select(hit => hit.Entity).ToArray(), Is.EqualTo(expected)); + } + private void Setup(ISimulation sim, out MapId mapId) { var entManager = sim.Resolve(); @@ -120,7 +205,7 @@ private void Setup(ISimulation sim, out MapId mapId) sim.System().CreateMap(out mapId); - var grid = sim.Resolve().CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); for (var i = 0; i < 3; i++) { @@ -142,4 +227,38 @@ private void Setup(ISimulation sim, out MapId mapId) entManager.System().SetLocalRotation(grid.Owner, Angle.FromDegrees(90)); entManager.System().SetLocalPosition(grid.Owner, Vector2.UnitX / 2f); } + + private void SetupSensorFixture(ISimulation sim, out MapId mapId, out EntityUid target, bool hard) + { + var entManager = sim.Resolve(); + var mapSystem = entManager.System(); + var fixtureSystem = entManager.System(); + var physicsSystem = entManager.System(); + + mapSystem.CreateMap(out mapId); + var grid = mapSystem.CreateGridEntity(mapId); + + for (var i = 0; i < 3; i++) + { + mapSystem.SetTile(grid, new Vector2i(0, i), new Tile(1)); + } + + target = SpawnCastTarget(entManager, fixtureSystem, physicsSystem, grid.Owner, new Vector2(0.5f, 1f), hard); + } + + private EntityUid SpawnCastTarget( + IEntityManager entManager, + FixtureSystem fixtureSystem, + SharedPhysicsSystem physicsSystem, + EntityUid parent, + Vector2 position, + bool hard) + { + var uid = entManager.SpawnEntity(null, new EntityCoordinates(parent, position)); + var physics = entManager.AddComponent(uid); + fixtureSystem.CreateFixture(uid, "fix1", new Fixture(new PhysShapeCircle(0.25f), 1, 1, hard)); + physicsSystem.SetCanCollide(uid, true, body: physics); + Assert.That(physics.CanCollide); + return uid; + } } diff --git a/Robust.Shared.IntegrationTests/Physics/RecursiveUpdateTest.cs b/Robust.Shared.IntegrationTests/Physics/RecursiveUpdateTest.cs index 4b061376888..10781409e90 100644 --- a/Robust.Shared.IntegrationTests/Physics/RecursiveUpdateTest.cs +++ b/Robust.Shared.IntegrationTests/Physics/RecursiveUpdateTest.cs @@ -21,13 +21,12 @@ public void ContainerRecursiveUpdateTest() { var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var xforms = entManager.System(); var mapSystem = entManager.System(); var containers = entManager.System(); var mapId = sim.CreateMap().MapId; - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var guid = grid.Owner; mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1)); Assert.That(entManager.HasComponent(guid)); @@ -161,7 +160,6 @@ public void RecursiveMoveTest() { var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var mapSystem = entManager.EntitySysManager.GetEntitySystem(); var transforms = entManager.EntitySysManager.GetEntitySystem(); var lookup = entManager.EntitySysManager.GetEntitySystem(); @@ -216,7 +214,7 @@ public void RecursiveMoveTest() Assert.That(ents, Does.Contain(child)); // Try again, but this time with a parent change. - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var guid = grid.Owner; mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1)); var gridBroadphase = entManager.GetComponent(guid); diff --git a/Robust.Shared.IntegrationTests/Physics/Stack_Test.cs b/Robust.Shared.IntegrationTests/Physics/Stack_Test.cs index eb90f663401..568aeb28251 100644 --- a/Robust.Shared.IntegrationTests/Physics/Stack_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/Stack_Test.cs @@ -155,7 +155,6 @@ public async Task TestCircleStack() await server.WaitIdleAsync(); var entityManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); var entitySystemManager = server.ResolveDependency(); var fixtureSystem = entitySystemManager.GetEntitySystem(); var physSystem = entitySystemManager.GetEntitySystem(); diff --git a/Robust.Shared.IntegrationTests/Prototypes/HotReloadTest.cs b/Robust.Shared.IntegrationTests/Prototypes/HotReloadTest.cs index 22fa7147303..713ee695edf 100644 --- a/Robust.Shared.IntegrationTests/Prototypes/HotReloadTest.cs +++ b/Robust.Shared.IntegrationTests/Prototypes/HotReloadTest.cs @@ -34,7 +34,6 @@ internal sealed class HotReloadTest : OurRobustUnitTest - type: {HotReloadTestComponentTwoId}"; private PrototypeManager _prototypes = default!; - private IMapManager _maps = default!; private IEntityManager _entities = default!; protected override Type[]? ExtraComponents => new[] {typeof(HotReloadTestOneComponent), typeof(HotReloadTestTwoComponent)}; @@ -48,7 +47,6 @@ public void Setup() _prototypes.LoadString(InitialPrototypes); _prototypes.ResolveResults(); - _maps = IoCManager.Resolve(); _entities = IoCManager.Resolve(); } diff --git a/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs b/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs new file mode 100644 index 00000000000..4bd2f0f84d3 --- /dev/null +++ b/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs @@ -0,0 +1,150 @@ +using JetBrains.Annotations; +using NUnit.Framework; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Robust.UnitTesting.Shared.Prototypes; + +/// +/// Tests the HasComp family of methods as well as serialization. +/// +[UsedImplicitly] +[TestOf(typeof(EntityPrototype))] +[Description($"Tests the {nameof(EntityPrototype)} HasComp family of methods")] +internal sealed class PrototypeHasCompTest : OurRobustUnitTest +{ + private IComponentFactory _factory = default!; + private IPrototypeManager _proto = default!; + + protected override Type[] ExtraComponents => + [ + typeof(TestDefinedComponent), + typeof(TestInheritedComponent), + typeof(TestMissingComponent), + typeof(TestCompNameComponent) + ]; + + // TestEntity is expected to have TestDefinedComponent, TestInheritedComponent but not TestMissingComponent + const string TestEntity = "TestEntity"; + // expected to have TestCompNameComponent + const string TestInception = "TestInception"; + // A bogus component name. + const string TestFail = "TestFail"; + + [OneTimeSetUp] + public void Setup() + { + IoCManager.Resolve().Initialize(); + _factory = IoCManager.Resolve(); + _proto = IoCManager.Resolve(); + + _proto.Initialize(); + _proto.LoadString(TestPrototypes); + _proto.ResolveResults(); + } + + [Test] + public void TestHasCompGeneric() + { + var proto = _proto.Index(TestEntity); + Assert.That(proto.HasComp(_factory)); + Assert.That(proto.HasComp(_factory)); + Assert.That(!proto.HasComp(_factory)); + + // and with CompName separately + Assert.That(proto.HasComp(_factory.CompName())); + Assert.That(proto.HasComp(_factory.CompName())); + Assert.That(!proto.HasComp(_factory.CompName())); + } + + [Test] + public void TestHasCompType() + { + var proto = _proto.Index(TestEntity); + + // first test with GetRegistration + var defined = _factory.GetRegistration().Type; + var inherited = _factory.GetRegistration().Type; + var missing = _factory.GetRegistration().Type; + + void TestThem() + { + Assert.That(proto.HasComp(defined, _factory)); + Assert.That(proto.HasComp(inherited, _factory)); + Assert.That(!proto.HasComp(missing, _factory)); + } + + TestThem(); + + // then test with typeof + defined = typeof(TestDefinedComponent); + inherited = typeof(TestInheritedComponent); + missing = typeof(TestMissingComponent); + TestThem(); + } + + [Test] + public void TestHasCompString() + { + var proto = _proto.Index(TestEntity); + var defined = new CompName("TestDefined", _factory); + var inherited = new CompName("TestInherited", _factory); + var missing = new CompName("TestMissing", _factory); + Assert.That(proto.HasComp(defined)); + Assert.That(proto.HasComp(inherited)); + Assert.That(!proto.HasComp(missing)); + } + + [Test] + public void TestCompNameSerialization() + { + var proto = _proto.Index(TestInception); + Assert.That(proto.TryComp(out var inception, _factory)); + var name = inception!.Comp; + Assert.That(name, Is.EqualTo(_factory.CompName())); // inception + Assert.That(proto.HasComp(name)); + Assert.That(_factory.HasRegistration(name)); + + // gibberish component should prevent it from loading + Assert.That(!_proto.HasIndex(TestFail)); + } + + const string TestPrototypes = $@" +- type: entity + parent: TestEntityParent # making sure inheritance wouldn't break it + id: {TestEntity} + components: + - type: TestDefined + +- type: entity + abstract: true + id: TestEntityParent + components: + - type: TestInherited + +- type: entity + id: TestInception + components: + - type: TestCompName + comp: TestCompName # inception + +- type: entity + id: TestFail + components: + - type: TestCompName + comp: Afnuaghdjngbjda # this prevents this prototype from loading, hopefully nobody adds this component to rt :godo: +"; +} + +internal sealed partial class TestDefinedComponent : Component; +internal sealed partial class TestInheritedComponent : Component; +internal sealed partial class TestMissingComponent : Component; + +internal sealed partial class TestCompNameComponent : Component +{ + [DataField(required: true)] + public CompName Comp; +} diff --git a/Robust.Shared.IntegrationTests/Robust.Shared.IntegrationTests.csproj b/Robust.Shared.IntegrationTests/Robust.Shared.IntegrationTests.csproj index 8d287bd2917..a5bf54a7f2c 100644 --- a/Robust.Shared.IntegrationTests/Robust.Shared.IntegrationTests.csproj +++ b/Robust.Shared.IntegrationTests/Robust.Shared.IntegrationTests.csproj @@ -36,4 +36,5 @@ + diff --git a/Robust.Shared.IntegrationTests/Serialization/DataRecordTest.cs b/Robust.Shared.IntegrationTests/Serialization/DataRecordTest.cs index 7f160205d77..c6e24003bfd 100644 --- a/Robust.Shared.IntegrationTests/Serialization/DataRecordTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/DataRecordTest.cs @@ -15,16 +15,79 @@ public sealed partial class DataRecordTest : OurSerializationTest public partial record TwoIntRecord(int aTest, int AnotherTest); [DataRecord] - public partial record OneByteOneDefaultIntRecord(byte A, int B = 5); + public partial record struct TwoIntRecordStruct(int aTest, int AnotherTest); [DataRecord] - public partial record OneLongRecord(long A); + public partial record PrimitiveRecord( + bool Bool, + byte Byte, + sbyte Sbyte, + char Char, + //decimal Decimal, + double Double, + float Float, + int Int, + uint Uint, + nint Nint, + nuint Nuint, + long Long, + ulong Ulong, + short Short, + ushort UShort); [DataRecord] - public partial record OneLongDefaultRecord(long A = 5); + public partial record struct PrimitiveRecordStruct( + bool Bool, + byte Byte, + sbyte Sbyte, + char Char, + //decimal Decimal, + double Double, + float Float, + int Int, + uint Uint, + nint Nint, + nuint Nuint, + long Long, + ulong Ulong, + short Short, + ushort UShort); [DataRecord] - public partial record OneULongRecord(ulong A); + public partial record PrimitiveDefaultsRecord( + bool Bool = true, + byte Byte = byte.MaxValue, + sbyte Sbyte = sbyte.MinValue, + char Char = 'A', + //decimal Decimal = -1, + double Double = -1d, + float Float = -1f, + int Int = int.MinValue, + uint Uint = uint.MaxValue, + nint Nint = int.MinValue, + nuint Nuint = uint.MaxValue, + long Long = long.MinValue, + ulong Ulong = ulong.MaxValue, + short Short = short.MinValue, + ushort UShort = ushort.MaxValue); + + [DataRecord] + public partial record struct PrimitiveDefaultsRecordStruct( + bool Bool = true, + byte Byte = byte.MaxValue, + sbyte Sbyte = sbyte.MinValue, + char Char = 'A', + //decimal Decimal = -1, + double Double = -1d, + float Float = -1f, + int Int = int.MinValue, + uint Uint = uint.MaxValue, + nint Nint = int.MinValue, + nuint Nuint = uint.MaxValue, + long Long = long.MinValue, + ulong Ulong = ulong.MaxValue, + short Short = short.MinValue, + ushort UShort = ushort.MaxValue); [PrototypeRecord("emptyTestPrototypeRecord")] public partial record PrototypeRecord([field: IdDataField] string ID) : IPrototype; @@ -32,6 +95,9 @@ public partial record PrototypeRecord([field: IdDataField] string ID) : IPrototy [DataRecord] public partial record IntStructHolder(IntStruct Struct); + [DataRecord] + public partial record struct IntStructHolderStruct(IntStruct Struct); + [DataDefinition] public partial struct IntStruct { @@ -46,6 +112,9 @@ public IntStruct(int value) [DataRecord] public partial record TwoIntStructHolder(IntStruct Struct1, IntStruct Struct2); + [DataRecord] + public partial record struct TwoIntStructHolderStruct(IntStruct Struct1, IntStruct Struct2); + [DataRecord] public partial record struct DataRecordStruct(IntStruct Struct, string String, int Integer); @@ -58,6 +127,18 @@ public partial record struct DataRecordWithProperties public float X => Position.X; } + [DataRecord] + public partial record struct DataRecordWithDefaultFields() + { + public int A = 1; + } + + [DataRecord] + public partial record struct DataRecordWithDefaultFields2(int A = 1) + { + public int B = 2; + } + [DataRecord] public readonly partial record struct ReadonlyDataRecord { @@ -98,61 +179,178 @@ public void TwoIntRecordTest() } [Test] - public void OneByteOneDefaultIntRecordTest() + public void TwoIntRecordStructTest() { - var mapping = new MappingDataNode {{"a", "1"}}; - var val = Serialization.Read(mapping, notNullableOverride: true); + var mapping = new MappingDataNode + { + {"aTest", "1"}, + {"anotherTest", "2"} + }; + + var val = Serialization.Read(mapping); Assert.Multiple(() => { - Assert.That(val.A, Is.EqualTo(1)); - Assert.That(val.B, Is.EqualTo(5)); + Assert.That(val.aTest, Is.EqualTo(1)); + Assert.That(val.AnotherTest, Is.EqualTo(2)); }); - } - [Test] - public void OneLongRecordTest() - { - var mapping = new MappingDataNode {{"a", "1"}}; - var val = Serialization.Read(mapping, notNullableOverride: true); + var newMapping = Serialization.WriteValueAs(val); - Assert.That(val.A, Is.EqualTo(1)); - } + Assert.Multiple(() => + { + Assert.That(newMapping, Has.Count.EqualTo(2)); - [Test] - public void OneLongMinValueRecordTest() - { - var mapping = new MappingDataNode {{"a", long.MinValue.ToString()}}; - var val = Serialization.Read(mapping, notNullableOverride: true); + Assert.That(newMapping.TryGet("aTest", out var aTestNode)); + Assert.That(aTestNode!.Value, Is.EqualTo("1")); - Assert.That(val.A, Is.EqualTo(long.MinValue)); + Assert.That(newMapping.TryGet("anotherTest", out var anotherTestNode)); + Assert.That(anotherTestNode!.Value, Is.EqualTo("2")); + }); } [Test] - public void OneLongMaxValueRecordTest() + public void PrimitiveRecordTest() { - var mapping = new MappingDataNode {{"a", long.MaxValue.ToString()}}; - var val = Serialization.Read(mapping, notNullableOverride: true); + var mapping = new MappingDataNode(); + var val1 = Serialization.Read(mapping, notNullableOverride: true); + var val2 = Serialization.Read(mapping); - Assert.That(val.A, Is.EqualTo(long.MaxValue)); + Assert.Multiple(() => + { + Assert.That(val1.Bool, Is.EqualTo(false)); + Assert.That(val2.Bool, Is.EqualTo(false)); + Assert.That(val1.Byte, Is.EqualTo(0)); + Assert.That(val2.Byte, Is.EqualTo(0)); + Assert.That(val1.Sbyte, Is.EqualTo(0)); + Assert.That(val2.Sbyte, Is.EqualTo(0)); + Assert.That(val1.Char, Is.EqualTo(default(char))); + Assert.That(val2.Char, Is.EqualTo(default(char))); + //Assert.That(val1.Decimal, Is.EqualTo(0)); + //Assert.That(val2.Decimal, Is.EqualTo(0)); + Assert.That(val1.Double, Is.EqualTo(0)); + Assert.That(val2.Double, Is.EqualTo(0)); + Assert.That(val1.Float, Is.EqualTo(0)); + Assert.That(val2.Float, Is.EqualTo(0)); + Assert.That(val1.Int, Is.EqualTo(0)); + Assert.That(val2.Int, Is.EqualTo(0)); + Assert.That(val1.Uint, Is.EqualTo(0)); + Assert.That(val2.Uint, Is.EqualTo(0)); + Assert.That(val1.Nint, Is.EqualTo((nint) 0)); + Assert.That(val2.Nint, Is.EqualTo((nint) 0)); + Assert.That(val1.Nuint, Is.EqualTo((nuint) 0)); + Assert.That(val2.Nuint, Is.EqualTo((nuint) 0)); + Assert.That(val1.Long, Is.EqualTo(0)); + Assert.That(val2.Long, Is.EqualTo(0)); + Assert.That(val1.Ulong, Is.EqualTo(0)); + Assert.That(val2.Ulong, Is.EqualTo(0)); + Assert.That(val1.Short, Is.EqualTo(0)); + Assert.That(val2.Short, Is.EqualTo(0)); + Assert.That(val1.UShort, Is.EqualTo(0)); + Assert.That(val2.UShort, Is.EqualTo(0)); + }); } [Test] - public void OneLongDefaultRecordTest() + public void PrimitiveDefaultsRecordTest() { var mapping = new MappingDataNode(); - var val = Serialization.Read(mapping, notNullableOverride: true); + var val1 = Serialization.Read(mapping, notNullableOverride: true); + var val2 = Serialization.Read(mapping); - Assert.That(val.A, Is.EqualTo(5)); + Assert.Multiple(() => + { + Assert.That(val1.Bool, Is.EqualTo(true)); + Assert.That(val2.Bool, Is.EqualTo(true)); + Assert.That(val1.Byte, Is.EqualTo(byte.MaxValue)); + Assert.That(val2.Byte, Is.EqualTo(byte.MaxValue)); + Assert.That(val1.Sbyte, Is.EqualTo(sbyte.MinValue)); + Assert.That(val2.Sbyte, Is.EqualTo(sbyte.MinValue)); + Assert.That(val1.Char, Is.EqualTo('A')); + Assert.That(val2.Char, Is.EqualTo('A')); + //Assert.That(val1.Decimal, Is.EqualTo(-1)); + //Assert.That(val2.Decimal, Is.EqualTo(-1)); + Assert.That(val1.Double, Is.EqualTo(-1)); + Assert.That(val2.Double, Is.EqualTo(-1)); + Assert.That(val1.Float, Is.EqualTo(-1)); + Assert.That(val2.Float, Is.EqualTo(-1)); + Assert.That(val1.Int, Is.EqualTo(int.MinValue)); + Assert.That(val2.Int, Is.EqualTo(int.MinValue)); + Assert.That(val1.Uint, Is.EqualTo(uint.MaxValue)); + Assert.That(val2.Uint, Is.EqualTo(uint.MaxValue)); + Assert.That(val1.Nint, Is.EqualTo((nint) int.MinValue)); + Assert.That(val2.Nint, Is.EqualTo((nint) int.MinValue)); + Assert.That(val1.Nuint, Is.EqualTo((nuint) uint.MaxValue)); + Assert.That(val2.Nuint, Is.EqualTo((nuint) uint.MaxValue)); + Assert.That(val1.Long, Is.EqualTo(long.MinValue)); + Assert.That(val2.Long, Is.EqualTo(long.MinValue)); + Assert.That(val1.Ulong, Is.EqualTo(ulong.MaxValue)); + Assert.That(val2.Ulong, Is.EqualTo(ulong.MaxValue)); + Assert.That(val1.Short, Is.EqualTo(short.MinValue)); + Assert.That(val2.Short, Is.EqualTo(short.MinValue)); + Assert.That(val1.UShort, Is.EqualTo(ushort.MaxValue)); + Assert.That(val2.UShort, Is.EqualTo(ushort.MaxValue)); + }); } [Test] - public void OneULongRecordMaxValueTest() + public void PrimitiveRecordMinMaxValueTest() { - var mapping = new MappingDataNode {{"a", ulong.MaxValue.ToString()}}; - var val = Serialization.Read(mapping, notNullableOverride: true); + var mapping = new MappingDataNode + { + {"bool", "true"}, + {"byte", byte.MaxValue.ToString()}, + {"sbyte", sbyte.MinValue.ToString()}, + {"char", "A"}, + //{"decimal", "-1"}, + {"double", "-1"}, + {"float", "-1"}, + {"int", int.MinValue.ToString()}, + {"uint", uint.MaxValue.ToString()}, + // TODO SERIALIZATION add nint yaml serializer? + //{"nint", nint.MinValue.ToString()}, + //{"nuint", nuint.MinValue.ToString()}, + {"long", long.MinValue.ToString()}, + {"ulong", ulong.MaxValue.ToString()}, + {"short", short.MinValue.ToString()}, + {"ushort", ushort.MaxValue.ToString()}, + }; + var val1 = Serialization.Read(mapping, notNullableOverride: true); + var val2 = Serialization.Read(mapping); - Assert.That(val.A, Is.EqualTo(ulong.MaxValue)); + Assert.Multiple(() => + { + Assert.That(val1.Bool, Is.EqualTo(true)); + Assert.That(val2.Bool, Is.EqualTo(true)); + Assert.That(val1.Byte, Is.EqualTo(byte.MaxValue)); + Assert.That(val2.Byte, Is.EqualTo(byte.MaxValue)); + Assert.That(val1.Sbyte, Is.EqualTo(sbyte.MinValue)); + Assert.That(val2.Sbyte, Is.EqualTo(sbyte.MinValue)); + Assert.That(val1.Char, Is.EqualTo('A')); + Assert.That(val2.Char, Is.EqualTo('A')); + //Assert.That(val1.Decimal, Is.EqualTo(-1)); + //Assert.That(val2.Decimal, Is.EqualTo(-1)); + Assert.That(val1.Double, Is.EqualTo(-1)); + Assert.That(val2.Double, Is.EqualTo(-1)); + Assert.That(val1.Float, Is.EqualTo(-1)); + Assert.That(val2.Float, Is.EqualTo(-1)); + Assert.That(val1.Int, Is.EqualTo(int.MinValue)); + Assert.That(val2.Int, Is.EqualTo(int.MinValue)); + Assert.That(val1.Uint, Is.EqualTo(uint.MaxValue)); + Assert.That(val2.Uint, Is.EqualTo(uint.MaxValue)); + //Assert.That(val1.Nint, Is.EqualTo(nint.MinValue)); + //Assert.That(val2.Nint, Is.EqualTo(nint.MinValue)); + //Assert.That(val1.Nuint, Is.EqualTo(nuint.MaxValue)); + //Assert.That(val2.Nuint, Is.EqualTo(nuint.MaxValue)); + Assert.That(val1.Long, Is.EqualTo(long.MinValue)); + Assert.That(val2.Long, Is.EqualTo(long.MinValue)); + Assert.That(val1.Ulong, Is.EqualTo(ulong.MaxValue)); + Assert.That(val2.Ulong, Is.EqualTo(ulong.MaxValue)); + Assert.That(val1.Short, Is.EqualTo(short.MinValue)); + Assert.That(val2.Short, Is.EqualTo(short.MinValue)); + Assert.That(val1.UShort, Is.EqualTo(ushort.MaxValue)); + Assert.That(val2.UShort, Is.EqualTo(ushort.MaxValue)); + }); } [Test] @@ -164,6 +362,18 @@ public void PrototypeTest() Assert.That(val.ID, Is.EqualTo("ABC")); } + [Test] + public void DataRecordWithDefaultFieldsTest() + { + var mapping = new MappingDataNode (); + var val = Serialization.Read(mapping); + Assert.That(val.A, Is.EqualTo(1)); + + var val2 = Serialization.Read(mapping); + Assert.That(val2.A, Is.EqualTo(1)); + Assert.That(val2.B, Is.EqualTo(2)); + } + [Test] public void RegisterPrototypeTest() { @@ -186,8 +396,10 @@ public void IntStructHolderTest() } }; var val = Serialization.Read(mapping, notNullableOverride: true); + var structVal = Serialization.Read(mapping); Assert.That(val.Struct.Value, Is.EqualTo(42)); + Assert.That(structVal.Struct.Value, Is.EqualTo(42)); } [Test] diff --git a/Robust.Shared.IntegrationTests/Serialization/DataStructTest.cs b/Robust.Shared.IntegrationTests/Serialization/DataStructTest.cs index d9f62d1cc46..5c5771f902e 100644 --- a/Robust.Shared.IntegrationTests/Serialization/DataStructTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/DataStructTest.cs @@ -9,19 +9,56 @@ internal sealed partial class DataStructTest : OurSerializationTest [DataDefinition] public partial struct DefaultIntDataStruct { + [DataField] public int A = 5; + [DataField] + public int B; + public DefaultIntDataStruct() { + B = 1; } } + [DataDefinition] + public partial struct DefaultIntDataStructNoConstructor + { + [DataField] + public int A = 5; + + [DataField] + public int B; + } + [Test] public void DefaultIntDataStructTest() { var mapping = new MappingDataNode(); var val = Serialization.Read(mapping); + var val2 = Serialization.Read(mapping); + + Assert.That(val.A, Is.EqualTo(5)); + Assert.That(val.B, Is.EqualTo(1)); + Assert.That(val2.A, Is.EqualTo(5)); + Assert.That(val2.B, Is.EqualTo(0)); + + mapping = new MappingDataNode {{"a", "10"}}; + val = Serialization.Read(mapping); + val2 = Serialization.Read(mapping); + + Assert.That(val.A, Is.EqualTo(10)); + Assert.That(val.B, Is.EqualTo(1)); + Assert.That(val2.A, Is.EqualTo(10)); + Assert.That(val2.B, Is.EqualTo(0)); + + mapping = new MappingDataNode {{"b", "10"}}; + val = Serialization.Read(mapping); + val2 = Serialization.Read(mapping); Assert.That(val.A, Is.EqualTo(5)); + Assert.That(val.B, Is.EqualTo(10)); + Assert.That(val2.A, Is.EqualTo(5)); + Assert.That(val2.B, Is.EqualTo(10)); } } diff --git a/Robust.Shared.IntegrationTests/Serialization/PropertyAndFieldDefinitionTest.cs b/Robust.Shared.IntegrationTests/Serialization/PropertyAndFieldDefinitionTest.cs index 641358f25bb..84ca86cbc5a 100644 --- a/Robust.Shared.IntegrationTests/Serialization/PropertyAndFieldDefinitionTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/PropertyAndFieldDefinitionTest.cs @@ -76,9 +76,8 @@ public void ParityTest() var dataDefinition = ((SerializationManager) Serialization).GetDefinition(propertyInfo.DeclaringType!); Assert.That(dataDefinition, Is.Not.Null); - var alwaysPushDataField = propertyInfo.GetAttribute(); var propertyDefinition = - dataDefinition!.BaseFieldDefinitions.Single(e => e.Attribute.Equals(alwaysPushDataField)); + dataDefinition!.BaseFieldDefinitions.Single(e => (e.Tag ?? e.CamelCasedName).Equals(GetOnlyPropertyWithOtherAttributeFieldTargetedName)); var inheritanceBehaviour = propertyDefinition.InheritanceBehavior; Assert.That(inheritanceBehaviour, Is.EqualTo(InheritanceBehavior.Always)); @@ -100,16 +99,15 @@ public void ParityTest() Assert.That(propertyInfo.GetBackingField()!.GetAttribute(), Is.Null); Assert.That(propertyInfo.GetAttribute(true), Is.Not.Null); - var neverPushDataField = propertyInfo.GetAttribute(); propertyDefinition = - dataDefinition!.BaseFieldDefinitions.Single(e => e.Attribute.Equals(neverPushDataField)); + dataDefinition!.BaseFieldDefinitions.Single(e => (e.Tag ?? e.CamelCasedName).Equals(GetOnlyPropertyFieldTargetedAndOtherAttributeName)); inheritanceBehaviour = propertyDefinition.InheritanceBehavior; dataDefinition = ((SerializationManager) Serialization).GetDefinition(property!.DeclaringType!); Assert.That(dataDefinition, Is.Not.Null); Assert.That(inheritanceBehaviour, Is.EqualTo(InheritanceBehavior.Never)); } - [Robust.Shared.Serialization.Manager.Attributes.DataDefinition] + [DataDefinition] internal sealed partial class PropertyAndFieldDefinitionTestDefinition { [DataField(GetOnlyPropertyName)] diff --git a/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs index 8c3eed5acaa..62c8845bf35 100644 --- a/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs @@ -25,7 +25,7 @@ internal sealed class ComponentRegistrySerializerTest : OurSerializationTest public void SerializationTest() { var component = new TestComponent(); - var registry = new ComponentRegistry {{"Test", new ComponentRegistryEntry(component, new MappingDataNode())}}; + var registry = new ComponentRegistry {{"Test", new ComponentRegistryEntry(component)}}; var node = Serialization.WriteValueAs(registry); Assert.That(node.Sequence.Count, Is.EqualTo(1)); diff --git a/Robust.Shared.IntegrationTests/Spawning/EntitySpawnHelpersTest.cs b/Robust.Shared.IntegrationTests/Spawning/EntitySpawnHelpersTest.cs index 3453141b83c..91e96400c01 100644 --- a/Robust.Shared.IntegrationTests/Spawning/EntitySpawnHelpersTest.cs +++ b/Robust.Shared.IntegrationTests/Spawning/EntitySpawnHelpersTest.cs @@ -24,9 +24,6 @@ public abstract partial class EntitySpawnHelpersTest : RobustIntegrationTest protected SharedTransformSystem Xforms = default!; 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 EntityUid Map; protected MapId MapId; protected EntityUid Parent; // entity parented to the map. @@ -44,7 +41,6 @@ protected async Task Setup() { Server = StartServer(); await Server.WaitIdleAsync(); - MapMan = Server.ResolveDependency(); EntMan = Server.ResolveDependency(); MapSys = EntMan.System(); Xforms = EntMan.System(); diff --git a/Robust.Shared.IntegrationTests/TransformTests/GridTraversalTest.cs b/Robust.Shared.IntegrationTests/TransformTests/GridTraversalTest.cs index 92401b30d83..eac38f87795 100644 --- a/Robust.Shared.IntegrationTests/TransformTests/GridTraversalTest.cs +++ b/Robust.Shared.IntegrationTests/TransformTests/GridTraversalTest.cs @@ -16,7 +16,6 @@ public async Task TestSpawnTraversal() var server = StartServer(); await server.WaitIdleAsync(); - var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var xforms = sEntMan.System(); var mapSys = sEntMan.System(); @@ -29,7 +28,7 @@ public async Task TestSpawnTraversal() await server.WaitPost(() => { map = sEntMan.System().CreateMap(out mapId); - var gridComp = mapMan.CreateGridEntity(mapId); + var gridComp = mapSys.CreateGridEntity(mapId); grid = gridComp.Owner; mapSys.SetTile(grid, gridComp, Vector2i.Zero, new Tile(1)); var gridCentre = new EntityCoordinates(grid, .5f, .5f); diff --git a/Robust.Shared.Maths.Tests/Box2i_Test.cs b/Robust.Shared.Maths.Tests/Box2i_Test.cs index c924fc50f6c..be1a5cdd5b2 100644 --- a/Robust.Shared.Maths.Tests/Box2i_Test.cs +++ b/Robust.Shared.Maths.Tests/Box2i_Test.cs @@ -1,38 +1,129 @@ using NUnit.Framework; -namespace Robust.Shared.Maths.Tests +namespace Robust.Shared.Maths.Tests; + +[TestFixture, Parallelizable, TestOf(typeof(Box2i))] +internal sealed class Box2i_Test { - [TestFixture, Parallelizable, TestOf(typeof(Box2i))] - internal sealed class Box2i_Test + [Test] + public void Box2iUnion() { - [Test] - public void Box2iUnion() - { - var boxOne = new Box2i(-1, -1, 1, 1); - var boxTwo = new Box2i(0, 0, 2, 2); + var boxOne = new Box2i(-1, -1, 1, 1); + var boxTwo = new Box2i(0, 0, 2, 2); - var result = boxOne.Union(boxTwo); + var result = boxOne.Union(boxTwo); + using (Assert.EnterMultipleScope()) + { Assert.That(result.Left, Is.EqualTo(-1)); Assert.That(result.Bottom, Is.EqualTo(-1)); Assert.That(result.Right, Is.EqualTo(2)); Assert.That(result.Top, Is.EqualTo(2)); } + } + + [Test] + public void Box2iVector2iUnion() + { + var box = new Box2i(); + Assert.That(box, Is.EqualTo(Box2i.Empty)); + + box = box.UnionTile(Vector2i.Zero); + Assert.That(box.Right, Is.EqualTo(1)); + + box = box.UnionTile(Vector2i.One); + Assert.That(box.Top, Is.EqualTo(2)); + + box = box.Union(new Vector2i(2, 0)); + Assert.That(box.Right, Is.EqualTo(2)); + } + + [Test] + public void Box2iUsesDirectDimensions() + { + var valid = new Box2i(-1, -2, 3, 4); + + using (Assert.EnterMultipleScope()) + { + Assert.That(valid.Width, Is.EqualTo(4)); + Assert.That(valid.Height, Is.EqualTo(6)); + Assert.That(valid.Size, Is.EqualTo(new Vector2i(4, 6))); + Assert.That(valid.IsValid(), Is.True); + } + } + + [Test] + public void Box2iValidatesConstruction() + { + using (Assert.EnterMultipleScope()) + { + Assert.Throws(() => new Box2i(3, 4, -1, -2)); + Assert.Throws(() => new Box2i(new Vector2i(3, 4), new Vector2i(-1, -2))); + } + } + + [Test] + public void Box2iValidatesProperties() + { + var box = new Box2i(-1, -2, 3, 4); + + using (Assert.EnterMultipleScope()) + { + Assert.Throws(() => box.Left = 4); + Assert.Throws(() => box.Bottom = 5); + Assert.Throws(() => box.Right = -2); + Assert.Throws(() => box.Top = -3); + Assert.Throws(() => box.BottomLeft = new Vector2i(4, 0)); + Assert.Throws(() => box.TopRight = new Vector2i(0, -3)); + } + } - [Test] - public void Box2iVector2iUnion() + [Test] + public void Box2iFromTwoPointsNormalizes() + { + var box = Box2i.FromTwoPoints(new Vector2i(3, -2), new Vector2i(-1, 4)); + + Assert.That(box, Is.EqualTo(new Box2i(-1, -2, 3, 4))); + Assert.That(box.IsValid(), Is.True); + } + + [Test] + public void Box2iContainsUsesValidBounds() + { + var box = new Box2i(-1, -1, 1, 1); + + using (Assert.EnterMultipleScope()) { - var box = new Box2i(); - Assert.That(box, Is.EqualTo(Box2i.Empty)); + Assert.That(box.Contains(Vector2i.Zero), Is.True); + Assert.That(box.Contains(new Vector2i(1, 1)), Is.True); + Assert.That(box.Contains(new Vector2i(1, 1), false), Is.False); + Assert.That(box.Contains(new Box2i(0, 0, 1, 1)), Is.True); + Assert.That(box.Encloses(new Box2i(0, 0, 1, 1)), Is.False); + } + } + + [Test] + public void Box2iIntersect() + { + var boxOne = new Box2i(-1, -1, 2, 2); + var boxTwo = new Box2i(0, 1, 3, 4); - box = box.UnionTile(Vector2i.Zero); - Assert.That(box.Right, Is.EqualTo(1)); + using (Assert.EnterMultipleScope()) + { + Assert.That(boxOne.Intersect(boxTwo), Is.EqualTo(new Box2i(0, 1, 2, 2))); + Assert.That(boxOne.Intersect(new Box2i(3, 3, 4, 4)), Is.EqualTo(Box2i.Empty)); + } + } - box = box.UnionTile(Vector2i.One); - Assert.That(box.Top, Is.EqualTo(2)); + [Test] + public void Box2iClosestPoint() + { + var box = new Box2i(-1, -2, 3, 4); - box = box.Union(new Vector2i(2, 0)); - Assert.That(box.Right, Is.EqualTo(2)); + using (Assert.EnterMultipleScope()) + { + Assert.That(box.ClosestPoint(new Vector2i(10, -10)), Is.EqualTo(new Vector2i(3, -2))); + Assert.That(box.ClosestPoint(Vector2i.Zero), Is.EqualTo(Vector2i.Zero)); } } } diff --git a/Robust.Shared.Maths.Tests/UIBox2i_Test.cs b/Robust.Shared.Maths.Tests/UIBox2i_Test.cs index 604d2c40363..99a9c31e195 100644 --- a/Robust.Shared.Maths.Tests/UIBox2i_Test.cs +++ b/Robust.Shared.Maths.Tests/UIBox2i_Test.cs @@ -1,116 +1,142 @@ using NUnit.Framework; -namespace Robust.Shared.Maths.Tests +namespace Robust.Shared.Maths.Tests; + +[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] +[TestFixture] +[TestOf(typeof(UIBox2i))] +internal sealed class UIBox2i_Test { - [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] - [TestFixture] - [TestOf(typeof(UIBox2i))] - internal sealed class UIBox2i_Test + private static IEnumerable<(int left, int top, int right, int bottom)> Sources => + [ + (0, 0, 0, 0), + (0, 0, 0, 10), + (0, 0, 10, 0), + (0, 0, 10, 10), + (0, -10, 0, 0), + (0, -10, 0, 10), + (0, -10, 10, 0), + (0, -10, 10, 10), + (-10, 0, 0, 0), + (-10, 0, 0, 10), + (-10, 0, 10, 0), + (-10, 0, 10, 10), + (-10, -10, 0, 0), + (-10, -10, 0, 10), + (-10, -10, 10, 0), + (-10, -10, 10, 10) + ]; + + private static IEnumerable<(int x, int y)> SmallTranslations => + [ + (0, 1), + (1, 0), + (1, 1), + (0, -1), + (1, -1), + (-1, 0), + (-1, 1), + (-1, -1) + ]; + + private static IEnumerable<(int x, int y)> LargeTranslations => + [ + (0, 20), + (20, 0), + (20, 20), + (0, -20), + (20, -20), + (-20, 0), + (-20, 20), + (-20, -20) + ]; + + private static IEnumerable<(UIBox2i a, UIBox2i b, UIBox2i? expected)> Intersections => + [ + (new UIBox2i(0, 0, 5, 5), new UIBox2i(2, 2, 4, 4), new UIBox2i(2, 2, 4, 4)), + (new UIBox2i(0, 0, 5, 5), new UIBox2i(3, 3, 7, 7), new UIBox2i(3, 3, 5, 5)), + (new UIBox2i(2, 0, 5, 5), new UIBox2i(0, 3, 4, 7), new UIBox2i(2, 3, 4, 5)), + (new UIBox2i(2, 0, 5, 5), new UIBox2i(6, 6, 10, 10), null) + ]; + + [Test] + public void Box2iVectorConstructor([ValueSource(nameof(Sources))] (int, int, int, int) test) { - private static IEnumerable<(int left, int top, int right, int bottom)> Sources => new (int, int, int, int)[] - { - (0, 0, 0, 0), - (0, 0, 0, 10), - (0, 0, 10, 0), - (0, 0, 10, 10), - (0, -10, 0, 0), - (0, -10, 0, 10), - (0, -10, 10, 0), - (0, -10, 10, 10), - (-10, 0, 0, 0), - (-10, 0, 0, 10), - (-10, 0, 10, 0), - (-10, 0, 10, 10), - (-10, -10, 0, 0), - (-10, -10, 0, 10), - (-10, -10, 10, 0), - (-10, -10, 10, 10) - }; - - private static IEnumerable<(int x, int y)> SmallTranslations => new (int, int)[] - { - (0, 1), - (1, 0), - (1, 1), - (0, -1), - (1, -1), - (-1, 0), - (-1, 1), - (-1, -1) - }; - - private static IEnumerable<(int x, int y)> LargeTranslations => new (int, int)[] - { - (0, 20), - (20, 0), - (20, 20), - (0, -20), - (20, -20), - (-20, 0), - (-20, 20), - (-20, -20) - }; - - private static IEnumerable<(UIBox2i a, UIBox2i b, UIBox2i? expected)> Intersections => - new (UIBox2i, UIBox2i, UIBox2i?)[] - { - (new UIBox2i(0, 0, 5, 5), new UIBox2i(2, 2, 4, 4), new UIBox2i(2, 2, 4, 4)), - (new UIBox2i(0, 0, 5, 5), new UIBox2i(3, 3, 7, 7), new UIBox2i(3, 3, 5, 5)), - (new UIBox2i(2, 0, 5, 5), new UIBox2i(0, 3, 4, 7), new UIBox2i(2, 3, 4, 5)), - (new UIBox2i(2, 0, 5, 5), new UIBox2i(6, 6, 10, 10), null), - }; - - [Test] - public void Box2iVectorConstructor([ValueSource(nameof(Sources))] (int, int, int, int) test) - { - var (left, top, right, bottom) = test; - var box = new UIBox2i(new Vector2i(left, top), new Vector2i(right, bottom)); + var (left, top, right, bottom) = test; + var box = new UIBox2i(new Vector2i(left, top), new Vector2i(right, bottom)); + using (Assert.EnterMultipleScope()) + { Assert.That(box.Left, Is.EqualTo(left)); Assert.That(box.Top, Is.EqualTo(top)); Assert.That(box.Right, Is.EqualTo(right)); Assert.That(box.Bottom, Is.EqualTo(bottom)); } + } - [Test] - public void Box2iEdgesConstructor([ValueSource(nameof(Sources))] (int, int, int, int) test) - { - var (left, top, right, bottom) = test; - var box = new UIBox2i(left, top, right, bottom); + [Test] + public void Box2iEdgesConstructor([ValueSource(nameof(Sources))] (int, int, int, int) test) + { + var (left, top, right, bottom) = test; + var box = new UIBox2i(left, top, right, bottom); + using (Assert.EnterMultipleScope()) + { Assert.That(box.Left, Is.EqualTo(left)); Assert.That(box.Top, Is.EqualTo(top)); Assert.That(box.Right, Is.EqualTo(right)); Assert.That(box.Bottom, Is.EqualTo(bottom)); } + } - [Test] - public void Box2iCornerVectorProperties([ValueSource(nameof(Sources))] (int, int, int, int) test) + [Test] + public void Box2iValidatesProperties() + { + var box = new UIBox2i(-1, -2, 3, 4); + + using (Assert.EnterMultipleScope()) { - var (left, top, right, bottom) = test; - var box = new UIBox2i(left, top, right, bottom); + Assert.Throws(() => box.Left = 4); + Assert.Throws(() => box.Top = 5); + Assert.Throws(() => box.Right = -2); + Assert.Throws(() => box.Bottom = -3); + Assert.Throws(() => box.TopLeft = new Vector2i(4, 0)); + Assert.Throws(() => box.BottomRight = new Vector2i(0, -3)); + } + } + + [Test] + public void Box2iCornerVectorProperties([ValueSource(nameof(Sources))] (int, int, int, int) test) + { + var (left, top, right, bottom) = test; + var box = new UIBox2i(left, top, right, bottom); - var br = new Vector2i(right, bottom); - var tl = new Vector2i(left, top); - var tr = new Vector2i(right, top); - var bl = new Vector2i(left, bottom); + var br = new Vector2i(right, bottom); + var tl = new Vector2i(left, top); + var tr = new Vector2i(right, top); + var bl = new Vector2i(left, bottom); + using (Assert.EnterMultipleScope()) + { Assert.That(box.BottomRight, Is.EqualTo(br)); Assert.That(box.TopLeft, Is.EqualTo(tl)); Assert.That(box.TopRight, Is.EqualTo(tr)); Assert.That(box.BottomLeft, Is.EqualTo(bl)); } + } - [Test] - public void Box2iFromDimensionsInt([ValueSource(nameof(Sources))] (int, int, int, int) test) - { - var (left, top, right, bottom) = test; + [Test] + public void Box2iFromDimensionsInt([ValueSource(nameof(Sources))] (int, int, int, int) test) + { + var (left, top, right, bottom) = test; - var width = Math.Abs(left - right); - var height = Math.Abs(top - bottom); + var width = Math.Abs(left - right); + var height = Math.Abs(top - bottom); - var box = UIBox2i.FromDimensions(left, top, width, height); + var box = UIBox2i.FromDimensions(left, top, width, height); + using (Assert.EnterMultipleScope()) + { Assert.That(box.Left, Is.EqualTo(left)); Assert.That(box.Top, Is.EqualTo(top)); Assert.That(box.Right, Is.EqualTo(left + width)); @@ -119,18 +145,21 @@ public void Box2iFromDimensionsInt([ValueSource(nameof(Sources))] (int, int, int Assert.That(box.Width, Is.EqualTo(width)); Assert.That(box.Height, Is.EqualTo(height)); } + } - [Test] - public void Box2iFromDimensionsVectors([ValueSource(nameof(Sources))] (int, int, int, int) test) - { - var (left, top, right, bottom) = test; + [Test] + public void Box2iFromDimensionsVectors([ValueSource(nameof(Sources))] (int, int, int, int) test) + { + var (left, top, right, bottom) = test; - var width = Math.Abs(left - right); - var height = Math.Abs(top - bottom); - var size = new Vector2i(width, height); + var width = Math.Abs(left - right); + var height = Math.Abs(top - bottom); + var size = new Vector2i(width, height); - var box = UIBox2i.FromDimensions(new Vector2i(left, top), size); + var box = UIBox2i.FromDimensions(new Vector2i(left, top), size); + using (Assert.EnterMultipleScope()) + { Assert.That(box.Left, Is.EqualTo(left)); Assert.That(box.Top, Is.EqualTo(top)); Assert.That(box.Right, Is.EqualTo(left + width)); @@ -138,95 +167,113 @@ public void Box2iFromDimensionsVectors([ValueSource(nameof(Sources))] (int, int, Assert.That(box.Size, Is.EqualTo(size)); } + } - [Test] - public void Box2iNotContainsSelfOpen() - { - var box = new UIBox2i(-1, -1, 1, 1); + [Test] + public void Box2iNotContainsSelfOpen() + { + var box = new UIBox2i(-1, -1, 1, 1); + using (Assert.EnterMultipleScope()) + { Assert.That(box.Contains(box.BottomLeft, false), Is.False); Assert.That(box.Contains(box.TopLeft, false), Is.False); Assert.That(box.Contains(box.TopRight, false), Is.False); Assert.That(box.Contains(box.BottomRight, false), Is.False); } + } - [Test] - public void Box2iContainsSelfClosed() - { - var box = new UIBox2i(-1, -1, 1, 1); + [Test] + public void Box2iContainsSelfClosed() + { + var box = new UIBox2i(-1, -1, 1, 1); + using (Assert.EnterMultipleScope()) + { Assert.That(box.Contains(box.BottomLeft)); Assert.That(box.Contains(box.TopLeft)); Assert.That(box.Contains(box.TopRight)); Assert.That(box.Contains(box.BottomRight)); + } - var bl = box.BottomLeft; - var tl = box.TopLeft; - var tr = box.TopRight; - var br = box.BottomRight; + var bl = box.BottomLeft; + var tl = box.TopLeft; + var tr = box.TopRight; + var br = box.BottomRight; + using (Assert.EnterMultipleScope()) + { Assert.That(box.Contains(bl.X, bl.Y)); Assert.That(box.Contains(tl.X, tl.Y)); Assert.That(box.Contains(tr.X, tr.Y)); Assert.That(box.Contains(br.X, br.Y)); } + } - [Test] - public void Box2iContains([ValueSource(nameof(SmallTranslations))] - (int, int) test) - { - var (x, y) = test; - var vec = new Vector2i(x, y); + [Test] + public void Box2iContains([ValueSource(nameof(SmallTranslations))] (int, int) test) + { + var (x, y) = test; + var vec = new Vector2i(x, y); - var box = new UIBox2i(-2, -2, 2, 2); + var box = new UIBox2i(-2, -2, 2, 2); + using (Assert.EnterMultipleScope()) + { Assert.That(box.Contains(x, y)); Assert.That(box.Contains(vec)); Assert.That(box.Contains(vec, false)); } + } - [Test] - public void Box2iNotContains([ValueSource(nameof(LargeTranslations))] - (int, int) test) - { - var (x, y) = test; - var vec = new Vector2i(x, y); + [Test] + public void Box2iNotContains([ValueSource(nameof(LargeTranslations))] (int, int) test) + { + var (x, y) = test; + var vec = new Vector2i(x, y); - var box = new UIBox2i(-2, -2, 2, 2); + var box = new UIBox2i(-2, -2, 2, 2); + using (Assert.EnterMultipleScope()) + { Assert.That(box.Contains(x, y), Is.False); Assert.That(box.Contains(vec), Is.False); Assert.That(box.Contains(vec, false), Is.False); } + } - [Test] - public void Box2iTranslated([ValueSource(nameof(LargeTranslations))] - (int, int) test) - { - var (x, y) = test; - var vec = new Vector2i(x, y); + [Test] + public void Box2iTranslated([ValueSource(nameof(LargeTranslations))] (int, int) test) + { + var (x, y) = test; + var vec = new Vector2i(x, y); - var box = new UIBox2i(-1, -1, 1, 1); - var scaledBox = box.Translated(vec); + var box = new UIBox2i(-1, -1, 1, 1); + var scaledBox = box.Translated(vec); + using (Assert.EnterMultipleScope()) + { Assert.That(scaledBox.Left, Is.EqualTo(box.Left + x)); Assert.That(scaledBox.Top, Is.EqualTo(box.Top + y)); Assert.That(scaledBox.Bottom, Is.EqualTo(box.Bottom + y)); Assert.That(scaledBox.Right, Is.EqualTo(box.Right + x)); } + } - [Test] - public void Box2iEquals([ValueSource(nameof(Sources))] (int, int, int, int) test) - { - var (left, top, right, bottom) = test; + [Test] + public void Box2iEquals([ValueSource(nameof(Sources))] (int, int, int, int) test) + { + var (left, top, right, bottom) = test; - var controlBox = new UIBox2i(left, top, right, bottom); - var differentBox = new UIBox2i(-3, -3, 3, 3); - var sameBox = new UIBox2i(left, top, right, bottom); - Object sameBoxAsObject = sameBox; - UIBox2i? nullBox = null; - Vector2i notBox = new Vector2i(left, top); + var controlBox = new UIBox2i(left, top, right, bottom); + var differentBox = new UIBox2i(-3, -3, 3, 3); + var sameBox = new UIBox2i(left, top, right, bottom); + Object sameBoxAsObject = sameBox; + UIBox2i? nullBox = null; + Vector2i notBox = new Vector2i(left, top); + using (Assert.EnterMultipleScope()) + { Assert.That(controlBox.Equals(controlBox)); Assert.That(controlBox.Equals(differentBox), Is.False); Assert.That(controlBox.Equals(sameBox)); @@ -234,13 +281,16 @@ public void Box2iEquals([ValueSource(nameof(Sources))] (int, int, int, int) test Assert.That(controlBox.Equals(nullBox), Is.False); Assert.That(controlBox.Equals(notBox), Is.False); } + } - [Test] - public void UIBox2iIntersection( - [ValueSource(nameof(Intersections))] (UIBox2i a, UIBox2i b, UIBox2i? expected) value) - { - var (a, b, expected) = value; + [Test] + public void UIBox2iIntersection( + [ValueSource(nameof(Intersections))] (UIBox2i a, UIBox2i b, UIBox2i? expected) value) + { + var (a, b, expected) = value; + using (Assert.EnterMultipleScope()) + { // This should be a symmetric operation. Assert.That(a.Intersection(b), Is.EqualTo(expected)); Assert.That(b.Intersection(a), Is.EqualTo(expected)); diff --git a/Robust.Shared.Maths/Box2.cs b/Robust.Shared.Maths/Box2.cs index f7c8db88a28..81c1c43c855 100644 --- a/Robust.Shared.Maths/Box2.cs +++ b/Robust.Shared.Maths/Box2.cs @@ -309,13 +309,18 @@ public readonly bool Contains(float x, float y) [Pure] public readonly bool Contains(Vector2 point, bool closedRegion = true) { - var xOk = closedRegion - ? point.X >= Left ^ point.X > Right - : point.X > Left ^ point.X >= Right; - var yOk = closedRegion - ? point.Y >= Bottom ^ point.Y > Top - : point.Y > Bottom ^ point.Y >= Top; - return xOk && yOk; + if (closedRegion) + { + return point.X >= Left + && point.X <= Right + && point.Y >= Bottom + && point.Y <= Top; + } + + return point.X > Left + && point.X < Right + && point.Y > Bottom + && point.Y < Top; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Robust.Shared.Maths/Box2i.cs b/Robust.Shared.Maths/Box2i.cs index f0596ed57ec..710af7a11f9 100644 --- a/Robust.Shared.Maths/Box2i.cs +++ b/Robust.Shared.Maths/Box2i.cs @@ -5,293 +5,514 @@ using System.Runtime.InteropServices; using Robust.Shared.Utility; -namespace Robust.Shared.Maths -{ - [Serializable] - [StructLayout(LayoutKind.Explicit)] - public struct Box2i : IEquatable, ISpanFormattable - { - public static Box2i Empty => new(); +namespace Robust.Shared.Maths; - [FieldOffset(sizeof(int) * 0)] public int Left; - [FieldOffset(sizeof(int) * 1)] public int Bottom; - [FieldOffset(sizeof(int) * 2)] public int Right; - [FieldOffset(sizeof(int) * 3)] public int Top; - - [FieldOffset(sizeof(int) * 0)] public Vector2i BottomLeft; - [FieldOffset(sizeof(int) * 2)] public Vector2i TopRight; +[Serializable] +[StructLayout(LayoutKind.Explicit)] +public struct Box2i : IEquatable, ISpanFormattable +{ + public static Box2i Empty => new(); - public readonly Vector2i BottomRight => new(Right, Bottom); - public readonly Vector2i TopLeft => new(Left, Top); - public readonly int Width => Math.Abs(Right - Left); - public readonly int Height => Math.Abs(Top - Bottom); - public readonly Vector2i Size => new(Width, Height); + [FieldOffset(sizeof(int) * 0)] internal int _left; + [FieldOffset(sizeof(int) * 1)] internal int _bottom; + [FieldOffset(sizeof(int) * 2)] internal int _right; + [FieldOffset(sizeof(int) * 3)] internal int _top; - public readonly int Area => Width * Height; - public readonly Vector2 Center => Size / 2f + BottomLeft; + [FieldOffset(sizeof(int) * 0)] internal Vector2i _bottomLeft; + [FieldOffset(sizeof(int) * 2)] internal Vector2i _topRight; - public Box2i(Vector2i bottomLeft, Vector2i topRight) + public int Left + { + readonly get => _left; + set { - Unsafe.SkipInit(out this); + if (value > _right) + throw new ArgumentOutOfRangeException(nameof(value), value, "Left cannot be greater than Right."); - BottomLeft = bottomLeft; - TopRight = topRight; + _left = value; } + } - public Box2i(int left, int bottom, int right, int top) + public int Bottom + { + readonly get => _bottom; + set { - Unsafe.SkipInit(out this); + if (value > _top) + throw new ArgumentOutOfRangeException(nameof(value), value, "Bottom cannot be greater than Top."); - Left = left; - Right = right; - Top = top; - Bottom = bottom; + _bottom = value; } + } - public static Box2i FromDimensions(int left, int bottom, int width, int height) + public int Right + { + readonly get => _right; + set { - return new(left, bottom, left + width, bottom + height); - } + if (value < _left) + throw new ArgumentOutOfRangeException(nameof(value), value, "Right cannot be less than Left."); - public static Box2i FromDimensions(Vector2i position, Vector2i size) - { - return FromDimensions(position.X, position.Y, size.X, size.Y); + _right = value; } + } - public readonly bool Contains(int x, int y) + public int Top + { + readonly get => _top; + set { - return Contains(new Vector2i(x, y)); - } + if (value < _bottom) + throw new ArgumentOutOfRangeException(nameof(value), value, "Top cannot be less than Bottom."); - public readonly bool Contains(Vector2i point, bool closedRegion = true) - { - var xOk = closedRegion - ? point.X >= Left ^ point.X > Right - : point.X > Left ^ point.X >= Right; - var yOk = closedRegion - ? point.Y >= Bottom ^ point.Y > Top - : point.Y > Bottom ^ point.Y >= Top; - return xOk && yOk; + _top = value; } + } - /// - /// Unlike Contains this assumes the Vector2i occupies an entire tile so we need the point to the top-right of it for consideration. - /// - public readonly bool ContainsTile(Vector2i tile, bool closedRegion = true) + public Vector2i BottomLeft + { + readonly get => _bottomLeft; + set { - var xOk = closedRegion - ? tile.X >= Left ^ tile.X + 1 > Right - : tile.X > Left ^ tile.X + 1 >= Right; - var yOk = closedRegion - ? tile.Y >= Bottom ^ tile.Y + 1 > Top - : tile.Y > Bottom ^ tile.Y + 1 >= Top; - return xOk && yOk; - } + if (value.X > _right) + throw new ArgumentOutOfRangeException(nameof(value), value, "BottomLeft.X cannot be greater than Right."); - public readonly bool IsEmpty() - { - return Bottom == Top || Left == Right; - } + if (value.Y > _top) + throw new ArgumentOutOfRangeException(nameof(value), value, "BottomLeft.Y cannot be greater than Top."); - /// Returns a UIBox2 translated by the given amount. - public readonly Box2i Translated(Vector2i point) - { - return new(Left + point.X, Bottom + point.Y, Right + point.X, Top + point.Y); + _bottomLeft = value; } + } - /// - /// Returns the smallest rectangle that contains both of the rectangles. - /// - [Pure] - public readonly Box2i Union(in Box2i other) + public Vector2i TopRight + { + readonly get => _topRight; + set { - var botLeft = Vector2i.ComponentMin(BottomLeft, other.BottomLeft); - var topRight = Vector2i.ComponentMax(TopRight, other.TopRight); + if (value.X < _left) + throw new ArgumentOutOfRangeException(nameof(value), value, "TopRight.X cannot be less than Left."); - if (botLeft.X <= topRight.X && botLeft.Y <= topRight.Y) - return new Box2i(botLeft, topRight); + if (value.Y < _bottom) + throw new ArgumentOutOfRangeException(nameof(value), value, "TopRight.Y cannot be less than Bottom."); - return new Box2i(); + _topRight = value; } + } - /// - /// Unions the box2i with the specified Vector2i. - /// - /// - /// Union treating other as a single point and not an entire tile. - /// - public readonly Box2i Union(in Vector2i other) - { - if (Contains(other)) - return this; + public readonly Vector2i BottomRight => new(Right, Bottom); - var botLeft = Vector2i.ComponentMin(BottomLeft, other); - var topRight = Vector2i.ComponentMax(TopRight, other); + public readonly Vector2i TopLeft => new(Left, Top); - return new Box2i(botLeft, topRight); - } + public readonly int Width => _right - _left; - /// - /// Unions the box2i with the specified Vector2i. - /// - /// - /// Union treating other as an entire tile and not a single point. - /// - public readonly Box2i UnionTile(in Vector2i other) - { - if (ContainsTile(other)) - return this; + public readonly int Height => _top - _bottom; - var botLeft = Vector2i.ComponentMin(BottomLeft, other); - var topRight = Vector2i.ComponentMax(TopRight, other + Vector2i.One); + public readonly Vector2i Size => new(Width, Height); - return new Box2i(botLeft, topRight); - } + public readonly int Area => Width * Height; + public readonly Vector2 Center => new Vector2(_left + _right, _bottom + _top) / 2f; - // override object.Equals - public readonly override bool Equals(object? obj) - { - if (obj is Box2i box) - { - return Equals(box); - } + private static void Validate(int left, int bottom, int right, int top) + { + if (left > right) + throw new ArgumentException("Left cannot be greater than Right.", nameof(left)); - return false; - } + if (bottom > top) + throw new ArgumentException("Bottom cannot be greater than Top.", nameof(bottom)); + } - public readonly bool Equals(Box2i other) - { - return other.Left == Left && other.Right == Right && other.Bottom == Bottom && other.Top == Top; - } + public Box2i(Vector2i bottomLeft, Vector2i topRight) + { + Unsafe.SkipInit(out this); - // override object.GetHashCode - public readonly override int GetHashCode() - { - var code = Left.GetHashCode(); - code = (code * 929) ^ Right.GetHashCode(); - code = (code * 929) ^ Top.GetHashCode(); - code = (code * 929) ^ Bottom.GetHashCode(); - return code; - } + Validate(bottomLeft.X, bottomLeft.Y, topRight.X, topRight.Y); - public static explicit operator Box2i(Box2 box) - { - return new((int) box.Left, (int) box.Bottom, (int) box.Right, (int) box.Top); - } + _bottomLeft = bottomLeft; + _topRight = topRight; + } - public static implicit operator Box2(Box2i box) - { - return new(box.Left, box.Bottom, box.Right, box.Top); - } + public Box2i(int left, int bottom, int right, int top) + { + Unsafe.SkipInit(out this); - public readonly override string ToString() - { - return $"({Left}, {Bottom}, {Right}, {Top})"; - } + Validate(left, bottom, right, top); - public readonly string ToString(string? format, IFormatProvider? formatProvider) - { - return ToString(); - } + _left = left; + _right = right; + _top = top; + _bottom = bottom; + } - public readonly bool TryFormat( - Span destination, - out int charsWritten, - ReadOnlySpan format, - IFormatProvider? provider) - { - return FormatHelpers.TryFormatInto( - destination, - out charsWritten, - $"({Left}, {Bottom}, {Right}, {Top})"); - } + /// + /// Creates a Box2i with no bounds validation applied, use at your own risk. + /// + internal static Box2i DangerousCreate(int left, int bottom, int right, int top) + { + Unsafe.SkipInit(out Box2i box); + box._left = left; + box._right = right; + box._top = top; + box._bottom = bottom; + return box; + } - /// - /// Multiplies each side of the box by the scalar. - /// - [Pure] - public Box2i Scale(int scalar) - { - return new Box2i( - Left * scalar, - Bottom * scalar, - Right * scalar, - Top * scalar); - } + [Pure] + public static Box2i FromDimensions(int left, int bottom, int width, int height) + { + return new Box2i(left, bottom, left + width, bottom + height); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - public bool Intersects(in Box2i other) - { - return other.Bottom <= this.Top && other.Top >= this.Bottom && other.Right >= this.Left && - other.Left <= this.Right; - } + [Pure] + public static Box2i FromDimensions(Vector2i position, Vector2i size) + { + return FromDimensions(position.X, position.Y, size.X, size.Y); + } + + [Pure] + public static Box2i FromTwoPoints(Vector2i a, Vector2i b) + { + return new Box2i(Vector2i.ComponentMin(a, b), Vector2i.ComponentMax(a, b)); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - public readonly Box2i Enlarged(int size) + [Pure] + public readonly bool Contains(int x, int y) + { + return Contains(new Vector2i(x, y)); + } + + [Pure] + public readonly bool Contains(in Box2i inner) + => Left <= inner.Left + && Bottom <= inner.Bottom + && Right >= inner.Right + && Top >= inner.Top; + + [Pure] + public readonly bool Contains(Vector2i point, bool closedRegion = true) + { + var xOk = closedRegion + ? point.X >= Left ^ point.X > Right + : point.X > Left ^ point.X >= Right; + var yOk = closedRegion + ? point.Y >= Bottom ^ point.Y > Top + : point.Y > Bottom ^ point.Y >= Top; + return xOk && yOk; + } + + /// + /// Unlike Contains this assumes the Vector2i occupies an entire tile so we need the point to the top-right of it for consideration. + /// + [Pure] + public readonly bool ContainsTile(Vector2i tile, bool closedRegion = true) + { + if (closedRegion) { - return new(Left - size, Bottom - size, Right + size, Top + size); + return tile.X >= Left + && tile.X + 1 <= Right + && tile.Y >= Bottom + && tile.Y + 1 <= Top; } + + return tile.X > Left + && tile.X + 1 < Right + && tile.Y > Bottom + && tile.Y + 1 < Top; + } + + [Pure] + public readonly bool IsEmpty() + { + return Bottom >= Top || Left >= Right; + } + + /// Returns a UIBox2 translated by the given amount. + [Pure] + public readonly Box2i Translated(Vector2i point) + { + return new Box2i(Left + point.X, Bottom + point.Y, Right + point.X, Top + point.Y); + } + + /// + /// Returns the smallest rectangle that contains both of the rectangles. + /// + [Pure] + public readonly Box2i Union(in Box2i other) + { + var botLeft = Vector2i.ComponentMin(BottomLeft, other.BottomLeft); + var topRight = Vector2i.ComponentMax(TopRight, other.TopRight); + + if (botLeft.X <= topRight.X && botLeft.Y <= topRight.Y) + return new Box2i(botLeft, topRight); + + return new Box2i(); + } + + /// + /// Unions the box2i with the specified Vector2i. + /// + /// + /// Union treating other as a single point and not an entire tile. + /// + [Pure] + public readonly Box2i Union(in Vector2i other) + { + if (Contains(other)) + return this; + + var botLeft = Vector2i.ComponentMin(BottomLeft, other); + var topRight = Vector2i.ComponentMax(TopRight, other); + + return new Box2i(botLeft, topRight); } /// - /// Iterates neighbouring tiles to a box2i. + /// Unions the box2i with the specified Vector2i. /// - public struct Box2iEdgeEnumerator + /// + /// Union treating other as an entire tile and not a single point. + /// + [Pure] + public readonly Box2i UnionTile(in Vector2i other) { - private readonly bool _corners; - private readonly Box2i _box; - private readonly int _offset; - private int _x; - private int _y; + if (ContainsTile(other)) + return this; - public Box2iEdgeEnumerator(Box2i box, bool corners, int offset = 1) + var botLeft = Vector2i.ComponentMin(BottomLeft, other); + var topRight = Vector2i.ComponentMax(TopRight, other + Vector2i.One); + + return new Box2i(botLeft, topRight); + } + + // override object.Equals + public readonly override bool Equals(object? obj) + { + if (obj is Box2i box) { - _box = box; - _corners = corners; - _x = _box.Left - offset; - _y = _box.Bottom - offset; - _offset = offset; + return Equals(box); } - public bool MoveNext(out Vector2i index) + return false; + } + + public readonly bool Equals(Box2i other) + { + return other.Left == Left && other.Right == Right && other.Bottom == Bottom && other.Top == Top; + } + + // override object.GetHashCode + public readonly override int GetHashCode() + { + var code = Left.GetHashCode(); + code = (code * 929) ^ Right.GetHashCode(); + code = (code * 929) ^ Top.GetHashCode(); + code = (code * 929) ^ Bottom.GetHashCode(); + return code; + } + + public static explicit operator Box2i(Box2 box) + { + return new Box2i((int) box.Left, (int) box.Bottom, (int) box.Right, (int) box.Top); + } + + public static implicit operator Box2(Box2i box) + { + return new Box2(box.Left, box.Bottom, box.Right, box.Top); + } + + public readonly override string ToString() + { + return $"({Left}, {Bottom}, {Right}, {Top})"; + } + + /// + /// Compares two objects for equality by value. + /// + public static bool operator ==(Box2i a, Box2i b) + { + return a.Equals(b); + } + + public static bool operator !=(Box2i a, Box2i b) + { + return !a.Equals(b); + } + + public readonly string ToString(string? format, IFormatProvider? formatProvider) + { + return ToString(); + } + + public readonly bool TryFormat( + Span destination, + out int charsWritten, + ReadOnlySpan format, + IFormatProvider? provider) + { + return FormatHelpers.TryFormatInto( + destination, + out charsWritten, + $"({Left}, {Bottom}, {Right}, {Top})"); + } + + /// + /// Multiplies each side of the box by the scalar. + /// + [Pure] + public readonly Box2i Scale(int scalar) + { + return new Box2i( + Left * scalar, + Bottom * scalar, + Right * scalar, + Top * scalar); + } + + [Pure] + public readonly bool Intersects(in Box2i other) + { + return other._bottom <= _top + && other._top >= _bottom + && other._right >= _left + && other._left <= _right; + } + + [Pure] + public readonly Box2i Enlarged(int size) + { + return new Box2i(Left - size, Bottom - size, Right + size, Top + size); + } + + /// + /// Returns the intersection box created when two boxes overlap. + /// + [Pure] + public readonly Box2i Intersect(in Box2i other) + { + var bottomLeft = Vector2i.ComponentMax(BottomLeft, other.BottomLeft); + var topRight = Vector2i.ComponentMin(TopRight, other.TopRight); + + if (bottomLeft.X <= topRight.X && bottomLeft.Y <= topRight.Y) + return new Box2i(bottomLeft, topRight); + + return new Box2i(); + } + + [Pure] + public readonly bool IsValid() + { + return Right >= Left && Top >= Bottom; + } + + [Pure] + public readonly bool Encloses(in Box2i inner) + { + return Left < inner.Left && Bottom < inner.Bottom && Right > inner.Right && Top > inner.Top; + } + + /// + /// Returns this box enlarged to also contain the specified position. + /// + [Pure] + public readonly Box2i ExtendToContain(Vector2i vec) + { + return new Box2i(Vector2i.ComponentMin(BottomLeft, vec), Vector2i.ComponentMax(TopRight, vec)); + } + + /// + /// Given a point, returns the closest point to it inside the box. + /// + [Pure] + public readonly Vector2i ClosestPoint(in Vector2i position) + { + return new Vector2i( + MathHelper.Clamp(position.X, Left, Right), + MathHelper.Clamp(position.Y, Bottom, Top)); + } + + public static int Perimeter(in Box2i box) + => (box.Width + box.Height) * 2; + + public static int UnionPerimeter(in Box2i a, in Box2i b) + { + var left = Math.Min(a._left, b._left); + var bottom = Math.Min(a._bottom, b._bottom); + var right = Math.Max(a._right, b._right); + var top = Math.Max(a._top, b._top); + + return 2 * ((right - left) + (top - bottom)); + } + + [Pure] + public static Box2i Union(Box2i a, Box2i b) + { + return new Box2i( + Vector2i.ComponentMin(a.BottomLeft, b.BottomLeft), + Vector2i.ComponentMax(a.TopRight, b.TopRight)); + } + + [Pure] + public static Box2i Union(in Vector2i a, in Vector2i b) + { + return FromTwoPoints(a, b); + } +} + +/// +/// Iterates neighbouring tiles to a box2i. +/// +public struct Box2iEdgeEnumerator +{ + private readonly bool _corners; + private readonly Box2i _box; + private readonly int _offset; + private int _x; + private int _y; + + public Box2iEdgeEnumerator(Box2i box, bool corners, int offset = 1) + { + _box = box; + _corners = corners; + _x = _box.Left - offset; + _y = _box.Bottom - offset; + _offset = offset; + } + + public bool MoveNext(out Vector2i index) + { + for (var x = _x; x < _box.Right + _offset; x++) { - for (var x = _x; x < _box.Right + _offset; x++) + for (var y = _y; y < _box.Top + _offset; y++) { - for (var y = _y; y < _box.Top + _offset; y++) + if (x != _box.Left - _offset && + x != _box.Right + (_offset - 1) && + y != _box.Bottom - _offset && + y != _box.Top + (_offset - 1)) { - if (x != _box.Left - _offset && - x != _box.Right + (_offset - 1) && - y != _box.Bottom - _offset && - y != _box.Top + (_offset - 1)) - { - continue; - } - - if (!_corners && - (x == _box.Left - _offset && (y == _box.Bottom - _offset || y == _box.Top + (_offset - 1)) || - x == _box.Right && (y == _box.Bottom - _offset || y == _box.Top + (_offset - 1)))) - { - continue; - } - - _x = x; - _y = y + 1; - - if (_y == _box.Top + _offset) - { - _x++; - _y = _box.Bottom - _offset; - } - - index = new Vector2i(x, y); - return true; + continue; + } + + if (!_corners && + (x == _box.Left - _offset && (y == _box.Bottom - _offset || y == _box.Top + (_offset - 1)) || + x == _box.Right && (y == _box.Bottom - _offset || y == _box.Top + (_offset - 1)))) + { + continue; } - } - index = default; - return false; + _x = x; + _y = y + 1; + + if (_y == _box.Top + _offset) + { + _x++; + _y = _box.Bottom - _offset; + } + + index = new Vector2i(x, y); + return true; + } } + + index = default; + return false; } } diff --git a/Robust.Shared.Maths/Direction.cs b/Robust.Shared.Maths/Direction.cs index d661e8ddc6f..188a45745e2 100644 --- a/Robust.Shared.Maths/Direction.cs +++ b/Robust.Shared.Maths/Direction.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Diagnostics.Contracts; using System.Numerics; +using System.Runtime.CompilerServices; namespace Robust.Shared.Maths { @@ -55,136 +56,81 @@ public static class DirectionExtensions private const double Segment = 2 * Math.PI / 8.0; // Cut the circle into 8 pieces + // 1f / MathF.Sqrt(2) except we can't const that. + private const float DiagonalComponent = 0.7071067811865476f; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Direction AsDir(this DirectionFlag directionFlag) { - switch (directionFlag) + return directionFlag switch { - case DirectionFlag.South: - return Direction.South; - case DirectionFlag.SouthEast: - return Direction.SouthEast; - case DirectionFlag.East: - return Direction.East; - case DirectionFlag.NorthEast: - return Direction.NorthEast; - case DirectionFlag.North: - return Direction.North; - case DirectionFlag.NorthWest: - return Direction.NorthWest; - case DirectionFlag.West: - return Direction.West; - case DirectionFlag.SouthWest: - return Direction.SouthWest; - default: - throw new ArgumentOutOfRangeException(); - } + DirectionFlag.South => Direction.South, + DirectionFlag.SouthEast => Direction.SouthEast, + DirectionFlag.East => Direction.East, + DirectionFlag.NorthEast => Direction.NorthEast, + DirectionFlag.North => Direction.North, + DirectionFlag.NorthWest => Direction.NorthWest, + DirectionFlag.West => Direction.West, + DirectionFlag.SouthWest => Direction.SouthWest, + _ => throw new ArgumentOutOfRangeException(nameof(directionFlag), directionFlag, null) + }; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static DirectionFlag AsFlag(this Direction direction) { - switch (direction) + return direction switch { - case Direction.South: - return DirectionFlag.South; - case Direction.SouthEast: - return DirectionFlag.SouthEast; - case Direction.East: - return DirectionFlag.East; - case Direction.NorthEast: - return DirectionFlag.NorthEast; - case Direction.North: - return DirectionFlag.North; - case Direction.NorthWest: - return DirectionFlag.NorthWest; - case Direction.West: - return DirectionFlag.West; - case Direction.SouthWest: - return DirectionFlag.SouthWest; - default: - throw new ArgumentOutOfRangeException(); - } + Direction.South => DirectionFlag.South, + Direction.SouthEast => DirectionFlag.SouthEast, + Direction.East => DirectionFlag.East, + Direction.NorthEast => DirectionFlag.NorthEast, + Direction.North => DirectionFlag.North, + Direction.NorthWest => DirectionFlag.NorthWest, + Direction.West => DirectionFlag.West, + Direction.SouthWest => DirectionFlag.SouthWest, + _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) + }; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static DirectionFlag AsDirectionFlag(this Vector2i indices) { - switch (indices.X) + return (indices.X, indices.Y) switch { - case -1: - switch (indices.Y) - { - case -1: - return DirectionFlag.SouthWest; - case 0: - return DirectionFlag.West; - case 1: - return DirectionFlag.NorthWest; - } - break; - case 0: - switch (indices.Y) - { - case -1: - return DirectionFlag.South; - case 1: - return DirectionFlag.North; - } - break; - case 1: - switch (indices.Y) - { - case -1: - return DirectionFlag.SouthEast; - case 0: - return DirectionFlag.East; - case 1: - return DirectionFlag.NorthEast; - } - break; - } - - throw new ArgumentOutOfRangeException( - $"Tried to use a non-supported Vector2i for conversion to direction flag"); + (-1, -1) => DirectionFlag.SouthWest, + (-1, 0) => DirectionFlag.West, + (-1, 1) => DirectionFlag.NorthWest, + (0, -1) => DirectionFlag.South, + (0, 1) => DirectionFlag.North, + (1, -1) => DirectionFlag.SouthEast, + (1, 0) => DirectionFlag.East, + (1, 1) => DirectionFlag.NorthEast, + _ => throw new ArgumentOutOfRangeException( + nameof(indices), + indices, + "Tried to use a non-supported Vector2i for conversion to direction flag") + }; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Direction AsDirection(this Vector2i indices) { - switch (indices.X) + return (indices.X, indices.Y) switch { - case -1: - switch (indices.Y) - { - case -1: - return Direction.SouthWest; - case 0: - return Direction.West; - case 1: - return Direction.NorthWest; - } - break; - case 0: - switch (indices.Y) - { - case -1: - return Direction.South; - case 1: - return Direction.North; - } - break; - case 1: - switch (indices.Y) - { - case -1: - return Direction.SouthEast; - case 0: - return Direction.East; - case 1: - return Direction.NorthEast; - } - break; - } - - throw new ArgumentOutOfRangeException( - $"Tried to use a non-supported Vector2i for conversion to direction"); + (-1, -1) => Direction.SouthWest, + (-1, 0) => Direction.West, + (-1, 1) => Direction.NorthWest, + (0, -1) => Direction.South, + (0, 1) => Direction.North, + (1, -1) => Direction.SouthEast, + (1, 0) => Direction.East, + (1, 1) => Direction.NorthEast, + _ => throw new ArgumentOutOfRangeException( + nameof(indices), + indices, + "Tried to use a non-supported Vector2i for conversion to direction") + }; } @@ -193,6 +139,7 @@ public static Direction AsDirection(this Vector2i indices) ///
/// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Direction GetDir(this Vector2 vec) { return Angle.FromWorldVec(vec).GetDir(); @@ -203,6 +150,7 @@ public static Direction GetDir(this Vector2 vec) ///
/// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Direction GetDir(this Vector2i vec) { return new Angle(vec).GetDir(); @@ -213,103 +161,88 @@ public static Direction GetDir(this Vector2i vec) /// /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Direction GetCardinalDir(this Vector2i vec) { return new Angle(vec).GetCardinalDir(); } - public static Direction GetOpposite(this Direction direction) + /// + extension(Direction direction) { - return direction switch + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Direction GetOpposite() { - Direction.East => Direction.West, - Direction.West => Direction.East, - Direction.North => Direction.South, - Direction.South => Direction.North, - Direction.NorthEast => Direction.SouthWest, - Direction.SouthWest => Direction.NorthEast, - Direction.NorthWest => Direction.SouthEast, - Direction.SouthEast => Direction.NorthWest, - _ => throw new ArgumentOutOfRangeException(nameof(direction)) - }; - } + return (Direction) (((int) direction + 4) & 7); + } - public static Direction GetClockwise90Degrees(this Direction direction) - { - return direction switch + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Direction GetClockwise90Degrees() { - Direction.East => Direction.South, - Direction.West => Direction.North, - Direction.North => Direction.East, - Direction.South => Direction.West, - Direction.NorthEast => Direction.SouthEast, - Direction.SouthWest => Direction.NorthWest, - Direction.NorthWest => Direction.NorthEast, - Direction.SouthEast => Direction.SouthWest, - _ => throw new ArgumentOutOfRangeException(nameof(direction)) - }; - } - - /// - /// Converts a direction to an angle, where angle is -PI to +PI. - /// - /// - /// - [Pure] - public static Angle ToAngle(this Direction dir) - { - var ang = Segment * (int) dir; - - if (ang > Math.PI) // convert 0 > 2PI to -PI > +PI - ang -= 2 * Math.PI; + return (Direction) (((int) direction + 6) & 7); + } - return ang; - } + /// + /// Converts a direction to an angle, where angle is -PI to +PI. + /// + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Angle ToAngle() + { + var ang = Segment * (int) direction; - private static readonly Vector2[] DirectionVectors = { - new (0, -1), - new Vector2(1, -1).Normalized(), - new (1, 0), - new Vector2(1, 1).Normalized(), - new (0, 1), - new Vector2(-1, 1).Normalized(), - new (-1, 0), - new Vector2(-1, -1).Normalized() - }; + if (ang > Math.PI) // convert 0 > 2PI to -PI > +PI + ang -= 2 * Math.PI; - private static readonly Vector2i[] IntDirectionVectors = { - new (0, -1), - new (1, -1), - new (1, 0), - new (1, 1), - new (0, 1), - new (-1, 1), - new (-1, 0), - new (-1, -1) - }; + return ang; + } - /// - /// Converts a Direction to a normalized Direction vector. - /// - /// - /// a normalized 2D Vector - /// if invalid Direction is used - /// - public static Vector2 ToVec(this Direction dir) - { - return DirectionVectors[(int) dir]; - } + /// + /// Converts a Direction to a normalized Direction vector. + /// + /// a normalized 2D Vector + /// if invalid Direction is used + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector2 ToVec() + { + return direction switch + { + Direction.South => new Vector2(0, -1), + Direction.SouthEast => new Vector2(DiagonalComponent, -DiagonalComponent), + Direction.East => new Vector2(1, 0), + Direction.NorthEast => new Vector2(DiagonalComponent, DiagonalComponent), + Direction.North => new Vector2(0, 1), + Direction.NorthWest => new Vector2(-DiagonalComponent, DiagonalComponent), + Direction.West => new Vector2(-1, 0), + Direction.SouthWest => new Vector2(-DiagonalComponent, -DiagonalComponent), + _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) + }; + } - /// - /// Converts a Direction to a Vector2i. Useful for getting adjacent tiles. - /// - /// Direction - /// an 2D int Vector - /// if invalid Direction is used - /// - public static Vector2i ToIntVec(this Direction dir) - { - return IntDirectionVectors[(int) dir]; + /// + /// Converts a Direction to a Vector2i. Useful for getting adjacent tiles. + /// + /// an 2D int Vector + /// if invalid Direction is used + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector2i ToIntVec() + { + return direction switch + { + Direction.South => new Vector2i(0, -1), + Direction.SouthEast => new Vector2i(1, -1), + Direction.East => new Vector2i(1, 0), + Direction.NorthEast => new Vector2i(1, 1), + Direction.North => new Vector2i(0, 1), + Direction.NorthWest => new Vector2i(-1, 1), + Direction.West => new Vector2i(-1, 0), + Direction.SouthWest => new Vector2i(-1, -1), + _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) + }; + } } /// @@ -319,6 +252,7 @@ public static Vector2i ToIntVec(this Direction dir) /// 2D integer vector /// Direction by which we offset /// a newly vector offset by the dir or exception if the direction is invalid + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2i Offset(this Vector2i vec, Direction dir) { return vec + dir.ToIntVec(); @@ -329,11 +263,13 @@ public static Vector2i Offset(this Vector2i vec, Direction dir) /// /// Vector to get the angle from. /// Angle of the vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Angle ToAngle(this Vector2 vec) { return new(vec); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Angle ToWorldAngle(this Vector2 vec) { return Angle.FromWorldVec(vec); diff --git a/Robust.Shared.Maths/Matrix3Helpers.cs b/Robust.Shared.Maths/Matrix3Helpers.cs index 8df6f1c2b22..0fc00780315 100644 --- a/Robust.Shared.Maths/Matrix3Helpers.cs +++ b/Robust.Shared.Maths/Matrix3Helpers.cs @@ -34,7 +34,9 @@ public static Box2Rotated TransformBounds(this Matrix3x2 refFromBox, Box2Rotated [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Box2 TransformBox(this Matrix3x2 refFromBox, in Box2Rotated box) { - return (box.Transform * refFromBox).TransformBox(box.Box); + refFromBox.TransformBox(box, out var x, out var y); + var aabb = SimdHelpers.GetAABB(x, y); + return Unsafe.As, Box2>(ref aabb); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -44,7 +46,14 @@ internal static void TransformBox( out Vector128 x, out Vector128 y) { - (box.Transform * refFromBox).TransformBox(box.Box, out x, out y); + box.GetVertices(out var boxX, out var boxY); + + 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); } public static Box2 TransformBox(this Matrix3x2 refFromBox, in Box2 box) diff --git a/Robust.Shared.Maths/UIBox2i.cs b/Robust.Shared.Maths/UIBox2i.cs index 2ccadf89b11..2fb50ecc193 100644 --- a/Robust.Shared.Maths/UIBox2i.cs +++ b/Robust.Shared.Maths/UIBox2i.cs @@ -4,165 +4,260 @@ using System.Runtime.InteropServices; using Robust.Shared.Utility; -namespace Robust.Shared.Maths +namespace Robust.Shared.Maths; + +[Serializable] +[StructLayout(LayoutKind.Explicit)] +public struct UIBox2i : IEquatable, ISpanFormattable { - [Serializable] - [StructLayout(LayoutKind.Explicit)] - public struct UIBox2i : IEquatable, ISpanFormattable - { - [FieldOffset(sizeof(int) * 0)] public int Left; - [FieldOffset(sizeof(int) * 1)] public int Top; - [FieldOffset(sizeof(int) * 2)] public int Right; - [FieldOffset(sizeof(int) * 3)] public int Bottom; - - [FieldOffset(sizeof(int) * 0)] public Vector2i TopLeft; - [FieldOffset(sizeof(int) * 2)] public Vector2i BottomRight; - - public readonly Vector2i TopRight => new(Right, Top); - public readonly Vector2i BottomLeft => new(Left, Bottom); - public readonly int Width => Math.Abs(Right - Left); - public readonly int Height => Math.Abs(Top - Bottom); - public readonly Vector2i Size => new(Width, Height); - public readonly Vector2 Center => new Vector2(Left + Right, Top + Bottom) / 2f; - public UIBox2i(Vector2i topLeft, Vector2i bottomRight) - { - Unsafe.SkipInit(out this); + [FieldOffset(sizeof(int) * 0)] internal int _left; + [FieldOffset(sizeof(int) * 1)] internal int _top; + [FieldOffset(sizeof(int) * 2)] internal int _right; + [FieldOffset(sizeof(int) * 3)] internal int _bottom; - TopLeft = topLeft; - BottomRight = bottomRight; - } + [FieldOffset(sizeof(int) * 0)] internal Vector2i _topLeft; + [FieldOffset(sizeof(int) * 2)] internal Vector2i _bottomRight; - public UIBox2i(int left, int top, int right, int bottom) + public int Left + { + readonly get => _left; + set { - Unsafe.SkipInit(out this); + if (value > _right) + throw new ArgumentOutOfRangeException(nameof(value), value, "Left cannot be greater than Right."); - Left = left; - Right = right; - Top = top; - Bottom = bottom; + _left = value; } + } - public static UIBox2i FromDimensions(int left, int top, int width, int height) + public int Top + { + readonly get => _top; + set { - return new(left, top, left + width, top + height); - } + if (value > _bottom) + throw new ArgumentOutOfRangeException(nameof(value), value, "Top cannot be greater than Bottom."); - public static UIBox2i FromDimensions(Vector2i position, Vector2i size) - { - return FromDimensions(position.X, position.Y, size.X, size.Y); + _top = value; } + } - public readonly bool Contains(int x, int y) + public int Right + { + readonly get => _right; + set { - return Contains(new Vector2i(x, y)); - } + if (value < _left) + throw new ArgumentOutOfRangeException(nameof(value), value, "Right cannot be less than Left."); - public readonly bool Contains(Vector2i point, bool closedRegion = true) - { - var xOk = closedRegion - ? point.X >= Left ^ point.X > Right - : point.X > Left ^ point.X >= Right; - var yOk = closedRegion - ? point.Y >= Top ^ point.Y > Bottom - : point.Y > Top ^ point.Y >= Bottom; - return xOk && yOk; + _right = value; } + } - /// Returns a UIBox2 translated by the given amount. - public readonly UIBox2i Translated(Vector2i point) + public int Bottom + { + readonly get => _bottom; + set { - return new(Left + point.X, Top + point.Y, Right + point.X, Bottom + point.Y); - } + if (value < _top) + throw new ArgumentOutOfRangeException(nameof(value), value, "Bottom cannot be less than Top."); - /// - /// Calculates the "intersection" of this and another box. - /// Basically, the smallest region that fits in both boxes. - /// - /// The box to calculate the intersection with. - /// - /// null if there is no intersection, otherwise the smallest region that fits in both boxes. - /// - public readonly UIBox2i? Intersection(in UIBox2i other) - { - if (!Intersects(other)) - { - return null; - } - - return new UIBox2i( - Vector2i.ComponentMax(TopLeft, other.TopLeft), - Vector2i.ComponentMin(BottomRight, other.BottomRight)); + _bottom = value; } + } - public readonly bool Intersects(in UIBox2i other) + public Vector2i TopLeft + { + readonly get => _topLeft; + set { - return other.Bottom >= this.Top && other.Top <= this.Bottom && other.Right >= this.Left && - other.Left <= this.Right; - } + if (value.X > _right) + throw new ArgumentOutOfRangeException(nameof(value), value, "TopLeft.X cannot be greater than Right."); - // override object.Equals - public override readonly bool Equals(object? obj) - { - if (obj is UIBox2i box) - { - return Equals(box); - } + if (value.Y > _bottom) + throw new ArgumentOutOfRangeException(nameof(value), value, "TopLeft.Y cannot be greater than Bottom."); - return false; + _topLeft = value; } + } - public readonly bool Equals(UIBox2i other) + public Vector2i BottomRight + { + readonly get => _bottomRight; + set { - return other.Left == Left && other.Right == Right && other.Bottom == Bottom && other.Top == Top; - } + if (value.X < _left) + throw new ArgumentOutOfRangeException(nameof(value), value, "BottomRight.X cannot be less than Left."); - // override object.GetHashCode - public override readonly int GetHashCode() - { - var code = Left.GetHashCode(); - code = (code * 929) ^ Right.GetHashCode(); - code = (code * 929) ^ Top.GetHashCode(); - code = (code * 929) ^ Bottom.GetHashCode(); - return code; - } + if (value.Y < _top) + throw new ArgumentOutOfRangeException(nameof(value), value, "BottomRight.Y cannot be less than Top."); - public static explicit operator UIBox2i(UIBox2 box) - { - return new((int) box.Left, (int) box.Top, (int) box.Right, (int) box.Bottom); + _bottomRight = value; } + } - public static implicit operator UIBox2(UIBox2i box) - { - return new(box.Left, box.Top, box.Right, box.Bottom); - } + public readonly Vector2i TopRight => new(Right, Top); - public static UIBox2i operator +(UIBox2i box, (int lo, int to, int ro, int bo) offsets) - { - var (lo, to, ro, bo) = offsets; + public readonly Vector2i BottomLeft => new(Left, Bottom); - return new UIBox2i(box.Left + lo, box.Top + to, box.Right + ro, box.Bottom + bo); - } + public readonly int Width => Right - Left; - public override readonly string ToString() - { - return $"({Left}, {Top}, {Right}, {Bottom})"; - } + public readonly int Height => Bottom - Top; + + public readonly Vector2i Size => new(Width, Height); + + public readonly Vector2 Center => new Vector2(_left + _right, _top + _bottom) / 2f; + + public UIBox2i(Vector2i topLeft, Vector2i bottomRight) + { + Unsafe.SkipInit(out this); + + _topLeft = topLeft; + _bottomRight = bottomRight; + } + + public UIBox2i(int left, int top, int right, int bottom) + { + Unsafe.SkipInit(out this); + + _left = left; + _right = right; + _top = top; + _bottom = bottom; + } + + public static UIBox2i FromDimensions(int left, int top, int width, int height) + { + return new UIBox2i(left, top, left + width, top + height); + } + + public static UIBox2i FromDimensions(Vector2i position, Vector2i size) + { + return FromDimensions(position.X, position.Y, size.X, size.Y); + } - public readonly string ToString(string? format, IFormatProvider? formatProvider) + public readonly bool Contains(int x, int y) + { + return Contains(new Vector2i(x, y)); + } + + public readonly bool Contains(Vector2i point, bool closedRegion = true) + { + var xOk = closedRegion + ? point.X >= Left ^ point.X > Right + : point.X > Left ^ point.X >= Right; + var yOk = closedRegion + ? point.Y >= Top ^ point.Y > Bottom + : point.Y > Top ^ point.Y >= Bottom; + return xOk && yOk; + } + + /// Returns a UIBox2 translated by the given amount. + public readonly UIBox2i Translated(Vector2i point) + { + return new UIBox2i(Left + point.X, Top + point.Y, Right + point.X, Bottom + point.Y); + } + + /// + /// Calculates the "intersection" of this and another box. + /// Basically, the smallest region that fits in both boxes. + /// + /// The box to calculate the intersection with. + /// + /// null if there is no intersection, otherwise the smallest region that fits in both boxes. + /// + public readonly UIBox2i? Intersection(in UIBox2i other) + { + if (!Intersects(other)) { - return ToString(); + return null; } - public readonly bool TryFormat( - Span destination, - out int charsWritten, - ReadOnlySpan format, - IFormatProvider? provider) + return new UIBox2i( + Vector2i.ComponentMax(TopLeft, other.TopLeft), + Vector2i.ComponentMin(BottomRight, other.BottomRight)); + } + + public readonly bool Intersects(in UIBox2i other) + { + return other._bottom >= _top + && other._top <= _bottom + && other._right >= _left + && other._left <= _right; + } + + // override object.Equals + public readonly override bool Equals(object? obj) + { + if (obj is UIBox2i box) { - return FormatHelpers.TryFormatInto( - destination, - out charsWritten, - $"({Left}, {Top}, {Right}, {Bottom})"); + return Equals(box); } + + return false; + } + + public readonly bool Equals(UIBox2i other) + { + return other.Left == Left && other.Right == Right && other.Bottom == Bottom && other.Top == Top; + } + + // override object.GetHashCode + public readonly override int GetHashCode() + { + var code = Left.GetHashCode(); + code = (code * 929) ^ Right.GetHashCode(); + code = (code * 929) ^ Top.GetHashCode(); + code = (code * 929) ^ Bottom.GetHashCode(); + return code; + } + + public static explicit operator UIBox2i(UIBox2 box) + { + return new UIBox2i((int) box.Left, (int) box.Top, (int) box.Right, (int) box.Bottom); + } + + public static implicit operator UIBox2(UIBox2i box) + { + return new UIBox2(box.Left, box.Top, box.Right, box.Bottom); + } + + public static bool operator ==(UIBox2i a, UIBox2i b) + { + return a.Equals(b); + } + + public static bool operator !=(UIBox2i a, UIBox2i b) + { + return !a.Equals(b); + } + + public static UIBox2i operator +(UIBox2i box, (int lo, int to, int ro, int bo) offsets) + { + var (lo, to, ro, bo) = offsets; + + return new UIBox2i(box.Left + lo, box.Top + to, box.Right + ro, box.Bottom + bo); + } + + public readonly override string ToString() + { + return $"({Left}, {Top}, {Right}, {Bottom})"; + } + + public readonly string ToString(string? format, IFormatProvider? formatProvider) + { + return ToString(); + } + + public readonly bool TryFormat( + Span destination, + out int charsWritten, + ReadOnlySpan format, + IFormatProvider? provider) + { + return FormatHelpers.TryFormatInto( + destination, + out charsWritten, + $"({Left}, {Top}, {Right}, {Bottom})"); } } diff --git a/Robust.Shared.Scripting/ScriptGlobalsShared.cs b/Robust.Shared.Scripting/ScriptGlobalsShared.cs index f384b772c0e..801df76606d 100644 --- a/Robust.Shared.Scripting/ScriptGlobalsShared.cs +++ b/Robust.Shared.Scripting/ScriptGlobalsShared.cs @@ -33,8 +33,6 @@ public abstract partial class ScriptGlobalsShared : IInvocationContext public IEntitySystemManager esm => _esm; [Dependency] private IPrototypeManager _prot = null!; public IPrototypeManager prot => _prot; - [Dependency] private IMapManager _map = null!; - public IMapManager map => _map; [Dependency] private IDependencyCollection _dependencies = null!; public IDependencyCollection dependencies => _dependencies; [Dependency] private ToolshedManager _shed = null!; diff --git a/Robust.Shared.Tests/Collections/ValueListTest.cs b/Robust.Shared.Tests/Collections/ValueListTest.cs new file mode 100644 index 00000000000..e8f7024c04c --- /dev/null +++ b/Robust.Shared.Tests/Collections/ValueListTest.cs @@ -0,0 +1,162 @@ +using System.Collections.Generic; +using System.Reflection; +using NUnit.Framework; +using Robust.Shared.Collections; + +namespace Robust.Shared.Tests.Collections; + +[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] +[TestFixture, TestOf(typeof(ValueList<>))] +internal sealed class ValueListTest +{ + [Test] + public void PeekCorrectReference() + { + var list = new ValueList(2) + { + false, + true, + }; + + Assert.That(list.TryPeek(out var popped), Is.True); + Assert.That(popped, Is.True); + + list.TryPop(out _); + + Assert.That(list.TryPeek(out popped), Is.True); + Assert.That(popped, Is.False); + + list.TryPop(out _); + Assert.That(list.TryPeek(out popped), Is.False); + } + + [Test] + public void TryPopClearsRemovedReference() + { + var list = new ValueList(1); + var item = new object(); + list.Add(item); + + Assert.That(list.TryPop(out var popped), Is.True); + Assert.That(popped, Is.SameAs(item)); + + var itemsField = typeof(ValueList).GetField("_items", BindingFlags.NonPublic | BindingFlags.Instance); + var items = (object?[]?) itemsField!.GetValue(list); + + Assert.That(items, Is.Not.Null); + Assert.That(items![0], Is.Null); + } + + [Test] + public void IListMethodsMatchList() + { + IList expected = new List(); + IList actual = new ValueList(); + + AssertListEqual(expected, actual); + Assert.That(actual.IsReadOnly, Is.EqualTo(expected.IsReadOnly)); + + expected.Add(1); + actual.Add(1); + AssertListEqual(expected, actual); + + expected.Add(3); + actual.Add(3); + AssertListEqual(expected, actual); + + expected.Insert(1, 2); + actual.Insert(1, 2); + AssertListEqual(expected, actual); + + expected[2] = 4; + actual[2] = 4; + AssertListEqual(expected, actual); + + Assert.That(actual.IndexOf(2), Is.EqualTo(expected.IndexOf(2))); + Assert.That(actual.IndexOf(99), Is.EqualTo(expected.IndexOf(99))); + Assert.That(actual.Contains(4), Is.EqualTo(expected.Contains(4))); + Assert.That(actual.Contains(99), Is.EqualTo(expected.Contains(99))); + + var expectedCopy = new int[5]; + var actualCopy = new int[5]; + expected.CopyTo(expectedCopy, 1); + actual.CopyTo(actualCopy, 1); + Assert.That(actualCopy, Is.EqualTo(expectedCopy)); + + Assert.That(actual.Remove(2), Is.EqualTo(expected.Remove(2))); + AssertListEqual(expected, actual); + + expected.RemoveAt(1); + actual.RemoveAt(1); + AssertListEqual(expected, actual); + + expected.Clear(); + actual.Clear(); + AssertListEqual(expected, actual); + } + + [Test] + public void IListInsertGrowsDefaultList() + { + IList expected = new List(); + IList actual = new ValueList(); + + expected.Insert(0, 10); + actual.Insert(0, 10); + + AssertListEqual(expected, actual); + } + + [Test] + public void IListCopyToExceptionsMatchList() + { + IList expected = new List { 1, 2, 3 }; + IList actual = new ValueList(expected); + + AssertSameException(() => expected.CopyTo(null!, 0), () => actual.CopyTo(null!, 0)); + AssertSameException(() => expected.CopyTo(new int[3], -1), () => actual.CopyTo(new int[3], -1)); + AssertSameException(() => expected.CopyTo(new int[3], 1), () => actual.CopyTo(new int[3], 1)); + AssertSameException(() => expected.CopyTo(new int[2], 0), () => actual.CopyTo(new int[2], 0)); + } + + [Test] + public void IListIndexExceptionsMatchList() + { + IList expected = new List { 1, 2, 3 }; + IList actual = new ValueList(expected); + + AssertSameException(() => _ = expected[-1], () => _ = actual[-1]); + AssertSameException(() => _ = expected[3], () => _ = actual[3]); + AssertSameException(() => expected[-1] = 0, () => actual[-1] = 0); + AssertSameException(() => expected[3] = 0, () => actual[3] = 0); + } + + [Test] + public void IListMutationExceptionsMatchList() + { + IList expected = new List { 1, 2, 3 }; + IList actual = new ValueList(expected); + + AssertSameException(() => expected.Insert(-1, 0), () => actual.Insert(-1, 0)); + AssertSameException(() => expected.Insert(4, 0), () => actual.Insert(4, 0)); + AssertSameException(() => expected.RemoveAt(-1), () => actual.RemoveAt(-1)); + AssertSameException(() => expected.RemoveAt(3), () => actual.RemoveAt(3)); + } + + private static void AssertListEqual(IList expected, IList actual) + { + Assert.Multiple(() => + { + Assert.That(actual.Count, Is.EqualTo(expected.Count)); + Assert.That(actual, Is.EqualTo(expected)); + }); + } + + private static void AssertSameException(TestDelegate expected, TestDelegate actual) + { + var expectedException = Assert.Throws(Is.InstanceOf(), expected); + var actualException = Assert.Throws(Is.InstanceOf(), actual); + + Assert.That(actualException, Is.TypeOf(expectedException!.GetType())); + } +} diff --git a/Robust.Shared.Tests/Physics/B2DynamicTree_Test.cs b/Robust.Shared.Tests/Physics/B2DynamicTree_Test.cs index c632cb87dee..9b136c970cb 100644 --- a/Robust.Shared.Tests/Physics/B2DynamicTree_Test.cs +++ b/Robust.Shared.Tests/Physics/B2DynamicTree_Test.cs @@ -66,5 +66,39 @@ public void AddAndQuery() } }); } + + [Test] + public void RebuildFullPreservesQueries() + { + var dt = new B2DynamicTree(); + + for (var i = 0; i < aabbs1.Length; ++i) + { + dt.CreateProxy(aabbs1[i], uint.MaxValue, i); + } + + dt.Rebuild(true); + + var point = new Vector2(0, 0); + var box = Box2.CenteredAround(point, new Vector2(0.1f, 0.1f)); + var results = new HashSet(); + + dt.Query(proxy => + { + results.Add(dt.GetUserData(proxy)); + return true; + }, box); + + Assert.Multiple(() => + { + for (var i = 0; i < aabbs1.Length; i++) + { + if (aabbs1[i].Intersects(box)) + { + Assert.That(results, Does.Contain(i)); + } + } + }); + } } } diff --git a/Robust.Shared.Tests/Physics/DynamicTree_Test.cs b/Robust.Shared.Tests/Physics/DynamicTree_Test.cs index e73458a8f30..369c70087be 100644 --- a/Robust.Shared.Tests/Physics/DynamicTree_Test.cs +++ b/Robust.Shared.Tests/Physics/DynamicTree_Test.cs @@ -60,11 +60,12 @@ internal sealed class DynamicTree_Test [Test] public void AddAndGrow() { - var dt = new DynamicTree((in int x) => aabbs1[x], capacity: 16, growthFunc: x => x += 2); + const int proxyCapacity = 16; + var dt = new DynamicTree((in int x) => aabbs1[x], capacity: proxyCapacity, growthFunc: x => x += 2); var initCap = dt.Capacity; - Assert.That(initCap, Is.EqualTo(16)); + Assert.That(initCap, Is.EqualTo(2 * proxyCapacity - 1)); Assert.Multiple(() => { @@ -194,6 +195,38 @@ public void AddThenRemove() }); } + [Test] + public void ClearRemovesAllEntries() + { + var aabbs = new[] + { + ((Box2) default).Enlarged(1), + new Box2(float.NaN, float.NaN, float.NaN, float.NaN), + }; + + var dt = new DynamicTree((in int x) => aabbs[x], capacity: 16, growthFunc: x => x += 2); + + Assert.That(dt.Add(0), Is.True); + Assert.That(dt.Add(1), Is.True); + Assert.That(dt.Count, Is.EqualTo(2)); + + dt.Clear(); + + Assert.Multiple(() => + { + Assert.That(dt.Count, Is.Zero); + Assert.That(dt.Contains(0), Is.False); + Assert.That(dt.Contains(1), Is.False); + Assert.That(dt, Is.Empty); + }); + + Assert.Multiple(() => + { + Assert.That(dt.Add(0), Is.True); + Assert.That(dt.Add(1), Is.True); + }); + } + [Test] public void AddAndQuery() { var dt = new DynamicTree((in int x) => aabbs1[x], capacity: 16, growthFunc: x => x += 2); diff --git a/Robust.Shared.Tests/RichText/FormattedStringBuilderTest.cs b/Robust.Shared.Tests/RichText/FormattedStringBuilderTest.cs new file mode 100644 index 00000000000..431cd925222 --- /dev/null +++ b/Robust.Shared.Tests/RichText/FormattedStringBuilderTest.cs @@ -0,0 +1,255 @@ +using System.Text; +using NUnit.Framework; +using Robust.Shared.Maths; +using Robust.Shared.RichText; +using Robust.Shared.Utility; + +namespace Robust.UnitTesting.Shared.RichText; + +[Parallelizable(ParallelScope.All)] +[TestFixture, TestOf(typeof(FormattedStringBuilder))] +public static class FormattedStringBuilderTest +{ + [Test] + public static void TestPlainText() + { + var fsb = new FormattedStringBuilder(); + fsb.AppendText("Foobar"); + fsb.AppendLine(); + fsb.AppendMarkup("Wawa"); + + AssertMarkup(fsb, "Foobar\nWawa"); + } + + [Test] + public static void TestPlainTextExistingBuilder() + { + var sb = new StringBuilder(); + sb.Append("Guh"); + var fsb = new FormattedStringBuilder(sb); + fsb.AppendText("Foobar"); + fsb.AppendLine(); + fsb.AppendMarkup("Wawa"); + + AssertMarkup(fsb, "GuhFoobar\nWawa"); + } + + [Test] + public static void TestBasicTag() + { + var fsb = new FormattedStringBuilder(); + fsb.BeginTag("bold"); + fsb.FinishTagOpen(); + + fsb.AppendText("bar"); + fsb.PopTag(); + + AssertMarkup(fsb, "[bold]bar[/bold]"); + } + + [Test] + public static void TestBasicTagFormattedString() + { + var fsb = new FormattedStringBuilder(); + fsb.BeginTag("bold"); + fsb.FinishTagOpen(); + + fsb.AppendText("bar"); + fsb.PopTag(); + + Assert.That((FormattedMessage)fsb.ToFormattedString(), Is.EqualTo(FormattedMessage.FromMarkupOrThrow("[bold]bar[/bold]"))); + } + + [Test] + public static void TestSelfClosingTag() + { + var fsb = new FormattedStringBuilder(); + fsb.BeginTag("bold"); + fsb.FinishTagSelfClosed(); + + AssertMarkup(fsb, "[bold /]"); + } + + [Test] + public static void TestTagValueLong() + { + var fsb = new FormattedStringBuilder(); + fsb.BeginTag("bold", 10); + fsb.FinishTagSelfClosed(); + + AssertMarkup(fsb, "[bold=10 /]"); + } + + [Test] + public static void TestTagValueString() + { + var fsb = new FormattedStringBuilder(); + fsb.BeginTag("bold", "wawa"); + fsb.FinishTagSelfClosed(); + + AssertMarkup(fsb, "[bold=\"wawa\" /]"); + } + + [Test] + public static void TestTagValueColor() + { + var fsb = new FormattedStringBuilder(); + fsb.BeginTag("bold", Color.FromHex("#AAA")); + fsb.FinishTagSelfClosed(); + + AssertMarkup(fsb, "[bold=#AAA /]"); + } + + [Test] + public static void TestTagAttributeString() + { + AssertMarkup( + Fsb().BeginTag("bold").TagAttribute("a", "b").FinishTagSelfClosed(), + "[bold a=\"b\" /]"); + } + + [Test] + public static void TestTagAttributeLong() + { + AssertMarkup( + Fsb().BeginTag("bold").TagAttribute("a", 10).FinishTagSelfClosed(), + "[bold a=10 /]"); + } + + [Test] + public static void TestTagAttributeColor() + { + AssertMarkup( + Fsb().BeginTag("bold").TagAttribute("a", Color.FromHex("#AAA")).FinishTagSelfClosed(), + "[bold a=#AAA /]"); + } + + [Test] + public static void TestAppendMarkup() + { + AssertMarkup( + Fsb().AppendMarkup("[bold /]"), + "[bold /]"); + } + + [Test] + public static void TestAppendMarkupLine() + { + AssertMarkup( + Fsb().AppendMarkupLine("[bold /]"), + "[bold /]\n"); + } + + [Test] + public static void TestBeginInvalid() + { + var fsb = Fsb().BeginTag("a"); + + Assert.That(() => fsb.BeginTag("a"), Throws.TypeOf()); + } + + [Test] + public static void TestBeginValueInvalid() + { + var fsb = Fsb().BeginTag("a"); + + Assert.That(() => fsb.BeginTag("a", "b"), Throws.TypeOf()); + } + + [Test] + public static void TestTagAttributeInvalid() + { + var fsb = Fsb(); + + Assert.That(() => fsb.TagAttribute("a", "b"), Throws.TypeOf()); + } + + [Test] + public static void TestFinishTagSelfClosedInvalid() + { + var fsb = Fsb(); + + Assert.That(() => fsb.FinishTagSelfClosed(), Throws.TypeOf()); + } + + [Test] + public static void TestFinishTagOpenInvalid() + { + var fsb = Fsb(); + + Assert.That(() => fsb.FinishTagOpen(), Throws.TypeOf()); + } + + [Test] + public static void TestPopTagEmpty() + { + var fsb = Fsb(); + + Assert.That(() => fsb.PopTag(), Throws.TypeOf()); + } + + [Test] + public static void TestPopTagInvalid() + { + var fsb = Fsb().BeginTag("a"); + + Assert.That(() => fsb.PopTag(), Throws.TypeOf()); + } + + [Test] + public static void TestAppendTextInvalid() + { + var fsb = Fsb().BeginTag("a"); + + Assert.That(() => fsb.AppendText("A"), Throws.TypeOf()); + } + + [Test] + public static void TestAppendMarkupInvalid() + { + var fsb = Fsb().BeginTag("a"); + + Assert.That(() => fsb.AppendMarkup("A"), Throws.TypeOf()); + } + + [Test] + public static void TestAppendMarkupInvalidMarkup() + { + var fsb = Fsb(); + + Assert.That(() => fsb.AppendMarkup("[wawa"), Throws.TypeOf()); + } + + [Test] + public static void TestAppendLineInvalid() + { + var fsb = Fsb().BeginTag("a"); + + Assert.That(() => fsb.AppendLine(), Throws.TypeOf()); + } + + [Test] + public static void TestAppendMarkupLineInvalid() + { + var fsb = Fsb().BeginTag("a"); + + Assert.That(() => fsb.AppendMarkupLine("guh"), Throws.TypeOf()); + } + + [Test] + public static void TestAppendMarkupLineInvalidMarkup() + { + var fsb = Fsb(); + + Assert.That(() => fsb.AppendMarkupLine("[guh"), Throws.TypeOf()); + } + + private static void AssertMarkup(FormattedStringBuilder fsb, string expected) + { + Assert.That( + FormattedMessage.FromMarkupOrThrow(fsb.ToString()), + Is.EqualTo(FormattedMessage.FromMarkupOrThrow(expected))); + } + + private static FormattedStringBuilder Fsb() => new FormattedStringBuilder(); +} diff --git a/Robust.Shared.Tests/Utility/CollectionExtensions_Test.cs b/Robust.Shared.Tests/Utility/CollectionExtensions_Test.cs index eeabd0c76f1..a135d941448 100644 --- a/Robust.Shared.Tests/Utility/CollectionExtensions_Test.cs +++ b/Robust.Shared.Tests/Utility/CollectionExtensions_Test.cs @@ -30,5 +30,47 @@ public void TestFirstOrNull() Assert.That(new[] {1}.FirstOrNull(p => p == 2), Is.Null); Assert.That(new[] {1, 2, 3}.FirstOrNull(p => p == 2), Is.EqualTo(2)); } + + [Test] + public void DictionaryEqualsTest() + { + var dict = new Dictionary + { + ["one"] = 1, + ["two"] = 2, + }; + + var same = new Dictionary + { + ["two"] = 2, + ["one"] = 1, + }; + + var differentCount = new Dictionary + { + ["one"] = 1, + }; + + var differentKey = new Dictionary + { + ["one"] = 1, + ["three"] = 2, + }; + + var differentValue = new Dictionary + { + ["one"] = 1, + ["two"] = 3, + }; + + Assert.Multiple(() => + { + Assert.That(dict.DictionaryEquals(dict), Is.True); + Assert.That(dict.DictionaryEquals(same), Is.True); + Assert.That(dict.DictionaryEquals(differentCount), Is.False); + Assert.That(dict.DictionaryEquals(differentKey), Is.False); + Assert.That(dict.DictionaryEquals(differentValue), Is.False); + }); + } } } diff --git a/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs b/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs new file mode 100644 index 00000000000..2ae74f3157b --- /dev/null +++ b/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs @@ -0,0 +1,74 @@ +using NUnit.Framework; +using Robust.Shared.Maths; +using Robust.Shared.Utility; + +namespace Robust.Shared.Tests.Utility; + +[Parallelizable(ParallelScope.All)] +[TestOf(typeof(ColorExtensions))] +internal sealed class ColorExtensionsTest +{ + [Test] + public void TestAnalogousPalette() + { + var palette = ColorExtensions.GetAnalogousComplementaries(Color.Red); + + using (Assert.EnterMultipleScope()) + { + Assert.That(palette, Has.Length.EqualTo(3)); + Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); + + Assert.That(Color.ToHsl(palette[0]).X, Is.Zero); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, 30f / 360f)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - 30f / 360f)); + } + } + + [Test] + public void TestTriadicPalette() + { + var palette = ColorExtensions.GetTriadicComplementaries(Color.Red); + + using (Assert.EnterMultipleScope()) + { + Assert.That(palette, Has.Length.EqualTo(3)); + Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); + + Assert.That(Color.ToHsl(palette[0]).X, Is.Zero); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, 120f / 360f)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - 120f / 360f)); + } + } + + [Test] + public void TestSplitComplementaryPalette() + { + var palette = ColorExtensions.GetSplitComplementaries(Color.Red); + + using (Assert.EnterMultipleScope()) + { + Assert.That(palette, Has.Length.EqualTo(3)); + Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); + + Assert.That(Color.ToHsl(palette[0]).X, Is.Zero); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, 150f / 360f)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - 150f / 360f)); + } + } + + [Test] + public void TestComplementaryPalette() + { + var palette = ColorExtensions.GetOneComplementary(Color.Red); + + using (Assert.EnterMultipleScope()) + { + Assert.That(palette, Has.Length.EqualTo(3)); + Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); + + Assert.That(Color.ToHsl(palette[0]).X, Is.Zero); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, 180f / 360f)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - 180f / 360f)); + } + } +} diff --git a/Robust.Shared.Tests/Utility/CommandParsing_Test.cs b/Robust.Shared.Tests/Utility/CommandParsing_Test.cs index b99089f2bbe..3a9d7c213ac 100644 --- a/Robust.Shared.Tests/Utility/CommandParsing_Test.cs +++ b/Robust.Shared.Tests/Utility/CommandParsing_Test.cs @@ -33,5 +33,22 @@ public void TestEscape(string source, string expected) Assert.That(escaped, Is.EqualTo(expected)); } + + [TestCase("foo;bar")] + [TestCase("\"foo;bar")] + [TestCase("f oo;bar")] + [TestCase("f\\ oo;bar")] + public void TestEscapeCommand(string source) + { + var args = source.Split(';'); + + var result = new List(); + var escapedCommand = CommandParsing.EscapeCommand(args); + TestContext.Out.WriteLine($"Escaped command: {escapedCommand}"); + + CommandParsing.ParseArguments(escapedCommand, result); + + Assert.That(result, Is.EquivalentTo(args)); + } } } diff --git a/Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs b/Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs new file mode 100644 index 00000000000..3a5572ec94f --- /dev/null +++ b/Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs @@ -0,0 +1,61 @@ +using System; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; + +namespace Robust.Shared.Analyzers; + +// These annotations direct the operation of `Robust.Shared.EntitySystemSubscriptionsGenerator`'s +// `EntitySystemSubscriptionGenerator` and `EntitySystemSubscriptionGeneratorErrorAnalyser`. + +/// +/// This attribute indicates that the annotated method is a handler for an event subscription. Methods annotated with +/// this attribute will have a call generated, using the method as the handler, +/// with the event type (and component, as relevant) inferred from the method signature. +/// +/// +/// For this to work, the annotated method must be compatible with one of the following delegate types: +/// +/// +/// +/// +/// +/// +/// Note that this is not any different from the normal requirements to use . +/// +[AttributeUsage(AttributeTargets.Method)] +[MeansImplicitUse] +public sealed class SubscribeLocalEventAttribute : Attribute; + +/// +/// This attribute indicates that the annotated method is a handler for an event subscription. Methods annotated with +/// this attribute will have a call generated, using the method as the handler, +/// with the event type inferred from the method signature. +/// +/// +/// For this to work, the annotated method must be compatible with one of the following delegate types: +/// +/// +/// +/// +/// Note that this is not any different from the normal requirements to use . +/// +[AttributeUsage(AttributeTargets.Method)] +[MeansImplicitUse] +public sealed class SubscribeNetworkEventAttribute : Attribute; + +/// +/// This attribute indicates that the annotated method is a handler for an event subscription. Methods annotated with +/// this attribute will have a call generated, using the method as the handler, +/// with the event type inferred from the method signature. +/// +/// +/// For this to work, the annotated method must be compatible with one of the following delegate types: +/// +/// +/// +/// +/// Note that this is not any different from the normal requirements to use . +/// +[AttributeUsage(AttributeTargets.Method)] +[MeansImplicitUse] +public sealed class EventSubscriptionAttribute : Attribute; diff --git a/Robust.Shared/Audio/AudioParams.cs b/Robust.Shared/Audio/AudioParams.cs index 426691abcc0..876899bce83 100644 --- a/Robust.Shared/Audio/AudioParams.cs +++ b/Robust.Shared/Audio/AudioParams.cs @@ -164,6 +164,20 @@ public readonly AudioParams WithVariation(float? variation) return me; } + /// + /// Returns a copy of this instance with a modified variation set, for easy chaining. + /// + [Pure] + public readonly AudioParams AddVariation(float? variation) + { + var me = this; + if (variation == null) + return me; + + me.Variation = (me.Variation ?? 0f) + variation; + return me; + } + /// /// Returns a copy of this instance with a new pitch scale set, for easy chaining. /// diff --git a/Robust.Shared/Audio/Components/AudioAuxiliaryComponent.cs b/Robust.Shared/Audio/Components/AudioAuxiliaryComponent.cs index a1648d655b5..b02017926e1 100644 --- a/Robust.Shared/Audio/Components/AudioAuxiliaryComponent.cs +++ b/Robust.Shared/Audio/Components/AudioAuxiliaryComponent.cs @@ -20,5 +20,5 @@ public sealed partial class AudioAuxiliaryComponent : Component public EntityUid? Effect; [ViewVariables] - internal IAuxiliaryAudio Auxiliary = new DummyAuxiliaryAudio(); + public IAuxiliaryAudio Auxiliary = new DummyAuxiliaryAudio(); } diff --git a/Robust.Shared/Audio/Components/AudioComponent.cs b/Robust.Shared/Audio/Components/AudioComponent.cs index 2f8275694cb..d8652146634 100644 --- a/Robust.Shared/Audio/Components/AudioComponent.cs +++ b/Robust.Shared/Audio/Components/AudioComponent.cs @@ -262,6 +262,12 @@ public void Dispose() } } +/// +/// An event raised against an AudioComponent's parent when it is despawned. +/// +[ByRefEvent] +public readonly record struct AttachedAudioDespawnedEvent; + [Serializable, NetSerializable] public enum AudioState : byte { diff --git a/Robust.Shared/Audio/SoundSpecifier.cs b/Robust.Shared/Audio/SoundSpecifier.cs index 3378d34f8ad..3bc6fdb28b2 100644 --- a/Robust.Shared/Audio/SoundSpecifier.cs +++ b/Robust.Shared/Audio/SoundSpecifier.cs @@ -12,14 +12,14 @@ namespace Robust.Shared.Audio; -[ImplicitDataDefinitionForInheritors, Serializable, NetSerializable] +[CopyByRef, ImplicitDataDefinitionForInheritors, Serializable, NetSerializable] public abstract partial class SoundSpecifier { [DataField("params")] public AudioParams Params { get; set; } = AudioParams.Default; } -[Serializable, NetSerializable] +[CopyByRef, Serializable, NetSerializable] public sealed partial class SoundPathSpecifier : SoundSpecifier { public const string Node = "path"; @@ -47,7 +47,7 @@ public SoundPathSpecifier(ResPath path, AudioParams? @params = null) } } -[Serializable, NetSerializable] +[CopyByRef, Serializable, NetSerializable] public sealed partial class SoundCollectionSpecifier : SoundSpecifier { public const string Node = "collection"; diff --git a/Robust.Shared/Audio/Systems/SharedAudioSystem.cs b/Robust.Shared/Audio/Systems/SharedAudioSystem.cs index 747b973562f..651a88e45c4 100644 --- a/Robust.Shared/Audio/Systems/SharedAudioSystem.cs +++ b/Robust.Shared/Audio/Systems/SharedAudioSystem.cs @@ -32,7 +32,6 @@ public abstract partial class SharedAudioSystem : EntitySystem [Dependency] protected IConfigurationManager CfgManager = default!; [Dependency] protected IGameTiming Timing = default!; [Dependency] private INetManager _netManager = default!; - [Dependency] protected IPrototypeManager ProtoMan = default!; [Dependency] protected IRobustRandom RandMan = default!; [Dependency] protected MetaDataSystem MetadataSys = default!; [Dependency] protected SharedTransformSystem XformSystem = default!; @@ -59,6 +58,7 @@ public override void Initialize() Subs.CVar(CfgManager, CVars.AudioZOffset, SetZOffset); SubscribeLocalEvent(OnAudioGetStateAttempt); SubscribeLocalEvent(OnAudioUnpaused); + SubscribeLocalEvent(OnTimedDespawn); } /// @@ -265,6 +265,16 @@ private void OnAudioGetStateAttempt(EntityUid uid, AudioComponent component, ref } } + private void OnTimedDespawn(Entity ent, ref TimedDespawnEvent args) + { + var parent = Transform(ent).ParentUid; + if (!Exists(parent)) + return; + + var ev = new AttachedAudioDespawnedEvent(); + RaiseLocalEvent(parent, ref ev); + } + /// /// Considers Z-offset for audio and gets the adjusted distance. /// diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs index 77d1a5114ca..d034bd73492 100644 --- a/Robust.Shared/CVars.cs +++ b/Robust.Shared/CVars.cs @@ -35,6 +35,50 @@ protected CVars() public static readonly CVarDef NetMaxConnections = CVarDef.Create("net.max_connections", 256, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER); + /// + /// How many seconds after the last message from the server or a client before we consider it timed out. + /// + public static readonly CVarDef NetConnectionTimeout = + CVarDef.Create("net.connection_timeout", 25.0f, CVar.ARCHIVE); + + /// + /// Hard max-cap of concurrent connections per client IP address. + /// Clients will always get a disconnection packet. + /// + /// + /// This cannot be bypassed in any way, since it is used by Lidgren internally. + /// + public static readonly CVarDef NetMaxIpConnections = + CVarDef.Create("net.max_ip_connections", 8, CVar.ARCHIVE); + + /// + /// Hard max-cap of connections a single client IP address can attempt in a window of net.rapid_connection_window. + /// Clients will only get a disconnection packet the first time, their packets are dropped afterwards. + /// + /// + /// This cannot be bypassed in any way, since it is used by Lidgren internally. + /// + public static readonly CVarDef NetMaxRapidConnections = + CVarDef.Create("net.max_rapid_connections", 3, CVar.ARCHIVE); + + /// + /// Whether Lidgren should send disconnection reasons when rejecting connections due to internal limits. + /// + public static readonly CVarDef NetSendConnectionRejectionReasons = + CVarDef.Create("net.send_connection_rejection_reasons", true, CVar.ARCHIVE); + + /// + /// How many seconds until connection count decays for net.max_rapid_connections. + /// + public static readonly CVarDef NetRapidConnectionWindow = + CVarDef.Create("net.rapid_connection_window", 60.0, CVar.ARCHIVE); + + /// + /// How many connections are "forgotten" every net.rapid_connection_window seconds. + /// + public static readonly CVarDef NetRapidConnectionDecay = + CVarDef.Create("net.rapid_connection_decay", 1, CVar.ARCHIVE); + /// /// UDP port to bind to for main game networking. /// Each address specified in net.bindto is bound with this port. @@ -108,6 +152,18 @@ protected CVars() public static readonly CVarDef NetMtuExpandFailAttempts = CVarDef.Create("net.mtu_expand_fail_attempts", 5, CVar.ARCHIVE); + /// + /// Maximum bytes used by incomplete Lidgren fragment groups for one connection. + /// + public static readonly CVarDef NetMaxFragmentReassemblyBytesPerConnection = + CVarDef.Create("net.max_fragment_reassembly_bytes_per_connection", 32 * 1024 * 1024, CVar.ARCHIVE); + + /// + /// How many seconds Lidgren keeps an incomplete fragment group alive. + /// + public static readonly CVarDef NetFragmentGroupTimeout = + CVarDef.Create("net.fragment_group_timeout", 30.0f, CVar.ARCHIVE); + /// /// Whether to enable verbose debug logging in Lidgren. /// @@ -298,12 +354,6 @@ protected CVars() public static readonly CVarDef NetTimeStartOffset = CVarDef.Create("net.time_start_offset", 0, CVar.SERVERONLY); - /// - /// How many seconds after the last message from the server before we consider it timed out. - /// - public static readonly CVarDef ConnectionTimeout = - CVarDef.Create("net.connection_timeout", 25.0f, CVar.ARCHIVE | CVar.CLIENTONLY); - /// /// When doing the connection handshake, how long to wait before initial connection attempt packets. /// @@ -383,6 +433,31 @@ protected CVars() public static readonly CVarDef NetLidgrenLogError = CVarDef.Create("net.lidgren_log_error", true); + /// + /// Controls whether repeated malformed network input logs from Lidgren are rate limited. + /// + public static readonly CVarDef NetLidgrenLogRateLimit = + CVarDef.Create("net.lidgren_log_rate_limit", true); + + /// + /// Bitmask of malformed network input log categories that Lidgren should rate limit. + /// + /// + public static readonly CVarDef NetLidgrenLogRateLimitTargets = + CVarDef.Create("net.lidgren_log_rate_limit_targets", (int) NetLogRateLimitTarget.All); + + /// + /// How many matching Lidgren logs are emitted per endpoint and category before suppression starts. + /// + public static readonly CVarDef NetLidgrenLogRateLimitBurst = + CVarDef.Create("net.lidgren_log_rate_limit_burst", 5); + + /// + /// Window in seconds used by Lidgren's malformed network input log rate limiter. + /// + public static readonly CVarDef NetLidgrenLogRateLimitWindow = + CVarDef.Create("net.lidgren_log_rate_limit_window", 10.0f); + /// /// If true, run network message encryption on another thread. /// @@ -1012,6 +1087,18 @@ protected CVars() public static readonly CVarDef AuthServer = CVarDef.Create("auth.server", AuthManager.DefaultAuthServer, CVar.SERVERONLY); + /// + /// Trust score for unauthenticated localhost connections + /// + public static readonly CVarDef AuthLocalTrust = + CVarDef.Create("auth.localtrust", 1f, CVar.SERVERONLY); + + /// + /// Trust score for guest connections + /// + public static readonly CVarDef AuthGuestTrust = + CVarDef.Create("auth.guesttrust", 0f, CVar.SERVERONLY); + #region Starlight // Only respected on server, client goes through IAuthManager for security. @@ -1319,7 +1406,7 @@ protected CVars() /// Audio device to try to output audio to by default. /// public static readonly CVarDef AudioDevice = - CVarDef.Create("audio.device", string.Empty, CVar.CLIENTONLY); + CVarDef.Create("audio.device", string.Empty, CVar.CLIENTONLY | CVar.ARCHIVE); /// /// Master volume for audio output. @@ -1766,6 +1853,16 @@ protected CVars() /// public static readonly CVarDef ProfEnabled = CVarDef.Create("prof.enabled", false); + /// + /// Enables the Tracy profiling system. Tracing will stay enabled for the entire runtime of the program even if + /// you turn this cvar off. + /// + /// + /// By default, this will listen for Tracy connections on all interfaces! Set the TRACY_ONLY_LOCALHOST + /// env var to 1 if you want to restrict to localhost. + /// + public static readonly CVarDef TracyProfEnabled = CVarDef.Create("prof.tracy.enabled", false); + /// /// Event log buffer size for the profiling system. /// diff --git a/Robust.Shared/Collections/ValueList.cs b/Robust.Shared/Collections/ValueList.cs index 4d45c1b4bd7..f115356c507 100644 --- a/Robust.Shared/Collections/ValueList.cs +++ b/Robust.Shared/Collections/ValueList.cs @@ -23,6 +23,9 @@ namespace Robust.Shared.Collections; /// public APIs probably shouldn't expose it unless you know what you're doing. /// /// +/// If you use this as an IList then make sure it's passed as a generic to avoid boxing. +/// +/// /// This implementation does not complain if you modify it during iteration. Be careful! /// /// @@ -34,7 +37,7 @@ namespace Robust.Shared.Collections; /// /// /// The type of item to store in the list. -public struct ValueList : IEnumerable +public struct ValueList : IList { private const int DefaultCapacity = 4; @@ -142,6 +145,7 @@ public static ValueList OwningArray(T[]? array, int count) } public int Count { get; private set; } + public readonly bool IsReadOnly => false; // Sets or Gets the element at the given index. public readonly ref T this[int index] @@ -280,6 +284,22 @@ public readonly bool Contains(T item) return IndexOf(item) >= 0; } + public readonly void CopyTo(T[] array, int arrayIndex) + { + ArgumentNullException.ThrowIfNull(array); + + if (arrayIndex < 0) + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + + if (array.Length - arrayIndex < Count) + throw new ArgumentException("Destination array is not long enough."); + + if (Count == 0) + return; + + Array.Copy(_items!, 0, array, arrayIndex, Count); + } + /// /// Ensures that the capacity of this list is at least the specified . /// If the current capacity of the list is less than specified , @@ -397,13 +417,16 @@ public void Insert(int index, T item) throw new ArgumentOutOfRangeException(); } - if (Count == _items!.Length) Grow(Count + 1); + if (Count == Capacity) + Grow(Count + 1); + + var items = _items!; if (index < Count) { - Array.Copy(_items, index, _items, index + 1, Count - index); + Array.Copy(items, index, items, index + 1, Count - index); } - _items[index] = item; + items[index] = item; Count++; } @@ -504,6 +527,24 @@ public void RemoveAt(int index) _items![Count] = default!; } + T IList.this[int index] + { + readonly get + { + if ((uint) index >= (uint) Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + return _items![index]; + } + set + { + if ((uint) index >= (uint) Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + _items![index] = value; + } + } + public void Sort() => Span.Sort(); public void Sort(IComparer? comparer) => Span.Sort(comparer); public void Sort(Comparison comparison) => Span.Sort(comparison); @@ -674,7 +715,12 @@ public bool TryPop([MaybeNullWhen(false)] out T value) return false; } - value = _items![--Count]; + var index = --Count; + value = _items![index]; + + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + _items[index] = default!; + return true; } @@ -690,7 +736,7 @@ public bool TryPeek([MaybeNullWhen(false)] out T value) return false; } - value = _items![Count]; + value = _items![Count - 1]; return true; } } diff --git a/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs b/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs index ded59959fe0..5c42e361671 100644 --- a/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs +++ b/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs @@ -25,7 +25,6 @@ public abstract partial class ComponentTreeSystem : EntitySyst { [Dependency] private RecursiveMoveSystem _recursiveMoveSys = default!; [Dependency] protected SharedTransformSystem XformSystem = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private SharedMapSystem _mapSystem = default!; private readonly Queue> _updateQueue = new(); @@ -242,9 +241,9 @@ public void UpdateTreePositions() { (pos, rot) = XformSystem.GetRelativePositionRotation( entry.Transform, - newTree!.Value); + newTree.Value); - newTreeComp!.Tree.Update(entry, ExtractAabb(entry, pos, rot)); + newTreeComp?.Tree.Update(entry, ExtractAabb(entry, pos, rot)); continue; } @@ -258,7 +257,7 @@ public void UpdateTreePositions() (pos, rot) = XformSystem.GetRelativePositionRotation( entry.Transform, - newTree!.Value); + newTree.Value); newTreeComp.Tree.Add(entry, ExtractAabb(entry, pos, rot)); } @@ -312,7 +311,7 @@ protected virtual Box2 ExtractAabb(in ComponentTreeEntry entry) var state = (EntityManager, trees); - _mapManager.FindGridsIntersecting(mapId, worldAABB, ref state, + _mapSystem.FindGridsIntersecting(mapId, worldAABB, ref state, (EntityUid uid, MapGridComponent grid, ref (EntityManager EntityManager, ValueList<(EntityUid, TTreeComp)> trees) tuple) => { diff --git a/Robust.Shared/Console/Commands/ListCommand.cs b/Robust.Shared/Console/Commands/ListCommand.cs index 53df9233f26..6b9c7f5ded4 100644 --- a/Robust.Shared/Console/Commands/ListCommand.cs +++ b/Robust.Shared/Console/Commands/ListCommand.cs @@ -18,7 +18,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) var builder = new StringBuilder(Loc.GetString("cmd-list-heading")); foreach (var command in host.AvailableCommands.Values - .Where(p => p.Command.Contains(filter)) + .Where(p => p.Command.Contains(filter) && !host.IsCommandHidden(p)) .OrderBy(c => c.Command)) { //TODO: Make this actually check permissions. diff --git a/Robust.Shared/Console/Commands/MapCommands.cs b/Robust.Shared/Console/Commands/MapCommands.cs index 1c679f414f3..58eb49de4d4 100644 --- a/Robust.Shared/Console/Commands/MapCommands.cs +++ b/Robust.Shared/Console/Commands/MapCommands.cs @@ -157,7 +157,6 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) internal sealed partial class ListMapsCommand : LocalizedEntityCommands { [Dependency] private IEntityManager _entManager = default!; - [Dependency] private IMapManager _map = default!; [Dependency] private SharedMapSystem _mapSystem = default!; public override string Command => "lsmap"; @@ -180,7 +179,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) _mapSystem.IsInitialized(mapUid), _mapSystem.IsPaused(mapId), _entManager.GetNetEntity(mapUid), - string.Join(",", _map.GetAllGrids(mapId).Select(grid => grid.Owner))); + string.Join(",", _mapSystem.GetAllGrids(mapId).Select(grid => grid.Owner))); } // Trim the newline diff --git a/Robust.Shared/Console/Commands/TeleportCommands.cs b/Robust.Shared/Console/Commands/TeleportCommands.cs index c028ae2a897..e7e5fd8d447 100644 --- a/Robust.Shared/Console/Commands/TeleportCommands.cs +++ b/Robust.Shared/Console/Commands/TeleportCommands.cs @@ -17,7 +17,6 @@ namespace Robust.Shared.Console.Commands; internal sealed partial class TeleportCommand : LocalizedEntityCommands { - [Dependency] private IMapManager _map = default!; [Dependency] private IEntityManager _entityManager = default!; [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private SharedMapSystem _mapSystem = default!; @@ -53,7 +52,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) return; } - if (_map.TryFindGridAt(mapId, position, out var gridUid, out var grid)) + if (_mapSystem.TryFindGridAt(mapId, position, out var gridUid, out var grid)) { var gridPos = Vector2.Transform(position, _transform.GetInvWorldMatrix(gridUid)); diff --git a/Robust.Shared/Console/ConsoleHost.cs b/Robust.Shared/Console/ConsoleHost.cs index 5b6c239ec1a..58fd66fa270 100644 --- a/Robust.Shared/Console/ConsoleHost.cs +++ b/Robust.Shared/Console/ConsoleHost.cs @@ -81,6 +81,11 @@ public void LoadConsoleCommands() } } + public bool IsCommandHidden(IConsoleCommand command) + { + return command.Command.StartsWith('_'); + } + protected virtual void UpdateAvailableCommands() { } diff --git a/Robust.Shared/Console/IConsoleCommand.cs b/Robust.Shared/Console/IConsoleCommand.cs index b59d220729e..9f6544d25d7 100644 --- a/Robust.Shared/Console/IConsoleCommand.cs +++ b/Robust.Shared/Console/IConsoleCommand.cs @@ -21,6 +21,9 @@ public interface IConsoleCommand /// /// A string as identifier for this command. /// + /// + /// Commands starting with '_' are treated as "hidden". They will not be shown in listings or completions. + /// string Command { get; } /// diff --git a/Robust.Shared/Console/IConsoleHost.cs b/Robust.Shared/Console/IConsoleHost.cs index 66b86419260..6ee2f4ea897 100644 --- a/Robust.Shared/Console/IConsoleHost.cs +++ b/Robust.Shared/Console/IConsoleHost.cs @@ -66,6 +66,12 @@ public interface IConsoleHost /// void LoadConsoleCommands(); + /// + /// Check whether a console command is hidden. + /// + /// + bool IsCommandHidden(IConsoleCommand command); + #region RegisterCommand /// /// Registers a console command into the console system. This is an alternative to diff --git a/Robust.Shared/Containers/SharedContainerSystem.Validation.cs b/Robust.Shared/Containers/SharedContainerSystem.Validation.cs index 1993c8209a6..bd00f1f90b4 100644 --- a/Robust.Shared/Containers/SharedContainerSystem.Validation.cs +++ b/Robust.Shared/Containers/SharedContainerSystem.Validation.cs @@ -4,7 +4,6 @@ namespace Robust.Shared.Containers; - // This partial class just exists for debug asserts and bug fixing public abstract partial class SharedContainerSystem : EntitySystem { @@ -65,5 +64,4 @@ private void ValidateChildren(TransformComponent xform, EntityQuery _managerQuery; - private EntityQuery _gridQuery; - private EntityQuery _mapQuery; - protected EntityQuery MetaQuery; - protected EntityQuery PhysicsQuery; - protected EntityQuery JointQuery; - protected EntityQuery TransformQuery; - - /// - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnParentChanged); - SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(OnStartupValidation); - SubscribeLocalEvent(OnContainerGetState); - SubscribeLocalEvent(OnContainerManagerRemove); - - _managerQuery = GetEntityQuery(); - _gridQuery = GetEntityQuery(); - _mapQuery = GetEntityQuery(); - MetaQuery = GetEntityQuery(); - PhysicsQuery = GetEntityQuery(); - JointQuery = GetEntityQuery(); - TransformQuery = GetEntityQuery(); - } + [Dependency] private IDynamicTypeFactoryInternal _dynFactory = default!; + [Dependency] private INetManager _net = default!; + [Dependency] private SharedPhysicsSystem _physics = default!; + [Dependency] private EntityLookupSystem _lookup = default!; + [Dependency] private SharedTransformSystem _transform = default!; + [Dependency] private SharedJointSystem _joint = default!; + [Dependency] private IGameTiming _timing = default!; + + [Dependency] private EntityQuery _managerQuery = default!; + [Dependency] private EntityQuery _gridQuery = default!; + [Dependency] private EntityQuery _mapQuery = default!; + [Dependency] protected EntityQuery MetaQuery = default!; + [Dependency] protected EntityQuery PhysicsQuery = default!; + [Dependency] protected EntityQuery JointQuery = default!; + [Dependency] protected EntityQuery TransformQuery = default!; + + /// + public override void Initialize() + { + base.Initialize(); - private void OnInit(Entity ent, ref ComponentInit args) + SubscribeLocalEvent(OnParentChanged); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnStartupValidation); + SubscribeLocalEvent(OnContainerGetState); + SubscribeLocalEvent(OnContainerManagerRemove); + } + + private void OnInit(Entity ent, ref ComponentInit args) + { + foreach (var (id, container) in ent.Comp.Containers) { - foreach (var (id, container) in ent.Comp.Containers) - { - container.Init(this, id, ent); - } + container.Init(this, id, ent); } + } - private void OnContainerGetState(EntityUid uid, ContainerManagerComponent component, ref ComponentGetState args) + private void OnContainerGetState(EntityUid uid, ContainerManagerComponent component, ref ComponentGetState args) + { + Dictionary containerSet = + new(component.Containers.Count); + + foreach (var container in component.Containers.Values) { - Dictionary containerSet = - new(component.Containers.Count); + var uidArr = new NetEntity[container.ContainedEntities.Count]; - foreach (var container in component.Containers.Values) + for (var index = 0; index < container.ContainedEntities.Count; index++) { - var uidArr = new NetEntity[container.ContainedEntities.Count]; - - for (var index = 0; index < container.ContainedEntities.Count; index++) - { - uidArr[index] = GetNetEntity(container.ContainedEntities[index]); - } - - var sContainer = - new ContainerManagerComponent.ContainerManagerComponentState.ContainerData(container.GetType().Name, - container.ShowContents, - container.OccludesLight, - uidArr); - containerSet.Add(container.ID, sContainer); + uidArr[index] = GetNetEntity(container.ContainedEntities[index]); } - args.State = new ContainerManagerComponent.ContainerManagerComponentState(containerSet); + var sContainer = + new ContainerManagerComponent.ContainerManagerComponentState.ContainerData(container.GetType().Name, + container.ShowContents, + container.OccludesLight, + uidArr); + containerSet.Add(container.ID, sContainer); } - private void OnContainerManagerRemove(EntityUid uid, ContainerManagerComponent component, ComponentRemove args) - { - foreach (var container in component.Containers.Values) - { - ShutdownContainer(container); - } + args.State = new ContainerManagerComponent.ContainerManagerComponentState(containerSet); + } - component.Containers.Clear(); + private void OnContainerManagerRemove(EntityUid uid, ContainerManagerComponent component, ComponentRemove args) + { + foreach (var container in component.Containers.Values) + { + ShutdownContainer(container); } - // TODO: Make ContainerManagerComponent ECS and make these proxy methods the real deal. + component.Containers.Clear(); + } - #region Proxy Methods + // TODO: Make ContainerManagerComponent ECS and make these proxy methods the real deal. - public T MakeContainer(EntityUid uid, string id, ContainerManagerComponent? containerManager = null) - where T : BaseContainer - { - if (!Resolve(uid, ref containerManager, false)) - containerManager = AddComp(uid); // Happy Vera. + #region Proxy Methods - if (HasContainer(uid, id, containerManager)) - throw new ArgumentException($"Container with specified ID already exists: '{id}'"); + public T MakeContainer(EntityUid uid, string id, ContainerManagerComponent? containerManager = null) + where T : BaseContainer + { + if (!Resolve(uid, ref containerManager, false)) + containerManager = AddComp(uid); // Happy Vera. - var container = _dynFactory.CreateInstanceUnchecked(typeof(T), inject: false); - container.Init(this, id, (uid, containerManager)); - containerManager.Containers[id] = container; - Dirty(uid, containerManager); - return container; - } + if (HasContainer(uid, id, containerManager)) + throw new ArgumentException($"Container with specified ID already exists: '{id}'"); - public virtual void ShutdownContainer(BaseContainer container) - { - container.InternalShutdown(EntityManager, this, _net.IsClient); - container.Manager.Containers.Remove(container.ID); - container.ExpectedEntities.Clear(); - } + var container = _dynFactory.CreateInstanceUnchecked(typeof(T), inject: false); + container.Init(this, id, (uid, containerManager)); + containerManager.Containers[id] = container; + Dirty(uid, containerManager); + return container; + } - public T EnsureContainer( - EntityUid uid, - string id, - out bool alreadyExisted, - ContainerManagerComponent? containerManager = null) - where T : BaseContainer - { - if (!Resolve(uid, ref containerManager, false)) - containerManager = AddComp(uid); + public virtual void ShutdownContainer(BaseContainer container) + { + container.InternalShutdown(EntityManager, this, _net.IsClient); + container.Manager.Containers.Remove(container.ID); + container.ExpectedEntities.Clear(); + } - if (TryGetContainer(uid, id, out var container, containerManager)) - { - alreadyExisted = true; - if (container is T cast) - return cast; + public T EnsureContainer( + EntityUid uid, + string id, + out bool alreadyExisted, + ContainerManagerComponent? containerManager = null) + where T : BaseContainer + { + if (!Resolve(uid, ref containerManager, false)) + containerManager = AddComp(uid); - throw new InvalidOperationException( - $"The container exists but is of a different type: {container.GetType()}"); - } + if (TryGetContainer(uid, id, out var container, containerManager)) + { + alreadyExisted = true; + if (container is T cast) + return cast; - alreadyExisted = false; - return MakeContainer(uid, id, containerManager); + throw new InvalidOperationException( + $"The container exists but is of a different type: {container.GetType()}"); } - public T EnsureContainer(EntityUid uid, string id, ContainerManagerComponent? containerManager = null) - where T : BaseContainer - { - return EnsureContainer(uid, id, out _, containerManager); - } + alreadyExisted = false; + return MakeContainer(uid, id, containerManager); + } - public BaseContainer GetContainer(EntityUid uid, string id, ContainerManagerComponent? containerManager = null) - { - if (!Resolve(uid, ref containerManager)) - throw new ArgumentException("Entity does not have a ContainerManagerComponent!", nameof(uid)); + public T EnsureContainer(EntityUid uid, string id, ContainerManagerComponent? containerManager = null) + where T : BaseContainer + { + return EnsureContainer(uid, id, out _, containerManager); + } - return containerManager.Containers[id]; - } + public BaseContainer GetContainer(EntityUid uid, string id, ContainerManagerComponent? containerManager = null) + { + if (!Resolve(uid, ref containerManager)) + throw new ArgumentException("Entity does not have a ContainerManagerComponent!", nameof(uid)); - public bool HasContainer(EntityUid uid, string id, ContainerManagerComponent? containerManager) - { - if (!Resolve(uid, ref containerManager, false)) - return false; + return containerManager.Containers[id]; + } - return containerManager.Containers.ContainsKey(id); - } + public bool HasContainer(EntityUid uid, string id, ContainerManagerComponent? containerManager) + { + if (!Resolve(uid, ref containerManager, false)) + return false; - public bool TryGetContainer( - EntityUid uid, - string id, - [NotNullWhen(true)] out BaseContainer? container, - ContainerManagerComponent? containerManager = null) + return containerManager.Containers.ContainsKey(id); + } + + public bool TryGetContainer( + EntityUid uid, + string id, + [NotNullWhen(true)] out BaseContainer? container, + ContainerManagerComponent? containerManager = null) + { + if (!Resolve(uid, ref containerManager, false)) { - if (!Resolve(uid, ref containerManager, false)) - { - container = null; - return false; - } + container = null; + return false; + } - if (!containerManager.Containers.TryGetValue(id, out container)) - return false; + if (!containerManager.Containers.TryGetValue(id, out container)) + return false; - DebugTools.AssertEqual(container.ID, id); - DebugTools.AssertNotNull(container.Manager); - DebugTools.AssertNotEqual(container.Owner, EntityUid.Invalid); - return true; + DebugTools.AssertEqual(container.ID, id); + DebugTools.AssertNotNull(container.Manager); + DebugTools.AssertNotEqual(container.Owner, EntityUid.Invalid); + return true; + } + + public bool TryGetContainingContainer( + EntityUid uid, + EntityUid containedUid, + [NotNullWhen(true)] out BaseContainer? container, + ContainerManagerComponent? containerManager = null) + { + DebugTools.Assert(Exists(containedUid)); + if (!Resolve(uid, ref containerManager, false)) + { + container = null; + return false; } - public bool TryGetContainingContainer( - EntityUid uid, - EntityUid containedUid, - [NotNullWhen(true)] out BaseContainer? container, - ContainerManagerComponent? containerManager = null) + foreach (var contain in containerManager.Containers.Values) { - DebugTools.Assert(Exists(containedUid)); - if (!Resolve(uid, ref containerManager, false)) + if (contain.Contains(containedUid)) { - container = null; - return false; + container = contain; + return true; } + } - foreach (var contain in containerManager.Containers.Values) - { - if (contain.Contains(containedUid)) - { - container = contain; - return true; - } - } + container = default; + return false; + } - container = default; + public bool ContainsEntity( + EntityUid uid, + EntityUid containedUid, + ContainerManagerComponent? containerManager = null) + { + DebugTools.Assert(Exists(containedUid)); + if (!Resolve(uid, ref containerManager, false)) return false; - } - public bool ContainsEntity( - EntityUid uid, - EntityUid containedUid, - ContainerManagerComponent? containerManager = null) + foreach (var container in containerManager.Containers.Values) { - DebugTools.Assert(Exists(containedUid)); - if (!Resolve(uid, ref containerManager, false)) - return false; + if (container.Contains(containedUid)) + return true; + } - foreach (var container in containerManager.Containers.Values) - { - if (container.Contains(containedUid)) - return true; - } + return false; + } + public bool RemoveEntity( + EntityUid uid, + EntityUid toremove, + ContainerManagerComponent? containerManager = null, + TransformComponent? containedXform = null, + MetaDataComponent? containedMeta = null, + bool reparent = true, + bool force = false, + EntityCoordinates? destination = null, + Angle? localRotation = null) + { + if (!Resolve(uid, ref containerManager) || !Resolve(toremove, ref containedMeta, ref containedXform)) return false; - } - public bool RemoveEntity( - EntityUid uid, - EntityUid toremove, - ContainerManagerComponent? containerManager = null, - TransformComponent? containedXform = null, - MetaDataComponent? containedMeta = null, - bool reparent = true, - bool force = false, - EntityCoordinates? destination = null, - Angle? localRotation = null) + foreach (var containers in containerManager.Containers.Values) { - if (!Resolve(uid, ref containerManager) || !Resolve(toremove, ref containedMeta, ref containedXform)) - return false; - - foreach (var containers in containerManager.Containers.Values) - { - if (containers.Contains(toremove)) - return Remove((toremove, containedXform, containedMeta), - containers, - reparent, - force, - destination, - localRotation); - } - - return true; // If we don't contain the entity, it will always be removed + if (containers.Contains(toremove)) + return Remove((toremove, containedXform, containedMeta), + containers, + reparent, + force, + destination, + localRotation); } - public ContainerManagerComponent.AllContainersEnumerable GetAllContainers( - EntityUid uid, - ContainerManagerComponent? containerManager = null) - { - if (!Resolve(uid, ref containerManager)) - return new ContainerManagerComponent.AllContainersEnumerable(); + return true; // If we don't contain the entity, it will always be removed + } - return new(containerManager); - } + public ContainerManagerComponent.AllContainersEnumerable GetAllContainers( + EntityUid uid, + ContainerManagerComponent? containerManager = null) + { + if (!Resolve(uid, ref containerManager)) + return new ContainerManagerComponent.AllContainersEnumerable(); - #endregion + return new(containerManager); + } - #region Container Helpers + #endregion - public bool TryGetContainingContainer( - Entity ent, - [NotNullWhen(true)] out BaseContainer? container) - { - container = null; + #region Container Helpers - if (!Resolve(ent, ref ent.Comp2, false)) - return false; + public bool TryGetContainingContainer( + Entity ent, + [NotNullWhen(true)] out BaseContainer? container) + { + container = null; - if ((ent.Comp2.Flags & MetaDataFlags.InContainer) == MetaDataFlags.None) - return false; + if (!Resolve(ent, ref ent.Comp2, false)) + return false; - if (!Resolve(ent, ref ent.Comp1, false)) - return false; + if ((ent.Comp2.Flags & MetaDataFlags.InContainer) == MetaDataFlags.None) + return false; - return TryGetContainingContainer(ent.Comp1.ParentUid, ent, out container); - } + if (!Resolve(ent, ref ent.Comp1, false)) + return false; - /// - /// Checks whether the given entity is inside of a container. This will only check if this entity's direct - /// parent is containing it. To recursively if the entity, or any parent, is inside a container, use - /// - /// If the entity is inside of a container. - public bool IsEntityInContainer(EntityUid uid, MetaDataComponent? meta = null) - { - if (!Resolve(uid, ref meta, false)) - return false; + return TryGetContainingContainer(ent.Comp1.ParentUid, ent, out container); + } - return (meta.Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer; - } + /// + /// Checks whether the given entity is inside of a container. This will only check if this entity's direct + /// parent is containing it. To recursively if the entity, or any parent, is inside a container, use + /// + /// If the entity is inside of a container. + public bool IsEntityInContainer(EntityUid uid, MetaDataComponent? meta = null) + { + if (!Resolve(uid, ref meta, false)) + return false; - /// - /// Recursively check if the entity or any parent is inside of a container. - /// - /// If the entity is inside of a container. - public bool IsEntityOrParentInContainer( - EntityUid uid, - MetaDataComponent? meta = null, - TransformComponent? xform = null) - { - if (!MetaQuery.Resolve(uid, ref meta)) - return false; + return (meta.Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer; + } - if ((meta.Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer) - return true; + /// + /// Recursively check if the entity or any parent is inside of a container. + /// + /// If the entity is inside of a container. + public bool IsEntityOrParentInContainer( + EntityUid uid, + MetaDataComponent? meta = null, + TransformComponent? xform = null) + { + if (!MetaQuery.Resolve(uid, ref meta)) + return false; - if (!TransformQuery.Resolve(uid, ref xform)) - return false; + if ((meta.Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer) + return true; - if (!xform.ParentUid.Valid) - return false; + if (!TransformQuery.Resolve(uid, ref xform)) + return false; - return IsEntityOrParentInContainer(xform.ParentUid); - } + if (!xform.ParentUid.Valid) + return false; - /// - /// Finds the first instance of a component on the recursive parented containers that hold an entity - /// - public bool TryFindComponentOnEntityContainerOrParent( - EntityUid uid, - EntityQuery entityQuery, - [NotNullWhen(true)] ref T? foundComponent, - MetaDataComponent? meta = null, - TransformComponent? xform = null) where T : IComponent - { - if (!MetaQuery.Resolve(uid, ref meta)) - return false; + return IsEntityOrParentInContainer(xform.ParentUid); + } - if ((meta.Flags & MetaDataFlags.InContainer) != MetaDataFlags.InContainer) - return false; + /// + /// Finds the first instance of a component on the recursive parented containers that hold an entity + /// + public bool TryFindComponentOnEntityContainerOrParent( + EntityUid uid, + EntityQuery entityQuery, + [NotNullWhen(true)] ref T? foundComponent, + MetaDataComponent? meta = null, + TransformComponent? xform = null) where T : IComponent + { + if (!MetaQuery.Resolve(uid, ref meta)) + return false; - if (!TransformQuery.Resolve(uid, ref xform)) - return false; + if ((meta.Flags & MetaDataFlags.InContainer) != MetaDataFlags.InContainer) + return false; - if (!xform.ParentUid.Valid) - return false; + if (!TransformQuery.Resolve(uid, ref xform)) + return false; - if (entityQuery.TryComp(xform.ParentUid, out foundComponent)) - return true; + if (!xform.ParentUid.Valid) + return false; - return TryFindComponentOnEntityContainerOrParent(xform.ParentUid, entityQuery, ref foundComponent); - } + if (entityQuery.TryComp(xform.ParentUid, out foundComponent)) + return true; - /// - /// Finds all instances of a component on the recursive parented containers that hold an entity - /// - public bool TryFindComponentsOnEntityContainerOrParent( - EntityUid uid, - EntityQuery entityQuery, - List foundComponents, - MetaDataComponent? meta = null, - TransformComponent? xform = null) where T : IComponent - { - if (!MetaQuery.Resolve(uid, ref meta)) - return foundComponents.Any(); + return TryFindComponentOnEntityContainerOrParent(xform.ParentUid, entityQuery, ref foundComponent); + } - if ((meta.Flags & MetaDataFlags.InContainer) != MetaDataFlags.InContainer) - return foundComponents.Any(); + /// + /// Finds all instances of a component on the recursive parented containers that hold an entity + /// + public bool TryFindComponentsOnEntityContainerOrParent( + EntityUid uid, + EntityQuery entityQuery, + List foundComponents, + MetaDataComponent? meta = null, + TransformComponent? xform = null) where T : IComponent + { + if (!MetaQuery.Resolve(uid, ref meta)) + return foundComponents.Any(); - if (!TransformQuery.Resolve(uid, ref xform)) - return foundComponents.Any(); + if ((meta.Flags & MetaDataFlags.InContainer) != MetaDataFlags.InContainer) + return foundComponents.Any(); - if (!xform.ParentUid.Valid) - return foundComponents.Any(); + if (!TransformQuery.Resolve(uid, ref xform)) + return foundComponents.Any(); - if (TryComp(xform.ParentUid, out T? foundComponent)) - foundComponents.Add(foundComponent); + if (!xform.ParentUid.Valid) + return foundComponents.Any(); - return TryFindComponentsOnEntityContainerOrParent(xform.ParentUid, entityQuery, foundComponents); - } + if (TryComp(xform.ParentUid, out T? foundComponent)) + foundComponents.Add(foundComponent); - /// - /// Returns true if the two entities are not contained, or are contained in the same container. - /// - public bool IsInSameOrNoContainer( - Entity user, - Entity other) - { - var isUserContained = TryGetContainingContainer(user, out var userContainer); - var isOtherContained = TryGetContainingContainer(other, out var otherContainer); + return TryFindComponentsOnEntityContainerOrParent(xform.ParentUid, entityQuery, foundComponents); + } - // Both entities are not in a container - if (!isUserContained && !isOtherContained) return true; + /// + /// Returns true if the two entities are not contained, or are contained in the same container. + /// + public bool IsInSameOrNoContainer( + Entity user, + Entity other) + { + var isUserContained = TryGetContainingContainer(user, out var userContainer); + var isOtherContained = TryGetContainingContainer(other, out var otherContainer); - // Both entities are in different contained states - if (isUserContained != isOtherContained) return false; + // Both entities are not in a container + if (!isUserContained && !isOtherContained) return true; - // Both entities are in the same container - return userContainer == otherContainer; - } + // Both entities are in different contained states + if (isUserContained != isOtherContained) return false; - /// - /// Returns true if the two entities are not contained, or are contained in the same container, or if one - /// entity contains the other (i.e., is the parent). - /// - public bool IsInSameOrParentContainer( - Entity user, - Entity other) - { - return IsInSameOrParentContainer(user, other, out _, out _); - } + // Both entities are in the same container + return userContainer == otherContainer; + } - /// - public bool IsInSameOrParentContainer( - Entity user, - Entity other, - out BaseContainer? userContainer, - out BaseContainer? otherContainer) - { - var isUserContained = TryGetContainingContainer(user, out userContainer); - var isOtherContained = TryGetContainingContainer(other, out otherContainer); + /// + /// Returns true if the two entities are not contained, or are contained in the same container, or if one + /// entity contains the other (i.e., is the parent). + /// + public bool IsInSameOrParentContainer( + Entity user, + Entity other) + { + return IsInSameOrParentContainer(user, other, out _, out _); + } - // Both entities are not in a container - if (!isUserContained && !isOtherContained) return true; + /// + public bool IsInSameOrParentContainer( + Entity user, + Entity other, + out BaseContainer? userContainer, + out BaseContainer? otherContainer) + { + var isUserContained = TryGetContainingContainer(user, out userContainer); + var isOtherContained = TryGetContainingContainer(other, out otherContainer); - // One contains the other - if (userContainer?.Owner == other || otherContainer?.Owner == user) return true; + // Both entities are not in a container + if (!isUserContained && !isOtherContained) return true; - // Both entities are in different contained states - if (isUserContained != isOtherContained) return false; + // One contains the other + if (userContainer?.Owner == other || otherContainer?.Owner == user) return true; - // Both entities are in the same container - return userContainer == otherContainer; - } + // Both entities are in different contained states + if (isUserContained != isOtherContained) return false; - /// - /// Check whether a given entity can see another entity despite whatever containers they may be in. - /// - /// - /// This is effectively a variant of that also checks whether the - /// containers are transparent. Additionally, an entity can "see" the entity that contains it, but unless - /// otherwise specified the containing entity cannot see into itself. For example, a human in a locker can - /// see the locker and other items in that locker, but the human cannot see their own organs. Note that - /// this means that the two entity arguments are NOT interchangeable. - /// - public bool IsInSameOrTransparentContainer( - Entity user, - Entity other, - BaseContainer? userContainer = null, - BaseContainer? otherContainer = null, - bool userSeeInsideSelf = false) - { - if (userContainer == null) - TryGetContainingContainer(user, out userContainer); + // Both entities are in the same container + return userContainer == otherContainer; + } - if (otherContainer == null) - TryGetContainingContainer(other, out otherContainer); + /// + /// Check whether a given entity can see another entity despite whatever containers they may be in. + /// + /// + /// This is effectively a variant of that also checks whether the + /// containers are transparent. Additionally, an entity can "see" the entity that contains it, but unless + /// otherwise specified the containing entity cannot see into itself. For example, a human in a locker can + /// see the locker and other items in that locker, but the human cannot see their own organs. Note that + /// this means that the two entity arguments are NOT interchangeable. + /// + public bool IsInSameOrTransparentContainer( + Entity user, + Entity other, + BaseContainer? userContainer = null, + BaseContainer? otherContainer = null, + bool userSeeInsideSelf = false) + { + if (userContainer == null) + TryGetContainingContainer(user, out userContainer); - // Are both entities in the same container (or none)? - if (userContainer == otherContainer) return true; + if (otherContainer == null) + TryGetContainingContainer(other, out otherContainer); - // Is the user contained in the other entity? - if (userContainer?.Owner == other) return true; + // Are both entities in the same container (or none)? + if (userContainer == otherContainer) return true; - // Does the user contain the other and can they see through themselves? - if (userSeeInsideSelf && otherContainer?.Owner == user) return true; + // Is the user contained in the other entity? + if (userContainer?.Owner == other) return true; - // Next we check for see-through containers. This uses some recursion, but it should be fine unless people - // start spawning in glass matryoshka dolls. + // Does the user contain the other and can they see through themselves? + if (userSeeInsideSelf && otherContainer?.Owner == user) return true; - // Is the user in a see-through container? - if (userContainer?.ShowContents ?? false) - return IsInSameOrTransparentContainer((userContainer.Owner, null, null), other, otherContainer: otherContainer); + // Next we check for see-through containers. This uses some recursion, but it should be fine unless people + // start spawning in glass matryoshka dolls. - // Is the other entity in a see-through container? - if (otherContainer?.ShowContents ?? false) - return IsInSameOrTransparentContainer(user, (otherContainer.Owner, null, null), userContainer: userContainer, userSeeInsideSelf: userSeeInsideSelf); + // Is the user in a see-through container? + if (userContainer?.ShowContents ?? false) + return IsInSameOrTransparentContainer((userContainer.Owner, null, null), other, otherContainer: otherContainer); - return false; - } + // Is the other entity in a see-through container? + if (otherContainer?.ShowContents ?? false) + return IsInSameOrTransparentContainer(user, (otherContainer.Owner, null, null), userContainer: userContainer, userSeeInsideSelf: userSeeInsideSelf); - /// - /// Returns the full chain of containers containing the entity passed in, from innermost to outermost. - /// - /// - /// The resulting collection includes the container directly containing the entity (if any), - /// the container containing that container, and so on until reaching the outermost container. - /// - public IEnumerable GetContainingContainers(Entity ent) - { - if (!ent.Owner.IsValid()) - yield break; + return false; + } + + /// + /// Returns the full chain of containers containing the entity passed in, from innermost to outermost. + /// + /// + /// The resulting collection includes the container directly containing the entity (if any), + /// the container containing that container, and so on until reaching the outermost container. + /// + public IEnumerable GetContainingContainers(Entity ent) + { + if (!ent.Owner.IsValid()) + yield break; - if (!Resolve(ent, ref ent.Comp)) - yield break; + if (!Resolve(ent, ref ent.Comp)) + yield break; - var child = ent.Owner; - var parent = ent.Comp.ParentUid; + var child = ent.Owner; + var parent = ent.Comp.ParentUid; - while (parent.IsValid()) + while (parent.IsValid()) + { + if (((MetaQuery.GetComponent(child).Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer) && + _managerQuery.TryGetComponent(parent, out var conManager) && + TryGetContainingContainer(parent, child, out var parentContainer, conManager)) { - if (((MetaQuery.GetComponent(child).Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer) && - _managerQuery.TryGetComponent(parent, out var conManager) && - TryGetContainingContainer(parent, child, out var parentContainer, conManager)) - { - yield return parentContainer; - } - - var parentXform = TransformQuery.GetComponent(parent); - child = parent; - parent = parentXform.ParentUid; + yield return parentContainer; } - } - /// - /// Gets the top-most container in the hierarchy for this entity, if it exists. - /// - public bool TryGetOuterContainer(EntityUid uid, TransformComponent xform, [NotNullWhen(true)] out BaseContainer? container) - { - return TryGetOuterContainer(uid, xform, out container, TransformQuery); + var parentXform = TransformQuery.GetComponent(parent); + child = parent; + parent = parentXform.ParentUid; } + } - public bool TryGetOuterContainer(EntityUid uid, TransformComponent xform, - [NotNullWhen(true)] out BaseContainer? container, EntityQuery xformQuery) - { - container = null; + /// + /// Gets the top-most container in the hierarchy for this entity, if it exists. + /// + public bool TryGetOuterContainer(EntityUid uid, TransformComponent xform, [NotNullWhen(true)] out BaseContainer? container) + { + return TryGetOuterContainer(uid, xform, out container, TransformQuery); + } + + public bool TryGetOuterContainer(EntityUid uid, TransformComponent xform, + [NotNullWhen(true)] out BaseContainer? container, EntityQuery xformQuery) + { + container = null; - if (!uid.IsValid()) - return false; + if (!uid.IsValid()) + return false; - var child = uid; - var parent = xform.ParentUid; + var child = uid; + var parent = xform.ParentUid; - while (parent.IsValid()) + while (parent.IsValid()) + { + if (((MetaQuery.GetComponent(child).Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer) && + _managerQuery.TryGetComponent(parent, out var conManager) && + TryGetContainingContainer(parent, child, out var parentContainer, conManager)) { - if (((MetaQuery.GetComponent(child).Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer) && - _managerQuery.TryGetComponent(parent, out var conManager) && - TryGetContainingContainer(parent, child, out var parentContainer, conManager)) - { - container = parentContainer; - } - - var parentXform = xformQuery.GetComponent(parent); - child = parent; - parent = parentXform.ParentUid; + container = parentContainer; } - return container != null; + var parentXform = xformQuery.GetComponent(parent); + child = parent; + parent = parentXform.ParentUid; } - /// - /// Attempts to remove an entity from its container, if any. - /// - /// Entity that might be inside a container. - /// Whether to forcibly remove the entity from the container. - /// Whether the entity was actually inside a container or not. - /// If the entity could be removed. Also returns false if it wasn't inside a container. - public bool TryRemoveFromContainer(Entity entity, bool force, out bool wasInContainer) - { - DebugTools.Assert(Exists(entity)); + return container != null; + } - if (TryGetContainingContainer(entity, out var container)) - { - wasInContainer = true; + /// + /// Attempts to remove an entity from its container, if any. + /// + /// Entity that might be inside a container. + /// Whether to forcibly remove the entity from the container. + /// Whether the entity was actually inside a container or not. + /// If the entity could be removed. Also returns false if it wasn't inside a container. + public bool TryRemoveFromContainer(Entity entity, bool force, out bool wasInContainer) + { + DebugTools.Assert(Exists(entity)); - if (!force) - return Remove(entity, container); + if (TryGetContainingContainer(entity, out var container)) + { + wasInContainer = true; - Remove(entity, container, force: true); - return true; - } + if (!force) + return Remove(entity, container); - wasInContainer = false; - return false; + Remove(entity, container, force: true); + return true; } - /// - /// Attempts to remove an entity from its container, if any. - /// - /// Entity that might be inside a container. - /// Whether to forcibly remove the entity from the container. - /// If the entity could be removed. Also returns false if it wasn't inside a container. - public bool TryRemoveFromContainer(Entity entity, bool force = false) - { - return TryRemoveFromContainer(entity, force, out _); - } + wasInContainer = false; + return false; + } - /// - /// Attempts to remove all entities in a container. Returns removed entities. - /// - public List EmptyContainer( - BaseContainer container, - bool force = false, - EntityCoordinates? destination = null, - bool reparent = true) - { - var removed = new List(container.ContainedEntities); - for (var i = removed.Count - 1; i >= 0; i--) - { - if (Remove(removed[i], container, reparent: reparent, force: force, destination: destination)) - continue; + /// + /// Attempts to remove an entity from its container, if any. + /// + /// Entity that might be inside a container. + /// Whether to forcibly remove the entity from the container. + /// If the entity could be removed. Also returns false if it wasn't inside a container. + public bool TryRemoveFromContainer(Entity entity, bool force = false) + { + return TryRemoveFromContainer(entity, force, out _); + } - // failed to remove entity. - DebugTools.Assert(container.Contains(removed[i])); - removed.RemoveSwap(i); - } + /// + /// Attempts to remove all entities in a container. Returns removed entities. + /// + public List EmptyContainer( + BaseContainer container, + bool force = false, + EntityCoordinates? destination = null, + bool reparent = true) + { + var removed = new List(container.ContainedEntities); + for (var i = removed.Count - 1; i >= 0; i--) + { + if (Remove(removed[i], container, reparent: reparent, force: force, destination: destination)) + continue; - return removed; + // failed to remove entity. + DebugTools.Assert(container.Contains(removed[i])); + removed.RemoveSwap(i); } - /// - /// Attempts to remove and delete all entities in a container. - /// - public void CleanContainer(BaseContainer container) + return removed; + } + + /// + /// Attempts to remove and delete all entities in a container. + /// + public void CleanContainer(BaseContainer container) + { + foreach (var ent in container.ContainedEntities.ToArray()) { - foreach (var ent in container.ContainedEntities.ToArray()) - { - if (Deleted(ent)) - continue; + if (Deleted(ent)) + continue; - Remove(ent, container, force: true); - PredictedDel(ent); - } + Remove(ent, container, force: true); + PredictedDel(ent); } + } - public void AttachParentToContainerOrGrid(Entity transform) + public void AttachParentToContainerOrGrid(Entity transform) + { + // TODO make this check upwards for any container, and parent to that. + // Currently this just checks the direct parent, so entities will still teleport through containers. + if (!transform.Comp.ParentUid.IsValid() + || !TryGetContainingContainer((transform.Comp.ParentUid, Transform(transform.Comp.ParentUid)), out var container) + || !TryInsertIntoContainer(transform, container)) { - // TODO make this check upwards for any container, and parent to that. - // Currently this just checks the direct parent, so entities will still teleport through containers. - if (!transform.Comp.ParentUid.IsValid() - || !TryGetContainingContainer((transform.Comp.ParentUid, Transform(transform.Comp.ParentUid)), out var container) - || !TryInsertIntoContainer(transform, container)) - { - _transform.AttachToGridOrMap(transform, transform.Comp); - } + _transform.AttachToGridOrMap(transform, transform.Comp); } + } - private bool TryInsertIntoContainer(Entity transform, BaseContainer container) - { - if (Insert((transform.Owner, transform.Comp, null, null), container)) - return true; + private bool TryInsertIntoContainer(Entity transform, BaseContainer container) + { + if (Insert((transform.Owner, transform.Comp, null, null), container)) + return true; - var ownerXform = Transform(container.Owner); - if (ownerXform.ParentUid.IsValid() - && TryGetContainingContainer((container.Owner, ownerXform), out var newContainer)) - return TryInsertIntoContainer(transform, newContainer); + var ownerXform = Transform(container.Owner); + if (ownerXform.ParentUid.IsValid() + && TryGetContainingContainer((container.Owner, ownerXform), out var newContainer)) + return TryInsertIntoContainer(transform, newContainer); - return false; - } + return false; + } - internal bool TryGetManagerComp(EntityUid entity, [NotNullWhen(true)] out ContainerManagerComponent? manager) - { - DebugTools.Assert(Exists(entity)); + internal bool TryGetManagerComp(EntityUid entity, [NotNullWhen(true)] out ContainerManagerComponent? manager) + { + DebugTools.Assert(Exists(entity)); - if (TryComp(entity, out manager)) - return true; + if (TryComp(entity, out manager)) + return true; - // RECURSION ALERT - var transform = Transform(entity); - if (transform.ParentUid.IsValid()) - return TryGetManagerComp(transform.ParentUid, out manager); + // RECURSION ALERT + var transform = Transform(entity); + if (transform.ParentUid.IsValid()) + return TryGetManagerComp(transform.ParentUid, out manager); - return false; - } + return false; + } - #endregion + #endregion - protected virtual void OnParentChanged(ref EntParentChangedMessage message) - { - var meta = MetaData(message.Entity); - if ((meta.Flags & MetaDataFlags.InContainer) == 0) - return; + protected virtual void OnParentChanged(ref EntParentChangedMessage message) + { + var meta = MetaData(message.Entity); + if ((meta.Flags & MetaDataFlags.InContainer) == 0) + return; - // Eject entities from their parent container if the parent change is done via setting the transform. - if (TryComp(message.OldParent, out ContainerManagerComponent? containerManager)) - RemoveEntity(message.OldParent.Value, message.Entity, containerManager, message.Transform, meta, reparent: false, force: true); - } + // Eject entities from their parent container if the parent change is done via setting the transform. + if (TryComp(message.OldParent, out ContainerManagerComponent? containerManager)) + RemoveEntity(message.OldParent.Value, message.Entity, containerManager, message.Transform, meta, reparent: false, force: true); + } - [Conditional("DEBUG"), Access(typeof(BaseContainer))] - public void AssertInContainer(EntityUid uid, BaseContainer container) - { - if (_timing.ApplyingState) - return; // Entity might not yet have had its state updated. + [Conditional("DEBUG"), Access(typeof(BaseContainer))] + public void AssertInContainer(EntityUid uid, BaseContainer container) + { + if (_timing.ApplyingState) + return; // Entity might not yet have had its state updated. - var flags = MetaData(uid).Flags; - DebugTools.Assert((flags & MetaDataFlags.InContainer) != 0, - $"Entity has bad container flags. Ent: {ToPrettyString(uid)}. Container: {container.ID}, Owner: {ToPrettyString(container.Owner)}"); - } + var flags = MetaData(uid).Flags; + DebugTools.Assert((flags & MetaDataFlags.InContainer) != 0, + $"Entity has bad container flags. Ent: {ToPrettyString(uid)}. Container: {container.ID}, Owner: {ToPrettyString(container.Owner)}"); } } diff --git a/Robust.Shared/EntitySerialization/EntityDeserializer.cs b/Robust.Shared/EntitySerialization/EntityDeserializer.cs index a5be87519cb..66221b13e6c 100644 --- a/Robust.Shared/EntitySerialization/EntityDeserializer.cs +++ b/Robust.Shared/EntitySerialization/EntityDeserializer.cs @@ -617,8 +617,9 @@ private void LoadEntity( } var datanode = compData; - if (proto != null && proto.Components.TryGetValue(name, out var protoData)) - datanode = _seriMan.CombineMappings(compData, protoData.Mapping); + + if (proto != null && _proto.GetPrototypeData(proto).TryGetValue(name, out var protoData)) + datanode = _seriMan.CombineMappings(compData, protoData); _components.Add(name, datanode); } @@ -689,7 +690,6 @@ private void LoadEntity( // I'm scared turning over this rock will reveal a lot of bugs. So leaving that to a future PR. // I.e., creating "temp" here just unnecessarily slows everything down. var temp = (IComponent) _seriMan.Read(compReg.Type, data, this)!; - _seriMan.CopyTo(temp, ref existing, this, notNullableOverride: true); } diff --git a/Robust.Shared/EntitySerialization/MapChunkSerializer.cs b/Robust.Shared/EntitySerialization/MapChunkSerializer.cs index 0b3bb6c1c6a..c5df77dfadb 100644 --- a/Robust.Shared/EntitySerialization/MapChunkSerializer.cs +++ b/Robust.Shared/EntitySerialization/MapChunkSerializer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; @@ -41,8 +42,8 @@ public MapChunk Read(ISerializationManager serializationManager, MappingDataNode using var stream = new MemoryStream(tileBytes); using var reader = new BinaryReader(stream); - var mapManager = dependencies.Resolve(); - mapManager.SuppressOnTileChanged = true; + var mapSystem = dependencies.Resolve().System(); + mapSystem.SuppressOnTileChanged = true; ushort size = 16; @@ -112,7 +113,7 @@ public MapChunk Read(ISerializationManager serializationManager, MappingDataNode } chunk.SuppressCollisionRegeneration = false; - mapManager.SuppressOnTileChanged = false; + mapSystem.SuppressOnTileChanged = false; return chunk; } @@ -192,8 +193,8 @@ public MapChunk CreateCopy( SerializationHookContext hookCtx, ISerializationContext? context = null) { - var mapManager = dependencies.Resolve(); - mapManager.SuppressOnTileChanged = true; + var mapSystem = dependencies.Resolve().System(); + mapSystem.SuppressOnTileChanged = true; var chunk = new MapChunk(source.X, source.Y, source.ChunkSize) { SuppressCollisionRegeneration = true @@ -207,7 +208,7 @@ public MapChunk CreateCopy( } } - mapManager.SuppressOnTileChanged = false; + mapSystem.SuppressOnTileChanged = false; chunk.SuppressCollisionRegeneration = false; return chunk; diff --git a/Robust.Shared/GameObjects/CompName.cs b/Robust.Shared/GameObjects/CompName.cs new file mode 100644 index 00000000000..3dd84f58ce0 --- /dev/null +++ b/Robust.Shared/GameObjects/CompName.cs @@ -0,0 +1,67 @@ +using System; +using Robust.Shared.Serialization.TypeSerializers.Implementations; +using Robust.Shared.Toolshed.TypeParsers; + +namespace Robust.Shared.GameObjects; + +/// +/// Wrapper type for the name of a component. +/// All methods of creating it are checked to be valid, so the string value is also guaranteed to be valid. +/// +/// +/// This will be automatically validated by if used in data fields. +/// Doing so however will NOT skip ignored components, move your shitcode to shared! +/// +public readonly record struct CompName : + IEquatable, + IComparable, + IAsType +{ + /// + /// Wrap a component name string, throwing an exception if it is not registered. + /// + public CompName(string name, IComponentFactory factory) + { + if (!factory.HasRegistration(name)) + throw new UnknownComponentException($"Tried to create CompName with unregistered component name '{name}'"); + Name = name; + } + + private CompName([ForbidLiteral] string name) + { + // no checking, only used by Get methods below which are guaranteed to be valid + Name = name; + } + + public static implicit operator string(CompName name) + => name.Name; + + /// + /// Get the name of a given component using a and . + /// + public static CompName Get(IComponentFactory factory) where T : IComponent, new() + => new CompName(factory.GetComponentName()); + + /// + /// Get the name of a given component using a and a component's . + /// Will throw for non-component types. + /// + public static CompName Get(Type type, IComponentFactory factory) + => new CompName(factory.GetComponentName(type)); + + /// + /// The underlying string for this component name. + /// E.g. for TransformComponent this is "Transform". + /// + public string Name { get; private init; } + + public bool Equals(string? other) + => Name == other; + + public int CompareTo(CompName other) + => string.Compare(Name, other.Name, StringComparison.Ordinal); + + public string AsType() => Name; + + public override string ToString() => Name; +} diff --git a/Robust.Shared/GameObjects/ComponentFactory.cs b/Robust.Shared/GameObjects/ComponentFactory.cs index 20bedc1d21e..aaab8e16b07 100644 --- a/Robust.Shared/GameObjects/ComponentFactory.cs +++ b/Robust.Shared/GameObjects/ComponentFactory.cs @@ -349,6 +349,14 @@ public string GetComponentName(ushort netID) return GetRegistration(netID).Name; } + [Pure] + public CompName CompName() where T : IComponent, new() + => GameObjects.CompName.Get(this); + + [Pure] + public CompName CompName(Type type) + => GameObjects.CompName.Get(type, this); + public ComponentRegistration GetRegistration(ushort netID) { if (_networkedComponents is null) @@ -407,6 +415,9 @@ public bool TryGetRegistration(string componentName, [NotNullWhen(true)] out Com return false; } + public bool HasRegistration(string componentName) + => _names.ContainsKey(componentName); + public bool TryGetRegistration(Type reference, [NotNullWhen(true)] out ComponentRegistration? registration) { if (_types.TryGetValue(reference, out var tempRegistration)) diff --git a/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs b/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs index b71418b9476..d48e425623f 100644 --- a/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs +++ b/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs @@ -30,7 +30,7 @@ public sealed partial class EyeComponent : Component [DataField, AutoNetworkedField] public bool DrawFov = true; - [AutoNetworkedField] + [DataField, AutoNetworkedField] public bool DrawLight = true; // yes it's not networked, don't ask. diff --git a/Robust.Shared/GameObjects/Components/Renderable/SpriteLayerData.cs b/Robust.Shared/GameObjects/Components/Renderable/SpriteLayerData.cs index 67d1395813d..cfa15bec80e 100644 --- a/Robust.Shared/GameObjects/Components/Renderable/SpriteLayerData.cs +++ b/Robust.Shared/GameObjects/Components/Renderable/SpriteLayerData.cs @@ -61,7 +61,7 @@ public sealed partial class PrototypeCopyToShaderParameters /// /// The map key of the layer that will have its shader modified. /// - [DataField(required: true)] public string LayerKey; + [DataField(required: true)] public string LayerKey = null!; /// /// The name of the shader parameter that will receive the actual selected texture. diff --git a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs index f2e132d06cd..2b23fb359fd 100644 --- a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs +++ b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Numerics; using JetBrains.Annotations; using Robust.Shared.Animations; @@ -8,7 +7,6 @@ using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; -using Robust.Shared.Physics; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -103,8 +101,6 @@ public Matrix3x2 InvLocalMatrix [ViewVariables] internal readonly HashSet _children = new(); - [Dependency] private IMapManager _mapManager = default!; - /// /// Returns the index of the map which this object is on /// @@ -135,6 +131,7 @@ public Matrix3x2 InvLocalMatrix public bool NoLocalRotation { get => _noLocalRotation; + [Obsolete("Use SharedTransformSystem.SetNoLocalRotation() instead")] set { if (value) @@ -153,11 +150,14 @@ public bool NoLocalRotation public Angle LocalRotation { get => _localRotation; + [Obsolete("Use SharedTransformSystem.SetLocalRotation() instead")] set { if(_noLocalRotation) return; + value = SharedTransformSystem.NormalizeRotation(value); + if (_localRotation.EqualsApprox(value)) return; @@ -178,7 +178,7 @@ public Angle LocalRotation /// Current world rotation of the entity. /// [ViewVariables(VVAccess.ReadWrite)] - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.Get/SetWorldRotation() instead")] public Angle WorldRotation { get @@ -219,7 +219,7 @@ public Angle WorldRotation /// /// Matrix for transforming points from local to world space. /// - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.GetWorldMatrix() instead")] public Matrix3x2 WorldMatrix { get @@ -245,7 +245,7 @@ public Matrix3x2 WorldMatrix /// /// Matrix for transforming points from world to local space. /// - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.GetInvWorldMatrix() instead")] public Matrix3x2 InvWorldMatrix { get @@ -274,7 +274,7 @@ public Matrix3x2 InvWorldMatrix /// [Animatable] [ViewVariables(VVAccess.ReadWrite)] - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.Get/SetWorldPosition() instead")] public Vector2 WorldPosition { get @@ -315,7 +315,7 @@ public EntityCoordinates Coordinates var valid = _parent.IsValid(); return new EntityCoordinates(valid ? _parent : Owner, valid ? LocalPosition : Vector2.Zero); } - [Obsolete("Use the system's setter method instead.")] + [Obsolete("Use SharedTransformSystem.SetCoordinates() instead")] set => _entMan.EntitySysManager.GetEntitySystem().SetCoordinates(Owner, this, value); } @@ -324,7 +324,7 @@ public EntityCoordinates Coordinates /// This is effectively a more complete version of /// [ViewVariables(VVAccess.ReadWrite)] - [Obsolete("Use TransformSystem.GetMapCoordinates")] + [Obsolete("Use SharedTransformSystem.GetMapCoordinates() instead")] public MapCoordinates MapPosition => new(WorldPosition, MapID); /// @@ -336,7 +336,7 @@ public EntityCoordinates Coordinates public Vector2 LocalPosition { get => _localPosition; - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.SetLocalPosition() instead")] set { if(Anchored) @@ -375,7 +375,7 @@ public bool Anchored { _anchored = value; } - else if (value && !_anchored && _mapManager.TryFindGridAt(MapPosition, out _, out var grid)) + else if (value && !_anchored && _entMan.EntitySysManager.GetEntitySystem().TryFindGridAt(MapPosition, out _, out var grid)) { _anchored = _entMan.EntitySysManager.GetEntitySystem().AnchorEntity(Owner, this, grid); } @@ -399,7 +399,7 @@ public bool Anchored /// /// Detaches this entity from its parent. /// - [Obsolete("Use the system's method instead.")] + [Obsolete("Use SharedTransformSystem.AttachToGridOrMap() instead")] public void AttachToGridOrMap() { _entMan.EntitySysManager.GetEntitySystem().AttachToGridOrMap(Owner, this); @@ -414,7 +414,7 @@ public void AttachParent(EntityUid parent) /// /// Get the WorldPosition and WorldRotation of this entity faster than each individually. /// - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.GetWorldPositionRotation() instead")] public (Vector2 WorldPosition, Angle WorldRotation) GetWorldPositionRotation() { // Worldmatrix needs calculating anyway for worldpos so we'll just drop it. @@ -425,7 +425,7 @@ public void AttachParent(EntityUid parent) /// /// Get the WorldPosition, WorldRotation, and WorldMatrix of this entity faster than each individually. /// - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.GetWorldPositionRotationMatrix() instead")] public (Vector2 WorldPosition, Angle WorldRotation, Matrix3x2 WorldMatrix) GetWorldPositionRotationMatrix(EntityQuery xforms) { var parent = _parent; @@ -451,7 +451,7 @@ public void AttachParent(EntityUid parent) /// /// Get the WorldPosition, WorldRotation, and WorldMatrix of this entity faster than each individually. /// - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.GetWorldPositionRotationMatrix() instead")] public (Vector2 WorldPosition, Angle WorldRotation, Matrix3x2 WorldMatrix) GetWorldPositionRotationMatrix() { var xforms = _entMan.GetEntityQuery(); @@ -461,7 +461,7 @@ public void AttachParent(EntityUid parent) /// /// Get the WorldPosition, WorldRotation, and InvWorldMatrix of this entity faster than each individually. /// - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.WorldPositionRotationInvMatrix() instead")] public (Vector2 WorldPosition, Angle WorldRotation, Matrix3x2 InvWorldMatrix) GetWorldPositionRotationInvMatrix(EntityQuery xformQuery) { var (worldPos, worldRot, _, invWorldMatrix) = GetWorldPositionRotationMatrixWithInv(xformQuery); @@ -471,7 +471,7 @@ public void AttachParent(EntityUid parent) /// /// Get the WorldPosition, WorldRotation, WorldMatrix, and InvWorldMatrix of this entity faster than each individually. /// - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.GetWorldPositionRotationMatrixWithInv() instead")] public (Vector2 WorldPosition, Angle WorldRotation, Matrix3x2 WorldMatrix, Matrix3x2 InvWorldMatrix) GetWorldPositionRotationMatrixWithInv() { var xformQuery = _entMan.GetEntityQuery(); @@ -481,7 +481,7 @@ public void AttachParent(EntityUid parent) /// /// Get the WorldPosition, WorldRotation, WorldMatrix, and InvWorldMatrix of this entity faster than each individually. /// - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.GetWorldPositionRotationMatrixWithInv() instead")] public (Vector2 WorldPosition, Angle WorldRotation, Matrix3x2 WorldMatrix, Matrix3x2 InvWorldMatrix) GetWorldPositionRotationMatrixWithInv(EntityQuery xformQuery) { var parent = _parent; @@ -525,6 +525,7 @@ public void RebuildMatrices() _invLocalMatrix = Matrix3Helpers.CreateInverseTransform(_localPosition, _localRotation); } + [Obsolete("Use SharedTransformSystem.GetDebugString() instead")] public string GetDebugString() { return $"pos/rot/wpos/wrot: {Coordinates}/{LocalRotation}/{WorldPosition}/{WorldRotation}"; @@ -554,6 +555,7 @@ public readonly struct MoveEvent( public TransformComponent Component => Entity.Comp1; public bool ParentChanged => NewPosition.EntityId != OldPosition.EntityId; + public bool OnlyRotation => OldPosition.Equals(NewPosition); } public struct TransformChildrenEnumerator : IDisposable diff --git a/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs b/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs index b44986238ec..20e61731cf4 100644 --- a/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs +++ b/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs @@ -13,11 +13,11 @@ namespace Robust.Shared.GameObjects [RegisterComponent, NetworkedComponent, Access(typeof(SharedUserInterfaceSystem))] public sealed partial class UserInterfaceComponent : Component, IComponentDelta { - /// - public GameTick LastFieldUpdate { get; set; } + /// + public GameTick LastUnclassifiedDirty { get; set; } /// - public GameTick[] LastModifiedFields { get; set; } + public GameTick[] LastModifiedFields { get; set; } = []; /// /// The currently open interfaces. Used clientside to store the UI. @@ -160,6 +160,15 @@ public sealed class BoundUserInterfaceMessageAttempt( public readonly BoundUserInterfaceMessage Message = message; } + /// + /// Raised whenever the server receives a BUI wrap message from a client for a valid interface. + /// + [ByRefEvent] + public readonly record struct BoundUserInterfaceMessageReceivedEvent( + EntityUid Actor, + EntityUid Target, + Enum UiKey); + [NetSerializable, Serializable] public abstract class BoundUserInterfaceState { diff --git a/Robust.Shared/GameObjects/EntityManager.ComponentDeltas.cs b/Robust.Shared/GameObjects/EntityManager.ComponentDeltas.cs index ae7c772bddf..08660cd8d64 100644 --- a/Robust.Shared/GameObjects/EntityManager.ComponentDeltas.cs +++ b/Robust.Shared/GameObjects/EntityManager.ComponentDeltas.cs @@ -5,19 +5,26 @@ namespace Robust.Shared.GameObjects; public abstract partial class EntityManager { - public uint GetModifiedFields(IComponentDelta delta, GameTick fromTick) + public static ulong GetModifiedAspects(IComponentDelta delta, GameTick fromTick) { - uint fields = 0; + if (delta.LastUnclassifiedDirty > fromTick) + { + // By returning max value here, we short-circuit the more expensive evaluation below while returning a value + // with the unclassified change bit set. This will over-represent changes, technically, but in this specific + // case components should be doing a full update anyway. + return ulong.MaxValue; + } + ulong fields = 0; for (var i = 0; i < delta.LastModifiedFields.Length; i++) { var lastUpdate = delta.LastModifiedFields[i]; // Field not dirty - if (lastUpdate < fromTick) + if (lastUpdate <= fromTick) continue; - fields |= (uint) (1 << i); + fields |= 1UL << i; } return fields; @@ -34,9 +41,8 @@ public void DirtyField(EntityUid uid, IComponentDelta comp, string fieldName, Me } var curTick = _gameTiming.CurTick; - comp.LastFieldUpdate = curTick; comp.LastModifiedFields[idx] = curTick; - Dirty(uid, comp, metadata); + DirtyInternal(uid, comp, metadata, false); } public virtual void DirtyField(EntityUid uid, T comp, [ValidateMember] string fieldName, MetaDataComponent? metadata = null) @@ -54,9 +60,8 @@ public virtual void DirtyField(EntityUid uid, T comp, [ValidateMember] string } var curTick = _gameTiming.CurTick; - comp.LastFieldUpdate = curTick; comp.LastModifiedFields[idx] = curTick; - Dirty(uid, comp, metadata); + DirtyInternal(uid, comp, metadata, false); } public virtual void DirtyFields(EntityUid uid, T comp, MetaDataComponent? meta, params string[] fields) @@ -73,8 +78,7 @@ public virtual void DirtyFields(EntityUid uid, T comp, MetaDataComponent? met comp.LastModifiedFields[idx] = curTick; } - comp.LastFieldUpdate = curTick; - Dirty(uid, comp, meta); + DirtyInternal(uid, comp, meta, false); } } @@ -83,18 +87,22 @@ public virtual void DirtyFields(EntityUid uid, T comp, MetaDataComponent? met /// public partial interface IComponentDelta : IComponent { - // TODO: This isn't entirely robust but not sure how else to handle this? /// - /// Track last time a field was dirtied. if the full component dirty exceeds this then we send a full state update. + /// The last unclassified modification to this component. /// - public GameTick LastFieldUpdate { get; set; } + public GameTick LastUnclassifiedDirty { get; set; } /// /// Stores the last modified tick for fields. /// - public GameTick[] LastModifiedFields - { - get; - set; - } + public GameTick[] LastModifiedFields { get; set; } +} + +/// +/// Component delta system aspects. These are flags returned via . Fields +/// occupy the lower bits and grow upwards. System aspects occupy the upper bits and grow downwards. +/// +public static class DeltaAspect +{ + public const ulong Unclassified = 1UL << 63; } diff --git a/Robust.Shared/GameObjects/EntityManager.Components.cs b/Robust.Shared/GameObjects/EntityManager.Components.cs index b36fa6d1b2a..4facdde86f4 100644 --- a/Robust.Shared/GameObjects/EntityManager.Components.cs +++ b/Robust.Shared/GameObjects/EntityManager.Components.cs @@ -397,6 +397,7 @@ private void AddComponentInternal(EntityUid uid, T component, ComponentRegist if (component is IComponentDelta delta) { var curTick = _gameTiming.CurTick; + delta.LastUnclassifiedDirty = curTick; delta.LastModifiedFields = new GameTick[reg.NetworkedFields.Length]; Array.Fill(delta.LastModifiedFields, curTick); } @@ -2308,6 +2309,28 @@ public void Dispose() { _traitDict.Dispose(); } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public struct Enumerator(EntityQueryEnumerator enumerator) + { + private EntityQueryEnumerator _enumerator = enumerator; + + public Entity Current { get; private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (!_enumerator.MoveNext(out var id, out var comp)) + return false; + + Current = (id, comp); + return true; + } + } } /// @@ -2377,6 +2400,28 @@ public void Dispose() { _traitDict.Dispose(); } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public struct Enumerator(EntityQueryEnumerator enumerator) + { + private EntityQueryEnumerator _enumerator = enumerator; + + public Entity Current { get; private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (!_enumerator.MoveNext(out var id, out var comp1, out var comp2)) + return false; + + Current = (id, comp1, comp2); + return true; + } + } } /// @@ -2461,6 +2506,28 @@ public void Dispose() { _traitDict.Dispose(); } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public struct Enumerator(EntityQueryEnumerator enumerator) + { + private EntityQueryEnumerator _enumerator = enumerator; + + public Entity Current { get; private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (!_enumerator.MoveNext(out var id, out var comp1, out var comp2, out var comp3)) + return false; + + Current = (id, comp1, comp2, comp3); + return true; + } + } } /// @@ -2558,6 +2625,28 @@ public void Dispose() { _traitDict.Dispose(); } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public struct Enumerator(EntityQueryEnumerator enumerator) + { + private EntityQueryEnumerator _enumerator = enumerator; + + public Entity Current { get; private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (!_enumerator.MoveNext(out var id, out var comp1, out var comp2, out var comp3, out var comp4)) + return false; + + Current = (id, comp1, comp2, comp3, comp4); + return true; + } + } } #endregion @@ -2640,6 +2729,28 @@ public void Dispose() { _traitDict.Dispose(); } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public struct Enumerator(AllEntityQueryEnumerator enumerator) + { + private AllEntityQueryEnumerator _enumerator = enumerator; + + public Entity Current { get; private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (!_enumerator.MoveNext(out var id, out var comp)) + return false; + + Current = (id, comp); + return true; + } + } } /// @@ -2700,6 +2811,28 @@ public void Dispose() { _traitDict.Dispose(); } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public struct Enumerator(AllEntityQueryEnumerator enumerator) + { + private AllEntityQueryEnumerator _enumerator = enumerator; + + public Entity Current { get; private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (!_enumerator.MoveNext(out var id, out var comp1, out var comp2)) + return false; + + Current = (id, comp1, comp2); + return true; + } + } } /// @@ -2774,6 +2907,28 @@ public void Dispose() { _traitDict.Dispose(); } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public struct Enumerator(AllEntityQueryEnumerator enumerator) + { + private AllEntityQueryEnumerator _enumerator = enumerator; + + public Entity Current { get; private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (!_enumerator.MoveNext(out var id, out var comp1, out var comp2, out var comp3)) + return false; + + Current = (id, comp1, comp2, comp3); + return true; + } + } } /// @@ -2860,6 +3015,28 @@ public void Dispose() { _traitDict.Dispose(); } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public struct Enumerator(AllEntityQueryEnumerator enumerator) + { + private AllEntityQueryEnumerator _enumerator = enumerator; + + public Entity Current { get; private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (!_enumerator.MoveNext(out var id, out var comp1, out var comp2, out var comp3, out var comp4)) + return false; + + Current = (id, comp1, comp2, comp3, comp4); + return true; + } + } } #endregion diff --git a/Robust.Shared/GameObjects/EntityManager.Spawn.cs b/Robust.Shared/GameObjects/EntityManager.Spawn.cs index 94a86489b37..046cf3deab5 100644 --- a/Robust.Shared/GameObjects/EntityManager.Spawn.cs +++ b/Robust.Shared/GameObjects/EntityManager.Spawn.cs @@ -125,7 +125,11 @@ public virtual EntityUid Spawn(string? protoName, MapCoordinates coordinates, Co [MethodImpl(MethodImplOptions.AggressiveInlining)] public EntityUid SpawnAtPosition(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null) - => Spawn(protoName, _xforms.ToMapCoordinates(coordinates), overrides); + => SpawnAtPosition(protoName, coordinates, _xforms.GetWorldRotation(coordinates.EntityId), overrides); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public EntityUid SpawnAtPosition(string? protoName, EntityCoordinates coordinates, Angle rotation, ComponentRegistry? overrides = null) + => Spawn(protoName, _xforms.ToMapCoordinates(coordinates), overrides, rotation: rotation); public bool TrySpawnNextTo( string? protoName, @@ -254,6 +258,11 @@ public virtual EntityUid PredictedSpawnAtPosition(string? protoName, EntityCoord return SpawnAtPosition(protoName, coordinates, overrides); } + public virtual EntityUid PredictedSpawnAtPosition(string? protoName, EntityCoordinates coordinates, Angle rotation, ComponentRegistry? overrides = null) + { + return SpawnAtPosition(protoName, coordinates, rotation, overrides); + } + public virtual bool PredictedTrySpawnNextTo( string? protoName, EntityUid target, diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index 2df50e9dbf9..de734da9fce 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -37,7 +37,6 @@ public abstract partial class EntityManager : IEntityManager [IoC.Dependency] protected IPrototypeManager PrototypeManager = default!; [IoC.Dependency] protected ILogManager LogManager = default!; [IoC.Dependency] private IEntitySystemManager _entitySystemManager = default!; - [IoC.Dependency] private IMapManager _mapManager = default!; [IoC.Dependency] private IGameTiming _gameTiming = default!; [IoC.Dependency] private ISerializationManager _serManager = default!; [IoC.Dependency] private ProfManager _prof = default!; @@ -73,6 +72,12 @@ public abstract partial class EntityManager : IEntityManager protected readonly Queue QueuedDeletions = new(); protected readonly HashSet QueuedDeletionsSet = new(); + private Histogram? _tickUpdateHistogram; + private Histogram.Child? _entitySystemsHistogram; + private Histogram.Child? _entityEventBusHistogram; + private Histogram.Child? _queuedDeletionHistogram; + private Histogram.Child? _componentCullHistogram; + private EntityDiffContext _context = new(); /// @@ -273,31 +278,45 @@ public virtual void Cleanup() public virtual void TickUpdate(float frameTime, bool noPredictions, Histogram? histogram) { - using (histogram?.WithLabels("EntitySystems").NewTimer()) + UpdateTickHistogram(histogram); + + using (_entitySystemsHistogram?.NewTimer()) using (_prof.Group("Systems")) { _entitySystemManager.TickUpdate(frameTime, noPredictions); } - using (histogram?.WithLabels("EntityEventBus").NewTimer()) + using (_entityEventBusHistogram?.NewTimer()) using (_prof.Group("Events")) { EventBusInternal.ProcessEventQueue(); } - using (histogram?.WithLabels("QueuedDeletion").NewTimer()) + using (_queuedDeletionHistogram?.NewTimer()) using (_prof.Group("QueueDel")) { ProcessQueueudDeletions(); } - using (histogram?.WithLabels("ComponentCull").NewTimer()) + using (_componentCullHistogram?.NewTimer()) using (_prof.Group("ComponentCull")) { CullRemovedComponents(); } } + private void UpdateTickHistogram(Histogram? histogram) + { + if (ReferenceEquals(_tickUpdateHistogram, histogram)) + return; + + _tickUpdateHistogram = histogram; + _entitySystemsHistogram = histogram?.WithLabels("EntitySystems"); + _entityEventBusHistogram = histogram?.WithLabels("EntityEventBus"); + _queuedDeletionHistogram = histogram?.WithLabels("QueuedDeletion"); + _componentCullHistogram = histogram?.WithLabels("ComponentCull"); + } + internal virtual void ProcessQueueudDeletions() { while (QueuedDeletions.TryDequeue(out var uid)) @@ -351,7 +370,9 @@ public virtual EntityUid CreateEntityUninitialized(string? prototypeName, MapCoo if (coordinates.MapId == MapId.Nullspace) { transform._parent = EntityUid.Invalid; +#pragma warning disable CS0618 // AnchorEntity/Unanchor only work on initialized entities transform.Anchored = false; +#pragma warning restore CS0618 return newEntity; } @@ -360,7 +381,7 @@ public virtual EntityUid CreateEntityUninitialized(string? prototypeName, MapCoo throw new ArgumentException($"Attempted to spawn entity on an invalid map. Coordinates: {coordinates}"); EntityCoordinates coords; - if (_mapManager.TryFindGridAt(coordinates, out var gridUid, out var grid) + if (_mapSystem.TryFindGridAt(coordinates, out var gridUid, out var grid) && MetaQuery.TryGetComponentInternal(gridUid, out var meta) && meta.EntityLifeStage < EntityLifeStage.Terminating) { @@ -383,6 +404,21 @@ public virtual EntityUid CreateEntityUninitialized(string? prototypeName, MapCoo /// public IEnumerable GetEntities() => Entities; + #region Dirtying + + private bool DirtyComponent(IComponent component, bool isUnclassifiedChange = true) + { + var newChange = component.LastModifiedTick != CurrentTick; + component.LastModifiedTick = CurrentTick; + + if (isUnclassifiedChange && component is IComponentDelta delta) + { + delta.LastUnclassifiedDirty = CurrentTick; + } + + return newChange; + } + /// public virtual void DirtyEntity(EntityUid uid, MetaDataComponent? metadata = null) { @@ -402,14 +438,16 @@ public virtual void DirtyEntity(EntityUid uid, MetaDataComponent? metadata = nul } /// - [Obsolete("use override with an EntityUid or Entity")] - public void Dirty(IComponent component, MetaDataComponent? meta = null) + public virtual void Dirty(EntityUid uid, IComponent component, MetaDataComponent? meta = null) { - Dirty(component.Owner, component, meta); + DirtyInternal(uid, component, meta); } - /// - public virtual void Dirty(EntityUid uid, IComponent component, MetaDataComponent? meta = null) + internal void DirtyInternal( + EntityUid uid, + IComponent component, + MetaDataComponent? meta = null, + bool isUnclassifiedChange = true) { DebugTools.Assert(component.GetType().HasCustomAttribute(), $"Attempted to dirty a non-networked component: {component.GetType()}"); @@ -418,15 +456,18 @@ public virtual void Dirty(EntityUid uid, IComponent component, MetaDataComponent if (component.LifeStage >= ComponentLifeStage.Removing || !component.NetSyncEnabled) return; - if (component.LastModifiedTick == CurrentTick) - return; - - DirtyEntity(uid, meta); - component.LastModifiedTick = CurrentTick; + if (DirtyComponent(component, isUnclassifiedChange)) + DirtyEntity(uid, meta); } /// public virtual void Dirty(Entity ent, MetaDataComponent? meta = null) where T : IComponent + { + DirtyInternal(ent, meta); + } + + internal void DirtyInternal(Entity ent, MetaDataComponent? meta = null, bool isUnclassifiedChange = true) + where T : IComponent { DebugTools.Assert(ent.Comp.GetType().HasCustomAttribute(), $"Attempted to dirty a non-networked component: {ent.Comp.GetType()}"); @@ -434,11 +475,8 @@ public virtual void Dirty(Entity ent, MetaDataComponent? meta = null) wher if (ent.Comp.LifeStage >= ComponentLifeStage.Removing || !ent.Comp.NetSyncEnabled) return; - if (ent.Comp.LastModifiedTick == CurrentTick) - return; - - DirtyEntity(ent, meta); - ent.Comp.LastModifiedTick = CurrentTick; + if (DirtyComponent(ent.Comp, isUnclassifiedChange)) + DirtyEntity(ent, meta); } /// @@ -454,8 +492,8 @@ public virtual void Dirty(Entity ent, MetaDataComponent? meta = // We're not gonna bother checking ent.Comp.NetSyncEnabled // chances are at least one of these components didn't get net-sync disabled. DirtyEntity(ent, meta); - ent.Comp1.LastModifiedTick = CurrentTick; - ent.Comp2.LastModifiedTick = CurrentTick; + DirtyComponent(ent.Comp1); + DirtyComponent(ent.Comp2); } /// @@ -474,9 +512,9 @@ public virtual void Dirty(Entity ent, MetaDataComponent? // We're not gonna bother checking ent.Comp.NetSyncEnabled // chances are at least one of these components didn't get net-sync disabled. DirtyEntity(ent, meta); - ent.Comp1.LastModifiedTick = CurrentTick; - ent.Comp2.LastModifiedTick = CurrentTick; - ent.Comp3.LastModifiedTick = CurrentTick; + DirtyComponent(ent.Comp1); + DirtyComponent(ent.Comp2); + DirtyComponent(ent.Comp3); } /// @@ -498,12 +536,14 @@ public virtual void Dirty(Entity ent, MetaDataCo // We're not gonna bother checking ent.Comp.NetSyncEnabled // chances are at least one of these components didn't get net-sync disabled. DirtyEntity(ent, meta); - ent.Comp1.LastModifiedTick = CurrentTick; - ent.Comp2.LastModifiedTick = CurrentTick; - ent.Comp3.LastModifiedTick = CurrentTick; - ent.Comp4.LastModifiedTick = CurrentTick; + DirtyComponent(ent.Comp1); + DirtyComponent(ent.Comp2); + DirtyComponent(ent.Comp3); + DirtyComponent(ent.Comp4); } + #endregion + public bool TryQueueDeleteEntity(EntityUid? uid) { if (uid == null) diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index 8a264e68720..8a8c7d380f6 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -8,6 +8,7 @@ using Robust.Shared.Maths; using Robust.Shared.Prototypes; using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Robust.Shared.GameObjects; @@ -494,6 +495,14 @@ protected bool TryComp(EntityUid uid, [NotNullWhen(true)] out T? comp) where return EntityManager.TryGetComponent(uid, out comp); } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ProxyFor(typeof(EntityManager), nameof(EntityManager.TryGetComponent))] + protected bool TryComp(EntityUid uid, Type type, [NotNullWhen(true)] out IComponent? comp) + { + return EntityManager.TryGetComponent(uid, type, out comp); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected bool TryComp(EntityUid uid, [NotNullWhen(true)] out TransformComponent? comp) @@ -522,6 +531,20 @@ protected bool TryComp([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(true) return EntityManager.TryGetComponent(uid.Value, out comp); } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ProxyFor(typeof(EntityManager), nameof(EntityManager.TryGetComponent))] + protected bool TryComp([NotNullWhen(true)] EntityUid? uid, Type type, [NotNullWhen(true)] out IComponent? comp) + { + if (!uid.HasValue) + { + comp = null; + return false; + } + + return EntityManager.TryGetComponent(uid.Value, type, out comp); + } + /// protected bool TryComp([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(true)] out TransformComponent? comp) { @@ -692,6 +715,49 @@ protected bool HasComp([NotNullWhen(true)] EntityUid? uid, Type type) return EntityManager.HasComponent(uid, type); } + /// + /// Returns true if an entity prototype has a . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool HasComp([ForbidLiteral] EntProtoId id) where T : IComponent, new() + => HasComp(id, Factory.CompName()); + + /// + /// Returns true if an entity prototype has a component with a given type. + /// Will throw if the type does not belong to a component. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool HasComp([ForbidLiteral] EntProtoId id, Type type) + => HasComp(id, Factory.CompName(type)); + + /// + /// Returns true if a entity prototype contains a component with a given name. + /// Logs errors in case of the prototype not existing. + /// If you call this a lot on the same prototype, resolve it first and call the -using HasComp variants. + /// + [Pure] + protected bool HasComp([ForbidLiteral] EntProtoId id, CompName name) + => ProtoMan.Resolve(id, out var proto) && proto.HasComp(name); + + /// + /// Returns true if an already-resolved entity prototype has a component of type . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool HasComp(EntityPrototype proto) where T : IComponent, new() + => proto.HasComp(Factory.CompName()); + + /// + /// Returns true if an already-resolved entity prototype has a component with a given type. + /// Will throw if the type does not belong to a component. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool HasComp(EntityPrototype proto, Type type) + => proto.HasComp(Factory.CompName(type)); + #endregion #region Component Add diff --git a/Robust.Shared/GameObjects/EntitySystem.cs b/Robust.Shared/GameObjects/EntitySystem.cs index e4f7e94480c..8881fece6e4 100644 --- a/Robust.Shared/GameObjects/EntitySystem.cs +++ b/Robust.Shared/GameObjects/EntitySystem.cs @@ -9,6 +9,7 @@ using Robust.Shared.Log; using Robust.Shared.Network; using Robust.Shared.Player; +using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Replays; @@ -24,6 +25,7 @@ namespace Robust.Shared.GameObjects public abstract partial class EntitySystem : IEntitySystem, IPostInjectInit { [Dependency] protected EntityManager EntityManager = default!; + [Dependency] protected IPrototypeManager ProtoMan = default!; [Dependency] protected ILogManager LogManager = default!; [Dependency] private ISharedPlayerManager _playerMan = default!; [Dependency] private IReplayRecordingManager _replayMan = default!; @@ -92,6 +94,10 @@ protected EntitySystem() [MustCallBase(true)] public virtual void Initialize() { } + /// + [MustCallBase(true)] + public virtual void AutoSubscriptions() { } + /// /// /// Not ran on the client if prediction is disabled and diff --git a/Robust.Shared/GameObjects/EntitySystemManager.cs b/Robust.Shared/GameObjects/EntitySystemManager.cs index c22fb8b7523..edae00b15c4 100644 --- a/Robust.Shared/GameObjects/EntitySystemManager.cs +++ b/Robust.Shared/GameObjects/EntitySystemManager.cs @@ -65,7 +65,7 @@ public IDependencyCollection DependencyCollection private bool _initialized; [ViewVariables] private UpdateReg[] _updateOrder = Array.Empty(); - [ViewVariables] private IEntitySystem[] _frameUpdateOrder = Array.Empty(); + [ViewVariables] private FrameUpdateReg[] _frameUpdateOrder = Array.Empty(); public bool MetricsEnabled { get; set; } @@ -213,18 +213,32 @@ public void Initialize(bool discover = true) { var system = (IEntitySystem)SystemDependencyCollection.ResolveType(systemType); system.Initialize(); + if (system is EntitySystem entitySystem) + entitySystem.AutoSubscriptions(); SystemLoaded?.Invoke(this, new SystemChangedArgs(system)); } // Create update order for entity systems. var (fUpdate, update) = CalculateUpdateOrder(_systemTypes, subTypes, SystemDependencyCollection); - _frameUpdateOrder = fUpdate.ToArray(); _updateOrder = update - .Select(s => new UpdateReg + .Select(s => + { + var profileName = s.GetType().Name; + return new UpdateReg + { + System = s, + ProfileName = profileName, + Monitor = _tickUsageHistogram.WithLabels(profileName) + }; + }) + .ToArray(); + + _frameUpdateOrder = fUpdate + .Select(s => new FrameUpdateReg { System = s, - Monitor = _tickUsageHistogram.WithLabels(s.GetType().Name) + ProfileName = s.GetType().Name }) .ToArray(); @@ -308,7 +322,7 @@ public void Clear() _extraLoadedTypes.Clear(); _systemTypes.Clear(); _updateOrder = Array.Empty(); - _frameUpdateOrder = Array.Empty(); + _frameUpdateOrder = Array.Empty(); _initialized = false; SystemDependencyCollection?.Clear(); } @@ -329,7 +343,7 @@ public void TickUpdate(float frameTime, bool noPredictions) try { #endif - using (_profManager.Value(updReg.System.GetType().Name)) + using (_profManager.Value(updReg.ProfileName)) { updReg.System.Update(frameTime); } @@ -351,15 +365,15 @@ public void TickUpdate(float frameTime, bool noPredictions) /// public void FrameUpdate(float frameTime) { - foreach (var system in _frameUpdateOrder) + foreach (var updReg in _frameUpdateOrder) { #if EXCEPTION_TOLERANCE try { #endif - using (_profManager.Value(system.GetType().Name)) + using (_profManager.Value(updReg.ProfileName)) { - system.FrameUpdate(frameTime); + updReg.System.FrameUpdate(frameTime); } #if EXCEPTION_TOLERANCE } @@ -427,12 +441,24 @@ private static bool NeedsFrameUpdate(Type type) return mFrameUpdate!.DeclaringType != typeof(EntitySystem); } - internal IEnumerable FrameUpdateOrder => _frameUpdateOrder.Select(c => c.GetType()); + internal IEnumerable FrameUpdateOrder => _frameUpdateOrder.Select(c => c.System.GetType()); internal IEnumerable TickUpdateOrder => _updateOrder.Select(c => c.System.GetType()); + private struct FrameUpdateReg + { + [ViewVariables] public IEntitySystem System; + [ViewVariables] public string ProfileName; + + public override string? ToString() + { + return System.ToString(); + } + } + private struct UpdateReg { [ViewVariables] public IEntitySystem System; + [ViewVariables] public string ProfileName; [ViewVariables] public Histogram.Child Monitor; public override string? ToString() diff --git a/Robust.Shared/GameObjects/IComponentFactory.cs b/Robust.Shared/GameObjects/IComponentFactory.cs index 219826c6dbf..e2cb49ab509 100644 --- a/Robust.Shared/GameObjects/IComponentFactory.cs +++ b/Robust.Shared/GameObjects/IComponentFactory.cs @@ -193,6 +193,19 @@ public interface IComponentFactory [Pure] string GetComponentName(ushort netID); + /// + /// Get a wrapped for a given component type. + /// + [Pure] + CompName CompName() where T : IComponent, new(); + + /// + /// Get a wrapped for a component's . + /// Throws for non-component types. + /// + [Pure] + CompName CompName(Type type); + /// /// Gets the registration belonging to a component, throwing an exception if it does not exist. /// @@ -250,6 +263,12 @@ public interface IComponentFactory /// bool IsIgnored(string componentName); + /// + /// Returns true if a given name has a component registration associated with it. + /// This check is always case sensitive. + /// + bool HasRegistration(string componentName); + /// /// Tries to get the registration belonging to a component. /// diff --git a/Robust.Shared/GameObjects/IEntityManager.cs b/Robust.Shared/GameObjects/IEntityManager.cs index d0763cf1bea..5c9547adc26 100644 --- a/Robust.Shared/GameObjects/IEntityManager.cs +++ b/Robust.Shared/GameObjects/IEntityManager.cs @@ -131,9 +131,6 @@ public partial interface IEntityManager public void DirtyEntity(EntityUid uid, MetaDataComponent? metadata = null); - [Obsolete("use override with an EntityUid")] - public void Dirty(IComponent component, MetaDataComponent? metadata = null); - public void Dirty(EntityUid uid, IComponent component, MetaDataComponent? meta = null); public void Dirty(Entity ent, MetaDataComponent? meta = null) where T : IComponent; diff --git a/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs b/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs index 7d7d2ead228..fa7df579f5e 100644 --- a/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs +++ b/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.Contracts; using System.Numerics; using System.Runtime.CompilerServices; using Robust.Shared.Collections; @@ -93,7 +94,7 @@ private void AddEntitiesIntersecting(MapId mapId, flags); // Need to include maps - _mapManager.FindGridsIntersecting(mapId, worldAABB, ref state, + _map.FindGridsIntersecting(mapId, worldAABB, ref state, static (EntityUid uid, MapGridComponent _, ref EntityQueryState state) => { var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, uid); @@ -244,7 +245,7 @@ private bool AnyEntitiesIntersecting(MapId mapId, flags); // Need to include maps - _mapManager.FindGridsIntersecting(mapId, worldAABB, ref state, + _map.FindGridsIntersecting(mapId, worldAABB, ref state, static (EntityUid uid, MapGridComponent _, ref AnyEntityQueryState state) => { var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, uid); @@ -556,7 +557,7 @@ public void GetEntitiesIntersecting(EntityUid uid, HashSet intersecti var state = (uid, transform, intersecting, _fixturesQuery, this, _physics, flags); // Unfortuantely I can't think of a way to de-dupe this with the other ones as it's slightly different. - _mapManager.FindGridsIntersecting(mapId, worldAABB, ref state, + _map.FindGridsIntersecting(mapId, worldAABB, ref state, static (EntityUid gridUid, MapGridComponent grid, ref (EntityUid entity, Transform transform, HashSet intersecting, EntityQuery fixturesQuery, EntityLookupSystem lookup, SharedPhysicsSystem physics, LookupFlags flags) state) => @@ -786,7 +787,7 @@ public void FindLookupsIntersecting(MapId mapId, Box2Rotated worldBounds, Compon var state = (callback, _broadQuery); - _mapManager.FindGridsIntersecting(mapId, worldBounds, ref state, + _map.FindGridsIntersecting(mapId, worldBounds, ref state, static (EntityUid uid, MapGridComponent grid, ref (ComponentQueryCallback callback, EntityQuery _broadQuery) tuple) => @@ -803,18 +804,21 @@ public void FindLookupsIntersecting(MapId mapId, Box2Rotated worldBounds, Compon #region Bounds + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public Box2 GetLocalBounds(Vector2i gridIndices, ushort tileSize) { return new Box2(gridIndices * tileSize, (gridIndices + 1) * tileSize); } + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public Box2 GetLocalBounds(TileRef tileRef, ushort tileSize) { return GetLocalBounds(tileRef.GridIndices, tileSize); } + [Pure] public Box2Rotated GetWorldBounds(TileRef tileRef, Matrix3x2? worldMatrix = null, Angle? angle = null) { var grid = _gridQuery.GetComponent(tileRef.GridUid); diff --git a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs index 16d33310d35..f579ecc7bc6 100644 --- a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs +++ b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs @@ -458,7 +458,7 @@ public bool AnyComponentsIntersecting(Type type, MapId mapId, T shape, Transf // Get grid entities var state = (this, worldAABB, flags, query, ignored, found: false); - _mapManager.FindGridsIntersecting(mapId, worldAABB, ref state, + _map.FindGridsIntersecting(mapId, worldAABB, ref state, static (EntityUid uid, MapGridComponent grid, ref (EntityLookupSystem system, Box2 worldAABB, @@ -550,7 +550,7 @@ public void GetEntitiesIntersecting(Type type, MapId mapId, T shape, Transfor // Get grid entities var state = new GridQueryState(intersecting, shape, shapeTransform, this, _physics, flags, query); - _mapManager.FindGridsIntersecting(mapId, worldAABB, ref state, + _map.FindGridsIntersecting(mapId, worldAABB, ref state, static (EntityUid uid, MapGridComponent grid, ref GridQueryState state) => { var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, uid); @@ -596,7 +596,7 @@ public void GetEntitiesIntersecting(MapId mapId, TShape shape, Transf // Get grid entities var state = new GridQueryState(entities, shape, shapeTransform, this, _physics, flags, query); - _mapManager.FindGridsIntersecting(mapId, worldAABB, ref state, + _map.FindGridsIntersecting(mapId, worldAABB, ref state, static (EntityUid uid, MapGridComponent grid, ref GridQueryState state) => { var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, uid); diff --git a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs index 1d1e90c671d..25e124922f9 100644 --- a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs +++ b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs @@ -73,7 +73,6 @@ public record struct WorldAABBEvent public sealed partial class EntityLookupSystem : EntitySystem { [Dependency] private IManifoldManager _manifoldManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private INetManager _netMan = default!; [Dependency] private SharedContainerSystem _container = default!; diff --git a/Robust.Shared/GameObjects/Systems/MetaDataSystem.cs b/Robust.Shared/GameObjects/Systems/MetaDataSystem.cs index cd02f4c1a94..dff87b4930e 100644 --- a/Robust.Shared/GameObjects/Systems/MetaDataSystem.cs +++ b/Robust.Shared/GameObjects/Systems/MetaDataSystem.cs @@ -10,7 +10,6 @@ namespace Robust.Shared.GameObjects; public abstract partial class MetaDataSystem : EntitySystem { [Dependency] private IGameTiming _timing = default!; - [Dependency] private IPrototypeManager _proto = default!; private EntityPausedEvent _pausedEvent; @@ -18,6 +17,8 @@ public abstract partial class MetaDataSystem : EntitySystem public override void Initialize() { + base.Initialize(); + _metaQuery = GetEntityQuery(); SubscribeLocalEvent(OnMetaDataHandle); SubscribeLocalEvent(OnMetaDataGetState); @@ -37,7 +38,7 @@ private void OnMetaDataHandle(EntityUid uid, MetaDataComponent component, ref Co component._entityDescription = state.Description; if(state.PrototypeId != null && state.PrototypeId != component._entityPrototype?.ID) - component._entityPrototype = _proto.Index(state.PrototypeId); + component._entityPrototype = ProtoMan.Index(state.PrototypeId); component.PauseTime = state.PauseTime; } diff --git a/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs b/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs index e514d97e8e9..d36822a54fc 100644 --- a/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs +++ b/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs @@ -11,11 +11,12 @@ namespace Robust.Shared.GameObjects; /// internal sealed partial class PrototypeReloadSystem : EntitySystem { - [Dependency] private IPrototypeManager _prototypes = default!; - [Dependency] private IComponentFactory _componentFactory = default!; + [Dependency] private MetaDataSystem _meta = default!; public override void Initialize() { + base.Initialize(); + SubscribeLocalEvent(OnPrototypesReloaded); } @@ -31,7 +32,7 @@ private void OnPrototypesReloaded(PrototypesReloadedEventArgs eventArgs) if (id == null || !set.Modified.ContainsKey(id)) continue; - var proto = _prototypes.Index(id); + var proto = ProtoMan.Index(id); UpdateEntity(uid, metadata, proto); } } @@ -42,12 +43,12 @@ private void UpdateEntity(EntityUid entity, MetaDataComponent metaData, EntityPr var oldPrototypeComponents = oldPrototype?.Components.Keys .Where(n => n != "Transform" && n != "MetaData") - .Select(name => (name, _componentFactory.GetRegistration(name).Type)) + .Select(name => (name, Factory.GetRegistration(name).Type)) .ToList() ?? new List<(string name, Type Type)>(); var newPrototypeComponents = newPrototype.Components.Keys .Where(n => n != "Transform" && n != "MetaData") - .Select(name => (name, _componentFactory.GetRegistration(name).Type)) + .Select(name => (name, Factory.GetRegistration(name).Type)) .ToList(); var ignoredComponents = new List(); @@ -71,13 +72,13 @@ private void UpdateEntity(EntityUid entity, MetaDataComponent metaData, EntityPr .Except(oldPrototypeComponents)) { var data = newPrototype.Components[name]; - var component = _componentFactory.GetComponent(name); + var component = Factory.GetComponent(name); if (!HasComp(entity, component.GetType())) AddComp(entity, component); } // Update entity metadata - metaData.EntityPrototype = newPrototype; + _meta.SetEntityPrototype(entity, newPrototype, metaData); } } diff --git a/Robust.Shared/GameObjects/Systems/SharedGridTraversalSystem.cs b/Robust.Shared/GameObjects/Systems/SharedGridTraversalSystem.cs index 5cbdd7f6f63..ef0f8f0108c 100644 --- a/Robust.Shared/GameObjects/Systems/SharedGridTraversalSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedGridTraversalSystem.cs @@ -13,7 +13,7 @@ namespace Robust.Shared.GameObjects; /// public sealed partial class SharedGridTraversalSystem : EntitySystem { - [Dependency] private IMapManagerInternal _mapManager = default!; + [Dependency] private SharedMapSystem _mapSystem = default!; [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private IGameTiming _timing = default!; @@ -97,7 +97,7 @@ public void CheckTraversal(EntityUid entity, TransformComponent xform, EntityUid : Vector2.Transform(xform.LocalPosition, Transform(xform.ParentUid).LocalMatrix); // Change parent if necessary - if (_mapManager.TryFindGridAt(map, mapPos, out var gridUid, out _)) + if (_mapSystem.TryFindGridAt(map, mapPos, out var gridUid, out _)) { // Some minor duplication here with AttachParent but only happens when going on/off grid so not a big deal ATM. if (gridUid != xform.GridUid && !TerminatingOrDeleted(gridUid)) diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Coordinates.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Coordinates.cs index 7ff85746be3..de2ad55a9d2 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Coordinates.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Coordinates.cs @@ -25,7 +25,7 @@ public EntityCoordinates AlignToGrid(EntityCoordinates coordinates) // Check if mappos intersects a grid. var mapPos = _transform.ToMapCoordinates(coordinates); - if (_mapInternal.TryFindGridAt(mapPos, out var gridUid, out gridComponent)) + if (TryFindGridAt(mapPos, out var gridUid, out gridComponent)) { var tile = CoordinatesToTile(gridUid, gridComponent, coordinates); return ToCenterCoordinates(gridUid, tile, gridComponent); diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.Queries.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.Queries.cs new file mode 100644 index 00000000000..4d754b59031 --- /dev/null +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.Queries.cs @@ -0,0 +1,612 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Map.Enumerators; +using Robust.Shared.Maths; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Collision.Shapes; +using Robust.Shared.Physics.Shapes; +using Transform = Robust.Shared.Physics.Transform; + +namespace Robust.Shared.GameObjects; + +public abstract partial class SharedMapSystem +{ + + /// + /// Whether and its extended family should only approximately check for intersection by default. + /// + public const bool Approximate = false; + + /// + /// Whether and its extended family should also check the map itself by default. + /// + public const bool IncludeMap = true; + + #region TryFindGridAt + + /// + /// Attempts to find a grid which overlaps with a given position on a given map. + /// If the map is itself a grid and there is no other grid overlapping with the given position this will return the map itself as such a grid. + /// + /// The uid of the map to search for a valid grid. + /// The exact position within and relative to the map to search for a valid grid. + /// Returns the uid of the grid found, if any. + /// Returns the component of the grid found, if any. + /// True if a grid overlapping with the given position within the given map was found, or false otherwise. + public bool TryFindGridAt(EntityUid mapEnt, Vector2 worldPos, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid) + { + var rangeVec = new Vector2(0.2f, 0.2f); + + // Need to enlarge the AABB by at least the grid shrinkage size. + var aabb = new Box2(worldPos - rangeVec, worldPos + rangeVec); + + uid = EntityUid.Invalid; + grid = null; + var state = (uid, grid, worldPos, this, _transform); + + FindGridsIntersecting(mapEnt, aabb, ref state, static (EntityUid iUid, MapGridComponent iGrid, ref ( + EntityUid uid, + MapGridComponent? grid, + Vector2 worldPos, + SharedMapSystem mapSystem, + SharedTransformSystem xformSystem) tuple) => + { + // Turn the worldPos into a localPos and work out the relevant chunk we need to check + // This is much faster than iterating over every chunk individually. + // (though now we need some extra calcs up front). + + // Doesn't use WorldBounds because it's just an AABB. + var matrix = tuple.xformSystem.GetInvWorldMatrix(iUid); + var localPos = Vector2.Transform(tuple.worldPos, matrix); + + // NOTE: + // If you change this to use fixtures instead (i.e. if you want half-tiles) then you need to make sure + // you account for the fact that fixtures are shrunk slightly! + var chunkIndices = GetChunkIndices(localPos, iGrid.ChunkSize); + + if (!iGrid.Chunks.TryGetValue(chunkIndices, out var chunk)) + return true; + + var chunkRelative = GetChunkRelative(localPos, iGrid.ChunkSize); + var chunkTile = chunk.GetTile(chunkRelative); + + if (chunkTile.IsEmpty) + return true; + + tuple.uid = iUid; + tuple.grid = iGrid; + return false; + }, approx: true, includeMap: false); + + if (state.grid == null && _gridQuery.TryGetComponent(mapEnt, out var mapGrid)) + { + uid = mapEnt; + grid = mapGrid; + return true; + } + + uid = state.uid; + grid = state.grid; + return grid != null; + } + + /// + /// The id of the map to search for a valid grid. + public bool TryFindGridAt(MapId mapId, Vector2 worldPos, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid) + { + if (TryGetMap(mapId, out var map)) + return TryFindGridAt(map.Value, worldPos, out uid, out grid); + + uid = default; + grid = null; + return false; + } + + /// + /// The map position to search for a valid grid. + public bool TryFindGridAt(MapCoordinates mapCoordinates, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid) + { + return TryFindGridAt(mapCoordinates.MapId, mapCoordinates.Position, out uid, out grid); + } + + #endregion + + #region MapId + + /// + /// Adds every grid on the specified map which intersects the given region to the provided collection. + /// + /// The shape of the region to check. + /// The transform, relative to the map, of the region to check. + public void FindGridsIntersecting( + MapId mapId, + TShape shape, + Transform transform, + ref List> grids, + bool approx = Approximate, + bool includeMap = IncludeMap) where TShape : IPhysShape + { + if (TryGetMap(mapId, out var mapEnt)) + FindGridsIntersecting(mapEnt.Value, shape, transform, ref grids, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// + /// The shape of the region to check. + /// The transform, relative to the map, of the region to check. + public void FindGridsIntersecting( + MapId mapId, + TShape shape, + Transform transform, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) where TShape : IPhysShape + { + if (TryGetMap(mapId, out var mapEnt)) + FindGridsIntersecting(mapEnt.Value, shape, transform, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// Allows providing some additional to pass to the callback when it is invoked. + /// + /// The shape of the region to check. + /// The transform, relative to the map, of the region to check. + public void FindGridsIntersecting( + MapId mapId, + TShape shape, + Transform transform, + ref TState state, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) where TShape : IPhysShape + { + if (TryGetMap(mapId, out var mapEnt)) + FindGridsIntersecting(mapEnt.Value, shape, transform, ref state, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// + public void FindGridsIntersecting( + MapId mapId, + Box2 worldAABB, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + if (TryGetMap(mapId, out var mapEnt)) + FindGridsIntersecting(mapEnt.Value, worldAABB, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// Allows providing some additional to pass to the callback when it is invoked. + /// + public void FindGridsIntersecting( + MapId mapId, + Box2 worldAABB, + ref TState state, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + if (TryGetMap(mapId, out var map)) + FindGridsIntersecting(map.Value, worldAABB, ref state, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Adds every grid on the specified map which intersects the given region to the provided collection. + /// + public void FindGridsIntersecting( + MapId mapId, + Box2 worldAABB, + ref List> grids, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + if (TryGetMap(mapId, out var map)) + FindGridsIntersecting(map.Value, worldAABB, ref grids, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// + public void FindGridsIntersecting( + MapId mapId, + Box2Rotated worldBounds, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + if (TryGetMap(mapId, out var mapEnt)) + FindGridsIntersecting(mapEnt.Value, worldBounds, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// Allows providing some additional to pass to the callback when it is invoked. + /// + public void FindGridsIntersecting( + MapId mapId, + Box2Rotated worldBounds, + ref TState state, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + if (TryGetMap(mapId, out var mapEnt)) + FindGridsIntersecting(mapEnt.Value, worldBounds, ref state, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Adds every grid on the specified map which intersects the given region to the provided collection. + /// + public void FindGridsIntersecting( + MapId mapId, + Box2Rotated worldBounds, + ref List> grids, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + if (TryGetMap(mapId, out var mapEnt)) + FindGridsIntersecting(mapEnt.Value, worldBounds, ref grids, approx: approx, includeMap: includeMap); + } + + #endregion + + #region EntityUid + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// + /// The shape of the region to check. + /// The transform, relative to the map, of the region to check. + public void FindGridsIntersecting( + EntityUid mapEnt, + TShape shape, + Transform transform, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) where TShape : IPhysShape + { + FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// Allows providing some additional to pass to the callback when it is invoked. + /// + /// The shape of the region to check. + /// The transform, relative to the map, of the region to check. + public void FindGridsIntersecting( + EntityUid mapEnt, + TShape shape, + Transform transform, + ref TState state, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) where TShape : IPhysShape + { + FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, ref state, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Adds every grid on the specified map which intersects the given region to the provided list. + /// + /// The shape of the region to check. + /// The transform, relative to the map, of the region to check. + public void FindGridsIntersecting( + EntityUid mapEnt, + TShape shape, + Transform transform, + ref List> grids, + bool approx = Approximate, + bool includeMap = IncludeMap) where TShape : IPhysShape + { + FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, ref grids, approx: approx, includeMap: includeMap); + } + + /// + /// Adds every grid on the specified map which intersects the given regions to the provided collection. + /// + /// A set of regions to check. + /// The transform, relative to the map, of the regions to check. + public void FindGridsIntersecting( + EntityUid mapEnt, + List shapes, + Transform transform, + ref List> entities, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + foreach (var shape in shapes) + { + FindGridsIntersecting(mapEnt, shape, transform, ref entities, approx: approx, includeMap: includeMap); + } + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// + public void FindGridsIntersecting( + EntityUid mapEnt, + Box2 worldAABB, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + var shape = new SlimPolygon(worldAABB); + FindGridsIntersecting(mapEnt, shape, worldAABB, Robust.Shared.Physics.Transform.Empty, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// Allows providing some additional to pass to the callback when it is invoked. + /// + public void FindGridsIntersecting( + EntityUid mapEnt, + Box2 worldAABB, + ref TState state, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + var shape = new SlimPolygon(worldAABB); + FindGridsIntersecting(mapEnt, shape, worldAABB, Robust.Shared.Physics.Transform.Empty, ref state, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Adds every grid on the specified map which intersects the given regions to the provided list. + /// + public void FindGridsIntersecting( + EntityUid mapEnt, + Box2 worldAABB, + ref List> grids, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + var shape = new SlimPolygon(worldAABB); + FindGridsIntersecting(mapEnt, shape, worldAABB, Robust.Shared.Physics.Transform.Empty, ref grids, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// + public void FindGridsIntersecting( + EntityUid mapEnt, + Box2Rotated worldBounds, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + var shape = new SlimPolygon(worldBounds); + FindGridsIntersecting(mapEnt, shape, Robust.Shared.Physics.Transform.Empty, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// Allows providing some additional to pass to the callback when it is invoked. + /// + public void FindGridsIntersecting( + EntityUid mapEnt, + Box2Rotated worldBounds, + ref TState state, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + var shape = new SlimPolygon(worldBounds); + FindGridsIntersecting(mapEnt, shape, Robust.Shared.Physics.Transform.Empty, ref state, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Adds every grid on the specified map which intersects the given regions to the provided list. + /// + public void FindGridsIntersecting( + EntityUid mapEnt, + Box2Rotated worldBounds, + ref List> grids, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + var shape = new SlimPolygon(worldBounds); + FindGridsIntersecting(mapEnt, shape, Robust.Shared.Physics.Transform.Empty, ref grids, approx: approx, includeMap: includeMap); + } + + #endregion + + /// + /// Enumerates all of the grids located on a given map. + /// + public IEnumerable> GetAllGrids(MapId mapId) + { + var query = AllEntityQuery(); + while (query.MoveNext(out var uid, out var grid, out var xform)) + { + if (xform.MapID != mapId) + continue; + + yield return (uid, grid); + } + } + + /// + /// This version only provides the component without the uid and should not be used. + /// + /// + [Obsolete("use GetAllGrids instead")] + public IEnumerable GetAllMapGrids(MapId mapId) + { + var query = AllEntityQuery(); + while (query.MoveNext(out var grid, out var xform)) + { + if (xform.MapID == mapId) + yield return grid; + } + } + + /// + /// Adds every grid on the specified map which intersects the given regions to the provided list. + /// + /// The shape of the region to check. + /// The world-local axis aligned bounding box of the region to check. + /// The transform, relative to the map, of the region to check. + private void FindGridsIntersecting( + EntityUid mapEnt, + TShape shape, + Box2 worldAABB, + Transform transform, + ref List> grids, + bool approx, + bool includeMap) where TShape : IPhysShape + { + var state = grids; + FindGridsIntersecting(mapEnt, shape, worldAABB, transform, ref state, + static (EntityUid uid, MapGridComponent grid, ref List> state) => + { + state.Add((uid, grid)); + return true; + }, + approx, includeMap + ); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// + /// The shape of the region to check. + /// The world-local axis aligned bounding box of the region to check. + /// The transform, relative to the map, of the region to check. + private void FindGridsIntersecting( + EntityUid mapEnt, + TShape shape, + Box2 worldAABB, + Transform transform, + GridCallback callback, + bool approx, + bool includeMap) where TShape : IPhysShape + { + var state = callback; + FindGridsIntersecting(mapEnt, shape, worldAABB, transform, ref state, + static (EntityUid uid, MapGridComponent grid, ref GridCallback state) => state.Invoke(uid, grid), + approx, includeMap + ); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// Allows providing some additional to pass to the callback when it is invoked. + /// + /// The shape of the region to check. + /// The world-local axis aligned bounding box of the region to check. + /// The transform, relative to the map, of the region to check. + private void FindGridsIntersecting( + EntityUid mapEnt, + TShape shape, + Box2 worldAABB, + Transform transform, + ref TState state, + GridCallback callback, + bool approx, + bool includeMap) where TShape : IPhysShape + { + if (!_gridTreeQuery.TryGetComponent(mapEnt, out var gridTree)) + return; + + if (includeMap && _gridQuery.TryGetComponent(mapEnt, out var mapGrid)) + { + callback(mapEnt, mapGrid, ref state); + } + + var gridState = new GridQueryState( + callback, + state, + worldAABB, + shape, + transform, + gridTree.Tree, + this, + _transform, + approx); + + gridTree.Tree.Query(ref gridState, static (ref GridQueryState state, DynamicTree.Proxy proxy) => + { + // Even for approximate we'll check if any chunks roughly overlap. + var data = state.Tree.GetUserData(proxy); + var gridInvMatrix = state.TransformSystem.GetInvWorldMatrix(data.Uid); + var localAABB = gridInvMatrix.TransformBox(state.WorldAABB); + + var overlappingChunks = state.MapSystem.GetLocalMapChunks(data.Uid, data.Grid, localAABB); + + if (state.Approximate) + { + if (!overlappingChunks.MoveNext(out _)) + return true; + } + else if (!state.MapSystem.IsIntersecting(overlappingChunks, state.Shape, state.Transform, (data.Uid, data.Fixtures))) + { + return true; + } + + var callbackState = state.State; + var result = state.Callback(data.Uid, data.Grid, ref callbackState); + state.State = callbackState; + + return result; + }, worldAABB); + + // By-ref things + state = gridState.State; + } + + /// + /// Tests whether any of a collection of grid chunks intersect with a given region. + /// + private bool IsIntersecting( + ChunkEnumerator enumerator, + TShape shape, + Transform shapeTransform, + Entity grid) where TShape : IPhysShape + { + var gridTransform = _physics.GetPhysicsTransform(grid); + + while (enumerator.MoveNext(out var chunk)) + { + foreach (var id in chunk.Fixtures) + { + var fixture = grid.Comp.Fixtures[id]; + + for (var j = 0; j < fixture.Shape.ChildCount; j++) + { + if (_manifolds.TestOverlap(shape, 0, fixture.Shape, j, shapeTransform, gridTransform)) + { + return true; + } + } + } + } + + return false; + } + + private record struct GridQueryState( + GridCallback Callback, + TState State, + Box2 WorldAABB, + TShape Shape, + Transform Transform, + B2DynamicTree<(EntityUid Uid, FixturesComponent Fixtures, MapGridComponent Grid)> Tree, + SharedMapSystem MapSystem, + SharedTransformSystem TransformSystem, + bool Approximate + ) where TShape : IPhysShape; +} + +public delegate bool GridCallback(EntityUid gridUid, MapGridComponent gridComp); +public delegate bool GridCallback(EntityUid gridUid, MapGridComponent gridComp, ref TState state); diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs index 5e4709ffc36..2188eb2f514 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs @@ -22,6 +22,52 @@ namespace Robust.Shared.GameObjects; public abstract partial class SharedMapSystem { + #region CreateGrid + + /// + /// Creates a new grid entity on a given map. + /// + public Entity CreateGridEntity(MapId mapId, GridCreateOptions? options = null) + { + return CreateGridEntity(GetMap(mapId), options); + } + + /// + /// Creates a new grid entity on a given map. + /// + public Entity CreateGridEntity(EntityUid mapEnt, GridCreateOptions? options = null) + { + options ??= GridCreateOptions.Default; + return CreateGridInternal(mapEnt, options.Value); + } + + protected Entity CreateGridInternal(EntityUid mapEnt, GridCreateOptions options) + { + var gridEnt = EntityManager.CreateEntityUninitialized(null); + + var grid = EnsureComp(gridEnt); + grid.ChunkSize = options.ChunkSize; + + Log.Debug("Binding new grid {gridEnt}"); + + //TODO: This is a hack to get TransformComponent.MapId working before entity states + //are applied. After they are applied the parent may be different, but the MapId will + //be the same. This causes TransformComponent.ParentUid of a grid to be unsafe to + //use in transform states anytime before the state parent is properly set. + _transform.SetParent(gridEnt, mapEnt); + + var meta = _metaQuery.GetComponent(gridEnt); + EntityManager.System().SetEntityName(gridEnt, $"grid", meta); + EntityManager.InitializeComponents(gridEnt, meta); + EntityManager.StartComponents(gridEnt); + // Note that this does not actually map-initialize the grid entity, even if the map its being spawn on has already been initialized. + // I don't know whether that is intentional or not. + + return (gridEnt, grid); + } + + #endregion + #region Chunk helpers [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -323,7 +369,7 @@ private void ApplyChunkData( var gridIndices = deletedChunk.ChunkTileToGridTile((x, y)); var newTileRef = new TileRef(uid, gridIndices, Tile.Empty); - _mapInternal.RaiseOnTileChanged(gridEnt, newTileRef, oldTile, index); + RaiseOnTileChanged(gridEnt, newTileRef, oldTile, index); } } @@ -485,6 +531,20 @@ private void GetFullState(EntityUid uid, MapGridComponent component, ref Compone #endif } + /// + /// Prunes tracked grid chunk deletions older than some given game tick. + /// + public void CullDeletionHistory(GameTick upToTick) + { + var query = AllEntityQuery(); + + while (query.MoveNext(out var grid)) + { + var chunks = grid.ChunkDeletionHistory; + chunks.RemoveAll(t => t.tick < upToTick); + } + } + private void OnGridAdd(EntityUid uid, MapGridComponent component, ComponentAdd args) { var msg = new GridAddEvent(uid); @@ -859,7 +919,7 @@ public void SetTiles(EntityUid uid, MapGridComponent grid, List<(Vector2i GridIn // Suppress sending out events for each tile changed // We're going to send them all out together at the end - MapManager.SuppressOnTileChanged = true; + SuppressOnTileChanged = true; foreach (var (gridIndices, tile) in tiles) { @@ -896,7 +956,7 @@ public void SetTiles(EntityUid uid, MapGridComponent grid, List<(Vector2i GridIn RegenerateCollision(uid, grid, modified); // Back to normal - MapManager.SuppressOnTileChanged = false; + SuppressOnTileChanged = false; } public TilesEnumerator GetLocalTilesEnumerator(EntityUid uid, MapGridComponent grid, Box2 aabb, @@ -1644,10 +1704,10 @@ private void OnTileModified(EntityUid uid, MapGridComponent grid, MapChunk mapCh // The map serializer currently sets tiles of unbound grids as part of the deserialization process // It properly sets SuppressOnTileChanged so that the event isn't spammed for every tile on the grid. // ParentMapId is not able to be accessed on unbound grids, so we can't even call this function for unbound grids. - if (!MapManager.SuppressOnTileChanged) + if (!SuppressOnTileChanged) { var newTileRef = new TileRef(uid, gridTile, newTile); - _mapInternal.RaiseOnTileChanged((uid, grid), newTileRef, oldTile, mapChunk.Indices); + RaiseOnTileChanged((uid, grid), newTileRef, oldTile, mapChunk.Indices); } if (shapeChanged && !mapChunk.SuppressCollisionRegeneration) @@ -1656,6 +1716,18 @@ private void OnTileModified(EntityUid uid, MapGridComponent grid, MapChunk mapCh } } + /// + /// Raises on the provided grid unless is set. + /// + internal void RaiseOnTileChanged(Entity entity, TileRef tileRef, Tile oldTile, Vector2i chunk) + { + if (SuppressOnTileChanged) + return; + + var ev = new TileChangedEvent(entity, tileRef, oldTile, chunk); + EntityManager.EventBus.RaiseLocalEvent(entity.Owner, ref ev, true); + } + /// /// Iterates the local tiles of the specified data. /// @@ -1751,3 +1823,12 @@ public bool MoveNext(out TileRef tile) } } } + +/// +/// Additional parameters used when creating a new grid entity. +/// +/// The number of tiles long/wide the grids chunks should be. +public record struct GridCreateOptions(ushort ChunkSize) +{ + public readonly static GridCreateOptions Default = new(ChunkSize: 16); +} diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs index 2f99eb9c7ae..9564f3b849a 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs @@ -8,6 +8,7 @@ using Robust.Shared.Maths; using Robust.Shared.Network; using Robust.Shared.Physics; +using Robust.Shared.Physics.Collision; using Robust.Shared.Physics.Systems; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -21,8 +22,7 @@ public abstract partial class SharedMapSystem : EntitySystem { [Dependency] private ITileDefinitionManager _tileMan = default!; [Dependency] private IGameTiming _timing = default!; - [Dependency] protected IMapManager MapManager = default!; - [Dependency] private IMapManagerInternal _mapInternal = default!; + [Dependency] private IManifoldManager _manifolds = default!; [Dependency] private INetManager _netManager = default!; [Dependency] private FixtureSystem _fixtures = default!; [Dependency] private SharedPhysicsSystem _physics = default!; @@ -34,9 +34,18 @@ public abstract partial class SharedMapSystem : EntitySystem private EntityQuery _gridQuery; private EntityQuery _metaQuery; private EntityQuery _xformQuery; + [Dependency] EntityQuery _gridTreeQuery; internal Dictionary Maps { get; } = new(); + /// + /// If set, this prevents the from being raised when modifying grids. + /// + /// + /// Useful if you want to create a new grid, delete an existing grid, or bulk-modify tiles and don't want to spam ten billion individual tile-changed events. + /// + internal bool SuppressOnTileChanged { get; set; } + /// /// This hashset is used to try prevent MapId re-use. This is mainly for auto-assigned map ids. /// Loading a map with a specific id (e.g., the various mapping commands) may still result in an id being diff --git a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs index 2fa10c984a6..ed74453afb6 100644 --- a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs +++ b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs @@ -45,7 +45,7 @@ internal void ReAnchor( var oldRot = xform._localRotation; var oldMap = xform.MapUid; xform._localPosition = tilePos + newGrid.TileSizeHalfVector; - xform._localRotation += rotation; + xform._localRotation = NormalizeRotation(xform._localRotation + rotation); var meta = MetaData(uid); SetGridId((uid, xform, meta), newGridUid); @@ -283,7 +283,7 @@ private void OnCompInit(EntityUid uid, TransformComponent component, ComponentIn // Entity may not be directly parented to the grid (e.g., spawned using some relative entity coordinates) // in that case, we attempt to attach to a grid. var pos = new MapCoordinates(GetWorldPosition(component), component.MapID); - if (_mapManager.TryFindGridAt(pos, out var gridUid, out gridComp)) + if (_map.TryFindGridAt(pos, out var gridUid, out gridComp)) grid = (gridUid, gridComp); } @@ -428,7 +428,7 @@ public void SetLocalPositionNoLerp(EntityUid uid, Vector2 value, TransformCompon if (!XformQuery.Resolve(uid, ref xform)) return; -#pragma warning disable CS0618 +#pragma warning disable CS0618 // TODO: move LocalRotation/Position manipulation into TransformSystem (don't piggyback off TransformComponent) xform.LocalPosition = value; #pragma warning restore CS0618 } @@ -437,12 +437,19 @@ public void SetLocalPositionNoLerp(EntityUid uid, Vector2 value, TransformCompon #region Local Rotation + internal static Angle NormalizeRotation(Angle rotation) + { + return rotation.Reduced(); + } + public void SetLocalRotationNoLerp(EntityUid uid, Angle value, TransformComponent? xform = null) { if (!XformQuery.Resolve(uid, ref xform)) return; +#pragma warning disable CS0618 // TODO: move LocalRotation/Position manipulation into TransformSystem (don't piggyback off TransformComponent) xform.LocalRotation = value; +#pragma warning restore CS0618 } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -457,6 +464,27 @@ public void SetLocalRotation(TransformComponent xform, Angle value) #endregion + #region No Local Rotation + + public void SetNoLocalRotation(EntityUid uid, bool value, TransformComponent? xform = null) + { + if (!XformQuery.Resolve(uid, ref xform)) + return; + + SetNoLocalRotation((uid, xform), value); + } + + public void SetNoLocalRotation(Entity entity, bool value) + { + if (value) + SetLocalRotation(entity.Owner, Angle.Zero, entity.Comp); + + entity.Comp._noLocalRotation = value; + Dirty(entity); + } + + #endregion + #region Coordinates public void SetCoordinates(EntityUid uid, EntityCoordinates value) @@ -519,7 +547,7 @@ public void SetCoordinates( xform._localPosition = value.Position; if (rotation != null && !xform.NoLocalRotation) - xform._localRotation = rotation.Value; + xform._localRotation = NormalizeRotation(rotation.Value); DebugTools.Assert(!xform.NoLocalRotation || xform.LocalRotation == 0); @@ -621,7 +649,7 @@ public void SetCoordinates( { // preserve world rotation if (rotation == null && oldParent != null && newParent != null && !xform.NoLocalRotation) - xform._localRotation += GetWorldRotation(oldParent) - GetWorldRotation(newParent); + xform._localRotation = NormalizeRotation(xform._localRotation + GetWorldRotation(oldParent) - GetWorldRotation(newParent)); DebugTools.Assert(!xform.NoLocalRotation || xform.LocalRotation == 0); } @@ -861,7 +889,9 @@ internal void OnHandleState(EntityUid uid, TransformComponent xform, ref Compone } else { +#pragma warning disable CS0618 // AnchorEntity/Unanchored can't be used from uninitialized states. xform.Anchored = newState.Anchored; +#pragma warning restore CS0618 } if (oldAnchored != newState.Anchored && xform.Initialized) @@ -990,7 +1020,7 @@ public void SetMapCoordinates(Entity entity, MapCoordinates { var mapUid = _map.GetMap(coordinates.MapId); if (!_gridQuery.HasComponent(entity) && - _mapManager.TryFindGridAt(mapUid, coordinates.Position, out var targetGrid, out _)) + _map.TryFindGridAt(mapUid, coordinates.Position, out var targetGrid, out _)) { var invWorldMatrix = GetInvWorldMatrix(targetGrid); SetCoordinates((entity.Owner, entity.Comp, MetaData(entity.Owner)), new EntityCoordinates(targetGrid, Vector2.Transform(coordinates.Position, invWorldMatrix))); @@ -1233,7 +1263,7 @@ private void SetWorldPositionRotationInternal(EntityUid uid, Vector2 worldPos, A return; } - if (component.GridUid != uid && _mapManager.TryFindGridAt(component.MapUid.Value, worldPos, out var targetGrid, out _)) + if (component.GridUid != uid && _map.TryFindGridAt(component.MapUid.Value, worldPos, out var targetGrid, out _)) { var targetGridXform = XformQuery.GetComponent(targetGrid); var invLocalMatrix = targetGridXform.InvLocalMatrix; @@ -1277,7 +1307,7 @@ public virtual void SetLocalPositionRotation(EntityUid uid, Vector2 pos, Angle r xform._localPosition = pos; if (!xform.NoLocalRotation) - xform._localRotation = rot; + xform._localRotation = NormalizeRotation(rot); DebugTools.Assert(!xform.NoLocalRotation || xform.LocalRotation == 0); @@ -1487,7 +1517,7 @@ public bool TryGetMapOrGridCoordinates( return false; var oldPos = GetWorldPosition(xform); - if (_mapManager.TryFindGridAt(map, oldPos, out var gridUid, out _) && !TerminatingOrDeleted(gridUid)) + if (_map.TryFindGridAt(map, oldPos, out var gridUid, out _) && !TerminatingOrDeleted(gridUid)) { coordinates = gridUid == xform.ParentUid ? new EntityCoordinates(gridUid, xform.LocalPosition) @@ -1747,7 +1777,7 @@ public bool SwapPositions(Entity entity1, Entity entity1, Entity entA, Entity + /// Returns a readable string with the entity's local and world position and rotation. Useful for debugging. + /// + public string GetDebugString(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, logMissing: false)) + return "invalid"; + + return $"pos/rot/wpos/wrot: {ent.Comp.Coordinates}/{ent.Comp.LocalRotation}/{GetWorldPosition(ent.Comp)}/{GetWorldRotation(ent.Comp)}"; + } } diff --git a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.cs b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.cs index 3d9b8a545c1..730c8f25660 100644 --- a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.cs @@ -19,7 +19,6 @@ namespace Robust.Shared.GameObjects public abstract partial class SharedTransformSystem : EntitySystem { [Dependency] private IGameTiming _gameTiming = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private EntityLookupSystem _lookup = default!; [Dependency] private SharedMapSystem _map = default!; [Dependency] private MetaDataSystem _metaData = default!; diff --git a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs index 21c1492d5e0..e7a1492ad8c 100644 --- a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs @@ -10,7 +10,6 @@ using Robust.Shared.IoC; using Robust.Shared.Network; using Robust.Shared.Player; -using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Threading; using Robust.Shared.Timing; @@ -31,7 +30,6 @@ public abstract partial class SharedUserInterfaceSystem : EntitySystem [Dependency] private IGameTiming _timing = default!; [Dependency] private INetManager _netManager = default!; [Dependency] private IParallelManager _parallel = default!; - [Dependency] protected IPrototypeManager ProtoManager = default!; [Dependency] private IReflectionManager _reflection = default!; [Dependency] protected ISharedPlayerManager Player = default!; [Dependency] private SharedTransformSystem _transforms = default!; @@ -43,6 +41,17 @@ public abstract partial class SharedUserInterfaceSystem : EntitySystem private ActorRangeCheckJob _rangeJob; + /// Update type to apply + private enum QueuedUpdate + { + /// Upen a UI + Open, + /// Apply new state to the UI + ApplyState, + /// Close the UI + Close + }; + #region Starlight private static readonly Histogram UpdateHistogram = Metrics.CreateHistogram( "robust_bui_update_usage", @@ -91,7 +100,7 @@ public abstract partial class SharedUserInterfaceSystem : EntitySystem /// /// Defer BUIs during state handling so client doesn't spam a BUI constantly during prediction. /// - private readonly List<(BoundUserInterface Bui, bool value)> _queuedBuis = new(); + private readonly List<(BoundUserInterface bui, QueuedUpdate updateType)> _queuedBuis = new(); public override void Initialize() { @@ -142,9 +151,9 @@ public override void Initialize() SubscribeLocalEvent(OnActorShutdown); } - private void AddQueued(BoundUserInterface bui, bool value) + private void AddQueued(BoundUserInterface bui, QueuedUpdate type) { - _queuedBuis.Add((bui, value)); + _queuedBuis.Add((bui, type)); } /// @@ -167,6 +176,9 @@ private void OnMessageReceived(BoundUIWrapMessage msg, EntityUid sender) return; } + var received = new BoundUserInterfaceMessageReceivedEvent(sender, uid, msg.UiKey); + RaiseLocalEvent(ref received); + // If it's not an open message check we're even a subscriber. if (msg.Message is not OpenBoundInterfaceMessage && (!uiComp.Actors.TryGetValue(msg.UiKey, out var actors) || @@ -298,7 +310,7 @@ private void CloseUiInternal(Entity ent, Enum key, Enti if (ent.Comp.ClientOpenInterfaces.TryGetValue(key, out var cBui)) { - AddQueued(cBui, false); + AddQueued(cBui, QueuedUpdate.Close); } if (ent.Comp.Actors.Count == 0) @@ -340,7 +352,7 @@ private void OnUserInterfaceStartup(Entity ent, ref Comp // PlayerAttachedEvent will catch some of these. foreach (var (key, bui) in ent.Comp.ClientOpenInterfaces) { - AddQueued(bui, true); + AddQueued(bui, QueuedUpdate.Open); } } @@ -360,37 +372,36 @@ protected void OnUserInterfaceShutdown(Entity ent, ref C DebugTools.Assert(!ent.Comp.Actors.ContainsKey(key)); } - DebugTools.Assert(ent.Comp.ClientOpenInterfaces.Values.All(x => _queuedBuis.Contains((x, false)))); + DebugTools.Assert(ent.Comp.ClientOpenInterfaces.Values.All(x => _queuedBuis.Contains((x, QueuedUpdate.Close)))); } private void OnUserInterfaceGetState(Entity ent, ref ComponentGetState args) { - if (args.FromTick > ent.Comp.CreationTick && ent.Comp.LastFieldUpdate >= args.FromTick) - { - var fields = EntityManager.GetModifiedFields(ent.Comp, args.FromTick); + var aspects = EntityManager.GetModifiedAspects(ent.Comp, args.FromTick); - switch (fields) + switch (aspects) + { + case >= DeltaAspect.Unclassified: + break; + case 1 << 0: { - case 1 << 0: - { - var state = new UserInterfaceActorsDeltaState(); - AddActors(ent, state.Actors, ref args); + var state = new UserInterfaceActorsDeltaState(); + AddActors(ent, state.Actors, ref args); - args.State = state; - return; - } - case 1 << 2: - { - var states = ent.Comp.States; + args.State = state; + return; + } + case 1 << 2: + { + var states = ent.Comp.States; - // TODO Game State - // Force the client to serialize & de-serialize implicitly generated component states. - if (_netManager.IsClient) - states = new(states); + // TODO Game State + // Force the client to serialize & de-serialize implicitly generated component states. + if (_netManager.IsClient) + states = new(states); - args.State = new UserInterfaceStatesDeltaState {States = states}; - return; - } + args.State = new UserInterfaceStatesDeltaState {States = states}; + return; } } @@ -522,7 +533,7 @@ private void OnUserInterfaceHandleState(Entity ent, ref } var bui = ent.Comp.ClientOpenInterfaces[key]; - AddQueued(bui, false); + AddQueued(bui, QueuedUpdate.Close); } } @@ -549,9 +560,7 @@ private void OnUserInterfaceHandleState(Entity ent, ref if (!ent.Comp.ClientOpenInterfaces.TryGetValue(key, out var cBui) || !cBui.IsOpened) continue; - cBui.State = buiState; - cBui.UpdateState(buiState); - cBui.Update(); + AddQueued(cBui, QueuedUpdate.ApplyState); } } @@ -579,7 +588,7 @@ private void EnsureClientBui(Entity entity, Enum key, In // Existing BUI just keep it. if (entity.Comp.ClientOpenInterfaces.TryGetValue(key, out var existing)) { - _queuedBuis.Remove((existing, false)); + _queuedBuis.Remove((existing, QueuedUpdate.Close)); return; } @@ -604,7 +613,7 @@ private void EnsureClientBui(Entity entity, Enum key, In if (!open) return; - AddQueued(boundUserInterface, true); + AddQueued(boundUserInterface, QueuedUpdate.Open); } /// @@ -1173,15 +1182,18 @@ public override void Update(float frameTime) { if (_timing.IsFirstTimePredicted) { - foreach (var (bui, open) in _queuedBuis) + foreach (var (bui, updateType) in _queuedBuis) { - if (open) + if (updateType == QueuedUpdate.Open || updateType == QueuedUpdate.ApplyState) { #if EXCEPTION_TOLERANCE try { #endif - bui.Open(); + if (updateType == QueuedUpdate.Open) + { + bui.Open(); + } if (UIQuery.TryComp(bui.Owner, out var uiComp)) { diff --git a/Robust.Shared/Graphics/Eye.cs b/Robust.Shared/Graphics/Eye.cs index 461fbcaecc5..d41e07fc176 100644 --- a/Robust.Shared/Graphics/Eye.cs +++ b/Robust.Shared/Graphics/Eye.cs @@ -16,7 +16,7 @@ public class Eye : IEye private MapCoordinates _coords; /// - [ViewVariables(VVAccess.ReadWrite)] + [ViewVariables] public bool DrawFov { get; set; } = true; /// diff --git a/Robust.Shared/Localization/LocalizationManager.Functions.cs b/Robust.Shared/Localization/LocalizationManager.Functions.cs index 82e3f292bb0..354db76939f 100644 --- a/Robust.Shared/Localization/LocalizationManager.Functions.cs +++ b/Robust.Shared/Localization/LocalizationManager.Functions.cs @@ -9,6 +9,7 @@ using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Components.Localization; using Robust.Shared.Maths; +using Robust.Shared.Utility; namespace Robust.Shared.Localization { @@ -42,6 +43,10 @@ private void AddBuiltInFunctions(FluentBundle bundle) AddCtxFunction(bundle, "ATTRIB", args => FuncAttrib(bundle, args)); AddCtxFunction(bundle, "CAPITALIZE", FuncCapitalize); AddCtxFunction(bundle, "INDEFINITE", FuncIndefinite); + + // Rich text + AddCtxFunction(bundle, "ESCAPE", FuncEscape); + AddCtxFunction(bundle, "ESCAPE-PARAM", FuncEscapeParam); } /// @@ -370,6 +375,24 @@ public void AddFunction(CultureInfo culture, string name, LocFunction function) bundle.AddFunctionOverriding(name, (args, options) => CallFunction(function, bundle, args, options)); } + + /// + /// Escape the provided string argument for insertion among rich text markup. + /// + private static ILocValue FuncEscape(LocArgs args) + { + var input = args.Args[0].Format(new LocContext()); + return new LocValueString(FormattedMessage.EscapeText(input)); + } + + /// + /// Escape the provided string argument for insertion as a string markup parameter. + /// + private static ILocValue FuncEscapeParam(LocArgs args) + { + var input = args.Args[0].Format(new LocContext()); + return new LocValueString(FormattedMessage.EscapeStringParameter(input)); + } } internal sealed class FluentLocWrapperType : IFluentType diff --git a/Robust.Shared/Localization/LocalizationManager.cs b/Robust.Shared/Localization/LocalizationManager.cs index b7d3ea8f817..f3e17c726ac 100644 --- a/Robust.Shared/Localization/LocalizationManager.cs +++ b/Robust.Shared/Localization/LocalizationManager.cs @@ -27,6 +27,7 @@ namespace Robust.Shared.Localization internal abstract partial class LocalizationManager : ILocalizationManagerInternal { protected static readonly ResPath LocaleDirPath = new("/Locale"); + protected static readonly ResPath UploadedDirPath = new("/Uploaded"); [Dependency] private IConfigurationManager _configuration = default!; [Dependency] private IResourceManager _res = default!; @@ -403,6 +404,20 @@ public List GetFoundCultures() result.Add(CultureInfo.GetCultureInfo(cultureName, predefinedOnly: false)); } + var uploadedFiles = _res.ContentFindFiles(UploadedDirPath) + .Where(c => c.Filename.EndsWith(".ftl", StringComparison.InvariantCultureIgnoreCase)); + + foreach (var file in uploadedFiles) + { + var cultureName = Path.GetFileNameWithoutExtension(file.Filename); + if (CultureInfo.GetCultures(CultureTypes.AllCultures).Any(c => c.Name.Equals(cultureName, StringComparison.InvariantCultureIgnoreCase))) + { + var culture = CultureInfo.GetCultureInfo(cultureName, predefinedOnly: false); + if (!result.Contains(culture)) + result.Add(culture); + } + } + return result; } @@ -485,10 +500,13 @@ private void _initData(IResourceManager resourceManager, CultureInfo culture, Fl { // Load data from .ftl files. // Data is loaded from /Locale//* + // and from /Uploaded/**/Locale//* var root = LocaleDirPath / culture.Name; var files = resourceManager.ContentFindFiles(root) + .Concat(resourceManager.ContentFindFiles(UploadedDirPath) + .Where(c => c.CanonPath.Contains($"/Locale/{culture.Name}/", StringComparison.InvariantCultureIgnoreCase))) .Where(c => c.Filename.EndsWith(".ftl", StringComparison.InvariantCultureIgnoreCase)) .ToArray(); diff --git a/Robust.Shared/Log/LogManager.Sawmill.cs b/Robust.Shared/Log/LogManager.Sawmill.cs index 60ec8f92a86..c25e1189b52 100644 --- a/Robust.Shared/Log/LogManager.Sawmill.cs +++ b/Robust.Shared/Log/LogManager.Sawmill.cs @@ -78,19 +78,22 @@ public bool IsLogLevelEnabled(LogLevel level) public void Log(LogLevel level, Exception? exception, string message, params object?[] args) { + if (!IsLogLevelEnabled(level)) + return; + if (!_sLogger.BindMessageTemplate(message, args, out var parsedTemplate, out var properties)) return; var msg = new LogEvent(DateTimeOffset.Now, level.ToSerilog(), exception, parsedTemplate, properties); - if (!IsLogLevelEnabled(level)) - return; - LogInternal(Name, msg); } public void Log(LogLevel level, string message, params object?[] args) { + if (!IsLogLevelEnabled(level)) + return; + if (args.Length != 0 && message.Contains("{0")) { // Fallback for logs that still use the string.Format approach. diff --git a/Robust.Shared/Map/Commands/MapPausingCommands.cs b/Robust.Shared/Map/Commands/MapPausingCommands.cs new file mode 100644 index 00000000000..2b74ac3c15a --- /dev/null +++ b/Robust.Shared/Map/Commands/MapPausingCommands.cs @@ -0,0 +1,90 @@ +using System.Globalization; +using Robust.Shared.Console; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; + +namespace Robust.Shared.Map.Commands; + +/// +/// Pauses a given map, halting all entity processing on it. +/// +public sealed partial class PauseMapCommand : LocalizedEntityCommands +{ + [Dependency] SharedMapSystem _mapSystem = default!; + public override string Command => "pausemap"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length != 1) + { + shell.WriteError("Need to supply a valid MapId"); + return; + } + + var mapId = new MapId(int.Parse(args[0], CultureInfo.InvariantCulture)); + + if (!_mapSystem.MapExists(mapId)) + { + shell.WriteError("That map does not exist."); + return; + } + + _mapSystem.SetPaused(mapId, true); + } +} + +/// +/// Unpauses a given map, resuming all entity processing on it. +/// +public sealed partial class UnpauseMapCommand : LocalizedEntityCommands +{ + [Dependency] SharedMapSystem _mapSystem = default!; + public override string Command => "unpausemap"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length != 1) + { + shell.WriteError("Need to supply a valid MapId"); + return; + } + + var mapId = new MapId(int.Parse(args[0], CultureInfo.InvariantCulture)); + + if (!_mapSystem.MapExists(mapId)) + { + shell.WriteError("That map does not exist."); + return; + } + + _mapSystem.SetPaused(mapId, false); + } +} + +/// +/// Checks whether a given map is currently paused. +/// +public sealed partial class QueryMapPausedCommand : LocalizedEntityCommands +{ + [Dependency] SharedMapSystem _mapSystem = default!; + public override string Command => "querymappaused"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length != 1) + { + shell.WriteError("Need to supply a valid MapId"); + return; + } + + var mapId = new MapId(int.Parse(args[0], CultureInfo.InvariantCulture)); + + if (!_mapSystem.MapExists(mapId)) + { + shell.WriteError("That map does not exist."); + return; + } + + shell.WriteLine(_mapSystem.IsPaused(mapId).ToString()); + } +} diff --git a/Robust.Shared/Map/Components/MapComponent.cs b/Robust.Shared/Map/Components/MapComponent.cs index 3633ad41cbd..5fae7461a3a 100644 --- a/Robust.Shared/Map/Components/MapComponent.cs +++ b/Robust.Shared/Map/Components/MapComponent.cs @@ -18,10 +18,10 @@ public sealed partial class MapComponent : Component [ViewVariables(VVAccess.ReadOnly), Access(typeof(SharedMapSystem), Other = AccessPermissions.ReadExecute)] public MapId MapId { get; internal set; } = MapId.Nullspace; - [DataField, Access(typeof(SharedMapSystem), typeof(MapManager))] + [DataField, Access(typeof(SharedMapSystem))] public bool MapPaused; - [DataField, Access(typeof(SharedMapSystem), typeof(MapManager))] + [DataField, Access(typeof(SharedMapSystem))] public bool MapInitialized; } diff --git a/Robust.Shared/Map/CoordinatesExtensions.cs b/Robust.Shared/Map/CoordinatesExtensions.cs index 120afdf691b..38c8560efa7 100644 --- a/Robust.Shared/Map/CoordinatesExtensions.cs +++ b/Robust.Shared/Map/CoordinatesExtensions.cs @@ -8,9 +8,9 @@ namespace Robust.Shared.Map { public static class CoordinatesExtensions { - public static EntityCoordinates AlignWithClosestGridTile(this EntityCoordinates coords, float searchBoxSize = 1.5f, IEntityManager? entityManager = null, IMapManager? mapManager = null) + public static EntityCoordinates AlignWithClosestGridTile(this EntityCoordinates coords, float searchBoxSize = 1.5f, IEntityManager? entityManager = null) { - IoCManager.Resolve(ref entityManager, ref mapManager); + IoCManager.Resolve(ref entityManager); var xform = entityManager.System(); var gridId = xform.GetGrid(coords); @@ -23,7 +23,7 @@ public static EntityCoordinates AlignWithClosestGridTile(this EntityCoordinates var mapCoords = xform.ToMapCoordinates(coords); - if (mapManager.TryFindGridAt(mapCoords, out var gridUid, out mapGrid)) + if (mapSystem.TryFindGridAt(mapCoords, out var gridUid, out mapGrid)) { return mapSystem.GridTileToLocal(gridUid, mapGrid, mapSystem.CoordinatesToTile(gridUid, mapGrid, coords)); } @@ -34,7 +34,7 @@ public static EntityCoordinates AlignWithClosestGridTile(this EntityCoordinates // find grids in search box var gridsInArea = new List>(); - mapManager.FindGridsIntersecting(mapCoords.MapId, gridSearchBox, ref gridsInArea); + mapSystem.FindGridsIntersecting(mapCoords.MapId, gridSearchBox, ref gridsInArea); // find closest grid intersecting our search box. gridUid = EntityUid.Invalid; diff --git a/Robust.Shared/Map/EntityCoordinates.cs b/Robust.Shared/Map/EntityCoordinates.cs index 323fe187cbf..4eaae6b7cea 100644 --- a/Robust.Shared/Map/EntityCoordinates.cs +++ b/Robust.Shared/Map/EntityCoordinates.cs @@ -95,18 +95,11 @@ public static EntityCoordinates FromMap(EntityUid entity, MapCoordinates coordin return transformSystem.ToCoordinates(entity, coordinates); } - [Obsolete("Use SharedTransformSystem.ToCoordinates()")] - public static EntityCoordinates FromMap(IMapManager mapManager, MapCoordinates coordinates) - { - return IoCManager.Resolve().System().ToCoordinates(coordinates); - } - /// /// Converts this set of coordinates to Vector2i. /// public Vector2i ToVector2i( IEntityManager entityManager, - IMapManager mapManager, SharedTransformSystem transformSystem) { if(!IsValid(entityManager)) diff --git a/Robust.Shared/Map/GridEventHandler.cs b/Robust.Shared/Map/GridEventHandler.cs deleted file mode 100644 index d0f6f374755..00000000000 --- a/Robust.Shared/Map/GridEventHandler.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Robust.Shared.GameObjects; - -namespace Robust.Shared.Map -{ - /// - /// Invoked when a grid is altered. - /// - /// Passed to the delegate given it may no longer be retrievable. - /// The index of the grid being changed. - public delegate void GridEventHandler(MapId mapId, EntityUid gridId); -} diff --git a/Robust.Shared/Map/IMapManager.cs b/Robust.Shared/Map/IMapManager.cs deleted file mode 100644 index d0aaea4a5a4..00000000000 --- a/Robust.Shared/Map/IMapManager.cs +++ /dev/null @@ -1,246 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Numerics; -using JetBrains.Annotations; -using Robust.Shared.GameObjects; -using Robust.Shared.Map.Components; -using Robust.Shared.Maths; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Collision.Shapes; - -namespace Robust.Shared.Map -{ - public delegate bool GridCallback(EntityUid uid, MapGridComponent grid); - - public delegate bool GridCallback(EntityUid uid, MapGridComponent grid, ref TState state); - - /// - /// This manages all the grids and maps in the world. Largely superseded by . - /// - [NotContentImplementable] - public interface IMapManager - { - public const bool Approximate = false; - public const bool IncludeMap = true; - - /// - /// Should the OnTileChanged event be suppressed? This is useful for initially loading the map - /// so that you don't spam an event for each of the million station tiles. - /// - bool SuppressOnTileChanged { get; set; } - - /// - /// Starts up the map system. - /// - void Initialize(); - - void Shutdown(); - void Startup(); - - void Restart(); - - [Obsolete("Use MapSystem")] - MapId CreateMap(MapId? mapId = null); - - /// - /// Check whether a map with specified ID exists. - /// - /// The map ID to check existence of. - /// True if the map exists, false otherwise. - [Obsolete("Use MapSystem")] - bool MapExists([NotNullWhen(true)] MapId? mapId); - - /// - /// Returns the map entity ID for a given map, or an invalid entity Id if the map does not exist. - /// - [Obsolete("Use MapSystem")] - EntityUid GetMapEntityId(MapId mapId); - - /// - /// Replaces GetMapEntity()'s throw-on-failure semantics. - /// - [Obsolete("Use MapSystem")] - EntityUid GetMapEntityIdOrThrow(MapId mapId); - - [Obsolete("Use MapSystem")] - IEnumerable GetAllMapIds(); - - [Obsolete("Use MapSystem")] - void DeleteMap(MapId mapId); - - // ReSharper disable once MethodOverloadWithOptionalParameter - MapGridComponent CreateGrid(MapId currentMapId, ushort chunkSize = 16); - MapGridComponent CreateGrid(MapId currentMapId, in GridCreateOptions options); - MapGridComponent CreateGrid(MapId currentMapId); - Entity CreateGridEntity(MapId currentMapId, GridCreateOptions? options = null); - Entity CreateGridEntity(EntityUid map, GridCreateOptions? options = null); - - IEnumerable GetAllMapGrids(MapId mapId); - - IEnumerable> GetAllGrids(MapId mapId); - - #region MapId - - public void FindGridsIntersecting(MapId mapId, T shape, Transform transform, - ref List> grids, bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape; - - public void FindGridsIntersecting(MapId mapId, T shape, Transform transform, GridCallback callback, - bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape; - - public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, GridCallback callback, bool approx = Approximate, - bool includeMap = IncludeMap); - - public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, ref TState state, - GridCallback callback, bool approx = Approximate, bool includeMap = IncludeMap); - - public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, ref List> grids, - bool approx = Approximate, bool includeMap = IncludeMap); - - public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, GridCallback callback, - bool approx = Approximate, - bool includeMap = IncludeMap); - - public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, ref TState state, - GridCallback callback, - bool approx = Approximate, bool includeMap = IncludeMap); - - public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, ref List> grids, - bool approx = Approximate, bool includeMap = IncludeMap); - - #endregion - - #region MapEnt - - public void FindGridsIntersecting(EntityUid mapEnt, T shape, Transform transform, GridCallback callback, - bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape; - - public void FindGridsIntersecting(EntityUid mapEnt, T shape, Transform transform, - ref TState state, GridCallback callback, bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape; - - /// - /// Returns true if any grids overlap the specified shapes. - /// - public void FindGridsIntersecting(EntityUid mapEnt, List shapes, Transform transform, - ref List> entities, bool approx = Approximate, bool includeMap = IncludeMap); - - public void FindGridsIntersecting(EntityUid mapEnt, T shape, Transform transform, - ref List> grids, bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape; - - public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, GridCallback callback, - bool approx = Approximate, bool includeMap = IncludeMap); - - public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, ref TState state, - GridCallback callback, bool approx = Approximate, bool includeMap = IncludeMap); - - public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, ref List> grids, - bool approx = Approximate, bool includeMap = IncludeMap); - - public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, GridCallback callback, - bool approx = Approximate, - bool includeMap = IncludeMap); - - public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, ref TState state, - GridCallback callback, - bool approx = Approximate, bool includeMap = IncludeMap); - - public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, - ref List> grids, - bool approx = Approximate, bool includeMap = IncludeMap); - - #endregion - - #region TryFindGridAt - - public bool TryFindGridAt( - EntityUid mapEnt, - Vector2 worldPos, - out EntityUid uid, - [NotNullWhen(true)] out MapGridComponent? grid); - - /// - /// Attempts to find the map grid under the map location. - /// - public bool TryFindGridAt(MapId mapId, Vector2 worldPos, out EntityUid uid, - [NotNullWhen(true)] out MapGridComponent? grid); - - /// - /// Attempts to find the map grid under the map location. - /// - public bool TryFindGridAt(MapCoordinates mapCoordinates, out EntityUid uid, - [NotNullWhen(true)] out MapGridComponent? grid); - - #endregion - - #region Obsolete - - [Obsolete] - public bool TryFindGridAt(MapId mapId, Vector2 worldPos, EntityQuery query, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid) - { - return TryFindGridAt(mapId, worldPos, out uid, out grid); - } - - [Obsolete] - public IEnumerable FindGridsIntersecting(MapId mapId, Box2 worldAabb, bool approx = false, bool includeMap = true) - { - var grids = new List>(); - FindGridsIntersecting(mapId, worldAabb, ref grids, approx, includeMap); - - foreach (var grid in grids) - { - yield return grid.Comp; - } - } - - [Obsolete] - public IEnumerable FindGridsIntersecting(MapId mapId, Box2Rotated worldArea, bool approx = false, bool includeMap = true) - { - var grids = new List>(); - FindGridsIntersecting(mapId, worldArea, ref grids, approx, includeMap); - - foreach (var grid in grids) - { - yield return grid.Comp; - } - } - - #endregion - - - [Obsolete("Just delete the grid entity")] - void DeleteGrid(EntityUid euid); - - [Obsolete("Use HasComp")] - bool IsGrid(EntityUid uid); - - [Obsolete("Use HasComp")] - bool IsMap(EntityUid uid); - - // - // Pausing functions - // - - [Obsolete("Use MapSystem")] - void SetMapPaused(MapId mapId, bool paused); - - [Obsolete("Use MapSystem")] - void DoMapInitialize(MapId mapId); - - [Obsolete("Use MapSystem")] - bool IsMapPaused(MapId mapId); - - [Obsolete("Use MapSystem")] - bool IsMapInitialized(MapId mapId); - - } - - public struct GridCreateOptions - { - public static readonly GridCreateOptions Default = new() - { - ChunkSize = 16 - }; - - public ushort ChunkSize; - } -} diff --git a/Robust.Shared/Map/IMapManagerInternal.cs b/Robust.Shared/Map/IMapManagerInternal.cs deleted file mode 100644 index e6a5b5b3637..00000000000 --- a/Robust.Shared/Map/IMapManagerInternal.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Robust.Shared.GameObjects; -using Robust.Shared.Map.Components; -using Robust.Shared.Maths; - -namespace Robust.Shared.Map -{ - /// - internal interface IMapManagerInternal : IMapManager - { - /// - /// Raises the OnTileChanged event. - /// - /// A reference to the new tile. - /// The old tile that got replaced. - void RaiseOnTileChanged(Entity entity, TileRef tileRef, Tile oldTile, Vector2i chunk); - } -} diff --git a/Robust.Shared/Map/MapId.cs b/Robust.Shared/Map/MapId.cs index 4045791de7c..e8848ee8f36 100644 --- a/Robust.Shared/Map/MapId.cs +++ b/Robust.Shared/Map/MapId.cs @@ -11,7 +11,6 @@ namespace Robust.Shared.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 diff --git a/Robust.Shared/Map/MapManager.GridCollection.cs b/Robust.Shared/Map/MapManager.GridCollection.cs deleted file mode 100644 index cf23be33c1c..00000000000 --- a/Robust.Shared/Map/MapManager.GridCollection.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Robust.Shared.GameObjects; -using Robust.Shared.Map.Components; -using Robust.Shared.Maths; -using Robust.Shared.Utility; - -// All the obsolete warnings about GridId are probably useless here. -#pragma warning disable CS0618 - -namespace Robust.Shared.Map; -internal partial class MapManager -{ - // ReSharper disable once MethodOverloadWithOptionalParameter - public MapGridComponent CreateGrid(MapId currentMapId, ushort chunkSize = 16) - { - return CreateGrid(GetMapEntityIdOrThrow(currentMapId), chunkSize, default); - } - - public MapGridComponent CreateGrid(MapId currentMapId, in GridCreateOptions options) - { - return CreateGrid(GetMapEntityIdOrThrow(currentMapId), options.ChunkSize, default); - } - - public MapGridComponent CreateGrid(MapId currentMapId) - { - return CreateGrid(currentMapId, GridCreateOptions.Default); - } - - public Entity CreateGridEntity(MapId currentMapId, GridCreateOptions? options = null) - { - return CreateGridEntity(GetMapEntityIdOrThrow(currentMapId), options); - } - - public Entity CreateGridEntity(EntityUid map, GridCreateOptions? options = null) - { - options ??= GridCreateOptions.Default; - return CreateGrid(map, options.Value.ChunkSize, default); - } - - [Obsolete("Use HasComponent(uid)")] - public bool IsGrid(EntityUid uid) - { - return EntityManager.HasComponent(uid); - } - - public IEnumerable GetAllMapGrids(MapId mapId) - { - var query = EntityManager.AllEntityQueryEnumerator(); - while (query.MoveNext(out var grid, out var xform)) - { - if (xform.MapID == mapId) - yield return grid; - } - } - - public IEnumerable> GetAllGrids(MapId mapId) - { - var query = EntityManager.AllEntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var grid, out var xform)) - { - if (xform.MapID != mapId) - continue; - - yield return (uid, grid); - } - } - - public virtual void DeleteGrid(EntityUid euid) - { - // Possible the grid was already deleted / is invalid - if (!EntityManager.TryGetComponent(euid, out var iGrid)) - { - DebugTools.Assert($"Calling {nameof(DeleteGrid)} with unknown uid {euid}."); - return; // Silently fail on release - } - - if (!EntityManager.TryGetComponent(euid, out MetaDataComponent? metaComp)) - { - DebugTools.Assert($"Calling {nameof(DeleteGrid)} with {euid}, but there was no allocated entity."); - return; // Silently fail on release - } - - // DeleteGrid may be triggered by the entity being deleted, - // so make sure that's not the case. - if (metaComp.EntityLifeStage < EntityLifeStage.Terminating) - EntityManager.DeleteEntity(euid); - } - - /// - public bool SuppressOnTileChanged { get; set; } - - /// - /// Raises the OnTileChanged event. - /// - /// A reference to the new tile. - /// The old tile that got replaced. - void IMapManagerInternal.RaiseOnTileChanged(Entity entity, TileRef tileRef, Tile oldTile, Vector2i chunk) - { - if (SuppressOnTileChanged) - return; - - var ev = new TileChangedEvent(entity, tileRef, oldTile, chunk); - EntityManager.EventBus.RaiseLocalEvent(entity.Owner, ref ev, true); - } - - protected Entity CreateGrid(EntityUid map, ushort chunkSize, EntityUid forcedGridEuid) - { - var gridEnt = EntityManager.CreateEntityUninitialized(null, forcedGridEuid); - - var grid = EntityManager.AddComponent(gridEnt); - grid.ChunkSize = chunkSize; - - _sawmill.Debug($"Binding new grid {gridEnt}"); - - //TODO: This is a hack to get TransformComponent.MapId working before entity states - //are applied. After they are applied the parent may be different, but the MapId will - //be the same. This causes TransformComponent.ParentUid of a grid to be unsafe to - //use in transform states anytime before the state parent is properly set. - EntityManager.GetComponent(gridEnt).AttachParent(map); - - var meta = EntityManager.GetComponent(gridEnt); - EntityManager.System().SetEntityName(gridEnt, $"grid", meta); - EntityManager.InitializeComponents(gridEnt, meta); - EntityManager.StartComponents(gridEnt); - // Note that this does not actually map-initialize the grid entity, even if the map its being spawn on has already been initialized. - // I don't know whether that is intentional or not. - - return (gridEnt, grid); - } -} diff --git a/Robust.Shared/Map/MapManager.MapCollection.cs b/Robust.Shared/Map/MapManager.MapCollection.cs deleted file mode 100644 index e8a5f44e181..00000000000 --- a/Robust.Shared/Map/MapManager.MapCollection.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Robust.Shared.GameObjects; -using Robust.Shared.Map.Components; - -namespace Robust.Shared.Map; - -/// -/// Arguments for when a map is created or deleted locally ore remotely. -/// -public sealed class MapEventArgs : EventArgs -{ - /// - /// Creates a new instance of this class. - /// - public MapEventArgs(MapId map) - { - Map = map; - } - - /// - /// Map that is being modified. - /// - public MapId Map { get; } -} - -internal partial class MapManager -{ - /// - public virtual void DeleteMap(MapId mapId) - { - _mapSystem.DeleteMap(mapId); - } - - /// - public MapId CreateMap(MapId? mapId = null) - { - if (mapId != null) - { - _mapSystem.CreateMap(mapId.Value); - return mapId.Value; - } - - _mapSystem.CreateMap(out var map); - return map; - } - - /// - public bool MapExists([NotNullWhen(true)] MapId? mapId) - { - return _mapSystem.MapExists(mapId); - } - - /// - public EntityUid GetMapEntityId(MapId mapId) - { - return _mapSystem.GetMapOrInvalid(mapId); - } - - /// - /// Replaces GetMapEntity()'s throw-on-failure semantics. - /// - public EntityUid GetMapEntityIdOrThrow(MapId mapId) - { - return _mapSystem.GetMap(mapId); - } - - public bool TryGetMap([NotNullWhen(true)] MapId? mapId, [NotNullWhen(true)] out EntityUid? uid) - { - return _mapSystem.TryGetMap(mapId, out uid); - } - - /// - public IEnumerable GetAllMapIds() - { - return _mapSystem.GetAllMapIds(); - } - - /// - public bool IsMap(EntityUid uid) - { - return EntityManager.HasComponent(uid); - } -} diff --git a/Robust.Shared/Map/MapManager.Pause.cs b/Robust.Shared/Map/MapManager.Pause.cs deleted file mode 100644 index d4fb8f58976..00000000000 --- a/Robust.Shared/Map/MapManager.Pause.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System.Globalization; -using Robust.Shared.GameObjects; - -namespace Robust.Shared.Map -{ - internal partial class MapManager - { - public void SetMapPaused(MapId mapId, bool paused) - { - _mapSystem.SetPaused(mapId, paused); - } - - public void SetMapPaused(EntityUid uid, bool paused) - { - _mapSystem.SetPaused(uid, paused); - } - - public void DoMapInitialize(MapId mapId) - { - _mapSystem.InitializeMap(mapId); - } - - public bool IsMapInitialized(MapId mapId) - { - return _mapSystem.IsInitialized(mapId); - } - - /// - public bool IsMapPaused(MapId mapId) - { - return _mapSystem.IsPaused(mapId); - } - - /// - public bool IsMapPaused(EntityUid uid) - { - return _mapSystem.IsPaused(uid); - } - - /// - /// Initializes the map pausing system. - /// - private void InitializeMapPausing() - { - _conhost.RegisterCommand("pausemap", - "Pauses a map, pausing all simulation processing on it.", - "pausemap ", - (shell, _, args) => - { - if (args.Length != 1) - { - shell.WriteError("Need to supply a valid MapId"); - return; - } - - var mapId = new MapId(int.Parse(args[0], CultureInfo.InvariantCulture)); - - if (!MapExists(mapId)) - { - shell.WriteError("That map does not exist."); - return; - } - - SetMapPaused(mapId, true); - }); - - _conhost.RegisterCommand("querymappaused", - "Check whether a map is paused or not.", - "querymappaused ", - (shell, _, args) => - { - var mapId = new MapId(int.Parse(args[0], CultureInfo.InvariantCulture)); - - if (!MapExists(mapId)) - { - shell.WriteError("That map does not exist."); - return; - } - - shell.WriteLine(_mapSystem.IsPaused(mapId).ToString()); - }); - - _conhost.RegisterCommand("unpausemap", - "unpauses a map, resuming all simulation processing on it.", - "Usage: unpausemap ", - (shell, _, args) => - { - if (args.Length != 1) - { - shell.WriteLine("Need to supply a valid MapId"); - return; - } - - var mapId = new MapId(int.Parse(args[0], CultureInfo.InvariantCulture)); - - if (!MapExists(mapId)) - { - shell.WriteLine("That map does not exist."); - return; - } - - SetMapPaused(mapId, false); - }); - } - } -} diff --git a/Robust.Shared/Map/MapManager.Queries.cs b/Robust.Shared/Map/MapManager.Queries.cs deleted file mode 100644 index f37a545525f..00000000000 --- a/Robust.Shared/Map/MapManager.Queries.cs +++ /dev/null @@ -1,365 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Numerics; -using Robust.Shared.GameObjects; -using Robust.Shared.Map.Components; -using Robust.Shared.Map.Enumerators; -using Robust.Shared.Maths; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Collision.Shapes; -using Robust.Shared.Physics.Shapes; - -namespace Robust.Shared.Map; - -internal partial class MapManager -{ - private bool IsIntersecting( - ChunkEnumerator enumerator, - T shape, - Transform shapeTransform, - Entity grid) where T : IPhysShape - { - var gridTransform = _physics.GetPhysicsTransform(grid); - - while (enumerator.MoveNext(out var chunk)) - { - foreach (var id in chunk.Fixtures) - { - var fixture = grid.Comp.Fixtures[id]; - - for (var j = 0; j < fixture.Shape.ChildCount; j++) - { - if (_manifolds.TestOverlap(shape, 0, fixture.Shape, j, shapeTransform, gridTransform)) - { - return true; - } - } - } - } - - return false; - } - - #region MapId - - public void FindGridsIntersecting(MapId mapId, T shape, Transform transform, - ref List> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape - { - if (_mapSystem.TryGetMap(mapId, out var mapEnt)) - FindGridsIntersecting(mapEnt.Value, shape, transform, ref grids, approx: approx, includeMap: includeMap); - } - - public void FindGridsIntersecting(MapId mapId, T shape, Transform transform, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape - { - if (_mapSystem.TryGetMap(mapId, out var mapEnt)) - FindGridsIntersecting(mapEnt.Value, shape, transform, callback, approx: approx, includeMap: includeMap); - } - - public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - if (_mapSystem.TryGetMap(mapId, out var mapEnt)) - FindGridsIntersecting(mapEnt.Value, worldAABB, callback, approx: approx, includeMap: includeMap); - } - - public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, ref TState state, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - if (_mapSystem.TryGetMap(mapId, out var map)) - FindGridsIntersecting(map.Value, worldAABB, ref state, callback, approx: approx, includeMap: includeMap); - } - - public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, ref List> grids, - bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - if (_mapSystem.TryGetMap(mapId, out var map)) - FindGridsIntersecting(map.Value, worldAABB, ref grids, approx: approx, includeMap: includeMap); - } - - public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, GridCallback callback, bool approx = IMapManager.Approximate, - bool includeMap = IMapManager.IncludeMap) - { - if (_mapSystem.TryGetMap(mapId, out var mapEnt)) - FindGridsIntersecting(mapEnt.Value, worldBounds, callback, approx: approx, includeMap: includeMap); - } - - public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, ref TState state, GridCallback callback, - bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - if (_mapSystem.TryGetMap(mapId, out var mapEnt)) - FindGridsIntersecting(mapEnt.Value, worldBounds, ref state, callback, approx: approx, includeMap: includeMap); - } - - public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, ref List> grids, - bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - if (_mapSystem.TryGetMap(mapId, out var mapEnt)) - FindGridsIntersecting(mapEnt.Value, worldBounds, ref grids, approx: approx, includeMap: includeMap); - } - - #endregion - - #region MapEnt - - public void FindGridsIntersecting( - EntityUid mapEnt, - T shape, - Transform transform, - GridCallback callback, - bool approx = IMapManager.Approximate, - bool includeMap = IMapManager.IncludeMap) where T : IPhysShape - { - FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, callback, approx: approx, includeMap: includeMap); - } - - private void FindGridsIntersecting(EntityUid mapEnt, T shape, Box2 worldAABB, Transform transform, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape - { - // This is here so we don't double up on code. - var state = callback; - - FindGridsIntersecting(mapEnt, shape, worldAABB, transform, ref state, - static (EntityUid uid, MapGridComponent grid, ref GridCallback state) => state.Invoke(uid, grid), - approx: approx, includeMap: includeMap); - } - - public void FindGridsIntersecting( - EntityUid mapEnt, - T shape, - Transform transform, - ref TState state, - GridCallback callback, - bool approx = IMapManager.Approximate, - bool includeMap = IMapManager.IncludeMap) where T : IPhysShape - { - FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, ref state, callback, approx: approx, includeMap: includeMap); - } - - private void FindGridsIntersecting( - EntityUid mapEnt, - T shape, - Box2 worldAABB, - Transform transform, - ref TState state, - GridCallback callback, - bool approx = IMapManager.Approximate, - bool includeMap = IMapManager.IncludeMap) where T : IPhysShape - { - if (!_gridTreeQuery.TryGetComponent(mapEnt, out var gridTree)) - return; - - if (includeMap && _gridQuery.TryGetComponent(mapEnt, out var mapGrid)) - { - callback(mapEnt, mapGrid, ref state); - } - - var gridState = new GridQueryState( - callback, - state, - worldAABB, - shape, - transform, - gridTree.Tree, - _mapSystem, - this, - _transformSystem, - approx); - - gridTree.Tree.Query(ref gridState, static (ref GridQueryState state, DynamicTree.Proxy proxy) => - { - // Even for approximate we'll check if any chunks roughly overlap. - var data = state.Tree.GetUserData(proxy); - var gridInvMatrix = state.TransformSystem.GetInvWorldMatrix(data.Uid); - var localAABB = gridInvMatrix.TransformBox(state.WorldAABB); - - var overlappingChunks = state.MapSystem.GetLocalMapChunks(data.Uid, data.Grid, localAABB); - - if (state.Approximate) - { - if (!overlappingChunks.MoveNext(out _)) - return true; - } - else if (!state.MapManager.IsIntersecting(overlappingChunks, state.Shape, state.Transform, (data.Uid, data.Fixtures))) - { - return true; - } - - var callbackState = state.State; - var result = state.Callback(data.Uid, data.Grid, ref callbackState); - state.State = callbackState; - - return result; - }, worldAABB); - - // By-ref things - state = gridState.State; - } - - /// - /// Returns true if any grids overlap the specified shapes. - /// - public void FindGridsIntersecting(EntityUid mapEnt, List shapes, Transform transform, ref List> entities, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - foreach (var shape in shapes) - { - FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, ref entities, approx: approx, includeMap: includeMap); - } - } - - public void FindGridsIntersecting(EntityUid mapEnt, T shape, Transform transform, - ref List> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape - { - FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, ref grids, approx: approx, includeMap: includeMap); - } - - public void FindGridsIntersecting(EntityUid mapEnt, T shape, Box2 worldAABB, Transform transform, - ref List> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape - { - var state = grids; - - FindGridsIntersecting(mapEnt, shape, worldAABB, transform, ref state, - static (EntityUid uid, MapGridComponent grid, ref List> list) => - { - list.Add((uid, grid)); - return true; - }, approx: approx, includeMap: includeMap); - } - - public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - var polygon = new SlimPolygon(worldAABB); - FindGridsIntersecting(mapEnt, polygon, worldAABB, Transform.Empty, callback, approx: approx, includeMap: includeMap); - } - - public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, ref TState state, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - var polygon = new SlimPolygon(worldAABB); - FindGridsIntersecting(mapEnt, polygon, worldAABB, Transform.Empty, ref state, callback, approx: approx, includeMap: includeMap); - } - - public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, ref List> grids, - bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - var polygon = new SlimPolygon(worldAABB); - FindGridsIntersecting(mapEnt, polygon, worldAABB, Transform.Empty, ref grids, approx: approx, includeMap: includeMap); - } - - public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, GridCallback callback, bool approx = IMapManager.Approximate, - bool includeMap = IMapManager.IncludeMap) - { - var polygon = new SlimPolygon(worldBounds); - FindGridsIntersecting(mapEnt, polygon, worldBounds.CalcBoundingBox(), Transform.Empty, callback, approx: approx, includeMap: includeMap); - } - - public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, ref TState state, GridCallback callback, - bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - var polygon = new SlimPolygon(worldBounds); - FindGridsIntersecting(mapEnt, polygon, worldBounds.CalcBoundingBox(), Transform.Empty, ref state, callback, approx: approx, includeMap: includeMap); - } - - public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, ref List> grids, - bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - var polygon = new SlimPolygon(worldBounds); - FindGridsIntersecting(mapEnt, polygon, worldBounds.CalcBoundingBox(), Transform.Empty, ref grids, approx: approx, includeMap: includeMap); - } - - #endregion - - #region TryFindGridAt - - public bool TryFindGridAt( - EntityUid mapEnt, - Vector2 worldPos, - out EntityUid uid, - [NotNullWhen(true)] out MapGridComponent? grid) - { - var rangeVec = new Vector2(0.2f, 0.2f); - - // Need to enlarge the AABB by at least the grid shrinkage size. - var aabb = new Box2(worldPos - rangeVec, worldPos + rangeVec); - - uid = EntityUid.Invalid; - grid = null; - var state = (uid, grid, worldPos, _mapSystem, _transformSystem); - - FindGridsIntersecting(mapEnt, aabb, ref state, static (EntityUid iUid, MapGridComponent iGrid, ref ( - EntityUid uid, - MapGridComponent? grid, - Vector2 worldPos, - SharedMapSystem mapSystem, - SharedTransformSystem xformSystem) tuple) => - { - // Turn the worldPos into a localPos and work out the relevant chunk we need to check - // This is much faster than iterating over every chunk individually. - // (though now we need some extra calcs up front). - - // Doesn't use WorldBounds because it's just an AABB. - var matrix = tuple.xformSystem.GetInvWorldMatrix(iUid); - var localPos = Vector2.Transform(tuple.worldPos, matrix); - - // NOTE: - // If you change this to use fixtures instead (i.e. if you want half-tiles) then you need to make sure - // you account for the fact that fixtures are shrunk slightly! - var chunkIndices = SharedMapSystem.GetChunkIndices(localPos, iGrid.ChunkSize); - - if (!iGrid.Chunks.TryGetValue(chunkIndices, out var chunk)) - return true; - - var chunkRelative = SharedMapSystem.GetChunkRelative(localPos, iGrid.ChunkSize); - var chunkTile = chunk.GetTile(chunkRelative); - - if (chunkTile.IsEmpty) - return true; - - tuple.uid = iUid; - tuple.grid = iGrid; - return false; - }, approx: true, includeMap: false); - - if (state.grid == null && _gridQuery.TryGetComponent(mapEnt, out var mapGrid)) - { - uid = mapEnt; - grid = mapGrid; - return true; - } - - uid = state.uid; - grid = state.grid; - return grid != null; - } - - /// - /// Attempts to find the map grid under the map location. - /// - public bool TryFindGridAt(MapId mapId, Vector2 worldPos, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid) - { - if (_mapSystem.TryGetMap(mapId, out var map)) - return TryFindGridAt(map.Value, worldPos, out uid, out grid); - - uid = default; - grid = null; - return false; - } - - /// - /// Attempts to find the map grid under the map location. - /// - public bool TryFindGridAt(MapCoordinates mapCoordinates, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid) - { - return TryFindGridAt(mapCoordinates.MapId, mapCoordinates.Position, out uid, out grid); - } - - #endregion - - private record struct GridQueryState( - GridCallback Callback, - TState State, - Box2 WorldAABB, - T Shape, - Transform Transform, - B2DynamicTree<(EntityUid Uid, FixturesComponent Fixtures, MapGridComponent Grid)> Tree, - SharedMapSystem MapSystem, - MapManager MapManager, - SharedTransformSystem TransformSystem, - bool Approximate); -} diff --git a/Robust.Shared/Map/MapManager.cs b/Robust.Shared/Map/MapManager.cs deleted file mode 100644 index c5e853c8b63..00000000000 --- a/Robust.Shared/Map/MapManager.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Robust.Shared.Console; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Log; -using Robust.Shared.Map.Components; -using Robust.Shared.Physics.Collision; -using Robust.Shared.Physics.Systems; -using Robust.Shared.Timing; - -namespace Robust.Shared.Map; - -/// -[Virtual] -internal partial class MapManager : IMapManagerInternal, IEntityEventSubscriber -{ - [Dependency] public IGameTiming GameTiming = default!; - [Dependency] public IEntityManager EntityManager = default!; - [Dependency] private IManifoldManager _manifolds = default!; - [Dependency] private ILogManager _logManager = default!; - [Dependency] private IConsoleHost _conhost = default!; - - private ISawmill _sawmill = default!; - - private SharedMapSystem _mapSystem = default!; - private SharedPhysicsSystem _physics = default!; - private SharedTransformSystem _transformSystem = default!; - - private EntityQuery _gridTreeQuery; - private EntityQuery _gridQuery; - - /// - public void Initialize() - { - _gridTreeQuery = EntityManager.GetEntityQuery(); - _gridQuery = EntityManager.GetEntityQuery(); - InitializeMapPausing(); - _sawmill = _logManager.GetSawmill("system.map"); - } - - /// - public void Startup() - { - _physics = EntityManager.System(); - _transformSystem = EntityManager.System(); - _mapSystem = EntityManager.System(); - - _sawmill.Debug("Starting..."); - } - - /// - public void Shutdown() - { - _sawmill.Debug("Stopping..."); - - // TODO: AllEntityQuery instead??? - var query = EntityManager.EntityQueryEnumerator(); - - while (query.MoveNext(out var uid, out _)) - { - EntityManager.DeleteEntity(uid); - } - } - - /// - public void Restart() - { - _sawmill.Debug("Restarting..."); - - // Don't just call Shutdown / Startup because we don't want to touch the subscriptions on gridtrees - // Restart can be called any time during a game, whereas shutdown / startup are typically called upon connection. - var query = EntityManager.EntityQueryEnumerator(); - - while (query.MoveNext(out var uid, out _)) - { - EntityManager.DeleteEntity(uid); - } - } -} diff --git a/Robust.Shared/Map/NetworkedMapManager.cs b/Robust.Shared/Map/NetworkedMapManager.cs deleted file mode 100644 index 683916bc46d..00000000000 --- a/Robust.Shared/Map/NetworkedMapManager.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Robust.Shared.Map.Components; -using Robust.Shared.Timing; - -namespace Robust.Shared.Map; - -internal interface INetworkedMapManager : IMapManagerInternal -{ - void CullDeletionHistory(GameTick upToTick); -} - -internal sealed class NetworkedMapManager : MapManager, INetworkedMapManager -{ - public void CullDeletionHistory(GameTick upToTick) - { - var query = EntityManager.AllEntityQueryEnumerator(); - - while (query.MoveNext(out var grid)) - { - var chunks = grid.ChunkDeletionHistory; - chunks.RemoveAll(t => t.tick < upToTick); - } - } -} diff --git a/Robust.Shared/Map/TileDefinitionManager.cs b/Robust.Shared/Map/TileDefinitionManager.cs index 3b14440fca1..41989358adf 100644 --- a/Robust.Shared/Map/TileDefinitionManager.cs +++ b/Robust.Shared/Map/TileDefinitionManager.cs @@ -47,6 +47,7 @@ public Tile GetVariantTile(string name, IRobustRandom random) return GetVariantTile(tileDef, random); } + [Obsolete("Use method with IRobustRandom parameter")] public Tile GetVariantTile(string name, System.Random random) { var tileDef = this[name]; @@ -58,6 +59,7 @@ public Tile GetVariantTile(ITileDefinition tileDef, IRobustRandom random) return new Tile(tileDef.TileId, variant: random.NextByte(tileDef.Variants)); } + [Obsolete("Use method with IRobustRandom parameter")] public Tile GetVariantTile(ITileDefinition tileDef, System.Random random) { return new Tile(tileDef.TileId, variant: random.NextByte(tileDef.Variants)); diff --git a/Robust.Shared/Network/Messages/MsgState.cs b/Robust.Shared/Network/Messages/MsgState.cs index 95eed008e8d..0b20558c7dd 100644 --- a/Robust.Shared/Network/Messages/MsgState.cs +++ b/Robust.Shared/Network/Messages/MsgState.cs @@ -21,6 +21,7 @@ public sealed class MsgState : NetMessage // TODO PVS make this a cvar // TODO PVS figure out optimal value public const int CompressionThreshold = 256; + private const int CompressedSizeEstimateDivisor = 4; public override MsgGroups MsgGroup => MsgGroups.Entity; @@ -33,6 +34,38 @@ public sealed class MsgState : NetMessage internal bool ForceSendReliably; + public override int EstimateBufferSize() + { + var uncompressedLength = (int) StateStream.Length; + // We have no idea what the compressed size will be but we'll try and guess slightly over + // mostly want to avoid a small initial size and then immediately copying. + var payloadLength = uncompressedLength > CompressionThreshold + ? Math.Max(CompressionThreshold, uncompressedLength / CompressedSizeEstimateDivisor) + : uncompressedLength; + var compressedLength = uncompressedLength > CompressionThreshold ? payloadLength : 0; + + // Message ID + two variable-length size prefixes + payload. + return 1 + + VariableInt32Size(uncompressedLength) + + VariableInt32Size(compressedLength) + + payloadLength; + } + + private static int VariableInt32Size(int value) + { + // Look at NetSerializer if you want to understand the zigzag. + var zigzag = (uint) ((value << 1) ^ (value >> 31)); + var size = 1; + + while (zigzag >= 0x80) + { + zigzag >>= 7; + size++; + } + + return size; + } + public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { MsgSize = buffer.LengthBytes; diff --git a/Robust.Shared/Network/NetEncryption.cs b/Robust.Shared/Network/NetEncryption.cs index 625df53cf2a..e65d35af4a5 100644 --- a/Robust.Shared/Network/NetEncryption.cs +++ b/Robust.Shared/Network/NetEncryption.cs @@ -9,6 +9,8 @@ namespace Robust.Shared.Network; internal sealed class NetEncryption { + public const int EncryptionOverhead = sizeof(ulong) + CryptoAeadXChaCha20Poly1305Ietf.AddBytes; + // Use a counter for nonces. The counter is 64-bit, I will be impressed if you ever manage to run it out. // 64-bit counter (incl over the wire) is fine, don't need the whole 192-bit. // Server starts at 0, client starts at 1, increment by two. @@ -31,7 +33,7 @@ public unsafe void Encrypt(NetOutgoingMessage message) var nonce = Interlocked.Add(ref _nonce, 2); var lengthBytes = message.LengthBytes; - var encryptedSize = CryptoAeadXChaCha20Poly1305Ietf.AddBytes + lengthBytes + sizeof(ulong); + var encryptedSize = lengthBytes + EncryptionOverhead; var data = message.Data.AsSpan(0, lengthBytes); diff --git a/Robust.Shared/Network/NetManager.ClientConnect.cs b/Robust.Shared/Network/NetManager.ClientConnect.cs index e5708cfd43d..0633ab05f68 100644 --- a/Robust.Shared/Network/NetManager.ClientConnect.cs +++ b/Robust.Shared/Network/NetManager.ClientConnect.cs @@ -346,7 +346,7 @@ async Task AttemptConnection(IPAddress address, CancellationT // Connection failed, clean up and yeet an exception peer.Shutdown(reason); _toCleanNetPeers.Add(peer); - throw new Exception($"Connection failed: {reason}"); + throw new ConnectionFailedException(reason); } return new ConnectionAttempt(peerData, connection, this); @@ -400,13 +400,32 @@ async Task AwaitNonInitStatusChange(NetConnection connection, Cancellati catch (AggregateException ae) { // ParallelTask throws AggregateException with all connection failures - // We just take the first one - var message = ae.InnerExceptions.First().Message; + // We'll try to take the most useful one. + var message = SelectConnectionFailureMessage(ae); OnConnectFailed(message); return null; } } + private static string SelectConnectionFailureMessage(AggregateException aggregateException) + { + var connectionFailures = aggregateException.InnerExceptions + .OfType() + .ToArray(); + + if (connectionFailures.Length == 0) + return aggregateException.InnerExceptions.First().Message; + + return connectionFailures.FirstOrDefault(e => !e.IsNoResponseFromRemoteHost)?.Message + ?? connectionFailures[0].Message; + } + + private sealed class ConnectionFailedException(string reason) : Exception($"Connection failed: {reason}") + { + public bool IsNoResponseFromRemoteHost { get; } = + reason.Contains("no response from remote host", StringComparison.OrdinalIgnoreCase); + } + private Task AwaitStatusChange(NetConnection connection, CancellationToken cancellationToken = default) { if (_awaitingStatusChange.ContainsKey(connection)) diff --git a/Robust.Shared/Network/NetManager.Send.cs b/Robust.Shared/Network/NetManager.Send.cs index 3da72909ed9..9d86bd029f1 100644 --- a/Robust.Shared/Network/NetManager.Send.cs +++ b/Robust.Shared/Network/NetManager.Send.cs @@ -62,7 +62,7 @@ private void CoreSendMessage( return; } - var packet = BuildMessage(message, channel.Connection.Peer); + var packet = BuildMessage(message, channel); var method = message.DeliveryMethod; var seqChannel = message.SequenceChannel; diff --git a/Robust.Shared/Network/NetManager.ServerAuth.cs b/Robust.Shared/Network/NetManager.ServerAuth.cs index e267656e881..c35c7474f84 100644 --- a/Robust.Shared/Network/NetManager.ServerAuth.cs +++ b/Robust.Shared/Network/NetManager.ServerAuth.cs @@ -211,7 +211,8 @@ private async void HandleHandshake(NetPeerData peer, NetConnection connection) HWId = legacyHwid, ModernHWIds = modernHWIds, Trust = joinedRespJson.ConnectionData!.Trust, - CreatedTime = joinedRespJson.UserData.CreatedTime + CreatedTime = joinedRespJson.UserData.CreatedTime, + IsLocal = isLocal }; padSuccessMessage = false; type = LoginType.LoggedIn; @@ -252,10 +253,15 @@ private async void HandleHandshake(NetPeerData peer, NetConnection connection) _logger.Verbose( $"{connection.RemoteEndPoint}: Assigned user ID: {userId}"); + var localTrust = _config.GetCVar(CVars.AuthLocalTrust); + var guestTrust = _config.GetCVar(CVars.AuthGuestTrust); + userData = new NetUserData(userId, name) { HWId = [], - ModernHWIds = [] + ModernHWIds = [], + Trust = isLocal ? localTrust : guestTrust, + IsLocal = isLocal }; } diff --git a/Robust.Shared/Network/NetManager.cs b/Robust.Shared/Network/NetManager.cs index 85928091300..8382b0a284b 100644 --- a/Robust.Shared/Network/NetManager.cs +++ b/Robust.Shared/Network/NetManager.cs @@ -269,6 +269,18 @@ public void Initialize(bool isServer) _config.OnValueChanged(CVars.NetLidgrenLogWarning, LidgrenLogWarningChanged); _config.OnValueChanged(CVars.NetLidgrenLogError, LidgrenLogErrorChanged); + _config.OnValueChanged(CVars.NetLidgrenLogRateLimit, LidgrenLogRateLimitChanged); + _config.OnValueChanged(CVars.NetLidgrenLogRateLimitTargets, LidgrenLogRateLimitTargetsChanged); + _config.OnValueChanged(CVars.NetLidgrenLogRateLimitBurst, LidgrenLogRateLimitBurstChanged); + _config.OnValueChanged(CVars.NetLidgrenLogRateLimitWindow, LidgrenLogRateLimitWindowChanged); + _config.OnValueChanged(CVars.NetConnectionTimeout, ConnectionTimeoutChanged); + _config.OnValueChanged(CVars.NetMaxIpConnections, MaxIpConnectionsChanged); + _config.OnValueChanged(CVars.NetMaxRapidConnections, MaxRapidConnectionsChanged); + _config.OnValueChanged(CVars.NetSendConnectionRejectionReasons, SendConnectionRejectionReasonsChanged); + _config.OnValueChanged(CVars.NetRapidConnectionWindow, RapidConnectionWindowChanged); + _config.OnValueChanged(CVars.NetRapidConnectionDecay, RapidConnectionDecayChanged); + _config.OnValueChanged(CVars.NetMaxFragmentReassemblyBytesPerConnection, MaxFragmentReassemblyBytesPerConnectionChanged); + _config.OnValueChanged(CVars.NetFragmentGroupTimeout, FragmentGroupTimeoutChanged); _config.OnValueChanged(CVars.NetVerbose, NetVerboseChanged); _config.OnValueChanged(CVars.NetLogging, NetLoggingChanged); @@ -317,6 +329,102 @@ private void LidgrenLogErrorChanged(bool newValue) } } + private void LidgrenLogRateLimitChanged(bool newValue) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.LogRateLimiterEnabled = newValue; + } + } + + private void LidgrenLogRateLimitTargetsChanged(int newValue) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.LogRateLimitTargets = (NetLogRateLimitTarget) newValue; + } + } + + private void LidgrenLogRateLimitBurstChanged(int newValue) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.LogRateLimitBurst = newValue; + } + } + + private void LidgrenLogRateLimitWindowChanged(float newValue) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.LogRateLimitWindow = newValue; + } + } + + private void ConnectionTimeoutChanged(float timeout) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.ConnectionTimeout = timeout; + } + } + + private void MaxIpConnectionsChanged(int limit) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.MaximumIpConnections = limit; + } + } + + private void MaxRapidConnectionsChanged(int limit) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.MaximumRapidConnections = limit; + } + } + + private void SendConnectionRejectionReasonsChanged(bool sendReasons) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.SendConnectionRejectionReasons = sendReasons; + } + } + + private void RapidConnectionWindowChanged(double time) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.RapidConnectionWindow = time; + } + } + + private void RapidConnectionDecayChanged(int decay) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.RapidConnectionDecay = decay; + } + } + + private void MaxFragmentReassemblyBytesPerConnectionChanged(int limit) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.MaximumFragmentReassemblyBytesPerConnection = limit; + } + } + + private void FragmentGroupTimeoutChanged(float timeout) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.FragmentGroupTimeout = timeout; + } + } + private void OnAuthModeChanged(int mode) { Auth = (AuthMode)mode; @@ -495,6 +603,18 @@ public void Shutdown(string reason) _config.UnsubValueChanged(CVars.NetFakeDuplicates, FakeDuplicatesChanged); _config.UnsubValueChanged(CVars.NetLidgrenLogWarning, LidgrenLogWarningChanged); _config.UnsubValueChanged(CVars.NetLidgrenLogError, LidgrenLogErrorChanged); + _config.UnsubValueChanged(CVars.NetLidgrenLogRateLimit, LidgrenLogRateLimitChanged); + _config.UnsubValueChanged(CVars.NetLidgrenLogRateLimitTargets, LidgrenLogRateLimitTargetsChanged); + _config.UnsubValueChanged(CVars.NetLidgrenLogRateLimitBurst, LidgrenLogRateLimitBurstChanged); + _config.UnsubValueChanged(CVars.NetLidgrenLogRateLimitWindow, LidgrenLogRateLimitWindowChanged); + _config.UnsubValueChanged(CVars.NetConnectionTimeout, ConnectionTimeoutChanged); + _config.UnsubValueChanged(CVars.NetMaxIpConnections, MaxIpConnectionsChanged); + _config.UnsubValueChanged(CVars.NetMaxRapidConnections, MaxRapidConnectionsChanged); + _config.UnsubValueChanged(CVars.NetSendConnectionRejectionReasons, SendConnectionRejectionReasonsChanged); + _config.UnsubValueChanged(CVars.NetRapidConnectionWindow, RapidConnectionWindowChanged); + _config.UnsubValueChanged(CVars.NetRapidConnectionDecay, RapidConnectionDecayChanged); + _config.UnsubValueChanged(CVars.NetMaxFragmentReassemblyBytesPerConnection, MaxFragmentReassemblyBytesPerConnectionChanged); + _config.UnsubValueChanged(CVars.NetFragmentGroupTimeout, FragmentGroupTimeoutChanged); _serializer.ClientHandshakeComplete -= OnSerializerOnClientHandshakeComplete; @@ -668,6 +788,11 @@ private NetPeerConfiguration _getBaseNetPeerConfig() NetIncomingMessageType.ErrorMessage, _config.GetCVar(CVars.NetLidgrenLogError)); + netConfig.LogRateLimiterEnabled = _config.GetCVar(CVars.NetLidgrenLogRateLimit); + netConfig.LogRateLimitTargets = (NetLogRateLimitTarget) _config.GetCVar(CVars.NetLidgrenLogRateLimitTargets); + netConfig.LogRateLimitBurst = _config.GetCVar(CVars.NetLidgrenLogRateLimitBurst); + netConfig.LogRateLimitWindow = _config.GetCVar(CVars.NetLidgrenLogRateLimitWindow); + var poolSize = _config.GetCVar(CVars.NetPoolSize); if (poolSize <= 0) @@ -693,11 +818,15 @@ private NetPeerConfiguration _getBaseNetPeerConfig() } else { - netConfig.ConnectionTimeout = _config.GetCVar(CVars.ConnectionTimeout); netConfig.ResendHandshakeInterval = _config.GetCVar(CVars.ResendHandshakeInterval); netConfig.MaximumHandshakeAttempts = _config.GetCVar(CVars.MaximumHandshakeAttempts); } - + netConfig.ConnectionTimeout = _config.GetCVar(CVars.NetConnectionTimeout); + netConfig.MaximumIpConnections = _config.GetCVar(CVars.NetMaxIpConnections); + netConfig.MaximumRapidConnections = _config.GetCVar(CVars.NetMaxRapidConnections); + netConfig.SendConnectionRejectionReasons = _config.GetCVar(CVars.NetSendConnectionRejectionReasons); + netConfig.RapidConnectionWindow = _config.GetCVar(CVars.NetRapidConnectionWindow); + netConfig.RapidConnectionDecay = _config.GetCVar(CVars.NetRapidConnectionDecay); //Simulate Latency netConfig.SimulatedLoss = _config.GetCVar(CVars.NetFakeLoss); @@ -715,6 +844,8 @@ private NetPeerConfiguration _getBaseNetPeerConfig() netConfig.AutoExpandMTU = _config.GetCVar(CVars.NetMtuExpand); netConfig.ExpandMTUFrequency = _config.GetCVar(CVars.NetMtuExpandFrequency); netConfig.ExpandMTUFailAttempts = _config.GetCVar(CVars.NetMtuExpandFailAttempts); + netConfig.MaximumFragmentReassemblyBytesPerConnection = _config.GetCVar(CVars.NetMaxFragmentReassemblyBytesPerConnection); + netConfig.FragmentGroupTimeout = _config.GetCVar(CVars.NetFragmentGroupTimeout); return netConfig; } @@ -1102,9 +1233,14 @@ public T CreateNetMessage() return new T(); } - private NetOutgoingMessage BuildMessage(NetMessage message, NetPeer peer) + private NetOutgoingMessage BuildMessage(NetMessage message, NetChannel channel) { - var packet = peer.CreateMessage(4); + var initialCapacity = message.EstimateBufferSize(); + + if (channel.Encryption != null) + initialCapacity += NetEncryption.EncryptionOverhead; + + var packet = channel.Connection.Peer.CreateMessage(initialCapacity); if (!_strings.TryFindStringId(message.MsgName, out int msgId)) throw new NetManagerException( diff --git a/Robust.Shared/Network/NetMessage.cs b/Robust.Shared/Network/NetMessage.cs index 83b0f805ca6..7efdf23939b 100644 --- a/Robust.Shared/Network/NetMessage.cs +++ b/Robust.Shared/Network/NetMessage.cs @@ -86,6 +86,12 @@ protected NetMessage() /// public abstract void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer); + /// + /// Estimated size of the serialized payload, excluding transport encryption overhead. + /// Used to size Lidgren's outgoing buffer before . + /// + public virtual int EstimateBufferSize() => 4; + public virtual NetDeliveryMethod DeliveryMethod { get diff --git a/Robust.Shared/Network/NetUserData.cs b/Robust.Shared/Network/NetUserData.cs index 7ee61388e9a..1aa7aa25a87 100644 --- a/Robust.Shared/Network/NetUserData.cs +++ b/Robust.Shared/Network/NetUserData.cs @@ -41,6 +41,11 @@ public sealed record NetUserData /// public float Trust { get; init; } + /// + /// True if the player is connecting from a local address. + /// + public bool IsLocal { get; init; } + public NetUserData(NetUserId userId, string userName) { UserId = userId; diff --git a/Robust.Shared/Physics/B2DynamicTree.cs b/Robust.Shared/Physics/B2DynamicTree.cs index f00eef18a79..1deb2d69c4f 100644 --- a/Robust.Shared/Physics/B2DynamicTree.cs +++ b/Robust.Shared/Physics/B2DynamicTree.cs @@ -251,7 +251,8 @@ public B2DynamicTree(float aabbExtendSize = 1f / 32, int capacity = 256, Func(stackalloc Proxy[256]); + var stack = new GrowableStack(stackalloc Proxy[TreeStackSize]); ref var baseRef = ref _nodes[0]; stack.Push(_root); @@ -2141,15 +2149,10 @@ internal void RayCastNew(RayCastInput input, long mask, ref WorldRayCastContext } else { - var stackCount = stack.GetCount(); - Assert( stackCount < 256 - 1 ); - if (stackCount < 256 - 1 ) - { - // TODO_ERIN just put one node on the stack, continue on a child node - // TODO_ERIN test ordering children by nearest to ray origin - stack.Push(node.Child1); - stack.Push(node.Child2); - } + // TODO_ERIN just put one node on the stack, continue on a child node + // TODO_ERIN test ordering children by nearest to ray origin + stack.Push(node.Child1); + stack.Push(node.Child2); } } } @@ -2205,7 +2208,7 @@ internal void ShapeCast(ShapeCastInput input, long maskBits, TreeShapeCastCallba var subInput = input; ref var baseRef = ref _nodes[0]; - var stack = new GrowableStack(stackalloc Proxy[256]); + var stack = new GrowableStack(stackalloc Proxy[TreeStackSize]); stack.Push(_root); while (stack.GetCount() > 0) @@ -2258,16 +2261,10 @@ internal void ShapeCast(ShapeCastInput input, long maskBits, TreeShapeCastCallba } else { - var stackCount = stack.GetCount(); - Assert(stackCount < 256 - 1); - - if (stackCount < 255) - { - // TODO_ERIN just put one node on the stack, continue on a child node - // TODO_ERIN test ordering children by nearest to ray origin - stack.Push(node.Child1); - stack.Push(node.Child2); - } + // TODO_ERIN just put one node on the stack, continue on a child node + // TODO_ERIN test ordering children by nearest to ray origin + stack.Push(node.Child1); + stack.Push(node.Child2); } } } diff --git a/Robust.Shared/Physics/Collision/CollisionManager.Overlap.cs b/Robust.Shared/Physics/Collision/CollisionManager.Overlap.cs index bdd41857eeb..a6a816a67bf 100644 --- a/Robust.Shared/Physics/Collision/CollisionManager.Overlap.cs +++ b/Robust.Shared/Physics/Collision/CollisionManager.Overlap.cs @@ -18,8 +18,8 @@ public bool TestOverlap(T shapeA, int indexA, U shapeB, int indexB, in Tra { var input = new DistanceInput(); - input.ProxyA.Set(shapeA, indexA); - input.ProxyB.Set(shapeB, indexB); + input.ProxyA.Set(in shapeA, indexA); + input.ProxyB.Set(in shapeB, indexB); input.TransformA = xfA; input.TransformB = xfB; input.UseRadii = true; diff --git a/Robust.Shared/Physics/Collision/DistanceProxy.cs b/Robust.Shared/Physics/Collision/DistanceProxy.cs index 6f8a70b0122..07eb2d18faf 100644 --- a/Robust.Shared/Physics/Collision/DistanceProxy.cs +++ b/Robust.Shared/Physics/Collision/DistanceProxy.cs @@ -39,7 +39,7 @@ internal ref struct DistanceProxy { internal float Radius; internal ReadOnlySpan Vertices; - internal FixedArray2 Buffer; + internal FixedArray8 Buffer; // GJK using Voronoi regions (Christer Ericson) and Barycentric coordinates. @@ -54,7 +54,7 @@ internal DistanceProxy(ReadOnlySpan vertices, float radius) /// must remain in scope while the proxy is in use. /// /// The shape. - internal void Set(T shape, int index) where T : IPhysShape + internal void Set(in T shape, int index) where T : IPhysShape { switch (shape.ShapeType) { @@ -68,16 +68,14 @@ internal void Set(T shape, int index) where T : IPhysShape case ShapeType.Polygon: if (shape is Polygon poly) { - Span verts = new Vector2[poly.VertexCount]; - poly._vertices.AsSpan[..poly.VertexCount].CopyTo(verts); - Vertices = verts; + poly._vertices.AsSpan[..poly.VertexCount].CopyTo(Buffer.AsSpan); + Vertices = Buffer.AsSpan[..poly.VertexCount]; Radius = poly.Radius; } else if (shape is SlimPolygon fast) { - Span verts = new Vector2[fast.VertexCount]; - fast._vertices.AsSpan[..fast.VertexCount].CopyTo(verts); - Vertices = verts; + fast._vertices.AsSpan[..fast.VertexCount].CopyTo(Buffer.AsSpan); + Vertices = Buffer.AsSpan[..fast.VertexCount]; Radius = fast.Radius; } else @@ -95,7 +93,7 @@ internal void Set(T shape, int index) where T : IPhysShape Buffer._00 = chain.Vertices[index]; Buffer._01 = index + 1 < chain.Vertices.Length ? chain.Vertices[index + 1] : chain.Vertices[0]; - Vertices = Buffer.AsSpan; + Vertices = Buffer.AsSpan[..2]; Radius = chain.Radius; break; @@ -104,7 +102,7 @@ internal void Set(T shape, int index) where T : IPhysShape Buffer._00 = edge.Vertex1; Buffer._01 = edge.Vertex2; - Vertices = Buffer.AsSpan; + Vertices = Buffer.AsSpan[..2]; Radius = edge.Radius; break; diff --git a/Robust.Shared/Physics/Collision/InternalPhysicsHull.cs b/Robust.Shared/Physics/Collision/InternalPhysicsHull.cs index c28614d499d..13f1bde246a 100644 --- a/Robust.Shared/Physics/Collision/InternalPhysicsHull.cs +++ b/Robust.Shared/Physics/Collision/InternalPhysicsHull.cs @@ -10,7 +10,9 @@ namespace Robust.Shared.Physics; /// internal ref struct InternalPhysicsHull { - public Span Points; + private FixedArray8 _points; + + public Span Points => field.IsEmpty ? _points.AsSpan : field; public int Count; internal InternalPhysicsHull(Span vertices, int count) : this() @@ -66,7 +68,6 @@ private static InternalPhysicsHull RecurseHull(Vector2 p1, Vector2 p2, Span points, int return hull; } - hull.Points = new Vector2[PhysicsConstants.MaxPolygonVertices]; - // stitch hulls together, preserving CCW winding order hull.Points[hull.Count++] = p1; diff --git a/Robust.Shared/Physics/Components/PhysicsComponent.Physics.cs b/Robust.Shared/Physics/Components/PhysicsComponent.Physics.cs index 1ce7042b779..88cf18efeac 100644 --- a/Robust.Shared/Physics/Components/PhysicsComponent.Physics.cs +++ b/Robust.Shared/Physics/Components/PhysicsComponent.Physics.cs @@ -38,7 +38,7 @@ namespace Robust.Shared.Physics.Components; [RegisterComponent, NetworkedComponent] public sealed partial class PhysicsComponent : Component, IComponentDelta { - public GameTick LastFieldUpdate { get; set; } + public GameTick LastUnclassifiedDirty { get; set; } public GameTick[] LastModifiedFields { get; set; } /// diff --git a/Robust.Shared/Physics/DynamicTree.cs b/Robust.Shared/Physics/DynamicTree.cs index e4795f029c7..6f681104be3 100644 --- a/Robust.Shared/Physics/DynamicTree.cs +++ b/Robust.Shared/Physics/DynamicTree.cs @@ -103,8 +103,11 @@ public void Clear() { foreach (var proxy in _nodeLookup.Values) { - _b2Tree.DestroyProxy(proxy); + if (proxy != DynamicTree.Proxy.Free) + _b2Tree.DestroyProxy(proxy); } + + _nodeLookup.Clear(); } public bool Contains(T item) diff --git a/Robust.Shared/Physics/Events/PhysicsBodyStatusChangedEvent.cs b/Robust.Shared/Physics/Events/PhysicsBodyStatusChangedEvent.cs new file mode 100644 index 00000000000..5f1d22bf72b --- /dev/null +++ b/Robust.Shared/Physics/Events/PhysicsBodyStatusChangedEvent.cs @@ -0,0 +1,13 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.Physics.Components; + +namespace Robust.Shared.Physics.Events; + +// This is called PhysicsBody and not just Body to differentiate from a potential literal living body +/// +/// Raised on an entity when it's 's is being changed. +/// Raised after is set to the new status. +/// +/// of the entity which this event was directed at. +[ByRefEvent] +public readonly record struct PhysicsBodyStatusChangedEvent(PhysicsComponent PhysicsComponent, BodyStatus OldStatus, BodyStatus NewStatus); diff --git a/Robust.Shared/Physics/PhysicsHull.cs b/Robust.Shared/Physics/PhysicsHull.cs index 2145576c19a..aa66d6daa87 100644 --- a/Robust.Shared/Physics/PhysicsHull.cs +++ b/Robust.Shared/Physics/PhysicsHull.cs @@ -8,6 +8,6 @@ public struct PhysicsHull public static Span ComputePoints(ReadOnlySpan points, int count) { var hull = InternalPhysicsHull.ComputeHull(points, count); - return hull.Points; + return hull.Count == 0 ? Span.Empty : hull.Points.ToArray(); } } diff --git a/Robust.Shared/Physics/Shapes/Polygon.cs b/Robust.Shared/Physics/Shapes/Polygon.cs index 5961d482d65..64aed0d8b60 100644 --- a/Robust.Shared/Physics/Shapes/Polygon.cs +++ b/Robust.Shared/Physics/Shapes/Polygon.cs @@ -9,7 +9,8 @@ namespace Robust.Shared.Physics.Shapes; // Internal so people don't use it when it will have breaking changes very soon. -internal record struct Polygon : IPhysShape +[DataDefinition] +internal partial record struct Polygon : IPhysShape { [DataField] public byte VertexCount { get; internal set; } diff --git a/Robust.Shared/Physics/Shapes/SlimPolygon.cs b/Robust.Shared/Physics/Shapes/SlimPolygon.cs index 53e69a16106..72272513bac 100644 --- a/Robust.Shared/Physics/Shapes/SlimPolygon.cs +++ b/Robust.Shared/Physics/Shapes/SlimPolygon.cs @@ -15,7 +15,8 @@ namespace Robust.Shared.Physics.Shapes; /// Polygon backed by FixedArray4 to be smaller. /// Useful for internal ops where the inputs are boxes to avoid the additional padding. /// -internal record struct SlimPolygon : IPhysShape +[DataDefinition] +internal partial record struct SlimPolygon : IPhysShape { public Vector2[] Vertices => _vertices.AsSpan[..VertexCount].ToArray(); diff --git a/Robust.Shared/Physics/Systems/RayCastSystem.Geometry.cs b/Robust.Shared/Physics/Systems/RayCastSystem.Geometry.cs index cd523d29aef..912e8e072c1 100644 --- a/Robust.Shared/Physics/Systems/RayCastSystem.Geometry.cs +++ b/Robust.Shared/Physics/Systems/RayCastSystem.Geometry.cs @@ -112,6 +112,11 @@ private static float RayCastCallback(RayCastInput input, FixtureProxy proxy, ref return input.MaxFraction; } + if ((worldContext.Filter.Flags & QueryFlags.Sensors) == 0x0 && !proxy.Fixture.Hard) + { + return input.MaxFraction; + } + if (worldContext.Filter.IsIgnored?.Invoke(proxy.Entity) == true) { return input.MaxFraction; diff --git a/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs b/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs index e6fbf61f9b2..0a15fd2c9ec 100644 --- a/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs +++ b/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs @@ -18,7 +18,6 @@ namespace Robust.Shared.Physics.Systems public abstract partial class SharedBroadphaseSystem : EntitySystem { [Dependency] private IConfigurationManager _cfg = default!; - [Dependency] private IMapManagerInternal _mapManager = default!; [Dependency] private IParallelManager _parallel = default!; [Dependency] private EntityLookupSystem _lookup = default!; [Dependency] private SharedGridTraversalSystem _traversal = default!; @@ -33,6 +32,7 @@ public abstract partial class SharedBroadphaseSystem : EntitySystem private EntityQuery _xformQuery; private readonly HashSet _gridMoveBuffer = new(); + private readonly HashSet _gridSundriesMoveBuffer = new(); private float _frameTime; @@ -51,9 +51,9 @@ public override void Initialize() _contactJob = new() { - MapManager = _mapManager, System = this, TransformSys = EntityManager.System(), + MapSys = _map, // TODO: EntityManager one isn't ready yet? XformQuery = GetEntityQuery(), }; @@ -106,6 +106,7 @@ private void FindGridContacts(HashSet movedGrids) // 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(); + _gridSundriesMoveBuffer.Clear(); foreach (var gridUid in movedGrids) { @@ -122,6 +123,10 @@ private void FindGridContacts(HashSet movedGrids) QueryMapBroadphase(mapBroadphase.DynamicTree, ref state, enlargedAABB); QueryMapBroadphase(mapBroadphase.StaticTree, ref state, enlargedAABB); + + var sundriesState = _gridSundriesMoveBuffer; + QueryMapSundries(mapBroadphase.SundriesTree, ref sundriesState, enlargedAABB); + QueryMapSundries(mapBroadphase.StaticSundriesTree, ref sundriesState, enlargedAABB); } foreach (var proxy in _gridMoveBuffer) @@ -130,6 +135,11 @@ private void FindGridContacts(HashSet movedGrids) // If something is in our AABB then try grid traversal for it _traversal.CheckTraverse((proxy.Entity, _xformQuery.GetComponent(proxy.Entity))); } + + foreach (var uid in _gridSundriesMoveBuffer) + { + _traversal.CheckTraverse((uid, _xformQuery.GetComponent(uid))); + } } private float GetBroadphaseExpand(PhysicsComponent body, float frameTime) @@ -160,6 +170,15 @@ private void QueryMapBroadphase(IBroadPhase broadPhase, }, enlargedAABB, true); } + private void QueryMapSundries(DynamicTree sundriesTree, ref HashSet state, Box2 enlargedAABB) + { + sundriesTree.QueryAabb(ref state, static (ref HashSet moveBuffer, in EntityUid value) => + { + moveBuffer.Add(value); + return true; + }, enlargedAABB, true); + } + /// /// Go through every single created, moved, or touched proxy on the map and try to find any new contacts that should be created. /// @@ -265,7 +284,7 @@ private void HandleGridCollisions(HashSet movedGrids) _physicsQuery, _xformQuery); - _mapManager.FindGridsIntersecting(xform.MapID, aabb, ref state, + _map.FindGridsIntersecting(xform.MapID, aabb, ref state, static (EntityUid gridBUid, MapGridComponent gridBMapComp, ref (Entity gridA, Transform gridAToWorldRigid, @@ -541,7 +560,7 @@ internal void GetBroadphases(MapId mapId, Box2 aabb, BroadphaseCallback callback if (_broadphaseQuery.TryGetComponent(map.Value, out var mapBroadphase)) callback((map.Value, mapBroadphase)); - _mapManager.FindGridsIntersecting(map.Value, + _map.FindGridsIntersecting(map.Value, aabb, ref internalState, static ( @@ -569,7 +588,7 @@ internal void GetBroadphases(MapId mapId, Box2 aabb, ref TState state, B if (_broadphaseQuery.TryGetComponent(map.Value, out var mapBroadphase)) callback((map.Value, mapBroadphase), ref state); - _mapManager.FindGridsIntersecting(map.Value, + _map.FindGridsIntersecting(map.Value, aabb, ref internalState, static ( @@ -596,7 +615,7 @@ private record struct BroadphaseContactJob() : IParallelRobustJob { public SharedBroadphaseSystem System = default!; public SharedTransformSystem TransformSys = default!; - public IMapManager MapManager = default!; + public SharedMapSystem MapSys = default!; public EntityQuery XformQuery; @@ -626,7 +645,7 @@ public void Execute(int index) var state = (System, proxy, worldAABB, Pairs); // Get every broadphase we may be intersecting. - MapManager.FindGridsIntersecting(mapUid, worldAABB.Enlarged(broadphaseExpand), ref state, + MapSys.FindGridsIntersecting(mapUid, worldAABB.Enlarged(broadphaseExpand), ref state, static (EntityUid uid, MapGridComponent _, ref ( SharedBroadphaseSystem system, FixtureProxy proxy, diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs index 88670635e7e..c3161597184 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs @@ -73,7 +73,7 @@ private void OnPhysicsInit(EntityUid uid, PhysicsComponent component, ComponentI private void OnPhysicsGetState(EntityUid uid, PhysicsComponent component, ref ComponentGetState args) { - if (args.FromTick > component.CreationTick && component.LastFieldUpdate >= args.FromTick) + if (component.LastUnclassifiedDirty <= args.FromTick) { var slowPath = false; @@ -81,7 +81,7 @@ private void OnPhysicsGetState(EntityUid uid, PhysicsComponent component, ref Co { var field = component.LastModifiedFields[i]; - if (field < args.FromTick) + if (field <= args.FromTick) continue; slowPath = true; @@ -91,7 +91,7 @@ private void OnPhysicsGetState(EntityUid uid, PhysicsComponent component, ref Co // We can do a smaller delta with no list index overhead. if (!slowPath) { - var angularDirty = component.LastModifiedFields[_angularVelocityIndex] >= args.FromTick; + var angularDirty = component.LastModifiedFields[_angularVelocityIndex] > args.FromTick; if (angularDirty) { @@ -153,7 +153,7 @@ private void OnPhysicsHandleState(EntityUid uid, PhysicsComponent component, ref SetSleepingAllowed(uid, component, newState.SleepingAllowed, dirty: false); SetFixedRotation(uid, newState.FixedRotation, body: component, dirty: false); SetCanCollide(uid, newState.CanCollide, body: component, dirty: false); - component.BodyStatus = newState.Status; + SetBodyStatus(uid, component, newState.Status, dirty: false); SetLinearVelocity(uid, newState.LinearVelocity, dirty: false, body: component, manager: manager); SetAngularVelocity(uid, newState.AngularVelocity, dirty: false, body: component, manager: manager); @@ -224,7 +224,7 @@ public void ApplyLinearImpulse(EntityUid uid, Vector2 impulse, FixturesComponent return; } - SetLinearVelocity(uid,body.LinearVelocity + impulse * body._invMass, body: body); + SetLinearVelocity(uid, body.LinearVelocity + impulse * body._invMass, body: body); } public void ApplyLinearImpulse(EntityUid uid, Vector2 impulse, Vector2 point, FixturesComponent? manager = null, PhysicsComponent? body = null) @@ -235,7 +235,8 @@ public void ApplyLinearImpulse(EntityUid uid, Vector2 impulse, Vector2 point, Fi } SetLinearVelocity(uid, body.LinearVelocity + impulse * body._invMass, body: body); - SetAngularVelocity(uid, body.AngularVelocity + body.InvI * Vector2Helpers.Cross(point - body._localCenter, impulse), body: body); + var matrix = _transform.GetWorldMatrix(uid); + SetAngularVelocity(uid, body.AngularVelocity + body.InvI * Vector2Helpers.Cross(Vector2.Transform(point, matrix) - Vector2.Transform(body._localCenter, matrix), impulse), body: body); } #endregion @@ -342,7 +343,7 @@ public void ResetMassData(EntityUid uid, FixturesComponent? manager = null, Phys var oldCenter = body._localCenter; body._localCenter = localCenter; - if (((int) body.BodyType & (int) (BodyType.Kinematic | BodyType.Static)) == 0) + if (((int)body.BodyType & (int)(BodyType.Kinematic | BodyType.Static)) == 0) { // Update center of mass velocity. var comVelocityDiff = Vector2Helpers.Cross(body.AngularVelocity, localCenter - oldCenter); @@ -549,7 +550,12 @@ public void SetBodyStatus(EntityUid uid, PhysicsComponent body, BodyStatus statu if (body.BodyStatus == status) return; + var oldStatus = body.BodyStatus; body.BodyStatus = status; + + var ev = new PhysicsBodyStatusChangedEvent(body, oldStatus, status); + RaiseLocalEvent(uid, ref ev); + if (dirty) DirtyField(uid, body, nameof(PhysicsComponent.BodyStatus)); } @@ -770,7 +776,7 @@ public Box2 GetWorldAABB(EntityUid uid, FixturesComponent? manager = null, Physi var (worldPos, worldRot) = _transform.GetWorldPositionRotation(xform); - var transform = new Transform(worldPos, (float) worldRot.Theta); + var transform = new Transform(worldPos, (float)worldRot.Theta); var bounds = new Box2(transform.Position, transform.Position); @@ -797,7 +803,7 @@ public Box2 GetHardAABB(EntityUid uid, FixturesComponent? manager = null, Physic var (worldPos, worldRot) = _transform.GetWorldPositionRotation(xform); - var transform = new Transform(worldPos, (float) worldRot.Theta); + var transform = new Transform(worldPos, (float)worldRot.Theta); var bounds = new Box2(transform.Position, transform.Position); diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Fixtures.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Fixtures.cs index d7243920e35..b3267cbb24c 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Fixtures.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Fixtures.cs @@ -168,7 +168,7 @@ public bool IsHardCollidable(Entity bodyA public bool IsHardCollidable(FixturesComponent fixturesA, FixturesComponent fixturesB) { var (aLayer, aMask) = GetHardCollision(fixturesA); - var (bLayer, bMask) = GetHardCollision(fixturesA); + var (bLayer, bMask) = GetHardCollision(fixturesB); return ((aLayer & bMask) | (bLayer & aMask)) != 0; } diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Island.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Island.cs index 6bf52122f80..d8dfdd9f20c 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Island.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Island.cs @@ -332,7 +332,7 @@ private void Solve(float frameTime, float dtRatio, float invDt, bool prediction) // when contact broke so if you want to try that then GOOD LUCK. if (seed.Island) continue; - var seedUid = seed.Owner; + var seedUid = ent.Owner; var mapUid = xform.MapUid; // TODO: Handle this on client. @@ -377,7 +377,7 @@ private void Solve(float frameTime, float dtRatio, float invDt, bool prediction) if (body.BodyType == BodyType.Static) continue; // As static bodies can never be awake (unlike Farseer) we'll set this after the check. - SetAwake(bodyUid, body, true, updateSleepTime: false); + SetAwake(bodyEnt, true, updateSleepTime: false); var node = body.Contacts.First; @@ -1139,7 +1139,7 @@ private void SleepBodies(in IslandData island, bool[] sleepStatus) var body = island.Bodies[i]; - SetAwake(body.Owner, body, false); + SetAwake(body, false); } } } diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Queries.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Queries.cs index b8ac8c333cb..b3b24b8de44 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Queries.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Queries.cs @@ -553,18 +553,20 @@ public bool TryGetNearest(EntityUid uidA, EntityUid uidB, if (bodyA.Hard && !fixtureA.Hard) continue; - for (var i = 0; i < fixtureA.Shape.ChildCount; i++) + var shapeA = fixtureA.Shape; + for (var i = 0; i < shapeA.ChildCount; i++) { - input.ProxyA.Set(fixtureA.Shape, i); + input.ProxyA.Set(in shapeA, i); foreach (var fixtureB in managerB.Fixtures.Values) { if (bodyB.Hard && !fixtureB.Hard) continue; - for (var j = 0; j < fixtureB.Shape.ChildCount; j++) + var shapeB = fixtureB.Shape; + for (var j = 0; j < shapeB.ChildCount; j++) { - input.ProxyB.Set(fixtureB.Shape, j); + input.ProxyB.Set(in shapeB, j); DistanceManager.ComputeDistance(out var output, out _, input); if (distance < output.Distance) @@ -626,8 +628,9 @@ public bool TryGetNearest(EntityUid uid, MapCoordinates coordinates, DebugTools.Assert(fixtureA.ProxyCount <= 1); - input.ProxyA.Set(fixtureA.Shape, 0); - input.ProxyB.Set(pointShape, 0); + var shapeA = fixtureA.Shape; + input.ProxyA.Set(in shapeA, 0); + input.ProxyB.Set(in pointShape, 0); DistanceManager.ComputeDistance(out var output, out _, input); if (distance < output.Distance) diff --git a/Robust.Shared/Profiling/ProfManager.Tracy.cs b/Robust.Shared/Profiling/ProfManager.Tracy.cs new file mode 100644 index 00000000000..9b8b41fcffd --- /dev/null +++ b/Robust.Shared/Profiling/ProfManager.Tracy.cs @@ -0,0 +1,151 @@ +using System; +using bottlenoselabs.C2CS.Runtime; +using Robust.Shared.Maths; +using static Tracy.PInvoke; + +namespace Robust.Shared.Profiling; + +// Tracy-side partial ProfManager + +public sealed partial class ProfManager +{ + /// + /// Proxy to prof.tracy.enabled CVar. + /// + public bool IsTracyEnabled { get; private set; } + + partial void InitializeTracyCvars() + { + _cfg.OnValueChanged(CVars.TracyProfEnabled, b => IsTracyEnabled = b, true); + } + + private bool _tracyPlotsInitialized; + private long _tracyLastAllocatedBytes; + private CString _tracyPlotManagedHeap; + private CString _tracyPlotAllocRate; + private CString _tracyPlotGen0; + private CString _tracyPlotGen1; + private CString _tracyPlotGen2; + private CString _tracyEntityCount; + + internal unsafe void EmitFrameImage(void* image, ushort width, ushort height, byte offset, bool flip) + { + if (!IsTracyEnabled) + return; + + TracyEmitFrameImage(image, width, height, offset, flip ? 1 : 0); + } + + internal void EmitMemoryPlots(int gcGen0, int gcGen1, int gcGen2) + { + if (!IsTracyEnabled) + return; + + if (!_tracyPlotsInitialized) + InitTracyPlots(); + + var allocated = GC.GetTotalAllocatedBytes(); + + TracyEmitPlotInt(_tracyPlotManagedHeap, GC.GetTotalMemory(false)); + TracyEmitPlotInt(_tracyPlotAllocRate, allocated - _tracyLastAllocatedBytes); + TracyEmitPlotInt(_tracyPlotGen0, gcGen0); + TracyEmitPlotInt(_tracyPlotGen1, gcGen1); + TracyEmitPlotInt(_tracyPlotGen2, gcGen2); + + _tracyLastAllocatedBytes = allocated; + + } + + internal void EmitEntities(int entityCount) + { + if (!IsTracyEnabled) + return; + + if (!_tracyPlotsInitialized) + InitTracyPlots(); + + TracyEmitPlotInt(_tracyEntityCount, entityCount); + } + + private void InitTracyPlots() + { + _tracyPlotsInitialized = true; + _tracyLastAllocatedBytes = GC.GetTotalAllocatedBytes(); + + _tracyPlotManagedHeap = CString.FromString("Managed Heap"); + _tracyPlotAllocRate = CString.FromString("Alloc Rate"); + _tracyPlotGen0 = CString.FromString("GC Gen 0"); + _tracyPlotGen1 = CString.FromString("GC Gen 1"); + _tracyPlotGen2 = CString.FromString("GC Gen 2"); + _tracyEntityCount = CString.FromString("Entities"); + + const int memoryFormat = (int)TracyPlotFormatEnum.TracyPlotFormatMemory; + TracyEmitPlotConfig(_tracyPlotManagedHeap, memoryFormat, step: 0, fill: 1, color: 0); + TracyEmitPlotConfig(_tracyPlotAllocRate, memoryFormat, step: 0, fill: 1, color: 0); + + const int numberFormat = (int)TracyPlotFormatEnum.TracyPlotFormatNumber; + TracyEmitPlotConfig(_tracyPlotGen0, numberFormat, step: 1, fill: 0, color: 0); + TracyEmitPlotConfig(_tracyPlotGen1, numberFormat, step: 1, fill: 0, color: 0); + TracyEmitPlotConfig(_tracyPlotGen2, numberFormat, step: 1, fill: 0, color: 0); + TracyEmitPlotConfig(_tracyEntityCount, numberFormat, step: 1, fill: 0, color: 0); + } + + /// + /// Marks the boundary of a continuous Tracy frame. Used by . + /// + private static void EmitFrameMark() + { + TracyEmitFrameMark(null); + } + + /// + /// Creates a for use by Tracy. Also returns the + /// length of the string for interop convenience. + /// + internal static CString GetCString(string? fromString, out ulong cLength) + { + if (fromString == null) + { + cLength = 0; + return new CString(0); + } + + cLength = (ulong)fromString.Length; + return CString.FromString(fromString); + } + + private static TracyProfilerZone BeginTracyZone(string name, int lineNumber, Color? color, string? filePath, string? memberName) + { + using var fileStr = GetCString(filePath, out var fileLn); + using var memberStr = GetCString(memberName, out var memberLn); + using var nameStr = GetCString(name, out var nameLn); + var srcLocId = TracyAllocSrclocName((uint)lineNumber, fileStr, fileLn, memberStr, memberLn, nameStr, nameLn, (uint) (color?.ToArgb() ?? 0)); + var context = TracyEmitZoneBeginAlloc(srcLocId, 1); + return new TracyProfilerZone(context); + } +} + +internal readonly struct TracyProfilerZone : IDisposable +{ + private readonly TracyCZoneCtx _context; + + private uint Id => _context.Data.Id; + + private int Active => _context.Data.Active; + + internal TracyProfilerZone(TracyCZoneCtx context) + { + _context = context; + } + + internal void EmitText(string text) + { + using var textStr = ProfManager.GetCString(text, out var textLn); + TracyEmitZoneText(_context, textStr, textLn); + } + + public void Dispose() + { + TracyEmitZoneEnd(_context); + } +} diff --git a/Robust.Shared/Profiling/ProfManager.cs b/Robust.Shared/Profiling/ProfManager.cs index 67684503c76..47cf7ef374d 100644 --- a/Robust.Shared/Profiling/ProfManager.cs +++ b/Robust.Shared/Profiling/ProfManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; @@ -6,6 +6,7 @@ using Robust.Shared.Collections; using Robust.Shared.Configuration; using Robust.Shared.Log; +using Robust.Shared.Maths; using Robust.Shared.Utility; namespace Robust.Shared.Profiling; @@ -14,6 +15,8 @@ namespace Robust.Shared.Profiling; // See ProfData.cs for description of profiling data layout. +// Tracy integration lives in ProfManager.Tracy.cs. + public sealed partial class ProfManager { [IoC.Dependency] private IConfigurationManager _cfg = default!; @@ -62,8 +65,11 @@ internal void Initialize() }, true); _cfg.OnValueChanged(CVars.ProfEnabled, b => IsEnabled = b, true); + InitializeTracyCvars(); } + partial void InitializeTracyCvars(); + /// /// Write an index covering the region from to the current write position. /// @@ -135,9 +141,13 @@ public long WriteValue(string text, in ProfValue value) /// /// Make a guarded value for usage with using blocks. /// - public ValueGuard Value(string text) + public ValueGuard Value( + string text, + [CallerLineNumber] int lineNumber = 0, + [CallerFilePath] string? filePath = null, + [CallerMemberName] string? memberName = null) { - return new ValueGuard(this, text); + return new ValueGuard(this, text, lineNumber, filePath, memberName); } /// @@ -191,10 +201,29 @@ public void WriteGroupEnd(long startIndex, string text, in ProfSampler sampler) /// /// Make a guarded group for usage with using blocks. /// - public GroupGuard Group(string name) + /// The name of this group as it will show in the profiler. + /// + /// The color of this zone and all of its children, as it will show in Tracy. This does + /// not affect the in game profiler. + /// + public GroupGuard Group( + string name, + Color? color = null, + [CallerLineNumber] int lineNumber = 0, + [CallerFilePath] string? filePath = null, + [CallerMemberName] string? memberName = null) + { + var start = WriteGroupStart(); + return new GroupGuard(this, start, name, color ?? Color.Black, lineNumber, filePath, memberName); + } + + /// + /// Make a guarded group that emits Tracy frame markers instead of a zone, for usage with using blocks. + /// + internal FrameGuard Frame(string name) { var start = WriteGroupStart(); - return new GroupGuard(this, start, name); + return new FrameGuard(this, start, name); } /// @@ -245,7 +274,51 @@ private ref ProfLog WriteCmd() private readonly string _groupName; private readonly ProfSampler _sampler; - public GroupGuard(ProfManager mgr, long startIndex, string groupName) + private TracyProfilerZone? TracyZone { get; } + + internal GroupGuard( + ProfManager mgr, + long startIndex, + string groupName, + Color? color, + int lineNumber, + string? filePath, + string? memberName) + { + _mgr = mgr; + _startIndex = startIndex; + _groupName = groupName; + _sampler = ProfSampler.StartNew(); + if (_mgr.IsTracyEnabled) + TracyZone = BeginTracyZone(groupName, lineNumber, color, filePath, memberName); + } + + /// + /// Adds text to show up in Tracy for this zone. + /// + /// If Tracy is not enabled, this does nothing. + /// + public void EmitText(string str) + { + TracyZone?.EmitText(str); + } + + public void Dispose() + { + _mgr.WriteGroupEnd(_startIndex, _groupName, _sampler); + if (_mgr.IsTracyEnabled) + TracyZone?.Dispose(); + } + } + + internal readonly struct FrameGuard : IDisposable + { + private readonly ProfManager _mgr; + private readonly long _startIndex; + private readonly string _groupName; + private readonly ProfSampler _sampler; + + public FrameGuard(ProfManager mgr, long startIndex, string groupName) { _mgr = mgr; _startIndex = startIndex; @@ -256,6 +329,8 @@ public GroupGuard(ProfManager mgr, long startIndex, string groupName) public void Dispose() { _mgr.WriteGroupEnd(_startIndex, _groupName, _sampler); + if (_mgr.IsTracyEnabled) + EmitFrameMark(); } } @@ -264,17 +339,27 @@ public void Dispose() private readonly ProfManager _mgr; private readonly string _text; private readonly ProfSampler _sampler; - - public ValueGuard(ProfManager mgr, string text) + private readonly TracyProfilerZone? _tracyZone; + + public ValueGuard( + ProfManager mgr, + string text, + int lineNumber, + string? filePath, + string? memberName) { _mgr = mgr; _text = text; _sampler = ProfSampler.StartNew(); + if (_mgr.IsTracyEnabled) + _tracyZone = BeginTracyZone(_text, lineNumber, Color.Black, filePath, memberName); } public void Dispose() { _mgr.WriteValue(_text, _sampler); + if (_mgr.IsTracyEnabled) + _tracyZone?.Dispose(); } } } diff --git a/Robust.Shared/Prototypes/EntProtoId.cs b/Robust.Shared/Prototypes/EntProtoId.cs index 673da72980d..8cfd69ba5c1 100644 --- a/Robust.Shared/Prototypes/EntProtoId.cs +++ b/Robust.Shared/Prototypes/EntProtoId.cs @@ -97,7 +97,7 @@ public T Get(IPrototypeManager? prototypes, IComponentFactory compFactory) { prototypes ??= IoCManager.Resolve(); var proto = prototypes.Index(this); - if (!proto.TryGetComponent(out T? comp, compFactory)) + if (!proto.TryComp(out T? comp, compFactory)) { throw new ArgumentException($"{nameof(EntityPrototype)} {proto.ID} has no {nameof(T)}"); } @@ -110,6 +110,6 @@ public bool TryGet([NotNullWhen(true)] out T? comp, IPrototypeManager? prototype comp = default; prototypes ??= IoCManager.Resolve(); return prototypes.TryIndex(this, out var proto) && - proto.TryGetComponent(out comp, compFactory); + proto.TryComp(out comp, compFactory); } } diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index b5be7c3702b..45309ff97ce 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; using Robust.Shared.EntitySerialization; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -10,7 +12,6 @@ using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; @@ -151,16 +152,16 @@ public sealed partial class EntityPrototype : IPrototype, IInheritingPrototype, /// /// A dictionary mapping the component type list to the YAML mapping containing their settings. /// - [DataField("components")] + [DataField] [AlwaysPushInheritance] public ComponentRegistry Components = new(); public EntityPrototype() { // Everybody gets a transform component! - Components.Add("Transform", new ComponentRegistryEntry(new TransformComponent(), new MappingDataNode())); + Components.Add("Transform", new ComponentRegistryEntry(new TransformComponent())); // And a metadata component too! - Components.Add("MetaData", new ComponentRegistryEntry(new MetaDataComponent(), new MappingDataNode())); + Components.Add("MetaData", new ComponentRegistryEntry(new MetaDataComponent())); } void ISerializationHooks.AfterDeserialization() @@ -169,29 +170,46 @@ void ISerializationHooks.AfterDeserialization() } [Obsolete("Pass in IComponentFactory")] - public bool TryGetComponent([NotNullWhen(true)] out T? component) - where T : IComponent, new() - { - var compName = IoCManager.Resolve().GetComponentName(); - return TryGetComponent(compName, out component); - } + public bool TryGetComponent([NotNullWhen(true)] out T? component) where T : IComponent, new() + => TryGetComponent(out component, IoCManager.Resolve()); + [Pure] + [Obsolete("TryComp is shorter, use it instead")] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetComponent([NotNullWhen(true)] out T? component, IComponentFactory factory) where T : IComponent, new() - { - var compName = factory.GetComponentName(); - return TryGetComponent(compName, out component); - } + => TryComp(out component, factory); + [Obsolete("Use TryComp with a CompName instead")] public bool TryGetComponent(string name, [NotNullWhen(true)] out T? component) where T : IComponent, new() { - DebugTools.AssertEqual(IoCManager.Resolve().GetComponentName(), name); + // please do not use this method ever ^_^ + var factory = IoCManager.Resolve(); + return TryComp(factory.CompName(), out component); + } - if (!Components.TryGetValue(name, out var componentUnCast)) + /// + /// Tries to get and cast a component from this prototype with . + /// Returns false if the component is missing or is not assignable to . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryComp([NotNullWhen(true)] out T? component, IComponentFactory factory) where T : IComponent, new() + => TryComp(factory.CompName(), out component); + + /// + /// Tries to get and cast a component from this prototype with a associated with it. + /// Returns false if the component is missing or is not assignable to . + /// + [Pure] + public bool TryComp(CompName name, [NotNullWhen(true)] out T? component) where T : IComponent, new() + { + if (!Components.TryGetValue(name.Name, out var componentUnCast)) { component = default; return false; } + // TODO: should this throw? might break some crazy shitcode though if (componentUnCast.Component is not T cast) { component = default; @@ -202,6 +220,33 @@ public bool TryGetComponent([NotNullWhen(true)] out T? component) return true; } + /// + /// Returns true if this prototype contains a . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasComp(IComponentFactory factory) where T : IComponent, new() + => HasComp(factory.CompName()); + + /// + /// Returns true if this prototype contains a component with a given type. + /// It is a programmer error if the name does not belong to a component. + /// This is not caught by a debug assert, so only use it with types from a + /// component registry, typeof(SomeComponent), etc. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasComp(Type type, IComponentFactory factory) + => HasComp(factory.CompName(type)); + + /// + /// Returns true if this prototype contains a component with a given . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasComp(CompName name) + => Components.ContainsKey(name.Name); + internal static void LoadEntity( Entity ent, IComponentFactory factory, @@ -262,22 +307,28 @@ public static void EnsureCompExistsAndDeserialize(EntityUid entity, IComponent data, ISerializationContext? context) { + var existed = true; if (!entityManager.TryGetComponent(entity, compReg.Idx, out var component)) { + existed = false; var newComponent = factory.GetComponent(compName); - entityManager.AddComponent(entity, newComponent); + newComponent.Owner = entity; component = newComponent; } if (context is not EntityDeserializer map) { serManager.CopyTo(data, ref component, context, notNullableOverride: true); - return; + } + else + { + map.CurrentComponent = compName; + serManager.CopyTo(data, ref component, context, notNullableOverride: true); + map.CurrentComponent = null; } - map.CurrentComponent = compName; - serManager.CopyTo(data, ref component, context, notNullableOverride: true); - map.CurrentComponent = null; + if (!existed) + entityManager.AddComponent(entity, component); } public override string ToString() @@ -285,8 +336,15 @@ public override string ToString() return $"EntityPrototype({ID})"; } - [DataRecord] - public partial record ComponentRegistryEntry(IComponent Component, MappingDataNode Mapping); + public sealed class ComponentRegistryEntry + { + public IComponent Component { get; } + + public ComponentRegistryEntry(IComponent component) + { + Component = component; + } + } [DataDefinition] public sealed partial class EntityPlacementProperties @@ -334,73 +392,6 @@ public HashSet SnapFlags } } } - /*private class PrototypeSerializationContext : YamlObjectSerializer.Context - { - readonly EntityPrototype? prototype; - - public PrototypeSerializationContext(EntityPrototype? owner) - { - prototype = owner; - } - - public override void SetCachedField(string field, T value) - { - if (StackDepth != 0 || prototype?.CurrentDeserializingComponent == null) - { - base.SetCachedField(field, value); - return; - } - - if (!prototype.FieldCache.TryGetValue(prototype.CurrentDeserializingComponent, out var fieldList)) - { - fieldList = new Dictionary<(string, Type), object?>(); - prototype.FieldCache[prototype.CurrentDeserializingComponent] = fieldList; - } - - fieldList[(field, typeof(T))] = value; - } - - public override bool TryGetCachedField(string field, [MaybeNullWhen(false)] out T value) - { - if (StackDepth != 0 || prototype?.CurrentDeserializingComponent == null) - { - return base.TryGetCachedField(field, out value); - } - - if (prototype.FieldCache.TryGetValue(prototype.CurrentDeserializingComponent, out var dict)) - { - if (dict.TryGetValue((field, typeof(T)), out var theValue)) - { - value = (T) theValue!; - return true; - } - } - - value = default!; - return false; - } - - public override void SetDataCache(string field, object value) - { - if (StackDepth != 0 || prototype == null) - { - base.SetDataCache(field, value); - return; - } - - prototype.DataCache[field] = value; - } - - public override bool TryGetDataCache(string field, out object? value) - { - if (StackDepth != 0 || prototype == null) - { - return base.TryGetDataCache(field, out value); - } - - return prototype.DataCache.TryGetValue(field, out value); - } - }*/ } public sealed class ComponentRegistry : Dictionary, IEntityLoadContext diff --git a/Robust.Shared/Prototypes/IPrototypeManager.cs b/Robust.Shared/Prototypes/IPrototypeManager.cs index fcb5cedb18e..e2741e70c02 100644 --- a/Robust.Shared/Prototypes/IPrototypeManager.cs +++ b/Robust.Shared/Prototypes/IPrototypeManager.cs @@ -175,19 +175,6 @@ bool TryGetInstances([NotNullWhen(true)] out FrozenDictionary? ins /// bool Resolve([ForbidLiteral] EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prototype); - /// - /// Retrieve an by ID, optionally logging an error if it does not exist. - /// - /// The prototype ID to look up. - /// The prototype that was resolved, null if it does not exist. - /// If true (default), log an error if the prototype does not exist. - /// True if the prototype exists, false if it does not. - [Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")] - bool TryIndex( - [ForbidLiteral] EntProtoId id, - [NotNullWhen(true)] out EntityPrototype? prototype, - bool logError = true); - /// /// Resolve an by ID. /// @@ -236,17 +223,6 @@ bool TryIndex( /// bool Resolve([ForbidLiteral] ProtoId id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype; - /// - /// Retrieve a prototype by ID, optionally logging an error if it does not exist. - /// - /// The prototype ID to look up. - /// The prototype that was resolved, null if it does not exist. - /// If true (default), log an error if the prototype does not exist. - /// True if the prototype exists, false if it does not. - [Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")] - bool TryIndex([ForbidLiteral] ProtoId id, [NotNullWhen(true)] out T? prototype, bool logError = true) - where T : class, IPrototype; - /// /// Resolve a prototype by ID. /// @@ -299,25 +275,6 @@ bool TryIndex([ForbidLiteral] ProtoId id, [NotNullWhen(true)] out T? proto /// bool Resolve([ForbidLiteral] EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? prototype); - /// - /// Retrieve an by ID, gracefully handling null, - /// and optionally logging an error if it does not exist. - /// - /// - /// - /// No error is logged if is null. - /// - /// - /// The prototype ID to look up. - /// The prototype that was resolved, null if it does not exist. - /// If true (default), log an error if the prototype does not exist. - /// True if the prototype exists, false if was null, or it does not exist. - [Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")] - bool TryIndex( - [ForbidLiteral] EntProtoId? id, - [NotNullWhen(true)] out EntityPrototype? prototype, - bool logError = true); - /// /// Resolve an by ID, gracefully handling null. /// @@ -369,22 +326,6 @@ bool TryIndex( /// bool Resolve([ForbidLiteral] ProtoId? id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype; - /// - /// Retrieve a prototype by ID, gracefully handling null, - /// and optionally logging an error if it does not exist. - /// - /// - /// - /// No error is logged if is null. - /// - /// - /// The prototype ID to look up. - /// The prototype that was resolved, null if it does not exist. - /// If true (default), log an error if the prototype does not exist. - /// True if the prototype exists, false if was null, or it does not exist. - [Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")] - bool TryIndex([ForbidLiteral] ProtoId? id, [NotNullWhen(true)] out T? prototype, bool logError = true) where T : class, IPrototype; - /// /// Resolve a prototype by ID, gracefully handling null. /// @@ -413,8 +354,11 @@ bool TryIndex( // ReSharper restore MethodOverloadWithOptionalParameter bool HasMapping(string id); + bool TryGetMapping(Type kind, string id, [NotNullWhen(true)] out MappingDataNode? mappings); + bool TryGetMapping(string id, [NotNullWhen(true)] out MappingDataNode? mappings); + /// /// Returns whether a prototype kind exists. /// diff --git a/Robust.Shared/Prototypes/ProtoId.cs b/Robust.Shared/Prototypes/ProtoId.cs index 8d69ad02b17..1b7fb85a17f 100644 --- a/Robust.Shared/Prototypes/ProtoId.cs +++ b/Robust.Shared/Prototypes/ProtoId.cs @@ -1,4 +1,5 @@ using System; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic; using Robust.Shared.Toolshed.TypeParsers; @@ -14,6 +15,7 @@ namespace Robust.Shared.Prototypes; /// /// for an alias. [Serializable] +[CopyByRef] [PreferOtherType(typeof(EntityPrototype), typeof(EntProtoId))] public readonly record struct ProtoId(string Id) : IEquatable, diff --git a/Robust.Shared/Prototypes/PrototypeManager.ValidateFields.cs b/Robust.Shared/Prototypes/PrototypeManager.ValidateFields.cs index 62801514590..0b9dc8d5268 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.ValidateFields.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.ValidateFields.cs @@ -177,7 +177,9 @@ private IEnumerable GetEnumerableIds(IEnumerable ids) private bool TryGetFieldPrototype(FieldInfo field, [NotNullWhen(true)] out Type? proto) { // Validate anything with the attribute +#pragma warning disable CS0618 // This is supporting the attribute, not using it var attrib = field.GetCustomAttribute(typeof(ValidatePrototypeIdAttribute<>), false); +#pragma warning restore CS0618 if (attrib != null) { proto = attrib.GetType().GetGenericArguments().First(); diff --git a/Robust.Shared/Prototypes/PrototypeManager.cs b/Robust.Shared/Prototypes/PrototypeManager.cs index 527ae7ea2cf..7b0ff387ad1 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.cs @@ -20,6 +20,7 @@ using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Markdown.Mapping; +using Robust.Shared.Serialization.Markdown.Sequence; using Robust.Shared.Serialization.Markdown.Value; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -30,7 +31,6 @@ public abstract partial class PrototypeManager : IPrototypeManagerInternal { [Dependency] private IReflectionManager _reflectionManager = default!; [Dependency] protected IResourceManager Resources = default!; - [Dependency] protected ITaskManager TaskManager = default!; [Dependency] private ISerializationManager _serializationManager = default!; [Dependency] private ILogManager _logManager = default!; [Dependency] private ILocalizationManager _locMan = default!; @@ -38,8 +38,9 @@ public abstract partial class PrototypeManager : IPrototypeManagerInternal [Dependency] private IEntityManager _entMan = default!; [Dependency] private IRobustRandom _random = default!; - private readonly Dictionary> _prototypeDataCache = new(); - private EntityDiffContext _context = new(); + private readonly Dictionary> _prototypeDataCache = new(); + + private readonly Dictionary _tempMappingData = new(); private readonly Dictionary _kindNames = new(); private readonly Dictionary _kindPriorities = new(); @@ -801,14 +802,6 @@ public bool Resolve(EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prot return false; } - [Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")] - public bool TryIndex(EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prototype, bool logError = true) - { - if (logError) - return Resolve(id, out prototype); - return TryIndex(id, out prototype); - } - public bool TryIndex([ForbidLiteral] EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prototype) { return TryIndex(id.Id, out prototype); @@ -823,15 +816,6 @@ public bool Resolve(ProtoId id, [NotNullWhen(true)] out T? prototype) wher return false; } - [Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")] - public bool TryIndex(ProtoId id, [NotNullWhen(true)] out T? prototype, bool logError = true) - where T : class, IPrototype - { - if (logError) - return Resolve(id, out prototype); - return TryIndex(id, out prototype); - } - public bool TryIndex(ProtoId id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype { @@ -849,14 +833,6 @@ public bool Resolve(EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? pro return Resolve(id.Value, out prototype); } - [Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")] - public bool TryIndex(EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? prototype, bool logError = true) - { - if (logError) - return Resolve(id, out prototype); - return TryIndex(id, out prototype); - } - public bool TryIndex(EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? prototype) { if (id == null) @@ -879,15 +855,6 @@ public bool Resolve(ProtoId? id, [NotNullWhen(true)] out T? prototype) whe return Resolve(id.Value, out prototype); } - [Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")] - public bool TryIndex(ProtoId? id, [NotNullWhen(true)] out T? prototype, bool logError = true) - where T : class, IPrototype - { - if (logError) - return Resolve(id, out prototype); - return TryIndex(id, out prototype); - } - public bool TryIndex(ProtoId? id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype { @@ -919,6 +886,11 @@ public bool TryGetMapping(Type kind, string id, [NotNullWhen(true)] out MappingD return _kinds[kind].Results.TryGetValue(id, out mappings); } + public bool TryGetMapping(string id, [NotNullWhen(true)] out MappingDataNode? mappings) + { + return _kinds[typeof(T)].Results.TryGetValue(id, out mappings); + } + public bool HasKind(string kind) { return _kindNames.ContainsKey(kind); @@ -949,7 +921,7 @@ public FrozenDictionary GetInstances() where T : IPrototype if (TryGetInstances(out var dict)) return dict; - throw new Exception($"Failed to fetch instances for kind {nameof(T)}"); + throw new Exception($"Failed to fetch instances for kind {typeof(T).Name}"); } public bool TryGetInstances([NotNullWhen(true)] out FrozenDictionary? instances) @@ -1215,28 +1187,26 @@ public IReadOnlyDictionary GetPrototypeData(EntityProto if (_prototypeDataCache.TryGetValue(prototype.ID, out var data)) return data; - _context.WritingReadingPrototypes = true; - data = new(); + _tempMappingData.Clear(); - var xform = _factory.GetRegistration(typeof(TransformComponent)).Name; - try + if (TryGetMapping(prototype.ID, out var mapping) + && mapping.TryGet("components", out var components)) { - foreach (var (compType, comp) in prototype.Components) + foreach (var component in components) { - if (compType == xform) + if (component is not MappingDataNode componentMapping + || !componentMapping.TryGet("type", out var type)) + { continue; + } - var node = _serializationManager.WriteValueAs(comp.Component.GetType(), comp.Component, - alwaysWrite: true, context: _context); - data.Add(compType, node); + var copy = componentMapping.Copy(); + copy.Remove("type"); + _tempMappingData[type.Value] = copy; } } - catch (Exception e) - { - Sawmill.Error($"Failed to convert prototype {prototype.ID} into yaml. Exception: {e.Message}"); - } - _context.WritingReadingPrototypes = false; + data = _tempMappingData.ToFrozenDictionary(); _prototypeDataCache[prototype.ID] = data; return data; } diff --git a/Robust.Shared/Reflection/ReflectionManager.cs b/Robust.Shared/Reflection/ReflectionManager.cs index 5e947ec7904..ac866a881de 100644 --- a/Robust.Shared/Reflection/ReflectionManager.cs +++ b/Robust.Shared/Reflection/ReflectionManager.cs @@ -43,8 +43,11 @@ public abstract partial class ReflectionManager : IReflectionManager private readonly ReaderWriterLockSlim _yamlTypeTagCacheLock = new(); private readonly List _getAllTypesCache = new(); + private readonly Dictionary<(Type BaseType, bool Inclusive), Type[]> _getAllChildrenCache = new(); private ISawmill _sawmill = default!; + private readonly List _childrenCache = new(); + public void Initialize() { _sawmill = _logMan.GetSawmill("Reflection"); @@ -61,6 +64,12 @@ public IEnumerable GetAllChildren(Type baseType, bool inclusive = false) { EnsureGetAllTypesCache(); + var key = (baseType, inclusive); + if (_getAllChildrenCache.TryGetValue(key, out var cached)) + return cached; + + _childrenCache.Clear(); + foreach (var type in _getAllTypesCache) { if (!baseType.IsAssignableFrom(type) || type.IsAbstract) @@ -69,8 +78,12 @@ public IEnumerable GetAllChildren(Type baseType, bool inclusive = false) if (baseType == type && !inclusive) continue; - yield return type; + _childrenCache.Add(type); } + + cached = _childrenCache.ToArray(); + _getAllChildrenCache.Add(key, cached); + return cached; } private void EnsureGetAllTypesCache() @@ -114,6 +127,7 @@ public void LoadAssemblies(IEnumerable assemblies) this.assemblies.AddRange(assembliesArray); _getAllTypesCache.Clear(); + _getAllChildrenCache.Clear(); OnAssemblyAdded?.Invoke(this, new ReflectionUpdateEventArgs(this)); } @@ -293,10 +307,7 @@ public bool TryParseEnumReference(string reference, [NotNullWhen(true)] out Enum { foreach (var type in assembly.DefinedTypes) { - if (!type.IsEnum || !( - type.FullName!.Equals(typeName) || - type.FullName!.EndsWith("." + typeName) || - type.FullName!.EndsWith("+" + typeName))) + if (!type.IsEnum || !TypeNameMatchesEnumReference(type.FullName!, typeName)) { continue; } @@ -316,6 +327,21 @@ public bool TryParseEnumReference(string reference, [NotNullWhen(true)] out Enum return false; } + private static bool TypeNameMatchesEnumReference(string fullName, string typeName) + { + if (fullName.Equals(typeName)) + return true; + + if (fullName.Length <= typeName.Length) + return false; + + var prefixIndex = fullName.Length - typeName.Length - 1; + var separator = fullName[prefixIndex]; + + return (separator == '.' || separator == '+') + && fullName.AsSpan(prefixIndex + 1).SequenceEqual(typeName); + } + public Type? YamlTypeTagLookup(Type baseType, string typeName) { using (_yamlTypeTagCacheLock.ReadGuard()) diff --git a/Robust.Shared/RichText/FormattedStringBuilder.cs b/Robust.Shared/RichText/FormattedStringBuilder.cs new file mode 100644 index 00000000000..0f059dd8b26 --- /dev/null +++ b/Robust.Shared/RichText/FormattedStringBuilder.cs @@ -0,0 +1,367 @@ +using System; +using System.Text; +using Robust.Shared.Collections; +using Robust.Shared.Maths; +using Robust.Shared.Utility; + +namespace Robust.Shared.RichText; + +/// +/// A wrapper around , with convenience methods for safely constructing rich text markup. +/// +/// +/// +/// Tags are written with multiple consecutive calls. Functions may throw if not in the right state, +/// and this can be checked with . +/// It should go without saying that care must be taken to use the underlying +/// while this is the case. +/// +/// +/// While the underlying is accessible, you are of course responsible for writing valid +/// markup and escaping if necessary. +/// +/// +public sealed class FormattedStringBuilder +{ + private ValueList _tagStack; + + /// + /// The underlying used by this instance. + /// + /// + /// you are responsible for writing valid markup and escaping where necessary, if you access this property. + /// + public StringBuilder Builder { get; } + + /// + /// If true, we are currently writing a tag. + /// + /// + /// This can be ended through or . + /// + public bool IsInsideTag { get; private set; } = true; + + /// + /// Create a new builder with an empty underlying . + /// + public FormattedStringBuilder() : this(new StringBuilder()) + { + + } + + /// + /// Create a new builder wrapping an existing . + /// + /// + /// The provided instance is not initially mutated. + /// + public FormattedStringBuilder(StringBuilder builder) + { + Builder = builder; + } + + /// + /// Begin a new tag with the specified name. + /// + /// The name of the tag to begin. + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're already inside a tag. + /// + public FormattedStringBuilder BeginTag(string tagName) + { + if (!IsInsideTag) + throw new InvalidOperationException("Cannot begin tag: we're already in a tag"); + + _tagStack.Push(tagName); + IsInsideTag = false; + Builder.Append($"[{tagName}"); + + return this; + } + + /// + /// Begin a new tag with the specified name and a value. + /// + /// The name of the tag to begin. + /// The value of the markup tag. + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're already inside a tag. + /// + public FormattedStringBuilder BeginTag(string tagName, MarkupParameter value) + { + BeginTag(tagName); + + Builder.Append(value.ToString()); + + return this; + } + + /// + /// Begin a new tag with the specified name and a value. + /// + /// The name of the tag to begin. + /// The value of the markup tag. + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're already inside a tag. + /// + public FormattedStringBuilder BeginTag(string tagName, string value) + { + return BeginTag(tagName, new MarkupParameter(value)); + } + + /// + /// Begin a new tag with the specified name and a value. + /// + /// The name of the tag to begin. + /// The value of the markup tag. + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're already inside a tag. + /// + public FormattedStringBuilder BeginTag(string tagName, long value) + { + return BeginTag(tagName, new MarkupParameter(value)); + } + + /// + /// Begin a new tag with the specified name and a value. + /// + /// The name of the tag to begin. + /// The value of the markup tag. + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're already inside a tag. + /// + public FormattedStringBuilder BeginTag(string tagName, Color value) + { + return BeginTag(tagName, new MarkupParameter(value)); + } + + /// + /// Specify an attribute for the tag currently being written. + /// + /// + /// This does not check for duplicates. + /// + /// The name of the attribute to write. + /// The value of the attribute. + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're not currently inside a tag. + /// + public FormattedStringBuilder TagAttribute(string attributeName, MarkupParameter value) + { + if (IsInsideTag) + throw new InvalidOperationException("Cannot write attribute: we aren't in a tag!"); + + Builder.Append($" {attributeName}{value}"); + return this; + } + + /// + /// Specify an attribute for the tag currently being written. + /// + /// + /// This does not check for duplicates. + /// + /// The name of the attribute to write. + /// The value of the attribute. + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're not currently inside a tag. + /// + public FormattedStringBuilder TagAttribute(string attributeName, string value) + { + return TagAttribute(attributeName, new MarkupParameter(value)); + } + + /// + /// Specify an attribute for the tag currently being written. + /// + /// + /// This does not check for duplicates. + /// + /// The name of the attribute to write. + /// The value of the attribute. + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're not currently inside a tag. + /// + public FormattedStringBuilder TagAttribute(string attributeName, long value) + { + return TagAttribute(attributeName, new MarkupParameter(value)); + } + + /// + /// Specify an attribute for the tag currently being written. + /// + /// + /// This does not check for duplicates. + /// + /// The name of the attribute to write. + /// The value of the attribute. + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're not currently inside a tag. + /// + public FormattedStringBuilder TagAttribute(string attributeName, Color value) + { + return TagAttribute(attributeName, new MarkupParameter(value)); + } + + /// + /// Finish writing the current tag as self-closed. + /// + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're not currently inside a tag. + /// + public FormattedStringBuilder FinishTagSelfClosed() + { + if (IsInsideTag) + throw new InvalidOperationException("Cannot finish tag: we aren't in a tag!"); + + _tagStack.Pop(); + Builder.Append("/]"); + IsInsideTag = true; + return this; + } + + /// + /// Finish writing the current tag as open. You will have to close it with . + /// + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're not currently inside a tag. + /// + public FormattedStringBuilder FinishTagOpen() + { + if (IsInsideTag) + throw new InvalidOperationException("Cannot finish tag: we aren't in a tag!"); + + Builder.Append(']'); + IsInsideTag = true; + return this; + } + + /// + /// Write a closing tag for the most recent open tag. + /// + /// + /// The stack of open tags (from ) is automatically tracked. + /// + /// The current instance, to enable easy method call chaining. + /// + public FormattedStringBuilder PopTag() + { + if (!IsInsideTag) + throw new InvalidOperationException("Cannot begin tag: we're already in a tag"); + + Builder.Append($"[/{_tagStack.Pop()}]"); + return this; + } + + /// + /// Append plain text. + /// + /// The text to append without interpreting formatting. + /// + /// Thrown if we're currently inside a tag. + /// + /// The current instance, to enable easy method call chaining. + public FormattedStringBuilder AppendText(string text) + { + CheckSafe(); + Builder.Append(FormattedMessage.EscapeText(text)); + return this; + } + + /// + /// Append markup. + /// + /// The text to append as markup. + /// + /// Thrown if is not valid markup. + /// + /// + /// Thrown if we're currently inside a tag. + /// + /// The current instance, to enable easy method call chaining. + public FormattedStringBuilder AppendMarkup(string markup) + { + CheckSafe(); + + if (!FormattedMessage.ValidMarkup(markup)) + throw new ArgumentException("Not valid markup!", nameof(markup)); + + Builder.Append(markup); + return this; + } + + /// + /// Append markup, followed by a newline. + /// + /// + /// The added line is always a single Line Feed (LF), not . + /// + /// The text to append as markup. + /// + /// Thrown if is not valid markup. + /// + /// + /// Thrown if we're currently inside a tag. + /// + /// The current instance, to enable easy method call chaining. + public FormattedStringBuilder AppendMarkupLine(string markup) + { + AppendMarkup(markup); + AppendLine(); + return this; + } + + /// + /// Append a newline. + /// + /// + /// The added line is always a single Line Feed (LF), not . + /// + /// + /// Thrown if we're currently inside a tag. + /// + /// The current instance, to enable easy method call chaining. + public FormattedStringBuilder AppendLine() + { + CheckSafe(); + Builder.Append('\n'); + return this; + } + + /// + /// Returns the internal value as a string. + /// + public override string ToString() + { + return Builder.ToString(); + } + + /// + /// Returns the internal value as a . + /// + /// + /// Thrown if the contained string is not valid markup + /// (e.g. if you manually messed with the underlying .) + /// + public FormattedString ToFormattedString() + { + return FormattedString.FromMarkup(ToString()); + } + + private void CheckSafe() + { + if (!IsInsideTag) + throw new InvalidOperationException("Cannot append: we are currently writing a tag."); + } +} diff --git a/Robust.Shared/RichText/FormattedStringBuilderExtensions.cs b/Robust.Shared/RichText/FormattedStringBuilderExtensions.cs new file mode 100644 index 00000000000..6449f9c054f --- /dev/null +++ b/Robust.Shared/RichText/FormattedStringBuilderExtensions.cs @@ -0,0 +1,28 @@ +namespace Robust.Shared.RichText; + +/// +/// Extension methods for . +/// +public static class FormattedStringBuilderExtensions +{ + extension(FormattedStringBuilder builder) + { + /// + /// Write a cmdlink tag. + /// + /// The user-visible tag for the link. + /// The command executed when the user clicks. + /// The tooltip (title) when the user hovers over the link. + /// The current instance, to enable easy method call chaining. + public FormattedStringBuilder MakeCommandLinkTag(string text, string command, string? title = null) + { + builder.BeginTag("cmdlink", text); + builder.TagAttribute("command", command); + if (title != null) + builder.TagAttribute("title", title); + builder.FinishTagSelfClosed(); + + return builder; + } + } +} diff --git a/Robust.Shared/Robust.Shared.csproj b/Robust.Shared/Robust.Shared.csproj index cdd7bf9746e..8750448cb22 100644 --- a/Robust.Shared/Robust.Shared.csproj +++ b/Robust.Shared/Robust.Shared.csproj @@ -28,6 +28,7 @@ + @@ -62,4 +63,5 @@ + diff --git a/Robust.Shared/Serialization/ISerializationGenerated.cs b/Robust.Shared/Serialization/ISerializationGenerated.cs index 467844aa4ea..0b65cfb4499 100644 --- a/Robust.Shared/Serialization/ISerializationGenerated.cs +++ b/Robust.Shared/Serialization/ISerializationGenerated.cs @@ -1,5 +1,11 @@ using System; +using System.Collections.Generic; +using System.Collections.Immutable; using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Definition; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Mapping; +using Robust.Shared.Serialization.Markdown.Validation; #pragma warning disable CS0612 // Type or member is obsolete @@ -7,6 +13,24 @@ namespace Robust.Shared.Serialization; public interface ISerializationGenerated : ISerializationGenerated { + /// + [Obsolete("Use ISerializationManager.CreateCopy instead")] + T Instantiate(); + + /// + [Obsolete("Use ISerializationManager.CreateCopy instead")] + static virtual T StaticInstantiate() + { + throw new NotImplementedException(); + } + + /// + [Obsolete("Use ISerializationManager.CreateCopy instead")] + static virtual object StaticInstantiateObject() + { + throw new NotImplementedException(); + } + /// [Obsolete("Use ISerializationManager.CopyTo instead")] void Copy( @@ -15,17 +39,47 @@ void Copy( SerializationHookContext hookCtx, ISerializationContext? context = null); - /// - [Obsolete("Use ISerializationManager.CopyTo instead")] - void InternalCopy( + /// + [Obsolete("Use ISerializationManager.Read instead")] + static virtual void Read( ref T target, + MappingDataNode mappingDataNode, ISerializationManager serialization, SerializationHookContext hookCtx, - ISerializationContext? context = null); + ISerializationContext? context) + { + throw new NotImplementedException(); + } - /// - [Obsolete("Use ISerializationManager.CreateCopy instead")] - T Instantiate(); + /// + [Obsolete("Use ISerializationManager.Write instead")] + static virtual void Write( + T obj, + MappingDataNode mapping, + ISerializationManager serialization, + ISerializationContext? context, + bool alwaysWrite, + ImmutableDictionary defaultValues) + { + throw new NotImplementedException(); + } + + /// + [Obsolete("Use ISerializationManager.ValidateNode instead")] + static virtual void Validate( + Dictionary nodes, + MappingDataNode node, + ISerializationManager serialization, + ISerializationContext? context = null) + { + throw new NotImplementedException(); + } + + [Obsolete("Used only in serialization source generation internally")] + static virtual void GetFieldDefinitions(T? instance, List fields, string[]? fieldsParsed = null) + { + throw new NotImplementedException(); + } } public interface ISerializationGenerated @@ -38,3 +92,15 @@ void Copy( SerializationHookContext hookCtx, ISerializationContext? context = null); } + +[Obsolete("Used only in serialization source generation internally")] +public enum ISerializationGeneratedRead +{ + Default = 0 +} + +[Obsolete("Used only in serialization source generation internally")] +public enum ISerializationGeneratedCopy +{ + Default = 0 +} diff --git a/Robust.Shared/Serialization/Manager/Definition/DataDefinition.Delegates.cs b/Robust.Shared/Serialization/Manager/Definition/DataDefinition.Delegates.cs deleted file mode 100644 index 08ae3b7fec3..00000000000 --- a/Robust.Shared/Serialization/Manager/Definition/DataDefinition.Delegates.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Robust.Shared.Serialization.Markdown; -using Robust.Shared.Serialization.Markdown.Mapping; -using Robust.Shared.Serialization.Markdown.Validation; - -namespace Robust.Shared.Serialization.Manager.Definition -{ - internal partial class DataDefinition - { - //todo paul make these use the mngr delegates - public delegate void PopulateDelegateSignature( - ref T target, - MappingDataNode mappingDataNode, - SerializationHookContext hookCtx, - ISerializationContext? context); - - public delegate MappingDataNode SerializeDelegateSignature( - T obj, - ISerializationContext? context, - bool alwaysWrite); - - public delegate void CopyDelegateSignature( - T source, - ref T target, - SerializationHookContext hookCtx, - ISerializationContext? context); - - private delegate ValidationNode ValidateFieldDelegate( - DataNode node, - ISerializationContext? context); - } -} diff --git a/Robust.Shared/Serialization/Manager/Definition/DataDefinition.Emitters.cs b/Robust.Shared/Serialization/Manager/Definition/DataDefinition.Emitters.cs deleted file mode 100644 index 890e162f0dc..00000000000 --- a/Robust.Shared/Serialization/Manager/Definition/DataDefinition.Emitters.cs +++ /dev/null @@ -1,513 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using Robust.Shared.Network; -using Robust.Shared.Physics.Dynamics; -using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Serialization.Manager.Exceptions; -using Robust.Shared.Serialization.Markdown; -using Robust.Shared.Serialization.Markdown.Mapping; -using Robust.Shared.Serialization.Markdown.Sequence; -using Robust.Shared.Serialization.Markdown.Value; -using Robust.Shared.Utility; - -namespace Robust.Shared.Serialization.Manager.Definition -{ - internal partial class DataDefinition - { - private PopulateDelegateSignature EmitPopulateDelegate(SerializationManager manager) - { - var isServer = manager.DependencyCollection.Resolve().IsServer; - - var managerConst = Expression.Constant(manager); - - var targetParam = Expression.Parameter(typeof(T).MakeByRefType()); - var mappingDataParam = Expression.Parameter(typeof(MappingDataNode)); - var hookCtxParam = Expression.Parameter(typeof(SerializationHookContext)); - var contextParam = Expression.Parameter(typeof(ISerializationContext)); - - var expressions = new List(); - - for (var i = 0; i < BaseFieldDefinitions.Length; i++) - { - var fieldDefinition = BaseFieldDefinitions[i]; - - if (fieldDefinition.Attribute.ServerOnly && !isServer) - { - continue; - } - - var isNullable = NullableHelper.IsMarkedAsNullable(fieldDefinition.FieldInfo); - - var nodeVariable = Expression.Variable(typeof(DataNode)); - var valueVariable = Expression.Variable(fieldDefinition.FieldType); - Expression call; - if (fieldDefinition.Attribute.CustomTypeSerializer != null && (FieldInterfaceInfos[i].Reader.Value || - FieldInterfaceInfos[i].Reader.Sequence || - FieldInterfaceInfos[i].Reader.Mapping)) - { - var switchCases = new List(); - var nullable = fieldDefinition.FieldType.IsNullable(); - var fieldType = fieldDefinition.FieldType.EnsureNotNullableType(); - if (FieldInterfaceInfos[i].Reader.Value) - { - switchCases.Add(Expression.SwitchCase(Expression.Block(typeof(void), - Expression.Assign(valueVariable, SerializationManager.WrapNullableIfNeededExpression( - Expression.Call( - managerConst, - "Read", - new []{fieldType, typeof(ValueDataNode), fieldDefinition.Attribute.CustomTypeSerializer}, - Expression.Convert(nodeVariable, typeof(ValueDataNode)), - hookCtxParam, - contextParam, - Expression.Constant(null, typeof(ISerializationManager.InstantiationDelegate<>).MakeGenericType(fieldType)), - Expression.Constant(!isNullable)), nullable))), - Expression.Constant(typeof(ValueDataNode)))); - } - - if (FieldInterfaceInfos[i].Reader.Sequence) - { - switchCases.Add(Expression.SwitchCase(Expression.Block(typeof(void), - Expression.Assign(valueVariable, SerializationManager.WrapNullableIfNeededExpression(Expression.Call( - managerConst, - "Read", - new []{fieldType, typeof(SequenceDataNode), fieldDefinition.Attribute.CustomTypeSerializer}, - Expression.Convert(nodeVariable, typeof(SequenceDataNode)), - hookCtxParam, - contextParam, - Expression.Constant(null, typeof(ISerializationManager.InstantiationDelegate<>).MakeGenericType(fieldType)), - Expression.Constant(!isNullable)), nullable))), - Expression.Constant(typeof(SequenceDataNode)))); - } - - if (FieldInterfaceInfos[i].Reader.Mapping) - { - switchCases.Add(Expression.SwitchCase(Expression.Block(typeof(void), - Expression.Assign(valueVariable, SerializationManager.WrapNullableIfNeededExpression(Expression.Call( - managerConst, - "Read", - new []{fieldType, typeof(MappingDataNode), fieldDefinition.Attribute.CustomTypeSerializer}, - Expression.Convert(nodeVariable, typeof(MappingDataNode)), - hookCtxParam, - contextParam, - Expression.Constant(null, typeof(ISerializationManager.InstantiationDelegate<>).MakeGenericType(fieldType)), - Expression.Constant(!isNullable)), nullable))), - Expression.Constant(typeof(MappingDataNode)))); - } - - call = Expression.Switch(ExpressionUtils.GetTypeExpression(nodeVariable), - ExpressionUtils.ThrowExpression($"Unable to read node for {fieldDefinition} as valid."), - switchCases.ToArray()); - - call = Expression.IfThenElse( - Expression.Call(typeof(SerializationManager), "IsNull", Type.EmptyTypes, nodeVariable), - isNullable - ? Expression.Block(typeof(void), - Expression.Assign(valueVariable, - SerializationManager.GetNullExpression(managerConst, fieldType))) - : ExpressionUtils.ThrowExpression(), - call); - } - else - { - call = Expression.Assign(valueVariable, Expression.Call( - managerConst, - "Read", - new[] { fieldDefinition.FieldType }, - nodeVariable, - hookCtxParam, - contextParam, - Expression.Constant(null, typeof(ISerializationManager.InstantiationDelegate<>).MakeGenericType(fieldDefinition.FieldType)), - Expression.Constant(!isNullable))); - } - - call = Expression.Block( - new[] { valueVariable }, - call, - AssignIfNotDefaultExpression(i, targetParam, valueVariable)); - - - if (fieldDefinition.Attribute is DataFieldAttribute dfa) - { - var tagConst = Expression.Constant(dfa.Tag); - - expressions.Add(Expression.Block( - new []{nodeVariable}, - Expression.IfThenElse( - Expression.Call( - mappingDataParam, - typeof(MappingDataNode).GetMethod("TryGet", - new [] { typeof(string), typeof(DataNode).MakeByRefType() })!, - tagConst, - nodeVariable), - call, - dfa.Required - ? ExpressionUtils.ThrowExpression(fieldDefinition.FieldType, tagConst, typeof(T)) - : AssignIfNotDefaultExpression(i, targetParam, Expression.Constant(DefaultValues[i], fieldDefinition.FieldType)) - ))); - } - else - { - expressions.Add(Expression.Block( - new []{nodeVariable}, - Expression.Assign(nodeVariable, mappingDataParam), - call)); - } - } - - return Expression.Lambda( - Expression.Block(expressions), - targetParam, - mappingDataParam, - hookCtxParam, - contextParam).Compile(); - } - - private SerializeDelegateSignature EmitSerializeDelegate(SerializationManager manager) - { - var managerConst = Expression.Constant(manager); - var isServer = manager.DependencyCollection.Resolve().IsServer; - - var objParam = Expression.Parameter(typeof(T)); - var contextParam = Expression.Parameter(typeof(ISerializationContext)); - var alwaysWriteParam = Expression.Parameter(typeof(bool)); - - var expressions = new List(); - var mappingDataVar = Expression.Variable(typeof(MappingDataNode)); - - expressions.Add( - Expression.Assign( - mappingDataVar, - ExpressionUtils.NewExpression() - )); - - for (var i = BaseFieldDefinitions.Length - 1; i >= 0; i--) - { - var fieldDefinition = BaseFieldDefinitions[i]; - - if (fieldDefinition.Attribute.ReadOnly) - { - continue; - } - - if (fieldDefinition.Attribute.ServerOnly && !isServer) - { - continue; - } - - var isNullable = NullableHelper.IsMarkedAsNullable(fieldDefinition.FieldInfo); - - Expression call; - var valueVar = Expression.Variable(fieldDefinition.FieldType); - if (fieldDefinition.Attribute.CustomTypeSerializer != null && FieldInterfaceInfos[i].Writer) - { - var fieldType = fieldDefinition.FieldType.EnsureNotNullableType(); - Expression valueAccess = fieldDefinition.FieldType.IsValueType && isNullable - ? Expression.Variable(fieldType) - : Expression.Convert(valueVar, fieldType); - - call = Expression.Call( - managerConst, - "WriteValue", - new[]{fieldType, fieldDefinition.Attribute.CustomTypeSerializer}, - valueAccess, - alwaysWriteParam, - contextParam, - Expression.Constant(!isNullable)); - - if (fieldDefinition.FieldType.IsValueType && isNullable) - { - var nodeVar = Expression.Variable(typeof(DataNode)); - call = Expression.Block( - new []{nodeVar}, - Expression.IfThenElse( - SerializationManager.StructNullHasValue(valueVar), - Expression.Block( - new[] { (ParameterExpression)valueAccess }, - Expression.Assign(valueAccess, Expression.Convert(valueVar, fieldType)), - Expression.Assign(nodeVar, SerializationManager.WrapNullableIfNeededExpression(call, true))), - isNullable - ? Expression.Assign(nodeVar, Expression.Constant(ValueDataNode.Null())) - : ExpressionUtils.ThrowExpression()), - nodeVar); - } - } - else - { - call = Expression.Call( - managerConst, - "WriteValue", - new[] { fieldDefinition.FieldType }, - valueVar, - alwaysWriteParam, - contextParam, - Expression.Constant(!isNullable)); - } - - Expression writeExpression; - var nodeVariable = Expression.Variable(typeof(DataNode)); - if (fieldDefinition.Attribute is DataFieldAttribute dfa) - { - writeExpression = Expression.IfThen(Expression.Not(Expression.Call( - mappingDataVar, - typeof(MappingDataNode).GetMethod("Has", new[] { typeof(string) })!, - Expression.Constant(dfa - .Tag))), //check if this node was already written by a type higher up the includetree - Expression.Call( - mappingDataVar, - typeof(MappingDataNode).GetMethod("Add", new[] { typeof(string), typeof(DataNode) })!, - Expression.Constant(dfa.Tag), - nodeVariable)); - } - else - { - writeExpression = Expression.IfThenElse(Expression.TypeIs(nodeVariable, typeof(MappingDataNode)), - Expression.Call( - mappingDataVar, - "Insert", - Type.EmptyTypes, - Expression.Convert(nodeVariable, typeof(MappingDataNode)), - Expression.Constant(true)), - ExpressionUtils.ThrowExpression( - $"Writing field {fieldDefinition} for type {typeof(T)} did not return a {nameof(MappingDataNode)} but was annotated to be included.")); - } - - writeExpression = Expression.Block( - new[] { nodeVariable }, - Expression.Assign(nodeVariable, call), - writeExpression); - - if (fieldDefinition.Attribute is not DataFieldAttribute { Required: true }) - { - expressions.Add(Expression.Block( - new []{valueVar}, - Expression.Assign(valueVar, AccessExpression(i, objParam)), - Expression.IfThen( - Expression.OrElse(alwaysWriteParam, - Expression.Not(IsDefault(i, valueVar, fieldDefinition))), - writeExpression))); - } - else - { - expressions.Add( - Expression.Block( - new []{valueVar}, - Expression.Assign(valueVar, AccessExpression(i, objParam)), - writeExpression)); - } - } - - expressions.Add(mappingDataVar); - - return Expression.Lambda( - Expression.Block( - new []{mappingDataVar}, - expressions), - objParam, - contextParam, - alwaysWriteParam).Compile(); - } - - private CopyDelegateSignature EmitCopyDelegate(SerializationManager manager) - { - var managerConst = Expression.Constant(manager); - var isServer = manager.DependencyCollection.Resolve().IsServer; - - var sourceParam = Expression.Parameter(typeof(T)); - var targetParam = Expression.Parameter(typeof(T).MakeByRefType()); - var contextParam = Expression.Parameter(typeof(ISerializationContext)); - var hookCtxParam = Expression.Parameter(typeof(SerializationHookContext)); - - var expressions = new List(); - - for (var i = 0; i < BaseFieldDefinitions.Length; i++) - { - var fieldDefinition = BaseFieldDefinitions[i]; - - if (fieldDefinition.Attribute.ServerOnly && !isServer) - { - continue; - } - - var isNullable = NullableHelper.IsMarkedAsNullable(fieldDefinition.FieldInfo); - - Expression call; - if (fieldDefinition.Attribute.CustomTypeSerializer != null && FieldInterfaceInfos[i].Copier) - { - var targetValue = Expression.Variable(fieldDefinition.FieldType.EnsureNotNullableType()); - var finalTargetValue = Expression.Variable(fieldDefinition.FieldType); - var fieldType = fieldDefinition.FieldType.EnsureNotNullableType(); - - var sourceAccess = fieldType.IsValueType && isNullable - ? Expression.Variable(fieldType) - : AccessExpression(i, sourceParam); - - call = Expression.Block( - Expression.Call( - managerConst, - "CopyTo", - new[]{fieldType, fieldDefinition.Attribute.CustomTypeSerializer}, - sourceAccess, - targetValue, - hookCtxParam, - contextParam, - Expression.Constant(!isNullable)), - Expression.Assign(finalTargetValue, Expression.Convert(targetValue, fieldDefinition.FieldType))); - - //null check for non-value types is handled in copyto. we are just making sure the types match up - if (isNullable && fieldType.IsValueType) - { - var sourceValue = Expression.Variable(fieldDefinition.FieldType); - call = Expression.Block( - new[] { sourceValue, (ParameterExpression)sourceAccess }, - Expression.Assign(sourceValue, AccessExpression(i, sourceParam)), - Expression.IfThenElse(SerializationManager.StructNullHasValue(sourceValue), - Expression.Block( - Expression.Assign(sourceAccess, Expression.Convert(sourceValue, fieldType)), - call), - Expression.Assign(finalTargetValue, - SerializationManager.GetNullExpression(managerConst, fieldType)))); - } - - call = Expression.Block( - new[] { finalTargetValue, targetValue }, - Expression.Assign(targetValue, manager.InstantiationExpression(managerConst, fieldDefinition.FieldType.EnsureNotNullableType())), - call, - finalTargetValue); - } - else if (fieldDefinition.Attribute.CustomTypeSerializer != null && FieldInterfaceInfos[i].CopyCreator) - { - call = Expression.Call( - managerConst, - "CreateCopy", - new []{fieldDefinition.FieldType, fieldDefinition.Attribute.CustomTypeSerializer}, - AccessExpression(i, sourceParam), - hookCtxParam, - contextParam, - Expression.Constant(!isNullable)); - } - else - { - call = Expression.Call( - managerConst, - "CreateCopy", - new[] { fieldDefinition.FieldType }, - AccessExpression(i, sourceParam), - hookCtxParam, - contextParam, - Expression.Constant(!isNullable)); - } - - expressions.Add(AssignIfNotDefaultExpression(i, targetParam, call)); - } - - return Expression.Lambda( - Expression.Block(expressions), - sourceParam, - targetParam, - hookCtxParam, - contextParam).Compile(); - } - - private ValidateFieldDelegate EmitFieldValidationDelegate(SerializationManager manager, int i) - { - var managerConst = Expression.Constant(manager); - - var nodeParam = Expression.Parameter(typeof(DataNode)); - var contextParam = Expression.Parameter(typeof(ISerializationContext)); - - var field = BaseFieldDefinitions[i]; - var interfaceInfo = FieldInterfaceInfos[i]; - - var fieldType = field.FieldType.EnsureNotNullableType(); - - var switchCases = new List(); - if (interfaceInfo.Validator.Value) - { - switchCases.Add(Expression.SwitchCase( - Expression.Call( - managerConst, - "ValidateNode", - new []{fieldType, typeof(ValueDataNode), field.Attribute.CustomTypeSerializer!}, - Expression.Convert(nodeParam, typeof(ValueDataNode)), - contextParam), - Expression.Constant(typeof(ValueDataNode)))); - } - - if (interfaceInfo.Validator.Sequence) - { - switchCases.Add(Expression.SwitchCase( - Expression.Call( - managerConst, - "ValidateNode", - new []{fieldType, typeof(SequenceDataNode), field.Attribute.CustomTypeSerializer!}, - Expression.Convert(nodeParam, typeof(SequenceDataNode)), - contextParam), - Expression.Constant(typeof(SequenceDataNode)))); - } - - if (interfaceInfo.Validator.Mapping) - { - switchCases.Add(Expression.SwitchCase( - Expression.Call( - managerConst, - "ValidateNode", - new []{fieldType, typeof(MappingDataNode), field.Attribute.CustomTypeSerializer!}, - Expression.Convert(nodeParam, typeof(MappingDataNode)), - contextParam), - Expression.Constant(typeof(MappingDataNode)))); - } - - var @switch = Expression.Switch(ExpressionUtils.GetTypeExpression(nodeParam), - Expression.Call( - managerConst, - "ValidateNode", - new[] { fieldType }, - nodeParam, - contextParam), - switchCases.ToArray()); - - return Expression.Lambda( - @switch, - nodeParam, - contextParam).Compile(); - } - - private Expression AssignIfNotDefaultExpression(int i, Expression obj, Expression value) - { - var assigner = FieldAssigners[i]; - Expression assignerExpr; - - if (assigner is FieldInfo fieldInfo) - assignerExpr = Expression.Assign(Expression.Field(obj, fieldInfo), value); - else if (assigner is MethodInfo methodInfo) - assignerExpr = Expression.Call(obj, methodInfo, value); - else - assignerExpr = Expression.Invoke(Expression.Constant(assigner), obj, value); - - return Expression.IfThen( - Expression.Not(ExpressionUtils.EqualExpression( - Expression.Constant(DefaultValues[i], BaseFieldDefinitions[i].FieldType), value)), - assignerExpr); - } - - private Expression AccessExpression(int i, Expression obj) - { - var accessor = FieldAccessors[i]; - if (accessor is FieldInfo fieldInfo) - return Expression.Field(obj, fieldInfo); - - if (accessor is MethodInfo methodInfo) - return Expression.Call(obj, methodInfo); - - return Expression.Invoke(Expression.Constant(accessor), obj); - } - - private Expression IsDefault(int i, Expression left, FieldDefinition fieldDefinition) - { - return ExpressionUtils.EqualExpression(left, Expression.Constant(DefaultValues[i], fieldDefinition.FieldType)); - } - } -} diff --git a/Robust.Shared/Serialization/Manager/Definition/DataDefinition.cs b/Robust.Shared/Serialization/Manager/Definition/DataDefinition.cs index d7ab422540f..dfac4a42339 100644 --- a/Robust.Shared/Serialization/Manager/Definition/DataDefinition.cs +++ b/Robust.Shared/Serialization/Manager/Definition/DataDefinition.cs @@ -1,217 +1,119 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using System.Linq; using JetBrains.Annotations; -using Robust.Shared.Log; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Exceptions; using Robust.Shared.Serialization.Markdown.Mapping; -using Robust.Shared.Serialization.Markdown.Sequence; using Robust.Shared.Serialization.Markdown.Validation; using Robust.Shared.Serialization.Markdown.Value; -using Robust.Shared.Serialization.TypeSerializers.Interfaces; using Robust.Shared.Utility; -using YamlDotNet.Serialization.NamingConventions; using static Robust.Shared.Serialization.Manager.SerializationManager; namespace Robust.Shared.Serialization.Manager.Definition { public abstract class DataDefinition { - internal ImmutableArray BaseFieldDefinitions { get; init; } +#pragma warning disable CS0618 + internal ImmutableArray BaseFieldDefinitions { get; init; } internal bool IsRecord { get; init; } + internal abstract PopulateDelegateSignature PopulateObj { get; init; } + internal abstract ISerializationManager.InstantiationDelegate InstantiateObj { get; init; } +#pragma warning restore CS0618 - public abstract bool TryGetDuplicates([NotNullWhen(true)] out string[] duplicates); + public abstract bool TryGetDuplicates(out string[] duplicates); } - internal sealed partial class DataDefinition : DataDefinition where T : notnull + internal sealed class DataDefinition : DataDefinition where T : ISerializationGenerated { - private readonly struct FieldInterfaceInfo - { - public readonly (bool Value, bool Sequence, bool Mapping) Reader; - public readonly bool Writer; - public readonly bool Copier; - public readonly bool CopyCreator; - public readonly (bool Value, bool Sequence, bool Mapping) Validator; - - - public FieldInterfaceInfo((bool Value, bool Sequence, bool Mapping) reader, bool writer, bool copier, bool copyCreator, (bool Value, bool Sequence, bool Mapping) validator) - { - Reader = reader; - Writer = writer; - Copier = copier; - CopyCreator = copyCreator; - Validator = validator; - } - } +#pragma warning disable CS0618 + internal readonly PopulateDelegateSignature Populate; + internal readonly SerializeDelegateSignature Serialize; + internal readonly CopyDelegateSignature CopyTo; - internal readonly PopulateDelegateSignature Populate; - internal readonly SerializeDelegateSignature Serialize; - internal readonly CopyDelegateSignature CopyTo; + internal override PopulateDelegateSignature PopulateObj { get; init; } +#pragma warning restore CS0618 + internal override ISerializationManager.InstantiationDelegate InstantiateObj { get; init; } [UsedImplicitly] internal DataDefinition(SerializationManager manager, bool isRecord) { IsRecord = isRecord; - var fieldDefs = GetFieldDefinitions(manager, isRecord); - foreach (var field in fieldDefs) +#pragma warning disable CS0618 + var fieldDefs = new List(); + T.GetFieldDefinitions(default, fieldDefs); +#pragma warning restore CS0618 + for (var i = 0; i < fieldDefs.Count; i++) { - if (field.Attribute is not DataFieldAttribute attribute || - attribute.Tag != null) - { - continue; - } - - attribute.Tag = DataDefinitionUtility.AutoGenerateTag(field.FieldInfo.Name); + var field = fieldDefs[i]; + if (field is { IsDataField: true, Tag: null }) + field.Tag = DataDefinitionUtility.AutoGenerateTag(field.FieldInfoName); } + fieldDefs.Sort((a, b) => + { + var priority = b.Priority.CompareTo(a.Priority); + if (priority != 0) + return priority; + + return b.FieldInfoName.CompareTo(a.FieldInfoName, StringComparison.OrdinalIgnoreCase); + }); + var dataFields = fieldDefs - .Select(f => f.Attribute) - .OfType().ToArray(); + .Where(f => f.IsDataField) + .Select(f => f.Tag ?? f.CamelCasedName) + .ToArray(); Duplicates = dataFields .Where(f => - dataFields.Count(df => df.Tag == f.Tag) > 1) - .Select(f => f.Tag!) + dataFields.Count(df => df == f) > 1) .Distinct() .ToArray(); - var fields = fieldDefs; - - fields.Sort((a, b) => b.Attribute.Priority.CompareTo(a.Attribute.Priority)); - - BaseFieldDefinitions = fields.ToImmutableArray(); - - DefaultValues = fieldDefs.Select(f => f.DefaultValue).ToArray(); - var fieldAssigners = new object[BaseFieldDefinitions.Length]; - var fieldAccessors = new object[BaseFieldDefinitions.Length]; - var fieldValidators = new ValidateFieldDelegate[BaseFieldDefinitions.Length]; - - var interfaceInfos = new FieldInterfaceInfo[BaseFieldDefinitions.Length]; - - for (var i = 0; i < BaseFieldDefinitions.Length; i++) - { - var fieldDefinition = BaseFieldDefinitions[i]; - fieldAssigners[i] = InternalReflectionUtils.EmitFieldAssigner(typeof(T), fieldDefinition.BackingField); - fieldAccessors[i] = InternalReflectionUtils.EmitFieldAccessor(typeof(T), fieldDefinition); - - if (fieldDefinition.Attribute.CustomTypeSerializer != null) - { - //reader (value, sequence, mapping), writer, copier - var reader = (false, false, false); - var writer = false; - var copier = false; - var copyCreator = false; - var validator = (false, false, false); - foreach (var @interface in fieldDefinition.Attribute.CustomTypeSerializer.GetInterfaces()) - { - DebugTools.Assert(@interface.IsGenericType, $"Tried to use a custom type serializer for {GetType()} that isn't generic?"); - var genericTypedef = @interface.GetGenericTypeDefinition(); - if (genericTypedef == typeof(ITypeWriter<>)) - { - if (@interface.GenericTypeArguments[0].IsAssignableTo(fieldDefinition.FieldType)) - { - writer = true; - } - } - else if (genericTypedef == typeof(ITypeCopier<>)) - { - if (@interface.GenericTypeArguments[0].IsAssignableTo(fieldDefinition.FieldType)) - { - copier = true; - } - } - else if (genericTypedef == typeof(ITypeCopyCreator<>)) - { - if (@interface.GenericTypeArguments[0].IsAssignableTo(fieldDefinition.FieldType)) - { - copyCreator = true; - } - } - else if (genericTypedef == typeof(ITypeReader<,>)) - { - if (@interface.GenericTypeArguments[0].IsAssignableTo(fieldDefinition.FieldType)) - { - if (@interface.GenericTypeArguments[1] == typeof(ValueDataNode)) - { - reader.Item1 = true; - } - else if (@interface.GenericTypeArguments[1] == typeof(SequenceDataNode)) - { - reader.Item2 = true; - } - else if (@interface.GenericTypeArguments[1] == typeof(MappingDataNode)) - { - reader.Item3 = true; - } - } - } - else if (genericTypedef == typeof(ITypeValidator<,>)) - { - if (@interface.GenericTypeArguments[0].IsAssignableTo(fieldDefinition.FieldType)) - { - if (@interface.GenericTypeArguments[1] == typeof(ValueDataNode)) - { - validator.Item1 = true; - } - else if (@interface.GenericTypeArguments[1] == typeof(SequenceDataNode)) - { - validator.Item2 = true; - } - else if (@interface.GenericTypeArguments[1] == typeof(MappingDataNode)) - { - validator.Item3 = true; - } - } - } - } + BaseFieldDefinitions = fieldDefs.ToImmutableArray(); - if (!reader.Item1 && !reader.Item2 && !reader.Item3 && !writer && !copier && !validator.Item1 && !validator.Item2 && !validator.Item3) - { - throw new InvalidOperationException( - $"Could not find any fitting implementation of ITypeReader, ITypeWriter or ITypeCopier for field {fieldDefinition}({fieldDefinition.FieldType}) on type {typeof(T)} on CustomTypeSerializer {fieldDefinition.Attribute.CustomTypeSerializer}"); - } + DefaultValues = fieldDefs.Select(f => f.DefaultValue).ToImmutableArray(); + DefaultValuesDict = fieldDefs.ToImmutableDictionary( + f => f.IsDataField + ? f.Tag ?? f.CamelCasedName + : f.CamelCasedName, + f => f.DefaultValue + ); - interfaceInfos[i] = new FieldInterfaceInfo(reader, writer, copier, copyCreator, validator); - } - } - - FieldInterfaceInfos = interfaceInfos.ToImmutableArray(); - FieldAssigners = fieldAssigners.ToImmutableArray(); - FieldAccessors = fieldAccessors.ToImmutableArray(); - - for (int i = 0; i < BaseFieldDefinitions.Length; i++) + //has to be done after fieldinterfaceinfos are done +#pragma warning disable CS0618 + // TODO source gen this one too! + PopulateObj = (ref target, node, serialization, ctx, context) => { - //has to be done after fieldinterfaceinfos are done - fieldValidators[i] = EmitFieldValidationDelegate(manager, i); - } - - FieldValidators = fieldValidators.ToImmutableArray(); - - Populate = EmitPopulateDelegate(manager); - Serialize = EmitSerializeDelegate(manager); - CopyTo = EmitCopyDelegate(manager); + var obj = (T) target; + T.Read(ref obj, node, serialization, ctx, context); + target = obj; + }; + + Populate = T.Read; + Serialize = T.Write; + CopyTo = (source, ref target, ctx, context) => source.Copy(ref target, manager, ctx, context); // TODO source gen this one too! + FieldValidators = T.Validate; + InstantiateObj = T.StaticInstantiateObject; +#pragma warning restore CS0618 } private string[] Duplicates { get; } - private object?[] DefaultValues { get; } - - private ImmutableArray FieldInterfaceInfos { get; } - - private ImmutableArray FieldAssigners { get; } - private ImmutableArray FieldAccessors { get; } + internal ImmutableArray DefaultValues { get; } + internal ImmutableDictionary DefaultValuesDict { get; } - private ImmutableArray FieldValidators { get; } +#pragma warning disable CS0618 + private ValidateAllFieldsDelegate FieldValidators { get; } +#pragma warning restore CS0618 private bool TryGetIndex(string tag, out int index) { for (index = 0; index < BaseFieldDefinitions.Length; index++) { - if (BaseFieldDefinitions[index].Attribute is DataFieldAttribute dataFieldAttribute && - dataFieldAttribute.Tag == tag) + var field = BaseFieldDefinitions[index]; + if (field.IsDataField && field.Tag == tag) return true; } @@ -223,8 +125,9 @@ private bool TryGetIncludeMappingPair(List includeValidati foreach (var includeValidation in includeValidations) { if (includeValidation.Mapping.TryFirstOrNull(x => - x.Key is ValidatedValueNode valVal && valVal.DataNode is ValueDataNode valNode && - valNode.Value == key, out var validatedPair)) + x.Key is ValidatedValueNode { DataNode: ValueDataNode valNode } && + valNode.Value == key, + out var validatedPair)) { pair = validatedPair.Value; return true; @@ -241,18 +144,19 @@ public ValidationNode Validate( ISerializationContext? context) { var validatedMapping = new Dictionary(); - var includeValidations = new List(); - for (var i = 0; i < BaseFieldDefinitions.Length; i++) + var validations = new Dictionary(); + FieldValidators(validations, mapping, serialization, context); + + foreach (var fieldDefinition in BaseFieldDefinitions) { - var fieldDefinition = BaseFieldDefinitions[i]; - if (fieldDefinition.Attribute is not IncludeDataFieldAttribute) continue; + if (!fieldDefinition.IsIncludeDataField) continue; - var validationNode = FieldValidators[i](mapping, context); + var validationNode = validations[fieldDefinition.CamelCasedName]; if (validationNode is ErrorNode errorNode) { - validatedMapping.Add(new InconclusiveNode(new ValueDataNode($"<{nameof(IncludeDataFieldAttribute)}={fieldDefinition.FieldInfo.Name}>")), errorNode); + validatedMapping.Add(new InconclusiveNode(new ValueDataNode($"<{nameof(IncludeDataFieldAttribute)}={fieldDefinition.FieldInfoName}>")), errorNode); continue; } @@ -285,7 +189,7 @@ public ValidationNode Validate( ValidationNode valNode; if (IsNull(val)) { - if (!NullableHelper.IsMarkedAsNullable(BaseFieldDefinitions[idx].FieldInfo)) + if (!BaseFieldDefinitions[idx].FieldInfoNullable) { var error = new ErrorNode( val, @@ -299,7 +203,7 @@ public ValidationNode Validate( } else { - valNode = FieldValidators[idx](val, context); + valNode = validations[key]; } //include node errors override successful nodes on the main datadef @@ -318,128 +222,10 @@ public ValidationNode Validate( return new ValidatedMappingNode(validatedMapping); } - public override bool TryGetDuplicates([NotNullWhen(true)] out string[] duplicates) + public override bool TryGetDuplicates(out string[] duplicates) { duplicates = Duplicates; return duplicates.Length > 0; } - - private bool GatherFieldData(AbstractFieldInfo fieldInfo, out DataFieldBaseAttribute? dataFieldBaseAttribute, - [NotNullWhen(true)]out AbstractFieldInfo? backingField, [NotNullWhen(true)] ref InheritanceBehavior? inheritanceBehavior) - { - dataFieldBaseAttribute = null; - backingField = fieldInfo; - inheritanceBehavior ??= InheritanceBehavior.Default; - - if (fieldInfo.HasAttribute(true)) - inheritanceBehavior = InheritanceBehavior.Always; - else if (fieldInfo.HasAttribute(true)) - inheritanceBehavior = InheritanceBehavior.Never; - - if (fieldInfo is SpecificPropertyInfo propertyInfo) - { - // We only want the most overriden instance of a property for the type we are working with - if (!propertyInfo.IsMostOverridden(typeof(T))) - return false; - - if (propertyInfo.PropertyInfo.GetMethod == null) - { - Logger.ErrorS(LogCategory, $"Property {propertyInfo} is annotated with DataFieldAttribute but has no getter"); - return false; - } - } - - // Most data fields have an explicit data field attribute - if (fieldInfo.TryGetAttribute(out var dataFieldAttribute, true)) - return GatherDataFieldData(fieldInfo, out dataFieldBaseAttribute, ref backingField, dataFieldAttribute); - - if (fieldInfo.TryGetAttribute(out var includeDataFieldAttribute, true)) - { - dataFieldBaseAttribute = includeDataFieldAttribute; - return true; - } - - // This field/property has no explicit data field related annotations. However, things like - // DataRecordAttribute will cause all fields to be interpreted as data fields, so we still handle them - - if (fieldInfo is not SpecificPropertyInfo) - return true; - - var potentialBackingField = fieldInfo.GetBackingField(); - if (potentialBackingField == null) - return false; - - return GatherFieldData(potentialBackingField, - out dataFieldBaseAttribute, - out backingField, - ref inheritanceBehavior); - } - - private static bool GatherDataFieldData( - AbstractFieldInfo fieldInfo, - out DataFieldBaseAttribute dataFieldBaseAttribute, - ref AbstractFieldInfo backingField, - DataFieldAttribute dataFieldAttribute) - { - dataFieldBaseAttribute = dataFieldAttribute; - - if (fieldInfo is not SpecificPropertyInfo property - || dataFieldAttribute.ReadOnly - || property.PropertyInfo.SetMethod != null) - { - return true; - } - - if (!property.TryGetBackingField(out var backingFieldInfo)) - { - Logger.ErrorS(LogCategory, $"Property {property} in type {property.DeclaringType} is annotated with DataFieldAttribute as non-readonly but has no auto-setter"); - return false; - } - - backingField = backingFieldInfo; - return true; - } - - private List GetFieldDefinitions(SerializationManager manager, bool isRecord) - { - var dummyObject = manager.GetOrCreateInstantiator(isRecord)(); - var fieldDefinitions = new List(); - - foreach (var abstractFieldInfo in typeof(T).GetAllPropertiesAndFields()) - { - if (abstractFieldInfo.IsBackingField()) - continue; - - if (isRecord && abstractFieldInfo.IsAutogeneratedRecordMember()) - continue; - - InheritanceBehavior? inheritanceBehavior = InheritanceBehavior.Default; - if (!GatherFieldData(abstractFieldInfo, out var dataFieldBaseAttribute, out var backingField, - ref inheritanceBehavior)) - continue; - - if (dataFieldBaseAttribute == null) - { - if (!isRecord) - continue; - - dataFieldBaseAttribute = new DataFieldAttribute(CamelCaseNamingConvention.Instance.Apply(abstractFieldInfo.Name)); - } - - var fieldDefinition = new FieldDefinition( - dataFieldBaseAttribute, - abstractFieldInfo.GetValue(dummyObject), - abstractFieldInfo, - backingField, - inheritanceBehavior.Value); - - fieldDefinitions.Add(fieldDefinition); - } - - // There should be no duplicates - // I.e., we haven't accidentally included a property's backing field twice? - DebugTools.Assert(fieldDefinitions.Select(x=> x.FieldInfo).Distinct().Count() == fieldDefinitions.Count); - return fieldDefinitions; - } } } diff --git a/Robust.Shared/Serialization/Manager/Definition/DataDefinitionDelegates.cs b/Robust.Shared/Serialization/Manager/Definition/DataDefinitionDelegates.cs new file mode 100644 index 00000000000..278fda6c4e5 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Definition/DataDefinitionDelegates.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Mapping; +using Robust.Shared.Serialization.Markdown.Validation; + +namespace Robust.Shared.Serialization.Manager.Definition; + +//todo paul make these use the mngr delegates +[Obsolete("Used only in source generation")] +internal delegate void CopyDelegateSignature( + T source, + ref T target, + SerializationHookContext hookCtx, + ISerializationContext? context); + +[Obsolete("Used only in source generation")] +internal delegate void PopulateDelegateSignature( + ref T target, + MappingDataNode mappingDataNode, + ISerializationManager serialization, + SerializationHookContext hookCtx, + ISerializationContext? context +); + +[Obsolete("Used only in source generation")] +internal delegate void SerializeDelegateSignature( + T obj, + MappingDataNode mapping, + ISerializationManager serialization, + ISerializationContext? context, + bool alwaysWrite, + ImmutableDictionary defaultValues +); + +[Obsolete("Used only in source generation")] +internal delegate void ValidateAllFieldsDelegate( + Dictionary nodes, + MappingDataNode node, + ISerializationManager serialization, + ISerializationContext? context = null +); diff --git a/Robust.Shared/Serialization/Manager/Definition/DataDefinitionUtility.cs b/Robust.Shared/Serialization/Manager/Definition/DataDefinitionUtility.cs index 72aec126e46..f16632ad7db 100644 --- a/Robust.Shared/Serialization/Manager/Definition/DataDefinitionUtility.cs +++ b/Robust.Shared/Serialization/Manager/Definition/DataDefinitionUtility.cs @@ -6,6 +6,9 @@ public static class DataDefinitionUtility { public static string AutoGenerateTag(string name) { + if (name == "ID") + return "id"; + var span = name.AsSpan(); return $"{char.ToLowerInvariant(span[0])}{span.Slice(1).ToString()}"; } diff --git a/Robust.Shared/Serialization/Manager/Definition/DataFieldDefinition.cs b/Robust.Shared/Serialization/Manager/Definition/DataFieldDefinition.cs new file mode 100644 index 00000000000..5d581fbc6e9 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Definition/DataFieldDefinition.cs @@ -0,0 +1,27 @@ +using System; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Utility; +using YamlDotNet.Serialization.NamingConventions; + +namespace Robust.Shared.Serialization.Manager.Definition; + +[Obsolete("Only used in serialization source generators")] +public record struct DataFieldDefinition( + string? Tag, + int Priority, + bool IsDataField, + bool IsIncludeDataField, + object? DefaultValue, + InheritanceBehavior InheritanceBehavior, + string FieldInfoName, + Type FieldType, + bool FieldInfoNullable, + string CamelCasedName, + Type? CustomTypeSerializer +) +{ + public override string ToString() + { + return $"{FieldInfoName}({Tag ?? CamelCasedName})"; + } +} diff --git a/Robust.Shared/Serialization/Manager/Definition/FieldDefinition.cs b/Robust.Shared/Serialization/Manager/Definition/FieldDefinition.cs deleted file mode 100644 index 1d62d7aa350..00000000000 --- a/Robust.Shared/Serialization/Manager/Definition/FieldDefinition.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Utility; - -namespace Robust.Shared.Serialization.Manager.Definition -{ - internal sealed class FieldDefinition - { - public FieldDefinition( - DataFieldBaseAttribute attr, - object? defaultValue, - AbstractFieldInfo fieldInfo, - AbstractFieldInfo backingField, - InheritanceBehavior inheritanceBehavior) - { - BackingField = backingField; - Attribute = attr; - DefaultValue = defaultValue; - FieldInfo = fieldInfo; - InheritanceBehavior = inheritanceBehavior; - } - - public DataFieldBaseAttribute Attribute { get; } - - public object? DefaultValue { get; } - - public InheritanceBehavior InheritanceBehavior { get; } - - public AbstractFieldInfo BackingField { get; } - - public AbstractFieldInfo FieldInfo { get; } - - public Type FieldType => FieldInfo.FieldType; - - public object? GetValue(object? obj) - { - return BackingField.GetValue(obj); - } - - public void SetValue(object? obj, object? value) - { - BackingField.SetValue(obj, value); - } - - public override string ToString() - { - return $"{FieldInfo.Name}({Attribute})"; - } - } -} diff --git a/Robust.Shared/Serialization/Manager/Definition/InheritanceBehavior.cs b/Robust.Shared/Serialization/Manager/Definition/InheritanceBehavior.cs index b992b19852f..42b6cd97530 100644 --- a/Robust.Shared/Serialization/Manager/Definition/InheritanceBehavior.cs +++ b/Robust.Shared/Serialization/Manager/Definition/InheritanceBehavior.cs @@ -2,8 +2,8 @@ { public enum InheritanceBehavior : byte { - Default, - Always, - Never + Default = 0, + Always = 1, + Never = 2 } } diff --git a/Robust.Shared/Serialization/Manager/ISerializationManager.cs b/Robust.Shared/Serialization/Manager/ISerializationManager.cs index 4abe1d444bb..09cd166b5e1 100644 --- a/Robust.Shared/Serialization/Manager/ISerializationManager.cs +++ b/Robust.Shared/Serialization/Manager/ISerializationManager.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; using Robust.Shared.Reflection; @@ -14,6 +16,8 @@ public interface ISerializationManager { public delegate T InstantiationDelegate(); + bool IsServer { get; } + /// /// Initializes the serialization manager. /// @@ -109,6 +113,7 @@ ValidationNode ValidateNode(TNode node, /// The type of object to create and populate. /// The deserialized object, or null. T Read(DataNode node, ISerializationContext? context = null, bool skipHook = false, InstantiationDelegate? instanceProvider = null, [NotNullableFlag(nameof(T))] bool notNullableOverride = false); + T Read( DataNode node, SerializationHookContext hookCtx, @@ -116,6 +121,34 @@ T Read( InstantiationDelegate? instanceProvider = null, [NotNullableFlag(nameof(T))] bool notNullableOverride = false); + T[] ReadArray( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + InstantiationDelegate? instanceProvider = null, + [NotNullableFlag(nameof(T))] bool notNullableOverride = false); + + T ReadDefinition( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + InstantiationDelegate? instanceProvider = null, + [NotNullableFlag(nameof(T))] bool notNullableOverride = false) where T : ISerializationGenerated; + + T ReadEnum( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + InstantiationDelegate? instanceProvider = null, + [NotNullableFlag(nameof(T))] bool notNullableOverride = false) where T : struct, Enum; + + T ReadStructDefinition( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + InstantiationDelegate? instanceProvider = null, + [NotNullableFlag(nameof(T))] bool notNullableOverride = false) where T : struct, ISerializationGenerated; + /// /// Deserializes a node into a populated object of the given generic type using the provided instance. diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.Composition.cs b/Robust.Shared/Serialization/Manager/SerializationManager.Composition.cs index 13d797637fa..8675fff33c0 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.Composition.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.Composition.cs @@ -181,17 +181,17 @@ private MappingDataNode PushInheritanceDefinition(MappingDataNode child, Mapping { var newMapping = child.Copy(); var processedTags = new HashSet(); - var fieldQueue = new Queue(definition.BaseFieldDefinitions); + var fieldQueue = new Queue(definition.BaseFieldDefinitions); while (fieldQueue.TryDequeue(out var field)) { if (field.InheritanceBehavior == InheritanceBehavior.Never) continue; - if (field.Attribute is DataFieldAttribute dfa) + if (field.IsDataField) { // tag is set on data definition creation - if(!processedTags.Add(dfa.Tag!)) continue; //tag was already processed, probably because we are using the same tag in an include + if(!processedTags.Add(field.Tag!)) continue; //tag was already processed, probably because we are using the same tag in an include - var key = dfa.Tag!; + var key = field.Tag!; if (parent.TryGetValue(key, out var parentValue)) { if (newMapping.TryGetValue(key, out var childValue)) diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.Copying.cs b/Robust.Shared/Serialization/Manager/SerializationManager.Copying.cs index ede48426650..8994e3f696b 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.Copying.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.Copying.cs @@ -109,12 +109,18 @@ static object ValueFactory(Type baseType, Type actualType, SerializationManager Expression.Constant(false)), Expression.Constant(true)); } - else + else if (actualType.IsAssignableTo(typeof(ISerializationGenerated<>).MakeGenericType(actualType))) { call = Expression.Call(instanceParam, nameof(CopyToInternal), new[] { actualType }, sourceVar, targetVar, Expression.Constant(manager.GetDefinition(actualType), typeof(DataDefinition<>).MakeGenericType(actualType)), instanceParam, hookCtxParam, contextParam); } + else + { + call = Expression.Block( + Expression.Assign(targetVar, sourceVar), + Expression.Constant(true)); + } if (!sameType) { @@ -235,7 +241,7 @@ private CreateCopyGenericDelegate GetOrCreateCreateCopyGenericDelegate() contextParam, Expression.Constant(false)), type); } - else + else if (typeof(T).IsAssignableTo(typeof(ISerializationGenerated))) { call = Expression.Call( instanceParam, @@ -246,6 +252,16 @@ private CreateCopyGenericDelegate GetOrCreateCreateCopyGenericDelegate() contextParam, Expression.Constant(manager.GetDefinition(type), typeof(DataDefinition<>).MakeGenericType(type))); } + else + { + call = Expression.Call( + instanceParam, + nameof(CreateCopyInternalNotGenerated), + new[] {type}, + sourceParamAccess, + hookCtxParam, + contextParam); + } } return Expression.Lambda>( @@ -271,13 +287,15 @@ private bool CopyToInternal( ISerializationManager serializationManager, SerializationHookContext hookCtx, ISerializationContext? context) - where TCommon : notnull + where TCommon : ISerializationGenerated { if (context != null && context.SerializerProvider.TryGetTypeSerializer, TCommon>(out var copier)) { var commonTarget = target; copier.CopyTo(this, source, ref commonTarget, DependencyCollection, hookCtx, context); + target = commonTarget; + return true; } if (ShouldReturnSource(typeof(TCommon))) //todo paul can be precomputed @@ -338,7 +356,7 @@ private T[] CreateArrayCopy(T[] source, SerializationHookContext hookCtx, ISe return copy; } - private T CreateCopyInternal(T source, SerializationHookContext hookCtx, ISerializationContext context, DataDefinition? definition) where T : notnull + private T CreateCopyInternal(T source, SerializationHookContext hookCtx, ISerializationContext context, DataDefinition? definition) where T : ISerializationGenerated { if (source is DataNode node) return (T)(object)node.Copy(); @@ -370,6 +388,26 @@ private T CreateCopyInternal(T source, SerializationHookContext hookCtx, ISer } } + private T CreateCopyInternalNotGenerated(T source, SerializationHookContext hookCtx, ISerializationContext context) + { + if (source is DataNode node) + return (T)(object)node.Copy(); + + ref readonly var information = ref SerializedType.Information; + if (information.ReturnSource || typeof(T).IsValueType) + { + return source; + } + + var target = GetOrCreateInstantiator(false)(); + + if (!GetOrCreateCopyToGenericDelegate(source)(source, ref target, hookCtx, context)) + { + throw new CopyToFailedException(); + } + return target!; + } + private void NotNullOverrideCheck(bool notNullableOverride, Type? type = null) { if (notNullableOverride || (type != null && !type.IsNullable())) throw new NullNotAllowedException(); @@ -397,13 +435,6 @@ public void CopyTo( return; } - if (source is ISerializationGenerated generated) - { - generated.Copy(ref target!, this, hookCtx, context); - RunAfterHook(target, hookCtx); - return; - } - if (target == null) { target = CreateCopy(source, hookCtx, context); @@ -438,7 +469,7 @@ public void CopyTo( } ref readonly var information = ref SerializedType.Information; - if (information.SerializationGenerated) + if (information.SerializationGenerated && !typeof(T).IsAbstract && !typeof(T).IsInterface) { var generated = Unsafe.As>(source); target ??= generated.Instantiate(); diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.Instantiation.cs b/Robust.Shared/Serialization/Manager/SerializationManager.Instantiation.cs index 4202e9494cb..eeefefe85a0 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.Instantiation.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.Instantiation.cs @@ -47,11 +47,21 @@ private static void CreateClassInstantiator(ILGenerator generator, Type type) generator.Emit(OpCodes.Ret); } + /// + /// This generates IL code that will try to invoke a record's constructor by passing default values to any arguments. + /// private static void CreateRecordInstantiator(ILGenerator generator, Type type) { var constructors = type.GetConstructors(); if (constructors.Length == 0) - throw new ArgumentException($"Could not find a constructor for record class {type}"); + { + if (!type.IsValueType) + throw new ArgumentException($"Could not find a constructor for record class {type}"); + + // Handle constructorless data record struct by treating it like a normal struct + CreateValueTypeInstantiator(generator, type); + return; + } var constructor = constructors[0]; foreach (var parameter in constructor.GetParameters()) @@ -60,11 +70,61 @@ private static void CreateRecordInstantiator(ILGenerator generator, Type type) if (parameterType.IsPrimitive) { - var defaultValue = Convert.ToInt64(parameter.HasDefaultValue ? parameter.DefaultValue! : 0); - generator.Emit(OpCodes.Ldc_I4, defaultValue); - if (parameterType == typeof(long) || parameterType == typeof(ulong)) - generator.Emit(OpCodes.Conv_I8); + if (parameterType == typeof(decimal)) + { + // I CBF figuring out how to support them, so fuck it. + throw new NotSupportedException($"Record class {type} contains decimals. DataRecords don't currently support decimals."); + // If anyone wants to try, a value of 0 looks like this in IL: + // > ldsfld valuetype [System.Runtime]System.Decimal [System.Runtime]System.Decimal::Zero + // While a default value of -1 uses another static field: + // > ldsfld valuetype [System.Runtime]System.Decimal [System.Runtime]System.Decimal::MinusOne + } + + if (parameterType == typeof(float)) + { + var floatDefault = parameter.HasDefaultValue ? (float) parameter.DefaultValue! : 0f; + generator.Emit(OpCodes.Ldc_R4, floatDefault); + } + else if (parameterType == typeof(double)) + { + var doubleDefault = parameter.HasDefaultValue ? (double) parameter.DefaultValue! : 0d; + generator.Emit(OpCodes.Ldc_R8, doubleDefault); + } + else if (parameterType == typeof(nint) || parameterType == typeof(nuint)) + { + int nintDefault = parameter.HasDefaultValue ? (int)Convert.ToInt64(parameter.DefaultValue) : 0; + generator.Emit(OpCodes.Ldc_I4, nintDefault); + + if (parameterType == typeof(nuint) && nintDefault < 0) // I'm only like 50% sure this is correct, but it makes the tests pass, so.... + generator.Emit(OpCodes.Conv_U); + else + generator.Emit(OpCodes.Conv_I); + } + else if (parameterType == typeof(long) || parameterType == typeof(ulong)) + { + var longDefault = 0L; + if (parameter.HasDefaultValue) + { + longDefault = parameterType == typeof(ulong) + ? (long) (ulong) parameter.DefaultValue! + : Convert.ToInt64(parameter.DefaultValue); + } + + generator.Emit(OpCodes.Ldc_I8, longDefault); + } + else + { + var intDefault = 0; + if (parameter.HasDefaultValue) + { + intDefault = parameterType == typeof(uint) + ? (int) (uint) parameter.DefaultValue! + : Convert.ToInt32(parameter.DefaultValue); + } + + generator.Emit(OpCodes.Ldc_I4, intDefault); + } } else if (parameterType.IsValueType) { @@ -96,6 +156,16 @@ internal ISerializationManager.InstantiationDelegate GetOrCreateInstantiator< return (ISerializationManager.InstantiationDelegate)_instantiators.GetOrAdd(type, static (type, isRecord) => { + var generated = typeof(ISerializationGenerated<>).MakeGenericType(type); + if (type.IsAssignableTo(generated)) + { + var instantiate = type + .GetMethod(nameof(ISerializationGenerated<>.StaticInstantiate), BindingFlags.Public | BindingFlags.Static)! + .CreateDelegate(typeof(ISerializationManager.InstantiationDelegate<>).MakeGenericType(type)); + + return instantiate; + } + var method = new DynamicMethod( "Instantiator", type, @@ -104,13 +174,13 @@ internal ISerializationManager.InstantiationDelegate GetOrCreateInstantiator< var generator = method.GetILGenerator(); - if (type.IsValueType) + if (isRecord) { - CreateValueTypeInstantiator(generator, type); + CreateRecordInstantiator(generator, type); } - else if (isRecord) + else if (type.IsValueType) { - CreateRecordInstantiator(generator, type); + CreateValueTypeInstantiator(generator, type); } else { @@ -120,15 +190,4 @@ internal ISerializationManager.InstantiationDelegate GetOrCreateInstantiator< return method.CreateDelegate(typeof(ISerializationManager.InstantiationDelegate<>).MakeGenericType(type)); }, isDataRecord); } - - //we can safely set isDataRecord to false here due to a delegate already existing if it if it were - private T InstantiateValue() => GetOrCreateInstantiator(false)(); - - internal MethodCallExpression InstantiationExpression(ConstantExpression managerConst, Type type) - { - return Expression.Call( - managerConst, - nameof(InstantiateValue), - new[] { type }); - } } diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.Reading.cs b/Robust.Shared/Serialization/Manager/SerializationManager.Reading.cs index 11b9b7abfc9..7fc5e606fbd 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.Reading.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.Reading.cs @@ -1,8 +1,12 @@ using System; using System.Collections.Concurrent; using System.Diagnostics; +using System.Linq; using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager.Definition; using Robust.Shared.Serialization.Manager.Exceptions; using Robust.Shared.Serialization.Markdown; @@ -30,9 +34,18 @@ private delegate T ReadGenericDelegate( ISerializationContext? context = null, ISerializationManager.InstantiationDelegate? instanceProvider = null); + private delegate object? ReadDelegate( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + bool notNullableOverride = false + ); + + private MethodInfo _read = default!; private readonly ConcurrentDictionary<(Type type, bool notNullableOverride), ReadBoxingDelegate> _readBoxingDelegates = new(); private readonly ConcurrentDictionary<(Type baseType, Type actualType, Type node, bool notNullableOverride), object> _readGenericBaseDelegates = new(); private readonly ConcurrentDictionary<(Type value, Type node, bool notNullableOverride), object> _readGenericDelegates = new(); + private readonly ConcurrentDictionary> _reads = new(); public T Read(DataNode node, ISerializationContext? context = null, bool skipHook = false, ISerializationManager.InstantiationDelegate? instanceProvider = null, bool notNullableOverride = false) { @@ -44,6 +57,492 @@ public T Read(DataNode node, ISerializationContext? context = null, bool skip notNullableOverride); } + public T[] ReadArray( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + ISerializationManager.InstantiationDelegate? instanceProvider = null, + bool notNullableOverride = false) + { + var baseType = typeof(T); + var nullable = baseType.IsNullable(); + var isValueType = baseType.IsValueType; + + T[]? val = null; + if (instanceProvider != null) + val ??= []; + + if (context != null) + { + switch (node) + { + case MappingDataNode mapping: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializerArray, T, MappingDataNode>( + out var serializer)) + { + val = serializer.Read(this, mapping, DependencyCollection, hookCtx, context, instanceProvider); + if (notNullableOverride) + Debug.Assert(val != null, "Reader call returned null value! Forbidden!"); + } + else + { + RegularRead(); + } + break; + } + case SequenceDataNode sequence: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializerArray, T, SequenceDataNode>( + out var serializer)) + { + val = serializer.Read(this, sequence, DependencyCollection, hookCtx, context, instanceProvider); + if (notNullableOverride) + Debug.Assert(val != null, "Reader call returned null value! Forbidden!"); + } + else + { + RegularRead(); + } + break; + } + case ValueDataNode value: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializerArray, T, ValueDataNode>( + out var serializer)) + { + val = serializer.Read(this, value, DependencyCollection, hookCtx, context, instanceProvider); + if (notNullableOverride) + Debug.Assert(val != null, "Reader call returned null value! Forbidden!"); + } + else + { + RegularRead(); + } + break; + } + } + } + else + { + RegularRead(); + } + + if (node.IsNull) + { + if (nullable && !notNullableOverride) + val = null; + else + throw new NullNotAllowedException(); + } + + if (!nullable && !isValueType && val == null) + throw new ReadCallReturnedNullException(); + + return val!; + + void RegularRead() + { + switch (node) + { + case MappingDataNode mapping: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializerArray, T, MappingDataNode>(out var reader)) + { + val = Read(reader, mapping, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case SequenceDataNode sequence: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializerArray, T, SequenceDataNode>(out var reader)) + { + val = Read(reader, sequence, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case ValueDataNode value: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializerArray, T, ValueDataNode>(out var reader)) + { + val = Read(reader, value, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + } + + val = node switch + { + SequenceDataNode sequence => ReadArraySequence(sequence, hookCtx, context), + ValueDataNode value => ReadArrayValue(value, hookCtx, context), + _ => throw new ArgumentException($"Cannot read array from data node type {node.GetType()}") + }; + } + } + + public T ReadStructDefinition( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + ISerializationManager.InstantiationDelegate? instanceProvider = null, + bool notNullableOverride = false) + where T : struct, ISerializationGenerated + { + var baseType = typeof(T); + var nullable = baseType.IsNullable(); + + T val = default!; + + if (instanceProvider != null) + val = instanceProvider.Invoke(); + + if (context != null) + { + switch (node) + { + case MappingDataNode mapping: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, MappingDataNode>( + out var serializer)) + { + val = Read( + serializer, + mapping, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + break; + } + case SequenceDataNode sequence: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, SequenceDataNode>( + out var serializer)) + { + val = Read( + serializer, + sequence, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + break; + } + case ValueDataNode value: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, ValueDataNode>( + out var serializer)) + { + val = Read( + serializer, + value, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + break; + } + } + } + else + { + RegularRead(); + } + + if (node.IsNull) + { + if (nullable && !notNullableOverride) + val = default!; + else if (baseType == typeof(EntityUid)) + val = default!; + else + throw new NullNotAllowedException(); + } + + return val; + + void RegularRead() + { + var hasSerializer = false; + switch (node) + { + case MappingDataNode mapping: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, MappingDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, mapping, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case SequenceDataNode sequence: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, SequenceDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, sequence, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case ValueDataNode value: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, ValueDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, value, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + } + + if (!hasSerializer) + { + if (baseType.IsArray) + { + val = node switch + { + SequenceDataNode sequence => (T)(object)ReadArraySequence(sequence, hookCtx, context), + ValueDataNode value => (T)(object)ReadArrayValue(value, hookCtx, context), + _ => throw new ArgumentException($"Cannot read array from data node type {node.GetType()}") + }; + } + else if (baseType.IsEnum) + { + val = node switch + { + SequenceDataNode sequence => ReadEnumSequence(sequence), + ValueDataNode value => ReadEnumValue(value), + _ => throw new InvalidNodeTypeException( + $"Cannot serialize node as {baseType}, unsupported node type {node.GetType()}") + }; + } + else if (baseType.IsAssignableTo(typeof(ISelfSerialize))) + { + switch (node) + { + case ValueDataNode value: + { + instanceProvider ??= T.StaticInstantiate; + val = instanceProvider(); + var selfSerialize = (ISelfSerialize) val; + selfSerialize.Deserialize(value.Value); + val = (T) selfSerialize; + break; + } + default: + throw new InvalidNodeTypeException($"Cannot read {nameof(ISelfSerialize)} from node type {node.GetType()}. Expected {nameof(ValueDataNode)}"); + } + } + else + { + switch (node) + { + case MappingDataNode mapping: + { + instanceProvider ??= T.StaticInstantiate; + val = instanceProvider(); + T.Read(ref val, mapping, this, hookCtx, context); + break; + } + case ValueDataNode value: + { + instanceProvider ??= T.StaticInstantiate; + val = instanceProvider(); + if (value.Value != string.Empty) + throw new ArgumentException($"No mapping node provided for type {typeof(T)} at line: {node.Start.Line}"); + + break; + } + default: + throw new ArgumentException($"No mapping or value node provided for type {baseType}."); + } + + RunAfterHook(val, hookCtx); + } + } + } + } + + public T ReadEnum( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + ISerializationManager.InstantiationDelegate? instanceProvider = null, + bool notNullableOverride = false) + where T : struct, Enum + { + var baseType = typeof(T); + var nullable = baseType.IsNullable(); + + T val = default!; + + if (instanceProvider != null) + val = instanceProvider.Invoke(); + + if (context != null) + { + switch (node) + { + case MappingDataNode mapping: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, MappingDataNode>( + out var serializer)) + { + val = Read( + serializer, + mapping, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + break; + } + case SequenceDataNode sequence: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, SequenceDataNode>( + out var serializer)) + { + val = Read( + serializer, + sequence, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + break; + } + case ValueDataNode value: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, ValueDataNode>( + out var serializer)) + { + val = Read( + serializer, + value, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + break; + } + } + } + else + { + RegularRead(); + } + + if (node.IsNull) + { + if (nullable && !notNullableOverride) + val = default!; + else + throw new NullNotAllowedException(); + } + + return val; + + void RegularRead() + { + var hasSerializer = false; + switch (node) + { + case MappingDataNode mapping: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, MappingDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, mapping, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case SequenceDataNode sequence: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, SequenceDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, sequence, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case ValueDataNode value: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, ValueDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, value, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + } + + if (hasSerializer) + return; + + val = node switch + { + SequenceDataNode sequence => ReadEnumSequence(sequence), + ValueDataNode value => ReadEnumValue(value), + _ => throw new InvalidNodeTypeException( + $"Cannot serialize node as {baseType}, unsupported node type {node.GetType()}") + }; + } + } + + private object? ReadObject( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + bool notNullableOverride = false) + { + return Read(node, hookCtx, context, null, notNullableOverride); + } + public T Read( DataNode node, SerializationHookContext hookCtx, @@ -64,10 +563,12 @@ public T Read( { if (instanceProvider != null) { - var val = instanceProvider(); + var instantiatedVal = instanceProvider(); //make this debug-only? -(false, type)(); @@ -79,13 +580,431 @@ public T Read( this))(node, hookCtx, context, instanceProvider); } - return ((ReadGenericDelegate)_readGenericDelegates.GetOrAdd((typeof(T), node.GetType()!, notNullableOverride), - static (tuple, manager) => ReadDelegateValueFactory(tuple.value, tuple.value, tuple.node, tuple.notNullableOverride, manager), this))(node, hookCtx, context, instanceProvider); + var baseType = typeof(T); + if (baseType.IsEnum || baseType.IsArray || + (baseType.IsGenericType && baseType.GetGenericTypeDefinition() == typeof(Nullable<>))) + { + return ((ReadGenericDelegate)_readGenericDelegates.GetOrAdd((typeof(T), node.GetType()!, notNullableOverride), + static (tuple, manager) => ReadDelegateValueFactory(tuple.value, tuple.value, tuple.node, tuple.notNullableOverride, manager), this))(node, hookCtx, context, instanceProvider); + } + var nullable = baseType.IsNullable(); + + T val = default!; + if (node.IsNull) + { + if (nullable && !notNullableOverride) + return default!; + + if (baseType == typeof(EntityUid)) + return (T) (object) EntityUid.Invalid; + + throw new NullNotAllowedException(); + } + + if (instanceProvider != null) + val = instanceProvider.Invoke(); + + if (context != null) + { + switch (node) + { + // TODO actual type for type tag + case MappingDataNode mapping: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, MappingDataNode>( + out var serializer)) + { + val = Read( + serializer, + mapping, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + + break; + } + case SequenceDataNode sequence: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, SequenceDataNode>( + out var serializer)) + { + val = Read( + serializer, + sequence, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + + break; + } + case ValueDataNode value: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, ValueDataNode>( + out var serializer)) + { + val = Read( + serializer, + value, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + + break; + } + } + } + else + { + RegularRead(); + } + + var isValueType = baseType.IsValueType; + if (!nullable && !isValueType && val == null) + throw new ReadCallReturnedNullException(); + + return val; + + void RegularRead() + { + var hasSerializer = false; + switch (node) + { + case MappingDataNode mapping: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, MappingDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, mapping, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case SequenceDataNode sequence: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, SequenceDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, sequence, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case ValueDataNode value: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, ValueDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, value, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + } + + if (!hasSerializer) + { + if (baseType.IsAssignableTo(typeof(ISelfSerialize))) + { + switch (node) + { + case ValueDataNode value: + { + instanceProvider ??= GetOrCreateInstantiator(false); + val = instanceProvider(); + var selfSerialize = (ISelfSerialize?) val; + selfSerialize!.Deserialize(value.Value); + val = (T) selfSerialize; + break; + } + default: + throw new InvalidNodeTypeException($"Cannot read {nameof(ISelfSerialize)} from node type {node.GetType()}. Expected {nameof(ValueDataNode)}"); + } + } + else + { + switch (node) + { + case MappingDataNode mapping: + { + var definition = GetDefinition(typeof(T)); + if (definition == null) + throw new ArgumentException($"No data definition found for type {baseType} with node type {node.GetType()} when reading"); + + var valObj = instanceProvider == null ? definition.InstantiateObj() : instanceProvider.Invoke()!; + definition.PopulateObj(ref valObj, mapping, this, hookCtx, context); + val = (T) valObj; + + break; + } + case ValueDataNode value: + { + instanceProvider ??= GetOrCreateInstantiator(false); + val = instanceProvider(); + if (value.Value != string.Empty) + throw new ArgumentException($"No mapping node provided for type {baseType} at line: {node.Start.Line}"); + + break; + } + default: + throw new ArgumentException($"No mapping or value node provided for type {baseType}."); + } + + RunAfterHook(val, hookCtx); + } + } + } + } + + public T ReadDefinition( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + ISerializationManager.InstantiationDelegate? instanceProvider = null, + bool notNullableOverride = false) where T : ISerializationGenerated + { + if (node.Tag?.StartsWith("!type:") ?? false) + { + var type = ResolveConcreteType(typeof(T), node.Tag.Substring(6)); + if (type.IsInterface || type.IsAbstract) + { + throw new ArgumentException($"Interface or abstract type used for !type node. Type: {type}"); + } + + //!type tag overrides null value on default. i did this because i couldnt come up with a usecase where you'd specify the type but have a null value. yell at me if you found one -paul + if (node.IsEmpty || node.IsNull) + { + if (instanceProvider != null) + { + var instantiatedVal = instanceProvider(); + //make this debug-only? -(false, type)(); + } + + return ((ReadGenericDelegate)_readGenericBaseDelegates.GetOrAdd( + (typeof(T), type, node.GetType()!, notNullableOverride), + static (tuple, manager) => ReadDelegateValueFactory(tuple.baseType, tuple.actualType, tuple.node, tuple.notNullableOverride, manager), + this))(node, hookCtx, context, instanceProvider); + } + + var baseType = typeof(T); + var nullable = baseType.IsNullable(); + + T val = default!; + if (node.IsNull) + { + if (nullable && !notNullableOverride) + return default!; + + if (baseType == typeof(EntityUid)) + return (T) (object) EntityUid.Invalid; + + throw new NullNotAllowedException(); + } + + if (instanceProvider != null) + val = instanceProvider.Invoke(); + + if (context != null) + { + switch (node) + { + case MappingDataNode mapping: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, MappingDataNode>( + out var serializer)) + { + val = Read( + serializer, + mapping, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + + break; + } + case SequenceDataNode sequence: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, SequenceDataNode>( + out var serializer)) + { + val = Read( + serializer, + sequence, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + + break; + } + case ValueDataNode value: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, ValueDataNode>( + out var serializer)) + { + val = Read( + serializer, + value, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + + break; + } + } + } + else + { + RegularRead(); + } + + var isValueType = baseType.IsValueType; + if (!nullable && !isValueType && val == null) + throw new ReadCallReturnedNullException(); + + return val; + + void RegularRead() + { + var hasSerializer = false; + switch (node) + { + case MappingDataNode mapping: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, MappingDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, mapping, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case SequenceDataNode sequence: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, SequenceDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, sequence, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case ValueDataNode value: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, ValueDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, value, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + } + + if (!hasSerializer) + { + if (baseType.IsAssignableTo(typeof(ISelfSerialize))) + { + switch (node) + { + case ValueDataNode value: + { + instanceProvider ??= T.StaticInstantiate; + val = instanceProvider(); + var selfSerialize = (ISelfSerialize) val; + selfSerialize.Deserialize(value.Value); + val = (T) selfSerialize; + break; + } + default: + throw new InvalidNodeTypeException($"Cannot read {nameof(ISelfSerialize)} from node type {node.GetType()}. Expected {nameof(ValueDataNode)}"); + } + } + else + { + switch (node) + { + case MappingDataNode mapping: + { + instanceProvider ??= T.StaticInstantiate; + val = instanceProvider(); + T.Read(ref val, mapping, this, hookCtx, context); + break; + } + case ValueDataNode value: + { + instanceProvider ??= T.StaticInstantiate; + val = instanceProvider(); + if (value.Value != string.Empty) + throw new ArgumentException($"No mapping node provided for type {baseType} at line: {node.Start.Line}"); + + break; + } + default: + throw new ArgumentException($"No mapping or value node provided for type {baseType}."); + } + + RunAfterHook(val, hookCtx); + } + } + } } - public T Read(ITypeReader reader, TNode node, ISerializationContext? context = null, - bool skipHook = false, ISerializationManager.InstantiationDelegate? instanceProvider = null, bool notNullableOverride = false) + public T Read( + ITypeReader reader, + TNode node, + ISerializationContext? context = null, + bool skipHook = false, + ISerializationManager.InstantiationDelegate? instanceProvider = null, + bool notNullableOverride = false) where TNode : DataNode { return Read( @@ -328,18 +1247,29 @@ Expression BaseInstantiatorToActual() } else if (nodeType == typeof(MappingDataNode)) { - var definition = manager.GetDefinition(actualType); - var definitionConst = Expression.Constant(definition, typeof(DataDefinition<>).MakeGenericType(actualType)); + if (actualType.IsAssignableTo(typeof(ISerializationGenerated<>).MakeGenericType(actualType))) + { + var definition = manager.GetDefinition(actualType); + var definitionConst = Expression.Constant(definition, typeof(DataDefinition<>).MakeGenericType(actualType)); - call = Expression.Call( - managerConst, - nameof(ReadGenericMapping), - new[] { actualType }, - Expression.Convert(nodeParam, typeof(MappingDataNode)), - definitionConst, - hookCtxParam, - contextParam, - instantiatorVariable); + call = Expression.Call( + managerConst, + nameof(ReadGenericMapping), + new[] { actualType }, + Expression.Convert(nodeParam, typeof(MappingDataNode)), + definitionConst, + hookCtxParam, + contextParam, + instantiatorVariable); + } + else + { + call = Expression.Call( + managerConst, + nameof(ReadNoSerializer), + new[] { actualType }, + nodeParam); + } } else { @@ -508,20 +1438,23 @@ private TValue ReadGenericMapping( SerializationHookContext hookCtx, ISerializationContext? context, ISerializationManager.InstantiationDelegate instanceProvider) - where TValue : notnull + where TValue : ISerializationGenerated { if (definition == null) - { throw new ArgumentException($"No data definition found for type {typeof(TValue)} with node type {node.GetType()} when reading"); - } var instance = instanceProvider(); - definition.Populate(ref instance, node, hookCtx, context); + definition.Populate(ref instance, node, this, hookCtx, context); RunAfterHook(instance, hookCtx); return instance; } + + private TValue ReadNoSerializer(DataNode node) + { + throw new ArgumentException($"No type serializer or data definition found for type {typeof(TValue)} with node type {node.GetType()} when reading"); + } } } diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.SerializerProvider.cs b/Robust.Shared/Serialization/Manager/SerializationManager.SerializerProvider.cs index f4430d182d4..c07b43d37e3 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.SerializerProvider.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.SerializerProvider.cs @@ -71,7 +71,10 @@ public bool TryGetCopierOrCreator(out ITypeCopier? copier, out ITy [Obsolete] public bool TryCustomCopy(T source, ref T target, SerializationHookContext hookCtx, bool hasHooks, ISerializationContext? context = null) { - if (TryGetCopierOrCreator(out var copier, out var copyCreator)) + if (target != null && source is ISerializationGenerated) + return false; + + if (TryGetCopierOrCreator(out var copier, out var copyCreator, context)) { if (copier != null) { @@ -142,6 +145,18 @@ public bool TryGetTypeNodeSerializer([NotNullWhen(true return true; } + internal bool TryGetTypeNodeSerializerArray([NotNullWhen(true)] out TInterface? serializer) + where TInterface : BaseSerializerInterfaces.ITypeNodeInterface + where TNode : DataNode + { + serializer = default; + if (!TryGetTypeNodeSerializer(typeof(TInterface).GetGenericTypeDefinition(), typeof(TType[]), typeof(TNode), out var rawSerializer)) + return false; + + serializer = (TInterface)rawSerializer; + return true; + } + public bool TryGetTypeNodeSerializer(Type interfaceType, Type objectType, Type nodeType, [NotNullWhen(true)] out object? serializer) { lock (_lock) @@ -234,23 +249,29 @@ internal bool TryGetCopierOrCreator(out ITypeCopier? copier, out I copyCreator = null; var information = SerializedType.Information; - if (information.Id >= _typeSerializersArray.Length) - return false; + if (information.Id < _typeSerializersArray.Length && + _typeSerializersArray[information.Id] is { } serializerArray) + { + var copiers = serializerArray[CopierIndex]; + var copyCreators = serializerArray[CopyCreatorIndex]; + copier = Unsafe.As?>(copiers.Regular); + copyCreator = Unsafe.As?>(copyCreators.Regular); - var serializerArray = _typeSerializersArray[information.Id]; - if (serializerArray == null) - return false; + if (copier != null || copyCreator != null) + return true; - var copiers = serializerArray[CopierIndex]; - var copyCreators = serializerArray[CopyCreatorIndex]; - copier = Unsafe.As?>(copiers.Regular); - copyCreator = Unsafe.As?>(copyCreators.Regular); + copier = Unsafe.As?>(copiers.Generic); + copyCreator = Unsafe.As?>(copyCreators.Generic); - if (copier != null || copyCreator != null) - return true; + if (copier != null || copyCreator != null) + return true; + } + + if (TryGetTypeSerializer(typeof(ITypeCopier<>), typeof(TType), out var rawCopier)) + copier = (ITypeCopier) rawCopier; - copier = Unsafe.As?>(copiers.Generic); - copyCreator = Unsafe.As?>(copyCreators.Generic); + if (TryGetTypeSerializer(typeof(ITypeCopyCreator<>), typeof(TType), out var rawCopyCreator)) + copyCreator = (ITypeCopyCreator) rawCopyCreator; return copier != null || copyCreator != null; } @@ -415,13 +436,17 @@ private void RegisterSerializerInterface(Type type) private void RegisterIndexedSerializer(Type elementType, int interfaceIndex, object serializer, bool regular) { var id = SerializedType.GetId(elementType); - if (id >= _typeSerializers.Count) + if (id >= _typeSerializersArray.Length) { Array.Resize(ref _typeSerializersArray, (id + 1) * 2); } - var array = new (object? Regular, object? Generic)[SerializerInterfaces.Length]; - _typeSerializersArray[id] = array; + var array = _typeSerializersArray[id]; + if (array == null) + { + array = new (object? Regular, object? Generic)[SerializerInterfaces.Length]; + _typeSerializersArray[id] = array; + } if (regular) { @@ -453,7 +478,7 @@ internal static int GetId(Type type) } } - private static class SerializedType + internal static class SerializedType { // ReSharper disable once StaticMemberInGenericType internal static readonly TypeInformation Information; @@ -467,7 +492,7 @@ static SerializedType() } } - private readonly struct TypeInformation + internal readonly struct TypeInformation { internal readonly int Id; internal readonly bool ReturnSource; diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.Validation.cs b/Robust.Shared/Serialization/Manager/SerializationManager.Validation.cs index b55f06c10c7..584769794fa 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.Validation.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.Validation.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; +using System.Reflection; using Robust.Shared.Serialization.Manager.Definition; using Robust.Shared.Serialization.Markdown; using Robust.Shared.Serialization.Markdown.Mapping; @@ -21,119 +23,91 @@ public sealed partial class SerializationManager private readonly ConcurrentDictionary<(Type type, Type node), ValidationDelegate> _validationDelegates = new(); - private ValidationDelegate GetOrCreateValidationDelegate(Type type, Type node) + private ValidationDelegate GetOrCreateValidationDelegate(Type type, Type nodeType) { - return _validationDelegates.GetOrAdd((type, node), static (key, manager) => + return _validationDelegates.GetOrAdd((type, nodeType), static (key, manager) => { - var managerConst = Expression.Constant(manager); - var nodeParam = Expression.Parameter(typeof(DataNode), "node"); - var contextParam = Expression.Parameter(typeof(ISerializationContext), "context"); - - Expression call; - if (manager._regularSerializerProvider.TryGetTypeNodeSerializer(typeof(ITypeValidator<,>), key.type, key.node, out var serializer)) - { - var serializerConst = Expression.Constant(serializer); - - call = Expression.Call( - managerConst, - nameof(ValidateNode), - new []{key.type, key.node}, - serializerConst, - Expression.Convert(nodeParam, key.node), - contextParam); - } - else if (key.type.IsArray) - { - if (!key.node.IsAssignableTo(typeof(SequenceDataNode))) - { - call = manager.ErrorNodeExpression(nodeParam, "Invalid nodetype for array.", true); - } - else - { - var elementType = key.type.GetElementType(); - if (elementType == null) - throw new ArgumentException($"Failed to get ElementType of ArrayType {key.type}"); - - call = Expression.Call( - managerConst, - nameof(ValidateArray), - new[] { elementType }, - Expression.Convert(nodeParam, typeof(SequenceDataNode)), - contextParam); - } - } - else if (key.type.IsEnum) - { - // Does not include cases where the target type is System.Enum. - // Those get handled by the generic enum serializer which uses reflection to resolve strings into enums. - DebugTools.Assert(key.type != typeof(Enum)); - - call = Expression.Call( - managerConst, - nameof(ValidateEnum), - new[] { key.type }, - nodeParam); - } - else if (key.type.IsAssignableTo(typeof(ISelfSerialize))) - { - if (key.node.IsAssignableTo(typeof(ValueDataNode))) - { - call = manager.ValidateNodeExpression(nodeParam); - } - else - { - call = manager.ErrorNodeExpression(nodeParam, "Invalid nodetype for ISelfSerialize"); - } - } - else if (manager.TryGetDefinition(key.type, out var dataDefinition)) + var validate = Validate(manager, key.type, key.node); + return (node, context) => { - var dataDefConst = Expression.Constant(dataDefinition, typeof(DataDefinition<>).MakeGenericType(key.type)); - - call = Expression.Call( - managerConst, - nameof(ValidateDataDefinition), - new []{key.type}, - nodeParam, - dataDefConst, - contextParam); - } - else - { - call = Expression.Call( - managerConst, - nameof(ValidateGenericValue), - new[] { key.type, key.node }, - nodeParam, - contextParam); - } + if (!IsNull(node)) + return validate(node, context); + + if (key.type.IsNullable()) + return new ValidatedValueNode(node); - //insert a nullcheck at the beginning, but ONLY if we are actually found a way of validating this node - call = Expression.Condition( - Expression.Call( - typeof(SerializationManager), - nameof(IsNull), - Type.EmptyTypes, - nodeParam), - Expression.Convert(key.type.IsNullable() - ? manager.ValidateNodeExpression(nodeParam) - : manager.ErrorNodeExpression(nodeParam, "Non-nullable field contained a null value", true), typeof(ValidationNode)), - Expression.Convert(call, typeof(ValidationNode))); - - return Expression.Lambda( - call, - nodeParam, - contextParam).Compile(); + return new ErrorNode(node, "Non-nullable field contained a null value"); + }; }, this); } - private Expression ErrorNodeExpression(ParameterExpression nodeParam, string message, bool alwaysRelevant = true) + private static Func Validate(SerializationManager serialization, Type type, Type nodeType) { - return ExpressionUtils.NewExpression(nodeParam, message, alwaysRelevant); - } + if (serialization._regularSerializerProvider.TryGetTypeNodeSerializer(typeof(ITypeValidator<,>), + type, + nodeType, + out var serializer)) + { + var method = typeof(SerializationManager) + .GetMethods(BindingFlags.Instance | BindingFlags.Public) + .First(m => m.Name == nameof(ValidateNode) && m.GetGenericArguments().Length == 2) + .MakeGenericMethod(type, nodeType); + return (node, context) => (ValidationNode) method.Invoke(serialization, [serializer, node, context])!; + } - private Expression ValidateNodeExpression(ParameterExpression nodeParam) - { - return ExpressionUtils.NewExpression(nodeParam); + if (type.IsArray) + { + if (!nodeType.IsAssignableTo(typeof(SequenceDataNode))) + return (node, _) => new ErrorNode(node, "Invalid nodetype for array."); + + var elementType = type.GetElementType(); + if (elementType == null) + throw new ArgumentException($"Failed to get ElementType of ArrayType {type}"); + + var method = typeof(SerializationManager) + .GetMethod(nameof(ValidateArray), + BindingFlags.Instance | BindingFlags.NonPublic, + [typeof(SequenceDataNode), typeof(ISerializationContext)])! + .MakeGenericMethod(elementType); + return (node, context) => (ValidationNode) method.Invoke(serialization, [node, context])!; + } + + if (type.IsEnum) + { + DebugTools.Assert(type != typeof(Enum)); + var method = typeof(SerializationManager) + .GetMethod(nameof(ValidateEnum), BindingFlags.Instance | BindingFlags.NonPublic, [typeof(DataNode)])! + .MakeGenericMethod(type); + return (node, _) => (ValidationNode)method.Invoke(serialization, [node])!; + } + + if (type.IsAssignableTo(typeof(ISelfSerialize))) + { + if (nodeType.IsAssignableTo(typeof(ValueDataNode))) + return (node, _) => new ValidatedValueNode(node); + + return (node, _) => new ErrorNode(node, "Invalid nodetype for ISelfSerialize"); + } + + if (serialization.TryGetDefinition(type, out var dataDefinition)) + { + var method = typeof(SerializationManager) + .GetMethods(BindingFlags.Instance | BindingFlags.NonPublic) + .First(m => m.Name == nameof(ValidateDataDefinition)) + .MakeGenericMethod(type); + return (node, context) => (ValidationNode)method.Invoke(serialization, [node, dataDefinition, context])!; + } + else + { + var method = typeof(SerializationManager) + .GetMethod(nameof(ValidateGenericValue), + BindingFlags.Instance | BindingFlags.NonPublic, + [ + typeof(DataNode), typeof(ISerializationContext) + ])! + .MakeGenericMethod(type, nodeType); + return (node, context) => (ValidationNode)method.Invoke(serialization, [node, context])!; + } } private ValidationNode ValidateArray(SequenceDataNode sequenceDataNode, ISerializationContext? context) @@ -170,7 +144,7 @@ private ValidationNode ValidateEnum(DataNode node) return new ValidatedValueNode(node); } - private ValidationNode ValidateDataDefinition(DataNode node, DataDefinition dataDefinition, ISerializationContext? context) where T : notnull + private ValidationNode ValidateDataDefinition(DataNode node, DataDefinition dataDefinition, ISerializationContext? context) where T : notnull, ISerializationGenerated { return node switch { @@ -231,11 +205,7 @@ public ValidationNode ValidateNode(Type type, DataNode node, ISerializationConte if (node.Tag?.StartsWith("!type:") == true) { var typeString = node.Tag.Substring(6); - try - { - underlyingType = ResolveConcreteType(underlyingType, typeString); - } - catch (InvalidOperationException) + if (!TryResolveConcreteType(underlyingType, typeString, out underlyingType)) { return new ErrorNode(node, $"Failed to resolve !type tag: {typeString}", false); } diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.Writing.cs b/Robust.Shared/Serialization/Manager/SerializationManager.Writing.cs index 3bc5109dc28..c4f95c00fbf 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.Writing.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.Writing.cs @@ -6,6 +6,7 @@ using Robust.Shared.Serialization.Manager.Definition; using Robust.Shared.Serialization.Manager.Exceptions; using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Serialization.Markdown.Sequence; using Robust.Shared.Serialization.Markdown.Value; using Robust.Shared.Serialization.TypeSerializers.Interfaces; @@ -146,7 +147,7 @@ static object ValueFactory(Type baseType, Type actualType, bool notNullableOverr Type.EmptyTypes, Expression.Convert(objParam, typeof(ISelfSerialize))); } - else + else if (actualType.IsAssignableTo(typeof(ISerializationGenerated<>).MakeGenericType(actualType))) { call = Expression.Call( instanceParam, @@ -167,6 +168,14 @@ static object ValueFactory(Type baseType, Type actualType, bool notNullableOverr nodeVar); } } + else + { + call = Expression.Call( + instanceParam, + nameof(WriteNoSerializer), + Type.EmptyTypes, + Expression.Constant(actualType, typeof(Type))); + } // check for customtypeserializer before anything var serializerType = typeof(ITypeWriter<>).MakeGenericType(actualType); @@ -221,6 +230,11 @@ private DataNode WriteSelfSerializable(ISelfSerialize obj) return new ValueDataNode(obj.Serialize()); } + private DataNode WriteNoSerializer(Type type) + { + throw new ArgumentException($"No type serializer or data definition found for type {type} when writing"); + } + private DataNode WriteArray(TElement[] obj, bool alwaysWrite, ISerializationContext? context) { var sequenceNode = new SequenceDataNode(); @@ -239,13 +253,14 @@ private DataNode WriteValueInternal( DataDefinition? definition, bool alwaysWrite, ISerializationContext? context) - where T : notnull + where T : ISerializationGenerated { //this check is in here on purpose. we cannot check this during expression tree generation due to the value maybe being handled by a custom typeserializer if(definition == null) throw new InvalidOperationException($"No data definition found for type {typeof(T)} when writing"); - var mapping = definition.Serialize(value, context, alwaysWrite); + var mapping = new MappingDataNode(); + definition.Serialize(value, mapping, this, context, alwaysWrite, definition.DefaultValuesDict); return mapping; } diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.cs b/Robust.Shared/Serialization/Manager/SerializationManager.cs index be2253cb9cc..5d83e594393 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.cs @@ -9,17 +9,20 @@ using System.Threading.Tasks; using Robust.Shared.IoC; using Robust.Shared.Log; +using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Definition; +using Robust.Shared.Serialization.Markdown; using Robust.Shared.Utility; namespace Robust.Shared.Serialization.Manager { public sealed partial class SerializationManager : ISerializationManager { - [Dependency] private IReflectionManager _reflectionManager = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly IReflectionManager _reflectionManager = default!; public IReflectionManager ReflectionManager => _reflectionManager; @@ -33,10 +36,10 @@ public sealed partial class SerializationManager : ISerializationManager // Always has a dummy value of 0 for any types that should be copied by ref private readonly ConcurrentDictionary _copyByRefRegistrations = new(); - [IoC.Dependency] - private IDependencyCollection _dependencyCollection = null!; + [field: Dependency] + public IDependencyCollection DependencyCollection { get; } = default!; - public IDependencyCollection DependencyCollection => _dependencyCollection; + public bool IsServer { get; private set; } public void Initialize() { @@ -46,8 +49,19 @@ public void Initialize() if (_initialized) throw new InvalidOperationException($"{nameof(SerializationManager)} has already been initialized."); + IsServer = _net.IsServer; _initializing = true; + _read = typeof(SerializationManager) + .GetMethods(BindingFlags.Instance | BindingFlags.NonPublic) + .First(m => m.Name == nameof(ReadObject) && + m.GetGenericArguments().Length == 1 && + GetParametersBase(m) + .SequenceEqual([ + typeof(DataNode), typeof(SerializationHookContext), typeof(ISerializationContext), + typeof(bool) + ])); + var flagsTypes = new ConcurrentBag(); var constantsTypes = new ConcurrentBag(); var typeSerializers = new ConcurrentBag(); @@ -142,10 +156,7 @@ IEnumerable GetImplicitTypes(Type type) foreach (var (type, definition) in _dataDefinitions) { var invalidTypes = new List(); - foreach (var includedField in definition.BaseFieldDefinitions.Where(x => x.Attribute is IncludeDataFieldAttribute - { - CustomTypeSerializer: null - })) + foreach (var includedField in definition.BaseFieldDefinitions.Where(x => x is { IsIncludeDataField: true, CustomTypeSerializer: null })) { if (!dataDefs.Contains(includedField.FieldType)) { @@ -196,7 +207,7 @@ IEnumerable GetImplicitTypes(Type type) if (field.FieldType.ContainsGenericParameters) continue; // This just isn't supported yet, can't validate it so just skip it. - if (field.Attribute.CustomTypeSerializer != null) + if (field.CustomTypeSerializer != null) continue; // Assume that anything with a custom type serializer can be handled. if (!ValidateIsSerializable(field.FieldType, forbidden)) @@ -280,9 +291,11 @@ private void CollectAttributedTypes( private DataDefinition CreateDataDefinition(Type t, bool isRecord) { return (DataDefinition)typeof(DataDefinition<>).MakeGenericType(t) - .GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, new[] - { typeof(SerializationManager), typeof(bool) })! - .Invoke(new object[]{this, isRecord}); + .GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, + [ + typeof(SerializationManager), typeof(bool) + ])! + .Invoke([this, isRecord]); } public void Shutdown() @@ -301,19 +314,17 @@ public void Shutdown() _initialized = false; } - internal DataDefinition? GetDefinition() where T : notnull + internal DataDefinition? GetDefinition() where T : ISerializationGenerated { return GetDefinition(typeof(T)) as DataDefinition; } internal DataDefinition? GetDefinition(Type type) { - return _dataDefinitions.TryGetValue(type, out var dataDefinition) - ? dataDefinition - : null; + return _dataDefinitions.GetValueOrDefault(type); } - internal bool TryGetDefinition([NotNullWhen(true)] out DataDefinition? dataDefinition) where T : notnull + internal bool TryGetDefinition([NotNullWhen(true)] out DataDefinition? dataDefinition) where T : ISerializationGenerated { dataDefinition = GetDefinition(); return dataDefinition != null; @@ -332,28 +343,30 @@ public bool TryGetVariableType(Type type, string variableName, [NotNullWhen(true variableType = null; return false; } - var foundFieldDef = definition.BaseFieldDefinitions.FirstOrDefault(fieldDef => fieldDef?.Attribute is DataFieldAttribute attr && attr.Tag==variableName, null); - if(foundFieldDef != null) + + var foundFieldDef = definition.BaseFieldDefinitions.FirstOrDefault(fieldDef => fieldDef.IsDataField && fieldDef.Tag == variableName, default); + if (foundFieldDef != default) { - variableType = foundFieldDef.BackingField.FieldType; + variableType = foundFieldDef.FieldType; return true; } - else - { - variableType = null; - return false; - } + + variableType = null; + return false; + } + + private bool TryResolveConcreteType(Type baseType, string typeName, [NotNullWhen(true)] out Type? concreteType) + { + concreteType = ReflectionManager.YamlTypeTagLookup(baseType, typeName); + return concreteType != null; } private Type ResolveConcreteType(Type baseType, string typeName) { - var type = ReflectionManager.YamlTypeTagLookup(baseType, typeName); - if (type == null) - { - throw new InvalidOperationException($"Type '{baseType}' is abstract, but could not find concrete type '{typeName}'."); - } + if (TryResolveConcreteType(baseType, typeName, out var concreteType)) + return concreteType; - return type; + throw new InvalidOperationException($"Type '{baseType}' is abstract, but could not find concrete type '{typeName}'."); } #pragma warning disable CS0618 @@ -375,6 +388,15 @@ private static void RunAfterHookGenerated(TValue instance, Serialization else instance.AfterDeserialization(); } + + private static IEnumerable GetParametersBase(MethodInfo method) + { + return method.GetParameters() + .Select(p => + p.ParameterType.IsGenericType + ? p.ParameterType.GetGenericTypeDefinition() + : p.ParameterType); + } #pragma warning restore CS0618 } } diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/CompNameSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/CompNameSerializer.cs new file mode 100644 index 00000000000..bd1a51ab8b0 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/CompNameSerializer.cs @@ -0,0 +1,39 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.Markdown.Value; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; +using static Robust.Shared.Serialization.Manager.ISerializationManager; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations; + +/// +/// Serializer used automatically for types. +/// +[TypeSerializer] +public sealed class CompNameSerializer : ITypeSerializer, ITypeCopyCreator +{ + private IComponentFactory? _factory; + + public ValidationNode Validate(ISerializationManager serialization, ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context = null) + { + var name = node.Value; + _factory ??= dependencies.Resolve(); + if (_factory.HasRegistration(name)) + return new ValidatedValueNode(node); + + return new ErrorNode(node, $"No component found with name {name}"); + } + + public CompName Read(ISerializationManager serialization, ValueDataNode node, IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context = null, InstantiationDelegate? instanceProvider = null) + => new CompName(node.Value, _factory ??= dependencies.Resolve()); + + public DataNode Write(ISerializationManager serialization, CompName value, IDependencyCollection dependencies, bool alwaysWrite = false, ISerializationContext? context = null) + => new ValueDataNode(value.Name); + + public CompName CreateCopy(ISerializationManager serializationManager, CompName source, IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context = null) + => source; +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs index 8101c34a8fe..b7e4dae9b44 100644 --- a/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Robust.Shared.Collections; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; @@ -29,6 +30,8 @@ public ComponentRegistry Read(ISerializationManager serializationManager, { var factory = dependencies.Resolve(); var components = instanceProvider != null ? instanceProvider() : new ComponentRegistry(); + var referenceTypes = node.Count <= 1024 ? stackalloc CompIdx[node.Count] : new CompIdx[node.Count]; + var refIdx = 0; foreach (var sequenceEntry in node.Sequence) { @@ -61,29 +64,24 @@ public ComponentRegistry Read(ISerializationManager serializationManager, continue; } - var copy = componentMapping.Copy()!; - copy.Remove("type"); - - var type = factory.GetRegistration(compType).Type; - var read = (IComponent)serializationManager.Read(type, copy, hookCtx, context)!; - - components[compType] = new ComponentRegistryEntry(read, copy); - } - - var referenceTypes = new List(); - // Assert that there are no conflicting component references. - foreach (var componentName in components.Keys) - { - var registration = factory.GetRegistration(componentName); - var compType = registration.Idx; + var registration = factory.GetRegistration(compType); + var compIdx = registration.Idx; - if (referenceTypes.Contains(compType)) + if (referenceTypes[..refIdx].Contains(compIdx)) { throw new InvalidOperationException( - $"Duplicate component reference in prototype: '{compType}'"); + $"Duplicate component reference in prototype: '{compIdx}'"); } - referenceTypes.Add(compType); + referenceTypes[refIdx++] = compIdx; + + var copy = componentMapping.Copy()!; + copy.Remove("type"); + + var read = (IComponent)serializationManager.Read(registration.Type, copy, hookCtx, context)!; + + // The full YAML mapping is already retained by PrototypeManager. + components[compType] = new ComponentRegistryEntry(read); } return components; @@ -95,8 +93,10 @@ public ValidationNode Validate(ISerializationManager serializationManager, ISerializationContext? context = null) { var factory = dependencies.Resolve(); - var components = new ComponentRegistry(); + var componentNames = new HashSet(); var list = new List(); + var referenceTypes = node.Count <= 1024 ? stackalloc CompIdx[node.Count] : new CompIdx[node.Count]; + var refIdx = 0; foreach (var sequenceEntry in node.Sequence) { @@ -122,34 +122,27 @@ public ValidationNode Validate(ISerializationManager serializationManager, } // Has this type already been added? - if (components.ContainsKey(compType)) + if (!componentNames.Add(compType)) { list.Add(new ErrorNode(componentMapping, "Duplicate Component.")); continue; } - var copy = componentMapping.Copy()!; - copy.Remove("type"); - - var type = factory.GetRegistration(compType).Type; - - list.Add(serializationManager.ValidateNode(type, copy, context)); - } - - var referenceTypes = new List(); + var registration = factory.GetRegistration(compType); + var compIdx = registration.Idx; - // Assert that there are no conflicting component references. - foreach (var componentName in components.Keys) - { - var registration = factory.GetRegistration(componentName); - var compType = registration.Idx; - - if (referenceTypes.Contains(compType)) + if (referenceTypes[..refIdx].Contains(compIdx)) { - return new ErrorNode(node, "Duplicate ComponentReference."); + list.Add(new ErrorNode(componentMapping, "Duplicate ComponentReference.")); + continue; } - referenceTypes.Add(compType); + referenceTypes[refIdx++] = compIdx; + + var copy = componentMapping.Copy(); + copy.Remove("type"); + + list.Add(serializationManager.ValidateNode(registration.Type, copy, context)); } return new ValidatedSequenceNode(list); @@ -186,7 +179,8 @@ public void CopyTo(ISerializationManager serializationManager, ComponentRegistry foreach (var (id, component) in source) { - target.Add(id, serializationManager.CreateCopy(component, context, notNullableOverride: true)); + var copy = serializationManager.CreateCopy(component.Component, context, notNullableOverride: true); + target.Add(id, new ComponentRegistryEntry(copy)); } } diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/TimeOffsetSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/TimeOffsetSerializer.cs index 0c8c4ac2ce8..63394f82568 100644 --- a/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/TimeOffsetSerializer.cs +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/TimeOffsetSerializer.cs @@ -21,7 +21,7 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; /// to prevent time-offsets from being unintentionally saved to maps while mapping. If an entity must have an initial /// non-zero time, then that time should just be configured during map-init. /// -public sealed class TimeOffsetSerializer : ITypeSerializer +public sealed class TimeOffsetSerializer : ITypeSerializer, ITypeCopyCreator { public TimeSpan Read(ISerializationManager serializationManager, ValueDataNode node, IDependencyCollection dependencies, @@ -81,4 +81,14 @@ public DataNode Write( return new ValueDataNode(value.TotalSeconds.ToString(CultureInfo.InvariantCulture)); } + + public TimeSpan CreateCopy( + ISerializationManager serializationManager, + TimeSpan source, + IDependencyCollection dependencies, + SerializationHookContext hookCtx, + ISerializationContext? context = null) + { + return source; + } } diff --git a/Robust.Shared/Timing/GameLoop.cs b/Robust.Shared/Timing/GameLoop.cs index 64030fb6d6f..b25383b97f3 100644 --- a/Robust.Shared/Timing/GameLoop.cs +++ b/Robust.Shared/Timing/GameLoop.cs @@ -310,7 +310,7 @@ public void Run() try #endif { - using (_prof.Group("Render")) + using (_prof.Frame("Render")) { Render?.Invoke(this, realFrameEvent); } @@ -325,9 +325,13 @@ public void Run() { using var gc = _prof.Group("GC Overview"); - _prof.WriteValue("Gen 0 Count", ProfData.Int32(GC.CollectionCount(0) - profFrameGcGen0)); - _prof.WriteValue("Gen 1 Count", ProfData.Int32(GC.CollectionCount(1) - profFrameGcGen1)); - _prof.WriteValue("Gen 2 Count", ProfData.Int32(GC.CollectionCount(2) - profFrameGcGen2)); + var gcGen0 = GC.CollectionCount(0); + var gcGen1 = GC.CollectionCount(1); + var gcGen2 = GC.CollectionCount(2); + _prof.WriteValue("Gen 0 Count", ProfData.Int32(gcGen0 - profFrameGcGen0)); + _prof.WriteValue("Gen 1 Count", ProfData.Int32(gcGen1 - profFrameGcGen1)); + _prof.WriteValue("Gen 2 Count", ProfData.Int32(gcGen2 - profFrameGcGen2)); + _prof.EmitMemoryPlots(gcGen0, gcGen1, gcGen2); } _prof.WriteGroupEnd(profFrameGroupStart, "Frame", profFrameSw); diff --git a/Robust.Shared/Upload/SharedNetworkResourceManager.cs b/Robust.Shared/Upload/SharedNetworkResourceManager.cs index d3937a1ac59..cc61f7e792a 100644 --- a/Robust.Shared/Upload/SharedNetworkResourceManager.cs +++ b/Robust.Shared/Upload/SharedNetworkResourceManager.cs @@ -7,6 +7,7 @@ using Robust.Shared.Asynchronous; using Robust.Shared.ContentPack; using Robust.Shared.IoC; +using Robust.Shared.Localization; using Robust.Shared.Log; using Robust.Shared.Network; using Robust.Shared.Network.Transfer; @@ -38,6 +39,7 @@ public abstract partial class SharedNetworkResourceManager : IDisposable, IPostI [Dependency] protected IResourceManager ResourceManager = default!; [Dependency] protected ITransferManager TransferManager = default!; [Dependency] protected ILogManager LogManager = default!; + [Dependency] protected ILocalizationManager LocalizationManager = default!; [Dependency] private ITaskManager _taskManager = default!; protected ISawmill Sawmill = default!; @@ -112,9 +114,13 @@ protected virtual void ValidateUpload(uint size) protected async Task> IngestFileStream(Stream stream) { var list = new List<(ResPath Relative, byte[] Data)>(); + var anyLoc = false; await foreach (var (relative, data) in ReadTransferStream(stream).ConfigureAwait(false)) { + if (relative.Extension == "ftl") + anyLoc = true; + Sawmill.Verbose($"Storing uploaded file: {relative} ({ByteHelpers.FormatBytes(data.Length)})"); _taskManager.RunOnMainThread(() => { @@ -123,6 +129,14 @@ protected virtual void ValidateUpload(uint size) list.Add((relative, data)); } + if (anyLoc) + { + _taskManager.RunOnMainThread(() => + { + LocalizationManager.ReloadLocalizations(); + }); + } + return list; } diff --git a/Robust.Shared/Utility/CollectionExtensions.cs b/Robust.Shared/Utility/CollectionExtensions.cs index 12bbd677294..b53112ea183 100644 --- a/Robust.Shared/Utility/CollectionExtensions.cs +++ b/Robust.Shared/Utility/CollectionExtensions.cs @@ -56,6 +56,39 @@ public static Dictionary ShallowClone(this Dictionar return dict; } + /// + /// Compares the entries inside of 2 dictionaries to check equality. + /// + /// + /// The base Equals implementation checks references hence this. + /// + /// + /// + /// + /// + /// + public static bool DictionaryEquals( + this IReadOnlyDictionary self, + IReadOnlyDictionary other) + where TKey : notnull + { + if (self.Count != other.Count) + return false; + + // Checking itself. + if (self.Equals(other)) + return true; + + var valueComparer = EqualityComparer.Default; + foreach (var (key, value) in self) + { + if (!other.TryGetValue(key, out var otherValue) || !valueComparer.Equals(value, otherValue)) + return false; + } + + return true; + } + public static bool TryGetValue(this IList list, int index, out T value) { if (index >= 0 && list.Count > index) diff --git a/Robust.Shared/Utility/ColorExtensions.cs b/Robust.Shared/Utility/ColorExtensions.cs new file mode 100644 index 00000000000..007914540c9 --- /dev/null +++ b/Robust.Shared/Utility/ColorExtensions.cs @@ -0,0 +1,91 @@ +using System; +using System.Numerics; +using Robust.Shared.Random; +using Robust.Shared.Maths; + +namespace Robust.Shared.Utility; + +public static class ColorExtensions +{ + public static readonly float AnalogousHueDelta = 30f / 360f; // +/- 1/12. 30 degrees, 0.08333... over hue + public static readonly float TriadicHueDelta = 120f / 360f; // +/- 1/3. 120 degrees, 0.333 over hue + public static readonly float SplitComplementaryHueDelta = 150f / 360f; // +/- 5/12. 150 degrees, 0.4166... over hue + public static readonly float ComplementaryHueDelta = 180f / 360f; // +/- 1/2. 180 degrees + + /// + /// Generates a list of analogous complementary colors + /// + public static Color[] GetAnalogousComplementaries(this Color color) + { + return GetComplementaryColors(color, AnalogousHueDelta); + } + + /// + /// Generates a list of triadic complementary colors + /// + public static Color[] GetTriadicComplementaries(this Color color) + { + return GetComplementaryColors(color, TriadicHueDelta); + } + + /// + /// Generates a list of split complementary colors + /// + public static Color[] GetSplitComplementaries(this Color color) + { + return GetComplementaryColors(color, SplitComplementaryHueDelta); + } + + /// + /// Generates a list containing the base color and two copies of a single complementary color + /// + public static Color[] GetOneComplementary(this Color color) + { + return GetComplementaryColors(color, ComplementaryHueDelta); + } + + /// + /// Generates a complementary color palette for a provided + /// color by rotating a set amount of degrees around the + /// color wheel, and then varying the value and saturation + /// slightly. + /// + /// + /// A list of 3 colors. + /// + public static Color[] GetComplementaryColors(Color color, float hueDelta) + { + var hsl = Color.ToHsl(color); + var random = new RobustRandom(); + + // sorry about how messy these are, but to get all random values we need to reroll for positive and negative HSL. + // since we want to rotate x degrees around the colour wheel, we need to do so in both directions- doing x + x degrees will give us the wrong hue! + + // also varying the saturation and lightness just a little to add some contrast. + // since 'color' is our main color, we are desaturating our secondary colors. + // this means our main color will always stand out the most. + + var hVal = hsl.X + hueDelta; + hVal -= MathF.Floor(hVal); + var positiveHSL = new Vector4( + hVal, + MathHelper.Clamp01(hsl.Y + random.Next(-20 / 100, 0)), + MathHelper.Clamp01(hsl.Z + random.Next(-15 / 100, 16 / 100)), + hsl.W); + + var hVal1 = hsl.X - hueDelta; + if (hVal1 < 0f) + hVal1 += 1f; + var negativeHSL = new Vector4( + hVal1, + MathHelper.Clamp01(hsl.Y + random.Next(-20 / 100, 0)), + MathHelper.Clamp01(hsl.Z + random.Next(-15 / 100, 16 / 100)), + hsl.W); + + var c0 = Color.FromHsl(positiveHSL); + var c1 = Color.FromHsl(negativeHSL); + + var palette = new Color[] { color, c0, c1 }; + return palette; + } +} diff --git a/Robust.Shared/Utility/CommandParsing.cs b/Robust.Shared/Utility/CommandParsing.cs index a6e5e16a92c..1fd48dd5baf 100644 --- a/Robust.Shared/Utility/CommandParsing.cs +++ b/Robust.Shared/Utility/CommandParsing.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Text; using Robust.Shared.Collections; @@ -7,6 +8,8 @@ namespace Robust.Shared.Utility { public static class CommandParsing { + private static readonly SearchValues CommandArgumentSeparator = SearchValues.Create(" "); + /// /// Parses a full console command into a list of arguments. /// @@ -63,7 +66,7 @@ internal static void ParseArguments( continue; } - if (chr == ' ' && !inQuotes) + if (CommandArgumentSeparator.Contains(chr) && !inQuotes) { if (sb.Length != 0) { @@ -93,5 +96,38 @@ public static string Escape(string text) { return text.Replace("\\", "\\\\").Replace("\"", "\\\""); } + + /// + /// Split a set of arguments into a string that can be parsed round-trip. + /// + /// + /// + /// This is effectively the inverse of . + /// + /// + public static string EscapeCommand(params string[] arguments) + { + var sb = new StringBuilder(); + + var first = true; + + foreach (var entry in arguments) + { + if (!first) + sb.Append(' '); + first = false; + + var quoted = entry.ContainsAny(CommandArgumentSeparator); + if (quoted) + sb.Append('"'); + + sb.Append(Escape(entry)); + + if (quoted) + sb.Append('"'); + } + + return sb.ToString(); + } } } diff --git a/Robust.Shared/Utility/InternalReflectionUtils.cs b/Robust.Shared/Utility/InternalReflectionUtils.cs index 10f3e609681..920c5a20769 100644 --- a/Robust.Shared/Utility/InternalReflectionUtils.cs +++ b/Robust.Shared/Utility/InternalReflectionUtils.cs @@ -37,46 +37,6 @@ private static void EmitSetField(ILGenerator rGenerator, AbstractFieldInfo info) } } - internal static object EmitFieldAccessor(Type obj, FieldDefinition fieldDefinition) - { - if (fieldDefinition.BackingField is SpecificFieldInfo fieldInfo) - return fieldInfo.FieldInfo; - - if (fieldDefinition.BackingField is SpecificPropertyInfo propertyInfo) - return propertyInfo.PropertyInfo.GetGetMethod(true) ?? throw new InvalidOperationException("Property has no getter"); - - var method = new DynamicMethod( - "AccessField", - fieldDefinition.BackingField.FieldType, - new[] {obj.MakeByRefType()}, - true); - - method.DefineParameter(1, ParameterAttributes.Out, "target"); - - var generator = method.GetILGenerator(); - - generator.Emit(OpCodes.Ldarg_0); - - if(!obj.IsValueType) - generator.Emit(OpCodes.Ldind_Ref); - - switch (fieldDefinition.BackingField) - { - case SpecificFieldInfo field: - generator.Emit(OpCodes.Ldfld, field.FieldInfo); - break; - case SpecificPropertyInfo property: - var getter = property.PropertyInfo.GetGetMethod(true) ?? throw new NullReferenceException(); - var opCode = fieldDefinition.BackingField.FieldType.IsValueType ? OpCodes.Call : OpCodes.Callvirt; - generator.Emit(opCode, getter); - break; - } - - generator.Emit(OpCodes.Ret); - - return method.CreateDelegate(typeof(AccessField<,>).MakeGenericType(obj, fieldDefinition.BackingField.FieldType)); - } - internal static object EmitFieldAssigner(Type objType, AbstractFieldInfo backingField, bool boxing = false) { if (!boxing) diff --git a/Robust.Shared/Utility/QuadTree.cs b/Robust.Shared/Utility/QuadTree.cs deleted file mode 100644 index 1c7737d2213..00000000000 --- a/Robust.Shared/Utility/QuadTree.cs +++ /dev/null @@ -1,529 +0,0 @@ -// From http://csharpquadtree.codeplex.com/ - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Numerics; -using Robust.Shared.Maths; - -#nullable disable - -namespace Robust.Shared.Utility -{ - public sealed class QuadTree where T : class, IQuadObject - { - private readonly bool sort; - private readonly Vector2 minLeafSizeF; - private readonly int maxObjectsPerLeaf; - private QuadNode root = null; - private Dictionary objectToNodeLookup = new(); - private Dictionary objectSortOrder = new(); - public QuadNode Root { get { return root; } } - private object syncLock = new(); - private int objectSortId = 0; - - public QuadTree(Vector2 minLeafSizeF, int maxObjectsPerLeaf) - { - this.minLeafSizeF = minLeafSizeF; - this.maxObjectsPerLeaf = maxObjectsPerLeaf; - } - - public int GetSortOrder(T quadObject) - { - lock (objectSortOrder) - { - if (!objectSortOrder.ContainsKey(quadObject)) - return -1; - else - { - return objectSortOrder[quadObject]; - } - } - } - - /// - /// - /// - /// The smallest SizeF a leaf will split into - /// Maximum number of objects per leaf before it forces a split into sub quadrants - /// Whether or not queries will return objects in the order in which they were added - public QuadTree(Vector2 minLeafSizeF, int maxObjectsPerLeaf, bool sort) - : this(minLeafSizeF, maxObjectsPerLeaf) - { - this.sort = sort; - } - - public void Insert(T quadObject) - { - lock (syncLock) - { - if (sort && !objectSortOrder.ContainsKey(quadObject)) - { - objectSortOrder.Add(quadObject, objectSortId++); - } - - var bounds = quadObject.Bounds; - if (root == null) - { - var rootSizeF = new Vector2((float)Math.Ceiling(bounds.Width / minLeafSizeF.X), - (float)Math.Ceiling(bounds.Height / minLeafSizeF.Y)); - double multiplier = Math.Max(rootSizeF.X, rootSizeF.Y); - rootSizeF = new Vector2((float)(minLeafSizeF.X * multiplier), (float)(minLeafSizeF.Y * multiplier)); - var center = new Vector2i((int)(bounds.Left + bounds.Width / 2), (int)(bounds.Top + bounds.Height / 2)); - var rootOrigin = new Vector2i((int)(center.X - rootSizeF.X / 2), (int)(center.Y - rootSizeF.Y / 2)); - root = new QuadNode(Box2.FromDimensions(rootOrigin, rootSizeF)); - } - - while (!root.Bounds.Encloses(bounds)) - { - ExpandRoot(bounds); - } - - InsertNodeObject(root, quadObject); - } - } - - public List Query(Box2 bounds) - { - lock (syncLock) - { - List results = new List(); - if (root != null) - Query(bounds, root, results); - if (sort) - results.Sort((a, b) => { return objectSortOrder[a].CompareTo(objectSortOrder[b]); }); - return results; - } - } - - private void Query(Box2 bounds, QuadNode node, List results) - { - lock (syncLock) - { - if (node == null) return; - - if (bounds.Intersects(node.Bounds)) - { - foreach (T quadObject in node.Objects) - { - if (bounds.Intersects(quadObject.Bounds)) - results.Add(quadObject); - } - - foreach (QuadNode childNode in node.Nodes) - { - Query(bounds, childNode, results); - } - } - } - } - - private void ExpandRoot(Box2 newChildBounds) - { - lock (syncLock) - { - bool isNorth = root.Bounds.Top < newChildBounds.Top; - bool isWest = root.Bounds.Left < newChildBounds.Left; - - DiRectangleFion rootDiRectangleFion; - if (isNorth) - { - rootDiRectangleFion = isWest ? DiRectangleFion.NW : DiRectangleFion.NE; - } - else - { - rootDiRectangleFion = isWest ? DiRectangleFion.SW : DiRectangleFion.SE; - } - - double newX = (rootDiRectangleFion == DiRectangleFion.NW || rootDiRectangleFion == DiRectangleFion.SW) - ? root.Bounds.Left - : root.Bounds.Left - root.Bounds.Width; - double newY = (rootDiRectangleFion == DiRectangleFion.NW || rootDiRectangleFion == DiRectangleFion.NE) - ? root.Bounds.Top - : root.Bounds.Top - root.Bounds.Height; - var newRootBounds = Box2.FromDimensions((float)newX, (float)newY, root.Bounds.Width * 2f, root.Bounds.Height * 2f); - QuadNode newRoot = new QuadNode(newRootBounds); - SetupChildNodes(newRoot); - newRoot[rootDiRectangleFion] = root; - root = newRoot; - } - } - - private void InsertNodeObject(QuadNode node, T quadObject) - { - lock (syncLock) - { - if (!node.Bounds.Encloses(quadObject.Bounds)) - throw new Exception("This should not happen, child does not fit within node bounds"); - - if (!node.HasChildNodes() && node.Objects.Count + 1 > maxObjectsPerLeaf) - { - SetupChildNodes(node); - - List childObjects = new List(node.Objects); - List childrenToRelocate = new List(); - - foreach (T childObject in childObjects) - { - foreach (QuadNode childNode in node.Nodes) - { - if (childNode == null) - continue; - - if (childNode.Bounds.Encloses(childObject.Bounds)) - { - childrenToRelocate.Add(childObject); - } - } - } - - foreach (T childObject in childrenToRelocate) - { - RemoveQuadObjectFromNode(childObject); - InsertNodeObject(node, childObject); - } - } - - foreach (QuadNode childNode in node.Nodes) - { - if (childNode != null) - { - if (childNode.Bounds.Encloses(quadObject.Bounds)) - { - InsertNodeObject(childNode, quadObject); - return; - } - } - } - - AddQuadObjectToNode(node, quadObject); - } - } - - private void ClearQuadObjectsFromNode(QuadNode node) - { - lock (syncLock) - { - List quadObjects = new List(node.Objects); - foreach (T quadObject in quadObjects) - { - RemoveQuadObjectFromNode(quadObject); - } - } - } - - private void RemoveQuadObjectFromNode(T quadObject) - { - lock (syncLock) - { - QuadNode node = objectToNodeLookup[quadObject]; - node.quadObjects.Remove(quadObject); - objectToNodeLookup.Remove(quadObject); - } - } - - private void AddQuadObjectToNode(QuadNode node, T quadObject) - { - lock (syncLock) - { - node.quadObjects.Add(quadObject); - objectToNodeLookup.Add(quadObject, node); - } - } - - private void SetupChildNodes(QuadNode node) - { - lock (syncLock) - { - if (minLeafSizeF.X <= node.Bounds.Width / 2 && minLeafSizeF.Y <= node.Bounds.Height / 2) - { - node[DiRectangleFion.NW] = new QuadNode(node.Bounds.Left, node.Bounds.Top, node.Bounds.Width / 2, - node.Bounds.Height / 2); - node[DiRectangleFion.NE] = new QuadNode(node.Bounds.Left + node.Bounds.Width / 2, node.Bounds.Top, - node.Bounds.Width / 2, - node.Bounds.Height / 2); - node[DiRectangleFion.SW] = new QuadNode(node.Bounds.Left, node.Bounds.Top + node.Bounds.Height / 2, - node.Bounds.Width / 2, - node.Bounds.Height / 2); - node[DiRectangleFion.SE] = new QuadNode(node.Bounds.Left + node.Bounds.Width / 2, - node.Bounds.Top + node.Bounds.Height / 2, - node.Bounds.Width / 2, node.Bounds.Height / 2); - - } - } - } - - public void Remove(T quadObject) - { - lock (syncLock) - { - if (sort && objectSortOrder.ContainsKey(quadObject)) - { - objectSortOrder.Remove(quadObject); - } - - if (!objectToNodeLookup.ContainsKey(quadObject)) - throw new KeyNotFoundException("QuadObject not found in dictionary for removal"); - - QuadNode containingNode = objectToNodeLookup[quadObject]; - RemoveQuadObjectFromNode(quadObject); - - if (containingNode.Parent != null) - CheckChildNodes(containingNode.Parent); - } - } - - - - private void CheckChildNodes(QuadNode node) - { - lock (syncLock) - { - if (GetQuadObjectCount(node) <= maxObjectsPerLeaf) - { - // Move child objects into this node, and delete sub nodes - List subChildObjects = GetChildObjects(node); - foreach (T childObject in subChildObjects) - { - if (!node.Objects.Contains(childObject)) - { - RemoveQuadObjectFromNode(childObject); - AddQuadObjectToNode(node, childObject); - } - } - if (node[DiRectangleFion.NW] != null) - { - node[DiRectangleFion.NW].Parent = null; - node[DiRectangleFion.NW] = null; - } - if (node[DiRectangleFion.NE] != null) - { - node[DiRectangleFion.NE].Parent = null; - node[DiRectangleFion.NE] = null; - } - if (node[DiRectangleFion.SW] != null) - { - node[DiRectangleFion.SW].Parent = null; - node[DiRectangleFion.SW] = null; - } - if (node[DiRectangleFion.SE] != null) - { - node[DiRectangleFion.SE].Parent = null; - node[DiRectangleFion.SE] = null; - } - - if (node.Parent != null) - CheckChildNodes(node.Parent); - else - { - // Its the root node, see if we're down to one quadrant, with none in local storage - if so, ditch the other three - int numQuadrantsWithObjects = 0; - QuadNode nodeWithObjects = null; - foreach (QuadNode childNode in node.Nodes) - { - if (childNode != null && GetQuadObjectCount(childNode) > 0) - { - numQuadrantsWithObjects++; - nodeWithObjects = childNode; - if (numQuadrantsWithObjects > 1) break; - } - } - if (numQuadrantsWithObjects == 1) - { - foreach (QuadNode childNode in node.Nodes) - { - if (childNode != nodeWithObjects) - childNode.Parent = null; - } - root = nodeWithObjects; - } - } - } - } - } - - - private List GetChildObjects(QuadNode node) - { - lock (syncLock) - { - List results = new List(); - results.AddRange(node.quadObjects); - foreach (QuadNode childNode in node.Nodes) - { - if (childNode != null) - results.AddRange(GetChildObjects(childNode)); - } - return results; - } - } - - public int GetQuadObjectCount() - { - lock (syncLock) - { - if (root == null) - return 0; - int count = GetQuadObjectCount(root); - return count; - } - } - - private int GetQuadObjectCount(QuadNode node) - { - lock (syncLock) - { - int count = node.Objects.Count; - foreach (QuadNode childNode in node.Nodes) - { - if (childNode != null) - { - count += GetQuadObjectCount(childNode); - } - } - return count; - } - } - - public int GetQuadNodeCount() - { - lock (syncLock) - { - if (root == null) - return 0; - int count = GetQuadNodeCount(root, 1); - return count; - } - } - - private int GetQuadNodeCount(QuadNode node, int count) - { - lock (syncLock) - { - if (node == null) return count; - - foreach (QuadNode childNode in node.Nodes) - { - if (childNode != null) - count++; - } - return count; - } - } - - public List GetAllNodes() - { - lock (syncLock) - { - List results = new List(); - if (root != null) - { - results.Add(root); - GetChildNodes(root, results); - } - return results; - } - } - - private void GetChildNodes(QuadNode node, ICollection results) - { - lock (syncLock) - { - foreach (QuadNode childNode in node.Nodes) - { - if (childNode != null) - { - results.Add(childNode); - GetChildNodes(childNode, results); - } - } - } - } - - public sealed class QuadNode - { - private static int _id = 0; - public readonly int ID = _id++; - - public QuadNode Parent { get; internal set; } - - private QuadNode[] _nodes = new QuadNode[4]; - public QuadNode this[DiRectangleFion diRectangleFion] - { - get - { - switch (diRectangleFion) - { - case DiRectangleFion.NW: - return _nodes[0]; - case DiRectangleFion.NE: - return _nodes[1]; - case DiRectangleFion.SW: - return _nodes[2]; - case DiRectangleFion.SE: - return _nodes[3]; - default: - return null; - } - } - set - { - switch (diRectangleFion) - { - case DiRectangleFion.NW: - _nodes[0] = value; - break; - case DiRectangleFion.NE: - _nodes[1] = value; - break; - case DiRectangleFion.SW: - _nodes[2] = value; - break; - case DiRectangleFion.SE: - _nodes[3] = value; - break; - } - if (value != null) - value.Parent = this; - } - } - - public ReadOnlyCollection Nodes { get; set; } - internal List quadObjects = new(); - public Box2 Bounds { get; internal set; } - public ReadOnlyCollection Objects { get; set; } - - public bool HasChildNodes() - { - return _nodes[0] != null; - } - - public QuadNode(Box2 bounds) - { - Bounds = bounds; - Nodes = new ReadOnlyCollection(_nodes); - Objects = new ReadOnlyCollection(quadObjects); - } - - public QuadNode(float x, float y, float width, float height) - : this(Box2.FromDimensions(x, y, width, height)) - { - - } - } - } - - public enum DiRectangleFion : byte - { - NW = 0, - NE = 1, - SW = 2, - SE = 3 - } - - [NotContentImplementable] - public interface IQuadObject - { - Box2 Bounds { get; } - } -} diff --git a/Robust.Shared/Utility/ResPath.cs b/Robust.Shared/Utility/ResPath.cs index d5a8822eaa5..1dc2126f07d 100644 --- a/Robust.Shared/Utility/ResPath.cs +++ b/Robust.Shared/Utility/ResPath.cs @@ -439,9 +439,20 @@ public bool TryRelativeTo(ResPath basePath, [NotNullWhen(true)] out ResPath? rel if (CanonPath.StartsWith(basePath.CanonPath)) { - var x = CanonPath[basePath.CanonPath.Length..] - .Trim('/'); - relative = x == "" ? Self : new ResPath(x); + var start = basePath.CanonPath.Length; + var end = CanonPath.Length; + + while (start < end && CanonPath[start] == '/') + { + start++; + } + + while (end > start && CanonPath[end - 1] == '/') + { + end--; + } + + relative = start == end ? Self : new ResPath(CanonPath[start..end]); return true; } diff --git a/Robust.UnitTesting/IIntegrationInstance.cs b/Robust.UnitTesting/IIntegrationInstance.cs index f7c00c1f5c2..5585120fb83 100644 --- a/Robust.UnitTesting/IIntegrationInstance.cs +++ b/Robust.UnitTesting/IIntegrationInstance.cs @@ -31,7 +31,6 @@ public interface IIntegrationInstance : IDisposable IConfigurationManager CfgMan { get; } ISharedPlayerManager PlayerMan { get; } INetManager NetMan { get; } - IMapManager MapMan { get; } IGameTiming Timing { get; } ISawmill Log { get; } diff --git a/Robust.UnitTesting/Pool/PoolManager.cs b/Robust.UnitTesting/Pool/PoolManager.cs index 9b31a41c66a..4b756bdefa0 100644 --- a/Robust.UnitTesting/Pool/PoolManager.cs +++ b/Robust.UnitTesting/Pool/PoolManager.cs @@ -180,7 +180,7 @@ await testOut.WriteLineAsync( if (pair != null) { pair.ActivateContext(testOut); - await testOut.WriteLineAsync($"{nameof(GetPair)}: Suitable pair found"); + await testOut.WriteLineAsync($"{nameof(GetPair)}: Suitable pair found (Pair {pair.Id})"); if (pair.Settings.CanFastRecycle(settings)) { diff --git a/Robust.UnitTesting/Pool/TestPair.Helpers.cs b/Robust.UnitTesting/Pool/TestPair.Helpers.cs index 624b7241e35..d587ecff17f 100644 --- a/Robust.UnitTesting/Pool/TestPair.Helpers.cs +++ b/Robust.UnitTesting/Pool/TestPair.Helpers.cs @@ -231,7 +231,7 @@ public async Task RunUntilSynced() /// Runs the server-client pair in sync, but also ensures they are both idle each tick. /// /// How many ticks to run - public async Task ReallyBeIdle(int runTicks = 25) + public async Task ReallyBeIdle(int runTicks = 5) { for (var i = 0; i < runTicks; i++) { @@ -281,7 +281,7 @@ public async Task CreateTestMap(bool initialized, ushort tileTypeId await Server.WaitPost(() => { TestMap.MapUid = sys.CreateMap(out TestMap.MapId, runMapInit: initialized); - TestMap.Grid = Server.MapMan.CreateGridEntity(TestMap.MapId); + TestMap.Grid = sys.CreateGridEntity(TestMap.MapId); TestMap.GridCoords = new EntityCoordinates(TestMap.Grid, 0, 0); TestMap.MapCoords = new MapCoordinates(0, 0, TestMap.MapId); sys.SetTile(TestMap.Grid.Owner, TestMap.Grid.Comp, TestMap.GridCoords, new Tile(tileTypeId)); diff --git a/Robust.UnitTesting/Robust.UnitTesting.csproj b/Robust.UnitTesting/Robust.UnitTesting.csproj index 81d0d4fe5c8..f9077a4eb28 100644 --- a/Robust.UnitTesting/Robust.UnitTesting.csproj +++ b/Robust.UnitTesting/Robust.UnitTesting.csproj @@ -28,4 +28,5 @@ + diff --git a/Robust.UnitTesting/RobustIntegrationTest.cs b/Robust.UnitTesting/RobustIntegrationTest.cs index 48be5149599..966635c0fe0 100644 --- a/Robust.UnitTesting/RobustIntegrationTest.cs +++ b/Robust.UnitTesting/RobustIntegrationTest.cs @@ -182,6 +182,89 @@ protected virtual ClientIntegrationInstance StartClient(ClientIntegrationOptions return instance; } + /// + /// Connects a client integration instance to a server integration instance. + /// + protected static async Task ConnectClient( + ServerIntegrationInstance server, + ClientIntegrationInstance client, + string? userName = null) + { + await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); + Assert.DoesNotThrow(() => client.SetConnectTarget(server)); + await client.WaitPost(() => ((IClientNetManager) client.NetMan).ClientConnect(null!, 0, userName!)); + } + + /// + /// Starts a connected client/server pair. + /// + protected async Task StartConnectedPair( + ServerIntegrationOptions? serverOptions = null, + ClientIntegrationOptions? clientOptions = null, + string? userName = null) + { + var server = StartServer(serverOptions); + var client = StartClient(clientOptions); + await ConnectClient(server, client, userName); + return new ConnectedIntegrationPair(server, client); + } + + /// + /// Runs the server and client in lockstep. + /// + protected static async Task RunTicksSync( + ServerIntegrationInstance server, + ClientIntegrationInstance client, + int ticks) + { + for (var i = 0; i < ticks; i++) + { + await server.WaitRunTicks(1); + await client.WaitRunTicks(1); + } + } + + /// + /// Disconnects a client integration instance from its server and runs both sides long enough to process it. + /// + protected static async Task DisconnectClient( + ServerIntegrationInstance server, + ClientIntegrationInstance client, + string reason = "") + { + await client.WaitPost(() => ((IClientNetManager) client.NetMan).ClientDisconnect(reason)); + await RunTicksSync(server, client, 5); + } + + protected sealed class ConnectedIntegrationPair : IAsyncDisposable + { + public ServerIntegrationInstance Server { get; } + public ClientIntegrationInstance Client { get; } + + private bool _disposed; + + public ConnectedIntegrationPair(ServerIntegrationInstance server, ClientIntegrationInstance client) + { + Server = server; + Client = client; + } + + public void Deconstruct(out ClientIntegrationInstance client, out ServerIntegrationInstance server) + { + client = Client; + server = Server; + } + + public async ValueTask DisposeAsync() + { + if (_disposed) + return; + + _disposed = true; + await DisconnectClient(Server, Client); + } + } + private bool ShouldPool(IntegrationOptions? options) { // If no options are provided, we assume we should pool @@ -323,7 +406,6 @@ public abstract class IntegrationInstance : IIntegrationInstance public ISharedPlayerManager PlayerMan { get; private set; } = default!; public INetManager NetMan { get; private set; } = default!; public IGameTiming Timing { get; private set; } = default!; - public IMapManager MapMan { get; private set; } = default!; public IConsoleHost ConsoleHost { get; private set; } = default!; public ISawmill Log { get; private set; } = default!; @@ -335,7 +417,6 @@ protected virtual void ResolveIoC(IDependencyCollection deps) PlayerMan = deps.Resolve(); Timing = deps.Resolve(); NetMan = deps.Resolve(); - MapMan = deps.Resolve(); ConsoleHost = deps.Resolve(); Log = deps.Resolve().GetSawmill("test"); } diff --git a/Robust.UnitTesting/RobustUnitTest.cs b/Robust.UnitTesting/RobustUnitTest.cs index 71e63806ac7..bdfc90ed64f 100644 --- a/Robust.UnitTesting/RobustUnitTest.cs +++ b/Robust.UnitTesting/RobustUnitTest.cs @@ -161,7 +161,6 @@ public void BaseSetup() } var entMan = deps.Resolve(); - var mapMan = deps.Resolve(); // Avoid discovering EntityCommands since they may depend on systems // that aren't available in a unit test context. @@ -193,7 +192,6 @@ public void BaseSetup() // RobustUnitTest is complete hot garbage. // This makes EventTables ignore *all* the screwed up component abuse it causes. entMan.EventBus.OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare(); // The nightmare never ends - mapMan.Initialize(); systems.Initialize(); deps.Resolve().LoadAssemblies(assemblies); @@ -203,7 +201,6 @@ public void BaseSetup() modLoader.TryLoadModulesFrom(ResPath.Root, ""); entMan.Startup(); - mapMan.Startup(); } [OneTimeTearDown] diff --git a/RobustToolbox.slnx b/RobustToolbox.slnx index 70f64dbc92e..5dd38516a24 100644 --- a/RobustToolbox.slnx +++ b/RobustToolbox.slnx @@ -7,11 +7,8 @@ - - - @@ -38,6 +35,7 @@ + @@ -86,6 +84,10 @@ + + + +