diff --git a/.buildkite/premerge.definition.yaml b/.buildkite/premerge.definition.yaml index 4cc85ded0e..b6af83953c 100755 --- a/.buildkite/premerge.definition.yaml +++ b/.buildkite/premerge.definition.yaml @@ -1,11 +1,11 @@ agent_queue_id: trigger-pipelines description: Build Unreal GDK github: - branch_configuration: [] + branch_configuration: ['!dry-run/* !skip-ci/* !doc/* !docs/*'] default_branch: "master" - pull_request_branch_filter_configuration: [] + pull_request_branch_filter_configuration: ['!dry-run/* !skip-ci/* !doc/* !docs/*'] teams: - name: Everyone permission: BUILD_AND_READ - name: gen/team/unreal - permission: MANAGE_BUILD_AND_READ \ No newline at end of file + permission: MANAGE_BUILD_AND_READ diff --git a/.buildkite/premerge.steps.yaml b/.buildkite/premerge.steps.yaml index d9062a60fa..5ed8b27368 100755 --- a/.buildkite/premerge.steps.yaml +++ b/.buildkite/premerge.steps.yaml @@ -1,25 +1,19 @@ --- -ci_version: &ci_version "1.2" -# This is designed to trap and retry failures because agent lost -# connection. Agent exits with -1 in this case. -agent_transients: &agent_transients - exit_status: -1 - limit: 3 - -# BK system error -bk_system_error: &bk_system_error - exit_status: 255 - limit: 3 - -# Job was interrupted by a signal (e.g. ctrl+c etc) -bk_interrupted_by_signal: &bk_interrupted_by_signal - exit_status: 15 - limit: 3 - -# Hook failure -bk_hook_failure: &bk_hook_failure - exit_status: 127 - limit: 3 +ci_version: &ci_version "${CI_VERSION:-0.14.0}" +common_env_vars: &common_env_vars + BUILD_TYPE: "GDK" + GDK_BRANCH: "main_branch::${BUILDKITE_BRANCH}" + PROJECT_BRANCH: "${PROJECT_BRANCH:-match_branch_name_pref::0.14.0}" + USE_FASTBUILD: "${USE_FASTBUILD:-True}" + IS_BUILDKITE_BUILD: "${IS_BUILDKITE_BUILD:-True}" + BUILD_ANDROID: "${BUILD_ANDROID:-False}" + CLEAN_BUILD: "${CLEAN_BUILD:-False}" + RUN_DEFAULT_TESTS: "${RUN_DEFAULT_TESTS:-True}" + SLOW_TESTS: "${SLOW_TESTS:-False}" + EXTRA_TESTS: "${EXTRA_TESTS:-}" + EXTRA_TESTS_RUN_NATIVE: "${EXTRA_TESTS_RUN_NATIVE:-False}" + EXTRA_TESTS_RUN_REPGRAPH: "${EXTRA_TESTS_RUN_REPGRAPH:-False}" + EXTRA_TESTS_RUNS: "${EXTRA_TESTS_RUNS:-1}" steps: # New build pipeline @@ -30,17 +24,10 @@ steps: build: branch: *ci_version message: "gdk-4.26 ${BUILDKITE_MESSAGE}" - env: - BUILD_TYPE: "GDK" - GDK_BRANCH: "main_branch::${BUILDKITE_BRANCH}" - ENGINE_BRANCH: "${ENGINE_BRANCH_426:-match_branch_name_pref_engine::4.26-SpatialOSUnrealGDK-0.13.1}" + env: + <<: *common_env_vars + ENGINE_BRANCH: "${ENGINE_BRANCH_426:-match_branch_name_pref_engine::4.26-SpatialOSUnrealGDK-0.14.0}" ENGINE_MAJOR: "4.26" - PROJECT_BRANCH: "${PROJECT_BRANCH:-match_branch_name_pref::0.13.1}" - USE_FASTBUILD: "True" - IS_BUILDKITE_BUILD: "True" - BUILD_ANDROID: "False" - SKIP_TESTS: "False" - CLEAN_BUILD: "False" # Trigger a 4.25 build - trigger: "unrealgdkbuild-ci" @@ -49,17 +36,10 @@ steps: build: branch: *ci_version message: "gdk-4.25 ${BUILDKITE_MESSAGE}" - env: - BUILD_TYPE: "GDK" # GDK or ENGINE - GDK_BRANCH: "main_branch::${BUILDKITE_BRANCH}" - ENGINE_BRANCH: "${ENGINE_BRANCH_425:-match_branch_name_pref_engine::4.25-SpatialOSUnrealGDK-0.13.1}" + env: + <<: *common_env_vars + ENGINE_BRANCH: "${ENGINE_BRANCH_425:-match_branch_name_pref_engine::4.25-SpatialOSUnrealGDK-0.14.0}" ENGINE_MAJOR: "4.25" - PROJECT_BRANCH: "${PROJECT_BRANCH:-match_branch_name_pref::0.13.1}" - USE_FASTBUILD: "True" - IS_BUILDKITE_BUILD: "True" - BUILD_ANDROID: "False" - SKIP_TESTS: "False" - CLEAN_BUILD: "False" # Trigger an Example Project build for any merges into master, preview or release branches of UnrealGDK - trigger: "unrealgdkexampleproject-nightly" diff --git a/.github/pull-request-template.md b/.github/pull-request-template.md index d62e9bf28a..1a5b769103 100644 --- a/.github/pull-request-template.md +++ b/.github/pull-request-template.md @@ -1,3 +1,6 @@ +#### This is a public repo +So don't post or commit non-public information. + #### Description Describe your changes here. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b46f4d50f..146f5b5095 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 **Note**: Since GDK for Unreal v0.10.0, the changelog is published in both English and Chinese. The Chinese version of each changelog is shown after its English version.
**注意**:自虚幻引擎开发套件 v0.10.0 版本起,其日志提供中英文两个版本。每个日志的中文版本都置于英文版本之后。 -## [`x.y.z`] - Unreleased + +## [`0.14.0`] - 2021-08-16 + +### Breaking changes: +- The `Handover` variable specifier has been deprecated. It should be replaced with the standard `Replicated` variable specifier and restricting the replication with the new `COND_ServerOnly` replication condition in `GetLifetimeReplicatedProps`. `Handover` variables will try to replicate using the new replication condition, but support will be removed in the next release. +- Reworked AlwaysInterested functionality to run on authoritative servers, and owning clients. The previous behaviour was for it to only run on PlayerController classes, on the client only. +- `bUseNetOwnerActorGroup` actor setting has been removed with the default behavior now true inside LayeredLBStrategy. Extend this class if you wish to alter this default behavior. + +### Features: +- Added a "Clean and Generate Schema" option to the Schema menu, which lets you delete the SchemaDatabase.uasset, all generated schema files, and run a Full Scan in one click. +- GDK heartbeat settings are now used to control the worker heartbeat configurations. +- Added a setting to control which CrossServer RPC implementation is used. Both feature mentioned below are only enabled when the RoutingWorker is the chosen implementation. Spatial commands are still the default for now. +- Added reliable CrossServer RPC. Reliable CrossServer RPC now require a sender actor which will be the reference point for ordering in a multi-worker environment. An additional UFUNCTION Tag, Unordered, was added to opt-out of this requirement. +- Added NetWriteFence UFUNCTION Tag. This tag is used when Network writes to an actor should be ordered with regard to updates to another actor. This is relevant in worker recovery/snapshot reloading to get some ordering guarantees when SpatialOS can write updates to entities in any order. +- Inspector process is automatically started when starting PIE. This means you can re-use existing inspector browser sessions. +- Visual Logger now supports multi-worker environments. +- Gameplay Debugger now supports multi-worker environments. +- Added `StopInsights` command to `SpatialExecServerCmd`, which takes no additional parameters and disables any Insight capturing on the target worker + - Example usage: "SpatialExecServerCmd local StopInsights +- Renamed `StartInsights` command args - `trace` -> `channel` and `tracefile` -> `file` + +### Bug fixes: +- Added a pop-up message when schema generation fails, which suggests running a Clean and Generate to fix a bad schema state. +- Fixed a bug that left the SchemaDatabase.uasset file locked after a failed schema generation. +- Fixed an issue with migration diagnostic logging failing, when the actor did not have authority. +- Fixed an issue where migration diagnostic tool would crash if the target actor's owner couldn't be found. +- Fixed an issue where during shutdown unregistering NetGUIDs could cause an asset load and program stall. +- Fix RPC timeouts for parameters referencing assets that can be asynchronously loaded. +- Fixed the test settings overrides config filename in `Spatial World Settings` so that the file path is relative to the game directory. +- Fix editor encountering exceptions when shutting down during a PIE session. +- The runtime will shut down slightly faster after a PIE session. +- Fixed a rare issue where one would see a change to the owner field but not the changes to owner-only fields. +- Prevented a client crash that occurs if there is a mismatch between the client and server schema hash. +- Fixed an issue for actors with bNetLoadOnClient. A dynamic subobject removed from such an actor while out of a client's view will now be properly removed on the client when the actor comes back into the client's view. +- Fixed an issue that caused `UnrealGDK/Setup.sh` to report `sed: can't read : No such file or directory` when run on macOS. +- Static subobjects on bNetLoadOnClient actors are now removed on clients in a manner matching native unreal's behavior. This change affects subobjects removed by the server while the actor is not in the client's interest. +- Fixed an issue where multicast rpcs could be overwritten and then dropped on authority flicker. +- Fixed issue using the runtime snapshot endpoint with a local deployment, using `localhost:5006/snapshot` works again and creates a snapshot. +- Dormant actors will now always have their channels closed correctly when entering dormancy. + +### Internal: +- Hide the Test MultiworkerSettings and GridStrategy classes from displaying in the editor. These are meant to only be used in Tests. +- Reserved entity IDs previously expired after 3 minutes. Reserved Entity IDs now no longer expire, and persist until used. +- A test was calling `SetReplicates` on an actor over which it did not have authority. This was causing warnings to be triggered. We've fixed this by reverting the actor's role at the end of the test, so that the actor is not left in an unexpected state. +- Added support for clients to disconnect during a test in the automated test framework. +- Modified ActorSystem's Ownership and Simulated Subviews to take player ownership into account. ## [`0.13.1`] - 2021-06-02 + ### Breaking changes: - Event tracing has been optimised to reduce overhead when tracing events in general and in particular when events are not sampled. The tracing API has been modified to accommodate these improvements. You will have to modify your project if you use the API. @@ -23,6 +69,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Event tracing filter support (configured via `UEventTracingSamplingSettings`). ## [`0.13.0`] - 2021-05-17 + ### Breaking changes: - Removed support for Unreal Engine 4.24. - `MaxRPCRingBufferSize` setting has been removed. This was previously used to specify the RPC ring buffer size when generating schema. Now, `DefaultRPCRingBufferSize` is used, and can be overridden per RPC type using `RPCRingBufferSizeOverrides`. @@ -57,6 +104,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `bOnlyRelevantToOwner` is now supported. Ownership must be setup prior to the first replication of the `Actor` otherwise it will be ignored. - Added a property to specify the test settings overrides config filename in the `World Settings` so that maps can share config files during automated testing. This replaces the option to automatically use the map name to determine the config filename. - Added a `-FailOnNetworkFailure` flag that makes a Spatial-enabled game fail on any NetworkFailure. +- Simulated Player deployments no longer depend on DeploymentLauncher for readiness. You can now restart them via the Console and expect them to reconnect to your main deployment. DeploymentLauncher will also restart any crashed or incorrectly finished simulated players applications. +- Added `URemotePossessionComponent` to deal with Cross-Server Possession. Add this componenet to an AController, it will possess the Target Pawn after OnAuthorityGained. It can be implemented in C++ and Blueprint. ### Bug fixes: - Fixed the exception that was thrown when adding and removing components in Spatial component callbacks. @@ -78,6 +127,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed an issue with replicating references to stably named dynamically added subobjects of dynamic actors. - Fixed an issue during client logout where a client's corresponding Actors were not cleaned up correctly. - Fixed issue with `SpatialDebugger` crashing when client travelling. +- Fixed an issue where a NetworkFailure won't be reported when connecting to a deployment that doesn't support dev_login with a developer token, and in some other configuration-dependent cases. +- Fixed a Windows compile issue when updating the worker SDK which did not recompile code dependent on the updated libs/dlls. ## [`0.12.0`] - 2021-02-01 @@ -102,6 +153,7 @@ These functions and structs can be referenced in both code and blueprints and it - Running without Ring Buffered RPCs is no longer supported, and the option has been removed from SpatialGDKSettings. - The schema database format has been updated and versioning introduced. Please regenerate your schema after updating. - The CookAndGenerateSchemaCommandlet no longer automatically deletes previously generated schema. Deletion of previously generated schema is now controlled by the `-DeleteExistingGeneratedSchema` flag. +- Event tracing has been optimised to reduce overhead when tracing events in general and in particular when events are not sampled. The tracing API has been modified to accomidate these improvements. You will have to modify your project if you use the API. ### Features: - The DeploymentLauncher tool can be used to start multiple simulated player deployments at once. @@ -155,15 +207,13 @@ These functions and structs can be referenced in both code and blueprints and it - Inspector URL is now http://localhost:33333/inspector-v2 - Inspector version can be overridden in the SpatialGDKEditorSettings under `Inspector Version Override` - The SpatialNetDriver can disconnect a client worker when given the system entity ID for that client and does so when `GameMode::PreLogin` returns with a non-empty error message. -- Unreal Engine version 4.26.0 is supported! Refer to https://documentation.improbable.io/gdk-for-unreal/docs/keep-your-gdk-up-to-date for versioning information and how to upgrade. +- Unreal Engine version 4.26.0 is supported! Refer to https://networking.docs.improbable.io/gdk-for-unreal/v0.14.0/workflows/keep-your-gdk-up-to-date for versioning information and how to upgrade. - Running with an out-of-date schema database reports a version warning when attempting to launch in editor. - Reworked schema generation (incremental and full) pop-ups to be clearer. - Added cross-server variants of ability activation functions on the Ability System Component. - Added `SpatialSwitchHasAuthority` function to differentiate authoritative server, non-authoritative server, and clients. This can be called in code or used in blueprints that derive from actor. - Added blueprint callable function `GetMaxDynamicallyAttachedSubobjectsPerClass` to `USpatialStatics` that gets the maximum dynamically attached subobjects per class as set in `SpatialGDKSettings` -- Simulated Player deployments no longer depend on DeploymentLauncher for readiness. You can now restart them via the Console and expect them to reconnect to your main deployment. DeploymentLauncher will also restart any crashed or incorrectly finished simulated players applications. - Reworked schema generation (incremental + full) pop-ups to be clearer. -- Added `URemotePossessionComponent` to deal with Cross-Server Possession. Add this componenet to an AController, it will possess the Target Pawn after OnAuthorityGained. It can be implemented in C++ and Blueprint. ### Bug fixes: - Fixed a bug that stopped the travel URL being used for initial Spatial connection if the command line arguments could not be used. @@ -200,7 +250,6 @@ These functions and structs can be referenced in both code and blueprints and it - Fixed a bug where consecutive invocations of CookAndGenerateSchemaCommandlet for different levels could fail when running the schema compiler. - Fixed an issue where GameMode values won't be replicated between server workers if it's outside their Interest. - Fixed gameplay cues receiving OnActive/WhileActive events twice on the predicting client in a multi-worker single-process PIE environment. -- Fixed an issue where a NetworkFailure won't be reported when connecting to a deployment that doesn't support dev_login with a developer token, and in some other configuration-dependent cases. - Fixed a crash that occured when opening the session frontend with VS 16.8.0 using the bundled dbghelp.dll. - Spatial Debugger no longer consumes input. - Fixed an issue where we would always create a folder for a snapshots for a deployment even when we made no snapshots @@ -212,7 +261,7 @@ These functions and structs can be referenced in both code and blueprints and it ## [`0.11.0`] - 2020-09-03 ### Breaking changes: -- We no longer support Unreal Engine version 4.23. We recommend that you upgrade to the newest version 4.25 to continue receiving updates. See [Unreal Engine Version Support](https://documentation.improbable.io/gdk-for-unreal/docs/versioning-scheme#section-unreal-engine-version-support) for more information on versions. Follow the instructions in [Update your GDK](https://documentation.improbable.io/gdk-for-unreal/docs/keep-your-gdk-up-to-date) to update to 4.25. +- We no longer support Unreal Engine version 4.23. We recommend that you upgrade to the newest version 4.25 to continue receiving updates. See [Unreal Engine Version Support](https://networking.docs.improbable.io/gdk-for-unreal/v0.14.0/support/versioning-scheme#section-unreal-engine-version-support) for more information on versions. Follow the instructions in [Update your GDK](https://networking.docs.improbable.io/gdk-for-unreal/v0.14.0/workflows/keep-your-gdk-up-to-date) to update to 4.25. - We have removed multi-worker settings from the `SpatialWorldSettings` properties and added them to a new class `USpatialMultiWorkerSettings`. To update your project, create a derived `USpatialMultiWorkerSettings` class mimicking your previous configuration. Then, in your level’s World settings, select that class as the `Multi-worker settings class` property. - The `-nocompile` flag used with `Buildworker.bat` is now split into two. Use the following flags: - `-nobuild` to skip building the game binaries. @@ -690,7 +739,7 @@ Features listed in this section are not ready to use. However, in the spirit of 1. Run `Setup.bat`, which is located in the root directory of the `UnrealEngine` repository. 1. Run `GenerateProjectFiles.bat`, which is in the same root directory. -For more information, check the [Keep your GDK up to date](https://documentation.improbable.io/gdk-for-unreal/docs/keep-your-gdk-up-to-date) SpatialOS documentation. +For more information, check the [Keep your GDK up to date](https://networking.docs.improbable.io/gdk-for-unreal/v0.14.0/workflows/keep-your-gdk-up-to-date) SpatialOS documentation. ### Features: - You can now call `SpatialToggleMetricsDisplay` from the console in your Unreal clients in order to view metrics. `bEnableMetricsDisplay` must be enabled on clients where you want to use this feature. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5e42de6d2c..6a75501562 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ We welcome any and all ## Coding standards -See the [GDK for Unreal C++ coding standards guide](https://documentation.improbable.io/gdk-for-unreal/docs/coding-standards). +See the [GDK for Unreal C++ coding standards guide](https://networking.docs.improbable.io/gdk-for-unreal/v0.14.0/get-involved/contribute-to-the-gdk/coding-standards). ## Branches Most of our active development is in the `master` branch, so we prefer to take pull requests there. If you're contributing to our [Unreal Engine fork](https://github.com/improbableio/UnrealEngine/tree/4.26-SpatialOSUnrealGDK), please target your pull requests at the `4.26-SpatialOSUnrealGDK` branch, which is our development branch in that repo. diff --git a/README.md b/README.md index 6d63f43598..55e9ee0457 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![](SpatialGDK/Documentation/spatialos-gdkforunreal-header.png) -The SpatialOS Game Development Kit (GDK) for Unreal is an Unreal Engine fork and plugin with associated projects. It gives you the features of [SpatialOS](https://documentation.improbable.io/spatialos-overview/docs), within the familiar workflows and APIs of Unreal Engine. For more information, please see the GDK's [documentation website](https://documentation.improbable.io/gdk-for-unreal/docs). +The SpatialOS Game Development Kit (GDK) for Unreal is an Unreal Engine fork and plugin with associated projects. It gives you the features of [SpatialOS](https://networking.docs.improbable.io/spatialos-overview), within the familiar workflows and APIs of Unreal Engine. For more information, please see the GDK's [documentation website](https://networking.docs.improbable.io/gdk-for-unreal/). This is the repository for the GDK plugin, which includes the Starter Template (a blank starter project). @@ -13,7 +13,7 @@ In addition to the plugin, the GDK also includes: You must be a member of the [Epic Games organization](https://github.com/EpicGames) on GitHub to access this. If you aren't, the link returns a 404 error. * [The Example Project](https://github.com/spatialos/UnrealGDKExampleProject) -If you’re an Unreal game developer and you’re ready to try out the GDK, follow the [Get started guide](https://documentation.improbable.io/gdk-for-unreal/docs/get-started-introduction). +If you’re an Unreal game developer and you’re ready to try out the GDK, follow the [Get started guide](https://networking.docs.improbable.io/gdk-for-unreal/v0.14.0/get-started/get-started-introduction). ## SpatialOS Unreal Engine fork changes In order to transform Unreal from a single-server engine to a distributed model, we made a number of small changes to Unreal Engine code. We are attempting to consolidate and remove (or submit as PR to Epic) as many of these changes as possible. You can see the changes in the [SpatialOS Unreal Engine fork repository](https://github.com/improbableio/UnrealEngine). @@ -21,16 +21,16 @@ In order to transform Unreal from a single-server engine to a distributed model, > In order to get access to this fork, you need to link your GitHub account to a verified Epic Games account, and to have agreed to Epic's license. You will not be able to use the GDK for Unreal without doing this first. To do this, see the [Unreal documentation](https://www.unrealengine.com/en-US/ue4-on-github). ## Recommended use -To understand the feature-completeness, stability, performance, and support levels you can expect from the GDK, see the [product maturity lifecycle page](https://documentation.improbable.io/gdk-for-unreal/docs/product-maturity-lifecycle). For more information, visit the [development roadmap](https://github.com/spatialos/UnrealGDK/projects/1) and [Unreal features support](https://documentation.improbable.io/gdk-for-unreal/docs/unreal-features-support) pages, and contact us via our forums, or on Discord. +To understand the feature-completeness, stability, performance, and support levels you can expect from the GDK, see the [product maturity lifecycle page](https://networking.docs.improbable.io/gdk-for-unreal/v0.14.0/support/product-maturity-lifecycle). For more information, visit the [development roadmap](https://github.com/spatialos/UnrealGDK/projects/1) and [Unreal features support](https://networking.docs.improbable.io/gdk-for-unreal/v0.14.0/support/unreal-features-support) pages, and contact us via our forums, or on Discord. ## Versioning and support -Please visit [this page](https://documentation.improbable.io/gdk-for-unreal/docs/versioning-scheme) for a description of the GDK's versioning scheme, and which branches to use when developing. +Please visit [this page](https://networking.docs.improbable.io/gdk-for-unreal/v0.14.0/support/versioning-scheme) for a description of the GDK's versioning scheme, and which branches to use when developing. ## Contributions We welcome [Github issues](https://github.com/spatialos/UnrealGDK/issues) from all users, and accept public contributions subject to the signing of our Contributors License Agreement - please see our [contributions](CONTRIBUTING.md) policy for more details. ## Run into problems? -* [Troubleshooting](https://documentation.improbable.io/gdk-for-unreal/docs/troubleshooting) +* [Troubleshooting](https://networking.docs.improbable.io/gdk-for-unreal/v0.14.0/workflows/troubleshooting) * [Known issues](https://github.com/spatialos/UnrealGDK/projects/2) ## Give us feedback diff --git a/RequireSetup b/RequireSetup index 331ad12cbc..82102a8751 100644 --- a/RequireSetup +++ b/RequireSetup @@ -1,4 +1,4 @@ -Increment the below number whenever it is required to run Setup.bat as part of a new commit. +Increment the below number whenever it is required to run Setup.bat or Setup.sh as part of a new commit. Our git hooks will detect this file has been updated and automatically run Setup.bat on pull. -86 +91 diff --git a/Setup.bat b/Setup.bat index 4ae9b2bb44..40c8b26d57 100644 --- a/Setup.bat +++ b/Setup.bat @@ -60,9 +60,10 @@ call :MarkStartOfBlock "Setup variables" set BINARIES_DIR=%~dp0SpatialGDK\Binaries\ThirdParty\Improbable rem Copy schema to the projects spatial directory. - set SCHEMA_COPY_DIR=%~dp0..\..\..\spatial\schema\unreal\gdk - set SCHEMA_STD_COPY_DIR=%~dp0..\..\..\spatial\build\dependencies\schema\standard_library set SPATIAL_DIR=%~dp0..\..\..\spatial + set SPATIAL_TOOLS_COPY_DIR=%SPATIAL_DIR%\tools + set SCHEMA_COPY_DIR=%SPATIAL_DIR%\schema\unreal\gdk + set SCHEMA_STD_COPY_DIR=%SPATIAL_DIR%\build\dependencies\schema\standard_library set DOMAIN_ENVIRONMENT_VAR= set DOWNLOAD_MOBILE= for %%A in (%*) do ( @@ -94,6 +95,7 @@ call :MarkStartOfBlock "Clean folders" if exist "%SPATIAL_DIR%" ( rd /s /q "%SCHEMA_STD_COPY_DIR%" 2>nul rd /s /q "%SCHEMA_COPY_DIR%" 2>nul + rd /s /q "%SPATIAL_TOOLS_COPY_DIR%" 2>nul ) call :MarkEndOfBlock "Clean folders" @@ -109,11 +111,13 @@ call :MarkStartOfBlock "Create folders" if exist "%SPATIAL_DIR%" ( md "%SCHEMA_STD_COPY_DIR%" >nul 2>nul md "%SCHEMA_COPY_DIR%" >nul 2>nul + md "%SPATIAL_TOOLS_COPY_DIR%" >nul 2>nul ) call :MarkEndOfBlock "Create folders" call :MarkStartOfBlock "Retrieve dependencies" call :ExecuteAndCheck spatial package retrieve tools schema_compiler-x86_64-win32 %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\tools\schema_compiler-x86_64-win32.zip" + call :ExecuteAndCheck spatial package retrieve tools snapshot_converter-x86_64-win32 %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\tools\snapshot_converter-x86_64-win32.zip" call :ExecuteAndCheck spatial package retrieve schema standard_library %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\schema\standard_library.zip" call :ExecuteAndCheck spatial package retrieve worker_sdk c_headers %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c_headers.zip" call :ExecuteAndCheck spatial package retrieve worker_sdk c-dynamic-x86_64-vc141_md-win32 %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-vc141_md-win32.zip" @@ -137,6 +141,7 @@ call :MarkStartOfBlock "Unpack dependencies" "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-clang1000-linux.zip\" -DestinationPath \"%BINARIES_DIR%\Linux\" -Force; "^ "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\csharp_cinterop.zip\" -DestinationPath \"%BINARIES_DIR%\Programs\worker_sdk\csharp_cinterop\" -Force; "^ "Expand-Archive -Path \"%CORE_SDK_DIR%\tools\schema_compiler-x86_64-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Programs\" -Force; "^ + "Expand-Archive -Path \"%CORE_SDK_DIR%\tools\snapshot_converter-x86_64-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Programs\" -Force; "^ "Expand-Archive -Path \"%CORE_SDK_DIR%\schema\standard_library.zip\" -DestinationPath \"%BINARIES_DIR%\Programs\schema\" -Force;" if defined DOWNLOAD_MOBILE ( @@ -148,6 +153,13 @@ call :MarkStartOfBlock "Unpack dependencies" xcopy /s /i /q "%BINARIES_DIR%\Headers\include" "%WORKER_SDK_DIR%" call :MarkEndOfBlock "Unpack dependencies" +REM When the worker libs are updated outside of the build system, timestamps are not updated on unzip so +REM no change is registered, resulting in broken builds (lib / dll mismatch). +call :MarkStartOfBlock "Touching worker libs" +call :TouchFilesInFolder %BINARIES_DIR%\Win64\ +call :TouchFilesInFolder %BINARIES_DIR%\Linux\ +call :MarkEndOfBlock "Touching worker libs" + if exist "%SPATIAL_DIR%" ( call :MarkStartOfBlock "Copy standard library schema" echo Copying standard library schemas to "%SCHEMA_STD_COPY_DIR%" @@ -158,6 +170,17 @@ if exist "%SPATIAL_DIR%" ( echo Copying schemas to "%SCHEMA_COPY_DIR%". xcopy /s /i /q "%~dp0\SpatialGDK\Extras\schema" "%SCHEMA_COPY_DIR%" call :MarkEndOfBlock "Copy GDK schema" + + call :MarkStartOfBlock "Copy schema compiler" + echo Copying schema compiler to "%SPATIAL_TOOLS_COPY_DIR%". + xcopy /s /i /q "%BINARIES_DIR%\Programs\schema_compiler.exe" "%SPATIAL_TOOLS_COPY_DIR%" + call :MarkEndOfBlock "Copy schema compiler + + call :MarkStartOfBlock "Copy snapshot converter" + echo Copying snapshot converter to "%SPATIAL_TOOLS_COPY_DIR%" + xcopy /s /i /q "%BINARIES_DIR%\Programs\snapshot_converter.exe" "%SPATIAL_TOOLS_COPY_DIR%" + xcopy /s /i /q "%BINARIES_DIR%\Programs\snapshot.descriptor" "%SPATIAL_TOOLS_COPY_DIR%" + call :MarkEndOfBlock "Copy snapshot converter" ) call :MarkStartOfBlock "Build C# utilities" @@ -176,6 +199,13 @@ if not defined NO_PAUSE ( exit /b %ERRORLEVEL% +:TouchFilesInFolder +echo Touching lib/dll files in: %~1 +pushd "%~1" +for /R %%f in (*.lib,*.dll,*.so) do copy /b %%f +,, +popd +exit /b 0 + :MarkStartOfBlock echo Starting: %~1 exit /b 0 diff --git a/Setup.sh b/Setup.sh index 2cdef332f1..bf7c649662 100755 --- a/Setup.sh +++ b/Setup.sh @@ -42,7 +42,7 @@ if [[ -e .git/hooks ]]; then cp -R "$(pwd)/SpatialGDK/Extras/git/." "$(pwd)/.git/hooks" # We pass Setup.sh args, such as --mobile, to the post-merge hook to run Setup.sh with the same args in future. - sed -i "" -e "s/SETUP_ARGS/$*/g" .git/hooks/post-merge + sed -i -e "s/SETUP_ARGS/$*/g" .git/hooks/post-merge # This needs to be runnable. chmod +x .git/hooks/pre-commit diff --git a/SetupIncTraceLibs.bat b/SetupIncTraceLibs.bat index d498fa66f8..2b919bb2c6 100644 --- a/SetupIncTraceLibs.bat +++ b/SetupIncTraceLibs.bat @@ -18,8 +18,8 @@ call :MarkStartOfBlock "Create folders" call :MarkEndOfBlock "Create folders" call :MarkStartOfBlock "Retrieve dependencies" - spatial package retrieve internal trace-dynamic-x86_64-vc141_md-win32 15.0.1 "%CORE_SDK_DIR%\trace_lib\trace-win32.zip" - spatial package retrieve internal trace-dynamic-x86_64-clang1000-linux 15.0.1 "%CORE_SDK_DIR%\trace_lib\trace-linux.zip" + spatial package retrieve internal legacy-trace-dynamic-x86_64-vc141_md-win32 %PINNED_CORE_SDK_VERSION% "%CORE_SDK_DIR%\trace_lib\trace-win32.zip" + spatial package retrieve internal legacy-trace-dynamic-x86_64-clang1000-linux %PINNED_CORE_SDK_VERSION% "%CORE_SDK_DIR%\trace_lib\trace-linux.zip" call :MarkEndOfBlock "Retrieve dependencies" REM There is a race condition between retrieve and unzip, add version call to stall briefly @@ -28,12 +28,7 @@ call spatial version call :MarkStartOfBlock "Unpack dependencies" powershell -Command "Expand-Archive -Path \"%CORE_SDK_DIR%\trace_lib\trace-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Win64\" -Force;"^ "Expand-Archive -Path \"%CORE_SDK_DIR%\trace_lib\trace-linux.zip\" -DestinationPath \"%BINARIES_DIR%\Linux\" -Force;" - xcopy /s /i /q "%BINARIES_DIR%\Win64\include\improbable" "%WORKER_SDK_DIR%\improbable\legacy" - - set LEGACY_FOLDER=%WORKER_SDK_DIR%\improbable\legacy\ - set TRACE_HEADER="%LEGACY_FOLDER%trace.h" - powershell -Command "(Get-Content '%TRACE_HEADER%').replace('#include ', '#include ') | Set-Content -Force '%TRACE_HEADER%'" - + xcopy /s /i /q "%BINARIES_DIR%\Win64\include\improbable" "%WORKER_SDK_DIR%\improbable" call :MarkEndOfBlock "Unpack dependencies" call :MarkEndOfBlock "%~0" diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs index d1914089b6..7931e12953 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs @@ -13,6 +13,8 @@ namespace Improbable { public static class Build { + static string runUATBatPath; + public static void Main(string[] args) { var help = args.Count(arg => arg == "/?" || arg.ToLowerInvariant() == "--help") > 0; @@ -101,7 +103,7 @@ public static void Main(string[] args) Console.WriteLine("Engine is at: " + unrealEngine); } - string runUATBat = Path.Combine(unrealEngine, @"Engine\Build\BatchFiles\RunUAT.bat"); + runUATBatPath = Path.Combine(unrealEngine, @"Engine\Build\BatchFiles\RunUAT.bat"); string buildBat = Path.Combine(unrealEngine, @"Engine\Build\BatchFiles\Build.bat"); if (gameName == baseGameName + "Editor") @@ -141,17 +143,12 @@ public static void Main(string[] args) StartEditorScript, new UTF8Encoding(false)); // The runtime currently requires all workers to be in zip files. Zip the batch file. - Common.RunRedirected(runUATBat, new[] - { - "ZipUtils", - "-add=" + Quote(windowsEditorPath), - "-archive=" + Quote(Path.Combine(outputDir, "UnrealEditor@Windows.zip")), - }); + Zip(Quote(windowsEditorPath), Quote(Path.Combine(outputDir, "UnrealEditor@Windows.zip"))); } else if (gameName == baseGameName) { Common.WriteHeading(" > Building client."); - Common.RunRedirected(runUATBat, new[] + Common.RunRedirected(runUATBatPath, new[] { "BuildCookRun", noBuild ? "-nobuild" : "-build", @@ -184,17 +181,12 @@ public static void Main(string[] args) RenameExeForLauncher(windowsTargetPath, baseGameName); - Common.RunRedirected(runUATBat, new[] - { - "ZipUtils", - "-add=" + Quote(windowsTargetPath), - "-archive=" + Quote(Path.Combine(outputDir, "UnrealClient@Windows.zip")), - }); + Zip(Quote(windowsTargetPath), Quote(Path.Combine(outputDir, "UnrealClient@Windows.zip"))); } else if (gameName == baseGameName + "SimulatedPlayer") // This is for internal use only. We do not support Linux clients. { Common.WriteHeading(" > Building simulated player."); - Common.RunRedirected(runUATBat, new[] + Common.RunRedirected(runUATBatPath, new[] { "BuildCookRun", noBuild ? "-nobuild" : "-build", @@ -250,17 +242,12 @@ public static void Main(string[] args) } var archiveFileName = "UnrealSimulatedPlayer@Linux.zip"; - Common.RunRedirected(runUATBat, new[] - { - "ZipUtils", - "-add=" + Quote(linuxSimulatedPlayerPath), - "-archive=" + Quote(Path.Combine(outputDir, archiveFileName)), - }); + Zip(Quote(linuxSimulatedPlayerPath), Quote(Path.Combine(outputDir, archiveFileName))); } else if (gameName == baseGameName + "Server") { Common.WriteHeading(" > Building worker."); - Common.RunRedirected(runUATBat, new[] + Common.RunRedirected(runUATBatPath, new[] { "BuildCookRun", noBuild ? "-nobuild" : "-build", @@ -301,17 +288,12 @@ public static void Main(string[] args) LinuxScripts.WriteWithLinuxLineEndings(LinuxScripts.GetUnrealWorkerShellScript(baseGameName), Path.Combine(serverPath, "StartWorker.sh")); } - Common.RunRedirected(runUATBat, new[] - { - "ZipUtils", - "-add=" + Quote(serverPath), - "-archive=" + Quote(Path.Combine(outputDir, $"UnrealWorker@{assemblyPlatform}.zip")) - }); + Zip(Quote(serverPath), Quote(Path.Combine(outputDir, $"UnrealWorker@{assemblyPlatform}.zip"))); } else if (gameName == baseGameName + "Client") { Common.WriteHeading(" > Building client."); - Common.RunRedirected(runUATBat, new[] + Common.RunRedirected(runUATBatPath, new[] { "BuildCookRun", noBuild ? "-nobuild" : "-build", @@ -346,12 +328,7 @@ public static void Main(string[] args) RenameExeForLauncher(windowsClientPath, baseGameName + "Client"); - Common.RunRedirected(runUATBat, new[] - { - "ZipUtils", - "-add=" + Quote(windowsClientPath), - "-archive=" + Quote(Path.Combine(outputDir, "UnrealClient@Windows.zip")), - }); + Zip(Quote(windowsClientPath), Quote(Path.Combine(outputDir, "UnrealClient@Windows.zip"))); } else { @@ -367,6 +344,18 @@ public static void Main(string[] args) } } + private static void Zip(string pathToItem, string compressedOutputPath) + { + Common.RunRedirected(runUATBatPath, new[] + { + "ZipUtils", + "-NoP4", + "-add=" + pathToItem, + "-archive=" + compressedOutputPath, + }); + } + + private static string Quote(string toQuote) { return $"\"{toQuote}\""; diff --git a/SpatialGDK/Documentation/README.md b/SpatialGDK/Documentation/README.md index c2e6b7dc05..9c1fc83f1d 100644 --- a/SpatialGDK/Documentation/README.md +++ b/SpatialGDK/Documentation/README.md @@ -1,3 +1,3 @@ # The SpatialOS GDK for Unreal documentation -The GDK's documentation is available at https://documentation.improbable.io/gdk-for-unreal/docs. \ No newline at end of file +The GDK's documentation is available at https://networking.docs.improbable.io/gdk-for-unreal/. \ No newline at end of file diff --git a/SpatialGDK/Extras/git/pre-commit b/SpatialGDK/Extras/git/pre-commit index 7e0fb09383..b19938c87f 100644 --- a/SpatialGDK/Extras/git/pre-commit +++ b/SpatialGDK/Extras/git/pre-commit @@ -11,7 +11,7 @@ fi # Hit 'em with that clang-format for FILE in $(git diff --cached --name-only --diff-filter=d); do - if [[ ( "${FILE}" == *.h ) || ( "${FILE}" == *.cpp ) ]]; then + if [[ ( "${FILE}" == *.h ) || ( "${FILE}" == *.cpp ) || ( "${FILE}" == *.cxx ) ]]; then ${clang_command} --verbose -i "${FILE}" fi done diff --git a/SpatialGDK/Extras/internal-documentation/release-process.md b/SpatialGDK/Extras/internal-documentation/release-process.md index 8972d083b5..5052cf8efa 100644 --- a/SpatialGDK/Extras/internal-documentation/release-process.md +++ b/SpatialGDK/Extras/internal-documentation/release-process.md @@ -9,7 +9,7 @@ This document outlines the process for releasing a version of the GDK for Unreal ## Release 1. Before you start working towards a release, ask product and engineering management to define it, so that all stakeholders are on the same page. They should use [this template](https://brevi.link/justify-your-release). 1. Check [this filter](https://improbableio.atlassian.net/issues/?filter=-1&jql=project%20%3D%20UNR%20AND%20priority%20%3D%20Blocker%20AND%20resolution%20%3D%20Unresolved%20order%20by%20updated%20DESC&atlOrigin=eyJpIjoiMDk0ZDY1ZWY5OGEzNDUyNzg2YjVjZjg5ZWI0YzRiNDMiLCJwIjoiaiJ9) for unresolved blockers in the UNR Jira Project. All blockers must be resolved prior to the creation of the release candidate. -1. Notify `#dev-unreal-internal` that you intend to commence a release. Ask if anyone `@here` knows of any blocking defects in code or documentation that should be resolved prior to commencement of the release process. +1. Notify `#unreal-eng-safespace` that you intend to commence a release. Ask if anyone `@here` knows of any blocking defects in code or documentation that should be resolved prior to commencement of the release process. 1. Notify `@techwriters` in #docs that they may commence their [CHANGELOG review process](https://improbableio.atlassian.net/l/c/4FsZzbHk). 1. If nobody objects to the release, navigate to [unrealgdk-release](https://buildkite.com/improbable/unrealgdk-release/) and select the New Build button. 1. In the Message field type "Releasing [GDK release version]". @@ -20,9 +20,11 @@ This document outlines the process for releasing a version of the GDK for Unreal 1. In the "UnrealGDK component release version" field enter the GDK release version. 1. The "UnrealGDK source branch" field is prepopulated with `master`. Leave it as is if you're executing a major or minor release, change it to `release` if you're executing a patch release. 1. The "UnrealEngine source branches" field should be prepopulated with the source branches of the latest fully supported and legacy supported Unreal Engine versions. If you're executing a patch release you'll need to suffix each branch with `-release`.
Wrong prepopulated branches? If the prepopulated branches are wrong, select the button with an X at the upper-right corner of the form, and then select "Cancel" to stop this build of unrealgdk-release. Then, on the UnrealGDK's `master` branch at [`.buildkite/release.steps.yaml#L32`](https://github.com/spatialos/UnrealGDK/blob/master/.buildkite/release.steps.yaml#L32), update the default branches to the latest, merge that change and restart this release process
+1. Notify `#unreal-eng-safespace` that: "@here I'm about to cut release candidates from master branches. DO NOT MERGE into master branches in any repos until I give the all clear.". (Note: This step is not necessary if you're executing a patch release, as these are cut from the `release` branch). 1. Select "Continue" and move onto the next step. -1. Wait for the "Prepare the release" step to run, it takes about 20 minutes, maybe grab a coffee? -1. Once the "Prepare the release" step has passed the "Build & upload all UnrealEngine release candidates" step will commence.
While those builds run, take a look at the top of the build page, where you'll notice a new [annotation](https://buildkite.com/docs/agent/v3/cli-annotate): "your human labour is now required to complete the tasks listed in the PR descriptions and unblock the pipeline to resume the release."
Click through to the PRs using the links in the annotations and follow the steps. Come back when you're done. +1. Wait for the "Prepare the release" step to run, it takes about 45 minutes, maybe grab a coffee? +1. Once the "Prepare the release" step has passed, notify `#unreal-eng-safespace` that: "Release candidates have been cut, feel free to merge into master again" +1. The "Build & upload all UnrealEngine release candidates" step will now commence.
While those builds run, take a look at the top of the build page, where you'll notice a new [annotation](https://buildkite.com/docs/agent/v3/cli-annotate): "your human labour is now required to complete the tasks listed in the PR descriptions and unblock the pipeline to resume the release."
Click through to the PRs using the links in the annotations and follow the steps. Come back when you're done. 1. As soon as the "Build & upload all UnrealEngine release candidates" step has passed, select "Run all tests". 1. Once all test have passed, all PRs are approved and all tasks listed in the PR descriptions are complete, select "Unblock the release". This will trigger "Release `ci/release.sh`". 1. When "Release `ci/release.sh`" is complete, the unrealgdk-release pipeline will pass.
diff --git a/SpatialGDK/Extras/schema/actor_ownership.schema b/SpatialGDK/Extras/schema/actor_ownership.schema new file mode 100644 index 0000000000..183f2d8540 --- /dev/null +++ b/SpatialGDK/Extras/schema/actor_ownership.schema @@ -0,0 +1,8 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +package unreal; + +component ActorOwnership { + id = 9959; + + EntityId leader_entity = 1; +} diff --git a/SpatialGDK/Extras/schema/debug_metrics.schema b/SpatialGDK/Extras/schema/debug_metrics.schema index fc7e3ba16a..3432e4263e 100644 --- a/SpatialGDK/Extras/schema/debug_metrics.schema +++ b/SpatialGDK/Extras/schema/debug_metrics.schema @@ -10,7 +10,7 @@ type ModifySettingPayload { type ExecServerCommandPayload { string server_worker = 1; - string command = 2; + int32 command = 2; string args = 3; } diff --git a/SpatialGDK/Extras/schema/gameplay_debugger_component.schema b/SpatialGDK/Extras/schema/gameplay_debugger_component.schema new file mode 100644 index 0000000000..97a3fa7a0f --- /dev/null +++ b/SpatialGDK/Extras/schema/gameplay_debugger_component.schema @@ -0,0 +1,8 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +package unreal; + +component GameplayDebuggerComponent { + id = 9997; + uint32 delegated_virtual_worker_id = 1; + bool track_player = 2; +} diff --git a/SpatialGDK/Extras/schema/query_tags.schema b/SpatialGDK/Extras/schema/query_tags.schema index bf0f7deba4..d205bf9adf 100644 --- a/SpatialGDK/Extras/schema/query_tags.schema +++ b/SpatialGDK/Extras/schema/query_tags.schema @@ -11,6 +11,10 @@ component ActorTag { id = 2002; } +component ActorOwnerOnlyDataTag { + id = 2003; +} + component LBTag { id = 2005; } diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/RemotePossessionComponent.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/RemotePossessionComponent.cpp index 23d71c01c1..8193bc315e 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/RemotePossessionComponent.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/RemotePossessionComponent.cpp @@ -6,6 +6,7 @@ #include "EngineClasses/SpatialNetDriver.h" #include "LoadBalancing/AbstractLBStrategy.h" #include "LoadBalancing/OwnershipLockingPolicy.h" +#include "Net/UnrealNetwork.h" DEFINE_LOG_CATEGORY(LogRemotePossessionComponent); @@ -41,6 +42,13 @@ void URemotePossessionComponent::BeginPlay() } } +void URemotePossessionComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME_CONDITION(ThisClass, Target, COND_ServerOnly); +} + void URemotePossessionComponent::OnAuthorityGained() { if (Target == nullptr) diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp index d6240112eb..ecfa2a7f6f 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp @@ -24,6 +24,7 @@ #include "Interop/SpatialReceiver.h" #include "Interop/SpatialSender.h" #include "LoadBalancing/AbstractLBStrategy.h" +#include "Schema/ActorOwnership.h" #include "Schema/NetOwningClientWorker.h" #include "SpatialConstants.h" #include "SpatialGDKSettings.h" @@ -55,10 +56,19 @@ void UpdateChangelistHistory(TUniquePtr& RepState) { FSendingRepState* SendingRepState = RepState->GetSendingRepState(); - check(SendingRepState->HistoryEnd >= SendingRepState->HistoryStart); + if (!ensureAlwaysMsgf(SendingRepState->HistoryEnd >= SendingRepState->HistoryStart, + TEXT("HistoryEnd buffer index should never be smaller than HistoryStart"))) + { + return; + } const int32 HistoryCount = SendingRepState->HistoryEnd - SendingRepState->HistoryStart; - check(HistoryCount < MaxSendingChangeHistory); + + if (!ensureAlwaysMsgf(HistoryCount < MaxSendingChangeHistory, + TEXT("Changelist history should always be smaller than the MaxSendingChangeHistory"))) + { + return; + } for (int32 i = SendingRepState->HistoryStart; i < SendingRepState->HistoryEnd; i++) { @@ -66,8 +76,7 @@ void UpdateChangelistHistory(TUniquePtr& RepState) FRepChangedHistory& HistoryItem = SendingRepState->ChangeHistory[HistoryIndex]; - // All active history items should contain a change list - check(HistoryItem.Changed.Num() > 0); + ensureAlwaysMsgf(HistoryItem.Changed.Num() > 0, TEXT("All active history items should contain a change list")); HistoryItem.Changed.Empty(); HistoryItem.OutPacketIdRange = FPacketIdRange(); @@ -77,7 +86,10 @@ void UpdateChangelistHistory(TUniquePtr& RepState) // Remove any tiling in the history markers to keep them from wrapping over time const int32 NewHistoryCount = SendingRepState->HistoryEnd - SendingRepState->HistoryStart; - check(NewHistoryCount <= MaxSendingChangeHistory); + if (!ensureAlwaysMsgf(NewHistoryCount <= MaxSendingChangeHistory, TEXT("NewHistoryCount greater or equal to MaxSendingChangeHistory"))) + { + return; + } SendingRepState->HistoryStart = SendingRepState->HistoryStart % MaxSendingChangeHistory; SendingRepState->HistoryEnd = SendingRepState->HistoryStart + NewHistoryCount; @@ -223,9 +235,6 @@ void USpatialActorChannel::Init(UNetConnection* InConnection, int32 ChannelIndex FramesTillDormancyAllowed = 0; - ActorHandoverShadowData = nullptr; - HandoverShadowDataMap.Empty(); - NetDriver = Cast(Connection->Driver); check(NetDriver); Sender = NetDriver->Sender; @@ -256,10 +265,14 @@ void USpatialActorChannel::RetireEntityIfAuthoritative() if (Actor->GetTearOff()) { NetDriver->DelayedRetireEntity(EntityId, 1.0f, Actor->IsNetStartupActor()); - // Since the entity deletion is delayed, this creates a situation, - // when the Actor is torn off, but still replicates. - // Disabling replication makes RPC calls impossible for this Actor. - Actor->SetReplicates(false); + if (ensureMsgf(Actor->HasAuthority(), TEXT("EntityId %lld Actor %s doesn't have authority, can't disable replication"), + EntityId, *Actor->GetName())) + { + // Since the entity deletion is delayed, this creates a situation, + // when the Actor is torn off, but still replicates. + // Disabling replication makes RPC calls impossible for this Actor. + Actor->SetReplicates(false); + } } else { @@ -268,7 +281,12 @@ void USpatialActorChannel::RetireEntityIfAuthoritative() } else if (bCreatedEntity) // We have not gained authority yet { - Actor->SetReplicates(false); + if (ensureMsgf(Actor->HasAuthority(), TEXT("EntityId %lld Actor %s doesn't have authority, can't disable replication"), + EntityId, *Actor->GetName())) + { + Actor->SetReplicates(false); + } + NetDriver->ActorSystem->RetireWhenAuthoritative( EntityId, NetDriver->ClassInfoManager->GetComponentIdForClass(*Actor->GetClass()), Actor->IsNetStartupActor(), Actor->GetTearOff()); // Ensure we don't recreate the actor @@ -345,7 +363,8 @@ int64 USpatialActorChannel::Close(EChannelCloseReason Reason) } else if (Reason == EChannelCloseReason::Relevancy) { - check(IsAuthoritativeServer()); + ensureAlwaysMsgf(IsAuthoritativeServer(), + TEXT("Trying to close SpatialActorChannel because of Relevancy on a non-authoritative server")); // Do nothing except close actor channel - this should only get processed on auth server } else @@ -359,18 +378,12 @@ int64 USpatialActorChannel::Close(EChannelCloseReason Reason) return Super::Close(Reason); } -bool USpatialActorChannel::IsDynamicArrayHandle(UObject* Object, uint16 Handle) -{ - check(ObjectHasReplicator(Object)); - FObjectReplicator& Replicator = FindOrCreateReplicator(Object).Get(); - TSharedPtr& RepLayout = Replicator.RepLayout; - check(Handle - 1 < RepLayout->BaseHandleToCmdIndex.Num()); - return RepLayout->Cmds[RepLayout->BaseHandleToCmdIndex[Handle - 1].CmdIndex].Type == ERepLayoutCmdType::DynamicArray; -} - void USpatialActorChannel::UpdateShadowData() { - check(Actor); + if (!ensureAlwaysMsgf(Actor != nullptr, TEXT("Called UpdateShadowData but Actor was nullptr"))) + { + return; + } // If this channel was responsible for creating the actor, we do not want to initialize our shadow data // to the latest state since there could have been state that has changed between creation of the entity @@ -391,16 +404,6 @@ void USpatialActorChannel::UpdateShadowData() ResetShadowData(*ComponentReplicator.RepLayout, ComponentReplicator.ChangelistMgr->GetRepChangelistState()->StaticBuffer, ActorComponent); } - - // Update handover shadow data. - for (auto& HandoverData : HandoverShadowDataMap) - { - TArray& ShadowDataBuffer = HandoverData.Value.Get(); - UObject* Object = HandoverData.Key.Get(); - - // This updates ShadowDataBuffer to Object's current handover state. - GetHandoverChangeList(ShadowDataBuffer, Object); - } } FRepChangeState USpatialActorChannel::CreateInitialRepChangeState(TWeakObjectPtr Object) @@ -445,17 +448,6 @@ FRepChangeState USpatialActorChannel::CreateInitialRepChangeState(TWeakObjectPtr return { InitialRepChanged, *Replicator.RepLayout }; } -FHandoverChangeState USpatialActorChannel::CreateInitialHandoverChangeState(const FClassInfo& ClassInfo) -{ - FHandoverChangeState HandoverChanged; - for (const FHandoverPropertyInfo& PropertyInfo : ClassInfo.HandoverProperties) - { - HandoverChanged.Add(PropertyInfo.Handle); - } - - return HandoverChanged; -} - void USpatialActorChannel::UpdateVisibleComponent(AActor* InActor) { // Make sure that the InActor is not a PlayerController, GameplayDebuggerCategoryReplicator or GameMode. @@ -673,13 +665,6 @@ int64 USpatialActorChannel::ReplicateActor() const FClassInfo& Info = NetDriver->ClassInfoManager->GetOrCreateClassInfoByClass(Actor->GetClass()); - FHandoverChangeState HandoverChangeState; - - if (ActorHandoverShadowData != nullptr) - { - HandoverChangeState = GetHandoverChangeList(*ActorHandoverShadowData, Actor); - } - ReplicationBytesWritten = 0; if (!bCreatingNewEntity && NeedOwnerInterestUpdate() && NetDriver->InterestFactory->DoOwnersHaveEntityId(Actor)) @@ -689,7 +674,7 @@ int64 USpatialActorChannel::ReplicateActor() } // If any properties have changed, send a component update. - if (bCreatingNewEntity || RepChanged.Num() > 0 || HandoverChangeState.Num() > 0) + if (bCreatingNewEntity || RepChanged.Num() > 0) { if (bCreatingNewEntity) { @@ -713,7 +698,7 @@ int64 USpatialActorChannel::ReplicateActor() { FRepChangeState RepChangeState = { RepChanged, GetObjectRepLayout(Actor) }; - NetDriver->ActorSystem->SendComponentUpdates(Actor, Info, this, &RepChangeState, &HandoverChangeState, ReplicationBytesWritten); + NetDriver->ActorSystem->SendComponentUpdates(Actor, Info, this, &RepChangeState, ReplicationBytesWritten); bInterestDirty = false; } @@ -757,29 +742,6 @@ int64 USpatialActorChannel::ReplicateActor() // the same SpatialActorChannel::ReplicateSubobject. Actor->ReplicateSubobjects(this, &DummyOutBunch, &RepFlags); - for (auto& SubobjectInfoPair : GetHandoverSubobjects()) - { - UObject* Subobject = SubobjectInfoPair.Key; - const FClassInfo& SubobjectInfo = *SubobjectInfoPair.Value; - - // Handover shadow data should already exist for this object. If it doesn't, it must have - // started replicating after SetChannelActor was called on the owning actor. - TSharedRef>* SubobjectHandoverShadowData = HandoverShadowDataMap.Find(Subobject); - if (SubobjectHandoverShadowData == nullptr) - { - UE_LOG(LogSpatialActorChannel, Warning, TEXT("EntityId: %lld Actor: %s HandoverShadowData not found for Subobject %s"), - EntityId, *Actor->GetName(), *Subobject->GetName()); - continue; - } - - FHandoverChangeState SubobjectHandoverChangeState = GetHandoverChangeList(SubobjectHandoverShadowData->Get(), Subobject); - if (SubobjectHandoverChangeState.Num() > 0) - { - NetDriver->ActorSystem->SendComponentUpdates(Subobject, SubobjectInfo, this, nullptr, &SubobjectHandoverChangeState, - ReplicationBytesWritten); - } - } - // Look for deleted subobjects for (auto RepComp = ReplicationMap.CreateIterator(); RepComp; ++RepComp) { @@ -846,7 +808,10 @@ void USpatialActorChannel::DynamicallyAttachSubobject(UObject* Object) } } - check(Info != nullptr); + if (!ensureAlwaysMsgf(Info != nullptr, TEXT("Subobject info was nullptr. Actor: %s"), *GetNameSafe(Object))) + { + return; + } NetDriver->ActorSystem->SendAddComponentForSubobject(this, Object, *Info, ReplicationBytesWritten); } @@ -951,7 +916,7 @@ bool USpatialActorChannel::ReplicateSubobject(UObject* Object, const FReplicatio } const FClassInfo& Info = NetDriver->ClassInfoManager->GetOrCreateClassInfoByObject(Object); - NetDriver->ActorSystem->SendComponentUpdates(Object, Info, this, &RepChangeState, nullptr, ReplicationBytesWritten); + NetDriver->ActorSystem->SendComponentUpdates(Object, Info, this, &RepChangeState, ReplicationBytesWritten); SendingRepState->HistoryEnd++; } @@ -988,82 +953,6 @@ bool USpatialActorChannel::ReadyForDormancy(bool bSuppressLogs /*= false*/) return Super::ReadyForDormancy(bSuppressLogs); } -TMap USpatialActorChannel::GetHandoverSubobjects() -{ - const FClassInfo& Info = NetDriver->ClassInfoManager->GetOrCreateClassInfoByClass(Actor->GetClass()); - - TMap FoundSubobjects; - - for (auto& SubobjectInfoPair : Info.SubobjectInfo) - { - const FClassInfo& SubobjectInfo = SubobjectInfoPair.Value.Get(); - - if (SubobjectInfo.HandoverProperties.Num() == 0) - { - // Not interested in this component if it has no handover properties - continue; - } - - UObject* Object = nullptr; - if (EntityId == 0) - { - Object = Actor->GetDefaultSubobjectByName(SubobjectInfo.SubobjectName); - } - else - { - Object = NetDriver->PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(EntityId, SubobjectInfoPair.Key)).Get(); - } - - if (Object == nullptr) - { - continue; - } - - FoundSubobjects.Add(Object, &SubobjectInfo); - } - - return FoundSubobjects; -} - -void USpatialActorChannel::InitializeHandoverShadowData(TArray& ShadowData, UObject* Object) -{ - const FClassInfo& ClassInfo = NetDriver->ClassInfoManager->GetOrCreateClassInfoByClass(Object->GetClass()); - - ShadowData.SetNumUninitialized(ClassInfo.HandoverPropertiesSize); - for (const FHandoverPropertyInfo& PropertyInfo : ClassInfo.HandoverProperties) - { - if (PropertyInfo.ArrayIdx == 0) // For static arrays, the first element will handle the whole array - { - PropertyInfo.Property->InitializeValue(ShadowData.GetData() + PropertyInfo.ShadowOffset); - } - } -} - -FHandoverChangeState USpatialActorChannel::GetHandoverChangeList(TArray& ShadowData, UObject* Object) -{ - FHandoverChangeState HandoverChanged; - - const FClassInfo& ClassInfo = NetDriver->ClassInfoManager->GetOrCreateClassInfoByClass(Object->GetClass()); - - uint32 ShadowDataOffset = 0; - for (const FHandoverPropertyInfo& PropertyInfo : ClassInfo.HandoverProperties) - { - ShadowDataOffset = Align(ShadowDataOffset, PropertyInfo.Property->GetMinAlignment()); - - const uint8* Data = (uint8*)Object + PropertyInfo.Offset; - uint8* StoredData = ShadowData.GetData() + ShadowDataOffset; - // Compare and assign. - if (bCreatingNewEntity || !PropertyInfo.Property->Identical(StoredData, Data)) - { - HandoverChanged.Add(PropertyInfo.Handle); - PropertyInfo.Property->CopySingleValue(StoredData, Data); - } - ShadowDataOffset += PropertyInfo.Property->ElementSize; - } - - return HandoverChanged; -} - void USpatialActorChannel::SetChannelActor(AActor* InActor, ESetChannelActorFlags Flags) { Super::SetChannelActor(InActor, Flags); @@ -1090,25 +979,6 @@ void USpatialActorChannel::SetChannelActor(AActor* InActor, ESetChannelActorFlag NetDriver->AddActorChannel(EntityId, this); NetDriver->UnregisterDormantEntityId(EntityId); } - - // Set up the shadow data for the handover properties. This is used later to compare the properties and send only changed ones. - check(!HandoverShadowDataMap.Contains(InActor)); - - // Create the shadow map, and store a quick access pointer to it - const FClassInfo& Info = NetDriver->ClassInfoManager->GetOrCreateClassInfoByClass(InActor->GetClass()); - if (Info.SchemaComponents[SCHEMA_Handover] != SpatialConstants::INVALID_COMPONENT_ID) - { - ActorHandoverShadowData = &HandoverShadowDataMap.Add(InActor, MakeShared>()).Get(); - InitializeHandoverShadowData(*ActorHandoverShadowData, InActor); - } - - for (auto& SubobjectInfoPair : GetHandoverSubobjects()) - { - UObject* Subobject = SubobjectInfoPair.Key; - - check(!HandoverShadowDataMap.Contains(Subobject)); - InitializeHandoverShadowData(HandoverShadowDataMap.Add(Subobject, MakeShared>()).Get(), Subobject); - } } bool USpatialActorChannel::TryResolveActor() @@ -1245,8 +1115,8 @@ void USpatialActorChannel::RemoveRepNotifiesWithUnresolvedObjs(TArray= 0, TEXT("ParentIndex should always be >= 0, but it was %d."), + ObjRef.Value.ParentIndex)) { continue; } @@ -1286,7 +1156,12 @@ void USpatialActorChannel::ServerProcessOwnershipChange() bool bUpdatedThisActor = false; // Changing an Actor's owner can affect its NetConnection so we need to reevaluate this. - check(NetDriver->HasServerAuthority(EntityId)); + if (!ensureAlwaysMsgf(NetDriver->HasServerAuthority(EntityId), + TEXT("Trying to process ownership change on non-auth server. Entity: %lld"), EntityId)) + { + return; + } + TOptional CurrentNetOwningClientData = SpatialGDK::DeserializeComponent(NetDriver->Connection->GetCoordinator(), EntityId); const Worker_PartitionId CurrentClientPartitionId = CurrentNetOwningClientData->ClientPartitionId.IsSet() @@ -1306,6 +1181,17 @@ void USpatialActorChannel::ServerProcessOwnershipChange() bUpdatedThisActor = true; } + TOptional CurrentActorOwnershipData = + SpatialGDK::DeserializeComponent(NetDriver->Connection->GetCoordinator(), EntityId); + const SpatialGDK::ActorOwnership NewActorOwnership = SpatialGDK::ActorOwnership::CreateFromActor(*Actor, *NetDriver->PackageMap); + if (CurrentActorOwnershipData != NewActorOwnership) + { + NetDriver->Connection->GetCoordinator().SendComponentUpdate(EntityId, NewActorOwnership.CreateComponentUpdate(), + FSpatialGDKSpanId()); + + bUpdatedThisActor = true; + } + // Owner changed, update the actor's interest over it. NetDriver->ActorSystem->UpdateInterestComponent(Actor); SetNeedOwnerInterestUpdate(!NetDriver->InterestFactory->DoOwnersHaveEntityId(Actor)); diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp index e0e3945f7f..355d8072ba 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp @@ -98,17 +98,17 @@ void USpatialGameInstance::CreateNewSpatialConnectionManager() void USpatialGameInstance::DestroySpatialConnectionManager() { - if (SpatialConnectionManager != nullptr) - { - SpatialConnectionManager->DestroyConnection(); - SpatialConnectionManager = nullptr; - } - if (GlobalStateManager != nullptr) { GlobalStateManager->ConditionalBeginDestroy(); GlobalStateManager = nullptr; } + + if (SpatialConnectionManager != nullptr) + { + SpatialConnectionManager->DestroyConnection(); + SpatialConnectionManager = nullptr; + } } #if WITH_EDITOR @@ -247,10 +247,6 @@ void USpatialGameInstance::HandleOnConnected(USpatialNetDriver& NetDriver) SetSpatialWorkerId(SpatialConnectionManager->GetWorkerConnection()->GetWorkerId()); #if TRACE_LIB_ACTIVE SpatialLatencyTracer->SetWorkerId(GetSpatialWorkerId()); - - USpatialWorkerConnection* WorkerConnection = SpatialConnectionManager->GetWorkerConnection(); - WorkerConnection->OnEnqueueMessage.AddUObject(SpatialLatencyTracer, &USpatialLatencyTracer::OnEnqueueMessage); - WorkerConnection->OnDequeueMessage.AddUObject(SpatialLatencyTracer, &USpatialLatencyTracer::OnDequeueMessage); #endif OnSpatialConnected.Broadcast(); diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetConnection.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetConnection.cpp index 5912fc6ae1..2c3871fdd9 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetConnection.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetConnection.cpp @@ -71,7 +71,8 @@ void USpatialNetConnection::LowLevelSend(void* Data, int32 CountBits, FOutPacket bool USpatialNetConnection::ClientHasInitializedLevelFor(const AActor* TestActor) const { - check(Driver->IsServer()); + ensureAlwaysMsgf(Driver->IsServer(), TEXT("ClientHasInitializedLevelFor should only be called on servers. Actor %s"), + *GetNameSafe(TestActor)); return true; // Intentionally does not call Super:: } diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp index 80bac48ac5..7281749480 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp @@ -20,11 +20,14 @@ #include "EngineClasses/SpatialGameInstance.h" #include "EngineClasses/SpatialNetConnection.h" #include "EngineClasses/SpatialNetDriverDebugContext.h" +#include "EngineClasses/SpatialNetDriverGameplayDebuggerContext.h" +#include "EngineClasses/SpatialNetDriverRPC.h" #include "EngineClasses/SpatialPackageMapClient.h" #include "EngineClasses/SpatialPendingNetGame.h" #include "EngineClasses/SpatialReplicationGraph.h" #include "EngineClasses/SpatialWorldSettings.h" #include "Interop/ActorSetWriter.h" +#include "Interop/ActorSubviews.h" #include "Interop/ActorSystem.h" #include "Interop/AsyncPackageLoadFilter.h" #include "Interop/ClientConnectionManager.h" @@ -34,6 +37,7 @@ #include "Interop/GlobalStateManager.h" #include "Interop/InitialOnlyFilter.h" #include "Interop/MigrationDiagnosticsSystem.h" +#include "Interop/OwnershipCompletenessHandler.h" #include "Interop/RPCExecutor.h" #include "Interop/SpatialClassInfoManager.h" #include "Interop/SpatialDispatcher.h" @@ -51,6 +55,7 @@ #include "LoadBalancing/DebugLBStrategy.h" #include "LoadBalancing/LayeredLBStrategy.h" #include "LoadBalancing/OwnershipLockingPolicy.h" +#include "Schema/ActorOwnership.h" #include "Schema/ActorSetMember.h" #include "Schema/SpatialDebugging.h" #include "SpatialConstants.h" @@ -100,6 +105,7 @@ USpatialNetDriver::USpatialNetDriver(const FObjectInitializer& ObjectInitializer : Super(ObjectInitializer) , LoadBalanceStrategy(nullptr) , DebugCtx(nullptr) + , GameplayDebuggerCtx(nullptr) , LoadBalanceEnforcer(nullptr) , bAuthoritativeDestruction(true) , bConnectAsClient(false) @@ -144,6 +150,15 @@ bool USpatialNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, c } } + if (bInitAsClient) + { + OwnershipCompletenessHandler = SpatialGDK::FOwnershipCompletenessHandler::CreateClientOwnershipHandler(); + } + else + { + OwnershipCompletenessHandler = SpatialGDK::FOwnershipCompletenessHandler::CreateServerOwnershipHandler(); + } + if (!Super::InitBase(bInitAsClient, InNotify, URL, bReuseAddressAndPort, Error)) { return false; @@ -416,7 +431,7 @@ void USpatialNetDriver::CreateAndInitializeCoreClasses() USpatialGameInstance* GameInstance = GetGameInstance(); check(GameInstance != nullptr); - SpatialMetrics = NewObject(); + SpatialMetrics = NewObject(this); SpatialMetrics->Init(Connection, NetServerMaxTickRate, IsServer()); SpatialWorkerFlags = NewObject(); @@ -452,56 +467,9 @@ void USpatialNetDriver::CreateAndInitializeCoreClasses() CreateAndInitializeLoadBalancingClasses(); - ActorFilter = [this](const Worker_EntityId EntityId, const SpatialGDK::EntityViewElement& Element) { - if (Element.Components.ContainsByPredicate(SpatialGDK::ComponentIdEquality{ SpatialConstants::TOMBSTONE_COMPONENT_ID })) - { - // This actor has been tombstoned, we leave it alone. - return false; - } - - if (AsyncPackageLoadFilter != nullptr) - { - SpatialGDK::UnrealMetadata Metadata( - Element.Components.FindByPredicate(SpatialGDK::ComponentIdEquality{ SpatialConstants::UNREAL_METADATA_COMPONENT_ID }) - ->GetUnderlying()); + const SpatialGDK::FSubView& ActorSubview = SpatialGDK::ActorSubviews::CreateActorSubView(*this); - if (!AsyncPackageLoadFilter->IsAssetLoadedOrTriggerAsyncLoad(EntityId, Metadata.ClassPath)) - { - return false; - } - } - - if (InitialOnlyFilter != nullptr) - { - if (Element.Components.ContainsByPredicate( - SpatialGDK::ComponentIdEquality{ SpatialConstants::INITIAL_ONLY_PRESENCE_COMPONENT_ID })) - { - if (!InitialOnlyFilter->HasInitialOnlyDataOrRequestIfAbsent(EntityId)) - { - return false; - } - } - } - - // If we see a player controller component on this entity and we're a server we should hold it back until we - // also have the partition component. - return !IsServer() - || Element.Components.ContainsByPredicate( - SpatialGDK::ComponentIdEquality{ SpatialConstants::PLAYER_CONTROLLER_COMPONENT_ID }) - == Element.Components.ContainsByPredicate( - SpatialGDK::ComponentIdEquality{ SpatialConstants::PARTITION_COMPONENT_ID }); - }; - ActorRefreshCallbacks = { - Connection->GetCoordinator().CreateComponentExistenceRefreshCallback(SpatialConstants::TOMBSTONE_COMPONENT_ID), - Connection->GetCoordinator().CreateComponentExistenceRefreshCallback(SpatialConstants::PLAYER_CONTROLLER_COMPONENT_ID), - Connection->GetCoordinator().CreateComponentExistenceRefreshCallback(SpatialConstants::PARTITION_COMPONENT_ID) - }; - - const SpatialGDK::FSubView& ActorAuthSubview = - Connection->GetCoordinator().CreateSubView(SpatialConstants::ACTOR_AUTH_TAG_COMPONENT_ID, ActorFilter, ActorRefreshCallbacks); - - const SpatialGDK::FSubView& ActorSubview = - Connection->GetCoordinator().CreateSubView(SpatialConstants::ACTOR_TAG_COMPONENT_ID, ActorFilter, ActorRefreshCallbacks); + const SpatialGDK::FSubView& ActorAuthSubview = SpatialGDK::ActorSubviews::CreateActorAuthSubView(*this); const FFilterPredicate TombstoneActorFilter = [this](const Worker_EntityId, const SpatialGDK::EntityViewElement& Element) { return Element.Components.ContainsByPredicate(SpatialGDK::ComponentIdEquality{ SpatialConstants::TOMBSTONE_COMPONENT_ID }); @@ -516,8 +484,27 @@ void USpatialNetDriver::CreateAndInitializeCoreClasses() const SpatialGDK::FSubView& SystemEntitySubview = Connection->GetCoordinator().CreateSubView( SpatialConstants::SYSTEM_COMPONENT_ID, SpatialGDK::FSubView::NoFilter, SpatialGDK::FSubView::NoDispatcherCallbacks); - RPCService = MakeUnique(ActorAuthSubview, ActorSubview, USpatialLatencyTracer::GetTracer(GetWorld()), - Connection->GetEventTracer(), this); + const SpatialGDK::FSubView& WorkerEntitySubView = Connection->GetCoordinator().CreateSubView( + SpatialConstants::ROUTINGWORKER_TAG_COMPONENT_ID, SpatialGDK::FSubView::NoFilter, SpatialGDK::FSubView::NoDispatcherCallbacks); + + RPCService = + MakeUnique(ActorAuthSubview, ActorSubview, WorkerEntitySubView, + USpatialLatencyTracer::GetTracer(GetWorld()), Connection->GetEventTracer(), this); + + if (IsServer()) + { + TUniquePtr ServerRPCsPtr = + MakeUnique(*this, ActorAuthSubview, ActorSubview); + ServerRPCs = ServerRPCsPtr.Get(); + RPCs.Reset(ServerRPCsPtr.Release()); + } + else + { + TUniquePtr ClientRPCsPtr = + MakeUnique(*this, ActorAuthSubview, ActorSubview); + ClientRPCs = ClientRPCsPtr.Get(); + RPCs.Reset(ClientRPCsPtr.Release()); + } CrossServerRPCSender = MakeUnique(Connection->GetCoordinator(), SpatialMetrics, Connection->GetEventTracer()); @@ -526,7 +513,14 @@ void USpatialNetDriver::CreateAndInitializeCoreClasses() Connection->GetCoordinator(), MakeUnique(this, Connection->GetEventTracer()), Connection->GetEventTracer()); - ActorSystem = MakeUnique(ActorSubview, TombstoneActorSubview, this, Connection->GetEventTracer()); + { + const SpatialGDK::FSubView& AuthoritySubView = SpatialGDK::ActorSubviews::CreateAuthoritySubView(*this); + const SpatialGDK::FSubView& OwnershipSubView = SpatialGDK::ActorSubviews::CreatePlayerOwnershipSubView(*this); + const SpatialGDK::FSubView& SimulatedSubView = SpatialGDK::ActorSubviews::CreateSimulatedSubView(*this); + + ActorSystem = MakeUnique(ActorSubview, AuthoritySubView, OwnershipSubView, SimulatedSubView, + TombstoneActorSubview, this, Connection->GetEventTracer()); + } ClientConnectionManager = MakeUnique(SystemEntitySubview, this); @@ -545,7 +539,7 @@ void USpatialNetDriver::CreateAndInitializeCoreClasses() USpatialPackageMapClient* NewPackageMap = Cast(GetSpatialOSNetConnection()->PackageMap); check(NewPackageMap == PackageMap); - PackageMap->Init(this, &TimerManager); + PackageMap->Init(*this); if (IsServer()) { PackageMap->GetEntityPoolReadyDelegate().AddUObject(Connection, &USpatialWorkerConnection::CreateServerWorkerEntity); @@ -665,8 +659,11 @@ void USpatialNetDriver::CleanUpServerConnectionForPC(APlayerController* PC) if (ClientConnection->OwningActor == PC) { USpatialNetConnection* SpatialConnection = Cast(ClientConnection); - check(SpatialConnection != nullptr); - SpatialConnection->CleanUp(); + if (ensureAlwaysMsgf(SpatialConnection != nullptr, + TEXT("SpatialConnection was nullptr when trying to cleanup server connection"))) + { + SpatialConnection->CleanUp(); + } return; } } @@ -683,16 +680,6 @@ void USpatialNetDriver::ClientOnGSMQuerySuccess() { StartupClientDebugString.Empty(); - auto FlagNetworkFailure = [this](const FString& ErrorString) { - if (USpatialGameInstance* GameInstance = GetGameInstance()) - { - if (GEngine != nullptr && GameInstance->GetWorld() != nullptr) - { - GEngine->BroadcastNetworkFailure(GameInstance->GetWorld(), this, ENetworkFailure::OutdatedClient, ErrorString); - } - } - }; - const uint64 SnapshotVersion = GlobalStateManager->GetSnapshotVersion(); if (SpatialConstants::SPATIAL_SNAPSHOT_VERSION != SnapshotVersion) // Are we running with the same snapshot version? { @@ -701,8 +688,10 @@ void USpatialNetDriver::ClientOnGSMQuerySuccess() "version = '%llu'"), SnapshotVersion, SpatialConstants::SPATIAL_SNAPSHOT_VERSION); - FlagNetworkFailure( - TEXT("Your snapshot version of the game does not match that of the server. Please try updating your game snapshot.")); + PendingNetworkFailure = { + ENetworkFailure::OutdatedClient, + TEXT("Your snapshot version of the game does not match that of the server. Please try updating your game snapshot.") + }; return; } @@ -717,7 +706,10 @@ void USpatialNetDriver::ClientOnGSMQuerySuccess() TEXT("Your client's schema does not match your deployment's schema. Client hash: '%u' Server hash: '%u'"), ClassInfoManager->SchemaDatabase->SchemaBundleHash, ServerHash); - FlagNetworkFailure(TEXT("Your version of the game does not match that of the server. Please try updating your game version.")); + PendingNetworkFailure = { + ENetworkFailure::OutdatedClient, + TEXT("Your version of the game does not match that of the server. Please try updating your game version.") + }; return; } @@ -1117,10 +1109,12 @@ void USpatialNetDriver::NotifyActorDestroyed(AActor* ThisActor, bool IsSeamlessT if (UActorChannel* Channel = ClientConnection->ActorChannelMap().FindRef(ThisActor)) { - check(Channel->OpenedLocally); - Channel->bClearRecentActorRefs = false; - // TODO: UNR-952 - Add code here for cleaning up actor channels from our maps. - Channel->Close(EChannelCloseReason::Destroyed); + if (ensureAlwaysMsgf(Channel->OpenedLocally, TEXT("Trying to close non-locally-opened Actor channel when deleting Actor"))) + { + Channel->bClearRecentActorRefs = false; + // TODO: UNR-952 - Add code here for cleaning up actor channels from our maps. + Channel->Close(EChannelCloseReason::Destroyed); + } } // Remove it from any dormancy lists @@ -1322,9 +1316,12 @@ void USpatialNetDriver::ProcessOwnershipChanges() { if (bShouldWriteLoadBalancingData) { - check(IsValid(Channel->Actor)); - const SpatialGDK::ActorSetMember ActorSetData = SpatialGDK::GetActorSetData(*PackageMap, *Channel->Actor); - Connection->GetCoordinator().SendComponentUpdate(EntityId, ActorSetData.CreateComponentUpdate(), {}); + if (ensureAlwaysMsgf(IsValid(Channel->Actor), + TEXT("Tried to process ownership changes for invalid channel Actor. Entity: %lld"), EntityId)) + { + const SpatialGDK::ActorSetMember ActorSetData = SpatialGDK::GetActorSetData(*PackageMap, *Channel->Actor); + Connection->GetCoordinator().SendComponentUpdate(EntityId, ActorSetData.CreateComponentUpdate(), {}); + } } Channel->ServerProcessOwnershipChange(); @@ -1728,7 +1725,10 @@ void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConne continue; } - check(Actor->HasAuthority()); + if (!ensureAlwaysMsgf(Actor->HasAuthority(), TEXT("Trying to replicate Actor without authority"))) + { + continue; + } Channel = GetOrCreateSpatialActorChannel(Actor); if ((Channel == nullptr) && (Actor->NetUpdateFrequency < 1.0f)) @@ -1803,10 +1803,38 @@ void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConne #endif // WITH_SERVER_CODE -thread_local TArray GSenderStack; +namespace SpatialNetDriverPrivate +{ +struct SenderActorDesc +{ + enum ItemKind + { + Sender, + Dependent, + Resolution + }; + + SenderActorDesc(AActor* InActor, ItemKind InKind) + : Actor(InActor) + , Kind(InKind) + { + } + AActor* const Actor; + ItemKind Kind; +}; + +// "Stack extension" to push additional RPC parameters. +// This is to work around having to do deep plumbing into the engine to pass additional RPC parameters not part of the RPC payload. +// The sender actor is supposed to be reset as soon as it is used, or determined to be useless (see AActor::GetFunctionCallspace) +// This is done so that while we allow Reliable RPCs to omit a sender, a previously pushed sender for a RPC that short-circuuuited can't be +// used for another RPC that omits it. +thread_local TOptional GSenderActor; +} // namespace SpatialNetDriverPrivate void USpatialNetDriver::ProcessRPC(AActor* Actor, UObject* SubObject, UFunction* Function, void* Parameters) { + using namespace SpatialNetDriverPrivate; + // The RPC might have been called by an actor directly, or by a subobject on that actor UObject* CallingObject = SubObject != nullptr ? SubObject : Actor; @@ -1814,7 +1842,11 @@ void USpatialNetDriver::ProcessRPC(AActor* Actor, UObject* SubObject, UFunction* { if (PackageMap->GetEntityIdFromObject(CallingObject) == SpatialConstants::INVALID_ENTITY_ID) { - check(Actor != nullptr); + if (!ensureAlwaysMsgf(Actor != nullptr, TEXT("Trying to process RPC for nullptr Actor"))) + { + return; + } + if (!Actor->HasAuthority() && Actor->IsNameStableForNetworking() && Actor->GetIsReplicated()) { // We don't want GetOrCreateSpatialActorChannel to pre-allocate an entity id here, because it exists on another worker. @@ -1851,6 +1883,54 @@ void USpatialNetDriver::ProcessRPC(AActor* Actor, UObject* SubObject, UFunction* } const FRPCInfo& Info = ClassInfoManager->GetRPCInfo(CallingObject, Function); + + if (Info.Type == ERPCType::ServerReliable || Info.Type == ERPCType::ServerUnreliable || Info.Type == ERPCType::ClientReliable + || Info.Type == ERPCType::ClientUnreliable) + { + FRPCPayload Payload; + Payload.Index = Info.Index; + Payload.Offset = CallingObjectRef.Offset; + Payload.PayloadData = RPCs->CreateRPCPayloadData(Function, Parameters); + FSpatialGDKSpanId SpanId = RPCs->CreatePushRPCEvent(CallingObject, Function); + + SpatialGDK::TRPCQueue* Queue = nullptr; + switch (Info.Type) + { + case ERPCType::ClientReliable: + if (ensure(ServerRPCs != nullptr)) + { + Queue = ServerRPCs->ClientReliableQueue.Get(); + } + break; + case ERPCType::ClientUnreliable: + if (ensure(ServerRPCs != nullptr)) + { + Queue = ServerRPCs->ClientUnreliableQueue.Get(); + } + break; + case ERPCType::ServerReliable: + if (ensure(ClientRPCs != nullptr)) + { + Queue = ClientRPCs->ServerReliableQueue.Get(); + } + break; + case ERPCType::ServerUnreliable: + if (ensure(ClientRPCs != nullptr)) + { + Queue = ClientRPCs->ServerUnreliableQueue.Get(); + } + break; + } + + if (ensure(Queue)) + { + Queue->Push(CallingObjectRef.Entity, MoveTemp(Payload), MoveTemp(SpanId)); + RPCs->FlushRPCQueueForEntity(CallingObjectRef.Entity, *Queue); + } + + return; + } + RPCPayload Payload = RPCService->CreateRPCPayloadFromParams(CallingObject, CallingObjectRef, Function, Info.Type, Parameters); const USpatialGDKSettings* Settings = GetDefault(); @@ -1858,51 +1938,113 @@ void USpatialNetDriver::ProcessRPC(AActor* Actor, UObject* SubObject, UFunction* if (Info.Type == ERPCType::CrossServer) { - if (Settings->CrossServerRPCImplementation == ECrossServerRPCImplementation::SpatialCommand) + const bool bUseEntityInteractionSemantics = Settings->CrossServerRPCImplementation == ECrossServerRPCImplementation::RoutingWorker; + const bool bIsNetWriteFence = Function->HasAnyFunctionFlags(FUNC_NetWriteFence); + const bool bIsOnlyNetWriteFence = bIsNetWriteFence && !Function->HasAnyFunctionFlags(FUNC_NetCrossServer); + const bool bIsUnordered = Function->HasAnySpatialFunctionFlags(SPATIALFUNC_ExplicitlyUnordered); + const bool bIsReliable = Function->HasAnyFunctionFlags(FUNC_NetReliable); + + const bool bNeedSender = bUseEntityInteractionSemantics && ((bIsReliable && !bIsUnordered) || bIsNetWriteFence); + + if (!bUseEntityInteractionSemantics || (!bNeedSender && !bIsReliable)) { CrossServerRPCSender->SendCommand(MoveTemp(CallingObjectRef), CallingObject, Function, MoveTemp(Payload), Info); return; } - else + else // bUseEntityInteractionSemantics && (bNeedSender || bIsReliable) { - AActor* SenderActor = nullptr; + // NOTE : the (!bHasSenderAvailable) branch is only there to allow migration to take place + // When no sender is available, the RPC will be sent unordered. + // When the relevant users are migrated, we should remove the migration branch and enforce the presence of sender + // Removing it will allow the rest of the diagnostic code to emit the appropriate errors. + const bool bHasSenderAvailable = GSenderActor.IsSet(); - if (GSenderStack.Num() == 0) + if (bIsUnordered) { - // These will eventually become error cases - // UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Missing sender Actor to call RPC %s on target %s"), *Function->GetName(), - // *Actor->GetName()); - // return; + SenderInfo.Entity = WorkerEntityId; } - else - { - SenderActor = GSenderStack.Last(); - } - - if (SenderActor == nullptr) + else if (!bHasSenderAvailable) { - // These will eventually become error cases - // UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Null sender Actor set to call RPC %s on target %s"), *Function->GetName(), - // *Actor->GetName()); - // return; - } - - if (SenderActor && !SenderActor->HasAuthority()) - { - UE_LOG(LogSpatialOSNetDriver, Error, TEXT("No authority on sender Actor %s to call RPC %s on target %s"), - *SenderActor->GetName(), *Function->GetName(), *Actor->GetName()); - return; - } + // Migration branch + if (bIsNetWriteFence) + { + UE_LOG(LogSpatialOSNetDriver, Error, + TEXT("Net write fence will be dropped because no sender was provided. Function : %s, Target : %s"), + *Function->GetName(), *Actor->GetName()); + return; + } + else + { + UE_LOG(LogSpatialOSNetDriver, Warning, + TEXT("Ordered reliable RPC will be sent unordered because no sender was provided. Use SendCrossServerRPC to " + "provide a sender. Function : %s, Target : %s"), + *Function->GetName(), *Actor->GetName()); - if (SenderActor) - { - SenderInfo.Entity = PackageMap->GetUnrealObjectRefFromObject(SenderActor).Entity; + SenderInfo.Entity = WorkerEntityId; + } } else { - // TODO : check reliability tag to determine if this should be a command or an unordered reliable RPC. - // Keep the sender null for now and redirect to commands to have a migration path - // SenderInfo.Entity = WorkerEntityId; + // Long term branch + AActor* SenderActor = nullptr; + + if (!GSenderActor.IsSet()) + { + UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Missing sender Actor for CrossServer RPC. Function : %s, Target : %s"), + *Function->GetName(), *Actor->GetName()); + return; + } + + const SenderActorDesc& Desc = GSenderActor.GetValue(); + SenderActor = Desc.Actor; + + if ((bIsOnlyNetWriteFence && Desc.Kind != SenderActorDesc::Dependent) + || (!bIsNetWriteFence && Desc.Kind == SenderActorDesc::Dependent)) + { + UE_LOG(LogSpatialOSNetDriver, Error, + TEXT("Wrong kind of sender Actor. Check that the right AActor function was used with the right kind of RPC " + "(CrossServer and NetWriteFence). Function : %s, Target : %s"), + *Function->GetName(), *Actor->GetName()); + return; + } + + GSenderActor.Reset(); + + if (SenderActor == nullptr) + { + UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Null sender Actor. Function : %s, Target : %s"), *Function->GetName(), + *Actor->GetName()); + return; + } + + if (!SenderActor->HasAuthority()) + { + if (!ensure(!bIsOnlyNetWriteFence)) + { + UE_LOG(LogSpatialOSNetDriver, Error, + TEXT(" {INTERNAL GDK ERROR} No authority on sender Actor for NetWriteFence. Function : %s, Target : %s, " + "Sender %s"), + *Function->GetName(), *Actor->GetName(), *SenderActor->GetName()); + return; + } + + // Migration branch, keep it a warning for now. + UE_LOG(LogSpatialOSNetDriver, Warning, + TEXT("Ordered reliable RPC will be sent unordered because the sender does not have authority. Function : %s, " + "Target : %s, Sender : %s"), + *Function->GetName(), *Actor->GetName(), *SenderActor->GetName()); + + SenderInfo.Entity = WorkerEntityId; + } + else + { + if (bIsNetWriteFence) + { + SenderActor->ForceNetUpdate(); + } + + SenderInfo.Entity = PackageMap->GetUnrealObjectRefFromObject(SenderActor).Entity; + } } } } @@ -1935,6 +2077,13 @@ int32 USpatialNetDriver::ServerReplicateActors(float DeltaSeconds) DebugCtx->TickServer(); } +#if WITH_GAMEPLAY_DEBUGGER + if (GameplayDebuggerCtx != nullptr) + { + GameplayDebuggerCtx->TickServer(); + } +#endif + if (UReplicationDriver* RepDriver = GetReplicationDriver()) { return RepDriver->ServerReplicateActors(DeltaSeconds); @@ -2116,11 +2265,23 @@ void USpatialNetDriver::TickDispatch(float DeltaTime) RPCService->AdvanceView(); } + if (RPCs.IsValid()) + { + RPCs->AdvanceView(); + } + if (DebugCtx != nullptr) { DebugCtx->AdvanceView(); } +#if WITH_GAMEPLAY_DEBUGGER + if (GameplayDebuggerCtx != nullptr) + { + GameplayDebuggerCtx->AdvanceView(); + } +#endif + if (ClientConnectionManager.IsValid()) { ClientConnectionManager->Advance(); @@ -2143,6 +2304,11 @@ void USpatialNetDriver::TickDispatch(float DeltaTime) RPCService->ProcessChanges(GetElapsedTime()); } + if (RPCs.IsValid()) + { + RPCs->ProcessReceivedRPCs(); + } + if (WellKnownEntitySystem.IsValid()) { WellKnownEntitySystem->Advance(); @@ -2216,6 +2382,22 @@ void USpatialNetDriver::TickDispatch(float DeltaTime) QueryHandler.ProcessOps(Connection->GetWorkerMessages()); } + + // Broadcast network failure if any network errors occurred + // NOTE: This should be performed at the end of this function to avoid shutting down the net driver while still running tick functions + // and indirectly destroying resources that those functions are still using. + if (PendingNetworkFailure) + { + if (USpatialGameInstance* GameInstance = GetGameInstance()) + { + if (GEngine != nullptr && GameInstance->GetWorld() != nullptr) + { + GEngine->BroadcastNetworkFailure(GameInstance->GetWorld(), this, PendingNetworkFailure->FailureType, + PendingNetworkFailure->Message); + } + } + PendingNetworkFailure.Reset(); + } } void USpatialNetDriver::ProcessRemoteFunction(AActor* Actor, UFunction* Function, void* Parameters, FOutParmRec* OutParms, FFrame* Stack, @@ -2242,7 +2424,8 @@ void USpatialNetDriver::ProcessRemoteFunction(AActor* Actor, UFunction* Function // owned by other AActor instances possessed by a UNetConnection. For native Unreal reference see ProcessRemoteFunction() of // IpNetDriver.cpp. However if we are on the server, and the RPC is a CrossServer or NetMulticast RPC, this can be invoked without an // owner. - if (!Actor->GetNetConnection() && !(Function->FunctionFlags & (FUNC_NetCrossServer | FUNC_NetMulticast) && IsServer())) + if (!Actor->GetNetConnection() + && !(Function->FunctionFlags & (FUNC_NetCrossServer | FUNC_NetMulticast | FUNC_NetWriteFence) && IsServer())) { UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("No owning connection for actor %s. Function %s will not be processed."), *Actor->GetName(), *Function->GetName()); @@ -2376,6 +2559,11 @@ void USpatialNetDriver::TickFlush(float DeltaTime) RPCService->PushUpdates(); } + if (RPCs.IsValid()) + { + RPCs->FlushRPCUpdates(); + } + if (IsServer()) { ProcessOwnershipChanges(); @@ -2541,7 +2729,10 @@ void USpatialNetDriver::AcceptNewPlayer(const FURL& InUrl, const FUniqueNetIdRep // This function is called for server workers who received the PC over the wire void USpatialNetDriver::PostSpawnPlayerController(APlayerController* PlayerController, const Worker_EntityId ClientSystemEntityId) { - check(PlayerController != nullptr); + if (!ensureAlwaysMsgf(PlayerController != nullptr, TEXT("PlayerController Actor was nullptr in PostSpawnPlayerController"))) + { + return; + } PlayerController->SetFlags(GetFlags() | RF_Transient); @@ -2739,7 +2930,7 @@ void USpatialNetDriver::RemoveActorChannel(Worker_EntityId EntityId, USpatialAct return; } - EntityToActorChannel.FindAndRemoveChecked(EntityId); + EntityToActorChannel.Remove(EntityId); } TMap& USpatialNetDriver::GetEntityToActorChannelMap() @@ -2749,7 +2940,11 @@ TMap& USpatialNetDriver::GetEntityTo USpatialActorChannel* USpatialNetDriver::GetOrCreateSpatialActorChannel(UObject* TargetObject) { - check(TargetObject); + if (!ensureAlwaysMsgf(TargetObject != nullptr, TEXT("TargetObject was nullptr when trying to get or create Actor channel"))) + { + return nullptr; + } + USpatialActorChannel* Channel = GetActorChannelByEntityId(PackageMap->GetEntityIdFromObject(TargetObject)); if (Channel == nullptr) { @@ -2758,14 +2953,19 @@ USpatialActorChannel* USpatialNetDriver::GetOrCreateSpatialActorChannel(UObject* { TargetActor = Cast(TargetObject->GetOuter()); } - check(TargetActor); + + if (!ensureAlwaysMsgf(TargetObject != nullptr, TEXT("Failed to find valid Actor when creating Actor channel. Object: %s"), + *GetNameSafe(TargetObject))) + { + return nullptr; + } if (USpatialActorChannel* ActorChannel = GetActorChannelByEntityId(PackageMap->GetEntityIdFromObject(TargetActor))) { // This can happen if schema database is out of date and had no entry for a static subobject. UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("GetOrCreateSpatialActorChannel: No channel for target object but channel already present for actor. Target " - "object: %s, actor: %s"), + "object: %s. Actor: %s"), *TargetObject->GetPathName(), *TargetActor->GetPathName()); return ActorChannel; } @@ -2784,7 +2984,8 @@ USpatialActorChannel* USpatialNetDriver::GetOrCreateSpatialActorChannel(UObject* if (Channel != nullptr && Channel->Actor == nullptr) { // This shouldn't occur, but can often crop up whilst we are refactoring entity/actor/channel lifecycles. - UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Failed to correctly initialize SpatialActorChannel for [%s]"), *TargetObject->GetName()); + UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Failed to correctly initialize SpatialActorChannel. Object: %s"), + *TargetObject->GetName()); } #endif // !UE_BUILD_SHIPPING return Channel; @@ -2797,8 +2998,15 @@ USpatialActorChannel* USpatialNetDriver::GetActorChannelByEntityId(Worker_Entity void USpatialNetDriver::RefreshActorDormancy(AActor* Actor, bool bMakeDormant) { - check(IsServer()); - check(Actor); + if (!ensureAlwaysMsgf(IsServer(), TEXT("RefreshActorDormancy should only be called on the server"))) + { + return; + } + + if (!ensureAlwaysMsgf(Actor != nullptr, TEXT("Called RefreshActorDormancy on nullptr Actor"))) + { + return; + } const Worker_EntityId EntityId = PackageMap->GetEntityIdFromObject(Actor); if (EntityId == SpatialConstants::INVALID_ENTITY_ID) @@ -2836,8 +3044,15 @@ void USpatialNetDriver::RefreshActorDormancy(AActor* Actor, bool bMakeDormant) void USpatialNetDriver::RefreshActorVisibility(AActor* Actor, bool bMakeVisible) { - check(IsServer()); - check(Actor); + if (!ensureAlwaysMsgf(IsServer(), TEXT("RefreshActorVisibility should only be called on the server"))) + { + return; + } + + if (!ensureAlwaysMsgf(Actor != nullptr, TEXT("Called RefreshActorVisibility on nullptr Actor"))) + { + return; + } const Worker_EntityId EntityId = PackageMap->GetEntityIdFromObject(Actor); if (EntityId == SpatialConstants::INVALID_ENTITY_ID) @@ -2900,13 +3115,14 @@ bool USpatialNetDriver::IsDormantEntity(Worker_EntityId EntityId) const USpatialActorChannel* USpatialNetDriver::CreateSpatialActorChannel(AActor* Actor) { // This should only be called from GetOrCreateSpatialActorChannel, otherwise we could end up clobbering an existing channel. + if (!ensureAlwaysMsgf(Actor != nullptr, TEXT("Tried to call CreateSpatialActorChannel for a nullptr Actor"))) + { + return nullptr; + } - check(Actor != nullptr); - check(PackageMap != nullptr); - - Worker_EntityId EntityId = PackageMap->GetEntityIdFromObject(Actor); - - check(GetActorChannelByEntityId(EntityId) == nullptr); + const Worker_EntityId EntityId = PackageMap->GetEntityIdFromObject(Actor); + ensureAlwaysMsgf(GetActorChannelByEntityId(EntityId) == nullptr, + TEXT("Called CreateSpatialActorChannel while Actor Channel already exists for entity %lld"), EntityId); USpatialNetConnection* NetConnection = GetSpatialOSNetConnection(); check(NetConnection != nullptr); @@ -3013,27 +3229,41 @@ void USpatialNetDriver::TryFinishStartup() ASpatialWorldSettings* WorldSettings = Cast(GetWorld()->GetWorldSettings()); if (WorldSettings && WorldSettings->bEnableDebugInterface) { - auto DebugCompFilter = [this](const Worker_EntityId EntityId, const SpatialGDK::EntityViewElement& Element) { - if (!Element.Components.ContainsByPredicate( - SpatialGDK::ComponentIdEquality{ SpatialConstants::GDK_DEBUG_COMPONENT_ID })) - { - return false; - } - - return ActorFilter(EntityId, Element); + const FFilterPredicate DebugCompFilter = [this](const Worker_EntityId EntityId, + const SpatialGDK::EntityViewElement& Element) { + return Element.Components.ContainsByPredicate( + SpatialGDK::ComponentIdEquality{ SpatialConstants::GDK_DEBUG_COMPONENT_ID }); }; - TArray DebugCompRefresh = ActorRefreshCallbacks; - DebugCompRefresh.Add( - Connection->GetCoordinator().CreateComponentExistenceRefreshCallback(SpatialConstants::GDK_DEBUG_COMPONENT_ID)); + const TArray DebugCompRefresh = { + Connection->GetCoordinator().CreateComponentExistenceRefreshCallback(SpatialConstants::GDK_DEBUG_COMPONENT_ID) + }; // Create the subview here rather than with the others as we only know if we need it or not at // this point. - const SpatialGDK::FSubView& DebugActorSubView = Connection->GetCoordinator().CreateSubView( - SpatialConstants::GDK_DEBUG_TAG_COMPONENT_ID, DebugCompFilter, DebugCompRefresh); + const SpatialGDK::FSubView& DebugActorSubView = SpatialGDK::ActorSubviews::CreateCustomActorSubView( + SpatialConstants::GDK_DEBUG_TAG_COMPONENT_ID, DebugCompFilter, DebugCompRefresh, *this); USpatialNetDriverDebugContext::EnableDebugSpatialGDK(DebugActorSubView, this); } #endif + +#if WITH_GAMEPLAY_DEBUGGER + const FFilterPredicate GameplayDebuggerCompFilter = [this](const Worker_EntityId EntityId, + const SpatialGDK::EntityViewElement& Element) { + return Element.Components.ContainsByPredicate( + SpatialGDK::ComponentIdEquality{ SpatialConstants::GDK_GAMEPLAY_DEBUGGER_COMPONENT_ID }); + }; + + const TArray GameplayDebuggerCompRefresh = { + Connection->GetCoordinator().CreateComponentExistenceRefreshCallback( + SpatialConstants::GDK_GAMEPLAY_DEBUGGER_COMPONENT_ID) + }; + + const SpatialGDK::FSubView& GameplayDebuggerActorSubView = + SpatialGDK::ActorSubviews::CreateCustomActorSubView({}, GameplayDebuggerCompFilter, GameplayDebuggerCompRefresh, *this); + USpatialNetDriverGameplayDebuggerContext::Enable(GameplayDebuggerActorSubView, *this); +#endif // WITH_GAMEPLAY_DEBUGGER + // We've found and dispatched all ops we need for startup, // trigger BeginPlay() on the GSM and process the queued ops. // Note that FindAndDispatchStartupOps() will have notified the Dispatcher @@ -3060,7 +3290,11 @@ void USpatialNetDriver::TryFinishStartup() // This should only be called once on each client, in the SpatialMetricsDisplay constructor after the class is replicated to each client. void USpatialNetDriver::SetSpatialMetricsDisplay(ASpatialMetricsDisplay* InSpatialMetricsDisplay) { - check(!IsServer()); + if (!ensureAlwaysMsgf(!IsServer(), TEXT("SetSpatialMetricsDisplay should only be called on the client"))) + { + return; + } + if (SpatialMetricsDisplay != nullptr) { UE_LOG(LogSpatialOSNetDriver, Error, TEXT("SpatialMetricsDisplay should only be set once on each client!")); @@ -3155,11 +3389,13 @@ void USpatialNetDriver::RegisterSpatialDebugger(ASpatialDebugger* InSpatialDebug { // Ideally we filter for the SPATIAL_DEBUGGING_COMPONENT_ID here as well, however as filters aren't compositional currently, and // it's more important for Actor correctness, for now we just rely on the existing Actor Filtering. - DebuggerSubViewPtr = - &Connection->GetCoordinator().CreateSubView(SpatialConstants::ACTOR_TAG_COMPONENT_ID, ActorFilter, ActorRefreshCallbacks); + DebuggerSubViewPtr = &SpatialGDK::ActorSubviews::CreateActorSubView(*this); } - check(DebuggerSubViewPtr != nullptr); + if (!ensureAlwaysMsgf(DebuggerSubViewPtr != nullptr, TEXT("Failed creating DebuggerSubViewPtr subview"))) + { + return; + } SpatialDebuggerSystem = MakeUnique(this, *DebuggerSubViewPtr); } @@ -3194,12 +3430,94 @@ FUnrealObjectRef USpatialNetDriver::GetCurrentPlayerControllerRef() void USpatialNetDriver::PushCrossServerRPCSender(AActor* SenderActor) { - GSenderStack.Add(SenderActor); + using namespace SpatialNetDriverPrivate; + check(!GSenderActor.IsSet()); + GSenderActor.Emplace(SenderActorDesc(SenderActor, SenderActorDesc::Sender)); +} + +void USpatialNetDriver::PopCrossServerRPCSender() +{ + using namespace SpatialNetDriverPrivate; + GSenderActor.Reset(); +} + +void USpatialNetDriver::PushDependentActor(AActor* Dependent) +{ + using namespace SpatialNetDriverPrivate; + check(!GSenderActor.IsSet()); + GSenderActor.Emplace(SenderActorDesc(Dependent, SenderActorDesc::Dependent)); +} + +void USpatialNetDriver::PopDependentActor() +{ + using namespace SpatialNetDriverPrivate; + GSenderActor.Reset(); +} + +bool USpatialNetDriver::RPCCallNeedWriteFence(AActor* Actor, UFunction* Function) +{ + using namespace SpatialNetDriverPrivate; + if (!GSenderActor.IsSet()) + { + UE_LOG(LogSpatialOSNetDriver, Error, + TEXT("Trying to execute NetWriteFence RPC without a dependent Actor. The RPC will be immediately executed. Actor : %s, " + "Function : %s"), + *Actor->GetName(), *Function->GetName()); + return false; + } + + SenderActorDesc& CurrentSender = GSenderActor.GetValue(); + if (CurrentSender.Kind == SenderActorDesc::Resolution) + { + GSenderActor.Reset(); + return false; + } + + if (Function->HasAnyFunctionFlags(FUNC_NetCrossServer)) + { + check(Function->HasAnyFunctionFlags(FUNC_NetWriteFence)); + if (CurrentSender.Kind != SenderActorDesc::Sender) + { + UE_LOG(LogSpatialOSNetDriver, Error, + TEXT("Trying to execute CrossServer RPC with the wrong kind of call method." + "Use SendCrossServerRPC instead of ExecuteWithNetWriteFence. The RPC will be executed without a write fence. Actor " + ": %s, Function : %s"), + *Actor->GetName(), *Function->GetName()); + return false; + } + return true; + } + + if (Function->HasAnyFunctionFlags(FUNC_NetWriteFence) && CurrentSender.Kind != SenderActorDesc::Dependent) + { + UE_LOG(LogSpatialOSNetDriver, Error, + TEXT("Trying to execute NetWriteFenceRPC with the wrong kind of call method." + "Use ExecuteWithNetWriteFence instead of SendCrossServerRPC. The RPC will be immediately executed. Actor : %s, " + "Function : %s"), + *Actor->GetName(), *Function->GetName()); + return false; + } + + if (CurrentSender.Actor == nullptr) + { + UE_LOG(LogSpatialOSNetDriver, Warning, + TEXT("Trying to execute NetWriteFence RPC with a null dependent Actor. The RPC will be immediately executed. Actor : %s, " + "Function : %s"), + *Actor->GetName(), *Function->GetName()); + return false; + } + + return CurrentSender.Actor->HasAuthority(); +} + +void USpatialNetDriver::PushNetWriteFenceResolution() +{ + using namespace SpatialNetDriverPrivate; + GSenderActor.Emplace(SenderActorDesc(nullptr, SenderActorDesc::Resolution)); } -void USpatialNetDriver::PopCrossServerRPCSender(AActor* SenderActor) +void USpatialNetDriver::PopNetWriteFenceResolution() { - check(GSenderStack.Num() > 0); - check(GSenderStack.Last() == SenderActor); - GSenderStack.Pop(); + using namespace SpatialNetDriverPrivate; + GSenderActor.Reset(); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriverDebugContext.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriverDebugContext.cpp index b8b4d2ab08..d29d97044c 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriverDebugContext.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriverDebugContext.cpp @@ -159,10 +159,12 @@ void USpatialNetDriverDebugContext::Reset() USpatialNetDriverDebugContext::DebugComponentAuthData& USpatialNetDriverDebugContext::GetAuthDebugComponent(AActor* Actor) { - check(Actor && Actor->HasAuthority()); + ensureAlwaysMsgf(Actor && Actor->HasAuthority(), TEXT("Called GetAuthDebugComponent without authority. Actor: %s"), + *GetNameSafe(Actor)); + SpatialGDK::DebugComponent* DbgComp = nullptr; - Worker_EntityId Entity = NetDriver->PackageMap->GetEntityIdFromObject(Actor); + const Worker_EntityId Entity = NetDriver->PackageMap->GetEntityIdFromObject(Actor); if (Entity != SpatialConstants::INVALID_ENTITY_ID) { DbgComp = DebugComponents.Find(Entity); @@ -213,7 +215,10 @@ void USpatialNetDriverDebugContext::AddComponent(Worker_EntityId EntityId) const SpatialGDK::ComponentData* Data = Element.Components.FindByPredicate([](const SpatialGDK::ComponentData& Component) { return Component.GetComponentId() == SpatialConstants::GDK_DEBUG_COMPONENT_ID; }); - check(Data != nullptr); + if (!ensureAlwaysMsgf(Data != nullptr, TEXT("Failed to access Debug component data for entity %lld"), EntityId)) + { + return; + } SpatialGDK::DebugComponent& DbgComp = DebugComponents.Add(EntityId, SpatialGDK::DebugComponent(Data->GetUnderlying())); if (!IsSetIntersectionEmpty(SemanticInterest, DbgComp.ActorTags)) @@ -231,7 +236,12 @@ void USpatialNetDriverDebugContext::RemoveComponent(Worker_EntityId EntityId) void USpatialNetDriverDebugContext::ApplyComponentUpdate(Worker_EntityId Entity, Schema_ComponentUpdate* Update) { SpatialGDK::DebugComponent* DbgComp = DebugComponents.Find(Entity); - check(DbgComp); + + if (!ensureAlwaysMsgf(DbgComp != nullptr, TEXT("Failed to ApplyComponentUpdate for debug context because component data wasn't found"))) + { + return; + } + DbgComp->ApplyComponentUpdate(Update); if (IsSetIntersectionEmpty(SemanticInterest, DbgComp->ActorTags)) diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriverGameplayDebuggerContext.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriverGameplayDebuggerContext.cpp new file mode 100644 index 0000000000..bd9b9042d5 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriverGameplayDebuggerContext.cpp @@ -0,0 +1,642 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "EngineClasses/SpatialNetDriverGameplayDebuggerContext.h" + +#if WITH_GAMEPLAY_DEBUGGER +#include "EngineClasses/SpatialActorChannel.h" +#include "EngineClasses/SpatialNetDriver.h" +#include "EngineClasses/SpatialPackageMapClient.h" +#include "EngineClasses/SpatialVirtualWorkerTranslator.h" +#include "Interop/Connection/SpatialWorkerConnection.h" +#include "LoadBalancing/GameplayDebuggerLBStrategy.h" +#include "Schema/AuthorityIntent.h" +#include "SpatialConstants.h" +#include "SpatialView/ComponentData.h" +#include "SpatialView/EntityView.h" +#include "SpatialView/SubView.h" +#include "SpatialView/ViewDelta.h" +#include "Utils/SpatialActorUtils.h" +#endif // WITH_GAMEPLAY_DEBUGGER + +DEFINE_LOG_CATEGORY_STATIC(LogSpatialNetDriverGameplayDebuggerContext, Log, All); + +#if WITH_GAMEPLAY_DEBUGGER +USpatialNetDriverGameplayDebuggerContext::~USpatialNetDriverGameplayDebuggerContext() +{ + Reset(); +} + +void USpatialNetDriverGameplayDebuggerContext::Enable(const SpatialGDK::FSubView& InSubView, USpatialNetDriver& NetDriver) +{ + if (NetDriver.GameplayDebuggerCtx != nullptr) + { + UE_LOG(LogSpatialNetDriverGameplayDebuggerContext, Error, TEXT("Enabling GDKGameplayDebugger more than once")); + return; + } + + if (NetDriver.LoadBalanceStrategy == nullptr) + { + UE_LOG(LogSpatialNetDriverGameplayDebuggerContext, Error, TEXT("Enabling GDKGameplayDebugger too soon")); + return; + } + + NetDriver.GameplayDebuggerCtx = NewObject(); + NetDriver.GameplayDebuggerCtx->Init(InSubView, NetDriver); +} + +void USpatialNetDriverGameplayDebuggerContext::Disable(USpatialNetDriver& NetDriver) +{ + if (NetDriver.GameplayDebuggerCtx == nullptr) + { + UE_LOG(LogSpatialNetDriverGameplayDebuggerContext, Error, TEXT("Disabling GDKGameplayDebugger before enabling it")); + return; + } + + NetDriver.GameplayDebuggerCtx = nullptr; +} + +void USpatialNetDriverGameplayDebuggerContext::Init(const SpatialGDK::FSubView& InSubView, USpatialNetDriver& InNetDriver) +{ + SubView = &InSubView; + NetDriver = &InNetDriver; + + check(NetDriver && NetDriver->Connection && NetDriver->Sender); + + if (NetDriver->LoadBalanceStrategy == nullptr) + { + UE_LOG(LogSpatialNetDriverGameplayDebuggerContext, Error, TEXT("Enabling GDKGameplayDebugger before LB strategy is setup")); + return; + } + + // Allocate and assign a specific "gameplay debugger LB strategy". This new + // strategy wraps the existing default strategy, and allows "replicator actors" + // to be intercepted and handled through specific gameplay debugger LB rules. + // All other actors are handled by the (wrapped) default strategy. + LBStrategy = NewObject(); + LBStrategy->Init(*this, *NetDriver->LoadBalanceStrategy); + NetDriver->LoadBalanceStrategy = LBStrategy; + NetDriver->Sender->UpdatePartitionEntityInterestAndPosition(); + + TSet VirtualWorkerIds = LBStrategy->GetVirtualWorkerIds(); + + PhysicalToVirtualWorkerIdMap.Reserve(VirtualWorkerIds.Num()); + for (const auto& VirtualWorkerId : VirtualWorkerIds) + { + const FString* PhysicalWorkerName = NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(VirtualWorkerId); + if (PhysicalWorkerName != nullptr) + { + PhysicalToVirtualWorkerIdMap.Add(*PhysicalWorkerName, VirtualWorkerId); + } + else + { + UE_LOG(LogSpatialNetDriverGameplayDebuggerContext, Error, TEXT("Failed to convert virtual worker to physical worker name")); + } + } +} + +void USpatialNetDriverGameplayDebuggerContext::Reset() +{ + for (auto& TrackedEntity : TrackedEntities) + { + if (AGameplayDebuggerCategoryReplicator* Replicator = + Cast(TrackedEntity.Value.ReplicatorWeakObjectPtr.Get())) + { + UnregisterServerRequestCallback(*Replicator, TrackedEntity.Value); + UnregisterPlayerControllerAuthorityLostCallback(*Replicator, TrackedEntity.Value); + UnregisterDebugActorChangedCallback(*Replicator, TrackedEntity.Value); + } + } + + TrackedEntities.Empty(); + ComponentsAdded.Empty(); + ComponentsUpdated.Empty(); + ActorsAdded.Empty(); +} + +TOptional USpatialNetDriverGameplayDebuggerContext::GetActorDelegatedWorkerId(const AActor& InActor) +{ + check(NetDriver && NetDriver->PackageMap); + + FEntityData* EntityData = nullptr; + + Worker_EntityId EntityId = NetDriver->PackageMap->GetEntityIdFromObject(&InActor); + if (EntityId != SpatialConstants::INVALID_ENTITY_ID) + { + EntityData = TrackedEntities.Find(EntityId); + } + + if (EntityData == nullptr) + { + return {}; + } + + return EntityData->Component.DelegatedVirtualWorkerId; +} + +void USpatialNetDriverGameplayDebuggerContext::AdvanceView() +{ + const SpatialGDK::FSubViewDelta& ViewDelta = SubView->GetViewDelta(); + for (const SpatialGDK::EntityDelta& Delta : ViewDelta.EntityDeltas) + { + switch (Delta.Type) + { + case SpatialGDK::EntityDelta::ADD: + TrackEntity(Delta.EntityId); + break; + case SpatialGDK::EntityDelta::REMOVE: + UntrackEntity(Delta.EntityId); + break; + case SpatialGDK::EntityDelta::TEMPORARILY_REMOVED: + UntrackEntity(Delta.EntityId); + TrackEntity(Delta.EntityId); + break; + case SpatialGDK::EntityDelta::UPDATE: + for (const SpatialGDK::AuthorityChange& Change : Delta.AuthorityGained) + { + if (Change.ComponentSetId == SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) + { + AddAuthority(Delta.EntityId, TrackedEntities.Find(Delta.EntityId)); + } + } + for (const SpatialGDK::AuthorityChange& Change : Delta.AuthorityLostTemporarily) + { + if (Change.ComponentSetId == SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) + { + RemoveAuthority(Delta.EntityId, TrackedEntities.Find(Delta.EntityId)); + } + } + for (const SpatialGDK::AuthorityChange& Change : Delta.AuthorityLost) + { + if (Change.ComponentSetId == SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) + { + RemoveAuthority(Delta.EntityId, TrackedEntities.Find(Delta.EntityId)); + } + } + break; + } + } +} + +void USpatialNetDriverGameplayDebuggerContext::TickServer() +{ + check(NetDriver && NetDriver->Connection && NetDriver->Sender && NetDriver->PackageMap); + + for (const auto& EntityAdded : ComponentsAdded) + { + if (NetDriver->HasServerAuthority(EntityAdded)) + { + if (FEntityData* EntityData = TrackedEntities.Find(EntityAdded)) + { + NetDriver->Connection->GetCoordinator().RefreshEntityCompleteness(EntityAdded); + } + } + } + ComponentsAdded.Reset(); + + for (auto It = ActorsAdded.CreateIterator(); It; It++) + { + // If authority lost, then forget about this actor + if (!NetDriver->HasServerAuthority(*It)) + { + It.RemoveCurrent(); + continue; + } + + // If entity lost, then forget about this actor + FEntityData* EntityData = TrackedEntities.Find(*It); + if (EntityData == nullptr) + { + It.RemoveCurrent(); + continue; + } + + TWeakObjectPtr EntityObjectWeakPtr = NetDriver->PackageMap->GetObjectFromEntityId(*It); + AGameplayDebuggerCategoryReplicator* CategoryReplicator = Cast(EntityObjectWeakPtr.Get()); + if (CategoryReplicator == nullptr) + { + // Not unexpected - assume latency and wait for actor to appear + continue; + } + + // Update tracking mode, and if changed send an update + const bool bShouldTrackPlayer = CategoryReplicator->GetServerTrackingMode() == EGameplayDebuggerServerTrackingMode::Player; + if (EntityData->Component.bTrackPlayer != bShouldTrackPlayer) + { + EntityData->Component.bTrackPlayer = bShouldTrackPlayer; + ComponentsUpdated.Add(*It); + } + + EntityData->ReplicatorWeakObjectPtr = TWeakObjectPtr(CategoryReplicator); + + // If we have authority over this replicator: + // a. Provide a list of available servers to the replicator actor - this allows the replicator + // to provide an interface where an authorative server (from available servers) can be selected + // b. Register a callback with the replicator actor, to handle authorative server and player controller change requests + if (CategoryReplicator->HasAuthority()) + { + // a. Build and provide available servers + TArray PhysicalWorkerIds; + PhysicalToVirtualWorkerIdMap.GetKeys(PhysicalWorkerIds); + CategoryReplicator->SetAvailableServers(PhysicalWorkerIds); + + // b. Register callback(s) + RegisterServerRequestCallback(*CategoryReplicator, *EntityData); + RegisterPlayerControllerAuthorityLostCallback(*CategoryReplicator, *EntityData); + RegisterDebugActorChangedCallback(*CategoryReplicator, *EntityData); + } + + It.RemoveCurrent(); + } + + for (const auto& EntityUpdated : ComponentsUpdated) + { + if (NetDriver->HasServerAuthority(EntityUpdated)) + { + if (FEntityData* EntityData = TrackedEntities.Find(EntityUpdated)) + { + FWorkerComponentUpdate ComponentUpdate = EntityData->Component.CreateComponentUpdate(); + NetDriver->Connection->SendComponentUpdate(EntityUpdated, &ComponentUpdate); + } + } + } + ComponentsUpdated.Reset(); +} + +void USpatialNetDriverGameplayDebuggerContext::TrackEntity(Worker_EntityId InEntityId) +{ + check(NetDriver && NetDriver->VirtualWorkerTranslator); + + const SpatialGDK::EntityViewElement& Element = SubView->GetView().FindChecked(InEntityId); + const SpatialGDK::ComponentData* Data = Element.Components.FindByPredicate([](const SpatialGDK::ComponentData& Component) { + return Component.GetComponentId() == SpatialConstants::GDK_GAMEPLAY_DEBUGGER_COMPONENT_ID; + }); + + if (Data == nullptr) + { + UE_LOG(LogSpatialNetDriverGameplayDebuggerContext, Error, TEXT("Failed to access component data for entity %lld"), InEntityId); + return; + } + + Schema_ComponentData* ComponentData = Data->GetUnderlying(); + if (ComponentData == nullptr) + { + UE_LOG(LogSpatialNetDriverGameplayDebuggerContext, Error, TEXT("Failed to get underlying component data for entity %lld"), + InEntityId); + return; + } + + FEntityData* EntityData = TrackedEntities.Find(InEntityId); + if (EntityData == nullptr) + { + EntityData = &TrackedEntities.Add(InEntityId); + EntityData->Component = SpatialGDK::GameplayDebuggerComponent(*ComponentData); + ComponentsAdded.Add(InEntityId); + ActorsAdded.Add(InEntityId); + } + else + { + UE_LOG(LogSpatialNetDriverGameplayDebuggerContext, Error, TEXT("Tracking entity twice, where id = %lld"), InEntityId); + } + + check(EntityData); + + const bool HasAuthority = NetDriver->HasServerAuthority(InEntityId); + if (HasAuthority) + { + AddAuthority(InEntityId, EntityData); + } + else + { + RemoveAuthority(InEntityId, EntityData); + } +} + +void USpatialNetDriverGameplayDebuggerContext::UntrackEntity(Worker_EntityId InEntityId) +{ + RemoveAuthority(InEntityId, TrackedEntities.Find(InEntityId)); + + TrackedEntities.Remove(InEntityId); + ComponentsAdded.Remove(InEntityId); + ComponentsUpdated.Remove(InEntityId); + ActorsAdded.Remove(InEntityId); +} + +void USpatialNetDriverGameplayDebuggerContext::AddAuthority(Worker_EntityId InEntityId, FEntityData* OptionalEntityData) +{ + check(NetDriver && NetDriver->VirtualWorkerTranslator); + + if (OptionalEntityData == nullptr) + { + return; + } + + OptionalEntityData->Component.DelegatedVirtualWorkerId = LBStrategy->GetLocalVirtualWorkerId(); + OptionalEntityData->Component.bTrackPlayer = false; // correct value is assigned when actor is resolved (on authorative server) + + const FString* PhysicalWorkerName = + NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(OptionalEntityData->Component.DelegatedVirtualWorkerId); + if (PhysicalWorkerName != nullptr) + { + OptionalEntityData->CurrentWorkerId = *PhysicalWorkerName; + } + else + { + UE_LOG(LogSpatialNetDriverGameplayDebuggerContext, Error, TEXT("Physical worker name not found")); + } + + ActorsAdded.AddUnique(InEntityId); +} + +void USpatialNetDriverGameplayDebuggerContext::RemoveAuthority(Worker_EntityId InEntityId, FEntityData* InOptionalEntityData) +{ + if (InOptionalEntityData == nullptr) + { + return; + } + + InOptionalEntityData->Component.DelegatedVirtualWorkerId = 0; + InOptionalEntityData->CurrentWorkerId.Empty(); + + if (AGameplayDebuggerCategoryReplicator* Replicator = + Cast(InOptionalEntityData->ReplicatorWeakObjectPtr.Get())) + { + UnregisterServerRequestCallback(*Replicator, *InOptionalEntityData); + UnregisterPlayerControllerAuthorityLostCallback(*Replicator, *InOptionalEntityData); + UnregisterDebugActorChangedCallback(*Replicator, *InOptionalEntityData); + } +} + +void USpatialNetDriverGameplayDebuggerContext::RegisterServerRequestCallback(AGameplayDebuggerCategoryReplicator& InReplicator, + FEntityData& InEntityData) +{ + if (!InEntityData.ServerTrackingRequestHandle.IsValid()) + { + InEntityData.ServerTrackingRequestHandle = + InReplicator.OnServerTrackingRequest().AddUObject(this, &USpatialNetDriverGameplayDebuggerContext::OnServerTrackingRequest); + } + else + { + UE_LOG(LogSpatialNetDriverGameplayDebuggerContext, Error, TEXT("Trying to bind change notification more than once")); + } +} + +void USpatialNetDriverGameplayDebuggerContext::UnregisterServerRequestCallback(AGameplayDebuggerCategoryReplicator& InReplicator, + FEntityData& InEntityData) +{ + if (InEntityData.ServerTrackingRequestHandle.IsValid()) + { + InReplicator.OnServerTrackingRequest().Remove(InEntityData.ServerTrackingRequestHandle); + InEntityData.ServerTrackingRequestHandle.Reset(); + } +} + +void USpatialNetDriverGameplayDebuggerContext::OnServerTrackingRequest(AGameplayDebuggerCategoryReplicator* InCategoryReplicator, + EGameplayDebuggerServerTrackingMode InServerTrackingMode, + FString InOptionalServerWorkerId) +{ + check(NetDriver && NetDriver->PackageMap); + + if (InCategoryReplicator == nullptr) + { + UE_LOG(LogSpatialNetDriverGameplayDebuggerContext, Error, TEXT("Callback with null replicator")); + return; + } + + if (!InCategoryReplicator->HasAuthority()) + { + UE_LOG(LogSpatialNetDriverGameplayDebuggerContext, Warning, + TEXT("Only expect to be registered and receive this callback when there is authority")); + return; + } + + const Worker_EntityId EntityId = NetDriver->PackageMap->GetEntityIdFromObject(InCategoryReplicator); + if (EntityId == SpatialConstants::INVALID_ENTITY_ID) + { + UE_LOG(LogSpatialNetDriverGameplayDebuggerContext, Warning, TEXT("Callback from an actor with no entity")); + return; + } + + FEntityData* const EntityData = TrackedEntities.Find(EntityId); + if (EntityData == nullptr) + { + UE_LOG(LogSpatialNetDriverGameplayDebuggerContext, Warning, TEXT("Callback from an entity we are not tracking")); + return; + } + + bool bShouldUpdateComponent = false; + + FString NewServerWorkerId = InOptionalServerWorkerId; + + const bool bShouldTrackPlayer = InServerTrackingMode == EGameplayDebuggerServerTrackingMode::Player ? true : false; + if (bShouldTrackPlayer != EntityData->Component.bTrackPlayer) + { + EntityData->Component.bTrackPlayer = bShouldTrackPlayer; + bShouldUpdateComponent = true; + } + + uint32 NewVirtualWorkerId = SpatialConstants::INVALID_VIRTUAL_WORKER_ID; + + if (bShouldTrackPlayer == true) + { + if (APlayerController* PlayerController = InCategoryReplicator->GetReplicationOwner()) + { + NewVirtualWorkerId = GetActorVirtualWorkerId(*PlayerController); + } + } + else if ((InOptionalServerWorkerId.Len() > 0) && (EntityData->CurrentWorkerId != InOptionalServerWorkerId)) + { + const uint32* VirtualWorkerId = PhysicalToVirtualWorkerIdMap.Find(InOptionalServerWorkerId); + if (VirtualWorkerId != nullptr) + { + NewVirtualWorkerId = *VirtualWorkerId; + } + else + { + UE_LOG(LogSpatialNetDriverGameplayDebuggerContext, Error, TEXT("Invalid server worker id provided ('%s')")); + } + } + + if ((NewVirtualWorkerId != SpatialConstants::INVALID_VIRTUAL_WORKER_ID) + && (NewVirtualWorkerId != EntityData->Component.DelegatedVirtualWorkerId)) + { + check(NetDriver != nullptr && NetDriver->VirtualWorkerTranslator != nullptr); + const FString* NewPhysicalWorkerName = NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(NewVirtualWorkerId); + if (NewPhysicalWorkerName != nullptr) + { + EntityData->Component.DelegatedVirtualWorkerId = NewVirtualWorkerId; + EntityData->CurrentWorkerId = *NewPhysicalWorkerName; + InCategoryReplicator->SetCurrentServer(*NewPhysicalWorkerName); + bShouldUpdateComponent = true; + } + } + + if (bShouldUpdateComponent) + { + ComponentsUpdated.Add(EntityId); + } +} + +void USpatialNetDriverGameplayDebuggerContext::RegisterDebugActorChangedCallback(AGameplayDebuggerCategoryReplicator& InReplicator, + FEntityData& InEntityData) +{ + if (!InEntityData.DebugActorChangedHandle.IsValid()) + { + InEntityData.DebugActorChangedHandle = + InReplicator.OnDebugActorChanged().AddUObject(this, &USpatialNetDriverGameplayDebuggerContext::OnDebugActorChanged); + } + else + { + UE_LOG(LogSpatialNetDriverGameplayDebuggerContext, Error, TEXT("Trying to bind change notification more than once")); + } +} + +void USpatialNetDriverGameplayDebuggerContext::UnregisterDebugActorChangedCallback(AGameplayDebuggerCategoryReplicator& InReplicator, + FEntityData& InEntityData) +{ + if (InEntityData.DebugActorChangedHandle.IsValid()) + { + InReplicator.OnDebugActorChanged().Remove(InEntityData.DebugActorChangedHandle); + InEntityData.DebugActorChangedHandle.Reset(); + } +} + +VirtualWorkerId USpatialNetDriverGameplayDebuggerContext::GetActorVirtualWorkerId(const AActor& InActor) const +{ + check(SubView != nullptr && NetDriver != nullptr && NetDriver->PackageMap != nullptr); + + const Worker_EntityId EntityId = NetDriver->PackageMap->GetEntityIdFromObject(&InActor); + if (EntityId == SpatialConstants::INVALID_ENTITY_ID) + { + return SpatialConstants::INVALID_VIRTUAL_WORKER_ID; + } + + const SpatialGDK::EntityViewElement* EntityViewPtr = SubView->GetView().Find(EntityId); + if (EntityViewPtr == nullptr) + { + return SpatialConstants::INVALID_VIRTUAL_WORKER_ID; + } + + const SpatialGDK::ComponentData* IntentComponentData = + EntityViewPtr->Components.FindByPredicate([](const SpatialGDK::ComponentData& Data) { + return Data.GetComponentId() == SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID; + }); + + if (IntentComponentData == nullptr) + { + return SpatialConstants::INVALID_VIRTUAL_WORKER_ID; + } + + SpatialGDK::AuthorityIntent Intent = SpatialGDK::AuthorityIntent(IntentComponentData->GetUnderlying()); + const VirtualWorkerId VirtualWorkerId = Intent.VirtualWorkerId; + return VirtualWorkerId; +}; + +void USpatialNetDriverGameplayDebuggerContext::RegisterPlayerControllerAuthorityLostCallback( + AGameplayDebuggerCategoryReplicator& InReplicator, FEntityData& InEntityData) +{ + if (!InEntityData.PlayerControllerAuthorityChangeHandle.IsValid()) + { + if (APlayerController* PlayerControllerActor = Cast(InReplicator.GetReplicationOwner())) + { + InEntityData.PlayerControllerAuthorityChangeHandle = PlayerControllerActor->OnAuthorityLostDelegate().AddUObject( + this, &USpatialNetDriverGameplayDebuggerContext::OnPlayerControllerAuthorityLost); + } + } + else + { + UE_LOG(LogSpatialNetDriverGameplayDebuggerContext, Error, TEXT("Trying to bind change notification more than once")); + } +} + +void USpatialNetDriverGameplayDebuggerContext::UnregisterPlayerControllerAuthorityLostCallback( + AGameplayDebuggerCategoryReplicator& InReplicator, FEntityData& InEntityData) +{ + check(NetDriver && NetDriver->PackageMap); + + if (InEntityData.PlayerControllerAuthorityChangeHandle.IsValid()) + { + if (APlayerController* PlayerController = InReplicator.GetReplicationOwner()) + { + PlayerController->OnAuthorityLostDelegate().Remove(InEntityData.PlayerControllerAuthorityChangeHandle); + InEntityData.PlayerControllerAuthorityChangeHandle.Reset(); + } + } +} + +void USpatialNetDriverGameplayDebuggerContext::OnDebugActorChanged(AGameplayDebuggerCategoryReplicator* InCategoryReplicator, + AActor* InDebugActor) +{ + check(NetDriver != nullptr && NetDriver->PackageMap != nullptr); + + if (InCategoryReplicator == nullptr) + { + return; + } + + const Worker_EntityId ReplicatorEntityId = NetDriver->PackageMap->GetEntityIdFromObject(InCategoryReplicator); + + USpatialActorChannel* ReplicatorChannel = NetDriver->GetActorChannelByEntityId(ReplicatorEntityId); + if (ReplicatorChannel == nullptr) + { + return; + } + + ReplicatorChannel->SetNeedOwnerInterestUpdate(true); +} + +void USpatialNetDriverGameplayDebuggerContext::OnPlayerControllerAuthorityLost(const APlayerController& InPlayerController) +{ + check(NetDriver && NetDriver->PackageMap); + + Worker_EntityId_Key* EntityId = nullptr; + FEntityData* EntityData = nullptr; + AGameplayDebuggerCategoryReplicator* Replicator = nullptr; + + for (auto& TrackedEntity : TrackedEntities) + { + Replicator = TrackedEntity.Value.ReplicatorWeakObjectPtr.Get(); + if (Replicator != nullptr) + { + if (Replicator->GetReplicationOwner() == &InPlayerController) + { + EntityId = &TrackedEntity.Key; + EntityData = &TrackedEntity.Value; + break; + } + } + } + + if (EntityId == nullptr || EntityData == nullptr || Replicator == nullptr) + { + return; + } + + if (EntityData->Component.bTrackPlayer == false) + { + // If we are not tracking the player, we do not need to track server authority changes + return; + } + + check(NetDriver->LoadBalanceStrategy); + const VirtualWorkerId AuthorativeWorkerId = GetActorVirtualWorkerId(InPlayerController); + + check(NetDriver->VirtualWorkerTranslator); + const FString* PhysicalAuthorativeWorkerName = + NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(AuthorativeWorkerId); + + if (PhysicalAuthorativeWorkerName == nullptr) + { + UE_LOG(LogSpatialNetDriverGameplayDebuggerContext, Error, TEXT("Failed to convert virtual worker to physical worker name")); + return; + } + + if (EntityData->CurrentWorkerId != *PhysicalAuthorativeWorkerName) + { + EntityData->Component.DelegatedVirtualWorkerId = AuthorativeWorkerId; + EntityData->CurrentWorkerId = *PhysicalAuthorativeWorkerName; + EntityData->ReplicatorWeakObjectPtr->SetCurrentServer(*PhysicalAuthorativeWorkerName); + ComponentsUpdated.Add(*EntityId); + } +} + +#endif // WITH_GAMEPLAY_DEBUGGER diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriverRPC.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriverRPC.cpp index 49ec9463cc..b81a8f2b12 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriverRPC.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriverRPC.cpp @@ -5,10 +5,14 @@ #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" #include "Interop/Connection/SpatialWorkerConnection.h" +#include "Interop/RPCs/RPCQueues.h" +#include "Interop/RPCs/RPC_RingBufferSerializer.h" +#include "Interop/RPCs/RPC_RingBufferWithACK_Receiver.h" +#include "Interop/RPCs/RPC_RingBufferWithACK_Sender.h" #include "Utils/RPCRingBuffer.h" #include "Utils/RepLayoutUtils.h" -#include "Interop/RPCs/RPCQueues.h" +#include "Algo/AnyOf.h" DEFINE_LOG_CATEGORY(LogSpatialNetDriverRPC); @@ -55,19 +59,28 @@ void FSpatialNetDriverRPC::OnRPCSent(SpatialGDK::SpatialEventTracer& EventTracer UpdateToSend& Update = OutUpdates.AddDefaulted_GetRef(); Update.EntityId = EntityId; Update.Update.component_id = ComponentId; + Update.Update.schema_type = nullptr; } OutUpdates.Last().Spans.Add(NewSpanId); } void FSpatialNetDriverRPC::OnDataWritten(TArray& OutArray, Worker_EntityId EntityId, Worker_ComponentId ComponentId, - Schema_ComponentData* InData) + Schema_ComponentUpdate* InData) { if (ensure(InData != nullptr)) { - FWorkerComponentData Data; - Data.component_id = ComponentId; - Data.schema_type = InData; - OutArray.Add(Data); + FWorkerComponentData* ExistingData = OutArray.FindByPredicate([ComponentId](const FWorkerComponentData& Data) { + return Data.component_id == ComponentId; + }); + if (ExistingData == nullptr) + { + FWorkerComponentData Data; + Data.component_id = ComponentId; + Data.schema_type = Schema_CreateComponentData(); + ExistingData = &OutArray.Add_GetRef(Data); + } + Schema_ApplyComponentUpdateToData(InData, ExistingData->schema_type); + Schema_DestroyComponentUpdate(InData); } } @@ -81,8 +94,18 @@ void FSpatialNetDriverRPC::OnUpdateWritten(TArray& OutUpdates, Wor UpdateToSend& Update = OutUpdates.AddDefaulted_GetRef(); Update.EntityId = EntityId; Update.Update.component_id = ComponentId; + Update.Update.schema_type = nullptr; + } + FWorkerComponentUpdate& Update = OutUpdates.Last().Update; + if (Update.schema_type == nullptr) + { + Update.schema_type = InUpdate; + } + else + { + Schema_MergeComponentUpdateIntoUpdate(InUpdate, Update.schema_type); + Schema_DestroyComponentUpdate(InUpdate); } - OutUpdates.Last().Update.schema_type = InUpdate; } } @@ -99,9 +122,9 @@ FSpatialNetDriverRPC::StandardQueue::SentRPCCallback FSpatialNetDriverRPC::MakeR return StandardQueue::SentRPCCallback(); } -RPCCallbacks::DataWritten FSpatialNetDriverRPC::MakeDataWriteCallback(TArray& OutArray) const +RPCCallbacks::UpdateWritten FSpatialNetDriverRPC::MakeDataWriteCallback(TArray& OutArray) const { - return [&OutArray](Worker_EntityId EntityId, Worker_ComponentId ComponentId, Schema_ComponentData* InData) { + return [&OutArray](Worker_EntityId EntityId, Worker_ComponentId ComponentId, Schema_ComponentUpdate* InData) { return OnDataWritten(OutArray, EntityId, ComponentId, InData); }; } @@ -117,6 +140,7 @@ RPCCallbacks::UpdateWritten FSpatialNetDriverRPC::MakeUpdateWriteCallback() FSpatialNetDriverRPC::FSpatialNetDriverRPC(USpatialNetDriver& InNetDriver, const SpatialGDK::FSubView& InActorAuthSubView, const SpatialGDK::FSubView& InActorNonAuthSubView) : NetDriver(InNetDriver) + , bUpdateCacheInUse(false) { EventTracer = NetDriver.Connection->GetEventTracer(); RPCService = MakeUnique(InActorNonAuthSubView, InActorAuthSubView); @@ -127,6 +151,8 @@ FSpatialNetDriverRPC::FSpatialNetDriverRPC(USpatialNetDriver& InNetDriver, const } } +FSpatialNetDriverRPC::~FSpatialNetDriverRPC() = default; + void FSpatialNetDriverRPC::AdvanceView() { RPCService->AdvanceView(); @@ -246,6 +272,30 @@ void FSpatialNetDriverRPC::FlushRPCQueueForEntity(Worker_EntityId EntityId, Stan Queue.Flush(EntityId, Ctx, MakeRPCSentCallback()); } +FSpatialGDKSpanId FSpatialNetDriverRPC::CreatePushRPCEvent(UObject* TargetObject, UFunction* Function) +{ + FSpatialGDKSpanId SpanId; + + if (EventTracer != nullptr) + { + SpanId = EventTracer->TraceEvent(PUSH_RPC_EVENT_NAME, "", EventTracer->GetFromStack().GetConstId(), /* NumCauses */ 1, + [TargetObject, Function](FSpatialTraceEventDataBuilder& EventBuilder) { + EventBuilder.AddObject(TargetObject); + EventBuilder.AddFunction(Function); + }); + } + + return SpanId; +} + +TArray FSpatialNetDriverRPC::CreateRPCPayloadData(UFunction* Function, void* Parameters) +{ + FSpatialNetBitWriter PayloadWriter(NetDriver.PackageMap); + const TSharedPtr RepLayout = NetDriver.GetFunctionRepLayout(Function); + SpatialGDK::RepLayout_SendPropertiesForRPC(*RepLayout, PayloadWriter, Parameters); + return TArray(PayloadWriter.GetData(), PayloadWriter.GetNumBytes()); +} + bool FSpatialNetDriverRPC::CanExtractRPC(Worker_EntityId EntityId) const { const TWeakObjectPtr ActorReceivingRPC = NetDriver.PackageMap->GetObjectFromEntityId(EntityId); @@ -308,7 +358,7 @@ struct RAIIParamsHolder : FStackOnly uint8* Parms; }; -bool FSpatialNetDriverRPC::ApplyRPC(Worker_EntityId EntityId, SpatialGDK::ReceivedRPC RPCData, const FRPCMetaData& MetaData) const +bool FSpatialNetDriverRPC::ApplyRPC(Worker_EntityId EntityId, const FRPCPayload& RPCData, const FRPCMetaData& MetaData) const { constexpr bool RPCConsumed = true; @@ -357,16 +407,38 @@ bool FSpatialNetDriverRPC::ApplyRPC(Worker_EntityId EntityId, SpatialGDK::Receiv const USpatialGDKSettings* SpatialSettings = GetDefault(); + const TOptional RPCType = SpatialConstants::RPCStringToType(MetaData.RPCName.ToString()); + const float TimeQueued = (FPlatformTime::Cycles64() - MetaData.Timestamp) * FPlatformTime::GetSecondsPerCycle64(); const int32 UnresolvedRefCount = UnresolvedRefs.Num(); - if (UnresolvedRefCount != 0 && TimeQueued < SpatialSettings->QueuedIncomingRPCWaitTime) + const bool bIsReliableChannel = RPCType.Get(/*DefaultValue*/ ERPCType::Invalid) == ERPCType::ClientReliable + || RPCType.Get(/*DefaultValue*/ ERPCType::Invalid) == ERPCType::ServerReliable; + + const bool bMissingServerObject = Algo::AnyOf(UnresolvedRefs, [&TargetObject, Function](const FUnrealObjectRef& MissingRef) { + if (MissingRef.bNoLoadOnClient) + { + return true; + } + else if (!ensureAlwaysMsgf(MissingRef.Path.IsSet(), + TEXT("Received reference to dynamic object as loadable. Target : %s, Parameter Entity : %llu, RPC : %s"), + *TargetObject->GetName(), MissingRef.Entity, *Function->GetName())) + { + // Validation code, to ensure that every loadable ref we receive has a name. + return true; + } + return false; + }); + + const bool bCannotWaitLongerThanQueueTime = !bIsReliableChannel || bMissingServerObject; + const bool bQueueTimeExpired = TimeQueued > SpatialSettings->QueuedIncomingRPCWaitTime; + const bool bMustExecuteRPC = UnresolvedRefCount == 0 || (bCannotWaitLongerThanQueueTime && bQueueTimeExpired); + + if (!bMustExecuteRPC) { return !RPCConsumed; } - TOptional RPCType = SpatialConstants::RPCStringToType(MetaData.RPCName.ToString()); - if (UnresolvedRefCount > 0 && RPCType.IsSet() && !SpatialSettings->ShouldRPCTypeAllowUnresolvedParameters(RPCType.GetValue()) && (Function->SpatialFunctionFlags & SPATIALFUNC_AllowUnresolvedParameters) == 0) { @@ -401,17 +473,59 @@ bool FSpatialNetDriverRPC::ApplyRPC(Worker_EntityId EntityId, SpatialGDK::Receiv return RPCConsumed; } +using ClientServerSender = MonotonicRingBufferWithACKSender>; +using ClientServerReceiver = + MonotonicRingBufferWithACKReceiver>; + void FSpatialNetDriverRPC::MakeRingBufferWithACKSender(ERPCType RPCType, Worker_ComponentSetId AuthoritySet, TUniquePtr& SenderPtr, TUniquePtr>& QueuePtr) { - // TODO UNR-5037 + auto RPCDesc = RPCRingBufferUtils::GetRingBufferDescriptor(RPCType); + RingBufferSerializer_Schema Serializer(RPCRingBufferUtils::GetRingBufferComponentId(RPCType), RPCDesc.LastSentRPCFieldId, + RPCDesc.SchemaFieldStart, RPCRingBufferUtils::GetAckComponentId(RPCType), + RPCRingBufferUtils::GetAckFieldId(RPCType)); + + auto Sender = MakeUnique(MoveTemp(Serializer), RPCDesc.RingBufferSize); + + RPCService::RPCQueueDescription Desc; + Desc.Sender = Sender.Get(); + + FName RPCName(SpatialConstants::RPCTypeToString(RPCType)); + if (RPCRingBufferUtils::ShouldQueueOverflowed(RPCType)) + { + QueuePtr = MakeUnique>(RPCName, *Sender); + } + else + { + QueuePtr = MakeUnique>(RPCName, *Sender, RPCDesc.RingBufferSize); + } + Desc.Queue = QueuePtr.Get(); + Desc.Authority = AuthoritySet; + + RPCService->AddRPCQueue(RPCName, MoveTemp(Desc)); + SenderPtr.Reset(Sender.Release()); } void FSpatialNetDriverRPC::MakeRingBufferWithACKReceiver(ERPCType RPCType, Worker_ComponentSetId AuthoritySet, TUniquePtr>& ReceiverPtr) { - // TODO UNR-5037 + auto RPCDesc = RPCRingBufferUtils::GetRingBufferDescriptor(RPCType); + + RingBufferSerializer_Schema Serializer(RPCRingBufferUtils::GetRingBufferComponentId(RPCType), RPCDesc.LastSentRPCFieldId, + RPCDesc.SchemaFieldStart, RPCRingBufferUtils::GetAckComponentId(RPCType), + RPCRingBufferUtils::GetAckFieldId(RPCType)); + + FName RPCName(SpatialConstants::RPCTypeToString(RPCType)); + + ReceiverPtr = MakeUnique(MoveTemp(Serializer), RPCDesc.RingBufferSize, + TimestampAndETWrapper(RPCName, Serializer.GetComponentId(), EventTracer)); + + RPCService::RPCReceiverDescription Desc; + Desc.Authority = AuthoritySet; + Desc.Receiver = ReceiverPtr.Get(); + + RPCService->AddRPCReceiver(RPCName, MoveTemp(Desc)); } FSpatialNetDriverServerRPC::FSpatialNetDriverServerRPC(USpatialNetDriver& InNetDriver, const SpatialGDK::FSubView& InActorAuthSubView, @@ -450,7 +564,7 @@ void FSpatialNetDriverServerRPC::ProcessReceivedRPCs() auto CanExtractRPCs = [this](Worker_EntityId EntityId) { return CanExtractRPCOnServer(EntityId); }; - auto ProcessRPC = [this](Worker_EntityId EntityId, ReceivedRPC RPCData, const FRPCMetaData& MetaData) { + auto ProcessRPC = [this](Worker_EntityId EntityId, const FRPCPayload& RPCData, const FRPCMetaData& MetaData) { return ApplyRPC(EntityId, RPCData, MetaData); }; @@ -499,7 +613,7 @@ void FSpatialNetDriverClientRPC::ProcessReceivedRPCs() auto CanExtractRPCs = [this](Worker_EntityId EntityId) { return CanExtractRPC(EntityId); }; - auto ProcessRPC = [this](Worker_EntityId EntityId, ReceivedRPC RPCData, const FRPCMetaData& MetaData) { + auto ProcessRPC = [this](Worker_EntityId EntityId, const FRPCPayload& RPCData, const FRPCMetaData& MetaData) { return ApplyRPC(EntityId, RPCData, MetaData); }; diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp index b3e675ab1f..ec4b1e1aed 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp @@ -8,7 +8,6 @@ #include "EngineClasses/SpatialNetDriver.h" #include "Interop/ActorSystem.h" #include "Interop/Connection/SpatialWorkerConnection.h" -#include "Interop/SpatialReceiver.h" #include "Interop/SpatialSender.h" #include "Schema/UnrealObjectRef.h" #include "SpatialConstants.h" @@ -21,17 +20,18 @@ #include "Kismet/GameplayStatics.h" #include "Runtime/Launch/Resources/Version.h" #include "UObject/UObjectGlobals.h" +#include "Utils/SpatialActorUtils.h" DEFINE_LOG_CATEGORY(LogSpatialPackageMap); -void USpatialPackageMapClient::Init(USpatialNetDriver* NetDriver, FTimerManager* TimerManager) +void USpatialPackageMapClient::Init(USpatialNetDriver& NetDriver) { - bIsServer = NetDriver->IsServer(); // Entity Pools should never exist on clients + bIsServer = NetDriver.IsServer(); if (bIsServer) { EntityPool = NewObject(); - EntityPool->Init(NetDriver, TimerManager); + EntityPool->Init(NetDriver); } } @@ -95,7 +95,7 @@ Worker_EntityId USpatialPackageMapClient::AllocateEntityIdAndResolveActor(AActor } // Register Actor with package map since we know what the entity id is. - if (!ResolveEntityActor(Actor, EntityId)) + if (!ResolveEntityActorAndSubobjects(EntityId, Actor)) { UE_LOG(LogSpatialPackageMap, Error, TEXT("Unable to resolve an Entity for Actor: %s"), *Actor->GetName()); return SpatialConstants::INVALID_ENTITY_ID; @@ -150,7 +150,7 @@ void USpatialPackageMapClient::RemovePendingCreationEntityId(Worker_EntityId Ent PendingCreationEntityIds.Remove(EntityId); } -bool USpatialPackageMapClient::ResolveEntityActor(AActor* Actor, Worker_EntityId EntityId) +bool USpatialPackageMapClient::ResolveEntityActorAndSubobjects(const Worker_EntityId EntityId, AActor* Actor) { FSpatialNetGUIDCache* SpatialGuidCache = static_cast(GuidCache.Get()); FNetworkGUID NetGUID = SpatialGuidCache->GetNetGUIDFromEntityId(EntityId); @@ -163,8 +163,9 @@ bool USpatialPackageMapClient::ResolveEntityActor(AActor* Actor, Worker_EntityId if (GetEntityIdFromObject(Actor) != EntityId) { - UE_LOG(LogSpatialPackageMap, Error, TEXT("ResolveEntityActor failed for Actor: %s with NetGUID: %s and passed entity ID: %lld"), - *Actor->GetName(), *NetGUID.ToString(), EntityId); + UE_LOG(LogSpatialPackageMap, Error, + TEXT("ResolveEntityActorAndSubobjects failed for Actor: %s with NetGUID: %s and passed entity ID: %lld"), *Actor->GetName(), + *NetGUID.ToString(), EntityId); return false; } @@ -244,32 +245,6 @@ TWeakObjectPtr USpatialPackageMapClient::GetObjectFromUnrealObjectRef(c return nullptr; } -FNetworkGUID* USpatialPackageMapClient::GetRemovedDynamicSubobjectNetGUID(const FUnrealObjectRef& ObjectRef) -{ - if (FNetworkGUID* NetGUID = RemovedDynamicSubobjectObjectRefs.Find(ObjectRef)) - { - return NetGUID; - } - return nullptr; -} - -void USpatialPackageMapClient::AddRemovedDynamicSubobjectObjectRef(const FUnrealObjectRef& ObjectRef, const FNetworkGUID& NetGUID) -{ - RemovedDynamicSubobjectObjectRefs.Emplace(ObjectRef, NetGUID); -} - -void USpatialPackageMapClient::ClearRemovedDynamicSubobjectObjectRefs(const Worker_EntityId& InEntityId) -{ - for (auto DynamicSubobjectIterator = RemovedDynamicSubobjectObjectRefs.CreateIterator(); DynamicSubobjectIterator; - ++DynamicSubobjectIterator) - { - if (DynamicSubobjectIterator->Key.Entity == InEntityId) - { - DynamicSubobjectIterator.RemoveCurrent(); - } - } -} - TWeakObjectPtr USpatialPackageMapClient::GetObjectFromEntityId(const Worker_EntityId EntityId) { return GetObjectFromUnrealObjectRef(FUnrealObjectRef(EntityId, 0)); @@ -416,62 +391,14 @@ FSpatialNetGUIDCache::FSpatialNetGUIDCache(USpatialNetDriver* InDriver) { } -using FSubobjectToOffsetMap = TMap; - -static FSubobjectToOffsetMap CreateOffsetMapFromActor(USpatialPackageMapClient* PackageMap, AActor* Actor, const FClassInfo& Info) +FNetworkGUID FSpatialNetGUIDCache::AssignNewEntityActorNetGUID(AActor* Actor, Worker_EntityId EntityId) { - FSubobjectToOffsetMap SubobjectNameToOffset; - - for (auto& SubobjectInfoPair : Info.SubobjectInfo) + if (!ensureAlwaysMsgf(IsValid(Actor), TEXT("Tried to assign net guid for invalid actor. EntityId: %lld"), EntityId) + || !ensureAlwaysMsgf(EntityId > 0, TEXT("Tried to assign net guid for invalid entity ID. Actor: %s"), *GetNameSafe(Actor))) { - UObject* Subobject = StaticFindObjectFast(UObject::StaticClass(), Actor, SubobjectInfoPair.Value->SubobjectName); - const uint32 Offset = SubobjectInfoPair.Key; - - if (Subobject != nullptr && Subobject->IsPendingKill() == false && Subobject->IsSupportedForNetworking()) - { - SubobjectNameToOffset.Add(Subobject, Offset); - } - } - - if (Actor->GetInstanceComponents().Num() > 0) - { - // Process components attached to this object; this allows us to join up - // server- and client-side components added in the level. - TArray ActorInstanceComponents; - - // In non-editor builds, editor-only components can be allocated a slot in the array, but left as nullptrs. - Algo::CopyIf(Actor->GetInstanceComponents(), ActorInstanceComponents, [](UActorComponent* Component) -> bool { - return IsValid(Component); - }); - // These need to be ordered in case there are more than one component of the same type, or - // we may end up with wrong component instances having associations between them. - ActorInstanceComponents.Sort([](const UActorComponent& Lhs, const UActorComponent& Rhs) -> bool { - return Lhs.GetName().Compare(Rhs.GetName()) < 0; - }); - - for (UActorComponent* DynamicComponent : ActorInstanceComponents) - { - if (!DynamicComponent->IsSupportedForNetworking()) - { - continue; - } - - const FClassInfo* DynamicComponentClassInfo = PackageMap->TryResolveNewDynamicSubobjectAndGetClassInfo(DynamicComponent); - - if (DynamicComponentClassInfo != nullptr) - { - SubobjectNameToOffset.Add(DynamicComponent, DynamicComponentClassInfo->SchemaComponents[SCHEMA_Data]); - } - } + return FNetworkGUID(); } - return SubobjectNameToOffset; -} - -FNetworkGUID FSpatialNetGUIDCache::AssignNewEntityActorNetGUID(AActor* Actor, Worker_EntityId EntityId) -{ - check(EntityId > 0); - USpatialNetDriver* SpatialNetDriver = Cast(Driver); FNetworkGUID NetGUID; @@ -504,13 +431,14 @@ FNetworkGUID FSpatialNetGUIDCache::AssignNewEntityActorNetGUID(AActor* Actor, Wo UE_LOG(LogSpatialPackageMap, Verbose, TEXT("Registered new object ref for actor: %s. NetGUID: %s, entity ID: %lld"), *Actor->GetName(), *NetGUID.ToString(), EntityId); - const FClassInfo& Info = SpatialNetDriver->ClassInfoManager->GetOrCreateClassInfoByClass(Actor->GetClass()); - const FSubobjectToOffsetMap& SubobjectToOffset = CreateOffsetMapFromActor(SpatialNetDriver->PackageMap, Actor, Info); + const FClassInfo& ActorInfo = SpatialNetDriver->ClassInfoManager->GetOrCreateClassInfoByClass(Actor->GetClass()); + const SpatialGDK::FSubobjectToOffsetMap& SubobjectsToOffsets = + SpatialGDK::CreateOffsetMapFromActor(*SpatialNetDriver->PackageMap, *Actor, ActorInfo); - for (auto& Pair : SubobjectToOffset) + for (auto& SubobjectToOffset : SubobjectsToOffsets) { - UObject* Subobject = Pair.Key; - uint32 Offset = Pair.Value; + UObject* Subobject = SubobjectToOffset.Key; + const ObjectOffset Offset = SubobjectToOffset.Value; // AssignNewStablyNamedObjectNetGUID is not used due to using the wrong ObjectRef as the outer of the subobject. // So it is ok to use RegisterObjectRef in both cases since no prior bookkeeping was done (unlike Actors) @@ -527,7 +455,8 @@ FNetworkGUID FSpatialNetGUIDCache::AssignNewEntityActorNetGUID(AActor* Actor, Wo // Using StablyNamedRef for the outer since referencing ObjectRef in the map // will have the EntityId - FUnrealObjectRef StablyNamedSubobjectRef(0, 0, Subobject->GetFName().ToString(), StablyNamedRef); + FUnrealObjectRef StablyNamedSubobjectRef(0, 0, Subobject->GetFName().ToString(), StablyNamedRef, + !CanClientLoadObject(Subobject, SubobjectNetGUID)); // This is the only extra object ref that has to be registered for the subobject. UnrealObjectRefToNetGUID.Emplace(StablyNamedSubobjectRef, SubobjectNetGUID); @@ -608,7 +537,7 @@ void FSpatialNetGUIDCache::RemoveEntityNetGUID(Worker_EntityId EntityId) // Due to UnrealMetadata::GetNativeEntityClass using LoadObject, if we are shutting down and garbage collecting, // calling LoadObject will crash the editor. In this case, just return since everything will be cleaned up anyways. - if (IsInGameThread() && IsGarbageCollecting()) + if (IsEngineExitRequested() || (IsInGameThread() && IsGarbageCollecting())) { return; } @@ -633,8 +562,9 @@ void FSpatialNetGUIDCache::RemoveEntityNetGUID(Worker_EntityId EntityId) if (StablyNamedRefOption.IsSet()) { - UnrealObjectRefToNetGUID.Remove( - FUnrealObjectRef(0, 0, SubobjectInfoPair.Value->SubobjectName.ToString(), StablyNamedRefOption.GetValue())); + // bNoLoadOnClient is set to a fixed value because it does not affect equality + UnrealObjectRefToNetGUID.Remove(FUnrealObjectRef(0, 0, SubobjectInfoPair.Value->SubobjectName.ToString(), + StablyNamedRefOption.GetValue(), /*bNoLoadOnClient*/ false)); } } } @@ -688,7 +618,7 @@ void FSpatialNetGUIDCache::RemoveSubobjectNetGUID(const FUnrealObjectRef& Subobj // Due to UnrealMetadata::GetNativeEntityClass using LoadObject, if we are shutting down and garbage collecting, // calling LoadObject will crash the editor. In this case, just return since everything will be cleaned up anyways. - if (IsInGameThread() && IsGarbageCollecting()) + if (IsEngineExitRequested() || (IsInGameThread() && IsGarbageCollecting())) { return; } @@ -709,8 +639,9 @@ void FSpatialNetGUIDCache::RemoveSubobjectNetGUID(const FUnrealObjectRef& Subobj if (StablyNamedRefOption.IsSet()) { - UnrealObjectRefToNetGUID.Remove( - FUnrealObjectRef(0, 0, SubobjectInfoPtr->Get().SubobjectName.ToString(), StablyNamedRefOption.GetValue())); + // bNoLoadOnClient is set to a fixed value because it does not affect equality + UnrealObjectRefToNetGUID.Remove(FUnrealObjectRef(0, 0, SubobjectInfoPtr->Get().SubobjectName.ToString(), + StablyNamedRefOption.GetValue(), /*bNoLoadOnClient*/ false)); } } } diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp index ca1534dbdf..a1e1e58333 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp @@ -40,8 +40,13 @@ void SpatialVirtualWorkerTranslationManager::SetNumberOfVirtualWorkers(const uin void SpatialVirtualWorkerTranslationManager::AuthorityChanged(const Worker_ComponentSetAuthorityChangeOp& AuthOp) { - check(AuthOp.component_set_id == SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID - || AuthOp.component_set_id == SpatialConstants::SERVER_WORKER_ENTITY_AUTH_COMPONENT_SET_ID); + if (!ensureAlwaysMsgf(AuthOp.component_set_id == SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID + || AuthOp.component_set_id == SpatialConstants::SERVER_WORKER_ENTITY_AUTH_COMPONENT_SET_ID, + TEXT("Translation managed handled unexpected component set auth op. Entity: %lld. Set: %u"), AuthOp.entity_id, + AuthOp.component_set_id)) + { + return; + } const bool bAuthoritative = AuthOp.authority == WORKER_AUTHORITY_AUTHORITATIVE; diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/ActorSetWriter.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/ActorSetWriter.cpp index 6c06da31c9..098f46a38c 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/ActorSetWriter.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/ActorSetWriter.cpp @@ -10,10 +10,20 @@ namespace SpatialGDK ActorSetMember GetActorSetData(const USpatialPackageMapClient& PackageMap, const AActor& Actor) { const AActor* LeaderActor = GetReplicatedHierarchyRoot(&Actor); - check(IsValid(LeaderActor)); + + if (!ensureAlwaysMsgf(IsValid(LeaderActor), TEXT("Failed to get replicated hierarchy root when getting Actor set data for Actor: %s"), + *GetNameSafe(&Actor))) + { + return ActorSetMember(); + } const Worker_EntityId LeaderEntityId = PackageMap.GetEntityIdFromObject(LeaderActor); - check(LeaderEntityId != SpatialConstants::INVALID_ENTITY_ID); + + if (!ensureAlwaysMsgf(LeaderEntityId != SpatialConstants::INVALID_ENTITY_ID, + TEXT("Failed to get entity Id from package map for Actor: %s"), *GetNameSafe(LeaderActor))) + { + return ActorSetMember(); + } return ActorSetMember(LeaderEntityId); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/ActorSubviews.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/ActorSubviews.cpp new file mode 100644 index 0000000000..ca63eb77dc --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/ActorSubviews.cpp @@ -0,0 +1,271 @@ +#include "Interop/ActorSubviews.h" + +#include "EngineClasses/SpatialNetDriver.h" +#include "Interop/ActorSystem.h" +#include "Interop/Connection/SpatialWorkerConnection.h" +#include "Interop/InitialOnlyFilter.h" +#include "Interop/OwnershipCompletenessHandler.h" +#include "Schema/ActorOwnership.h" +#include "Schema/Restricted.h" +#include "Schema/Tombstone.h" +#include "Schema/UnrealMetadata.h" +#include "SpatialView/EntityComponentTypes.h" +#include "SpatialView/EntityView.h" +#include "SpatialView/SubView.h" +#include "SpatialView/ViewCoordinator.h" + +namespace SpatialGDK +{ +namespace ActorSubviews +{ +namespace MainActorSubviewSetup +{ +static bool IsActorEntity(const Worker_EntityId EntityId, const EntityViewElement& Entity, USpatialNetDriver& NetDriver); + +static TArray GetCallbacks(ViewCoordinator& Coordinator); +} // namespace MainActorSubviewSetup + +namespace AuthoritySubviewSetup +{ +static bool IsAuthorityActorEntity(const Worker_EntityId EntityId, const EntityViewElement& Element); + +static TArray GetCallbacks(ViewCoordinator& Coordinator); +} // namespace AuthoritySubviewSetup + +namespace OwnershipSubviewSetup +{ +static bool IsPlayerOwnedActorEntity(const Worker_EntityId EntityId, const EntityViewElement& Element); + +static TArray GetCallbacks(ViewCoordinator& Coordinator); +} // namespace OwnershipSubviewSetup + +namespace SimulatedSubviewSetup +{ +static bool IsSimulatedActorEntity(const Worker_EntityId EntityId, const EntityViewElement& Entity); + +static TArray GetCallbacks(ViewCoordinator& Coordinator); +} // namespace SimulatedSubviewSetup + +static TArray CombineCallbacks(TArray Lhs, + const TArray& Rhs) +{ + Lhs.Append(Rhs); + return Lhs; +} + +bool MainActorSubviewSetup::IsActorEntity(const Worker_EntityId EntityId, const EntityViewElement& Entity, USpatialNetDriver& NetDriver) +{ + if (Entity.Components.ContainsByPredicate(ComponentIdEquality{ Tombstone::ComponentId })) + { + return false; + } + + if (NetDriver.AsyncPackageLoadFilter != nullptr) + { + const UnrealMetadata Metadata( + Entity.Components.FindByPredicate(ComponentIdEquality{ SpatialConstants::UNREAL_METADATA_COMPONENT_ID })->GetUnderlying()); + + if (!NetDriver.AsyncPackageLoadFilter->IsAssetLoadedOrTriggerAsyncLoad(EntityId, Metadata.ClassPath)) + { + return false; + } + } + + if (NetDriver.InitialOnlyFilter != nullptr) + { + if (Entity.Components.ContainsByPredicate(ComponentIdEquality{ SpatialConstants::INITIAL_ONLY_PRESENCE_COMPONENT_ID })) + { + if (!NetDriver.InitialOnlyFilter->HasInitialOnlyDataOrRequestIfAbsent(EntityId)) + { + return false; + } + } + } + + // If we see a player controller component on this entity and we're a server we should hold it back until we + // also have the partition component. + return !NetDriver.IsServer() + || Entity.Components.ContainsByPredicate(ComponentIdEquality{ SpatialConstants::PLAYER_CONTROLLER_COMPONENT_ID }) + == Entity.Components.ContainsByPredicate(ComponentIdEquality{ SpatialConstants::PARTITION_COMPONENT_ID }); +} + +TArray MainActorSubviewSetup::GetCallbacks(ViewCoordinator& Coordinator) +{ + return { Coordinator.CreateComponentExistenceRefreshCallback(Tombstone::ComponentId), + Coordinator.CreateComponentExistenceRefreshCallback(Partition::ComponentId), + Coordinator.CreateComponentExistenceRefreshCallback(SpatialConstants::PLAYER_CONTROLLER_COMPONENT_ID) }; +} + +bool AuthoritySubviewSetup::IsAuthorityActorEntity(const Worker_EntityId EntityId, const EntityViewElement& Element) +{ + return Element.Authority.Contains(SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID); +} + +TArray AuthoritySubviewSetup::GetCallbacks(ViewCoordinator& Coordinator) +{ + return { + Coordinator.CreateAuthorityChangeRefreshCallback(SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID), + }; +} + +bool OwnershipSubviewSetup::IsPlayerOwnedActorEntity(const Worker_EntityId EntityId, const EntityViewElement& Element) +{ + return !AuthoritySubviewSetup::IsAuthorityActorEntity(EntityId, Element) + && Element.Authority.Contains(SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID); +} + +TArray OwnershipSubviewSetup::GetCallbacks(ViewCoordinator& Coordinator) +{ + return CombineCallbacks(AuthoritySubviewSetup::GetCallbacks(Coordinator), + { + Coordinator.CreateAuthorityChangeRefreshCallback(SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID), + }); +} + +bool SimulatedSubviewSetup::IsSimulatedActorEntity(const Worker_EntityId EntityId, const EntityViewElement& Entity) +{ + return !AuthoritySubviewSetup::IsAuthorityActorEntity(EntityId, Entity) + && !OwnershipSubviewSetup::IsPlayerOwnedActorEntity(EntityId, Entity); +} + +TArray SimulatedSubviewSetup::GetCallbacks(ViewCoordinator& Coordinator) +{ + return OwnershipSubviewSetup::GetCallbacks(Coordinator); +} + +FSubView& CreateActorSubView(USpatialNetDriver& NetDriver) +{ + return CreateCustomActorSubView(/*CustomComponentId =*/{}, /*CustomPredicate =*/{}, /*CustomRefresh =*/{}, NetDriver); +} + +FSubView& CreateCustomActorSubView(TOptional MaybeCustomComponentId, TOptional MaybeCustomPredicate, + TOptional> MaybeCustomRefresh, USpatialNetDriver& NetDriver) +{ + if (!MaybeCustomComponentId) + { + MaybeCustomComponentId = SpatialConstants::ACTOR_TAG_COMPONENT_ID; + } + + if (MaybeCustomPredicate) + { + MaybeCustomPredicate = [CustomPredicate = MaybeCustomPredicate.GetValue(), &NetDriver](const Worker_EntityId EntityId, + const EntityViewElement& Entity) { + if (!MainActorSubviewSetup::IsActorEntity(EntityId, Entity, NetDriver)) + { + return false; + } + + return CustomPredicate(EntityId, Entity); + }; + } + else + { + MaybeCustomPredicate = [&NetDriver](const Worker_EntityId EntityId, const EntityViewElement& Entity) { + return MainActorSubviewSetup::IsActorEntity(EntityId, Entity, NetDriver); + }; + } + + if (MaybeCustomRefresh) + { + MaybeCustomRefresh->Append(MainActorSubviewSetup::GetCallbacks(NetDriver.Connection->GetCoordinator())); + } + else + { + MaybeCustomRefresh = MainActorSubviewSetup::GetCallbacks(NetDriver.Connection->GetCoordinator()); + } + + return NetDriver.Connection->GetCoordinator().CreateSubView(*MaybeCustomComponentId, *MaybeCustomPredicate, *MaybeCustomRefresh); +} + +FSubView& CreateActorAuthSubView(USpatialNetDriver& NetDriver) +{ + return NetDriver.Connection->GetCoordinator().CreateSubView(SpatialConstants::ACTOR_AUTH_TAG_COMPONENT_ID, FSubView::NoFilter, + FSubView::NoDispatcherCallbacks); +} + +template +FFilterPredicate GetRoleFilterPredicate(TCallable RolePredicate, TValues... Values) +{ + return [RolePredicate, ExtraValues = TTuple(Values...)](const Worker_EntityId EntityId, + const EntityViewElement& Entity) -> bool { + return ExtraValues.ApplyAfter(RolePredicate, EntityId, Entity); + }; +} + +static FActorSubviewExtension GetNetDriverSubviewExtension(USpatialNetDriver& NetDriver) +{ + return { FActorFilterPredicateFactory([&NetDriver](FFilterPredicate BasePredicate) { + return [&NetDriver, BasePredicate = MoveTemp(BasePredicate)](const Worker_EntityId EntityId, + const EntityViewElement& Element) { + if (!MainActorSubviewSetup::IsActorEntity(EntityId, Element, NetDriver)) + { + return false; + } + + return BasePredicate(EntityId, Element); + }; + }), + FActorRefreshCallbackPredicateFactory([&NetDriver](TArray RefreshCallbacks) { + RefreshCallbacks.Append(MainActorSubviewSetup::GetCallbacks(NetDriver.Connection->GetCoordinator())); + return RefreshCallbacks; + }) }; +} + +FSubView& CreateAuthoritySubView(USpatialNetDriver& NetDriver) +{ + const FActorSubviewExtension Extension = GetNetDriverSubviewExtension(NetDriver); + + return NetDriver.Connection->GetCoordinator().CreateSubView( + SpatialConstants::ACTOR_AUTH_TAG_COMPONENT_ID, + Extension.ExtendPredicate(GetRoleFilterPredicate(&AuthoritySubviewSetup::IsAuthorityActorEntity)), + Extension.ExtendCallbacks(AuthoritySubviewSetup::GetCallbacks(NetDriver.Connection->GetCoordinator()))); +} + +bool IsPlayerOwned(Worker_EntityId EntityId, const EntityViewElement& Entity, const FOwnershipCompletenessHandler* OwnershipHandler) +{ + return OwnershipSubviewSetup::IsPlayerOwnedActorEntity(EntityId, Entity) && OwnershipHandler->IsOwnershipComplete(EntityId, Entity); +} + +FSubView& CreatePlayerOwnershipSubView(ViewCoordinator& Coordinator, FOwnershipCompletenessHandler& OwnershipHandler, + const FActorSubviewExtension& Extension) +{ + FSubView& SubView = Coordinator.CreateSubView( + SpatialConstants::ACTOR_TAG_COMPONENT_ID, Extension.ExtendPredicate(GetRoleFilterPredicate(&IsPlayerOwned, &OwnershipHandler)), + Extension.ExtendCallbacks( + CombineCallbacks(FOwnershipCompletenessHandler::GetCallbacks(Coordinator), OwnershipSubviewSetup::GetCallbacks(Coordinator)))); + OwnershipHandler.AddSubView(SubView); + return SubView; +} + +FSubView& CreatePlayerOwnershipSubView(USpatialNetDriver& NetDriver) +{ + return CreatePlayerOwnershipSubView(NetDriver.Connection->GetCoordinator(), NetDriver.OwnershipCompletenessHandler.GetValue(), + GetNetDriverSubviewExtension(NetDriver)); +} + +bool IsSimulatedAndOwnershipComplete(Worker_EntityId EntityId, const EntityViewElement& Entity, + const FOwnershipCompletenessHandler* OwnershipHandler) +{ + return SimulatedSubviewSetup::IsSimulatedActorEntity(EntityId, Entity) && OwnershipHandler->IsOwnershipComplete(EntityId, Entity); +} + +FSubView& CreateSimulatedSubView(ViewCoordinator& Coordinator, FOwnershipCompletenessHandler& OwnershipHandler, + const FActorSubviewExtension& Extension) +{ + FSubView& SubView = + Coordinator.CreateSubView(SpatialConstants::ACTOR_TAG_COMPONENT_ID, + Extension.ExtendPredicate(GetRoleFilterPredicate(&IsSimulatedAndOwnershipComplete, &OwnershipHandler)), + Extension.ExtendCallbacks(CombineCallbacks(FOwnershipCompletenessHandler::GetCallbacks(Coordinator), + SimulatedSubviewSetup::GetCallbacks(Coordinator)))); + OwnershipHandler.AddSubView(SubView); + return SubView; +} + +FSubView& CreateSimulatedSubView(USpatialNetDriver& NetDriver) +{ + return CreateSimulatedSubView(NetDriver.Connection->GetCoordinator(), NetDriver.OwnershipCompletenessHandler.GetValue(), + GetNetDriverSubviewExtension(NetDriver)); +} +} // namespace ActorSubviews + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/ActorSubviews.h b/SpatialGDK/Source/SpatialGDK/Private/Interop/ActorSubviews.h new file mode 100644 index 0000000000..9cd2408610 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/ActorSubviews.h @@ -0,0 +1,61 @@ +#pragma once + +#include "Misc/Optional.h" + +#include "SpatialCommonTypes.h" +#include "SpatialView/SubView.h" + +class USpatialNetDriver; + +namespace SpatialGDK +{ +class ViewCoordinator; +class FOwnershipCompletenessHandler; + +namespace ActorSubviews +{ +using FActorFilterPredicateFactory = TFunction; +using FActorRefreshCallbackPredicateFactory = TFunction(TArray)>; + +struct FActorSubviewExtension +{ + TOptional PredicateFactory; + TOptional RefreshCallbackFactory; + + FFilterPredicate ExtendPredicate(FFilterPredicate Predicate) const + { + if (PredicateFactory) + { + return (*PredicateFactory)(Predicate); + } + return Predicate; + } + + TArray ExtendCallbacks(TArray Callbacks) const + { + if (RefreshCallbackFactory) + { + return (*RefreshCallbackFactory)(Callbacks); + } + return Callbacks; + } +}; + +FSubView& CreateActorSubView(USpatialNetDriver& NetDriver); +FSubView& CreateCustomActorSubView(TOptional MaybeCustomComponentId, TOptional MaybeCustomPredicate, + TOptional> MaybeCustomRefresh, USpatialNetDriver& NetDriver); + +FSubView& CreateActorAuthSubView(USpatialNetDriver& NetDriver); + +FSubView& CreateAuthoritySubView(USpatialNetDriver& NetDriver); + +FSubView& CreatePlayerOwnershipSubView(USpatialNetDriver& NetDriver); +FSubView& CreatePlayerOwnershipSubView(ViewCoordinator& Coordinator, FOwnershipCompletenessHandler& OwnershipHandler, + const FActorSubviewExtension& Extension = {}); + +FSubView& CreateSimulatedSubView(USpatialNetDriver& NetDriver); +FSubView& CreateSimulatedSubView(ViewCoordinator& Coordinator, FOwnershipCompletenessHandler& OwnershipHandler, + const FActorSubviewExtension& Extension = {}); +} // namespace ActorSubviews + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/ActorSystem.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/ActorSystem.cpp index 9d8b26c927..a56c1c6eaa 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/ActorSystem.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/ActorSystem.cpp @@ -28,7 +28,6 @@ DECLARE_CYCLE_STAT(TEXT("Actor System SendComponentUpdates"), STAT_ActorSystemSe DECLARE_CYCLE_STAT(TEXT("Actor System UpdateInterestComponent"), STAT_ActorSystemUpdateInterestComponent, STATGROUP_SpatialNet); DECLARE_CYCLE_STAT(TEXT("Actor System RemoveEntity"), STAT_ActorSystemRemoveEntity, STATGROUP_SpatialNet); DECLARE_CYCLE_STAT(TEXT("Actor System ApplyData"), STAT_ActorSystemApplyData, STATGROUP_SpatialNet); -DECLARE_CYCLE_STAT(TEXT("Actor System ApplyHandover"), STAT_ActorSystemApplyHandover, STATGROUP_SpatialNet); DECLARE_CYCLE_STAT(TEXT("Actor System ReceiveActor"), STAT_ActorSystemReceiveActor, STATGROUP_SpatialNet); DECLARE_CYCLE_STAT(TEXT("Actor System RemoveActor"), STAT_ActorSystemRemoveActor, STATGROUP_SpatialNet); @@ -136,37 +135,18 @@ struct ActorSystem::RepStateUpdateHelper #endif }; -struct FSubViewDelta; - -ActorSystem::ActorSystem(const FSubView& InActorSubView, const FSubView& InTombstoneSubView, USpatialNetDriver* InNetDriver, - SpatialEventTracer* InEventTracer) - : ActorSubView(&InActorSubView) - , TombstoneSubView(&InTombstoneSubView) - , NetDriver(InNetDriver) - , EventTracer(InEventTracer) - , ClaimPartitionHandler(*InNetDriver->Connection) +struct ActorSystem::FEntitySubViewUpdate { -} + const TArray& EntityDeltas; + ENetRole SubViewType; +}; -void ActorSystem::Advance() +void ActorSystem::ProcessUpdates(const FEntitySubViewUpdate& SubViewUpdate) { - for (const EntityDelta& Delta : ActorSubView->GetViewDelta().EntityDeltas) + for (const EntityDelta& Delta : SubViewUpdate.EntityDeltas) { - switch (Delta.Type) - { - case EntityDelta::UPDATE: + if (Delta.Type == EntityDelta::UPDATE) { - // We process authority lost temporarily twice. Once at the start, to lose authority, and again at the end - // to regain it. Why? Because if we temporarily lost authority, we may see surprising updates during the - // tick, such as updates for components we would otherwise think we were authoritative over and ignore them. - for (const AuthorityChange& Change : Delta.AuthorityLostTemporarily) - { - AuthorityLost(Delta.EntityId, Change.ComponentSetId); - } - for (const AuthorityChange& Change : Delta.AuthorityLost) - { - AuthorityLost(Delta.EntityId, Change.ComponentSetId); - } for (const ComponentChange& Change : Delta.ComponentsAdded) { ApplyComponentAdd(Delta.EntityId, Change.ComponentId, Change.Data); @@ -185,35 +165,143 @@ void ActorSystem::Advance() { ComponentRemoved(Delta.EntityId, Change.ComponentId); } - for (const AuthorityChange& Change : Delta.AuthorityGained) + } + } +} + +void ActorSystem::ProcessAdds(const FEntitySubViewUpdate& SubViewUpdate) +{ + for (const EntityDelta& Delta : SubViewUpdate.EntityDeltas) + { + if (Delta.Type == EntityDelta::ADD || Delta.Type == EntityDelta::TEMPORARILY_REMOVED) + { + const Worker_EntityId EntityId = Delta.EntityId; + + if (!PresentEntities.Contains(Delta.EntityId)) { - AuthorityGained(Delta.EntityId, Change.ComponentSetId); + // Create new actor for the entity. + EntityAdded(Delta.EntityId); + + PresentEntities.Emplace(Delta.EntityId); } - for (const AuthorityChange& Change : Delta.AuthorityLostTemporarily) + else { - AuthorityGained(Delta.EntityId, Change.ComponentSetId); + RefreshEntity(Delta.EntityId); + } + + if (SubViewUpdate.SubViewType != ENetRole::ROLE_SimulatedProxy) + { + const Worker_ComponentSetId AuthorityComponentSet = SubViewUpdate.SubViewType == ENetRole::ROLE_Authority + ? SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID + : SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID; + + AuthorityGained(EntityId, AuthorityComponentSet); } - break; } - case EntityDelta::ADD: - PopulateDataStore(Delta.EntityId); - EntityAdded(Delta.EntityId); - break; - case EntityDelta::REMOVE: - EntityRemoved(Delta.EntityId); - ActorDataStore.Remove(Delta.EntityId); - break; - case EntityDelta::TEMPORARILY_REMOVED: + } +} + +void ActorSystem::ProcessRemoves(const FEntitySubViewUpdate& SubViewUpdate) +{ + if (SubViewUpdate.SubViewType == ENetRole::ROLE_SimulatedProxy) + { + return; + } + + for (const EntityDelta& Delta : SubViewUpdate.EntityDeltas) + { + if (Delta.Type == EntityDelta::REMOVE || Delta.Type == EntityDelta::TEMPORARILY_REMOVED) + { + const Worker_EntityId EntityId = Delta.EntityId; + if (PresentEntities.Contains(EntityId)) + { + const Worker_ComponentSetId AuthorityComponentSet = SubViewUpdate.SubViewType == ENetRole::ROLE_Authority + ? SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID + : SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID; + + AuthorityLost(EntityId, AuthorityComponentSet); + } + } + } +} + +ActorSystem::ActorSystem(const FSubView& InActorSubView, const FSubView& InAuthoritySubView, const FSubView& InOwnershipSubView, + const FSubView& InSimulatedSubView, const FSubView& InTombstoneSubView, USpatialNetDriver* InNetDriver, + SpatialEventTracer* InEventTracer) + : ActorSubView(&InActorSubView) + , AuthoritySubView(&InAuthoritySubView) + , OwnershipSubView(&InOwnershipSubView) + , SimulatedSubView(&InSimulatedSubView) + , TombstoneSubView(&InTombstoneSubView) + , NetDriver(InNetDriver) + , EventTracer(InEventTracer) + , ClientNetLoadActorHelper(*InNetDriver) + , ClaimPartitionHandler(*InNetDriver->Connection) +{ +} + +#if DO_CHECK +static void ValidateNoSubviewIntersections(const FSubView& Lhs, const FSubView& Rhs, const FString& SubviewDescription) +{ + TSet LhsEntities, RhsEntities; + Algo::Copy(Lhs.GetCompleteEntities(), LhsEntities); + Algo::Copy(Rhs.GetCompleteEntities(), RhsEntities); + for (const Worker_EntityId Overlapping : LhsEntities.Intersect(RhsEntities)) + { + UE_LOG(LogActorSystem, Warning, TEXT("Entity %lld is doubly complete on %s"), Overlapping, *SubviewDescription); + } +} +#endif // DO_CHECK + +void ActorSystem::Advance() +{ + for (const EntityDelta& Delta : ActorSubView->GetViewDelta().EntityDeltas) + { + if (Delta.Type == EntityDelta::REMOVE) + { EntityRemoved(Delta.EntityId); - ActorDataStore.Remove(Delta.EntityId); - PopulateDataStore(Delta.EntityId); - EntityAdded(Delta.EntityId); - break; - default: - break; + + const int32 EntitiesRemoved = PresentEntities.Remove(Delta.EntityId); } } + struct FEntitySubView + { + const FSubView* SubView; + ENetRole Type; + + operator FEntitySubViewUpdate() const { return { SubView->GetViewDelta().EntityDeltas, Type }; } + }; + +#if DO_CHECK + { + ValidateNoSubviewIntersections(*AuthoritySubView, *OwnershipSubView, TEXT("Authority and Ownership")); + ValidateNoSubviewIntersections(*AuthoritySubView, *SimulatedSubView, TEXT("Authority and Simulated")); + ValidateNoSubviewIntersections(*SimulatedSubView, *OwnershipSubView, TEXT("Simulated and Ownership")); + } +#endif // DO_CHECK + + const FEntitySubView SubViews[]{ + { AuthoritySubView, ENetRole::ROLE_Authority }, + { OwnershipSubView, ENetRole::ROLE_AutonomousProxy }, + { SimulatedSubView, ENetRole::ROLE_SimulatedProxy }, + }; + + for (const FEntitySubView& SubView : SubViews) + { + ProcessRemoves(SubView); + } + + for (const FEntitySubView& SubView : SubViews) + { + ProcessUpdates(SubView); + } + + for (const FEntitySubView& SubView : SubViews) + { + ProcessAdds(SubView); + } + for (const EntityDelta& Delta : TombstoneSubView->GetViewDelta().EntityDeltas) { if (Delta.Type == EntityDelta::ADD || Delta.Type == EntityDelta::TEMPORARILY_REMOVED) @@ -229,7 +317,7 @@ void ActorSystem::Advance() UE_LOG(LogActorSystem, Verbose, TEXT("The received actor with entity ID %lld was tombstoned. The actor will be deleted."), Delta.EntityId); // We must first Resolve the EntityId to the Actor in order for RemoveActor to succeed. - NetDriver->PackageMap->ResolveEntityActor(EntityActor, Delta.EntityId); + NetDriver->PackageMap->ResolveEntityActorAndSubobjects(Delta.EntityId, EntityActor); RemoveActor(Delta.EntityId); } } @@ -581,17 +669,11 @@ void ActorSystem::ComponentUpdated(const Worker_EntityId EntityId, const Worker_ ESchemaComponentType Category = NetDriver->ClassInfoManager->GetCategoryByComponentId(ComponentId); - if (Category == SCHEMA_Data || Category == SCHEMA_OwnerOnly || Category == SCHEMA_InitialOnly) + if (Category != SCHEMA_Invalid) { + ensureAlways(Category != SCHEMA_ServerOnly || NetDriver->IsServer()); SCOPE_CYCLE_COUNTER(STAT_ActorSystemApplyData); - ApplyComponentUpdate(ComponentId, Update, *TargetObject, *Channel, /* bIsHandover */ false); - } - else if (Category == SCHEMA_Handover) - { - SCOPE_CYCLE_COUNTER(STAT_ActorSystemApplyHandover); - check(NetDriver->IsServer()); - - ApplyComponentUpdate(ComponentId, Update, *TargetObject, *Channel, /* bIsHandover */ true); + ApplyComponentUpdate(ComponentId, Update, *TargetObject, *Channel); } else { @@ -612,36 +694,43 @@ void ActorSystem::ComponentRemoved(const Worker_EntityId EntityId, const Worker_ if (AActor* Actor = Cast(NetDriver->PackageMap->GetObjectFromEntityId(EntityId).Get())) { - FUnrealObjectRef ObjectRef(EntityId, ComponentId); + const FUnrealObjectRef ObjectRef(EntityId, ComponentId); if (ComponentId == SpatialConstants::DORMANT_COMPONENT_ID) { GetOrRecreateChannelForDormantActor(Actor, EntityId); } else if (UObject* Object = NetDriver->PackageMap->GetObjectFromUnrealObjectRef(ObjectRef).Get()) { - if (USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(EntityId)) - { - TWeakObjectPtr WeakPtr(Object); - Channel->OnSubobjectDeleted(ObjectRef, Object, WeakPtr); + DestroySubObject(ObjectRef, *Object); + } + } +} - Actor->OnSubobjectDestroyFromReplication(Object); +void ActorSystem::DestroySubObject(const FUnrealObjectRef& ObjectRef, UObject& Object) const +{ + const Worker_EntityId EntityId = ObjectRef.Entity; + if (AActor* Actor = Cast(NetDriver->PackageMap->GetObjectFromEntityId(EntityId).Get())) + { + if (USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(EntityId)) + { + UE_LOG(LogActorSystem, Verbose, TEXT("Destroying subobject with offset %u on entity %d"), ObjectRef.Offset, EntityId); - Object->PreDestroyFromReplication(); - Object->MarkPendingKill(); + Channel->OnSubobjectDeleted(ObjectRef, &Object, TWeakObjectPtr(&Object)); - NetDriver->PackageMap->RemoveSubobject(FUnrealObjectRef(EntityId, ComponentId)); - } + Actor->OnSubobjectDestroyFromReplication(&Object); + + Object.PreDestroyFromReplication(); + Object.MarkPendingKill(); + + NetDriver->PackageMap->RemoveSubobject(ObjectRef); } } } void ActorSystem::EntityAdded(const Worker_EntityId EntityId) { + PopulateDataStore(EntityId); ReceiveActor(EntityId); - for (const auto& AuthoritativeComponentSet : ActorSubView->GetView()[EntityId].Authority) - { - AuthorityGained(EntityId, AuthoritativeComponentSet); - } } void ActorSystem::EntityRemoved(const Worker_EntityId EntityId) @@ -664,6 +753,8 @@ void ActorSystem::EntityRemoved(const Worker_EntityId EntityId) { EntitiesToRetireOnAuthorityGain.RemoveAtSwap(RetiredActorIndex); } + + ActorDataStore.Remove(EntityId); } bool ActorSystem::HasEntityBeenRequestedForDelete(Worker_EntityId EntityId) const @@ -757,7 +848,7 @@ void ActorSystem::HandleIndividualAddComponent(const Worker_EntityId EntityId, c } // Check if this is a static subobject that's been destroyed by the receiver. - if (!IsDynamicSubObject(Actor, Offset)) + if (!IsDynamicSubObject(*NetDriver, *Actor, Offset)) { UE_LOG(LogActorSystem, Verbose, TEXT("Tried to apply component data on add component for a static subobject that's been deleted, will skip. Entity: %lld, " @@ -783,8 +874,9 @@ void ActorSystem::HandleIndividualAddComponent(const Worker_EntityId EntityId, c Worker_ComponentId ComponentFilter[SCHEMA_Count]; ComponentFilter[SCHEMA_Data] = true; ComponentFilter[SCHEMA_OwnerOnly] = bIsServer || bIsAuthClient; - ComponentFilter[SCHEMA_Handover] = bIsServer; + ComponentFilter[SCHEMA_ServerOnly] = bIsServer; ComponentFilter[SCHEMA_InitialOnly] = bInitialOnlyExpected; + static_assert(SCHEMA_Count == 4, "Unexpected number of Schema type components, please check the enclosing function is still correct."); bool bComponentsComplete = true; for (int i = 0; i < SCHEMA_Count; ++i) @@ -827,7 +919,7 @@ void ActorSystem::AttachDynamicSubobject(AActor* Actor, Worker_EntityId EntityId TSet& Components = PendingDynamicSubobjectComponents.FindChecked(EntityId); ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { - Worker_ComponentId ComponentId = Info.SchemaComponents[Type]; + const Worker_ComponentId ComponentId = Info.SchemaComponents[Type]; if (ComponentId == SpatialConstants::INVALID_COMPONENT_ID) { @@ -858,7 +950,7 @@ void ActorSystem::ApplyComponentData(USpatialActorChannel& Channel, UObject& Tar ESchemaComponentType ComponentType = NetDriver->ClassInfoManager->GetCategoryByComponentId(ComponentId); - if (ComponentType == SCHEMA_Data || ComponentType == SCHEMA_OwnerOnly || ComponentType == SCHEMA_InitialOnly) + if (ComponentType != SCHEMA_Invalid) { if (ComponentType == SCHEMA_Data && TargetObject.IsA()) { @@ -873,17 +965,7 @@ void ActorSystem::ApplyComponentData(USpatialActorChannel& Channel, UObject& Tar ComponentReader Reader(NetDriver, RepStateHelper.GetRefMap(), NetDriver->Connection->GetEventTracer()); bool bOutReferencesChanged = false; - Reader.ApplyComponentData(ComponentId, Data, TargetObject, Channel, /* bIsHandover */ false, bOutReferencesChanged); - - RepStateHelper.Update(*this, Channel, bOutReferencesChanged); - } - else if (ComponentType == SCHEMA_Handover) - { - RepStateUpdateHelper RepStateHelper(Channel, TargetObject); - - ComponentReader Reader(NetDriver, RepStateHelper.GetRefMap(), NetDriver->Connection->GetEventTracer()); - bool bOutReferencesChanged = false; - Reader.ApplyComponentData(ComponentId, Data, TargetObject, Channel, /* bIsHandover */ true, bOutReferencesChanged); + Reader.ApplyComponentData(ComponentId, Data, TargetObject, Channel, bOutReferencesChanged); RepStateHelper.Update(*this, Channel, bOutReferencesChanged); } @@ -894,12 +976,6 @@ void ActorSystem::ApplyComponentData(USpatialActorChannel& Channel, UObject& Tar } } -bool ActorSystem::IsDynamicSubObject(AActor* Actor, uint32 SubObjectOffset) -{ - const FClassInfo& ActorClassInfo = NetDriver->ClassInfoManager->GetOrCreateClassInfoByClass(Actor->GetClass()); - return !ActorClassInfo.SubobjectInfo.Contains(SubObjectOffset); -} - void ActorSystem::ResolvePendingOperations(UObject* Object, const FUnrealObjectRef& ObjectRef) { UE_LOG(LogActorSystem, Verbose, TEXT("Resolving pending object refs and RPCs which depend on object: %s %s."), *Object->GetName(), @@ -1036,9 +1112,7 @@ void ActorSystem::ResolveObjectReferences(FRepLayout& RepLayout, UObject* Replic continue; } - // ParentIndex is -1 for handover properties - bool bIsHandover = ObjectReferences.ParentIndex == -1; - FRepParentCmd* Parent = ObjectReferences.ParentIndex >= 0 ? &RepLayout.Parents[ObjectReferences.ParentIndex] : nullptr; + const FRepParentCmd& Parent = RepLayout.Parents[ObjectReferences.ParentIndex]; int32 StoredDataOffset = ObjectReferences.ShadowOffset; @@ -1047,19 +1121,15 @@ void ActorSystem::ResolveObjectReferences(FRepLayout& RepLayout, UObject* Replic GDK_PROPERTY(ArrayProperty)* ArrayProperty = GDK_CASTFIELD(Property); check(ArrayProperty != nullptr); - if (!bIsHandover) - { - Property->CopySingleValue(StoredData + StoredDataOffset, Data + AbsOffset); - } + Property->CopySingleValue(StoredData + StoredDataOffset, Data + AbsOffset); - FScriptArray* StoredArray = bIsHandover ? nullptr : (FScriptArray*)(StoredData + StoredDataOffset); + FScriptArray* StoredArray = (FScriptArray*)(StoredData + StoredDataOffset); FScriptArray* Array = (FScriptArray*)(Data + AbsOffset); int32 NewMaxOffset = Array->Num() * ArrayProperty->Inner->ElementSize; - ResolveObjectReferences(RepLayout, ReplicatedObject, RepState, *ObjectReferences.Array, - bIsHandover ? nullptr : (uint8*)StoredArray->GetData(), (uint8*)Array->GetData(), NewMaxOffset, - RepNotifies, bOutSomeObjectsWereMapped); + ResolveObjectReferences(RepLayout, ReplicatedObject, RepState, *ObjectReferences.Array, (uint8*)StoredArray->GetData(), + (uint8*)Array->GetData(), NewMaxOffset, RepNotifies, bOutSomeObjectsWereMapped); continue; } @@ -1101,7 +1171,7 @@ void ActorSystem::ResolveObjectReferences(FRepLayout& RepLayout, UObject* Replic bOutSomeObjectsWereMapped = true; } - if (Parent && Parent->Property->HasAnyPropertyFlags(CPF_RepNotify)) + if (Parent.Property->HasAnyPropertyFlags(CPF_RepNotify)) { Property->CopySingleValue(StoredData + StoredDataOffset, Data + AbsOffset); } @@ -1124,8 +1194,8 @@ void ActorSystem::ResolveObjectReferences(FRepLayout& RepLayout, UObject* Replic check(Property->IsA()); UScriptStruct* NetDeltaStruct = GetFastArraySerializerProperty(GDK_CASTFIELD(Property)); - FSpatialNetDeltaSerializeInfo::DeltaSerializeRead(NetDriver, ValueDataReader, ReplicatedObject, Parent->ArrayIndex, - Parent->Property, NetDeltaStruct); + FSpatialNetDeltaSerializeInfo::DeltaSerializeRead(NetDriver, ValueDataReader, ReplicatedObject, Parent.ArrayIndex, + Parent.Property, NetDeltaStruct); ObjectReferences.MappedRefs.Append(NewMappedRefs); } @@ -1144,11 +1214,11 @@ void ActorSystem::ResolveObjectReferences(FRepLayout& RepLayout, UObject* Replic ObjectReferences.MappedRefs.Append(NewMappedRefs); } - if (Parent && Parent->Property->HasAnyPropertyFlags(CPF_RepNotify)) + if (Parent.Property->HasAnyPropertyFlags(CPF_RepNotify)) { - if (Parent->RepNotifyCondition == REPNOTIFY_Always || !Property->Identical(StoredData + StoredDataOffset, Data + AbsOffset)) + if (Parent.RepNotifyCondition == REPNOTIFY_Always || !Property->Identical(StoredData + StoredDataOffset, Data + AbsOffset)) { - RepNotifies.AddUnique(Parent->Property); + RepNotifies.AddUnique(Parent.Property); } } } @@ -1173,13 +1243,13 @@ USpatialActorChannel* ActorSystem::GetOrRecreateChannelForDormantActor(AActor* A } void ActorSystem::ApplyComponentUpdate(const Worker_ComponentId ComponentId, Schema_ComponentUpdate* ComponentUpdate, UObject& TargetObject, - USpatialActorChannel& Channel, bool bIsHandover) + USpatialActorChannel& Channel) { RepStateUpdateHelper RepStateHelper(Channel, TargetObject); ComponentReader Reader(NetDriver, RepStateHelper.GetRefMap(), NetDriver->Connection->GetEventTracer()); bool bOutReferencesChanged = false; - Reader.ApplyComponentUpdate(ComponentId, ComponentUpdate, TargetObject, Channel, bIsHandover, bOutReferencesChanged); + Reader.ApplyComponentUpdate(ComponentId, ComponentUpdate, TargetObject, Channel, bOutReferencesChanged); RepStateHelper.Update(*this, Channel, bOutReferencesChanged); // This is a temporary workaround, see UNR-841: @@ -1205,8 +1275,6 @@ void ActorSystem::ReceiveActor(Worker_EntityId EntityId) ActorData& ActorComponents = ActorDataStore[EntityId]; - const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); - AActor* EntityActor = Cast(NetDriver->PackageMap->GetObjectFromEntityId(EntityId)); if (EntityActor != nullptr) { @@ -1234,7 +1302,7 @@ void ActorSystem::ReceiveActor(Worker_EntityId EntityId) } // Make sure ClassInfo exists - const FClassInfo& ActorClassInfo = NetDriver->ClassInfoManager->GetOrCreateClassInfoByClass(Class); + NetDriver->ClassInfoManager->GetOrCreateClassInfoByClass(Class); // If the received actor is torn off, don't bother spawning it. // (This is only needed due to the delay between tearoff and deleting the entity. See https://improbableio.atlassian.net/browse/UNR-841) @@ -1255,7 +1323,7 @@ void ActorSystem::ReceiveActor(Worker_EntityId EntityId) return; } - if (!NetDriver->PackageMap->ResolveEntityActor(EntityActor, EntityId)) + if (!NetDriver->PackageMap->ResolveEntityActorAndSubobjects(EntityId, EntityActor)) { UE_LOG(LogActorSystem, Warning, TEXT("Failed to resolve entity actor when receiving entity. Actor will not be spawned. Entity: %lld, actor: %s"), EntityId, @@ -1274,19 +1342,52 @@ void ActorSystem::ReceiveActor(Worker_EntityId EntityId) return; } + ApplyFullState(EntityId, *Channel, *EntityActor); + + const UNetConnection* ActorNetConnection = EntityActor->GetNetConnection(); + if (IsValid(ActorNetConnection) && NetDriver->ServerConnection == ActorNetConnection) + { + if (ensureMsgf(NetDriver->OwnershipCompletenessHandler, + TEXT("OwnershipCompletenessHandler must be valid throughout ActorSystem's lifetime"))) + { + NetDriver->OwnershipCompletenessHandler->AddPlayerEntity(EntityId); + } + } +} + +void ActorSystem::RefreshEntity(const Worker_EntityId EntityId) +{ + AActor* EntityActor = Cast(NetDriver->PackageMap->GetObjectFromEntityId(EntityId)); + + checkf(IsValid(EntityActor), TEXT("RefreshEntity must have an actor for entity %lld"), EntityId); + + checkf(NetDriver, TEXT("We should have a NetDriver whilst processing ops.")); + checkf(NetDriver->GetWorld(), TEXT("We should have a World whilst processing ops.")); + + USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(EntityId); + check(IsValid(Channel)); + check(Channel->Actor == EntityActor); + + ApplyFullState(EntityId, *Channel, *EntityActor); +} + +void ActorSystem::ApplyFullState(const Worker_EntityId EntityId, USpatialActorChannel& EntityActorChannel, AActor& EntityActor) +{ TArray ObjectsToResolvePendingOpsFor; + const TArray& EntityComponents = ActorSubView->GetView()[EntityId].Components; + // Apply initial replicated properties. // This was moved to after FinishingSpawning because components existing only in blueprints aren't added until spawning is complete // Potentially we could split out the initial actor state and the initial component state - for (const ComponentData& Component : ActorSubView->GetView()[EntityId].Components) + for (const ComponentData& Component : EntityComponents) { if (NetDriver->ClassInfoManager->IsGeneratedQBIMarkerComponent(Component.GetComponentId()) || Component.GetComponentId() < SpatialConstants::STARTING_GENERATED_COMPONENT_ID) { continue; } - ApplyComponentDataOnActorCreation(EntityId, Component.GetComponentId(), Component.GetUnderlying(), *Channel, + ApplyComponentDataOnActorCreation(EntityId, Component.GetComponentId(), Component.GetUnderlying(), EntityActorChannel, ObjectsToResolvePendingOpsFor); } @@ -1296,12 +1397,18 @@ void ActorSystem::ReceiveActor(Worker_EntityId EntityId) { for (const ComponentData& Component : *InitialOnlyComponents) { - ApplyComponentDataOnActorCreation(EntityId, Component.GetComponentId(), Component.GetUnderlying(), *Channel, + ApplyComponentDataOnActorCreation(EntityId, Component.GetComponentId(), Component.GetUnderlying(), EntityActorChannel, ObjectsToResolvePendingOpsFor); } } } + if (EntityActor.IsFullNameStableForNetworking()) + { + // bNetLoadOnClient actors could have components removed while out of the client's interest + ClientNetLoadActorHelper.RemoveRuntimeRemovedComponents(EntityId, EntityComponents, EntityActor); + } + // Resolve things like RepNotify or RPCs after applying component data. for (const ObjectPtrRefPair& ObjectToResolve : ObjectsToResolvePendingOpsFor) { @@ -1314,34 +1421,34 @@ void ActorSystem::ReceiveActor(Worker_EntityId EntityId) // This is a bit of a hack unfortunately, among the core classes only PlayerController implements this function and it requires // a player index. For now we don't support split screen, so the number is always 0. - if (EntityActor->IsA(APlayerController::StaticClass())) + if (EntityActor.IsA(APlayerController::StaticClass())) { uint8 PlayerIndex = 0; // FInBunch takes size in bits not bytes FInBunch Bunch(NetDriver->ServerConnection, &PlayerIndex, sizeof(PlayerIndex) * 8); - EntityActor->OnActorChannelOpen(Bunch, NetDriver->ServerConnection); + EntityActor.OnActorChannelOpen(Bunch, NetDriver->ServerConnection); } else { FInBunch Bunch(NetDriver->ServerConnection); - EntityActor->OnActorChannelOpen(Bunch, NetDriver->ServerConnection); + EntityActor.OnActorChannelOpen(Bunch, NetDriver->ServerConnection); } } // Any Actor created here will have been received over the wire as an entity so we can mark it ready. - EntityActor->SetActorReady(NetDriver->IsServer() && EntityActor->bNetStartup); + EntityActor.SetActorReady(NetDriver->IsServer() && EntityActor.bNetStartup); // Taken from PostNetInit - if (NetDriver->GetWorld()->HasBegunPlay() && !EntityActor->HasActorBegunPlay()) + if (NetDriver->GetWorld()->HasBegunPlay() && !EntityActor.HasActorBegunPlay()) { - EntityActor->DispatchBeginPlay(); + EntityActor.DispatchBeginPlay(); } - EntityActor->UpdateOverlaps(); + EntityActor.UpdateOverlaps(); if (ActorSubView->HasComponent(EntityId, SpatialConstants::DORMANT_COMPONENT_ID)) { - NetDriver->AddPendingDormantChannel(Channel); + NetDriver->AddPendingDormantChannel(&EntityActorChannel); } } @@ -1496,7 +1603,7 @@ void ActorSystem::ApplyComponentDataOnActorCreation(const Worker_EntityId Entity TWeakObjectPtr TargetObject = NetDriver->PackageMap->GetObjectFromUnrealObjectRef(TargetObjectRef); if (!TargetObject.IsValid()) { - if (!IsDynamicSubObject(Actor, Offset)) + if (!IsDynamicSubObject(*NetDriver, *Actor, Offset)) { UE_LOG(LogActorSystem, Verbose, TEXT("Tried to apply component data on actor creation for a static subobject that's been deleted, will skip. Entity: " @@ -1506,16 +1613,11 @@ void ActorSystem::ApplyComponentDataOnActorCreation(const Worker_EntityId Entity } // If we can't find this subobject, it's a dynamically attached object. Check if we created previously. - if (FNetworkGUID* SubobjectNetGUID = NetDriver->PackageMap->GetRemovedDynamicSubobjectNetGUID(TargetObjectRef)) + if (UObject* DynamicSubObject = ClientNetLoadActorHelper.GetReusableDynamicSubObject(TargetObjectRef)) { - if (UObject* DynamicSubobject = NetDriver->PackageMap->GetObjectFromNetGUID(*SubobjectNetGUID, false)) - { - NetDriver->PackageMap->ResolveSubobject(DynamicSubobject, TargetObjectRef); - ApplyComponentData(Channel, *DynamicSubobject, ComponentId, Data); - - OutObjectsToResolve.Add(ObjectPtrRefPair(DynamicSubobject, TargetObjectRef)); - return; - } + ApplyComponentData(Channel, *DynamicSubObject, ComponentId, Data); + OutObjectsToResolve.Add(ObjectPtrRefPair(DynamicSubObject, TargetObjectRef)); + return; } // If the dynamically attached object was not created before. Create it now. @@ -1572,7 +1674,7 @@ USpatialActorChannel* ActorSystem::SetUpActorChannel(AActor* Actor, const Worker USpatialActorChannel* ActorSystem::TryRestoreActorChannelForStablyNamedActor(AActor* StablyNamedActor, const Worker_EntityId EntityId) { - if (!NetDriver->PackageMap->ResolveEntityActor(StablyNamedActor, EntityId)) + if (!NetDriver->PackageMap->ResolveEntityActorAndSubobjects(EntityId, StablyNamedActor)) { UE_LOG(LogActorSystem, Warning, TEXT("Failed to restore actor channel for stably named actor: failed to resolve actor. Entity: %lld, actor: %s"), EntityId, @@ -1597,6 +1699,12 @@ void ActorSystem::RemoveActor(const Worker_EntityId EntityId) TWeakObjectPtr WeakActor = NetDriver->PackageMap->GetObjectFromEntityId(EntityId); + if (ensureMsgf(NetDriver->OwnershipCompletenessHandler, + TEXT("OwnershipCompletenessHandler must be valid throughout ActorSystem's lifetime"))) + { + NetDriver->OwnershipCompletenessHandler->TryRemovePlayerEntity(EntityId); + } + // Actor has not been resolved yet or has already been destroyed. Clean up surrounding bookkeeping. if (!WeakActor.IsValid()) { @@ -1669,27 +1777,11 @@ void ActorSystem::RemoveActor(const Worker_EntityId EntityId) } } - // Actor is a startup actor that is a part of the level. If it's not Tombstone'd, then it + // Actor is a startup actor that is a part of the level. If it's not Tombstone-d, then it // has just fallen out of our view and we should only remove the entity. if (Actor->IsFullNameStableForNetworking() && !ActorSubView->HasComponent(EntityId, SpatialConstants::TOMBSTONE_COMPONENT_ID)) { - NetDriver->PackageMap->ClearRemovedDynamicSubobjectObjectRefs(EntityId); - if (USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(EntityId)) - { - for (UObject* DynamicSubobject : Channel->CreateSubObjects) - { - FNetworkGUID SubobjectNetGUID = NetDriver->PackageMap->GetNetGUIDFromObject(DynamicSubobject); - if (SubobjectNetGUID.IsValid()) - { - FUnrealObjectRef SubobjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromNetGUID(SubobjectNetGUID); - - if (SubobjectRef.IsValid() && IsDynamicSubObject(Actor, SubobjectRef.Offset)) - { - NetDriver->PackageMap->AddRemovedDynamicSubobjectObjectRef(SubobjectRef, SubobjectNetGUID); - } - } - } - } + ClientNetLoadActorHelper.EntityRemoved(EntityId, *Actor); // We can't call CleanupDeletedEntity here as we need the NetDriver to maintain the EntityId // to Actor Channel mapping for the DestroyActor to function correctly NetDriver->PackageMap->RemoveEntityActor(EntityId); @@ -1806,19 +1898,26 @@ void ActorSystem::RetireEntity(Worker_EntityId EntityId, bool bIsNetStartupActor } void ActorSystem::SendComponentUpdates(UObject* Object, const FClassInfo& Info, USpatialActorChannel* Channel, - const FRepChangeState* RepChanges, const FHandoverChangeState* HandoverChanges, - uint32& OutBytesWritten) + const FRepChangeState* RepChanges, uint32& OutBytesWritten) { SCOPE_CYCLE_COUNTER(STAT_ActorSystemSendComponentUpdates); const Worker_EntityId EntityId = Channel->GetEntityId(); + // It's not clear if this is ever valid for authority to not be true anymore (since component sets), but still possible if we attempt + // to process updates whilst an entity creation is in progress, or after the entity has been deleted or removed from view. So in the + // meantime we've kept the checking with an error message. + if (!NetDriver->HasServerAuthority(EntityId)) + { + UE_LOG(LogActorSystem, Error, TEXT("Trying to send component update but don't have authority! entity: %lld"), EntityId); + return; + } + UE_LOG(LogActorSystem, Verbose, TEXT("Sending component update (object: %s, entity: %lld)"), *Object->GetName(), EntityId); - USpatialLatencyTracer* Tracer = USpatialLatencyTracer::GetTracer(Object); - ComponentFactory UpdateFactory(Channel->GetInterestDirty(), NetDriver, Tracer); + ComponentFactory UpdateFactory(Channel->GetInterestDirty(), NetDriver); TArray ComponentUpdates = - UpdateFactory.CreateComponentUpdates(Object, Info, EntityId, RepChanges, HandoverChanges, OutBytesWritten); + UpdateFactory.CreateComponentUpdates(Object, Info, EntityId, RepChanges, OutBytesWritten); TArray PropertySpans; if (EventTracer != nullptr && RepChanges != nullptr @@ -1846,15 +1945,6 @@ void ActorSystem::SendComponentUpdates(UObject* Object, const FClassInfo& Info, } } - // It's not clear if this is ever valid for authority to not be true anymore (since component sets), but still possible if we attempt - // to process updates whilst an entity creation is in progress, or after the entity has been deleted or removed from view. So in the - // meantime we've kept the checking and queuing of updates, along with an error message. - if (!NetDriver->HasServerAuthority(EntityId)) - { - UE_LOG(LogActorSystem, Error, TEXT("Trying to send component update but don't have authority! entity: %lld"), EntityId); - return; - } - for (int i = 0; i < ComponentUpdates.Num(); i++) { FWorkerComponentUpdate& Update = ComponentUpdates[i]; @@ -1942,12 +2032,11 @@ void ActorSystem::SendAddComponentForSubobject(USpatialActorChannel* Channel, UO uint32& OutBytesWritten) { FRepChangeState SubobjectRepChanges = Channel->CreateInitialRepChangeState(Subobject); - FHandoverChangeState SubobjectHandoverChanges = Channel->CreateInitialHandoverChangeState(SubobjectInfo); - ComponentFactory DataFactory(false, NetDriver, USpatialLatencyTracer::GetTracer(Subobject)); + ComponentFactory DataFactory(false, NetDriver); TArray SubobjectDatas = - DataFactory.CreateComponentDatas(Subobject, SubobjectInfo, SubobjectRepChanges, SubobjectHandoverChanges, OutBytesWritten); + DataFactory.CreateComponentDatas(Subobject, SubobjectInfo, SubobjectRepChanges, OutBytesWritten); SendAddComponents(Channel->GetEntityId(), SubobjectDatas); Channel->PendingDynamicSubobjects.Remove(TWeakObjectPtr(Subobject)); @@ -2285,7 +2374,11 @@ void ActorSystem::DeleteEntityComponentData(TArray& Entity void ActorSystem::AddTombstoneToEntity(Worker_EntityId EntityId) const { - check(ActorSubView->HasAuthority(EntityId, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID)); + if (!ensureAlwaysMsgf(ActorSubView->HasAuthority(EntityId, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID), + TEXT("Trying to add tombstone to entity without authority"))) + { + return; + } FWorkerComponentData TombstoneData = Tombstone().CreateComponentData(); NetDriver->Connection->SendAddComponent(EntityId, &TombstoneData); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/ClientNetLoadActorHelper.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/ClientNetLoadActorHelper.cpp new file mode 100644 index 0000000000..164775a4f7 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/ClientNetLoadActorHelper.cpp @@ -0,0 +1,186 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Interop/ClientNetLoadActorHelper.h" + +#include "EngineClasses/SpatialNetDriver.h" +#include "Interop/ActorSystem.h" +#include "Interop/SpatialReceiver.h" +#include "Interop/SpatialSender.h" +#include "Schema/Restricted.h" +#include "SpatialConstants.h" +#include "Utils/RepLayoutUtils.h" +#include "Utils/SpatialActorUtils.h" + +DEFINE_LOG_CATEGORY(LogClientNetLoadActorHelper); + +namespace SpatialGDK +{ +FClientNetLoadActorHelper::FClientNetLoadActorHelper(USpatialNetDriver& InNetDriver) +{ + NetDriver = &InNetDriver; +} + +UObject* FClientNetLoadActorHelper::GetReusableDynamicSubObject(const FUnrealObjectRef ObjectRef) +{ + if (FNetworkGUID* SubObjectNetGUID = GetSavedDynamicSubObjectNetGUID(ObjectRef)) + { + if (UObject* DynamicSubObject = NetDriver->PackageMap->GetObjectFromNetGUID(*SubObjectNetGUID, /* bIgnoreMustBeMapped */ false)) + { + NetDriver->PackageMap->ResolveSubobject(DynamicSubObject, ObjectRef); + UE_LOG(LogClientNetLoadActorHelper, Verbose, + TEXT("Found reusable dynamic SubObject (ObjectRef offset: %u) for ClientNetLoad actor with entityId %d"), + ObjectRef.Offset, ObjectRef.Entity); + return DynamicSubObject; + } + } + return nullptr; +} + +void FClientNetLoadActorHelper::EntityRemoved(const Worker_EntityId EntityId, const AActor& Actor) +{ + ClearDynamicSubobjectMetadata(EntityId); + SaveDynamicSubobjectsMetadata(EntityId, Actor); +} + +void FClientNetLoadActorHelper::SaveDynamicSubobjectsMetadata(const Worker_EntityId EntityId, const AActor& Actor) +{ + if (USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(EntityId)) + { + for (UObject* DynamicSubObject : Channel->CreateSubObjects) + { + FNetworkGUID SubObjectNetGUID = NetDriver->PackageMap->GetNetGUIDFromObject(DynamicSubObject); + if (SubObjectNetGUID.IsValid()) + { + FUnrealObjectRef SubObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromNetGUID(SubObjectNetGUID); + if (SubObjectRef.IsValid() && IsDynamicSubObject(*NetDriver, Actor, SubObjectRef.Offset)) + { + SaveDynamicSubobjectMetadata(SubObjectRef, SubObjectNetGUID); + UE_LOG( + LogClientNetLoadActorHelper, Verbose, + TEXT("Saved reusable dynamic SubObject ObjectRef (ObjectRef offset: %u) for ClientNetLoad actor with entityId %d"), + SubObjectRef.Offset, SubObjectRef.Entity); + } + } + } + } +} + +FNetworkGUID* FClientNetLoadActorHelper::GetSavedDynamicSubObjectNetGUID(const FUnrealObjectRef& ObjectRef) +{ + if (TMap* SubobjectOffsetToNetGuid = SpatialEntityRemovedSubobjectMetadata.Find(ObjectRef.Entity)) + { + if (FNetworkGUID* NetGUID = SubobjectOffsetToNetGuid->Find(ObjectRef.Offset)) + { + return NetGUID; + } + } + return nullptr; +} + +void FClientNetLoadActorHelper::SaveDynamicSubobjectMetadata(const FUnrealObjectRef& ObjectRef, const FNetworkGUID& NetGUID) +{ + TMap& SubobjectOffsetToNetGuid = SpatialEntityRemovedSubobjectMetadata.FindOrAdd(ObjectRef.Entity); + SubobjectOffsetToNetGuid.Emplace(ObjectRef.Offset, NetGUID); +} + +void FClientNetLoadActorHelper::ClearDynamicSubobjectMetadata(const Worker_EntityId InEntityId) +{ + SpatialEntityRemovedSubobjectMetadata.Remove(InEntityId); +} + +void FClientNetLoadActorHelper::RemoveRuntimeRemovedComponents(const Worker_EntityId EntityId, const TArray& NewComponents, + AActor& EntityActor) +{ + RemoveDynamicComponentsRemovedByRuntime(EntityId, NewComponents); + RemoveStaticComponentsRemovedByRuntime(EntityId, NewComponents, EntityActor); +} + +void FClientNetLoadActorHelper::RemoveDynamicComponentsRemovedByRuntime(const Worker_EntityId EntityId, + const TArray& NewComponents) +{ + if (TMap* SubobjectOffsetToNetGuid = SpatialEntityRemovedSubobjectMetadata.Find(EntityId)) + { + // Go over each stored sub-object and determine whether it is contained within the new components array + // If it is not contained within the new components array, it means the sub-object was removed while out of the client's interest + // If so, remove it now + for (auto OffsetToNetGuidIterator = SubobjectOffsetToNetGuid->CreateIterator(); OffsetToNetGuidIterator; ++OffsetToNetGuidIterator) + { + const ObjectOffset ObjectOffset = OffsetToNetGuidIterator->Key; + if (!SubobjectWithOffsetStillExists(NewComponents, ObjectOffset)) + { + if (UObject* Object = + NetDriver->PackageMap->GetObjectFromNetGUID(OffsetToNetGuidIterator->Value, false /* bIgnoreMustBeMapped */)) + { + const FUnrealObjectRef EntityObjectRef(EntityId, ObjectOffset); + SubobjectRemovedByRuntime(EntityObjectRef, *Object); + } + OffsetToNetGuidIterator.RemoveCurrent(); + } + } + } +} + +void FClientNetLoadActorHelper::RemoveStaticComponentsRemovedByRuntime(const Worker_EntityId EntityId, + const TArray& NewComponents, AActor& EntityActor) +{ + const FClassInfo& ActorInfo = NetDriver->ClassInfoManager->GetOrCreateClassInfoByClass(EntityActor.GetClass()); + FSubobjectToOffsetMap SubobjectsToOffsets = CreateStaticOffsetMapFromActor(EntityActor, ActorInfo); + + for (auto& SubobjectToOffset : SubobjectsToOffsets) + { + UObject& Subobject = *SubobjectToOffset.Key; + const ObjectOffset Offset = SubobjectToOffset.Value; + if (SubobjectIsReplicated(Subobject, EntityId) && !SubobjectWithOffsetStillExists(NewComponents, Offset)) + { + const FUnrealObjectRef ObjectRef(EntityId, Offset); + SubobjectRemovedByRuntime(ObjectRef, Subobject); + } + } +} + +void FClientNetLoadActorHelper::SubobjectRemovedByRuntime(const FUnrealObjectRef& EntityObjectRef, UObject& Subobject) +{ + UE_LOG(LogClientNetLoadActorHelper, Verbose, + TEXT("A SubObject (ObjectRef offset: %u) on bNetLoadOnClient actor with entityId %lld was destroyed while the " + "actor was out of the client's interest. Destroying the SubObject now."), + EntityObjectRef.Offset, EntityObjectRef.Entity); + NetDriver->ActorSystem.Get()->DestroySubObject(EntityObjectRef, Subobject); +} + +bool FClientNetLoadActorHelper::SubobjectWithOffsetStillExists(const TArray& Components, + const ObjectOffset OffsetToCheckIfContained) const +{ + for (const ComponentData& Component : Components) + { + // Skip if this isn't a generated component + if (Component.GetComponentId() < SpatialConstants::STARTING_GENERATED_COMPONENT_ID) + { + continue; + } + + ObjectOffset NewComponentOffset = 0; + NetDriver->ClassInfoManager->GetOffsetByComponentId(Component.GetComponentId(), NewComponentOffset); + + if (NewComponentOffset == OffsetToCheckIfContained) + { + return true; + } + } + return false; +} + +bool FClientNetLoadActorHelper::SubobjectIsReplicated(const UObject& Object, const Worker_EntityId EntityId) const +{ + if (const USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(EntityId)) + { + const TSharedRef* ReplicatorRefPtr = Channel->ReplicationMap.Find(&Object); + // Condition taken from private method UActorChannel::ObjectHasReplicator + const bool bIsReplicated = ReplicatorRefPtr != nullptr && &Object == ReplicatorRefPtr->Get().GetObject(); + // NOTE: In theory, this could lead to a static subobject being unintentionally deleted on the client if the server sets it to not + // replicate while it is out of the client’s interest. See https://improbableio.atlassian.net/browse/UNR-5609 for more. + return bIsReplicated; + } + return false; +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp index 380f01f483..93034e3370 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp @@ -117,12 +117,18 @@ struct ConfigureConnection Params.network.kcp.upstream_kcp.flush_interval_millis = Config.UdpUpstreamIntervalMS; Params.network.kcp.downstream_kcp.flush_interval_millis = Config.UdpDownstreamIntervalMS; + const USpatialGDKSettings* Settings = GetDefault(); #if WITH_EDITOR + float TimeoutSeconds = Settings->HeartbeatTimeoutWithEditorSeconds; +#else + float TimeoutSeconds = Settings->HeartbeatTimeoutSeconds; +#endif + HeartbeatParams = { static_cast(Settings->HeartbeatIntervalSeconds * 1000), + static_cast(TimeoutSeconds * 1000) }; Params.network.tcp.downstream_heartbeat = &HeartbeatParams; Params.network.tcp.upstream_heartbeat = &HeartbeatParams; Params.network.kcp.downstream_heartbeat = &HeartbeatParams; Params.network.kcp.upstream_heartbeat = &HeartbeatParams; -#endif // Use insecure connections default. Params.network.kcp.security_type = WORKER_NETWORK_SECURITY_TYPE_INSECURE; @@ -174,10 +180,7 @@ struct ConfigureConnection Worker_LogsinkParameters Logsink{}; Worker_NameVersionPair UnrealGDKVersionPair{}; Worker_FlowControlParameters WorkerFowControlParameters{}; - -#if WITH_EDITOR - Worker_HeartbeatParameters HeartbeatParams{ WORKER_DEFAULTS_HEARTBEAT_INTERVAL_MILLIS, MAX_int64 }; -#endif + Worker_HeartbeatParameters HeartbeatParams{}; }; void USpatialConnectionManager::FinishDestroy() diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialEventTracer.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialEventTracer.cpp index 5e5ee83dd8..8cef84624f 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialEventTracer.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialEventTracer.cpp @@ -10,25 +10,6 @@ DEFINE_LOG_CATEGORY(LogSpatialEventTracer); namespace SpatialGDK { -TraceQueryPtr ParseOrDefault(const FString& Str, const TCHAR* FilterForLog) -{ - TraceQueryPtr Ptr; - if (Str.Len() > 0) - { - Ptr.Reset(Trace_ParseSimpleQuery(TCHAR_TO_ANSI(*Str))); - UE_LOG(LogSpatialEventTracer, Log, TEXT("Applied %s query: %s"), FilterForLog, *Str); - } - - if (!Ptr.IsValid()) - { - UE_LOG(LogSpatialEventTracer, Warning, TEXT("The specified query \"%s\" is invalid; defaulting to \"false\" query. %s"), - FilterForLog, Trace_GetLastError()); - Ptr.Reset(Trace_ParseSimpleQuery("false")); - } - - return Ptr; -} - void SpatialEventTracer::TraceCallback(void* UserData, const Trace_Item* Item) { SpatialEventTracer* EventTracer = static_cast(UserData); @@ -44,7 +25,7 @@ void SpatialEventTracer::TraceCallback(void* UserData, const Trace_Item* Item) const bool bTrackFileSize = EventTracer->MaxFileSize != 0; if (!bTrackFileSize || (EventTracer->BytesWrittenToStream + ItemSize <= EventTracer->MaxFileSize)) { - if (bTrackFileSize) + if (bTrackFileSize) { EventTracer->BytesWrittenToStream += ItemSize; } @@ -130,8 +111,8 @@ SpatialEventTracer::SpatialEventTracer(const FString& WorkerId) Parameters.span_sampling_parameters.probabilistic_parameters.probabilities = SpanSamplingProbabilities.GetData(); // Filters - TraceQueryPtr PreFilter = ParseOrDefault(SamplingSettings->GDKEventPreFilter, TEXT("pre-filter")); - TraceQueryPtr PostFilter = ParseOrDefault(SamplingSettings->GDKEventPostFilter, TEXT("post-filter")); + UEventTracingSamplingSettings::TraceQueryPtr PreFilter = SamplingSettings->GetGDKEventPreFilter(); + UEventTracingSamplingSettings::TraceQueryPtr PostFilter = SamplingSettings->GetGDKEventPostFilter(); checkf(PreFilter.Get() != nullptr, TEXT("Pre-filter is invalid.")); checkf(PostFilter.Get() != nullptr, TEXT("Post-filter is invalid.")); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp index f5f1e6887a..46dfd5305f 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp @@ -60,8 +60,11 @@ void ServerWorkerEntityCreator::CreateWorkerEntity() if (Settings->CrossServerRPCImplementation == ECrossServerRPCImplementation::RoutingWorker) { - Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID)); - Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID)); + Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID)); + Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID)); + Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID)); + Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID)); + Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::ROUTINGWORKER_TAG_COMPONENT_ID)); DelegationMap.Add(SpatialConstants::ROUTING_WORKER_AUTH_COMPONENT_SET_ID, SpatialConstants::INITIAL_ROUTING_PARTITION_ENTITY_ID); } Components.Add(AuthorityDelegation(DelegationMap).CreateComponentData()); @@ -131,13 +134,13 @@ void USpatialWorkerConnection::FinishDestroy() const TArray& USpatialWorkerConnection::GetEntityDeltas() { check(Coordinator.IsValid()); - return Coordinator->GetViewDelta().GetEntityDeltas(); + return Coordinator->GetEntityDeltas(); } const TArray& USpatialWorkerConnection::GetWorkerMessages() { check(Coordinator.IsValid()); - return Coordinator->GetViewDelta().GetWorkerMessages(); + return Coordinator->GetWorkerMessages(); } void USpatialWorkerConnection::DestroyConnection() @@ -349,6 +352,11 @@ void USpatialWorkerConnection::SetStartupComplete() StartupComplete = true; } +SpatialGDK::ISpatialOSWorker* USpatialWorkerConnection::GetSpatialWorkerInterface() const +{ + return Coordinator.Get(); +} + void USpatialWorkerConnection::CreateServerWorkerEntity() { if (ensure(!WorkerEntityCreator.IsSet())) diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/CrossServerRPCSender.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/CrossServerRPCSender.cpp index 3d5ff8b794..1ae3cd60dc 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/CrossServerRPCSender.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/CrossServerRPCSender.cpp @@ -50,7 +50,7 @@ void CrossServerRPCSender::SendCommand(const FUnrealObjectRef InTargetObjectRef, } else { - Coordinator->SendEntityCommandRequest(InTargetObjectRef.Entity, MoveTemp(CommandRequest), TOptional(), SpanId); + Coordinator->SendEntityCommandRequest(InTargetObjectRef.Entity, MoveTemp(CommandRequest), NO_RETRIES, SpanId); } #if !UE_BUILD_SHIPPING diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp index 7e0f1d965c..f995efe5f9 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp @@ -413,7 +413,7 @@ void UGlobalStateManager::HandleActorBasedOnLoadBalancer(AActor* Actor) const return; } - if (USpatialStatics::IsSpatialOffloadingEnabled(GetWorld()) && !USpatialStatics::IsActorGroupOwnerForActor(Actor) + if (USpatialStatics::IsSpatialOffloadingEnabled(Actor->GetWorld()) && !USpatialStatics::IsActorGroupOwnerForActor(Actor) && !Actor->bNetLoadOnNonAuthServer) { Actor->Destroy(true); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/OwnershipCompletenessHandler.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/OwnershipCompletenessHandler.cpp new file mode 100644 index 0000000000..c8f357f88e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/OwnershipCompletenessHandler.cpp @@ -0,0 +1,76 @@ +#include "Interop/OwnershipCompletenessHandler.h" + +#include "Schema/ActorOwnership.h" +#include "SpatialView/EntityComponentTypes.h" +#include "SpatialView/SubView.h" +#include "SpatialView/ViewCoordinator.h" + +namespace SpatialGDK +{ +bool FOwnershipCompletenessHandler::IsOwnershipComplete(Worker_EntityId EntityId, const EntityViewElement& Entity) const +{ + const bool bShouldHaveOwnerOnlyComponents = ShouldHaveOwnerOnlyComponents(EntityId, Entity); + + const bool bHasOwnerOnlyComponents = + Entity.Components.ContainsByPredicate(ComponentIdEquality{ SpatialConstants::ACTOR_OWNER_ONLY_DATA_TAG_COMPONENT_ID }); + + return bShouldHaveOwnerOnlyComponents == bHasOwnerOnlyComponents; +} + +bool FOwnershipCompletenessHandler::ShouldHaveOwnerOnlyComponents(Worker_EntityId EntityId, const EntityViewElement& Entity) const +{ + switch (Strategy) + { + case EOwnershipCompletenessStrategy::AlwaysHasOwnerComponents: + return true; + case EOwnershipCompletenessStrategy::RequiresPlayerOwnership: + { + const ComponentData* OwnershipData = Entity.Components.FindByPredicate(ComponentIdEquality{ ActorOwnership::ComponentId }); + if (!ensureMsgf(OwnershipData != nullptr, TEXT("Entity should have ActorOwnership, EntityId: %lld component ID %ld"), EntityId, + ActorOwnership::ComponentId)) + { + return false; + } + const ActorOwnership Value(*OwnershipData); + const bool bIsPlayerOwned = Value.OwnerActorEntityId != SpatialConstants::INVALID_ENTITY_ID + && (Value.OwnerActorEntityId == EntityId || PlayerOwnedEntities.Contains(EntityId) + || PlayerOwnedEntities.Contains(Value.OwnerActorEntityId)); + return bIsPlayerOwned; + } + default: + checkNoEntry(); + } + return false; +} + +void FOwnershipCompletenessHandler::AddPlayerEntity(Worker_EntityId EntityId) +{ + PlayerOwnedEntities.Add(EntityId); + + for (FSubView* SubViewToRefresh : SubViewsToRefresh) + { + SubViewToRefresh->Refresh(); + } +} + +void FOwnershipCompletenessHandler::TryRemovePlayerEntity(Worker_EntityId EntityId) +{ + PlayerOwnedEntities.Remove(EntityId); + + for (FSubView* SubViewToRefresh : SubViewsToRefresh) + { + SubViewToRefresh->Refresh(); + } +} + +void FOwnershipCompletenessHandler::AddSubView(FSubView& InSubView) +{ + SubViewsToRefresh.Emplace(&InSubView); +} + +TArray FOwnershipCompletenessHandler::GetCallbacks(ViewCoordinator& Coordinator) +{ + return { Coordinator.CreateComponentChangedRefreshCallback(ActorOwnership::ComponentId), + Coordinator.CreateComponentExistenceRefreshCallback(SpatialConstants::ACTOR_OWNER_ONLY_DATA_TAG_COMPONENT_ID) }; +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCExecutor.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCExecutor.cpp index 004fa8f608..06725691ba 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCExecutor.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCExecutor.cpp @@ -113,22 +113,15 @@ TOptional RPCExecutor::TryRetrieveCrossServerRPCParams(co } AActor* TargetActor = Cast(NetDriver->PackageMap->GetObjectFromEntityId(Op.op.command_request.entity_id)); -#if TRACE_LIB_ACTIVE - TraceKey TraceId = Payload.Trace; -#else - TraceKey TraceId = InvalidTraceKey; -#endif - FSpatialGDKSpanId SpanId; if (EventTracer != nullptr) { SpanId = EventTracer->TraceEvent(RECEIVE_COMMAND_REQUEST_EVENT_NAME, "", /* Causes */ nullptr, /* NumCauses */ 0, - [TargetActor, TargetObject, Function, TraceId, Op](FSpatialTraceEventDataBuilder& EventBuilder) { + [TargetActor, TargetObject, Function, Op](FSpatialTraceEventDataBuilder& EventBuilder) { EventBuilder.AddCommand("RPC_COMMAND_REQUEST"); EventBuilder.AddObject(TargetActor); EventBuilder.AddObject(TargetActor != TargetObject ? TargetObject : nullptr, "target_object"); EventBuilder.AddFunction(Function); - EventBuilder.AddKeyValue("trace_id", TraceId); EventBuilder.AddRequestId(Op.op.command_request.request_id); }); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/ClientServerRPCService.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/ClientServerRPCService.cpp index c2d0be5a34..06bd12e481 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/ClientServerRPCService.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/ClientServerRPCService.cpp @@ -124,14 +124,6 @@ uint64 ClientServerRPCService::GetAckFromView(const Worker_EntityId EntityId, co { switch (Type) { - case ERPCType::ClientReliable: - return ClientServerDataStore[EntityId].Client.ReliableRPCAck; - case ERPCType::ClientUnreliable: - return ClientServerDataStore[EntityId].Client.UnreliableRPCAck; - case ERPCType::ServerReliable: - return ClientServerDataStore[EntityId].Server.ReliableRPCAck; - case ERPCType::ServerUnreliable: - return ClientServerDataStore[EntityId].Server.UnreliableRPCAck; case ERPCType::ServerAlwaysWrite: return ClientServerDataStore[EntityId].Server.AlwaysWriteRPCAck; default: @@ -201,26 +193,14 @@ void ClientServerRPCService::OnEndpointAuthorityGained(const Worker_EntityId Ent case SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID: { const ClientEndpoint& Endpoint = ClientServerDataStore[EntityId].Client; - LastSeenRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientReliable), Endpoint.ReliableRPCAck); - LastSeenRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientUnreliable), Endpoint.UnreliableRPCAck); - LastAckedRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientReliable), Endpoint.ReliableRPCAck); - LastAckedRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientUnreliable), Endpoint.UnreliableRPCAck); - RPCStore->LastSentRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerReliable), Endpoint.ReliableRPCBuffer.LastSentRPCId); - RPCStore->LastSentRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerUnreliable), Endpoint.UnreliableRPCBuffer.LastSentRPCId); RPCStore->LastSentRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerAlwaysWrite), Endpoint.AlwaysWriteRPCBuffer.LastSentRPCId); break; } case SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID: { const ServerEndpoint& Endpoint = ClientServerDataStore[EntityId].Server; - LastSeenRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerReliable), Endpoint.ReliableRPCAck); - LastSeenRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerUnreliable), Endpoint.UnreliableRPCAck); LastSeenRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerAlwaysWrite), Endpoint.AlwaysWriteRPCAck); - LastAckedRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerReliable), Endpoint.ReliableRPCAck); - LastAckedRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerUnreliable), Endpoint.UnreliableRPCAck); LastAckedRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerAlwaysWrite), Endpoint.AlwaysWriteRPCAck); - RPCStore->LastSentRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientReliable), Endpoint.ReliableRPCBuffer.LastSentRPCId); - RPCStore->LastSentRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientUnreliable), Endpoint.UnreliableRPCBuffer.LastSentRPCId); break; } default: @@ -235,26 +215,13 @@ void ClientServerRPCService::OnEndpointAuthorityLost(const Worker_EntityId Entit { case SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID: { - LastSeenRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ClientReliable)); - LastSeenRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ClientUnreliable)); - LastAckedRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ClientReliable)); - LastAckedRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ClientUnreliable)); - RPCStore->LastSentRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerReliable)); - RPCStore->LastSentRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerUnreliable)); RPCStore->LastSentRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerAlwaysWrite)); ClearOverflowedRPCs(EntityId); break; } case SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID: { - LastSeenRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerReliable)); - LastSeenRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerUnreliable)); - LastSeenRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerAlwaysWrite)); - LastAckedRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerReliable)); - LastAckedRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerUnreliable)); LastAckedRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerAlwaysWrite)); - RPCStore->LastSentRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ClientReliable)); - RPCStore->LastSentRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ClientUnreliable)); ClearOverflowedRPCs(EntityId); break; } @@ -294,13 +261,9 @@ void ClientServerRPCService::ExtractRPCsForEntity(const Worker_EntityId EntityId switch (ComponentId) { case SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID: - ExtractRPCsForType(EntityId, ERPCType::ServerReliable); - ExtractRPCsForType(EntityId, ERPCType::ServerUnreliable); ExtractRPCsForType(EntityId, ERPCType::ServerAlwaysWrite); break; case SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID: - ExtractRPCsForType(EntityId, ERPCType::ClientReliable); - ExtractRPCsForType(EntityId, ERPCType::ClientUnreliable); break; default: checkNoEntry(); @@ -379,17 +342,6 @@ const RPCRingBuffer& ClientServerRPCService::GetBufferFromView(const Worker_Enti { switch (Type) { - // Server sends Client RPCs, so ClientReliable & ClientUnreliable buffers live on ServerEndpoint. - case ERPCType::ClientReliable: - return ClientServerDataStore[EntityId].Server.ReliableRPCBuffer; - case ERPCType::ClientUnreliable: - return ClientServerDataStore[EntityId].Server.UnreliableRPCBuffer; - - // Client sends Server RPCs, so ServerReliable & ServerUnreliable buffers live on ClientEndpoint. - case ERPCType::ServerReliable: - return ClientServerDataStore[EntityId].Client.ReliableRPCBuffer; - case ERPCType::ServerUnreliable: - return ClientServerDataStore[EntityId].Client.UnreliableRPCBuffer; case ERPCType::ServerAlwaysWrite: return ClientServerDataStore[EntityId].Client.AlwaysWriteRPCBuffer; default: diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/CrossServerRPCService.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/CrossServerRPCService.cpp index 387b4e8099..508a84c24e 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/CrossServerRPCService.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/CrossServerRPCService.cpp @@ -12,12 +12,13 @@ DEFINE_LOG_CATEGORY(LogCrossServerRPCService); namespace SpatialGDK { CrossServerRPCService::CrossServerRPCService(const ActorCanExtractRPCDelegate InCanExtractRPCDelegate, - const ExtractRPCDelegate InExtractRPCCallback, const FSubView& InSubView, - FRPCStore& InRPCStore) + const ExtractRPCDelegate InExtractRPCCallback, const FSubView& InActorSubView, + const FSubView& InWorkerEntitySubView, FRPCStore& InRPCStore) : CanExtractRPCDelegate(InCanExtractRPCDelegate) , ExtractRPCCallback(InExtractRPCCallback) - , SubView(&InSubView) - , RPCStore(&InRPCStore) + , ActorSubView(InActorSubView) + , WorkerEntitySubView(InWorkerEntitySubView) + , RPCStore(InRPCStore) { } @@ -26,7 +27,7 @@ EPushRPCResult CrossServerRPCService::PushCrossServerRPC(Worker_EntityId EntityI { CrossServerEndpoints* Endpoints = CrossServerDataStore.Find(Sender.Entity); Schema_Object* EndpointObject = nullptr; - EntityComponentId SenderEndpointId(Sender.Entity, SpatialConstants::CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID); + EntityComponentId SenderEndpointId(Sender.Entity, SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID); if (!Endpoints) { @@ -35,12 +36,12 @@ EPushRPCResult CrossServerRPCService::PushCrossServerRPC(Worker_EntityId EntityI return EPushRPCResult::EntityBeingCreated; } - EndpointObject = Schema_GetComponentDataFields(RPCStore->GetOrCreateComponentData(SenderEndpointId)); + EndpointObject = Schema_GetComponentDataFields(RPCStore.GetOrCreateComponentData(SenderEndpointId)); Endpoints = &CrossServerDataStore.Add(Sender.Entity); } else { - EndpointObject = Schema_GetComponentUpdateFields(RPCStore->GetOrCreateComponentUpdate(SenderEndpointId)); + EndpointObject = Schema_GetComponentUpdateFields(RPCStore.GetOrCreateComponentUpdate(SenderEndpointId)); } CrossServer::WriterState& SenderState = Endpoints->SenderState; @@ -77,73 +78,90 @@ EPushRPCResult CrossServerRPCService::PushCrossServerRPC(Worker_EntityId EntityI void CrossServerRPCService::AdvanceView() { - const FSubViewDelta& SubViewDelta = SubView->GetViewDelta(); - for (const EntityDelta& Delta : SubViewDelta.EntityDeltas) + const FSubViewDelta* SubViewDeltas[] = { &ActorSubView.GetViewDelta(), &WorkerEntitySubView.GetViewDelta() }; + for (auto SubViewDelta : SubViewDeltas) { - switch (Delta.Type) + for (const EntityDelta& Delta : SubViewDelta->EntityDeltas) { - case EntityDelta::UPDATE: + AdvanceViewForEntityDelta(Delta); + } + } +} + +void CrossServerRPCService::AdvanceViewForEntityDelta(const EntityDelta& Delta) +{ + switch (Delta.Type) + { + case EntityDelta::UPDATE: + { + for (const ComponentChange& Change : Delta.ComponentUpdates) { - for (const ComponentChange& Change : Delta.ComponentUpdates) - { - ComponentUpdate(Delta.EntityId, Change.ComponentId, Change.Update); - } - break; + ComponentUpdate(Delta.EntityId, Change.ComponentId, Change.Update); } - case EntityDelta::ADD: + break; + } + case EntityDelta::ADD: + PopulateDataStore(Delta.EntityId); + break; + case EntityDelta::REMOVE: + case EntityDelta::TEMPORARILY_REMOVED: + CrossServerDataStore.Remove(Delta.EntityId); + RPCStore.PendingComponentUpdatesToSend.Remove( + EntityComponentId(Delta.EntityId, SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID)); + RPCStore.PendingComponentUpdatesToSend.Remove( + EntityComponentId(Delta.EntityId, SpatialConstants::CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID)); + if (Delta.Type == EntityDelta::TEMPORARILY_REMOVED) + { PopulateDataStore(Delta.EntityId); - break; - case EntityDelta::REMOVE: - case EntityDelta::TEMPORARILY_REMOVED: - CrossServerDataStore.Remove(Delta.EntityId); - RPCStore->PendingComponentUpdatesToSend.Remove( - EntityComponentId(Delta.EntityId, SpatialConstants::CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID)); - RPCStore->PendingComponentUpdatesToSend.Remove( - EntityComponentId(Delta.EntityId, SpatialConstants::CROSSSERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID)); - if (Delta.Type == EntityDelta::TEMPORARILY_REMOVED) - { - PopulateDataStore(Delta.EntityId); - } - break; - default: - break; } + break; + default: + checkNoEntry(); + break; } } void CrossServerRPCService::ProcessChanges() { - const FSubViewDelta& SubViewDelta = SubView->GetViewDelta(); - for (const EntityDelta& Delta : SubViewDelta.EntityDeltas) + const FSubViewDelta* SubViewDeltas[] = { &ActorSubView.GetViewDelta(), &WorkerEntitySubView.GetViewDelta() }; + for (auto SubViewDelta : SubViewDeltas) { - switch (Delta.Type) - { - case EntityDelta::UPDATE: + for (const EntityDelta& Delta : SubViewDelta->EntityDeltas) { - for (const ComponentChange& Change : Delta.ComponentUpdates) - { - ProcessComponentChange(Delta.EntityId, Change.ComponentId); - } - break; + ProcessChangesForEntityDelta(Delta); } - case EntityDelta::ADD: - EntityAdded(Delta.EntityId); - break; - case EntityDelta::REMOVE: + } +} - break; - case EntityDelta::TEMPORARILY_REMOVED: - EntityAdded(Delta.EntityId); - break; - default: - break; +void CrossServerRPCService::ProcessChangesForEntityDelta(const EntityDelta& Delta) +{ + switch (Delta.Type) + { + case EntityDelta::UPDATE: + { + for (const ComponentChange& Change : Delta.ComponentUpdates) + { + ProcessComponentChange(Delta.EntityId, Change.ComponentId); } + break; + } + case EntityDelta::ADD: + EntityAdded(Delta.EntityId); + break; + case EntityDelta::REMOVE: + + break; + case EntityDelta::TEMPORARILY_REMOVED: + EntityAdded(Delta.EntityId); + break; + default: + break; } } void CrossServerRPCService::EntityAdded(const Worker_EntityId EntityId) { - for (const ComponentData& Component : SubView->GetView()[EntityId].Components) + for (const ComponentData& Component : ActorSubView.GetView()[EntityId].Components) { if (!IsCrossServerEndpoint(Component.GetComponentId())) { @@ -168,11 +186,11 @@ void CrossServerRPCService::ComponentUpdate(const Worker_EntityId EntityId, cons { switch (ComponentId) { - case SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID: + case SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID: Endpoints->ReceivedRPCs->ApplyComponentUpdate(Update); break; - case SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID: + case SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID: Endpoints->ACKedRPCs->ApplyComponentUpdate(Update); break; default: @@ -192,11 +210,11 @@ void CrossServerRPCService::ProcessComponentChange(const Worker_EntityId EntityI { switch (ComponentId) { - case SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID: + case SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID: HandleRPC(EntityId, Endpoints->ReceivedRPCs.GetValue()); break; - case SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID: + case SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID: UpdateSentRPCsACKs(EntityId, Endpoints->ACKedRPCs.GetValue()); break; default: @@ -207,13 +225,14 @@ void CrossServerRPCService::ProcessComponentChange(const Worker_EntityId EntityI void CrossServerRPCService::PopulateDataStore(const Worker_EntityId EntityId) { - const EntityViewElement& Entity = SubView->GetView()[EntityId]; + const EntityViewElement& Entity = ActorSubView.GetView()[EntityId]; Schema_ComponentData* SenderACKData = - Entity.Components.FindByPredicate(ComponentIdEquality{ SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID }) + Entity.Components.FindByPredicate(ComponentIdEquality{ SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID }) ->GetUnderlying(); + Schema_ComponentData* ReceiverData = - Entity.Components.FindByPredicate(ComponentIdEquality{ SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID }) + Entity.Components.FindByPredicate(ComponentIdEquality{ SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID }) ->GetUnderlying(); CrossServerEndpoints& NewEntry = CrossServerDataStore.FindOrAdd(EntityId); @@ -225,7 +244,7 @@ void CrossServerRPCService::OnEndpointAuthorityGained(const Worker_EntityId Enti { switch (Component.GetComponentId()) { - case SpatialConstants::CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID: + case SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID: { CrossServerEndpoint SenderEndpoint(Component.GetUnderlying()); CrossServer::WriterState& SenderState = CrossServerDataStore.FindChecked(EntityId).SenderState; @@ -250,7 +269,7 @@ void CrossServerRPCService::OnEndpointAuthorityGained(const Worker_EntityId Enti } break; } - case SpatialConstants::CROSSSERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID: + case SpatialConstants::CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID: { CrossServerEndpointACK ReceiverACKEndpoint(Component.GetUnderlying()); CrossServer::ReaderState& ReceiverACKState = CrossServerDataStore.FindChecked(EntityId).ReceiverACKState; @@ -278,7 +297,7 @@ void CrossServerRPCService::OnEndpointAuthorityGained(const Worker_EntityId Enti void CrossServerRPCService::HandleRPC(const Worker_EntityId EntityId, const CrossServerEndpoint& Receiver) { - if (SubView->HasAuthority(EntityId, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID)) + if (ActorSubView.HasAuthority(EntityId, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID)) { if (!CanExtractRPCDelegate.Execute(EntityId)) { @@ -290,10 +309,10 @@ void CrossServerRPCService::HandleRPC(const Worker_EntityId EntityId, const Cros bool CrossServerRPCService::IsCrossServerEndpoint(const Worker_ComponentId ComponentId) { - return ComponentId == SpatialConstants::CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID - || ComponentId == SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID - || ComponentId == SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID - || ComponentId == SpatialConstants::CROSSSERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID; + return ComponentId == SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID + || ComponentId == SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID + || ComponentId == SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID + || ComponentId == SpatialConstants::CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID; } void CrossServerRPCService::ExtractCrossServerRPCs(Worker_EntityId EndpointId, const CrossServerEndpoint& Receiver) @@ -353,9 +372,9 @@ void CrossServerRPCService::WriteCrossServerACKFor(Worker_EntityId Receiver, con ACK.Sender = Sender.Entity; ACK.Result = static_cast(CrossServer::Result::Success); - EntityComponentId Pair(Receiver, SpatialConstants::CROSSSERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID); + EntityComponentId Pair(Receiver, SpatialConstants::CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID); - Schema_ComponentUpdate* Update = RPCStore->GetOrCreateComponentUpdate(Pair); + Schema_ComponentUpdate* Update = RPCStore.GetOrCreateComponentUpdate(Pair); Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update); Schema_Object* NewEntry = Schema_AddObject(UpdateObject, 1 + SlotIdx); @@ -382,8 +401,8 @@ void CrossServerRPCService::UpdateSentRPCsACKs(Worker_EntityId SenderId, const C SenderState.Alloc.FreeSlot(SentRPC->SourceSlot); SenderState.Mailbox.Remove(RPCKey); - EntityComponentId Pair(ACK.Sender, SpatialConstants::CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID); - RPCStore->GetOrCreateComponentUpdate(Pair); + EntityComponentId Pair(ACK.Sender, SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID); + RPCStore.GetOrCreateComponentUpdate(Pair); } } } @@ -426,14 +445,14 @@ void CrossServerRPCService::CleanupACKsFor(Worker_EntityId EndpointId, const Cro } } - EntityComponentId Pair(EndpointId, SpatialConstants::CROSSSERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID); + EntityComponentId Pair(EndpointId, SpatialConstants::CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID); for (auto const& SlotToClear : ACKSToClear) { uint32 SlotIdx = SlotToClear.Value.ACKSlot; State.RPCSlots.Remove(SlotToClear.Key); - RPCStore->GetOrCreateComponentUpdate(Pair); + RPCStore.GetOrCreateComponentUpdate(Pair); State.ACKAlloc.FreeSlot(SlotIdx); } @@ -442,7 +461,7 @@ void CrossServerRPCService::CleanupACKsFor(Worker_EntityId EndpointId, const Cro void CrossServerRPCService::FlushPendingClearedFields(TPair& UpdateToSend) { - if (UpdateToSend.Key.ComponentId == SpatialConstants::CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID) + if (UpdateToSend.Key.ComponentId == SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID) { CrossServer::WriterState& SenderState = CrossServerDataStore.FindChecked(UpdateToSend.Key.EntityId).SenderState; RPCRingBufferDescriptor Descriptor = RPCRingBufferUtils::GetRingBufferDescriptor(ERPCType::CrossServer); @@ -455,7 +474,7 @@ void CrossServerRPCService::FlushPendingClearedFields(TPair SpatialConstants::MULTICAST_RPCS_COMPONENT_ID) + { + break; + } + } for (const AuthorityChange& Change : Delta.AuthorityGained) { AuthorityGained(Delta.EntityId, Change.ComponentSetId); @@ -91,7 +102,11 @@ void MulticastRPCService::ProcessChanges() { for (const ComponentChange& Change : Delta.ComponentUpdates) { - ComponentUpdate(Delta.EntityId, Change.ComponentId, Change.Update); + ComponentUpdate(Delta.EntityId, Change.ComponentId); + } + for (const ComponentChange& Change : Delta.ComponentsRefreshed) + { + ComponentUpdate(Delta.EntityId, Change.ComponentId); } break; } @@ -99,7 +114,7 @@ void MulticastRPCService::ProcessChanges() EntityAdded(Delta.EntityId); break; case EntityDelta::TEMPORARILY_REMOVED: - EntityAdded(Delta.EntityId); + EntityRefresh(Delta.EntityId); break; default: break; @@ -116,8 +131,22 @@ void MulticastRPCService::EntityAdded(const Worker_EntityId EntityId) } } -void MulticastRPCService::ComponentUpdate(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId, - Schema_ComponentUpdate* Update) +void MulticastRPCService::EntityRefresh(Worker_EntityId EntityId) +{ + if (SubView->HasAuthority(EntityId, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID)) + { + const MulticastRPCs& Component = MulticastDataStore[EntityId]; + + // Update last seen and last sent ids to latest component data + LastSeenMulticastRPCIds.Add(EntityId, Component.MulticastRPCBuffer.LastSentRPCId); + RPCStore->LastSentRPCIds.Add(EntityRPCType(EntityId, ERPCType::NetMulticast), Component.MulticastRPCBuffer.LastSentRPCId); + } + + // If this is a non-auth refresh, process any new RPC updates. This is a no-op for the auth worker. + ExtractRPCs(EntityId); +} + +void MulticastRPCService::ComponentUpdate(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) { if (ComponentId != SpatialConstants::MULTICAST_RPCS_COMPONENT_ID) { @@ -126,22 +155,22 @@ void MulticastRPCService::ComponentUpdate(const Worker_EntityId EntityId, const ExtractRPCs(EntityId); } -void MulticastRPCService::AuthorityGained(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +void MulticastRPCService::AuthorityGained(const Worker_EntityId EntityId, const Worker_ComponentSetId ComponentSetId) { - if (ComponentId != SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) + if (ComponentSetId != SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) { return; } - OnEndpointAuthorityGained(EntityId, ComponentId); + OnEndpointAuthorityGained(EntityId, ComponentSetId); } -void MulticastRPCService::AuthorityLost(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +void MulticastRPCService::AuthorityLost(const Worker_EntityId EntityId, const Worker_ComponentSetId ComponentSetId) { - if (ComponentId != SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) + if (ComponentSetId != SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) { return; } - OnEndpointAuthorityLost(EntityId, ComponentId); + OnEndpointAuthorityLost(EntityId, ComponentSetId); } void MulticastRPCService::PopulateDataStore(const Worker_EntityId EntityId) @@ -170,7 +199,7 @@ void MulticastRPCService::OnRemoveMulticastRPCComponentForEntity(const Worker_En LastSeenMulticastRPCIds.Remove(EntityId); } -void MulticastRPCService::OnEndpointAuthorityGained(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +void MulticastRPCService::OnEndpointAuthorityGained(const Worker_EntityId EntityId, const Worker_ComponentSetId ComponentSetId) { const MulticastRPCs& Component = MulticastDataStore[EntityId]; @@ -191,7 +220,7 @@ void MulticastRPCService::OnEndpointAuthorityGained(const Worker_EntityId Entity } } -void MulticastRPCService::OnEndpointAuthorityLost(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +void MulticastRPCService::OnEndpointAuthorityLost(const Worker_EntityId EntityId, const Worker_ComponentSetId ComponentSetId) { // Set last seen to last sent, so we don't process own RPCs after crossing the boundary. LastSeenMulticastRPCIds.Add(EntityId, RPCStore->LastSentRPCIds[EntityRPCType(EntityId, ERPCType::NetMulticast)]); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/SpatialRPCService.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/SpatialRPCService.cpp index 5734faf198..3a3001e9cb 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/SpatialRPCService.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/SpatialRPCService.cpp @@ -6,12 +6,16 @@ #include "EngineClasses/SpatialNetBitReader.h" #include "EngineClasses/SpatialNetConnection.h" #include "EngineClasses/SpatialPackageMapClient.h" +#include "Interop/Connection/SpatialEventTracer.h" #include "Interop/Connection/SpatialWorkerConnection.h" -#include "Net/NetworkProfiler.h" #include "SpatialConstants.h" +#include "Utils/ObjectAllocUtils.h" #include "Utils/RepLayoutUtils.h" #include "Utils/SpatialLatencyTracer.h" +#include "Algo/AnyOf.h" +#include "Net/NetworkProfiler.h" + DEFINE_LOG_CATEGORY(LogSpatialRPCService); DECLARE_CYCLE_STAT(TEXT("SpatialRPCService SendRPC"), STAT_SpatialRPCServiceSendRPC, STATGROUP_SpatialNet); @@ -19,8 +23,8 @@ DECLARE_CYCLE_STAT(TEXT("SpatialRPCService SendRPC"), STAT_SpatialRPCServiceSend namespace SpatialGDK { SpatialRPCService::SpatialRPCService(const FSubView& InActorAuthSubView, const FSubView& InActorNonAuthSubView, - USpatialLatencyTracer* InSpatialLatencyTracer, SpatialEventTracer* InEventTracer, - USpatialNetDriver* InNetDriver) + const FSubView& InWorkerEntitySubView, USpatialLatencyTracer* InSpatialLatencyTracer, + SpatialEventTracer* InEventTracer, USpatialNetDriver* InNetDriver) : NetDriver(InNetDriver) , SpatialLatencyTracer(InSpatialLatencyTracer) , EventTracer(InEventTracer) @@ -38,7 +42,7 @@ SpatialRPCService::SpatialRPCService(const FSubView& InActorAuthSubView, const F { CrossServerRPCs.Emplace(CrossServerRPCService(ActorCanExtractRPCDelegate::CreateRaw(this, &SpatialRPCService::ActorCanExtractRPC), ExtractRPCDelegate::CreateRaw(this, &SpatialRPCService::ProcessOrQueueIncomingRPC), - InActorAuthSubView, RPCStore)); + InActorAuthSubView, InWorkerEntitySubView, RPCStore)); } IncomingRPCs.BindProcessingFunction(FProcessRPCDelegate::CreateRaw(this, &SpatialRPCService::ApplyRPC)); OutgoingRPCs.BindProcessingFunction(FProcessRPCDelegate::CreateRaw(this, &SpatialRPCService::SendRPC)); @@ -115,10 +119,6 @@ EPushRPCResult SpatialRPCService::PushRPC(const Worker_EntityId EntityId, const EPushRPCResult Result = EPushRPCResult::Success; PendingRPCPayload PendingPayload = { Payload, SpanId }; -#if TRACE_LIB_ACTIVE - TraceKey Trace = Payload.Trace; -#endif - if (Type == ERPCType::CrossServer) { Result = CrossServerRPCs->PushCrossServerRPC(EntityId, Sender, PendingPayload, bCreatedEntity); @@ -147,10 +147,6 @@ EPushRPCResult SpatialRPCService::PushRPC(const Worker_EntityId EntityId, const } } -#if TRACE_LIB_ACTIVE - ProcessResultToLatencyTrace(Result, Trace); -#endif - return Result; } @@ -207,9 +203,6 @@ void SpatialRPCService::PushOverflowedRPCs() default: checkNoEntry(); } -#if TRACE_LIB_ACTIVE - ProcessResultToLatencyTrace(Result, Payload.Payload.Trace); -#endif // This includes the valid case of RPCs still overflowing (EPushRPCResult::QueueOverflowed), as well as the error cases. if (Result != EPushRPCResult::Success) @@ -254,12 +247,6 @@ TArray SpatialRPCService::GetRPCsAndAcksToSend( EventBuilder.AddComponentId(UpdateToSend.Update.component_id); }); } - -#if TRACE_LIB_ACTIVE - TraceKey Trace = InvalidTraceKey; - PendingTraces.RemoveAndCopyValue(It.Key, Trace); - UpdateToSend.Update.Trace = Trace; -#endif } RPCStore.PendingComponentUpdatesToSend.Empty(); @@ -269,10 +256,8 @@ TArray SpatialRPCService::GetRPCsAndAcksToSend( TArray SpatialRPCService::GetRPCComponentsOnEntityCreation(const Worker_EntityId EntityId) { - static TArray EndpointComponentIds = { SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID, - SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID, - SpatialConstants::MULTICAST_RPCS_COMPONENT_ID, - SpatialConstants::CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID }; + static TArray EndpointComponentIds = { SpatialConstants::MULTICAST_RPCS_COMPONENT_ID, + SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID }; TArray Components; @@ -302,11 +287,6 @@ TArray SpatialRPCService::GetRPCComponentsOnEntityCreation } Component.schema_type = *ComponentData; -#if TRACE_LIB_ACTIVE - TraceKey Trace = InvalidTraceKey; - PendingTraces.RemoveAndCopyValue(EntityComponent, Trace); - Component.Trace = Trace; -#endif RPCStore.PendingRPCsOnEntityCreation.Remove(EntityComponent); } else @@ -384,12 +364,7 @@ RPCPayload SpatialRPCService::CreateRPCPayloadFromParams(UObject* TargetObject, Id = FMath::RandRange(static_cast(0), INT64_MAX); } -#if TRACE_LIB_ACTIVE - return RPCPayload(TargetObjectRef.Offset, RPCInfo.Index, Id, TArray(PayloadWriter.GetData(), PayloadWriter.GetNumBytes()), - USpatialLatencyTracer::GetTracer(TargetObject)->RetrievePendingTrace(TargetObject, Function)); -#else return RPCPayload(TargetObjectRef.Offset, RPCInfo.Index, Id, TArray(PayloadWriter.GetData(), PayloadWriter.GetNumBytes())); -#endif } EPushRPCResult SpatialRPCService::PushRPCInternal(const Worker_EntityId EntityId, const ERPCType Type, PendingRPCPayload Payload, @@ -462,22 +437,6 @@ EPushRPCResult SpatialRPCService::PushRPCInternal(const Worker_EntityId EntityId } RPCRingBufferUtils::WriteRPCToSchema(EndpointObject, Type, NewRPCId, Payload.Payload); - -#if TRACE_LIB_ACTIVE - if (SpatialLatencyTracer != nullptr && Payload.Payload.Trace != InvalidTraceKey) - { - if (PendingTraces.Find(EntityComponent) == nullptr) - { - PendingTraces.Add(EntityComponent, Payload.Payload.Trace); - } - else - { - SpatialLatencyTracer->WriteAndEndTrace(Payload.Payload.Trace, - TEXT("Multiple rpc updates in single update, ending further stack tracing"), true); - } - } -#endif - RPCStore.LastSentRPCIds.Add(EntityType, NewRPCId); } else @@ -538,8 +497,43 @@ FRPCErrorInfo SpatialRPCService::ApplyRPC(const FPendingRPCParams& Params) return ApplyRPCInternal(TargetObject, Function, Params); } +namespace SpatialRPCServicePrivate +{ +/** + * When receiving a NetWriteFence, it will look like we are trying to + * make the initial call again, without the sender/dependent. + * So we push a new entry in the NetDriver's stack of sender/dependent to indicate + * that it is coming from the network and is actually the resolution of a previous call made earlier. + */ +struct NetWriteFenceResolutionHandler : FStackOnly +{ + NetWriteFenceResolutionHandler(USpatialNetDriver& InNetDriver, UFunction& Function) + : NetDriver(InNetDriver) + , bIsNetWriteFence(Function.HasAnyFunctionFlags(FUNC_NetWriteFence)) + { + if (bIsNetWriteFence) + { + NetDriver.PushNetWriteFenceResolution(); + } + } + + ~NetWriteFenceResolutionHandler() + { + if (bIsNetWriteFence) + { + NetDriver.PopNetWriteFenceResolution(); + } + } + +private: + USpatialNetDriver& NetDriver; + const bool bIsNetWriteFence; +}; +} // namespace SpatialRPCServicePrivate + FRPCErrorInfo SpatialRPCService::ApplyRPCInternal(UObject* TargetObject, UFunction* Function, const FPendingRPCParams& PendingRPCParams) { + using namespace SpatialRPCServicePrivate; FRPCErrorInfo ErrorInfo = { TargetObject, Function, ERPCResult::UnresolvedParameters }; uint8* Parms = (uint8*)FMemory_Alloca(Function->ParmsSize); @@ -561,7 +555,27 @@ FRPCErrorInfo SpatialRPCService::ApplyRPCInternal(UObject* TargetObject, UFuncti const float TimeQueued = (FDateTime::Now() - PendingRPCParams.Timestamp).GetTotalSeconds(); const int32 UnresolvedRefCount = UnresolvedRefs.Num(); - if (UnresolvedRefCount == 0 || SpatialSettings->QueuedIncomingRPCWaitTime < TimeQueued) + const bool bIsReliableChannel = PendingRPCParams.Type == ERPCType::ClientReliable || PendingRPCParams.Type == ERPCType::ServerReliable; + const bool bMissingServerObject = Algo::AnyOf(UnresolvedRefs, [&TargetObject, Function](const FUnrealObjectRef& MissingRef) { + if (MissingRef.bNoLoadOnClient) + { + return true; + } + else if (!ensureAlwaysMsgf(MissingRef.Path.IsSet(), + TEXT("Received reference to dynamic object as loadable. Target : %s, Parameter Entity : %llu, RPC : %s"), + *TargetObject->GetName(), MissingRef.Entity, *Function->GetName())) + { + // Validation code, to ensure that every loadable ref we receive has a name. + return true; + } + return false; + }); + + const bool bCannotWaitLongerThanQueueTime = !bIsReliableChannel || bMissingServerObject; + const bool bQueueTimeExpired = TimeQueued > SpatialSettings->QueuedIncomingRPCWaitTime; + const bool bMustExecuteRPC = UnresolvedRefCount == 0 || (bCannotWaitLongerThanQueueTime && bQueueTimeExpired); + + if (bMustExecuteRPC) { if (UnresolvedRefCount > 0 && !SpatialSettings->ShouldRPCTypeAllowUnresolvedParameters(PendingRPCParams.Type) && (Function->SpatialFunctionFlags & SPATIALFUNC_AllowUnresolvedParameters) == 0) @@ -600,7 +614,10 @@ FRPCErrorInfo SpatialRPCService::ApplyRPCInternal(UObject* TargetObject, UFuncti EventTracer->AddToStack(SpanId); } - TargetObject->ProcessEvent(Function, Parms); + { + NetWriteFenceResolutionHandler Resolution(*NetDriver, *Function); + TargetObject->ProcessEvent(Function, Parms); + } if (bUseEventTracer) { @@ -802,50 +819,4 @@ FSpatialNetBitWriter SpatialRPCService::PackRPCDataToSpatialNetBitWriter(UFuncti return PayloadWriter; } - -#if TRACE_LIB_ACTIVE -void SpatialRPCService::ProcessResultToLatencyTrace(const EPushRPCResult Result, const TraceKey Trace) -{ - if (SpatialLatencyTracer != nullptr && Trace != InvalidTraceKey) - { - bool bEndTrace = false; - FString TraceMsg; - switch (Result) - { - case SpatialGDK::EPushRPCResult::Success: - // No further action - break; - case SpatialGDK::EPushRPCResult::QueueOverflowed: - TraceMsg = TEXT("Overflowed"); - break; - case SpatialGDK::EPushRPCResult::DropOverflowed: - TraceMsg = TEXT("OverflowedAndDropped"); - bEndTrace = true; - break; - case SpatialGDK::EPushRPCResult::HasAckAuthority: - TraceMsg = TEXT("NoAckAuth"); - bEndTrace = true; - break; - case SpatialGDK::EPushRPCResult::NoRingBufferAuthority: - TraceMsg = TEXT("NoRingBufferAuth"); - bEndTrace = true; - break; - default: - TraceMsg = TEXT("UnrecognisedResult"); - break; - } - - if (bEndTrace) - { - // This RPC has been dropped, end the trace - SpatialLatencyTracer->WriteAndEndTrace(Trace, TraceMsg, false); - } - else if (!TraceMsg.IsEmpty()) - { - // This RPC will be sent later - SpatialLatencyTracer->WriteToLatencyTrace(Trace, TraceMsg); - } - } -} -#endif // TRACE_LIB_ACTIVE } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp index bc0ea2ec66..a4b735243c 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp @@ -23,7 +23,6 @@ #include "LoadBalancing/SpatialMultiWorkerSettings.h" #include "Utils/GDKPropertyMacros.h" #include "Utils/RepLayoutUtils.h" -#include "Utils/SpatialStatics.h" DEFINE_LOG_CATEGORY(LogSpatialClassInfoManager); @@ -92,7 +91,7 @@ ERPCType GetRPCType(UFunction* RemoteFunction) { return ERPCType::NetMulticast; } - else if (RemoteFunction->HasAnyFunctionFlags(FUNC_NetCrossServer)) + else if (RemoteFunction->HasAnyFunctionFlags(FUNC_NetCrossServer | FUNC_NetWriteFence)) { return ERPCType::CrossServer; } @@ -140,6 +139,19 @@ void USpatialClassInfoManager::CreateClassInfoForClass(UClass* Class) GEngine->NetworkRemapPath(NetDriver, ClassPath, false /*bIsReading*/); #endif + if (!bHandoverActive.IsSet()) + { + if (NetDriver->LoadBalanceStrategy != nullptr) + { + bHandoverActive = NetDriver->LoadBalanceStrategy->RequiresHandoverData(); + } + else + { + UE_LOG(LogSpatialClassInfoManager, Log, TEXT("Load Balancing Strategy not set, handover will be disabled.")); + bHandoverActive = false; + } + } + TSharedRef Info = ClassInfoMap.Add(Class, MakeShared()); Info->Class = Class; @@ -200,25 +212,10 @@ void USpatialClassInfoManager::CreateClassInfoForClass(UClass* Class) } } - const bool bTrackHandoverProperties = ShouldTrackHandoverProperties(); for (TFieldIterator PropertyIt(Class); PropertyIt; ++PropertyIt) { GDK_PROPERTY(Property)* Property = *PropertyIt; - if (bTrackHandoverProperties && (Property->PropertyFlags & CPF_Handover)) - { - for (int32 ArrayIdx = 0; ArrayIdx < PropertyIt->ArrayDim; ++ArrayIdx) - { - FHandoverPropertyInfo HandoverInfo; - HandoverInfo.Handle = Info->HandoverProperties.Num() + 1; // 1-based index - HandoverInfo.Offset = Property->GetOffset_ForGC() + Property->ElementSize * ArrayIdx; - HandoverInfo.ArrayIdx = ArrayIdx; - HandoverInfo.Property = Property; - - Info->HandoverProperties.Add(HandoverInfo); - } - } - if (Property->PropertyFlags & CPF_AlwaysInterested) { for (int32 ArrayIdx = 0; ArrayIdx < PropertyIt->ArrayDim; ++ArrayIdx) @@ -232,26 +229,6 @@ void USpatialClassInfoManager::CreateClassInfoForClass(UClass* Class) } } - if (bTrackHandoverProperties) - { - uint32 Offset = 0; - - for (FHandoverPropertyInfo& PropertyInfo : Info->HandoverProperties) - { - if (PropertyInfo.ArrayIdx == 0) // For static arrays, the first element will handle the whole array - { - // Make sure we conform to Unreal's alignment requirements - Offset = Align(Offset, PropertyInfo.Property->GetMinAlignment()); - - PropertyInfo.ShadowOffset = Offset; - - Offset += PropertyInfo.Property->GetSize(); - } - } - - Info->HandoverPropertiesSize = Offset; - } - if (bIsActorClass) { FinishConstructingActorClassInfo(ClassPath, Info); @@ -266,13 +243,7 @@ void USpatialClassInfoManager::FinishConstructingActorClassInfo(const FString& C { ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { Worker_ComponentId ComponentId = SchemaDatabase->ActorClassPathToSchema[ClassPath].SchemaComponents[Type]; - - if (!ShouldTrackHandoverProperties() && Type == SCHEMA_Handover) - { - return; - } - - if (ComponentId != SpatialConstants::INVALID_COMPONENT_ID) + if (IsComponentIdForTypeValid(ComponentId, Type)) { Info->SchemaComponents[Type] = ComponentId; ComponentToClassInfoMap.Add(ComponentId, Info); @@ -303,13 +274,8 @@ void USpatialClassInfoManager::FinishConstructingActorClassInfo(const FString& C ActorSubobjectInfo->SubobjectName = SubobjectSchemaData.Name; ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { - if (!ShouldTrackHandoverProperties() && Type == SCHEMA_Handover) - { - return; - } - Worker_ComponentId ComponentId = SubobjectSchemaData.SchemaComponents[Type]; - if (ComponentId != 0) + if (IsComponentIdForTypeValid(ComponentId, Type)) { ActorSubobjectInfo->SchemaComponents[Type] = ComponentId; ComponentToClassInfoMap.Add(ComponentId, ActorSubobjectInfo); @@ -331,12 +297,16 @@ void USpatialClassInfoManager::FinishConstructingSubobjectClassInfo(const FStrin SpecificDynamicSubobjectInfo->bDynamicSubobject = true; int32 Offset = DynamicSubobjectData.SchemaComponents[SCHEMA_Data]; - check(Offset != SpatialConstants::INVALID_COMPONENT_ID); + if (!ensureAlwaysMsgf(Offset != SpatialConstants::INVALID_COMPONENT_ID, + TEXT("Failed to get dynamic subobject data offset when constructing subobject. Is Schema up to date?"))) + { + continue; + } ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { Worker_ComponentId ComponentId = DynamicSubobjectData.SchemaComponents[Type]; - if (ComponentId != SpatialConstants::INVALID_COMPONENT_ID) + if (IsComponentIdForTypeValid(ComponentId, Type)) { SpecificDynamicSubobjectInfo->SchemaComponents[Type] = ComponentId; ComponentToClassInfoMap.Add(ComponentId, SpecificDynamicSubobjectInfo); @@ -349,18 +319,10 @@ void USpatialClassInfoManager::FinishConstructingSubobjectClassInfo(const FStrin } } -bool USpatialClassInfoManager::ShouldTrackHandoverProperties() const +bool USpatialClassInfoManager::IsComponentIdForTypeValid(const Worker_ComponentId ComponentId, const ESchemaComponentType Type) const { - // There's currently a bug that lets handover data get sent to clients in the initial - // burst of data for an entity, which leads to log spam in the SpatialReceiver. By tracking handover - // properties on clients, we can prevent that spam. Cannot be removed yet because of Kraken, - // UNR-4358 will remove this in a squid-only world. - if (!NetDriver->IsServer()) - { - return true; - } - - return USpatialStatics::IsHandoverEnabled(NetDriver); + // If handover is inactive, mark server only components as invalid. + return ComponentId != SpatialConstants::INVALID_COMPONENT_ID && (Type != SCHEMA_ServerOnly || bHandoverActive.Get(false)); } void USpatialClassInfoManager::TryCreateClassInfoForComponentId(Worker_ComponentId ComponentId) @@ -494,14 +456,14 @@ TArray USpatialClassInfoManager::GetComponentIdsForClassHier return OutComponentIds; } -bool USpatialClassInfoManager::GetOffsetByComponentId(Worker_ComponentId ComponentId, uint32& OutOffset) +bool USpatialClassInfoManager::GetOffsetByComponentId(Worker_ComponentId ComponentId, ObjectOffset& OutOffset) { if (!ComponentToOffsetMap.Contains(ComponentId)) { TryCreateClassInfoForComponentId(ComponentId); } - if (uint32* Offset = ComponentToOffsetMap.Find(ComponentId)) + if (ObjectOffset* Offset = ComponentToOffsetMap.Find(ComponentId)) { OutOffset = *Offset; return true; @@ -641,7 +603,11 @@ void USpatialClassInfoManager::QuitGame() Worker_ComponentId USpatialClassInfoManager::ComputeActorInterestComponentId(const AActor* Actor) const { - check(Actor); + if (!ensureAlwaysMsgf(Actor != nullptr, TEXT("Trying to compute Actor interest component ID for nullptr Actor"))) + { + return SpatialConstants::INVALID_COMPONENT_ID; + } + const AActor* ActorForRelevancy = Actor; // bAlwaysRelevant takes precedence over bNetUseOwnerRelevancy - see AActor::IsNetRelevantFor while (!ActorForRelevancy->bAlwaysRelevant && ActorForRelevancy->bNetUseOwnerRelevancy && ActorForRelevancy->GetOwner() != nullptr) diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialDispatcher.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialDispatcher.cpp index 2da973f9dd..da76e5b31a 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialDispatcher.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialDispatcher.cpp @@ -58,8 +58,13 @@ bool SpatialDispatcher::IsExternalSchemaOp(const Worker_Op& Op) const void SpatialDispatcher::ProcessExternalSchemaOp(const Worker_Op& Op) { - Worker_ComponentId ComponentId = SpatialGDK::GetComponentId(Op); - check(ComponentId != SpatialConstants::INVALID_COMPONENT_ID); + const Worker_ComponentId ComponentId = SpatialGDK::GetComponentId(Op); + + if (!ensureAlwaysMsgf(ComponentId != SpatialConstants::INVALID_COMPONENT_ID, + TEXT("Tried to process external schema op with invalid component ID"))) + { + return; + } switch (Op.op_type) { @@ -130,7 +135,12 @@ SpatialDispatcher::FCallbackId SpatialDispatcher::OnCommandResponse(Worker_Compo SpatialDispatcher::FCallbackId SpatialDispatcher::AddGenericOpCallback(Worker_ComponentId ComponentId, Worker_OpType OpType, const TFunction& Callback) { - check(SpatialConstants::MIN_EXTERNAL_SCHEMA_ID <= ComponentId && ComponentId <= SpatialConstants::MAX_EXTERNAL_SCHEMA_ID); + if (!ensureAlwaysMsgf( + SpatialConstants::MIN_EXTERNAL_SCHEMA_ID <= ComponentId && ComponentId <= SpatialConstants::MAX_EXTERNAL_SCHEMA_ID, + TEXT("Tried to add op callback for external schema component ID outside of permitted range"))) + { + return -1; + } const FCallbackId NewCallbackId = NextCallbackId++; ComponentOpTypeToCallbacksMap.FindOrAdd(ComponentId).FindOrAdd(OpType).Add(UserOpCallbackData{ NewCallbackId, Callback }); CallbackIdToDataMap.Add(NewCallbackId, CallbackIdData{ ComponentId, OpType }); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp index c01d254807..3f517e4720 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp @@ -4,6 +4,7 @@ #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialVirtualWorkerTranslator.h" +#include "Interop/Connection/SpatialEventTracer.h" #include "Interop/Connection/SpatialWorkerConnection.h" #include "Interop/SpatialReceiver.h" #include "LoadBalancing/AbstractLBStrategy.h" diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialRoutingSystem.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialRoutingSystem.cpp index 50bbda0113..6b2a959185 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialRoutingSystem.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialRoutingSystem.cpp @@ -13,7 +13,7 @@ void SpatialRoutingSystem::ProcessUpdate(Worker_EntityId Entity, const Component { switch (Change.ComponentId) { - case SpatialConstants::CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID: + case SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID: switch (Change.Type) { case ComponentChange::COMPLETE_UPDATE: @@ -29,11 +29,11 @@ void SpatialRoutingSystem::ProcessUpdate(Worker_EntityId Entity, const Component } OnSenderChanged(Entity, Components); break; - case SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID: - case SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID: + case SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID: + case SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID: check(Change.Type == ComponentChange::COMPLETE_UPDATE); break; - case SpatialConstants::CROSSSERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID: + case SpatialConstants::CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID: switch (Change.Type) { case ComponentChange::COMPLETE_UPDATE: @@ -100,7 +100,7 @@ void SpatialRoutingSystem::OnSenderChanged(Worker_EntityId SenderId, RoutingComp { const CrossServer::RPCSlots& Slots = SlotToClear.Value; { - EntityComponentId SenderPair(SenderId, SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID); + EntityComponentId SenderPair(SenderId, SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID); Components.SenderACKState.ACKAlloc.FreeSlot(Slots.ACKSlot); GetOrCreateComponentUpdate(SenderPair); @@ -131,7 +131,7 @@ void SpatialRoutingSystem::ClearReceiverSlot(Worker_EntityId Receiver, CrossServ CrossServer::SentRPCEntry* SentRPC = ReceiverComponents.ReceiverState.Mailbox.Find(RPCKey); check(SentRPC != nullptr); - EntityComponentId ReceiverPair(Receiver, SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID); + EntityComponentId ReceiverPair(Receiver, SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID); check(SentRPC->DestinationSlot.IsSet()); uint32 SlotIdx = SentRPC->DestinationSlot.GetValue(); @@ -179,7 +179,7 @@ void SpatialRoutingSystem::TransferRPCsToReceiver(Worker_EntityId ReceiverId, Ro check(Element.IsSet()); Schema_ComponentUpdate* ReceiverUpdate = - GetOrCreateComponentUpdate(EntityComponentId(ReceiverId, SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID)); + GetOrCreateComponentUpdate(EntityComponentId(ReceiverId, SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID)); Schema_Object* EndpointObject = Schema_GetComponentUpdateFields(ReceiverUpdate); const uint32 SlotIdx = FreeSlot.GetValue(); @@ -209,7 +209,7 @@ void SpatialRoutingSystem::WriteACKToSender(CrossServer::RPCKey RPCKey, RoutingC if (TOptional ReservedSlot = SenderComponents.SenderACKState.ACKAlloc.ReserveSlot()) { Slots->ACKSlot = ReservedSlot.GetValue(); - EntityComponentId SenderPair(RPCKey.Get<0>(), SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID); + EntityComponentId SenderPair(RPCKey.Get<0>(), SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID); Schema_ComponentUpdate* Update = GetOrCreateComponentUpdate(SenderPair); Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update); @@ -324,11 +324,11 @@ void SpatialRoutingSystem::Advance(SpatialOSWorkerInterface* Connection) { switch (ComponentDesc.GetComponentId()) { - case SpatialConstants::CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID: + case SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID: Components.Sender = CrossServerEndpoint(ComponentDesc.GetUnderlying()); // TODO : Should inspect the component if we were reloading a snapshot break; - case SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID: + case SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID: { CrossServerEndpointACK TempView(ComponentDesc.GetUnderlying()); for (int32 SlotIdx = 0; SlotIdx < TempView.ACKArray.Num(); ++SlotIdx) @@ -344,7 +344,7 @@ void SpatialRoutingSystem::Advance(SpatialOSWorkerInterface* Connection) } } break; - case SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID: + case SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID: { CrossServerEndpoint TempView(ComponentDesc.GetUnderlying()); for (int32 SlotIdx = 0; SlotIdx < TempView.ReliableRPCBuffer.RingBuffer.Num(); ++SlotIdx) @@ -372,7 +372,7 @@ void SpatialRoutingSystem::Advance(SpatialOSWorkerInterface* Connection) } } break; - case SpatialConstants::CROSSSERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID: + case SpatialConstants::CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID: Components.ReceiverACK = CrossServerEndpointACK(ComponentDesc.GetUnderlying()); // TODO : Should inspect the component if we were reloading a snapshot break; @@ -385,9 +385,9 @@ void SpatialRoutingSystem::Advance(SpatialOSWorkerInterface* Connection) { ReceiversToInspect.Remove(Delta.EntityId); PendingComponentUpdatesToSend.Remove( - EntityComponentId(Delta.EntityId, SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID)); + EntityComponentId(Delta.EntityId, SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID)); PendingComponentUpdatesToSend.Remove( - EntityComponentId(Delta.EntityId, SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID)); + EntityComponentId(Delta.EntityId, SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID)); if (RoutingComponents* Components = RoutingWorkerView.Find(Delta.EntityId)) { @@ -443,7 +443,7 @@ void SpatialRoutingSystem::Flush(SpatialOSWorkerInterface* Connection) if (RoutingComponents* Components = RoutingWorkerView.Find(Entity)) { - if (CompId == SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID) + if (CompId == SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID) { RPCRingBufferDescriptor Descriptor = RPCRingBufferUtils::GetRingBufferDescriptor(ERPCType::CrossServer); @@ -455,7 +455,7 @@ void SpatialRoutingSystem::Flush(SpatialOSWorkerInterface* Connection) }); } - if (CompId == SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID) + if (CompId == SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID) { Components->SenderACKState.ACKAlloc.ForeachClearedSlot([&](uint32_t ToClear) { Schema_AddComponentUpdateClearedField(Entry.Value, ToClear + 1); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp index 7072b66efb..a602872444 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp @@ -99,11 +99,24 @@ void USpatialSender::UpdatePartitionEntityInterestAndPosition() void USpatialSender::SendAuthorityIntentUpdate(const AActor& InActor, VirtualWorkerId NewAuthoritativeVirtualWorkerId) const { const Worker_EntityId EntityId = PackageMap->GetEntityIdFromObject(&InActor); - check(EntityId != SpatialConstants::INVALID_ENTITY_ID); + + if (!ensureAlwaysMsgf(EntityId != SpatialConstants::INVALID_ENTITY_ID, + TEXT("Couldn't find entity ID from package map when sending auth intent update. Actor: %s"), + *GetNameSafe(&InActor))) + { + return; + } TOptional AuthorityIntentComponent = DeserializeComponent(NetDriver->Connection->GetCoordinator(), EntityId); - check(AuthorityIntentComponent.IsSet()); + + if (!ensureAlwaysMsgf(AuthorityIntentComponent.IsSet(), + TEXT("Failed to get currnet AuthorityIntent data from view coordinator when sending update. Actor: %s"), + *GetNameSafe(&InActor))) + { + return; + } + if (AuthorityIntentComponent->VirtualWorkerId == NewAuthoritativeVirtualWorkerId) { /* This seems to occur when using the replication graph, however we're still unsure the cause. */ diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GameplayDebuggerLBStrategy.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GameplayDebuggerLBStrategy.cpp new file mode 100644 index 0000000000..b29f79c67b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GameplayDebuggerLBStrategy.cpp @@ -0,0 +1,119 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "LoadBalancing/GameplayDebuggerLBStrategy.h" + +#include "EngineClasses/SpatialNetDriverGameplayDebuggerContext.h" +#include "EngineClasses/SpatialWorldSettings.h" + +DEFINE_LOG_CATEGORY(LogGameplayDebuggerLBStrategy); + +UGameplayDebuggerLBStrategy::UGameplayDebuggerLBStrategy() + : WrappedStrategy(nullptr) + , GameplayDebuggerCtx(nullptr) + , VirtualWorkerIds() +{ +} + +void UGameplayDebuggerLBStrategy::Init(USpatialNetDriverGameplayDebuggerContext& InGameplayDebuggerCtx, + UAbstractLBStrategy& InWrappedStrategy) +{ + GameplayDebuggerCtx = &InGameplayDebuggerCtx; + WrappedStrategy = &InWrappedStrategy; + LocalVirtualWorkerId = InWrappedStrategy.GetLocalVirtualWorkerId(); +} + +FString UGameplayDebuggerLBStrategy::ToString() const +{ + return TEXT("GameplayDebugger"); +} + +void UGameplayDebuggerLBStrategy::SetLocalVirtualWorkerId(VirtualWorkerId InLocalVirtualWorkerId) +{ + check(WrappedStrategy); + WrappedStrategy->SetLocalVirtualWorkerId(InLocalVirtualWorkerId); + LocalVirtualWorkerId = WrappedStrategy->GetLocalVirtualWorkerId(); +} + +bool UGameplayDebuggerLBStrategy::ShouldHaveAuthority(const AActor& Actor) const +{ + check(WrappedStrategy); + +#if WITH_GAMEPLAY_DEBUGGER + TOptional WorkerId = GameplayDebuggerCtx->GetActorDelegatedWorkerId(Actor); + if (WorkerId) + { + return WorkerId.GetValue() == GetLocalVirtualWorkerId(); + } +#endif // WITH_GAMEPLAY_DEBUGGER + + return WrappedStrategy->ShouldHaveAuthority(Actor); +} + +VirtualWorkerId UGameplayDebuggerLBStrategy::WhoShouldHaveAuthority(const AActor& Actor) const +{ + check(WrappedStrategy); + +#if WITH_GAMEPLAY_DEBUGGER + TOptional WorkerId = GameplayDebuggerCtx->GetActorDelegatedWorkerId(Actor); + if (WorkerId) + { + return WorkerId.GetValue(); + } +#endif // WITH_GAMEPLAY_DEBUGGER + + return WrappedStrategy->WhoShouldHaveAuthority(Actor); +} + +SpatialGDK::FActorLoadBalancingGroupId UGameplayDebuggerLBStrategy::GetActorGroupId(const AActor& Actor) const +{ + check(WrappedStrategy); + return WrappedStrategy->GetActorGroupId(Actor); +} + +SpatialGDK::QueryConstraint UGameplayDebuggerLBStrategy::GetWorkerInterestQueryConstraint(const VirtualWorkerId VirtualWorker) const +{ + check(WrappedStrategy); + return WrappedStrategy->GetWorkerInterestQueryConstraint(VirtualWorker); +} + +FVector UGameplayDebuggerLBStrategy::GetWorkerEntityPosition() const +{ + check(WrappedStrategy); + return WrappedStrategy->GetWorkerEntityPosition(); +} + +uint32 UGameplayDebuggerLBStrategy::GetMinimumRequiredWorkers() const +{ + check(WrappedStrategy); + return WrappedStrategy->GetMinimumRequiredWorkers(); +} + +void UGameplayDebuggerLBStrategy::SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtualWorkerId, + const VirtualWorkerId& LastVirtualWorkerId) +{ + check(WrappedStrategy); + WrappedStrategy->SetVirtualWorkerIds(FirstVirtualWorkerId, LastVirtualWorkerId); +} + +TSet UGameplayDebuggerLBStrategy::GetVirtualWorkerIds() const +{ + check(WrappedStrategy); + return WrappedStrategy->GetVirtualWorkerIds(); +} + +UAbstractLBStrategy* UGameplayDebuggerLBStrategy::GetLBStrategyForVisualRendering() const +{ + check(WrappedStrategy); + return WrappedStrategy->GetLBStrategyForVisualRendering(); +} + +bool UGameplayDebuggerLBStrategy::RequiresHandoverData() const +{ + check(WrappedStrategy); + return WrappedStrategy->RequiresHandoverData(); +} + +UAbstractLBStrategy* UGameplayDebuggerLBStrategy::GetWrappedStrategy() const +{ + return WrappedStrategy; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp index 99607df8b0..a052b1cb7e 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp @@ -118,7 +118,12 @@ VirtualWorkerId UGridBasedLBStrategy::WhoShouldHaveAuthority(const AActor& Actor const FVector2D Actor2DLocation = GetActorLoadBalancingPosition(Actor); - check(VirtualWorkerIds.Num() == WorkerCells.Num()); + if (!ensureAlwaysMsgf(VirtualWorkerIds.Num() == WorkerCells.Num(), + TEXT("Found a mismatch between virtual worker count and worker cells count in load balancing strategy"))) + { + return SpatialConstants::INVALID_VIRTUAL_WORKER_ID; + } + for (int i = 0; i < WorkerCells.Num(); i++) { if (IsInside(WorkerCells[i], Actor2DLocation)) @@ -154,7 +159,13 @@ SpatialGDK::QueryConstraint UGridBasedLBStrategy::GetWorkerInterestQueryConstrai const FVector Center3D{ Center2D.X, Center2D.Y, 0.0f }; const FVector2D EdgeLengths2D = Interest2D.GetSize(); - check(EdgeLengths2D.X > 0.0f && EdgeLengths2D.Y > 0.0f); + + if (!ensureAlwaysMsgf(EdgeLengths2D.X > 0.0f && EdgeLengths2D.Y > 0.0f, + TEXT("Failed to create worker interest constraint. Grid cell area was 0"))) + { + return SpatialGDK::QueryConstraint(); + } + const FVector EdgeLengths3D{ EdgeLengths2D.X, EdgeLengths2D.Y, FLT_MAX }; SpatialGDK::QueryConstraint Constraint; @@ -170,8 +181,17 @@ FVector2D UGridBasedLBStrategy::GetActorLoadBalancingPosition(const AActor& Acto FVector UGridBasedLBStrategy::GetWorkerEntityPosition() const { - check(IsReady()); - check(bIsStrategyUsedOnLocalWorker); + if (!ensureAlwaysMsgf(IsReady(), TEXT("Called GetWorkerEntityPosition before load balancing strategy is ready"))) + { + return FVector::ZeroVector; + } + + if (!ensureAlwaysMsgf(bIsStrategyUsedOnLocalWorker, + TEXT("Called GetWorkerEntityPosition on load balancing stratey that isn't in use by the local worker"))) + { + return FVector::ZeroVector; + } + const FVector2D Centre = WorkerCells[LocalCellId].GetCenter(); return FVector{ Centre.X, Centre.Y, 0.f }; } diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/LayeredLBStrategy.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/LayeredLBStrategy.cpp index 79f58a2100..7d6c1a4e38 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/LayeredLBStrategy.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/LayeredLBStrategy.cpp @@ -34,9 +34,11 @@ FString ULayeredLBStrategy::ToString() const { Description += FString::Printf(TEXT("%d = %s, "), Entry.Key, *Entry.Value.ToString()); } - check(Description.Len() > 1); - Description.LeftChopInline(2); - Description += TEXT("}"); + if (ensureAlwaysMsgf(Description.Len() > 1, TEXT("Load balancing strategy description should be more than 1 character in length"))) + { + Description.LeftChopInline(2); + Description += TEXT("}"); + } } return Description; } @@ -170,12 +172,10 @@ SpatialGDK::QueryConstraint ULayeredLBStrategy::GetWorkerInterestQueryConstraint Constraint.ComponentConstraint = 0; return Constraint; } - else - { - const FName& LayerName = VirtualWorkerIdToLayerName[VirtualWorker]; - check(LayerNameToLBStrategy.Contains(LayerName)); - return LayerNameToLBStrategy[LayerName]->GetWorkerInterestQueryConstraint(VirtualWorker); - } + + const FName& LayerName = VirtualWorkerIdToLayerName[VirtualWorker]; + check(LayerNameToLBStrategy.Contains(LayerName)); + return LayerNameToLBStrategy[LayerName]->GetWorkerInterestQueryConstraint(VirtualWorker); } bool ULayeredLBStrategy::RequiresHandoverData() const @@ -192,18 +192,26 @@ bool ULayeredLBStrategy::RequiresHandoverData() const FVector ULayeredLBStrategy::GetWorkerEntityPosition() const { - check(IsReady()); + if (!ensureAlwaysMsgf(IsReady(), TEXT("Called GetWorkerEntityPosition before load balancing strategy was ready"))) + { + return FVector::ZeroVector; + } + if (!VirtualWorkerIdToLayerName.Contains(LocalVirtualWorkerId)) { UE_LOG(LogLayeredLBStrategy, Error, TEXT("LayeredLBStrategy doesn't have a LBStrategy for worker %d."), LocalVirtualWorkerId); - return FVector{ 0.f, 0.f, 0.f }; + return FVector::ZeroVector; } - else + + const FName& LayerName = VirtualWorkerIdToLayerName[LocalVirtualWorkerId]; + + if (!ensureAlwaysMsgf(LayerNameToLBStrategy.Contains(LayerName), + TEXT("Called GetWorkerEntityPosition but couldn't find layer %s in local map"), *LayerName.ToString())) { - const FName& LayerName = VirtualWorkerIdToLayerName[LocalVirtualWorkerId]; - check(LayerNameToLBStrategy.Contains(LayerName)); - return LayerNameToLBStrategy[LayerName]->GetWorkerEntityPosition(); + return FVector::ZeroVector; } + + return LayerNameToLBStrategy[LayerName]->GetWorkerEntityPosition(); } uint32 ULayeredLBStrategy::GetMinimumRequiredWorkers() const @@ -264,17 +272,25 @@ void ULayeredLBStrategy::SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtual // Once they are pick up this code, they should be able to switch to another method and we can remove this. bool ULayeredLBStrategy::CouldHaveAuthority(const TSubclassOf Class) const { - check(IsReady()); + if (!ensureAlwaysMsgf(IsReady(), TEXT("Called CouldHaveAuthority before load balancing strategy was ready"))) + { + return false; + } + return *VirtualWorkerIdToLayerName.Find(LocalVirtualWorkerId) == GetLayerNameForClass(Class); } UAbstractLBStrategy* ULayeredLBStrategy::GetLBStrategyForVisualRendering() const { // The default strategy is guaranteed to exist as long as the strategy is ready. - checkf(LayerNameToLBStrategy.Contains(SpatialConstants::DefaultLayer), - TEXT("Load balancing strategy does not contain default layer which is needed to render worker debug visualization. " - "Default layer presence should be enforced by MultiWorkerSettings edit validation. Class: %s"), - *GetNameSafe(this)); + if (!ensureAlwaysMsgf( + LayerNameToLBStrategy.Contains(SpatialConstants::DefaultLayer), + TEXT("Load balancing strategy does not contain default layer which is needed to render worker debug visualization. " + "Default layer presence should be enforced by MultiWorkerSettings edit validation. Class: %s"), + *GetNameSafe(this))) + { + return nullptr; + } return GetLBStrategyForLayer(SpatialConstants::DefaultLayer); } @@ -282,7 +298,10 @@ UAbstractLBStrategy* ULayeredLBStrategy::GetLBStrategyForLayer(FName Layer) cons { // Editor has the option to display the load balanced zones and could query the strategy anytime. #ifndef WITH_EDITOR - check(IsReady()); + if (!ensureAlwaysMsgf(IsReady(), TEXT("Called GetLBStrategyForLayer before load balancing strategy was ready"))) + { + return nullptr; + } #endif if (UAbstractLBStrategy* const* Entry = LayerNameToLBStrategy.Find(Layer)) diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/OwnershipLockingPolicy.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/OwnershipLockingPolicy.cpp index 9e2f582320..2d8a7c82c5 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/OwnershipLockingPolicy.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/OwnershipLockingPolicy.cpp @@ -69,7 +69,11 @@ bool UOwnershipLockingPolicy::ReleaseLock(const ActorLockToken Token) UE_LOG(LogOwnershipLockingPolicy, Verbose, TEXT("Releasing Actor migration lock. Actor: %s. Token: %lld. Lock name: %s"), *Actor->GetName(), Token, *Name); - check(ActorToLockingState.Contains(Actor)); + if (!ensureAlwaysMsgf(ActorToLockingState.Contains(Actor), + TEXT("Tried to release lock on Actor which wasn't present in locking state map. Actor: %s"), *GetNameSafe(Actor))) + { + return false; + } { // Reduce the reference count and erase the entry if reduced to 0. @@ -177,7 +181,10 @@ bool UOwnershipLockingPolicy::ReleaseLockFromDelegate(AActor* ActorToRelease, co void UOwnershipLockingPolicy::OnOwnerUpdated(const AActor* Actor, const AActor* OldOwner) { - check(Actor != nullptr); + if (!ensureAlwaysMsgf(Actor != nullptr, TEXT("Attempted to call owner update locking policy callback for nullptr Actor"))) + { + return; + } // If an explicitly locked Actor is changing owner. if (IsExplicitlyLocked(Actor)) @@ -223,7 +230,12 @@ void UOwnershipLockingPolicy::OnExplicitlyLockedActorDeleted(AActor* DestroyedAc void UOwnershipLockingPolicy::OnHierarchyRootActorDeleted(AActor* DeletedHierarchyRoot) { - check(LockedOwnershipRootActorToExplicitlyLockedActors.Contains(DeletedHierarchyRoot)); + if (!ensureAlwaysMsgf(LockedOwnershipRootActorToExplicitlyLockedActors.Contains(DeletedHierarchyRoot), + TEXT("OnHierarchyRootActorDeleted called but couldn't find hierarchy root %s in local map"), + *GetNameSafe(DeletedHierarchyRoot))) + { + return; + } // For all explicitly locked Actors where this Actor is on the ownership path, recalculate the // ownership path information to account for this Actor's deletion. @@ -233,9 +245,17 @@ void UOwnershipLockingPolicy::OnHierarchyRootActorDeleted(AActor* DeletedHierarc void UOwnershipLockingPolicy::RecalculateAllExplicitlyLockedActorsInThisHierarchy(const AActor* HierarchyRoot) { - TArray ExplicitlyLockedActorsWithThisActorInOwnershipPath = - LockedOwnershipRootActorToExplicitlyLockedActors.FindChecked(HierarchyRoot); - for (const AActor* ExplicitlyLockedActor : ExplicitlyLockedActorsWithThisActorInOwnershipPath) + TArray* ExplicitlyLockedActorsWithThisActorInOwnershipPath = + LockedOwnershipRootActorToExplicitlyLockedActors.Find(HierarchyRoot); + + if (!ensureAlwaysMsgf(ExplicitlyLockedActorsWithThisActorInOwnershipPath != nullptr, + TEXT("Tried to recalculate hierarchy locking state but couldn't find root Actor %s in map"), + *GetNameSafe(HierarchyRoot))) + { + return; + } + + for (const AActor* ExplicitlyLockedActor : *ExplicitlyLockedActorsWithThisActorInOwnershipPath) { RecalculateLockedActorOwnershipHierarchyInformation(ExplicitlyLockedActor); } @@ -244,8 +264,13 @@ void UOwnershipLockingPolicy::RecalculateAllExplicitlyLockedActorsInThisHierarch void UOwnershipLockingPolicy::RecalculateLockedActorOwnershipHierarchyInformation(const AActor* ExplicitlyLockedActor) { // For the old ownership path, update ownership path Actor mapping to explicitly locked Actors to remove this Actor. - AActor* OldHierarchyRoot = ActorToLockingState.FindChecked(ExplicitlyLockedActor).HierarchyRoot; - RemoveOwnershipHierarchyRootInformation(OldHierarchyRoot, ExplicitlyLockedActor); + MigrationLockElement* OldHierarchyLockData = ActorToLockingState.Find(ExplicitlyLockedActor); + if (ensureAlwaysMsgf(OldHierarchyLockData != nullptr, + TEXT("Tried to update Actor %s hierarchy locking data but previous hierarchy root wasn't found in local mapping"), + *GetNameSafe(ExplicitlyLockedActor))) + { + RemoveOwnershipHierarchyRootInformation(OldHierarchyLockData->HierarchyRoot, ExplicitlyLockedActor); + } // For the new ownership path, update ownership path Actor mapping to explicitly locked Actors to include this Actor. AActor* NewOwnershipHierarchyRoot = SpatialGDK::GetTopmostReplicatedOwner(ExplicitlyLockedActor); @@ -261,20 +286,24 @@ void UOwnershipLockingPolicy::RemoveOwnershipHierarchyRootInformation(AActor* Hi } // Find Actors in this root Actor's hierarchy which are explicitly locked. - TArray& ExplicitlyLockedActorsWithThisActorOnPath = - LockedOwnershipRootActorToExplicitlyLockedActors.FindChecked(HierarchyRoot); - check(ExplicitlyLockedActorsWithThisActorOnPath.Num() > 0); + TArray* ExplicitlyLockedActorsWithThisActorOnPath = LockedOwnershipRootActorToExplicitlyLockedActors.Find(HierarchyRoot); + + if (!ensureAlwaysMsgf(ExplicitlyLockedActorsWithThisActorOnPath != nullptr && ExplicitlyLockedActorsWithThisActorOnPath->Num() > 0, + TEXT("Tried to cleanup Actor hierarchy locking information but the hierarchy data was empty or invalid"))) + { + return; + } // If there's only one explicitly locked Actor in the hierarchy, we're removing the only Actor with this root, // so we can stop caring about the root itself. Otherwise, just remove the specific Actor entry in the root's list. - if (ExplicitlyLockedActorsWithThisActorOnPath.Num() == 1) + if (ExplicitlyLockedActorsWithThisActorOnPath->Num() == 1) { LockedOwnershipRootActorToExplicitlyLockedActors.Remove(HierarchyRoot); HierarchyRoot->OnDestroyed.RemoveDynamic(this, &UOwnershipLockingPolicy::OnHierarchyRootActorDeleted); } else { - ExplicitlyLockedActorsWithThisActorOnPath.Remove(ExplicitlyLockedActor); + ExplicitlyLockedActorsWithThisActorOnPath->Remove(ExplicitlyLockedActor); } } diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/SpatialMultiWorkerSettings.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/SpatialMultiWorkerSettings.cpp index 4a67726715..71380e8b9f 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/SpatialMultiWorkerSettings.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/SpatialMultiWorkerSettings.cpp @@ -40,7 +40,11 @@ void UAbstractSpatialMultiWorkerSettings::PostEditChangeProperty(struct FPropert void UAbstractSpatialMultiWorkerSettings::EditorRefreshSpatialDebugger() const { const UWorld* World = GEditor->GetEditorWorldContext().World(); - check(World != nullptr); + + if (!ensureAlwaysMsgf(World != nullptr, TEXT("Tried to refresh editor spatial debugger but failed to access World from GEditor"))) + { + return; + } const TSubclassOf VisibleMultiWorkerSettingsClass = USpatialStatics::GetSpatialMultiWorkerClass(World); @@ -58,8 +62,11 @@ uint32 UAbstractSpatialMultiWorkerSettings::GetMinimumRequiredWorkerCount() cons for (const FLayerInfo& LayerInfo : WorkerLayers) { - check(*LayerInfo.LoadBalanceStrategy != nullptr); - WorkerCount += GetDefault(*LayerInfo.LoadBalanceStrategy)->GetMinimumRequiredWorkers(); + if (ensureAlwaysMsgf(*LayerInfo.LoadBalanceStrategy != nullptr, + TEXT("Failed to get minimum worker count for multiserver settings because strategy on a layer was invalid"))) + { + WorkerCount += GetDefault(*LayerInfo.LoadBalanceStrategy)->GetMinimumRequiredWorkers(); + } } return WorkerCount; diff --git a/SpatialGDK/Source/SpatialGDK/Private/Schema/ActorOwnership.cpp b/SpatialGDK/Source/SpatialGDK/Private/Schema/ActorOwnership.cpp new file mode 100644 index 0000000000..8f3d1c3084 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Schema/ActorOwnership.cpp @@ -0,0 +1,78 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Schema/ActorOwnership.h" + +#include "Engine/NetConnection.h" +#include "GameFramework/Actor.h" +#include "GameFramework/PlayerController.h" + +#include "EngineClasses/SpatialPackageMapClient.h" + +namespace SpatialGDK +{ +ActorOwnership::ActorOwnership(const ComponentData& Data) +{ + ApplySchema(Data.GetFields()); +} + +ActorOwnership::ActorOwnership(const Worker_ComponentData& Data) +{ + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + + ApplySchema(ComponentObject); +} + +ComponentData ActorOwnership::CreateComponentData() const +{ + return CreateComponentDataHelper(*this); +} + +ComponentUpdate ActorOwnership::CreateComponentUpdate() const +{ + return CreateComponentUpdateHelper(*this); +} + +void ActorOwnership::ApplyComponentUpdate(const ComponentUpdate& Update) +{ + ApplySchema(Update.GetFields()); +} + +void ActorOwnership::ApplySchema(Schema_Object* Schema) +{ + if (Schema != nullptr) + { + if (Schema_GetEntityIdCount(Schema, SpatialConstants::ACTOR_OWNERSHIP_COMPONENT_OWNER_ACTOR_ID) == 1) + { + OwnerActorEntityId = Schema_GetEntityId(Schema, SpatialConstants::ACTOR_OWNERSHIP_COMPONENT_OWNER_ACTOR_ID); + } + } +} + +void ActorOwnership::WriteSchema(Schema_Object* Schema) const +{ + if (Schema != nullptr) + { + Schema_AddInt64(Schema, SpatialConstants::ACTOR_OWNERSHIP_COMPONENT_OWNER_ACTOR_ID, OwnerActorEntityId); + } +} + +ActorOwnership ActorOwnership::CreateFromActor(const AActor& Actor, const USpatialPackageMapClient& PackageMap) +{ + ActorOwnership Ownership; + UNetConnection* OwningConnection = Actor.GetNetConnection(); + + if (IsValid(OwningConnection)) + { + // Add owning PlayerController's EntityId. + const Worker_EntityId ControllerEntity = PackageMap.GetEntityIdFromObject(OwningConnection->PlayerController); + check(ControllerEntity != SpatialConstants::INVALID_ENTITY_ID); + Ownership.OwnerActorEntityId = ControllerEntity; + } + else + { + // When no player owns an actor, use INVALID_ENTITY_ID. + Ownership.OwnerActorEntityId = SpatialConstants::INVALID_ENTITY_ID; + } + return Ownership; +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Schema/ActorOwnership.h b/SpatialGDK/Source/SpatialGDK/Private/Schema/ActorOwnership.h new file mode 100644 index 0000000000..37cf31d424 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Schema/ActorOwnership.h @@ -0,0 +1,57 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Schema/Component.h" +#include "SpatialCommonTypes.h" +#include "Utils/SchemaUtils.h" + +#include "WorkerSDK/improbable/c_worker.h" + +class USpatialPackageMapClient; + +namespace SpatialGDK +{ +// The ActorOwnership component defines actor's player ownership; OwnerActorEntityId +// points to the PlayerController entity that owns a given actor. +struct ActorOwnership +{ + static const Worker_ComponentId ComponentId = SpatialConstants::ACTOR_OWNERSHIP_COMPONENT_ID; + + explicit ActorOwnership(Worker_EntityId InLeaderEntityId) + : OwnerActorEntityId(InLeaderEntityId) + { + } + + ActorOwnership() + : ActorOwnership(Worker_EntityId(SpatialConstants::INVALID_ENTITY_ID)) + { + } + + explicit ActorOwnership(const ComponentData& Data); + + explicit ActorOwnership(const Worker_ComponentData& Data); + + static ActorOwnership CreateFromActor(const AActor& Actor, const USpatialPackageMapClient& PackageMap); + + ComponentData CreateComponentData() const; + + ComponentUpdate CreateComponentUpdate() const; + + void ApplyComponentUpdate(const ComponentUpdate& Update); + + void ApplySchema(Schema_Object* Schema); + + void WriteSchema(Schema_Object* Schema) const; + + Worker_EntityId OwnerActorEntityId; + + friend bool operator==(const ActorOwnership& Lhs, const ActorOwnership& Rhs) + { + return Lhs.OwnerActorEntityId == Rhs.OwnerActorEntityId; + } + + friend bool operator!=(const ActorOwnership& Lhs, const ActorOwnership& Rhs) { return !(Lhs == Rhs); } +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp index 31cf20ae53..fc732c86c6 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp @@ -98,34 +98,74 @@ void CheckCmdLineOverrideOptionalStringWithCallback(const TCHAR* CommandLine, co } // namespace +FString UEventTracingSamplingSettings::DefaultFilter = "false"; + +UEventTracingSamplingSettings::UEventTracingSamplingSettings() + : SamplingProbability(1.0) + , GDKEventPreFilter(DefaultFilter) + , GDKEventPostFilter(DefaultFilter) + , RuntimeEventPreFilter(DefaultFilter) + , RuntimeEventPostFilter(DefaultFilter) +{ +} + +UEventTracingSamplingSettings::TraceQueryPtr UEventTracingSamplingSettings::ParseOrDefault(const FString& Str, const TCHAR* FilterForLog) +{ + TraceQueryPtr Ptr; + if (!Str.IsEmpty()) + { + Ptr.Reset(Trace_ParseSimpleQuery(TCHAR_TO_ANSI(*Str))); + } + + if (!Ptr.IsValid()) + { + UE_LOG(LogSpatialGDKSettings, Warning, TEXT("The specified query \"%s\" is invalid; defaulting to \"false\" query. %s"), + FilterForLog, Trace_GetLastError()); + Ptr.Reset(Trace_ParseSimpleQuery("false")); + } + + return Ptr; +} + +bool UEventTracingSamplingSettings::IsFilterValid(const FString& Str) +{ + return !Str.IsEmpty() && TraceQueryPtr(Trace_ParseSimpleQuery(TCHAR_TO_ANSI(*Str))).Get() != nullptr; +} + +const FString& UEventTracingSamplingSettings::GetFilterString(const FString& Filter) +{ + return IsFilterValid(Filter) ? Filter : DefaultFilter; +} + #if WITH_EDITOR void UEventTracingSamplingSettings::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) { - auto CheckQueryValid = [](const char* QueryStr) { - if (strlen(QueryStr) > 0 && SpatialGDK::TraceQueryPtr(Trace_ParseSimpleQuery(QueryStr)).Get() == nullptr) + auto CheckQueryValid = [](const FString& QueryStr) { + if (!IsFilterValid(QueryStr)) { FMessageDialog::Open(EAppMsgType::Ok, FText::Format(LOCTEXT("EventTracingSamplingSetting_QueryInvalid", "The query entered is not valid. {0}"), FText::FromString(ANSI_TO_TCHAR(Trace_GetLastError())))); } }; + Super::PostEditChangeProperty(PropertyChangedEvent); const FName Name = (PropertyChangedEvent.MemberProperty != nullptr) ? PropertyChangedEvent.MemberProperty->GetFName() : NAME_None; if (Name == GET_MEMBER_NAME_CHECKED(UEventTracingSamplingSettings, GDKEventPreFilter)) { - CheckQueryValid(TCHAR_TO_ANSI(*GDKEventPreFilter)); + CheckQueryValid(GDKEventPreFilter); } else if (Name == GET_MEMBER_NAME_CHECKED(UEventTracingSamplingSettings, RuntimeEventPreFilter)) { - CheckQueryValid(TCHAR_TO_ANSI(*RuntimeEventPreFilter)); + CheckQueryValid(RuntimeEventPreFilter); } else if (Name == GET_MEMBER_NAME_CHECKED(UEventTracingSamplingSettings, GDKEventPostFilter)) { - CheckQueryValid(TCHAR_TO_ANSI(*GDKEventPostFilter)); + CheckQueryValid(GDKEventPostFilter); } else if (Name == GET_MEMBER_NAME_CHECKED(UEventTracingSamplingSettings, RuntimeEventPostFilter)) { - CheckQueryValid(TCHAR_TO_ANSI(*RuntimeEventPostFilter)); + CheckQueryValid(RuntimeEventPostFilter); } } #endif diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/OpList/EntityComponentOpList.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/OpList/EntityComponentOpList.cpp index 5201bcbd4d..81ed67ebef 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/OpList/EntityComponentOpList.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/OpList/EntityComponentOpList.cpp @@ -12,14 +12,6 @@ EntityComponentOpListBuilder::EntityComponentOpListBuilder() { } -EntityComponentOpListBuilder EntityComponentOpListBuilder::Move() -{ - EntityComponentOpListBuilder MovedBuilder; - Swap(OpListData, MovedBuilder.OpListData); - - return MoveTemp(MovedBuilder); -} - EntityComponentOpListBuilder& EntityComponentOpListBuilder::AddEntity(Worker_EntityId EntityId) { Worker_Op Op = {}; @@ -101,6 +93,24 @@ EntityComponentOpListBuilder& EntityComponentOpListBuilder::SetDisconnect(Worker return *this; } +EntityComponentOpListBuilder& EntityComponentOpListBuilder::StartCriticalSection() +{ + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_CRITICAL_SECTION; + Op.op.critical_section.in_critical_section = true; + OpListData->Ops.Add(Op); + return *this; +} + +EntityComponentOpListBuilder& EntityComponentOpListBuilder::EndCriticalSection() +{ + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_CRITICAL_SECTION; + Op.op.critical_section.in_critical_section = false; + OpListData->Ops.Add(Op); + return *this; +} + EntityComponentOpListBuilder& EntityComponentOpListBuilder::AddCreateEntityCommandResponse(Worker_EntityId EntityID, Worker_RequestId RequestId, Worker_StatusCode StatusCode, diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/SubView.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/SubView.cpp index 713700dc73..54c511971d 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/SubView.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/SubView.cpp @@ -2,6 +2,7 @@ #include "SpatialView/SubView.h" +#include "Algo/Copy.h" #include "SpatialView/EntityComponentTypes.h" #include "Utils/ComponentFactory.h" @@ -54,6 +55,19 @@ const FSubViewDelta& FSubView::GetViewDelta() const return SubViewDelta; } +const TArray& FSubView::GetCompleteEntities() const +{ + return CompleteEntities; +} + +void FSubView::Refresh() +{ + for (const Worker_EntityId_Key TaggedEntityId : TaggedEntities) + { + CheckEntityAgainstFilter(TaggedEntityId); + } +} + void FSubView::RefreshEntity(const Worker_EntityId EntityId) { if (TaggedEntities.Contains(EntityId)) diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewCoordinator.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewCoordinator.cpp index 490d5988fc..352785c8d9 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewCoordinator.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewCoordinator.cpp @@ -60,11 +60,6 @@ const ViewDelta& ViewCoordinator::GetViewDelta() const return View.GetViewDelta(); } -const EntityView& ViewCoordinator::GetView() const -{ - return View.GetView(); -} - void ViewCoordinator::FlushMessagesToSend() { ConnectionHandler->SendMessages(View.FlushLocalChanges()); @@ -85,6 +80,21 @@ void ViewCoordinator::RefreshEntityCompleteness(Worker_EntityId EntityId) } } +const TArray& ViewCoordinator::GetEntityDeltas() const +{ + return View.GetViewDelta().GetEntityDeltas(); +} + +const TArray& ViewCoordinator::GetWorkerMessages() const +{ + return View.GetViewDelta().GetWorkerMessages(); +} + +const EntityView& ViewCoordinator::GetView() const +{ + return View.GetView(); +} + const ComponentData* ViewCoordinator::GetComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const { const EntityViewElement* EntityDataPtr = GetView().Find(EntityId); @@ -112,47 +122,46 @@ void ViewCoordinator::SendRemoveComponent(Worker_EntityId EntityId, Worker_Compo View.SendRemoveComponent(EntityId, ComponentId, SpanId); } -Worker_RequestId ViewCoordinator::SendReserveEntityIdsRequest(uint32 NumberOfEntityIds, TOptional TimeoutMillis) +Worker_RequestId ViewCoordinator::SendEntityCommandRequest(Worker_EntityId EntityId, CommandRequest Request, FRetryData RetryData, + const FSpatialGDKSpanId& SpanId) { - View.SendReserveEntityIdsRequest({ NextRequestId, NumberOfEntityIds, TimeoutMillis }); + EntityCommandRetryHandler.SendRequest(NextRequestId, { EntityId, MoveTemp(Request), SpanId }, RetryData, View); return NextRequestId++; } -Worker_RequestId ViewCoordinator::SendCreateEntityRequest(TArray EntityComponents, TOptional EntityId, - TOptional TimeoutMillis, const FSpatialGDKSpanId& SpanId) +void ViewCoordinator::SendEntityCommandResponse(Worker_RequestId RequestId, CommandResponse Response, const FSpatialGDKSpanId& SpanId) { - View.SendCreateEntityRequest({ NextRequestId, MoveTemp(EntityComponents), EntityId, TimeoutMillis, SpanId }); - return NextRequestId++; + View.SendEntityCommandResponse({ RequestId, MoveTemp(Response), SpanId }); } -Worker_RequestId ViewCoordinator::SendDeleteEntityRequest(Worker_EntityId EntityId, TOptional TimeoutMillis, - const FSpatialGDKSpanId& SpanId) +void ViewCoordinator::SendEntityCommandFailure(Worker_RequestId RequestId, FString Message, const FSpatialGDKSpanId& SpanId) { - View.SendDeleteEntityRequest({ NextRequestId, EntityId, TimeoutMillis, SpanId }); - return NextRequestId++; + View.SendEntityCommandFailure({ RequestId, MoveTemp(Message), SpanId }); } -Worker_RequestId ViewCoordinator::SendEntityQueryRequest(EntityQuery Query, TOptional TimeoutMillis) +Worker_RequestId ViewCoordinator::SendReserveEntityIdsRequest(uint32 NumberOfEntityIds, FRetryData RetryData) { - View.SendEntityQueryRequest({ NextRequestId, MoveTemp(Query), TimeoutMillis }); + ReserveEntityIdRetryHandler.SendRequest(NextRequestId, NumberOfEntityIds, RetryData, View); return NextRequestId++; } -Worker_RequestId ViewCoordinator::SendEntityCommandRequest(Worker_EntityId EntityId, CommandRequest Request, - TOptional TimeoutMillis, const FSpatialGDKSpanId& SpanId) +Worker_RequestId ViewCoordinator::SendCreateEntityRequest(TArray EntityComponents, TOptional EntityId, + FRetryData RetryData, const FSpatialGDKSpanId& SpanId) { - View.SendEntityCommandRequest({ EntityId, NextRequestId, MoveTemp(Request), TimeoutMillis, SpanId }); + CreateEntityRetryHandler.SendRequest(NextRequestId, { MoveTemp(EntityComponents), EntityId, SpanId }, RetryData, View); return NextRequestId++; } -void ViewCoordinator::SendEntityCommandResponse(Worker_RequestId RequestId, CommandResponse Response, const FSpatialGDKSpanId& SpanId) +Worker_RequestId ViewCoordinator::SendDeleteEntityRequest(Worker_EntityId EntityId, FRetryData RetryData, const FSpatialGDKSpanId& SpanId) { - View.SendEntityCommandResponse({ RequestId, MoveTemp(Response), SpanId }); + DeleteEntityRetryHandler.SendRequest(NextRequestId, { EntityId, SpanId }, RetryData, View); + return NextRequestId++; } -void ViewCoordinator::SendEntityCommandFailure(Worker_RequestId RequestId, FString Message, const FSpatialGDKSpanId& SpanId) +Worker_RequestId ViewCoordinator::SendEntityQueryRequest(EntityQuery Query, FRetryData RetryData) { - View.SendEntityCommandFailure({ RequestId, MoveTemp(Message), SpanId }); + EntityQueryRetryHandler.SendRequest(NextRequestId, MoveTemp(Query), RetryData, View); + return NextRequestId++; } void ViewCoordinator::SendMetrics(SpatialMetrics Metrics) @@ -165,36 +174,27 @@ void ViewCoordinator::SendLogMessage(Worker_LogLevel Level, const FName& LoggerN View.SendLogMessage({ Level, LoggerName, MoveTemp(Message) }); } -Worker_RequestId ViewCoordinator::SendReserveEntityIdsRequest(uint32 NumberOfEntityIds, FRetryData RetryData) -{ - ReserveEntityIdRetryHandler.SendRequest(NextRequestId, NumberOfEntityIds, RetryData, View); - return NextRequestId++; -} - -Worker_RequestId ViewCoordinator::SendCreateEntityRequest(TArray EntityComponents, TOptional EntityId, - FRetryData RetryData, const FSpatialGDKSpanId& SpanId) -{ - CreateEntityRetryHandler.SendRequest(NextRequestId, { MoveTemp(EntityComponents), EntityId, SpanId }, RetryData, View); - return NextRequestId++; -} - -Worker_RequestId ViewCoordinator::SendDeleteEntityRequest(Worker_EntityId EntityId, FRetryData RetryData, const FSpatialGDKSpanId& SpanId) +bool ViewCoordinator::HasEntity(Worker_EntityId EntityId) const { - DeleteEntityRetryHandler.SendRequest(NextRequestId, { EntityId, SpanId }, RetryData, View); - return NextRequestId++; + return View.GetView().Contains(EntityId); } -Worker_RequestId ViewCoordinator::SendEntityQueryRequest(EntityQuery Query, FRetryData RetryData) +bool ViewCoordinator::HasComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const { - EntityQueryRetryHandler.SendRequest(NextRequestId, MoveTemp(Query), RetryData, View); - return NextRequestId++; + if (const EntityViewElement* Element = View.GetView().Find(EntityId)) + { + return Element->Components.ContainsByPredicate(ComponentIdEquality{ ComponentId }); + } + return false; } -Worker_RequestId ViewCoordinator::SendEntityCommandRequest(Worker_EntityId EntityId, CommandRequest Request, FRetryData RetryData, - const FSpatialGDKSpanId& SpanId) +bool ViewCoordinator::HasAuthority(Worker_EntityId EntityId, Worker_ComponentSetId ComponentSetId) const { - EntityCommandRetryHandler.SendRequest(NextRequestId, { EntityId, MoveTemp(Request), SpanId }, RetryData, View); - return NextRequestId++; + if (const EntityViewElement* Element = View.GetView().Find(EntityId)) + { + return Element->Authority.Contains(ComponentSetId); + } + return false; } CallbackId ViewCoordinator::RegisterComponentAddedCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback) @@ -250,29 +250,6 @@ FDispatcherRefreshCallback ViewCoordinator::CreateAuthorityChangeRefreshCallback return FSubView::CreateAuthorityChangeRefreshCallback(Dispatcher, ComponentId, RefreshPredicate); } -bool ViewCoordinator::HasEntity(Worker_EntityId EntityId) const -{ - return View.GetView().Contains(EntityId); -} - -bool ViewCoordinator::HasComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const -{ - if (const EntityViewElement* Element = View.GetView().Find(EntityId)) - { - return Element->Components.ContainsByPredicate(ComponentIdEquality{ ComponentId }); - } - return false; -} - -bool ViewCoordinator::HasAuthority(Worker_EntityId EntityId, Worker_ComponentSetId ComponentSetId) const -{ - if (const EntityViewElement* Element = View.GetView().Find(EntityId)) - { - return Element->Authority.Contains(ComponentSetId); - } - return false; -} - const FString& ViewCoordinator::GetWorkerId() const { return ConnectionHandler->GetWorkerId(); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/ActorSubviewsTests.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/ActorSubviewsTests.cpp new file mode 100644 index 0000000000..73e5c570f6 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/ActorSubviewsTests.cpp @@ -0,0 +1,115 @@ +#include "Interop/ActorSubviews.h" +#include "Interop/OwnershipCompletenessHandler.h" +#include "Schema/ActorOwnership.h" +#include "SpatialView/ViewCoordinator.h" + +#include "SpatialView/OpList/EntityComponentOpList.h" +#include "Tests/SpatialView/SpatialViewUtils.h" +#include "Tests/SpatialView/TargetView.h" +#include "Tests/SpatialView/TestWorker.h" +#include "Tests/TestDefinitions.h" + +namespace SpatialGDK +{ +struct FOwnershipCompletenessTestFixture +{ + FOwnershipCompletenessTestFixture() + : Worker(FTestWorker::Create({ + SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID, + SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID, + })) + , OwnershipCompletenessHandler(FOwnershipCompletenessHandler::CreateClientOwnershipHandler()) + , OwnershipSubview(ActorSubviews::CreatePlayerOwnershipSubView(Worker.GetCoordinator(), OwnershipCompletenessHandler)) + , SimulatedSubview(ActorSubviews::CreateSimulatedSubView(Worker.GetCoordinator(), OwnershipCompletenessHandler)) + { + } + +public: + FTestWorker Worker; + FOwnershipCompletenessHandler OwnershipCompletenessHandler; + FSubView& OwnershipSubview; + FSubView& SimulatedSubview; +}; + +GDK_TEST(Core, OwnershipCompleteness, PlayerGainsEntityOwnership) +{ + using namespace ActorSubviews; + + constexpr Worker_EntityId ActorEntityId = 1; + constexpr Worker_EntityId LocalClientControllerEntityId = 2; + + FOwnershipCompletenessTestFixture Fixture; + + Fixture.OwnershipCompletenessHandler.AddPlayerEntity(LocalClientControllerEntityId); + + { + Fixture.Worker.GetTargetView().AddEntity(ActorEntityId); + Fixture.Worker.GetTargetView().AddOrSetComponent(ActorEntityId, ComponentData(SpatialConstants::ACTOR_TAG_COMPONENT_ID)); + Fixture.Worker.GetTargetView().AddOrSetComponent(ActorEntityId, + ActorOwnership(LocalClientControllerEntityId).CreateComponentData()); + } + + constexpr float ViewAdvanceDeltaTime = 1.0f; + Fixture.Worker.AdvanceToTargetView(ViewAdvanceDeltaTime); + + TestFalse(TEXT("Entity was player ownership incomplete when considering ownership"), + Fixture.OwnershipSubview.IsEntityComplete(ActorEntityId)); + TestFalse(TEXT("Entity was simulated incomplete when considering ownership"), Fixture.SimulatedSubview.IsEntityComplete(ActorEntityId)); + + { + Fixture.Worker.GetTargetView().AddOrSetComponent(ActorEntityId, + ComponentData(SpatialConstants::ACTOR_OWNER_ONLY_DATA_TAG_COMPONENT_ID)); + Fixture.Worker.GetTargetView().AddOrSetComponent(ActorEntityId, ComponentData(SpatialConstants::ACTOR_AUTH_TAG_COMPONENT_ID)); + Fixture.Worker.GetTargetView().AddAuthority(ActorEntityId, SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID); + } + + Fixture.Worker.AdvanceToTargetView(ViewAdvanceDeltaTime); + + TestTrue(TEXT("Entity became player ownership complete"), Fixture.OwnershipSubview.IsEntityComplete(ActorEntityId)); + TestFalse(TEXT("Entity stayed simulated incomplete"), Fixture.SimulatedSubview.IsEntityComplete(ActorEntityId)); + + return true; +} + +GDK_TEST(Core, OwnershipCompleteness, PlayerLosesEntityOwnership) +{ + using namespace ActorSubviews; + + constexpr Worker_EntityId ActorEntityId = 1; + constexpr Worker_EntityId LocalClientControllerEntityId = 2; + constexpr Worker_EntityId RemoteClientControllerEntityId = 3; + + FOwnershipCompletenessTestFixture Fixture; + + Fixture.OwnershipCompletenessHandler.AddPlayerEntity(LocalClientControllerEntityId); + + { + Fixture.Worker.GetTargetView().AddEntity(ActorEntityId); + Fixture.Worker.GetTargetView().AddOrSetComponent(ActorEntityId, ComponentData(SpatialConstants::ACTOR_TAG_COMPONENT_ID)); + Fixture.Worker.GetTargetView().AddOrSetComponent(ActorEntityId, + ActorOwnership(LocalClientControllerEntityId).CreateComponentData()); + Fixture.Worker.GetTargetView().AddOrSetComponent(ActorEntityId, + ComponentData(SpatialConstants::ACTOR_OWNER_ONLY_DATA_TAG_COMPONENT_ID)); + Fixture.Worker.GetTargetView().AddOrSetComponent(ActorEntityId, ComponentData(SpatialConstants::ACTOR_AUTH_TAG_COMPONENT_ID)); + Fixture.Worker.GetTargetView().AddAuthority(ActorEntityId, SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID); + } + + constexpr float ViewAdvanceDeltaTime = 1.0f; + Fixture.Worker.AdvanceToTargetView(ViewAdvanceDeltaTime); + + TestTrue(TEXT("Entity was player ownership complete"), Fixture.OwnershipSubview.IsEntityComplete(ActorEntityId)); + TestFalse(TEXT("Entity was simulated incomplete"), Fixture.SimulatedSubview.IsEntityComplete(ActorEntityId)); + + { + Fixture.Worker.GetTargetView().UpdateComponent(ActorEntityId, + ActorOwnership(RemoteClientControllerEntityId).CreateComponentUpdate()); + } + + Fixture.Worker.AdvanceToTargetView(ViewAdvanceDeltaTime); + + TestFalse(TEXT("Entity became player ownership incomplete"), Fixture.OwnershipSubview.IsEntityComplete(ActorEntityId)); + TestFalse(TEXT("Entity stayed simulated incomplete"), Fixture.SimulatedSubview.IsEntityComplete(ActorEntityId)); + + return true; +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/RPCServiceTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/RPCServiceTest.cpp index 10d1c151cb..5509732e3f 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Tests/RPCServiceTest.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/RPCServiceTest.cpp @@ -64,6 +64,9 @@ SpatialGDK::FSubView AuthSubView = SpatialGDK::FSubView(SpatialConstants::ACTOR_ &TestView, TestDispatcher, SpatialGDK::FSubView::NoDispatcherCallbacks); SpatialGDK::FSubView NonAuthSubView = SpatialGDK::FSubView(SpatialConstants::ACTOR_TAG_COMPONENT_ID, SpatialGDK::FSubView::NoFilter, &TestView, TestDispatcher, SpatialGDK::FSubView::NoDispatcherCallbacks); +SpatialGDK::FSubView WorkerEntitySubView = + SpatialGDK::FSubView(SpatialConstants::ROUTINGWORKER_TAG_COMPONENT_ID, SpatialGDK::FSubView::NoFilter, &TestView, TestDispatcher, + SpatialGDK::FSubView::NoDispatcherCallbacks); FTimerManager Timer; SpatialGDK::ComponentData MakeComponentDataFromData(Schema_ComponentData* Data, const Worker_ComponentId ComponentId) @@ -140,7 +143,11 @@ SpatialGDK::SpatialRPCService CreateRPCService(const TArray& En SpatialGDK::FSubView::NoDispatcherCallbacks); NonAuthSubView = SpatialGDK::FSubView(SpatialConstants::ACTOR_TAG_COMPONENT_ID, SpatialGDK::FSubView::NoFilter, &View, TestDispatcher, SpatialGDK::FSubView::NoDispatcherCallbacks); - SpatialGDK::SpatialRPCService RPCService = SpatialGDK::SpatialRPCService(AuthSubView, NonAuthSubView, nullptr, nullptr, nullptr); + WorkerEntitySubView = SpatialGDK::FSubView(SpatialConstants::ROUTINGWORKER_TAG_COMPONENT_ID, SpatialGDK::FSubView::NoFilter, &View, + TestDispatcher, SpatialGDK::FSubView::NoDispatcherCallbacks); + + SpatialGDK::SpatialRPCService RPCService = + SpatialGDK::SpatialRPCService(AuthSubView, NonAuthSubView, WorkerEntitySubView, nullptr, nullptr, nullptr); const SpatialGDK::ViewDelta Delta; AuthSubView.Advance(Delta); @@ -200,90 +207,6 @@ FWorkerComponentData GetComponentDataOnEntityCreationFromRPCService(SpatialGDK:: } // anonymous namespace -RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_client_reliable_rpcs_to_the_service_THEN_rpc_push_result_success) -{ - SpatialGDK::EntityView View; - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH, View); - const SpatialGDK::EPushRPCResult Result = - RPCService.PushRPC(RPCTestEntityId_1, SpatialGDK::RPCSender(), ERPCType::ClientReliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::Success); - return true; -} - -RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_client_unreliable_rpcs_to_the_service_THEN_rpc_push_result_success) -{ - SpatialGDK::EntityView View; - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH, View); - const SpatialGDK::EPushRPCResult Result = - RPCService.PushRPC(RPCTestEntityId_1, SpatialGDK::RPCSender(), ERPCType::ClientUnreliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::Success); - return true; -} - -RPC_SERVICE_TEST( - GIVEN_authority_over_server_endpoint_WHEN_push_server_reliable_rpcs_to_the_service_THEN_rpc_push_result_no_buffer_authority) -{ - SpatialGDK::EntityView View; - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH, View); - const SpatialGDK::EPushRPCResult Result = - RPCService.PushRPC(RPCTestEntityId_1, SpatialGDK::RPCSender(), ERPCType::ServerReliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority); - return true; -} - -RPC_SERVICE_TEST( - GIVEN_authority_over_server_endpoint_WHEN_push_server_unreliable_rpcs_to_the_service_THEN_rpc_push_result_no_buffer_authority) -{ - SpatialGDK::EntityView View; - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH, View); - const SpatialGDK::EPushRPCResult Result = - RPCService.PushRPC(RPCTestEntityId_1, SpatialGDK::RPCSender(), ERPCType::ServerUnreliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority); - return true; -} - -RPC_SERVICE_TEST( - GIVEN_authority_over_client_endpoint_WHEN_push_client_reliable_rpcs_to_the_service_THEN_rpc_push_result_no_buffer_authority) -{ - SpatialGDK::EntityView View; - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH, View); - const SpatialGDK::EPushRPCResult Result = - RPCService.PushRPC(RPCTestEntityId_1, SpatialGDK::RPCSender(), ERPCType::ClientReliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority); - return true; -} - -RPC_SERVICE_TEST( - GIVEN_authority_over_client_endpoint_WHEN_push_client_unreliable_rpcs_to_the_service_THEN_rpc_push_result_no_buffer_authority) -{ - SpatialGDK::EntityView View; - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH, View); - const SpatialGDK::EPushRPCResult Result = - RPCService.PushRPC(RPCTestEntityId_1, SpatialGDK::RPCSender(), ERPCType::ClientUnreliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority); - return true; -} - -RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_server_reliable_rpcs_to_the_service_THEN_rpc_push_result_success) -{ - SpatialGDK::EntityView View; - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH, View); - const SpatialGDK::EPushRPCResult Result = - RPCService.PushRPC(RPCTestEntityId_1, SpatialGDK::RPCSender(), ERPCType::ServerReliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::Success); - return true; -} - -RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_server_unreliable_rpcs_to_the_service_THEN_rpc_push_result_success) -{ - SpatialGDK::EntityView View; - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH, View); - const SpatialGDK::EPushRPCResult Result = - RPCService.PushRPC(RPCTestEntityId_1, SpatialGDK::RPCSender(), ERPCType::ServerUnreliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::Success); - return true; -} - RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_multicast_rpcs_to_the_service_THEN_rpc_push_result_no_buffer_authority) { SpatialGDK::EntityView View; @@ -304,92 +227,6 @@ RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_multicast_rpcs_t return true; } -RPC_SERVICE_TEST(GIVEN_authority_over_server_and_client_endpoint_WHEN_push_rpcs_to_the_service_THEN_rpc_push_result_has_ack_authority) -{ - SpatialGDK::EntityView View; - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AND_CLIENT_AUTH, View); - const SpatialGDK::EPushRPCResult Result = - RPCService.PushRPC(RPCTestEntityId_1, SpatialGDK::RPCSender(), ERPCType::ClientReliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::HasAckAuthority); - return true; -} - -RPC_SERVICE_TEST( - GIVEN_authority_over_server_endpoint_WHEN_push_overflow_client_reliable_rpcs_to_the_service_THEN_rpc_push_result_queue_overflowed) -{ - SpatialGDK::EntityView View; - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH, View); - - // Send RPCs to the point where we will overflow - const uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ClientReliable); - for (uint32 i = 0; i < RPCsToSend; ++i) - { - RPCService.PushRPC(RPCTestEntityId_1, SpatialGDK::RPCSender(), ERPCType::ClientReliable, SimplePayload, false); - } - - const SpatialGDK::EPushRPCResult Result = - RPCService.PushRPC(RPCTestEntityId_1, SpatialGDK::RPCSender(), ERPCType::ClientReliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::QueueOverflowed); - return true; -} - -RPC_SERVICE_TEST( - GIVEN_authority_over_server_endpoint_WHEN_push_overflow_client_unreliable_rpcs_to_the_service_THEN_rpc_push_result_drop_overflow) -{ - SpatialGDK::EntityView View; - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH, View); - - // Send RPCs to the point where we will overflow - const uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ClientUnreliable); - for (uint32 i = 0; i < RPCsToSend; ++i) - { - RPCService.PushRPC(RPCTestEntityId_1, SpatialGDK::RPCSender(), ERPCType::ClientUnreliable, SimplePayload, false); - } - - const SpatialGDK::EPushRPCResult Result = - RPCService.PushRPC(RPCTestEntityId_1, SpatialGDK::RPCSender(), ERPCType::ClientUnreliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::DropOverflowed); - return true; -} - -RPC_SERVICE_TEST( - GIVEN_authority_over_client_endpoint_WHEN_push_overflow_client_reliable_rpcs_to_the_service_THEN_rpc_push_result_queue_overflowed) -{ - SpatialGDK::EntityView View; - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH, View); - - // Send RPCs to the point where we will overflow - const uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ServerReliable); - for (uint32 i = 0; i < RPCsToSend; ++i) - { - RPCService.PushRPC(RPCTestEntityId_1, SpatialGDK::RPCSender(), ERPCType::ServerReliable, SimplePayload, false); - } - - const SpatialGDK::EPushRPCResult Result = - RPCService.PushRPC(RPCTestEntityId_1, SpatialGDK::RPCSender(), ERPCType::ServerReliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::QueueOverflowed); - return true; -} - -RPC_SERVICE_TEST( - GIVEN_authority_over_client_endpoint_WHEN_push_overflow_client_unreliable_rpcs_to_the_service_THEN_rpc_push_result_drop_overflow) -{ - SpatialGDK::EntityView View; - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH, View); - - // Send RPCs to the point where we will overflow - const uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ServerUnreliable); - for (uint32 i = 0; i < RPCsToSend; ++i) - { - RPCService.PushRPC(RPCTestEntityId_1, SpatialGDK::RPCSender(), ERPCType::ServerUnreliable, SimplePayload, false); - } - - const SpatialGDK::EPushRPCResult Result = - RPCService.PushRPC(RPCTestEntityId_1, SpatialGDK::RPCSender(), ERPCType::ServerUnreliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::DropOverflowed)); - return true; -} - RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_overflow_multicast_rpcs_to_the_service_THEN_rpc_push_result_success) { SpatialGDK::EntityView View; @@ -408,64 +245,6 @@ RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_overflow_multica return true; } -RPC_SERVICE_TEST( - GIVEN_authority_over_client_endpoint_WHEN_push_server_unreliable_rpcs_to_the_service_THEN_payloads_are_written_correctly_to_component_updates) -{ - SpatialGDK::EntityView View; - TArray EntityPayloads; - EntityPayloads.Add(EntityPayload(RPCTestEntityId_1, SimplePayload)); - EntityPayloads.Add(EntityPayload(RPCTestEntityId_2, SimplePayload)); - - TArray EntityIdArray; - EntityIdArray.Add(RPCTestEntityId_1); - EntityIdArray.Add(RPCTestEntityId_2); - - SpatialGDK::SpatialRPCService RPCService = CreateRPCService(EntityIdArray, CLIENT_AUTH, View); - for (const EntityPayload& EntityPayloadItem : EntityPayloads) - { - RPCService.PushRPC(EntityPayloadItem.EntityId, SpatialGDK::RPCSender(), ERPCType::ServerUnreliable, EntityPayloadItem.Payload, - false); - } - - TArray UpdateToSendArray = RPCService.GetRPCsAndAcksToSend(); - - bool bTestPassed = true; - if (UpdateToSendArray.Num() != EntityPayloads.Num()) - { - bTestPassed = false; - } - else - { - for (int i = 0; i < EntityPayloads.Num(); ++i) - { - if (!CompareUpdateToSendAndEntityPayload(UpdateToSendArray[i], EntityPayloads[i], ERPCType::ServerUnreliable, 1)) - { - bTestPassed = false; - break; - } - } - } - - TestTrue("UpdateToSend have expected payloads", bTestPassed); - return true; -} - -RPC_SERVICE_TEST(GIVEN_no_authority_over_rpc_endpoint_WHEN_push_client_reliable_rpcs_to_the_service_THEN_component_data_matches_payload) -{ - SpatialGDK::EntityView View; - // Create RPCService with empty component view - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({}, NO_AUTH, View); - - RPCService.PushRPC(RPCTestEntityId_1, SpatialGDK::RPCSender(), ERPCType::ClientReliable, SimplePayload, false); - - const FWorkerComponentData ComponentData = - GetComponentDataOnEntityCreationFromRPCService(RPCService, RPCTestEntityId_1, ERPCType::ClientReliable); - const bool bTestPassed = - CompareComponentDataAndEntityPayload(ComponentData, EntityPayload(RPCTestEntityId_1, SimplePayload), ERPCType::ClientReliable, 1); - TestTrue("Entity creation test returned expected results", bTestPassed); - return true; -} - RPC_SERVICE_TEST(GIVEN_no_authority_over_rpc_endpoint_WHEN_push_multicast_rpcs_to_the_service_THEN_initially_present_set) { SpatialGDK::EntityView View; diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/RPCs/RPCRingBufferTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/RPCs/RPCRingBufferTest.cpp new file mode 100644 index 0000000000..6e909a35f9 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/RPCs/RPCRingBufferTest.cpp @@ -0,0 +1,323 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/TestDefinitions.h" + +#include "Interop/RPCs/RPCQueues.h" +#include "Interop/RPCs/RPC_RingBufferWithACK_Receiver.h" +#include "Interop/RPCs/RPC_RingBufferWithACK_Sender.h" + +#define RPCRINGBUFFER_TEST(TestName) GDK_TEST(Core, RPCRingBuffer, TestName) + +namespace RPCRingBufferTestPrivate +{ +using namespace SpatialGDK; + +struct Payload +{ + Payload() = default; + Payload(uint32 Id) + : Identifier(Id) + , ValidRPC(true) + { + } + + uint32 Identifier; + bool ValidRPC = false; +}; + +struct RingBufferComponent +{ + static constexpr int32 Capacity = 32; + RingBufferComponent() { Buffer.SetNum(Capacity); } + TArray Buffer; + uint32 CountWritten = 0; +}; + +struct ACKComponent +{ + uint32 ACKCount = 0; +}; + +struct EntityComponentMock +{ + TMap Buffers; + TMap ACKs; +}; + +constexpr Worker_ComponentId BufferComponentId = 1; +constexpr Worker_ComponentId ACKComponentId = 2; + +struct Serializer +{ +public: + enum LocalRole + { + Reader, + Writer + }; + + Serializer(EntityComponentMock& InData, LocalRole InRole) + : Data(InData) + , Role(InRole) + { + } + + Worker_ComponentId GetComponentId() { return BufferComponentId; } + + Worker_ComponentId GetACKComponentId() { return ACKComponentId; } + + TOptional ReadRPCCount(const RPCReadingContext& Ctx) { return GetBufferComponent(Ctx.EntityId).CountWritten; } + + TOptional ReadACKCount(const RPCReadingContext& Ctx) { return GetACKComponent(Ctx.EntityId).ACKCount; } + + void ReadRPC(const RPCReadingContext& Ctx, uint32 Slot, Payload& OutPayload) + { + OutPayload = GetBufferComponent(Ctx.EntityId).Buffer[Slot]; + } + + void WriteRPC(RPCWritingContext::EntityWrite& Ctx, uint32 Slot, const Payload& InPayload) + { + check(Role == Writer); + GetBufferComponent(Ctx.EntityId).Buffer[Slot] = InPayload; + } + + void WriteRPCCount(RPCWritingContext::EntityWrite& Ctx, uint64 Count) + { + check(Role == Writer); + GetBufferComponent(Ctx.EntityId).CountWritten = Count; + } + + void WriteACKCount(RPCWritingContext::EntityWrite& Ctx, uint64 Count) + { + check(Role == Reader); + GetACKComponent(Ctx.EntityId).ACKCount = Count; + } + +private: + RingBufferComponent& GetBufferComponent(Worker_EntityId EntityId) { return Data.Buffers[EntityId]; } + + ACKComponent& GetACKComponent(Worker_EntityId EntityId) { return Data.ACKs[EntityId]; } + + EntityComponentMock& Data; + LocalRole Role; +}; + +using Sender = MonotonicRingBufferWithACKSender; +using Receiver = MonotonicRingBufferWithACKReceiver; +using UnboundedQueue = TRPCUnboundedQueue; + +struct RPCRingBufferTest_Fixture +{ + EntityComponentMock Data; + + Sender ServerWorker; + Receiver ClientWorker; + UnboundedQueue ServerQueue; + + RPCRingBufferTest_Fixture(uint32 BufferSize) + : ServerWorker(Serializer(Data, Serializer::Writer), BufferSize) + , ClientWorker(Serializer(Data, Serializer::Reader), BufferSize, NullReceiveWrapper()) + , ServerQueue(FName(TEXT("DummyQueue")), ServerWorker) + { + } + + void AddEntity(Worker_EntityId EntityId) + { + Data.Buffers.Add(EntityId); + Data.ACKs.Add(EntityId); + EntityViewElement Element; + Element.Components.Add(ComponentData(BufferComponentId)); + Element.Components.Add(ComponentData(ACKComponentId)); + ServerWorker.OnAuthGained(EntityId, Element); + ServerQueue.OnAuthGained(EntityId, Element); + ClientWorker.OnAdded(FName(TEXT("DummyName")), EntityId, Element); + } +}; + +RPCRINGBUFFER_TEST(TestRingBufferRPCCapacityUpdate) +{ + const uint32 BufferSize = 1; + RPCRingBufferTest_Fixture Fixture(BufferSize); + + const Worker_EntityId Entity = 1; + + Fixture.AddEntity(Entity); + Fixture.ServerQueue.Push(Entity, Payload(1)); + + bool bSentRPC = false; + auto SentRPCCallback = [&bSentRPC](FName, Worker_EntityId, Worker_ComponentId, uint64, const RPCEmptyData&) { + bSentRPC = true; + }; + + RPCWritingContext WritingCtx(Fixture.ServerQueue.Name, RPCCallbacks::UpdateWritten()); + Fixture.ServerQueue.FlushAll(WritingCtx, SentRPCCallback); + + TestTrue(TEXT("RPC Sent with available capacity"), bSentRPC); + bSentRPC = false; + + Fixture.ServerQueue.FlushAll(WritingCtx, SentRPCCallback); + TestFalse(TEXT("RPC Sent only once"), bSentRPC); + + auto CanExtractCallback = [](Worker_EntityId) { + return true; + }; + bool bExtractedRPC = false; + auto ExtractCallback = [this, &bExtractedRPC](Worker_EntityId, const Payload& Data, const RPCEmptyData&) { + bExtractedRPC = true; + return true; + }; + + Fixture.ClientWorker.ExtractReceivedRPCs(CanExtractCallback, ExtractCallback); + TestFalse(TEXT("No extraction done before update"), bExtractedRPC); + + RPCReadingContext BufferUpdateReadingCtx; + BufferUpdateReadingCtx.EntityId = Entity; + BufferUpdateReadingCtx.ComponentId = BufferComponentId; + + RPCReadingContext ACKUpdateReadingCtx; + ACKUpdateReadingCtx.EntityId = Entity; + ACKUpdateReadingCtx.ComponentId = ACKComponentId; + + Fixture.ClientWorker.OnUpdate(BufferUpdateReadingCtx); + Fixture.ClientWorker.ExtractReceivedRPCs(CanExtractCallback, ExtractCallback); + TestTrue(TEXT("Extracted RPC after update"), bExtractedRPC); + bExtractedRPC = false; + + Fixture.ServerQueue.Push(Entity, Payload(1)); + + Fixture.ServerQueue.FlushAll(WritingCtx, SentRPCCallback); + TestFalse(TEXT("No RPC sent without capacity"), bSentRPC); + + Fixture.ClientWorker.OnUpdate(BufferUpdateReadingCtx); + Fixture.ClientWorker.ExtractReceivedRPCs(CanExtractCallback, ExtractCallback); + TestFalse(TEXT("No writes before ACK sent"), bExtractedRPC); + + Fixture.ClientWorker.FlushUpdates(WritingCtx); + + Fixture.ServerQueue.FlushAll(WritingCtx, SentRPCCallback); + TestFalse(TEXT("No RPC sent without capacity"), bSentRPC); + + Fixture.ServerWorker.OnUpdate(ACKUpdateReadingCtx); + Fixture.ServerQueue.FlushAll(WritingCtx, SentRPCCallback); + TestTrue(TEXT("RPC sent after receiving ACK"), bSentRPC); + + Fixture.ClientWorker.OnUpdate(BufferUpdateReadingCtx); + Fixture.ClientWorker.ExtractReceivedRPCs(CanExtractCallback, ExtractCallback); + TestTrue(TEXT("Extracted second RPC after ACK"), bExtractedRPC); + + return true; +} + +RPCRINGBUFFER_TEST(TestRingBufferPartialExtractionAndOverflow) +{ + const uint32 BufferSize = 4; + RPCRingBufferTest_Fixture Fixture(BufferSize); + + const Worker_EntityId EntityForTest = 1; + + Fixture.AddEntity(EntityForTest); + Fixture.AddEntity(EntityForTest + 1); + + bool bQueueOverflowed = false; + auto QueueErrorCallback = [&bQueueOverflowed, EntityForTest, this](FName, Worker_EntityId Entity, QueueError Error) { + TestEqual(TEXT("Test right entity is reported"), Entity, EntityForTest); + TestEqual(TEXT("Test Queue error is overflow"), Error, QueueError::BufferOverflow); + bQueueOverflowed = true; + }; + Fixture.ServerQueue.SetErrorCallback(QueueErrorCallback); + + int32 RpcId = 1; + + for (uint32 i = 0; i < BufferSize; ++i) + { + Fixture.ServerQueue.Push(EntityForTest, Payload(RpcId++)); + } + TestFalse(TEXT("Queue did not overflow 1"), bQueueOverflowed); + + RPCWritingContext WritingCtx(Fixture.ServerQueue.Name, RPCCallbacks::UpdateWritten()); + Fixture.ServerQueue.FlushAll(WritingCtx); + + auto CanExtractCallback = [](Worker_EntityId) { + return true; + }; + + int32 ExpectedRpcId = 1; + int32 Extracted = 0; + int32 NumRPCToExtract = BufferSize / 2; + auto ExtractCallback = [this, &NumRPCToExtract, &Extracted, &ExpectedRpcId, EntityForTest](Worker_EntityId Entity, const Payload& Data, + const RPCEmptyData&) { + if (Extracted >= NumRPCToExtract) + { + return false; + } + TestEqual(TEXT("Test right entity has received"), Entity, EntityForTest); + TestTrue(TEXT("Received a valid RPC"), Data.ValidRPC); + TestEqual(TEXT("Read RPC in the expected order"), Data.Identifier, ExpectedRpcId); + ++ExpectedRpcId; + ++Extracted; + return true; + }; + + RPCReadingContext BufferUpdateReadingCtx; + BufferUpdateReadingCtx.EntityId = EntityForTest; + BufferUpdateReadingCtx.ComponentId = BufferComponentId; + + RPCReadingContext ACKUpdateReadingCtx; + ACKUpdateReadingCtx.EntityId = EntityForTest; + ACKUpdateReadingCtx.ComponentId = ACKComponentId; + + Fixture.ClientWorker.OnUpdate(BufferUpdateReadingCtx); + Fixture.ClientWorker.ExtractReceivedRPCs(CanExtractCallback, ExtractCallback); + TestEqual(TEXT("Partial extraction step 1 (1/2 buffer size)"), Extracted, BufferSize / 2); + Extracted = 0; + + Fixture.ClientWorker.ExtractReceivedRPCs(CanExtractCallback, ExtractCallback); + TestEqual(TEXT("Partial extraction step 2 (1 buffer size)"), Extracted, BufferSize / 2); + Extracted = 0; + + Fixture.ClientWorker.FlushUpdates(WritingCtx); + Fixture.ServerWorker.OnUpdate(ACKUpdateReadingCtx); + for (uint32 i = 0; i < BufferSize; ++i) + { + Fixture.ServerQueue.Push(EntityForTest, Payload(RpcId++)); + } + TestFalse(TEXT("Queue did not overflow 2"), bQueueOverflowed); + + NumRPCToExtract += BufferSize; + Fixture.ServerQueue.FlushAll(WritingCtx); + Fixture.ClientWorker.OnUpdate(BufferUpdateReadingCtx); + Fixture.ClientWorker.ExtractReceivedRPCs(CanExtractCallback, ExtractCallback); + TestEqual(TEXT("Partial extraction step 3 (2 buffer size)"), Extracted, BufferSize); + Extracted = 0; + + Fixture.ClientWorker.FlushUpdates(WritingCtx); + Fixture.ServerWorker.OnUpdate(ACKUpdateReadingCtx); + // Write 6 additional RPC, 2 will overflow + for (uint32 i = 0; i < BufferSize * 1.5; ++i) + { + Fixture.ServerQueue.Push(EntityForTest, Payload(RpcId++)); + } + Fixture.ServerQueue.FlushAll(WritingCtx); + TestTrue(TEXT("Queue did overflow"), bQueueOverflowed); + bQueueOverflowed = false; + + Fixture.ClientWorker.OnUpdate(BufferUpdateReadingCtx); + Fixture.ClientWorker.ExtractReceivedRPCs(CanExtractCallback, ExtractCallback); + TestEqual(TEXT("Full extraction step 4 (3 buffer size)"), Extracted, BufferSize); + Extracted = 0; + + Fixture.ClientWorker.FlushUpdates(WritingCtx); + Fixture.ServerWorker.OnUpdate(ACKUpdateReadingCtx); + + Fixture.ServerQueue.FlushAll(WritingCtx); + TestFalse(TEXT("Queue did not overflow 3"), bQueueOverflowed); + + Fixture.ClientWorker.OnUpdate(BufferUpdateReadingCtx); + Fixture.ClientWorker.ExtractReceivedRPCs(CanExtractCallback, ExtractCallback); + TestEqual(TEXT("Full extraction step 5 (3.5 buffer size)"), Extracted, BufferSize / 2); + Extracted = 0; + + return true; +} + +} // namespace RPCRingBufferTestPrivate diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/TargetView.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/TargetView.cpp new file mode 100644 index 0000000000..97062d45ad --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/TargetView.cpp @@ -0,0 +1,155 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/SpatialView/TargetView.h" + +#include "SpatialView/EntityComponentTypes.h" + +namespace SpatialGDK +{ +void FTargetView::AddEntity(Worker_EntityId EntityId) +{ + check(!bDisconnected); + Builder.AddEntity(EntityId); + View.Emplace(EntityId); +} + +void FTargetView::RemoveEntity(Worker_EntityId EntityId) +{ + check(!bDisconnected); + // Make sure ops are generated to remove all authority and components for the removed entity. + EntityViewElement EntityData = View.FindAndRemoveChecked(EntityId); + for (const Worker_ComponentSetId& SetId : EntityData.Authority) + { + Builder.SetAuthority(EntityId, SetId, WORKER_AUTHORITY_NOT_AUTHORITATIVE, {}); + } + for (const ComponentData& Component : EntityData.Components) + { + Builder.RemoveComponent(EntityId, Component.GetComponentId()); + } + Builder.RemoveEntity(EntityId); +} + +void FTargetView::AddOrSetComponent(Worker_EntityId EntityId, ComponentData Data) +{ + check(!bDisconnected); + EntityViewElement* EntityData = View.Find(EntityId); + if (EntityData == nullptr) + { + Builder.AddEntity(EntityId); + EntityData = &View.Emplace(EntityId); + } + ComponentData* Component = EntityData->Components.FindByPredicate(ComponentIdEquality{ Data.GetComponentId() }); + if (Component != nullptr) + { + *Component = Data.DeepCopy(); + } + else + { + EntityData->Components.Add(Data.DeepCopy()); + } + Builder.AddComponent(EntityId, MoveTemp(Data)); +} + +void FTargetView::UpdateComponent(Worker_EntityId EntityId, ComponentUpdate Update) +{ + check(!bDisconnected); + EntityViewElement& EntityData = View.FindChecked(EntityId); + ComponentData* Component = EntityData.Components.FindByPredicate(ComponentIdEquality{ Update.GetComponentId() }); + check(Component != nullptr); + Component->ApplyUpdate(Update); + Builder.UpdateComponent(EntityId, MoveTemp(Update)); +} + +void FTargetView::RemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) +{ + check(!bDisconnected); + EntityViewElement& EntityData = View.FindChecked(EntityId); + ComponentData* Component = EntityData.Components.FindByPredicate(ComponentIdEquality{ ComponentId }); + check(Component != nullptr); + EntityData.Components.RemoveAtSwap(Component - EntityData.Components.GetData()); + Builder.RemoveComponent(EntityId, ComponentId); +} + +void FTargetView::AddAuthority(Worker_EntityId EntityId, Worker_ComponentSetId ComponentSetId) +{ + check(!bDisconnected); + EntityViewElement* EntityData = View.Find(EntityId); + if (EntityData == nullptr) + { + Builder.AddEntity(EntityId); + EntityData = &View.Emplace(EntityId); + } + check(!EntityData->Authority.Contains(ComponentSetId)); + EntityData->Authority.Add(ComponentSetId); + Builder.SetAuthority(EntityId, ComponentSetId, WORKER_AUTHORITY_AUTHORITATIVE, {}); +} + +void FTargetView::RemoveAuthority(Worker_EntityId EntityId, Worker_ComponentSetId ComponentSetId) +{ + check(!bDisconnected); + EntityViewElement& EntityData = View.FindChecked(EntityId); + const int32 Removed = EntityData.Authority.RemoveSingleSwap(ComponentSetId); + check(Removed); + Builder.SetAuthority(EntityId, ComponentSetId, WORKER_AUTHORITY_NOT_AUTHORITATIVE, {}); +} + +void FTargetView::Disconnect(Worker_ConnectionStatusCode StatusCode, StringStorage DisconnectReason) +{ + check(!bDisconnected); + bDisconnected = true; + Builder.SetDisconnect(StatusCode, MoveTemp(DisconnectReason)); +} + +void FTargetView::AddCreateEntityCommandResponse(Worker_EntityId EntityId, Worker_RequestId RequestId, Worker_StatusCode StatusCode, + StringStorage Message) +{ + check(!bDisconnected); + Builder.AddCreateEntityCommandResponse(EntityId, RequestId, StatusCode, MoveTemp(Message)); +} + +void FTargetView::AddEntityQueryCommandResponse(Worker_RequestId RequestId, TArray Results, Worker_StatusCode StatusCode, + StringStorage Message) +{ + check(!bDisconnected); + Builder.AddEntityQueryCommandResponse(RequestId, MoveTemp(Results), StatusCode, MoveTemp(Message)); +} + +void FTargetView::AddEntityCommandRequest(Worker_EntityId EntityId, Worker_RequestId RequestId, CommandRequest CommandRequest) +{ + check(!bDisconnected); + Builder.AddEntityCommandRequest(EntityId, RequestId, MoveTemp(CommandRequest)); +} + +void FTargetView::AddEntityCommandResponse(Worker_EntityId EntityId, Worker_RequestId RequestId, Worker_StatusCode StatusCode, + StringStorage Message) +{ + check(!bDisconnected); + Builder.AddEntityCommandResponse(EntityId, RequestId, StatusCode, MoveTemp(Message)); +} + +void FTargetView::AddDeleteEntityCommandResponse(Worker_EntityId EntityId, Worker_RequestId RequestId, Worker_StatusCode StatusCode, + StringStorage Message) +{ + check(!bDisconnected); + Builder.AddDeleteEntityCommandResponse(EntityId, RequestId, StatusCode, MoveTemp(Message)); +} + +void FTargetView::AddReserveEntityIdsCommandResponse(Worker_EntityId EntityId, uint32 NumberOfEntities, Worker_RequestId RequestId, + Worker_StatusCode StatusCode, StringStorage Message) +{ + check(!bDisconnected); + Builder.AddReserveEntityIdsCommandResponse(EntityId, NumberOfEntities, RequestId, StatusCode, MoveTemp(Message)); +} + +const EntityView& FTargetView::GetView() const +{ + return View; +} + +OpList FTargetView::CreateOpListFromChanges() +{ + EntityComponentOpListBuilder Temp = MoveTemp(Builder); + Builder = EntityComponentOpListBuilder(); + return MoveTemp(Temp).CreateOpList(); +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/TestRoutingWorker.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/TestRoutingWorker.cpp index eb0431856f..48078be936 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Tests/TestRoutingWorker.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/TestRoutingWorker.cpp @@ -204,7 +204,8 @@ class ConnectionHandlerStub : public AbstractConnectionHandler check(!HasNextOpList()); TArray> NewListsOfOpLists; TArray OpLists; - OpLists.Add(Builder.Move().CreateOpList()); + OpLists.Add(MoveTemp(Builder).CreateOpList()); + Builder = EntityComponentOpListBuilder(); NewListsOfOpLists.Add(MoveTemp(OpLists)); SetListsOfOpLists(MoveTemp(NewListsOfOpLists)); } @@ -245,12 +246,12 @@ static FComponentSetData const& GetComponentSetData() static FComponentSetData s_ComponentSetData = [] { FComponentSetData Data; auto& ServerSet = Data.ComponentSets.Add(SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID); - ServerSet.Add(SpatialConstants::CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID); - ServerSet.Add(SpatialConstants::CROSSSERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID); + ServerSet.Add(SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID); + ServerSet.Add(SpatialConstants::CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID); auto& RoutingSet = Data.ComponentSets.Add(SpatialConstants::ROUTING_WORKER_AUTH_COMPONENT_SET_ID); - RoutingSet.Add(SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID); - RoutingSet.Add(SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID); + RoutingSet.Add(SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID); + RoutingSet.Add(SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID); return Data; }(); @@ -261,10 +262,10 @@ static FComponentSetData const& GetComponentSetData() void AddEntityAndCrossServerComponents(EntityComponentOpListBuilder& Builder, Worker_EntityId Id) { Builder.AddEntity(Id); - Builder.AddComponent(Id, ComponentData{ SpatialConstants::CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID }); - Builder.AddComponent(Id, ComponentData{ SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID }); - Builder.AddComponent(Id, ComponentData{ SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID }); - Builder.AddComponent(Id, ComponentData{ SpatialConstants::CROSSSERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID }); + Builder.AddComponent(Id, ComponentData{ SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID }); + Builder.AddComponent(Id, ComponentData{ SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID }); + Builder.AddComponent(Id, ComponentData{ SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID }); + Builder.AddComponent(Id, ComponentData{ SpatialConstants::CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID }); Builder.AddComponent(Id, ComponentData{ SpatialConstants::ROUTINGWORKER_TAG_COMPONENT_ID }); Builder.AddComponent(Id, ComponentData{ SpatialConstants::ACTOR_AUTH_TAG_COMPONENT_ID }); } @@ -272,8 +273,8 @@ void AddEntityAndCrossServerComponents(EntityComponentOpListBuilder& Builder, Wo void AddComponentAuthForRoutingWorker(EntityComponentOpListBuilder& Builder, Worker_EntityId Id) { TArray CanonicalData; - CanonicalData.Emplace(ComponentData{ SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID }); - CanonicalData.Emplace(ComponentData{ SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID }); + CanonicalData.Emplace(ComponentData{ SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID }); + CanonicalData.Emplace(ComponentData{ SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID }); Builder.SetAuthority(Id, SpatialConstants::ROUTING_WORKER_AUTH_COMPONENT_SET_ID, WORKER_AUTHORITY_AUTHORITATIVE, MoveTemp(CanonicalData)); } @@ -281,21 +282,21 @@ void AddComponentAuthForRoutingWorker(EntityComponentOpListBuilder& Builder, Wor void AddComponentAuthForServerWorker(EntityComponentOpListBuilder& Builder, Worker_EntityId Id) { TArray CanonicalData; - CanonicalData.Emplace(ComponentData{ SpatialConstants::CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID }); - CanonicalData.Emplace(ComponentData{ SpatialConstants::CROSSSERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID }); + CanonicalData.Emplace(ComponentData{ SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID }); + CanonicalData.Emplace(ComponentData{ SpatialConstants::CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID }); Builder.SetAuthority(Id, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID, WORKER_AUTHORITY_AUTHORITATIVE, MoveTemp(CanonicalData)); } void RemoveEntityAndCrossServerComponents(ViewCoordinator& Coordinator, EntityComponentOpListBuilder& Builder, Worker_EntityId Id) { - Coordinator.SendRemoveComponent(Id, SpatialConstants::CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID, {}); - Builder.RemoveComponent(Id, SpatialConstants::CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID); - Coordinator.SendRemoveComponent(Id, SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID, {}); - Builder.RemoveComponent(Id, SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID); - Coordinator.SendRemoveComponent(Id, SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID, {}); - Builder.RemoveComponent(Id, SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID); - Coordinator.SendRemoveComponent(Id, SpatialConstants::CROSSSERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID, {}); - Builder.RemoveComponent(Id, SpatialConstants::CROSSSERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID); + Coordinator.SendRemoveComponent(Id, SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID, {}); + Builder.RemoveComponent(Id, SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID); + Coordinator.SendRemoveComponent(Id, SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID, {}); + Builder.RemoveComponent(Id, SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID); + Coordinator.SendRemoveComponent(Id, SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID, {}); + Builder.RemoveComponent(Id, SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID); + Coordinator.SendRemoveComponent(Id, SpatialConstants::CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID, {}); + Builder.RemoveComponent(Id, SpatialConstants::CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID); Coordinator.SendRemoveComponent(Id, SpatialConstants::ROUTINGWORKER_TAG_COMPONENT_ID, {}); Builder.RemoveComponent(Id, SpatialConstants::ROUTINGWORKER_TAG_COMPONENT_ID); Coordinator.SendRemoveComponent(Id, SpatialConstants::ACTOR_AUTH_TAG_COMPONENT_ID, {}); @@ -326,19 +327,19 @@ struct Components for (auto& Data : Element.Components) { - if (Data.GetComponentId() == SpatialConstants::CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID) + if (Data.GetComponentId() == SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID) { Sender.Emplace(CrossServerEndpoint(Data.GetUnderlying())); } - if (Data.GetComponentId() == SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID) + if (Data.GetComponentId() == SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID) { SenderACK.Emplace(CrossServerEndpointACK(Data.GetUnderlying())); } - if (Data.GetComponentId() == SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID) + if (Data.GetComponentId() == SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID) { Receiver.Emplace(CrossServerEndpoint(Data.GetUnderlying())); } - if (Data.GetComponentId() == SpatialConstants::CROSSSERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID) + if (Data.GetComponentId() == SpatialConstants::CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID) { ReceiverACK.Emplace(CrossServerEndpointACK(Data.GetUnderlying())); } @@ -434,11 +435,10 @@ struct RoutingWorkerMock : WorkerMock struct ServerWorkerMock : WorkerMock { ServerWorkerMock() - : SubView(Coordinator.CreateSubView(SpatialConstants::ACTOR_AUTH_TAG_COMPONENT_ID, - [](const Worker_EntityId, const SpatialGDK::EntityViewElement&) { - return true; - }, - {})) + : SubView(Coordinator.CreateSubView(SpatialConstants::ACTOR_AUTH_TAG_COMPONENT_ID, SpatialGDK::FSubView::NoFilter, + SpatialGDK::FSubView::NoDispatcherCallbacks)) + , DummyRoutingWorkerSubView(Coordinator.CreateSubView(SpatialConstants::ROUTINGWORKER_TAG_COMPONENT_ID, + SpatialGDK::FSubView::NoFilter, SpatialGDK::FSubView::NoDispatcherCallbacks)) { } @@ -453,6 +453,7 @@ struct ServerWorkerMock : WorkerMock } SpatialGDK::FSubView& SubView; + SpatialGDK::FSubView& DummyRoutingWorkerSubView; }; struct TestRoutingFixture @@ -553,7 +554,8 @@ ROUTING_SERVICE_TEST(TestRoutingWorker_WhiteBox_SendOneMessage) SpatialGDK::FRPCStore RPCStore; SpatialGDK::CrossServerRPCService RPCService(TestFixture.CanExtractDelegate, ExtractRPCDelegate::CreateLambda(ExtractRPCCallback), - TestFixture.ServerWorker.SubView, RPCStore); + TestFixture.ServerWorker.SubView, TestFixture.ServerWorker.DummyRoutingWorkerSubView, + RPCStore); RPCServiceBackptr = &RPCService; RPCService.AdvanceView(); @@ -652,7 +654,8 @@ ROUTING_SERVICE_TEST(TestRoutingWorker_BlackBox_SendSeveralMessagesToSeveralEnti SpatialGDK::FRPCStore RPCStore; SpatialGDK::CrossServerRPCService RPCService(TestFixture.CanExtractDelegate, ExtractRPCDelegate::CreateLambda(ExtractRPCCallback), - TestFixture.ServerWorker.SubView, RPCStore); + TestFixture.ServerWorker.SubView, TestFixture.ServerWorker.DummyRoutingWorkerSubView, + RPCStore); RPCServiceBackptr = &RPCService; RPCService.AdvanceView(); RPCService.ProcessChanges(); @@ -728,7 +731,8 @@ ROUTING_SERVICE_TEST(TestRoutingWorker_BlackBox_SendOneMessageBetweenDeletedEnti SpatialGDK::FRPCStore RPCStore; SpatialGDK::CrossServerRPCService RPCService(TestFixture.CanExtractDelegate, ExtractRPCDelegate::CreateLambda(ExtractRPCCallback), - TestFixture.ServerWorker.SubView, RPCStore); + TestFixture.ServerWorker.SubView, TestFixture.ServerWorker.DummyRoutingWorkerSubView, + RPCStore); RPCServiceBackptr = &RPCService; RPCService.AdvanceView(); @@ -814,7 +818,8 @@ ROUTING_SERVICE_TEST(TestRoutingWorker_BlackBox_SendMoreMessagesThanRingBufferCa SpatialGDK::FRPCStore RPCStore; SpatialGDK::CrossServerRPCService RPCService(TestFixture.CanExtractDelegate, ExtractRPCDelegate::CreateLambda(ExtractRPCCallback), - TestFixture.ServerWorker.SubView, RPCStore); + TestFixture.ServerWorker.SubView, TestFixture.ServerWorker.DummyRoutingWorkerSubView, + RPCStore); RPCServiceBackptr = &RPCService; diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp index 68168fa6ef..ed33c417bf 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp @@ -18,40 +18,26 @@ #include "Utils/GDKPropertyMacros.h" #include "Utils/InterestFactory.h" #include "Utils/RepLayoutUtils.h" -#include "Utils/SpatialLatencyTracer.h" DEFINE_LOG_CATEGORY(LogComponentFactory); DECLARE_CYCLE_STAT(TEXT("Factory ProcessPropertyUpdates"), STAT_FactoryProcessPropertyUpdates, STATGROUP_SpatialNet); DECLARE_CYCLE_STAT(TEXT("Factory ProcessFastArrayUpdate"), STAT_FactoryProcessFastArrayUpdate, STATGROUP_SpatialNet); -namespace -{ -template -TraceKey* GetTraceKeyFromComponentObject(T& Obj) -{ -#if TRACE_LIB_ACTIVE - return &Obj.Trace; -#else - return nullptr; -#endif -} -} // namespace namespace SpatialGDK { -ComponentFactory::ComponentFactory(bool bInterestDirty, USpatialNetDriver* InNetDriver, USpatialLatencyTracer* InLatencyTracer) +ComponentFactory::ComponentFactory(bool bInterestDirty, USpatialNetDriver* InNetDriver) : NetDriver(InNetDriver) , PackageMap(InNetDriver->PackageMap) , ClassInfoManager(InNetDriver->ClassInfoManager) , bInterestHasChanged(bInterestDirty) , bInitialOnlyDataWritten(false) , bInitialOnlyReplicationEnabled(GetDefault()->bEnableInitialOnlyReplicationCondition) - , LatencyTracer(InLatencyTracer) { } uint32 ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FRepChangeState& Changes, - ESchemaComponentType PropertyGroup, bool bIsInitialData, TraceKey* OutLatencyTraceId, + ESchemaComponentType PropertyGroup, bool bIsInitialData, TArray* ClearedIds /*= nullptr*/) { SCOPE_CYCLE_COUNTER(STAT_FactoryProcessPropertyUpdates); @@ -69,30 +55,6 @@ uint32 ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObjec const FRepLayoutCmd& Cmd = Changes.RepLayout.Cmds[HandleIterator.CmdIndex]; const FRepParentCmd& Parent = Changes.RepLayout.Parents[Cmd.ParentIndex]; -#if TRACE_LIB_ACTIVE - if (LatencyTracer != nullptr && OutLatencyTraceId != nullptr) - { - TraceKey PropertyKey = InvalidTraceKey; - PropertyKey = LatencyTracer->RetrievePendingTrace(Object, Cmd.Property); - if (PropertyKey == InvalidTraceKey) - { - // Check for sending a nested property - PropertyKey = LatencyTracer->RetrievePendingTrace(Object, Parent.Property); - } - if (PropertyKey != InvalidTraceKey) - { - // If we have already got a trace for this actor/component, we will end one of them here - if (*OutLatencyTraceId != InvalidTraceKey) - { - UE_LOG(LogComponentFactory, Warning, - TEXT("%s property trace being dropped because too many active on this actor (%s)"), *Cmd.Property->GetName(), - *Object->GetName()); - LatencyTracer->WriteAndEndTrace(*OutLatencyTraceId, TEXT("Multiple actor component traces not supported"), true); - } - *OutLatencyTraceId = PropertyKey; - } - } -#endif if (GetGroupFromCondition(Parent.Condition) == PropertyGroup) { const uint8* Data = (uint8*)Object + Cmd.Offset; @@ -164,40 +126,6 @@ uint32 ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObjec return BytesEnd - BytesStart; } -uint32 ComponentFactory::FillHandoverSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FClassInfo& Info, - const FHandoverChangeState& Changes, bool bIsInitialData, TraceKey* OutLatencyTraceId, - TArray* ClearedIds /* = nullptr */) -{ - const uint32 BytesStart = Schema_GetWriteBufferLength(ComponentObject); - - for (uint16 ChangedHandle : Changes) - { - check(ChangedHandle > 0 && ChangedHandle - 1 < Info.HandoverProperties.Num()); - const FHandoverPropertyInfo& PropertyInfo = Info.HandoverProperties[ChangedHandle - 1]; - - const uint8* Data = (uint8*)Object + PropertyInfo.Offset; - -#if TRACE_LIB_ACTIVE - if (LatencyTracer != nullptr && OutLatencyTraceId != nullptr) - { - // If we have already got a trace for this actor/component, we will end one of them here - if (*OutLatencyTraceId != InvalidTraceKey) - { - UE_LOG(LogComponentFactory, Warning, TEXT("%s handover trace being dropped because too many active on this actor (%s)"), - *PropertyInfo.Property->GetName(), *Object->GetName()); - LatencyTracer->WriteAndEndTrace(*OutLatencyTraceId, TEXT("Multiple actor component traces not supported"), true); - } - *OutLatencyTraceId = LatencyTracer->RetrievePendingTrace(Object, PropertyInfo.Property); - } -#endif - AddProperty(ComponentObject, ChangedHandle, PropertyInfo.Property, Data, ClearedIds); - } - - const uint32 BytesEnd = Schema_GetWriteBufferLength(ComponentObject); - - return BytesEnd - BytesStart; -} - void ComponentFactory::AddProperty(Schema_Object* Object, Schema_FieldId FieldId, GDK_PROPERTY(Property) * Property, const uint8* Data, TArray* ClearedIds) { @@ -316,6 +244,17 @@ void ComponentFactory::AddProperty(Schema_Object* Object, Schema_FieldId FieldId AddProperty(Object, FieldId, ArrayProperty->Inner, ArrayHelper.GetRawPtr(i), ClearedIds); } + if (ArrayHelper.Num() > 0 || (ArrayHelper.Num() == 0 && ClearedIds)) + { + if (ArrayProperty->Inner->IsA()) + { + if (ArrayProperty->PropertyFlags & CPF_AlwaysInterested) + { + bInterestHasChanged = true; + } + } + } + if (ArrayHelper.Num() == 0 && ClearedIds) { ClearedIds->Add(FieldId); @@ -355,12 +294,12 @@ void ComponentFactory::AddProperty(Schema_Object* Object, Schema_FieldId FieldId } TArray ComponentFactory::CreateComponentDatas(UObject* Object, const FClassInfo& Info, - const FRepChangeState& RepChangeState, - const FHandoverChangeState& HandoverChangeState, - uint32& OutBytesWritten) + const FRepChangeState& RepChangeState, uint32& OutBytesWritten) { TArray ComponentDatas; + static_assert(SCHEMA_Count == 4, "Unexpected number of Schema type components, please check the enclosing function is still correct."); + if (Info.SchemaComponents[SCHEMA_Data] != SpatialConstants::INVALID_COMPONENT_ID) { ComponentDatas.Add(CreateComponentData(Info.SchemaComponents[SCHEMA_Data], Object, RepChangeState, SCHEMA_Data, OutBytesWritten)); @@ -372,10 +311,10 @@ TArray ComponentFactory::CreateComponentDatas(UObject* Obj CreateComponentData(Info.SchemaComponents[SCHEMA_OwnerOnly], Object, RepChangeState, SCHEMA_OwnerOnly, OutBytesWritten)); } - if (Info.SchemaComponents[SCHEMA_Handover] != SpatialConstants::INVALID_COMPONENT_ID) + if (Info.SchemaComponents[SCHEMA_ServerOnly] != SpatialConstants::INVALID_COMPONENT_ID) { ComponentDatas.Add( - CreateHandoverComponentData(Info.SchemaComponents[SCHEMA_Handover], Object, Info, HandoverChangeState, OutBytesWritten)); + CreateComponentData(Info.SchemaComponents[SCHEMA_ServerOnly], Object, RepChangeState, SCHEMA_ServerOnly, OutBytesWritten)); } if (Info.SchemaComponents[SCHEMA_InitialOnly] != SpatialConstants::INVALID_COMPONENT_ID) @@ -412,8 +351,7 @@ FWorkerComponentData ComponentFactory::CreateComponentData(Worker_ComponentId Co // We're currently ignoring ClearedId fields, which is problematic if the initial replicated state // is different to what the default state is (the client will have the incorrect data). UNR:959 - OutBytesWritten += - FillSchemaObject(ComponentObject, Object, Changes, PropertyGroup, true, GetTraceKeyFromComponentObject(ComponentData)); + OutBytesWritten += FillSchemaObject(ComponentObject, Object, Changes, PropertyGroup, true); return ComponentData; } @@ -427,25 +365,13 @@ FWorkerComponentData ComponentFactory::CreateEmptyComponentData(Worker_Component return ComponentData; } -FWorkerComponentData ComponentFactory::CreateHandoverComponentData(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, - const FHandoverChangeState& Changes, uint32& OutBytesWritten) -{ - FWorkerComponentData ComponentData = CreateEmptyComponentData(ComponentId); - Schema_Object* ComponentObject = Schema_GetComponentDataFields(ComponentData.schema_type); - - OutBytesWritten += - FillHandoverSchemaObject(ComponentObject, Object, Info, Changes, true, GetTraceKeyFromComponentObject(ComponentData)); - - return ComponentData; -} - TArray ComponentFactory::CreateComponentUpdates(UObject* Object, const FClassInfo& Info, Worker_EntityId EntityId, - const FRepChangeState* RepChangeState, - const FHandoverChangeState* HandoverChangeState, - uint32& OutBytesWritten) + const FRepChangeState* RepChangeState, uint32& OutBytesWritten) { TArray ComponentUpdates; + static_assert(SCHEMA_Count == 4, "Unexpected number of Schema type components, please check the enclosing function is still correct."); + if (RepChangeState) { if (Info.SchemaComponents[SCHEMA_Data] != SpatialConstants::INVALID_COMPONENT_ID) @@ -472,6 +398,18 @@ TArray ComponentFactory::CreateComponentUpdates(UObject* } } + if (Info.SchemaComponents[SCHEMA_ServerOnly] != SpatialConstants::INVALID_COMPONENT_ID) + { + uint32 BytesWritten = 0; + FWorkerComponentUpdate HandoverUpdate = + CreateComponentUpdate(Info.SchemaComponents[SCHEMA_ServerOnly], Object, *RepChangeState, SCHEMA_ServerOnly, BytesWritten); + if (BytesWritten > 0) + { + ComponentUpdates.Add(HandoverUpdate); + OutBytesWritten += BytesWritten; + } + } + if (Info.SchemaComponents[SCHEMA_InitialOnly] != SpatialConstants::INVALID_COMPONENT_ID) { // Initial only data on dynamic subobjects is not currently supported. @@ -493,21 +431,6 @@ TArray ComponentFactory::CreateComponentUpdates(UObject* } } - if (HandoverChangeState) - { - if (Info.SchemaComponents[SCHEMA_Handover] != SpatialConstants::INVALID_COMPONENT_ID) - { - uint32 BytesWritten = 0; - FWorkerComponentUpdate HandoverUpdate = - CreateHandoverComponentUpdate(Info.SchemaComponents[SCHEMA_Handover], Object, Info, *HandoverChangeState, BytesWritten); - if (BytesWritten > 0) - { - ComponentUpdates.Add(HandoverUpdate); - OutBytesWritten += BytesWritten; - } - } - } - // Only support Interest for Actors for now. if (Object->IsA() && bInterestHasChanged) { @@ -536,39 +459,7 @@ FWorkerComponentUpdate ComponentFactory::CreateComponentUpdate(Worker_ComponentI TArray ClearedIds; - uint32 BytesWritten = FillSchemaObject(ComponentObject, Object, Changes, PropertyGroup, false, - GetTraceKeyFromComponentObject(ComponentUpdate), &ClearedIds); - - for (Schema_FieldId Id : ClearedIds) - { - Schema_AddComponentUpdateClearedField(ComponentUpdate.schema_type, Id); - BytesWritten++; // Workaround so we don't drop updates that *only* contain cleared fields - JIRA UNR-3371 - } - - if (BytesWritten == 0) - { - Schema_DestroyComponentUpdate(ComponentUpdate.schema_type); - } - - OutBytesWritten += BytesWritten; - - return ComponentUpdate; -} - -FWorkerComponentUpdate ComponentFactory::CreateHandoverComponentUpdate(Worker_ComponentId ComponentId, UObject* Object, - const FClassInfo& Info, const FHandoverChangeState& Changes, - uint32& OutBytesWritten) -{ - FWorkerComponentUpdate ComponentUpdate = {}; - - ComponentUpdate.component_id = ComponentId; - ComponentUpdate.schema_type = Schema_CreateComponentUpdate(); - Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(ComponentUpdate.schema_type); - - TArray ClearedIds; - - uint32 BytesWritten = FillHandoverSchemaObject(ComponentObject, Object, Info, Changes, false, - GetTraceKeyFromComponentObject(ComponentUpdate), &ClearedIds); + uint32 BytesWritten = FillSchemaObject(ComponentObject, Object, Changes, PropertyGroup, false, &ClearedIds); for (Schema_FieldId Id : ClearedIds) { diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp index 5fe7e6933a..07f7567bd6 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp @@ -19,7 +19,6 @@ DEFINE_LOG_CATEGORY(LogSpatialComponentReader); DECLARE_CYCLE_STAT(TEXT("Reader ApplyPropertyUpdates"), STAT_ReaderApplyPropertyUpdates, STATGROUP_SpatialNet); -DECLARE_CYCLE_STAT(TEXT("Reader ApplyHandoverPropertyUpdates"), STAT_ReaderApplyHandoverPropertyUpdates, STATGROUP_SpatialNet); DECLARE_CYCLE_STAT(TEXT("Reader ApplyFastArrayUpdate"), STAT_ReaderApplyFastArrayUpdate, STATGROUP_SpatialNet); DECLARE_CYCLE_STAT(TEXT("Reader ApplyProperty"), STAT_ReaderApplyProperty, STATGROUP_SpatialNet); DECLARE_CYCLE_STAT(TEXT("Reader ApplyArray"), STAT_ReaderApplyArray, STATGROUP_SpatialNet); @@ -99,13 +98,13 @@ ComponentReader::ComponentReader(USpatialNetDriver* InNetDriver, } void ComponentReader::ApplyComponentData(const Worker_ComponentData& ComponentData, UObject& Object, USpatialActorChannel& Channel, - bool bIsHandover, bool& bOutReferencesChanged) + bool& bOutReferencesChanged) { - ApplyComponentData(ComponentData.component_id, ComponentData.schema_type, Object, Channel, bIsHandover, bOutReferencesChanged); + ApplyComponentData(ComponentData.component_id, ComponentData.schema_type, Object, Channel, bOutReferencesChanged); } void ComponentReader::ApplyComponentData(const Worker_ComponentId ComponentId, Schema_ComponentData* Data, UObject& Object, - USpatialActorChannel& Channel, bool bIsHandover, bool& bOutReferencesChanged) + USpatialActorChannel& Channel, bool& bOutReferencesChanged) { if (Object.IsPendingKill()) { @@ -119,18 +118,11 @@ void ComponentReader::ApplyComponentData(const Worker_ComponentId ComponentId, S // that component type (Data, OwnerOnly, Handover, etc.). const TArray& InitialIds = ClassInfoManager->GetFieldIdsByComponentId(ComponentId); - if (bIsHandover) - { - ApplyHandoverSchemaObject(ComponentObject, Object, Channel, true, InitialIds, ComponentId, bOutReferencesChanged); - } - else - { - ApplySchemaObject(ComponentObject, Object, Channel, true, InitialIds, ComponentId, bOutReferencesChanged); - } + ApplySchemaObject(ComponentObject, Object, Channel, true, InitialIds, ComponentId, bOutReferencesChanged); } void ComponentReader::ApplyComponentUpdate(const Worker_ComponentId ComponentId, Schema_ComponentUpdate* ComponentUpdate, UObject& Object, - USpatialActorChannel& Channel, bool bIsHandover, bool& bOutReferencesChanged) + USpatialActorChannel& Channel, bool& bOutReferencesChanged) { if (Object.IsPendingKill()) { @@ -154,14 +146,7 @@ void ComponentReader::ApplyComponentUpdate(const Worker_ComponentId ComponentId, if (UpdatedIds.Num() > 0) { - if (bIsHandover) - { - ApplyHandoverSchemaObject(ComponentObject, Object, Channel, false, UpdatedIds, ComponentId, bOutReferencesChanged); - } - else - { - ApplySchemaObject(ComponentObject, Object, Channel, false, UpdatedIds, ComponentId, bOutReferencesChanged); - } + ApplySchemaObject(ComponentObject, Object, Channel, false, UpdatedIds, ComponentId, bOutReferencesChanged); } } @@ -382,51 +367,6 @@ void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject& Channel.PostReceiveSpatialUpdate(&Object, RepNotifies, PropertySpanIds); } -void ComponentReader::ApplyHandoverSchemaObject(Schema_Object* ComponentObject, UObject& Object, USpatialActorChannel& Channel, - bool bIsInitialData, const TArray& UpdatedIds, - Worker_ComponentId ComponentId, bool& bOutReferencesChanged) -{ - SCOPE_CYCLE_COUNTER(STAT_ReaderApplyHandoverPropertyUpdates); - - FObjectReplicator* Replicator = Channel.PreReceiveSpatialUpdate(&Object); - if (Replicator == nullptr) - { - // Can't apply this schema object. Error printed from PreReceiveSpatialUpdate. - return; - } - - const FClassInfo& ClassInfo = ClassInfoManager->GetOrCreateClassInfoByClass(Object.GetClass()); - - for (uint32 FieldId : UpdatedIds) - { - // FieldId is the same as handover handle - if (FieldId == 0 || (int)FieldId - 1 >= ClassInfo.HandoverProperties.Num()) - { - UE_LOG(LogSpatialComponentReader, Error, - TEXT("ApplyHandoverSchemaObject: Encountered an invalid field Id while applying schema. Object: %s, Field: %d, Entity: " - "%lld, Component: %d"), - *Object.GetPathName(), FieldId, Channel.GetEntityId(), ComponentId); - continue; - } - const FHandoverPropertyInfo& PropertyInfo = ClassInfo.HandoverProperties[FieldId - 1]; - - uint8* Data = (uint8*)&Object + PropertyInfo.Offset; - - if (GDK_PROPERTY(ArrayProperty)* ArrayProperty = GDK_CASTFIELD(PropertyInfo.Property)) - { - ApplyArray(ComponentObject, FieldId, RootObjectReferencesMap, ArrayProperty, Data, PropertyInfo.Offset, -1, -1, - bOutReferencesChanged); - } - else - { - ApplyProperty(ComponentObject, FieldId, RootObjectReferencesMap, 0, PropertyInfo.Property, Data, PropertyInfo.Offset, -1, -1, - bOutReferencesChanged); - } - } - - Channel.PostReceiveSpatialUpdate(&Object, TArray(), {}); -} - void ComponentReader::ApplyProperty(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, uint32 Index, GDK_PROPERTY(Property) * Property, uint8* Data, int32 Offset, int32 ShadowOffset, int32 ParentIndex, bool& bOutReferencesChanged) diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityFactory.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityFactory.cpp index ce395a2a61..78c56d01fa 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityFactory.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityFactory.cpp @@ -5,6 +5,7 @@ #include "EngineClasses/SpatialActorChannel.h" #include "EngineClasses/SpatialNetConnection.h" #include "EngineClasses/SpatialNetDriver.h" +#include "EngineClasses/SpatialNetDriverRPC.h" #include "EngineClasses/SpatialPackageMapClient.h" #include "EngineClasses/SpatialVirtualWorkerTranslator.h" #include "Interop/ActorGroupWriter.h" @@ -12,6 +13,7 @@ #include "Interop/RPCs/SpatialRPCService.h" #include "LoadBalancing/AbstractLBStrategy.h" #include "Schema/ActorGroupMember.h" +#include "Schema/ActorOwnership.h" #include "Schema/ActorSetMember.h" #include "Schema/AuthorityIntent.h" #include "Schema/NetOwningClientWorker.h" @@ -33,6 +35,10 @@ #include "GameFramework/GameStateBase.h" #include "Runtime/Launch/Resources/Version.h" +#if WITH_GAMEPLAY_DEBUGGER +#include "GameplayDebuggerCategoryReplicator.h" +#endif + DEFINE_LOG_CATEGORY(LogEntityFactory); namespace SpatialGDK @@ -93,10 +99,18 @@ TArray EntityFactory::CreateSkeletonEntityComponents(AActo // Add Actor completeness tags. ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::ACTOR_AUTH_TAG_COMPONENT_ID)); ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::ACTOR_TAG_COMPONENT_ID)); + ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::ACTOR_OWNER_ONLY_DATA_TAG_COMPONENT_ID)); ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::LB_TAG_COMPONENT_ID)); ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::ROUTINGWORKER_TAG_COMPONENT_ID)); ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::GDK_DEBUG_TAG_COMPONENT_ID)); +#if WITH_GAMEPLAY_DEBUGGER + if (Actor->IsA()) + { + ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::GDK_GAMEPLAY_DEBUGGER_COMPONENT_ID)); + } +#endif // WITH_GAMEPLAY_DEBUGGER + return ComponentDatas; } @@ -123,19 +137,21 @@ void EntityFactory::WriteLBComponents(TArray& ComponentDat #if !UE_BUILD_SHIPPING if (NetDriver->SpatialDebugger != nullptr) { - check(NetDriver->VirtualWorkerTranslator != nullptr); - - const PhysicalWorkerName* PhysicalWorkerName = - NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(IntendedVirtualWorkerId); - FColor InvalidServerTintColor = NetDriver->SpatialDebugger->InvalidServerTintColor; - FColor IntentColor = - PhysicalWorkerName != nullptr ? SpatialGDK::GetColorForWorkerName(*PhysicalWorkerName) : InvalidServerTintColor; + if (ensureAlwaysMsgf(NetDriver->VirtualWorkerTranslator != nullptr, + TEXT("Failed to add debugging utilities. Translator was invalid"))) + { + const PhysicalWorkerName* PhysicalWorkerName = + NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(IntendedVirtualWorkerId); + const FColor InvalidServerTintColor = NetDriver->SpatialDebugger->InvalidServerTintColor; + const FColor IntentColor = + PhysicalWorkerName != nullptr ? SpatialGDK::GetColorForWorkerName(*PhysicalWorkerName) : InvalidServerTintColor; - const bool bIsLocked = NetDriver->LockingPolicy->IsLocked(Actor); + const bool bIsLocked = NetDriver->LockingPolicy->IsLocked(Actor); - SpatialDebugging DebuggingInfo(SpatialConstants::INVALID_VIRTUAL_WORKER_ID, InvalidServerTintColor, IntendedVirtualWorkerId, - IntentColor, bIsLocked); - ComponentDatas.Add(DebuggingInfo.CreateComponentData()); + SpatialDebugging DebuggingInfo(SpatialConstants::INVALID_VIRTUAL_WORKER_ID, InvalidServerTintColor, IntendedVirtualWorkerId, + IntentColor, bIsLocked); + ComponentDatas.Add(DebuggingInfo.CreateComponentData()); + } } #endif // !UE_BUILD_SHIPPING @@ -244,18 +260,18 @@ void EntityFactory::WriteUnrealComponents(TArray& Componen ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::PLAYER_CONTROLLER_COMPONENT_ID)); } - USpatialLatencyTracer* Tracer = USpatialLatencyTracer::GetTracer(Actor); - - ComponentFactory DataFactory(false, NetDriver, Tracer); + ComponentFactory DataFactory(false, NetDriver); FRepChangeState InitialRepChanges = Channel->CreateInitialRepChangeState(Actor); - FHandoverChangeState InitialHandoverChanges = Channel->CreateInitialHandoverChangeState(Info); - TArray ActorDataComponents = - DataFactory.CreateComponentDatas(Actor, Info, InitialRepChanges, InitialHandoverChanges, OutBytesWritten); + TArray ActorDataComponents = DataFactory.CreateComponentDatas(Actor, Info, InitialRepChanges, OutBytesWritten); ComponentDatas.Append(ActorDataComponents); + ComponentDatas.Add(Worker_ComponentData{ nullptr, ActorOwnership::ComponentId, + ActorOwnership::CreateFromActor(*Actor, *PackageMap).CreateComponentData().Release(), + nullptr }); + ComponentDatas.Add(NetDriver->InterestFactory->CreateInterestData(Actor, Info, EntityId)); Channel->SetNeedOwnerInterestUpdate(!NetDriver->InterestFactory->DoOwnersHaveEntityId(Actor)); @@ -263,14 +279,15 @@ void EntityFactory::WriteUnrealComponents(TArray& Componen if (SpatialSettings->CrossServerRPCImplementation == ECrossServerRPCImplementation::RoutingWorker) { // Addition of CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID is handled in GetRPCComponentsOnEntityCreation - ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID)); - ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID)); - ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::CROSSSERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID)); + ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID)); + ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID)); + ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID)); } ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::SERVER_TO_SERVER_COMMAND_ENDPOINT_COMPONENT_ID)); checkf(RPCService != nullptr, TEXT("Attempting to create an entity with a null RPCService.")); ComponentDatas.Append(RPCService->GetRPCComponentsOnEntityCreation(EntityId)); + ComponentDatas.Append(NetDriver->RPCs->GetRPCComponentsOnEntityCreation(EntityId)); // Only add subobjects which are replicating for (auto RepSubobject = Channel->ReplicationMap.CreateIterator(); RepSubobject; ++RepSubobject) @@ -298,50 +315,13 @@ void EntityFactory::WriteUnrealComponents(TArray& Componen const FClassInfo& SubobjectInfo = ClassInfoManager->GetOrCreateClassInfoByObject(Subobject); FRepChangeState SubobjectRepChanges = Channel->CreateInitialRepChangeState(Subobject); - FHandoverChangeState SubobjectHandoverChanges = Channel->CreateInitialHandoverChangeState(SubobjectInfo); TArray ActorSubobjectDatas = - DataFactory.CreateComponentDatas(Subobject, SubobjectInfo, SubobjectRepChanges, SubobjectHandoverChanges, OutBytesWritten); + DataFactory.CreateComponentDatas(Subobject, SubobjectInfo, SubobjectRepChanges, OutBytesWritten); ComponentDatas.Append(ActorSubobjectDatas); } } - // Or if the subobject has handover properties, add it as well. - // NOTE: this is only for subobjects that are a part of the CDO. - // NOT dynamic subobjects which have been added before entity creation. - for (auto& SubobjectInfoPair : Info.SubobjectInfo) - { - const FClassInfo& SubobjectInfo = SubobjectInfoPair.Value.Get(); - - // Static subobjects aren't guaranteed to exist on actor instances, check they are present before adding to delegation component - TWeakObjectPtr WeakSubobject = - PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(Channel->GetEntityId(), SubobjectInfoPair.Key)); - if (!WeakSubobject.IsValid()) - { - continue; - } - - UObject* Subobject = WeakSubobject.Get(); - - if (SubobjectInfo.SchemaComponents[SCHEMA_Handover] == SpatialConstants::INVALID_COMPONENT_ID) - { - continue; - } - - // If it contains it, we've already created handover data for it. - if (Channel->ReplicationMap.Contains(Subobject)) - { - continue; - } - - FHandoverChangeState SubobjectHandoverChanges = Channel->CreateInitialHandoverChangeState(SubobjectInfo); - - FWorkerComponentData SubobjectHandoverData = DataFactory.CreateHandoverComponentData( - SubobjectInfo.SchemaComponents[SCHEMA_Handover], Subobject, SubobjectInfo, SubobjectHandoverChanges, OutBytesWritten); - - ComponentDatas.Add(SubobjectHandoverData); - } - if (DataFactory.WasInitialOnlyDataWritten()) { ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::INITIAL_ONLY_PRESENCE_COMPONENT_ID)); @@ -362,9 +342,11 @@ TArray EntityFactory::CreateEntityComponents(USpatialActor { if (ComponentIds.Contains(ComponentData.component_id)) { - UE_LOG(LogEntityFactory, Error, - TEXT("Constructed entity components for an Unreal actor channel contained a duplicate component. This is unexpected and " - "could cause problems later on.")); + UE_LOG( + LogEntityFactory, Error, + TEXT("Constructed entity components for an Unreal actor channel contained a duplicate component %i. This is unexpected and " + "could cause problems later on."), + ComponentData.component_id); } ComponentIds.Add(ComponentData.component_id); } @@ -374,7 +356,10 @@ TArray EntityFactory::CreateEntityComponents(USpatialActor TArray EntityFactory::CreateTombstoneEntityComponents(AActor* Actor) const { - check(Actor->IsNetStartupActor()); + if (!ensureAlwaysMsgf(Actor->IsNetStartupActor(), TEXT("Tried to create tombstone entity components for non-net-startup Actor"))) + { + return TArray{}; + } const UClass* Class = Actor->GetClass(); @@ -421,6 +406,7 @@ TArray EntityFactory::CreateTombstoneEntityComponents(AAct // Add Actor completeness tags. Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::ACTOR_AUTH_TAG_COMPONENT_ID)); Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::ACTOR_TAG_COMPONENT_ID)); + Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::ACTOR_OWNER_ONLY_DATA_TAG_COMPONENT_ID)); Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::LB_TAG_COMPONENT_ID)); Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::ROUTINGWORKER_TAG_COMPONENT_ID)); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityPool.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityPool.cpp index 390d15124e..a456e43e1e 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityPool.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityPool.cpp @@ -6,16 +6,13 @@ #include "Interop/SpatialSender.h" #include "SpatialGDKSettings.h" -#include "TimerManager.h" - DEFINE_LOG_CATEGORY(LogSpatialEntityPool); using namespace SpatialGDK; -void UEntityPool::Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimerManager) +void UEntityPool::Init(USpatialNetDriver& InNetDriver) { - NetDriver = InNetDriver; - TimerManager = InTimerManager; + NetDriver = &InNetDriver; ReserveEntityIDs(GetDefault()->EntityPoolInitialReservationCount); } @@ -24,7 +21,10 @@ void UEntityPool::ReserveEntityIDs(uint32 EntitiesToReserve) { UE_LOG(LogSpatialEntityPool, Verbose, TEXT("Sending bulk entity ID Reservation Request for %d IDs"), EntitiesToReserve); - checkf(!bIsAwaitingResponse, TEXT("Trying to reserve Entity IDs while another reserve request is in flight")); + if (!ensureAlwaysMsgf(!bIsAwaitingResponse, TEXT("Trying to reserve Entity IDs while another reserve request is in flight"))) + { + return; + } // TODO: UNR-4979 Allow full range of uint32 when SQD-1150 is fixed const uint32 TempMaxEntitiesToReserve = static_cast(MAX_int32); @@ -57,35 +57,18 @@ void UEntityPool::ReserveEntityIDs(uint32 EntitiesToReserve) } // Ensure we received the same number of reserved IDs as we requested - check(EntitiesToReserve == Op.number_of_entity_ids); - - // Clean up any expired Entity ranges - ReservedEntityIDRanges = ReservedEntityIDRanges.FilterByPredicate([](const EntityRange& Element) { - return !Element.bExpired; - }); + ensureAlwaysMsgf(EntitiesToReserve == Op.number_of_entity_ids, + TEXT("Received a different number of reserved entity IDs to what was requested")); EntityRange NewEntityRange = {}; NewEntityRange.CurrentEntityId = Op.first_entity_id; NewEntityRange.LastEntityId = Op.first_entity_id + (Op.number_of_entity_ids - 1); - NewEntityRange.EntityRangeId = NextEntityRangeId++; - UE_LOG(LogSpatialEntityPool, Verbose, TEXT("Reserved %d entities, caching in pool, Entity IDs: (%d, %d) Range ID: %d"), - Op.number_of_entity_ids, Op.first_entity_id, NewEntityRange.LastEntityId, NewEntityRange.EntityRangeId); + UE_LOG(LogSpatialEntityPool, Verbose, TEXT("Reserved %d entities, caching in pool, Entity IDs: (%d, %d)"), Op.number_of_entity_ids, + Op.first_entity_id, NewEntityRange.LastEntityId); ReservedEntityIDRanges.Add(NewEntityRange); - FTimerHandle ExpirationTimer; - TWeakObjectPtr WeakThis(this); - TimerManager->SetTimer( - ExpirationTimer, - [WeakThis, ExpiringEntityRangeId = NewEntityRange.EntityRangeId]() { - if (UEntityPool* Pool = WeakThis.Get()) - { - Pool->OnEntityRangeExpired(ExpiringEntityRangeId); - } - }, - SpatialConstants::ENTITY_RANGE_EXPIRATION_INTERVAL_SECONDS, false); - if (!bIsReady) { bIsReady = true; @@ -105,41 +88,6 @@ void UEntityPool::Advance() ReserveEntityIdsHandler.ProcessOps(NetDriver->Connection->GetCoordinator().GetViewDelta().GetWorkerMessages()); } -void UEntityPool::OnEntityRangeExpired(uint32 ExpiringEntityRangeId) -{ - UE_LOG(LogSpatialEntityPool, Verbose, TEXT("Entity range expired! Range ID: %d"), ExpiringEntityRangeId); - - int32 FoundEntityRangeIndex = ReservedEntityIDRanges.IndexOfByPredicate([ExpiringEntityRangeId](const EntityRange& Element) { - return Element.EntityRangeId == ExpiringEntityRangeId; - }); - - if (FoundEntityRangeIndex == INDEX_NONE) - { - // This entity range has already been cleaned up as a result of running out of Entity IDs. - UE_LOG(LogSpatialEntityPool, Verbose, TEXT("Entity range ID: %d has already been depleted"), ExpiringEntityRangeId); - return; - } - - if (FoundEntityRangeIndex < ReservedEntityIDRanges.Num() - 1) - { - // This is not the most recent entity range, just clean up without requesting additional IDs. - UE_LOG(LogSpatialEntityPool, Verbose, TEXT("Newer range detected, cleaning up Entity range ID: %d without new request"), - ExpiringEntityRangeId); - ReservedEntityIDRanges.RemoveAt(FoundEntityRangeIndex); - } - else - { - // Reserve then cleanup - if (!bIsAwaitingResponse) - { - UE_LOG(LogSpatialEntityPool, Verbose, TEXT("Reserving new Entity range to replace Entity range ID: %d"), ExpiringEntityRangeId); - ReserveEntityIDs(GetDefault()->EntityPoolRefreshCount); - } - // Mark this entity range as expired, so it gets cleaned up when we receive a new entity range from Spatial. - ReservedEntityIDRanges[FoundEntityRangeIndex].bExpired = true; - } -} - Worker_EntityId UEntityPool::GetNextEntityId() { if (ReservedEntityIDRanges.Num() == 0) @@ -151,7 +99,7 @@ Worker_EntityId UEntityPool::GetNextEntityId() } EntityRange& CurrentEntityRange = ReservedEntityIDRanges[0]; - Worker_EntityId NextId = CurrentEntityRange.CurrentEntityId++; + const Worker_EntityId NextId = CurrentEntityRange.CurrentEntityId++; uint32_t TotalRemainingEntityIds = 0; for (EntityRange Range : ReservedEntityIDRanges) diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/Interest/NetCullDistanceInterest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/Interest/NetCullDistanceInterest.cpp index a0e39085d2..000d7d0e69 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/Interest/NetCullDistanceInterest.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/Interest/NetCullDistanceInterest.cpp @@ -249,7 +249,11 @@ TMap NetCullDistanceInterest::GetActorTypeToRadius() // Can't do inline removal since the sorted order is only guaranteed when the map isn't changed. for (const auto& ActorInterestDistance : DiscoveredInterestDistancesSquared) { - check(ActorInterestDistance.Key); + if (!ensureAlwaysMsgf(ActorInterestDistance.Key != nullptr, + TEXT("Failed to add an ActorInterestDistance setting because the relevant UCLass was nullptr"))) + { + continue; + } // Spatial distance works in meters, whereas unreal distance works in cm^2. Here we do the dimensionally strange conversion between // the two. @@ -327,7 +331,12 @@ float NetCullDistanceInterest::NetCullDistanceSquaredToSpatialDistance(float Net void NetCullDistanceInterest::AddTypeHierarchyToConstraint(const UClass& BaseType, QueryConstraint& OutConstraint, USpatialClassInfoManager* ClassInfoManager) { - check(ClassInfoManager); + if (!ensureAlwaysMsgf(ClassInfoManager != nullptr, + TEXT("Failed to add type hierarchy constraint to interset because the ClassInfoManager was nullptr"))) + { + return; + } + TArray ComponentIds = ClassInfoManager->GetComponentIdsForClassHierarchy(BaseType); for (Worker_ComponentId ComponentId : ComponentIds) { diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp index 2bffa7a69f..fdaf9811d4 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp @@ -18,6 +18,10 @@ #include "GameFramework/PlayerController.h" #include "UObject/UObjectIterator.h" +#if WITH_GAMEPLAY_DEBUGGER +#include "GameplayDebuggerCategoryReplicator.h" +#endif + DEFINE_LOG_CATEGORY(LogInterestFactory); DECLARE_STATS_GROUP(TEXT("InterestFactory"), STATGROUP_SpatialInterestFactory, STATCAT_Advanced); @@ -29,6 +33,7 @@ InterestFactory::InterestFactory(USpatialClassInfoManager* InClassInfoManager, U : ClassInfoManager(InClassInfoManager) , PackageMap(InPackageMap) { + check(ClassInfoManager != nullptr); CreateAndCacheInterestState(); } @@ -143,6 +148,15 @@ Interest InterestFactory::CreateServerWorkerInterest(const UAbstractLBStrategy* ServerQuery.Constraint = CreateGDKSnapshotEntitiesConstraint(); AddComponentQueryPairToInterestComponent(ServerInterest, SpatialConstants::SERVER_WORKER_ENTITY_AUTH_COMPONENT_SET_ID, ServerQuery); + // SelfInterest for routing worker components. + ServerQuery = Query(); + ServerQuery.ResultComponentIds = { SpatialConstants::ROUTINGWORKER_TAG_COMPONENT_ID, + SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID, + SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID, + SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID }; + ServerQuery.Constraint.bSelfConstraint = true; + AddComponentQueryPairToInterestComponent(ServerInterest, SpatialConstants::SERVER_WORKER_ENTITY_AUTH_COMPONENT_SET_ID, ServerQuery); + return ServerInterest; } @@ -183,6 +197,14 @@ Interest InterestFactory::CreatePartitionInterest(const UAbstractLBStrategy* LBS PartitionQuery); } +#if WITH_GAMEPLAY_DEBUGGER + // Query to know about all the actors tagged with a gameplay debugger component + PartitionQuery = Query(); + PartitionQuery.ResultComponentIds = { SpatialConstants::GDK_GAMEPLAY_DEBUGGER_COMPONENT_ID }; + PartitionQuery.Constraint.ComponentConstraint = SpatialConstants::GDK_GAMEPLAY_DEBUGGER_COMPONENT_ID; + AddComponentQueryPairToInterestComponent(PartitionInterest, SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID, PartitionQuery); +#endif + return PartitionInterest; } @@ -204,10 +226,10 @@ Interest InterestFactory::CreateRoutingWorkerInterest() ServerQuery.ResultComponentIds = { SpatialConstants::ROUTINGWORKER_TAG_COMPONENT_ID, - SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID, - SpatialConstants::CROSSSERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID, - SpatialConstants::CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID, - SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID, + SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID, + SpatialConstants::CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID, + SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID, + SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID, }; ServerQuery.Constraint.ComponentConstraint = SpatialConstants::ROUTINGWORKER_TAG_COMPONENT_ID; @@ -225,26 +247,38 @@ Interest InterestFactory::CreateInterest(AActor* InActor, const FClassInfo& InIn if (InActor->IsA(APlayerController::StaticClass())) { - // Put the "main" interest queries on the player controller - AddPlayerControllerActorInterest(ResultInterest, InActor, InInfo); + // Put the "main" client interest queries on the player controller + AddClientPlayerControllerActorInterest(ResultInterest, InActor, InInfo); } +#if WITH_GAMEPLAY_DEBUGGER + if (AGameplayDebuggerCategoryReplicator* Replicator = Cast(InActor)) + { + // Put special server interest on the replicator for the auth server to ensure player controller visibility + AddServerGameplayDebuggerCategoryReplicatorActorInterest(ResultInterest, *Replicator); + } +#endif + // Clients need to see owner only and server RPC components on entities they have authority over AddClientSelfInterest(ResultInterest); // Every actor needs a self query for the server to the client RPC endpoint AddServerSelfInterest(ResultInterest); - AddOwnerInterestOnServer(ResultInterest, InActor, InEntityId); + // Ensure the auth server has interest in an actor's ownership chain + AddServerActorOwnerInterest(ResultInterest, InActor, InEntityId); + + // Add interest in AlwaysInterested UProperties + AddAlwaysInterestedInterest(ResultInterest, InActor, InInfo); return ResultInterest; } -void InterestFactory::AddPlayerControllerActorInterest(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo) const +void InterestFactory::AddClientPlayerControllerActorInterest(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo) const { const QueryConstraint LevelConstraint = CreateLevelConstraints(InActor); - AddAlwaysRelevantAndInterestedQuery(OutInterest, InActor, InInfo, LevelConstraint); + AddClientAlwaysRelevantQuery(OutInterest, InActor, InInfo, LevelConstraint); AddUserDefinedQueries(OutInterest, InActor, LevelConstraint); @@ -255,6 +289,57 @@ void InterestFactory::AddPlayerControllerActorInterest(Interest& OutInterest, co } } +#if WITH_GAMEPLAY_DEBUGGER +void InterestFactory::AddServerGameplayDebuggerCategoryReplicatorActorInterest(Interest& OutInterest, + const AGameplayDebuggerCategoryReplicator& Replicator) const +{ + APlayerController* PlayerController = Replicator.GetReplicationOwner(); + if (PlayerController == nullptr) + { + UE_LOG(LogInterestFactory, Warning, + TEXT("Gameplay debugger category replicator actor %s doesn't have a player controller set as its replication owner. An " + "interest query for the authoritative server could not be added and using the gameplay debugger might fail."), + *Replicator.GetName()); + return; + } + + const UNetDriver* NetDriver = PlayerController->GetNetDriver(); + if (NetDriver == nullptr) + { + UE_LOG(LogInterestFactory, Warning, + TEXT("Player controller actor %s doesn't have an associated net driver. An interest query for the authoritative server " + "could not be added and using the gameplay debugger might fail."), + *PlayerController->GetName()); + return; + } + + Query ReplicatorAuthServerQuery; + + // Add a query for the authoritative server to see the player controller + QueryConstraint PlayerControllerConstraint; + PlayerControllerConstraint.EntityIdConstraint = NetDriver->GetActorEntityId(*PlayerController); + ReplicatorAuthServerQuery.Constraint.OrConstraint.Add(PlayerControllerConstraint); + + // Add a query for the authoritative server to see the selected debug actor, if there is one with an entity ID + const AActor* const DebugActor = Replicator.GetDebugActor(); + if (DebugActor != nullptr) + { + const int64 DebugActorEntityId = NetDriver->GetActorEntityId(*DebugActor); + if (DebugActorEntityId != SpatialConstants::INVALID_ENTITY_ID) + { + QueryConstraint DebugActorConstraint; + DebugActorConstraint.EntityIdConstraint = DebugActorEntityId; + ReplicatorAuthServerQuery.Constraint.OrConstraint.Add(DebugActorConstraint); + } + } + + ReplicatorAuthServerQuery.ResultComponentIds = ServerNonAuthInterestResultType.ComponentIds; + ReplicatorAuthServerQuery.ResultComponentSetIds = ServerNonAuthInterestResultType.ComponentSetsIds; + + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID, ReplicatorAuthServerQuery); +} +#endif + void InterestFactory::AddClientSelfInterest(Interest& OutInterest) const { Query NewQuery; @@ -300,7 +385,7 @@ bool InterestFactory::DoOwnersHaveEntityId(const AActor* Actor) const return true; } -void InterestFactory::AddOwnerInterestOnServer(Interest& OutInterest, const AActor* InActor, const Worker_EntityId& EntityId) const +void InterestFactory::AddServerActorOwnerInterest(Interest& OutInterest, const AActor* InActor, const Worker_EntityId& EntityId) const { AActor* Owner = InActor->GetOwner(); Query OwnerChainQuery; @@ -329,51 +414,57 @@ void InterestFactory::AddOwnerInterestOnServer(Interest& OutInterest, const AAct } } -void InterestFactory::AddAlwaysRelevantAndInterestedQuery(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo, - const QueryConstraint& LevelConstraint) const +void InterestFactory::AddClientAlwaysRelevantQuery(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo, + const QueryConstraint& LevelConstraint) const { const USpatialGDKSettings* Settings = GetDefault(); - QueryConstraint AlwaysInterestedConstraint = CreateAlwaysInterestedConstraint(InActor, InInfo); QueryConstraint AlwaysRelevantConstraint = CreateClientAlwaysRelevantConstraint(); - QueryConstraint SystemDefinedConstraints; - - if (AlwaysInterestedConstraint.IsValid()) - { - SystemDefinedConstraints.OrConstraint.Add(AlwaysInterestedConstraint); - } - - if (AlwaysRelevantConstraint.IsValid()) - { - SystemDefinedConstraints.OrConstraint.Add(AlwaysRelevantConstraint); - } - // Add the level constraint here as all client queries need to make sure they don't check out anything outside their loaded levels. - QueryConstraint SystemAndLevelConstraint; - SystemAndLevelConstraint.AndConstraint.Add(SystemDefinedConstraints); - SystemAndLevelConstraint.AndConstraint.Add(LevelConstraint); + QueryConstraint AlwaysRelevantAndLevelConstraint; + AlwaysRelevantAndLevelConstraint.AndConstraint.Add(AlwaysRelevantConstraint); + AlwaysRelevantAndLevelConstraint.AndConstraint.Add(LevelConstraint); Query ClientSystemQuery; - ClientSystemQuery.Constraint = SystemAndLevelConstraint; + ClientSystemQuery.Constraint = AlwaysRelevantAndLevelConstraint; ClientSystemQuery.ResultComponentIds = ClientNonAuthInterestResultType.ComponentIds; ClientSystemQuery.ResultComponentSetIds = ClientNonAuthInterestResultType.ComponentSetsIds; AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID, ClientSystemQuery); +} - // Add always interested constraint to the server as well to make sure the server sees the same as the client. - // The always relevant constraint is added as part of the server worker query, so leave that out here. - // Servers also don't need to be level constrained. - if (Settings->bEnableClientQueriesOnServer) +void InterestFactory::AddAlwaysInterestedInterest(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo) const +{ + QueryConstraint AlwaysInterestedConstraint = CreateAlwaysInterestedConstraint(InActor, InInfo); + + if (AlwaysInterestedConstraint.IsValid()) { - Query ServerSystemQuery; - QueryConstraint ServerSystemConstraint; - ServerSystemConstraint.OrConstraint.Add(AlwaysInterestedConstraint); - ServerSystemQuery.Constraint = ServerSystemConstraint; - ServerSystemQuery.ResultComponentIds = ServerNonAuthInterestResultType.ComponentIds; - ServerSystemQuery.ResultComponentSetIds = ServerNonAuthInterestResultType.ComponentSetsIds; - - AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID, ServerSystemQuery); + { + const QueryConstraint LevelConstraint = CreateLevelConstraints(InActor); + + QueryConstraint AlwaysInterestAndLevelConstraint; + AlwaysInterestAndLevelConstraint.AndConstraint.Add(AlwaysInterestedConstraint); + AlwaysInterestAndLevelConstraint.AndConstraint.Add(LevelConstraint); + + Query ClientAlwaysInterestedQuery; + ClientAlwaysInterestedQuery.Constraint = AlwaysInterestAndLevelConstraint; + ClientAlwaysInterestedQuery.ResultComponentIds = ClientNonAuthInterestResultType.ComponentIds; + ClientAlwaysInterestedQuery.ResultComponentSetIds = ClientNonAuthInterestResultType.ComponentSetsIds; + + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID, + ClientAlwaysInterestedQuery); + } + + { + Query ServerAlwaysInterestedQuery; + ServerAlwaysInterestedQuery.Constraint = AlwaysInterestedConstraint; + ServerAlwaysInterestedQuery.ResultComponentIds = ServerNonAuthInterestResultType.ComponentIds; + ServerAlwaysInterestedQuery.ResultComponentSetIds = ServerNonAuthInterestResultType.ComponentSetsIds; + + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID, + ServerAlwaysInterestedQuery); + } } } @@ -466,8 +557,6 @@ FrequencyToConstraintsMap InterestFactory::GetUserDefinedFrequencyToConstraintsM void InterestFactory::GetActorUserDefinedQueryConstraints(const AActor* InActor, FrequencyToConstraintsMap& OutFrequencyToConstraints, bool bRecurseChildren) const { - check(ClassInfoManager); - if (InActor == nullptr) { return; @@ -563,7 +652,6 @@ bool InterestFactory::ShouldAddNetCullDistanceInterest(const AActor* InActor) co if (ActorInterestComponents.Num() == 1) { const UActorInterestComponent* ActorInterest = ActorInterestComponents[0]; - check(ActorInterest); if (!ActorInterest->bUseNetCullDistanceSquaredForCheckoutRadius) { return false; @@ -650,9 +738,20 @@ QueryConstraint InterestFactory::CreateLevelConstraints(const AActor* InActor) c LevelConstraint.OrConstraint.Add(DefaultConstraint); UNetConnection* Connection = InActor->GetNetConnection(); - check(Connection); + if (!ensureAlwaysMsgf(Connection != nullptr, + TEXT("Failed to create interest level constraints. Couldn't access net connection for Actor %s"), + *GetNameSafe(InActor))) + { + return QueryConstraint{}; + } + APlayerController* PlayerController = Connection->GetPlayerController(nullptr); - check(PlayerController); + if (!ensureAlwaysMsgf(PlayerController != nullptr, + TEXT("Failed to create interest level constraints. Couldn't find player controller for Actor %s"), + *GetNameSafe(InActor))) + { + return QueryConstraint{}; + } const TSet& LoadedLevels = PlayerController->NetConnection->ClientVisibleLevelNames; diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp index 1c0c50c420..3f75f8aab1 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp @@ -74,9 +74,12 @@ void LogRPCError(const FRPCErrorInfo& ErrorInfo, ERPCQueueType QueueType, const *ERPCResultToString(ErrorInfo.ErrorCode)); const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); - check(SpatialGDKSettings != nullptr); + if (!ensureAlwaysMsgf(SpatialGDKSettings != nullptr, TEXT("Failed to log RPC error. Couldn't access GDK settings"))) + { + return; + } - if (TimeDiff.GetTotalSeconds() > SpatialGDKSettings->GetSecondsBeforeWarning(ErrorInfo.ErrorCode)) + if (TimeDiff.GetTotalSeconds() >= SpatialGDKSettings->GetSecondsBeforeWarning(ErrorInfo.ErrorCode)) { UE_LOG(LogRPCContainer, Warning, TEXT("%s"), *OutputLog); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCRingBuffer.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCRingBuffer.cpp index e293d635f4..51b94c6520 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCRingBuffer.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCRingBuffer.cpp @@ -31,6 +31,8 @@ Worker_ComponentId GetRingBufferComponentId(ERPCType Type) return SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID; case ERPCType::NetMulticast: return SpatialConstants::MULTICAST_RPCS_COMPONENT_ID; + case ERPCType::CrossServer: + return SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID; default: checkNoEntry(); return SpatialConstants::INVALID_COMPONENT_ID; diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebugger.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebugger.cpp index d8402f088b..491a562bc3 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebugger.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebugger.cpp @@ -80,7 +80,10 @@ void ASpatialDebugger::BeginPlay() { Super::BeginPlay(); - check(NetDriver != nullptr); + if (!ensureAlwaysMsgf(NetDriver != nullptr, TEXT("Failed to call BeginPlay on SpatialDebugger. NetDriver was nullptr"))) + { + return; + } NetDriver->RegisterSpatialDebugger(this); @@ -92,7 +95,10 @@ void ASpatialDebugger::BeginPlay() { AActor* PresentActor = PresentActorPair.Value.Get(); - check(IsValid(PresentActor)); + if (!ensureAlwaysMsgf(IsValid(PresentActor), TEXT("Actor was invalid when iterating through debugger system"))) + { + continue; + } OnEntityAdded(PresentActor); } @@ -120,7 +126,10 @@ void ASpatialDebugger::Tick(float DeltaSeconds) { Super::Tick(DeltaSeconds); - check(NetDriver != nullptr); + if (!ensureAlwaysMsgf(NetDriver != nullptr, TEXT("Failed to call SpatialDebugger::Tick. NetDriver was nullptr"))) + { + return; + } if (!NetDriver->IsServer()) { @@ -219,7 +228,12 @@ void ASpatialDebugger::CreateWorkerRegions() } SpawnParams.bHideFromSceneOutliner = true; #endif - check(World != nullptr); + + if (!ensureAlwaysMsgf(World != nullptr, TEXT("Failed to call SpatialDebugger::CreateWorkerRegions. World was nullptr"))) + { + return; + } + SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; for (const FWorkerRegionInfo& WorkerRegionData : WorkerRegions) { @@ -265,7 +279,11 @@ void ASpatialDebugger::Destroyed() void ASpatialDebugger::LoadIcons() { - check(NetDriver != nullptr && !NetDriver->IsServer()); + if (!ensureAlwaysMsgf(NetDriver != nullptr && !NetDriver->IsServer(), + TEXT("Failed to call SpatialDebugger::LoadIcons. NetDriver was nullptr OR running on server"))) + { + return; + } UTexture2D* DefaultTexture = LoadObject(nullptr, TEXT("/Engine/EngineResources/DefaultTexture.DefaultTexture")); @@ -432,7 +450,11 @@ void ASpatialDebugger::DrawTag(UCanvas* Canvas, const FVector2D& ScreenLocation, { SCOPE_CYCLE_COUNTER(STAT_DrawTag); - check(NetDriver != nullptr && !NetDriver->IsServer()); + if (!ensureAlwaysMsgf(NetDriver != nullptr && !NetDriver->IsServer(), + TEXT("Failed to call SpatialDebugger::DrawTag. NetDriver was nullptr OR running on server"))) + { + return; + } // TODO: UNR-5481 - Fix this hack for fixing spatial debugger crash after client travel if (!NetDriver->Connection->HasValidCoordinator()) @@ -578,7 +600,11 @@ void ASpatialDebugger::DrawDebug(UCanvas* Canvas, APlayerController* /* Controll { SCOPE_CYCLE_COUNTER(STAT_DrawDebug); - check(NetDriver != nullptr && !NetDriver->IsServer()); + if (!ensureAlwaysMsgf(NetDriver != nullptr && !NetDriver->IsServer(), + TEXT("Failed to call SpatialDebugger::DrawDebug. NetDriver was nullptr OR running on server"))) + { + return; + } #if WITH_EDITOR // Prevent one client's data rendering in another client's view in PIE when using UDebugDrawService. Lifted from EQSRenderingComponent. @@ -868,7 +894,11 @@ void ASpatialDebugger::DrawDebugLocalPlayer(UCanvas* Canvas) void ASpatialDebugger::SpatialToggleDebugger() { - check(NetDriver != nullptr && !NetDriver->IsServer()); + if (!ensureAlwaysMsgf(NetDriver != nullptr && !NetDriver->IsServer(), + TEXT("Failed to call SpatialDebugger::SpatialToggleDebugger. NetDriver was nullptr OR running on server"))) + { + return; + } if (DrawDebugDelegateHandle.IsValid()) { @@ -932,7 +962,11 @@ void ASpatialDebugger::EditorInitialiseWorkerRegions() WorkerRegions.Empty(); const UWorld* World = GEditor->GetEditorWorldContext().World(); - check(World != nullptr); + + if (!ensureAlwaysMsgf(World != nullptr, TEXT("Failed to EditorInitialiseWorkerRegions. Couldn't access World from GEditor"))) + { + return; + } const UAbstractSpatialMultiWorkerSettings* MultiWorkerSettings = USpatialStatics::GetSpatialMultiWorkerClass(World)->GetDefaultObject(); @@ -984,6 +1018,9 @@ void ASpatialDebugger::PostEditChangeProperty(FPropertyChangedEvent& PropertyCha SpatialDebuggerSystem* ASpatialDebugger::GetDebuggerSystem() const { - check(NetDriver->SpatialDebuggerSystem.IsValid()); + if (!ensureAlwaysMsgf(NetDriver->SpatialDebuggerSystem.IsValid(), TEXT("Failed to access invalid debugger system"))) + { + return nullptr; + } return NetDriver->SpatialDebuggerSystem.Get(); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebuggerSystem.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebuggerSystem.cpp index a7a8c194cd..c127851307 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebuggerSystem.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebuggerSystem.cpp @@ -91,7 +91,11 @@ void SpatialDebuggerSystem::UpdateSpatialDebuggingData(Worker_EntityId EntityId, void SpatialDebuggerSystem::OnEntityAdded(const Worker_EntityId EntityId) { - check(NetDriver != nullptr); + if (!ensureAlwaysMsgf(NetDriver != nullptr, TEXT("NetDriver was nullptr in OnEntityAdded %lld callback"), EntityId)) + { + return; + } + if (NetDriver->IsServer()) { if (SubView->HasAuthority(EntityId, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID)) @@ -102,7 +106,10 @@ void SpatialDebuggerSystem::OnEntityAdded(const Worker_EntityId EntityId) return; } - check(!EntityActorMapping.Contains(EntityId)); + if (!ensureAlwaysMsgf(!EntityActorMapping.Contains(EntityId), TEXT("NetDriver was nullptr in OnEntityAdded %lld callback"), EntityId)) + { + return; + } if (AActor* Actor = Cast(NetDriver->PackageMap->GetObjectFromEntityId(EntityId).Get())) { @@ -162,12 +169,21 @@ void SpatialDebuggerSystem::ActorAuthorityGained(const Worker_EntityId EntityId) void SpatialDebuggerSystem::ActorAuthorityIntentChanged(Worker_EntityId EntityId, VirtualWorkerId NewIntentVirtualWorkerId) const { TOptional DebuggingInfo = GetDebuggingData(EntityId); - check(DebuggingInfo.IsSet()); + if (!ensureAlwaysMsgf(DebuggingInfo.IsSet(), + TEXT("Failed to process auth intent change for entity %lld because debugging info was invalid"), EntityId)) + { + return; + } + DebuggingInfo->IntentVirtualWorkerId = NewIntentVirtualWorkerId; const PhysicalWorkerName* NewAuthoritativePhysicalWorkerName = NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(NewIntentVirtualWorkerId); - check(NewAuthoritativePhysicalWorkerName != nullptr); + if (!ensureAlwaysMsgf(NewAuthoritativePhysicalWorkerName != nullptr, TEXT("Failed to get physical worker name for virtual worker %u"), + NewIntentVirtualWorkerId)) + { + return; + } DebuggingInfo->IntentColor = GetColorForWorkerName(*NewAuthoritativePhysicalWorkerName); FWorkerComponentUpdate DebuggingUpdate = DebuggingInfo->CreateSpatialDebuggingUpdate(); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLatencyTracer.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLatencyTracer.cpp index cfa652bc1f..ef9535ff7e 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLatencyTracer.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLatencyTracer.cpp @@ -36,13 +36,13 @@ class UEStream : public std::stringbuf UEStream UStream; #if TRACE_LIB_ACTIVE -const improbable::trace::SpanContext ReadSpanContext(const void* TraceBytes, const void* SpanBytes) +const improbable::legacy::trace::SpanContext ReadSpanContext(const void* TraceBytes, const void* SpanBytes) { - improbable::trace::TraceId _TraceId; - memcpy(&_TraceId[0], TraceBytes, sizeof(improbable::trace::TraceId)); + improbable::legacy::trace::TraceId _TraceId; + memcpy(&_TraceId[0], TraceBytes, sizeof(improbable::legacy::trace::TraceId)); - improbable::trace::SpanId _SpanId; - memcpy(&_SpanId[0], SpanBytes, sizeof(improbable::trace::SpanId)); + improbable::legacy::trace::SpanId _SpanId; + memcpy(&_SpanId[0], SpanBytes, sizeof(improbable::legacy::trace::SpanId)); return { _TraceId, _SpanId }; } @@ -60,7 +60,7 @@ USpatialLatencyTracer::USpatialLatencyTracer() void USpatialLatencyTracer::RegisterProject(UObject* WorldContextObject, const FString& ProjectId) { #if TRACE_LIB_ACTIVE - using namespace improbable::exporters::trace; + using namespace improbable::legacy::exporters::trace; StackdriverExporter::Register({ TCHAR_TO_UTF8(*ProjectId) }); @@ -196,26 +196,6 @@ bool USpatialLatencyTracer::IsValidKey(const TraceKey Key) return (TraceMap.Find(Key) != nullptr); } -TraceKey USpatialLatencyTracer::RetrievePendingTrace(const UObject* Obj, const UFunction* Function) -{ - FScopeLock Lock(&Mutex); - - ActorFuncKey FuncKey{ Cast(Obj), Function }; - TraceKey ReturnKey = InvalidTraceKey; - TrackingRPCs.RemoveAndCopyValue(FuncKey, ReturnKey); - return ReturnKey; -} - -TraceKey USpatialLatencyTracer::RetrievePendingTrace(const UObject* Obj, const GDK_PROPERTY(Property) * Property) -{ - FScopeLock Lock(&Mutex); - - ActorPropertyKey PropKey{ Cast(Obj), Property }; - TraceKey ReturnKey = InvalidTraceKey; - TrackingProperties.RemoveAndCopyValue(PropKey, ReturnKey); - return ReturnKey; -} - TraceKey USpatialLatencyTracer::RetrievePendingTrace(const UObject* Obj, const FString& Tag) { FScopeLock Lock(&Mutex); @@ -254,71 +234,6 @@ void USpatialLatencyTracer::WriteAndEndTrace(const TraceKey Key, const FString& } } -void USpatialLatencyTracer::WriteTraceToSchemaObject(const TraceKey Key, Schema_Object* Obj, const Schema_FieldId FieldId) -{ - FScopeLock Lock(&Mutex); - - if (TraceSpan* Trace = TraceMap.Find(Key)) - { - Schema_Object* TraceObj = Schema_AddObject(Obj, FieldId); - - const improbable::trace::SpanContext& TraceContext = Trace->context(); - improbable::trace::TraceId _TraceId = TraceContext.trace_id(); - improbable::trace::SpanId _SpanId = TraceContext.span_id(); - - SpatialGDK::AddBytesToSchema(TraceObj, SpatialConstants::UNREAL_RPC_TRACE_ID, &_TraceId[0], _TraceId.size()); - SpatialGDK::AddBytesToSchema(TraceObj, SpatialConstants::UNREAL_RPC_SPAN_ID, &_SpanId[0], _SpanId.size()); - } -} - -TraceKey USpatialLatencyTracer::ReadTraceFromSchemaObject(Schema_Object* Obj, const Schema_FieldId FieldId) -{ - FScopeLock Lock(&Mutex); - - if (Schema_GetObjectCount(Obj, FieldId) > 0) - { - Schema_Object* TraceData = Schema_IndexObject(Obj, FieldId, 0); - - const uint8* TraceBytes = Schema_GetBytes(TraceData, SpatialConstants::UNREAL_RPC_TRACE_ID); - const uint8* SpanBytes = Schema_GetBytes(TraceData, SpatialConstants::UNREAL_RPC_SPAN_ID); - - const improbable::trace::SpanContext DestContext = ReadSpanContext(TraceBytes, SpanBytes); - - TraceKey Key = InvalidTraceKey; - - for (const auto& TracePair : TraceMap) - { - const TraceKey& _Key = TracePair.Key; - const TraceSpan& Span = TracePair.Value; - - if (Span.context().trace_id() == DestContext.trace_id()) - { - Key = _Key; - break; - } - } - - if (Key != InvalidTraceKey) - { - TraceSpan* Span = TraceMap.Find(Key); - - WriteKeyFrameToTrace(Span, TEXT("Local Trace - Schema Obj Read")); - } - else - { - FString SpanMsg = FormatMessage(TEXT("Remote Parent Trace - Schema Obj Read")); - TraceSpan RetrieveTrace = improbable::trace::Span::StartSpanWithRemoteParent(TCHAR_TO_UTF8(*SpanMsg), DestContext); - - Key = GenerateNewTraceKey(); - TraceMap.Add(Key, MoveTemp(RetrieveTrace)); - } - - return Key; - } - - return InvalidTraceKey; -} - FSpatialLatencyPayload USpatialLatencyTracer::RetrievePayload_Internal(const UObject* Obj, const FString& Tag) { FScopeLock Lock(&Mutex); @@ -328,10 +243,12 @@ FSpatialLatencyPayload USpatialLatencyTracer::RetrievePayload_Internal(const UOb { if (const TraceSpan* Span = TraceMap.Find(Key)) { - const improbable::trace::SpanContext& TraceContext = Span->context(); + const improbable::legacy::trace::SpanContext& TraceContext = Span->context(); - TArray TraceBytes = TArray((const uint8_t*)&TraceContext.trace_id()[0], sizeof(improbable::trace::TraceId)); - TArray SpanBytes = TArray((const uint8_t*)&TraceContext.span_id()[0], sizeof(improbable::trace::SpanId)); + TArray TraceBytes = + TArray((const uint8_t*)&TraceContext.trace_id()[0], sizeof(improbable::legacy::trace::TraceId)); + TArray SpanBytes = + TArray((const uint8_t*)&TraceContext.span_id()[0], sizeof(improbable::legacy::trace::SpanId)); return FSpatialLatencyPayload(MoveTemp(TraceBytes), MoveTemp(SpanBytes), Key); } } @@ -343,50 +260,6 @@ void USpatialLatencyTracer::ResetWorkerId() WorkerId = TEXT("DeviceId_") + FPlatformMisc::GetDeviceId(); } -void USpatialLatencyTracer::OnEnqueueMessage(const SpatialGDK::FOutgoingMessage* Message) -{ - if (Message->Type == SpatialGDK::EOutgoingMessageType::ComponentUpdate) - { - const SpatialGDK::FComponentUpdate* ComponentUpdate = static_cast(Message); - WriteToLatencyTrace(ComponentUpdate->Update.Trace, TEXT("Moved componentUpdate to Worker queue")); - } - else if (Message->Type == SpatialGDK::EOutgoingMessageType::AddComponent) - { - const SpatialGDK::FAddComponent* ComponentAdd = static_cast(Message); - WriteToLatencyTrace(ComponentAdd->Data.Trace, TEXT("Moved componentAdd to Worker queue")); - } - else if (Message->Type == SpatialGDK::EOutgoingMessageType::CreateEntityRequest) - { - const SpatialGDK::FCreateEntityRequest* CreateEntityRequest = static_cast(Message); - for (auto& Component : CreateEntityRequest->Components) - { - WriteToLatencyTrace(Component.Trace, TEXT("Moved createEntityRequest to Worker queue")); - } - } -} - -void USpatialLatencyTracer::OnDequeueMessage(const SpatialGDK::FOutgoingMessage* Message) -{ - if (Message->Type == SpatialGDK::EOutgoingMessageType::ComponentUpdate) - { - const SpatialGDK::FComponentUpdate* ComponentUpdate = static_cast(Message); - WriteAndEndTrace(ComponentUpdate->Update.Trace, TEXT("Sent componentUpdate to Worker SDK"), true); - } - else if (Message->Type == SpatialGDK::EOutgoingMessageType::AddComponent) - { - const SpatialGDK::FAddComponent* ComponentAdd = static_cast(Message); - WriteAndEndTrace(ComponentAdd->Data.Trace, TEXT("Sent componentAdd to Worker SDK"), true); - } - else if (Message->Type == SpatialGDK::EOutgoingMessageType::CreateEntityRequest) - { - const SpatialGDK::FCreateEntityRequest* CreateEntityRequest = static_cast(Message); - for (auto& Component : CreateEntityRequest->Components) - { - WriteAndEndTrace(Component.Trace, TEXT("Sent createEntityRequest to Worker SDK"), true); - } - } -} - bool USpatialLatencyTracer::BeginLatencyTrace_Internal(const FString& TraceDesc, FSpatialLatencyPayload& OutLatencyPayload) { // TODO: UNR-2787 - Improve mutex-related latency @@ -395,14 +268,14 @@ bool USpatialLatencyTracer::BeginLatencyTrace_Internal(const FString& TraceDesc, FScopeLock Lock(&Mutex); FString SpanMsg = FormatMessage(TraceDesc, true); - TraceSpan NewTrace = improbable::trace::Span::StartSpan(TCHAR_TO_UTF8(*SpanMsg), nullptr); + TraceSpan NewTrace = improbable::legacy::trace::Span::StartSpan(TCHAR_TO_UTF8(*SpanMsg), nullptr); // Construct payload data from trace - const improbable::trace::SpanContext& TraceContext = NewTrace.context(); + const improbable::legacy::trace::SpanContext& TraceContext = NewTrace.context(); { - TArray TraceBytes = TArray((const uint8_t*)&TraceContext.trace_id()[0], sizeof(improbable::trace::TraceId)); - TArray SpanBytes = TArray((const uint8_t*)&TraceContext.span_id()[0], sizeof(improbable::trace::SpanId)); + TArray TraceBytes = TArray((const uint8_t*)&TraceContext.trace_id()[0], sizeof(improbable::legacy::trace::TraceId)); + TArray SpanBytes = TArray((const uint8_t*)&TraceContext.span_id()[0], sizeof(improbable::legacy::trace::SpanId)); OutLatencyPayload = FSpatialLatencyPayload(MoveTemp(TraceBytes), MoveTemp(SpanBytes), GenerateNewTraceKey()); } @@ -573,10 +446,10 @@ void USpatialLatencyTracer::ResolveKeyInLatencyPayload(FSpatialLatencyPayload& P // Uninitialized key, generate and add to map Payload.Key = GenerateNewTraceKey(); - const improbable::trace::SpanContext DestContext = ReadSpanContext(Payload.TraceId.GetData(), Payload.SpanId.GetData()); + const improbable::legacy::trace::SpanContext DestContext = ReadSpanContext(Payload.TraceId.GetData(), Payload.SpanId.GetData()); FString SpanMsg = FormatMessage(TEXT("Remote Parent Trace - Payload Obj Read")); - TraceSpan RetrieveTrace = improbable::trace::Span::StartSpanWithRemoteParent(TCHAR_TO_UTF8(*SpanMsg), DestContext); + TraceSpan RetrieveTrace = improbable::legacy::trace::Span::StartSpanWithRemoteParent(TCHAR_TO_UTF8(*SpanMsg), DestContext); TraceMap.Add(Payload.Key, MoveTemp(RetrieveTrace)); } @@ -587,7 +460,7 @@ void USpatialLatencyTracer::WriteKeyFrameToTrace(const TraceSpan* Trace, const F if (Trace != nullptr) { FString TraceMsg = FormatMessage(TraceDesc); - improbable::trace::Span::StartSpan(TCHAR_TO_UTF8(*TraceMsg), Trace).End(); + improbable::legacy::trace::Span::StartSpan(TCHAR_TO_UTF8(*TraceMsg), Trace).End(); } } @@ -609,7 +482,7 @@ void USpatialLatencyTracer::Debug_SendTestTrace() { #if TRACE_LIB_ACTIVE AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [] { - using namespace improbable::trace; + using namespace improbable::legacy::trace; std::cout << "Sending test trace" << std::endl; diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLatencyTracerMinimal.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLatencyTracerMinimal.cpp deleted file mode 100644 index 747722e1d0..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLatencyTracerMinimal.cpp +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#include "Utils/SpatialLatencyTracerMinimal.h" -#include "SpatialConstants.h" -#include "Utils/SpatialLatencyTracer.h" - -// We can't use the types directly because of a mismatch between the legacy headers included by SpatialLatencyTracer -static_assert(sizeof(uint32) == sizeof(Schema_FieldId), "Expected size match"); -static_assert(sizeof(int32) == sizeof(TraceKey), "Expected size match"); - -int32 FSpatialLatencyTracerMinimal::ReadTraceFromSchemaObject(worker::c::Schema_Object* Obj, uint32 FieldId) -{ -#if TRACE_LIB_ACTIVE - if (USpatialLatencyTracer* Tracer = USpatialLatencyTracer::GetTracer(nullptr)) - { - return Tracer->ReadTraceFromSchemaObject(Obj, static_cast(FieldId)); - } -#endif - return static_cast(InvalidTraceKey); -} - -void FSpatialLatencyTracerMinimal::WriteTraceToSchemaObject(int32 Key, worker::c::Schema_Object* Obj, uint32 FieldId) -{ -#if TRACE_LIB_ACTIVE - if (USpatialLatencyTracer* Tracer = USpatialLatencyTracer::GetTracer(nullptr)) - { - Tracer->WriteTraceToSchemaObject(static_cast(Key), Obj, - static_cast(SpatialConstants::UNREAL_RPC_PAYLOAD_TRACE_ID)); - } -#endif -} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLoadBalancingHandler.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLoadBalancingHandler.cpp index 1e5a3dd282..b4efb41f6e 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLoadBalancingHandler.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLoadBalancingHandler.cpp @@ -79,10 +79,10 @@ FSpatialLoadBalancingHandler::EvaluateActorResult FSpatialLoadBalancingHandler:: } const bool bNetOwnerHasAuth = NetOwner->HasAuthority(); + const bool bShouldHaveAuthority = NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*NetOwner); // Load balance if we are not supposed to be on this worker, or if we are separated from our owner. - if ((!NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*NetOwner) || !bNetOwnerHasAuth) - && !NetDriver->LockingPolicy->IsLocked(Actor)) + if ((!bShouldHaveAuthority || !bNetOwnerHasAuth) && !NetDriver->LockingPolicy->IsLocked(Actor)) { uint64 HierarchyAuthorityReceivedTimestamp = GetLatestAuthorityChangeFromHierarchy(NetOwner); @@ -125,6 +125,13 @@ FSpatialLoadBalancingHandler::EvaluateActorResult FSpatialLoadBalancingHandler:: UE_LOG(LogSpatialLoadBalancingHandler, Error, TEXT("Load Balancing Strategy returned invalid virtual worker for actor %s"), *Actor->GetName()); } + else if (!bShouldHaveAuthority && NewAuthVirtualWorkerId == NetDriver->LoadBalanceStrategy->GetLocalVirtualWorkerId()) + { + UE_LOG(LogSpatialLoadBalancingHandler, Error, + TEXT("ShouldHaveAuthority returned false for actor %s, but WhoShouldHaveAuthority returned this worker's id. " + "Actor will not be migrated."), + *Actor->GetName()); + } else { OutNetOwner = NetOwner; diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp index 7959fdf524..7bcf9dcb82 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp @@ -2,8 +2,11 @@ #include "Utils/SpatialMetrics.h" +#include "CoreGlobals.h" #include "Engine/Engine.h" #include "EngineGlobals.h" +#include "TimerManager.h" + #if ENGINE_MINOR_VERSION >= 26 #include "ProfilingDebugging/TraceAuxiliary.h" #endif @@ -15,37 +18,6 @@ DEFINE_LOG_CATEGORY(LogSpatialMetrics); -namespace SpatialMetricsPrivate -{ -enum class EServerCommands : uint8 -{ - StartInsights, - - ServerCommandsCount, - ServerCommandInvalid = ServerCommandsCount, -}; - -const FString ServerCommandNames[static_cast(EServerCommands::ServerCommandsCount) + 1] = { TEXT("StartInsights"), TEXT("Invalid") }; - -const FString& ServerCommandsEnumToString(const EServerCommands Command) -{ - return ServerCommandNames[static_cast(Command)]; -} - -const EServerCommands ServerCommandsStringToEnum(const FString& Command) -{ - for (uint8 i = 0; i < static_cast(EServerCommands::ServerCommandsCount); ++i) - { - if (Command.Equals(ServerCommandNames[i], ESearchCase::IgnoreCase)) - { - return static_cast(i); - } - } - - return EServerCommands::ServerCommandInvalid; -} -} // namespace SpatialMetricsPrivate - void USpatialMetrics::Init(USpatialWorkerConnection* InConnection, float InNetServerMaxTickRate, bool bInIsServer) { Connection = InConnection; @@ -381,14 +353,125 @@ void USpatialMetrics::OnModifySettingCommand(Schema_Object* CommandPayload) void USpatialMetrics::SpatialExecServerCmd(const FString& ServerName, const FString& Command, const FString& Args) { - const SpatialMetricsPrivate::EServerCommands ServerCommand = SpatialMetricsPrivate::ServerCommandsStringToEnum(Command); - if (ServerCommand == SpatialMetricsPrivate::EServerCommands::ServerCommandInvalid) + const int32 Index = StaticEnum()->GetIndexByNameString(Command); + if (Index == INDEX_NONE) { UE_LOG(LogSpatialMetrics, Error, TEXT("SpatialExecServerCmd: Failed to execute server command. Command not found. Command %s (%s)"), *Command, *Args); return; } + SpatialExecServerCmd_Internal(ServerName, static_cast(Index), Args); +} + +void USpatialMetrics::OnExecServerCmdCommand(Schema_Object* CommandPayload) +{ + const FString ServerName = + SpatialGDK::GetStringFromSchema(CommandPayload, SpatialConstants::EXEC_SERVER_COMMAND_PAYLOAD_SERVER_NAME_ID); + const int32 Command = Schema_GetInt32(CommandPayload, SpatialConstants::EXEC_SERVER_COMMAND_PAYLOAD_COMMAND_ID); + const FString Args = SpatialGDK::GetStringFromSchema(CommandPayload, SpatialConstants::EXEC_SERVER_COMMAND_PAYLOAD_ARGS_ID); + + if (!StaticEnum()->IsValidEnumValue(Command)) + { + UE_LOG(LogSpatialMetrics, Error, + TEXT("OnExecServerCmdCommand: Failed to execute server command. Command not found. Command %d (%s)"), Command, *Args); + return; + } + + SpatialExecServerCmd_Internal(ServerName, static_cast(Command), Args); +} + +void USpatialMetrics::TrackSentRPC(UFunction* Function, ERPCType RPCType, int PayloadSize) +{ + if (!bRPCTrackingEnabled) + { + return; + } + + FString FunctionName = FString::Printf(TEXT("%s::%s"), *Function->GetOuter()->GetName(), *Function->GetName()); + + if (RecentRPCs.Find(FunctionName) == nullptr) + { + RPCStat Stat; + Stat.Name = FunctionName; + Stat.Type = RPCType; + Stat.Calls = 0; + Stat.TotalPayload = 0; + + RecentRPCs.Add(FunctionName, Stat); + } + + RPCStat& Stat = RecentRPCs[FunctionName]; + Stat.Calls++; + Stat.TotalPayload += PayloadSize; +} + +void USpatialMetrics::HandleWorkerMetrics(const Worker_Op& Op) +{ + int32 NumGaugeMetrics = Op.op.metrics.metrics.gauge_metric_count; + int32 NumHistogramMetrics = Op.op.metrics.metrics.histogram_metric_count; + if (NumGaugeMetrics > 0 || NumHistogramMetrics > 0) // We store these here so we can forward them with our metrics submission + { + FString StringTmp; + StringTmp.Reserve(128); + + for (int32 i = 0; i < NumGaugeMetrics; i++) + { + const Worker_GaugeMetric& WorkerMetric = Op.op.metrics.metrics.gauge_metrics[i]; + StringTmp = WorkerMetric.key; + WorkerSDKGaugeMetrics.FindOrAdd(StringTmp) = WorkerMetric.value; + } + + for (int32 i = 0; i < NumHistogramMetrics; i++) + { + const Worker_HistogramMetric& WorkerMetric = Op.op.metrics.metrics.histogram_metrics[i]; + StringTmp = WorkerMetric.key; + WorkerHistogramValues& HistogramMetrics = WorkerSDKHistogramMetrics.FindOrAdd(StringTmp); + HistogramMetrics.Sum = WorkerMetric.sum; + int32 NumBuckets = WorkerMetric.bucket_count; + HistogramMetrics.Buckets.SetNum(NumBuckets); + for (int32 j = 0; j < NumBuckets; j++) + { + HistogramMetrics.Buckets[j] = + TTuple{ WorkerMetric.buckets[j].upper_bound, WorkerMetric.buckets[j].samples }; + } + } + + if (WorkerMetricsUpdated.IsBound()) + { + WorkerMetricsUpdated.Broadcast(WorkerSDKGaugeMetrics, WorkerSDKHistogramMetrics); + } + } +} + +void USpatialMetrics::SetCustomMetric(const FString& Metric, const UserSuppliedMetric& Delegate) +{ + UE_LOG(LogSpatialMetrics, Log, TEXT("USpatialMetrics: Adding custom metric %s (%s)"), *Metric, + Delegate.GetUObject() ? *GetNameSafe(Delegate.GetUObject()) : TEXT("Not attached to UObject")); + if (UserSuppliedMetric* ExistingMetric = UserSuppliedMetrics.Find(Metric)) + { + *ExistingMetric = Delegate; + } + else + { + UserSuppliedMetrics.Add(Metric, Delegate); + } +} + +void USpatialMetrics::RemoveCustomMetric(const FString& Metric) +{ + if (UserSuppliedMetric* ExistingMetric = UserSuppliedMetrics.Find(Metric)) + { + UE_LOG(LogSpatialMetrics, Log, TEXT("USpatialMetrics: Removing custom metric %s (%s)"), *Metric, + ExistingMetric->GetUObject() ? *GetNameSafe(ExistingMetric->GetUObject()) : TEXT("Not attached to UObject")); + UserSuppliedMetrics.Remove(Metric); + } +} + +void USpatialMetrics::SpatialExecServerCmd_Internal(const FString& ServerName, const ESpatialServerCommands& ServerCommand, + const FString& Args) +{ + const FString Command = StaticEnum()->GetNameStringByIndex(static_cast(ServerCommand)); if (!bIsServer && ControllerRefProvider.IsBound()) { const FUnrealObjectRef PCObjectRef = ControllerRefProvider.Execute(); @@ -404,7 +487,8 @@ void USpatialMetrics::SpatialExecServerCmd(const FString& ServerName, const FStr Schema_Object* RequestObject = Schema_GetCommandRequestObject(Request.schema_type); SpatialGDK::AddStringToSchema(RequestObject, SpatialConstants::EXEC_SERVER_COMMAND_PAYLOAD_SERVER_NAME_ID, ServerName); - SpatialGDK::AddStringToSchema(RequestObject, SpatialConstants::EXEC_SERVER_COMMAND_PAYLOAD_COMMAND_ID, Command); + Schema_AddInt32(RequestObject, SpatialConstants::EXEC_SERVER_COMMAND_PAYLOAD_COMMAND_ID, + static_cast(ServerCommand)); SpatialGDK::AddStringToSchema(RequestObject, SpatialConstants::EXEC_SERVER_COMMAND_PAYLOAD_ARGS_ID, Args); Connection->SendCommandRequest(ControllerEntityId, &Request, SpatialGDK::RETRY_MAX_TIMES, {}); @@ -449,15 +533,43 @@ void USpatialMetrics::SpatialExecServerCmd(const FString& ServerName, const FStr switch (ServerCommand) { - case SpatialMetricsPrivate::EServerCommands::StartInsights: -#if ENGINE_MINOR_VERSION >= 26 - FTraceAuxiliary::UpdateTraceCapture(*Args); -#else - UE_LOG(LogSpatialMetrics, Warning, - TEXT("SpatialExecServerCmd: Failed to execute server StartInsights command. Command only available prior to 4.26."), - *Command, *Args); -#endif + case ESpatialServerCommands::StartInsights: + { + if (StartInsightsCapture(Args)) + { + FString TraceTimeString; + if (FParse::Value(*Args, TEXT("-tracetime="), TraceTimeString)) + { + const int32 TraceTime = FCString::Atoi(*TraceTimeString); + + if (TraceTime <= 0) + { + UE_LOG(LogSpatialMetrics, Warning, + TEXT("SpatialExecServerCmd: Invalid `tracetime` param %d. Trace will not be stopped."), TraceTime); + } + else if (UWorld* World = GetWorld()) + { + FTimerHandle Handle; + World->GetTimerManager().SetTimer( + Handle, + [WeakThis = TWeakObjectPtr(this)]() { + if (WeakThis.IsValid()) + { + WeakThis->StopInsightsCapture(); + } + }, + TraceTime, false); + } + } + } break; + } + + case ESpatialServerCommands::StopInsights: + { + StopInsightsCapture(); + break; + } default: UE_LOG(LogSpatialMetrics, Error, @@ -480,7 +592,8 @@ void USpatialMetrics::SpatialExecServerCmd(const FString& ServerName, const FStr Schema_Object* RequestObject = Schema_GetCommandRequestObject(Request.schema_type); SpatialGDK::AddStringToSchema(RequestObject, SpatialConstants::EXEC_SERVER_COMMAND_PAYLOAD_SERVER_NAME_ID, ServerName); - SpatialGDK::AddStringToSchema(RequestObject, SpatialConstants::EXEC_SERVER_COMMAND_PAYLOAD_COMMAND_ID, Command); + Schema_AddInt32(RequestObject, SpatialConstants::EXEC_SERVER_COMMAND_PAYLOAD_COMMAND_ID, + static_cast(ServerCommand)); SpatialGDK::AddStringToSchema(RequestObject, SpatialConstants::EXEC_SERVER_COMMAND_PAYLOAD_ARGS_ID, Args); Connection->SendCommandRequest(ServerWorkerEntityId, &Request, SpatialGDK::RETRY_MAX_TIMES, {}); @@ -494,99 +607,34 @@ void USpatialMetrics::SpatialExecServerCmd(const FString& ServerName, const FStr } } -void USpatialMetrics::OnExecServerCmdCommand(Schema_Object* CommandPayload) -{ - const FString ServerName = - SpatialGDK::GetStringFromSchema(CommandPayload, SpatialConstants::EXEC_SERVER_COMMAND_PAYLOAD_SERVER_NAME_ID); - const FString Command = SpatialGDK::GetStringFromSchema(CommandPayload, SpatialConstants::EXEC_SERVER_COMMAND_PAYLOAD_COMMAND_ID); - const FString Args = SpatialGDK::GetStringFromSchema(CommandPayload, SpatialConstants::EXEC_SERVER_COMMAND_PAYLOAD_ARGS_ID); - - SpatialExecServerCmd(ServerName, Command, Args); -} - -void USpatialMetrics::TrackSentRPC(UFunction* Function, ERPCType RPCType, int PayloadSize) -{ - if (!bRPCTrackingEnabled) - { - return; - } - - FString FunctionName = FString::Printf(TEXT("%s::%s"), *Function->GetOuter()->GetName(), *Function->GetName()); - - if (RecentRPCs.Find(FunctionName) == nullptr) - { - RPCStat Stat; - Stat.Name = FunctionName; - Stat.Type = RPCType; - Stat.Calls = 0; - Stat.TotalPayload = 0; - - RecentRPCs.Add(FunctionName, Stat); - } - - RPCStat& Stat = RecentRPCs[FunctionName]; - Stat.Calls++; - Stat.TotalPayload += PayloadSize; -} - -void USpatialMetrics::HandleWorkerMetrics(const Worker_Op& Op) +bool USpatialMetrics::StartInsightsCapture(const FString& Args) { - int32 NumGaugeMetrics = Op.op.metrics.metrics.gauge_metric_count; - int32 NumHistogramMetrics = Op.op.metrics.metrics.histogram_metric_count; - if (NumGaugeMetrics > 0 || NumHistogramMetrics > 0) // We store these here so we can forward them with our metrics submission - { - FString StringTmp; - StringTmp.Reserve(128); - - for (int32 i = 0; i < NumGaugeMetrics; i++) - { - const Worker_GaugeMetric& WorkerMetric = Op.op.metrics.metrics.gauge_metrics[i]; - StringTmp = WorkerMetric.key; - WorkerSDKGaugeMetrics.FindOrAdd(StringTmp) = WorkerMetric.value; - } - - for (int32 i = 0; i < NumHistogramMetrics; i++) - { - const Worker_HistogramMetric& WorkerMetric = Op.op.metrics.metrics.histogram_metrics[i]; - StringTmp = WorkerMetric.key; - WorkerHistogramValues& HistogramMetrics = WorkerSDKHistogramMetrics.FindOrAdd(StringTmp); - HistogramMetrics.Sum = WorkerMetric.sum; - int32 NumBuckets = WorkerMetric.bucket_count; - HistogramMetrics.Buckets.SetNum(NumBuckets); - for (int32 j = 0; j < NumBuckets; j++) - { - HistogramMetrics.Buckets[j] = - TTuple{ WorkerMetric.buckets[j].upper_bound, WorkerMetric.buckets[j].samples }; - } - } +#if ENGINE_MINOR_VERSION < 26 + UE_LOG(LogSpatialMetrics, Warning, + TEXT("SpatialExecServerCmd: Failed to execute server StartInsights command. Command only available post 4.26.")); +#elif !UE_TRACE_ENABLED + UE_LOG(LogSpatialMetrics, Warning, + TEXT("SpatialExecServerCmd: Failed to execute server StartInsights command. UE_TRACE_ENABLE not defined.")); +#else + GCycleStatsShouldEmitNamedEvents++; - if (WorkerMetricsUpdated.IsBound()) - { - WorkerMetricsUpdated.Broadcast(WorkerSDKGaugeMetrics, WorkerSDKHistogramMetrics); - } - } + return FTraceAuxiliary::StartTraceCapture(*Args); +#endif + return false; } -void USpatialMetrics::SetCustomMetric(const FString& Metric, const UserSuppliedMetric& Delegate) +bool USpatialMetrics::StopInsightsCapture() { - UE_LOG(LogSpatialMetrics, Log, TEXT("USpatialMetrics: Adding custom metric %s (%s)"), *Metric, - Delegate.GetUObject() ? *GetNameSafe(Delegate.GetUObject()) : TEXT("Not attached to UObject")); - if (UserSuppliedMetric* ExistingMetric = UserSuppliedMetrics.Find(Metric)) - { - *ExistingMetric = Delegate; - } - else - { - UserSuppliedMetrics.Add(Metric, Delegate); - } -} +#if ENGINE_MINOR_VERSION < 26 + UE_LOG(LogSpatialMetrics, Warning, + TEXT("SpatialExecServerCmd: Failed to execute server StopInsights command. Command only available post 4.26.")); +#elif !UE_TRACE_ENABLED + UE_LOG(LogSpatialMetrics, Warning, + TEXT("SpatialExecServerCmd: Failed to execute server StopInsights command. UE_TRACE_ENABLE not defined.")); +#else + GCycleStatsShouldEmitNamedEvents = FMath::Max(0, GCycleStatsShouldEmitNamedEvents - 1); -void USpatialMetrics::RemoveCustomMetric(const FString& Metric) -{ - if (UserSuppliedMetric* ExistingMetric = UserSuppliedMetrics.Find(Metric)) - { - UE_LOG(LogSpatialMetrics, Log, TEXT("USpatialMetrics: Removing custom metric %s (%s)"), *Metric, - ExistingMetric->GetUObject() ? *GetNameSafe(ExistingMetric->GetUObject()) : TEXT("Not attached to UObject")); - UserSuppliedMetrics.Remove(Metric); - } + return FTraceAuxiliary::StopTraceCapture(); +#endif + return false; } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetricsDisplay.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetricsDisplay.cpp index 2f41c0b7b9..65e585903d 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetricsDisplay.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetricsDisplay.cpp @@ -162,7 +162,10 @@ void ASpatialMetricsDisplay::DrawDebug(class UCanvas* Canvas, APlayerController* void ASpatialMetricsDisplay::SpatialToggleStatDisplay() { #if !UE_BUILD_SHIPPING - check(GetNetMode() == NM_Client); + if (!ensureAlwaysMsgf(GetNetMode() == NM_Client, TEXT("Tried to toggle metrics stat display from a server"))) + { + return; + } if (DrawDebugDelegateHandle.IsValid()) { diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp index 969c3c22c8..da531a0b7f 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp @@ -10,6 +10,7 @@ #include "GeneralProjectSettings.h" #include "Interop/SpatialWorkerFlags.h" #include "Kismet/KismetSystemLibrary.h" +#include "LoadBalancing/GameplayDebuggerLBStrategy.h" #include "LoadBalancing/LayeredLBStrategy.h" #include "LoadBalancing/SpatialMultiWorkerSettings.h" #include "SpatialConstants.h" @@ -43,6 +44,22 @@ bool CanProcessActor(const AActor* Actor) return true; } + +const ULayeredLBStrategy* GetLayeredLBStrategy(const USpatialNetDriver* NetDriver) +{ + if (const ULayeredLBStrategy* LayeredLBStrategy = Cast(NetDriver->LoadBalanceStrategy)) + { + return LayeredLBStrategy; + } + if (const UGameplayDebuggerLBStrategy* DebuggerLBStrategy = Cast(NetDriver->LoadBalanceStrategy)) + { + if (const ULayeredLBStrategy* LayeredLBStrategy = Cast(DebuggerLBStrategy->GetWrappedStrategy())) + { + return LayeredLBStrategy; + } + } + return nullptr; +} } // anonymous namespace bool USpatialStatics::IsSpatialNetworkingEnabled() @@ -73,10 +90,7 @@ bool USpatialStatics::IsHandoverEnabled(const UObject* WorldContextObject) return true; } - if (const ULayeredLBStrategy* LBStrategy = Cast(SpatialNetDriver->LoadBalanceStrategy)) - { - return LBStrategy->RequiresHandoverData(); - } + return SpatialNetDriver->LoadBalanceStrategy->RequiresHandoverData(); } return true; } @@ -221,7 +235,7 @@ bool USpatialStatics::IsActorGroupOwnerForClass(const UObject* WorldContextObjec return true; } - if (const ULayeredLBStrategy* LBStrategy = Cast(SpatialNetDriver->LoadBalanceStrategy)) + if (const ULayeredLBStrategy* LBStrategy = GetLayeredLBStrategy(SpatialNetDriver)) { return LBStrategy->CouldHaveAuthority(ActorClass); } @@ -345,8 +359,12 @@ FName USpatialStatics::GetLayerName(const UObject* WorldContextObject) return NAME_None; } - const ULayeredLBStrategy* LBStrategy = Cast(SpatialNetDriver->LoadBalanceStrategy); - check(LBStrategy != nullptr); + const ULayeredLBStrategy* LBStrategy = GetLayeredLBStrategy(SpatialNetDriver); + if (!ensureAlwaysMsgf(LBStrategy != nullptr, TEXT("Failed calling GetLayerName because load balancing strategy was nullptr"))) + { + return FName(); + } + return LBStrategy->GetLocalLayerName(); } @@ -391,14 +409,23 @@ void USpatialStatics::SpatialDebuggerSetOnConfigUIClosedCallback(const UObject* void USpatialStatics::SpatialSwitchHasAuthority(const AActor* Target, ESpatialHasAuthority& Authority) { + if (!ensureAlwaysMsgf(IsValid(Target) && Target->IsA(AActor::StaticClass()), + TEXT("Called SpatialSwitchHasAuthority for an invalid or non-Actor target: %s"), *GetNameSafe(Target))) + { + return; + } + + if (!ensureAlwaysMsgf(Target->GetNetDriver() != nullptr, + TEXT("Called SpatialSwitchHasAuthority for %s but couldn't access NetDriver through Actor."), + *GetNameSafe(Target))) + { + return; + } + // A static UFunction does not have the Target parameter, here it is recreated by adding our own Target parameter // that is defaulted to self and hidden so that the user does not need to set it - check(IsValid(Target)); - check(Target->IsA(AActor::StaticClass())); - check(Target->GetNetDriver() != nullptr); - - const bool bHasAuthority = Target->HasAuthority(); const bool bIsServer = Target->GetNetDriver()->IsServer(); + const bool bHasAuthority = Target->HasAuthority(); if (bHasAuthority && bIsServer) { diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/RemotePossessionComponent.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/RemotePossessionComponent.h index 0009603fd9..96844dcbf9 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/RemotePossessionComponent.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/RemotePossessionComponent.h @@ -34,12 +34,14 @@ class SPATIALGDK_API URemotePossessionComponent : public UActorComponent virtual void BeginPlay() override; + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + protected: UFUNCTION(BlueprintCallable, Category = "Utilities") void MarkToDestroy(); public: - UPROPERTY(Category = Sprite, handover, EditAnywhere, BlueprintReadWrite) + UPROPERTY(Category = Sprite, Replicated, EditAnywhere, BlueprintReadWrite) APawn* Target; private: diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h index 118777f8fd..d33c3f30ed 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h @@ -120,7 +120,7 @@ class FSpatialObjectRepState void UpdateRefToRepStateMap(FObjectToRepStateMap& ReplicatorMap); bool MoveMappedObjectToUnmapped(const FUnrealObjectRef& ObjRef); - bool HasUnresolved() const { return UnresolvedRefs.Num() == 0; } + bool HasUnresolved() const { return UnresolvedRefs.Num() != 0; } const FChannelObjectPair& GetChannelObjectPair() const { return ThisObj; } @@ -243,14 +243,7 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel bool ReplicateSubobject(UObject* Obj, const FReplicationFlags& RepFlags); - TMap GetHandoverSubobjects(); - FRepChangeState CreateInitialRepChangeState(TWeakObjectPtr Object); - FHandoverChangeState CreateInitialHandoverChangeState(const FClassInfo& ClassInfo); - - // For an object that is replicated by this channel (i.e. this channel's actor or its component), find out whether a given handle is an - // array. - bool IsDynamicArrayHandle(UObject* Object, uint16 Handle); FObjectReplicator* PreReceiveSpatialUpdate(UObject* TargetObject); void PostReceiveSpatialUpdate(UObject* TargetObject, const TArray& RepNotifies, @@ -290,9 +283,6 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel void SendPositionUpdate(AActor* InActor, Worker_EntityId InEntityId, const FVector& NewPosition); - void InitializeHandoverShadowData(TArray& ShadowData, UObject* Object); - FHandoverChangeState GetHandoverChangeList(TArray& ShadowData, UObject* Object); - void UpdateVisibleComponent(AActor* Actor); bool SatisfiesSpatialPositionUpdateRequirements(); @@ -343,15 +333,6 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel // ReplicationBytesWritten is reset back to 0 at the start of ReplicateActor. uint32 ReplicationBytesWritten = 0; - // Shadow data for Handover properties. - // For each object with handover properties, we store a blob of memory which contains - // the state of those properties at the last time we sent them, and is used to detect - // when those properties change. - TArray* ActorHandoverShadowData; - TMap, TSharedRef>, FDefaultSetAllocator, - TWeakObjectPtrMapKeyFuncs, TSharedRef>, false>> - HandoverShadowDataMap; - // Band-aid until we get Actor Sets. // Used on server-side workers only. // Record when this worker receives SpatialOS Position component authority over the Actor. diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h index e2ada81b93..9ef2257a76 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h @@ -3,9 +3,11 @@ #pragma once #include "Interop/CrossServerRPCHandler.h" +#include "Engine/EngineBaseTypes.h" #include "Interop/Connection/ConnectionConfig.h" #include "Interop/CrossServerRPCSender.h" #include "Interop/EntityQueryHandler.h" +#include "Interop/OwnershipCompletenessHandler.h" #include "Utils/SpatialBasicAwaiter.h" #include "Utils/SpatialDebugger.h" @@ -23,6 +25,9 @@ class ASpatialDebugger; class ASpatialMetricsDisplay; class FSpatialLoadBalancingHandler; +class FSpatialNetDriverRPC; +class FSpatialNetDriverClientRPC; +class FSpatialNetDriverServerRPC; class FSpatialOutputDevice; class SpatialDispatcher; class SpatialSnapshotManager; @@ -38,6 +43,7 @@ class USpatialGameInstance; class USpatialMetrics; class USpatialNetConnection; class USpatialNetDriverDebugContext; +class USpatialNetDriverGameplayDebuggerContext; class USpatialPackageMapClient; class USpatialPlayerSpawner; class USpatialReceiver; @@ -121,7 +127,12 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver virtual void NotifyStreamingLevelUnload(class ULevel* Level) override; virtual void PushCrossServerRPCSender(AActor* Sender) override; - virtual void PopCrossServerRPCSender(AActor* Sender) override; + virtual void PopCrossServerRPCSender() override; + virtual void PushDependentActor(AActor* Dependent) override; + virtual void PopDependentActor() override; + virtual void PushNetWriteFenceResolution(); + virtual void PopNetWriteFenceResolution(); + virtual bool RPCCallNeedWriteFence(AActor* Target, UFunction* Function) override; // End UNetDriver interface. void OnConnectionToSpatialOSSucceeded(); @@ -209,15 +220,17 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver UPROPERTY() USpatialNetDriverDebugContext* DebugCtx; UPROPERTY() + USpatialNetDriverGameplayDebuggerContext* GameplayDebuggerCtx; + UPROPERTY() UAsyncPackageLoadFilter* AsyncPackageLoadFilter; - // Stored as fields here to be reused for creating the debug context subview if the world settings dictates it. - FFilterPredicate ActorFilter; - TArray ActorRefreshCallbacks; - TUniquePtr SpatialDebuggerSystem; + TOptional OwnershipCompletenessHandler; TUniquePtr ActorSystem; TUniquePtr RPCService; + TUniquePtr RPCs; + FSpatialNetDriverClientRPC* ClientRPCs = nullptr; + FSpatialNetDriverServerRPC* ServerRPCs = nullptr; TUniquePtr RoutingSystem; TUniquePtr StrategySystem; @@ -293,6 +306,12 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver bool bIsReadyToStart; bool bMapLoaded; + struct FPendingNetworkFailure + { + ENetworkFailure::Type FailureType; + FString Message; + }; + TOptional PendingNetworkFailure; FString SnapshotToLoad; // Client variable which stores the SessionId given to us by the server in the URL options. diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriverGameplayDebuggerContext.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriverGameplayDebuggerContext.h new file mode 100644 index 0000000000..f57fd2abeb --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriverGameplayDebuggerContext.h @@ -0,0 +1,95 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" + +#if WITH_GAMEPLAY_DEBUGGER +#include "GameplayDebuggerCategoryReplicator.h" +#include "Schema/GameplayDebuggerComponent.h" +#include "Schema/Interest.h" +#include "SpatialCommonTypes.h" +#include "UObject/WeakInterfacePtr.h" +#endif // WITH_GAMEPLAY_DEBUGGER + +#include "SpatialNetDriverGameplayDebuggerContext.generated.h" + +class UGameplayDebuggerLBStrategy; +class USpatialNetDriver; + +namespace SpatialGDK +{ +class FSubView; +struct ComponentChange; +} // namespace SpatialGDK + +/* + * A helper object that allocates a custom LB strategy to handle gameplay debugger + * replicated actors, through a custom subview that tracks actor add/remove/auth changes. + */ + +UCLASS() +class SPATIALGDK_API USpatialNetDriverGameplayDebuggerContext : public UObject +{ + GENERATED_BODY() + +public: + USpatialNetDriverGameplayDebuggerContext() = default; + +#if WITH_GAMEPLAY_DEBUGGER + virtual ~USpatialNetDriverGameplayDebuggerContext(); + + static void Enable(const SpatialGDK::FSubView& InSubView, USpatialNetDriver& InNetDriver); + static void Disable(USpatialNetDriver& NetDriver); + + void Init(const SpatialGDK::FSubView& InSubView, USpatialNetDriver& InNetDriver); + void Reset(); + + /** Given an actor, return the delegated worker ID if the actor is a gameplay + * debugger replicator actor tracked by this context */ + TOptional GetActorDelegatedWorkerId(const AActor& InActor); + + void AdvanceView(); + void TickServer(); +#endif // WITH_GAMEPLAY_DEBUGGER + + UPROPERTY() + UGameplayDebuggerLBStrategy* LBStrategy = nullptr; + +protected: +#if WITH_GAMEPLAY_DEBUGGER + struct FEntityData + { + SpatialGDK::GameplayDebuggerComponent Component; + TWeakObjectPtr ReplicatorWeakObjectPtr; + FString CurrentWorkerId; + FDelegateHandle ServerTrackingRequestHandle; + FDelegateHandle PlayerControllerAuthorityChangeHandle; + FDelegateHandle DebugActorChangedHandle; + }; + + void TrackEntity(Worker_EntityId InEntityId); + void UntrackEntity(Worker_EntityId InEntityId); + void AddAuthority(Worker_EntityId InEntityId, FEntityData* InOptionalEntityData); + void RemoveAuthority(Worker_EntityId InEntityId, FEntityData* InOptionalEntityData); + void RegisterServerRequestCallback(AGameplayDebuggerCategoryReplicator& InReplicator, FEntityData& InEntityData); + void UnregisterServerRequestCallback(AGameplayDebuggerCategoryReplicator& InReplicator, FEntityData& InEntityData); + void OnServerTrackingRequest(AGameplayDebuggerCategoryReplicator* InCategoryReplicator, + EGameplayDebuggerServerTrackingMode InServerTrackingMode, FString InOptionalServerWorkerId); + VirtualWorkerId GetActorVirtualWorkerId(const AActor& InActor) const; + void RegisterPlayerControllerAuthorityLostCallback(AGameplayDebuggerCategoryReplicator& InReplicator, FEntityData& InEntityData); + void UnregisterPlayerControllerAuthorityLostCallback(AGameplayDebuggerCategoryReplicator& InReplicator, FEntityData& InEntityData); + void OnPlayerControllerAuthorityLost(const APlayerController& InPlayerController); + void RegisterDebugActorChangedCallback(AGameplayDebuggerCategoryReplicator& InReplicator, FEntityData& InEntityData); + void UnregisterDebugActorChangedCallback(AGameplayDebuggerCategoryReplicator& InReplicator, FEntityData& InEntityData); + void OnDebugActorChanged(AGameplayDebuggerCategoryReplicator* InCategoryReplicator, AActor* InDebugActor); + + USpatialNetDriver* NetDriver = nullptr; + const SpatialGDK::FSubView* SubView = nullptr; + TMap TrackedEntities; + TSet ComponentsAdded; + TSet ComponentsUpdated; + TArray ActorsAdded; + TMap PhysicalToVirtualWorkerIdMap; +#endif // WITH_GAMEPLAY_DEBUGGER +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriverRPC.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriverRPC.h index 9436d10a5c..89556ed6ca 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriverRPC.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriverRPC.h @@ -4,12 +4,14 @@ #include "CoreMinimal.h" #include "Interop/RPCs/RPCService.h" +#include "SpatialConstants.h" #include "SpatialView/EntityComponentId.h" #include DECLARE_LOG_CATEGORY_EXTERN(LogSpatialNetDriverRPC, Log, All); +struct FRPCInfo; namespace SpatialGDK { class SpatialEventTracer; @@ -102,7 +104,7 @@ struct TimestampAndETWrapper WrappedData Wrapper(MoveTemp(Data), RPCName); if (EventTracer != nullptr) { - Wrapper.MetaData.ComputeSpanId(RPCName, *EventTracer, SpatialGDK::EntityComponentId(EntityId, ComponentId), RPCId); + Wrapper.MetaData.ComputeSpanId(*EventTracer, SpatialGDK::EntityComponentId(EntityId, ComponentId), RPCId); } return Wrapper; @@ -120,7 +122,7 @@ struct TimestampAndETWrapper * The base class only contains a receiver for NetMulticast RPCs. * Derived class will contain additional RPC components */ -class SPATIALGDK_API FSpatialNetDriverRPC : public UObject +class SPATIALGDK_API FSpatialNetDriverRPC { public: // Definition for a "standard queue", that is, all queued RPC for sending could have an accompanying SpanId. @@ -129,6 +131,7 @@ class SPATIALGDK_API FSpatialNetDriverRPC : public UObject FSpatialNetDriverRPC(USpatialNetDriver& InNetDriver, const SpatialGDK::FSubView& InActorAuthSubView, const SpatialGDK::FSubView& InActorNonAuthSubView); + virtual ~FSpatialNetDriverRPC(); void AdvanceView(); virtual void ProcessReceivedRPCs(); @@ -137,6 +140,9 @@ class SPATIALGDK_API FSpatialNetDriverRPC : public UObject void FlushRPCQueueForEntity(Worker_EntityId, StandardQueue& Queue); TArray GetRPCComponentsOnEntityCreation(const Worker_EntityId EntityId); + FSpatialGDKSpanId CreatePushRPCEvent(UObject* TargetObject, UFunction* Function); + TArray CreateRPCPayloadData(UFunction* Function, void* Parameters); + protected: void MakeRingBufferWithACKSender(ERPCType RPCType, Worker_ComponentSetId AuthoritySet, TUniquePtr& SenderPtr, @@ -153,21 +159,20 @@ class SPATIALGDK_API FSpatialNetDriverRPC : public UObject FWorkerComponentUpdate Update; TArray Spans; }; - StandardQueue::SentRPCCallback MakeRPCSentCallback(); - SpatialGDK::RPCCallbacks::DataWritten MakeDataWriteCallback(TArray& OutArray) const; + SpatialGDK::RPCCallbacks::UpdateWritten MakeDataWriteCallback(TArray& OutArray) const; SpatialGDK::RPCCallbacks::UpdateWritten MakeUpdateWriteCallback(); static void OnRPCSent(SpatialGDK::SpatialEventTracer& EventTracer, TArray& OutUpdates, FName Name, Worker_EntityId EntityId, Worker_ComponentId ComponentId, uint64 RPCId, const FSpatialGDKSpanId& SpanId); static void OnDataWritten(TArray& OutArray, Worker_EntityId EntityId, Worker_ComponentId ComponentId, - Schema_ComponentData* InData); + Schema_ComponentUpdate* InData); static void OnUpdateWritten(TArray& OutUpdates, Worker_EntityId EntityId, Worker_ComponentId ComponentId, Schema_ComponentUpdate* InUpdate); bool CanExtractRPC(Worker_EntityId EntityId) const; bool CanExtractRPCOnServer(Worker_EntityId EntityId) const; - bool ApplyRPC(Worker_EntityId EntityId, SpatialGDK::ReceivedRPC ReceivedCallback, const FRPCMetaData& MetaData) const; + bool ApplyRPC(Worker_EntityId EntityId, const FRPCPayload& RPCData, const FRPCMetaData& MetaData) const; USpatialNetDriver& NetDriver; SpatialGDK::SpatialEventTracer* EventTracer = nullptr; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h index b37da819f5..8a1ae291e3 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h @@ -17,14 +17,13 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialPackageMap, Log, All); class USpatialNetDriver; class UEntityPool; -class FTimerManager; UCLASS() class SPATIALGDK_API USpatialPackageMapClient : public UPackageMapClient { GENERATED_BODY() public: - void Init(USpatialNetDriver* NetDriver, FTimerManager* TimerManager); + void Init(USpatialNetDriver& NetDriver); void Advance(); @@ -34,7 +33,7 @@ class SPATIALGDK_API USpatialPackageMapClient : public UPackageMapClient bool IsEntityIdPendingCreation(Worker_EntityId EntityId) const; void RemovePendingCreationEntityId(Worker_EntityId EntityId); - bool ResolveEntityActor(AActor* Actor, Worker_EntityId EntityId); + bool ResolveEntityActorAndSubobjects(Worker_EntityId EntityId, AActor* Actor); void ResolveSubobject(UObject* Object, const FUnrealObjectRef& ObjectRef); void RemoveEntityActor(Worker_EntityId EntityId); @@ -58,10 +57,6 @@ class SPATIALGDK_API USpatialPackageMapClient : public UPackageMapClient AActor* GetUniqueActorInstanceByClassRef(const FUnrealObjectRef& ClassRef); AActor* GetUniqueActorInstanceByClass(UClass* Class) const; - FNetworkGUID* GetRemovedDynamicSubobjectNetGUID(const FUnrealObjectRef& ObjectRef); - void AddRemovedDynamicSubobjectObjectRef(const FUnrealObjectRef& ObjectRef, const FNetworkGUID& NetGUID); - void ClearRemovedDynamicSubobjectObjectRefs(const Worker_EntityId& InEntityId); - // Expose FNetGUIDCache::CanClientLoadObject so we can include this info with UnrealObjectRef. bool CanClientLoadObject(UObject* Object); @@ -86,7 +81,6 @@ class SPATIALGDK_API USpatialPackageMapClient : public UPackageMapClient // Entities that have been assigned on this server and not created yet TSet PendingCreationEntityIds; - TMap RemovedDynamicSubobjectObjectRefs; }; class SPATIALGDK_API FSpatialNetGUIDCache : public FNetGUIDCache diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialWorldSettings.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialWorldSettings.h index fd280c2eb1..9eb377a014 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialWorldSettings.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialWorldSettings.h @@ -25,7 +25,7 @@ class SPATIALGDK_API ASpatialWorldSettings : public AWorldSettings TSubclassOf GetMultiWorkerSettingsClass(bool bForceNonEditorSettings = false); #if WITH_EDITORONLY_DATA - UPROPERTY(EditAnywhere, Category = "Testing", Meta = (FilePathFilter = "ini")) + UPROPERTY(EditAnywhere, Category = "Testing", Meta = (FilePathFilter = "ini", RelativeToGameDir)) FFilePath SettingsOverride; UPROPERTY(EditAnywhere, Category = "Testing") diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/ActorSystem.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/ActorSystem.h index b4525c84a2..d3fd212d0d 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/ActorSystem.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/ActorSystem.h @@ -4,9 +4,9 @@ #include "ClaimPartitionHandler.h" #include "Schema/SpawnData.h" #include "Schema/UnrealMetadata.h" -#include "SpatialConstants.h" #include "Utils/RepDataUtils.h" +#include "Interop/ClientNetLoadActorHelper.h" #include "Interop/CreateEntityHandler.h" DECLARE_LOG_CATEGORY_EXTERN(LogActorSystem, Log, All); @@ -22,7 +22,6 @@ struct FClassInfo; class USpatialNetDriver; class SpatialActorChannel; -class USpatialNetDriver; using FChannelsToUpdatePosition = TSet, TWeakObjectPtrKeyFuncs, false>>; @@ -41,7 +40,8 @@ struct ActorData class ActorSystem { public: - ActorSystem(const FSubView& InActorSubView, const FSubView& InTombstoneSubView, USpatialNetDriver* InNetDriver, + ActorSystem(const FSubView& InActorSubView, const FSubView& InAuthoritySubView, const FSubView& InOwnershipSubView, + const FSubView& InSimulatedSubView, const FSubView& InTombstoneSubView, USpatialNetDriver* InNetDriver, SpatialEventTracer* InEventTracer); void Advance(); @@ -60,7 +60,7 @@ class ActorSystem // Updates void SendComponentUpdates(UObject* Object, const FClassInfo& Info, USpatialActorChannel* Channel, const FRepChangeState* RepChanges, - const FHandoverChangeState* HandoverChanges, uint32& OutBytesWritten); + uint32& OutBytesWritten); void SendActorTornOffUpdate(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const; void ProcessPositionUpdates(); void RegisterChannelForPositionUpdate(USpatialActorChannel* Channel); @@ -79,6 +79,8 @@ class ActorSystem static Worker_ComponentData CreateLevelComponentData(const AActor& Actor, const UWorld& NetDriverWorld, const USpatialClassInfoManager& ClassInfoManager); + void DestroySubObject(const FUnrealObjectRef& ObjectRef, UObject& Object) const; + private: // Helper struct to manage FSpatialObjectRepState update cycle. // TODO: move into own class. @@ -98,6 +100,13 @@ class ActorSystem FObjectToRepStateMap ObjectRefToRepStateMap; void PopulateDataStore(Worker_EntityId EntityId); + + struct FEntitySubViewUpdate; + + void ProcessUpdates(const FEntitySubViewUpdate& SubViewUpdate); + void ProcessAdds(const FEntitySubViewUpdate& SubViewUpdate); + void ProcessRemoves(const FEntitySubViewUpdate& SubViewUpdate); + void ApplyComponentAdd(Worker_EntityId EntityId, Worker_ComponentId ComponentId, Schema_ComponentData* Data); void AuthorityLost(Worker_EntityId EntityId, Worker_ComponentSetId ComponentSetId); @@ -110,6 +119,8 @@ class ActorSystem void EntityAdded(Worker_EntityId EntityId); void EntityRemoved(Worker_EntityId EntityId); + void RefreshEntity(const Worker_EntityId EntityId); + void ApplyFullState(const Worker_EntityId EntityId, USpatialActorChannel& EntityActorChannel, AActor& EntityActor); // Authority bool HasEntityBeenRequestedForDelete(Worker_EntityId EntityId) const; @@ -124,7 +135,6 @@ class ActorSystem void ApplyComponentData(USpatialActorChannel& Channel, UObject& TargetObject, const Worker_ComponentId ComponentId, Schema_ComponentData* Data); - bool IsDynamicSubObject(AActor* Actor, uint32 SubObjectOffset); void ResolveIncomingOperations(UObject* Object, const FUnrealObjectRef& ObjectRef); void ResolveObjectReferences(FRepLayout& RepLayout, UObject* ReplicatedObject, FSpatialObjectRepState& RepState, FObjectReferencesMap& ObjectReferencesMap, uint8* RESTRICT StoredData, uint8* RESTRICT Data, @@ -133,7 +143,7 @@ class ActorSystem // Component update USpatialActorChannel* GetOrRecreateChannelForDormantActor(AActor* Actor, Worker_EntityId EntityID) const; void ApplyComponentUpdate(Worker_ComponentId ComponentId, Schema_ComponentUpdate* ComponentUpdate, UObject& TargetObject, - USpatialActorChannel& Channel, bool bIsHandover); + USpatialActorChannel& Channel); // Entity add void ReceiveActor(Worker_EntityId EntityId); @@ -161,13 +171,20 @@ class ActorSystem void SendRemoveComponents(Worker_EntityId EntityId, TArray ComponentIds) const; const FSubView* ActorSubView; + const FSubView* AuthoritySubView; + const FSubView* OwnershipSubView; + const FSubView* SimulatedSubView; const FSubView* TombstoneSubView; + USpatialNetDriver* NetDriver; SpatialEventTracer* EventTracer; + FClientNetLoadActorHelper ClientNetLoadActorHelper; CreateEntityHandler CreateEntityHandler; ClaimPartitionHandler ClaimPartitionHandler; + TSet PresentEntities; + TMap> CreateEntityRequestIdToActorChannel; TMap> PendingDynamicSubobjectComponents; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/ClientNetLoadActorHelper.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/ClientNetLoadActorHelper.h new file mode 100644 index 0000000000..a9be1022e6 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/ClientNetLoadActorHelper.h @@ -0,0 +1,47 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once +#include "Schema/UnrealMetadata.h" +#include "SpatialConstants.h" +#include "Utils/RepDataUtils.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogClientNetLoadActorHelper, Log, All); + +class USpatialNetDriver; + +namespace SpatialGDK +{ +class FClientNetLoadActorHelper +{ +public: + FClientNetLoadActorHelper(USpatialNetDriver& InNetDriver); + void EntityRemoved(const Worker_EntityId EntityId, const AActor& Actor); + UObject* GetReusableDynamicSubObject(const FUnrealObjectRef ObjectRef); + + // The runtime can remove components from a ClientNetLoad Actor while the actor is out of the client's interest + // The client doesn't receive these updates when they happen, so the difference must be reconciled + void RemoveRuntimeRemovedComponents(const Worker_EntityId EntityId, const TArray& NewComponents, AActor& EntityActor); + +private: + USpatialNetDriver* NetDriver; + + // Stores subobjects from client net load actors that have gone out of the client's interest + TMap> SpatialEntityRemovedSubobjectMetadata; + + void SaveDynamicSubobjectsMetadata(Worker_EntityId EntityId, const AActor& Actor); + + // BNetLoadOnClient component edge case handling + FNetworkGUID* GetSavedDynamicSubObjectNetGUID(const FUnrealObjectRef& ObjectRef); + void SaveDynamicSubobjectMetadata(const FUnrealObjectRef& ObjectRef, const FNetworkGUID& NetGUID); + void ClearDynamicSubobjectMetadata(const Worker_EntityId InEntityId); + + void RemoveDynamicComponentsRemovedByRuntime(const Worker_EntityId EntityId, const TArray& NewComponents); + void RemoveStaticComponentsRemovedByRuntime(const Worker_EntityId EntityId, const TArray& NewComponents, + AActor& EntityActor); + void SubobjectRemovedByRuntime(const FUnrealObjectRef& EntityObjectRef, UObject& Subobject); + + bool SubobjectWithOffsetStillExists(const TArray& Components, const ObjectOffset OffsetToCheckIfContained) const; + bool SubobjectIsReplicated(const UObject& Object, Worker_EntityId EntityId) const; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/OutgoingMessages.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/OutgoingMessages.h index 4702258af1..91be02aff3 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/OutgoingMessages.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/OutgoingMessages.h @@ -10,7 +10,6 @@ #include "Templates/UniquePtr.h" #include "Templates/UnrealTemplate.h" #include "UObject/NameTypes.h" -#include "Utils/SpatialLatencyTracer.h" #include diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialEventTracer.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialEventTracer.h index 04d513afc4..ed37d7d9d9 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialEventTracer.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialEventTracer.h @@ -16,18 +16,6 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialEventTracer, Log, All); namespace SpatialGDK { -struct TraceQueryDeleter -{ - void operator()(Trace_Query* Query) const - { - if (Query != nullptr) - { - Trace_Query_Destroy(Query); - } - } -}; -typedef TUniquePtr TraceQueryPtr; - // SpatialEventTracer wraps Trace_EventTracer related functionality class SPATIALGDK_API SpatialEventTracer { diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h index dd5bddcbbc..afe85bc901 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h @@ -113,12 +113,7 @@ class SPATIALGDK_API USpatialWorkerConnection : public UObject, public SpatialOS void SetStartupComplete(); - DECLARE_MULTICAST_DELEGATE_OneParam(FOnEnqueueMessage, const SpatialGDK::FOutgoingMessage*); - FOnEnqueueMessage OnEnqueueMessage; - - DECLARE_MULTICAST_DELEGATE_OneParam(FOnDequeueMessage, const SpatialGDK::FOutgoingMessage*); - FOnDequeueMessage OnDequeueMessage; - + SpatialGDK::ISpatialOSWorker* GetSpatialWorkerInterface() const; SpatialGDK::SpatialEventTracer* GetEventTracer() const { return EventTracer; } private: diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/CreateEntityHandler.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/CreateEntityHandler.h index a1ea8b0763..72befc57ab 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/CreateEntityHandler.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/CreateEntityHandler.h @@ -21,7 +21,10 @@ class CreateEntityHandler public: void AddRequest(Worker_RequestId RequestId, CreateEntityDelegate&& Handler) { - check(Handler.IsBound()); + if (!ensureAlwaysMsgf(Handler.IsBound(), TEXT("Failed to add create entity requested handler. Handler delegate was unbound"))) + { + return; + } Handlers.Emplace(RequestId, MoveTemp(Handler)); } diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/OwnershipCompletenessHandler.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/OwnershipCompletenessHandler.h new file mode 100644 index 0000000000..87189fa517 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/OwnershipCompletenessHandler.h @@ -0,0 +1,51 @@ +#pragma once + +#include "Misc/Optional.h" + +#include "SpatialCommonTypes.h" +#include "SpatialView/SubView.h" + +class USpatialNetDriver; + +namespace SpatialGDK +{ +class ViewCoordinator; + +class FOwnershipCompletenessHandler +{ +public: + enum class EOwnershipCompletenessStrategy + { + AlwaysHasOwnerComponents, + RequiresPlayerOwnership, + }; + + static FOwnershipCompletenessHandler CreateServerOwnershipHandler() + { + return FOwnershipCompletenessHandler(EOwnershipCompletenessStrategy::AlwaysHasOwnerComponents); + } + static FOwnershipCompletenessHandler CreateClientOwnershipHandler() + { + return FOwnershipCompletenessHandler(EOwnershipCompletenessStrategy::RequiresPlayerOwnership); + } + + bool IsOwnershipComplete(Worker_EntityId EntityId, const EntityViewElement& Entity) const; + void AddPlayerEntity(Worker_EntityId EntityId); + void TryRemovePlayerEntity(Worker_EntityId EntityId); + void AddSubView(FSubView& InSubView); + + static TArray GetCallbacks(ViewCoordinator& Coordinator); + +private: + bool ShouldHaveOwnerOnlyComponents(Worker_EntityId EntityId, const EntityViewElement& Entity) const; + + explicit FOwnershipCompletenessHandler(const EOwnershipCompletenessStrategy InStrategy) + : Strategy(InStrategy) + { + } + + EOwnershipCompletenessStrategy Strategy; + TSet PlayerOwnedEntities; + TArray SubViewsToRefresh; +}; +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/CrossServerRPCService.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/CrossServerRPCService.h index 3dc8f308e0..e7d5413a68 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/CrossServerRPCService.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/CrossServerRPCService.h @@ -38,7 +38,7 @@ class SPATIALGDK_API CrossServerRPCService { public: CrossServerRPCService(const ActorCanExtractRPCDelegate InCanExtractRPCDelegate, const ExtractRPCDelegate InExtractRPCCallback, - const FSubView& InSubView, FRPCStore& InRPCStore); + const FSubView& InActorSubView, const FSubView& InWorkerEntitySubView, FRPCStore& InRPCStore); void AdvanceView(); void ProcessChanges(); @@ -50,6 +50,9 @@ class SPATIALGDK_API CrossServerRPCService void FlushPendingClearedFields(TPair& UpdateToSend); private: + void AdvanceViewForEntityDelta(const EntityDelta& Delta); + void ProcessChangesForEntityDelta(const EntityDelta& Delta); + // Process relevant view delta changes. void EntityAdded(const Worker_EntityId EntityId); void ComponentUpdate(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId, Schema_ComponentUpdate* Update); @@ -74,9 +77,10 @@ class SPATIALGDK_API CrossServerRPCService ActorCanExtractRPCDelegate CanExtractRPCDelegate; ExtractRPCDelegate ExtractRPCCallback; - const FSubView* SubView; + const FSubView& ActorSubView; + const FSubView& WorkerEntitySubView; - FRPCStore* RPCStore; + FRPCStore& RPCStore; // Deserialized state store for client/server RPC components. TMap CrossServerDataStore; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/MulticastRPCService.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/MulticastRPCService.h index 969fb9a318..890fbc1202 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/MulticastRPCService.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/MulticastRPCService.h @@ -1,4 +1,4 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved #pragma once @@ -28,9 +28,10 @@ class SPATIALGDK_API MulticastRPCService private: // Process relevant view delta changes. void EntityAdded(Worker_EntityId EntityId); - void ComponentUpdate(Worker_EntityId EntityId, Worker_ComponentId ComponentId, Schema_ComponentUpdate* Update); - void AuthorityGained(Worker_EntityId EntityId, Worker_ComponentId ComponentId); - void AuthorityLost(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + void EntityRefresh(Worker_EntityId EntityId); + void ComponentUpdate(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + void AuthorityGained(Worker_EntityId EntityId, Worker_ComponentSetId ComponentSetId); + void AuthorityLost(Worker_EntityId EntityId, Worker_ComponentSetId ComponentSetId); // Maintain local state of multicast RPCs. void PopulateDataStore(Worker_EntityId EntityId); @@ -39,8 +40,8 @@ class SPATIALGDK_API MulticastRPCService // Multicast system responses to state changes. void OnCheckoutMulticastRPCComponentOnEntity(Worker_EntityId EntityId); void OnRemoveMulticastRPCComponentForEntity(Worker_EntityId EntityId); - void OnEndpointAuthorityGained(Worker_EntityId EntityId, Worker_ComponentId ComponentId); - void OnEndpointAuthorityLost(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + void OnEndpointAuthorityGained(Worker_EntityId EntityId, Worker_ComponentSetId ComponentSetId); + void OnEndpointAuthorityLost(Worker_EntityId EntityId, Worker_ComponentSetId ComponentSetId); // Calls ExtractRPCCallback for each RPC it extracts from a given component. If the callback returns false, // stops retrieving RPCs. diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPCQueues.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPCQueues.h index 7259f563e8..59bfa0ac54 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPCQueues.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPCQueues.h @@ -6,6 +6,9 @@ namespace SpatialGDK { +// Limit for unbounded queue, where we should consider disconnecting a client. +constexpr int32 UnboundedQueueOverflowLimit = 1024; + /** * Unbounded queue. * Will try to flush everything each frame, and keep the Items that could not be flushed for later @@ -13,34 +16,81 @@ namespace SpatialGDK template struct TRPCUnboundedQueue : public TRPCQueue { + using Super = TRPCQueue; + using typename Super::QueueData; + using typename Super::SentRPCCallback; + TRPCUnboundedQueue(FName InName, TRPCBufferSender& Sender) - : TRPCQueue(InName, Sender) + : Super(InName, Sender) { } - void FlushAll(RPCWritingContext& Ctx, const typename TWrappedRPCQueue::SentRPCCallback& SentCallback) override + virtual void FlushAll(RPCWritingContext& Ctx, const SentRPCCallback& SentCallback = SentRPCCallback()) override { for (auto Iterator = this->Queues.CreateIterator(); Iterator; ++Iterator) { - typename TRPCQueue::QueueData& Queue = Iterator->Value; + QueueData& Queue = Iterator->Value; if (Queue.bAdded) { this->FlushQueue(Iterator->Key, Queue, Ctx, SentCallback); + CheckOverflow(Iterator->Key, Queue); } } } - void Flush(Worker_EntityId EntityId, RPCWritingContext& Ctx, - const typename TWrappedRPCQueue::SentRPCCallback& SentCallback, bool bIgnoreAdded = false) override + virtual void Flush(Worker_EntityId EntityId, RPCWritingContext& Ctx, const SentRPCCallback& SentCallback = SentRPCCallback(), + bool bIgnoreAdded = false) override { - typename TRPCQueue::QueueData* Queue = this->Queues.Find(EntityId); + QueueData* Queue = this->Queues.Find(EntityId); if (Queue == nullptr || (!Queue->bAdded && !bIgnoreAdded)) { return; } this->FlushQueue(EntityId, *Queue, Ctx, SentCallback); + CheckOverflow(EntityId, *Queue); + } + + virtual void OnAuthLost(Worker_EntityId EntityId) override + { + Super::OnAuthLost(EntityId); + OverflowSizes.Remove(EntityId); } + +protected: + void CheckOverflow(Worker_EntityId EntityId, QueueData& Queue) + { + const int32 RemainingRPCs = Queue.RPCs.Num(); + if (RemainingRPCs == 0) + { + OverflowSizes.Remove(EntityId); + } + else + { + int32* Overflow = OverflowSizes.Find(EntityId); + if (Overflow == nullptr) + { + if (this->ErrorCallback) + { + this->ErrorCallback(this->Name, EntityId, QueueError::BufferOverflow); + } + Overflow = &OverflowSizes.Add(EntityId, RemainingRPCs); + } + *Overflow = RemainingRPCs; + if (*Overflow > UnboundedQueueOverflowLimit) + { + if (this->ErrorCallback) + { + this->ErrorCallback(this->Name, EntityId, QueueError::QueueFull); + } + // Clear to avoid 1) spamming errors 2) leaking resources. + Queue.RPCs.Empty(); + Queue.AddData.Empty(); + } + } + } + + TMap OverflowSizes; }; /** @@ -51,30 +101,38 @@ struct TRPCUnboundedQueue : public TRPCQueue template struct TRPCFixedCapacityQueue : public TRPCQueue { + using Super = TRPCQueue; + using typename Super::QueueData; + using typename Super::SentRPCCallback; + TRPCFixedCapacityQueue(FName InName, TRPCBufferSender& Sender, uint32 InCapacity) - : TRPCUnboundedQueue(InName, Sender) + : Super(InName, Sender) , Capacity(InCapacity) { } int32 Capacity; - void Push(Worker_EntityId EntityId, Payload&& Data, AdditionalSendingData&& AddData) override + virtual void Push(Worker_EntityId EntityId, Payload&& Data, AdditionalSendingData&& AddData = AdditionalSendingData()) override { - typename TRPCQueue::QueueData& Queue = this->Queues.FindOrAdd(EntityId); + QueueData& Queue = this->Queues.FindOrAdd(EntityId); if (Queue.RPCs.Num() < Capacity) { Queue.RPCs.Add(MoveTemp(Data)); Queue.AddData.Add(AddData); } + else if (this->ErrorCallback) + { + this->ErrorCallback(this->Name, EntityId, QueueError::QueueFull); + } } - void FlushAll(RPCWritingContext& Ctx, const typename TWrappedRPCQueue::SentRPCCallback& SentCallback) override + virtual void FlushAll(RPCWritingContext& Ctx, const SentRPCCallback& SentCallback = SentRPCCallback()) override { for (auto Iterator = this->Queues.CreateIterator(); Iterator; ++Iterator) { - typename TRPCQueue::QueueData& Queue = Iterator->Value; + QueueData& Queue = Iterator->Value; if (Queue.bAdded && Queue.RPCs.Num() > 0) { this->FlushQueue(Iterator->Key, Queue, Ctx, SentCallback); @@ -84,10 +142,10 @@ struct TRPCFixedCapacityQueue : public TRPCQueue } } - void Flush(Worker_EntityId EntityId, RPCWritingContext& Ctx, - const typename TWrappedRPCQueue::SentRPCCallback& SentCallback, bool bIgnoreAdded = false) override + virtual void Flush(Worker_EntityId EntityId, RPCWritingContext& Ctx, const SentRPCCallback& SentCallback = SentRPCCallback(), + bool bIgnoreAdded = false) override { - typename TRPCQueue::QueueData* Queue = this->Queues.Find(EntityId); + QueueData* Queue = this->Queues.Find(EntityId); if (Queue == nullptr || (!Queue->bAdded && !bIgnoreAdded)) { return; @@ -105,14 +163,17 @@ struct TRPCFixedCapacityQueue : public TRPCQueue template struct TRPCMostRecentQueue : public TRPCFixedCapacityQueue { + using Super = TRPCFixedCapacityQueue; + using typename Super::QueueData; + TRPCMostRecentQueue(FName InName, TRPCBufferSender& Sender, uint32 InCapacity) - : TRPCFixedCapacityQueue(InName, Sender, InCapacity) + : Super(InName, Sender, InCapacity) { } - void Push(Worker_EntityId EntityId, Payload&& Data, AdditionalSendingData&& AddData) override + virtual void Push(Worker_EntityId EntityId, Payload&& Data, AdditionalSendingData&& AddData = AdditionalSendingData()) override { - typename TRPCQueue::QueueData& Queue = this->Queues.FindOrAdd(EntityId); + QueueData& Queue = this->Queues.FindOrAdd(EntityId); if (Queue.RPCs.Num() == this->Capacity) { diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPCTypes.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPCTypes.h index ac8d293165..8320525f2c 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPCTypes.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPCTypes.h @@ -4,23 +4,15 @@ #include "CoreMinimal.h" #include "Interop/Connection/SpatialGDKSpanId.h" -#include "Schema/RPCPayload.h" #include "SpatialView/EntityView.h" #include "Utils/ObjectAllocUtils.h" namespace SpatialGDK { -struct ReceivedRPC : FNoHeapAllocation +enum class QueueError { - ReceivedRPC(uint32 InOffset, uint32 InIndex, TArrayView InPayloadData) - : Offset(InOffset) - , Index(InIndex) - , PayloadData(InPayloadData) - { - } - const uint32 Offset; - const uint32 Index; - const TArrayView PayloadData; + BufferOverflow, // The buffer sender is full, RPCs will be locally queued. + QueueFull, // The queue is full, additional RPCs will be dropped. }; namespace RPCCallbacks @@ -30,7 +22,7 @@ using UpdateWritten = TFunction; using ResponseWritten = TFunction; using RPCWritten = TFunction; - +using QueueErrorCallback = TFunction; using CanExtractRPCs = TFunction; } // namespace RPCCallbacks @@ -178,6 +170,8 @@ struct NullReceiveWrapper using AdditionalData = RPCEmptyData; struct WrappedData { + WrappedData() {} + WrappedData(T&& InData) : Data(MoveTemp(InData)) { @@ -197,27 +191,27 @@ struct NullReceiveWrapper WrappedData MakeWrappedData(Worker_EntityId EntityId, T&& Data, uint64 RPCId) { return MoveTemp(Data); } }; -template class PayloadWrapper = NullReceiveWrapper> +template class PayloadWrapper = NullReceiveWrapper> class TRPCBufferReceiver : public RPCBufferReceiver { public: - TRPCBufferReceiver(PayloadWrapper&& InWrapper = PayloadWrapper()) + TRPCBufferReceiver(PayloadWrapper&& InWrapper = PayloadWrapper()) : Wrapper(MoveTemp(InWrapper)) { } - using ProcessRPC = TFunction::AdditionalData const&)>; + using ProcessRPC = TFunction::AdditionalData const&)>; virtual void ExtractReceivedRPCs(const RPCCallbacks::CanExtractRPCs&, const ProcessRPC&) = 0; - void QueueReceivedRPC(Worker_EntityId EntityId, T&& Data, uint64 RPCId) + void QueueReceivedRPC(Worker_EntityId EntityId, PayloadType&& Data, uint64 RPCId) { auto& RPCs = ReceivedRPCs.FindOrAdd(EntityId); RPCs.Emplace(Wrapper.MakeWrappedData(EntityId, MoveTemp(Data), RPCId)); } protected: - TMap::WrappedData>> ReceivedRPCs; - PayloadWrapper Wrapper; + TMap::WrappedData>> ReceivedRPCs; + PayloadWrapper Wrapper; }; /** @@ -234,12 +228,15 @@ struct RPCQueue const FName Name; + void SetErrorCallback(RPCCallbacks::QueueErrorCallback InCallback) { ErrorCallback = MoveTemp(InCallback); } + protected: RPCQueue(FName InName) : Name(InName) { } TSet ComponentsToReadOnAuthGained; + RPCCallbacks::QueueErrorCallback ErrorCallback; }; /** @@ -324,7 +321,7 @@ struct TRPCQueue : TWrappedRPCQueue const uint32 WrittenRPCsReported = this->Sender.Write(Ctx, EntityId, Queue.RPCs, WrittenCallback); // Basic check that the written callback was called for every individual RPC. - check(WrittenRPCs == WrittenRPCsReported); + ensureAlwaysMsgf(WrittenRPCs == WrittenRPCsReported, TEXT("Failed to add callbacks for every written RPC")); if (WrittenRPCs == QueuedRPCs) { @@ -334,13 +331,14 @@ struct TRPCQueue : TWrappedRPCQueue } else { - for (uint32 i = 0; i < WrittenRPCs; ++i) + const int32 RemainingRPCs = QueuedRPCs - WrittenRPCs; + for (int32 i = 0; i < RemainingRPCs; ++i) { Queue.RPCs[i] = MoveTemp(Queue.RPCs[i + WrittenRPCs]); Queue.AddData[i] = MoveTemp(Queue.AddData[i + WrittenRPCs]); } - Queue.RPCs.SetNum(QueuedRPCs - WrittenRPCs); - Queue.AddData.SetNum(QueuedRPCs - WrittenRPCs); + Queue.RPCs.SetNum(RemainingRPCs); + Queue.AddData.SetNum(RemainingRPCs); } return false; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPC_RingBufferSerializer.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPC_RingBufferSerializer.h new file mode 100644 index 0000000000..969fd876ee --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPC_RingBufferSerializer.h @@ -0,0 +1,79 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Interop/RPCs/RPCTypes.h" + +namespace SpatialGDK +{ +template +class RingBufferSerializer_Schema +{ +public: + RingBufferSerializer_Schema(Worker_ComponentId InComponentId, Schema_FieldId InCountFieldId, + Schema_FieldId InFirstRingBufferSlotFieldId, Worker_ComponentId InACKComponentId, + Schema_FieldId InACKCountFieldId) + : ComponentId(InComponentId) + , CountFieldId(InCountFieldId) + , FirstRingBufferSlotFieldId(InFirstRingBufferSlotFieldId) + , ACKComponentId(InACKComponentId) + , ACKCountFieldId(InACKCountFieldId) + { + check(ComponentId != 0); + check(ACKComponentId != 0); + } + + Worker_ComponentId GetComponentId() { return ComponentId; } + + Worker_ComponentId GetACKComponentId() { return ACKComponentId; } + + TOptional ReadRPCCount(const RPCReadingContext& Ctx) + { + if (Schema_GetUint64Count(Ctx.Fields, CountFieldId)) + { + return Schema_GetUint64(Ctx.Fields, CountFieldId); + } + return {}; + } + + TOptional ReadACKCount(const RPCReadingContext& Ctx) + { + if (Schema_GetUint64Count(Ctx.Fields, ACKCountFieldId)) + { + return Schema_GetUint64(Ctx.Fields, ACKCountFieldId); + } + return {}; + } + + void ReadRPC(const RPCReadingContext& Ctx, uint32 Slot, Payload& OutPayload) + { + Schema_Object* PayloadObject = Schema_GetObject(Ctx.Fields, FirstRingBufferSlotFieldId + Slot); + if (ensure(PayloadObject)) + { + OutPayload.ReadFromSchema(PayloadObject); + } + } + + void WriteRPC(RPCWritingContext::EntityWrite& Ctx, uint32 Slot, const Payload& InPayload) + { + Schema_Object* NewField = Schema_AddObject(Ctx.GetFieldsToWrite(), Slot + FirstRingBufferSlotFieldId); + InPayload.WriteToSchema(NewField); + } + + void WriteRPCCount(RPCWritingContext::EntityWrite& Ctx, uint64 Count) { Schema_AddUint64(Ctx.GetFieldsToWrite(), CountFieldId, Count); } + + void WriteACKCount(RPCWritingContext::EntityWrite& Ctx, uint64 Count) + { + Schema_AddUint64(Ctx.GetFieldsToWrite(), ACKCountFieldId, Count); + } + +private: + const Worker_ComponentId ComponentId; + const Schema_FieldId CountFieldId; + const Schema_FieldId FirstRingBufferSlotFieldId; + + const Worker_ComponentId ACKComponentId; + const Schema_FieldId ACKCountFieldId; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPC_RingBufferWithACK_Receiver.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPC_RingBufferWithACK_Receiver.h new file mode 100644 index 0000000000..f86e2deff0 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPC_RingBufferWithACK_Receiver.h @@ -0,0 +1,173 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "EngineClasses/SpatialNetDriverRPC.h" +#include "Interop/RPCs/RPCTypes.h" +#include "SpatialView/EntityComponentTypes.h" + +namespace SpatialGDK +{ +template class PayloadWrapper, typename SerializerType> +class MonotonicRingBufferWithACKReceiver : public TRPCBufferReceiver +{ + using Super = TRPCBufferReceiver; + using Super::ComponentsToRead; + using typename Super::ProcessRPC; + +public: + MonotonicRingBufferWithACKReceiver(SerializerType&& InSerializer, int32 InNumberOfSlots, PayloadWrapper&& InWrapper) + : Super(MoveTemp(InWrapper)) + , Serializer(MoveTemp(InSerializer)) + , NumberOfSlots(InNumberOfSlots) + { + ComponentsToRead.Add(Serializer.GetComponentId()); + ComponentsToRead.Add(Serializer.GetACKComponentId()); + } + + virtual void OnAdded(FName ReceiverName, Worker_EntityId EntityId, EntityViewElement const& Element) override + { + const ComponentData* RPCComponentData = Element.Components.FindByPredicate(ComponentIdEquality{ Serializer.GetComponentId() }); + const ComponentData* ACKComponentData = Element.Components.FindByPredicate(ComponentIdEquality{ Serializer.GetACKComponentId() }); + RPCReadingContext ReadCtx; + ReadCtx.ReaderName = ReceiverName; + ReadCtx.EntityId = EntityId; + + // Making sure we read component data in order, first ACK, then RPC + // to know which RPC we actually need to extract + if (ensure(ACKComponentData != nullptr)) + { + ReadCtx.ComponentId = Serializer.GetACKComponentId(); + ReadCtx.Fields = Schema_GetComponentDataFields(ACKComponentData->GetUnderlying()); + OnAdded_ReadACKComponent(ReadCtx); + } + + if (ensure(RPCComponentData != nullptr)) + { + ReadCtx.ComponentId = Serializer.GetComponentId(); + ReadCtx.Fields = Schema_GetComponentDataFields(RPCComponentData->GetUnderlying()); + OnAdded_ReadRPCComponent(ReadCtx); + } + } + + virtual void OnAdded_ReadComponent(const RPCReadingContext& Ctx) override { checkNoEntry(); } + + void OnAdded_ReadRPCComponent(const RPCReadingContext& Ctx) + { + ReceiverState& State = ReceiverStates.FindChecked(Ctx.EntityId); + ReadRPCs(Ctx, State); + } + + void OnAdded_ReadACKComponent(const RPCReadingContext& Ctx) + { + ReceiverState& State = ReceiverStates.FindOrAdd(Ctx.EntityId); + TOptional ACKCount = Serializer.ReadACKCount(Ctx); + State.LastExecuted = ACKCount ? ACKCount.GetValue() : 0; + State.LastRead = State.LastExecuted; + State.LastWrittenACK = State.LastExecuted; + } + + virtual void OnRemoved(Worker_EntityId EntityId) override + { + ReceiverStates.Remove(EntityId); + this->ReceivedRPCs.Remove(EntityId); + } + + virtual void OnUpdate(const RPCReadingContext& Ctx) override + { + if (ComponentsToRead.Contains(Ctx.ComponentId)) + { + ReceiverState& State = ReceiverStates.FindChecked(Ctx.EntityId); + ReadRPCs(Ctx, State); + } + } + + virtual void FlushUpdates(RPCWritingContext& Ctx) override + { + for (auto& Receiver : ReceiverStates) + { + Worker_EntityId EntityId = Receiver.Key; + ReceiverState& State = Receiver.Value; + if (State.LastExecuted != State.LastWrittenACK) + { + auto EntityWriter = Ctx.WriteTo(EntityId, Serializer.GetACKComponentId()); + Serializer.WriteACKCount(EntityWriter, State.LastExecuted); + State.LastWrittenACK = State.LastExecuted; + } + } + } + + virtual void ExtractReceivedRPCs(const RPCCallbacks::CanExtractRPCs& CanExtract, const ProcessRPC& Process) override + { + for (auto Iterator = this->ReceivedRPCs.CreateIterator(); Iterator; ++Iterator) + { + Worker_EntityId EntityId = Iterator->Key; + if (!CanExtract(EntityId)) + { + continue; + } + ReceiverState& State = ReceiverStates.FindChecked(EntityId); + auto& Payloads = Iterator->Value; + uint32 ProcessedRPCs = 0; + for (const auto& Payload : Payloads) + { + const PayloadType& PayloadData = Payload.GetData(); + if (Process(EntityId, PayloadData, Payload.GetAdditionalData())) + { + ++ProcessedRPCs; + } + else + { + break; + } + } + if (ProcessedRPCs == Payloads.Num()) + { + Iterator.RemoveCurrent(); + } + else + { + uint32_t RemainingRPCs = Payloads.Num() - ProcessedRPCs; + for (uint32_t i = 0; i < RemainingRPCs; ++i) + { + Payloads[i] = MoveTemp(Payloads[i + ProcessedRPCs]); + } + Payloads.SetNum(RemainingRPCs); + } + + State.LastExecuted += ProcessedRPCs; + } + } + +protected: + struct ReceiverState + { + uint64 LastRead; + uint64 LastWrittenACK; + uint64 LastExecuted; + }; + + void ReadRPCs(const RPCReadingContext& Ctx, ReceiverState& State) + { + TOptional NewRPCCount = Serializer.ReadRPCCount(Ctx); + if (NewRPCCount.IsSet() && NewRPCCount.GetValue() != State.LastRead) + { + uint64 RPCCount = NewRPCCount.GetValue(); + for (uint64 RPCId = State.LastRead + 1; RPCId <= RPCCount; ++RPCId) + { + uint32 Slot = (RPCId - 1) % NumberOfSlots; + PayloadType NewPayload; + Serializer.ReadRPC(Ctx, Slot, NewPayload); + this->QueueReceivedRPC(Ctx.EntityId, MoveTemp(NewPayload), RPCId); + } + State.LastRead = RPCCount; + } + } + + TMap ReceiverStates; + + SerializerType Serializer; + int32 NumberOfSlots; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPC_RingBufferWithACK_Sender.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPC_RingBufferWithACK_Sender.h new file mode 100644 index 0000000000..ce56e89506 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPC_RingBufferWithACK_Sender.h @@ -0,0 +1,104 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "EngineClasses/SpatialNetDriverRPC.h" +#include "Interop/RPCs/RPCTypes.h" + +namespace SpatialGDK +{ +template +class MonotonicRingBufferWithACKSender : public TRPCBufferSender +{ + using Super = TRPCBufferSender; + using Super::ComponentsToReadOnAuthGained; + using Super::ComponentsToReadOnUpdate; + +public: + MonotonicRingBufferWithACKSender(SerializerType&& InSerializer, int32 InNumberOfSlots) + : Serializer(MoveTemp(InSerializer)) + , NumberOfSlots(InNumberOfSlots) + { + ComponentsToReadOnAuthGained.Add(Serializer.GetComponentId()); + ComponentsToReadOnAuthGained.Add(Serializer.GetACKComponentId()); + ComponentsToReadOnUpdate.Add(Serializer.GetACKComponentId()); + } + + virtual void OnUpdate(const RPCReadingContext& Ctx) override + { + if (Ctx.ComponentId == Serializer.GetACKComponentId()) + { + BufferStateData& State = BufferState.FindChecked(Ctx.EntityId); + + TOptional NewACKCount = Serializer.ReadACKCount(Ctx); + if (NewACKCount) + { + State.LastACK = NewACKCount.GetValue(); + } + } + } + + virtual void OnAuthGained_ReadComponent(const RPCReadingContext& Ctx) override + { + if (Ctx.ComponentId == Serializer.GetComponentId()) + { + OnAuthGained_ReadRPCComponent(Ctx); + } + if (Ctx.ComponentId == Serializer.GetACKComponentId()) + { + OnAuthGained_ReadACKComponent(Ctx); + } + } + + void OnAuthGained_ReadRPCComponent(const RPCReadingContext& Ctx) + { + BufferStateData& State = BufferState.FindOrAdd(Ctx.EntityId); + TOptional RPCCount = Serializer.ReadRPCCount(Ctx); + State.CountWritten = RPCCount.Get(/*DefaultValue*/ 0); + } + + void OnAuthGained_ReadACKComponent(const RPCReadingContext& Ctx) + { + BufferStateData& State = BufferState.FindOrAdd(Ctx.EntityId); + TOptional ACKCount = Serializer.ReadACKCount(Ctx); + State.LastACK = ACKCount.Get(/*DefaultValue*/ 0); + } + + virtual void OnAuthLost(Worker_EntityId Entity) override { BufferState.Remove(Entity); } + + virtual uint32 Write(RPCWritingContext& Ctx, Worker_EntityId EntityId, TArrayView RPCs, + const RPCCallbacks::RPCWritten& WrittenCallback) override + { + BufferStateData& NextSlot = BufferState.FindOrAdd(EntityId); + int32 AvailableSlots = FMath::Max(0, int32(NumberOfSlots) - int32(NextSlot.CountWritten - NextSlot.LastACK)); + + int32 RPCsToWrite = FMath::Min(RPCs.Num(), AvailableSlots); + if (RPCsToWrite > 0) + { + auto EntityWrite = Ctx.WriteTo(EntityId, Serializer.GetComponentId()); + for (int32 i = 0; i < RPCsToWrite; ++i) + { + uint64 RPCId = ++NextSlot.CountWritten; + uint64 Slot = (RPCId - 1) % NumberOfSlots; + Serializer.WriteRPC(EntityWrite, Slot, RPCs[i]); + WrittenCallback(Serializer.GetComponentId(), RPCId); + } + Serializer.WriteRPCCount(EntityWrite, NextSlot.CountWritten); + } + + return RPCsToWrite; + } + +private: + struct BufferStateData + { + uint64 CountWritten = 0; + uint64 LastACK = 0; + }; + TMap BufferState; + + SerializerType Serializer; + int32 NumberOfSlots; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/SpatialRPCService.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/SpatialRPCService.h index 5332be9fd2..5a6fcd37c3 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/SpatialRPCService.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/SpatialRPCService.h @@ -7,7 +7,6 @@ #include "ClientServerRPCService.h" #include "CrossServerRPCService.h" #include "EngineClasses/SpatialNetBitWriter.h" -#include "Interop/Connection/SpatialEventTracer.h" #include "Interop/Connection/SpatialGDKSpanId.h" #include "Interop/SpatialClassInfoManager.h" #include "MulticastRPCService.h" @@ -25,12 +24,14 @@ struct RPCRingBuffer; namespace SpatialGDK { +class SpatialEventTracer; + class SPATIALGDK_API SpatialRPCService { public: explicit SpatialRPCService(const FSubView& InActorAuthSubView, const FSubView& InActorNonAuthSubView, - USpatialLatencyTracer* InSpatialLatencyTracer, SpatialEventTracer* InEventTracer, - USpatialNetDriver* InNetDriver); + const FSubView& InWorkerEntitySubView, USpatialLatencyTracer* InSpatialLatencyTracer, + SpatialEventTracer* InEventTracer, USpatialNetDriver* InNetDriver); void AdvanceView(); void ProcessChanges(const float NetDriverTime); @@ -97,11 +98,6 @@ class SPATIALGDK_API SpatialRPCService float LastIncomingProcessingTime; float LastOutgoingProcessingTime; - -#if TRACE_LIB_ACTIVE - void ProcessResultToLatencyTrace(const EPushRPCResult Result, const TraceKey Trace); - TMap PendingTraces; -#endif }; } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h index cb81a7aeff..282adc926a 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h @@ -21,6 +21,9 @@ FORCEINLINE void ForAllSchemaComponentTypes(TFunction RPCs; TMap RPCInfoMap; - uint32 HandoverPropertiesSize; - TArray HandoverProperties; TArray InterestProperties; // For Actors and default Subobjects belonging to Actors Worker_ComponentId SchemaComponents[ESchemaComponentType::SCHEMA_Count] = {}; // Only for Actors - TMap> SubobjectInfo; + TMap> SubobjectInfo; // Only for default Subobjects belonging to Actors FName SubobjectName; @@ -110,7 +104,7 @@ class SPATIALGDK_API USpatialClassInfoManager : public UObject const FClassInfo& GetClassInfoByComponentId(Worker_ComponentId ComponentId); UClass* GetClassByComponentId(Worker_ComponentId ComponentId); - bool GetOffsetByComponentId(Worker_ComponentId ComponentId, uint32& OutOffset); + bool GetOffsetByComponentId(Worker_ComponentId ComponentId, ObjectOffset& OutOffset); ESchemaComponentType GetCategoryByComponentId(Worker_ComponentId ComponentId); const TArray& GetFieldIdsByComponentId(Worker_ComponentId ComponentId); @@ -148,7 +142,7 @@ class SPATIALGDK_API USpatialClassInfoManager : public UObject void FinishConstructingActorClassInfo(const FString& ClassPath, TSharedRef& Info); void FinishConstructingSubobjectClassInfo(const FString& ClassPath, TSharedRef& Info); - bool ShouldTrackHandoverProperties() const; + bool IsComponentIdForTypeValid(const Worker_ComponentId ComponentId, const ESchemaComponentType Type) const; private: UPROPERTY() @@ -158,6 +152,8 @@ class SPATIALGDK_API USpatialClassInfoManager : public UObject TWeakObjectPtrMapKeyFuncs, TSharedRef, false>> ClassInfoMap; TMap> ComponentToClassInfoMap; - TMap ComponentToOffsetMap; + TMap ComponentToOffsetMap; TMap ComponentToCategoryMap; + + TOptional bHandoverActive; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialConditionMapFilter.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialConditionMapFilter.h index e0ee0fbd60..3c51c6a91a 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialConditionMapFilter.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialConditionMapFilter.h @@ -62,6 +62,8 @@ class FSpatialConditionMapFilter ConditionMap[COND_ReplayOnly] = bIsReplay; ConditionMap[COND_SkipReplay] = !bIsReplay; + ConditionMap[COND_ServerOnly] = !bIsClient; + ConditionMap[COND_Custom] = true; ConditionMap[COND_Never] = false; } diff --git a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GameplayDebuggerLBStrategy.h b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GameplayDebuggerLBStrategy.h new file mode 100644 index 0000000000..690af5ad8b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GameplayDebuggerLBStrategy.h @@ -0,0 +1,56 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" + +#include "LoadBalancing/AbstractLBStrategy.h" +#include "Utils/LayerInfo.h" + +#include "GameplayDebuggerLBStrategy.generated.h" + +class USpatialNetDriverGameplayDebuggerContext; +class UAbstractLBStrategy; + +DECLARE_LOG_CATEGORY_EXTERN(LogGameplayDebuggerLBStrategy, Log, All) + +/* + * Load balancing strategy to manage gameplay debugger replicated actors. + * This strategy wraps whatever LB strategy is already in place, and allows + * said replicated actors to be authority assigned to any server. + */ +UCLASS(HideDropdown, NotBlueprintable) +class SPATIALGDK_API UGameplayDebuggerLBStrategy : public UAbstractLBStrategy +{ + GENERATED_BODY() + +public: + UGameplayDebuggerLBStrategy(); + + void Init(USpatialNetDriverGameplayDebuggerContext& GameplayDebuggerCtx, UAbstractLBStrategy& InWrappedStrategy); + + /* UAbstractLBStrategy Interface */ + virtual void Init() override{}; + virtual FString ToString() const; + virtual void SetLocalVirtualWorkerId(VirtualWorkerId InLocalVirtualWorkerId) override; + virtual bool ShouldHaveAuthority(const AActor& Actor) const override; + virtual VirtualWorkerId WhoShouldHaveAuthority(const AActor& Actor) const override; + virtual SpatialGDK::FActorLoadBalancingGroupId GetActorGroupId(const AActor& Actor) const override; + virtual SpatialGDK::QueryConstraint GetWorkerInterestQueryConstraint(const VirtualWorkerId VirtualWorker) const override; + virtual FVector GetWorkerEntityPosition() const override; + virtual uint32 GetMinimumRequiredWorkers() const override; + virtual void SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtualWorkerId, const VirtualWorkerId& LastVirtualWorkerId) override; + virtual TSet GetVirtualWorkerIds() const override; + virtual UAbstractLBStrategy* GetLBStrategyForVisualRendering() const override; + virtual bool RequiresHandoverData() const override; + /* End UAbstractLBStrategy Interface */ + + UAbstractLBStrategy* GetWrappedStrategy() const; + +private: + UPROPERTY() + UAbstractLBStrategy* WrappedStrategy; + + USpatialNetDriverGameplayDebuggerContext* GameplayDebuggerCtx; + TArray VirtualWorkerIds; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GridBasedLBStrategy.h b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GridBasedLBStrategy.h index 5bd837997e..9bddebdc4c 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GridBasedLBStrategy.h +++ b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GridBasedLBStrategy.h @@ -61,6 +61,11 @@ class SPATIALGDK_API UGridBasedLBStrategy : public UAbstractLBStrategy LBStrategyRegions GetLBStrategyRegions() const; + uint32 GetRows() const { return Rows; } + uint32 GetCols() const { return Cols; } + float GetWorldWidth() const { return WorldWidth; } + float GetWorldHeight() const { return WorldHeight; } + #if WITH_EDITOR virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; #endif // WITH_EDITOR diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/ActorSetMember.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/ActorSetMember.h index d8a8ee0e98..70e0e25d41 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/ActorSetMember.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/ActorSetMember.h @@ -44,10 +44,9 @@ struct SPATIALGDK_API ActorSetMember void WriteSchema(Schema_Object* Schema) const { - Schema_AddUint32(Schema, SpatialConstants::ACTOR_SET_MEMBER_COMPONENT_LEADER_ENTITY_ID, ActorSetId); + Schema_AddInt64(Schema, SpatialConstants::ACTOR_SET_MEMBER_COMPONENT_LEADER_ENTITY_ID, ActorSetId); } Worker_EntityId ActorSetId; }; - } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/GameplayDebuggerComponent.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/GameplayDebuggerComponent.h new file mode 100644 index 0000000000..2c9eab446e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/GameplayDebuggerComponent.h @@ -0,0 +1,69 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Schema/Component.h" +#include "SpatialConstants.h" + +#include +#include + +namespace SpatialGDK +{ +struct GameplayDebuggerComponent +{ + static const Worker_ComponentId ComponentId = SpatialConstants::GDK_GAMEPLAY_DEBUGGER_COMPONENT_ID; + + GameplayDebuggerComponent() {} + + explicit GameplayDebuggerComponent(Schema_ComponentData& Data) + { + Schema_Object* ComponentObject = Schema_GetComponentDataFields(&Data); + ReadFromSchema(ComponentObject); + } + + Worker_ComponentData CreateComponent() const + { + Worker_ComponentData Data = {}; + Data.component_id = ComponentId; + Data.schema_type = Schema_CreateComponentData(); + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + WriteToSchema(ComponentObject); + return Data; + } + + Worker_ComponentUpdate CreateComponentUpdate() const + { + Worker_ComponentUpdate Data = {}; + Data.component_id = ComponentId; + Data.schema_type = Schema_CreateComponentUpdate(); + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Data.schema_type); + WriteToSchema(ComponentObject); + + return Data; + } + + void ApplyComponentUpdate(Schema_ComponentUpdate* Update) + { + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update); + ReadFromSchema(ComponentObject); + } + + VirtualWorkerId DelegatedVirtualWorkerId = 0; + bool bTrackPlayer = false; + +private: + void ReadFromSchema(Schema_Object* ComponentObject) + { + DelegatedVirtualWorkerId = Schema_GetInt32(ComponentObject, 1); + bTrackPlayer = !!Schema_GetBool(ComponentObject, 2); + } + + void WriteToSchema(Schema_Object* ComponentObject) const + { + Schema_AddInt32(ComponentObject, 1, DelegatedVirtualWorkerId); + Schema_AddBool(ComponentObject, 2, bTrackPlayer); + } +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/MigrationDiagnostic.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/MigrationDiagnostic.h index 28e3ab7dcd..ee3fbd5d88 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/MigrationDiagnostic.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/MigrationDiagnostic.h @@ -41,14 +41,17 @@ struct MigrationDiagnostic : Component static Worker_CommandResponse CreateMigrationDiagnosticResponse(USpatialNetDriver* NetDriver, Worker_EntityId EntityId, AActor* BlockingActor) { - check(NetDriver != nullptr); - check(NetDriver->Connection != nullptr); - check(NetDriver->LockingPolicy != nullptr); - check(NetDriver->VirtualWorkerTranslator != nullptr); - check(NetDriver->PackageMap != nullptr); - Worker_CommandResponse CommandResponse = {}; + if (!ensureAlwaysMsgf( + NetDriver != nullptr && NetDriver->Connection != nullptr && NetDriver->LockingPolicy != nullptr + && NetDriver->VirtualWorkerTranslator != nullptr && NetDriver->PackageMap != nullptr, + TEXT("Failed to create migration disagnostic response. Some core class was undefined. EntityId: %lld. Actor: %s"), EntityId, + *GetNameSafe(BlockingActor))) + { + return CommandResponse; + } + CommandResponse.component_id = SpatialConstants::MIGRATION_DIAGNOSTIC_COMPONENT_ID; CommandResponse.command_index = SpatialConstants::MIGRATION_DIAGNOSTIC_COMMAND_ID; CommandResponse.schema_type = Schema_CreateCommandResponse(); @@ -64,7 +67,7 @@ struct MigrationDiagnostic : Component NetDriver->VirtualWorkerTranslator->GetLocalVirtualWorkerId()); Schema_AddBool(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_LOCKED_ID, NetDriver->LockingPolicy->IsLocked(BlockingActor)); - AActor* NetOwner; + AActor* NetOwner = nullptr; VirtualWorkerId NewAuthWorkerId; FSpatialLoadBalancingHandler MigrationHandler(NetDriver); @@ -84,13 +87,16 @@ struct MigrationDiagnostic : Component // worker over the actor that is blocking the migration and log the results. static FString CreateMigrationDiagnosticLog(USpatialNetDriver* NetDriver, Schema_Object* ResponseObject, AActor* BlockingActor) { - check(NetDriver != nullptr); - check(NetDriver->Connection != nullptr); - check(NetDriver->LockingPolicy != nullptr); + if (!ensureAlwaysMsgf(NetDriver != nullptr && NetDriver->Connection != nullptr && NetDriver->LockingPolicy != nullptr, + TEXT("Failed to create migration disagnostic log. Some core class was undefined. Actor: %s"), + *GetNameSafe(BlockingActor))) + { + return FString(); + } if (ResponseObject == nullptr) { - return FString::Printf(TEXT("Migration diaganostic log failed as response was empty.")); + return FString::Printf(TEXT("Migration diagnostic log failed as response was empty.")); } VirtualWorkerId AuthoritativeWorkerId = Schema_GetInt32(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_AUTHORITY_WORKER_ID); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/RPCPayload.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/RPCPayload.h index d38455c2c9..6ef7625d3e 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/RPCPayload.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/RPCPayload.h @@ -6,8 +6,6 @@ #include "SpatialConstants.h" #include "Utils/SchemaUtils.h" -#include "Utils/SpatialLatencyTracerMinimal.h" - #include #include @@ -74,12 +72,11 @@ struct RPCPayload { RPCPayload() = default; - RPCPayload(uint32 InOffset, uint32 InIndex, TOptional InId, TArray&& Data, TraceKey InTraceKey = InvalidTraceKey) + RPCPayload(uint32 InOffset, uint32 InIndex, TOptional InId, TArray&& Data) : Offset(InOffset) , Index(InIndex) , Id(InId) , PayloadData(MoveTemp(Data)) - , Trace(InTraceKey) { } @@ -95,8 +92,6 @@ struct RPCPayload } PayloadData = GetBytesFromSchema(RPCObject, SpatialConstants::UNREAL_RPC_PAYLOAD_RPC_PAYLOAD_ID); - - Trace = FSpatialLatencyTracerMinimal::ReadTraceFromSchemaObject(RPCObject, SpatialConstants::UNREAL_RPC_PAYLOAD_TRACE_ID); } int64 CountDataBits() const { return PayloadData.Num() * 8; } @@ -104,8 +99,6 @@ struct RPCPayload void WriteToSchemaObject(Schema_Object* RPCObject) const { WriteToSchemaObject(RPCObject, Offset, Index, Id, PayloadData.GetData(), PayloadData.Num()); - - FSpatialLatencyTracerMinimal::WriteTraceToSchemaObject(Trace, RPCObject, SpatialConstants::UNREAL_RPC_PAYLOAD_TRACE_ID); } static void WriteToSchemaObject(Schema_Object* RPCObject, uint32 Offset, uint32 Index, TOptional UniqueId, const uint8* Data, @@ -125,7 +118,6 @@ struct RPCPayload uint32 Index; TOptional Id; TArray PayloadData; - TraceKey Trace = InvalidTraceKey; }; } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h index 50c135b2fd..cf2f4a74ab 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h @@ -7,6 +7,8 @@ #include "Utils/SchemaOption.h" #include +typedef uint32 ObjectOffset; + class USpatialPackageMapClient; struct SPATIALGDK_API FUnrealObjectRef @@ -14,13 +16,13 @@ struct SPATIALGDK_API FUnrealObjectRef FUnrealObjectRef() = default; FUnrealObjectRef(const FUnrealObjectRef&) = default; - FUnrealObjectRef(Worker_EntityId Entity, uint32 Offset) + FUnrealObjectRef(Worker_EntityId Entity, ObjectOffset Offset) : Entity(Entity) , Offset(Offset) { } - FUnrealObjectRef(Worker_EntityId Entity, uint32 Offset, FString Path, FUnrealObjectRef Outer, bool bNoLoadOnClient = false) + FUnrealObjectRef(Worker_EntityId Entity, ObjectOffset Offset, FString Path, FUnrealObjectRef Outer, bool bNoLoadOnClient) : Entity(Entity) , Offset(Offset) , Path(Path) @@ -31,7 +33,11 @@ struct SPATIALGDK_API FUnrealObjectRef FUnrealObjectRef& operator=(const FUnrealObjectRef&) = default; - FORCEINLINE FString ToString() const { return FString::Printf(TEXT("(entity ID: %lld, offset: %u)"), Entity, Offset); } + FORCEINLINE FString ToString() const + { + return FString::Printf(TEXT("(entity ID: %lld, offset: %u, path: %s)"), Entity, Offset, + Path.IsSet() ? *Path.GetValue() : TEXT("not set")); + } FORCEINLINE FUnrealObjectRef GetLevelReference() const { @@ -75,10 +81,10 @@ struct SPATIALGDK_API FUnrealObjectRef static const FUnrealObjectRef UNRESOLVED_OBJECT_REF; Worker_EntityId Entity; - uint32 Offset; + ObjectOffset Offset; SpatialGDK::TSchemaOption Path; SpatialGDK::TSchemaOption Outer; - bool bNoLoadOnClient = false; + bool bNoLoadOnClient = true; // If this field is set to true, we are saying that the Actor will exist at most once on the given worker. // In addition, if we receive information for an Actor of this class over the network, then this data // should be applied to the Actor we've already spawned (where another worker created the entity). This diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialCommonTypes.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialCommonTypes.h index 75ffc10e4c..75a5855670 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialCommonTypes.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialCommonTypes.h @@ -31,26 +31,6 @@ using FObjectToRepStateMap = TMap; -template -struct FTrackableWorkerType : public T -{ - FTrackableWorkerType() = default; - - FTrackableWorkerType(const T& Update) - : T(Update) - { - } - - FTrackableWorkerType(T&& Update) - : T(MoveTemp(Update)) - { - } - -#if TRACE_LIB_ACTIVE - TraceKey Trace{ InvalidTraceKey }; -#endif -}; - template struct TWeakObjectPtrKeyFuncs : DefaultKeyFuncs { @@ -63,8 +43,8 @@ struct TWeakObjectPtrKeyFuncs : DefaultKeyFuncs; -using FWorkerComponentData = FTrackableWorkerType; +using FWorkerComponentUpdate = Worker_ComponentUpdate; +using FWorkerComponentData = Worker_ComponentData; /* A little helper to ensure no re-entrancy of modifications of data structures. */ struct FUsageLock diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.cxx b/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.cxx index d635b64364..4457fdae23 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.cxx +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.cxx @@ -6,62 +6,59 @@ using namespace SpatialGDK; namespace SpatialConstants { - FString RPCTypeToString(ERPCType RPCType) { - switch (RPCType) - { - case ERPCType::ClientReliable: - return TEXT("Client, Reliable"); - case ERPCType::ClientUnreliable: - return TEXT("Client, Unreliable"); - case ERPCType::ServerReliable: - return TEXT("Server, Reliable"); - case ERPCType::ServerUnreliable: - return TEXT("Server, Unreliable"); - case ERPCType::ServerAlwaysWrite: - return TEXT("Server, AlwaysWrite"); - case ERPCType::NetMulticast: - return TEXT("Multicast"); - case ERPCType::CrossServer: - return TEXT("CrossServer"); - default: - checkNoEntry(); - } - - return FString(); + switch (RPCType) + { + case ERPCType::ClientReliable: + return TEXT("Client, Reliable"); + case ERPCType::ClientUnreliable: + return TEXT("Client, Unreliable"); + case ERPCType::ServerReliable: + return TEXT("Server, Reliable"); + case ERPCType::ServerUnreliable: + return TEXT("Server, Unreliable"); + case ERPCType::ServerAlwaysWrite: + return TEXT("Server, AlwaysWrite"); + case ERPCType::NetMulticast: + return TEXT("Multicast"); + case ERPCType::CrossServer: + return TEXT("CrossServer"); + default: + checkNoEntry(); + } + + return FString(); } TOptional RPCStringToType(const FString& String) { - static const TMap s_NamesMap = [] - { - TMap NamesMap; - for (uint8 RPCTypeIdx = static_cast(ERPCType::RingBufferTypeBegin); - RPCTypeIdx <= static_cast(ERPCType::RingBufferTypeEnd); RPCTypeIdx++) - { - ERPCType RPCType = static_cast(RPCTypeIdx); - NamesMap.Add(RPCTypeToString(RPCType), RPCType); - } - return NamesMap; - }(); - - const ERPCType* Entry = s_NamesMap.Find(String); - - if (Entry) - { - return *Entry; - } - - return {}; + static const TMap s_NamesMap = [] { + TMap NamesMap; + for (uint8 RPCTypeIdx = static_cast(ERPCType::RingBufferTypeBegin); + RPCTypeIdx <= static_cast(ERPCType::RingBufferTypeEnd); RPCTypeIdx++) + { + ERPCType RPCType = static_cast(RPCTypeIdx); + NamesMap.Add(RPCTypeToString(RPCType), RPCType); + } + return NamesMap; + }(); + + const ERPCType* Entry = s_NamesMap.Find(String); + + if (Entry) + { + return *Entry; + } + + return {}; } - const FString SERVER_AUTH_COMPONENT_SET_NAME = TEXT("ServerAuthoritativeComponentSet"); const FString CLIENT_AUTH_COMPONENT_SET_NAME = TEXT("ClientAuthoritativeComponentSet"); const FString DATA_COMPONENT_SET_NAME = TEXT("DataComponentSet"); const FString OWNER_ONLY_COMPONENT_SET_NAME = TEXT("OwnerOnlyComponentSet"); -const FString HANDOVER_COMPONENT_SET_NAME = TEXT("HandoverComponentSet"); +const FString SERVER_ONLY_COMPONENT_SET_NAME = TEXT("ServerOnlyComponentSet"); const FString ROUTING_WORKER_COMPONENT_SET_NAME = TEXT("RoutingWorkerComponentSet"); const FString INITIAL_ONLY_COMPONENT_SET_NAME = TEXT("InitialOnlyComponentSet"); @@ -83,19 +80,18 @@ const FString CONSOLE_HOST_CN = TEXT("console.spatialoschina.com"); const FString AssemblyPattern = TEXT("^[a-zA-Z0-9_.-]{5,64}$"); const FText AssemblyPatternHint = -LOCTEXT("AssemblyPatternHint", - "Assembly name may only contain alphanumeric characters, '_', '.', or '-', and must be between 5 and 64 characters long."); + LOCTEXT("AssemblyPatternHint", + "Assembly name may only contain alphanumeric characters, '_', '.', or '-', and must be between 5 and 64 characters long."); const FString ProjectPattern = TEXT("^[a-z0-9_]{3,32}$"); const FText ProjectPatternHint = -LOCTEXT("ProjectPatternHint", - "Project name may only contain lowercase alphanumeric characters or '_', and must be between 3 and 32 characters long."); + LOCTEXT("ProjectPatternHint", + "Project name may only contain lowercase alphanumeric characters or '_', and must be between 3 and 32 characters long."); const FString DeploymentPattern = TEXT("^[a-z0-9_]{2,32}$"); const FText DeploymentPatternHint = -LOCTEXT("DeploymentPatternHint", - "Deployment name may only contain lowercase alphanumeric characters or '_', and must be between 2 and 32 characters long."); + LOCTEXT("DeploymentPatternHint", + "Deployment name may only contain lowercase alphanumeric characters or '_', and must be between 2 and 32 characters long."); const FString Ipv4Pattern = TEXT("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$"); - const FString SPATIALOS_METRICS_DYNAMIC_FPS = TEXT("Dynamic.FPS"); const FString RECONNECT_USING_COMMANDLINE_ARGUMENTS = TEXT("0.0.0.0"); const FString URL_LOGIN_OPTION = TEXT("login="); @@ -118,143 +114,185 @@ const FString EMPTY_TEST_MAP_PATH = TEXT("/SpatialGDK/Maps/Empty"); const FString DEV_LOGIN_TAG = TEXT("dev_login"); -const TArray REQUIRED_COMPONENTS_FOR_NON_AUTH_CLIENT_INTEREST = -TArray{ // Actor components - UNREAL_METADATA_COMPONENT_ID, SPAWN_DATA_COMPONENT_ID, TOMBSTONE_COMPONENT_ID, - DORMANT_COMPONENT_ID, INITIAL_ONLY_PRESENCE_COMPONENT_ID, +const TArray REQUIRED_COMPONENTS_FOR_NON_AUTH_CLIENT_INTEREST = TArray{ + // Actor components + UNREAL_METADATA_COMPONENT_ID, + SPAWN_DATA_COMPONENT_ID, + TOMBSTONE_COMPONENT_ID, + DORMANT_COMPONENT_ID, + INITIAL_ONLY_PRESENCE_COMPONENT_ID, - // Multicast RPCs - MULTICAST_RPCS_COMPONENT_ID, + // Multicast RPCs + MULTICAST_RPCS_COMPONENT_ID, - // Global state components - DEPLOYMENT_MAP_COMPONENT_ID, STARTUP_ACTOR_MANAGER_COMPONENT_ID, GSM_SHUTDOWN_COMPONENT_ID, + // Global state components + DEPLOYMENT_MAP_COMPONENT_ID, + STARTUP_ACTOR_MANAGER_COMPONENT_ID, + GSM_SHUTDOWN_COMPONENT_ID, - // Debugging information - DEBUG_METRICS_COMPONENT_ID, SPATIAL_DEBUGGING_COMPONENT_ID, + // Debugging information + DEBUG_METRICS_COMPONENT_ID, + SPATIAL_DEBUGGING_COMPONENT_ID, - // Strategy Worker LB components - ACTOR_SET_MEMBER_COMPONENT_ID, ACTOR_GROUP_MEMBER_COMPONENT_ID, + // Strategy Worker LB components + ACTOR_SET_MEMBER_COMPONENT_ID, + ACTOR_GROUP_MEMBER_COMPONENT_ID, - // Actor tag - ACTOR_TAG_COMPONENT_ID + // Actor tag + ACTOR_TAG_COMPONENT_ID, + + ACTOR_OWNERSHIP_COMPONENT_ID, }; -const TArray REQUIRED_COMPONENTS_FOR_AUTH_CLIENT_INTEREST = -TArray{ // RPCs from the server - SERVER_ENDPOINT_COMPONENT_ID, +const TArray REQUIRED_COMPONENTS_FOR_AUTH_CLIENT_INTEREST = TArray{ + // RPCs from the server + SERVER_ENDPOINT_COMPONENT_ID, + + // Actor tags + ACTOR_TAG_COMPONENT_ID, + ACTOR_AUTH_TAG_COMPONENT_ID, - // Actor tags - ACTOR_TAG_COMPONENT_ID, ACTOR_AUTH_TAG_COMPONENT_ID + ACTOR_OWNER_ONLY_DATA_TAG_COMPONENT_ID, }; const TArray REQUIRED_COMPONENTS_FOR_NON_AUTH_SERVER_INTEREST = TArray{ - // Actor components - UNREAL_METADATA_COMPONENT_ID, SPAWN_DATA_COMPONENT_ID, TOMBSTONE_COMPONENT_ID, DORMANT_COMPONENT_ID, - NET_OWNING_CLIENT_WORKER_COMPONENT_ID, + // Actor components + UNREAL_METADATA_COMPONENT_ID, + SPAWN_DATA_COMPONENT_ID, + TOMBSTONE_COMPONENT_ID, + DORMANT_COMPONENT_ID, + NET_OWNING_CLIENT_WORKER_COMPONENT_ID, + + // Multicast RPCs + MULTICAST_RPCS_COMPONENT_ID, - // Multicast RPCs - MULTICAST_RPCS_COMPONENT_ID, + // Global state components + DEPLOYMENT_MAP_COMPONENT_ID, + STARTUP_ACTOR_MANAGER_COMPONENT_ID, + GSM_SHUTDOWN_COMPONENT_ID, + SNAPSHOT_VERSION_COMPONENT_ID, - // Global state components - DEPLOYMENT_MAP_COMPONENT_ID, STARTUP_ACTOR_MANAGER_COMPONENT_ID, GSM_SHUTDOWN_COMPONENT_ID, SNAPSHOT_VERSION_COMPONENT_ID, + // Unreal load balancing components + VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID, - // Unreal load balancing components - VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID, + // Authority intent component to handle scattered hierarchies + AUTHORITY_INTENT_COMPONENT_ID, - // Authority intent component to handle scattered hierarchies - AUTHORITY_INTENT_COMPONENT_ID, + // Tags: Well known entities, non-auth actors, and tombstone tags + GDK_KNOWN_ENTITY_TAG_COMPONENT_ID, + ACTOR_TAG_COMPONENT_ID, - // Tags: Well known entities, non-auth actors, and tombstone tags - GDK_KNOWN_ENTITY_TAG_COMPONENT_ID, ACTOR_TAG_COMPONENT_ID, + // Strategy Worker LB components + ACTOR_SET_MEMBER_COMPONENT_ID, + ACTOR_GROUP_MEMBER_COMPONENT_ID, - // Strategy Worker LB components - ACTOR_SET_MEMBER_COMPONENT_ID, ACTOR_GROUP_MEMBER_COMPONENT_ID, + PLAYER_CONTROLLER_COMPONENT_ID, + PARTITION_COMPONENT_ID, - PLAYER_CONTROLLER_COMPONENT_ID, PARTITION_COMPONENT_ID + ACTOR_OWNER_ONLY_DATA_TAG_COMPONENT_ID, + + ACTOR_OWNERSHIP_COMPONENT_ID, }; const TArray REQUIRED_COMPONENTS_FOR_AUTH_SERVER_INTEREST = -TArray{ // RPCs from clients - CLIENT_ENDPOINT_COMPONENT_ID, + TArray{ // RPCs from clients + CLIENT_ENDPOINT_COMPONENT_ID, - // Player controller - PLAYER_CONTROLLER_COMPONENT_ID, + // Player controller + PLAYER_CONTROLLER_COMPONENT_ID, - // Cross server endpoint - CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID, CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID, + // Cross server endpoint + CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID, CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID, - // Actor tags - ACTOR_TAG_COMPONENT_ID, ACTOR_AUTH_TAG_COMPONENT_ID, + // Actor tags + ACTOR_TAG_COMPONENT_ID, ACTOR_AUTH_TAG_COMPONENT_ID, - PARTITION_COMPONENT_ID -}; + PARTITION_COMPONENT_ID + }; const TArray ServerAuthorityWellKnownSchemaImports = { - "improbable/standard_library.schema", - "unreal/gdk/authority_intent.schema", - "unreal/gdk/debug_component.schema", - "unreal/gdk/debug_metrics.schema", - "unreal/gdk/net_owning_client_worker.schema", - "unreal/gdk/not_streamed.schema", - "unreal/gdk/query_tags.schema", - "unreal/gdk/relevant.schema", - "unreal/gdk/rpc_components.schema", - "unreal/gdk/spatial_debugging.schema", - "unreal/gdk/spawndata.schema", - "unreal/gdk/tombstone.schema", - "unreal/gdk/unreal_metadata.schema", - "unreal/gdk/actor_group_member.schema", - "unreal/gdk/actor_set_member.schema", - "unreal/generated/rpc_endpoints.schema", - "unreal/generated/NetCullDistance/ncdcomponents.schema", + "improbable/standard_library.schema", + "unreal/gdk/authority_intent.schema", + "unreal/gdk/debug_component.schema", + "unreal/gdk/gameplay_debugger_component.schema", + "unreal/gdk/debug_metrics.schema", + "unreal/gdk/net_owning_client_worker.schema", + "unreal/gdk/not_streamed.schema", + "unreal/gdk/query_tags.schema", + "unreal/gdk/relevant.schema", + "unreal/gdk/rpc_components.schema", + "unreal/gdk/spatial_debugging.schema", + "unreal/gdk/spawndata.schema", + "unreal/gdk/tombstone.schema", + "unreal/gdk/unreal_metadata.schema", + "unreal/gdk/actor_group_member.schema", + "unreal/gdk/actor_set_member.schema", + "unreal/gdk/migration_diagnostic.schema", + "unreal/gdk/actor_ownership.schema", + "unreal/generated/rpc_endpoints.schema", + "unreal/generated/NetCullDistance/ncdcomponents.schema", }; const TMap ServerAuthorityWellKnownComponents = { - { POSITION_COMPONENT_ID, "improbable.Position" }, - { INTEREST_COMPONENT_ID, "improbable.Interest" }, - { AUTHORITY_DELEGATION_COMPONENT_ID, "improbable.AuthorityDelegation" }, - { AUTHORITY_INTENT_COMPONENT_ID, "unreal.AuthorityIntent" }, - { GDK_DEBUG_COMPONENT_ID, "unreal.DebugComponent" }, - { DEBUG_METRICS_COMPONENT_ID, "unreal.DebugMetrics" }, - { NET_OWNING_CLIENT_WORKER_COMPONENT_ID, "unreal.NetOwningClientWorker" }, - { NOT_STREAMED_COMPONENT_ID, "unreal.NotStreamed" }, - { ALWAYS_RELEVANT_COMPONENT_ID, "unreal.AlwaysRelevant" }, - { DORMANT_COMPONENT_ID, "unreal.Dormant" }, - { VISIBLE_COMPONENT_ID, "unreal.Visible" }, - { SERVER_TO_SERVER_COMMAND_ENDPOINT_COMPONENT_ID, "unreal.UnrealServerToServerCommandEndpoint" }, - { SPATIAL_DEBUGGING_COMPONENT_ID, "unreal.SpatialDebugging" }, - { SPAWN_DATA_COMPONENT_ID, "unreal.SpawnData" }, - { TOMBSTONE_COMPONENT_ID, "unreal.Tombstone" }, - { UNREAL_METADATA_COMPONENT_ID, "unreal.UnrealMetadata" }, - { ACTOR_GROUP_MEMBER_COMPONENT_ID, "unreal.ActorGroupMember" }, - { ACTOR_SET_MEMBER_COMPONENT_ID, "unreal.ActorSetMember" }, - { SERVER_ENDPOINT_COMPONENT_ID, "unreal.generated.UnrealServerEndpoint" }, - { MULTICAST_RPCS_COMPONENT_ID, "unreal.generated.UnrealMulticastRPCs" }, - { SERVER_ENDPOINT_COMPONENT_ID, "unreal.generated.UnrealServerEndpoint" }, - { CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID, "unreal.generated.UnrealCrossServerSenderRPCs" }, - { CROSSSERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID, "unreal.generated.UnrealCrossServerReceiverACKRPCs" }, + { POSITION_COMPONENT_ID, "improbable.Position" }, + { INTEREST_COMPONENT_ID, "improbable.Interest" }, + { AUTHORITY_DELEGATION_COMPONENT_ID, "improbable.AuthorityDelegation" }, + { AUTHORITY_INTENT_COMPONENT_ID, "unreal.AuthorityIntent" }, + { GDK_DEBUG_COMPONENT_ID, "unreal.DebugComponent" }, + { GDK_GAMEPLAY_DEBUGGER_COMPONENT_ID, "unreal.GameplayDebuggerComponent" }, + { DEBUG_METRICS_COMPONENT_ID, "unreal.DebugMetrics" }, + { NET_OWNING_CLIENT_WORKER_COMPONENT_ID, "unreal.NetOwningClientWorker" }, + { NOT_STREAMED_COMPONENT_ID, "unreal.NotStreamed" }, + { ALWAYS_RELEVANT_COMPONENT_ID, "unreal.AlwaysRelevant" }, + { DORMANT_COMPONENT_ID, "unreal.Dormant" }, + { VISIBLE_COMPONENT_ID, "unreal.Visible" }, + { SERVER_TO_SERVER_COMMAND_ENDPOINT_COMPONENT_ID, "unreal.UnrealServerToServerCommandEndpoint" }, + { SPATIAL_DEBUGGING_COMPONENT_ID, "unreal.SpatialDebugging" }, + { SPAWN_DATA_COMPONENT_ID, "unreal.SpawnData" }, + { TOMBSTONE_COMPONENT_ID, "unreal.Tombstone" }, + { UNREAL_METADATA_COMPONENT_ID, "unreal.UnrealMetadata" }, + { ACTOR_GROUP_MEMBER_COMPONENT_ID, "unreal.ActorGroupMember" }, + { ACTOR_SET_MEMBER_COMPONENT_ID, "unreal.ActorSetMember" }, + { SERVER_ENDPOINT_COMPONENT_ID, "unreal.generated.UnrealServerEndpoint" }, + { MULTICAST_RPCS_COMPONENT_ID, "unreal.generated.UnrealMulticastRPCs" }, + { SERVER_ENDPOINT_COMPONENT_ID, "unreal.generated.UnrealServerEndpoint" }, + { CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID, "unreal.generated.UnrealCrossServerSenderRPCs" }, + { CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID, "unreal.generated.UnrealCrossServerReceiverACKRPCs" }, + { MIGRATION_DIAGNOSTIC_COMPONENT_ID, "unreal.MigrationDiagnostic" }, + { ACTOR_OWNERSHIP_COMPONENT_ID, "unreal.ActorOwnership" }, }; const TArray ClientAuthorityWellKnownSchemaImports = { "unreal/gdk/player_controller.schema", "unreal/gdk/rpc_components.schema", - "unreal/generated/rpc_endpoints.schema" }; + "unreal/generated/rpc_endpoints.schema" }; const TMap ClientAuthorityWellKnownComponents = { - { PLAYER_CONTROLLER_COMPONENT_ID, "unreal.PlayerController" }, - { CLIENT_ENDPOINT_COMPONENT_ID, "unreal.generated.UnrealClientEndpoint" }, + { PLAYER_CONTROLLER_COMPONENT_ID, "unreal.PlayerController" }, + { CLIENT_ENDPOINT_COMPONENT_ID, "unreal.generated.UnrealClientEndpoint" }, }; const TMap RoutingWorkerComponents = { - { CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID, "unreal.generated.UnrealCrossServerSenderACKRPCs" }, - { CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID, "unreal.generated.UnrealCrossServerReceiverRPCs" }, + { CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID, "unreal.generated.UnrealCrossServerSenderACKRPCs" }, + { CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID, "unreal.generated.UnrealCrossServerReceiverRPCs" }, }; const TArray RoutingWorkerSchemaImports = { "unreal/gdk/rpc_components.schema", "unreal/generated/rpc_endpoints.schema" }; const TArray KnownEntityAuthorityComponents = { POSITION_COMPONENT_ID, METADATA_COMPONENT_ID, - INTEREST_COMPONENT_ID, PLAYER_SPAWNER_COMPONENT_ID, - DEPLOYMENT_MAP_COMPONENT_ID, STARTUP_ACTOR_MANAGER_COMPONENT_ID, - GSM_SHUTDOWN_COMPONENT_ID, VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID}; + INTEREST_COMPONENT_ID, PLAYER_SPAWNER_COMPONENT_ID, + DEPLOYMENT_MAP_COMPONENT_ID, STARTUP_ACTOR_MANAGER_COMPONENT_ID, + GSM_SHUTDOWN_COMPONENT_ID, VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID }; + +const TMap WorkerEntityAuthorityComponents = { + { POSITION_COMPONENT_ID, "improbable.Position" }, + { INTEREST_COMPONENT_ID, "improbable.Interest" }, + { SERVER_WORKER_COMPONENT_ID, "unreal.ServerWorker" }, + { AUTHORITY_DELEGATION_COMPONENT_ID, "improbable.AuthorityDelegation" }, + { CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID, "unreal.generated.UnrealCrossServerSenderRPCs" }, + { CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID, "unreal.generated.UnrealCrossServerReceiverACKRPCs" }, +}; -} +const TArray WorkerEntitySchemaImports = { "unreal/gdk/rpc_components.schema", "unreal/generated/rpc_endpoints.schema" }; + +} // namespace SpatialConstants #undef LOCTEXT_NAMESPACE diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h index 7f29298d64..8cbe3cb928 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h @@ -36,7 +36,7 @@ enum ESchemaComponentType : int32 // Properties SCHEMA_Data, // Represents properties being replicated to all workers SCHEMA_OwnerOnly, - SCHEMA_Handover, + SCHEMA_ServerOnly, SCHEMA_InitialOnly, SCHEMA_Count, @@ -84,6 +84,7 @@ const Worker_ComponentId MAX_RESERVED_SPATIAL_SYSTEM_COMPONENT_ID = 100; const Worker_ComponentId SPAWN_DATA_COMPONENT_ID = 9999; const Worker_ComponentId PLAYER_SPAWNER_COMPONENT_ID = 9998; +const Worker_ComponentId GDK_GAMEPLAY_DEBUGGER_COMPONENT_ID = 9997; const Worker_ComponentId UNREAL_METADATA_COMPONENT_ID = 9996; const Worker_ComponentId GDK_DEBUG_COMPONENT_ID = 9995; const Worker_ComponentId DEPLOYMENT_MAP_COMPONENT_ID = 9994; @@ -106,7 +107,7 @@ extern const FString SERVER_AUTH_COMPONENT_SET_NAME; extern const FString CLIENT_AUTH_COMPONENT_SET_NAME; extern const FString DATA_COMPONENT_SET_NAME; extern const FString OWNER_ONLY_COMPONENT_SET_NAME; -extern const FString HANDOVER_COMPONENT_SET_NAME; +extern const FString SERVER_ONLY_COMPONENT_SET_NAME; extern const FString ROUTING_WORKER_COMPONENT_SET_NAME; extern const FString INITIAL_ONLY_COMPONENT_SET_NAME; @@ -120,10 +121,10 @@ const Worker_ComponentId VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID = 9979; const Worker_ComponentId VISIBLE_COMPONENT_ID = 9970; const Worker_ComponentId SERVER_ONLY_ALWAYS_RELEVANT_COMPONENT_ID = 9968; -const Worker_ComponentId CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID = 9960; -const Worker_ComponentId CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID = 9961; -const Worker_ComponentId CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID = 9962; -const Worker_ComponentId CROSSSERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID = 9963; +const Worker_ComponentId CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID = 9960; +const Worker_ComponentId CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID = 9961; +const Worker_ComponentId CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID = 9962; +const Worker_ComponentId CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID = 9963; const Worker_ComponentId CLIENT_ENDPOINT_COMPONENT_ID = 9978; const Worker_ComponentId SERVER_ENDPOINT_COMPONENT_ID = 9977; @@ -139,12 +140,15 @@ const Worker_ComponentId INITIAL_ONLY_PRESENCE_COMPONENT_ID = 9966; const Worker_ComponentId ACTOR_SET_MEMBER_COMPONENT_ID = 9965; const Worker_ComponentId ACTOR_GROUP_MEMBER_COMPONENT_ID = 9964; +const Worker_ComponentId ACTOR_OWNERSHIP_COMPONENT_ID = 9959; + const Worker_ComponentId STARTING_GENERATED_COMPONENT_ID = 10000; // System query tags for entity completeness const Worker_ComponentId FIRST_EC_COMPONENT_ID = 2001; const Worker_ComponentId ACTOR_AUTH_TAG_COMPONENT_ID = 2001; const Worker_ComponentId ACTOR_TAG_COMPONENT_ID = 2002; +const Worker_ComponentId ACTOR_OWNER_ONLY_DATA_TAG_COMPONENT_ID = 2003; const Worker_ComponentId LB_TAG_COMPONENT_ID = 2005; const Worker_ComponentId GDK_KNOWN_ENTITY_TAG_COMPONENT_ID = 2007; @@ -289,8 +293,8 @@ const Schema_FieldId ACTOR_SET_MEMBER_COMPONENT_LEADER_ENTITY_ID = 1; // ActorGroupMember field IDs const Schema_FieldId ACTOR_GROUP_MEMBER_COMPONENT_ACTOR_GROUP_ID = 1; -// Reserved entity IDs expire in 5 minutes, we will refresh them every 3 minutes to be safe. -const float ENTITY_RANGE_EXPIRATION_INTERVAL_SECONDS = 180.0f; +// ActorOwnership field IDs +const Schema_FieldId ACTOR_OWNERSHIP_COMPONENT_OWNER_ACTOR_ID = 1; const float FIRST_COMMAND_RETRY_WAIT_SECONDS = 0.2f; const uint32 MAX_NUMBER_COMMAND_ATTEMPTS = 5u; diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKLoader.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKLoader.h index 81b93a3c34..3bcf8f5feb 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKLoader.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKLoader.h @@ -31,7 +31,7 @@ class FSpatialGDKLoader #if TRACE_LIB_ACTIVE - FString TraceFilePath = Path / TEXT("trace_dynamic.dll"); + FString TraceFilePath = Path / TEXT("legacy_trace_dynamic.dll"); TraceLibraryHandle = FPlatformProcess::GetDllHandle(*TraceFilePath); if (TraceLibraryHandle == nullptr) { diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h index 5d98fbeda7..b75d097aad 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h @@ -70,8 +70,23 @@ class SPATIALGDK_API UEventTracingSamplingSettings : public UObject { GENERATED_BODY() public: + struct TraceQueryDeleter + { + void operator()(Trace_Query* Query) const + { + if (Query != nullptr) + { + Trace_Query_Destroy(Query); + } + } + }; + + using TraceQueryPtr = TUniquePtr; + + UEventTracingSamplingSettings(); + UPROPERTY(EditAnywhere, Category = "Event Tracing", meta = (ClampMin = 0.0f, ClampMax = 1.0f)) - double SamplingProbability = 1.0f; + double SamplingProbability; UPROPERTY(EditAnywhere, Category = "Event Tracing") TMap EventSamplingModeOverrides; @@ -90,9 +105,25 @@ class SPATIALGDK_API UEventTracingSamplingSettings : public UObject UPROPERTY(EditAnywhere, Category = "Event Tracing") FString RuntimeEventPostFilter; + const FString& GetGDKEventPreFilterString() const { return GetFilterString(GDKEventPreFilter); } + const FString& GetGDKEventPostFilterString() const { return GetFilterString(GDKEventPostFilter); } + const FString& GetRuntimeEventPreFilterString() const { return GetFilterString(RuntimeEventPreFilter); } + const FString& GetRuntimeEventPostFilterString() const { return GetFilterString(RuntimeEventPostFilter); } + + TraceQueryPtr GetGDKEventPreFilter() const { return ParseOrDefault(GDKEventPreFilter, TEXT("gdk-pre-filter")); } + TraceQueryPtr GetGDKEventPostFilter() const { return ParseOrDefault(GDKEventPostFilter, TEXT("gdk-post-filter")); } + TraceQueryPtr GetRuntimeEventPreFilter() const { return ParseOrDefault(RuntimeEventPreFilter, TEXT("runtime-pre-filter")); } + TraceQueryPtr GetRuntimeEventPostFilter() const { return ParseOrDefault(RuntimeEventPostFilter, TEXT("runtime-post-filter")); } + #if WITH_EDITOR virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override; #endif +private: + static FString DefaultFilter; + + static bool IsFilterValid(const FString& Str); + static TraceQueryPtr ParseOrDefault(const FString& Str, const TCHAR* FilterForLog); + static const FString& GetFilterString(const FString& Filter); }; UCLASS(config = SpatialGDKSettings, defaultconfig) diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandRetryHandler.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandRetryHandler.h index 5c71bbdf71..f3706b205d 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandRetryHandler.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandRetryHandler.h @@ -1,7 +1,8 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved #pragma once +#include "SpatialConstants.h" #include "SpatialView/OpList/OpList.h" #include "SpatialView/WorkerView.h" diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/EntityComponentOpList.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/EntityComponentOpList.h index 48c3d75668..04588b25ed 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/EntityComponentOpList.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/EntityComponentOpList.h @@ -34,9 +34,6 @@ class EntityComponentOpListBuilder public: EntityComponentOpListBuilder(); - // MoveTemp leaves the builder in an undefined state. This op allows the current builder to be reused. - EntityComponentOpListBuilder Move(); - EntityComponentOpListBuilder& AddEntity(Worker_EntityId EntityId); EntityComponentOpListBuilder& RemoveEntity(Worker_EntityId EntityId); EntityComponentOpListBuilder& AddComponent(Worker_EntityId EntityId, ComponentData Data); @@ -45,6 +42,8 @@ class EntityComponentOpListBuilder EntityComponentOpListBuilder& SetAuthority(Worker_EntityId EntityId, Worker_ComponentSetId ComponentSetId, Worker_Authority Authority, TArray Components); EntityComponentOpListBuilder& SetDisconnect(Worker_ConnectionStatusCode StatusCode, StringStorage DisconnectReason); + EntityComponentOpListBuilder& StartCriticalSection(); + EntityComponentOpListBuilder& EndCriticalSection(); EntityComponentOpListBuilder& AddCreateEntityCommandResponse(Worker_EntityId EntityID, Worker_RequestId RequestId, Worker_StatusCode StatusCode, StringStorage Message); EntityComponentOpListBuilder& AddEntityQueryCommandResponse(Worker_RequestId RequestId, TArray Results, diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/StringStorage.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/StringStorage.h index 6707236410..d60ebc3b06 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/StringStorage.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/StringStorage.h @@ -10,6 +10,7 @@ namespace SpatialGDK { +/** Stores a utf8 string. */ class StringStorage { public: diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/SpatialOSWorker.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/SpatialOSWorker.h new file mode 100644 index 0000000000..f5ef19714e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/SpatialOSWorker.h @@ -0,0 +1,52 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once +#include "Interop/Connection/SpatialGDKSpanId.h" +#include "SpatialView/CommandRequest.h" +#include "SpatialView/CommandResponse.h" +#include "SpatialView/CommandRetryHandler.h" +#include "SpatialView/ComponentData.h" +#include "SpatialView/ComponentUpdate.h" +#include "SpatialView/EntityQuery.h" +#include "SpatialView/ViewDelta.h" + +namespace SpatialGDK +{ +class SPATIALGDK_API ISpatialOSWorker +{ +public: + virtual ~ISpatialOSWorker() = default; + + virtual const TArray& GetEntityDeltas() const = 0; + virtual const TArray& GetWorkerMessages() const = 0; + virtual const EntityView& GetView() const = 0; + + virtual const FString& GetWorkerId() const = 0; + virtual Worker_EntityId GetWorkerSystemEntityId() const = 0; + + virtual const ComponentData* GetComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const = 0; + + virtual void SendAddComponent(Worker_EntityId EntityId, ComponentData Data, const FSpatialGDKSpanId& SpanId = {}) = 0; + virtual void SendComponentUpdate(Worker_EntityId EntityId, ComponentUpdate Update, const FSpatialGDKSpanId& SpanId = {}) = 0; + virtual void SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, const FSpatialGDKSpanId& SpanId = {}) = 0; + + virtual Worker_RequestId SendEntityCommandRequest(Worker_EntityId EntityId, CommandRequest Request, FRetryData RetryData = NO_RETRIES, + const FSpatialGDKSpanId& SpanId = {}) = 0; + virtual void SendEntityCommandResponse(Worker_RequestId RequestId, CommandResponse Response, const FSpatialGDKSpanId& SpanId = {}) = 0; + virtual void SendEntityCommandFailure(Worker_RequestId RequestId, FString Message, const FSpatialGDKSpanId& SpanId = {}) = 0; + + virtual Worker_RequestId SendReserveEntityIdsRequest(uint32 NumberOfEntityIds, FRetryData RetryData = NO_RETRIES) = 0; + virtual Worker_RequestId SendCreateEntityRequest(TArray EntityComponents, TOptional EntityId, + FRetryData RetryData = NO_RETRIES, const FSpatialGDKSpanId& SpanId = {}) = 0; + virtual Worker_RequestId SendDeleteEntityRequest(Worker_EntityId EntityId, FRetryData RetryData = NO_RETRIES, + const FSpatialGDKSpanId& SpanId = {}) = 0; + virtual Worker_RequestId SendEntityQueryRequest(EntityQuery Query, FRetryData RetryData = NO_RETRIES) = 0; + + virtual void SendMetrics(SpatialMetrics Metrics) = 0; + virtual void SendLogMessage(Worker_LogLevel Level, const FName& LoggerName, FString Message) = 0; + + virtual bool HasEntity(Worker_EntityId EntityId) const = 0; + virtual bool HasComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const = 0; + virtual bool HasAuthority(Worker_EntityId EntityId, Worker_ComponentSetId ComponentSetId) const = 0; +}; +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/SubView.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/SubView.h index e570572285..6680bf207b 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/SubView.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/SubView.h @@ -40,6 +40,8 @@ class FSubView void Advance(const ViewDelta& Delta); const FSubViewDelta& GetViewDelta() const; + const TArray& GetCompleteEntities() const; + void Refresh(); void RefreshEntity(const Worker_EntityId EntityId); const EntityView& GetView() const; diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewCoordinator.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewCoordinator.h index ba34cf17a1..7e7eb2c510 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewCoordinator.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewCoordinator.h @@ -8,6 +8,7 @@ #include "SpatialView/CriticalSectionFilter.h" #include "SpatialView/Dispatcher.h" #include "SpatialView/ReceivedOpEventHandler.h" +#include "SpatialView/SpatialOSWorker.h" #include "SpatialView/SubView.h" #include "SpatialView/WorkerView.h" @@ -17,7 +18,7 @@ namespace SpatialGDK { class SpatialEventTracer; -class ViewCoordinator +class ViewCoordinator : public ISpatialOSWorker { public: explicit ViewCoordinator(TUniquePtr ConnectionHandler, TSharedPtr EventTracer, @@ -33,7 +34,6 @@ class ViewCoordinator void Advance(float DeltaTimeS); const ViewDelta& GetViewDelta() const; - const EntityView& GetView() const; void FlushMessagesToSend(); // Create a subview with the specified tag, filter, and refresh callbacks. @@ -49,34 +49,40 @@ class ViewCoordinator // In the future when there could be an unbounded number of user systems this should probably be revisited. void RefreshEntityCompleteness(Worker_EntityId EntityId); - const FString& GetWorkerId() const; - Worker_EntityId GetWorkerSystemEntityId() const; - - const ComponentData* GetComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const; - - void SendAddComponent(Worker_EntityId EntityId, ComponentData Data, const FSpatialGDKSpanId& SpanId); - void SendComponentUpdate(Worker_EntityId EntityId, ComponentUpdate Update, const FSpatialGDKSpanId& SpanId); - void SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, const FSpatialGDKSpanId& SpanId); - Worker_RequestId SendReserveEntityIdsRequest(uint32 NumberOfEntityIds, TOptional TimeoutMillis = {}); - Worker_RequestId SendCreateEntityRequest(TArray EntityComponents, TOptional EntityId, - TOptional TimeoutMillis = {}, const FSpatialGDKSpanId& SpanId = {}); - Worker_RequestId SendDeleteEntityRequest(Worker_EntityId EntityId, TOptional TimeoutMillis = {}, - const FSpatialGDKSpanId& SpanId = {}); - Worker_RequestId SendEntityQueryRequest(EntityQuery Query, TOptional TimeoutMillis = {}); - Worker_RequestId SendEntityCommandRequest(Worker_EntityId EntityId, CommandRequest Request, TOptional TimeoutMillis = {}, - const FSpatialGDKSpanId& SpanId = {}); - void SendEntityCommandResponse(Worker_RequestId RequestId, CommandResponse Response, const FSpatialGDKSpanId& SpanId); - void SendEntityCommandFailure(Worker_RequestId RequestId, FString Message, const FSpatialGDKSpanId& SpanId); - void SendMetrics(SpatialMetrics Metrics); - void SendLogMessage(Worker_LogLevel Level, const FName& LoggerName, FString Message); - - Worker_RequestId SendReserveEntityIdsRequest(uint32 NumberOfEntityIds, FRetryData RetryData); - Worker_RequestId SendCreateEntityRequest(TArray EntityComponents, TOptional EntityId, - FRetryData RetryData, const FSpatialGDKSpanId& SpanId); - Worker_RequestId SendDeleteEntityRequest(Worker_EntityId EntityId, FRetryData RetryData, const FSpatialGDKSpanId& SpanId); - Worker_RequestId SendEntityQueryRequest(EntityQuery Query, FRetryData RetryData); - Worker_RequestId SendEntityCommandRequest(Worker_EntityId EntityId, CommandRequest Request, FRetryData RetryData, - const FSpatialGDKSpanId& SpanId); + virtual const TArray& GetEntityDeltas() const override; + virtual const TArray& GetWorkerMessages() const override; + virtual const EntityView& GetView() const override; + + virtual const FString& GetWorkerId() const override; + + virtual Worker_EntityId GetWorkerSystemEntityId() const override; + + virtual const ComponentData* GetComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const override; + + virtual void SendAddComponent(Worker_EntityId EntityId, ComponentData Data, const FSpatialGDKSpanId& SpanId = {}) override; + virtual void SendComponentUpdate(Worker_EntityId EntityId, ComponentUpdate Update, const FSpatialGDKSpanId& SpanId = {}) override; + virtual void SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, + const FSpatialGDKSpanId& SpanId = {}) override; + + virtual Worker_RequestId SendEntityCommandRequest(Worker_EntityId EntityId, CommandRequest Request, FRetryData RetryData = NO_RETRIES, + const FSpatialGDKSpanId& SpanId = {}) override; + virtual void SendEntityCommandResponse(Worker_RequestId RequestId, CommandResponse Response, + const FSpatialGDKSpanId& SpanId = {}) override; + virtual void SendEntityCommandFailure(Worker_RequestId RequestId, FString Message, const FSpatialGDKSpanId& SpanId = {}) override; + + virtual Worker_RequestId SendReserveEntityIdsRequest(uint32 NumberOfEntityIds, FRetryData RetryData = NO_RETRIES) override; + virtual Worker_RequestId SendCreateEntityRequest(TArray EntityComponents, TOptional EntityId, + FRetryData RetryData = NO_RETRIES, const FSpatialGDKSpanId& SpanId = {}) override; + virtual Worker_RequestId SendDeleteEntityRequest(Worker_EntityId EntityId, FRetryData RetryData = NO_RETRIES, + const FSpatialGDKSpanId& SpanId = {}) override; + virtual Worker_RequestId SendEntityQueryRequest(EntityQuery Query, FRetryData RetryData = NO_RETRIES) override; + + virtual void SendMetrics(SpatialMetrics Metrics) override; + virtual void SendLogMessage(Worker_LogLevel Level, const FName& LoggerName, FString Message) override; + + virtual bool HasEntity(Worker_EntityId EntityId) const override; + virtual bool HasComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const override; + virtual bool HasAuthority(Worker_EntityId EntityId, Worker_ComponentSetId ComponentSetId) const override; CallbackId RegisterComponentAddedCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback); CallbackId RegisterComponentRemovedCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback); @@ -96,10 +102,6 @@ class ViewCoordinator Worker_ComponentId ComponentId, const FAuthorityChangeRefreshPredicate& RefreshPredicate = FSubView::NoAuthorityChangeRefreshPredicate); - bool HasEntity(Worker_EntityId EntityId) const; - bool HasComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const; - bool HasAuthority(Worker_EntityId EntityId, Worker_ComponentSetId ComponentSetId) const; - private: WorkerView View; TUniquePtr ConnectionHandler; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/TargetView.h b/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/TargetView.h new file mode 100644 index 0000000000..aa20f3ca3f --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/TargetView.h @@ -0,0 +1,93 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialView/EntityView.h" +#include "SpatialView/MessagesToSend.h" +#include "SpatialView/OpList/EntityComponentOpList.h" + +namespace SpatialGDK +{ +/** + * Generates OpLists to describe changes to a View. + * No further changes can be made after calling Disconnect. + * Functions that add things to the view will add the entity if it does not already exist. + * Functions that remove or modify things require that thing to have previously existed. + * Functions that add command requests and responses have no entity prerequisites. + */ +class FTargetView +{ +public: + /** + * Adds an entity to the view. + * Requires the entity to not be in the view. + */ + void AddEntity(Worker_EntityId EntityId); + + /** + * Removes an entity from the view. + * Requires the entity to be in the view. + * Removes present authority and components. + */ + void RemoveEntity(Worker_EntityId EntityId); + + /** + * Add component to the view, or set all values if the component already exists. + * Adds the entity to the view if it does not exist. + */ + void AddOrSetComponent(Worker_EntityId EntityId, ComponentData Data); + + /** + * Applies an update to an entity-component in the view. + * Requires the entity-component to be in the view. + */ + void UpdateComponent(Worker_EntityId EntityId, ComponentUpdate Update); + + /** + * Removes an entity-component from the view. + * Requires the entity-component to be in the view. + */ + void RemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + + /** + * Adds authority over an entity-component-set to the view. + * Adds the entity to the view if it does not exist. + */ + void AddAuthority(Worker_EntityId EntityId, Worker_ComponentSetId ComponentSetId); + + /** + * Removes authority over an entity-component-set to the view. + * Requires authority over an entity-component-set. + */ + void RemoveAuthority(Worker_EntityId EntityId, Worker_ComponentSetId ComponentSetId); + + /** + * Sets the view as disconnected. No other further changes can be made after calling this. + */ + void Disconnect(Worker_ConnectionStatusCode StatusCode, StringStorage DisconnectReason); + + void AddCreateEntityCommandResponse(Worker_EntityId EntityId, Worker_RequestId RequestId, Worker_StatusCode StatusCode, + StringStorage Message); + void AddEntityQueryCommandResponse(Worker_RequestId RequestId, TArray Results, Worker_StatusCode StatusCode, + StringStorage Message); + void AddEntityCommandRequest(Worker_EntityId EntityId, Worker_RequestId RequestId, CommandRequest CommandRequest); + void AddEntityCommandResponse(Worker_EntityId EntityId, Worker_RequestId RequestId, Worker_StatusCode StatusCode, + StringStorage Message); + void AddDeleteEntityCommandResponse(Worker_EntityId EntityId, Worker_RequestId RequestId, Worker_StatusCode StatusCode, + StringStorage Message); + void AddReserveEntityIdsCommandResponse(Worker_EntityId EntityId, uint32 NumberOfEntities, Worker_RequestId RequestId, + Worker_StatusCode StatusCode, StringStorage Message); + + /** Get the current state of the view. */ + const EntityView& GetView() const; + + /** Create an OpList representing the changes made to the view since the last call. */ + OpList CreateOpListFromChanges(); + +private: + EntityView View; + EntityComponentOpListBuilder Builder; + bool bDisconnected = false; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/TestConnectionHandler.h b/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/TestConnectionHandler.h new file mode 100644 index 0000000000..2d1d0852a2 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/TestConnectionHandler.h @@ -0,0 +1,59 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialView/ConnectionHandler/AbstractConnectionHandler.h" +#include "SpatialView/WorkerView.h" + +namespace SpatialGDK +{ +using FSendMessageCallback = TUniqueFunction)>; + +class FTestConnectionHandler : public AbstractConnectionHandler +{ +public: + using FSendMessageCallback = TUniqueFunction)>; + + explicit FTestConnectionHandler(FString InWorkerId = {}, Worker_EntityId InWorkerSystemEntityId = 0) + : WorkerId(InWorkerId) + , WorkerEntityId(InWorkerSystemEntityId) + { + } + + virtual void Advance() override {} + + virtual uint32 GetOpListCount() override { return PendingOpLists.Num(); } + + virtual OpList GetNextOpList() override + { + if (PendingOpLists.Num() == 0) + { + return {}; + } + OpList Temp = MoveTemp(PendingOpLists[0]); + PendingOpLists.RemoveAt(0); + return Temp; + } + + virtual void SendMessages(TUniquePtr Messages) override + { + if (SendMessageCallback) + { + SendMessageCallback(MoveTemp(Messages)); + } + } + + virtual const FString& GetWorkerId() const override { return WorkerId; } + + virtual Worker_EntityId GetWorkerSystemEntityId() const override { return WorkerEntityId; } + + void SetSendMessageCallback(FSendMessageCallback InSendMessageCallback) { SendMessageCallback = MoveTemp(InSendMessageCallback); } + void AddOpList(OpList Ops) { PendingOpLists.Add(MoveTemp(Ops)); } + +private: + FString WorkerId; + Worker_EntityId WorkerEntityId; + FSendMessageCallback SendMessageCallback; + TArray PendingOpLists; +}; +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/TestWorker.h b/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/TestWorker.h new file mode 100644 index 0000000000..31b2e6a4fc --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/TestWorker.h @@ -0,0 +1,54 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialView/ViewCoordinator.h" +#include "TestConnectionHandler.h" +#include "Tests/SpatialView/TargetView.h" + +namespace SpatialGDK +{ +class FTestWorker +{ +public: + static FTestWorker Create(TArray ComponentSetIds, FString InWorkerId = {}, + Worker_EntityId InWorkerSystemEntityId = 0) + { + TUniquePtr Handler = MakeUnique(InWorkerId, InWorkerSystemEntityId); + FComponentSetData SetData; + for (Worker_ComponentSetId SetId : ComponentSetIds) + { + SetData.ComponentSets.Add(SetId, {}); + } + return FTestWorker(MoveTemp(Handler), MoveTemp(SetData)); + } + + void SetSendMessageCallback(FSendMessageCallback InSendMessageCallback) + { + ConnectionHandler->SetSendMessageCallback(MoveTemp(InSendMessageCallback)); + } + + FTargetView& GetTargetView() { return TargetView; } + + void AdvanceToTargetView(float DeltaTimeS) + { + ConnectionHandler->AddOpList(TargetView.CreateOpListFromChanges()); + Coordinator.Advance(DeltaTimeS); + } + + ViewCoordinator& GetCoordinator() { return Coordinator; } + const ViewCoordinator& GetCoordinator() const { return Coordinator; } + +private: + explicit FTestWorker(TUniquePtr Handler, FComponentSetData SetData) + : ConnectionHandler(Handler.Get()) + , Coordinator(MoveTemp(Handler), /*EventTracer =*/nullptr, MoveTemp(SetData)) + { + } + + FTestConnectionHandler* ConnectionHandler; + FTargetView TargetView; + ViewCoordinator Coordinator; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentFactory.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentFactory.h index 14b2cdbe17..6515ba7677 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentFactory.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentFactory.h @@ -27,16 +27,12 @@ namespace SpatialGDK class SPATIALGDK_API ComponentFactory { public: - ComponentFactory(bool bInterestDirty, USpatialNetDriver* InNetDriver, USpatialLatencyTracer* LatencyTracer); + ComponentFactory(bool bInterestDirty, USpatialNetDriver* InNetDriver); TArray CreateComponentDatas(UObject* Object, const FClassInfo& Info, const FRepChangeState& RepChangeState, - const FHandoverChangeState& HandoverChangeState, uint32& OutBytesWritten); + uint32& OutBytesWritten); TArray CreateComponentUpdates(UObject* Object, const FClassInfo& Info, Worker_EntityId EntityId, - const FRepChangeState* RepChangeState, - const FHandoverChangeState* HandoverChangeState, uint32& OutBytesWritten); - - FWorkerComponentData CreateHandoverComponentData(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, - const FHandoverChangeState& Changes, uint32& OutBytesWritten); + const FRepChangeState* RepChangeState, uint32& OutBytesWritten); bool WasInitialOnlyDataWritten() const { return bInitialOnlyDataWritten; } @@ -49,15 +45,7 @@ class SPATIALGDK_API ComponentFactory ESchemaComponentType PropertyGroup, uint32& OutBytesWritten); uint32 FillSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FRepChangeState& Changes, - ESchemaComponentType PropertyGroup, bool bIsInitialData, TraceKey* OutLatencyTraceId, - TArray* ClearedIds = nullptr); - - FWorkerComponentUpdate CreateHandoverComponentUpdate(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, - const FHandoverChangeState& Changes, uint32& OutBytesWritten); - - uint32 FillHandoverSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FClassInfo& Info, - const FHandoverChangeState& Changes, bool bIsInitialData, TraceKey* OutLatencyTraceId, - TArray* ClearedIds = nullptr); + ESchemaComponentType PropertyGroup, bool bIsInitialData, TArray* ClearedIds = nullptr); void AddProperty(Schema_Object* Object, Schema_FieldId FieldId, GDK_PROPERTY(Property) * Property, const uint8* Data, TArray* ClearedIds); @@ -69,8 +57,6 @@ class SPATIALGDK_API ComponentFactory bool bInterestHasChanged; bool bInitialOnlyDataWritten; bool bInitialOnlyReplicationEnabled; - - USpatialLatencyTracer* LatencyTracer; }; } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentReader.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentReader.h index 67729bb424..2a6582a191 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentReader.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentReader.h @@ -18,18 +18,16 @@ class ComponentReader explicit ComponentReader(class USpatialNetDriver* InNetDriver, FObjectReferencesMap& InObjectReferencesMap, SpatialEventTracer* InEventTracer); - void ApplyComponentData(const Worker_ComponentData& ComponentData, UObject& Object, USpatialActorChannel& Channel, bool bIsHandover, + void ApplyComponentData(const Worker_ComponentData& ComponentData, UObject& Object, USpatialActorChannel& Channel, bool& bOutReferencesChanged); void ApplyComponentData(Worker_ComponentId ComponentId, Schema_ComponentData* Data, UObject& Object, USpatialActorChannel& Channel, - bool bIsHandover, bool& bOutReferencesChanged); + bool& bOutReferencesChanged); void ApplyComponentUpdate(Worker_ComponentId ComponentId, Schema_ComponentUpdate* Update, UObject& Object, - USpatialActorChannel& Channel, bool bIsHandover, bool& bOutReferencesChanged); + USpatialActorChannel& Channel, bool& bOutReferencesChanged); private: void ApplySchemaObject(Schema_Object* ComponentObject, UObject& Object, USpatialActorChannel& Channel, bool bIsInitialData, const TArray& UpdatedIds, Worker_ComponentId ComponentId, bool& bOutReferencesChanged); - void ApplyHandoverSchemaObject(Schema_Object* ComponentObject, UObject& Object, USpatialActorChannel& Channel, bool bIsInitialData, - const TArray& UpdatedIds, Worker_ComponentId ComponentId, bool& bOutReferencesChanged); void ApplyProperty(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, uint32 Index, GDK_PROPERTY(Property) * Property, uint8* Data, int32 Offset, int32 CmdIndex, int32 ParentIndex, diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/EngineVersionCheck.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/EngineVersionCheck.h index 105ecb19d4..b975a79258 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/EngineVersionCheck.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/EngineVersionCheck.h @@ -7,7 +7,7 @@ // GDK Version to be updated with SPATIAL_ENGINE_VERSION // when breaking changes are made to the engine that requires // changes to the GDK to remain compatible -#define SPATIAL_GDK_VERSION 39 +#define SPATIAL_GDK_VERSION 42 // Check if GDK is compatible with the current version of Unreal Engine // SPATIAL_ENGINE_VERSION is incremented in engine when breaking changes diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityPool.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityPool.h index ec6767e680..4127b3b9ba 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityPool.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityPool.h @@ -18,12 +18,9 @@ struct EntityRange { Worker_EntityId CurrentEntityId; Worker_EntityId LastEntityId; - bool bExpired; - uint32 EntityRangeId; // Used to identify an entity range when it has expired. }; class USpatialReceiver; -class FTimerManager; DECLARE_LOG_CATEGORY_EXTERN(LogSpatialEntityPool, Log, All) @@ -35,7 +32,7 @@ class SPATIALGDK_API UEntityPool : public UObject GENERATED_BODY() public: - void Init(USpatialNetDriver* InNetDriver, FTimerManager* TimerManager); + void Init(USpatialNetDriver& InNetDriver); void ReserveEntityIDs(uint32 EntitiesToReserve); Worker_EntityId GetNextEntityId(); FEntityPoolReadyEvent& GetEntityPoolReadyDelegate(); @@ -45,18 +42,13 @@ class SPATIALGDK_API UEntityPool : public UObject void Advance(); private: - void OnEntityRangeExpired(uint32 ExpiringEntityRangeId); - UPROPERTY() USpatialNetDriver* NetDriver; - FTimerManager* TimerManager; TArray ReservedEntityIDRanges; - bool bIsReady; - bool bIsAwaitingResponse; - - uint32 NextEntityRangeId; + bool bIsReady = false; + bool bIsAwaitingResponse = false; FEntityPoolReadyEvent EntityPoolReadyDelegate; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/InterestFactory.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/InterestFactory.h index b6c4f6a34c..54e7373274 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/InterestFactory.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/InterestFactory.h @@ -30,6 +30,10 @@ * made by that server worker. */ +#if WITH_GAMEPLAY_DEBUGGER +class AGameplayDebuggerCategoryReplicator; +#endif + class UAbstractLBStrategy; class USpatialClassInfoManager; class USpatialPackageMapClient; @@ -68,17 +72,24 @@ class SPATIALGDK_API InterestFactory Interest CreateInterest(AActor* InActor, const FClassInfo& InInfo, const Worker_EntityId InEntityId) const; // Defined Constraint AND Level Constraint - void AddPlayerControllerActorInterest(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo) const; + void AddClientPlayerControllerActorInterest(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo) const; +#if WITH_GAMEPLAY_DEBUGGER + // Entity ID query for the player controller responsible for the replicator + void AddServerGameplayDebuggerCategoryReplicatorActorInterest(Interest& OutInterest, + const AGameplayDebuggerCategoryReplicator& Replicator) const; +#endif // The components clients need to see on entities they have authority over that they don't already see through authority. void AddClientSelfInterest(Interest& OutInterest) const; // The components servers need to see on entities they have authority over that they don't already see through authority. void AddServerSelfInterest(Interest& OutInterest) const; // Add interest to the actor's owner. - void AddOwnerInterestOnServer(Interest& OutInterest, const AActor* InActor, const Worker_EntityId& EntityId) const; + void AddServerActorOwnerInterest(Interest& OutInterest, const AActor* InActor, const Worker_EntityId& EntityId) const; // Add the always relevant and the always interested query. - void AddAlwaysRelevantAndInterestedQuery(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo, - const QueryConstraint& LevelConstraint) const; + void AddClientAlwaysRelevantQuery(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo, + const QueryConstraint& LevelConstraint) const; + + void AddAlwaysInterestedInterest(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo) const; void AddUserDefinedQueries(Interest& OutInterest, const AActor* InActor, const QueryConstraint& LevelConstraint) const; FrequencyToConstraintsMap GetUserDefinedFrequencyToConstraintsMap(const AActor* InActor) const; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/RepDataUtils.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/RepDataUtils.h index 14a07c7d28..4ebf497308 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/RepDataUtils.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/RepDataUtils.h @@ -13,5 +13,4 @@ struct FRepChangeState FRepLayout& RepLayout; }; -using FHandoverChangeState = TArray; // changed handover properties using FInterestChangeState = TArray; // changed interest properties diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/RepLayoutUtils.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/RepLayoutUtils.h index 22bd8c32a9..2ba479229d 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/RepLayoutUtils.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/RepLayoutUtils.h @@ -170,8 +170,8 @@ inline TArray GetClassRPCFunctions(const UClass* Class) EFieldIteratorFlags::IncludeInterfaces); for (; RemoteFunction; ++RemoteFunction) { - if (RemoteFunction->FunctionFlags & FUNC_NetClient || RemoteFunction->FunctionFlags & FUNC_NetServer - || RemoteFunction->FunctionFlags & FUNC_NetCrossServer || RemoteFunction->FunctionFlags & FUNC_NetMulticast) + if (RemoteFunction->HasAnyFunctionFlags(FUNC_NetClient | FUNC_NetServer | FUNC_NetCrossServer | FUNC_NetMulticast + | FUNC_NetWriteFence)) { AllClassFunctions.Add(*RemoteFunction); } diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaDatabase.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaDatabase.h index 597bef061d..4249c55862 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaDatabase.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaDatabase.h @@ -101,6 +101,7 @@ enum class ESchemaDatabaseVersion : uint8 AlwaysWriteRPCAdded, InitialOnlyDataAdded, FieldIDsAdded, + HandoverToServerOnlyChanged, // Add new versions here diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialActorUtils.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialActorUtils.h index b0aaca41f2..912aa22e0a 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialActorUtils.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialActorUtils.h @@ -2,19 +2,21 @@ #pragma once +#include "Algo/Copy.h" #include "EngineClasses/SpatialNetConnection.h" #include "EngineClasses/SpatialNetDriver.h" #include "Components/SceneComponent.h" #include "Containers/UnrealString.h" #include "Engine/EngineTypes.h" +#include "EngineClasses/SpatialPackageMapClient.h" #include "GameFramework/Actor.h" #include "GameFramework/Controller.h" #include "GameFramework/GameMode.h" #include "GameFramework/PlayerController.h" #include "Math/Vector.h" -#if WITH_UNREAL_DEVELOPER_TOOLS || (!UE_BUILD_SHIPPING && !UE_BUILD_TEST) +#if WITH_GAMEPLAY_DEBUGGER #include "GameplayDebuggerCategoryReplicator.h" #endif @@ -22,7 +24,10 @@ namespace SpatialGDK { inline AActor* GetTopmostReplicatedOwner(const AActor* Actor) { - check(Actor != nullptr); + if (!ensureAlwaysMsgf(Actor != nullptr, TEXT("Called GetTopmostReplicatedOwner for nullptr Actor"))) + { + return nullptr; + } AActor* Owner = Actor->GetOwner(); if (Owner == nullptr || Owner->IsPendingKillPending() || !Owner->GetIsReplicated()) @@ -105,7 +110,7 @@ inline FVector GetActorSpatialPosition(const AActor* InActor) inline bool DoesActorClassIgnoreVisibilityCheck(AActor* InActor) { if (InActor->IsA(APlayerController::StaticClass()) || InActor->IsA(AGameModeBase::StaticClass()) -#if WITH_UNREAL_DEVELOPER_TOOLS || (!UE_BUILD_SHIPPING && !UE_BUILD_TEST) +#if WITH_GAMEPLAY_DEBUGGER || InActor->IsA(AGameplayDebuggerCategoryReplicator::StaticClass()) #endif ) @@ -127,4 +132,67 @@ inline bool ShouldActorHaveVisibleComponent(AActor* InActor) return false; } +inline bool IsDynamicSubObject(const USpatialNetDriver& NetDriver, const AActor& Actor, const ObjectOffset SubObjectOffset) +{ + const FClassInfo& ActorClassInfo = NetDriver.ClassInfoManager->GetOrCreateClassInfoByClass(Actor.GetClass()); + return !ActorClassInfo.SubobjectInfo.Contains(SubObjectOffset); +} + +using FSubobjectToOffsetMap = TMap; +static FSubobjectToOffsetMap CreateStaticOffsetMapFromActor(AActor& Actor, const FClassInfo& ActorInfo) +{ + FSubobjectToOffsetMap SubobjectNameToOffset; + + for (auto& SubobjectOffsetToInfoPair : ActorInfo.SubobjectInfo) + { + UObject* Subobject = StaticFindObjectFast(UObject::StaticClass(), &Actor, SubobjectOffsetToInfoPair.Value->SubobjectName); + const ObjectOffset Offset = SubobjectOffsetToInfoPair.Key; + + if (Subobject != nullptr && Subobject->IsPendingKill() == false && Subobject->IsSupportedForNetworking()) + { + SubobjectNameToOffset.Add(Subobject, Offset); + } + } + + return SubobjectNameToOffset; +} + +static FSubobjectToOffsetMap CreateOffsetMapFromActor(USpatialPackageMapClient& PackageMap, AActor& Actor, const FClassInfo& ActorInfo) +{ + FSubobjectToOffsetMap SubobjectNameToOffset = CreateStaticOffsetMapFromActor(Actor, ActorInfo); + + if (Actor.GetInstanceComponents().Num() > 0) + { + // Process components attached to this object; this allows us to join up + // server- and client-side components added in the level. + TArray ActorInstanceComponents; + + // In non-editor builds, editor-only components can be allocated a slot in the array, but left as nullptrs. + Algo::CopyIf(Actor.GetInstanceComponents(), ActorInstanceComponents, [](UActorComponent* Component) -> bool { + return IsValid(Component); + }); + // These need to be ordered in case there are more than one component of the same type, or + // we may end up with wrong component instances having associations between them. + ActorInstanceComponents.Sort([](const UActorComponent& Lhs, const UActorComponent& Rhs) -> bool { + return Lhs.GetName().Compare(Rhs.GetName()) < 0; + }); + + for (UActorComponent* DynamicComponent : ActorInstanceComponents) + { + if (!DynamicComponent->IsSupportedForNetworking()) + { + continue; + } + + const FClassInfo* DynamicComponentClassInfo = PackageMap.TryResolveNewDynamicSubobjectAndGetClassInfo(DynamicComponent); + + if (DynamicComponentClassInfo != nullptr) + { + SubobjectNameToOffset.Add(DynamicComponent, DynamicComponentClassInfo->SchemaComponents[SCHEMA_Data]); + } + } + } + + return SubobjectNameToOffset; +} } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyTracer.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyTracer.h index 7581b8d30e..ea4153b352 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyTracer.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyTracer.h @@ -11,8 +11,8 @@ #include "Utils/GDKPropertyMacros.h" #if TRACE_LIB_ACTIVE -#include -#endif +#include +#endif // TRACE_LIB_ACTIVE #include "SpatialLatencyTracer.generated.h" @@ -51,23 +51,20 @@ class SPATIALGDK_API USpatialLatencyTracer : public UObject // // EXPERIMENTAL: We do not support this functionality currently: Do not use it unless you are Improbable staff. // - // USpatialLatencyTracer allows for tracing of gameplay events across multiple workers, from their user - // instigation, to their observed results. Each of these multi-worker events are tracked through `traces` - // which allow the user to see collected timings of these events in a single location. Key timings related + // `USpatialLatencyTracer` allows you to trace gameplay events across multiple workers, from their user + // instigation to their observed results. Each of these multi-worker events are tracked through `traces`, + // which allow you to see collected timings of these events in a single location. Key timings related // to these events are logged throughout the Unreal GDK networking stack. This API makes the assumption // that the distributed workers have had their clocks synced by some time syncing protocol (eg. NTP). To // give accurate timings, the trace payload is embedded directly within the relevant networking component - // updates. This framework also assumes that the worker that calls BeginLatencyTrace will also eventually - // call EndLatencyTrace on the trace. This allows accurate end-to-end timings. + // updates. This framework also assumes that the worker that calls `BeginLatencyTrace` will also eventually + // call `EndLatencyTrace` on the trace. This allows accurate end-to-end timings. // - // These timings are logged to Google's Stackdriver (https://cloud.google.com/stackdriver/) + // These timings are logged to Google Cloud's operations suite (formerly Stackdriver). + // https://cloud.google.com/products/operations // // Setup: - // 1. Run UnrealGDK SetupIncTraceLibs.bat to include latency tracking libraries. - // 2. Setup a Google project with access to Stackdriver. - // 3. Create and download a service-account certificate - // 4. Set an environment variable GOOGLE_APPLICATION_CREDENTIALS to certificate path - // 5. Set an environment variable GRPC_DEFAULT_SSL_ROOTS_FILE_PATH to your `roots.pem` gRPC path + // See https://github.com/spatialos/UnrealGDKTestGyms/blob/master/USER_MANUAL.md#latency-gym // // Usage: // 1. Register your Google's project id with `RegisterProject` @@ -136,27 +133,19 @@ class SPATIALGDK_API USpatialLatencyTracer : public UObject #if TRACE_LIB_ACTIVE bool IsValidKey(TraceKey Key); - TraceKey RetrievePendingTrace(const UObject* Obj, const UFunction* Function); - TraceKey RetrievePendingTrace(const UObject* Obj, const GDK_PROPERTY(Property) * Property); TraceKey RetrievePendingTrace(const UObject* Obj, const FString& Tag); void WriteToLatencyTrace(const TraceKey Key, const FString& TraceDesc); void WriteAndEndTrace(const TraceKey Key, const FString& TraceDesc, bool bOnlyEndIfTraceRootIsRemote); - void WriteTraceToSchemaObject(const TraceKey Key, Schema_Object* Obj, const Schema_FieldId FieldId); - TraceKey ReadTraceFromSchemaObject(Schema_Object* Obj, const Schema_FieldId FieldId); - void SetWorkerId(const FString& NewWorkerId) { WorkerId = NewWorkerId; } void ResetWorkerId(); - void OnEnqueueMessage(const SpatialGDK::FOutgoingMessage*); - void OnDequeueMessage(const SpatialGDK::FOutgoingMessage*); - private: using ActorFuncKey = TPair; using ActorPropertyKey = TPair; using ActorTagKey = TPair; - using TraceSpan = improbable::trace::Span; + using TraceSpan = improbable::legacy::trace::Span; bool BeginLatencyTrace_Internal(const FString& TraceDesc, FSpatialLatencyPayload& OutLatencyPayload); bool ContinueLatencyTrace_Internal(const AActor* Actor, const FString& Target, ETraceType::Type Type, const FString& TraceDesc, diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyTracerMinimal.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyTracerMinimal.h deleted file mode 100644 index 96a3f63df2..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyTracerMinimal.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#pragma once - -#include "CoreMinimal.h" - -namespace worker -{ -namespace c -{ -struct Schema_Object; -} -} // namespace worker - -class FSpatialLatencyTracerMinimal -{ -public: - static int32 ReadTraceFromSchemaObject(worker::c::Schema_Object* Obj, uint32 FieldId); - static void WriteTraceToSchemaObject(int32 Key, worker::c::Schema_Object* Obj, uint32 FieldId); -}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h index 9262e32dd9..1cdf970c74 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h @@ -17,6 +17,13 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialMetrics, Log, All); DECLARE_DELEGATE_RetVal(double, UserSuppliedMetric); +UENUM() +enum class ESpatialServerCommands : uint8 +{ + StartInsights, + StopInsights, +}; + UCLASS() class SPATIALGDK_API USpatialMetrics : public UObject { @@ -71,6 +78,12 @@ class SPATIALGDK_API USpatialMetrics : public UObject void SetCustomMetric(const FString& Metric, const UserSuppliedMetric& Delegate); void RemoveCustomMetric(const FString& Metric); +private: + void SpatialExecServerCmd_Internal(const FString& ServerName, const ESpatialServerCommands& Command, const FString& Args); + + bool StartInsightsCapture(const FString& Args); + bool StopInsightsCapture(); + private: // Worker SDK metrics WorkerGaugeMetric WorkerSDKGaugeMetrics; diff --git a/SpatialGDK/Source/SpatialGDK/SpatialGDK.Build.cs b/SpatialGDK/Source/SpatialGDK/SpatialGDK.Build.cs index e48a0b0340..930b25b613 100644 --- a/SpatialGDK/Source/SpatialGDK/SpatialGDK.Build.cs +++ b/SpatialGDK/Source/SpatialGDK/SpatialGDK.Build.cs @@ -44,6 +44,11 @@ public SpatialGDK(ReadOnlyTargetRules Target) : base(Target) Target.Configuration != UnrealTargetConfiguration.Test)) { PublicDependencyModuleNames.Add("GameplayDebugger"); + PublicDefinitions.Add("WITH_GAMEPLAY_DEBUGGER=1"); + } + else + { + PublicDefinitions.Add("WITH_GAMEPLAY_DEBUGGER=0"); } if (Target.bBuildEditor) @@ -144,14 +149,14 @@ public SpatialGDK(ReadOnlyTargetRules Target) : base(Target) string TraceDynamicLibPath = ""; if (Target.Platform == UnrealTargetPlatform.Win32 || Target.Platform == UnrealTargetPlatform.Win64) { - TraceStaticLibPath = Path.Combine(WorkerLibraryDir, "trace_dynamic.lib"); - TraceDynamicLib = "trace_dynamic.dll"; + TraceStaticLibPath = Path.Combine(WorkerLibraryDir, "legacy_trace_dynamic.lib"); + TraceDynamicLib = "legacy_trace_dynamic.dll"; TraceDynamicLibPath = Path.Combine(WorkerLibraryDir, TraceDynamicLib); } else if (Target.Platform == UnrealTargetPlatform.Linux) { - TraceStaticLibPath = Path.Combine(WorkerLibraryDir, "libtrace_dynamic.so"); - TraceDynamicLib = "libtrace_dynamic.so"; + TraceStaticLibPath = Path.Combine(WorkerLibraryDir, "liblegacy_trace_dynamic.so"); + TraceDynamicLib = "liblegacy_trace_dynamic.so"; TraceDynamicLibPath = Path.Combine(WorkerLibraryDir, TraceDynamicLib); } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.cpp index 902c2d15bc..3543a86edb 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.cpp @@ -23,20 +23,22 @@ namespace { ESchemaComponentType PropertyGroupToSchemaComponentType(EReplicatedPropertyGroup Group) { - if (Group == REP_MultiClient) + static_assert(REP_Count == 4, + "Unexpected number of ReplicatedPropertyGroups, please make sure PropertyGroupToSchemaComponentType is still correct."); + static_assert(SCHEMA_Count == 4, + "Unexpected number of Schema component types, please make sure PropertyGroupToSchemaComponentType is still correct."); + + switch (Group) { + case REP_MultiClient: return SCHEMA_Data; - } - else if (Group == REP_SingleClient) - { + case REP_SingleClient: return SCHEMA_OwnerOnly; - } - else if (Group == REP_InitialOnly) - { + case REP_InitialOnly: return SCHEMA_InitialOnly; - } - else - { + case REP_ServerOnly: + return SCHEMA_ServerOnly; + default: checkNoEntry(); return SCHEMA_Invalid; } @@ -129,11 +131,6 @@ void WriteSchemaRepField(FCodeWriter& Writer, const TSharedPtr Writer.Printf("{0} {1} = {2};", *PropertyToSchemaType(RepProp->Property), *SchemaFieldName(RepProp), FieldCounter); } -void WriteSchemaHandoverField(FCodeWriter& Writer, const TSharedPtr HandoverProp, const int FieldCounter) -{ - Writer.Printf("{0} {1} = {2};", *PropertyToSchemaType(HandoverProp->Property), *SchemaFieldName(HandoverProp), FieldCounter); -} - // Generates schema for a statically attached subobject on an Actor. FActorSpecificSubobjectSchemaData GenerateSchemaForStaticallyAttachedSubobject(FCodeWriter& Writer, FComponentIdGenerator& IdGenerator, FString PropertyName, TSharedPtr& TypeInfo, @@ -177,32 +174,6 @@ FActorSpecificSubobjectSchemaData GenerateSchemaForStaticallyAttachedSubobject(F AddComponentId(ComponentId, SubobjectData.SchemaComponents, PropertyGroupToSchemaComponentType(Group)); } - FCmdHandlePropertyMap HandoverData = GetFlatHandoverData(TypeInfo); - if (HandoverData.Num() > 0) - { - Worker_ComponentId ComponentId = 0; - if (ExistingSchemaData != nullptr && ExistingSchemaData->SchemaComponents[ESchemaComponentType::SCHEMA_Handover] != 0) - { - ComponentId = ExistingSchemaData->SchemaComponents[ESchemaComponentType::SCHEMA_Handover]; - } - else - { - ComponentId = IdGenerator.Next(); - } - - Writer.PrintNewLine(); - - // Handover (server to server) replicated properties. - FString ComponentName = PropertyName + TEXT("Handover"); - Writer.Printf("component {0} {", *ComponentName); - Writer.Indent(); - Writer.Printf("id = {0};", ComponentId); - Writer.Printf("data unreal.generated.{0};", *SchemaHandoverDataName(ComponentClass)); - Writer.Outdent().Print("}"); - - AddComponentId(ComponentId, SubobjectData.SchemaComponents, ESchemaComponentType::SCHEMA_Handover); - } - return SubobjectData; } @@ -404,25 +375,6 @@ void GenerateSubobjectSchema(FComponentIdGenerator& IdGenerator, UClass* Class, } } - // Also check the HandoverData - FCmdHandlePropertyMap HandoverData = GetFlatHandoverData(TypeInfo); - for (auto& PropertyPair : HandoverData) - { - GDK_PROPERTY(Property)* Property = PropertyPair.Value->Property; - if (Property->IsA()) - { - bShouldIncludeCoreTypes = true; - } - - if (Property->IsA()) - { - if (GDK_CASTFIELD(Property)->Inner->IsA()) - { - bShouldIncludeCoreTypes = true; - } - } - } - if (bShouldIncludeCoreTypes) { Writer.PrintNewLine(); @@ -465,21 +417,6 @@ void GenerateSubobjectSchema(FComponentIdGenerator& IdGenerator, UClass* Class, Writer.Outdent().Print("}"); } - if (HandoverData.Num() > 0) - { - Writer.PrintNewLine(); - - Writer.Printf("type {0} {", *SchemaHandoverDataName(Class)); - Writer.Indent(); - int FieldCounter = 0; - for (auto& Prop : HandoverData) - { - FieldCounter++; - WriteSchemaHandoverField(Writer, Prop.Value, FieldCounter); - } - Writer.Outdent().Print("}"); - } - // Use the max number of dynamically attached subobjects per class to generate // that many schema components for this subobject. const uint32 DynamicComponentsPerClass = GetDefault()->MaxDynamicallyAttachedSubobjectsPerClass; @@ -535,31 +472,6 @@ void GenerateSubobjectSchema(FComponentIdGenerator& IdGenerator, UClass* Class, AddComponentId(ComponentId, DynamicSubobjectComponents.SchemaComponents, PropertyGroupToSchemaComponentType(Group)); } - if (HandoverData.Num() > 0) - { - Writer.PrintNewLine(); - - Worker_ComponentId ComponentId = 0; - if (ExistingSchemaData != nullptr) - { - ComponentId = ExistingSchemaData->GetDynamicSubobjectComponentId(i - 1, SCHEMA_Handover); - } - - if (ComponentId == 0) - { - ComponentId = IdGenerator.Next(); - } - FString ComponentName = SchemaHandoverDataName(Class) + TEXT("Dynamic") + FString::FromInt(i); - - Writer.Printf("component {0} {", *ComponentName); - Writer.Indent(); - Writer.Printf("id = {0};", ComponentId); - Writer.Printf("data {0};", *SchemaHandoverDataName(Class)); - Writer.Outdent().Print("}"); - - AddComponentId(ComponentId, DynamicSubobjectComponents.SchemaComponents, ESchemaComponentType::SCHEMA_Handover); - } - SubobjectSchemaData.DynamicSubobjectComponents.Add(MoveTemp(DynamicSubobjectComponents)); } @@ -569,6 +481,29 @@ void GenerateSubobjectSchema(FComponentIdGenerator& IdGenerator, UClass* Class, SubobjectClassPathToSchema.Add(Class->GetPathName(), SubobjectSchemaData); } +EReplicatedPropertyGroup SchemaComponentTypeToPropertyGroup(ESchemaComponentType SchemaType) +{ + static_assert(REP_Count == 4, + "Unexpected number of ReplicatedPropertyGroups, please make sure SchemaComponentTypeToPropertyGroup is still correct."); + static_assert(SCHEMA_Count == 4, + "Unexpected number of Schema component types, please make sure SchemaComponentTypeToPropertyGroup is still correct."); + + switch (SchemaType) + { + case SCHEMA_Data: + return REP_MultiClient; + case SCHEMA_OwnerOnly: + return REP_SingleClient; + case SCHEMA_InitialOnly: + return REP_InitialOnly; + case SCHEMA_ServerOnly: + return REP_ServerOnly; + default: + checkNoEntry(); + return REP_MultiClient; + } +} + void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSharedPtr TypeInfo, FString SchemaPath) { const FActorSchemaData* const SchemaData = ActorClassPathToSchema.Find(Class->GetPathName()); @@ -642,38 +577,6 @@ void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSha Writer.Outdent().Print("}"); } - FCmdHandlePropertyMap HandoverData = GetFlatHandoverData(TypeInfo); - if (HandoverData.Num() > 0) - { - Worker_ComponentId ComponentId = 0; - if (SchemaData != nullptr && SchemaData->SchemaComponents[ESchemaComponentType::SCHEMA_Handover] != 0) - { - ComponentId = SchemaData->SchemaComponents[ESchemaComponentType::SCHEMA_Handover]; - } - else - { - ComponentId = IdGenerator.Next(); - } - - Writer.PrintNewLine(); - - // Handover (server to server) replicated properties. - FString ComponentName = SchemaHandoverDataName(Class); - Writer.Printf("component {0} {", *ComponentName); - Writer.Indent(); - Writer.Printf("id = {0};", ComponentId); - - AddComponentId(ComponentId, ActorSchemaData.SchemaComponents, ESchemaComponentType::SCHEMA_Handover); - - int FieldCounter = 0; - for (auto& Prop : HandoverData) - { - FieldCounter++; - WriteSchemaHandoverField(Writer, Prop.Value, FieldCounter); - } - Writer.Outdent().Print("}"); - } - GenerateSubobjectSchemaForActor(IdGenerator, Class, TypeInfo, SchemaPath, ActorSchemaData, ActorClassPathToSchema.Find(Class->GetPathName())); @@ -719,13 +622,13 @@ void GenerateRPCEndpointsSchema(FString SchemaPath) { ERPCType::ClientReliable, ERPCType::ClientUnreliable }, { ERPCType::ServerReliable, ERPCType::ServerUnreliable, ERPCType::ServerAlwaysWrite }); GenerateRPCEndpoint(Writer, TEXT("MulticastRPCs"), SpatialConstants::MULTICAST_RPCS_COMPONENT_ID, { ERPCType::NetMulticast }, {}); - GenerateRPCEndpoint(Writer, TEXT("CrossServerSenderRPCs"), SpatialConstants::CROSSSERVER_SENDER_ENDPOINT_COMPONENT_ID, + GenerateRPCEndpoint(Writer, TEXT("CrossServerSenderRPCs"), SpatialConstants::CROSS_SERVER_SENDER_ENDPOINT_COMPONENT_ID, { ERPCType::CrossServer }, {}); - GenerateRPCEndpoint(Writer, TEXT("CrossServerReceiverRPCs"), SpatialConstants::CROSSSERVER_RECEIVER_ENDPOINT_COMPONENT_ID, + GenerateRPCEndpoint(Writer, TEXT("CrossServerReceiverRPCs"), SpatialConstants::CROSS_SERVER_RECEIVER_ENDPOINT_COMPONENT_ID, { ERPCType::CrossServer }, {}); - GenerateRPCEndpoint(Writer, TEXT("CrossServerSenderACKRPCs"), SpatialConstants::CROSSSERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID, {}, + GenerateRPCEndpoint(Writer, TEXT("CrossServerSenderACKRPCs"), SpatialConstants::CROSS_SERVER_SENDER_ACK_ENDPOINT_COMPONENT_ID, {}, { ERPCType::CrossServer }); - GenerateRPCEndpoint(Writer, TEXT("CrossServerReceiverACKRPCs"), SpatialConstants::CROSSSERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID, {}, + GenerateRPCEndpoint(Writer, TEXT("CrossServerReceiverACKRPCs"), SpatialConstants::CROSS_SERVER_RECEIVER_ACK_ENDPOINT_COMPONENT_ID, {}, { ERPCType::CrossServer }); Writer.WriteToFile(*FPaths::Combine(*SchemaPath, TEXT("rpc_endpoints.schema"))); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.h b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.h index 228cb623be..9f303c9577 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.h @@ -18,6 +18,8 @@ extern TMap SubobjectClassPathToSchema; extern TMap LevelPathToComponentId; extern TMap NetCullDistanceToComponentId; +EReplicatedPropertyGroup SchemaComponentTypeToPropertyGroup(ESchemaComponentType SchemaType); + // Generates schema for an Actor void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSharedPtr TypeInfo, FString SchemaPath); // Generates schema for a Subobject class - the schema type and the dynamic schema components diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp index 831979a7a4..a38b38e053 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp @@ -189,33 +189,6 @@ void CheckIdentifierNameValidity(TSharedPtr TypeInfo, bool& bOutSuc } } - // Check Handover data. - FCmdHandlePropertyMap HandoverData = GetFlatHandoverData(TypeInfo); - TMap> SchemaHandoverDataNames; - for (auto& Prop : HandoverData) - { - FString NextSchemaHandoverDataName = SchemaFieldName(Prop.Value); - - if (!CheckSchemaNameValidity(NextSchemaHandoverDataName, Prop.Value->Property->GetPathName(), TEXT("Handover property"))) - { - bOutSuccess = false; - } - - if (TSharedPtr* ExistingHandoverData = SchemaHandoverDataNames.Find(NextSchemaHandoverDataName)) - { - UE_LOG(LogSpatialGDKSchemaGenerator, Error, - TEXT("Handover data name collision after removing non-alphanumeric characters, schema not generated. Name '%s' collides " - "for '%s' and '%s'"), - *NextSchemaHandoverDataName, *ExistingHandoverData->Get()->Property->GetPathName(), - *Prop.Value->Property->GetPathName()); - bOutSuccess = false; - } - else - { - SchemaHandoverDataNames.Add(NextSchemaHandoverDataName, Prop.Value); - } - } - // Check subobject name validity. FSubobjects Subobjects = GetAllSubobjects(TypeInfo); TMap> SchemaSubobjectNames; @@ -550,14 +523,16 @@ TMap CreateComponentIdToClassPathMap() FString GetComponentSetNameBySchemaType(ESchemaComponentType SchemaType) { + static_assert(SCHEMA_Count == 4, "Unexpected number of Schema type components, please check the enclosing function is still correct."); + switch (SchemaType) { case SCHEMA_Data: return SpatialConstants::DATA_COMPONENT_SET_NAME; case SCHEMA_OwnerOnly: return SpatialConstants::OWNER_ONLY_COMPONENT_SET_NAME; - case SCHEMA_Handover: - return SpatialConstants::HANDOVER_COMPONENT_SET_NAME; + case SCHEMA_ServerOnly: + return SpatialConstants::SERVER_ONLY_COMPONENT_SET_NAME; case SCHEMA_InitialOnly: return SpatialConstants::INITIAL_ONLY_COMPONENT_SET_NAME; default: @@ -571,13 +546,15 @@ FString GetComponentSetNameBySchemaType(ESchemaComponentType SchemaType) Worker_ComponentId GetComponentSetIdBySchemaType(ESchemaComponentType SchemaType) { + static_assert(SCHEMA_Count == 4, "Unexpected number of Schema type components, please check the enclosing function is still correct."); + switch (SchemaType) { case SCHEMA_Data: return SpatialConstants::DATA_COMPONENT_SET_ID; case SCHEMA_OwnerOnly: return SpatialConstants::OWNER_ONLY_COMPONENT_SET_ID; - case SCHEMA_Handover: + case SCHEMA_ServerOnly: return SpatialConstants::HANDOVER_COMPONENT_SET_ID; case SCHEMA_InitialOnly: return SpatialConstants::INITIAL_ONLY_COMPONENT_SET_ID; @@ -659,23 +636,8 @@ void WriteServerAuthorityComponentSet(const USchemaDatabase* SchemaDatabase, con const Worker_ComponentId ComponentId = GeneratedActorClass.Value.SchemaComponents[SchemaType]; if (ComponentId != 0) { - switch (SchemaType) - { - case SCHEMA_Data: - Writer.Printf("unreal.generated.{0}.{1},", ActorClassName.ToLower(), ActorClassName); - break; - case SCHEMA_OwnerOnly: - Writer.Printf("unreal.generated.{0}.{1}OwnerOnly,", ActorClassName.ToLower(), ActorClassName); - break; - case SCHEMA_Handover: - Writer.Printf("unreal.generated.{0}.{1}Handover,", ActorClassName.ToLower(), ActorClassName); - break; - case SCHEMA_InitialOnly: - Writer.Printf("unreal.generated.{0}.{1}InitialOnly,", ActorClassName.ToLower(), ActorClassName); - break; - default: - break; - } + Writer.Printf("unreal.generated.{0}.{1}{2},", ActorClassName.ToLower(), ActorClassName, + GetReplicatedPropertyGroupName(SchemaComponentTypeToPropertyGroup(SchemaType))); } }); @@ -687,23 +649,8 @@ void WriteServerAuthorityComponentSet(const USchemaDatabase* SchemaDatabase, con const Worker_ComponentId& ComponentId = ActorSubObjectData.Value.SchemaComponents[SchemaType]; if (ComponentId != 0) { - switch (SchemaType) - { - case SCHEMA_Data: - Writer.Printf("unreal.generated.{0}.subobjects.{1},", ActorClassName.ToLower(), ActorSubObjectName); - break; - case SCHEMA_OwnerOnly: - Writer.Printf("unreal.generated.{0}.subobjects.{1}OwnerOnly,", ActorClassName.ToLower(), ActorSubObjectName); - break; - case SCHEMA_Handover: - Writer.Printf("unreal.generated.{0}.subobjects.{1}Handover,", ActorClassName.ToLower(), ActorSubObjectName); - break; - case SCHEMA_InitialOnly: - Writer.Printf("unreal.generated.{0}.subobjects.{1}InitialOnly,", ActorClassName.ToLower(), ActorSubObjectName); - break; - default: - break; - } + Writer.Printf("unreal.generated.{0}.subobjects.{1}{2},", ActorClassName.ToLower(), ActorSubObjectName, + GetReplicatedPropertyGroupName(SchemaComponentTypeToPropertyGroup(SchemaType))); } }); } @@ -722,23 +669,8 @@ void WriteServerAuthorityComponentSet(const USchemaDatabase* SchemaDatabase, con const Worker_ComponentId& ComponentId = SubObjectSchemaData.SchemaComponents[SchemaType]; if (ComponentId != 0) { - switch (SchemaType) - { - case SCHEMA_Data: - Writer.Printf("unreal.generated.{0}Dynamic{1},", SubObjectClassName, SubObjectNumber + 1); - break; - case SCHEMA_OwnerOnly: - Writer.Printf("unreal.generated.{0}OwnerOnlyDynamic{1},", SubObjectClassName, SubObjectNumber + 1); - break; - case SCHEMA_Handover: - Writer.Printf("unreal.generated.{0}HandoverDynamic{1},", SubObjectClassName, SubObjectNumber + 1); - break; - case SCHEMA_InitialOnly: - Writer.Printf("unreal.generated.{0}InitialOnlyDynamic{1},", SubObjectClassName, SubObjectNumber + 1); - break; - default: - break; - } + Writer.Printf("unreal.generated.{0}{1}Dynamic{2},", SubObjectClassName, + GetReplicatedPropertyGroupName(SchemaComponentTypeToPropertyGroup(SchemaType)), SubObjectNumber + 1); } }); } @@ -868,6 +800,8 @@ void WriteComponentSetBySchemaType(const USchemaDatabase* SchemaDatabase, ESchem Writer.Printf("id = {0};", GetComponentSetIdBySchemaType(SchemaType)); Writer.Printf("components = [").Indent(); + FString SchemaTypeString = GetReplicatedPropertyGroupName(SchemaComponentTypeToPropertyGroup(SchemaType)); + // Write all components. { for (const auto& GeneratedActorClass : SchemaDatabase->ActorClassPathToSchema) @@ -876,23 +810,7 @@ void WriteComponentSetBySchemaType(const USchemaDatabase* SchemaDatabase, ESchem const FString& ActorClassName = UnrealNameToSchemaComponentName(GeneratedActorClass.Value.GeneratedSchemaName); if (GeneratedActorClass.Value.SchemaComponents[SchemaType] != 0) { - switch (SchemaType) - { - case SCHEMA_Data: - Writer.Printf("unreal.generated.{0}.{1},", ActorClassName.ToLower(), ActorClassName); - break; - case SCHEMA_OwnerOnly: - Writer.Printf("unreal.generated.{0}.{1}OwnerOnly,", ActorClassName.ToLower(), ActorClassName); - break; - case SCHEMA_Handover: - Writer.Printf("unreal.generated.{0}.{1}Handover,", ActorClassName.ToLower(), ActorClassName); - break; - case SCHEMA_InitialOnly: - Writer.Printf("unreal.generated.{0}.{1}InitialOnly,", ActorClassName.ToLower(), ActorClassName); - break; - default: - break; - } + Writer.Printf("unreal.generated.{0}.{1}{2},", ActorClassName.ToLower(), ActorClassName, SchemaTypeString); } // Actor static subobjects. for (const auto& ActorSubObjectData : GeneratedActorClass.Value.SubobjectData) @@ -900,23 +818,8 @@ void WriteComponentSetBySchemaType(const USchemaDatabase* SchemaDatabase, ESchem const FString ActorSubObjectName = UnrealNameToSchemaComponentName(ActorSubObjectData.Value.Name.ToString()); if (ActorSubObjectData.Value.SchemaComponents[SchemaType] != 0) { - switch (SchemaType) - { - case SCHEMA_Data: - Writer.Printf("unreal.generated.{0}.subobjects.{1},", ActorClassName.ToLower(), ActorSubObjectName); - break; - case SCHEMA_OwnerOnly: - Writer.Printf("unreal.generated.{0}.subobjects.{1}OwnerOnly,", ActorClassName.ToLower(), ActorSubObjectName); - break; - case SCHEMA_Handover: - Writer.Printf("unreal.generated.{0}.subobjects.{1}Handover,", ActorClassName.ToLower(), ActorSubObjectName); - break; - case SCHEMA_InitialOnly: - Writer.Printf("unreal.generated.{0}.subobjects.{1}InitialOnly,", ActorClassName.ToLower(), ActorSubObjectName); - break; - default: - break; - } + Writer.Printf("unreal.generated.{0}.subobjects.{1}{2},", ActorClassName.ToLower(), ActorSubObjectName, + SchemaTypeString); } } } @@ -931,23 +834,7 @@ void WriteComponentSetBySchemaType(const USchemaDatabase* SchemaDatabase, ESchem GeneratedSubObjectClass.Value.DynamicSubobjectComponents[SubObjectNumber]; if (SubObjectSchemaData.SchemaComponents[SchemaType] != 0) { - switch (SchemaType) - { - case SCHEMA_Data: - Writer.Printf("unreal.generated.{0}Dynamic{1},", SubObjectClassName, SubObjectNumber + 1); - break; - case SCHEMA_OwnerOnly: - Writer.Printf("unreal.generated.{0}OwnerOnlyDynamic{1},", SubObjectClassName, SubObjectNumber + 1); - break; - case SCHEMA_Handover: - Writer.Printf("unreal.generated.{0}HandoverDynamic{1},", SubObjectClassName, SubObjectNumber + 1); - break; - case SCHEMA_InitialOnly: - Writer.Printf("unreal.generated.{0}InitialOnlyDynamic{1},", SubObjectClassName, SubObjectNumber + 1); - break; - default: - break; - } + Writer.Printf("unreal.generated.{0}{1}Dynamic{2},", SubObjectClassName, SchemaTypeString, SubObjectNumber + 1); } } } @@ -974,8 +861,9 @@ void WriteComponentSetFiles(const USchemaDatabase* SchemaDatabase, FString Schem WriteRoutingWorkerAuthorityComponentSet(SchemaOutputPath); WriteComponentSetBySchemaType(SchemaDatabase, SCHEMA_Data, SchemaOutputPath); WriteComponentSetBySchemaType(SchemaDatabase, SCHEMA_OwnerOnly, SchemaOutputPath); - WriteComponentSetBySchemaType(SchemaDatabase, SCHEMA_Handover, SchemaOutputPath); + WriteComponentSetBySchemaType(SchemaDatabase, SCHEMA_ServerOnly, SchemaOutputPath); WriteComponentSetBySchemaType(SchemaDatabase, SCHEMA_InitialOnly, SchemaOutputPath); + static_assert(SCHEMA_Count == 4, "Unexpected number of Schema type components, please check the enclosing function is still correct."); } USchemaDatabase* InitialiseSchemaDatabase(const FString& PackagePath) @@ -1266,8 +1154,6 @@ bool LoadGeneratorStateFromSchemaDatabase(const FString& FileName) return false; } - bool bResetSchema = false; - FFileStatData StatData = FPlatformFileManager::Get().GetPlatformFile().GetStatData(*RelativeFileName); if (StatData.bIsValid) { diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.cpp index e9e13de57a..a8608b29dd 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.cpp @@ -12,21 +12,31 @@ using namespace SpatialGDKEditor::Schema; TArray GetAllReplicatedPropertyGroups() { - return { REP_MultiClient, REP_SingleClient, REP_InitialOnly }; + static TArray ReplicatedPropertyGroups = []() { + TArray Temp; + for (uint32 i = EReplicatedPropertyGroup::REP_First; i < EReplicatedPropertyGroup::REP_Count; i++) + { + Temp.Add(static_cast(i)); + } + return Temp; + }(); + + return ReplicatedPropertyGroups; } FString GetReplicatedPropertyGroupName(EReplicatedPropertyGroup Group) { - if (Group == REP_SingleClient) + static_assert(REP_Count == 4, "Unexpected number of ReplicatedPropertyGroups, please update this function."); + + switch (Group) { + case REP_SingleClient: return TEXT("OwnerOnly"); - } - else if (Group == REP_InitialOnly) - { + case REP_InitialOnly: return TEXT("InitialOnly"); - } - else - { + case REP_ServerOnly: + return TEXT("ServerOnly"); + default: return TEXT(""); } } @@ -424,42 +434,23 @@ TSharedPtr CreateUnrealTypeInfo(UStruct* Type, uint32 ParentChecksu } } // END CMD FOR LOOP - // Find the handover properties. - uint16 HandoverDataHandle = 1; - VisitAllProperties(TypeNode, [&HandoverDataHandle, &Class](TSharedPtr PropertyInfo) { - if (PropertyInfo->Property->PropertyFlags & CPF_Handover) - { - if (GDK_PROPERTY(StructProperty)* StructProp = GDK_CASTFIELD(PropertyInfo->Property)) - { - if (StructProp->Struct->StructFlags & STRUCT_NetDeltaSerializeNative) - { - // Warn about delta serialization - UE_LOG(LogSpatialGDKSchemaGenerator, Warning, - TEXT("%s in %s uses delta serialization. " - "This is not supported and standard serialization will be used instead."), - *PropertyInfo->Property->GetName(), *Class->GetName()); - } - } - PropertyInfo->HandoverData = MakeShared(); - PropertyInfo->HandoverData->Handle = HandoverDataHandle++; - } - return true; - }); - return TypeNode; } FUnrealFlatRepData GetFlatRepData(TSharedPtr TypeInfo) { FUnrealFlatRepData RepData; - RepData.Add(REP_MultiClient); - RepData.Add(REP_SingleClient); - RepData.Add(REP_InitialOnly); + for (EReplicatedPropertyGroup Group : GetAllReplicatedPropertyGroups()) + { + RepData.Add(Group); + } VisitAllProperties(TypeInfo, [&RepData, &TypeInfo](TSharedPtr PropertyInfo) { if (PropertyInfo->ReplicationData.IsValid()) { EReplicatedPropertyGroup Group = REP_MultiClient; + static_assert(REP_Count == 4, + "Unexpected number of ReplicatedPropertyGroups. Please make sure the GetFlatRepData function is still correct."); switch (PropertyInfo->ReplicationData->Condition) { case COND_AutonomousOnly: @@ -470,6 +461,9 @@ FUnrealFlatRepData GetFlatRepData(TSharedPtr TypeInfo) case COND_InitialOnly: Group = REP_InitialOnly; break; + case COND_ServerOnly: + Group = REP_ServerOnly; + break; case COND_InitialOrOwner: UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("COND_InitialOrOwner not supported. COND_None will be used instead. %s::%s"), *TypeInfo->Type->GetName(), @@ -482,36 +476,15 @@ FUnrealFlatRepData GetFlatRepData(TSharedPtr TypeInfo) }); // Sort by replication handle. - RepData[REP_MultiClient].KeySort([](uint16 A, uint16 B) { - return A < B; - }); - RepData[REP_SingleClient].KeySort([](uint16 A, uint16 B) { - return A < B; - }); - RepData[REP_InitialOnly].KeySort([](uint16 A, uint16 B) { - return A < B; - }); + for (EReplicatedPropertyGroup Group : GetAllReplicatedPropertyGroups()) + { + RepData[Group].KeySort([](uint16 A, uint16 B) { + return A < B; + }); + } return RepData; } -FCmdHandlePropertyMap GetFlatHandoverData(TSharedPtr TypeInfo) -{ - FCmdHandlePropertyMap HandoverData; - VisitAllProperties(TypeInfo, [&HandoverData](TSharedPtr PropertyInfo) { - if (PropertyInfo->HandoverData.IsValid()) - { - HandoverData.Add(PropertyInfo->HandoverData->Handle, PropertyInfo); - } - return true; - }); - - // Sort by property handle. - HandoverData.KeySort([](uint16 A, uint16 B) { - return A < B; - }); - return HandoverData; -} - TArray> GetPropertyChain(TSharedPtr LeafProperty) { TArray> OutputChain; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.h b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.h index 892d704ebe..1f51353fc8 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.h @@ -52,8 +52,7 @@ FUnrealType [3] FUnrealProperty + Property: "SomeTransientProperty" + Type: nullptr - + ReplicationData: nullptr - + HandoverData: FUnrealHandoverData + + ReplicationData: FUnrealHandoverData + RepLayoutType: REPCMD_PropertyFloat + Handle: 1 ... @@ -61,17 +60,25 @@ FUnrealType // As we cannot fully implement replication conditions using SpatialOS's component interest API, we instead try // to emulate it by separating all replicated properties into three groups: properties which are meant for just one -// client (AutonomousProxy/OwnerOnly), initial only properties (InitialOnly), or many clients (everything else). +// client (AutonomousProxy/OwnerOnly), initial only properties (InitialOnly), server only properties (ServerOnly), or many clients +// (everything else). enum EReplicatedPropertyGroup { + // Beware that the order of these currently matters for actually applying the variables. + // Specifically, we will e.g. apply all MultiClient data, then all SingleClient data, etc. + // This is a difference from native. TODO: UNR-5576. + REP_MultiClient = 0, REP_SingleClient, - REP_MultiClient, - REP_InitialOnly + REP_InitialOnly, + REP_ServerOnly, + + // Iteration helpers + REP_Count, + REP_First = REP_MultiClient }; struct FUnrealProperty; struct FUnrealRepData; -struct FUnrealHandoverData; struct FUnrealSubobject; // A node which represents an unreal type, such as ACharacter or UCharacterMovementComponent. @@ -89,9 +96,8 @@ struct FUnrealType struct FUnrealProperty { GDK_PROPERTY(Property) * Property; - TSharedPtr Type; // Only set if strong reference to object/struct property. - TSharedPtr ReplicationData; // Only set if property is replicated. - TSharedPtr HandoverData; // Only set if property is marked for handover (and not replicated). + TSharedPtr Type; // Only set if strong reference to object/struct property. + TSharedPtr ReplicationData; // Only set if property is replicated. TWeakPtr ContainerType; // These variables are used for unique variable checksum generation. We do this to accurately match properties at run-time. @@ -118,12 +124,6 @@ struct FUnrealRepData int32 ArrayIndex; }; -// A node which represents handover (server to server) data. -struct FUnrealHandoverData -{ - uint16 Handle; -}; - using FUnrealFlatRepData = TMap>>; using FCmdHandlePropertyMap = TMap>; using FSubobjects = TArray; @@ -160,12 +160,6 @@ TSharedPtr CreateUnrealTypeInfo(UStruct* Type, uint32 ParentChecksu // This function will _not_ traverse into subobject properties (as the replication system deals with each object separately). FUnrealFlatRepData GetFlatRepData(TSharedPtr TypeInfo); -// Traverses an AST, and generates a flattened list of handover properties. The list of handover properties will all have -// the HandoverData field set to a value FUnrealHandoverData node which contains data such as the handle or replication type. -// -// This function will traverse into subobject properties. -FCmdHandlePropertyMap GetFlatHandoverData(TSharedPtr TypeInfo); - // Given a property, traverse up to the root property and create a list of properties needed to reach the leaf property. // For example: foo->bar->baz becomes {"foo", "bar", "baz"}. TArray> GetPropertyChain(TSharedPtr LeafProperty); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.cpp index 8fbd03f03f..c5815d3e19 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.cpp @@ -77,11 +77,6 @@ FString SchemaReplicatedDataName(EReplicatedPropertyGroup Group, UClass* Class) *GetReplicatedPropertyGroupName(Group)); } -FString SchemaHandoverDataName(UClass* Class) -{ - return FString::Printf(TEXT("%sHandover"), *UnrealNameToSchemaComponentName(ClassPathToSchemaName[Class->GetPathName()])); -} - FString SchemaFieldName(const TSharedPtr Property) { // Transform the property chain into a chain of names. diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.h b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.h index 1c5e2d03d7..2740d8a720 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.h @@ -23,10 +23,6 @@ FString UnrealNameToSchemaComponentName(const FString& UnrealName); // For example: UnrealCharacterMultiClientRepData FString SchemaReplicatedDataName(EReplicatedPropertyGroup Group, UClass* Class); -// Given an unreal type, generates the name of the component which stores server to server replication data. -// For example: UnrealCharacterHandoverData -FString SchemaHandoverDataName(UClass* Class); - // Given a property node, generates the schema field name. FString SchemaFieldName(const TSharedPtr Property); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp index b0065d5b84..8f7f915041 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp @@ -36,19 +36,7 @@ TArray UnpackedComponentData; void SetEntityData(Worker_Entity& Entity, const TArray& Components) { Entity.component_count = Components.Num(); - -#if TRACE_LIB_ACTIVE - // We have to unpack these as Worker_ComponentData is not the same as FWorkerComponentData - UnpackedComponentData.Empty(); - UnpackedComponentData.SetNum(Components.Num()); - for (int i = 0, Num = Components.Num(); i < Num; i++) - { - UnpackedComponentData[i] = Components[i]; - } - Entity.components = UnpackedComponentData.GetData(); -#else Entity.components = Components.GetData(); -#endif } bool CreateSpawnerEntity(Worker_SnapshotOutputStream* OutputStream) diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultLaunchConfigGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultLaunchConfigGenerator.cpp index d497f63403..7d8be98943 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultLaunchConfigGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultLaunchConfigGenerator.cpp @@ -201,10 +201,8 @@ bool GenerateLaunchConfig(const FString& LaunchConfigPath, const FSpatialLaunchC Writer->WriteObjectStart(TEXT("event_filter_configuration")); UEventTracingSamplingSettings* SamplingSettings = SpatialGDKSettings->GetEventTracingSamplingSettings(); - Writer->WriteValue(TEXT("event_pre_filter"), - SamplingSettings->RuntimeEventPreFilter.Len() ? *SamplingSettings->RuntimeEventPreFilter : TEXT("false")); - Writer->WriteValue(TEXT("event_post_filter"), - SamplingSettings->RuntimeEventPostFilter.Len() ? SamplingSettings->RuntimeEventPostFilter : TEXT("false")); + Writer->WriteValue(TEXT("event_pre_filter"), *SamplingSettings->GetRuntimeEventPreFilterString()); + Writer->WriteValue(TEXT("event_post_filter"), *SamplingSettings->GetRuntimeEventPostFilterString()); Writer->WriteObjectEnd(); // event_filter_configuration end Writer->WriteObjectEnd(); // event_tracing_configuration end diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp index ee382f62e1..8c525b522f 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp @@ -146,7 +146,7 @@ void FSpatialGDKEditor::GenerateSchema(ESchemaGenerationMethod Method, TFunction } // Make sure SchemaDatabase is not loaded. - if (UPackage* LoadedDatabase = FindPackage(nullptr, *FPaths::Combine(TEXT("/Game/"), *SpatialConstants::SCHEMA_DATABASE_FILE_PATH))) + if (UPackage* LoadedDatabase = FindPackage(nullptr, *SpatialConstants::SCHEMA_DATABASE_ASSET_PATH)) { TArray ToUnload; ToUnload.Add(LoadedDatabase); @@ -232,9 +232,18 @@ void FSpatialGDKEditor::GenerateSchema(ESchemaGenerationMethod Method, TFunction else { UE_LOG(LogSpatialGDKEditor, Error, TEXT("Schema generation failed. View earlier log messages for errors.")); + + // Make sure SchemaDatabase is not loaded (unlocks the file after any failed schema gen operations) + if (UPackage* LoadedDatabase = FindPackage(nullptr, *SpatialConstants::SCHEMA_DATABASE_ASSET_PATH)) + { + TArray ToUnload; + ToUnload.Add(LoadedDatabase); + UPackageTools::UnloadPackages(ToUnload); + } } ResultCallback(bResult); + return; } } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialTestSettings.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialTestSettings.cpp index 0254077cb9..f3dd8eb1f8 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialTestSettings.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialTestSettings.cpp @@ -49,12 +49,16 @@ void FSpatialTestSettings::Override(const FString& MapName) if (ASpatialWorldSettings* SpatialWorldSettings = Cast(World->GetWorldSettings())) { - FString GroupOverridesFilename = FPaths::ConvertRelativePathToFull(SpatialWorldSettings->SettingsOverride.FilePath); + FString GroupOverridesFilename = FPaths::ProjectDir() + SpatialWorldSettings->SettingsOverride.FilePath; if (FPaths::FileExists(GroupOverridesFilename)) { // Override the settings from the group specific config file, if it exists Load(GroupOverridesFilename); } + else if (!SpatialWorldSettings->SettingsOverride.FilePath.IsEmpty()) + { + UE_LOG(LogSpatialTestSettings, Error, TEXT("Could not find settings override file %s."), *GroupOverridesFilename); + } } // Generated config, applied to generated maps diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h index cecf140360..4cf324e8fb 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h @@ -134,7 +134,7 @@ struct FSpatialLaunchConfigDescription const FString& GetDefaultTemplateForRuntimeVariant() const; /** Use default template for deployments. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Use Default Template")) bool bUseDefaultTemplateForRuntimeVariant; /** Deployment template. */ diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKCloudDeploymentConfiguration.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKCloudDeploymentConfiguration.cpp index 64a3127947..73725a9b8d 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKCloudDeploymentConfiguration.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKCloudDeploymentConfiguration.cpp @@ -840,7 +840,7 @@ void SSpatialGDKCloudDeploymentConfiguration::OnCloudDocumentationClicked() { FString WebError; FPlatformProcess::LaunchURL( - TEXT("https://documentation.improbable.io/gdk-for-unreal/docs/cloud-deployment-workflow#section-build-server-worker-assembly"), + TEXT("https://networking.docs.improbable.io/gdk-for-unreal/v0.14.0/workflows/deployment-workflows/cloud-deployment-workflow"), TEXT(""), &WebError); if (!WebError.IsEmpty()) { diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp index 6e2979ffd4..ff4c239f94 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp @@ -248,6 +248,10 @@ void FSpatialGDKEditorToolbarModule::MapActions(TSharedPtr InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().DeleteSchemaDatabase, FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::DeleteSchemaDatabaseButtonClicked)); + InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().CleanGenerateSchema, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CleanSchemaGenerateButtonClicked), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CanExecuteSchemaGenerator)); + InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSnapshot, FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CreateSnapshotButtonClicked), FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CanExecuteSnapshotGenerator)); @@ -403,6 +407,7 @@ TSharedRef FSpatialGDKEditorToolbarModule::CreateGenerateSchemaMenuCont { MenuBuilder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSchemaFull); MenuBuilder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().DeleteSchemaDatabase); + MenuBuilder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().CleanGenerateSchema); } MenuBuilder.EndSection(); @@ -559,15 +564,49 @@ void FSpatialGDKEditorToolbarModule::DeleteSchemaDatabaseButtonClicked() LOCTEXT("DeleteSchemaDatabase_Prompt", "Are you sure you want to delete the schema database?")) == EAppReturnType::Yes) { - OnShowTaskStartNotification(TEXT("Deleting schema database")); - if (SpatialGDKEditor::Schema::DeleteSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_FILE_PATH)) - { - OnShowSuccessNotification(TEXT("Schema database deleted")); - } - else - { - OnShowFailedNotification(TEXT("Failed to delete schema database")); - } + DeleteSchemaDatabase(); + } +} + +bool FSpatialGDKEditorToolbarModule::DeleteSchemaDatabase() +{ + OnShowTaskStartNotification(TEXT("Deleting schema database")); + bool bResult = SpatialGDKEditor::Schema::DeleteSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_FILE_PATH); + + if (bResult) + { + OnShowSuccessNotification(TEXT("Schema database deleted")); + } + else + { + OnShowFailedNotification(TEXT("Failed to delete schema database")); + } + + return bResult; +} + +void FSpatialGDKEditorToolbarModule::CleanSchemaGenerateButtonClicked() +{ + if (FMessageDialog::Open( + EAppMsgType::YesNo, + LOCTEXT("DeleteSchemaDatabase_Prompt", + "Are you sure you want to delete the schema database, delete all generated schema, and regenerate schema?")) + == EAppReturnType::Yes) + { + CleanSchemaGenerate(); + } +} + +void FSpatialGDKEditorToolbarModule::CleanSchemaGenerate() +{ + if (DeleteSchemaDatabase()) + { + SpatialGDKEditor::Schema::ResetSchemaGeneratorStateAndCleanupFolders(); + GenerateSchema(true); + } + else + { + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Failed to delete Schema Database; schema will not be cleaned and regenerated.")); } } @@ -581,6 +620,24 @@ void FSpatialGDKEditorToolbarModule::SchemaGenerateFullButtonClicked() GenerateSchema(true); } +void FSpatialGDKEditorToolbarModule::HandleGenerateSchemaFailure() +{ + // Run the dialogue on a background task -- this allows the editor UI to update and display Schema Gen errors in the log + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] { + if (FMessageDialog::Open(EAppMsgType::YesNo, + LOCTEXT("DeleteAndRegenerateSchemaDatabase_Prompt", + "Schema generation failed. Common schema generation issues can be solved by deleting all schema " + "and generating again. Would you like to clean and retry now?")) + == EAppReturnType::Yes) + { + // GameThread is required for building schema + AsyncTask(ENamedThreads::GameThread, [this] { + CleanSchemaGenerate(); + }); + } + }); +} + void FSpatialGDKEditorToolbarModule::OnShowSingleFailureNotification(const FString& NotificationText) { AsyncTask(ENamedThreads::GameThread, [NotificationText] { @@ -915,7 +972,11 @@ void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment(FString ForceSnaps } FLocalDeploymentManager::LocalDeploymentCallback CallBack = [this](bool bSuccess) { - if (!bSuccess) + if (bSuccess) + { + StartInspectorProcess(/*OnReady*/ nullptr); + } + else { OnShowFailedNotification(TEXT("Local deployment failed to start")); } @@ -961,16 +1022,19 @@ void FSpatialGDKEditorToolbarModule::OpenInspectorURL() } } -void FSpatialGDKEditorToolbarModule::LaunchInspectorWebpageButtonClicked() +void FSpatialGDKEditorToolbarModule::StartInspectorProcess(TFunction OnReady) { const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); const FString InspectorVersion = SpatialGDKEditorSettings->GetInspectorVersion(); - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, InspectorVersion] { + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, InspectorVersion, OnReady] { if (InspectorProcess && InspectorProcess->Update()) { - // We already have an inspector running. Just open the URL. - OpenInspectorURL(); + // We already have an inspector process running. Call ready callback if any. + if (OnReady) + { + OnReady(); + } return; } @@ -1011,6 +1075,16 @@ void FSpatialGDKEditorToolbarModule::LaunchInspectorWebpageButtonClicked() InspectorProcess->Launch(); + if (OnReady) + { + OnReady(); + } + }); +} + +void FSpatialGDKEditorToolbarModule::LaunchInspectorWebpageButtonClicked() +{ + StartInspectorProcess([this]() { OpenInspectorURL(); }); } @@ -1284,6 +1358,8 @@ void FSpatialGDKEditorToolbarModule::GenerateSchema(bool bFullScan) else { OnShowFailedNotification(OnTaskFailMessage); + + HandleGenerateSchemaFailure(); } }); ; diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp index d1d1355b10..3ee0e5c6b7 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp @@ -10,8 +10,11 @@ void FSpatialGDKEditorToolbarCommands::RegisterCommands() EUserInterfaceActionType::Button, FInputGesture()); UI_COMMAND(CreateSpatialGDKSchemaFull, "Schema (Full Scan)", "Creates SpatialOS Unreal GDK schema for all assets.", EUserInterfaceActionType::Button, FInputGesture()); - UI_COMMAND(DeleteSchemaDatabase, "Delete schema database", "Deletes the schema database file", EUserInterfaceActionType::Button, + UI_COMMAND(DeleteSchemaDatabase, "Delete Schema Database", "Deletes the schema database file", EUserInterfaceActionType::Button, FInputGesture()); + UI_COMMAND(CleanGenerateSchema, "Clean and Generate Schema", + "Deletes the schema database file and all generated schema files, and then runs Schema Full Scan", + EUserInterfaceActionType::Button, FInputGesture()); UI_COMMAND(CreateSpatialGDKSnapshot, "Snapshot", "Creates SpatialOS Unreal GDK snapshot.", EUserInterfaceActionType::Button, FInputGesture()); UI_COMMAND(StartNative, "Start Deployment", "Use native Unreal networking", EUserInterfaceActionType::Button, FInputGesture()); diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h index 41c951f45d..dfdc57782f 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h @@ -120,7 +120,11 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable void CreateSnapshotButtonClicked(); void SchemaGenerateButtonClicked(); void SchemaGenerateFullButtonClicked(); + void CleanSchemaGenerateButtonClicked(); + void CleanSchemaGenerate(); void DeleteSchemaDatabaseButtonClicked(); + bool DeleteSchemaDatabase(); + void HandleGenerateSchemaFailure(); void OnPropertyChanged(UObject* ObjectBeingModified, FPropertyChangedEvent& PropertyChangedEvent); void ShowCloudDeploymentDialog(); @@ -138,6 +142,8 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable void GenerateTestMaps(); + void StartInspectorProcess(TFunction OnReady); + private: bool CanExecuteSchemaGenerator() const; bool CanExecuteSnapshotGenerator() const; diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h index 0f423f64c3..0344934194 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h @@ -22,6 +22,7 @@ class FSpatialGDKEditorToolbarCommands : public TCommands CreateSpatialGDKSchema; TSharedPtr CreateSpatialGDKSchemaFull; TSharedPtr DeleteSchemaDatabase; + TSharedPtr CleanGenerateSchema; TSharedPtr CreateSpatialGDKSnapshot; TSharedPtr StartNative; TSharedPtr StartLocalSpatialDeployment; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTest.cpp index 20dcc177fb..12833cf43e 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTest.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTest.cpp @@ -7,6 +7,7 @@ #include "Engine/World.h" #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialNetDriverDebugContext.h" +#include "EngineClasses/SpatialNetDriverGameplayDebuggerContext.h" #include "EngineClasses/SpatialPackageMapClient.h" #include "GameFramework/Actor.h" #include "GameFramework/GameModeBase.h" @@ -18,6 +19,7 @@ #include "Interfaces/IHttpResponse.h" #include "LoadBalancing/AbstractLBStrategy.h" #include "LoadBalancing/DebugLBStrategy.h" +#include "LoadBalancing/GameplayDebuggerLBStrategy.h" #include "LoadBalancing/LayeredLBStrategy.h" #include "Net/UnrealNetwork.h" #include "SpatialFunctionalTestAutoDestroyComponent.h" @@ -144,7 +146,7 @@ void ASpatialFunctionalTest::Tick(float DeltaSeconds) bool bAllAcknowledgedFinishedTest = true; for (const auto* FlowController : FlowControllers) { - if (!FlowController->HasAckFinishedTest()) + if (FlowController != nullptr && !FlowController->HasAckFinishedTest()) { bAllAcknowledgedFinishedTest = false; break; @@ -449,6 +451,18 @@ void ASpatialFunctionalTest::RegisterFlowController(ASpatialFunctionalTestFlowCo FlowControllers.Add(FlowController); } +void ASpatialFunctionalTest::DeregisterFlowController(ASpatialFunctionalTestFlowController* FlowController) +{ + if (FlowController->WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Client) + { + FlowControllers.Remove(FlowController); + } + else + { + UE_LOG(LogSpatialGDKFunctionalTests, Error, TEXT("DeregisterFlowController called on Server on test %s"), *GetName()); + } +} + ASpatialFunctionalTestFlowController* ASpatialFunctionalTest::GetLocalFlowController() { ensureMsgf(LocalFlowController, TEXT("GetLocalFlowController being called without it being set, shouldn't happen")); @@ -493,7 +507,7 @@ FString ASpatialFunctionalTest::GetLocalWorkerString() void ASpatialFunctionalTest::AddStepBlueprint(const FString& StepName, const FWorkerDefinition& Worker, const FStepIsReadyDelegate& IsReadyEvent, const FStepStartDelegate& StartEvent, - const FStepTickDelegate& TickEvent, float StepTimeLimit /*= 0.0f*/) + const FStepTickDelegate& TickEvent, float StepTimeLimit /*= 20.0f*/) { if (StepName.IsEmpty()) { @@ -565,9 +579,10 @@ void ASpatialFunctionalTest::StartStep(const int StepIndex) } for (auto* FlowController : FlowControllers) { - if (WorkerType == ESpatialFunctionalTestWorkerType::All - || (FlowController->WorkerDefinition.Type == WorkerType - && (WorkerId <= FWorkerDefinition::ALL_WORKERS_ID || FlowController->WorkerDefinition.Id == WorkerId))) + if (FlowController != nullptr + && (WorkerType == ESpatialFunctionalTestWorkerType::All + || (FlowController->WorkerDefinition.Type == WorkerType + && (WorkerId <= FWorkerDefinition::ALL_WORKERS_ID || FlowController->WorkerDefinition.Id == WorkerId)))) { FlowControllersExecutingStep.AddUnique(FlowController); } @@ -608,7 +623,7 @@ FSpatialFunctionalTestStepDefinition& ASpatialFunctionalTest::AddStep(const FStr FIsReadyEventFunc IsReadyEvent /*= nullptr*/, FStartEventFunc StartEvent /*= nullptr*/, FTickEventFunc TickEvent /*= nullptr*/, - float StepTimeLimit /*= 0.0f*/) + float StepTimeLimit /*= 20.0f*/) { if (StepName.IsEmpty()) { @@ -662,7 +677,7 @@ void ASpatialFunctionalTest::CrossServerNotifyStepFinished_Implementation(ASpati const FString FlowControllerDisplayName = FlowController->GetDisplayName(); - UE_LOG(LogSpatialGDKFunctionalTests, Display, TEXT("%s finished Step"), *FlowControllerDisplayName); + UE_LOG(LogSpatialGDKFunctionalTests, Verbose, TEXT("%s finished Step in %fs"), *FlowControllerDisplayName, TimeRunningStep); if (FlowControllersExecutingStep.RemoveSwap(FlowController) == 0) { @@ -779,6 +794,30 @@ void ASpatialFunctionalTest::DeleteActorsRegisteredForAutoDestroy() } } +APlayerController* ASpatialFunctionalTest::GetFlowPlayerController() +{ + ASpatialFunctionalTestFlowController* FlowController = GetLocalFlowController(); + if (ensureAlwaysMsgf(IsValid(FlowController), TEXT("FlowController must be valid. You may be calling this on a server."))) + { + return Cast(FlowController->GetOwner()); + } + return nullptr; +} + +APawn* ASpatialFunctionalTest::GetFlowPawn() +{ + APlayerController* PlayerController = GetFlowPlayerController(); + if (IsValid(PlayerController)) + { + APawn* PlayerCharacter = PlayerController->GetPawn(); + if (IsValid(PlayerCharacter)) + { + return PlayerCharacter; + } + } + return nullptr; +} + namespace { USpatialNetDriver* GetNetDriverAndCheckDebuggingEnabled(AActor* Actor) @@ -813,6 +852,10 @@ ULayeredLBStrategy* ASpatialFunctionalTest::GetLoadBalancingStrategy() { return Cast(NetDriver->DebugCtx->DebugStrategy->GetWrappedStrategy()); } + else if (NetDriver->GameplayDebuggerCtx != nullptr) + { + return Cast(NetDriver->GameplayDebuggerCtx->LBStrategy->GetWrappedStrategy()); + } else { return Cast(NetDriver->LoadBalanceStrategy); diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestFlowController.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestFlowController.cpp index a440a67dc5..87040f7751 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestFlowController.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestFlowController.cpp @@ -2,6 +2,7 @@ #include "SpatialFunctionalTestFlowController.h" +#include "EngineClasses/SpatialGameInstance.h" #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" #include "GameFramework/PlayerController.h" @@ -227,6 +228,11 @@ void ASpatialFunctionalTestFlowController::SetReadyToRunTest(bool bIsReady) } } +void ASpatialFunctionalTestFlowController::DeregisterFlowController() +{ + OwningTest->DeregisterFlowController(this); +} + void ASpatialFunctionalTestFlowController::ClientStartStep_Implementation(int StepIndex) { OwningTest->SetCurrentStepIndex(StepIndex); // Just in case we do not get the replication fast enough diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestRequireHandler.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestRequireHandler.cpp index 533511ecc8..c9a33dc050 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestRequireHandler.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestRequireHandler.cpp @@ -74,6 +74,34 @@ FString GetTransformAsString(const FTransform& Transform) } } // namespace +FString GenerateStatusMessage(bool bPassed, FString Received, FString Expected, FString Tolerance = FString(), bool bNotEqual = false) +{ + if (bNotEqual) + { + if (bPassed) + { + return FString::Printf(TEXT("Received (%s), not equal to (%s), as expected"), *Received, *Expected); + } + return FString::Printf(TEXT("Received (%s) but wasn't expecting it"), *Received); + } + if (bPassed) + { + if (Tolerance.IsEmpty()) + { + return FString::Printf(TEXT("Received (%s) as expected"), *Received); + } + return FString::Printf(TEXT("Received (%s) as expected (within tolerance %s of (%s))"), *Received, *Tolerance, *Expected); + } + else + { + if (Tolerance.IsEmpty()) + { + return FString::Printf(TEXT("Received (%s) but was expecting (%s)"), *Received, *Expected); + } + return FString::Printf(TEXT("Received (%s) but was expecting (%s) (tolerance %s)"), *Received, *Expected, *Tolerance); + } +} + SpatialFunctionalTestRequireHandler::SpatialFunctionalTestRequireHandler() : NextOrder(0) { @@ -81,267 +109,228 @@ SpatialFunctionalTestRequireHandler::SpatialFunctionalTestRequireHandler() bool SpatialFunctionalTestRequireHandler::RequireTrue(bool bCheckTrue, const FString& Msg) { - return GenericRequire(Msg, bCheckTrue, FString()); + bool bPassed = bCheckTrue; + FString ReceivedString = bCheckTrue ? TEXT("True") : TEXT("False"); + FString StatusMsg = GenerateStatusMessage(bPassed, ReceivedString, /*Expected =*/TEXT("True")); + + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireFalse(bool bCheckFalse, const FString& Msg) { - return GenericRequire(Msg, !bCheckFalse, FString()); + bool bPassed = !bCheckFalse; + FString ReceivedString = bCheckFalse ? TEXT("True") : TEXT("False"); + FString StatusMsg = GenerateStatusMessage(bPassed, ReceivedString, /*Expected = */ TEXT("False")); + + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireCompare(int A, EComparisonMethod Operator, int B, const FString& Msg) { bool bPassed = Compare(A, Operator, B); + FString StatusMsg; + FString OperatorStr = GetComparisonMethodAsString(Operator); - FString ErrorMsg; - - if (!bPassed) + if (bPassed) { - FString OperatorStr = GetComparisonMethodAsString(Operator); - ErrorMsg = FString::Printf(TEXT("Received %d %s %d but was expecting A %s B"), A, *OperatorStr, B, *OperatorStr); + StatusMsg = FString::Printf(TEXT("Received %d %s %d as expected"), A, *OperatorStr, B); + } + else + { + StatusMsg = FString::Printf(TEXT("Received %d %s %d but was expecting A %s B"), A, *OperatorStr, B, *OperatorStr); } - return GenericRequire(Msg, bPassed, ErrorMsg); + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireCompare(float A, EComparisonMethod Operator, float B, const FString& Msg) { bool bPassed = Compare(A, Operator, B); + FString StatusMsg; + FString OperatorStr = GetComparisonMethodAsString(Operator); - FString ErrorMsg; - - if (!bPassed) + if (bPassed) + { + StatusMsg = FString::Printf(TEXT("Received %f %s %f as expected"), A, *OperatorStr, B); + } + else { - FString OperatorStr = GetComparisonMethodAsString(Operator); - ErrorMsg = FString::Printf(TEXT("Received %f %s %f but was expected A %s B"), A, *OperatorStr, B, *OperatorStr); + StatusMsg = FString::Printf(TEXT("Received %f %s %f but expected A %s B"), A, *OperatorStr, B, *OperatorStr); } - return GenericRequire(Msg, bPassed, ErrorMsg); + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireEqual(bool bValue, bool bExpected, const FString& Msg) { bool bPassed = bValue == bExpected; - FString ErrorMsg; + FString ReceivedString = bValue ? TEXT("True") : TEXT("False"); + FString ExpectedString = bExpected ? TEXT("True") : TEXT("False"); + FString StatusMsg = GenerateStatusMessage(bPassed, ReceivedString, ExpectedString); - if (!bPassed) - { - FString ValueStr = bValue ? TEXT("True") : TEXT("False"); - FString ExpectedStr = bExpected ? TEXT("True") : TEXT("False"); - ErrorMsg = FString::Printf(TEXT("Received %s but was expecting %s"), *ValueStr, *ExpectedStr); - } - - return GenericRequire(Msg, bPassed, ErrorMsg); + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireEqual(int Value, int Expected, const FString& Msg) { bool bPassed = Value == Expected; - FString ErrorMsg; - - if (!bPassed) - { - ErrorMsg = FString::Printf(TEXT("Received %d but was expecting %d"), Value, Expected); - } + FString ReceivedString = FString::Printf(TEXT("%d"), Value); + FString ExpectedString = FString::Printf(TEXT("%d"), Expected); + FString StatusMsg = GenerateStatusMessage(bPassed, ReceivedString, ExpectedString); - return GenericRequire(Msg, bPassed, ErrorMsg); + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireEqual(float Value, float Expected, const FString& Msg, float Tolerance) { bool bPassed = FMath::Abs(Value - Expected) <= Tolerance; - FString ErrorMsg; - - if (!bPassed) - { - ErrorMsg = FString::Printf(TEXT("Received %f but was expecting %f (tolerance %f)"), Value, Expected, Tolerance); - } + FString ReceivedString = FString::Printf(TEXT("%f"), Value); + FString ExpectedString = FString::Printf(TEXT("%f"), Expected); + FString ToleranceString = FString::Printf(TEXT("%f"), Tolerance); + FString StatusMsg = GenerateStatusMessage(bPassed, ReceivedString, ExpectedString, ToleranceString); - return GenericRequire(Msg, bPassed, ErrorMsg); + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireEqual(const FString& Value, const FString& Expected, const FString& Msg) { bool bPassed = Value == Expected; - FString ErrorMsg; + FString StatusMsg = GenerateStatusMessage(bPassed, /*Received = */ Value, /*Expected = */ Expected); - if (!bPassed) - { - ErrorMsg = FString::Printf(TEXT("Received %s but was expecting %s"), *Value, *Expected); - } - - return GenericRequire(Msg, bPassed, ErrorMsg); + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireEqual(const FName& Value, const FName& Expected, const FString& Msg) { bool bPassed = Value == Expected; - FString ErrorMsg; + FString ReceivedString = Value.ToString(); + FString ExpectedString = Expected.ToString(); + FString StatusMsg = GenerateStatusMessage(bPassed, ReceivedString, ExpectedString); - if (!bPassed) - { - ErrorMsg = FString::Printf(TEXT("Received %s but was expecting %s"), *Value.ToString(), *Expected.ToString()); - } - - return GenericRequire(Msg, bPassed, ErrorMsg); + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireEqual(const FVector& Value, const FVector& Expected, const FString& Msg, float Tolerance) { bool bPassed = Value.Equals(Expected, Tolerance); - FString ErrorMsg; - - if (!bPassed) - { - ErrorMsg = FString::Printf(TEXT("Received (%s) but was expecting (%s) (tolerance %f)"), *Value.ToString(), *Expected.ToString(), - Tolerance); - } + FString ReceivedString = Value.ToString(); + FString ExpectedString = Expected.ToString(); + FString ToleranceString = FString::Printf(TEXT("%f"), Tolerance); + FString StatusMsg = GenerateStatusMessage(bPassed, ReceivedString, ExpectedString, ToleranceString); - return GenericRequire(Msg, bPassed, ErrorMsg); + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireEqual(const FRotator& Value, const FRotator& Expected, const FString& Msg, float Tolerance) { bool bPassed = Value.Equals(Expected, Tolerance); - FString ErrorMsg; + FString ReceivedString = Value.ToString(); + FString ExpectedString = Expected.ToString(); + FString ToleranceString = FString::Printf(TEXT("%f"), Tolerance); + FString StatusMsg = GenerateStatusMessage(bPassed, ReceivedString, ExpectedString, ToleranceString); - if (!bPassed) - { - ErrorMsg = FString::Printf(TEXT("Received (%s) but was expecting (%s) (tolerance %f)"), *Value.ToString(), *Expected.ToString(), - Tolerance); - } - - return GenericRequire(Msg, bPassed, ErrorMsg); + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireEqual(const FTransform& Value, const FTransform& Expected, const FString& Msg, float Tolerance) { bool bPassed = Value.Equals(Expected, Tolerance); - FString ErrorMsg; - - if (!bPassed) - { - ErrorMsg = FString::Printf(TEXT("Received {%s} but was expecting {%s} (tolerance %f)"), *GetTransformAsString(Value), - *GetTransformAsString(Expected), Tolerance); - } + FString ReceivedString = FString::Printf(TEXT("%s"), *GetTransformAsString(Value)); + FString ExpectedString = FString::Printf(TEXT("%s"), *GetTransformAsString(Expected)); + FString ToleranceString = FString::Printf(TEXT("%f"), Tolerance); + FString StatusMsg = GenerateStatusMessage(bPassed, ReceivedString, ExpectedString, ToleranceString); - return GenericRequire(Msg, bPassed, ErrorMsg); + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireNotEqual(bool bValue, bool bNotExpected, const FString& Msg) { bool bPassed = bValue != bNotExpected; - FString ErrorMsg; - - if (!bPassed) - { - FString ValueStr = bValue ? TEXT("True") : TEXT("False"); - ErrorMsg = FString::Printf(TEXT("Received %s but wasn't expecting it"), *ValueStr); - } + FString ReceivedString = bValue ? TEXT("True") : TEXT("False"); + FString ExpectedString = bNotExpected ? TEXT("True") : TEXT("False"); + FString StatusMsg = GenerateStatusMessage(bPassed, ReceivedString, ExpectedString, /*Tolerance = */ FString(), /*bNotEqual = */ true); - return GenericRequire(Msg, bPassed, ErrorMsg); + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireNotEqual(int Value, int NotExpected, const FString& Msg) { bool bPassed = Value != NotExpected; - FString ErrorMsg; + FString ReceivedString = FString::Printf(TEXT("%d"), Value); + FString ExpectedString = FString::Printf(TEXT("%d"), NotExpected); + FString StatusMsg = GenerateStatusMessage(bPassed, ReceivedString, ExpectedString, /*Tolerance = */ FString(), /*bNotEqual = */ true); - if (!bPassed) - { - ErrorMsg = FString::Printf(TEXT("Received %d but wasn't expecting it"), Value); - } - - return GenericRequire(Msg, bPassed, ErrorMsg); + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireNotEqual(float Value, float NotExpected, const FString& Msg) { bool bPassed = Value != NotExpected; - FString ErrorMsg; + FString ReceivedString = FString::Printf(TEXT("%f"), Value); + FString ExpectedString = FString::Printf(TEXT("%f"), NotExpected); + FString StatusMsg = GenerateStatusMessage(bPassed, ReceivedString, ExpectedString, /*Tolerance = */ FString(), /*bNotEqual = */ true); - if (!bPassed) - { - ErrorMsg = FString::Printf(TEXT("Received %f but wasn't expecting it"), Value); - } - - return GenericRequire(Msg, bPassed, ErrorMsg); + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireNotEqual(const FString& Value, const FString& NotExpected, const FString& Msg) { bool bPassed = Value != NotExpected; - FString ErrorMsg; - - if (!bPassed) - { - ErrorMsg = FString::Printf(TEXT("Received %s but wasn't expecting it"), *Value); - } + FString StatusMsg = GenerateStatusMessage(bPassed, /*Received = */ Value, /*Expected = */ NotExpected, /*Tolerance = */ FString(), + /*bNotEqual = */ true); - return GenericRequire(Msg, bPassed, ErrorMsg); + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireNotEqual(const FName& Value, const FName& NotExpected, const FString& Msg) { bool bPassed = Value != NotExpected; - FString ErrorMsg; + FString ReceivedString = FString::Printf(TEXT("%s"), *Value.ToString()); + FString ExpectedString = FString::Printf(TEXT("%s"), *NotExpected.ToString()); + FString StatusMsg = GenerateStatusMessage(bPassed, ReceivedString, ExpectedString, /*Tolerance = */ FString(), /*bNotEqual = */ true); - if (!bPassed) - { - ErrorMsg = FString::Printf(TEXT("Received %s but wasn't expecting it"), *Value.ToString()); - } - - return GenericRequire(Msg, bPassed, ErrorMsg); + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireNotEqual(const FVector& Value, const FVector& NotExpected, const FString& Msg) { bool bPassed = Value != NotExpected; - FString ErrorMsg; + FString ReceivedString = FString::Printf(TEXT("%s"), *Value.ToString()); + FString ExpectedString = FString::Printf(TEXT("%s"), *NotExpected.ToString()); + FString StatusMsg = GenerateStatusMessage(bPassed, ReceivedString, ExpectedString, /*Tolerance = */ FString(), /*bNotEqual = */ true); - if (!bPassed) - { - ErrorMsg = FString::Printf(TEXT("Received (%s) but wasn't expecting it"), *Value.ToString()); - } - - return GenericRequire(Msg, bPassed, ErrorMsg); + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireNotEqual(const FRotator& Value, const FRotator& NotExpected, const FString& Msg) { bool bPassed = Value != NotExpected; - FString ErrorMsg; - - if (!bPassed) - { - ErrorMsg = FString::Printf(TEXT("Received (%s) but wasn't expecting it"), *Value.ToString()); - } + FString ReceivedString = FString::Printf(TEXT("%s"), *Value.ToString()); + FString ExpectedString = FString::Printf(TEXT("%s"), *NotExpected.ToString()); + FString StatusMsg = GenerateStatusMessage(bPassed, ReceivedString, ExpectedString, /*Tolerance = */ FString(), /*bNotEqual = */ true); - return GenericRequire(Msg, bPassed, ErrorMsg); + return GenericRequire(Msg, bPassed, StatusMsg); } bool SpatialFunctionalTestRequireHandler::RequireNotEqual(const FTransform& Value, const FTransform& NotExpected, const FString& Msg) { bool bPassed = !Value.Equals(NotExpected); - FString ErrorMsg; - - if (!bPassed) - { - ErrorMsg = FString::Printf(TEXT("Received {%s} but wasn't expecting it"), *GetTransformAsString(Value)); - } + FString ReceivedString = FString::Printf(TEXT("%s"), *GetTransformAsString(Value)); + FString ExpectedString = FString::Printf(TEXT("%s"), *GetTransformAsString(NotExpected)); + FString StatusMsg = GenerateStatusMessage(bPassed, ReceivedString, ExpectedString, /*Tolerance = */ FString(), /*bNotEqual = */ true); - return GenericRequire(Msg, bPassed, ErrorMsg); + return GenericRequire(Msg, bPassed, StatusMsg); } -bool SpatialFunctionalTestRequireHandler::GenericRequire(const FString& Msg, bool bPassed, const FString& ErrorMsg) +bool SpatialFunctionalTestRequireHandler::GenericRequire(const FString& Msg, bool bPassed, const FString& StatusMsg) { ensureMsgf(!Msg.IsEmpty(), TEXT("Requires cannot have an empty message")); FSpatialFunctionalTestRequire Require; Require.Msg = Msg; Require.bPassed = bPassed; - Require.ErrorMsg = ErrorMsg; + Require.StatusMsg = StatusMsg; Require.Order = NextOrder++; Requires.Add(Msg, Require); @@ -371,13 +360,13 @@ void SpatialFunctionalTestRequireHandler::LogAndClearStepRequires() FString Msg; if (Require.bPassed) { - Msg = FString::Printf(TEXT("%s [Passed] %s"), *WorkerName, *Require.Msg); + Msg = FString::Printf(TEXT("%s [Passed] %s : \"%s\""), *WorkerName, *Require.Msg, *Require.StatusMsg); UE_VLOG(nullptr, LogSpatialGDKFunctionalTests, Display, TEXT("%s"), *Msg); UE_LOG(LogSpatialGDKFunctionalTests, Display, TEXT("%s"), *Msg); } else { - Msg = FString::Printf(TEXT("%s [Failed] %s : %s"), *WorkerName, *Require.Msg, *Require.ErrorMsg); + Msg = FString::Printf(TEXT("%s [Failed] %s : %s"), *WorkerName, *Require.Msg, *Require.StatusMsg); UE_VLOG(nullptr, LogSpatialGDKFunctionalTests, Error, TEXT("%s"), *Msg); UE_LOG(LogSpatialGDKFunctionalTests, Error, TEXT("%s"), *Msg); } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/TestMaps/Spatial2WorkerSmallInterestMap.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/TestMaps/Spatial2WorkerSmallInterestMap.cpp index ffbd68d28f..14f0e2f866 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/TestMaps/Spatial2WorkerSmallInterestMap.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/TestMaps/Spatial2WorkerSmallInterestMap.cpp @@ -3,6 +3,7 @@ #include "TestMaps/Spatial2WorkerSmallInterestMap.h" #include "EngineClasses/SpatialWorldSettings.h" #include "GameFramework/PlayerStart.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/AlwaysInterestedTest/AlwaysInterestedTest.h" #include "SpatialGDKFunctionalTests/SpatialGDK/SpatialCleanupConnectionTest/SpatialCleanupConnectionTest.h" #include "SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/SpatialTestHandoverActorComponentReplication.h" #include "TestWorkerSettings.h" @@ -10,22 +11,26 @@ USpatial2WorkerSmallInterestMap::USpatial2WorkerSmallInterestMap() : UGeneratedTestMap(EMapCategory::CI_PREMERGE_SPATIAL_ONLY, TEXT("Spatial2WorkerSmallInterestMap")) { + SetNumberOfClients(2); } void USpatial2WorkerSmallInterestMap::CreateCustomContentForMap() { ULevel* CurrentLevel = World->GetCurrentLevel(); + FVector Server1Pos(-50.f, -50.f, 0.f); + // Add the tests AddActorToLevel( - CurrentLevel, FTransform(FVector(-50, -50, 0))); // Seems like this position is required so that the LB plays nicely? + CurrentLevel, FTransform(Server1Pos)); // Seems like this position is required so that the LB plays nicely? AddActorToLevel(CurrentLevel, FTransform::Identity); + AddActorToLevel(CurrentLevel, FTransform(Server1Pos)); // Quirk of the test. We need the player spawns on the same portion of the map as the test, so they are LBed together AActor** PlayerStart = CurrentLevel->Actors.FindByPredicate([](AActor* Actor) { return Actor->GetClass() == APlayerStart::StaticClass(); }); - (*PlayerStart)->SetActorLocation(FVector(-50, -50, 100)); + (*PlayerStart)->SetActorLocation(Server1Pos); ASpatialWorldSettings* WorldSettings = CastChecked(World->GetWorldSettings()); WorldSettings->SetMultiWorkerSettingsClass(UTest1x2SmallInterestWorkerSettings::StaticClass()); diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/TestMaps/SpatialNetworkingMap.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/TestMaps/SpatialNetworkingMap.cpp index 6ad173f51a..fde6d90c87 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/TestMaps/SpatialNetworkingMap.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/TestMaps/SpatialNetworkingMap.cpp @@ -1,8 +1,13 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "TestMaps/SpatialNetworkingMap.h" + +#include "SpatialGDK/StaticSubobjectsTest/StaticSubobjectTestActor.h" +#include "SpatialGDK/StaticSubobjectsTest/StaticSubobjectsTest.h" #include "SpatialGDKFunctionalTests/SpatialGDK/DormancyAndTombstoneTest/DormancyAndTombstoneTest.h" #include "SpatialGDKFunctionalTests/SpatialGDK/DormancyAndTombstoneTest/DormancyTestActor.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubObjectTestActor.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubObjectsTest.h" #include "SpatialGDKFunctionalTests/SpatialGDK/RegisterAutoDestroyActorsTest/RegisterAutoDestroyActorsTest.h" #include "SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestPossession.h" #include "SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestRepossession.h" @@ -35,9 +40,13 @@ void USpatialNetworkingMap::CreateCustomContentForMap() AddActorToLevel(CurrentLevel, FTransform::Identity); AddActorToLevel(CurrentLevel, FTransform::Identity); AddActorToLevel(CurrentLevel, FTransform::Identity); + AddActorToLevel(CurrentLevel, FTransform::Identity); + AddActorToLevel(CurrentLevel, FTransform::Identity); // Add test helpers // Unfortunately, the nature of some tests requires them to have actors placed in the level, to trigger some Unreal behavior AddActorToLevel(CurrentLevel, FTransform::Identity); AddActorToLevel(CurrentLevel, FTransform::Identity); + AddActorToLevel(CurrentLevel, FTransform::Identity); + AddActorToLevel(CurrentLevel, FTransform(FVector(-20000.0f, -20000.0f, 40.0f))); } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/TestMaps/SpatialPlayerDisconnectMap.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/TestMaps/SpatialPlayerDisconnectMap.cpp new file mode 100644 index 0000000000..742701de39 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/TestMaps/SpatialPlayerDisconnectMap.cpp @@ -0,0 +1,30 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "TestMaps/SpatialPlayerDisconnectMap.h" +#include "EngineClasses/SpatialWorldSettings.h" +#include "GameFramework/PlayerStart.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/SpatialPlayerDisconnectGameMode.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/SpatialTestPlayerDisconnect.h" +#include "TestWorkerSettings.h" + +USpatialPlayerDisconnectMap::USpatialPlayerDisconnectMap() + : UGeneratedTestMap(EMapCategory::CI_PREMERGE, TEXT("SpatialPlayerDisconnectMap")) +{ +} + +void USpatialPlayerDisconnectMap::CreateCustomContentForMap() +{ + ULevel* CurrentLevel = World->GetCurrentLevel(); + + // Quirk of the test. We need the player spawn close to the ground so that they do not generate an intermittent error when falling + AActor** PlayerStart = CurrentLevel->Actors.FindByPredicate([](AActor* Actor) { + return Actor->GetClass() == APlayerStart::StaticClass(); + }); + (*PlayerStart)->SetActorLocation(FVector(-190, 0, 50)); + + // Add the test + ASpatialTestPlayerDisconnect& TriggerTest = AddActorToLevel(CurrentLevel, FTransform::Identity); + + ASpatialWorldSettings* WorldSettings = CastChecked(World->GetWorldSettings()); + WorldSettings->DefaultGameMode = ASpatialPlayerDisconnectGameMode::StaticClass(); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/TestMaps/SpatialRoutingWorkerMap.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/TestMaps/SpatialRoutingWorkerMap.cpp new file mode 100644 index 0000000000..9ee73f5f52 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/TestMaps/SpatialRoutingWorkerMap.cpp @@ -0,0 +1,34 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "TestMaps/SpatialRoutingWorkerMap.h" +#include "EngineClasses/SpatialWorldSettings.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/SpatialTestEntityInteration/EntityInteractionTest.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/SpatialTestEntityInteration/EntityInteractionTestActor.h" +#include "TestWorkerSettings.h" + +USpatialRoutingWorkerMap::USpatialRoutingWorkerMap() + : UGeneratedTestMap(EMapCategory::CI_PREMERGE_SPATIAL_ONLY, TEXT("SpatialRoutingWorkerMap")) +{ + SetNumberOfClients(2); + // clang-format off + SetCustomConfig(TEXT("[/Script/SpatialGDK.SpatialGDKSettings]") LINE_TERMINATOR + TEXT("CrossServerRPCImplementation=RoutingWorker")); + // clang-format on +} + +void USpatialRoutingWorkerMap::CreateCustomContentForMap() +{ + ULevel* CurrentLevel = World->GetCurrentLevel(); + + FTransform Server1Pos(FVector(250, -250, 0)); + FTransform Server2Pos(FVector(-250, 250, 0)); + + AddActorToLevel(CurrentLevel, Server2Pos).Index = 0; + AddActorToLevel(CurrentLevel, Server2Pos).Index = 1; + AddActorToLevel(CurrentLevel, Server1Pos).Index = 0; + AddActorToLevel(CurrentLevel, Server1Pos).Index = 1; + AddActorToLevel(CurrentLevel, FTransform::Identity); + + ASpatialWorldSettings* WorldSettings = CastChecked(World->GetWorldSettings()); + WorldSettings->SetMultiWorkerSettingsClass(UTest1x2FullInterestWorkerSettings::StaticClass()); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTest.h index 1776df32dd..0cd24c4f52 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTest.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTest.h @@ -107,6 +107,7 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialFunctionalTest : public AFunctionalT // # FlowController related APIs. void RegisterFlowController(ASpatialFunctionalTestFlowController* FlowController); + void DeregisterFlowController(ASpatialFunctionalTestFlowController* FlowController); // Get all the FlowControllers registered in this Test. const TArray& GetFlowControllers() const { return FlowControllers; } @@ -142,7 +143,7 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialFunctionalTest : public AFunctionalT ToolTip = "Adds a Test Step. Check GetAllWorkers(), GetAllServerWorkers() and GetAllClientWorkers() for convenience.\n\nIf you split the Worker pin you can define if you want to run on Server, Client or All.\n\nWorker Ids start from 1.\nIf you pass 0 it will run on all the Servers / Clients (there's also a convenience function GetAllWorkersId())\n\nIf you choose WorkerType 'All' it runs on all Servers and Clients (hence WorkerId is ignored).\n\nKeep in mind you can split the Worker pin for convenience.")) // clang-format on void AddStepBlueprint(const FString& StepName, const FWorkerDefinition& Worker, const FStepIsReadyDelegate& IsReadyEvent, - const FStepStartDelegate& StartEvent, const FStepTickDelegate& TickEvent, float StepTimeLimit = 0.0f); + const FStepStartDelegate& StartEvent, const FStepTickDelegate& TickEvent, float StepTimeLimit = 20.0f); // Add Steps for Blueprints and C++. @@ -167,7 +168,7 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialFunctionalTest : public AFunctionalT */ FSpatialFunctionalTestStepDefinition& AddStep(const FString& StepName, const FWorkerDefinition& Worker, FIsReadyEventFunc IsReadyEvent = nullptr, FStartEventFunc StartEvent = nullptr, - FTickEventFunc TickEvent = nullptr, float StepTimeLimit = 0.0f); + FTickEventFunc TickEvent = nullptr, float StepTimeLimit = 20.0f); // Start Running a Step. void StartStep(const int StepIndex); @@ -295,25 +296,25 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialFunctionalTest : public AFunctionalT bool RequireNotEqual_Bool(bool bValue, bool bNotExpected, const FString& Msg) { return RequireHandler.RequireNotEqual(bValue, bNotExpected, Msg); } UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Not Equal (Int)"), Category = "Spatial Functional Test") - bool RequireNotEqual_Int(int Value, int Expected, const FString& Msg) { return RequireHandler.RequireNotEqual(Value, Expected, Msg); } + bool RequireNotEqual_Int(int Value, int NotExpected, const FString& Msg) { return RequireHandler.RequireNotEqual(Value, NotExpected, Msg); } UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Not Equal (Float)"), Category = "Spatial Functional Test") - bool RequireNotEqual_Float(float Value, float Expected, const FString& Msg) { return RequireHandler.RequireNotEqual(Value, Expected, Msg); } + bool RequireNotEqual_Float(float Value, float NotExpected, const FString& Msg) { return RequireHandler.RequireNotEqual(Value, NotExpected, Msg); } UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Not Equal (String)"), Category = "Spatial Functional Test") - bool RequireNotEqual_String(const FString& Value, const FString& Expected, const FString& Msg) { return RequireHandler.RequireNotEqual(Value, Expected, Msg); } + bool RequireNotEqual_String(const FString& Value, const FString& NotExpected, const FString& Msg) { return RequireHandler.RequireNotEqual(Value, NotExpected, Msg); } UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Not Equal (Name)"), Category = "Spatial Functional Test") - bool RequireNotEqual_Name(const FName& Value, const FName& Expected, const FString& Msg) { return RequireHandler.RequireNotEqual(Value, Expected, Msg); } + bool RequireNotEqual_Name(const FName& Value, const FName& NotExpected, const FString& Msg) { return RequireHandler.RequireNotEqual(Value, NotExpected, Msg); } UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Not Equal (Vector)"), Category = "Spatial Functional Test") - bool RequireNotEqual_Vector(const FVector& Value, const FVector& Expected, const FString& Msg) { return RequireHandler.RequireNotEqual(Value, Expected, Msg); } + bool RequireNotEqual_Vector(const FVector& Value, const FVector& NotExpected, const FString& Msg) { return RequireHandler.RequireNotEqual(Value, NotExpected, Msg); } UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Not Equal (Rotator)"), Category = "Spatial Functional Test") - bool RequireNotEqual_Rotator(const FRotator& Value, const FRotator& Expected, const FString& Msg) { return RequireHandler.RequireNotEqual(Value, Expected, Msg); } + bool RequireNotEqual_Rotator(const FRotator& Value, const FRotator& NotExpected, const FString& Msg) { return RequireHandler.RequireNotEqual(Value, NotExpected, Msg); } UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Not Equal (Transform)"), Category = "Spatial Functional Test") - bool RequireNotEqual_Transform(const FTransform& Value, const FTransform& Expected, const FString& Msg) { return RequireHandler.RequireNotEqual(Value, Expected, Msg); } + bool RequireNotEqual_Transform(const FTransform& Value, const FTransform& NotExpected, const FString& Msg) { return RequireHandler.RequireNotEqual(Value, NotExpected, Msg); } // clang-format on // # Snapshot APIs. @@ -336,6 +337,18 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialFunctionalTest : public AFunctionalT UFUNCTION(BlueprintPure, Category = "Spatial Functional Test") bool WasLoadedFromTakenSnapshot(); + template + static int GetNumberOfActorsOfType(UWorld* World) + { + int Counter = 0; + for (TActorIterator Iter(World); Iter; ++Iter) + { + Counter++; + } + + return Counter; + } + // Get the path of the taken snapshot for this world's map. Returns an empty string if it's using the default snapshot. static FString GetTakenSnapshotPath(UWorld* World); @@ -348,6 +361,12 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialFunctionalTest : public AFunctionalT // Clears all the snapshots taken, not meant to be used directly. static void ClearAllTakenSnapshots(); + // Get the player controller owned by the current flow controller. + APlayerController* GetFlowPlayerController(); + + // Get the pawn that belongs to the PlayerController associated with the current flow controller. + APawn* GetFlowPawn(); + protected: int GetNumExpectedServers() const { return NumExpectedServers; } void DeleteActorsRegisteredForAutoDestroy(); diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestFlowController.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestFlowController.h index 6886384211..e3254d970c 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestFlowController.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestFlowController.h @@ -78,6 +78,9 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialFunctionalTestFlowController : publi // Let's you know if the owning worker has acknowledged the FinishTest flow. bool HasAckFinishedTest() const { return bHasAckFinishedTest; } + UFUNCTION() + void DeregisterFlowController(); + private: // Current Step being executed SpatialFunctionalTestStep CurrentStep; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestRequireHandler.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestRequireHandler.h index b32583120d..a4bf83d423 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestRequireHandler.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestRequireHandler.h @@ -11,7 +11,7 @@ struct FSpatialFunctionalTestRequire { FString Msg; bool bPassed; - FString ErrorMsg; + FString StatusMsg; uint32 Order; // Used to be able to log the messages in the same order they occurred at the end. }; @@ -54,7 +54,7 @@ class SPATIALGDKFUNCTIONALTESTS_API bool RequireNotEqual(const FRotator& Value, const FRotator& NotExpected, const FString& Msg); bool RequireNotEqual(const FTransform& Value, const FTransform& NotExpected, const FString& Msg); - bool GenericRequire(const FString& Key, bool bPassed, const FString& ErrorMsg); + bool GenericRequire(const FString& Key, bool bPassed, const FString& StatusMsg); void LogAndClearStepRequires(); diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestStep.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestStep.h index de1ae25b98..dfd6d28fd7 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestStep.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestStep.h @@ -76,7 +76,7 @@ struct FSpatialFunctionalTestStepDefinition */ FSpatialFunctionalTestStepDefinition(bool bIsNative = false) : bIsNativeDefinition(bIsNative) - , TimeLimit(0.0f) + , TimeLimit(20.0f) { } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/TestMaps/SpatialPlayerDisconnectMap.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/TestMaps/SpatialPlayerDisconnectMap.h new file mode 100644 index 0000000000..eff35d7c3c --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/TestMaps/SpatialPlayerDisconnectMap.h @@ -0,0 +1,21 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "TestMaps/GeneratedTestMap.h" +#include "SpatialPlayerDisconnectMap.generated.h" + +/** + * This map is custom-made for the SpatialPlayerDisconnectTest - it utilizes a gamemode override. + */ +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API USpatialPlayerDisconnectMap : public UGeneratedTestMap +{ + GENERATED_BODY() + +public: + USpatialPlayerDisconnectMap(); + +protected: + virtual void CreateCustomContentForMap() override; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/TestMaps/SpatialRoutingWorkerMap.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/TestMaps/SpatialRoutingWorkerMap.h new file mode 100644 index 0000000000..6be7bbcf18 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/TestMaps/SpatialRoutingWorkerMap.h @@ -0,0 +1,21 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "TestMaps/GeneratedTestMap.h" +#include "SpatialRoutingWorkerMap.generated.h" + +/** + * This map is a simple 2-server-worker map, where both workers see everything in the world. + */ +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API USpatialRoutingWorkerMap : public UGeneratedTestMap +{ + GENERATED_BODY() + +public: + USpatialRoutingWorkerMap(); + +protected: + virtual void CreateCustomContentForMap() override; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/TestWorkerSettings.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/TestWorkerSettings.h index a249943158..6f90b2d0eb 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/TestWorkerSettings.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/TestWorkerSettings.h @@ -19,7 +19,7 @@ * UTest2x1FullInterestGridStrategy * UTest2x2FullInterestGridStrategy */ -UCLASS() +UCLASS(NotBlueprintable, HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UTest1x2FullInterestGridStrategy : public UGridBasedLBStrategy { GENERATED_BODY() @@ -34,7 +34,7 @@ class SPATIALGDKFUNCTIONALTESTS_API UTest1x2FullInterestGridStrategy : public UG } }; -UCLASS() +UCLASS(NotBlueprintable, HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UTest2x1FullInterestGridStrategy : public UGridBasedLBStrategy { GENERATED_BODY() @@ -49,7 +49,7 @@ class SPATIALGDKFUNCTIONALTESTS_API UTest2x1FullInterestGridStrategy : public UG } }; -UCLASS() +UCLASS(NotBlueprintable, HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UTest2x2FullInterestGridStrategy : public UGridBasedLBStrategy { GENERATED_BODY() @@ -72,7 +72,7 @@ class SPATIALGDKFUNCTIONALTESTS_API UTest2x2FullInterestGridStrategy : public UG * UTest2x1SmallInterestGridStrategy * UTest2x2SmallInterestGridStrategy */ -UCLASS() +UCLASS(NotBlueprintable, HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UTest1x2SmallInterestGridStrategy : public UTest1x2FullInterestGridStrategy { GENERATED_BODY() @@ -81,7 +81,7 @@ class SPATIALGDKFUNCTIONALTESTS_API UTest1x2SmallInterestGridStrategy : public U UTest1x2SmallInterestGridStrategy() { InterestBorder = 150.0f; } }; -UCLASS() +UCLASS(NotBlueprintable, HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UTest2x1SmallInterestGridStrategy : public UTest2x1FullInterestGridStrategy { GENERATED_BODY() @@ -90,7 +90,7 @@ class SPATIALGDKFUNCTIONALTESTS_API UTest2x1SmallInterestGridStrategy : public U UTest2x1SmallInterestGridStrategy() { InterestBorder = 150.0f; } }; -UCLASS() +UCLASS(NotBlueprintable, HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UTest2x2SmallInterestGridStrategy : public UTest2x2FullInterestGridStrategy { GENERATED_BODY() @@ -107,7 +107,7 @@ class SPATIALGDKFUNCTIONALTESTS_API UTest2x2SmallInterestGridStrategy : public U * UTest2x1NoInterestGridStrategy * UTest2x2NoInterestGridStrategy */ -UCLASS() +UCLASS(NotBlueprintable, HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UTest1x2NoInterestGridStrategy : public UTest1x2FullInterestGridStrategy { GENERATED_BODY() @@ -116,7 +116,7 @@ class SPATIALGDKFUNCTIONALTESTS_API UTest1x2NoInterestGridStrategy : public UTes UTest1x2NoInterestGridStrategy() { InterestBorder = 0.0f; } }; -UCLASS() +UCLASS(NotBlueprintable, HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UTest2x1NoInterestGridStrategy : public UTest2x1FullInterestGridStrategy { GENERATED_BODY() @@ -125,7 +125,7 @@ class SPATIALGDKFUNCTIONALTESTS_API UTest2x1NoInterestGridStrategy : public UTes UTest2x1NoInterestGridStrategy() { InterestBorder = 0.0f; } }; -UCLASS() +UCLASS(NotBlueprintable, HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UTest2x2NoInterestGridStrategy : public UTest2x2FullInterestGridStrategy { GENERATED_BODY() @@ -137,7 +137,7 @@ class SPATIALGDKFUNCTIONALTESTS_API UTest2x2NoInterestGridStrategy : public UTes /** * Worker settings that use the above LB strategies. */ -UCLASS() +UCLASS(NotBlueprintable, HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UTest1x2FullInterestWorkerSettings : public USpatialMultiWorkerSettings { GENERATED_BODY() @@ -146,7 +146,7 @@ class SPATIALGDKFUNCTIONALTESTS_API UTest1x2FullInterestWorkerSettings : public UTest1x2FullInterestWorkerSettings() { WorkerLayers[0].LoadBalanceStrategy = UTest1x2FullInterestGridStrategy::StaticClass(); } }; -UCLASS() +UCLASS(NotBlueprintable, HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UTest2x1FullInterestWorkerSettings : public USpatialMultiWorkerSettings { GENERATED_BODY() @@ -155,7 +155,7 @@ class SPATIALGDKFUNCTIONALTESTS_API UTest2x1FullInterestWorkerSettings : public UTest2x1FullInterestWorkerSettings() { WorkerLayers[0].LoadBalanceStrategy = UTest2x1FullInterestGridStrategy::StaticClass(); } }; -UCLASS() +UCLASS(NotBlueprintable, HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UTest2x2FullInterestWorkerSettings : public USpatialMultiWorkerSettings { GENERATED_BODY() @@ -164,7 +164,7 @@ class SPATIALGDKFUNCTIONALTESTS_API UTest2x2FullInterestWorkerSettings : public UTest2x2FullInterestWorkerSettings() { WorkerLayers[0].LoadBalanceStrategy = UTest2x2FullInterestGridStrategy::StaticClass(); } }; -UCLASS() +UCLASS(NotBlueprintable, HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UTest1x2SmallInterestWorkerSettings : public USpatialMultiWorkerSettings { GENERATED_BODY() @@ -173,7 +173,7 @@ class SPATIALGDKFUNCTIONALTESTS_API UTest1x2SmallInterestWorkerSettings : public UTest1x2SmallInterestWorkerSettings() { WorkerLayers[0].LoadBalanceStrategy = UTest1x2SmallInterestGridStrategy::StaticClass(); } }; -UCLASS() +UCLASS(NotBlueprintable, HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UTest2x1SmallInterestWorkerSettings : public USpatialMultiWorkerSettings { GENERATED_BODY() @@ -182,7 +182,7 @@ class SPATIALGDKFUNCTIONALTESTS_API UTest2x1SmallInterestWorkerSettings : public UTest2x1SmallInterestWorkerSettings() { WorkerLayers[0].LoadBalanceStrategy = UTest2x1SmallInterestGridStrategy::StaticClass(); } }; -UCLASS() +UCLASS(NotBlueprintable, HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UTest2x2SmallInterestWorkerSettings : public USpatialMultiWorkerSettings { GENERATED_BODY() @@ -191,7 +191,7 @@ class SPATIALGDKFUNCTIONALTESTS_API UTest2x2SmallInterestWorkerSettings : public UTest2x2SmallInterestWorkerSettings() { WorkerLayers[0].LoadBalanceStrategy = UTest2x2SmallInterestGridStrategy::StaticClass(); } }; -UCLASS() +UCLASS(NotBlueprintable, HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UTest1x2NoInterestWorkerSettings : public USpatialMultiWorkerSettings { GENERATED_BODY() @@ -200,7 +200,7 @@ class SPATIALGDKFUNCTIONALTESTS_API UTest1x2NoInterestWorkerSettings : public US UTest1x2NoInterestWorkerSettings() { WorkerLayers[0].LoadBalanceStrategy = UTest1x2NoInterestGridStrategy::StaticClass(); } }; -UCLASS() +UCLASS(NotBlueprintable, HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UTest2x1NoInterestWorkerSettings : public USpatialMultiWorkerSettings { GENERATED_BODY() @@ -209,7 +209,7 @@ class SPATIALGDKFUNCTIONALTESTS_API UTest2x1NoInterestWorkerSettings : public US UTest2x1NoInterestWorkerSettings() { WorkerLayers[0].LoadBalanceStrategy = UTest2x1NoInterestGridStrategy::StaticClass(); } }; -UCLASS() +UCLASS(NotBlueprintable, HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UTest2x2NoInterestWorkerSettings : public USpatialMultiWorkerSettings { GENERATED_BODY() diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/AlwaysInterestedTest/AlwaysInterestedTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/AlwaysInterestedTest/AlwaysInterestedTest.cpp new file mode 100644 index 0000000000..bc2dad8be1 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/AlwaysInterestedTest/AlwaysInterestedTest.cpp @@ -0,0 +1,369 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "AlwaysInterestedTest.h" +#include "SpatialFunctionalTestFlowController.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/AlwaysInterestedTestActors.h" + +#include "LoadBalancing/LayeredLBStrategy.h" + +#include "Net/UnrealNetwork.h" + +/** + * This test tests that AlwaysInterested UProperties are replicated to clients and servers correctly. + * + * The test should include two servers and two clients + * The flow is as follows: + * - Setup: + * - Server 1 spawns an actor with AlwaysInterested properties, and two test actors, one of which is set to be AlwaysInterested + * - Test: + * - Move the test actors to server 2 authority area, to ensure interest on server 1 is lost via authority. + * - Validate actors are correctly in or out of view for auth and non-auth servers, and owning and non-owning clients. + * - Move the test actors to server 1 authority area, to ensure interest on server 2 is lost via authority. + * - Validate actors are correctly in the view of auth and non-auth servers. + * - Cleanup: + * - Destroy the actors + */ + +AAlwaysInterestedTest::AAlwaysInterestedTest() + : Super() +{ + Author = "Mike"; + Description = TEXT("Test Always Interested Functionality"); +} + +void AAlwaysInterestedTest::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ThisClass, ActorWithAlwaysInterestedProperty); + DOREPLIFETIME(ThisClass, InterestedInThisReplicatedActor); + DOREPLIFETIME(ThisClass, NotInterestedInThisReplicatedActor); + DOREPLIFETIME(ThisClass, OtherInterestedInThisReplicatedActor); +} + +void AAlwaysInterestedTest::PrepareTest() +{ + Super::PrepareTest(); + + const float StepTimeLimit = 5.0f; + + { // Step 0 - Cache worker positions, and spawn actors on auth server + AddStep(TEXT("AlwaysInterested_SpawnActors"), FWorkerDefinition::AllServers, nullptr, [this]() { + ULayeredLBStrategy* RootStrategy = GetLoadBalancingStrategy(); + UAbstractLBStrategy* DefaultStrategy = RootStrategy->GetLBStrategyForLayer(SpatialConstants::DefaultLayer); + UGridBasedLBStrategy* GridStrategy = Cast(DefaultStrategy); + AssertIsValid(GridStrategy, TEXT("Invalid LBS")); + const UGridBasedLBStrategy::LBStrategyRegions WorkerRegions = GridStrategy->GetLBStrategyRegions(); + const VirtualWorkerId LocalWorker = GridStrategy->GetLocalVirtualWorkerId(); + + auto GetWorkerPosition = [&](bool bLocalWorker) -> FVector { + for (const auto& WorkerRegion : WorkerRegions) + { + if ((bLocalWorker && WorkerRegion.Key == LocalWorker) || (!bLocalWorker && WorkerRegion.Key != LocalWorker)) + { + const FVector2D Centre = WorkerRegion.Value.GetCenter(); + return FVector{ Centre.X, Centre.Y, 0.f }; + } + } + return {}; + }; + + LocalWorkerPosition = GetWorkerPosition(true); + OtherWorkerPosition = GetWorkerPosition(false); + + if (HasAuthority()) + { + ActorWithAlwaysInterestedProperty = + GetWorld()->SpawnActor(LocalWorkerPosition, FRotator::ZeroRotator, FActorSpawnParameters()); + + InterestedInThisReplicatedActor = + GetWorld()->SpawnActor(LocalWorkerPosition, FRotator::ZeroRotator, FActorSpawnParameters()); + + NotInterestedInThisReplicatedActor = + GetWorld()->SpawnActor(LocalWorkerPosition, FRotator::ZeroRotator, FActorSpawnParameters()); + + // This actor is used later as a replacement for InterestedInThisReplicatedActor, so isn't immediate added to + // AlwaysInterested + OtherInterestedInThisReplicatedActor = + GetWorld()->SpawnActor(LocalWorkerPosition, FRotator::ZeroRotator, FActorSpawnParameters()); + + ActorWithAlwaysInterestedProperty->InterestedActors.Push(InterestedInThisReplicatedActor); + + AController* PlayerController1 = + Cast(GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1)->GetOwner()); + if (!AssertTrue(IsValid(PlayerController1), TEXT("Should have spawned a PlayerController 1"))) + { + return; + } + + if (!AssertTrue(PlayerController1->HasAuthority(), TEXT("Should have authority over the PlayerController 1"))) + { + return; + } + + AController* PlayerController2 = + Cast(GetFlowController(ESpatialFunctionalTestWorkerType::Client, 2)->GetOwner()); + if (!AssertTrue(IsValid(PlayerController2), TEXT("Should have spawned a PlayerController 2"))) + { + return; + } + + if (!AssertTrue(PlayerController2->HasAuthority(), TEXT("Should have authority over the PlayerController 2"))) + { + return; + } + + // Move PCs so they are roughly at worker's position, but with an offset to not trigger the interest of each other, + // or the SmallNCDActors located at the worker's position. + OriginalPawn1Position = PlayerController1->GetPawn()->GetActorLocation(); + OriginalPawn2Position = PlayerController2->GetPawn()->GetActorLocation(); + PlayerController1->GetPawn()->SetActorLocation(LocalWorkerPosition + FVector(200.f, 0.f, 0.f)); + PlayerController2->GetPawn()->SetActorLocation(LocalWorkerPosition - FVector(200.f, 0.f, 0.f)); + + ActorWithAlwaysInterestedProperty->SetOwner(PlayerController1); + + RegisterAutoDestroyActor(ActorWithAlwaysInterestedProperty); + RegisterAutoDestroyActor(InterestedInThisReplicatedActor); + RegisterAutoDestroyActor(NotInterestedInThisReplicatedActor); + RegisterAutoDestroyActor(OtherInterestedInThisReplicatedActor); + } + + FinishStep(); + }); + } + + { // Step 1 - Move actors to server 2 + AddStep( + TEXT("AlwaysInterested_MoveActors"), FWorkerDefinition::Server(1), + [this]() -> bool { + return (ActorWithAlwaysInterestedProperty->IsActorReady() && InterestedInThisReplicatedActor->IsActorReady() + && NotInterestedInThisReplicatedActor->IsActorReady() && OtherInterestedInThisReplicatedActor->IsActorReady()); + }, + [this]() { + // Move first two interested actors to server 2's area + AssertTrue(InterestedInThisReplicatedActor->HasAuthority(), + TEXT("Must have authority over InterestedInThisReplicatedActor")); + AssertTrue(NotInterestedInThisReplicatedActor->HasAuthority(), + TEXT("Must have authority over NotInterestedInThisReplicatedActor")); + InterestedInThisReplicatedActor->SetActorLocation(OtherWorkerPosition); + NotInterestedInThisReplicatedActor->SetActorLocation(OtherWorkerPosition); + + FinishStep(); + }); + } + + { // Step 2 - Validate visibility on server 2 + AddStep( + TEXT("AlwaysInterested_ValidateVisibilityOnServer2"), FWorkerDefinition::Server(2), nullptr, nullptr, + [this](float DeltaTime) { + RequireTrue(!IsValid(ActorWithAlwaysInterestedProperty), TEXT("Shouldn't see root actor")); + RequireTrue(IsValid(InterestedInThisReplicatedActor), TEXT("Should see interested actor via authority")); + RequireTrue(IsValid(NotInterestedInThisReplicatedActor), TEXT("Should see not-interested actor via authority")); + RequireTrue(!IsValid(OtherInterestedInThisReplicatedActor), TEXT("Shouldn't see other interested actor")); + FinishStep(); // This will only actually finish if requires are satisfied + }, + StepTimeLimit); + } + + { // Step 3 - Validate visibility on server 1 + AddStep( + TEXT("AlwaysInterested_ValidateVisibilityOnServer1"), FWorkerDefinition::Server(1), nullptr, nullptr, + [this](float DeltaTime) { + RequireTrue(IsValid(ActorWithAlwaysInterestedProperty), TEXT("Should see root actor via authority")); + RequireTrue(IsValid(InterestedInThisReplicatedActor), TEXT("Should see interested actor via always interest")); + RequireTrue(!IsValid(NotInterestedInThisReplicatedActor), TEXT("Shouldn't see not-interested actor")); + RequireTrue(IsValid(OtherInterestedInThisReplicatedActor), TEXT("Should see other interested actor via authority")); + FinishStep(); // This will only actually finish if requires are satisfied + }, + StepTimeLimit); + } + + { // Step 4 - Validate visibility on owning client + AddStep( + TEXT("AlwaysInterested_ValidateVisibilityOnOwningClient"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this](float DeltaTime) { + RequireTrue(IsValid(ActorWithAlwaysInterestedProperty), TEXT("Should see root actor via ownership")); + RequireTrue(IsValid(InterestedInThisReplicatedActor), TEXT("Should see interested actor via always interest")); + RequireTrue(!IsValid(NotInterestedInThisReplicatedActor), TEXT("Shouldn't see not-interested actor")); + RequireTrue(!IsValid(OtherInterestedInThisReplicatedActor), TEXT("Shouldn't see other interested actor")); + FinishStep(); // This will only actually finish if requires are satisfied + }, + StepTimeLimit); + } + + { // Step 5 - Validate visibility on non-owning client + AddStep( + TEXT("AlwaysInterested_ValidateVisibilityOnNonOwningClient"), FWorkerDefinition::Client(2), nullptr, nullptr, + [this](float DeltaTime) { + RequireTrue(!IsValid(ActorWithAlwaysInterestedProperty), TEXT("Shouldn't see root actor")); + RequireTrue(!IsValid(InterestedInThisReplicatedActor), TEXT("Shouldn't see interested actor")); + RequireTrue(!IsValid(NotInterestedInThisReplicatedActor), TEXT("Shouldn't see not-interested actor")); + RequireTrue(!IsValid(OtherInterestedInThisReplicatedActor), TEXT("Shouldn't see other interested actor")); + FinishStep(); // This will only actually finish if requires are satisfied + }, + StepTimeLimit); + } + + { // Step 6 - Replace Interest Actors + AddStep(TEXT("AlwaysInterested_ReplaceInterestActors"), FWorkerDefinition::Server(1), nullptr, [this]() { + // Replace InterestedInThisReplicatedActor with OtherInterestedInThisReplicatedActor + AssertTrue(ActorWithAlwaysInterestedProperty->HasAuthority(), + TEXT("Must have authority over ActorWithAlwaysInterestedProperty")); + ActorWithAlwaysInterestedProperty->InterestedActors[0] = OtherInterestedInThisReplicatedActor; + + FinishStep(); + }); + } + + { // Step 7 - Validate visibility on owning client + AddStep( + TEXT("AlwaysInterested_ValidateVisibilityOnOwningClient_Part2"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this](float DeltaTime) { + RequireTrue(IsValid(ActorWithAlwaysInterestedProperty), TEXT("Should see root actor via ownership")); + RequireTrue(!IsValid(InterestedInThisReplicatedActor), TEXT("Shouldn't see interested actor")); + RequireTrue(!IsValid(NotInterestedInThisReplicatedActor), TEXT("Shouldn't see not-interested actor")); + RequireTrue(IsValid(OtherInterestedInThisReplicatedActor), TEXT("Should see other interested actor via interest")); + FinishStep(); // This will only actually finish if requires are satisfied + }, + StepTimeLimit); + } + + { // Step 8 - Move other interest actors + AddStep(TEXT("AlwaysInterested_MoveOtherInterestActor"), FWorkerDefinition::Server(1), nullptr, [this]() { + // Replace InterestedInThisReplicatedActor with OtherInterestedInThisReplicatedActor + AssertTrue(OtherInterestedInThisReplicatedActor->HasAuthority(), + TEXT("Must have authority over OtherInterestedInThisReplicatedActor")); + OtherInterestedInThisReplicatedActor->SetActorLocation(OtherWorkerPosition); + + FinishStep(); + }); + } + + { // Step 9 - Validate new visibility on server 2 + AddStep( + TEXT("AlwaysInterested_ValidateVisibilityOnServer2_Part2"), FWorkerDefinition::Server(2), nullptr, nullptr, + [this](float DeltaTime) { + RequireTrue(!IsValid(ActorWithAlwaysInterestedProperty), TEXT("Shouldn't see root actor")); + RequireTrue(IsValid(InterestedInThisReplicatedActor), TEXT("Should see interested actor via authority")); + RequireTrue(IsValid(NotInterestedInThisReplicatedActor), TEXT("Should see not-interested actor via authority")); + RequireTrue(IsValid(OtherInterestedInThisReplicatedActor), TEXT("Should see other interested actor via authority")); + FinishStep(); // This will only actually finish if requires are satisfied + }, + StepTimeLimit); + } + + { // Step 10 - Validate visibility on server 1 + AddStep( + TEXT("AlwaysInterested_ValidateVisibilityOnServer1_Part2"), FWorkerDefinition::Server(1), nullptr, nullptr, + [this](float DeltaTime) { + RequireTrue(IsValid(ActorWithAlwaysInterestedProperty), TEXT("Should see root actor via authority")); + RequireTrue(!IsValid(InterestedInThisReplicatedActor), TEXT("Shouldn't see interested actor")); + RequireTrue(!IsValid(NotInterestedInThisReplicatedActor), TEXT("Shouldn't see not-interested actor")); + RequireTrue(IsValid(OtherInterestedInThisReplicatedActor), TEXT("Should see other interested actor via interest")); + FinishStep(); // This will only actually finish if requires are satisfied + }, + StepTimeLimit); + } + + { // Step 11 - Move actors back to original server + AddStep(TEXT("AlwaysInterestedMoveActorsBack"), FWorkerDefinition::Server(2), nullptr, [this]() { + // Move both interested actors back to server 1's area + AssertTrue(InterestedInThisReplicatedActor->HasAuthority(), TEXT("Must have authority over InterestedInThisReplicatedActor")); + AssertTrue(NotInterestedInThisReplicatedActor->HasAuthority(), + TEXT("Must have authority over NotInterestedInThisReplicatedActor")); + AssertTrue(OtherInterestedInThisReplicatedActor->HasAuthority(), + TEXT("Must have authority over OtherInterestedInThisReplicatedActor")); + InterestedInThisReplicatedActor->SetActorLocation(OtherWorkerPosition); + NotInterestedInThisReplicatedActor->SetActorLocation(OtherWorkerPosition); + OtherInterestedInThisReplicatedActor->SetActorLocation(OtherWorkerPosition); + + FinishStep(); + }); + } + + { // Step 12 - Validate visibility on server 1 again + AddStep( + TEXT("AlwaysInterested_ValidateVisibilityOnServer1_Part3"), FWorkerDefinition::Server(1), nullptr, nullptr, + [this](float DeltaTime) { + RequireTrue(IsValid(ActorWithAlwaysInterestedProperty), TEXT("Should see root actor via authority")); + // There's currently an issue where if an authoritative server loses interest over an actor reference, it won't + // correct the reference when the actor comes back into view - UNR-3917 + // RequireTrue(IsValid(InterestedInThisReplicatedActor), TEXT("Should see interested actor via authority")); + // RequireTrue(IsValid(NotInterestedInThisReplicatedActor), TEXT("Should see not-interested actor via authority")); + RequireEqual_Int(GetNumberOfActorsOfType(GetWorld()), 3, TEXT("Should see all actors via authority")); + RequireTrue(IsValid(OtherInterestedInThisReplicatedActor), TEXT("Should see other interested actor via authority")); + FinishStep(); // This will only actually finish if requires are satisfied + }, + StepTimeLimit); + } + + { // Step 13 - Validate visibility on server 2 again + AddStep( + TEXT("AlwaysInterested_ValidateVisibilityOnServer2_Part3"), FWorkerDefinition::Server(2), nullptr, nullptr, + [this](float DeltaTime) { + RequireTrue(!IsValid(ActorWithAlwaysInterestedProperty), TEXT("Shouldn't see root actor")); + RequireTrue(!IsValid(InterestedInThisReplicatedActor), TEXT("Shouldn't see interested actor")); + RequireTrue(!IsValid(NotInterestedInThisReplicatedActor), TEXT("Shouldn't see not-interested actor")); + RequireTrue(!IsValid(OtherInterestedInThisReplicatedActor), TEXT("Shouldn't see other interested actor")); + RequireEqual_Int(GetNumberOfActorsOfType(GetWorld()), 0, TEXT("Shouldn't see any Interest actors")); + FinishStep(); // This will only actually finish if requires are satisfied + }, + StepTimeLimit); + } + + { // Step 14 - Remove Interest Actors + AddStep(TEXT("AlwaysInterested_ReplaceInterestActors"), FWorkerDefinition::Server(1), nullptr, [this]() { + // Remove AlwaysInterested properties + AssertTrue(ActorWithAlwaysInterestedProperty->HasAuthority(), + TEXT("Must have authority over ActorWithAlwaysInterestedProperty")); + ActorWithAlwaysInterestedProperty->InterestedActors.Empty(); + + FinishStep(); + }); + } + + { // Step 15 - Validate visibility on owning client + AddStep( + TEXT("AlwaysInterested_ValidateVisibilityOnOwningClient_Part3"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this](float DeltaTime) { + RequireTrue(IsValid(ActorWithAlwaysInterestedProperty), TEXT("Should see root actor via ownership")); + RequireTrue(!IsValid(InterestedInThisReplicatedActor), TEXT("Shouldn't see interested actor")); + RequireTrue(!IsValid(NotInterestedInThisReplicatedActor), TEXT("Shouldn't see not-interested actor")); + RequireTrue(!IsValid(OtherInterestedInThisReplicatedActor), TEXT("Shouldn't see other interested actor")); + FinishStep(); // This will only actually finish if requires are satisfied + }, + StepTimeLimit); + } + + { // Step 16 - Test cleanup + AddStep(TEXT("AlwaysInterested_TestCleanup"), FWorkerDefinition::Server(1), nullptr, [this]() { + // Move Pawns back to starting positions otherwise other tests in this map may run incorrectly. + AController* PlayerController1 = Cast(GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1)->GetOwner()); + if (!AssertTrue(IsValid(PlayerController1), TEXT("Should have spawned a PlayerController 1"))) + { + return; + } + + if (!AssertTrue(PlayerController1->HasAuthority(), TEXT("Should have authority over the PlayerController 1"))) + { + return; + } + + AController* PlayerController2 = Cast(GetFlowController(ESpatialFunctionalTestWorkerType::Client, 2)->GetOwner()); + if (!AssertTrue(IsValid(PlayerController2), TEXT("Should have spawned a PlayerController 2"))) + { + return; + } + + if (!AssertTrue(PlayerController2->HasAuthority(), TEXT("Should have authority over the PlayerController 2"))) + { + return; + } + + PlayerController1->GetPawn()->SetActorLocation(OriginalPawn1Position); + PlayerController2->GetPawn()->SetActorLocation(OriginalPawn2Position); + + FinishStep(); + }); + } +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/AlwaysInterestedTest/AlwaysInterestedTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/AlwaysInterestedTest/AlwaysInterestedTest.h new file mode 100644 index 0000000000..2a65488c1b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/AlwaysInterestedTest/AlwaysInterestedTest.h @@ -0,0 +1,43 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialFunctionalTest.h" +#include "AlwaysInterestedTest.generated.h" + +class AAlwaysInterestedTestActor; +class ASmallNCDActor; + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API AAlwaysInterestedTest : public ASpatialFunctionalTest +{ + GENERATED_BODY() + +public: + AAlwaysInterestedTest(); + + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + virtual void PrepareTest() override; + + UPROPERTY(Replicated) + AAlwaysInterestedTestActor* ActorWithAlwaysInterestedProperty; + + UPROPERTY(Replicated) + ASmallNCDActor* InterestedInThisReplicatedActor; + + UPROPERTY(Replicated) + ASmallNCDActor* NotInterestedInThisReplicatedActor; + + UPROPERTY(Replicated) + ASmallNCDActor* OtherInterestedInThisReplicatedActor; + + // Only valid on server workers + FVector LocalWorkerPosition; + FVector OtherWorkerPosition; + + // Other tests rely on the pawns to be in the starting position, so cache and reset at end of test. + FVector OriginalPawn1Position; + FVector OriginalPawn2Position; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DebugInterface/SpatialDebugInterfaceTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DebugInterface/SpatialDebugInterfaceTest.cpp index 5e3bffe539..c9ad03c0ba 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DebugInterface/SpatialDebugInterfaceTest.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DebugInterface/SpatialDebugInterfaceTest.cpp @@ -6,7 +6,7 @@ #include "LoadBalancing/GridBasedLBStrategy.h" #include "LoadBalancing/LayeredLBStrategy.h" #include "SpatialFunctionalTestFlowController.h" -#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase_RepGraphAlwaysReplicate.h" #include "TestWorkerSettings.h" #include "Kismet/GameplayStatics.h" @@ -75,7 +75,7 @@ void ASpatialDebugInterfaceTest::PrepareTest() ULayeredLBStrategy* RootStrategy = GetLoadBalancingStrategy(); - bIsOnDefaultLayer = RootStrategy->CouldHaveAuthority(AReplicatedTestActorBase::StaticClass()); + bIsOnDefaultLayer = RootStrategy->CouldHaveAuthority(AReplicatedTestActorBase_RepGraphAlwaysReplicate::StaticClass()); if (bIsOnDefaultLayer) { FName LocalLayer = RootStrategy->GetLocalLayerName(); @@ -92,7 +92,8 @@ void ASpatialDebugInterfaceTest::PrepareTest() LocalWorker = GridStrategy->GetLocalVirtualWorkerId(); WorkerEntityPosition = GridStrategy->GetWorkerEntityPosition(); - AReplicatedTestActorBase* Actor = World->SpawnActor(WorkerEntityPosition, FRotator()); + AReplicatedTestActorBase_RepGraphAlwaysReplicate* Actor = + World->SpawnActor(WorkerEntityPosition, FRotator()); AddDebugTag(Actor, GetTestTag()); RegisterAutoDestroyActor(Actor); TimeStampSpinning = FPlatformTime::Cycles64(); @@ -117,7 +118,7 @@ void ASpatialDebugInterfaceTest::PrepareTest() UWorld* World = GetWorld(); TArray TestActors; - UGameplayStatics::GetAllActorsOfClass(World, AReplicatedTestActorBase::StaticClass(), TestActors); + UGameplayStatics::GetAllActorsOfClass(World, AReplicatedTestActorBase_RepGraphAlwaysReplicate::StaticClass(), TestActors); if (!AssertTrue(TestActors.Num() == 1, "We should only see a single actor at this point!!")) { return false; @@ -138,7 +139,7 @@ void ASpatialDebugInterfaceTest::PrepareTest() AddStep( TEXT("Wait for extra actors"), FWorkerDefinition::AllServers, [this]() -> bool { - return WaitToSeeActors(AReplicatedTestActorBase::StaticClass(), Workers.Num()); + return WaitToSeeActors(AReplicatedTestActorBase_RepGraphAlwaysReplicate::StaticClass(), Workers.Num()); }, [this]() { if (!bIsOnDefaultLayer) @@ -148,7 +149,7 @@ void ASpatialDebugInterfaceTest::PrepareTest() UWorld* World = GetWorld(); TArray TestActors; - UGameplayStatics::GetAllActorsOfClass(World, AReplicatedTestActorBase::StaticClass(), TestActors); + UGameplayStatics::GetAllActorsOfClass(World, AReplicatedTestActorBase_RepGraphAlwaysReplicate::StaticClass(), TestActors); AssertTrue(TestActors.Num() == Workers.Num(), TEXT("Not the expected number of actors")); @@ -176,7 +177,8 @@ void ASpatialDebugInterfaceTest::PrepareTest() bool bExpectedResult = true; TArray TestActors; - UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase::StaticClass(), TestActors); + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase_RepGraphAlwaysReplicate::StaticClass(), + TestActors); for (AActor* Actor : TestActors) { bExpectedResult &= Actor->HasAuthority() == bExpectedAuth; @@ -201,7 +203,8 @@ void ASpatialDebugInterfaceTest::PrepareTest() [this] { UWorld* World = GetWorld(); - AReplicatedTestActorBase* Actor = World->SpawnActor(WorkerEntityPosition, FRotator()); + AReplicatedTestActorBase_RepGraphAlwaysReplicate* Actor = + World->SpawnActor(WorkerEntityPosition, FRotator()); AddDebugTag(Actor, GetTestTag()); RegisterAutoDestroyActor(Actor); FinishStep(); @@ -211,7 +214,7 @@ void ASpatialDebugInterfaceTest::PrepareTest() AddStep( TEXT("Check new actors interest and delegation"), FWorkerDefinition::AllServers, [this]() -> bool { - return WaitToSeeActors(AReplicatedTestActorBase::StaticClass(), Workers.Num() * 2); + return WaitToSeeActors(AReplicatedTestActorBase_RepGraphAlwaysReplicate::StaticClass(), Workers.Num() * 2); }, nullptr, [this](float DeltaTime) { @@ -225,7 +228,7 @@ void ASpatialDebugInterfaceTest::PrepareTest() bool bExpectedResult = true; TArray TestActors; - UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase::StaticClass(), TestActors); + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase_RepGraphAlwaysReplicate::StaticClass(), TestActors); for (AActor* Actor : TestActors) { bExpectedResult &= Actor->HasAuthority() == bExpectedAuth; @@ -256,7 +259,7 @@ void ASpatialDebugInterfaceTest::PrepareTest() [this] { int32_t CurAuthWorker = Workers.Num() - 1; bool bExpectedAuth = Workers[CurAuthWorker] == LocalWorker; - return WaitToSeeActors(AReplicatedTestActorBase::StaticClass(), bExpectedAuth ? Workers.Num() * 2 : 2); + return WaitToSeeActors(AReplicatedTestActorBase_RepGraphAlwaysReplicate::StaticClass(), bExpectedAuth ? Workers.Num() * 2 : 2); }, [this] { FinishStep(); @@ -278,7 +281,7 @@ void ASpatialDebugInterfaceTest::PrepareTest() AddStep( TEXT("Wait for extra interest to come back"), FWorkerDefinition::AllServers, [this] { - return WaitToSeeActors(AReplicatedTestActorBase::StaticClass(), Workers.Num() * 2); + return WaitToSeeActors(AReplicatedTestActorBase_RepGraphAlwaysReplicate::StaticClass(), Workers.Num() * 2); }, [this] { FinishStep(); @@ -294,7 +297,7 @@ void ASpatialDebugInterfaceTest::PrepareTest() } TArray TestActors; - UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase::StaticClass(), TestActors); + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase_RepGraphAlwaysReplicate::StaticClass(), TestActors); for (AActor* Actor : TestActors) { if (Actor->HasAuthority()) @@ -310,7 +313,7 @@ void ASpatialDebugInterfaceTest::PrepareTest() AddStep( TEXT("Check state after tags removed"), FWorkerDefinition::AllServers, [this]() -> bool { - return WaitToSeeActors(AReplicatedTestActorBase::StaticClass(), 2); + return WaitToSeeActors(AReplicatedTestActorBase_RepGraphAlwaysReplicate::StaticClass(), 2); }, nullptr, [this](float DeltaTime) { @@ -323,7 +326,7 @@ void ASpatialDebugInterfaceTest::PrepareTest() uint32 NumAuth = 0; TArray TestActors; - UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase::StaticClass(), TestActors); + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase_RepGraphAlwaysReplicate::StaticClass(), TestActors); for (AActor* Actor : TestActors) { bExpectedResult &= Actor->HasAuthority(); @@ -350,7 +353,7 @@ void ASpatialDebugInterfaceTest::PrepareTest() uint32 NumUpdated = 0; TArray TestActors; - UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase::StaticClass(), TestActors); + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase_RepGraphAlwaysReplicate::StaticClass(), TestActors); for (AActor* Actor : TestActors) { if (Actor->HasAuthority()) @@ -370,7 +373,7 @@ void ASpatialDebugInterfaceTest::PrepareTest() AddStep( TEXT("Check state after delegation removal"), FWorkerDefinition::AllServers, [this] { - return WaitToSeeActors(AReplicatedTestActorBase::StaticClass(), Workers.Num() * 2); + return WaitToSeeActors(AReplicatedTestActorBase_RepGraphAlwaysReplicate::StaticClass(), Workers.Num() * 2); }, [this]() { if (!bIsOnDefaultLayer) @@ -380,7 +383,7 @@ void ASpatialDebugInterfaceTest::PrepareTest() bool bExpectedResult = true; TArray TestActors; - UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase::StaticClass(), TestActors); + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase_RepGraphAlwaysReplicate::StaticClass(), TestActors); for (AActor* Actor : TestActors) { bExpectedResult &= (Actor->HasAuthority() == WorkerEntityPosition.Equals(Actor->GetActorLocation())); @@ -407,7 +410,7 @@ void ASpatialDebugInterfaceTest::PrepareTest() AddStep( TEXT("Check state after debug reset"), FWorkerDefinition::AllServers, [this]() -> bool { - return WaitToSeeActors(AReplicatedTestActorBase::StaticClass(), 2); + return WaitToSeeActors(AReplicatedTestActorBase_RepGraphAlwaysReplicate::StaticClass(), 2); }, nullptr, [this](float DeltaTime) { @@ -419,7 +422,7 @@ void ASpatialDebugInterfaceTest::PrepareTest() bool bExpectedResult = true; TArray TestActors; - UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase::StaticClass(), TestActors); + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase_RepGraphAlwaysReplicate::StaticClass(), TestActors); for (AActor* Actor : TestActors) { bExpectedResult &= Actor->HasAuthority(); @@ -434,7 +437,8 @@ void ASpatialDebugInterfaceTest::PrepareTest() } USpatialDebugInterfaceMap::USpatialDebugInterfaceMap() - : UGeneratedTestMap(EMapCategory::CI_PREMERGE_SPATIAL_ONLY, TEXT("SpatialDebugInterfaceMap")) + // TODO: make EMapCategory::CI_PREMERGE_SPATIAL_ONLY when fixed UNR-5141 + : UGeneratedTestMap(EMapCategory::NO_CI, TEXT("SpatialDebugInterfaceMap")) { } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubObjectsTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubObjectsTest.h index 7eeaf5326a..942232dc9b 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubObjectsTest.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubObjectsTest.h @@ -4,10 +4,11 @@ #include "CoreMinimal.h" #include "SpatialFunctionalTest.h" + #include "DynamicSubobjectsTest.generated.h" class ATestMovementCharacter; -class AReplicatedGASTestActor; +class ADynamicSubObjectTestActor; UCLASS() class SPATIALGDKFUNCTIONALTESTS_API ADynamicSubobjectsTest : public ASpatialFunctionalTest @@ -18,19 +19,25 @@ class SPATIALGDKFUNCTIONALTESTS_API ADynamicSubobjectsTest : public ASpatialFunc ADynamicSubobjectsTest(); virtual void PrepareTest() override; + ADynamicSubObjectTestActor* GetReplicatedTestActor(); + int GetNumComponentsOnTestActor(); // A reference to the Default Pawn of Client 1 to allow for repossession in the final step of the test. APawn* ClientOneDefaultPawn; ATestMovementCharacter* ClientOneSpawnedPawn; - AReplicatedGASTestActor* TestActor; + ADynamicSubObjectTestActor* TestActor; // The spawn location for Client 1's Pawn; - FVector CharacterSpawnLocation; + FVector CharacterSpawnLocation = FVector(0.0f, 120.0f, 40.0f); // A remote location where Client 1's Pawn will be moved in order to not see the AReplicatedVisibilityTestActor. - FVector CharacterRemoteLocation; + FVector CharacterRemoteLocation = FVector(20000.0f, 20000.0f, 40.0f); // Outside of the interest range of the client + + static constexpr int32 InitialNumComponents = 1; + + static constexpr float TimeLimit = 100.0f; float StepTimer; }; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubobjectTestActor.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubobjectTestActor.cpp new file mode 100644 index 0000000000..406f86f2ad --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubobjectTestActor.cpp @@ -0,0 +1,24 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "DynamicSubObjectTestActor.h" +#include "Net/UnrealNetwork.h" + +ADynamicSubObjectTestActor::ADynamicSubObjectTestActor() +{ + TestIntProperty = -1; + bNetLoadOnClient = true; + bNetLoadOnNonAuthServer = true; + bReplicates = true; +} + +void ADynamicSubObjectTestActor::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ADynamicSubObjectTestActor, TestIntProperty); +} + +void ADynamicSubObjectTestActor::InitialiseTestIntProperty() +{ + TestIntProperty = -1; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubobjectTestActor.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubobjectTestActor.h new file mode 100644 index 0000000000..97d687c5f8 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubobjectTestActor.h @@ -0,0 +1,24 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h" + +#include "DynamicSubObjectTestActor.generated.h" + +UCLASS() +class ADynamicSubObjectTestActor : public AReplicatedTestActorBase +{ + GENERATED_BODY() + +public: + ADynamicSubObjectTestActor(); + + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + void InitialiseTestIntProperty(); + + UPROPERTY(Replicated) + int TestIntProperty; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubobjectsTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubobjectsTest.cpp index 3645db4f86..468bcd277d 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubobjectsTest.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubobjectsTest.cpp @@ -1,7 +1,7 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "DynamicSubobjectsTest.h" -#include "ReplicatedGASTestActor.h" +#include "DynamicSubObjectTestActor.h" #include "SpatialFunctionalTestFlowController.h" #include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/TestMovementCharacter.h" #include "SpatialGDKSettings.h" @@ -9,8 +9,12 @@ #include "GameFramework/PlayerController.h" #include "Kismet/GameplayStatics.h" +#include "Components/SceneComponent.h" + +#include "EngineClasses/SpatialNetDriver.h" + /** - * Tests if the dynamic Subobject of the AReplicatedGASTestActor is not duplicated on Clients when leaving + * Tests if the dynamic sub-object of the ADynamicSubObjectTestActor is not duplicated on Clients when leaving * and re-entering interest. * * The test includes a single server and one client worker. @@ -20,29 +24,38 @@ *true. * - The Server spawns a TestMovementCharacter and makes Client 1 possess it. * - Test: - * - Each worker tests if it can initially see the AReplicatedGASTestActor. + * - Each worker tests if it can initially see the ADynamicSubObjectTestActor. * - Repeat the following steps MaxDynamicallyAttachedSubobjectsPerClass + 1 times: * - After ensuring possession happened, the Server moves Client 1's Character to a remote location, so it cannot see the - *AReplicatedGASTestActor. - * - After ensuring movement replicated correctly, Client 1 checks it can no longer see the AReplicatedGASTestActor. - * - The Server moves the character of Client 1 back close to its spawn location, so that the AReplicatedGASTestActor is + *ADynamicSubObjectTestActor. + * - After ensuring movement replicated correctly, Client 1 checks it can no longer see the ADynamicSubObjectTestActor. + * - The Server moves the character of Client 1 back close to its spawn location, so that the ADynamicSubObjectTestActor is *in its interest area. * - If the "Too many dynamic sub objects" error does not appears in the log the test is successful. * - Cleanup: * - Client 1 repossesses its default pawn. * - The spawned Character is destroyed. + * + * + * A second test case is also tested with this same test. + * This tests that + * 1. The server adds a dynamic component to the actor + * 1. ADynamicSubObjectTestActor moves out of the client's interest + * 2. ADynamicSubObjectTestActor has the dynamic component removed + * 3. ADynamicSubObjectTestActor moves into the client's interest + * 4. The client sees ADynamicSubObjectTestActor no longer has the dynamic component + * + * This extra test case is implemented in steps 9.1 and 12.1 */ -const static float StepTimeLimit = 10.0f; +static constexpr float StepTimeLimit = 15.0f; +static const FName ToRemoveComponentName = TEXT("ToRemoveComponent"); ADynamicSubobjectsTest::ADynamicSubobjectsTest() : Super() { - Author = "Evi"; + Author = "Evi&Arthur&Miron"; Description = TEXT("Test Dynamic Subobjects Duplication in Client"); - - CharacterSpawnLocation = FVector(0.0f, 120.0f, 40.0f); - CharacterRemoteLocation = FVector(20000.0f, 20000.0f, 40.0f); // Outside of the interest range of the client } void ADynamicSubobjectsTest::PrepareTest() @@ -52,198 +65,253 @@ void ADynamicSubobjectsTest::PrepareTest() const int DynamicComponentsPerClass = GetDefault()->MaxDynamicallyAttachedSubobjectsPerClass; StepTimer = 0.0f; - { // Step 0 - The server spawn a TestMovementCharacter and makes Client 1 possess it. - AddStep(TEXT("DynamicSubobjectsTestSetup"), FWorkerDefinition::Server(1), nullptr, [this]() { - ASpatialFunctionalTestFlowController* ClientOneFlowController = GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1); - APlayerController* PlayerController = Cast(ClientOneFlowController->GetOwner()); + // Step 0 - The server spawn a TestMovementCharacter and makes Client 1 possess it. + AddStep(TEXT("DynamicSubobjectsTestSetup"), FWorkerDefinition::Server(1), nullptr, [this]() { + ASpatialFunctionalTestFlowController* ClientOneFlowController = GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1); + APlayerController* PlayerController = Cast(ClientOneFlowController->GetOwner()); - if (IsValid(PlayerController)) - { - ClientOneSpawnedPawn = - GetWorld()->SpawnActor(CharacterSpawnLocation, FRotator::ZeroRotator, FActorSpawnParameters()); - RegisterAutoDestroyActor(ClientOneSpawnedPawn); + if (AssertIsValid(PlayerController, TEXT("PlayerController should be valid"))) + { + ClientOneSpawnedPawn = GetWorld()->SpawnActor(CharacterSpawnLocation, FRotator::ZeroRotator); + RegisterAutoDestroyActor(ClientOneSpawnedPawn); - ClientOneDefaultPawn = PlayerController->GetPawn(); + ClientOneDefaultPawn = PlayerController->GetPawn(); + PlayerController->Possess(ClientOneSpawnedPawn); - PlayerController->Possess(ClientOneSpawnedPawn); + FinishStep(); + } + }); + + // Step 1 - All workers check if they have one ADynamicSubObjectTestActor in the world, and set a reference to it + AddStep( + TEXT("DynamicSubobjectsTestAllWorkers"), FWorkerDefinition::AllWorkers, nullptr, nullptr, + [this](float DeltaTime) { + TestActor = GetReplicatedTestActor(); + TestActor->InitialiseTestIntProperty(); + FinishStep(); + }, + StepTimeLimit); + // Step 2 - Client 1 checks if it has correctly possessed the TestMovementCharacter. + AddStep( + TEXT("DynamicSubobjectsTestClientCheckPossesion"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this](float DeltaTime) { + APawn* PlayerCharacter = GetFlowPawn(); + if (AssertIsValid(PlayerCharacter, TEXT("PlayerCharacter should be valid"))) + { + RequireTrue(PlayerCharacter == GetFlowPlayerController()->AcknowledgedPawn, TEXT("The client should possess the pawn.")); FinishStep(); } + }, + StepTimeLimit); + + // Step 3 - The client checks it has the right initial amount of components + AddStep(TEXT("DynamicSubobjectsTestClientCheckNumComponents"), FWorkerDefinition::Client(1), nullptr, [this]() { + AssertEqual_Int(GetNumComponentsOnTestActor(), InitialNumComponents, + TEXT("ADynamicSubObjectTestActor should have the initial number of components")); + FinishStep(); + }); + + // Step 4 - The server adds the new dynamic component + AddStep(TEXT("DynamicSubobjectsTestServerAddComponent"), FWorkerDefinition::Server(1), nullptr, [this]() { + AssertEqual_Int(GetNumComponentsOnTestActor(), InitialNumComponents, + TEXT("ADynamicSubObjectTestActor should have the initial number of components")); + + // add new dynamic component to test actor + USceneComponent* AddedComponent = NewObject(TestActor, ToRemoveComponentName); + AddedComponent->AttachToComponent(TestActor->GetRootComponent(), FAttachmentTransformRules::KeepWorldTransform); + AddedComponent->RegisterComponent(); + AddedComponent->SetIsReplicated(true); + + AssertEqual_Int(GetNumComponentsOnTestActor(), InitialNumComponents + 1, + TEXT("Now ADynamicSubObjectTestActor should have 1 more component")); + FinishStep(); + }); + + // Step 5 - The client waits till it can see the new component + AddStep( + TEXT("DynamicSubobjectsTestClientSeeNewComponent"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this](float DeltaTime) { + RequireEqual_Int(GetNumComponentsOnTestActor(), InitialNumComponents + 1, + TEXT("Now ADynamicSubObjectTestActor should have 1 more component")); + FinishStep(); + }, + StepTimeLimit); + + for (int i = 0; i < DynamicComponentsPerClass + 2; ++i) + { + const bool bLastStepLoop = i == DynamicComponentsPerClass + 1; + + // Step 6 - Server moves the TestMovementCharacter of Client 1 to a remote location, so that it does not see the + // ADynamicSubObjectTestActor. + AddStep(TEXT("DynamicSubobjectsTestServerMoveClient1"), FWorkerDefinition::Server(1), nullptr, [this]() { + ClientOneSpawnedPawn->SetActorLocation(CharacterRemoteLocation); + AssertEqual_Vector(ClientOneSpawnedPawn->GetActorLocation(), CharacterRemoteLocation, + TEXT("Client pawn was not moved to remote location"), 1.0f); + FinishStep(); }); - } - { // Step 1 - All workers check if they have one AReplicatedGASTestActor in the world, and set a reference to it. + // Step 7 - Client 1 makes sure that the movement was correctly replicated AddStep( - TEXT("DynamicSubobjectsTestAllWorkers"), FWorkerDefinition::AllWorkers, nullptr, nullptr, + TEXT("DynamicSubobjectsTestClientCheckFirstMovement"), FWorkerDefinition::Client(1), nullptr, nullptr, [this](float DeltaTime) { - TArray FoundActors; - UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedGASTestActor::StaticClass(), FoundActors); + APawn* PlayerCharacter = GetFlowPawn(); - if (FoundActors.Num() == 1) + if (AssertIsValid(PlayerCharacter, TEXT("PlayerCharacter should not be nullptr"))) { - TestActor = Cast(FoundActors[0]); - - if (IsValid(TestActor)) - { - FinishStep(); - } + RequireEqual_Vector(PlayerCharacter->GetActorLocation(), CharacterRemoteLocation, + TEXT("Character was not moved to remote location"), 1.0f); + FinishStep(); } }, StepTimeLimit); - } - { // Step 2 - Client 1 checks if it has correctly possessed the TestMovementCharacter. - AddStep( - TEXT("DynamicSubobjectsTestClientCheckPossesion"), FWorkerDefinition::Client(1), nullptr, nullptr, - [this](float DeltaTime) { - ASpatialFunctionalTestFlowController* FlowController = GetLocalFlowController(); - APlayerController* PlayerController = Cast(FlowController->GetOwner()); - ATestMovementCharacter* PlayerCharacter = Cast(PlayerController->GetPawn()); - if (IsValid(PlayerController)) - { - if (PlayerCharacter == PlayerController->AcknowledgedPawn) + // When in native, we need to wait for a while here - so the engine can update relevancy + const bool bIsSpatial = Cast(GetNetDriver()) != nullptr; + if (!bIsSpatial) + { + AddStep( + TEXT("DynamicSubobjectsTestNativeWaitABit"), FWorkerDefinition::Server(1), nullptr, + [this]() { + StepTimer = 0.f; + }, + [this](float DeltaTime) { + StepTimer += DeltaTime; + if (StepTimer > 7.5f) { FinishStep(); } + }); + } + + // Step 8 - Server increases ADynamicSubObjectTestActor's TestIntProperty to enable checking if the client is out of interest later. + AddStep(TEXT("DynamicSubobjectsTestServerIncreasesIntValue"), FWorkerDefinition::Server(1), nullptr, [this, i]() { + TestActor->TestIntProperty = i; + FinishStep(); + }); + + // Step 9 - Client 1 checks it can no longer see the ADynamicSubObjectTestActor by waiting for 0.5s and checking TestIntProperty + // hasn't updated + AddStep( + TEXT("DynamicSubobjectsTestClientCheckIntValueDidntIncrease"), FWorkerDefinition::Client(1), nullptr, + [this]() { + StepTimer = 0.f; + }, + [this, i](float DeltaTime) { + RequireNotEqual_Int(TestActor->TestIntProperty, i, TEXT("Check TestIntProperty didn't get replicated")); + StepTimer += DeltaTime; + if (StepTimer >= 0.5f) + { + FinishStep(); } }, StepTimeLimit); - } - for (int i = 0; i < DynamicComponentsPerClass + 1; ++i) - { - { // Step 3 - Server moves the TestMovementCharacter of Client 1 to a remote location, so that it does not see the - // AReplicatedGASTestActor. - AddStep(TEXT("DynamicSubobjectsTestServerMoveClient1"), FWorkerDefinition::Server(1), nullptr, [this, i]() { - if (ClientOneSpawnedPawn->SetActorLocation(CharacterRemoteLocation)) + if (bLastStepLoop) + { + // Step 9.1 - Server removes the component for secondary test case + AddStep(TEXT("DynamicSubobjectsTestServerDestroyActorComponent"), FWorkerDefinition::Server(1), nullptr, [this]() { + TArray AllSceneComps; + TestActor->GetComponents(AllSceneComps); + AssertEqual_Int(AllSceneComps.Num(), InitialNumComponents + 1, + TEXT("ADynamicSubObjectTestActor should have 1 more than the initial number of components")); + + // Delete the component with the right name + for (USceneComponent* SceneComponent : AllSceneComps) { - if (ClientOneSpawnedPawn->GetActorLocation().Equals(CharacterRemoteLocation, 1.0f)) + if (SceneComponent->GetName() == ToRemoveComponentName.ToString()) { - FinishStep(); + SceneComponent->DestroyComponent(); } } + + AssertEqual_Int(GetNumComponentsOnTestActor(), InitialNumComponents, + TEXT("ADynamicSubObjectTestActor should have the initial number of components again")); + FinishStep(); }); } - { // Step 4 - Client 1 makes sure that the movement was correctly replicated - AddStep( - TEXT("DynamicSubobjectsTestClientCheckFirstMovement"), FWorkerDefinition::Client(1), nullptr, nullptr, - [this](float DeltaTime) { - ASpatialFunctionalTestFlowController* FlowController = GetLocalFlowController(); - APlayerController* PlayerController = Cast(FlowController->GetOwner()); - ATestMovementCharacter* PlayerCharacter = Cast(PlayerController->GetPawn()); + // Step 10 - Server moves Client 1 close to the cube. + AddStep(TEXT("DynamicSubobjectsTestServerMoveClient1CloseToCube"), FWorkerDefinition::Server(1), nullptr, [this]() { + ClientOneSpawnedPawn->SetActorLocation(CharacterSpawnLocation); + AssertEqual_Vector(ClientOneSpawnedPawn->GetActorLocation(), CharacterSpawnLocation, + TEXT("Server 1 should see the pawn close to the initial spawn location"), 1.0f); + FinishStep(); + }); - if (IsValid(PlayerCharacter)) - { - if (PlayerCharacter->GetActorLocation().Equals(CharacterRemoteLocation, 1.0f)) - { - FinishStep(); - } - } - }, - StepTimeLimit); - } + // Step 11 - Client 1 checks that the movement was replicated correctly. + AddStep( + TEXT("DynamicSubobjectsTestClientCheckSecondMovement"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this](float DeltaTime) { + APawn* PlayerCharacter = GetFlowPawn(); - { // Step 5 - Server increases AReplicatedGASTestActor's TestIntProperty to enable checking if the client is out of interest later. - AddStep(TEXT("DynamicSubobjectsTestServerIncreasesIntValue"), FWorkerDefinition::Server(1), nullptr, [this, i]() { - TArray FoundActors; - UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedGASTestActor::StaticClass(), FoundActors); - if (FoundActors.Num() == 1) - { - TestActor = Cast(FoundActors[0]); - TestActor->TestIntProperty = i + 1; - } - if (TestActor->TestIntProperty == i + 1) + if (AssertIsValid(PlayerCharacter, TEXT("PlayerCharacter should be valid"))) { + RequireEqual_Vector(PlayerCharacter->GetActorLocation(), CharacterSpawnLocation, + TEXT("Client 1 should see themself close to the initial spawn location"), 1.0f); FinishStep(); } - }); - } - - { // Step 6 - Client 1 checks it can no longer see the AReplicatedGASTestActor - AddStep( - TEXT("DynamicSubobjectsTestClientCheckIntValueIncreased"), FWorkerDefinition::Client(1), nullptr, nullptr, - [this, i](float DeltaTime) { - TArray FoundActors; - UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedGASTestActor::StaticClass(), FoundActors); - if (FoundActors.Num() == 1) - { - TestActor = Cast(FoundActors[0]); - - RequireNotEqual_Int(TestActor->TestIntProperty, i + 1, TEXT("Check TestIntProperty didn't get replicated")); - StepTimer += DeltaTime; - if (StepTimer >= 0.5f) - { - FinishStep(); - StepTimer = 0.0f; // reset for the next time - } - } - }, - StepTimeLimit); - } + }, + StepTimeLimit); - { // Step7 - Server moves Client 1 close to the cube. - AddStep(TEXT("DynamicSubobjectsTestServerMoveClient1CloseToCube"), FWorkerDefinition::Server(1), nullptr, [this]() { - if (ClientOneSpawnedPawn->SetActorLocation(CharacterSpawnLocation)) - { - if (ClientOneSpawnedPawn->GetActorLocation().Equals(CharacterSpawnLocation, 1.0f)) - { - FinishStep(); - } - } - }); - } + // Step 12 - Client 1 checks it can see the ADynamicSubObjectTestActor + AddStep( + TEXT("DynamicSubobjectsTestClientCheckIntValueIncreased"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this, i](float DeltaTime) { + RequireEqual_Int(TestActor->TestIntProperty, i, TEXT("Client 1 should see the updated TestIntProperty value")); + FinishStep(); + }, + StepTimeLimit); - { // Step 8 - Client 1 checks that the movement was replicated correctly. + if (bLastStepLoop) + { + // Step 12.1 - Client 1 checks the dynamic component on ReplicatedGASTestActor has been removed AddStep( - TEXT("DynamicSubobjectsTestClientCheckSecondMovement"), FWorkerDefinition::Client(1), nullptr, nullptr, + TEXT("DynamicSubobjectsTestClientCheckNumComponentsDecreased"), FWorkerDefinition::Client(1), nullptr, nullptr, [this](float DeltaTime) { - ASpatialFunctionalTestFlowController* FlowController = GetLocalFlowController(); - APlayerController* PlayerController = Cast(FlowController->GetOwner()); - ATestMovementCharacter* PlayerCharacter = Cast(PlayerController->GetPawn()); + AssertEqual_Int(GetNumComponentsOnTestActor(), InitialNumComponents, + TEXT("ADynamicSubObjectTestActor's dynamic component should have been destroyed.")); - if (IsValid(PlayerCharacter)) - { - if (PlayerCharacter->GetActorLocation().Equals(CharacterSpawnLocation, 1.0f)) - { - FinishStep(); - } - } - }, - StepTimeLimit); - } - - { // Step 9 - Client 1 checks it can see the AReplicatedGASTestActor - AddStep( - TEXT("DynamicSubobjectsTestClientCheckIntValueIncreased2"), FWorkerDefinition::Client(1), nullptr, nullptr, - [this, i](float DeltaTime) { - TArray FoundActors; - UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedGASTestActor::StaticClass(), FoundActors); - if (FoundActors.Num() == 1) - { - TestActor = Cast(FoundActors[0]); - if (TestActor->TestIntProperty == i + 1) - { - FinishStep(); - } - } + FinishStep(); }, StepTimeLimit); } } - { // Step 10 - Server Cleanup. - AddStep(TEXT("DynamicSubobjectsTestServerCleanup"), FWorkerDefinition::Server(1), nullptr, [this]() { - // Possess the original pawn, so that the spawned character can get destroyed correctly - ASpatialFunctionalTestFlowController* ClientOneFlowController = GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1); - APlayerController* PlayerController = Cast(ClientOneFlowController->GetOwner()); + // Step 13 - Server Cleanup. + AddStep(TEXT("DynamicSubobjectsTestServerCleanup"), FWorkerDefinition::Server(1), nullptr, [this]() { + // Possess the original pawn, so that the spawned character can get destroyed correctly + ASpatialFunctionalTestFlowController* ClientOneFlowController = GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1); + APlayerController* PlayerController = Cast(ClientOneFlowController->GetOwner()); - if (IsValid(PlayerController)) - { - PlayerController->Possess(ClientOneDefaultPawn); + if (AssertIsValid(PlayerController, TEXT("PlayerController should be valid"))) + { + PlayerController->Possess(ClientOneDefaultPawn); + FinishStep(); + } + }); +} - FinishStep(); - } - }); +ADynamicSubObjectTestActor* ADynamicSubobjectsTest::GetReplicatedTestActor() +{ + TArray FoundActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), ADynamicSubObjectTestActor::StaticClass(), FoundActors); + if (AssertEqual_Int(FoundActors.Num(), 1, TEXT("There should only be one actor of type ADynamicSubObjectTestActor in the world"))) + { + TestActor = Cast(FoundActors[0]); + if (AssertIsValid(TestActor, TEXT("TestActor must be valid"))) + { + return TestActor; + } } + return nullptr; +} + +int ADynamicSubobjectsTest::GetNumComponentsOnTestActor() +{ + TestActor = GetReplicatedTestActor(); + TArray AllActorComp; + TestActor->GetComponents(AllActorComp); + + return AllActorComp.Num(); } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/ReplicatedGASTestActor.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/ReplicatedGASTestActor.cpp deleted file mode 100644 index dfc20c3bfe..0000000000 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/ReplicatedGASTestActor.cpp +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#include "ReplicatedGASTestActor.h" -#include "Net/UnrealNetwork.h" - -AReplicatedGASTestActor::AReplicatedGASTestActor() -{ - TestIntProperty = 0; - bNetLoadOnClient = true; - bNetLoadOnNonAuthServer = true; - bReplicates = true; -} - -void AReplicatedGASTestActor::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const -{ - Super::GetLifetimeReplicatedProps(OutLifetimeProps); - - DOREPLIFETIME(AReplicatedGASTestActor, TestIntProperty); -} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/ReplicatedGASTestActor.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/ReplicatedGASTestActor.h deleted file mode 100644 index ef5c3ed4f4..0000000000 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/ReplicatedGASTestActor.h +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#pragma once - -#include "CoreMinimal.h" -#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h" - -#include "ReplicatedGASTestActor.generated.h" - -UCLASS() -class AReplicatedGASTestActor : public AReplicatedTestActorBase -{ - GENERATED_BODY() - -public: - AReplicatedGASTestActor(); - - void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; - - UPROPERTY(Replicated) - int TestIntProperty; -}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/GameModeReplicationTest/GameModeReplicationTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/GameModeReplicationTest/GameModeReplicationTest.h index 64421fde34..9e8eb3d444 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/GameModeReplicationTest/GameModeReplicationTest.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/GameModeReplicationTest/GameModeReplicationTest.h @@ -13,7 +13,7 @@ #include "GameModeReplicationTest.generated.h" -UCLASS(BlueprintType) +UCLASS(HideDropdown) class UGameModeReplicationGridLBStrategy : public UGridBasedLBStrategy { public: @@ -29,7 +29,7 @@ class UGameModeReplicationGridLBStrategy : public UGridBasedLBStrategy } }; -UCLASS(BlueprintType) +UCLASS(HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API UGameModeReplicationMultiWorkerSettings : public USpatialMultiWorkerSettings { public: @@ -45,7 +45,7 @@ class SPATIALGDKFUNCTIONALTESTS_API UGameModeReplicationMultiWorkerSettings : pu UGameModeReplicationMultiWorkerSettings() { WorkerLayers.Append(GetLayerSetup()); } }; -UCLASS(BlueprintType) +UCLASS(HideDropdown) class SPATIALGDKFUNCTIONALTESTS_API AGameModeReplicationTestGameMode : public AGameModeBase { public: @@ -63,7 +63,7 @@ class SPATIALGDKFUNCTIONALTESTS_API AGameModeReplicationTestGameMode : public AG int ReplicatedValue = StartingValue; }; -UCLASS(BlueprintType) +UCLASS() class SPATIALGDKFUNCTIONALTESTS_API AGameModeReplicationTest : public ASpatialFunctionalTest { GENERATED_BODY() diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/PlayerDisconnectController.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/PlayerDisconnectController.cpp new file mode 100644 index 0000000000..0fec376c64 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/PlayerDisconnectController.cpp @@ -0,0 +1,18 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "PlayerDisconnectController.h" +#include "InputCoreTypes.h" + +void APlayerDisconnectController::SetupInputComponent() +{ + Super::SetupInputComponent(); + EnableInput(this); + check(InputComponent); + InputComponent->BindKey(EKeys::M, IE_Pressed, this, &APlayerDisconnectController::ReturnToMainMenu); +} + +void APlayerDisconnectController::ReturnToMainMenu() +{ + // Test a client disconnecting by returning to the main menu, in this case will travel to the default map + GetGameInstance()->ReturnToMainMenu(); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/PlayerDisconnectController.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/PlayerDisconnectController.h new file mode 100644 index 0000000000..5c3b57f6bf --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/PlayerDisconnectController.h @@ -0,0 +1,22 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/PlayerController.h" +#include "PlayerDisconnectController.generated.h" + +/** + * Used for testing that players are cleaned up correctly when they return to the main menu. + */ +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API APlayerDisconnectController : public APlayerController +{ + GENERATED_BODY() + +public: + virtual void SetupInputComponent() override; + + UFUNCTION() + void ReturnToMainMenu(); +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/SpatialPlayerDisconnectGameMode.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/SpatialPlayerDisconnectGameMode.cpp new file mode 100644 index 0000000000..adf92802af --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/SpatialPlayerDisconnectGameMode.cpp @@ -0,0 +1,13 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialPlayerDisconnectGameMode.h" +#include "EngineClasses/SpatialGameInstance.h" +#include "GameFramework/Character.h" +#include "PlayerDisconnectController.h" + +ASpatialPlayerDisconnectGameMode::ASpatialPlayerDisconnectGameMode() + : Super() +{ + PlayerControllerClass = APlayerDisconnectController::StaticClass(); + DefaultPawnClass = ACharacter::StaticClass(); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/SpatialPlayerDisconnectGameMode.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/SpatialPlayerDisconnectGameMode.h new file mode 100644 index 0000000000..dc2cd5be73 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/SpatialPlayerDisconnectGameMode.h @@ -0,0 +1,15 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "GameFramework/GameModeBase.h" +#include "SpatialPlayerDisconnectGameMode.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialPlayerDisconnectGameMode : public AGameModeBase +{ + GENERATED_BODY() + +public: + ASpatialPlayerDisconnectGameMode(); +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/SpatialTestPlayerDisconnect.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/SpatialTestPlayerDisconnect.cpp new file mode 100644 index 0000000000..08e11ab636 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/SpatialTestPlayerDisconnect.cpp @@ -0,0 +1,84 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialTestPlayerDisconnect.h" +#include "EngineClasses/SpatialNetDriver.h" +#include "GameFramework/Character.h" +#include "Kismet/GameplayStatics.h" +#include "PlayerDisconnectController.h" +#include "SpatialConstants.h" +#include "SpatialFunctionalTestFlowController.h" + +ASpatialTestPlayerDisconnect::ASpatialTestPlayerDisconnect() +{ + Author = "Victoria Bloom"; + Description = TEXT("Ensure players are cleaned up correctly when they disconnected by the return to main menu."); +} + +void ASpatialTestPlayerDisconnect::PrepareTest() +{ + Super::PrepareTest(); + + if (HasAuthority()) + { + AddExpectedLogError("OSS: No game present to leave for session", 2); + } + + AddStep( + TEXT("AllServers_ChecksBefore"), FWorkerDefinition::AllServers, nullptr, + [this]() { + int32 ActualNumberOfClients = GetNumberOfClientWorkers(); + RequireEqual_Int(ActualNumberOfClients, 2, TEXT("Expected two clients.")); + + TArray PlayerControllers; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), APlayerDisconnectController::StaticClass(), PlayerControllers); + RequireEqual_Int(PlayerControllers.Num(), 2, TEXT("Expected two player controllers.")); + + TArray PlayerCharacters; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), ACharacter::StaticClass(), PlayerCharacters); + RequireEqual_Int(PlayerCharacters.Num(), 2, TEXT("Expected two player characters.")); + + FinishStep(); + }, + nullptr, 5.0f); + + AddStep(TEXT("Client1_ReturnToMainMenu"), FWorkerDefinition::Client(1), nullptr, [this]() { + APlayerDisconnectController* LocalPlayerController = + Cast(UGameplayStatics::GetPlayerController(GetWorld(), 0)); + + LocalPlayerController->ReturnToMainMenu(); + + FinishStep(); + }); + + // Need this additional step after client returns to main menu to deregister their flowcontroller from the server + // If the client itself deregisters it's own flow controller it can never send the FinishStep command and will fail the step. + AddStep( + TEXT("AllServers_RemoveFlowControllerForClient1"), FWorkerDefinition::AllServers, nullptr, + [this]() { + if (ASpatialFunctionalTestFlowController* FlowController = GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1)) + { + FlowController->DeregisterFlowController(); + } + + FinishStep(); + }, + nullptr, 5.0f); + + AddStep( + TEXT("AllServers_ChecksAfter"), FWorkerDefinition::AllServers, nullptr, + [this]() { + int32 ActualNumberOfClients = GetNumberOfClientWorkers(); + RequireEqual_Int(ActualNumberOfClients, 1, TEXT("Expected one client.")); + + TArray PlayerControllers; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), APlayerDisconnectController::StaticClass(), PlayerControllers); + RequireEqual_Int(PlayerControllers.Num(), 1, TEXT("Expected one player controller.")); + + TArray PlayerCharacters; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), ACharacter::StaticClass(), PlayerCharacters); + RequireEqual_Int(PlayerCharacters.Num(), 1, TEXT("Expected one player character.")); + + FinishStep(); + }, + nullptr, 5.0f); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/SpatialTestPlayerDisconnect.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/SpatialTestPlayerDisconnect.h new file mode 100644 index 0000000000..1699c6afdb --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/PlayerDisconnect/SpatialTestPlayerDisconnect.h @@ -0,0 +1,16 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialFunctionalTest.h" +#include "SpatialTestPlayerDisconnect.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialTestPlayerDisconnect : public ASpatialFunctionalTest +{ + GENERATED_BODY() + + ASpatialTestPlayerDisconnect(); + + virtual void PrepareTest() override; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/RelevancyTest/RelevancyTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/RelevancyTest/RelevancyTest.cpp index 8d59b9b518..2f7fc8cfd7 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/RelevancyTest/RelevancyTest.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/RelevancyTest/RelevancyTest.cpp @@ -20,8 +20,6 @@ * - Destroy the actors */ -const static float StepTimeLimit = 5.0f; - ARelevancyTest::ARelevancyTest() : Super() { @@ -29,18 +27,6 @@ ARelevancyTest::ARelevancyTest() Description = TEXT("Test Actor Relevancy"); } -template -int GetNumberOfActorsOfType(UWorld* World) -{ - int Counter = 0; - for (TActorIterator Iter(World); Iter; ++Iter) - { - Counter++; - } - - return Counter; -} - void ARelevancyTest::PrepareTest() { Super::PrepareTest(); @@ -106,68 +92,54 @@ void ARelevancyTest::PrepareTest() } { // Step 2 - Check actors count is correct on servers - AddStep( - TEXT("RelevancyTestCountActorsOnServers"), FWorkerDefinition::AllServers, nullptr, nullptr, - [this](float DeltaTime) { - int NumAlwaysRelevantActors = GetNumberOfActorsOfType(GetWorld()); - int NumAlwaysServerOnlyRelevantActors = GetNumberOfActorsOfType(GetWorld()); - int NumOnlyRelevantToOwnerActors = GetNumberOfActorsOfType(GetWorld()); - int NumUseOwnerRelevancyActors = GetNumberOfActorsOfType(GetWorld()); - int NumServers = GetNumberOfServerWorkers(); - - RequireEqual_Int(NumAlwaysRelevantActors, NumServers, TEXT("Servers see expected number of always relevant actors")); - RequireEqual_Int(NumAlwaysServerOnlyRelevantActors, NumServers, - TEXT("Servers see expected number of server-only always relevant actors")); - RequireEqual_Int(NumOnlyRelevantToOwnerActors, 1, TEXT("Servers see expected number of only relevant to owner actors")); - RequireEqual_Int(NumUseOwnerRelevancyActors, 1, TEXT("Servers see expected number of use owner relevancy actors")); - FinishStep(); // This will only actually finish if requires are satisfied - }, - StepTimeLimit); + AddStep(TEXT("RelevancyTestCountActorsOnServers"), FWorkerDefinition::AllServers, nullptr, nullptr, [this](float DeltaTime) { + int NumAlwaysRelevantActors = GetNumberOfActorsOfType(GetWorld()); + int NumAlwaysServerOnlyRelevantActors = GetNumberOfActorsOfType(GetWorld()); + int NumOnlyRelevantToOwnerActors = GetNumberOfActorsOfType(GetWorld()); + int NumUseOwnerRelevancyActors = GetNumberOfActorsOfType(GetWorld()); + int NumServers = GetNumberOfServerWorkers(); + + RequireEqual_Int(NumAlwaysRelevantActors, NumServers, TEXT("Servers see expected number of always relevant actors")); + RequireEqual_Int(NumAlwaysServerOnlyRelevantActors, NumServers, + TEXT("Servers see expected number of server-only always relevant actors")); + RequireEqual_Int(NumOnlyRelevantToOwnerActors, 1, TEXT("Servers see expected number of only relevant to owner actors")); + RequireEqual_Int(NumUseOwnerRelevancyActors, 1, TEXT("Servers see expected number of use owner relevancy actors")); + FinishStep(); // This will only actually finish if requires are satisfied + }); } { // Step 3 - Check actors count is correct on clients - AddStep( - TEXT("RelevancyTestCountActorsOnClients"), FWorkerDefinition::AllClients, nullptr, nullptr, - [this](float DeltaTime) { - int NumAlwaysRelevantActors = GetNumberOfActorsOfType(GetWorld()); - int NumAlwaysServerOnlyRelevantActors = GetNumberOfActorsOfType(GetWorld()); - int NumServers = GetNumberOfServerWorkers(); - - RequireEqual_Int(NumAlwaysRelevantActors, NumServers, TEXT("Client see expected number of always relevant actors")); - RequireEqual_Int(NumAlwaysServerOnlyRelevantActors, 0, TEXT("Client see no always relevant server-only actors")); - FinishStep(); // This will only actually finish if requires are satisfied - }, - StepTimeLimit); + AddStep(TEXT("RelevancyTestCountActorsOnClients"), FWorkerDefinition::AllClients, nullptr, nullptr, [this](float DeltaTime) { + int NumAlwaysRelevantActors = GetNumberOfActorsOfType(GetWorld()); + int NumAlwaysServerOnlyRelevantActors = GetNumberOfActorsOfType(GetWorld()); + int NumServers = GetNumberOfServerWorkers(); + + RequireEqual_Int(NumAlwaysRelevantActors, NumServers, TEXT("Client see expected number of always relevant actors")); + RequireEqual_Int(NumAlwaysServerOnlyRelevantActors, 0, TEXT("Client see no always relevant server-only actors")); + FinishStep(); // This will only actually finish if requires are satisfied + }); } { // Step 4 - Check actors count is correct on owning client - AddStep( - TEXT("RelevancyTestCountActorsOnClients"), FWorkerDefinition::Client(1), nullptr, nullptr, - [this](float DeltaTime) { - int NumOnlyRelevantToOwnerActors = GetNumberOfActorsOfType(GetWorld()); - int NumUseOwnerRelevancyActors = GetNumberOfActorsOfType(GetWorld()); - - RequireEqual_Int(NumOnlyRelevantToOwnerActors, 1, - TEXT("Owning client sees expected number of only relevant to owner actors")); - RequireEqual_Int(NumUseOwnerRelevancyActors, 1, TEXT("Owning client sees expected number of use owner relevancy actors")); - FinishStep(); // This will only actually finish if requires are satisfied - }, - StepTimeLimit); + AddStep(TEXT("RelevancyTestCountActorsOnClients"), FWorkerDefinition::Client(1), nullptr, nullptr, [this](float DeltaTime) { + int NumOnlyRelevantToOwnerActors = GetNumberOfActorsOfType(GetWorld()); + int NumUseOwnerRelevancyActors = GetNumberOfActorsOfType(GetWorld()); + + RequireEqual_Int(NumOnlyRelevantToOwnerActors, 1, TEXT("Owning client sees expected number of only relevant to owner actors")); + RequireEqual_Int(NumUseOwnerRelevancyActors, 1, TEXT("Owning client sees expected number of use owner relevancy actors")); + FinishStep(); // This will only actually finish if requires are satisfied + }); } { // Step 5 - Check actors count is correct on non-owning client - AddStep( - TEXT("RelevancyTestCountActorsOnClients"), FWorkerDefinition::Client(2), nullptr, nullptr, - [this](float DeltaTime) { - int NumOnlyRelevantToOwnerActors = GetNumberOfActorsOfType(GetWorld()); - int NumUseOwnerRelevancyActors = GetNumberOfActorsOfType(GetWorld()); - - RequireEqual_Int(NumOnlyRelevantToOwnerActors, 0, - TEXT("Non-owning client sees expected number of only relevant to owner actors")); - RequireEqual_Int(NumUseOwnerRelevancyActors, 0, - TEXT("Non-owning client sees expected number of use owner relevancy actors")); - FinishStep(); // This will only actually finish if requires are satisfied - }, - StepTimeLimit); + AddStep(TEXT("RelevancyTestCountActorsOnClients"), FWorkerDefinition::Client(2), nullptr, nullptr, [this](float DeltaTime) { + int NumOnlyRelevantToOwnerActors = GetNumberOfActorsOfType(GetWorld()); + int NumUseOwnerRelevancyActors = GetNumberOfActorsOfType(GetWorld()); + + RequireEqual_Int(NumOnlyRelevantToOwnerActors, 0, + TEXT("Non-owning client sees expected number of only relevant to owner actors")); + RequireEqual_Int(NumUseOwnerRelevancyActors, 0, TEXT("Non-owning client sees expected number of use owner relevancy actors")); + FinishStep(); // This will only actually finish if requires are satisfied + }); } } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialCleanupConnectionTest/SpatialCleanupConnectionTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialCleanupConnectionTest/SpatialCleanupConnectionTest.cpp index f08efa46a3..a7ca751297 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialCleanupConnectionTest/SpatialCleanupConnectionTest.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialCleanupConnectionTest/SpatialCleanupConnectionTest.cpp @@ -62,6 +62,15 @@ void ASpatialCleanupConnectionTest::PrepareTest() FinishStep(); }); + AddStep( + TEXT("Wait for actor to be ready"), FWorkerDefinition::Server(1), + [this]() -> bool { + return SpawnedPawn->IsActorReady(); + }, + [this]() { + FinishStep(); + }); + AddStep( TEXT("Post spawn check connections on server 2"), FWorkerDefinition::Server(2), nullptr, nullptr, [this](float delta) { diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialComponentTest/SpatialComponentSettingsOverride.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialComponentTest/SpatialComponentSettingsOverride.cpp index a77d4eac9a..e78a6e3049 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialComponentTest/SpatialComponentSettingsOverride.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialComponentTest/SpatialComponentSettingsOverride.cpp @@ -30,26 +30,20 @@ void ASpatialComponentSettingsOverride::PrepareTest() Super::PrepareTest(); // Settings will have already been automatically overwritten when the map was loaded -> check the settings are as expected - AddStep( - TEXT("Check PIE override settings"), FWorkerDefinition::AllServers, nullptr, - [this]() { - int32 ExpectedNumberOfClients = 2; - int32 RequiredNumberOfClients = GetNumRequiredClients(); - RequireEqual_Int(RequiredNumberOfClients, ExpectedNumberOfClients, TEXT("Expected a certain number of required clients.")); - int32 ActualNumberOfClients = GetNumberOfClientWorkers(); - RequireEqual_Int(ActualNumberOfClients, ExpectedNumberOfClients, TEXT("Expected a certain number of actual clients.")); - - FinishStep(); - }, - nullptr, 5.0f); - - AddStep( - TEXT("Check Editor Peformance Settings"), FWorkerDefinition::AllServers, nullptr, - [this]() { - bool bThrottleCPUWhenNotForeground = GetDefault()->bThrottleCPUWhenNotForeground; - RequireFalse(bThrottleCPUWhenNotForeground, TEXT("Expected bThrottleCPUWhenNotForeground to be False")); - - FinishStep(); - }, - nullptr, 5.0f); + AddStep(TEXT("Check PIE override settings"), FWorkerDefinition::AllServers, nullptr, [this]() { + int32 ExpectedNumberOfClients = 2; + int32 RequiredNumberOfClients = GetNumRequiredClients(); + AssertEqual_Int(RequiredNumberOfClients, ExpectedNumberOfClients, TEXT("Expected a certain number of required clients.")); + int32 ActualNumberOfClients = GetNumberOfClientWorkers(); + AssertEqual_Int(ActualNumberOfClients, ExpectedNumberOfClients, TEXT("Expected a certain number of actual clients.")); + + FinishStep(); + }); + + AddStep(TEXT("Check Editor Peformance Settings"), FWorkerDefinition::AllServers, nullptr, [this]() { + bool bThrottleCPUWhenNotForeground = GetDefault()->bThrottleCPUWhenNotForeground; + AssertFalse(bThrottleCPUWhenNotForeground, TEXT("Expected bThrottleCPUWhenNotForeground to be False")); + + FinishStep(); + }); } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialComponentTest/SpatialComponentTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialComponentTest/SpatialComponentTest.cpp index 7ea403824e..5437d1dd67 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialComponentTest/SpatialComponentTest.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialComponentTest/SpatialComponentTest.cpp @@ -49,6 +49,7 @@ void ASpatialComponentTest::PrepareTest() TEXT("Replicated Level Actor - Verify Server Components"), FWorkerDefinition::AllWorkers, nullptr, nullptr, [this](float DeltaTime) { CheckComponents(LevelReplicatedActor, 1, 0, 0); + FinishStep(); }, 5.0f); } @@ -70,6 +71,7 @@ void ASpatialComponentTest::PrepareTest() nullptr, [this](float DeltaTime) { CheckComponents(DynamicReplicatedActor, 1, 0, 0); + FinishStep(); }, 5.0f); @@ -87,6 +89,7 @@ void ASpatialComponentTest::PrepareTest() [this](float DeltaTime) { // Client 1 OnClientOwnershipGained component and Client 2 no events expected CheckComponents(DynamicReplicatedActor, 1, 1, 0); + FinishStep(); }, 5.0f); @@ -105,6 +108,7 @@ void ASpatialComponentTest::PrepareTest() // Client 1 OnClientOwnershipGained component and OnClientOwnershipLost component and Client 2 OnClientOwnershipGained // component CheckComponents(DynamicReplicatedActor, 1, 2, 1); + FinishStep(); }, 5.0f); @@ -126,6 +130,7 @@ void ASpatialComponentTest::PrepareTest() FWorkerDefinition::AllWorkers, nullptr, nullptr, [this](float DeltaTime) { CheckComponentsCrossServer(DynamicReplicatedActor, 1, 2); + FinishStep(); }, 5.0f); @@ -144,6 +149,16 @@ void ASpatialComponentTest::GetLifetimeReplicatedProps(TArray void ASpatialComponentTest::CheckComponents(ASpatialComponentTestActor* Actor, int ExpectedServerId, int ExpectedClient1ComponentCount, int ExpectedClient2ComponentCount) { + if (!RequireTrue(IsValid(Actor), TEXT("Actor is valid"))) + { + return; + } + + if (!RequireTrue(Actor->HasActorBegunPlay(), TEXT("Actor has begun play"))) + { + return; + } + const FWorkerDefinition& LocalWorkerDefinition = GetLocalFlowController()->WorkerDefinition; if (LocalWorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server) { @@ -152,40 +167,31 @@ void ASpatialComponentTest::CheckComponents(ASpatialComponentTestActor* Actor, i { if (LocalWorkerDefinition.Id == ExpectedServerId) { - RequireTrue(VerifyTestActorComponents(Actor, 2), - TEXT("Server auth - OnAuthorityGained component and OnActorReady component")); - FinishStep(); + VerifyTestActorComponents(*Actor, 2, TEXT("Server auth - OnAuthorityGained component and OnActorReady component")); } else if (Actor->bNetStartup) { - RequireTrue(VerifyTestActorComponents(Actor, 1), - TEXT("Non-auth servers - Level actors receive OnActorReady only OnAuthorityGained")); - FinishStep(); + VerifyTestActorComponents(*Actor, 1, TEXT("Non-auth servers - Level actors receive OnActorReady only OnAuthorityGained")); } else { - RequireTrue(VerifyTestActorComponents(Actor, 0), TEXT("Non-auth servers - Dynamic actors do not receive OnActorReady")); - FinishStep(); + VerifyTestActorComponents(*Actor, 0, TEXT("Non-auth servers - Dynamic actors do not receive OnActorReady")); } } else // Support for Native / Single Worker. { - RequireTrue(VerifyTestActorComponents(Actor, 2), - TEXT("Native / Single Worker - OnActorReady component and OnAuthorityGained component")); - FinishStep(); + VerifyTestActorComponents(*Actor, 2, TEXT("Native / Single Worker - OnActorReady component and OnAuthorityGained component")); } } else // Clients { if (LocalWorkerDefinition.Id == 1) { - RequireTrue(VerifyTestActorComponents(Actor, ExpectedClient1ComponentCount), TEXT("Client 1")); - FinishStep(); + VerifyTestActorComponents(*Actor, ExpectedClient1ComponentCount, TEXT("Client 1")); } else if (LocalWorkerDefinition.Id == 2) { - RequireTrue(VerifyTestActorComponents(Actor, ExpectedClient2ComponentCount), TEXT("Client 2")); - FinishStep(); + VerifyTestActorComponents(*Actor, ExpectedClient2ComponentCount, TEXT("Client 2")); } } } @@ -193,6 +199,16 @@ void ASpatialComponentTest::CheckComponents(ASpatialComponentTestActor* Actor, i // Checks the number of components on the servers and clients when an actor migrates void ASpatialComponentTest::CheckComponentsCrossServer(ASpatialComponentTestActor* Actor, int StartServerId, int EndServerId) { + if (!RequireTrue(IsValid(Actor), TEXT("Actor is valid"))) + { + return; + } + + if (!RequireTrue(Actor->HasActorBegunPlay(), TEXT("Actor has begun play"))) + { + return; + } + const FWorkerDefinition& LocalWorkerDefinition = GetLocalFlowController()->WorkerDefinition; if (LocalWorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server) { @@ -201,41 +217,37 @@ void ASpatialComponentTest::CheckComponentsCrossServer(ASpatialComponentTestActo { if (LocalWorkerDefinition.Id == StartServerId) { - RequireTrue(VerifyTestActorComponents(Actor, 3), - TEXT("Spawning server - OnActorReady component, OnAuthorityGained component and OnAuthorityLost component")); - FinishStep(); + VerifyTestActorComponents( + *Actor, 3, TEXT("Spawning server - OnActorReady component, OnAuthorityGained component and OnAuthorityLost component")); } else if (LocalWorkerDefinition.Id == EndServerId) { - RequireTrue(VerifyTestActorComponents(Actor, 1), TEXT("Migrated server - OnAuthorityGained component")); - FinishStep(); + VerifyTestActorComponents(*Actor, 1, TEXT("Migrated server - OnAuthorityGained component")); } } else // Support for Native / Single Worker. { - RequireTrue(VerifyTestActorComponents(Actor, 2), - TEXT("Native / Single Worker - OnActorReady component and OnAuthorityGained component")); - FinishStep(); + VerifyTestActorComponents(*Actor, 2, TEXT("Native / Single Worker - OnActorReady component and OnAuthorityGained component")); } } else // Clients { - RequireTrue(VerifyTestActorComponents(Actor, 0), TEXT("Clients")); - FinishStep(); + VerifyTestActorComponents(*Actor, 0, TEXT("Clients")); } } -bool ASpatialComponentTest::VerifyTestActorComponents(ASpatialComponentTestActor* Actor, int ExpectedTestComponentCount) +bool ASpatialComponentTest::VerifyTestActorComponents(const ASpatialComponentTestActor& Actor, int ExpectedTestComponentCount, + const FString& Message) { - if (!IsValid(Actor) || !Actor->HasActorBegunPlay()) - { - return false; - } + return RequireEqual_Int(GetComponentsCount(Actor), ExpectedTestComponentCount, + FString::Printf(TEXT("(%s) Component count correct"), *Message)); +} +int32 ASpatialComponentTest::GetComponentsCount(const ASpatialComponentTestActor& Actor) +{ TArray FoundComponents; - Actor->GetComponents(USpatialComponentTestDummyComponent::StaticClass(), FoundComponents); - int FoundTestComponentCount = FoundComponents.Num(); - return FoundTestComponentCount == ExpectedTestComponentCount; + Actor.GetComponents(USpatialComponentTestDummyComponent::StaticClass(), FoundComponents); + return FoundComponents.Num(); } void ASpatialComponentTest::CrossServerSetDynamicReplicatedActor_Implementation(ASpatialComponentTestReplicatedActor* Actor) diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialComponentTest/SpatialComponentTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialComponentTest/SpatialComponentTest.h index 9f3f64ac79..79f3f2ebbb 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialComponentTest/SpatialComponentTest.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialComponentTest/SpatialComponentTest.h @@ -46,5 +46,6 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialComponentTest : public ASpatialFunct void CheckComponents(ASpatialComponentTestActor* Actor, int ExpectedServerId, int ExpectedClient1ComponentCount, int ExpectedClient2ComponentCount); void CheckComponentsCrossServer(ASpatialComponentTestActor* Actor, int StartServerId, int EndServerId); - bool VerifyTestActorComponents(ASpatialComponentTestActor* Actor, int ExpectedComponentCount); + bool VerifyTestActorComponents(const ASpatialComponentTestActor& Actor, int ExpectedComponentCount, const FString& Message); + static int32 GetComponentsCount(const ASpatialComponentTestActor& Actor); }; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestEntityInteration/EntityInteractionTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestEntityInteration/EntityInteractionTest.cpp new file mode 100644 index 0000000000..b62cc364dd --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestEntityInteration/EntityInteractionTest.cpp @@ -0,0 +1,160 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "EntityInteractionTest.h" +#include "EntityInteractionTestActor.h" +#include "Kismet/GameplayStatics.h" + +#include "SpatialGDKSettings.h" + +/** + * This test goes through the various methods we have added to handle remote action and interaction between Actors. + * It tests two things : + * - That all calls eventually make their way to the target actor + * - That calls that are expected to short circuit do so, and the ones that are not result in a remote call. + * The map is setup with two workers, each with auth over two actors. Both workers run symmetrical code. + * So for each RPC emitted, the symmetrical actor is expected to emit a similar one. + * This is how we test that the "delayed" RPCs have made their way through the network. + * This test is only relevant when the routing worker is enabled in the GDK settings. + */ + +ASpatialEntityInteractionTest::ASpatialEntityInteractionTest() + : Super() +{ + Author = "Nicolas"; + Description = TEXT(""); + // Ignore warnings emitted by the cross server RPC called without a sender as long as the testing framework is not migrated. + LogWarningHandling = EFunctionalTestLogHandling::OutputIgnored; +} + +#define EXPECT_IMMEDIATE(Actor, FunctionName, Context) \ + do \ + { \ + FString DebugString = FString::Printf(TEXT("Immediate execution of a %s on %s : %s"), *FunctionName, *Actor->GetName(), Context); \ + AssertTrue(Actor->Steps.Contains(NumSteps) && Actor->Steps[NumSteps] == FunctionName, DebugString); \ + ++NumSteps; \ + } while (false) + +#define EXPECT_DELAYED(Actor, FunctionName, Context) \ + do \ + { \ + FString DebugString = FString::Printf(TEXT("Delayed execution of a %s on %s : %s"), *FunctionName, *Actor->GetName(), Context); \ + AssertFalse(Actor->Steps.Contains(NumSteps), DebugString); \ + ExpectedResult.Add(NumSteps++, FunctionName); \ + } while (false) + +void ASpatialEntityInteractionTest::PrepareTest() +{ + Super::PrepareTest(); + + const USpatialGDKSettings* Settings = GetDefault(); + if (!ensureAlways(Settings->CrossServerRPCImplementation == ECrossServerRPCImplementation::RoutingWorker)) + { + AddStep( + "DummyStep", FWorkerDefinition::AllWorkers, nullptr, + [this] { + FinishStep(); + }, + nullptr); + return; + } + + AddStep( + "Discovery", FWorkerDefinition::AllServers, nullptr, + [this]() { + NumSteps = 0; + ExpectedResult.Empty(); + for (auto& Actor : LocalActors) + { + Actor = nullptr; + } + for (auto& Actor : RemoteActors) + { + Actor = nullptr; + } + TArray TestActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AEntityInteractionTestActor::StaticClass(), TestActors); + for (AActor* Actor : TestActors) + { + AEntityInteractionTestActor* TestActor = CastChecked(Actor); + + if (ensure(TestActor->Index < 2)) + { + if (TestActor->HasAuthority()) + { + LocalActors[TestActor->Index] = TestActor; + } + else + { + RemoteActors[TestActor->Index] = TestActor; + } + } + } + for (auto Actor : LocalActors) + { + AssertTrue(Actor != nullptr, TEXT("Missing test actors")); + } + for (auto Actor : RemoteActors) + { + AssertTrue(Actor != nullptr, TEXT("Missing test actors")); + } + FinishStep(); + }, + nullptr); + + AddStep( + "Send RPCs", FWorkerDefinition::AllServers, nullptr, + [this]() { + AActor::ExecuteWithNetWriteFence(*RemoteActors[0], *LocalActors[0], &AEntityInteractionTestActor::TestNetWriteFence, NumSteps); + EXPECT_IMMEDIATE(LocalActors[0], AEntityInteractionTestActor::s_NetWriteFenceName, TEXT("Remote dependent")); + + AActor::ExecuteWithNetWriteFence(*LocalActors[1], *LocalActors[0], &AEntityInteractionTestActor::TestNetWriteFence, NumSteps); + EXPECT_DELAYED(LocalActors[0], AEntityInteractionTestActor::s_NetWriteFenceName, TEXT("Local dependent")); + + LocalActors[1]->SendCrossServerRPC(*LocalActors[0], &AEntityInteractionTestActor::TestReliable, NumSteps); + EXPECT_IMMEDIATE(LocalActors[0], AEntityInteractionTestActor::s_ReliableName, TEXT("Local Receiver")); + + LocalActors[1]->SendCrossServerRPC(*RemoteActors[0], &AEntityInteractionTestActor::TestReliable, NumSteps); + EXPECT_DELAYED(RemoteActors[0], AEntityInteractionTestActor::s_ReliableName, TEXT("Remote Receiver")); + + LocalActors[0]->TestUnreliable(NumSteps); + EXPECT_IMMEDIATE(LocalActors[0], AEntityInteractionTestActor::s_UnreliableName, TEXT("Local Receiver")); + RemoteActors[0]->TestUnreliable(NumSteps); + EXPECT_DELAYED(RemoteActors[0], AEntityInteractionTestActor::s_UnreliableName, TEXT("Remote Receiver")); + + LocalActors[0]->TestUnordered(NumSteps); + EXPECT_IMMEDIATE(LocalActors[0], AEntityInteractionTestActor::s_UnorderedName, TEXT("Local Receiver")); + RemoteActors[0]->TestUnordered(NumSteps); + EXPECT_DELAYED(RemoteActors[0], AEntityInteractionTestActor::s_UnorderedName, TEXT("Remote Receiver")); + + LocalActors[1]->SendCrossServerRPC(*LocalActors[0], &AEntityInteractionTestActor::TestNoLoopback, NumSteps); + EXPECT_DELAYED(LocalActors[0], AEntityInteractionTestActor::s_NoLoopbackName, TEXT("Local Receiver")); + + LocalActors[1]->SendCrossServerRPC(*RemoteActors[0], &AEntityInteractionTestActor::TestNoLoopback, NumSteps); + EXPECT_DELAYED(RemoteActors[0], AEntityInteractionTestActor::s_NoLoopbackName, TEXT("Remote Receiver")); + + FinishStep(); + }, + nullptr, 5.0); + + AddStep( + "Check delayed RPCs", FWorkerDefinition::AllServers, + [this]() { + if (LocalActors[0]->Steps.Num() == NumSteps) + { + return true; + } + return false; + }, + [this]() { + for (auto const& Expected : ExpectedResult) + { + FString DebugString("Successful delayed execution of a "); + DebugString += Expected.Value; + AssertTrue(LocalActors[0]->Steps.Contains(Expected.Key) && LocalActors[0]->Steps[Expected.Key] == Expected.Value, + DebugString); + } + + FinishStep(); + }, + nullptr, 5.0); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestEntityInteration/EntityInteractionTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestEntityInteration/EntityInteractionTest.h new file mode 100644 index 0000000000..cbcbc3ea6c --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestEntityInteration/EntityInteractionTest.h @@ -0,0 +1,27 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialFunctionalTest.h" +#include "TestMaps/GeneratedTestMap.h" + +#include "EntityInteractionTest.generated.h" + +class AEntityInteractionTestActor; + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialEntityInteractionTest : public ASpatialFunctionalTest +{ + GENERATED_BODY() + +public: + ASpatialEntityInteractionTest(); + + virtual void PrepareTest() override; + + uint32 NumSteps = 0; + TMap ExpectedResult; + TStaticArray LocalActors; + TStaticArray RemoteActors; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestEntityInteration/EntityInteractionTestActor.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestEntityInteration/EntityInteractionTestActor.cpp new file mode 100644 index 0000000000..2691164aa5 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestEntityInteration/EntityInteractionTestActor.cpp @@ -0,0 +1,41 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "EntityInteractionTestActor.h" +#include "Components/SceneComponent.h" + +const FString AEntityInteractionTestActor::s_NetWriteFenceName(TEXT("NetWriteFence")); +const FString AEntityInteractionTestActor::s_ReliableName(TEXT("Reliable")); +const FString AEntityInteractionTestActor::s_UnreliableName(TEXT("Unreliable")); +const FString AEntityInteractionTestActor::s_UnorderedName(TEXT("Unordered")); +const FString AEntityInteractionTestActor::s_NoLoopbackName(TEXT("NoLoopback")); + +AEntityInteractionTestActor::AEntityInteractionTestActor() +{ + SceneComponent = CreateDefaultSubobject("Root"); + bReplicates = true; +} + +void AEntityInteractionTestActor::TestNetWriteFence_Implementation(int32 Param) +{ + Steps.Add(Param, s_NetWriteFenceName); +} + +void AEntityInteractionTestActor::TestReliable_Implementation(int32 Param) +{ + Steps.Add(Param, s_ReliableName); +} + +void AEntityInteractionTestActor::TestUnreliable_Implementation(int32 Param) +{ + Steps.Add(Param, s_UnreliableName); +} + +void AEntityInteractionTestActor::TestUnordered_Implementation(int32 Param) +{ + Steps.Add(Param, s_UnorderedName); +} + +void AEntityInteractionTestActor::TestNoLoopback_Implementation(int32 Param) +{ + Steps.Add(Param, s_NoLoopbackName); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestEntityInteration/EntityInteractionTestActor.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestEntityInteration/EntityInteractionTestActor.h new file mode 100644 index 0000000000..be8a197e75 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestEntityInteration/EntityInteractionTestActor.h @@ -0,0 +1,47 @@ +// Copyright (c) Improbable Worl*ds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "EntityInteractionTestActor.generated.h" + +class USceneComponent; + +UCLASS() +class AEntityInteractionTestActor : public AActor +{ + GENERATED_BODY() + +public: + static const FString s_NetWriteFenceName; + static const FString s_ReliableName; + static const FString s_UnreliableName; + static const FString s_UnorderedName; + static const FString s_NoLoopbackName; + + AEntityInteractionTestActor(); + + UFUNCTION(NetWriteFence, Reliable) + void TestNetWriteFence(int32 Param); + + UFUNCTION(CrossServer, Reliable) + void TestReliable(int32 Param); + + UFUNCTION(CrossServer, Unreliable) + void TestUnreliable(int32 Param); + + UFUNCTION(CrossServer, Reliable, Unordered) + void TestUnordered(int32 Param); + + UFUNCTION(CrossServer, Reliable, NetWriteFence) + void TestNoLoopback(int32 Param); + + UPROPERTY() + uint32 Index; + + TMap Steps; + + UPROPERTY() + USceneComponent* SceneComponent; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/SpatialTestHandoverActorComponentReplication.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/SpatialTestHandoverActorComponentReplication.cpp index 25d7c4db1b..ed931872f4 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/SpatialTestHandoverActorComponentReplication.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/SpatialTestHandoverActorComponentReplication.cpp @@ -21,6 +21,9 @@ void UTestHandoverComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; - UPROPERTY(Handover) + UPROPERTY(Replicated) int HandoverTestProperty = HandoverReplicationTestValues::BasicTestPropertyValue; - UPROPERTY(Handover) + UPROPERTY(Replicated) FHandoverReplicationTestStruct HandoverTestStruct; UPROPERTY(Replicated) @@ -99,16 +99,16 @@ class AHandoverReplicationTestCube : public AHandoverCube virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; - UPROPERTY(Handover) + UPROPERTY(Replicated) int HandoverTestProperty = HandoverReplicationTestValues::BasicTestPropertyValue; - UPROPERTY(Handover) + UPROPERTY(Replicated) FHandoverReplicationTestStruct HandoverTestStruct; UPROPERTY(Replicated) int ReplicatedTestProperty = HandoverReplicationTestValues::BasicTestPropertyValue; - UPROPERTY(Handover) + UPROPERTY(Replicated) EHandoverReplicationTestStage TestStage = EHandoverReplicationTestStage::Initial; UPROPERTY() diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/SpatialTestHandoverDynamicReplication.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/SpatialTestHandoverDynamicReplication.cpp index a89b852263..14c1049209 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/SpatialTestHandoverDynamicReplication.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/SpatialTestHandoverDynamicReplication.cpp @@ -1,4 +1,4 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "SpatialTestHandoverDynamicReplication.h" #include "EngineClasses/SpatialNetDriver.h" @@ -90,7 +90,7 @@ void ASpatialTestHandoverDynamicReplication::PrepareTest() AssertTrue(IsValid(NetDriver), TEXT("This test should be run with Spatial Networking")); - LoadBalancingStrategy = Cast(NetDriver->LoadBalanceStrategy); + LoadBalancingStrategy = GetLoadBalancingStrategy(); AssertTrue(IsValid(HandoverCube) && IsValid(LoadBalancingStrategy), TEXT("All servers should have a valid reference to the " "HandoverCube and the strategy")); diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestLoadBalancingData/SpatialTestLoadBalancingData.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestLoadBalancingData/SpatialTestLoadBalancingData.cpp index ec1e8b94d0..5404c625a9 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestLoadBalancingData/SpatialTestLoadBalancingData.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestLoadBalancingData/SpatialTestLoadBalancingData.cpp @@ -2,6 +2,7 @@ #include "SpatialGDK/SpatialTestLoadBalancingData/SpatialTestLoadBalancingData.h" +#include "Algo/AllOf.h" #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" #include "EngineClasses/SpatialWorldSettings.h" @@ -70,7 +71,8 @@ T* GetWorldActor(UWorld* World) { TArray DiscoveredActors; UGameplayStatics::GetAllActorsOfClass(World, T::StaticClass(), DiscoveredActors); - if (DiscoveredActors.Num() == 1) + // IsActorReady check is required to avoid potential nullptr in future test steps + if (DiscoveredActors.Num() == 1 && DiscoveredActors[0]->IsActorReady()) { return Cast(DiscoveredActors[0]); } @@ -87,9 +89,14 @@ void GetWorldActors(UWorld* World, TArray& OutActors) DiscoveredActors.Sort([](const AActor& Lhs, const AActor& Rhs) { return Lhs.GetActorLocation().Y < Rhs.GetActorLocation().Y; }); - Algo::Transform(DiscoveredActors, OutActors, [](AActor* Actor) -> U { - return Cast(Actor); - }); + if (Algo::AllOf(DiscoveredActors, [](AActor* Actor) -> bool { + return Actor->IsActorReady(); + })) + { + Algo::Transform(DiscoveredActors, OutActors, [](AActor* Actor) -> U { + return Cast(Actor); + }); + } } } @@ -108,14 +115,14 @@ void ASpatialTestLoadBalancingData::PrepareTest() const FWorkerDefinition OffloadedServer = FWorkerDefinition::Server(2); const FWorkerDefinition ZonedServers[]{ FWorkerDefinition::Server(3), FWorkerDefinition::Server(4) }; - AddStep(TEXT("Create an actor on the main server"), MainServer, nullptr, [this] { + AddStep(TEXT("Create an actor on the main server"), MainServer, nullptr, /*StartEvent= */ [this] { ASpatialTestLoadBalancingDataActor* SpawnedActor = GetWorld()->SpawnActor(); check(SpawnedActor->HasAuthority()); RegisterAutoDestroyActor(SpawnedActor); FinishStep(); }); - AddStep(TEXT("Create an offloaded actor on the offloaded server"), OffloadedServer, nullptr, [this] { + AddStep(TEXT("Create an offloaded actor on the offloaded server"), OffloadedServer, nullptr, /*StartEvent= */ [this] { ASpatialTestLoadBalancingDataOffloadedActor* SpawnedActor = GetWorld()->SpawnActor(); check(SpawnedActor->HasAuthority()); RegisterAutoDestroyActor(SpawnedActor); @@ -125,7 +132,7 @@ void ASpatialTestLoadBalancingData::PrepareTest() // One to the left of the boundary, one to the right. const static FVector ZonedActorsPositions[]{ { 100, -100, 100 }, { 100, 100, 100 } }; - AddStep(TEXT("Create zoned actor on zoned server 1"), ZonedServers[0], nullptr, [this] { + AddStep(TEXT("Create zoned actor on zoned server 1"), ZonedServers[0], nullptr, /*StartEvent= */ [this] { ASpatialTestLoadBalancingDataZonedActor* SpawnedActor = GetWorld()->SpawnActor(ZonedActorsPositions[0], FRotator::ZeroRotator); check(SpawnedActor->HasAuthority()); @@ -133,7 +140,7 @@ void ASpatialTestLoadBalancingData::PrepareTest() FinishStep(); }); - AddStep(TEXT("Create zoned actor on zoned server 2"), ZonedServers[1], nullptr, [this] { + AddStep(TEXT("Create zoned actor on zoned server 2"), ZonedServers[1], nullptr, /*StartEvent= */ [this] { ASpatialTestLoadBalancingDataZonedActor* SpawnedActor = GetWorld()->SpawnActor(ZonedActorsPositions[1], FRotator::ZeroRotator); check(SpawnedActor->HasAuthority()); @@ -141,75 +148,64 @@ void ASpatialTestLoadBalancingData::PrepareTest() FinishStep(); }); - constexpr float ActorReceiptTimeout = 5.0f; - AddStep( - TEXT("Retrieve actors on all workers"), FWorkerDefinition::AllWorkers, nullptr, nullptr, - [this](float) { - TargetActor = GetWorldActor(GetWorld()); - RequireTrue(TargetActor.IsValid(), TEXT("Received main actor")); + AddStep(TEXT("Trying to Retrieve ready actors on all workers"), FWorkerDefinition::AllWorkers, nullptr, nullptr, + /*TickEvent= */ [this](float) { + TargetActor = GetWorldActor(GetWorld()); + RequireTrue(TargetActor.IsValid(), TEXT("Received main actor")); - TargetOffloadedActor = GetWorldActor(GetWorld()); - RequireTrue(TargetOffloadedActor.IsValid(), TEXT("Received offloaded actor")); + TargetOffloadedActor = GetWorldActor(GetWorld()); + RequireTrue(TargetOffloadedActor.IsValid(), TEXT("Received offloaded actor")); - GetWorldActors(GetWorld(), TargetZonedActors); - RequireTrue(TargetZonedActors.Num() == 2, TEXT("Received zoned actors")); + GetWorldActors(GetWorld(), TargetZonedActors); + RequireEqual_Int(TargetZonedActors.Num(), 2, TEXT("Received zoned actors")); - FinishStep(); - }, - ActorReceiptTimeout); + FinishStep(); + }); - const float LoadBalancingDataReceiptTimeout = 5.0f; - AddStep( - TEXT("Confirm LB group IDs on the server"), FWorkerDefinition::AllServers, nullptr, nullptr, - [this](float) { - // ServerWorkers should have interest in LoadBalancingData so it should be available - TOptional LoadBalancingData = GetActorGroupData(TargetActor.Get()); - RequireTrue(LoadBalancingData.IsSet(), TEXT("Main actor entity has LoadBalancingData")); + AddStep(TEXT("Confirm LB group IDs on the server"), FWorkerDefinition::AllServers, nullptr, nullptr, /*TickEvent= */ [this](float) { + // ServerWorkers should have interest in LoadBalancingData so it should be available + TOptional LoadBalancingData = GetActorGroupData(TargetActor.Get()); + RequireTrue(LoadBalancingData.IsSet(), TEXT("Main actor entity has LoadBalancingData")); - TOptional OffloadedLoadBalancingData = GetActorGroupData(TargetOffloadedActor.Get()); - RequireTrue(OffloadedLoadBalancingData.IsSet(), TEXT("Offloaded actor entity has LoadBalancingData")); + TOptional OffloadedLoadBalancingData = GetActorGroupData(TargetOffloadedActor.Get()); + RequireTrue(OffloadedLoadBalancingData.IsSet(), TEXT("Offloaded actor entity has LoadBalancingData")); - const bool bIsValid = LoadBalancingData.IsSet() && OffloadedLoadBalancingData.IsSet() - && LoadBalancingData->ActorGroupId != OffloadedLoadBalancingData->ActorGroupId; + const bool bIsValid = LoadBalancingData.IsSet() && OffloadedLoadBalancingData.IsSet() + && LoadBalancingData->ActorGroupId != OffloadedLoadBalancingData->ActorGroupId; - RequireTrue(bIsValid, TEXT("Load balancing group ids are different for the main server and for the offloaded server")); + RequireTrue(bIsValid, TEXT("Load balancing group ids are different for the main server and for the offloaded server")); - FinishStep(); - }, - LoadBalancingDataReceiptTimeout); + FinishStep(); + }); - AddStep(TEXT("Put zoned actors to a single ownership group"), ZonedServers[0], nullptr, [this]() { + AddStep(TEXT("Put zoned actors to a single ownership group"), ZonedServers[0], nullptr, /*StartEvent= */ [this]() { AssertTrue(TargetZonedActors[0]->HasAuthority(), TEXT("Zoned actor 1 is owned by the zoned server 1")); TargetZonedActors[0]->SetOwner(TargetZonedActors[1].Get()); FinishStep(); }); - AddStep( - TEXT("Wait until actor set IDs are the same"), FWorkerDefinition::AllServers, nullptr, nullptr, - [this](float) { - RequireTrue(GetActorSetData(TargetZonedActors[0].Get())->ActorSetId == GetActorSetData(TargetZonedActors[1].Get())->ActorSetId, - TEXT("Actor set IDs are the same for the two zoned actors")); - const bool bShouldHaveAuthority = GetLocalWorkerId() == 4; - RequireTrue(TargetZonedActors[0]->HasAuthority() == bShouldHaveAuthority, TEXT("Authority was moved correctly")); - FinishStep(); - }, - LoadBalancingDataReceiptTimeout); - - AddStep(TEXT("Put main server actor and offloaded server actor into different ownership groups"), ZonedServers[1], nullptr, [this]() { - AssertTrue(TargetZonedActors[0]->HasAuthority(), TEXT("Zoned actor 1 is owned by the zoned server 2")); - TargetZonedActors[0]->SetOwner(nullptr); + AddStep(TEXT("Wait until actor set IDs are the same"), FWorkerDefinition::AllServers, nullptr, nullptr, /*TickEvent= */ [this](float) { + RequireTrue(GetActorSetData(TargetZonedActors[0].Get())->ActorSetId == GetActorSetData(TargetZonedActors[1].Get())->ActorSetId, + TEXT("Actor set IDs are the same for the two zoned actors")); + const bool bShouldHaveAuthority = GetLocalWorkerId() == 4; + RequireTrue(TargetZonedActors[0]->HasAuthority() == bShouldHaveAuthority, TEXT("Authority was moved correctly")); FinishStep(); }); + AddStep(TEXT("Put main server actor and offloaded server actor into different ownership groups"), ZonedServers[1], nullptr, + /*StartEvent= */ [this]() { + AssertTrue(TargetZonedActors[0]->HasAuthority(), TEXT("Zoned actor 1 is owned by the zoned server 2")); + TargetZonedActors[0]->SetOwner(nullptr); + FinishStep(); + }); + AddStep( - TEXT("Wait until actor set IDs different again"), FWorkerDefinition::AllServers, nullptr, nullptr, - [this](float) { + TEXT("Wait until actor set IDs different again"), FWorkerDefinition::AllServers, nullptr, nullptr, /*TickEvent= */ [this](float) { RequireTrue(GetActorSetData(TargetZonedActors[0].Get())->ActorSetId != GetActorSetData(TargetZonedActors[1].Get())->ActorSetId, TEXT("Actor set IDs are different for the two zoned actors")); FinishStep(); - }, - LoadBalancingDataReceiptTimeout); + }); } template diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestLoadBalancingData/SpatialTestLoadBalancingData.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestLoadBalancingData/SpatialTestLoadBalancingData.h index c1dbf5a12f..3cff83aa93 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestLoadBalancingData/SpatialTestLoadBalancingData.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestLoadBalancingData/SpatialTestLoadBalancingData.h @@ -25,7 +25,7 @@ class USpatialTestLoadBalancingDataTestMap : public UGeneratedTestMap virtual void CreateCustomContentForMap() override; }; -UCLASS() +UCLASS(HideDropdown) class USpatialTestLoadBalancingDataMultiWorkerSettings : public USpatialMultiWorkerSettings { GENERATED_BODY() diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestMultiServerUnrealComponents/TestUnrealComponents.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestMultiServerUnrealComponents/TestUnrealComponents.cpp index b01f954432..caad969767 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestMultiServerUnrealComponents/TestUnrealComponents.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestMultiServerUnrealComponents/TestUnrealComponents.cpp @@ -28,6 +28,7 @@ void UDataAndHandoverComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME_CONDITION(ThisClass, OriginalPawns, COND_ServerOnly); +} + void ASpatialTestRemotePossession::AddCleanupSteps() { AddStep(TEXT("Clean up the test"), FWorkerDefinition::AllServers, nullptr, /*StartEvent*/ [this]() { diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestRemotePossession.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestRemotePossession.h index a11c0512c2..faf57eaca5 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestRemotePossession.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestRemotePossession.h @@ -30,6 +30,8 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialTestRemotePossession : public ASpati virtual void PrepareTest() override; + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + ATestPossessionPawn* GetPawn(); bool IsReadyForPossess(); @@ -47,6 +49,6 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialTestRemotePossession : public ASpati void AddToOriginalPawns(ATestPossessionPlayerController* PlayerController, APawn* Pawn); // To save original Pawns and possess them back at the end - UPROPERTY(Handover, Transient) + UPROPERTY(Replicated, Transient) TArray OriginalPawns; }; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/TestPossessionPlayerController.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/TestPossessionPlayerController.cpp index a836679f60..36617437aa 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/TestPossessionPlayerController.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/TestPossessionPlayerController.cpp @@ -7,6 +7,7 @@ #include "EngineClasses/SpatialNetDriverDebugContext.h" #include "LoadBalancing/AbstractLBStrategy.h" #include "LoadBalancing/DebugLBStrategy.h" +#include "Net/UnrealNetwork.h" #include "SpatialConstants.h" #include "Utils/SpatialStatics.h" @@ -20,6 +21,14 @@ ATestPossessionPlayerController::ATestPossessionPlayerController() { } +void ATestPossessionPlayerController::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME_CONDITION(ThisClass, BeforePossessionWorkerId, COND_ServerOnly); + DOREPLIFETIME_CONDITION(ThisClass, AfterPossessionWorkerId, COND_ServerOnly); +} + void ATestPossessionPlayerController::OnPossess(APawn* InPawn) { Super::OnPossess(InPawn); diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/TestPossessionPlayerController.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/TestPossessionPlayerController.h index d6a3beea4f..a0cbe6400e 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/TestPossessionPlayerController.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/TestPossessionPlayerController.h @@ -21,6 +21,8 @@ class ATestPossessionPlayerController : public APlayerController public: ATestPossessionPlayerController(); + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + void RemotePossessOnServer(APawn* InPawn); void RemovePossessionComponent(); @@ -39,10 +41,10 @@ class ATestPossessionPlayerController : public APlayerController private: VirtualWorkerId GetCurrentWorkerId(); - UPROPERTY(Handover) + UPROPERTY(Replicated) uint32 BeforePossessionWorkerId; - UPROPERTY(Handover) + UPROPERTY(Replicated) uint32 AfterPossessionWorkerId; TArray Tokens; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestReplicationConditions/SpatialTestReplicationConditions.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestReplicationConditions/SpatialTestReplicationConditions.cpp index 824aba7c97..0f1c741ce2 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestReplicationConditions/SpatialTestReplicationConditions.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestReplicationConditions/SpatialTestReplicationConditions.cpp @@ -161,6 +161,7 @@ void ASpatialTestReplicationConditions::PrepareTest() bool bCondIgnore[COND_Max]{}; bCondIgnore[COND_AutonomousOnly] = true; bCondIgnore[COND_SkipOwner] = true; + bCondIgnore[COND_ServerOnly] = true; ProcessCommonActorProperties(bWrite, bCondIgnore); } @@ -249,6 +250,7 @@ void ASpatialTestReplicationConditions::PrepareTest() bCondIgnore[COND_OwnerOnly] = true; bCondIgnore[COND_AutonomousOnly] = true; bCondIgnore[COND_ReplayOrOwner] = true; + bCondIgnore[COND_ServerOnly] = true; ProcessCommonActorProperties(bWrite, bCondIgnore); } @@ -477,140 +479,141 @@ bool ASpatialTestReplicationConditions::ActorsReady() const return bReady; } -void ASpatialTestReplicationConditions::ProcessCommonActorProperties(bool bWrite, bool bCondIgnore[COND_Max]) +FString CondAsString(ELifetimeCondition Condition) { - // This function encapsulates the logic for both writing to, and reading from (and asserting) the properties of TestActor_Common. - // When bWrite is true, the Action lambda just writes the expected property values to the Actor. - // When bWrite is false, the Action lambda asserts that the each property has the expectant value. - // As not all properties are replicated to all workers, we need to also know which property types to expect. + const UEnum* EnumClass = FindObject(ANY_PACKAGE, TEXT("ELifetimeCondition"), true); + FName ConditionName = EnumClass->GetNameByValue((int64)Condition); + ensureAlwaysMsgf(ConditionName != NAME_None, TEXT("Could not find ELifetimeCondition")); + return ConditionName.ToString(); +} - auto Action = [&](int32& Source, int32 Expected, ELifetimeCondition Cond) { - if (bWrite) - { - Source = Expected + PropertyOffset; - } - else if (Cond == COND_SkipOwner && bSpatialEnabled) - { - // UNR-3714 - COND_SkipOwner broken on spatial in initial replication - } - else if (bCondIgnore[Cond]) - { - RequireEqual_Int(Source, 0, *FString::Printf(TEXT("Property replicated incorrectly on %s"), *GetLocalWorkerString())); - } - else - { - RequireEqual_Int(Source, Expected + PropertyOffset, - *FString::Printf(TEXT("Property replicated incorrectly on %s"), *GetLocalWorkerString())); - } +FString StaticCompText = TEXT(" on static component"); +FString DynamicCompText = TEXT(" on dynamic component"); + +// This function encapsulates the logic for both writing to, and reading from (and asserting) the properties of TestActor_Common. +// When bWrite is true, the Action lambda just writes the expected property values to the Actor. +// When bWrite is false, the Action lambda asserts that the each property has the expectant value. +// As not all properties are replicated to all workers, we need to also know which property types to expect. +void ASpatialTestReplicationConditions::Action(int32& Source, int32 Expected, ELifetimeCondition Cond, bool bWrite, + bool bCondIgnore[COND_Max], FString AdditionalText) +{ + if (bWrite) + { + Source = Expected + PropertyOffset; + } + else if (Cond == COND_SkipOwner && bSpatialEnabled) + { + // UNR-3714 - COND_SkipOwner broken on spatial in initial replication + } + else if (bCondIgnore[Cond]) + { + RequireEqual_Int(Source, 0, + *FString::Printf(TEXT("Property%s with condition %s should not be replicated to %s"), *AdditionalText, + *CondAsString(Cond), *GetLocalWorkerString())); + } + else + { + RequireEqual_Int(Source, Expected + PropertyOffset, + *FString::Printf(TEXT("Property%s with condition %s should be replicated to %s"), *AdditionalText, + *CondAsString(Cond), *GetLocalWorkerString())); + } +} + +void ASpatialTestReplicationConditions::ProcessCommonActorProperties(bool bWrite, bool bCondIgnore[COND_Max]) +{ + auto WrappedAction = [&](int32& Source, int32 Expected, ELifetimeCondition Cond, FString AdditionalText = TEXT("")) { + Action(Source, Expected, Cond, bWrite, bCondIgnore, AdditionalText); }; - Action(TestActor_Common->CondNone_Var, 10, COND_None); - Action(TestActor_Common->CondOwnerOnly_Var, 30, COND_OwnerOnly); - Action(TestActor_Common->CondSkipOwner_Var, 40, COND_SkipOwner); - Action(TestActor_Common->CondSimulatedOnly_Var, 50, COND_SimulatedOnly); - Action(TestActor_Common->CondAutonomousOnly_Var, 60, COND_AutonomousOnly); - Action(TestActor_Common->CondSimulatedOrPhysics_Var, 70, COND_SimulatedOrPhysics); - Action(TestActor_Common->CondReplayOrOwner_Var, 100, COND_ReplayOrOwner); - Action(TestActor_Common->CondSimulatedOnlyNoReplay_Var, 120, COND_SimulatedOnlyNoReplay); - Action(TestActor_Common->CondSimulatedOrPhysicsNoReplay_Var, 130, COND_SimulatedOrPhysicsNoReplay); - Action(TestActor_Common->CondSkipReplay_Var, 140, COND_SkipReplay); - - Action(TestActor_Common->StaticComponent->CondNone_Var, 210, COND_None); - Action(TestActor_Common->StaticComponent->CondOwnerOnly_Var, 230, COND_OwnerOnly); - Action(TestActor_Common->StaticComponent->CondSkipOwner_Var, 240, COND_SkipOwner); - Action(TestActor_Common->StaticComponent->CondSimulatedOnly_Var, 250, COND_SimulatedOnly); - Action(TestActor_Common->StaticComponent->CondAutonomousOnly_Var, 260, COND_AutonomousOnly); - Action(TestActor_Common->StaticComponent->CondSimulatedOrPhysics_Var, 270, COND_SimulatedOrPhysics); - Action(TestActor_Common->StaticComponent->CondReplayOrOwner_Var, 300, COND_ReplayOrOwner); - Action(TestActor_Common->StaticComponent->CondSimulatedOnlyNoReplay_Var, 320, COND_SimulatedOnlyNoReplay); - Action(TestActor_Common->StaticComponent->CondSimulatedOrPhysicsNoReplay_Var, 330, COND_SimulatedOrPhysicsNoReplay); - Action(TestActor_Common->StaticComponent->CondSkipReplay_Var, 340, COND_SkipReplay); - - Action(TestActor_Common->DynamicComponent->CondNone_Var, 410, COND_None); - Action(TestActor_Common->DynamicComponent->CondOwnerOnly_Var, 430, COND_OwnerOnly); - Action(TestActor_Common->DynamicComponent->CondSkipOwner_Var, 440, COND_SkipOwner); - Action(TestActor_Common->DynamicComponent->CondSimulatedOnly_Var, 450, COND_SimulatedOnly); - Action(TestActor_Common->DynamicComponent->CondAutonomousOnly_Var, 460, COND_AutonomousOnly); - Action(TestActor_Common->DynamicComponent->CondSimulatedOrPhysics_Var, 470, COND_SimulatedOrPhysics); - Action(TestActor_Common->DynamicComponent->CondReplayOrOwner_Var, 500, COND_ReplayOrOwner); - Action(TestActor_Common->DynamicComponent->CondSimulatedOnlyNoReplay_Var, 520, COND_SimulatedOnlyNoReplay); - Action(TestActor_Common->DynamicComponent->CondSimulatedOrPhysicsNoReplay_Var, 530, COND_SimulatedOrPhysicsNoReplay); - Action(TestActor_Common->DynamicComponent->CondSkipReplay_Var, 540, COND_SkipReplay); + WrappedAction(TestActor_Common->CondNone_Var, 10, COND_None); + WrappedAction(TestActor_Common->CondOwnerOnly_Var, 30, COND_OwnerOnly); + WrappedAction(TestActor_Common->CondSkipOwner_Var, 40, COND_SkipOwner); + WrappedAction(TestActor_Common->CondSimulatedOnly_Var, 50, COND_SimulatedOnly); + WrappedAction(TestActor_Common->CondAutonomousOnly_Var, 60, COND_AutonomousOnly); + WrappedAction(TestActor_Common->CondSimulatedOrPhysics_Var, 70, COND_SimulatedOrPhysics); + WrappedAction(TestActor_Common->CondReplayOrOwner_Var, 100, COND_ReplayOrOwner); + WrappedAction(TestActor_Common->CondSimulatedOnlyNoReplay_Var, 120, COND_SimulatedOnlyNoReplay); + WrappedAction(TestActor_Common->CondSimulatedOrPhysicsNoReplay_Var, 130, COND_SimulatedOrPhysicsNoReplay); + WrappedAction(TestActor_Common->CondSkipReplay_Var, 140, COND_SkipReplay); + WrappedAction(TestActor_Common->CondServerOnly_Var, 150, COND_ServerOnly); + + WrappedAction(TestActor_Common->StaticComponent->CondNone_Var, 210, COND_None, StaticCompText); + WrappedAction(TestActor_Common->StaticComponent->CondOwnerOnly_Var, 230, COND_OwnerOnly, StaticCompText); + WrappedAction(TestActor_Common->StaticComponent->CondSkipOwner_Var, 240, COND_SkipOwner, StaticCompText); + WrappedAction(TestActor_Common->StaticComponent->CondSimulatedOnly_Var, 250, COND_SimulatedOnly, StaticCompText); + WrappedAction(TestActor_Common->StaticComponent->CondAutonomousOnly_Var, 260, COND_AutonomousOnly, StaticCompText); + WrappedAction(TestActor_Common->StaticComponent->CondSimulatedOrPhysics_Var, 270, COND_SimulatedOrPhysics, StaticCompText); + WrappedAction(TestActor_Common->StaticComponent->CondReplayOrOwner_Var, 300, COND_ReplayOrOwner, StaticCompText); + WrappedAction(TestActor_Common->StaticComponent->CondSimulatedOnlyNoReplay_Var, 320, COND_SimulatedOnlyNoReplay, StaticCompText); + WrappedAction(TestActor_Common->StaticComponent->CondSimulatedOrPhysicsNoReplay_Var, 330, COND_SimulatedOrPhysicsNoReplay, + StaticCompText); + WrappedAction(TestActor_Common->StaticComponent->CondSkipReplay_Var, 340, COND_SkipReplay, StaticCompText); + WrappedAction(TestActor_Common->StaticComponent->CondServerOnly_Var, 350, COND_ServerOnly, StaticCompText); + + WrappedAction(TestActor_Common->DynamicComponent->CondNone_Var, 410, COND_None, DynamicCompText); + WrappedAction(TestActor_Common->DynamicComponent->CondOwnerOnly_Var, 430, COND_OwnerOnly, DynamicCompText); + WrappedAction(TestActor_Common->DynamicComponent->CondSkipOwner_Var, 440, COND_SkipOwner, DynamicCompText); + WrappedAction(TestActor_Common->DynamicComponent->CondSimulatedOnly_Var, 450, COND_SimulatedOnly, DynamicCompText); + WrappedAction(TestActor_Common->DynamicComponent->CondAutonomousOnly_Var, 460, COND_AutonomousOnly, DynamicCompText); + WrappedAction(TestActor_Common->DynamicComponent->CondSimulatedOrPhysics_Var, 470, COND_SimulatedOrPhysics, DynamicCompText); + WrappedAction(TestActor_Common->DynamicComponent->CondReplayOrOwner_Var, 500, COND_ReplayOrOwner, DynamicCompText); + WrappedAction(TestActor_Common->DynamicComponent->CondSimulatedOnlyNoReplay_Var, 520, COND_SimulatedOnlyNoReplay, DynamicCompText); + WrappedAction(TestActor_Common->DynamicComponent->CondSimulatedOrPhysicsNoReplay_Var, 530, COND_SimulatedOrPhysicsNoReplay, + DynamicCompText); + WrappedAction(TestActor_Common->DynamicComponent->CondSkipReplay_Var, 540, COND_SkipReplay, DynamicCompText); + WrappedAction(TestActor_Common->DynamicComponent->CondServerOnly_Var, 550, COND_ServerOnly, DynamicCompText); } void ASpatialTestReplicationConditions::ProcessCustomActorProperties(ATestReplicationConditionsActor_Custom* Actor, bool bWrite, bool bCustomEnabled) { - auto Action = [&](int32& Source, int32 Expected) { - if (bWrite) - { - Source = Expected + PropertyOffset; - } - else if (bCustomEnabled) - { - RequireEqual_Int(Source, Expected + PropertyOffset, - *FString::Printf(TEXT("Property replicated incorrectly on %s"), *GetLocalWorkerString())); - } - else - { - RequireEqual_Int(Source, 0, *FString::Printf(TEXT("Property replicated incorrectly on %s"), *GetLocalWorkerString())); - } + auto WrappedAction = [&](int32& Source, int32 Expected, FString AdditionalText = TEXT("")) { + bool CondIgnore[COND_Max]{}; + Action(Source, Expected, COND_Custom, bWrite, CondIgnore, AdditionalText); }; - Action(Actor->CondCustom_Var, bCustomEnabled ? 1010 : 2010); - Action(Actor->StaticComponent->CondCustom_Var, bCustomEnabled ? 1020 : 2020); - Action(Actor->DynamicComponent->CondCustom_Var, bCustomEnabled ? 1030 : 2030); + WrappedAction(Actor->CondCustom_Var, bCustomEnabled ? 1010 : 2010); + WrappedAction(Actor->StaticComponent->CondCustom_Var, bCustomEnabled ? 1020 : 2020, StaticCompText); + WrappedAction(Actor->DynamicComponent->CondCustom_Var, bCustomEnabled ? 1030 : 2030, DynamicCompText); } void ASpatialTestReplicationConditions::ProcessAutonomousOnlyActorProperties(bool bWrite, bool bAutonomousExpected, bool bSimulatedExpected) { - auto Action = [&](int32& Source, bool bExpected, int32 Expected) { - if (bWrite) - { - Source = Expected + PropertyOffset; - } - else if (bExpected) - { - RequireEqual_Int(Source, Expected + PropertyOffset, - *FString::Printf(TEXT("Property replicated incorrectly on %s"), *GetLocalWorkerString())); - } - else - { - RequireEqual_Int(Source, 0, *FString::Printf(TEXT("Property replicated incorrectly on %s"), *GetLocalWorkerString())); - } + auto WrappedAction = [&](int32& Source, bool bExpected, int32 Expected, ELifetimeCondition Cond, FString AdditionalText = TEXT("")) { + bool CondIgnore[COND_Max]{}; + CondIgnore[Cond] = !bExpected; + Action(Source, Expected, Cond, bWrite, CondIgnore, AdditionalText); }; - Action(TestActor_AutonomousOnly->CondAutonomousOnly_Var, bAutonomousExpected, 3010); - Action(TestActor_AutonomousOnly->CondSimulatedOnly_Var, bSimulatedExpected, 3020); - Action(TestActor_AutonomousOnly->StaticComponent->CondAutonomousOnly_Var, bAutonomousExpected, 3030); - Action(TestActor_AutonomousOnly->StaticComponent->CondSimulatedOnly_Var, bSimulatedExpected, 3040); - Action(TestActor_AutonomousOnly->DynamicComponent->CondAutonomousOnly_Var, bAutonomousExpected, 3050); - Action(TestActor_AutonomousOnly->DynamicComponent->CondSimulatedOnly_Var, bSimulatedExpected, 3060); + WrappedAction(TestActor_AutonomousOnly->CondAutonomousOnly_Var, bAutonomousExpected, 3010, COND_AutonomousOnly); + WrappedAction(TestActor_AutonomousOnly->CondSimulatedOnly_Var, bSimulatedExpected, 3020, COND_SimulatedOnly); + WrappedAction(TestActor_AutonomousOnly->StaticComponent->CondAutonomousOnly_Var, bAutonomousExpected, 3030, COND_AutonomousOnly, + StaticCompText); + WrappedAction(TestActor_AutonomousOnly->StaticComponent->CondSimulatedOnly_Var, bSimulatedExpected, 3040, COND_SimulatedOnly, + StaticCompText); + WrappedAction(TestActor_AutonomousOnly->DynamicComponent->CondAutonomousOnly_Var, bAutonomousExpected, 3050, COND_AutonomousOnly, + DynamicCompText); + WrappedAction(TestActor_AutonomousOnly->DynamicComponent->CondSimulatedOnly_Var, bSimulatedExpected, 3060, COND_SimulatedOnly, + DynamicCompText); } void ASpatialTestReplicationConditions::ProcessPhysicsActorProperties(ATestReplicationConditionsActor_Physics* Actor, bool bWrite, bool bPhysicsEnabled, bool bPhysicsExpected) { - auto Action = [&](int32& Source, int32 Expected) { - if (bWrite) - { - Source = Expected + PropertyOffset; - } - else if (bPhysicsExpected) - { - RequireEqual_Int(Source, Expected + PropertyOffset, - *FString::Printf(TEXT("Property replicated incorrectly on %s"), *GetLocalWorkerString())); - } - else - { - RequireEqual_Int(Source, 0, *FString::Printf(TEXT("Property replicated incorrectly on %s"), *GetLocalWorkerString())); - } + auto WrappedAction = [&](int32& Source, int32 Expected, ELifetimeCondition Cond, FString AdditionalText = TEXT("")) { + bool CondIgnore[COND_Max]{}; + CondIgnore[Cond] = !bPhysicsExpected; + Action(Source, Expected, Cond, bWrite, CondIgnore, AdditionalText); }; - Action(Actor->CondSimulatedOrPhysics_Var, (bPhysicsEnabled) ? 4010 : 5010); - Action(Actor->CondSimulatedOrPhysicsNoReplay_Var, (bPhysicsEnabled) ? 4020 : 5020); - Action(Actor->StaticComponent->CondSimulatedOrPhysics_Var, (bPhysicsEnabled) ? 4030 : 5030); - Action(Actor->StaticComponent->CondSimulatedOrPhysicsNoReplay_Var, (bPhysicsEnabled) ? 4040 : 5040); - Action(Actor->DynamicComponent->CondSimulatedOrPhysics_Var, (bPhysicsEnabled) ? 4050 : 5050); - Action(Actor->DynamicComponent->CondSimulatedOrPhysicsNoReplay_Var, (bPhysicsEnabled) ? 4060 : 5060); + WrappedAction(Actor->CondSimulatedOrPhysics_Var, (bPhysicsEnabled) ? 4010 : 5010, COND_SimulatedOrPhysics); + WrappedAction(Actor->CondSimulatedOrPhysicsNoReplay_Var, (bPhysicsEnabled) ? 4020 : 5020, COND_SimulatedOrPhysicsNoReplay); + WrappedAction(Actor->StaticComponent->CondSimulatedOrPhysics_Var, (bPhysicsEnabled) ? 4030 : 5030, COND_SimulatedOrPhysics, + StaticCompText); + WrappedAction(Actor->StaticComponent->CondSimulatedOrPhysicsNoReplay_Var, (bPhysicsEnabled) ? 4040 : 5040, + COND_SimulatedOrPhysicsNoReplay, StaticCompText); + WrappedAction(Actor->DynamicComponent->CondSimulatedOrPhysics_Var, (bPhysicsEnabled) ? 4050 : 5050, COND_SimulatedOrPhysics, + DynamicCompText); + WrappedAction(Actor->DynamicComponent->CondSimulatedOrPhysicsNoReplay_Var, (bPhysicsEnabled) ? 4060 : 5060, + COND_SimulatedOrPhysicsNoReplay, DynamicCompText); } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestReplicationConditions/SpatialTestReplicationConditions.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestReplicationConditions/SpatialTestReplicationConditions.h index 02d0b86eff..300f8d431c 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestReplicationConditions/SpatialTestReplicationConditions.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestReplicationConditions/SpatialTestReplicationConditions.h @@ -63,4 +63,7 @@ class ASpatialTestReplicationConditions : public ASpatialFunctionalTest EPropertyReplicationStage ReplicationStage; int32 PropertyOffset; + +private: + void Action(int32& Source, int32 Expected, ELifetimeCondition Cond, bool bWrite, bool bCondIgnore[COND_Max], FString AdditionalText); }; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestReplicationConditions/TestReplicationConditionsActor.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestReplicationConditions/TestReplicationConditionsActor.cpp index 0f33d5677b..bf5bb6aa48 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestReplicationConditions/TestReplicationConditionsActor.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestReplicationConditions/TestReplicationConditionsActor.cpp @@ -53,6 +53,9 @@ void UTestReplicationConditionsComponent_Common::GetLifetimeReplicatedProps(TArr DOREPLIFETIME_CONDITION(ThisClass, CondSimulatedOnlyNoReplay_Var, COND_SimulatedOnlyNoReplay); DOREPLIFETIME_CONDITION(ThisClass, CondSimulatedOrPhysicsNoReplay_Var, COND_SimulatedOrPhysics); DOREPLIFETIME_CONDITION(ThisClass, CondSkipReplay_Var, COND_SkipReplay); + + // Our added conditions + DOREPLIFETIME_CONDITION(ThisClass, CondServerOnly_Var, COND_ServerOnly); } ATestReplicationConditionsActor_Common::ATestReplicationConditionsActor_Common() @@ -82,6 +85,9 @@ void ATestReplicationConditionsActor_Common::GetLifetimeReplicatedProps(TArray DOREPLIFETIME(ThisClass, ReferencesArray); DOREPLIFETIME_CONDITION(ThisClass, OwnerOnlyReplicatedVar, COND_OwnerOnly); DOREPLIFETIME_CONDITION(ThisClass, InitialOnlyReplicatedVar, COND_InitialOnly); + DOREPLIFETIME_CONDITION(ThisClass, HandoverReplicatedVar, COND_ServerOnly); } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/TestDynamicComponent.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/TestDynamicComponent.h index 6ce8a7fe4e..2ff665ab9d 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/TestDynamicComponent.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/TestDynamicComponent.h @@ -28,6 +28,6 @@ class UTestDynamicComponent : public USceneComponent UPROPERTY(Replicated) int32 InitialOnlyReplicatedVar; - UPROPERTY(Handover) + UPROPERTY(Replicated) int32 HandoverReplicatedVar; }; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/StaticSubobjectsTest/StaticSubobjectTestActor.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/StaticSubobjectsTest/StaticSubobjectTestActor.cpp new file mode 100644 index 0000000000..68bacad6e7 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/StaticSubobjectsTest/StaticSubobjectTestActor.cpp @@ -0,0 +1,32 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "StaticSubobjectTestActor.h" +#include "Net/UnrealNetwork.h" + +AStaticSubobjectTestActor::AStaticSubobjectTestActor() +{ + TestIntProperty = 0; + bNetLoadOnClient = true; + bNetLoadOnNonAuthServer = true; + bReplicates = true; + + TestStaticComponent1 = CreateDefaultSubobject(TEXT("ToRemoveComponent1")); + TestStaticComponent1->SetIsReplicated(true); + + TestStaticComponent2 = CreateDefaultSubobject(TEXT("ToRemoveComponent2")); + TestStaticComponent2->SetIsReplicated(true); +} + +void AStaticSubobjectTestActor::InitialiseTestIntProperty() +{ + TestIntProperty = 0; +} + +void AStaticSubobjectTestActor::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(AStaticSubobjectTestActor, TestIntProperty); + DOREPLIFETIME(AStaticSubobjectTestActor, TestStaticComponent1); + DOREPLIFETIME(AStaticSubobjectTestActor, TestStaticComponent2); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/StaticSubobjectsTest/StaticSubobjectTestActor.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/StaticSubobjectsTest/StaticSubobjectTestActor.h new file mode 100644 index 0000000000..1f85960540 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/StaticSubobjectsTest/StaticSubobjectTestActor.h @@ -0,0 +1,30 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h" + +#include "StaticSubobjectTestActor.generated.h" + +UCLASS() +class AStaticSubobjectTestActor : public AReplicatedTestActorBase +{ + GENERATED_BODY() + +public: + AStaticSubobjectTestActor(); + + void InitialiseTestIntProperty(); + + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + UPROPERTY(Replicated) + int TestIntProperty; + + UPROPERTY(Replicated) + USceneComponent* TestStaticComponent1; + + UPROPERTY(Replicated) + USceneComponent* TestStaticComponent2; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/StaticSubobjectsTest/StaticSubobjectsTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/StaticSubobjectsTest/StaticSubobjectsTest.cpp new file mode 100644 index 0000000000..f1e2d31c0b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/StaticSubobjectsTest/StaticSubobjectsTest.cpp @@ -0,0 +1,330 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "StaticSubobjectsTest.h" +#include "SpatialFunctionalTestFlowController.h" +#include "StaticSubObjectTestActor.h" + +#include "GameFramework/PlayerController.h" +#include "Kismet/GameplayStatics.h" + +#include "Components/SceneComponent.h" + +#include "EngineClasses/SpatialNetDriver.h" +#include "SpatialGDK/TestActors/TestMovementCharacter.h" + +/** + * Tests the deletion of replicated static subobjects on a bNetLoadOnClient actor by the server while that actor isn't in a client's + *interest. + * + * There are two expected outcomes. + * + * Outcome 1. + * + * A static subobject can be deleted on the server before the server has ever communicated with the client about that subobject. + * When the client then gains interest in the actor, the subobject should remain UNDELETED. This is a byproduct of native's implementation + *which we are matching. + * + * Outcome 2. + * + * A static subobject is deleted on the server while its actor is not in the client's interest. However the client and the server have + *previously communicated about the subobject. In this case, when the actor comes back into the clients interest, that subobject should be + *deleted on the client. + * + * + * The test includes a single server and one client worker. + * + * The flow is as follows: + * - Setup: + * - One cube actor already placed in the level at Location FVector(-20000.0f, -20000.0f, 40.0f) needs to be a startup actor - + *bNetLoadOnClient = true. + * - The Server spawns a TestMovementCharacter and makes Client 1 possess it. + * - Test: + * - Each worker tests if the AStaticSubobjectTestActor has indeed been loaded into the map. + * - Server increases the TestIntProperty + * - Client checks it can't see the updated property as its pawn is still at spawn/the origin + * - Server removes a static component from the actor + * - Client moves to the remote location near the actor + * - Client checks it still sees the component on the actor that was removed on the server + * - Client moves back to spawn + * - Server removes another static component + * - Client moves to remote location + * - Client checks that the static component most recently removed by the server has also been removed on the client. + * + * - Cleanup: + * - Client repossesses its default pawn. + * - The spawned Character is destroyed. + * + */ + +static constexpr float StepTimeLimit = 15.0f; +static constexpr float ClientUpdateWaitTime = 0.5f; + +AStaticSubobjectsTest::AStaticSubobjectsTest() + : Super() +{ + Author = "Arthur"; + Description = TEXT("Test Static Subobjects Deletion in Client"); +} + +void AStaticSubobjectsTest::PrepareTest() +{ + Super::PrepareTest(); + + StepTimer = 0.0f; + + // Step 0 - The server spawns a TestMovementCharacter and makes Client 1 possess it. + AddStep(TEXT("StaticSubobjectsTestSetup"), FWorkerDefinition::Server(1), nullptr, [this]() { + ASpatialFunctionalTestFlowController* ClientOneFlowController = GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1); + APlayerController* PlayerOneController = Cast(ClientOneFlowController->GetOwner()); + + if (AssertIsValid(PlayerOneController, TEXT("PlayerOneController should be valid"))) + { + ClientSpawnedPawn = GetWorld()->SpawnActor(PawnSpawnLocation, FRotator::ZeroRotator); + RegisterAutoDestroyActor(ClientSpawnedPawn); + ClientDefaultPawn = PlayerOneController->GetPawn(); + PlayerOneController->Possess(ClientSpawnedPawn); + + FinishStep(); + } + }); + + // Step 1 - All workers check for one AStaticSubobjectTestActor in the world and initialise it. + AddStep(TEXT("StaticSubobjectsTestAllWorkersInitialise"), FWorkerDefinition::AllWorkers, nullptr, [this]() { + TestActor = GetReplicatedTestActor(); + TestActor->InitialiseTestIntProperty(); + FinishStep(); + }); + + // Step 2 - Client 1 checks if it has correctly possessed the TestMovementCharacter. + AddStep( + TEXT("StaticSubobjectsTestClientCheckPawnPossesion"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this](float DeltaTime) { + APawn* PlayerPawn = GetFlowPawn(); + RequireTrue(IsValid(PlayerPawn), TEXT("PlayerCharacter should be valid")); + RequireTrue(PlayerPawn == GetFlowPlayerController()->AcknowledgedPawn, TEXT("The client should possess the pawn.")); + FinishStep(); + }, + StepTimeLimit); + + // Step 3 + AddStep(TEXT("StaticSubobjectsTestClientSeeRightNumberComponentsWithoutWait1"), FWorkerDefinition::Client(1), nullptr, [this]() { + AssertIsValid(TestActor->TestStaticComponent1, TEXT("TestStaticComponent1 should be valid.")); + AssertIsValid(TestActor->TestStaticComponent2, TEXT("TestStaticComponent2 should be valid.")); + + AssertEqual_Int(GetNumComponentsOnTestActor(), InitialNumComponents, TEXT("The client should see the right number of components.")); + + FinishStep(); + }); + + // Step 4 + ServerSetIntProperty(1); + + // Step 5 + CheckClientCanNotSeeIntPropertyWithWait(1); + + // Step 6 - Server removes a static component + AddStep(TEXT("StaticSubobjectsTestServerDestroyOneSubobject"), FWorkerDefinition::Server(1), nullptr, [this]() { + AssertEqual_Int(GetNumComponentsOnTestActor(), InitialNumComponents, + TEXT("AStaticSubobjectTestActor should have the initial number of components")); + + TestActor->TestStaticComponent1->DestroyComponent(); + + AssertEqual_Int(GetNumComponentsOnTestActor(), InitialNumComponents - 1, + TEXT("AStaticSubobjectTestActor should have had one component removed")); + FinishStep(); + }); + + // Step 7 + MoveClientPawn(PawnRemoteLocation); + + // Step 8 + CheckClientSeeIntProperty(1); + + // Step 9 + AddStep(TEXT("StaticSubobjectsTestClientSeeRightNumberComponentsWithoutWait2"), FWorkerDefinition::Client(1), nullptr, [this]() { + AssertEqual_Int(GetNumComponentsOnTestActor(), InitialNumComponents, TEXT("The client should see the right number of components.")); + FinishStep(); + }); + + // Step 10 + MoveClientPawn(PawnSpawnLocation); + + WaitForRelevancyUpdateIfInNative(); + + // Step 11 + ServerSetIntProperty(2); + + // Step 12 + CheckClientCanNotSeeIntPropertyWithWait(2); + + // Step 12 - Remove another component from the subobject. This time the client should see the removal once the actor is back in its + // interest + AddStep(TEXT("StaticSubobjectsTestServerDestroySecondSubobject"), FWorkerDefinition::Server(1), nullptr, [this]() { + AssertEqual_Int(GetNumComponentsOnTestActor(), InitialNumComponents - 1, + TEXT("AStaticSubobjectTestActor should have one less than the initial number of components on the server.")); + TestActor->TestStaticComponent2->DestroyComponent(); + AssertEqual_Int(GetNumComponentsOnTestActor(), InitialNumComponents - 2, + TEXT("AStaticSubobjectTestActor should have had another component removed")); + FinishStep(); + }); + + // Step 14 + AddStep( + TEXT("StaticSubobjectsTestClientSeeRightNumberComponentsWithWait"), FWorkerDefinition::Client(1), nullptr, + [this]() { + StepTimer = 0.f; + }, + [this](const float DeltaTime) { + RequireEqual_Int(GetNumComponentsOnTestActor(), InitialNumComponents, + TEXT("The client should see the right number of components.")); + + StepTimer += DeltaTime; + if (StepTimer >= ClientUpdateWaitTime) + { + FinishStep(); + } + }, + StepTimeLimit); + + // Step 15 + MoveClientPawn(PawnRemoteLocation); + + // Step 16 + CheckClientSeeIntProperty(2); + + // Step 17 + AddStep(TEXT("StaticSubobjectsTestClientSeeRightNumberComponentsWithoutWait3"), FWorkerDefinition::Client(1), nullptr, [this]() { + RequireEqual_Int(GetNumComponentsOnTestActor(), InitialNumComponents - 1, + TEXT("The client should see the right number of components.")); + + FinishStep(); + }); + + // Step 18 - Server Cleanup. + AddStep(TEXT("StaticSubobjectsTestServerCleanup"), FWorkerDefinition::Server(1), nullptr, [this]() { + // Possess the original pawn, so that the spawned character can get destroyed correctly + ASpatialFunctionalTestFlowController* ClientOneFlowController = GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1); + APlayerController* PlayerController = Cast(ClientOneFlowController->GetOwner()); + + if (AssertIsValid(PlayerController, TEXT("PlayerController should be valid"))) + { + PlayerController->Possess(ClientDefaultPawn); + FinishStep(); + } + }); +} + +void AStaticSubobjectsTest::MoveClientPawn(FVector& ToLocation) +{ + // Server moves Client 1's pawn to the location + AddStep(TEXT("StaticSubobjectsTestServerMoveClient"), FWorkerDefinition::Server(1), nullptr, [this, &ToLocation]() { + if (AssertIsValid(ClientSpawnedPawn, TEXT("ClientSpawnedPawn should be valid"))) + { + ClientSpawnedPawn->SetActorLocation(ToLocation); + AssertEqual_Vector(ClientSpawnedPawn->GetActorLocation(), ToLocation, TEXT("Client pawn should have been moved to location"), + 1.0f); + } + FinishStep(); + }); + + // Client 1 makes sure that the movement was correctly replicated + AddStep( + TEXT("StaticSubobjectsTestClientCheckMovement"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this, &ToLocation](float DeltaTime) { + APawn* PlayerCharacter = GetFlowPawn(); + + if (AssertIsValid(PlayerCharacter, TEXT("PlayerCharacter should be valid"))) + { + RequireEqual_Vector(PlayerCharacter->GetActorLocation(), ToLocation, TEXT("Character was not moved to location"), 1.0f); + FinishStep(); + } + }, + StepTimeLimit); +} + +void AStaticSubobjectsTest::CheckClientCanNotSeeIntPropertyWithWait(int ShouldntSeeVal) +{ + AddStep( + FString::Printf(TEXT("StaticSubobjectsTestClientCheckIntValueDidntIncreaseTo%dWithWait"), ShouldntSeeVal), + FWorkerDefinition::Client(1), nullptr, + [this]() { + StepTimer = 0.f; + }, + [this, ShouldntSeeVal](const float DeltaTime) { + RequireNotEqual_Int(TestActor->TestIntProperty, ShouldntSeeVal, + TEXT("The updated TestIntProperty shouldn't have been replicated")); + StepTimer += DeltaTime; + if (StepTimer >= ClientUpdateWaitTime) + { + FinishStep(); + } + }, + StepTimeLimit); +} + +void AStaticSubobjectsTest::ServerSetIntProperty(int IntPropertyNewVal) +{ + AddStep(FString::Printf(TEXT("StaticSubobjectsTestServerSetsIntValueTo%d"), IntPropertyNewVal), FWorkerDefinition::Server(1), nullptr, + [this, IntPropertyNewVal]() { + TestActor->TestIntProperty = IntPropertyNewVal; + FinishStep(); + }); +} + +void AStaticSubobjectsTest::CheckClientSeeIntProperty(int IntPropertyVal) +{ + AddStep( + FString::Printf(TEXT("StaticSubobjectsTestClientCheckIntValueIncreasedTo%d"), IntPropertyVal), FWorkerDefinition::Client(1), + nullptr, nullptr, + [this, IntPropertyVal](const float DeltaTime) { + RequireEqual_Int(TestActor->TestIntProperty, IntPropertyVal, + TEXT("Client should TestIntProperty see the updated int property")); + FinishStep(); + }, + StepTimeLimit); +} + +AStaticSubobjectTestActor* AStaticSubobjectsTest::GetReplicatedTestActor() +{ + TArray FoundActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AStaticSubobjectTestActor::StaticClass(), FoundActors); + if (AssertEqual_Int(FoundActors.Num(), 1, TEXT("There should only be one actor of type AStaticSubobjectTestActor in the world"))) + { + TestActor = Cast(FoundActors[0]); + if (AssertIsValid(TestActor, TEXT("TestActor must be valid"))) + { + return TestActor; + } + } + return nullptr; +} + +int AStaticSubobjectsTest::GetNumComponentsOnTestActor() +{ + TestActor = GetReplicatedTestActor(); + TArray AllActorComp; + TestActor->GetComponents(AllActorComp); + + return AllActorComp.Num(); +} + +void AStaticSubobjectsTest::WaitForRelevancyUpdateIfInNative() +{ + const bool bIsSpatial = Cast(GetNetDriver()) != nullptr; + + if (!bIsSpatial) + { + AddStep( + TEXT("StaticSubobjectsTestNativeWaitABit1"), FWorkerDefinition::Server(1), nullptr, + [this]() { + StepTimer = 0.f; + }, + [this](float DeltaTime) { + StepTimer += DeltaTime; + if (StepTimer > 7.5f) + { + FinishStep(); + } + }); + } +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/StaticSubobjectsTest/StaticSubobjectsTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/StaticSubobjectsTest/StaticSubobjectsTest.h new file mode 100644 index 0000000000..1cc23cdef4 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/StaticSubobjectsTest/StaticSubobjectsTest.h @@ -0,0 +1,48 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialFunctionalTest.h" +#include "StaticSubobjectTestActor.h" + +#include "StaticSubobjectsTest.generated.h" + +class ATestMovementCharacter; +class ADynamicSubObjectTestActor; + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API AStaticSubobjectsTest : public ASpatialFunctionalTest +{ + GENERATED_BODY() + +public: + AStaticSubobjectsTest(); + + virtual void PrepareTest() override; + void MoveClientPawn(FVector& ToLocation); + void CheckClientCanNotSeeIntPropertyWithWait(int ShouldntSeeVal); + void ServerSetIntProperty(int IntPropertyNewVal); + void CheckClientSeeIntProperty(int IntPropertyVal); + + void WaitForRelevancyUpdateIfInNative(); + int GetNumComponentsOnTestActor(); + AStaticSubobjectTestActor* GetReplicatedTestActor(); + + // A reference to the Default Pawn of Client 1 to allow for repossession in the final step of the test. + APawn* ClientDefaultPawn; + + ATestMovementCharacter* ClientSpawnedPawn; + + AStaticSubobjectTestActor* TestActor; + + FVector PawnSpawnLocation = FVector(0.0f, 120.0f, 40.0f); + + FVector PawnRemoteLocation = FVector(-20000.0f, -20000.0f, 40.0f); + + static constexpr int32 InitialNumComponents = 3; + + static constexpr float TimeLimit = 100.0f; + + float StepTimer; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/AlwaysInterestedTestActors.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/AlwaysInterestedTestActors.cpp new file mode 100644 index 0000000000..5d55d53cdb --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/AlwaysInterestedTestActors.cpp @@ -0,0 +1,22 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "AlwaysInterestedTestActors.h" + +#include "Net/UnrealNetwork.h" + +AAlwaysInterestedTestActor::AAlwaysInterestedTestActor() +{ + NetCullDistanceSquared = 1; +} + +void AAlwaysInterestedTestActor::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ThisClass, InterestedActors); +} + +ASmallNCDActor::ASmallNCDActor() +{ + NetCullDistanceSquared = 1; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/AlwaysInterestedTestActors.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/AlwaysInterestedTestActors.h new file mode 100644 index 0000000000..d32afaef41 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/AlwaysInterestedTestActors.h @@ -0,0 +1,36 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "ReplicatedTestActorBase_RepGraphAlwaysReplicate.h" +#include "AlwaysInterestedTestActors.generated.h" + +/** + * A replicated actor with AlwaysInterested properties + */ +UCLASS() +class AAlwaysInterestedTestActor : public AReplicatedTestActorBase_RepGraphAlwaysReplicate +{ + GENERATED_BODY() + +public: + AAlwaysInterestedTestActor(); + + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + UPROPERTY(Replicated, AlwaysInterested) + TArray InterestedActors; +}; + +/** + * A replicated actor with a small NCD + */ +UCLASS() +class ASmallNCDActor : public AReplicatedTestActorBase_RepGraphAlwaysReplicate +{ + GENERATED_BODY() + +public: + ASmallNCDActor(); +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase_RepGraphAlwaysReplicate.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase_RepGraphAlwaysReplicate.h new file mode 100644 index 0000000000..c0aa717d05 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase_RepGraphAlwaysReplicate.h @@ -0,0 +1,17 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "ReplicatedTestActorBase.h" +#include "ReplicatedTestActorBase_RepGraphAlwaysReplicate.generated.h" + +/** + * A replicated Actor which is always replicated by RepGraph regardless of client view. + */ +UCLASS() +class AReplicatedTestActorBase_RepGraphAlwaysReplicate : public AReplicatedTestActorBase +{ + GENERATED_BODY() + +public: +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/TestPawnBase_RepGraphAlwaysReplicate.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/TestPawnBase_RepGraphAlwaysReplicate.h new file mode 100644 index 0000000000..0f1bafe64b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/TestPawnBase_RepGraphAlwaysReplicate.h @@ -0,0 +1,15 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "GameFramework/DefaultPawn.h" +#include "TestPawnBase_RepGraphAlwaysReplicate.generated.h" + +/** + * A Pawn which is always replicated by RepGraph regardless of client view. + */ +UCLASS() +class ATestPawnBase_RepGraphAlwaysReplicate : public ADefaultPawn +{ + GENERATED_BODY() +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestClientNetOwnership/SpatialTestNetOwnership.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestClientNetOwnership/SpatialTestNetOwnership.cpp index 4688715b38..f21e439d11 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestClientNetOwnership/SpatialTestNetOwnership.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestClientNetOwnership/SpatialTestNetOwnership.cpp @@ -72,7 +72,6 @@ void ASpatialTestNetOwnership::PrepareTest() // Step definition for Client 1 to send a Server RPC FSpatialFunctionalTestStepDefinition ClientSendRPCStepDefinition(/*bIsNativeDefinition*/ true); ClientSendRPCStepDefinition.StepName = TEXT("SpatialTestNetOwnershipClientSendRPC"); - ClientSendRPCStepDefinition.TimeLimit = 5.0f; ClientSendRPCStepDefinition.NativeStartEvent.BindLambda([this]() { NetOwnershipCube->ServerIncreaseRPCCount(); diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestHandover/SpatialTestHandover.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestHandover/SpatialTestHandover.cpp index 1170cef76b..6ddd80a322 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestHandover/SpatialTestHandover.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestHandover/SpatialTestHandover.cpp @@ -71,7 +71,7 @@ void ASpatialTestHandover::PrepareTest() AssertTrue(IsValid(NetDriver), TEXT("This test should be run with Spatial Networking")); - LoadBalancingStrategy = Cast(NetDriver->LoadBalanceStrategy); + LoadBalancingStrategy = GetLoadBalancingStrategy(); if (IsValid(HandoverCube) && IsValid(LoadBalancingStrategy)) { diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestMultipleOwnership/SpatialTestMultipleOwnership.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestMultipleOwnership/SpatialTestMultipleOwnership.cpp index c5e06ace09..a2bcf8fe84 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestMultipleOwnership/SpatialTestMultipleOwnership.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestMultipleOwnership/SpatialTestMultipleOwnership.cpp @@ -58,6 +58,19 @@ void ASpatialTestMultipleOwnership::PrepareTest() RegisterAutoDestroyActor(MultipleOwnershipPawn1); RegisterAutoDestroyActor(MultipleOwnershipPawn2); + for (ASpatialFunctionalTestFlowController* FlowController : GetFlowControllers()) + { + if (FlowController->WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Client) + { + AController* Controller = Cast(FlowController->GetOwner()); + if (AssertIsValid(Controller, TEXT("Each flow controller should have an associated PlayerController as an owner."))) + { + AssertIsValid(Controller->GetPawn(), TEXT("Each flow controller should have an associated pawn.")); + OriginalPossessedPawns.Emplace(Controller, Controller->GetPawn()); + } + } + } + FinishStep(); }); @@ -212,4 +225,13 @@ void ASpatialTestMultipleOwnership::PrepareTest() FinishStep(); }, 10.0f); + + // The server makes the player controllers possess the original pawns they had before the start of the test. + AddStep(TEXT("SpatialTestMultipleOwnershipCleanup"), FWorkerDefinition::Server(1), nullptr, [this]() { + for (const auto& Pair : OriginalPossessedPawns) + { + Pair.Key->Possess(Pair.Value); + } + FinishStep(); + }); } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestMultipleOwnership/SpatialTestMultipleOwnership.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestMultipleOwnership/SpatialTestMultipleOwnership.h index f9aaed9f5c..324ad4acfd 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestMultipleOwnership/SpatialTestMultipleOwnership.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestMultipleOwnership/SpatialTestMultipleOwnership.h @@ -20,4 +20,9 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialTestMultipleOwnership : public ASpat // Helper array used to avoid code duplication by storing the references to the MultipleOwnershipPawns on the test itself, instead of // calling GetAllActorsOfClass multiple times. TArray MultipleOwnershipPawns; + + // Helper map to store what the original pawns were before we started possessing different ones, so we can restore them at the end of + // the test. + UPROPERTY() + TMap OriginalPossessedPawns; }; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/VisibilityTest/VisibilityTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/VisibilityTest/VisibilityTest.cpp index 561de12275..6670a1225d 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/VisibilityTest/VisibilityTest.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/VisibilityTest/VisibilityTest.cpp @@ -75,7 +75,7 @@ void AVisibilityTest::PrepareTest() AssertTrue(HasAuthority(), TEXT("We should have authority over the test actor.")); - if (IsValid(PlayerController)) + if (AssertIsValid(PlayerController, TEXT("PlayerController for client 1 should be valid."))) { ClientOneSpawnedPawn = GetWorld()->SpawnActor(CharacterSpawnLocation, FRotator::ZeroRotator, FActorSpawnParameters()); @@ -97,14 +97,12 @@ void AVisibilityTest::PrepareTest() TArray FoundActors; UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedVisibilityTestActor::StaticClass(), FoundActors); - if (FoundActors.Num() == 1) + if (RequireEqual_Int(FoundActors.Num(), 1, TEXT("We should have exactly 1 ReplicatedVisibilityTestActor in the world."))) { TestActor = Cast(FoundActors[0]); - if (IsValid(TestActor)) - { - FinishStep(); - } + RequireTrue(IsValid(TestActor), TEXT("TestActor should be valid.")); + FinishStep(); } }, StepTimeLimit); @@ -280,7 +278,7 @@ void AVisibilityTest::PrepareTest() { // Step 12 - Server Set AReplicatedVisibilityTestActor to not be hidden anymore. AddStep(TEXT("VisibilityTestServerSetActorNotHidden"), FWorkerDefinition::Server(1), nullptr, [this]() { - if (IsValid(TestActor)) + if (AssertIsValid(TestActor, TEXT("TestActor should be valid."))) { TestActor->SetHidden(false); FinishStep(); diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDKFunctionalTests.Build.cs b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDKFunctionalTests.Build.cs index ac69701151..9ebb01e39c 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDKFunctionalTests.Build.cs +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDKFunctionalTests.Build.cs @@ -23,7 +23,9 @@ public SpatialGDKFunctionalTests(ReadOnlyTargetRules Target) : base(Target) "FunctionalTesting", "HTTP", "UnrealEd", - "EngineSettings" + "EngineSettings", + "InputCore", + "Slate" }); } } diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp index a55ac7ab6f..0e164c60e7 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp +++ b/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp @@ -2,13 +2,9 @@ #include "LocalDeploymentManager.h" -#include "AssetRegistryModule.h" #include "Async/Async.h" #include "DirectoryWatcherModule.h" -#include "Editor.h" #include "Engine/World.h" -#include "FileCache.h" -#include "GeneralProjectSettings.h" #include "HAL/FileManagerGeneric.h" #include "HttpModule.h" #include "IAutomationControllerModule.h" @@ -16,8 +12,6 @@ #include "Improbable/SpatialGDKSettingsBridge.h" #include "Interfaces/IHttpResponse.h" #include "Internationalization/Internationalization.h" -#include "Internationalization/Regex.h" -#include "Json/Public/Dom/JsonObject.h" #include "Misc/FileHelper.h" #include "Misc/MessageDialog.h" #include "Misc/MonitoredProcess.h" @@ -27,7 +21,6 @@ #include "SpatialCommandUtils.h" #include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesModule.h" -#include "UObject/CoreNet.h" DEFINE_LOG_CATEGORY(LogSpatialDeploymentManager); @@ -37,9 +30,17 @@ FLocalDeploymentManager::FLocalDeploymentManager() : bLocalDeploymentRunning(false) , bStartingDeployment(false) , bStoppingDeployment(false) + , bTestRunnning(false) { } +FLocalDeploymentManager::~FLocalDeploymentManager() +{ + // The idea behind this lock is to try and make the thread calling the destructor wait for the thread that is cleaning up processes + // if something tries to run on this object after the destructor then we're ruined anyway + FScopeLock StoppingDeploymentScoped(&StoppingDeployment); +} + void FLocalDeploymentManager::PreInit(bool bChinaEnabled) { // Ensure the worker.jsons are up to date. @@ -268,7 +269,11 @@ FLocalDeploymentManager::ERuntimeStartResponse FLocalDeploymentManager::StartLoc // Give the snapshot path a timestamp to ensure we don't overwrite snapshots from older deployments. // The snapshot service saves snapshots with the name `snapshot-n.snapshot` for a given deployment, // where 'n' is the number of snapshots taken since starting the deployment. - FString SnapshotPath = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSSnapshotFolderPath, *RuntimeStartTime.ToString()); + CurrentSnapshotPath = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSSnapshotFolderPath, *RuntimeStartTime.ToString()); + + // Create the folder for storing the snapshots. + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + PlatformFile.CreateDirectoryTree(*CurrentSnapshotPath); // Use the runtime start timestamp as the log directory, e.g. `/spatial/localdeployment//` FString LocalDeploymentLogsDir = FPaths::Combine(SpatialGDKServicesConstants::LocalDeploymentLogsDir, RuntimeStartTime.ToString()); @@ -276,7 +281,6 @@ FLocalDeploymentManager::ERuntimeStartResponse FLocalDeploymentManager::StartLoc // Store these logs alongside the GDK ones for convenience const TCHAR* RuntimeEventLogPaths = TEXT("EventTracing/runtime"); FString EventTracingPath = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectSavedDir(), RuntimeEventLogPaths)); - IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); if (!PlatformFile.CreateDirectoryTree(*EventTracingPath)) { UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Failed to create runtime event log path.")); @@ -290,7 +294,7 @@ FLocalDeploymentManager::ERuntimeStartResponse FLocalDeploymentManager::StartLoc TEXT("--config=\"%s\" --snapshot=\"%s\" --worker-port %s --http-port=%s --grpc-port=%s " "--snapshots-directory=\"%s\" --schema-bundle=\"%s\" --event-tracing-logs-directory=\"%s\" %s"), *LaunchConfig, *SnapshotName, *FString::FromInt(WorkerPort), *FString::FromInt(HTTPPort), - *FString::FromInt(SpatialGDKServicesConstants::RuntimeGRPCPort), *SnapshotPath, *SchemaBundle, *EventTracingPath, *LaunchArgs); + *FString::FromInt(SpatialGDKServicesConstants::RuntimeGRPCPort), *CurrentSnapshotPath, *SchemaBundle, *EventTracingPath, *LaunchArgs); if (!RuntimeIPToExpose.IsEmpty()) { @@ -300,7 +304,7 @@ FLocalDeploymentManager::ERuntimeStartResponse FLocalDeploymentManager::StartLoc // Setup the runtime file logger. SetupRuntimeFileLogger(LocalDeploymentLogsDir); - FString RuntimePath = SpatialGDKServicesConstants::GetRuntimeExecutablePath(RuntimeVersion); + RuntimePath = SpatialGDKServicesConstants::GetRuntimeExecutablePath(RuntimeVersion); RuntimeProcess = { *RuntimePath, *RuntimeArgs, SpatialGDKServicesConstants::SpatialOSDirectory, /*InHidden*/ true, /*InCreatePipes*/ true }; @@ -360,6 +364,11 @@ FLocalDeploymentManager::ERuntimeStartResponse FLocalDeploymentManager::StartLoc EAutomationControllerModuleState::Type TestState = AutomationController->GetTestState(); bTestRunnning = TestState == EAutomationControllerModuleState::Type::Running; + if (CallBack) + { + CallBack(/*bSuccess*/ true); + } + return ERuntimeStartResponse::Success; } @@ -391,17 +400,16 @@ bool FLocalDeploymentManager::SetupRuntimeFileLogger(const FString& RuntimeLogDi bool FLocalDeploymentManager::TryStopLocalDeployment() { + FScopeLock StoppingDeploymentScoped(&StoppingDeployment); if (!StartLocalDeploymentShutDown()) { return false; } - RuntimeProcess->Stop(); - - bool bRuntimeShutDownSuccesfully = WaitForRuntimeProcessToShutDown(); + const bool bRuntimeShutDownSuccessfully = ForceShutdownAndWaitForTermination(); FinishLocalDeploymentShutDown(); - return bRuntimeShutDownSuccesfully; + return bRuntimeShutDownSuccessfully; } bool FLocalDeploymentManager::TryStopLocalDeploymentGracefully() @@ -412,41 +420,20 @@ bool FLocalDeploymentManager::TryStopLocalDeploymentGracefully() return TryStopLocalDeployment(); } + FScopeLock StoppingDeploymentScoped(&StoppingDeployment); if (!StartLocalDeploymentShutDown()) { return false; } - FHttpModule& HttpModule = FModuleManager::LoadModuleChecked("HTTP"); -#if ENGINE_MINOR_VERSION >= 26 - TSharedRef HttpRequest = HttpModule.Get().CreateRequest(); -#else - TSharedRef HttpRequest = HttpModule.Get().CreateRequest(); -#endif - - FString URL = FString::Printf(TEXT("http://localhost:%d/shutdown"), HTTPPort); - HttpRequest->SetURL(URL); - HttpRequest->SetVerb("GET"); - HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { - int32 ResponseCode = HttpResponse->GetResponseCode(); - if (ResponseCode != 200) - { - UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Runtime shutdown http request failed with code: %d"), ResponseCode); - } - }); - - HttpRequest->ProcessRequest(); - - bool bRuntimeShutDownSuccesfully = WaitForRuntimeProcessToShutDown(); - if (!bRuntimeShutDownSuccesfully) + bool bShutdownSuccess = GracefulShutdownAndWaitForTermination(); + if (!bShutdownSuccess) { - RuntimeProcess->Stop(); - bRuntimeShutDownSuccesfully = WaitForRuntimeProcessToShutDown(); + bShutdownSuccess = ForceShutdownAndWaitForTermination(); } FinishLocalDeploymentShutDown(); - - return bRuntimeShutDownSuccesfully; + return bShutdownSuccess; } bool FLocalDeploymentManager::StartLocalDeploymentShutDown() @@ -467,6 +454,27 @@ bool FLocalDeploymentManager::StartLocalDeploymentShutDown() return true; } +bool FLocalDeploymentManager::GracefulShutdownAndWaitForTermination() +{ + if (RuntimeProcess.IsSet()) + { + const FString RuntimeProcName = RuntimePath.Replace(TEXT("/"), TEXT("\\")); + SpatialCommandUtils::TryGracefullyKill(RuntimeProcName, RuntimeProcess->GetProcessHandle()); + + const bool bGracefulShutdownSuccess = WaitForRuntimeProcessToShutDown(); + return bGracefulShutdownSuccess; + } + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Trying to stop deployment gracefully but RuntimeProcess is not set.")); + return false; +} + +bool FLocalDeploymentManager::ForceShutdownAndWaitForTermination() +{ + RuntimeProcess->Stop(); + const bool bForcefulShutdownSuccess = WaitForRuntimeProcessToShutDown(); + return bForcefulShutdownSuccess; +} + bool FLocalDeploymentManager::WaitForRuntimeProcessToShutDown() { double RuntimeStopTime = RuntimeProcess->GetDuration().GetTotalSeconds(); @@ -478,7 +486,6 @@ bool FLocalDeploymentManager::WaitForRuntimeProcessToShutDown() if (RuntimeProcess->GetDuration().GetTotalSeconds() > RuntimeStopTime + RuntimeTimeout) { UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Timed out waiting for the Runtime to stop.")); - bStoppingDeployment = false; return false; } } @@ -491,6 +498,21 @@ void FLocalDeploymentManager::FinishLocalDeploymentShutDown() // Kill the log file handle. RuntimeLogFileHandle.Reset(); + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + const auto IsDirectoryEmpty = [&PlatformFile](const TCHAR* Directory) -> bool { + bool bDirectoryIsEmpty = true; + PlatformFile.IterateDirectory(Directory, [&bDirectoryIsEmpty](const TCHAR* FilenameOrDirectory, bool bIsDirectory) -> bool { + bDirectoryIsEmpty = false; + return false; + }); + return bDirectoryIsEmpty; + }; + + if (IsDirectoryEmpty(*CurrentSnapshotPath)) + { + PlatformFile.DeleteDirectory(*CurrentSnapshotPath); + } + bLocalDeploymentRunning = false; bStoppingDeployment = false; } @@ -546,11 +568,6 @@ void SPATIALGDKSERVICES_API FLocalDeploymentManager::TakeSnapshot(UWorld* World, TSharedRef HttpRequest = HttpModule.Get().CreateRequest(); #endif - FString SnapshotPath = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSSnapshotFolderPath, *RuntimeStartTime.ToString()); - // Create the folder for storing the snapshots. - IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); - PlatformFile.CreateDirectoryTree(*SnapshotPath); - HttpRequest->OnProcessRequestComplete().BindLambda( [World, OnSnapshotTaken](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { if (!bSucceeded) diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/SpatialCommandUtils.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/SpatialCommandUtils.cpp index 6407b01434..452f95658c 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Private/SpatialCommandUtils.cpp +++ b/SpatialGDK/Source/SpatialGDKServices/Private/SpatialCommandUtils.cpp @@ -8,6 +8,9 @@ #include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesModule.h" +#if PLATFORM_WINDOWS +#include "Windows/WindowsHWrapper.h" +#endif DEFINE_LOG_CATEGORY(LogSpatialCommandUtils); #define LOCTEXT_NAMESPACE "SpatialCommandUtils" @@ -58,62 +61,6 @@ bool SpatialCommandUtils::AttemptSpatialAuth(bool bIsRunningInChina) return bSuccess; } -bool SpatialCommandUtils::StartSpatialService(const FString& Version, const FString& RuntimeIP, bool bIsRunningInChina, - const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode) -{ - FString Command = TEXT("service start"); - - if (bIsRunningInChina) - { - Command += SpatialGDKServicesConstants::ChinaEnvironmentArgument; - } - - if (!Version.IsEmpty()) - { - Command.Append(FString::Printf(TEXT(" --version=%s"), *Version)); - } - - if (!RuntimeIP.IsEmpty()) - { - Command.Append(FString::Printf(TEXT(" --runtime_ip=%s"), *RuntimeIP)); - UE_LOG(LogSpatialCommandUtils, Verbose, TEXT("Trying to start spatial service with exposed runtime ip: %s"), *RuntimeIP); - } - - FSpatialGDKServicesModule::ExecuteAndReadOutput(*SpatialGDKServicesConstants::SpatialExe, Command, DirectoryToRun, OutResult, - OutExitCode); - - bool bSuccess = OutExitCode == 0; - if (!bSuccess) - { - UE_LOG(LogSpatialCommandUtils, Warning, TEXT("Spatial start service failed. Error Code: %d, Error Message: %s"), OutExitCode, - *OutResult); - } - - return bSuccess; -} - -bool SpatialCommandUtils::StopSpatialService(bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode) -{ - FString Command = TEXT("service stop"); - - if (bIsRunningInChina) - { - Command += SpatialGDKServicesConstants::ChinaEnvironmentArgument; - } - - FSpatialGDKServicesModule::ExecuteAndReadOutput(*SpatialGDKServicesConstants::SpatialExe, Command, DirectoryToRun, OutResult, - OutExitCode); - - bool bSuccess = OutExitCode == 0; - if (!bSuccess) - { - UE_LOG(LogSpatialCommandUtils, Warning, TEXT("Spatial stop service failed. Error Code: %d, Error Message: %s"), OutExitCode, - *OutResult); - } - - return bSuccess; -} - bool SpatialCommandUtils::BuildWorkerConfig(bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode) { FString Command = TEXT("worker build build-config"); @@ -136,22 +83,6 @@ bool SpatialCommandUtils::BuildWorkerConfig(bool bIsRunningInChina, const FStrin return bSuccess; } -FProcHandle SpatialCommandUtils::LocalWorkerReplace(const FString& ServicePort, const FString& OldWorker, const FString& NewWorker, - bool bIsRunningInChina, uint32* OutProcessID) -{ - check(!ServicePort.IsEmpty()); - check(!OldWorker.IsEmpty()); - check(!NewWorker.IsEmpty()); - - FString Command = TEXT("worker build build-config"); - Command.Append(FString::Printf(TEXT(" --local_service_grpc_port %s"), *ServicePort)); - Command.Append(FString::Printf(TEXT(" --existing_worker_id %s"), *OldWorker)); - Command.Append(FString::Printf(TEXT(" --replacing_worker_id %s"), *NewWorker)); - - return FPlatformProcess::CreateProc(*SpatialGDKServicesConstants::SpatialExe, *Command, false, true, true, OutProcessID, - 2 /*PriorityModifier*/, nullptr, nullptr, nullptr); -} - bool SpatialCommandUtils::GenerateDevAuthToken(bool bIsRunningInChina, FString& OutTokenSecret, FText& OutErrorMessage) { FString Arguments = TEXT("project auth dev-auth-token create --description=\"Unreal GDK Token\" --json_output"); @@ -449,6 +380,39 @@ void SpatialCommandUtils::TryKillProcessWithName(const FString& ProcessName) } } +void SpatialCommandUtils::TryGracefullyKill(const FString& ProcName, const FProcHandle& ProcHandle) +{ +#if PLATFORM_LINUX || PLATFORM_MAC + // On Linux this sends a SIGTERM signal + // TODO: does this work on Mac? + FPlatformProcess::TerminateProc(ProcHandle); +#elif PLATFORM_WINDOWS + // TerminateProc is too forceful on Windows + TryGracefullyKillWindows(ProcName); +#endif +} + +#if PLATFORM_WINDOWS +void SpatialCommandUtils::TryGracefullyKillWindows(const FString& ProcName) +{ + // Use WM_CLOSE signal on windows as TerminateProc forcefully kills on Windows + + // Find runtime window + const HWND RuntimeWindowHandle = FindWindowA(nullptr, TCHAR_TO_ANSI(*ProcName)); + if (RuntimeWindowHandle != nullptr) + { + // Using SendMessageW to send message despite being 'supposed to' use SendMessage - as including "Windows/MinWindows.h" needed for + // SendMessage overrides the TEXT macro (SendMessageW is also used elsewhere in Unreal) + SendMessageW(RuntimeWindowHandle, WM_CLOSE, NULL, NULL); + } + else + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Tried to gracefully stop process '%s' but could not find runtime window."), + *ProcName); + } +} +#endif + bool SpatialCommandUtils::GetProcessInfoFromPort(int32 Port, FString& OutPid, FString& OutState, FString& OutProcessName) { #if PLATFORM_WINDOWS diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/LocalDeploymentManager.h b/SpatialGDK/Source/SpatialGDKServices/Public/LocalDeploymentManager.h index bd0667b2dc..92b25796d7 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Public/LocalDeploymentManager.h +++ b/SpatialGDK/Source/SpatialGDKServices/Public/LocalDeploymentManager.h @@ -15,10 +15,11 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialDeploymentManager, Log, All); class FJsonObject; -class FLocalDeploymentManager +class FLocalDeploymentManager final { public: FLocalDeploymentManager(); + ~FLocalDeploymentManager(); void SPATIALGDKSERVICES_API PreInit(bool bChinaEnabled); @@ -67,6 +68,8 @@ class FLocalDeploymentManager bool WaitForRuntimeProcessToShutDown(); bool StartLocalDeploymentShutDown(); + bool GracefulShutdownAndWaitForTermination(); + bool ForceShutdownAndWaitForTermination(); void FinishLocalDeploymentShutDown(); enum class ERuntimeStartResponse @@ -81,6 +84,8 @@ class FLocalDeploymentManager const FString& SnapshotName, const FString& RuntimeIPToExpose, const LocalDeploymentCallback& CallBack); + FCriticalSection StoppingDeployment; + TFuture AttemptSpatialAuthResult; TOptional RuntimeProcess = {}; @@ -102,8 +107,12 @@ class FLocalDeploymentManager bool bStoppingDeployment; bool bTestRunnning; + FString RuntimePath; + FString ExposedRuntimeIP; + FString CurrentSnapshotPath; + bool bRedeployRequired = false; bool bAutoDeploy = false; bool bIsInChina = false; diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialCommandUtils.h b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialCommandUtils.h index 9593c2b020..b726e2dcbb 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialCommandUtils.h +++ b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialCommandUtils.h @@ -13,14 +13,8 @@ class SpatialCommandUtils SPATIALGDKSERVICES_API static bool SpatialVersion(bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode); SPATIALGDKSERVICES_API static bool AttemptSpatialAuth(bool bIsRunningInChina); - SPATIALGDKSERVICES_API static bool StartSpatialService(const FString& Version, const FString& RuntimeIP, bool bIsRunningInChina, - const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode); - SPATIALGDKSERVICES_API static bool StopSpatialService(bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, - int32& OutExitCode); SPATIALGDKSERVICES_API static bool BuildWorkerConfig(bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode); - SPATIALGDKSERVICES_API static FProcHandle LocalWorkerReplace(const FString& ServicePort, const FString& OldWorker, - const FString& NewWorker, bool bIsRunningInChina, uint32* OutProcessID); SPATIALGDKSERVICES_API static bool GenerateDevAuthToken(bool bIsRunningInChina, FString& OutTokenSecret, FText& OutErrorMessage); SPATIALGDKSERVICES_API static bool HasDevLoginTag(const FString& DeploymentName, bool bIsRunningInChina, FText& OutErrorMessage); SPATIALGDKSERVICES_API static FProcHandle StartLocalReceptionistProxyServer(bool bIsRunningInChina, const FString& CloudDeploymentName, @@ -32,6 +26,7 @@ class SpatialCommandUtils SPATIALGDKSERVICES_API static bool GetProcessName(const FString& PID, FString& OutProcessName); SPATIALGDKSERVICES_API static bool TryKillProcessWithPID(const FString& PID); SPATIALGDKSERVICES_API static void TryKillProcessWithName(const FString& ProcessName); + SPATIALGDKSERVICES_API static void TryGracefullyKill(const FString& ProcName, const FProcHandle& ProcHandle); SPATIALGDKSERVICES_API static bool FetchRuntimeBinary(const FString& RuntimeVersion, const bool bIsRunningInChina); SPATIALGDKSERVICES_API static bool FetchInspectorBinary(const FString& InspectorVersion, const bool bIsRunningInChina); SPATIALGDKSERVICES_API static bool FetchPackageBinary(const FString& PackageVersion, const FString& PackageExe, @@ -43,6 +38,10 @@ class SpatialCommandUtils const int32 NumRetries = 3); private: +#if PLATFORM_WINDOWS + static void TryGracefullyKillWindows(const FString& ProcName); +#endif + // Timeout given in seconds. static constexpr double ProcessTimeoutTime = 120.0; }; diff --git a/SpatialGDK/Source/SpatialGDKTests/Public/GDKAutomationTestBase.h b/SpatialGDK/Source/SpatialGDKTests/Public/GDKAutomationTestBase.h index bc2ada49df..a31e97cfa3 100644 --- a/SpatialGDK/Source/SpatialGDKTests/Public/GDKAutomationTestBase.h +++ b/SpatialGDK/Source/SpatialGDKTests/Public/GDKAutomationTestBase.h @@ -52,8 +52,8 @@ class FGDKAutomationTestBase : public FAutomationTestBase FLocalDeploymentManager* LocalDeploymentManager = SpatialGDK::GetLocalDeploymentManager(); if (LocalDeploymentManager->IsLocalDeploymentRunning() || LocalDeploymentManager->IsDeploymentStarting()) { - UE_LOG(LogGDKTestBase, Warning, TEXT("Deployment found! (Was this left over from another test?)")) - UE_LOG(LogGDKTestBase, Warning, TEXT("Ending PIE session")) + UE_LOG(LogGDKTestBase, Log, TEXT("Deployment found! (Was this left over from another test?)")) + UE_LOG(LogGDKTestBase, Log, TEXT("Ending PIE session")) GEditor->RequestEndPlayMap(); ADD_LATENT_AUTOMATION_COMMAND(FStopDeployment); ADD_LATENT_AUTOMATION_COMMAND(FWaitForDeployment(this, EDeploymentState::IsNotRunning)); diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/OwnershipLockingPolicy/OwnershipLockingPolicyTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/OwnershipLockingPolicy/OwnershipLockingPolicyTest.cpp index 5ef284d66f..ac13373dfc 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/OwnershipLockingPolicy/OwnershipLockingPolicyTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/OwnershipLockingPolicy/OwnershipLockingPolicyTest.cpp @@ -416,6 +416,7 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_WHEN_the_locked_Actor_is ADD_LATENT_AUTOMATION_COMMAND(FSetActorRole(Data, "Actor", ROLE_SimulatedProxy)); ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "Actor", "First lock", false)); ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + ADD_LATENT_AUTOMATION_COMMAND(FSetActorRole(Data, "Actor", ROLE_Authority)); TearDown(this, Data); return true; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/NonSpatialTypeActor.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/NonSpatialTypeActor.schema index 7e72938313..890be9ffea 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/NonSpatialTypeActor.schema +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/NonSpatialTypeActor.schema @@ -10,15 +10,20 @@ component NonSpatialTypeActor { bool breplicatemovement = 2; bool btearoff = 3; bool bcanbedamaged = 4; - uint32 remoterole = 5; - bytes replicatedmovement = 6; - UnrealObjectRef attachmentreplication_attachparent = 7; - bytes attachmentreplication_locationoffset = 8; - bytes attachmentreplication_relativescale3d = 9; - bytes attachmentreplication_rotationoffset = 10; - string attachmentreplication_attachsocket = 11; - UnrealObjectRef attachmentreplication_attachcomponent = 12; - UnrealObjectRef owner = 13; - uint32 role = 14; - UnrealObjectRef instigator = 15; + uint32 remoterole = 6; + bytes replicatedmovement = 7; + UnrealObjectRef attachmentreplication_attachparent = 8; + bytes attachmentreplication_locationoffset = 9; + bytes attachmentreplication_relativescale3d = 10; + bytes attachmentreplication_rotationoffset = 11; + string attachmentreplication_attachsocket = 12; + UnrealObjectRef attachmentreplication_attachcomponent = 13; + UnrealObjectRef owner = 14; + uint32 role = 15; + UnrealObjectRef instigator = 16; +} + +component NonSpatialTypeActorServerOnly { + id = {{id}}; + bool bactorenablecollision = 5; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActor.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActor.schema index 7a2abbd41f..9ecc5d2c48 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActor.schema +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActor.schema @@ -10,15 +10,20 @@ component SpatialTypeActor { bool breplicatemovement = 2; bool btearoff = 3; bool bcanbedamaged = 4; - uint32 remoterole = 5; - bytes replicatedmovement = 6; - UnrealObjectRef attachmentreplication_attachparent = 7; - bytes attachmentreplication_locationoffset = 8; - bytes attachmentreplication_relativescale3d = 9; - bytes attachmentreplication_rotationoffset = 10; - string attachmentreplication_attachsocket = 11; - UnrealObjectRef attachmentreplication_attachcomponent = 12; - UnrealObjectRef owner = 13; - uint32 role = 14; - UnrealObjectRef instigator = 15; + uint32 remoterole = 6; + bytes replicatedmovement = 7; + UnrealObjectRef attachmentreplication_attachparent = 8; + bytes attachmentreplication_locationoffset = 9; + bytes attachmentreplication_relativescale3d = 10; + bytes attachmentreplication_rotationoffset = 11; + string attachmentreplication_attachsocket = 12; + UnrealObjectRef attachmentreplication_attachcomponent = 13; + UnrealObjectRef owner = 14; + uint32 role = 15; + UnrealObjectRef instigator = 16; +} + +component SpatialTypeActorServerOnly { + id = {{id}}; + bool bactorenablecollision = 5; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithActorComponent.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithActorComponent.schema index 05762b2692..39322e34b3 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithActorComponent.schema +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithActorComponent.schema @@ -10,16 +10,21 @@ component SpatialTypeActorWithActorComponent { bool breplicatemovement = 2; bool btearoff = 3; bool bcanbedamaged = 4; - uint32 remoterole = 5; - bytes replicatedmovement = 6; - UnrealObjectRef attachmentreplication_attachparent = 7; - bytes attachmentreplication_locationoffset = 8; - bytes attachmentreplication_relativescale3d = 9; - bytes attachmentreplication_rotationoffset = 10; - string attachmentreplication_attachsocket = 11; - UnrealObjectRef attachmentreplication_attachcomponent = 12; - UnrealObjectRef owner = 13; - uint32 role = 14; - UnrealObjectRef instigator = 15; - UnrealObjectRef spatialactorcomponent = 16; + uint32 remoterole = 6; + bytes replicatedmovement = 7; + UnrealObjectRef attachmentreplication_attachparent = 8; + bytes attachmentreplication_locationoffset = 9; + bytes attachmentreplication_relativescale3d = 10; + bytes attachmentreplication_rotationoffset = 11; + string attachmentreplication_attachsocket = 12; + UnrealObjectRef attachmentreplication_attachcomponent = 13; + UnrealObjectRef owner = 14; + uint32 role = 15; + UnrealObjectRef instigator = 16; + UnrealObjectRef spatialactorcomponent = 17; +} + +component SpatialTypeActorWithActorComponentServerOnly { + id = {{id}}; + bool bactorenablecollision = 5; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleActorComponents.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleActorComponents.schema index 1c91db5e19..4a43961364 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleActorComponents.schema +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleActorComponents.schema @@ -10,17 +10,22 @@ component SpatialTypeActorWithMultipleActorComponents { bool breplicatemovement = 2; bool btearoff = 3; bool bcanbedamaged = 4; - uint32 remoterole = 5; - bytes replicatedmovement = 6; - UnrealObjectRef attachmentreplication_attachparent = 7; - bytes attachmentreplication_locationoffset = 8; - bytes attachmentreplication_relativescale3d = 9; - bytes attachmentreplication_rotationoffset = 10; - string attachmentreplication_attachsocket = 11; - UnrealObjectRef attachmentreplication_attachcomponent = 12; - UnrealObjectRef owner = 13; - uint32 role = 14; - UnrealObjectRef instigator = 15; - UnrealObjectRef firstspatialactorcomponent = 16; - UnrealObjectRef secondspatialactorcomponent = 17; + uint32 remoterole = 6; + bytes replicatedmovement = 7; + UnrealObjectRef attachmentreplication_attachparent = 8; + bytes attachmentreplication_locationoffset = 9; + bytes attachmentreplication_relativescale3d = 10; + bytes attachmentreplication_rotationoffset = 11; + string attachmentreplication_attachsocket = 12; + UnrealObjectRef attachmentreplication_attachcomponent = 13; + UnrealObjectRef owner = 14; + uint32 role = 15; + UnrealObjectRef instigator = 16; + UnrealObjectRef firstspatialactorcomponent = 17; + UnrealObjectRef secondspatialactorcomponent = 18; +} + +component SpatialTypeActorWithMultipleActorComponentsServerOnly { + id = {{id}}; + bool bactorenablecollision = 5; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleObjectComponents.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleObjectComponents.schema index 021ec5f13e..1e74fbbc71 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleObjectComponents.schema +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleObjectComponents.schema @@ -10,17 +10,22 @@ component SpatialTypeActorWithMultipleObjectComponents { bool breplicatemovement = 2; bool btearoff = 3; bool bcanbedamaged = 4; - uint32 remoterole = 5; - bytes replicatedmovement = 6; - UnrealObjectRef attachmentreplication_attachparent = 7; - bytes attachmentreplication_locationoffset = 8; - bytes attachmentreplication_relativescale3d = 9; - bytes attachmentreplication_rotationoffset = 10; - string attachmentreplication_attachsocket = 11; - UnrealObjectRef attachmentreplication_attachcomponent = 12; - UnrealObjectRef owner = 13; - uint32 role = 14; - UnrealObjectRef instigator = 15; - UnrealObjectRef firstspatialobjectcomponent = 16; - UnrealObjectRef secondspatialobjectcomponent = 17; + uint32 remoterole = 6; + bytes replicatedmovement = 7; + UnrealObjectRef attachmentreplication_attachparent = 8; + bytes attachmentreplication_locationoffset = 9; + bytes attachmentreplication_relativescale3d = 10; + bytes attachmentreplication_rotationoffset = 11; + string attachmentreplication_attachsocket = 12; + UnrealObjectRef attachmentreplication_attachcomponent = 13; + UnrealObjectRef owner = 14; + uint32 role = 15; + UnrealObjectRef instigator = 16; + UnrealObjectRef firstspatialobjectcomponent = 17; + UnrealObjectRef secondspatialobjectcomponent = 18; +} + +component SpatialTypeActorWithMultipleObjectComponentsServerOnly { + id = {{id}}; + bool bactorenablecollision = 5; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/NonSpatialTypeActor.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/NonSpatialTypeActor.schema index 4a06758acd..fae6a9b87e 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/NonSpatialTypeActor.schema +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/NonSpatialTypeActor.schema @@ -10,15 +10,20 @@ component NonSpatialTypeActor { bool bhidden = 2; bool btearoff = 3; bool bcanbedamaged = 4; - uint32 remoterole = 5; - bytes replicatedmovement = 6; - UnrealObjectRef attachmentreplication_attachparent = 7; - bytes attachmentreplication_locationoffset = 8; - bytes attachmentreplication_relativescale3d = 9; - bytes attachmentreplication_rotationoffset = 10; - string attachmentreplication_attachsocket = 11; - UnrealObjectRef attachmentreplication_attachcomponent = 12; - UnrealObjectRef owner = 13; - uint32 role = 14; - UnrealObjectRef instigator = 15; + uint32 remoterole = 6; + bytes replicatedmovement = 7; + UnrealObjectRef attachmentreplication_attachparent = 8; + bytes attachmentreplication_locationoffset = 9; + bytes attachmentreplication_relativescale3d = 10; + bytes attachmentreplication_rotationoffset = 11; + string attachmentreplication_attachsocket = 12; + UnrealObjectRef attachmentreplication_attachcomponent = 13; + UnrealObjectRef owner = 14; + uint32 role = 15; + UnrealObjectRef instigator = 16; +} + +component NonSpatialTypeActorServerOnly { + id = {{id}}; + bool bactorenablecollision = 5; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/SpatialTypeActor.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/SpatialTypeActor.schema index 6b87cdc8b1..4fc93d3f86 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/SpatialTypeActor.schema +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/SpatialTypeActor.schema @@ -10,15 +10,20 @@ component SpatialTypeActor { bool bhidden = 2; bool btearoff = 3; bool bcanbedamaged = 4; - uint32 remoterole = 5; - bytes replicatedmovement = 6; - UnrealObjectRef attachmentreplication_attachparent = 7; - bytes attachmentreplication_locationoffset = 8; - bytes attachmentreplication_relativescale3d = 9; - bytes attachmentreplication_rotationoffset = 10; - string attachmentreplication_attachsocket = 11; - UnrealObjectRef attachmentreplication_attachcomponent = 12; - UnrealObjectRef owner = 13; - uint32 role = 14; - UnrealObjectRef instigator = 15; + uint32 remoterole = 6; + bytes replicatedmovement = 7; + UnrealObjectRef attachmentreplication_attachparent = 8; + bytes attachmentreplication_locationoffset = 9; + bytes attachmentreplication_relativescale3d = 10; + bytes attachmentreplication_rotationoffset = 11; + string attachmentreplication_attachsocket = 12; + UnrealObjectRef attachmentreplication_attachcomponent = 13; + UnrealObjectRef owner = 14; + uint32 role = 15; + UnrealObjectRef instigator = 16; +} + +component SpatialTypeActorServerOnly { + id = {{id}}; + bool bactorenablecollision = 5; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/SpatialTypeActorWithActorComponent.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/SpatialTypeActorWithActorComponent.schema index 5eb3a12870..b02679ca09 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/SpatialTypeActorWithActorComponent.schema +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/SpatialTypeActorWithActorComponent.schema @@ -10,16 +10,21 @@ component SpatialTypeActorWithActorComponent { bool bhidden = 2; bool btearoff = 3; bool bcanbedamaged = 4; - uint32 remoterole = 5; - bytes replicatedmovement = 6; - UnrealObjectRef attachmentreplication_attachparent = 7; - bytes attachmentreplication_locationoffset = 8; - bytes attachmentreplication_relativescale3d = 9; - bytes attachmentreplication_rotationoffset = 10; - string attachmentreplication_attachsocket = 11; - UnrealObjectRef attachmentreplication_attachcomponent = 12; - UnrealObjectRef owner = 13; - uint32 role = 14; - UnrealObjectRef instigator = 15; - UnrealObjectRef spatialactorcomponent = 16; + uint32 remoterole = 6; + bytes replicatedmovement = 7; + UnrealObjectRef attachmentreplication_attachparent = 8; + bytes attachmentreplication_locationoffset = 9; + bytes attachmentreplication_relativescale3d = 10; + bytes attachmentreplication_rotationoffset = 11; + string attachmentreplication_attachsocket = 12; + UnrealObjectRef attachmentreplication_attachcomponent = 13; + UnrealObjectRef owner = 14; + uint32 role = 15; + UnrealObjectRef instigator = 16; + UnrealObjectRef spatialactorcomponent = 17; +} + +component SpatialTypeActorWithActorComponentServerOnly { + id = {{id}}; + bool bactorenablecollision = 5; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/SpatialTypeActorWithMultipleActorComponents.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/SpatialTypeActorWithMultipleActorComponents.schema index df04065c8b..0cfa153dd1 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/SpatialTypeActorWithMultipleActorComponents.schema +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/SpatialTypeActorWithMultipleActorComponents.schema @@ -10,17 +10,22 @@ component SpatialTypeActorWithMultipleActorComponents { bool bhidden = 2; bool btearoff = 3; bool bcanbedamaged = 4; - uint32 remoterole = 5; - bytes replicatedmovement = 6; - UnrealObjectRef attachmentreplication_attachparent = 7; - bytes attachmentreplication_locationoffset = 8; - bytes attachmentreplication_relativescale3d = 9; - bytes attachmentreplication_rotationoffset = 10; - string attachmentreplication_attachsocket = 11; - UnrealObjectRef attachmentreplication_attachcomponent = 12; - UnrealObjectRef owner = 13; - uint32 role = 14; - UnrealObjectRef instigator = 15; - UnrealObjectRef firstspatialactorcomponent = 16; - UnrealObjectRef secondspatialactorcomponent = 17; + uint32 remoterole = 6; + bytes replicatedmovement = 7; + UnrealObjectRef attachmentreplication_attachparent = 8; + bytes attachmentreplication_locationoffset = 9; + bytes attachmentreplication_relativescale3d = 10; + bytes attachmentreplication_rotationoffset = 11; + string attachmentreplication_attachsocket = 12; + UnrealObjectRef attachmentreplication_attachcomponent = 13; + UnrealObjectRef owner = 14; + uint32 role = 15; + UnrealObjectRef instigator = 16; + UnrealObjectRef firstspatialactorcomponent = 17; + UnrealObjectRef secondspatialactorcomponent = 18; +} + +component SpatialTypeActorWithMultipleActorComponentsServerOnly { + id = {{id}}; + bool bactorenablecollision = 5; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/SpatialTypeActorWithMultipleObjectComponents.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/SpatialTypeActorWithMultipleObjectComponents.schema index 056c14c57a..a492775075 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/SpatialTypeActorWithMultipleObjectComponents.schema +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425/SpatialTypeActorWithMultipleObjectComponents.schema @@ -10,17 +10,22 @@ component SpatialTypeActorWithMultipleObjectComponents { bool bhidden = 2; bool btearoff = 3; bool bcanbedamaged = 4; - uint32 remoterole = 5; - bytes replicatedmovement = 6; - UnrealObjectRef attachmentreplication_attachparent = 7; - bytes attachmentreplication_locationoffset = 8; - bytes attachmentreplication_relativescale3d = 9; - bytes attachmentreplication_rotationoffset = 10; - string attachmentreplication_attachsocket = 11; - UnrealObjectRef attachmentreplication_attachcomponent = 12; - UnrealObjectRef owner = 13; - uint32 role = 14; - UnrealObjectRef instigator = 15; - UnrealObjectRef firstspatialobjectcomponent = 16; - UnrealObjectRef secondspatialobjectcomponent = 17; + uint32 remoterole = 6; + bytes replicatedmovement = 7; + UnrealObjectRef attachmentreplication_attachparent = 8; + bytes attachmentreplication_locationoffset = 9; + bytes attachmentreplication_relativescale3d = 10; + bytes attachmentreplication_rotationoffset = 11; + string attachmentreplication_attachsocket = 12; + UnrealObjectRef attachmentreplication_attachcomponent = 13; + UnrealObjectRef owner = 14; + uint32 role = 15; + UnrealObjectRef instigator = 16; + UnrealObjectRef firstspatialobjectcomponent = 17; + UnrealObjectRef secondspatialobjectcomponent = 18; +} + +component SpatialTypeActorWithMultipleObjectComponentsServerOnly { + id = {{id}}; + bool bactorenablecollision = 5; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SchemaGenObjectStub.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SchemaGenObjectStub.cpp index a93924d4d7..6032c7d139 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SchemaGenObjectStub.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SchemaGenObjectStub.cpp @@ -20,6 +20,14 @@ void USchemaGenObjectStubCondOwnerOnly::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME_CONDITION(USchemaGenObjectStubHandOver, IntValue, COND_ServerOnly); + DOREPLIFETIME_CONDITION(USchemaGenObjectStubHandOver, BoolValue, COND_ServerOnly); +} + void USchemaGenObjectStubInitialOnly::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SchemaGenObjectStub.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SchemaGenObjectStub.h index 4d5ac53983..3626cc4e66 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SchemaGenObjectStub.h +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SchemaGenObjectStub.h @@ -34,10 +34,10 @@ class USchemaGenObjectStubHandOver : public UObject { GENERATED_BODY() public: - UPROPERTY(Handover) + UPROPERTY(Replicated) int IntValue; - UPROPERTY(Handover) + UPROPERTY(Replicated) bool BoolValue; }; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SpatialGDKEditorSchemaGeneratorTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SpatialGDKEditorSchemaGeneratorTest.cpp index 65803a5667..9cacbe88de 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SpatialGDKEditorSchemaGeneratorTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SpatialGDKEditorSchemaGeneratorTest.cpp @@ -106,18 +106,21 @@ ComponentNamesAndIds ParseAvailableNamesAndIdsFromSchemaFile(const TArrayGeneratedSchemaName.Compare(ParsedNamesAndIds.Names[0]) != 0) { return false; @@ -166,9 +164,14 @@ bool TestEqualDatabaseEntryAndSchemaFile(const UClass* CurrentClass, const FStri // } //} - for (int i = 0; i < ParsedNamesAndIds.Ids.Num(); ++i) + uint32 IdIndex = 0; + for (int i = 0; i < SCHEMA_Count; ++i) { - if (ActorData->SchemaComponents[i] != ParsedNamesAndIds.Ids[i]) + if (ActorData->SchemaComponents[i] == SpatialConstants::INVALID_COMPONENT_ID) + { + continue; + } + if (ActorData->SchemaComponents[i] != ParsedNamesAndIds.Ids[IdIndex++]) { return false; } @@ -330,7 +333,16 @@ class SchemaValidator FString ExpectedContent; FFileHelper::LoadFileToString(ExpectedContent, *ExpectedContentFullPath); - ExpectedContent.ReplaceInline(TEXT("{{id}}"), *FString::FromInt(GetNextFreeId())); + while (true) + { + FString SearchString = TEXT("{{id}}"); + int32 Index = ExpectedContent.Find(SearchString, ESearchCase::IgnoreCase, ESearchDir::FromStart, -1); + if (Index == -1) + break; + ExpectedContent.RemoveAt(Index, SearchString.Len()); + ExpectedContent.InsertAt(Index, *FString::FromInt(GetNextFreeId())); + } + return (CleanSchema(GeneratedSchemaContent).Compare(CleanSchema(ExpectedContent)) == 0); } @@ -903,6 +915,7 @@ SCHEMA_GENERATOR_TEST(GIVEN_source_and_destination_of_well_known_schema_files_WH TArray GDKSchemaFilePaths = { "authority_intent.schema", "core_types.schema", "debug_component.schema", + "gameplay_debugger_component.schema", "debug_metrics.schema", "global_state_manager.schema", "initial_only_presence.schema", @@ -920,6 +933,7 @@ SCHEMA_GENERATOR_TEST(GIVEN_source_and_destination_of_well_known_schema_files_WH "spatial_debugging.schema", "actor_group_member.schema", "actor_set_member.schema", + "actor_ownership.schema", "spawndata.schema", "spawner.schema", "tombstone.schema", diff --git a/SpatialGDK/SpatialGDK.uplugin b/SpatialGDK/SpatialGDK.uplugin index 7d25b8ed3e..de7d3228ae 100644 --- a/SpatialGDK/SpatialGDK.uplugin +++ b/SpatialGDK/SpatialGDK.uplugin @@ -1,7 +1,7 @@ { "FileVersion": 3, - "Version": 11, - "VersionName": "0.13.1", + "Version": 12, + "VersionName": "0.14.0", "FriendlyName": "SpatialOS GDK for Unreal", "Description": "The SpatialOS Game Development Kit (GDK) for Unreal Engine allows you to host your game and combine multiple dedicated server instances across one seamless game world whilst using the Unreal Engine networking API.", "Category": "SpatialOS", diff --git a/UnrealGDKEngineNetTestVersion.txt b/UnrealGDKEngineNetTestVersion.txt index ed0d9e9902..0548fb4e9b 100644 --- a/UnrealGDKEngineNetTestVersion.txt +++ b/UnrealGDKEngineNetTestVersion.txt @@ -1 +1 @@ -0.13.1 \ No newline at end of file +0.14.0 \ No newline at end of file diff --git a/UnrealGDKTestGymsVersion.txt b/UnrealGDKTestGymsVersion.txt index ed0d9e9902..0548fb4e9b 100644 --- a/UnrealGDKTestGymsVersion.txt +++ b/UnrealGDKTestGymsVersion.txt @@ -1 +1 @@ -0.13.1 \ No newline at end of file +0.14.0 \ No newline at end of file diff --git a/ci/ReleaseTool/ReleaseCommand.cs b/ci/ReleaseTool/ReleaseCommand.cs index f646b175a5..60a3f0c94a 100644 --- a/ci/ReleaseTool/ReleaseCommand.cs +++ b/ci/ReleaseTool/ReleaseCommand.cs @@ -341,7 +341,7 @@ Release notes 将同时提供中英文。要浏览中文版本,向下滚动页 * You can find the corresponding UnrealEngine version(s) [here](https://github.com/improbableio/UnrealEngine/releases). * You can find the corresponding UnrealGDKExampleProject version [here](https://github.com/spatialos/UnrealGDKExampleProject/releases/tag/{options.Version}). -Follow **[these](https://documentation.improbable.io/gdk-for-unreal/docs/keep-your-gdk-up-to-date)** steps to upgrade your GDK, Engine fork and Example Project to the latest release. +Follow **[these](https://networking.docs.improbable.io/gdk-for-unreal/v0.14.0/workflows/keep-your-gdk-up-to-date)** steps to upgrade your GDK, Engine fork and Example Project to the latest release. You can read the full release notes [here](https://github.com/spatialos/UnrealGDK/blob/release/CHANGELOG.md) or below. @@ -374,7 +374,7 @@ Join the community on our [forums](https://forums.improbable.io/), or on [Discor * This Engine version corresponds to GDK version: [{options.Version}](https://github.com/spatialos/UnrealGDK/releases/tag/{options.Version}). * You can find the corresponding UnrealGDKExampleProject version [here](https://github.com/spatialos/UnrealGDKExampleProject/releases/tag/{options.Version}). -Follow [these steps](https://documentation.improbable.io/gdk-for-unreal/docs/keep-your-gdk-up-to-date) to upgrade your GDK, Unreal Engine fork and your Project to the latest release. +Follow [these steps](https://networking.docs.improbable.io/gdk-for-unreal/v0.14.0/workflows/keep-your-gdk-up-to-date) to upgrade your GDK, Unreal Engine fork and your Project to the latest release. You can read the full release notes [here](https://github.com/spatialos/UnrealGDK/blob/release/CHANGELOG.md). @@ -392,7 +392,7 @@ Happy developing!
* You can find the corresponding UnrealGDKExampleProject version [here](https://github.com/spatialos/UnrealGDKExampleProject/releases/tag/{options.Version}). * You can find the corresponding UnrealEngine version(s) [here](https://github.com/improbableio/UnrealEngine/releases). -Follow [these steps](https://documentation.improbable.io/gdk-for-unreal/docs/keep-your-gdk-up-to-date) to upgrade your GDK, Unreal Engine fork and your Project to the latest release. +Follow [these steps](https://networking.docs.improbable.io/gdk-for-unreal/v0.14.0/workflows/keep-your-gdk-up-to-date) to upgrade your GDK, Unreal Engine fork and your Project to the latest release. You can read the full release notes [here](https://github.com/spatialos/UnrealGDK/blob/release/CHANGELOG.md). @@ -411,7 +411,7 @@ Happy developing!
* You can find the corresponding UnrealGDKExampleProject version [here](https://github.com/spatialos/UnrealGDKExampleProject/releases/tag/{options.Version}). * You can find the corresponding UnrealEngine version(s) [here](https://github.com/improbableio/UnrealEngine/releases). -Follow [these steps](https://documentation.improbable.io/gdk-for-unreal/docs/keep-your-gdk-up-to-date) to upgrade your GDK, Unreal Engine fork and your Project to the latest release. +Follow [these steps](https://networking.docs.improbable.io/gdk-for-unreal/v0.14.0/workflows/keep-your-gdk-up-to-date) to upgrade your GDK, Unreal Engine fork and your Project to the latest release. You can read the full release notes [here](https://github.com/spatialos/UnrealGDK/blob/release/CHANGELOG.md). @@ -430,7 +430,7 @@ Happy developing!
* You can find the corresponding UnrealGDKExampleProject version [here](https://github.com/spatialos/UnrealGDKExampleProject/releases/tag/{options.Version}). * You can find the corresponding UnrealEngine version(s) [here](https://github.com/improbableio/UnrealEngine/releases). -Follow [these steps](https://documentation.improbable.io/gdk-for-unreal/docs/keep-your-gdk-up-to-date) to upgrade your GDK, Unreal Engine fork and your Project to the latest release. +Follow [these steps](https://networking.docs.improbable.io/gdk-for-unreal/v0.14.0/workflows/keep-your-gdk-up-to-date) to upgrade your GDK, Unreal Engine fork and your Project to the latest release. You can read the full release notes [here](https://github.com/spatialos/UnrealGDK/blob/release/CHANGELOG.md). @@ -447,7 +447,7 @@ Happy developing!
* This UnrealGDKExampleProject version corresponds to GDK version: [{options.Version}](https://github.com/spatialos/UnrealGDK/releases/tag/{options.Version}). * You can find the corresponding UnrealEngine version(s) [here](https://github.com/improbableio/UnrealEngine/releases). -Follow [these steps](https://documentation.improbable.io/gdk-for-unreal/docs/keep-your-gdk-up-to-date) to upgrade your GDK, Unreal Engine fork and your Project to the latest release. +Follow [these steps](https://networking.docs.improbable.io/gdk-for-unreal/v0.14.0/workflows/keep-your-gdk-up-to-date) to upgrade your GDK, Unreal Engine fork and your Project to the latest release. You can read the full release notes [here](https://github.com/spatialos/UnrealGDK/blob/release/CHANGELOG.md). diff --git a/ci/unreal-engine.version b/ci/unreal-engine.version index 15b38d1fcc..b8f7ddfcdb 100644 --- a/ci/unreal-engine.version +++ b/ci/unreal-engine.version @@ -1,2 +1,2 @@ -HEAD 4.26-SpatialOSUnrealGDK-0.13.1-rc -HEAD 4.25-SpatialOSUnrealGDK-0.13.1-rc +HEAD 4.26-SpatialOSUnrealGDK-0.14.0 +HEAD 4.25-SpatialOSUnrealGDK-0.14.0