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