diff --git a/.buildkite/premerge.steps.yaml b/.buildkite/premerge.steps.yaml
index e34cc50887..6bb2a0a29d 100755
--- a/.buildkite/premerge.steps.yaml
+++ b/.buildkite/premerge.steps.yaml
@@ -57,7 +57,7 @@ steps:
# Trigger an Example Project build for any merges into master, preview or release branches of UnrealGDK
- trigger: "unrealgdkexampleproject-nightly"
label: "post-merge-example-project-build"
- branches: "master preview release"
+ if: build.env("ENGINE_NET_TEST") != "true" && (build.branch == "master" || build.branch == "preview" || build.branch == "release")
async: true
build:
env:
@@ -78,6 +78,6 @@ steps:
<<: *script_runner
- label: "slack-notify"
- if: (build.env("SLACK_NOTIFY") == "true" || build.branch == "master") && build.env("SLACK_NOTIFY") != "false"
+ if: (build.env("SLACK_NOTIFY") == "true" || build.branch == "master") && build.env("SLACK_NOTIFY") != "false" && build.env("ENGINE_NET_TEST") != "true"
commands: "powershell ./ci/build-and-send-slack-notification.ps1"
<<: *windows
diff --git a/.buildkite/release.definition.yaml b/.buildkite/release.definition.yaml
new file mode 100644
index 0000000000..59478f7bc2
--- /dev/null
+++ b/.buildkite/release.definition.yaml
@@ -0,0 +1,11 @@
+agent_queue_id: trigger-pipelines
+description: Releases all UnrealGDK associated repos to `release` branches.
+github:
+ branch_configuration: []
+ default_branch: master
+ pull_request_branch_filter_configuration: []
+teams:
+- name: Everyone
+ permission: READ_ONLY
+- name: gbu/develop/unreal
+ permission: MANAGE_BUILD_AND_READ
diff --git a/.buildkite/release.steps.yaml b/.buildkite/release.steps.yaml
new file mode 100644
index 0000000000..9bff8f1779
--- /dev/null
+++ b/.buildkite/release.steps.yaml
@@ -0,0 +1,90 @@
+---
+common: &common
+ agents:
+ - "capable_of_building=gdk-for-unreal"
+ - "environment=production"
+ - "permission_set=builder"
+ - "platform=linux" # if you need a different platform, configure this: macos|linux|windows.
+ - "queue=${CI_LINUX_BUILDER_QUEUE:-v4-20-04-27-095849-bk10828-316979f1}"
+ - "scaler_version=2"
+ - "working_hours_time_zone=london"
+
+ retry:
+ automatic:
+ # This is designed to trap and retry failures because agent lost connection. Agent exits with -1 in this case.
+ - exit_status: -1
+ limit: 3
+
+steps:
+ # Stage 0: Tell Buildkite what you want to release.
+ - block: "Configure your release."
+ prompt: "Fill out these details for your release."
+ fields:
+ - text: "UnrealGDK component release version"
+ key: "gdk-version"
+ required: true
+ hint: "The name of component release version you want to create release candidates for. For example: `0.12.2` or `1.0.1`."
+
+ - text: "UnrealGDK source branch"
+ key: "gdk-source-branch"
+ required: true
+ hint: "The branch you want to create UnrealGDK, UnrealGDKExampleProject, UnrealGDKTestGyms, TestGymBuildKite & UnrealGDKEngineNetTest, release candidates from."
+ default: "master"
+
+ - text: "UnrealEngine source branches"
+ key: "engine-source-branches"
+ required: true
+ hint: "The Unreal Engine branch (or branches) that you want to create release candidates from. Put each branch on a separate line with the primary Engine version at the top."
+ default: "4.24-SpatialOSUnrealGDK\n4.23-SpatialOSUnrealGDK"
+
+ # Stage 1: Prepare the release candidates. Prepare steps create a PR and upload metadata but do not release anything.
+ - label: "Prepare the release"
+ command: ci/prepare-release.sh
+ <<: *common # This folds the YAML named anchor into this step. Overrides, if any, should follow, not precede.
+ retry:
+ manual:
+ permit_on_passed: true
+ concurrency: 1
+ concurrency_group: "unrealgdk-release"
+
+ - wait
+
+# Stage 2: Builds all UnrealEngine release candidates, compresses and uploads Engine artifacts to Google Cloud Storage for use by test pipelines.
+ - label: "Build & upload all UnrealEngine release candidates"
+ command: ci/generate-unrealengine-premerge-trigger.sh | tee /dev/fd/2 | buildkite-agent pipeline upload
+ <<: *common # This folds the YAML named anchor into this step. Overrides, if any, should follow, not precede.
+ retry:
+ manual:
+ permit_on_passed: true
+ concurrency: 1
+ concurrency_group: "unrealgdk-release"
+ #TODO: This step is actually not strictly necessary. It will be removed as part of: UNR-3662
+ skip: true
+
+ # Stage 3: Run all tests against the release candidates.
+ # Block steps require a human to click a button. This
+ - block: "Run all tests"
+ prompt: "This action triggers all tests. Tests depend on the presence of unrealengine-premerge artifacts in Google Cloud Storage. Only click OK if the above unrealengine-premerge build(s) have passed."
+
+ - label: "Trigger all automated tests"
+ command: ci/generate-release-qa-trigger.sh | tee /dev/fd/2 | buildkite-agent pipeline upload
+ <<: *common # This folds the YAML named anchor into this step. Overrides, if any, should follow, not precede.
+ retry:
+ manual:
+ permit_on_passed: true
+ concurrency: 1
+ concurrency_group: "unrealgdk-release"
+
+ - wait
+
+ # Stage 4: Promote the release candiates to their respective release branches.
+
+ # Block steps require a human to click a button, this is a safety precaution.
+ - block: "Unblock the release"
+ prompt: "This action will merge all release candidates into their respective release branches."
+
+ - label: Release
+ command: ci/release.sh
+ concurrency: 1
+ concurrency_group: "unrealgdk-release"
+ <<: *common # This folds the YAML named anchor into this step. Overrides, if any, should follow, not precede.
diff --git a/.github/pull-request-template.md b/.github/pull-request-template.md
index 556a100010..a86aa8cbb8 100644
--- a/.github/pull-request-template.md
+++ b/.github/pull-request-template.md
@@ -13,5 +13,12 @@ STRONGLY SUGGESTED: How can this be verified by QA?
#### Documentation
How is this documented (for example: release note, upgrade guide, feature page, in-code documentation)?
+#### Reminders (IMPORTANT)
+If your change relies on a breaking engine change:
+* Increment `SPATIAL_ENGINE_VERSION` in `Engine\Source\Runtime\Launch\Resources\SpatialVersion.h` (in the engine fork) as well as `SPATIAL_GDK_VERSION` in `SpatialGDK\Source\SpatialGDK\Public\Utils\EngineVersionCheck.h`. This helps others by providing a more helpful message during compilation to make sure the GDK and the Engine are up to date.
+
+If your change updates `Setup.bat`, `Setup.sh`, core SDK version, any C# tools in `SpatialGDK\Build\Programs\Improbable.Unreal.Scripts`, or hand-written schema in `SpatialGDK\Extras\schema`:
+* Increment the number in `RequireSetup`. This will automatically run `Setup.bat` or `Setup.sh` when the GDK is next pulled.
+
#### Primary reviewers
If your change will take a long time to review, you can name at most two primary reviewers who are ultimately responsible for reviewing this request. @ mention them.
diff --git a/.gitignore b/.gitignore
index d5ae672f0b..ccd739a27d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,9 @@
# Visual Studio Code user specific files
.vscode/
+# IDEA user files
+.idea/
+
# Visual Studio 2015 user specific files
.vs/
@@ -81,6 +84,9 @@ logs/
Scripts/spatialos.*.build.json
+# Docker needs this sln to build the Release Tool
+!ci/Tools.sln
+
!SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Improbable.Unreal.Scripts.sln
!SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Mac/Improbable.Unreal.Scripts.sln
!SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/packages/Newtonsoft.Json.12.0.2/lib/net45/Newtonsoft.Json.dll
@@ -93,3 +99,6 @@ SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.csproj.user
_ReSharper.caches
**/.DS_Store
+
+# File to indicate services region, created by Setup script
+UseChinaServicesRegion
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cb5c6a04b5..6c467df762 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,121 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
**Note**: Since GDK for Unreal v0.8.0, the changelog is published in both English and Chinese. The Chinese version of each changelog is shown after its English version.
**注意**:自虚幻引擎开发套件 v0.8.0 版本起,其日志提供中英文两个版本。每个日志的中文版本都置于英文版本之后。
+## [`x.y.z`] - Unreleased
+
+## [`0.10.0`] - 2020-07-08
+
+### New Known Issues:
+- Replicated properties that use the `COND_SkipOwner` replication condition can still replicate in the first few frames of an Actor becoming owned.
+- Microsoft have fixed a defect in MSVC that previously caused errors when building Unreal Engine. We documented a workaround for this issue in GDK version [`0.7.0-preview`](#070-preview---2019-10-11). If you set up the GDK on your computer between the release of `0.7.0` and `0.10.0`, you have performed this workaround, which is no longer necessary. To undo this workaround, follow these steps:
+1. Open Visual Studio Installer.
+1. Select **Modify** on your Visual Studio 2019 installation.
+1. In the **Installation details** section, clear all the checkboxes for workloads and components except **Visual Studio Code editor**.
+1. In the **Workloads** tab, select the following items:
+ - **Universal Windows Platform development**
+ - **.NET desktop development** (You must also select the **.NET Framework 4.6.2 development tools**.)
+ - **Desktop development with C++**
+5. Select **Modify** to confirm your changes.
+
+### Breaking Changes:
+- We've deprecated the `preview` branches. We now only release the GDK to the `release` branch, which is fully tested, stable, and documented. If you have the `preview` branches checked out, you must check out `release` to receive the latest changes.
+- The SpatialOS Runtime Standard variant requires the latest version of the SpatialOS CLI. Run `spatial update` to get the latest version.
+- Old snapshots are incompatible with version 0.10 of the GDK. You must generate new snapshots after upgrading to 0.10.
+- The old Inspector is incompatible with the SpatialOS Runtime Standard variant. The Standard variant uses the new Inspector by default.
+- We’ve removed `Singleton` as a class specifier, and you need to remove your uses of it. You can achieve the behavior of former Singleton Actors by ensuring that your Actor is spawned once by a single server-worker instance in your deployment.
+- We’ve renamed `OnConnected` and `OnConnectionFailed` (on `SpatialGameInstance`) to `OnSpatialConnected` and `OnSpatialConnectionFailed`. They are now also Blueprint-assignable.
+- The `GenerateSchema` and `GenerateSchemaAndSnapshots` commandlets do not generate schema any more. We’ve deprecated them in favor of `CookAndGenerateSchemaCommandlet`. (`GenerateSchemaAndSnapshots` still works if you use the `-SkipSchema` option.)
+- We’ve combined the settings for offloading and load balancing and moved them from the Editor and Runtime Settings to be per map in the World Settings. For more information, see the [offloading tutorial](https://documentation.improbable.io/gdk-for-unreal/docs/multiserver-offloading-1-set-up).
+- We’ve removed the command-line arguments `OverrideSpatialOffloading` and `OverrideLoadBalancer`, and GDK load balancing is always enabled. To override a map's `Enable Multi Worker` setting, use the command-line flag `OverrideMultiWorker`.
+- It is now mandatory to run a deployment with result types (previously result types were enabled by default). We’ve removed the Runtime setting `bEnableResultTypes` to reflect this.
+- Whether an Actor is offloaded depends on whether the root owner of that Actor is offloaded. This might affect you if you're using functions such as `IsActorGroupOwnerForActor`.
+- We’ve removed `QueuedOutgoingRPCWaitTime`. All RPC failure cases are now correctly queued or dropped.
+- We’ve removed `Max connection capacity limit` and `Login rate limit` from the generated worker configuration file, because we no longer support them.
+- We no longer support secure worker connections when you run your game within the Unreal Editor. We still support secure worker connections for packaged builds.
+
+### Features:
+- The GDK now uses the SpatialOS Worker SDK version [`14.6.1`](https://documentation.improbable.io/sdks-and-data/docs/release-notes#section-14-6-1).
+- Added support for the SpatialOS Runtime [Standard variant](https://documentation.improbable.io/gdk-for-unreal/docs/the-spatialos-runtime#section-runtime-variants), version 0.4.3.
+- Added support for the SpatialOS Runtime [Compatibility Mode variant](https://documentation.improbable.io/gdk-for-unreal/docs/the-spatialos-runtime#section-runtime-variants), version [`14.5.4`](https://forums.improbable.io/t/spatialos-13-runtime-release-notes-14-5-4/7333).
+- Added a new drop-down menu in Editor Settings so that you can select which SpatialOS Runtime variant to use. The two variants are Standard and Compatibility Mode. For Windows users, Standard is the default, but you can use Compatibility Mode if you experience networking issues when you upgrade to the latest GDK version. For macOS users, Compatibility Mode is the default, and you can’t use Standard. For more information, see [Runtime variants](https://documentation.improbable.io/gdk-for-unreal/docs/the-spatialos-runtime#section-runtime-variants).
+- Added new default game templates. Your default game template depends on the SpatialOS Runtime variant that you have selected, and on your primary deployment region.
+- The SpatialOS Runtime Standard variant uses the new Inspector by default, and is incompatible with the old Inspector. (The Compatibility Mode variant uses the old Inspector by default, and is incompatible with the new Inspector.)
+- The Example Project has a new default game mode: Control. This game mode replaces Deathmatch. In Control, two teams compete to capture control points on the map. NPCs guard the control points, and if you capture an NPC’s control point, then the NPC joins your team.
+- You can now generate valid schema for classes that start with a leading digit. The generated schema classes are prefixed with `ZZ` internally.
+- Handover properties are now automatically replicated when this is required for load balancing. `bEnableHandover` is off by default.
+- Added `OnSpatialPlayerSpawnFailed` delegate to `SpatialGameInstance`. This is useful if you have established a successful connection from the client-worker instance to the SpatialOS Runtime, but the server-worker instance crashed.
+- Added `bWorkerFlushAfterOutgoingNetworkOp` (default false) which sends RPCs and property replication changes over the network immediately, to allow for lower latencies. You can use this with `bRunSpatialWorkerConnectionOnGameThread` to achieve the lowest available latency at a trade-off with bandwidth.
+- You can now edit the project name field in the `Cloud Deployment Configuration` dialog box. Changes that you make here are reflected in your project's `spatialos.json` file.
+- You now define worker types in Runtime Settings.
+- Local deployments now use the map's load balancing strategy to get the launch configuration settings. The launch configuration file is saved per map in the `Intermediate/Improbable` folder.
+- Added a `Launch Configuration Editor` under the Cloud toolbar button.
+- In the `Cloud Deployment Configuration` dialog box you can now generate a launch configuration file from the current map, or you can click through to the `Launch Configuration Editor`.
+- You can now specify worker load in game logic by using `SpatialMetrics::SetWorkerLoadDelegate`.
+- You can now specify deployment tags in the `Cloud Deployment Configuration` dialog box.
+- You can now execute RPCs that were declared in a `UInterface`. Previously, this caused a runtime assertion.
+- Full Scan schema generation now uses the `CookAndGenerateSchema` commandlet, which results in faster and more stable schema generation for big projects.
+- Added an `Open Deployment Page` button to the `Cloud Deployment Configuration` dialog box.
+- The `Start Deployment` button in the `Cloud Deployment Configuration` dialog box now generates schema and a snapshot, builds all selected workers, and uploads the assembly before starting the deployment. There are checkboxes so that you can choose whether to generate schema and a snapshot, and whether to build the game client and add simulated players.
+- When you start a cloud deployment from the Unreal Editor, the cloud deployment now automatically has the dev_login deployment tag.
+- Several command-line parameter changes:
+ - Renamed the `enableProtocolLogging` command-line parameter to `enableWorkerSDKProtocolLogging`.
+ - Added a parameter named enableWorkerSDKOpLogging so that you can log user-level ops.
+ - Renamed the `protocolLoggingPrefix` parameter to workerSDKLogPrefix. This prefix is used for both protocol logging and op logging.
+ - Added a parameter named `workerSDKLogLevel` that takes the arguments `debug`, `info`, `warning`, and `error`.
+ - Added a parameter named `workerSDKLogFileSize` to control the maximum file size of the Worker SDK log file.
+- The icon on the `Start Deployment` toolbar button now changes depending on the connection flow that you select.
+- Created a new drop-down menu in the GDK toolbar. You can use it to configure how to connect your PIE client or your Launch on Device client:
+ - Choose between `Connect to a local deployment` and `Connect to a cloud deployment` to specify the flow that the client should automatically use when you select `Play` or `Launch`.
+ - Added the `Local Deployment IP` field to specify which local deployment the client should connect to. By default, the IP is `127.0.0.1`.
+ - Added the `Cloud deployment name` field to specify which cloud deployment the client should connect to. If you select Connect to cloud deployment but you don’t specify a cloud deployment, the client tries to connect to the first running deployment that has the `dev_login` deployment tag.
+ - Added the `Editor Settings` field so that you can quickly access the SpatialOS Editor Settings.
+- Added the `Build Client Worker` and `Build Simulated Player` checkboxes to the `Connection` drop-down menu, so that you can quickly choose whether to build and include the client-worker instance and simulated player worker instance in the assembly.
+- Updated the GDK toolbar icons.
+- When you specify a URL to connect a client to a deployment using the Receptionist, the URL port option is now respected. - --- However, in certain circumstances, the initial connection attempt uses the `-receptionistPort` command-line argument.
+- When you run `BuildWorker.bat` with `client`, this now builds the client target of your project.
+- When you change the project name in the `Cloud Deployment Configuration` dialog box, this automatically regenerates the development authentication token.
+- Changed the names of the following toolbar buttons:
+ - `Start` is now called `Start Deployment`
+ - `Deploy` is now called `Cloud`
+- Marked all the required fields in the `Cloud Deployment Configuration` dialog box with asterisks.
+- You can now change the project name in Editor Settings.
+- Replaced the `Generate from current map` button in the `Cloud Deployment Configuration` dialog box with a checkbox labelled `Automatically Generate Launch Configuration`. If you select this checkbox, the GDK generates an up-to-date launch configuration file from the current map when you select `Start Deployment`.
+- Android and iOS are now in preview. We support workflows for developing and doing in-studio playtests on Android and iOS devices, and have documentation for these workflows. We also support macOS (also in preview) for developing and testing iOS game clients.
+
+## Bug fixes:
+- Fixed a problem that caused load balanced cloud deployments to fail to start while under heavy load.
+- Fix to avoid using packages that are still being processed in the asynchronous loading thread.
+- Fixed a bug that sometimes caused GDK setup scripts to fail to unzip dependencies.
+- Fixed a bug where RPCs that were called before calling the `CreateEntityRequest` were not processed as early as possible in the RPC ring buffer system, resulting in startup delays on the client.
+- Fixed a crash that occurred when running a game with `nullrhi` and using `SpatialDebugger`.
+- When you use a URL with options in the command line, we now parse the Receptionist parameters correctly, using the URL if necessary.
+- Fixed a bug that occurred when creating multiple dynamic subobjects at the same time, and caused them to fail to be created on clients.
+- `OwnerOnly` components are now properly replicated when a worker instance gains authority over an Actor. Previously, they were sometimes only replicated when a value on them changed (after the worker instance had already gained authority).
+- Fixed a rare server crash that could occur when closing an Actor channel immediately after attaching a dynamic subobject to that Actor.
+- Fixed a defect in `InstallGDK.bat` that sometimes caused it to incorrectly report `Error: Could not clone…` when repositories were cloned correctly.
+- Actors from the same ownership hierarchy are now handled together when they are load balanced.
+
+## SpatialOS tooling compatibility:
+If you are using the Standard Runtime variant, note the following compatibility issues:
+- The [old Inspector](https://documentation.improbable.io/spatialos-tools/docs/the-inspector) won’t work. You must use the [new Inspector](https://documentation.improbable.io/spatialos-tools/docs/the-new-inspector) instead.
+- In the [Platform SDK in C#](https://documentation.improbable.io/sdks-and-data/docs/platform-csharp-introduction), you can’t set [capacity limits](https://documentation.improbable.io/sdks-and-data/docs/platform-csharp-capacity-limiting) or use the [remote interaction service](https://documentation.improbable.io/sdks-and-data/docs/platform-csharp-remote-interactions). You also can’t use the Platform SDK to take snapshots of cloud deployments, but we’ll fix this snapshot issue in a future release.
+- You can't generate a snapshot of a cloud deployment. We'll fix this in a future release.
+- In the [CLI](https://documentation.improbable.io/spatialos-tools/docs/cli-introduction), the following commands don’t work:
+ - `spatial local worker replace`
+ - `spatial project deployment worker replace`
+ - `spatial local worker-flag set`
+ - `spatial project deployment worker-flag delete`
+ - `spatial project deployment worker-flag set`
+ - `spatial cloud runtime flags set` (We’ll improve debug tooling and add functionality to [dynamically change worker flag values](https://documentation.improbable.io/gdk-for-unreal/docs/worker-flags#section-change-worker-flag-values-while-the-deployment-is-running) in future releases.)
+
+If you need any of the functionality mentioned above, [change your Runtime variant to Compatibility Mode](https://documentation.improbable.io/gdk-for-unreal/docs/the-spatialos-runtime#section-change-your-runtime-variant).
+
+### Internal:
+Features listed in this section are not ready to use. However, in the spirit of open development, we record every change that we make to the GDK.
+
+- The SpatialOS GDK for Unreal is now released automatically using Buildkite CI. This should result in more frequent releases.
+- Improbable now measures the non-functional characteristics of the GDK in Buildkite CI. This enables us to reason about and improve these characteristics. We track them as non-functional requirements (NFRs).
+
## [`0.9.0`] - 2020-05-05
### New Known Issues:
@@ -64,6 +179,7 @@ Usage: `DeploymentLauncher createsim **Editor Settings** > **Region Settings** has been moved to **SpatialOS GDK for Unreal** > **Runtime Settings** > **Region Settings**.
- You can now choose which SpatialOS service region you want to use by adjusting the **Region where services are located** setting. You must use the service region that you're geographically located in.
diff --git a/RequireSetup b/RequireSetup
index 77d0f13ca4..51ca8c42ac 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.
Our git hooks will detect this file has been updated and automatically run Setup.bat on pull.
-53
+60
diff --git a/Setup.bat b/Setup.bat
index 35a9fc4200..869d3dbfd7 100644
--- a/Setup.bat
+++ b/Setup.bat
@@ -23,7 +23,7 @@ call :MarkStartOfBlock "Setup the git hooks"
echo check_run() {>>.git\hooks\post-merge
echo echo "$changed_files" ^| grep --quiet "$1" ^&^& exec $2>>.git\hooks\post-merge
echo }>>.git\hooks\post-merge
- echo check_run RequireSetup "cmd.exe /c Setup.bat">>.git\hooks\post-merge
+ echo check_run RequireSetup "cmd.exe /c Setup.bat %*">>.git\hooks\post-merge
:SkipGitHooks
call :MarkEndOfBlock "Setup the git hooks"
@@ -67,6 +67,20 @@ call :MarkStartOfBlock "Setup variables"
)
call :MarkEndOfBlock "Setup variables"
+call :MarkStartOfBlock "Setup services region"
+ set USE_CHINA_SERVICES_REGION=
+ for %%A in (%*) do (
+ if "%%A"=="--china" set USE_CHINA_SERVICES_REGION=True
+ )
+
+ rem Create or remove an empty file in the plugin directory indicating whether to use China services region.
+ if defined USE_CHINA_SERVICES_REGION (
+ echo. 2> UseChinaServicesRegion
+ ) else (
+ if exist UseChinaServicesRegion del UseChinaServicesRegion
+ )
+call :MarkEndOfBlock "Setup services region"
+
call :MarkStartOfBlock "Clean folders"
rd /s /q "%CORE_SDK_DIR%" 2>nul
rd /s /q "%WORKER_SDK_DIR%" 2>nul
@@ -103,14 +117,17 @@ call :MarkStartOfBlock "Retrieve dependencies"
spatial package retrieve worker_sdk c-dynamic-x86_64-gcc510-linux %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-gcc510-linux.zip"
if defined DOWNLOAD_MOBILE (
spatial package retrieve worker_sdk c-static-fullylinked-arm-clang-ios %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-static-fullylinked-arm-clang-ios.zip"
- spatial package retrieve worker_sdk c-dynamic-arm64v8a-clang_ndk16b-android %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-arm64v8a-clang_ndk16b-android.zip"
- spatial package retrieve worker_sdk c-dynamic-armv7a-clang_ndk16b-android %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-armv7a-clang_ndk16b-android.zip"
- spatial package retrieve worker_sdk c-dynamic-x86_64-clang_ndk16b-android %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-clang_ndk16b-android.zip"
+ spatial package retrieve worker_sdk c-dynamic-arm64v8a-clang_ndk21-android %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-arm64v8a-clang_clang_ndk21-android.zip"
+ spatial package retrieve worker_sdk c-dynamic-armv7a-clang_ndk21-android %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-armv7a-clang_clang_ndk21-android.zip"
+ spatial package retrieve worker_sdk c-dynamic-x86_64-clang_ndk21-android %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-clang_clang_ndk21-android.zip"
)
spatial package retrieve worker_sdk csharp %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\csharp.zip"
spatial package retrieve spot spot-win64 %PINNED_SPOT_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%BINARIES_DIR%\Programs\spot.exe"
call :MarkEndOfBlock "Retrieve dependencies"
+REM There is a race condition between retrieve and unzip, add version call to stall briefly
+call spatial version
+
call :MarkStartOfBlock "Unpack dependencies"
powershell -Command "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c_headers.zip\" -DestinationPath \"%BINARIES_DIR%\Headers\" -Force; "^
"Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86-vc141_md-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Win32\" -Force; "^
@@ -122,9 +139,9 @@ call :MarkStartOfBlock "Unpack dependencies"
if defined DOWNLOAD_MOBILE (
powershell -Command "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-static-fullylinked-arm-clang-ios.zip\" -DestinationPath \"%BINARIES_DIR%\IOS\" -Force;"^
- "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-arm64v8a-clang_ndk16b-android.zip\" -DestinationPath \"%BINARIES_DIR%\Android\arm64-v8a\" -Force; "^
- "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-armv7a-clang_ndk16b-android.zip\" -DestinationPath \"%BINARIES_DIR%\Android\armeabi-v7a\" -Force; "^
- "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-clang_ndk16b-android.zip\" -DestinationPath \"%BINARIES_DIR%\Android\x86_64\" -Force; "^
+ "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-arm64v8a-clang_clang_ndk21-android.zip\" -DestinationPath \"%BINARIES_DIR%\Android\arm64-v8a\" -Force; "^
+ "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-armv7a-clang_clang_ndk21-android.zip\" -DestinationPath \"%BINARIES_DIR%\Android\armeabi-v7a\" -Force; "^
+ "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-clang_clang_ndk21-android.zip\" -DestinationPath \"%BINARIES_DIR%\Android\x86_64\" -Force; "^
)
xcopy /s /i /q "%BINARIES_DIR%\Headers\include" "%WORKER_SDK_DIR%"
call :MarkEndOfBlock "Unpack dependencies"
diff --git a/Setup.sh b/Setup.sh
index 633b3a427e..f893ad66a4 100755
--- a/Setup.sh
+++ b/Setup.sh
@@ -20,11 +20,14 @@ SCHEMA_COPY_DIR="$(pwd)/../../../spatial/schema/unreal/gdk"
SCHEMA_STD_COPY_DIR="$(pwd)/../../../spatial/build/dependencies/schema/standard_library"
SPATIAL_DIR="$(pwd)/../../../spatial"
DOWNLOAD_MOBILE=
+USE_CHINA_SERVICES_REGION=
while test $# -gt 0
do
case "$1" in
- --china) DOMAIN_ENVIRONMENT_VAR="--environment cn-production"
+ --china)
+ DOMAIN_ENVIRONMENT_VAR="--environment cn-production"
+ USE_CHINA_SERVICES_REGION=true
;;
--mobile) DOWNLOAD_MOBILE=true
;;
@@ -48,6 +51,13 @@ if [[ -e .git/hooks ]]; then
cp "$(pwd)/SpatialGDK/Extras/git/post-merge" "$(pwd)/.git/hooks"
fi
+# Create or remove an empty file in the plugin directory indicating whether to use China services region.
+if [[ -n "${USE_CHINA_SERVICES_REGION}" ]]; then
+ touch UseChinaServicesRegion
+else
+ rm -f UseChinaServicesRegion
+fi
+
echo "Clean folders"
rm -rf "${CORE_SDK_DIR}"
rm -rf "${WORKER_SDK_DIR}"
@@ -80,9 +90,9 @@ spatial package retrieve worker_sdk c-dynamic-x86_64-clang-macos "${
if [[ -n "${DOWNLOAD_MOBILE}" ]];
then
spatial package retrieve worker_sdk c-static-fullylinked-arm-clang-ios "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-static-fullylinked-arm-clang-ios.zip
- spatial package retrieve worker_sdk c-dynamic-arm64v8a-clang_ndk16b-android "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-arm64v8a-clang_ndk16b-android.zip
- spatial package retrieve worker_sdk c-dynamic-armv7a-clang_ndk16b-android "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-armv7a-clang_ndk16b-android.zip
- spatial package retrieve worker_sdk c-dynamic-x86_64-clang_ndk16b-android "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang_ndk16b-android.zip
+ spatial package retrieve worker_sdk c-dynamic-arm64v8a-clang_ndk21-android "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-arm64v8a-clang_ndk21-android.zip
+ spatial package retrieve worker_sdk c-dynamic-armv7a-clang_ndk21-android "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-armv7a-clang_ndk21-android.zip
+ spatial package retrieve worker_sdk c-dynamic-x86_64-clang_ndk21-android "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang_ndk21-android.zip
fi
spatial package retrieve worker_sdk csharp "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/csharp.zip
@@ -98,9 +108,9 @@ unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang-macos.zip
if [[ -n "${DOWNLOAD_MOBILE}" ]];
then
unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-static-fullylinked-arm-clang-ios.zip -d "${BINARIES_DIR}"/IOS/
- unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-arm64v8a-clang_ndk16b-android.zip -d "${BINARIES_DIR}"/Android/arm64-v8a/
- unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-armv7a-clang_ndk16b-android.zip -d "${BINARIES_DIR}"/Android/armeabi-v7a/
- unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang_ndk16b-android.zip -d "${BINARIES_DIR}"/Android/x86_64/
+ unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-arm64v8a-clang_ndk21-android.zip -d "${BINARIES_DIR}"/Android/arm64-v8a/
+ unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-armv7a-clang_ndk21-android.zip -d "${BINARIES_DIR}"/Android/armeabi-v7a/
+ unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang_ndk21-android.zip -d "${BINARIES_DIR}"/Android/x86_64/
fi
unzip -oq "${CORE_SDK_DIR}"/worker_sdk/csharp.zip -d "${BINARIES_DIR}"/Programs/worker_sdk/csharp/
diff --git a/SetupIncTraceLibs.bat b/SetupIncTraceLibs.bat
index b13e731b88..b7d1eb65b4 100644
--- a/SetupIncTraceLibs.bat
+++ b/SetupIncTraceLibs.bat
@@ -22,6 +22,9 @@ call :MarkStartOfBlock "Retrieve dependencies"
spatial package retrieve internal trace-dynamic-x86_64-gcc510-linux 14.3.0-b2647-85717ee-WORKER-SNAPSHOT "%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
+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;"
diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs
index 69f0e1b51e..b5c6492620 100644
--- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs
+++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs
@@ -178,31 +178,9 @@ public static void Main(string[] args)
var windowsNoEditorPath = Path.Combine(stagingDir, "WindowsNoEditor");
- if (additionalUATArgs.Contains("-pak"))
- {
- Console.WriteLine("Cannot force bSpatialNetworking with -pak argument.");
- }
- else
- {
- ForceSpatialNetworkingInConfig(windowsNoEditorPath, baseGameName);
- }
+ ForceSpatialNetworkingUnlessPakSpecified(additionalUATArgs, windowsNoEditorPath, baseGameName);
- // Add a _ to the start of the exe name, to ensure it is the exe selected by the launcher.
- // TO-DO: Remove this once LAUNCH-341 has been completed, and the _ is no longer necessary.
- var oldExe = Path.Combine(windowsNoEditorPath, $"{gameName}.exe");
- var renamedExe = Path.Combine(windowsNoEditorPath, $"_{gameName}.exe");
- if (File.Exists(renamedExe))
- {
- File.Delete(renamedExe);
- }
- if (File.Exists(oldExe))
- {
- File.Move(oldExe, renamedExe);
- }
- else
- {
- Console.WriteLine("Could not find the executable to rename.");
- }
+ RenameExeForLauncher(windowsNoEditorPath, baseGameName);
Common.RunRedirected(runUATBat, new[]
{
@@ -244,14 +222,7 @@ public static void Main(string[] args)
var linuxSimulatedPlayerPath = Path.Combine(stagingDir, "LinuxNoEditor");
- if (additionalUATArgs.Contains("-pak"))
- {
- Console.WriteLine("Cannot force bSpatialNetworking with -pak argument.");
- }
- else
- {
- ForceSpatialNetworkingInConfig(linuxSimulatedPlayerPath, baseGameName);
- }
+ ForceSpatialNetworkingUnlessPakSpecified(additionalUATArgs, linuxSimulatedPlayerPath, baseGameName);
LinuxScripts.WriteWithLinuxLineEndings(LinuxScripts.GetSimulatedPlayerWorkerShellScript(baseGameName), Path.Combine(linuxSimulatedPlayerPath, "StartSimulatedClient.sh"));
LinuxScripts.WriteWithLinuxLineEndings(LinuxScripts.GetSimulatedPlayerCoordinatorShellScript(baseGameName), Path.Combine(linuxSimulatedPlayerPath, "StartCoordinator.sh"));
@@ -318,14 +289,7 @@ public static void Main(string[] args)
var assemblyPlatform = isLinux ? "Linux" : "Windows";
var serverPath = Path.Combine(stagingDir, assemblyPlatform + "Server");
- if (additionalUATArgs.Contains("-pak"))
- {
- Console.WriteLine("Cannot force bSpatialNetworking with -pak argument.");
- }
- else
- {
- ForceSpatialNetworkingInConfig(serverPath, baseGameName);
- }
+ ForceSpatialNetworkingUnlessPakSpecified(additionalUATArgs, serverPath, baseGameName);
if (isLinux)
{
@@ -341,6 +305,51 @@ public static void Main(string[] args)
"-archive=" + Quote(Path.Combine(outputDir, $"UnrealWorker@{assemblyPlatform}.zip"))
});
}
+ else if (gameName == baseGameName + "Client")
+ {
+ Common.WriteHeading(" > Building client.");
+ Common.RunRedirected(runUATBat, new[]
+ {
+ "BuildCookRun",
+ noCompile ? "-nobuild" : "-build",
+ noCompile ? "-nocompile" : "-compile",
+ "-project=" + Quote(projectFile),
+ "-noP4",
+ "-clientconfig=" + configuration,
+ "-serverconfig=" + configuration,
+ "-utf8output",
+ "-cook",
+ "-stage",
+ "-package",
+ "-unversioned",
+ "-compressed",
+ "-stagingdirectory=" + Quote(stagingDir),
+ "-stdout",
+ "-FORCELOGFLUSH",
+ "-CrashForUAT",
+ "-unattended",
+ "-fileopenlog",
+ "-SkipCookingEditorContent",
+ "-client",
+ "-noserver",
+ "-platform=" + platform,
+ "-targetplatform=" + platform,
+ additionalUATArgs
+ });
+
+ var windowsClientPath = Path.Combine(stagingDir, "WindowsClient");
+
+ ForceSpatialNetworkingUnlessPakSpecified(additionalUATArgs, windowsClientPath, baseGameName);
+
+ RenameExeForLauncher(windowsClientPath, baseGameName + "Client");
+
+ Common.RunRedirected(runUATBat, new[]
+ {
+ "ZipUtils",
+ "-add=" + Quote(windowsClientPath),
+ "-archive=" + Quote(Path.Combine(outputDir, "UnrealClient@Windows.zip")),
+ });
+ }
else
{
// Pass-through to Unreal's Build.bat.
@@ -360,6 +369,38 @@ private static string Quote(string toQuote)
return $"\"{toQuote}\"";
}
+ private static void RenameExeForLauncher(string workerPath, string gameName)
+ {
+ // Add a _ to the start of the exe name, to ensure it is the exe selected by the launcher.
+ // TO-DO: Remove this once LAUNCH-341 has been completed, and the _ is no longer necessary.
+ var oldExe = Path.Combine(workerPath, $"{gameName}.exe");
+ var renamedExe = Path.Combine(workerPath, $"_{gameName}.exe");
+ if (File.Exists(renamedExe))
+ {
+ File.Delete(renamedExe);
+ }
+ if (File.Exists(oldExe))
+ {
+ File.Move(oldExe, renamedExe);
+ }
+ else
+ {
+ Console.WriteLine("Could not find the executable to rename.");
+ }
+ }
+
+ private static void ForceSpatialNetworkingUnlessPakSpecified(string additionalUATArgs, string workerPath, string gameName)
+ {
+ if (additionalUATArgs.Contains("-pak"))
+ {
+ Console.WriteLine("Cannot force bSpatialNetworking with -pak argument.");
+ }
+ else
+ {
+ ForceSpatialNetworkingInConfig(workerPath, gameName);
+ }
+ }
+
private static void ForceSpatialNetworkingInConfig(string workerPath, string gameName)
{
var defaultGameIniPath = Path.Combine(workerPath, gameName, "Config", "DefaultGame.ini");
diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/LinuxScripts.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/LinuxScripts.cs
index f5c6f7a12d..c64ef77ac8 100644
--- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/LinuxScripts.cs
+++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/LinuxScripts.cs
@@ -43,14 +43,9 @@ exit 1
WORKER_ID=$1
shift 1
-# 2>/dev/null silences errors by redirecting stderr to the null device. This is done to prevent errors when a machine attempts to add the same user more than once.
-useradd $NEW_USER -m -d /improbable/logs/ >> ""/improbable/logs/${{WORKER_ID}}.log"" 2>&1
-chown -R $NEW_USER:$NEW_USER $(pwd) >> ""/improbable/logs/${{WORKER_ID}}.log"" 2>&1
-chmod -R o+rw /improbable/logs >> ""/improbable/logs/${{WORKER_ID}}.log"" 2>&1
SCRIPT=""$(pwd)/{0}.sh""
-chmod +x $SCRIPT >> ""/improbable/logs/${{WORKER_ID}}.log"" 2>&1
-echo ""Trying to launch worker {0} with id ${{WORKER_ID}}"" > ""/improbable/logs/${{WORKER_ID}}.log""
+echo ""Trying to launch worker {0} with id ${{WORKER_ID}}"" >> ""/improbable/logs/${{WORKER_ID}}.log""
gosu $NEW_USER ""${{SCRIPT}}"" ""$@"" >> ""/improbable/logs/${{WORKER_ID}}.log"" 2>&1";
public const string SimulatedPlayerCoordinatorShellScript =
@@ -68,6 +63,11 @@ sleep 5
chmod +x StartSimulatedClient.sh
chmod +x {0}.sh
+NEW_USER=unrealworker
+useradd $NEW_USER -m -d /improbable/logs/ 2> /improbable/logs/CoordinatorErrors.log
+chown -R $NEW_USER:$NEW_USER $(pwd) 2> /improbable/logs/CoordinatorErrors.log
+chmod -R o+rw /improbable/logs 2> /improbable/logs/CoordinatorErrors.log
+
mono WorkerCoordinator.exe $@ 2> /improbable/logs/CoordinatorErrors.log";
// Returns a version of UnrealWorkerShellScript with baseGameName templated into the right places.
diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs
index d1fb157a51..dafdb7530f 100644
--- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs
+++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs
@@ -33,7 +33,7 @@ internal class DeploymentLauncher
"https://auth.spatialoschina.com/auth/v1/token");
private static string UploadSnapshot(SnapshotServiceClient client, string snapshotPath, string projectName,
- string deploymentName, string region)
+ string deploymentName, bool useChinaPlatform)
{
Console.WriteLine($"Uploading {snapshotPath} to project {projectName}");
@@ -69,7 +69,7 @@ private static string UploadSnapshot(SnapshotServiceClient client, string snapsh
httpRequest.ContentLength = snapshotToUpload.Size;
httpRequest.Headers.Add("Content-MD5", snapshotToUpload.Checksum);
- if (region == "CN")
+ if (useChinaPlatform)
{
httpRequest.Headers.Add("x-amz-server-side-encryption", "AES256");
}
@@ -92,24 +92,24 @@ private static string UploadSnapshot(SnapshotServiceClient client, string snapsh
return confirmUploadResponse.Snapshot.Id;
}
-
- private static PlatformApiEndpoint GetApiEndpoint(string region)
+
+ private static PlatformApiEndpoint GetApiEndpoint(bool useChinaPlatform)
{
- if (region == "CN")
+ if (useChinaPlatform)
{
return new PlatformApiEndpoint(CHINA_ENDPOINT_URL, CHINA_ENDPOINT_PORT);
}
return null; // Use default
}
- private static PlatformRefreshTokenCredential GetPlatformRefreshTokenCredential(string region)
+ private static PlatformRefreshTokenCredential GetPlatformRefreshTokenCredential(bool useChinaPlatform)
{
- return region == "CN" ? ChinaCredentials : null;
+ return useChinaPlatform ? ChinaCredentials : null;
}
- private static int CreateDeployment(string[] args)
+ private static int CreateDeployment(string[] args, bool useChinaPlatform)
{
- bool launchSimPlayerDeployment = args.Length == 12;
+ bool launchSimPlayerDeployment = args.Length == 15;
var projectName = args[1];
var assemblyName = args[2];
@@ -118,19 +118,23 @@ private static int CreateDeployment(string[] args)
var mainDeploymentJsonPath = args[5];
var mainDeploymentSnapshotPath = args[6];
var mainDeploymentRegion = args[7];
+ var mainDeploymentCluster = args[8];
+ var mainDeploymentTags = args[9];
var simDeploymentName = string.Empty;
var simDeploymentJson = string.Empty;
var simDeploymentRegion = string.Empty;
+ var simDeploymentCluster = string.Empty;
var simNumPlayers = 0;
if (launchSimPlayerDeployment)
{
- simDeploymentName = args[8];
- simDeploymentJson = args[9];
- simDeploymentRegion = args[10];
+ simDeploymentName = args[10];
+ simDeploymentJson = args[11];
+ simDeploymentRegion = args[12];
+ simDeploymentCluster = args[13];
- if (!Int32.TryParse(args[11], out simNumPlayers))
+ if (!Int32.TryParse(args[14], out simNumPlayers))
{
Console.WriteLine("Cannot parse the number of simulated players to connect.");
return 1;
@@ -139,83 +143,71 @@ private static int CreateDeployment(string[] args)
try
{
- var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(mainDeploymentRegion), GetPlatformRefreshTokenCredential(mainDeploymentRegion));
+ var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(useChinaPlatform), GetPlatformRefreshTokenCredential(useChinaPlatform));
if (DeploymentExists(deploymentServiceClient, projectName, mainDeploymentName))
{
StopDeploymentByName(deploymentServiceClient, projectName, mainDeploymentName);
}
- var createMainDeploymentOp = CreateMainDeploymentAsync(deploymentServiceClient, launchSimPlayerDeployment, projectName, assemblyName, runtimeVersion, mainDeploymentName, mainDeploymentJsonPath, mainDeploymentSnapshotPath, mainDeploymentRegion);
+ var createMainDeploymentOp = CreateMainDeploymentAsync(deploymentServiceClient,
+ launchSimPlayerDeployment, projectName, assemblyName, runtimeVersion,
+ mainDeploymentName, mainDeploymentJsonPath, mainDeploymentSnapshotPath,
+ mainDeploymentRegion, mainDeploymentCluster, mainDeploymentTags, useChinaPlatform);
- if (!launchSimPlayerDeployment)
- {
- // Don't launch a simulated player deployment. Wait for main deployment to be created and then return.
- Console.WriteLine("Waiting for deployment to be ready...");
- var result = createMainDeploymentOp.PollUntilCompleted().GetResultOrNull();
- if (result == null)
- {
- Console.WriteLine("Failed to create the main deployment");
- return 1;
- }
-
- Console.WriteLine("Successfully created the main deployment");
- return 0;
- }
-
- if (DeploymentExists(deploymentServiceClient, projectName, simDeploymentName))
+ if (launchSimPlayerDeployment && DeploymentExists(deploymentServiceClient, projectName, simDeploymentName))
{
StopDeploymentByName(deploymentServiceClient, projectName, simDeploymentName);
}
- var createSimDeploymentOp = CreateSimPlayerDeploymentAsync(deploymentServiceClient, projectName, assemblyName, runtimeVersion, mainDeploymentName, simDeploymentName, simDeploymentJson, simDeploymentRegion, simNumPlayers);
-
- // Wait for both deployments to be created.
- Console.WriteLine("Waiting for deployments to be ready...");
- var mainDeploymentResult = createMainDeploymentOp.PollUntilCompleted().GetResultOrNull();
- if (mainDeploymentResult == null)
+ // TODO: UNR-3550 - Re-add dynamic worker flags when supported with new runtime.
+ // We need to wait for the main deployment to be finished starting before we can launch the sim player deployment.
+ Console.WriteLine("Waiting for deployment to be ready...");
+ var result = createMainDeploymentOp.PollUntilCompleted().GetResultOrNull();
+ if (result == null)
{
Console.WriteLine("Failed to create the main deployment");
return 1;
}
Console.WriteLine("Successfully created the main deployment");
- var simPlayerDeployment = createSimDeploymentOp.PollUntilCompleted().GetResultOrNull();
- if (simPlayerDeployment == null)
+
+ if (launchSimPlayerDeployment)
{
- Console.WriteLine("Failed to create the simulated player deployment");
- return 1;
- }
+ // we are using the main deployment snapshot also for the sim player deployment, because we only need to specify a snapshot
+ // to be able to start the deployment. The sim players don't care about the actual snapshot.
+ var createSimDeploymentOp = CreateSimPlayerDeploymentAsync(deploymentServiceClient,
+ projectName, assemblyName, runtimeVersion, mainDeploymentName, simDeploymentName,
+ simDeploymentJson, mainDeploymentSnapshotPath, simDeploymentRegion, simDeploymentCluster,
+ simNumPlayers, useChinaPlatform);
- Console.WriteLine("Successfully created the simulated player deployment");
+ // Wait for both deployments to be created.
+ Console.WriteLine("Waiting for simulated player deployment to be ready...");
- // Update coordinator worker flag for simulated player deployment to notify target deployment is ready.
- simPlayerDeployment.WorkerFlags.Add(new WorkerFlag
- {
- Key = "target_deployment_ready",
- Value = "true",
- WorkerType = CoordinatorWorkerName
- });
- deploymentServiceClient.UpdateDeployment(new UpdateDeploymentRequest { Deployment = simPlayerDeployment });
+ var simPlayerDeployment = createSimDeploymentOp.PollUntilCompleted().GetResultOrNull();
+ if (simPlayerDeployment == null)
+ {
+ Console.WriteLine("Failed to create the simulated player deployment");
+ return 1;
+ }
- Console.WriteLine("Done! Simulated players will start to connect to your deployment");
+ Console.WriteLine("Done! Simulated players will start to connect to your deployment");
+ }
}
catch (Grpc.Core.RpcException e)
{
if (e.Status.StatusCode == Grpc.Core.StatusCode.NotFound)
{
- Console.WriteLine(
- $"Unable to launch the deployment(s). This is likely because the project '{projectName}' or assembly '{assemblyName}' doesn't exist.");
+ Console.WriteLine($"Unable to launch the deployment(s). This is likely because the project '{projectName}' or assembly '{assemblyName}' doesn't exist.");
+ Console.WriteLine($"Detail: '{e.Status.Detail}'");
}
else if (e.Status.StatusCode == Grpc.Core.StatusCode.ResourceExhausted)
{
- Console.WriteLine(
- $"Unable to launch the deployment(s). Cloud cluster resources exhausted, Detail: '{e.Status.Detail}'" );
+ Console.WriteLine($"Unable to launch the deployment(s). Cloud cluster resources exhausted, Detail: '{e.Status.Detail}'" );
}
else
{
- Console.WriteLine(
- $"Unable to launch the deployment(s). Detail: '{e.Status.Detail}'");
+ Console.WriteLine($"Unable to launch the deployment(s). Detail: '{e.Status.Detail}'");
}
return 1;
@@ -224,7 +216,7 @@ private static int CreateDeployment(string[] args)
return 0;
}
- private static int CreateSimDeployments(string[] args)
+ private static int CreateSimDeployments(string[] args, bool useChinaPlatform)
{
var projectName = args[1];
var assemblyName = args[2];
@@ -233,31 +225,28 @@ private static int CreateSimDeployments(string[] args)
var simDeploymentName = args[5];
var simDeploymentJson = args[6];
var simDeploymentRegion = args[7];
+ var simDeploymentCluster = args[8];
+ var simDeploymentSnapshotPath = args[9];
var simNumPlayers = 0;
- if (!Int32.TryParse(args[8], out simNumPlayers))
+ if (!Int32.TryParse(args[10], out simNumPlayers))
{
Console.WriteLine("Cannot parse the number of simulated players to connect.");
return 1;
}
- var autoConnect = false;
- if (!Boolean.TryParse(args[9], out autoConnect))
- {
- Console.WriteLine("Cannot parse the auto-connect flag.");
- return 1;
- }
-
try
{
- var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(simDeploymentRegion));
+ var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(useChinaPlatform));
if (DeploymentExists(deploymentServiceClient, projectName, simDeploymentName))
{
StopDeploymentByName(deploymentServiceClient, projectName, simDeploymentName);
}
- var createSimDeploymentOp = CreateSimPlayerDeploymentAsync(deploymentServiceClient, projectName, assemblyName, runtimeVersion, targetDeploymentName, simDeploymentName, simDeploymentJson, simDeploymentRegion, simNumPlayers);
+ var createSimDeploymentOp = CreateSimPlayerDeploymentAsync(deploymentServiceClient,
+ projectName, assemblyName, runtimeVersion, targetDeploymentName, simDeploymentName,
+ simDeploymentJson, simDeploymentSnapshotPath, simDeploymentRegion, simDeploymentCluster, simNumPlayers, useChinaPlatform);
// Wait for both deployments to be created.
Console.WriteLine("Waiting for the simulated player deployment to be ready...");
@@ -268,17 +257,6 @@ private static int CreateSimDeployments(string[] args)
return 1;
}
- Console.WriteLine("Successfully created the simulated player deployment");
-
- // Update coordinator worker flag for simulated player deployment to notify target deployment is ready.
- simPlayerDeployment.WorkerFlags.Add(new WorkerFlag
- {
- Key = "target_deployment_ready",
- Value = autoConnect.ToString(),
- WorkerType = CoordinatorWorkerName
- });
- deploymentServiceClient.UpdateDeployment(new UpdateDeploymentRequest { Deployment = simPlayerDeployment });
-
Console.WriteLine("Done! Simulated players will start to connect to your deployment");
}
catch (Grpc.Core.RpcException e)
@@ -329,13 +307,15 @@ private static void StopDeploymentByName(DeploymentServiceClient deploymentServi
}
private static Operation CreateMainDeploymentAsync(DeploymentServiceClient deploymentServiceClient,
- bool launchSimPlayerDeployment, string projectName, string assemblyName, string runtimeVersion, string mainDeploymentName, string mainDeploymentJsonPath, string mainDeploymentSnapshotPath, string regionCode)
+ bool launchSimPlayerDeployment, string projectName, string assemblyName, string runtimeVersion,
+ string mainDeploymentName, string mainDeploymentJsonPath, string mainDeploymentSnapshotPath,
+ string regionCode, string clusterCode, string deploymentTags, bool useChinaPlatform)
{
- var snapshotServiceClient = SnapshotServiceClient.Create(GetApiEndpoint(regionCode), GetPlatformRefreshTokenCredential(regionCode));
+ var snapshotServiceClient = SnapshotServiceClient.Create(GetApiEndpoint(useChinaPlatform), GetPlatformRefreshTokenCredential(useChinaPlatform));
// Upload snapshots.
var mainSnapshotId = UploadSnapshot(snapshotServiceClient, mainDeploymentSnapshotPath, projectName,
- mainDeploymentName, regionCode);
+ mainDeploymentName, useChinaPlatform);
if (mainSnapshotId.Length == 0)
{
@@ -353,11 +333,26 @@ private static Operation CreateMainDeploym
Name = mainDeploymentName,
ProjectName = projectName,
StartingSnapshotId = mainSnapshotId,
- RegionCode = regionCode,
RuntimeVersion = runtimeVersion
};
+ if (!String.IsNullOrEmpty(clusterCode))
+ {
+ mainDeploymentConfig.ClusterCode = clusterCode;
+ }
+ else
+ {
+ mainDeploymentConfig.RegionCode = regionCode;
+ }
+
mainDeploymentConfig.Tag.Add(DEPLOYMENT_LAUNCHED_BY_LAUNCHER_TAG);
+ foreach (String tag in deploymentTags.Split(' '))
+ {
+ if (tag.Length > 0)
+ {
+ mainDeploymentConfig.Tag.Add(tag);
+ }
+ }
if (launchSimPlayerDeployment)
{
@@ -378,9 +373,21 @@ private static Operation CreateMainDeploym
}
private static Operation CreateSimPlayerDeploymentAsync(DeploymentServiceClient deploymentServiceClient,
- string projectName, string assemblyName, string runtimeVersion, string mainDeploymentName, string simDeploymentName, string simDeploymentJsonPath, string regionCode, int simNumPlayers)
+ string projectName, string assemblyName, string runtimeVersion, string mainDeploymentName, string simDeploymentName,
+ string simDeploymentJsonPath, string simDeploymentSnapshotPath, string regionCode, string clusterCode, int simNumPlayers, bool useChinaPlatform)
{
- var playerAuthServiceClient = PlayerAuthServiceClient.Create(GetApiEndpoint(regionCode), GetPlatformRefreshTokenCredential(regionCode));
+ var snapshotServiceClient = SnapshotServiceClient.Create(GetApiEndpoint(useChinaPlatform), GetPlatformRefreshTokenCredential(useChinaPlatform));
+
+ // Upload snapshots.
+ var simDeploymentSnapshotId = UploadSnapshot(snapshotServiceClient, simDeploymentSnapshotPath, projectName,
+ simDeploymentName, useChinaPlatform);
+
+ if (simDeploymentSnapshotId.Length == 0)
+ {
+ throw new Exception("Error while uploading sim player snapshot.");
+ }
+
+ var playerAuthServiceClient = PlayerAuthServiceClient.Create(GetApiEndpoint(useChinaPlatform), GetPlatformRefreshTokenCredential(useChinaPlatform));
// Create development authentication token used by the simulated players.
var dat = playerAuthServiceClient.CreateDevelopmentAuthenticationToken(
@@ -488,11 +495,19 @@ private static Operation CreateSimPlayerDe
},
Name = simDeploymentName,
ProjectName = projectName,
- RegionCode = regionCode,
- RuntimeVersion = runtimeVersion
- // No snapshot included for the simulated player deployment
+ RuntimeVersion = runtimeVersion,
+ StartingSnapshotId = simDeploymentSnapshotId,
};
+ if (!String.IsNullOrEmpty(clusterCode))
+ {
+ simDeploymentConfig.ClusterCode = clusterCode;
+ }
+ else
+ {
+ simDeploymentConfig.RegionCode = regionCode;
+ }
+
simDeploymentConfig.Tag.Add(DEPLOYMENT_LAUNCHED_BY_LAUNCHER_TAG);
simDeploymentConfig.Tag.Add(SIM_PLAYER_DEPLOYMENT_TAG);
@@ -508,17 +523,16 @@ private static Operation CreateSimPlayerDe
}
- private static int StopDeployments(string[] args)
+ private static int StopDeployments(string[] args, bool useChinaPlatform)
{
var projectName = args[1];
- var regionCode = args[2];
- var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(regionCode), GetPlatformRefreshTokenCredential(regionCode));
+ var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(useChinaPlatform), GetPlatformRefreshTokenCredential(useChinaPlatform));
- if (args.Length == 4)
+ if (args.Length == 3)
{
// Stop only the specified deployment.
- var deploymentId = args[3];
+ var deploymentId = args[2];
StopDeploymentById(deploymentServiceClient, projectName, deploymentId);
return 0;
@@ -560,12 +574,11 @@ private static void StopDeploymentById(DeploymentServiceClient client, string pr
}
}
- private static int ListDeployments(string[] args)
+ private static int ListDeployments(string[] args, bool useChinaPlatform)
{
var projectName = args[1];
- var regionCode = args[2];
- var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(regionCode), GetPlatformRefreshTokenCredential(regionCode));
+ var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(useChinaPlatform), GetPlatformRefreshTokenCredential(useChinaPlatform));
var activeDeployments = ListLaunchedActiveDeployments(deploymentServiceClient, projectName);
foreach (var deployment in activeDeployments)
@@ -618,24 +631,33 @@ private static IEnumerable ListLaunchedActiveDeployments(DeploymentS
private static void ShowUsage()
{
Console.WriteLine("Usage:");
- Console.WriteLine("DeploymentLauncher create [ ]");
+ Console.WriteLine("DeploymentLauncher create [ ]");
Console.WriteLine($" Starts a cloud deployment, with optionally a simulated player deployment. The deployments can be started in different regions ('EU', 'US', 'AP' and 'CN').");
- Console.WriteLine("DeploymentLauncher createsim ");
+ Console.WriteLine("DeploymentLauncher createsim ");
Console.WriteLine($" Starts a simulated player deployment. Can be started in a different region from the target deployment ('EU', 'US', 'AP' and 'CN').");
- Console.WriteLine("DeploymentLauncher stop [deployment-id]");
+ Console.WriteLine("DeploymentLauncher stop [deployment-id]");
Console.WriteLine(" Stops the specified deployment within the project.");
Console.WriteLine(" If no deployment id argument is specified, all active deployments started by the deployment launcher in the project will be stopped.");
- Console.WriteLine("DeploymentLauncher list ");
+ Console.WriteLine("DeploymentLauncher list ");
Console.WriteLine(" Lists all active deployments within the specified project that are started by the deployment launcher.");
+ Console.WriteLine();
+ Console.WriteLine("Flags:");
+ Console.WriteLine(" --china Use China platform endpoints.");
}
private static int Main(string[] args)
{
+ // Filter flags from the rest of the arguments.
+ string[] flags = args.Where(arg => arg.StartsWith("--")).Select(arg => arg.ToLowerInvariant()).ToArray();
+ args = args.Where(arg => !arg.StartsWith("--")).ToArray();
+
+ bool useChinaPlatform = flags.Contains("--china");
+
if (args.Length == 0 ||
- (args[0] == "create" && (args.Length != 12 && args.Length != 8)) ||
- (args[0] == "createsim" && args.Length != 10) ||
- (args[0] == "stop" && (args.Length != 3 && args.Length != 4)) ||
- (args[0] == "list" && args.Length != 3))
+ (args[0] == "create" && (args.Length != 15 && args.Length != 10)) ||
+ (args[0] == "createsim" && args.Length != 11) ||
+ (args[0] == "stop" && (args.Length != 2 && args.Length != 3)) ||
+ (args[0] == "list" && args.Length != 2))
{
ShowUsage();
return 1;
@@ -645,22 +667,22 @@ private static int Main(string[] args)
{
if (args[0] == "create")
{
- return CreateDeployment(args);
+ return CreateDeployment(args, useChinaPlatform);
}
if (args[0] == "createsim")
{
- return CreateSimDeployments(args);
+ return CreateSimDeployments(args, useChinaPlatform);
}
if (args[0] == "stop")
{
- return StopDeployments(args);
+ return StopDeployments(args, useChinaPlatform);
}
if (args[0] == "list")
{
- return ListDeployments(args);
+ return ListDeployments(args, useChinaPlatform);
}
ShowUsage();
diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs
index 3e69e42ac9..edbfcd7d2e 100644
--- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs
+++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs
@@ -23,7 +23,6 @@ internal class ManagedWorkerCoordinator : AbstractWorkerCoordinator
private const string DevAuthTokenWorkerFlag = "simulated_players_dev_auth_token";
private const string TargetDeploymentWorkerFlag = "simulated_players_target_deployment";
private const string DeploymentTotalNumSimulatedPlayersWorkerFlag = "total_num_simulated_players";
- private const string TargetDeploymentReadyWorkerFlag = "target_deployment_ready";
private const int AverageDelayMillisBetweenConnections = 1500;
private const int PollTargetDeploymentReadyIntervalMillis = 5000;
@@ -122,10 +121,7 @@ public override void Run()
Option targetDeploymentOpt = connection.GetWorkerFlag(TargetDeploymentWorkerFlag);
int deploymentTotalNumSimulatedPlayers = int.Parse(GetWorkerFlagOrDefault(connection, DeploymentTotalNumSimulatedPlayersWorkerFlag, "100"));
- Logger.WriteLog("Waiting for target deployment to become ready.");
- WaitForTargetDeploymentReady(connection);
-
- Logger.WriteLog($"Target deployment is ready. Starting {NumSimulatedPlayersToStart} simulated players.");
+ Logger.WriteLog($"Starting {NumSimulatedPlayersToStart} simulated players.");
Thread.Sleep(InitialStartDelayMillis);
var maxDelayMillis = deploymentTotalNumSimulatedPlayers * AverageDelayMillisBetweenConnections;
@@ -201,20 +197,5 @@ private static string GetWorkerFlagOrDefault(Connection connection, string flagN
return defaultValue;
}
-
- private void WaitForTargetDeploymentReady(Connection connection)
- {
- while (true)
- {
- var readyFlagOpt = connection.GetWorkerFlag(TargetDeploymentReadyWorkerFlag);
- if (readyFlagOpt == "true")
- {
- // Ready.
- break;
- }
-
- Thread.Sleep(PollTargetDeploymentReadyIntervalMillis);
- }
- }
}
}
diff --git a/SpatialGDK/Content/SpatialDebugger/BP_SpatialDebugger.uasset b/SpatialGDK/Content/SpatialDebugger/BP_SpatialDebugger.uasset
index aad9ed3100..daf8c9b2b8 100644
Binary files a/SpatialGDK/Content/SpatialDebugger/BP_SpatialDebugger.uasset and b/SpatialGDK/Content/SpatialDebugger/BP_SpatialDebugger.uasset differ
diff --git a/SpatialGDK/Extras/core-sdk.version b/SpatialGDK/Extras/core-sdk.version
index d18453764c..e4ce12fcfb 100644
--- a/SpatialGDK/Extras/core-sdk.version
+++ b/SpatialGDK/Extras/core-sdk.version
@@ -1 +1 @@
-14.5.0
+14.6.1
diff --git a/SpatialGDK/Extras/internal-documentation/release-process.md b/SpatialGDK/Extras/internal-documentation/release-process.md
index 113e226af3..8d2cf928df 100644
--- a/SpatialGDK/Extras/internal-documentation/release-process.md
+++ b/SpatialGDK/Extras/internal-documentation/release-process.md
@@ -2,151 +2,33 @@
This document outlines the process for releasing a version of the GDK for Unreal and all associated projects.
-> This process will need to be reviewed once we hit beta.
-
## Terminology
-* **Release version** is the version of the SpatialOS GDK for Unreal that you are releasing by performing the steps in this document.
-* **Previous version** is the latest version of the SpatialOS GDK for Unreal that is currently released to customers. You can find out what this version is [here](https://github.com/spatialos/UnrealGDK/releases).
-* `` - The directory that contains your project’s .uproject file and Source folder.
-* `` - The directory that contains your `` directory.
-* `` - The name of your project and .uproject file (for example, `\\YourProject.uproject`).
-
-## Validation pre-requisites
-
-The following entry criteria must be met before you start the validation steps:
-
-### Ensure Xbox DLLs exist
-
-To check that Xbox-compatible Worker SDK DLLs are available.
-
-1. Identify the Worker SDK version pinned by the GDK. To do this, check [core-sdk.version](https://github.com/spatialos/UnrealGDK/blob/master/SpatialGDK/Extras/core-sdk.version) on `master`.
-1. Identify the XDK version(s) officially supported in all UE4 versions that the GDK version you're about to release supports. To do this:
- 1. Google search for the release notes for all UE4 versions that the GDK version you're about to release supports (for example [4.22 Release Notes](https://docs.unrealengine.com/en-US/Support/Builds/ReleaseNotes/4_22/index.html))
- 1. Search in page (ctrl-f) for `XDK` and note down the supported version (for example, the [4.22 Release Notes](https://docs.unrealengine.com/en-US/Support/Builds/ReleaseNotes/4_22/index.html) reveals that it supports `XDK: July 2018 QFE-4`).
- 1. Convert `XDK: July 2018 QFE-4` to the format that `spatial package get` expects. This is **year** **month** **QFE Version**. For example `XDK: July 2018 QFE-4` converts to `180704`.
- 1. Using the information you just ascertained, fill in the `<...>` blanks in the below command and run it via command line:
-`spatial package get worker_sdk c-dynamic-x86_64--xbone c-sdk-.zip`
-A correct command looks something like this:
-`spatial package get worker_sdk c-dynamic-x86_64-xdk180401-xbone 13.7.1 c-sdk-13.7.1-180401.zip`
-If it succeeds it will download a DLL.
-If it fails because the DLL is not available, file a WRK ticket for the Worker team to generate the required DLL(s). See [WRK-1676](https://improbableio.atlassian.net/browse/WRK-1676) for an example.
-
-### Create the `UnrealGDK` 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 docs that should be resolved prior to commencement of the release process.
-1. `git clone` the [UnrealGDK](https://github.com/spatialos/UnrealGDK).
-1. `git checkout master`
-1. `git pull`
-1. Using `git log`, take note of the latest commit hash.
-1. `git checkout -b x.y.z-rc` in order to create release candidate branch.
-1. Open `CHANGELOG.md`, which is in the root of the repository.
-1. Read **every** release note in the `Unreleased` section. Ensure that they make sense, they conform to [how-to-write-good-release-notes.md](https://github.com/spatialos/UnrealGDK/blob/master/SpatialGDK/Extras/internal-documentation/how-to-write-good-release-notes.md) structure.
-1. Compare `master` to `release` using the GitHub UI and ensure that every change that requires a release note has one.
-1. Enter the release version and planned date of release in a `##` block. Move the `Unreleased` section above this.
- - Look at the previous release versions in the changelog to see how this should be done.
-1. Commit your changes to `CHANGELOG.md`.
-1. Open `SpatialGDK/SpatialGDK.uplugin`.
-1. Increment the `VersionName` and `Version`.
-1. Commit your changes to `SpatialGDK/SpatialGDK.uplugin`.
-1. `git push --set-upstream origin x.y.z-rc` to push the branch.
-1. Announce the branch and the commit hash it uses in the `#unreal-gdk-release` channel.
-
-### Create the `improbableio/UnrealEngine` release candidate
-1. `git clone` the [improbableio/UnrealEngine](https://github.com/improbableio/UnrealEngine).
-1. `git checkout 4.xx-SpatialOSUnrealGDK`
-1. `git pull`
-1. Using `git log`, take note of the latest commit hash.
-1. `git checkout -b 4.xx-SpatialOSUnrealGDK-x.y.z-rc` in order to create release candidate branch.
-1. `git push --set-upstream origin 4.xx-SpatialOSUnrealGDK-x.y.z-rc` to push the branch.
-1. Repeat the above steps for all supported `4.xx` engine versions.
-1. Announce the branch and the commit hash it uses in the `#unreal-gdk-release` channel.
-1. Make sure to update UnrealGDKExampleProjectVersion.txt and UnrealGDKVersion.txt so that they contain the relevant release tag for the UnrealGDK and UnrealGDKExampleProject.
-
-### Create the `UnrealGDKExampleProject` release candidate
-1. `git clone` the [UnrealGDKExampleProject](https://github.com/spatialos/UnrealGDKExampleProject).
-1. `git checkout master`
-1. `git pull`
-1. Using `git log`, take note of the latest commit hash.
-1. `git checkout -b x.y.z-rc` in order to create release candidate branch.
-1. `git push --set-upstream origin x.y.z-rc` to push the branch.
-1. Announce the branch and the commit hash it uses in the #unreal-gdk-release channel.
-
-## Build your release candidate engine
-1. Open https://documentation.improbable.io/gdk-for-unreal/docs/get-started-1-get-the-dependencies.
-1. Uninstall all dependencies listed on this page so that you can accurately validate our installation steps.
-1. If you have one, delete your local clone of `UnrealEngine`.
-1. Follow the installation steps on https://documentation.improbable.io/gdk-for-unreal/docs/get-started-1-get-the-dependencies.
-1. When you clone the `UnrealEngine`, be sure to checkout `x.y.z-rc-x` so you're building the release version.
-
-## Implementing fixes
-
-If at any point in the below validation steps you encounter a blocker, you must fix that defect prior to releasing.
-
-The workflow for this is:
-
-1. Raise a bug ticket in JIRA detailing the blocker.
-1. `git checkout x.y.z-rc`
-1. `git pull`
-1. `git checkout -b bugfix/UNR-xxx`
-1. Fix the defect.
-1. `git commit`, `git push -u origin HEAD`, target your PR at `x.y.z-rc`.
-1. When the PR is merged, `git checkout x.y.z-rc`, `git pull` and re-test the defect to ensure you fixed it.
-1. Notify #unreal-gdk-release that the release candidate has been updated.
-1. **Judgment call**: If the fix was isolated, continue the validation steps from where you left off. If the fix was significant, restart testing from scratch. Consult the rest of the team if you are unsure which to choose.
-
-## Validation (GDK, Starter Template and Example Project)
-You must perform these steps twice, once in the EU region and once in CN.
-
-1. Open the [Component Release](https://improbabletest.testrail.io/index.php?/suites/view/72) test suite and click run test.
-1. Name your test run in this format: Component release: GDK [UnrealGDK version], UE (Unreal Engine version), [region].
-1. Execute the test runs.
-
-## Validation (Docs)
-1. @techwriters in [#docs](https://improbable.slack.com/archives/C0TBQAB5X) and ask them what's changes in the docs since the last release.
-1. Proof read the pages that have changed.
-1. Spend an additional 20 minutes reading the docs and ensuring that nothing is incorrect.
+* **GDK release version** is the version of the SpatialOS GDK for Unreal that you are releasing by performing the steps in this document. It's [semantically versioned](https://semver.org/) and looks like `x.y.z`.
+* **Previous GDK version** is the version of the SpatialOS GDK for Unreal that is currently at HEAD of the `release` branch. You can find out what this version is [here](https://github.com/spatialos/UnrealGDK/releases).
## Release
-
-All of the above tests **must** have passed and there must be no outstanding blocking issues before you start this, the release phase.
-
-The order of `git merge` operations in all UnrealGDK related repositories is:
-`release candidate` > `preview` > `release` > `master`
-
-If you want to soak test this release on the `preview` branch before promoting it to the `release` branch, only execute the steps that merge into `preview` and `master`.
-
-1. When merging the following PRs, you need to enable `Allow merge commits` option on the repos and choose `Create a merge commit` from the dropdown in the pull request UI to merge the branch, then disable `Allow merge commits` option on the repos once the release process is complete. You need to be an admin to perform this.
-
-**UnrealGDK**
-1. In `UnrealGDK`, merge `x.y.z-rc` into `preview`.
-1. If you want to soak test this release on the `preview`, use the [GitHub Release UI](https://github.com/spatialos/UnrealGDK/releases) to tag the commit you just made to `preview` as `x.y.z-preview`.
-Copy the latest release notes from `CHANGELOG.md` and paste them into the release description field.
-1. In `UnrealGDK`, merge `preview` into `release`.
-1. Use the [GitHub Release UI](https://github.com/spatialos/UnrealGDK/releases) to tag the commit you just made to `release` as `x.y.z`.
-Copy the latest release notes from `CHANGELOG.md` and paste them into the release description field.
-1. In `UnrealGDK`, merge `release` into `master`. This merge could have conflicts. Don't hesitate to ask for help resolving these if you are unsure.
-
-**improbableio/UnrealEngine**
-1. In `improbableio/UnrealEngine`, merge `4.xx-SpatialOSUnrealGDK-x.y.z-rc` into `4.xx-SpatialOSUnrealGDK-preview`.
-1. If you want to soak test this release on the `preview`, use the [GitHub Release UI](https://github.com/improbableio/UnrealEngine/releases) to tag the commit you just made to `4.xx-SpatialOSUnrealGDK-preview` as `4.xx-SpatialOSUnrealGDK-x.y.z-preview`.
-Copy the latest release notes from `CHANGELOG.md` and paste them into the release description field.
-1. In `improbableio/UnrealEngine`, merge `4.xx-SpatialOSUnrealGDK-preview` into `4.xx-SpatialOSUnrealGDK-release`.
-1. Use the [GitHub Release UI](https://github.com/improbableio/UnrealEngine/releases) to tag the commit you just made to `release` as `4.xx-SpatialOSUnrealGDK-x.y`.
-Copy the latest release notes from `CHANGELOG.md` and paste them into the release description field.
-1. In `improbableio/UnrealEngine`, merge `4.xx-SpatialOSUnrealGDK-release` into `master`. This merge could have conflicts. Don't hesitate to ask for help resolving these if you are unsure.
-
-**UnrealGDKExampleProject**
-1. In `UnrealGDKExampleProject`, merge `x.y.z-rc` into `preview`.
-1. If you want to soak test this release on the `preview`, use the [GitHub Release UI](https://github.com/spatialos/UnrealGDKExampleProject/releases) to tag the commit you just made to `preview` as `x.y.z-preview`.
-Copy the latest release notes from `CHANGELOG.md` and paste them into the release description field.
-1. In `UnrealGDKExampleProject`, merge `preview` into `release`.
-1. Use the [GitHub Release UI](https://github.com/spatialos/UnrealGDKExampleProject/releases) to tag the commit you just made to `release` as `x.y.z`.
-Copy the latest release notes from `CHANGELOG.md` and paste them into the release description field.
-1. In `UnrealGDK`, merge `release` into `master`.
-
-**Documentation**
+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. 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]".
+1. The "Commit" field is prepopulated with `HEAD`, leave it as is.
+1. The "Branch" field is prepopulated with `master`. Leave it as is. This determines which version of the unrealgdk-release pipeline is run.
+1. Select "Create Build".
+1. Wait about 20 seconds for `imp-ci steps run --from .buildkite/release.steps.yaml` to pass and then select "Configure your release."
+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. 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. 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.
+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):
+"Release Successful!"
+"Release hash: [hash]"
+"Draft release: [url]"
+1. Open every draft release link and click publish on each one.
1. Notify @techwriters in [#docs](https://improbable.slack.com/archives/C0TBQAB5X) that they may publish the new version of the docs.
-
-**Announce**
1. Announce the release in:
* Forums
@@ -158,19 +40,23 @@ Congratulations, you've completed the release process!
## Clean up
-1. Delete all `rc` branches.
+1. Delete all `-rc` branches.
## Appendix
Forum Post Template
- We are happy to announce the release of version x.y.z of the SpatialOS GDK for Unreal.
+ We are happy to announce the release of version [GDK release version] of the SpatialOS GDK for Unreal.
Please see the full release notes on GitHub:
Unreal GDK - https://github.com/spatialos/UnrealGDK/releases/tag/x.y.z
-Corresponding fork of Unreal Engine - https://github.com/improbableio/UnrealEngine/releases/tag/4.xx-SpatialOSUnrealGDK-x.y.z
+
+Corresponding Unreal Engine versions:
+- https://github.com/improbableio/UnrealEngine/releases/tag/4.xx-SpatialOSUnrealGDK-x.y.z
+- https://github.com/improbableio/UnrealEngine/releases/tag/4.xx-SpatialOSUnrealGDK-x.y.z
+
Corresponding version of the Example Project - https://github.com/spatialos/UnrealGDKExampleProject/releases/tag/x.y.z
diff --git a/SpatialGDK/Extras/schema/core_types.schema b/SpatialGDK/Extras/schema/core_types.schema
index d1629f67fa..eda04e2659 100644
--- a/SpatialGDK/Extras/schema/core_types.schema
+++ b/SpatialGDK/Extras/schema/core_types.schema
@@ -4,6 +4,9 @@ package unreal;
type Void {
}
+// Below, option property types are used for boolean behaviour where an empty
+// boolean indicates false. This is cheaper for having that property set to false
+// because empty options types involve no idea being sent across the network.
type UnrealObjectRef {
EntityId entity = 1;
uint32 offset = 2;
@@ -12,8 +15,10 @@ type UnrealObjectRef {
// a reference, e.g. anything inside streaming levels should not be loaded.
option no_load_on_client = 4;
option outer = 5;
- // Singleton objects can be referred to by their class path in the case of an
+ // Actors such as the game state, game mode and level script Actors (formerly
+ // known as Singletons) can be referred to by their class path in the case of an
// authoritative server that hasn't checked the singleton entity yet. This bool
- // will differentiate that from their class pointer.
- option use_singleton_class_path = 6;
+ // will indicate that non-auth servers should try to load such Actors from
+ // their package map.
+ option use_class_path_to_load_object = 6;
}
diff --git a/SpatialGDK/Extras/schema/global_state_manager.schema b/SpatialGDK/Extras/schema/global_state_manager.schema
index 5a0e2230ca..be49cf0e8d 100644
--- a/SpatialGDK/Extras/schema/global_state_manager.schema
+++ b/SpatialGDK/Extras/schema/global_state_manager.schema
@@ -10,11 +10,6 @@ type ShutdownMultiProcessResponse {
type ShutdownAdditionalServersEvent {
}
-component SingletonManager {
- id = 9995;
- map singleton_name_to_entity_id = 1;
-}
-
component DeploymentMap {
id = 9994;
string map_url = 1;
diff --git a/SpatialGDK/Extras/schema/singleton.schema b/SpatialGDK/Extras/schema/singleton.schema
deleted file mode 100644
index 30df1c47ea..0000000000
--- a/SpatialGDK/Extras/schema/singleton.schema
+++ /dev/null
@@ -1,6 +0,0 @@
-// Copyright (c) Improbable Worlds Ltd, All Rights Reserved
-package unreal;
-
-component Singleton {
- id = 9997;
-}
diff --git a/SpatialGDK/Resources/Cloud.png b/SpatialGDK/Resources/Cloud.png
index 7e69932be7..0b779f2055 100644
Binary files a/SpatialGDK/Resources/Cloud.png and b/SpatialGDK/Resources/Cloud.png differ
diff --git a/SpatialGDK/Resources/Cloud@0.5x.png b/SpatialGDK/Resources/Cloud@0.5x.png
index 8913079aa8..e9d7babfec 100644
Binary files a/SpatialGDK/Resources/Cloud@0.5x.png and b/SpatialGDK/Resources/Cloud@0.5x.png differ
diff --git a/SpatialGDK/Resources/Inspector.png b/SpatialGDK/Resources/Inspector.png
index 91abd3067b..85208f68be 100644
Binary files a/SpatialGDK/Resources/Inspector.png and b/SpatialGDK/Resources/Inspector.png differ
diff --git a/SpatialGDK/Resources/Inspector@0.5x.png b/SpatialGDK/Resources/Inspector@0.5x.png
index 4cd0722bcb..2d28f93bba 100644
Binary files a/SpatialGDK/Resources/Inspector@0.5x.png and b/SpatialGDK/Resources/Inspector@0.5x.png differ
diff --git a/SpatialGDK/Resources/Launch.png b/SpatialGDK/Resources/Launch.png
deleted file mode 100644
index b367ea269f..0000000000
Binary files a/SpatialGDK/Resources/Launch.png and /dev/null differ
diff --git a/SpatialGDK/Resources/Launch@0.5x.png b/SpatialGDK/Resources/Launch@0.5x.png
deleted file mode 100644
index 46ecd5831e..0000000000
Binary files a/SpatialGDK/Resources/Launch@0.5x.png and /dev/null differ
diff --git a/SpatialGDK/Resources/None.png b/SpatialGDK/Resources/None.png
new file mode 100644
index 0000000000..a433b029c8
Binary files /dev/null and b/SpatialGDK/Resources/None.png differ
diff --git a/SpatialGDK/Resources/None@0.5x.png b/SpatialGDK/Resources/None@0.5x.png
new file mode 100644
index 0000000000..fb3c907df5
Binary files /dev/null and b/SpatialGDK/Resources/None@0.5x.png differ
diff --git a/SpatialGDK/Resources/Schema.png b/SpatialGDK/Resources/Schema.png
index 9ed71d0901..96fbfad8c6 100644
Binary files a/SpatialGDK/Resources/Schema.png and b/SpatialGDK/Resources/Schema.png differ
diff --git a/SpatialGDK/Resources/Schema@0.5x.png b/SpatialGDK/Resources/Schema@0.5x.png
index e83b8fd399..ff6903d5f5 100644
Binary files a/SpatialGDK/Resources/Schema@0.5x.png and b/SpatialGDK/Resources/Schema@0.5x.png differ
diff --git a/SpatialGDK/Resources/Snapshot.png b/SpatialGDK/Resources/Snapshot.png
index 9541498f0b..df581e920e 100644
Binary files a/SpatialGDK/Resources/Snapshot.png and b/SpatialGDK/Resources/Snapshot.png differ
diff --git a/SpatialGDK/Resources/Snapshot@0.5x.png b/SpatialGDK/Resources/Snapshot@0.5x.png
index ecd3b87047..019a0d4d79 100644
Binary files a/SpatialGDK/Resources/Snapshot@0.5x.png and b/SpatialGDK/Resources/Snapshot@0.5x.png differ
diff --git a/SpatialGDK/Resources/StartCloud.png b/SpatialGDK/Resources/StartCloud.png
new file mode 100644
index 0000000000..4dd8b28aba
Binary files /dev/null and b/SpatialGDK/Resources/StartCloud.png differ
diff --git a/SpatialGDK/Resources/StartCloud@0.5x.png b/SpatialGDK/Resources/StartCloud@0.5x.png
new file mode 100644
index 0000000000..e82c43b380
Binary files /dev/null and b/SpatialGDK/Resources/StartCloud@0.5x.png differ
diff --git a/SpatialGDK/Resources/StartLocal.png b/SpatialGDK/Resources/StartLocal.png
new file mode 100644
index 0000000000..4701d32284
Binary files /dev/null and b/SpatialGDK/Resources/StartLocal.png differ
diff --git a/SpatialGDK/Resources/StartLocal@0.5x.png b/SpatialGDK/Resources/StartLocal@0.5x.png
new file mode 100644
index 0000000000..475327f243
Binary files /dev/null and b/SpatialGDK/Resources/StartLocal@0.5x.png differ
diff --git a/SpatialGDK/Resources/Stop.png b/SpatialGDK/Resources/Stop.png
deleted file mode 100644
index dfdf5fb6ef..0000000000
Binary files a/SpatialGDK/Resources/Stop.png and /dev/null differ
diff --git a/SpatialGDK/Resources/Stop@0.5x.png b/SpatialGDK/Resources/Stop@0.5x.png
deleted file mode 100644
index 06f0327559..0000000000
Binary files a/SpatialGDK/Resources/Stop@0.5x.png and /dev/null differ
diff --git a/SpatialGDK/Resources/StopCloud.png b/SpatialGDK/Resources/StopCloud.png
new file mode 100644
index 0000000000..de4978ea79
Binary files /dev/null and b/SpatialGDK/Resources/StopCloud.png differ
diff --git a/SpatialGDK/Resources/StopCloud@0.5x.png b/SpatialGDK/Resources/StopCloud@0.5x.png
new file mode 100644
index 0000000000..bd40858ef0
Binary files /dev/null and b/SpatialGDK/Resources/StopCloud@0.5x.png differ
diff --git a/SpatialGDK/Resources/StopLocal.png b/SpatialGDK/Resources/StopLocal.png
new file mode 100644
index 0000000000..900fd7225b
Binary files /dev/null and b/SpatialGDK/Resources/StopLocal.png differ
diff --git a/SpatialGDK/Resources/StopLocal@0.5x.png b/SpatialGDK/Resources/StopLocal@0.5x.png
new file mode 100644
index 0000000000..84f28e69d9
Binary files /dev/null and b/SpatialGDK/Resources/StopLocal@0.5x.png differ
diff --git a/SpatialGDK/Resources/Upload.png b/SpatialGDK/Resources/Upload.png
deleted file mode 100644
index 7e69932be7..0000000000
Binary files a/SpatialGDK/Resources/Upload.png and /dev/null differ
diff --git a/SpatialGDK/Resources/Upload@0.5x .png b/SpatialGDK/Resources/Upload@0.5x .png
deleted file mode 100644
index 9a0391f2a1..0000000000
Binary files a/SpatialGDK/Resources/Upload@0.5x .png and /dev/null differ
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/SpatialPingComponent.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/SpatialPingComponent.cpp
index 399fece084..6c56cd8408 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/SpatialPingComponent.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/SpatialPingComponent.cpp
@@ -196,6 +196,11 @@ FSpatialPingAverageData USpatialPingComponent::GetAverageData() const
Data.LastMeasurementsWindowAvg = Total / LastPingMeasurements.Num();
Data.LastMeasurementsWindowMin = FMath::Min(LastPingMeasurements);
Data.LastMeasurementsWindowMax = FMath::Max(LastPingMeasurements);
+
+ TArray Sorted = LastPingMeasurements;
+ Sorted.Sort();
+ Data.LastMeasurementsWindow50thPercentile = Sorted[static_cast(Sorted.Num() * 0.5f)];
+ Data.LastMeasurementsWindow90thPercentile = Sorted[static_cast(Sorted.Num() * 0.9f)];
}
Data.WindowSize = LastPingMeasurements.Num();
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp
index 74f05b7b9a..a33fa96aa1 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp
@@ -44,22 +44,14 @@ DECLARE_CYCLE_STAT(TEXT("IsAuthoritativeServer"), STAT_IsAuthoritativeServer, ST
namespace
{
-#if ENGINE_MINOR_VERSION <= 22
- const int32 MaxSendingChangeHistory = FRepState::MAX_CHANGE_HISTORY;
-#else
- const int32 MaxSendingChangeHistory = FSendingRepState::MAX_CHANGE_HISTORY;
-#endif
+const int32 MaxSendingChangeHistory = FSendingRepState::MAX_CHANGE_HISTORY;
// This is a bookkeeping function that is similar to the one in RepLayout.cpp, modified for our needs (e.g. no NaKs)
// We can't use the one in RepLayout.cpp because it's private and it cannot account for our approach.
// In this function, we poll for any changes in Unreal properties compared to the last time we replicated this actor.
void UpdateChangelistHistory(TUniquePtr& RepState)
{
-#if ENGINE_MINOR_VERSION <= 22
- FRepState* SendingRepState = RepState.Get();
-#else
FSendingRepState* SendingRepState = RepState->GetSendingRepState();
-#endif
check(SendingRepState->HistoryEnd >= SendingRepState->HistoryStart);
@@ -88,6 +80,23 @@ void UpdateChangelistHistory(TUniquePtr& RepState)
SendingRepState->HistoryStart = SendingRepState->HistoryStart % MaxSendingChangeHistory;
SendingRepState->HistoryEnd = SendingRepState->HistoryStart + NewHistoryCount;
}
+
+void ForceReplicateOnActorHierarchy(USpatialNetDriver* NetDriver, const AActor* HierarchyActor, const AActor* OriginalActorBeingReplicated)
+{
+ if (HierarchyActor->GetIsReplicated() && HierarchyActor != OriginalActorBeingReplicated)
+ {
+ if (USpatialActorChannel* Channel = NetDriver->GetOrCreateSpatialActorChannel(const_cast(HierarchyActor)))
+ {
+ Channel->ReplicateActor();
+ }
+ }
+
+ for (const AActor* Child : HierarchyActor->Children)
+ {
+ ForceReplicateOnActorHierarchy(NetDriver, Child, OriginalActorBeingReplicated);
+ }
+}
+
} // end anonymous namespace
bool FSpatialObjectRepState::MoveMappedObjectToUnmapped_r(const FUnrealObjectRef& ObjRef, FObjectReferencesMap& ObjectReferencesMap)
@@ -218,6 +227,7 @@ void USpatialActorChannel::Init(UNetConnection* InConnection, int32 ChannelIndex
bIsAuthServer = false;
LastPositionSinceUpdate = FVector::ZeroVector;
TimeWhenPositionLastUpdated = 0.0f;
+ AuthorityReceivedTimestamp = 0;
PendingDynamicSubobjects.Empty();
SavedConnectionOwningWorkerId.Empty();
@@ -234,34 +244,49 @@ void USpatialActorChannel::Init(UNetConnection* InConnection, int32 ChannelIndex
Receiver = NetDriver->Receiver;
}
-void USpatialActorChannel::DeleteEntityIfAuthoritative()
+void USpatialActorChannel::RetireEntityIfAuthoritative()
{
if (NetDriver->Connection == nullptr)
{
return;
}
- bool bHasAuthority = NetDriver->IsAuthoritativeDestructionAllowed() && NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialGDK::Position::ComponentId);
-
- UE_LOG(LogSpatialActorChannel, Log, TEXT("Delete entity request on %lld. Has authority: %d"), EntityId, (int)bHasAuthority);
+ if (!NetDriver->IsAuthoritativeDestructionAllowed())
+ {
+ return;
+ }
- if (bHasAuthority)
+ const bool bHasAuthority = NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialGDK::Position::ComponentId);
+ if (Actor != nullptr)
{
- // Workaround to delay the delete entity request if tearing off.
- // Task to improve this: https://improbableio.atlassian.net/browse/UNR-841
- if (Actor != nullptr && Actor->GetTearOff())
+ if (bHasAuthority)
{
- NetDriver->DelayedSendDeleteEntityRequest(EntityId, 1.0f);
- // 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);
+ // Workaround to delay the delete entity request if tearing off.
+ // Task to improve this: UNR-841
+ 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);
+ }
+ else
+ {
+ Sender->RetireEntity(EntityId, Actor->IsNetStartupActor());
+ }
}
- else
+ else if (bCreatedEntity) // We have not gained authority yet
{
- Sender->RetireEntity(EntityId);
+ Actor->SetReplicates(false);
+ Receiver->RetireWhenAuthoritive(EntityId, NetDriver->ClassInfoManager->GetComponentIdForClass(*Actor->GetClass()), Actor->IsNetStartupActor(), Actor->GetTearOff()); // Ensure we don't recreate the actor
}
}
+ else
+ {
+ // This is unsupported, and shouldn't happen, don't attempt to cleanup entity to better indicate something has gone wrong
+ UE_LOG(LogSpatialActorChannel, Error, TEXT("RetireEntityIfAuthoritative called on actor channel with null actor - entity id (%lld)"), EntityId);
+ }
}
bool USpatialActorChannel::CleanUp(const bool bForDestroy, EChannelCloseReason CloseReason)
@@ -277,7 +302,7 @@ bool USpatialActorChannel::CleanUp(const bool bForDestroy, EChannelCloseReason C
CloseReason != EChannelCloseReason::Dormancy)
{
// If we're a server worker, and the entity hasn't already been cleaned up, delete it on shutdown.
- DeleteEntityIfAuthoritative();
+ RetireEntityIfAuthoritative();
}
#endif // WITH_EDITOR
@@ -319,7 +344,7 @@ int64 USpatialActorChannel::Close(EChannelCloseReason Reason)
}
else
{
- DeleteEntityIfAuthoritative();
+ RetireEntityIfAuthoritative();
NetDriver->PackageMap->RemoveEntityActor(EntityId);
}
@@ -392,8 +417,8 @@ FRepChangeState USpatialActorChannel::CreateInitialRepChangeState(TWeakObjectPtr
DynamicArrayDepth++;
// For the first layer of each dynamic array encountered at the root level
- // add the number of array properties to conform to Unreal's RepLayout design and
- // allow FRepHandleIterator to jump over arrays. Cmd.EndCmd is an index into
+ // add the number of array properties to conform to Unreal's RepLayout design and
+ // allow FRepHandleIterator to jump over arrays. Cmd.EndCmd is an index into
// RepLayout->Cmds[] that points to the value after the termination NULL of this array.
if (DynamicArrayDepth == 1)
{
@@ -421,6 +446,25 @@ FHandoverChangeState USpatialActorChannel::CreateInitialHandoverChangeState(cons
return HandoverChanged;
}
+void USpatialActorChannel::GetLatestAuthorityChangeFromHierarchy(const AActor* HierarchyActor, uint64& OutTimestamp)
+{
+ if (HierarchyActor->GetIsReplicated())
+ {
+ if (USpatialActorChannel* Channel = NetDriver->GetOrCreateSpatialActorChannel(const_cast(HierarchyActor)))
+ {
+ if (Channel->AuthorityReceivedTimestamp > OutTimestamp)
+ {
+ OutTimestamp = Channel->AuthorityReceivedTimestamp;
+ }
+ }
+ }
+
+ for (const AActor* Child : HierarchyActor->Children)
+ {
+ GetLatestAuthorityChangeFromHierarchy(Child, OutTimestamp);
+ }
+}
+
int64 USpatialActorChannel::ReplicateActor()
{
SCOPE_CYCLE_COUNTER(STAT_SpatialActorChannelReplicateActor);
@@ -474,9 +518,7 @@ int64 USpatialActorChannel::ReplicateActor()
else if (Actor->IsPendingKillOrUnreachable())
{
bActorIsPendingKill = true;
-#if ENGINE_MINOR_VERSION > 22
ActorReplicator.Reset();
-#endif
FString Error(FString::Printf(TEXT("ReplicateActor called with PendingKill Actor! %s"), *Describe()));
UE_LOG(LogNet, Log, TEXT("%s"), *Error);
ensureMsgf(false, TEXT("%s"), *Error);
@@ -532,7 +574,7 @@ int64 USpatialActorChannel::ReplicateActor()
// Replicate Actor and Component properties and RPCs
// ----------------------------------------------------------
-#if USE_NETWORK_PROFILER
+#if USE_NETWORK_PROFILER
const uint32 ActorReplicateStartTime = GNetworkProfiler.IsTrackingEnabled() ? FPlatformTime::Cycles() : 0;
#endif
@@ -554,13 +596,8 @@ int64 USpatialActorChannel::ReplicateActor()
// Update the replicated property change list.
FRepChangelistState* ChangelistState = ActorReplicator->ChangelistMgr->GetRepChangelistState();
-#if ENGINE_MINOR_VERSION <= 22
- ActorReplicator->ChangelistMgr->Update(ActorReplicator->RepState.Get(), Actor, Connection->Driver->ReplicationFrame, RepFlags, bForceCompareProperties);
- FRepState* SendingRepState = ActorReplicator->RepState.Get();
-#else
ActorReplicator->RepLayout->UpdateChangelistMgr(ActorReplicator->RepState->GetSendingRepState(), *ActorReplicator->ChangelistMgr, Actor, Connection->Driver->ReplicationFrame, RepFlags, bForceCompareProperties);
FSendingRepState* SendingRepState = ActorReplicator->RepState->GetSendingRepState();
-#endif
const int32 PossibleNewHistoryIndex = SendingRepState->HistoryEnd % MaxSendingChangeHistory;
FRepChangedHistory& PossibleNewHistoryItem = SendingRepState->ChangeHistory[PossibleNewHistoryIndex];
@@ -609,17 +646,12 @@ int64 USpatialActorChannel::ReplicateActor()
bCreatedEntity = true;
- // If we're not offloading AND either load balancing isn't enabled or it is and we're spawning an Actor that we know
- // will be load-balanced to another worker then preemptively set the role to SimulatedProxy.
- if (!USpatialStatics::IsSpatialOffloadingEnabled() && (!SpatialGDKSettings->bEnableUnrealLoadBalancer || !NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*Actor)))
+ // We preemptively set the Actor role to SimulatedProxy if load balancing is disabled
+ // (since the legacy behaviour is to wait until Spatial tells us we have authority)
+ if (NetDriver->LoadBalanceStrategy == nullptr)
{
Actor->Role = ROLE_SimulatedProxy;
Actor->RemoteRole = ROLE_Authority;
-
- if (SpatialGDKSettings->bEnableUnrealLoadBalancer)
- {
- UE_LOG(LogSpatialActorChannel, Verbose, TEXT("Spawning Actor that will immediately become authoritative on a different worker. Actor: %s. Target virtual worker: %d"), *Actor->GetName(), NetDriver->LoadBalanceStrategy->WhoShouldHaveAuthority(*Actor));
- }
}
}
else
@@ -653,11 +685,7 @@ int64 USpatialActorChannel::ReplicateActor()
}
SendingRepState->LastChangelistIndex = ChangelistState->HistoryEnd;
-#if ENGINE_MINOR_VERSION <= 22
- SendingRepState->OpenAckedCalled = true;
-#else
SendingRepState->bOpenAckedCalled = true;
-#endif
ActorReplicator->bLastUpdateEmpty = 1;
if (bCreatingNewEntity)
@@ -717,25 +745,48 @@ int64 USpatialActorChannel::ReplicateActor()
// TODO: the 'bWroteSomethingImportant' check causes problems for actors that need to transition in groups (ex. Character, PlayerController, PlayerState),
// so disabling it for now. Figure out a way to deal with this to recover the perf lost by calling ShouldChangeAuthority() frequently. [UNR-2387]
- if (SpatialGDKSettings->bEnableUnrealLoadBalancer &&
+ if (NetDriver->LoadBalanceStrategy != nullptr &&
NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID))
{
if (!NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*Actor) && !NetDriver->LockingPolicy->IsLocked(Actor))
- {
- const VirtualWorkerId NewAuthVirtualWorkerId = NetDriver->LoadBalanceStrategy->WhoShouldHaveAuthority(*Actor);
- if (NewAuthVirtualWorkerId != SpatialConstants::INVALID_VIRTUAL_WORKER_ID)
+ {
+ const AActor* NetOwner = Actor->GetNetOwner();
+
+ uint64 HierarchyAuthorityReceivedTimestamp = AuthorityReceivedTimestamp;
+ if (NetOwner != nullptr)
{
- Sender->SendAuthorityIntentUpdate(*Actor, NewAuthVirtualWorkerId);
+ GetLatestAuthorityChangeFromHierarchy(NetOwner, HierarchyAuthorityReceivedTimestamp);
+ }
- // If we're setting a different authority intent, preemptively changed to ROLE_SimulatedProxy
- Actor->Role = ROLE_SimulatedProxy;
- Actor->RemoteRole = ROLE_Authority;
+ const float TimeSinceReceivingAuthInSeconds = double(FPlatformTime::Cycles64() - HierarchyAuthorityReceivedTimestamp) * FPlatformTime::GetSecondsPerCycle64();
+ const float MigrationBackoffTimeInSeconds = 1.0f;
- Actor->OnAuthorityLost();
+ if (TimeSinceReceivingAuthInSeconds < MigrationBackoffTimeInSeconds)
+ {
+ UE_LOG(LogSpatialActorChannel, Verbose, TEXT("Tried to change auth too early for actor %s"), *Actor->GetName());
}
else
{
- UE_LOG(LogSpatialActorChannel, Error, TEXT("Load Balancing Strategy returned invalid virtual worker for actor %s"), *Actor->GetName());
+ const VirtualWorkerId NewAuthVirtualWorkerId = NetDriver->LoadBalanceStrategy->WhoShouldHaveAuthority(*Actor);
+ if (NewAuthVirtualWorkerId == SpatialConstants::INVALID_VIRTUAL_WORKER_ID)
+ {
+ UE_LOG(LogSpatialActorChannel, Error, TEXT("Load Balancing Strategy returned invalid virtual worker for actor %s"), *Actor->GetName());
+ }
+ else
+ {
+ Sender->SendAuthorityIntentUpdate(*Actor, NewAuthVirtualWorkerId);
+
+ // If we're setting a different authority intent, preemptively changed to ROLE_SimulatedProxy
+ Actor->Role = ROLE_SimulatedProxy;
+ Actor->RemoteRole = ROLE_Authority;
+
+ Actor->OnAuthorityLost();
+
+ if (NetOwner != nullptr)
+ {
+ ForceReplicateOnActorHierarchy(NetDriver, NetOwner, Actor);
+ }
+ }
}
}
@@ -750,9 +801,9 @@ int64 USpatialActorChannel::ReplicateActor()
}
}
}
-#if USE_NETWORK_PROFILER
+#if USE_NETWORK_PROFILER
NETWORK_PROFILER(GNetworkProfiler.TrackReplicateActor(Actor, RepFlags, FPlatformTime::Cycles() - ActorReplicateStartTime, Connection));
-#endif
+#endif
// If we evaluated everything, mark LastUpdateTime, even if nothing changed.
LastUpdateTime = Connection->Driver->Time;
@@ -847,7 +898,7 @@ bool USpatialActorChannel::ReplicateSubobject(UObject* Object, const FReplicatio
FObjectReplicator& Replicator = FindOrCreateReplicator(Object, &bCreatedReplicator).Get();
- // If we're creating an entity, don't try replicating
+ // If we're creating an entity, don't try replicating
if (bCreatingNewEntity)
{
return false;
@@ -869,13 +920,8 @@ bool USpatialActorChannel::ReplicateSubobject(UObject* Object, const FReplicatio
FRepChangelistState* ChangelistState = Replicator.ChangelistMgr->GetRepChangelistState();
-#if ENGINE_MINOR_VERSION <= 22
- Replicator.ChangelistMgr->Update(Replicator.RepState.Get(), Object, Replicator.Connection->Driver->ReplicationFrame, RepFlags, bForceCompareProperties);
- FRepState* SendingRepState = Replicator.RepState.Get();
-#else
Replicator.RepLayout->UpdateChangelistMgr(Replicator.RepState->GetSendingRepState(), *Replicator.ChangelistMgr, Object, Replicator.Connection->Driver->ReplicationFrame, RepFlags, bForceCompareProperties);
FSendingRepState* SendingRepState = Replicator.RepState->GetSendingRepState();
-#endif
const int32 PossibleNewHistoryIndex = SendingRepState->HistoryEnd % MaxSendingChangeHistory;
FRepChangedHistory& PossibleNewHistoryItem = SendingRepState->ChangeHistory[PossibleNewHistoryIndex];
@@ -920,11 +966,7 @@ bool USpatialActorChannel::ReplicateSubobject(UObject* Object, const FReplicatio
UpdateChangelistHistory(Replicator.RepState);
SendingRepState->LastChangelistIndex = ChangelistState->HistoryEnd;
-#if ENGINE_MINOR_VERSION <= 22
- SendingRepState->OpenAckedCalled = true;
-#else
SendingRepState->bOpenAckedCalled = true;
-#endif
Replicator.bLastUpdateEmpty = 1;
return RepChanged.Num() > 0;
@@ -1043,15 +1085,9 @@ FHandoverChangeState USpatialActorChannel::GetHandoverChangeList(TArray&
return HandoverChanged;
}
-#if ENGINE_MINOR_VERSION <= 22
-void USpatialActorChannel::SetChannelActor(AActor* InActor)
-{
- Super::SetChannelActor(InActor);
-#else
void USpatialActorChannel::SetChannelActor(AActor* InActor, ESetChannelActorFlags Flags)
{
Super::SetChannelActor(InActor, Flags);
-#endif
USpatialPackageMapClient* PackageMap = NetDriver->PackageMap;
EntityId = PackageMap->GetEntityIdFromObject(InActor);
@@ -1105,12 +1141,6 @@ bool USpatialActorChannel::TryResolveActor()
return false;
}
- // If a Singleton was created, update the GSM with the proper Id.
- if (Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton))
- {
- NetDriver->GlobalStateManager->UpdateSingletonEntityId(Actor->GetClass()->GetPathName(), EntityId);
- }
-
// Inform USpatialNetDriver of this new actor channel/entity pairing
NetDriver->AddActorChannel(EntityId, this);
@@ -1140,11 +1170,7 @@ void USpatialActorChannel::PostReceiveSpatialUpdate(UObject* TargetObject, const
FObjectReplicator& Replicator = FindOrCreateReplicator(TargetObject).Get();
TargetObject->PostNetReceive();
-#if ENGINE_MINOR_VERSION <= 22
- Replicator.RepState->RepNotifies = RepNotifies;
-#else
Replicator.RepState->GetReceivingRepState()->RepNotifies = RepNotifies;
-#endif
Replicator.CallRepNotifies(false);
}
@@ -1155,7 +1181,7 @@ void USpatialActorChannel::OnCreateEntityResponse(const Worker_CreateEntityRespo
if (Actor == nullptr || Actor->IsPendingKill())
{
- UE_LOG(LogSpatialActorChannel, Warning, TEXT("Actor is invalid after trying to create entity"));
+ UE_LOG(LogSpatialActorChannel, Log, TEXT("Actor is invalid after trying to create entity"));
return;
}
@@ -1224,7 +1250,7 @@ void USpatialActorChannel::UpdateSpatialPosition()
if ((ActorOwner != nullptr || Actor->GetNetConnection() != nullptr) && !Actor->IsA())
{
// If this Actor's owner is not replicated (e.g. parent = AI Controller), the actor will not have it's spatial
- // position updated as this code will never be run for the parent.
+ // position updated as this code will never be run for the parent.
if (!(Actor->GetNetConnection() == nullptr && ActorOwner != nullptr && !ActorOwner->GetIsReplicated()))
{
return;
@@ -1334,7 +1360,7 @@ void USpatialActorChannel::ServerProcessOwnershipChange()
bUpdatedThisActor = true;
}
-
+
// Changing owner can affect which interest bucket the Actor should be in so we need to update it.
Worker_ComponentId NewInterestBucketComponentId = NetDriver->ClassInfoManager->ComputeActorInterestComponentId(Actor);
if (SavedInterestBucketComponentID != NewInterestBucketComponentId)
@@ -1371,11 +1397,6 @@ void USpatialActorChannel::ClientProcessOwnershipChange(bool bNewNetOwned)
if (bNewNetOwned != bNetOwned)
{
bNetOwned = bNewNetOwned;
- // Don't send dynamic interest for this ownership change if it is otherwise handled by result types.
- if (!GetDefault()->bEnableResultTypes)
- {
- Sender->SendComponentInterestForActor(this, GetEntityId(), bNetOwned);
- }
Actor->SetIsOwnedByClient(bNetOwned);
@@ -1406,11 +1427,7 @@ void USpatialActorChannel::ResetShadowData(FRepLayout& RepLayout, FRepStateStati
{
if (StaticBuffer.Num() == 0)
{
-#if ENGINE_MINOR_VERSION <= 22
- RepLayout.InitShadowData(StaticBuffer, TargetObject->GetClass(), reinterpret_cast(TargetObject));
-#else
RepLayout.InitRepStateStaticBuffer(StaticBuffer, reinterpret_cast(TargetObject));
-#endif
}
else
{
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp
index 1f9710a6fe..599c644377 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp
@@ -25,6 +25,11 @@
DEFINE_LOG_CATEGORY(LogSpatialGameInstance);
+USpatialGameInstance::USpatialGameInstance()
+ : Super()
+ , bIsSpatialNetDriverReady(false)
+{}
+
bool USpatialGameInstance::HasSpatialNetDriver() const
{
bool bHasSpatialNetDriver = false;
@@ -105,32 +110,38 @@ void USpatialGameInstance::DestroySpatialConnectionManager()
#if WITH_EDITOR
FGameInstancePIEResult USpatialGameInstance::StartPlayInEditorGameInstance(ULocalPlayer* LocalPlayer, const FGameInstancePIEParameters& Params)
+{
+ SpatialWorkerType = Params.SpatialWorkerType;
+ bIsSimulatedPlayer = Params.bIsSimulatedPlayer;
+
+ StartSpatialConnection();
+ return Super::StartPlayInEditorGameInstance(LocalPlayer, Params);
+}
+#endif
+
+void USpatialGameInstance::StartSpatialConnection()
{
if (HasSpatialNetDriver())
{
// If we are using spatial networking then prepare a spatial connection.
+ TryInjectSpatialLocatorIntoCommandLine();
CreateNewSpatialConnectionManager();
}
#if TRACE_LIB_ACTIVE
else
{
// In native, setup worker name here as we don't get a HandleOnConnected() callback
- FString WorkerName = FString::Printf(TEXT("%s:%s"), *Params.SpatialWorkerType.ToString(), *FGuid::NewGuid().ToString(EGuidFormats::Digits));
+ FString WorkerName = FString::Printf(TEXT("%s:%s"), *SpatialWorkerType.ToString(), *FGuid::NewGuid().ToString(EGuidFormats::Digits));
SpatialLatencyTracer->SetWorkerId(WorkerName);
}
#endif
-
- return Super::StartPlayInEditorGameInstance(LocalPlayer, Params);
}
-#endif
-void USpatialGameInstance::TryConnectToSpatial()
+void USpatialGameInstance::TryInjectSpatialLocatorIntoCommandLine()
{
- if (HasSpatialNetDriver())
+ if (!HasPreviouslyConnectedToSpatial())
{
- // If we are using spatial networking then prepare a spatial connection.
- CreateNewSpatialConnectionManager();
-
+ SetHasPreviouslyConnectedToSpatial();
// Native Unreal creates a NetDriver and attempts to automatically connect if a Host is specified as the first commandline argument.
// Since the SpatialOS Launcher does not specify this, we need to check for a locator loginToken to allow automatic connection to provide parity with native.
@@ -147,19 +158,18 @@ void USpatialGameInstance::TryConnectToSpatial()
FCommandLine::Set(*NewCommandLineArgs);
}
}
-#if TRACE_LIB_ACTIVE
- else
- {
- // In native, setup worker name here as we don't get a HandleOnConnected() callback
- FString WorkerName = FString::Printf(TEXT("%s:%s"), *SpatialWorkerType.ToString(), *FGuid::NewGuid().ToString(EGuidFormats::Digits));
- SpatialLatencyTracer->SetWorkerId(WorkerName);
- }
-#endif
}
void USpatialGameInstance::StartGameInstance()
{
- TryConnectToSpatial();
+ if (GetDefault()->GetPreventClientCloudDeploymentAutoConnect())
+ {
+ DisableShouldConnectUsingCommandLineArgs();
+ }
+ else
+ {
+ StartSpatialConnection();
+ }
Super::StartGameInstance();
}
@@ -200,12 +210,11 @@ void USpatialGameInstance::Init()
Super::Init();
SpatialLatencyTracer = NewObject(this);
- FWorldDelegates::LevelInitializedNetworkActors.AddUObject(this, &USpatialGameInstance::OnLevelInitializedNetworkActors);
- ActorGroupManager = MakeUnique();
- ActorGroupManager->Init();
-
- checkf(!(GetDefault()->bEnableUnrealLoadBalancer && USpatialStatics::IsSpatialOffloadingEnabled()), TEXT("Offloading and the Unreal Load Balancer are enabled at the same time, this is currently not supported. Please change your project settings."));
+ if (HasSpatialNetDriver())
+ {
+ FWorldDelegates::LevelInitializedNetworkActors.AddUObject(this, &USpatialGameInstance::OnLevelInitializedNetworkActors);
+ }
}
void USpatialGameInstance::HandleOnConnected()
@@ -219,7 +228,23 @@ void USpatialGameInstance::HandleOnConnected()
WorkerConnection->OnEnqueueMessage.AddUObject(SpatialLatencyTracer, &USpatialLatencyTracer::OnEnqueueMessage);
WorkerConnection->OnDequeueMessage.AddUObject(SpatialLatencyTracer, &USpatialLatencyTracer::OnDequeueMessage);
#endif
- OnConnected.Broadcast();
+
+ OnSpatialConnected.Broadcast();
+}
+
+void USpatialGameInstance::CleanupCachedLevelsAfterConnection()
+{
+ // Cleanup any actors which were created during level load.
+ UWorld* World = GetWorld();
+ check(World != nullptr);
+ for (ULevel* Level : CachedLevelsForNetworkIntialize)
+ {
+ if (World->ContainsLevel(Level))
+ {
+ CleanupLevelInitializedNetworkActors(Level);
+ }
+ }
+ CachedLevelsForNetworkIntialize.Empty();
}
void USpatialGameInstance::HandleOnConnectionFailed(const FString& Reason)
@@ -228,13 +253,17 @@ void USpatialGameInstance::HandleOnConnectionFailed(const FString& Reason)
#if TRACE_LIB_ACTIVE
SpatialLatencyTracer->ResetWorkerId();
#endif
- OnConnectionFailed.Broadcast(Reason);
+ OnSpatialConnectionFailed.Broadcast(Reason);
}
-void USpatialGameInstance::OnLevelInitializedNetworkActors(ULevel* LoadedLevel, UWorld* OwningWorld)
+void USpatialGameInstance::HandleOnPlayerSpawnFailed(const FString& Reason)
{
- const FString WorkerType = GetSpatialWorkerType().ToString();
+ UE_LOG(LogSpatialGameInstance, Error, TEXT("Could not spawn the local player on SpatialOS. Reason: %s"), *Reason);
+ OnSpatialPlayerSpawnFailed.Broadcast(Reason);
+}
+void USpatialGameInstance::OnLevelInitializedNetworkActors(ULevel* LoadedLevel, UWorld* OwningWorld)
+{
if (OwningWorld != GetWorld()
|| !OwningWorld->IsServer()
|| !GetDefault()->UsesSpatialNetworking()
@@ -246,6 +275,19 @@ void USpatialGameInstance::OnLevelInitializedNetworkActors(ULevel* LoadedLevel,
return;
}
+ if (bIsSpatialNetDriverReady)
+ {
+ CleanupLevelInitializedNetworkActors(LoadedLevel);
+ }
+ else
+ {
+ CachedLevelsForNetworkIntialize.Add(LoadedLevel);
+ }
+}
+
+void USpatialGameInstance::CleanupLevelInitializedNetworkActors(ULevel* LoadedLevel)
+{
+ bIsSpatialNetDriverReady = true;
for (int32 ActorIndex = 0; ActorIndex < LoadedLevel->Actors.Num(); ActorIndex++)
{
AActor* Actor = LoadedLevel->Actors[ActorIndex];
@@ -254,7 +296,7 @@ void USpatialGameInstance::OnLevelInitializedNetworkActors(ULevel* LoadedLevel,
continue;
}
- if (USpatialStatics::IsSpatialOffloadingEnabled())
+ if (USpatialStatics::IsSpatialOffloadingEnabled(GetWorld()))
{
if (!USpatialStatics::IsActorGroupOwnerForActor(Actor))
{
@@ -264,7 +306,7 @@ void USpatialGameInstance::OnLevelInitializedNetworkActors(ULevel* LoadedLevel,
}
else
{
- UE_LOG(LogSpatialGameInstance, Verbose, TEXT("WorkerType %s is not the actor group owner of startup actor %s, exchanging Roles"), *WorkerType, *GetPathNameSafe(Actor));
+ UE_LOG(LogSpatialGameInstance, Verbose, TEXT("This worker %s is not the owner of startup actor %s, exchanging Roles"), *GetPathNameSafe(Actor));
ENetRole Temp = Actor->Role;
Actor->Role = Actor->RemoteRole;
Actor->RemoteRole = Temp;
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitReader.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitReader.cpp
index d27974e684..0b2f520097 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitReader.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitReader.cpp
@@ -40,7 +40,7 @@ void FSpatialNetBitReader::DeserializeObjectRef(FUnrealObjectRef& ObjectRef)
}
SerializeBits(&ObjectRef.bNoLoadOnClient, 1);
- SerializeBits(&ObjectRef.bUseSingletonClassPath, 1);
+ SerializeBits(&ObjectRef.bUseClassPathToLoadObject, 1);
}
UObject* FSpatialNetBitReader::ReadObject(bool& bUnresolved)
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitWriter.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitWriter.cpp
index fd32bb3995..ea7a6b062d 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitWriter.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitWriter.cpp
@@ -37,7 +37,7 @@ void FSpatialNetBitWriter::SerializeObjectRef(FUnrealObjectRef& ObjectRef)
}
SerializeBits(&ObjectRef.bNoLoadOnClient, 1);
- SerializeBits(&ObjectRef.bUseSingletonClassPath, 1);
+ SerializeBits(&ObjectRef.bUseClassPathToLoadObject, 1);
}
FArchive& FSpatialNetBitWriter::operator<<(UObject*& Value)
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp
index 105a2168b3..6e7083644e 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp
@@ -14,6 +14,7 @@
#include "Net/DataReplication.h"
#include "Net/RepLayout.h"
#include "SocketSubsystem.h"
+#include "UObject/WeakObjectPtrTemplates.h"
#include "UObject/UObjectIterator.h"
#include "EngineClasses/SpatialActorChannel.h"
@@ -32,6 +33,7 @@
#include "Interop/SpatialWorkerFlags.h"
#include "LoadBalancing/AbstractLBStrategy.h"
#include "LoadBalancing/GridBasedLBStrategy.h"
+#include "LoadBalancing/LayeredLBStrategy.h"
#include "LoadBalancing/OwnershipLockingPolicy.h"
#include "SpatialConstants.h"
#include "SpatialGDKSettings.h"
@@ -40,8 +42,8 @@
#include "Utils/ErrorCodeRemapping.h"
#include "Utils/InterestFactory.h"
#include "Utils/OpUtils.h"
-#include "Utils/SpatialActorGroupManager.h"
#include "Utils/SpatialDebugger.h"
+#include "Utils/SpatialLatencyTracer.h"
#include "Utils/SpatialMetrics.h"
#include "Utils/SpatialMetricsDisplay.h"
#include "Utils/SpatialStatics.h"
@@ -53,6 +55,7 @@
using SpatialGDK::ComponentFactory;
using SpatialGDK::FindFirstOpOfType;
+using SpatialGDK::AppendAllOpsOfType;
using SpatialGDK::FindFirstOpOfTypeForComponent;
using SpatialGDK::InterestFactory;
using SpatialGDK::RPCPayload;
@@ -82,13 +85,11 @@ USpatialNetDriver::USpatialNetDriver(const FObjectInitializer& ObjectInitializer
, NextRPCIndex(0)
, TimeWhenPositionLastUpdated(0.f)
{
-#if ENGINE_MINOR_VERSION >= 23
// Due to changes in 4.23, we now use an outdated flow in ComponentReader::ApplySchemaObject
// Native Unreal now iterates over all commands on clients, and no longer has access to a BaseHandleToCmdIndex
// in the RepLayout, the below change forces its creation on clients, but this is a workaround
// TODO: UNR-2375
bMaySendProperties = true;
-#endif
}
bool USpatialNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, const FURL& URL, bool bReuseAddressAndPort, FString& Error)
@@ -135,8 +136,6 @@ bool USpatialNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, c
bPersistSpatialConnection = true;
}
- ActorGroupManager = GetGameInstance()->ActorGroupManager.Get();
-
// Initialize ClassInfoManager here because it needs to load SchemaDatabase.
// We shouldn't do that in CreateAndInitializeCoreClasses because it is called
// from OnConnectionToSpatialOSSucceeded callback which could be executed with the async
@@ -145,7 +144,7 @@ bool USpatialNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, c
ClassInfoManager = NewObject();
// If it fails to load, don't attempt to connect to spatial.
- if (!ClassInfoManager->TryInit(this, ActorGroupManager))
+ if (!ClassInfoManager->TryInit(this))
{
Error = TEXT("Failed to load Spatial SchemaDatabase! Make sure that schema has been generated for your project");
return false;
@@ -224,7 +223,11 @@ void USpatialNetDriver::InitiateConnectionToSpatialOS(const FURL& URL)
bPersistSpatialConnection = URL.HasOption(*SpatialConstants::ClientsStayConnectedURLOption);
}
- if (!bPersistSpatialConnection)
+ if (GameInstance->GetSpatialConnectionManager() == nullptr)
+ {
+ GameInstance->CreateNewSpatialConnectionManager();
+ }
+ else if (!bPersistSpatialConnection)
{
GameInstance->DestroySpatialConnectionManager();
GameInstance->CreateNewSpatialConnectionManager();
@@ -241,20 +244,21 @@ void USpatialNetDriver::InitiateConnectionToSpatialOS(const FURL& URL)
// If this is the first connection try using the command line arguments to setup the config objects.
// If arguments can not be found we will use the regular flow of loading from the input URL.
- FString SpatialWorkerType = GetGameInstance()->GetSpatialWorkerType().ToString();
+ FString SpatialWorkerType = GameInstance->GetSpatialWorkerType().ToString();
- if (!GameInstance->GetFirstConnectionToSpatialOSAttempted())
+ // Ensures that any connections attempting to using command line arguments have a valid locater host in the command line.
+ GameInstance->TryInjectSpatialLocatorIntoCommandLine();
+
+ UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Attempting connection to SpatialOS"));
+
+ if (GameInstance->GetShouldConnectUsingCommandLineArgs())
{
- GameInstance->SetFirstConnectionToSpatialOSAttempted();
- if (GetDefault()->GetPreventClientCloudDeploymentAutoConnect(bConnectAsClient))
- {
- // If first time connecting but the bGetPreventClientCloudDeploymentAutoConnect flag is set then use input URL to setup connection config.
- ConnectionManager->SetupConnectionConfigFromURL(URL, SpatialWorkerType);
- }
- // Otherwise, try using command line arguments to setup connection config.
- else if (!ConnectionManager->TrySetupConnectionConfigFromCommandLine(SpatialWorkerType))
+ GameInstance->DisableShouldConnectUsingCommandLineArgs();
+
+ // Try using command line arguments to setup connection config.
+ if (!ConnectionManager->TrySetupConnectionConfigFromCommandLine(SpatialWorkerType))
{
- // If the command line arguments can not be used, use the input URL to setup connection config.
+ // If the command line arguments can not be used, use the input URL to setup connection config instead.
ConnectionManager->SetupConnectionConfigFromURL(URL, SpatialWorkerType);
}
}
@@ -298,10 +302,6 @@ void USpatialNetDriver::OnConnectionToSpatialOSSucceeded()
{
QueryGSMToLoadMap();
}
- else
- {
- Sender->CreateServerWorkerEntity();
- }
USpatialGameInstance* GameInstance = GetGameInstance();
check(GameInstance != nullptr);
@@ -373,7 +373,7 @@ void USpatialNetDriver::CreateAndInitializeCoreClasses()
const USpatialGDKSettings* SpatialSettings = GetDefault();
#if !UE_BUILD_SHIPPING
- // If metrics display is enabled, spawn a singleton actor to replicate the information to each client
+ // If metrics display is enabled, spawn an Actor to replicate the information to each client
if (IsServer())
{
if (SpatialSettings->bEnableMetricsDisplay)
@@ -388,14 +388,11 @@ void USpatialNetDriver::CreateAndInitializeCoreClasses()
}
#endif
- if (SpatialSettings->bEnableUnrealLoadBalancer)
- {
- CreateAndInitializeLoadBalancingClasses();
- }
+ CreateAndInitializeLoadBalancingClasses();
if (SpatialSettings->UseRPCRingBuffer())
{
- RPCService = MakeUnique(ExtractRPCDelegate::CreateUObject(Receiver, &USpatialReceiver::OnExtractIncomingRPC), StaticComponentView);
+ RPCService = MakeUnique(ExtractRPCDelegate::CreateUObject(Receiver, &USpatialReceiver::OnExtractIncomingRPC), StaticComponentView, USpatialLatencyTracer::GetTracer(GetWorld()));
}
Dispatcher->Init(Receiver, StaticComponentView, SpatialMetrics, SpatialWorkerFlags);
@@ -404,6 +401,7 @@ void USpatialNetDriver::CreateAndInitializeCoreClasses()
GlobalStateManager->Init(this);
SnapshotManager->Init(Connection, GlobalStateManager, Receiver);
PlayerSpawner->Init(this, &TimerManager);
+ PlayerSpawner->OnPlayerSpawnFailed.BindUObject(GameInstance, &USpatialGameInstance::HandleOnPlayerSpawnFailed);
SpatialMetrics->Init(Connection, NetServerMaxTickRate, IsServer());
SpatialMetrics->ControllerRefProvider.BindUObject(this, &USpatialNetDriver::GetCurrentPlayerControllerRef);
@@ -413,6 +411,10 @@ void USpatialNetDriver::CreateAndInitializeCoreClasses()
check(NewPackageMap == PackageMap);
PackageMap->Init(this, &TimerManager);
+ if (IsServer())
+ {
+ PackageMap->GetEntityPoolReadyDelegate().AddDynamic(Sender, &USpatialSender::CreateServerWorkerEntity);
+ }
// The interest factory depends on the package map, so is created last.
InterestFactory = MakeUnique(ClassInfoManager, PackageMap);
@@ -423,23 +425,9 @@ void USpatialNetDriver::CreateAndInitializeLoadBalancingClasses()
const ASpatialWorldSettings* WorldSettings = GetWorld() ? Cast(GetWorld()->GetWorldSettings()) : nullptr;
if (IsServer())
{
- if (WorldSettings == nullptr || WorldSettings->LoadBalanceStrategy == nullptr)
- {
- if (WorldSettings == nullptr)
- {
- UE_LOG(LogSpatialOSNetDriver, Error, TEXT("If EnableUnrealLoadBalancer is set, WorldSettings should inherit from SpatialWorldSettings to get the load balancing strategy. Using a 1x1 grid."));
- }
- else
- {
- UE_LOG(LogSpatialOSNetDriver, Error, TEXT("If EnableUnrealLoadBalancer is set, there must be a LoadBalancing strategy set. Using a 1x1 grid."));
- }
- LoadBalanceStrategy = NewObject(this);
- }
- else
- {
- LoadBalanceStrategy = NewObject(this, WorldSettings->LoadBalanceStrategy);
- }
+ LoadBalanceStrategy = NewObject(this);
LoadBalanceStrategy->Init();
+ LoadBalanceStrategy->SetVirtualWorkerIds(1, LoadBalanceStrategy->GetMinimumRequiredWorkers());
}
VirtualWorkerTranslator = MakeUnique(LoadBalanceStrategy, Connection->GetWorkerId());
@@ -448,20 +436,24 @@ void USpatialNetDriver::CreateAndInitializeLoadBalancingClasses()
{
LoadBalanceEnforcer = MakeUnique(Connection->GetWorkerId(), StaticComponentView, VirtualWorkerTranslator.Get());
- if (WorldSettings == nullptr || WorldSettings->LockingPolicy == nullptr)
+ const bool bIsMultiWorkerEnabled = WorldSettings != nullptr && WorldSettings->IsMultiWorkerEnabled();
+ if (!bIsMultiWorkerEnabled)
+ {
+ LockingPolicy = NewObject(this);
+ }
+ else if (WorldSettings->DefaultLayerLockingPolicy == nullptr)
{
- UE_LOG(LogSpatialOSNetDriver, Error, TEXT("If EnableUnrealLoadBalancer is set, there must be a Locking Policy set. Using default policy."));
+ UE_LOG(LogSpatialOSNetDriver, Error, TEXT("If Load balancing is enabled, there must be a Locking Policy set. Using default policy."));
LockingPolicy = NewObject(this);
}
else
{
- LockingPolicy = NewObject(this, WorldSettings->LockingPolicy);
+ LockingPolicy = NewObject(this, WorldSettings->DefaultLayerLockingPolicy);
}
LockingPolicy->Init(AcquireLockDelegate, ReleaseLockDelegate);
}
}
-
void USpatialNetDriver::CreateServerSpatialOSNetConnection()
{
check(!bConnectAsClient);
@@ -539,9 +531,6 @@ void USpatialNetDriver::OnGSMQuerySuccess()
WorldContext.PendingNetGame->bSuccessfullyConnected = true;
WorldContext.PendingNetGame->bSentJoinRequest = false;
WorldContext.PendingNetGame->URL = RedirectURL;
-
- // Ensure the singleton map is reset as it will contain bad data from the old map
- GlobalStateManager->RemoveAllSingletons();
}
else
{
@@ -583,14 +572,19 @@ void USpatialNetDriver::GSMQueryDelegateFunction(const Worker_EntityQueryRespons
if (!bQueryResponseSuccess)
{
- UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Failed to extract AcceptingPlayers and SessionId from GSM query response. Will retry query for GSM."));
+ UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Failed to extract AcceptingPlayers and SessionId from GSM query response."));
RetryQueryGSM();
return;
}
- else if (bNewAcceptingPlayers != true ||
- QuerySessionId != SessionId)
+ else if (!bNewAcceptingPlayers)
{
- UE_LOG(LogSpatialOSNetDriver, Log, TEXT("GlobalStateManager did not match expected state. Will retry query for GSM."));
+ UE_LOG(LogSpatialOSNetDriver, Log, TEXT("GlobalStateManager not accepting players. Usually caused by servers not registering themselves with the deployment yet. Did you launch the correct number of servers?"));
+ RetryQueryGSM();
+ return;
+ }
+ else if (QuerySessionId != SessionId)
+ {
+ UE_LOG(LogSpatialOSNetDriver, Log, TEXT("GlobalStateManager session id mismatch - got (%d) expected (%d)."), QuerySessionId, SessionId);
RetryQueryGSM();
return;
}
@@ -614,19 +608,40 @@ void USpatialNetDriver::QueryGSMToLoadMap()
void USpatialNetDriver::OnActorSpawned(AActor* Actor)
{
+ const USpatialGDKSettings* SpatialGDKSettings = GetDefault();
+ if (!SpatialGDKSettings->bEnableMultiWorkerDebuggingWarnings)
+ {
+ return;
+ }
+
if (!Actor->GetIsReplicated() ||
Actor->GetLocalRole() != ROLE_Authority ||
- Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton) ||
!Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_SpatialType) ||
- USpatialStatics::IsActorGroupOwnerForActor(Actor))
+ (IsReady() && USpatialStatics::IsActorGroupOwnerForActor(Actor)))
{
- // We only want to delete actors which are replicated and we somehow gain local authority over, while not the actor group owner.
+ // We only want to delete actors which are replicated and we somehow gain local authority over,
+ // when they should be in a different Layer.
return;
}
- const FString WorkerType = GetGameInstance()->GetSpatialWorkerType().ToString();
- UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Worker %s spawned replicated actor %s (owner: %s) but is not actor group owner for actor group %s. The actor will be destroyed in 0.01s"),
- *WorkerType, *GetNameSafe(Actor), *GetNameSafe(Actor->GetOwner()), *USpatialStatics::GetActorGroupForActor(Actor).ToString());
+ if (!IsReady())
+ {
+ UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Spawned replicated actor %s (owner: %s) before the NetDriver was ready. This is not supported. Actors should only be spawned after BeginPlay is called."),
+ *GetNameSafe(Actor), *GetNameSafe(Actor->GetOwner()));
+ return;
+ }
+
+ if (LoadBalanceStrategy != nullptr)
+ {
+ UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Worker ID %d spawned replicated actor %s (owner: %s) but should not have authority. It should be owned by %d. The actor will be destroyed in 0.01s"),
+ LoadBalanceStrategy->GetLocalVirtualWorkerId(), *GetNameSafe(Actor), *GetNameSafe(Actor->GetOwner()), LoadBalanceStrategy->WhoShouldHaveAuthority(*Actor));
+ }
+ else
+ {
+ UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Worker spawned replicated actor %s (owner: %s) but should not have authority. The actor will be destroyed in 0.01s"),
+ *GetNameSafe(Actor), *GetNameSafe(Actor->GetOwner()));
+ }
+
// We tear off, because otherwise SetLifeSpan fails, we SetLifeSpan because we are just about to spawn the Actor and Unreal would complain if we destroyed it.
Actor->TearOff();
Actor->SetLifeSpan(0.01f);
@@ -693,24 +708,16 @@ void USpatialNetDriver::OnLevelAddedToWorld(ULevel* LoadedLevel, UWorld* OwningW
if (OwningWorld != World
|| !IsServer()
- || GlobalStateManager == nullptr
- || USpatialStatics::IsSpatialOffloadingEnabled())
+ || GlobalStateManager == nullptr)
{
// If the world isn't our owning world, we are a client, or we loaded the levels
- // before connecting to Spatial, or we are running with offloading, we return early.
+ // before connecting to Spatial, we return early.
return;
}
- const bool bLoadBalancingEnabled = GetDefault()->bEnableUnrealLoadBalancer;
const bool bHaveGSMAuthority = StaticComponentView->HasAuthority(SpatialConstants::INITIAL_GLOBAL_STATE_MANAGER_ENTITY_ID, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID);
- if (!bLoadBalancingEnabled && !bHaveGSMAuthority)
- {
- // If load balancing is disabled and this worker is not GSM authoritative then exit early.
- return;
- }
-
- if (bLoadBalancingEnabled && !LoadBalanceStrategy->IsReady())
+ if (!LoadBalanceStrategy->IsReady())
{
// Load balancer isn't ready, this should only occur when servers are loading composition levels on startup, before connecting to spatial.
return;
@@ -720,11 +727,11 @@ void USpatialNetDriver::OnLevelAddedToWorld(ULevel* LoadedLevel, UWorld* OwningW
{
// If load balancing is disabled, we must be the GSM-authoritative worker, so set Role_Authority
// otherwise, load balancing is enabled, so check the lb strategy.
- if (Actor->GetIsReplicated() &&
- (!bLoadBalancingEnabled || LoadBalanceStrategy->ShouldHaveAuthority(*Actor)))
+ if (Actor->GetIsReplicated())
{
- Actor->Role = ROLE_Authority;
- Actor->RemoteRole = ROLE_SimulatedProxy;
+ const bool bRoleAuthoritative = LoadBalanceStrategy->ShouldHaveAuthority(*Actor);
+ Actor->Role = bRoleAuthoritative ? ROLE_Authority : ROLE_SimulatedProxy;
+ Actor->RemoteRole = bRoleAuthoritative ? ROLE_SimulatedProxy : ROLE_Authority;
}
}
}
@@ -745,6 +752,12 @@ void USpatialNetDriver::SpatialProcessServerTravel(const FString& URL, bool bAbs
return;
}
+ if (NetDriver->LoadBalanceStrategy->GetMinimumRequiredWorkers() > 1)
+ {
+ UE_LOG(LogGameMode, Error, TEXT("Server travel is not supported on a deployment with multiple workers."));
+ return;
+ }
+
NetDriver->GlobalStateManager->ResetGSM();
GameMode->StartToLeaveMap();
@@ -827,6 +840,11 @@ void USpatialNetDriver::BeginDestroy()
if (WorkerEntityId != SpatialConstants::INVALID_ENTITY_ID)
{
Connection->SendDeleteEntityRequest(WorkerEntityId);
+
+ // Flush the connection and wait a moment to allow the message to propagate.
+ // TODO: UNR-3697 - This needs to be handled more correctly
+ Connection->Flush();
+ FPlatformProcess::Sleep(0.1f);
}
// Destroy the connection to disconnect from SpatialOS if we aren't meant to persist it.
@@ -847,8 +865,6 @@ void USpatialNetDriver::BeginDestroy()
GDKServices->GetLocalDeploymentManager()->OnDeploymentStart.Remove(SpatialDeploymentStartHandle);
}
#endif
-
- ActorGroupManager = nullptr;
}
void USpatialNetDriver::PostInitProperties()
@@ -882,14 +898,6 @@ void USpatialNetDriver::NotifyActorDestroyed(AActor* ThisActor, bool IsSeamlessT
RepChangedPropertyTrackerMap.Remove(ThisActor);
const bool bIsServer = ServerConnection == nullptr;
-
- // Remove the record of destroyed singletons.
- if (ThisActor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton))
- {
- // We check for this not being a server below to make sure we don't call this incorrectly in single process PIE sessions.
- GlobalStateManager->RemoveSingletonInstance(ThisActor);
- }
-
if (bIsServer)
{
// Check if this is a dormant entity, and if so retire the entity
@@ -897,13 +905,6 @@ void USpatialNetDriver::NotifyActorDestroyed(AActor* ThisActor, bool IsSeamlessT
{
const Worker_EntityId EntityId = PackageMap->GetEntityIdFromObject(ThisActor);
- // It is safe to check that we aren't destroying a singleton actor on a server if there is a valid entity ID and this is not a client.
- if (ThisActor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton) && EntityId != SpatialConstants::INVALID_ENTITY_ID)
- {
- UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Removed a singleton actor on a server. This should never happen. "
- "Actor: %s."), *ThisActor->GetName());
- }
-
// If the actor is an initially dormant startup actor that has not been replicated.
if (EntityId == SpatialConstants::INVALID_ENTITY_ID && ThisActor->IsNetStartupActor() && ThisActor->GetIsReplicated() && ThisActor->HasAuthority())
{
@@ -918,7 +919,7 @@ void USpatialNetDriver::NotifyActorDestroyed(AActor* ThisActor, bool IsSeamlessT
{
UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Retiring dormant entity that we don't have spatial authority over [%lld][%s]"), EntityId, *ThisActor->GetName());
}
- Sender->RetireEntity(EntityId);
+ Sender->RetireEntity(EntityId, ThisActor->IsNetStartupActor());
}
}
@@ -1357,12 +1358,7 @@ void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConne
continue;
}
- if (!Actor->HasAuthority())
- {
- // Trying to replicate Actor which we don't have authority over.
- // Remove after UNR-961
- continue;
- }
+ check(Actor->HasAuthority());
Channel = GetOrCreateSpatialActorChannel(Actor);
if ((Channel == nullptr) && (Actor->NetUpdateFrequency < 1.0f))
@@ -1414,18 +1410,13 @@ void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConne
}
}
- // UNR-865 - Handle closing actor channels for non-relevant actors without deleting the entity.
- // If the actor wasn't recently relevant, or if it was torn off, close the actor channel if it exists for this connection
+ // If the actor has been torn off, close the channel
+ // Native also checks here for !bIsRecentlyRelevant and if so closes due to relevancy, we're not doing because it's less likely
+ // in a SpatialOS game. Might be worth an investigation in future as a performance win - UNR-3063
if (Actor->GetTearOff() && Channel != NULL)
{
- // Non startup (map) actors have their channels closed immediately, which destroys them.
- // Startup actors get to keep their channels open.
- if (!Actor->IsNetStartupActor())
- {
- UE_LOG(LogNetTraffic, Log, TEXT("- Closing channel for no longer relevant actor %s"), *Actor->GetName());
- // TODO: UNR-952 - Add code here for cleaning up actor channels from our maps.
- Channel->Close(Actor->GetTearOff() ? EChannelCloseReason::TearOff : EChannelCloseReason::Relevancy);
- }
+ UE_LOG(LogNetTraffic, Log, TEXT("- Closing channel for no longer relevant actor %s"), *Actor->GetName());
+ Channel->Close(Actor->GetTearOff() ? EChannelCloseReason::TearOff : EChannelCloseReason::Relevancy);
}
}
}
@@ -1447,7 +1438,11 @@ void USpatialNetDriver::ProcessRPC(AActor* Actor, UObject* SubObject, UFunction*
if (IsServer())
{
// Creating channel to ensure that object will be resolvable
- GetOrCreateSpatialActorChannel(CallingObject);
+ if (GetOrCreateSpatialActorChannel(CallingObject) == nullptr)
+ {
+ // No point processing any further since there is no channel, possibly because the actor is being destroyed.
+ return;
+ }
}
// If this object's class isn't present in the schema database, we will log an error and tell the
@@ -1768,7 +1763,7 @@ void USpatialNetDriver::TickFlush(float DeltaTime)
PollPendingLoads();
- if (IsServer() && GetSpatialOSNetConnection() != nullptr && PackageMap->IsEntityPoolReady() && bIsReadyToStart)
+ if (IsServer() && GetSpatialOSNetConnection() != nullptr && bIsReadyToStart)
{
// Update all clients.
#if WITH_SERVER_CODE
@@ -1793,6 +1788,10 @@ void USpatialNetDriver::TickFlush(float DeltaTime)
}
}
+ if (Connection != nullptr)
+ {
+ Connection->MaybeFlush();
+ }
#endif // WITH_SERVER_CODE
}
@@ -2207,6 +2206,13 @@ USpatialActorChannel* USpatialNetDriver::GetOrCreateSpatialActorChannel(UObject*
UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("GetOrCreateSpatialActorChannel: No channel for target object but channel already present for actor. Target object: %s, actor: %s"), *TargetObject->GetPathName(), *TargetActor->GetPathName());
return ActorChannel;
}
+
+ if (TargetActor->IsPendingKillPending())
+ {
+ UE_LOG(LogSpatialOSNetDriver, Log, TEXT("A SpatialActorChannel will not be created for %s because the Actor is being destroyed."), *GetNameSafe(TargetActor));
+ return nullptr;
+ }
+
Channel = CreateSpatialActorChannel(TargetActor);
}
#if !UE_BUILD_SHIPPING
@@ -2312,32 +2318,16 @@ USpatialActorChannel* USpatialNetDriver::CreateSpatialActorChannel(AActor* Actor
USpatialNetConnection* NetConnection = GetSpatialOSNetConnection();
check(NetConnection != nullptr);
- USpatialActorChannel* Channel = nullptr;
- if (Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton))
+ USpatialActorChannel* Channel = static_cast(NetConnection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::OpenedLocally));
+ if (Channel == nullptr)
{
- Channel = GlobalStateManager->AddSingleton(Actor);
- }
- else
- {
- Channel = static_cast(NetConnection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::OpenedLocally));
- if (Channel == nullptr)
- {
- UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Failed to create a channel for actor %s."), *GetNameSafe(Actor));
- }
- else
- {
-#if ENGINE_MINOR_VERSION <= 22
- Channel->SetChannelActor(Actor);
-#else
- Channel->SetChannelActor(Actor, ESetChannelActorFlags::None);
-#endif
- }
+ UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Failed to create a channel for Actor %s."), *GetNameSafe(Actor));
+ return Channel;
}
- if (Channel != nullptr)
- {
- Channel->RefreshAuthority();
- }
+ Channel->SetChannelActor(Actor, ESetChannelActorFlags::None);
+
+ Channel->RefreshAuthority();
return Channel;
}
@@ -2347,12 +2337,12 @@ void USpatialNetDriver::WipeWorld(const PostWorldWipeDelegate& LoadSnapshotAfter
SnapshotManager->WorldWipe(LoadSnapshotAfterWorldWipe);
}
-void USpatialNetDriver::DelayedSendDeleteEntityRequest(Worker_EntityId EntityId, float Delay)
+void USpatialNetDriver::DelayedRetireEntity(Worker_EntityId EntityId, float Delay, bool bIsNetStartupActor)
{
FTimerHandle RetryTimer;
- TimerManager.SetTimer(RetryTimer, [this, EntityId]()
+ TimerManager.SetTimer(RetryTimer, [this, EntityId, bIsNetStartupActor]()
{
- Sender->RetireEntity(EntityId);
+ Sender->RetireEntity(EntityId, bIsNetStartupActor);
}, Delay, false);
}
@@ -2370,11 +2360,11 @@ void USpatialNetDriver::HandleStartupOpQueueing(const TArray& In
if (bIsReadyToStart)
{
- if (GetDefault()->bEnableUnrealLoadBalancer)
- {
- // We know at this point that we have all the information to set the worker's interest query.
- Sender->UpdateServerWorkerEntityInterestAndPosition();
- }
+ // Process levels which were loaded before the connection to Spatial was ready.
+ GetGameInstance()->CleanupCachedLevelsAfterConnection();
+
+ // We know at this point that we have all the information to set the worker's interest query.
+ Sender->UpdateServerWorkerEntityInterestAndPosition();
// We've found and dispatched all ops we need for startup,
// trigger BeginPlay() on the GSM and process the queued ops.
@@ -2410,22 +2400,34 @@ bool USpatialNetDriver::FindAndDispatchStartupOpsServer(const TArray FoundOps;
- Worker_Op* EntityQueryResponseOp = nullptr;
- FindFirstOpOfType(InOpLists, WORKER_OP_TYPE_ENTITY_QUERY_RESPONSE, &EntityQueryResponseOp);
+ AppendAllOpsOfType(InOpLists, WORKER_OP_TYPE_ENTITY_QUERY_RESPONSE, FoundOps);
- if (EntityQueryResponseOp != nullptr)
+ // To correctly initialize the ServerWorkerEntity on each server during op queueing, we need to catch several ops here.
+ // Note that this will break if any other CreateEntity requests are issued during the startup flow.
{
- FoundOps.Add(EntityQueryResponseOp);
- }
+ Worker_Op* CreateEntityResponseOp = nullptr;
+ FindFirstOpOfType(InOpLists, WORKER_OP_TYPE_CREATE_ENTITY_RESPONSE, &CreateEntityResponseOp);
- // CreateEntityResponseOps are needed for non-GSM-authoritative server workers sending an update
- // to the Runtime indicating that the worker is ready to begin play.
- Worker_Op* CreateEntityResponseOp = nullptr;
- FindFirstOpOfType(InOpLists, WORKER_OP_TYPE_CREATE_ENTITY_RESPONSE, &CreateEntityResponseOp);
+ Worker_Op* AddComponentOp = nullptr;
+ FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_ADD_COMPONENT, SpatialConstants::SERVER_WORKER_COMPONENT_ID, &AddComponentOp);
- if (CreateEntityResponseOp != nullptr)
- {
- FoundOps.Add(CreateEntityResponseOp);
+ Worker_Op* AuthorityChangedOp = nullptr;
+ FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_AUTHORITY_CHANGE, SpatialConstants::SERVER_WORKER_COMPONENT_ID, &AuthorityChangedOp);
+
+ if (CreateEntityResponseOp != nullptr)
+ {
+ FoundOps.Add(CreateEntityResponseOp);
+ }
+
+ if (AddComponentOp != nullptr)
+ {
+ FoundOps.Add(AddComponentOp);
+ }
+
+ if (AuthorityChangedOp != nullptr)
+ {
+ FoundOps.Add(AuthorityChangedOp);
+ }
}
// Search for entity id reservation response and process it. The entity id reservation
@@ -2502,17 +2504,24 @@ bool USpatialNetDriver::FindAndDispatchStartupOpsServer(const TArrayIsEntityPoolReady() &&
- GlobalStateManager->IsReady() &&
- (!VirtualWorkerTranslator.IsValid() || VirtualWorkerTranslator->IsReady()))
+ if (!PackageMap->IsEntityPoolReady())
{
- // Return whether or not we are ready to start
- UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Ready to begin processing."));
- return true;
+ UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Waiting for the EntityPool to be ready."));
+ return false;
}
-
- UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Not yet ready to begin processing, still processing startup ops."));
- return false;
+ else if (!GlobalStateManager->IsReady())
+ {
+ UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Waiting for the GSM to be ready."));
+ return false;
+ }
+ else if (VirtualWorkerTranslator.IsValid() && !VirtualWorkerTranslator->IsReady())
+ {
+ GlobalStateManager->QueryTranslation();
+ UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Waiting for the Load balancing system to be ready."));
+ return false;
+ }
+ UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Ready to begin processing."));
+ return true;
}
bool USpatialNetDriver::FindAndDispatchStartupOpsClient(const TArray& InOpLists)
@@ -2557,7 +2566,6 @@ void USpatialNetDriver::SelectiveProcessOps(TArray FoundOps)
}
// This should only be called once on each client, in the SpatialMetricsDisplay constructor after the class is replicated to each client.
-// This is enforced by the fact that the class is a Singleton spawned on servers by the SpatialNetDriver.
void USpatialNetDriver::SetSpatialMetricsDisplay(ASpatialMetricsDisplay* InSpatialMetricsDisplay)
{
check(SpatialMetricsDisplay == nullptr);
@@ -2571,8 +2579,12 @@ void USpatialNetDriver::TrackTombstone(const Worker_EntityId EntityId)
}
#endif
+bool USpatialNetDriver::IsReady() const
+{
+ return bIsReadyToStart;
+}
+
// This should only be called once on each client, in the SpatialDebugger constructor after the class is replicated to each client.
-// This is enforced by the fact that the class is a Singleton spawned on servers by the SpatialNetDriver.
void USpatialNetDriver::SetSpatialDebugger(ASpatialDebugger* InSpatialDebugger)
{
check(!IsServer());
@@ -2605,5 +2617,5 @@ FUnrealObjectRef USpatialNetDriver::GetCurrentPlayerControllerRef()
void USpatialNetDriver::InitializeVirtualWorkerTranslationManager()
{
VirtualWorkerTranslationManager = MakeUnique(Receiver, Connection, VirtualWorkerTranslator.Get());
- VirtualWorkerTranslationManager->AddVirtualWorkerIds(LoadBalanceStrategy->GetVirtualWorkerIds());
+ VirtualWorkerTranslationManager->SetNumberOfVirtualWorkers(LoadBalanceStrategy->GetMinimumRequiredWorkers());
}
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp
index a96e8fa76c..0a4d3069db 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp
@@ -2,11 +2,6 @@
#include "EngineClasses/SpatialPackageMapClient.h"
-#include "EngineUtils.h"
-#include "Engine/Engine.h"
-#include "GameFramework/Actor.h"
-#include "Kismet/GameplayStatics.h"
-
#include "EngineClasses/SpatialActorChannel.h"
#include "EngineClasses/SpatialNetDriver.h"
#include "EngineClasses/SpatialNetBitReader.h"
@@ -15,8 +10,12 @@
#include "Interop/SpatialSender.h"
#include "Schema/UnrealObjectRef.h"
#include "SpatialConstants.h"
-#include "Utils/EntityPool.h"
#include "Utils/SchemaOption.h"
+
+#include "EngineUtils.h"
+#include "Engine/Engine.h"
+#include "GameFramework/Actor.h"
+#include "Kismet/GameplayStatics.h"
#include "UObject/UObjectGlobals.h"
DEFINE_LOG_CATEGORY(LogSpatialPackageMap);
@@ -42,7 +41,7 @@ void GetSubobjects(UObject* ParentObject, TArray& InSubobjects)
{
// Walk up the outer chain and ensure that no object is PendingKill. This is required because although
// EInternalObjectFlags::PendingKill prevents objects that are PendingKill themselves from getting added
- // to the list, it'll still add children of PendingKill objects. This then causes an assertion within
+ // to the list, it'll still add children of PendingKill objects. This then causes an assertion within
// FNetGUIDCache::RegisterNetGUID_Server where it again iterates up the object's owner chain, assigning
// ids and ensuring that no object is set to PendingKill in the process.
UObject* Outer = Object->GetOuter();
@@ -75,7 +74,7 @@ Worker_EntityId USpatialPackageMapClient::AllocateEntityIdAndResolveActor(AActor
return SpatialConstants::INVALID_ENTITY_ID;
}
- Worker_EntityId EntityId = EntityPool->GetNextEntityId();
+ Worker_EntityId EntityId = AllocateEntityId();
if (EntityId == SpatialConstants::INVALID_ENTITY_ID)
{
UE_LOG(LogSpatialPackageMap, Error, TEXT("Unable to retrieve an Entity ID for Actor: %s"), *Actor->GetName());
@@ -112,12 +111,6 @@ FNetworkGUID USpatialPackageMapClient::TryResolveObjectAsEntity(UObject* Value)
return NetGUID;
}
- if (Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton))
- {
- // Singletons will always go through GlobalStateManager first.
- return NetGUID;
- }
-
// Resolve as an entity if it is an unregistered actor
if (Actor->Role == ROLE_Authority && GetEntityIdFromObject(Actor) == SpatialConstants::INVALID_ENTITY_ID)
{
@@ -265,39 +258,49 @@ Worker_EntityId USpatialPackageMapClient::GetEntityIdFromObject(const UObject* O
return GetUnrealObjectRefFromNetGUID(NetGUID).Entity;
}
-AActor* USpatialPackageMapClient::GetSingletonByClassRef(const FUnrealObjectRef& SingletonClassRef)
+bool USpatialPackageMapClient::CanClientLoadObject(UObject* Object)
{
- if (UClass* SingletonClass = Cast(GetObjectFromUnrealObjectRef(SingletonClassRef)))
- {
- TArray FoundActors;
- // USpatialPackageMapClient is an inner object of UNetConnection,
- // which in turn contains a NetDriver and gets the UWorld it references
- UGameplayStatics::GetAllActorsOfClass(this, SingletonClass, FoundActors);
-
- // There should be only one singleton actor per class
- if (FoundActors.Num() == 1)
- {
- return FoundActors[0];
- }
+ FNetworkGUID NetGUID = GetNetGUIDFromObject(Object);
+ return GuidCache->CanClientLoadObject(Object, NetGUID);
+}
- FString FullPath;
- SpatialGDK::GetFullPathFromUnrealObjectReference(SingletonClassRef, FullPath);
- UE_LOG(LogSpatialPackageMap, Verbose, TEXT("GetSingletonByClassRef: Found %d actors for singleton class: %s"), FoundActors.Num(), *FullPath);
- return nullptr;
+AActor* USpatialPackageMapClient::GetUniqueActorInstanceByClassRef(const FUnrealObjectRef& UniqueObjectClassRef)
+{
+ if (UClass* UniqueObjectClass = Cast(GetObjectFromUnrealObjectRef(UniqueObjectClassRef)))
+ {
+ return GetUniqueActorInstanceByClass(UniqueObjectClass);
}
else
{
FString FullPath;
- SpatialGDK::GetFullPathFromUnrealObjectReference(SingletonClassRef, FullPath);
- UE_LOG(LogSpatialPackageMap, Warning, TEXT("GetSingletonByClassRef: Can't resolve singleton class: %s"), *FullPath);
+ SpatialGDK::GetFullPathFromUnrealObjectReference(UniqueObjectClassRef, FullPath);
+ UE_LOG(LogSpatialPackageMap, Warning, TEXT("Can't resolve unique object class: %s"), *FullPath);
return nullptr;
}
}
-bool USpatialPackageMapClient::CanClientLoadObject(UObject* Object)
+AActor* USpatialPackageMapClient::GetUniqueActorInstanceByClass(UClass* UniqueObjectClass) const
{
- FNetworkGUID NetGUID = GetNetGUIDFromObject(Object);
- return GuidCache->CanClientLoadObject(Object, NetGUID);
+ check(UniqueObjectClass != nullptr);
+
+ TArray FoundActors;
+ // USpatialPackageMapClient is an inner object of UNetConnection,
+ // which in turn contains a NetDriver and gets the UWorld it references.
+ UGameplayStatics::GetAllActorsOfClass(this, UniqueObjectClass, FoundActors);
+
+ // There should be only one Actor per class.
+ if (FoundActors.Num() == 1)
+ {
+ return FoundActors[0];
+ }
+
+ UE_LOG(LogSpatialPackageMap, Warning, TEXT("Found %d Actors for class: %s. There should only be one."), FoundActors.Num(), *UniqueObjectClass->GetName());
+ return nullptr;
+}
+
+Worker_EntityId USpatialPackageMapClient::AllocateEntityId()
+{
+ return EntityPool->GetNextEntityId();
}
bool USpatialPackageMapClient::IsEntityPoolReady() const
@@ -305,6 +308,12 @@ bool USpatialPackageMapClient::IsEntityPoolReady() const
return (EntityPool != nullptr) && (EntityPool->IsReady());
}
+FEntityPoolReadyEvent& USpatialPackageMapClient::GetEntityPoolReadyDelegate()
+{
+ check(bIsServer);
+ return EntityPool->GetEntityPoolReadyDelegate();
+}
+
bool USpatialPackageMapClient::SerializeObject(FArchive& Ar, UClass* InClass, UObject*& Obj, FNetworkGUID *OutNetGUID)
{
// Super::SerializeObject is not called here on purpose
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp
index d376bf6ff2..a1539d9d4b 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp
@@ -19,14 +19,15 @@ SpatialVirtualWorkerTranslationManager::SpatialVirtualWorkerTranslationManager(
, bWorkerEntityQueryInFlight(false)
{}
-void SpatialVirtualWorkerTranslationManager::AddVirtualWorkerIds(const TSet& InVirtualWorkerIds)
+void SpatialVirtualWorkerTranslationManager::SetNumberOfVirtualWorkers(const uint32 NumVirtualWorkers)
{
+ UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT("TranslationManager is configured to look for %d workers"), NumVirtualWorkers);
+
// Currently, this should only be called once on startup. In the future we may allow for more
// flexibility.
- check(UnassignedVirtualWorkers.IsEmpty());
- for (VirtualWorkerId VirtualWorkerId : InVirtualWorkerIds)
+ for (uint32 i = 1; i <= NumVirtualWorkers; i++)
{
- UnassignedVirtualWorkers.Enqueue(VirtualWorkerId);
+ UnassignedVirtualWorkers.Enqueue(i);
}
}
@@ -172,7 +173,7 @@ void SpatialVirtualWorkerTranslationManager::ServerWorkerEntityQueryDelegate(con
}
else
{
- UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT(" Processing ServerWorker Entity query response"));
+ UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT("Processing ServerWorker Entity query response"));
ConstructVirtualWorkerMappingFromQueryResponse(Op);
}
diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp
index 935c573e45..4f8df69bcc 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp
@@ -7,7 +7,9 @@
#include "Async/Async.h"
#include "Improbable/SpatialEngineConstants.h"
+#include "Improbable/SpatialGDKSettingsBridge.h"
#include "Misc/Paths.h"
+#include "Modules/ModuleManager.h"
#include "Interop/Connection/SpatialWorkerConnection.h"
#include "SpatialGDKSettings.h"
@@ -23,14 +25,33 @@ struct ConfigureConnection
: Config(InConfig)
, Params()
, WorkerType(*Config.WorkerType)
- , ProtocolLogPrefix(*FormatProtocolPrefix())
+ , WorkerSDKLogFilePrefix(*FormatWorkerSDKLogFilePrefix())
{
Params = Worker_DefaultConnectionParameters();
Params.worker_type = WorkerType.Get();
- Params.enable_protocol_logging_at_startup = Config.EnableProtocolLoggingAtStartup;
- Params.protocol_logging.log_prefix = ProtocolLogPrefix.Get();
+ Logsink.logsink_type = WORKER_LOGSINK_TYPE_ROTATING_FILE;
+ Logsink.rotating_logfile_parameters.log_prefix = WorkerSDKLogFilePrefix.Get();
+ Logsink.rotating_logfile_parameters.max_log_files = 1;
+ // TODO: When upgrading to Worker SDK 14.6.2, remove the WorkerSDKLogFileSize parameter and set this to 0 for infinite file size
+ Logsink.rotating_logfile_parameters.max_log_file_size_bytes = Config.WorkerSDKLogFileSize;
+
+ uint32_t Categories = 0;
+ if (Config.EnableWorkerSDKOpLogging)
+ {
+ Categories |= WORKER_LOG_CATEGORY_API;
+ }
+ if (Config.EnableWorkerSDKProtocolLogging)
+ {
+ Categories |= WORKER_LOG_CATEGORY_NETWORK_STATUS | WORKER_LOG_CATEGORY_NETWORK_TRAFFIC;
+ }
+ Logsink.filter_parameters.categories = Categories;
+ Logsink.filter_parameters.level = Config.WorkerSDKLogLevel;
+
+ Params.logsinks = &Logsink;
+ Params.logsink_count = 1;
+ Params.enable_logging_at_startup = Categories != 0;
Params.component_vtable_count = 0;
Params.default_component_vtable = &DefaultVtable;
@@ -58,45 +79,42 @@ struct ConfigureConnection
Params.network.modular_kcp.upstream_heartbeat = &HeartbeatParams;
#endif
- if (!bConnectAsClient && GetDefault()->bUseSecureServerConnection)
- {
- Params.network.modular_kcp.security_type = WORKER_NETWORK_SECURITY_TYPE_TLS;
- Params.network.modular_tcp.security_type = WORKER_NETWORK_SECURITY_TYPE_TLS;
- }
- else if (bConnectAsClient && GetDefault()->bUseSecureClientConnection)
+ // Use insecure connections default.
+ Params.network.modular_kcp.security_type = WORKER_NETWORK_SECURITY_TYPE_INSECURE;
+ Params.network.modular_tcp.security_type = WORKER_NETWORK_SECURITY_TYPE_INSECURE;
+
+ // Override the security type to be secure only if the user has requested it and we are not using an editor build.
+ if ((!bConnectAsClient && GetDefault()->bUseSecureServerConnection) || (bConnectAsClient && GetDefault()->bUseSecureClientConnection))
{
+#if WITH_EDITOR
+ UE_LOG(LogSpatialWorkerConnection, Warning, TEXT("Secure connection requested but this is not supported in Editor builds. Connection will be insecure."));
+#else
Params.network.modular_kcp.security_type = WORKER_NETWORK_SECURITY_TYPE_TLS;
Params.network.modular_tcp.security_type = WORKER_NETWORK_SECURITY_TYPE_TLS;
- }
- else
- {
- Params.network.modular_kcp.security_type = WORKER_NETWORK_SECURITY_TYPE_INSECURE;
- Params.network.modular_tcp.security_type = WORKER_NETWORK_SECURITY_TYPE_INSECURE;
+#endif
}
Params.enable_dynamic_components = true;
}
- FString FormatProtocolPrefix() const
+ FString FormatWorkerSDKLogFilePrefix() const
{
- FString FinalProtocolLoggingPrefix = FPaths::ConvertRelativePathToFull(FPaths::ProjectLogDir());
- if (!Config.ProtocolLoggingPrefix.IsEmpty())
+ FString FinalLogFilePrefix = FPaths::ConvertRelativePathToFull(FPaths::ProjectLogDir());
+ if (!Config.WorkerSDKLogPrefix.IsEmpty())
{
- FinalProtocolLoggingPrefix += Config.ProtocolLoggingPrefix;
+ FinalLogFilePrefix += Config.WorkerSDKLogPrefix;
}
- else
- {
- FinalProtocolLoggingPrefix += Config.WorkerId;
- }
- return FinalProtocolLoggingPrefix;
+ FinalLogFilePrefix += Config.WorkerId + TEXT("-");
+ return FinalLogFilePrefix;
}
const FConnectionConfig& Config;
Worker_ConnectionParameters Params;
FTCHARToUTF8 WorkerType;
- FTCHARToUTF8 ProtocolLogPrefix;
+ FTCHARToUTF8 WorkerSDKLogFilePrefix;
Worker_ComponentVtable DefaultVtable{};
Worker_CompressionParameters EnableCompressionParams{};
+ Worker_LogsinkParameters Logsink{};
#if WITH_EDITOR
Worker_HeartbeatParameters HeartbeatParams{ WORKER_DEFAULTS_HEARTBEAT_INTERVAL_MILLIS, MAX_int64 };
@@ -154,13 +172,13 @@ void USpatialConnectionManager::Connect(bool bInitAsClient, uint32 PlayInEditorI
bConnectAsClient = bInitAsClient;
- const USpatialGDKSettings* SpatialGDKSettings = GetDefault();
- if (SpatialGDKSettings->bUseDevelopmentAuthenticationFlow && bInitAsClient)
+ const ISpatialGDKEditorModule* SpatialGDKEditorModule = FModuleManager::GetModulePtr("SpatialGDKEditor");
+ if (SpatialGDKEditorModule != nullptr && SpatialGDKEditorModule->ShouldConnectToCloudDeployment() && bInitAsClient)
{
- DevAuthConfig.Deployment = SpatialGDKSettings->DevelopmentDeploymentToConnect;
+ DevAuthConfig.Deployment = SpatialGDKEditorModule->GetSpatialOSCloudDeploymentName();
DevAuthConfig.WorkerType = SpatialConstants::DefaultClientWorkerType.ToString();
DevAuthConfig.UseExternalIp = true;
- StartDevelopmentAuth(SpatialGDKSettings->DevelopmentAuthenticationToken);
+ StartDevelopmentAuth(SpatialGDKEditorModule->GetDevAuthToken());
return;
}
@@ -205,24 +223,36 @@ void USpatialConnectionManager::ProcessLoginTokensResponse(const Worker_Alpha_Lo
return;
}
- const FString& DeploymentToConnect = DevAuthConfig.Deployment;
+ FString DeploymentToConnect = DevAuthConfig.Deployment;
// If not set, use the first deployment. It can change every query if you have multiple items available, because the order is not guaranteed.
if (DeploymentToConnect.IsEmpty())
{
DevAuthConfig.LoginToken = FString(LoginTokens->login_tokens[0].login_token);
+ DeploymentToConnect = UTF8_TO_TCHAR(LoginTokens->login_tokens[0].deployment_name);
}
else
{
+ bool bFoundDeployment = false;
+
for (uint32 i = 0; i < LoginTokens->login_token_count; i++)
{
- FString DeploymentName = FString(LoginTokens->login_tokens[i].deployment_name);
+ FString DeploymentName = UTF8_TO_TCHAR(LoginTokens->login_tokens[i].deployment_name);
if (DeploymentToConnect.Compare(DeploymentName) == 0)
{
DevAuthConfig.LoginToken = FString(LoginTokens->login_tokens[i].login_token);
+ bFoundDeployment = true;
break;
}
}
+
+ if (!bFoundDeployment)
+ {
+ OnConnectionFailure(WORKER_CONNECTION_STATUS_CODE_NETWORK_ERROR, FString::Printf(TEXT("Deployment not found! Make sure that the deployment with name '%s' is running and has the 'dev_login' deployment tag."), *DeploymentToConnect));
+ return;
+ }
}
+
+ UE_LOG(LogSpatialConnectionManager, Log, TEXT("Dev auth flow: connecting to deployment \"%s\""), *DeploymentToConnect);
ConnectToLocator(&DevAuthConfig);
}
@@ -287,7 +317,7 @@ void USpatialConnectionManager::ConnectToReceptionist(uint32 PlayInEditorID)
ConfigureConnection ConnectionConfig(ReceptionistConfig, bConnectAsClient);
Worker_ConnectionFuture* ConnectionFuture = Worker_ConnectAsync(
- TCHAR_TO_UTF8(*ReceptionistConfig.GetReceptionistHost()), ReceptionistConfig.ReceptionistPort,
+ TCHAR_TO_UTF8(*ReceptionistConfig.GetReceptionistHost()), ReceptionistConfig.GetReceptionistPort(),
TCHAR_TO_UTF8(*ReceptionistConfig.WorkerId), &ConnectionConfig.Params);
FinishConnecting(ConnectionFuture);
@@ -380,6 +410,7 @@ bool USpatialConnectionManager::TrySetupConnectionConfigFromCommandLine(const FS
bool bSuccessfullyLoaded = LocatorConfig.TryLoadCommandLineArgs();
if (bSuccessfullyLoaded)
{
+ UE_LOG(LogSpatialWorkerConnection, Log, TEXT("Successfully set up locator config from command line arguments"));
SetConnectionType(ESpatialConnectionType::Locator);
LocatorConfig.WorkerType = SpatialWorkerType;
}
@@ -388,11 +419,13 @@ bool USpatialConnectionManager::TrySetupConnectionConfigFromCommandLine(const FS
bSuccessfullyLoaded = DevAuthConfig.TryLoadCommandLineArgs();
if (bSuccessfullyLoaded)
{
+ UE_LOG(LogSpatialWorkerConnection, Log, TEXT("Successfully set up dev auth config from command line arguments"));
SetConnectionType(ESpatialConnectionType::DevAuthFlow);
DevAuthConfig.WorkerType = SpatialWorkerType;
}
else
{
+ UE_LOG(LogSpatialWorkerConnection, Log, TEXT("Setting up receptionist config from command line arguments"));
bSuccessfullyLoaded = ReceptionistConfig.TryLoadCommandLineArgs();
SetConnectionType(ESpatialConnectionType::Receptionist);
ReceptionistConfig.WorkerType = SpatialWorkerType;
@@ -404,6 +437,8 @@ bool USpatialConnectionManager::TrySetupConnectionConfigFromCommandLine(const FS
void USpatialConnectionManager::SetupConnectionConfigFromURL(const FURL& URL, const FString& SpatialWorkerType)
{
+ UE_LOG(LogSpatialWorkerConnection, Log, TEXT("Setting up connection config from URL"));
+
if (URL.HasOption(TEXT("locator")) || URL.HasOption(TEXT("devauth")))
{
FString LocatorHostOverride;
@@ -448,20 +483,8 @@ void USpatialConnectionManager::SetupConnectionConfigFromURL(const FURL& URL, co
{
SetConnectionType(ESpatialConnectionType::Receptionist);
- // If we have a non-empty host then use this to connect. If not - use the default configured in FReceptionistConfig initialisation.
- if (!URL.Host.IsEmpty())
- {
- ReceptionistConfig.SetReceptionistHost(URL.Host);
- }
-
+ ReceptionistConfig.SetupFromURL(URL);
ReceptionistConfig.WorkerType = SpatialWorkerType;
-
- const TCHAR* UseExternalIpForBridge = TEXT("useExternalIpForBridge");
- if (URL.HasOption(UseExternalIpForBridge))
- {
- FString UseExternalIpOption = URL.GetOption(UseExternalIpForBridge, TEXT(""));
- ReceptionistConfig.UseExternalIp = !UseExternalIpOption.Equals(TEXT("false"), ESearchCase::IgnoreCase);
- }
}
}
diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp
index d49a443b21..717df64bad 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp
@@ -20,6 +20,16 @@ void USpatialWorkerConnection::SetConnection(Worker_Connection* WorkerConnection
{
if (OpsProcessingThread == nullptr)
{
+ bool bCanWake = SpatialGDKSettings->bWorkerFlushAfterOutgoingNetworkOp;
+ float WaitTimeS = 1.0f / (GetDefault()->OpsUpdateRate);
+ int32 WaitTimeMs = static_cast(FTimespan::FromSeconds(WaitTimeS).GetTotalMilliseconds());
+ if (WaitTimeMs <= 0)
+ {
+ UE_LOG(LogSpatialWorkerConnection, Warning, TEXT("Clamping wait time for worker ops thread to the minimum rate of 1ms."));
+ WaitTimeMs = 1;
+ }
+ ThreadWaitCondition.Emplace(bCanWake, WaitTimeMs);
+
InitializeOpsProcessingThread();
}
}
@@ -43,6 +53,8 @@ void USpatialWorkerConnection::DestroyConnection()
OpsProcessingThread = nullptr;
}
+ ThreadWaitCondition.Reset(); // Set TOptional value to null
+
if (WorkerConnection)
{
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [WorkerConnection = WorkerConnection]
@@ -167,13 +179,6 @@ void USpatialWorkerConnection::CacheWorkerAttributes()
}
}
-bool USpatialWorkerConnection::Init()
-{
- OpsUpdateInterval = 1.0f / GetDefault()->OpsUpdateRate;
-
- return true;
-}
-
uint32 USpatialWorkerConnection::Run()
{
const USpatialGDKSettings* SpatialGDKSettings = GetDefault();
@@ -181,7 +186,7 @@ uint32 USpatialWorkerConnection::Run()
while (KeepRunning)
{
- FPlatformProcess::Sleep(OpsUpdateInterval);
+ ThreadWaitCondition->Wait();
QueueLatestOpList();
ProcessOutgoingMessages();
}
@@ -217,8 +222,11 @@ void USpatialWorkerConnection::QueueLatestOpList()
void USpatialWorkerConnection::ProcessOutgoingMessages()
{
+ bool bSentData = false;
while (!OutgoingMessagesQueue.IsEmpty())
{
+ bSentData = true;
+
TUniquePtr OutgoingMessage;
OutgoingMessagesQueue.Dequeue(OutgoingMessage);
@@ -388,6 +396,7 @@ void USpatialWorkerConnection::ProcessOutgoingMessages()
TArray WorkerHistogramMetrics;
TArray> WorkerHistogramMetricBuckets;
WorkerHistogramMetrics.SetNum(Message->Metrics.HistogramMetrics.Num());
+ WorkerHistogramMetricBuckets.SetNum(Message->Metrics.HistogramMetrics.Num());
for (int i = 0; i < Message->Metrics.HistogramMetrics.Num(); i++)
{
WorkerHistogramMetrics[i].key = Message->Metrics.HistogramMetrics[i].Key.c_str();
@@ -417,6 +426,34 @@ void USpatialWorkerConnection::ProcessOutgoingMessages()
}
}
}
+
+ // Flush worker API calls
+ if (bSentData)
+ {
+ Worker_Connection_Alpha_Flush(WorkerConnection);
+ }
+}
+
+void USpatialWorkerConnection::MaybeFlush()
+{
+ const USpatialGDKSettings* Settings = GetDefault();
+ if (Settings->bWorkerFlushAfterOutgoingNetworkOp)
+ {
+ Flush();
+ }
+}
+
+void USpatialWorkerConnection::Flush()
+{
+ const USpatialGDKSettings* Settings = GetDefault();
+ if (Settings->bRunSpatialWorkerConnectionOnGameThread)
+ {
+ ProcessOutgoingMessages();
+ }
+ else if (ensure(ThreadWaitCondition.IsSet()))
+ {
+ ThreadWaitCondition->Wake(); // No-op if wake is not enabled.
+ }
}
template
diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp
index 51a792d0ec..b9f6da832a 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp
@@ -54,23 +54,18 @@ void UGlobalStateManager::Init(USpatialNetDriver* InNetDriver)
}
}
#endif // WITH_EDITOR
-
+
bAcceptingPlayers = false;
bHasSentReadyForVirtualWorkerAssignment = false;
bCanBeginPlay = false;
bCanSpawnWithAuthority = false;
-}
-
-void UGlobalStateManager::ApplySingletonManagerData(const Worker_ComponentData& Data)
-{
- Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type);
- SingletonNameToEntityId = GetStringToEntityMapFromSchema(ComponentObject, SpatialConstants::SINGLETON_MANAGER_SINGLETON_NAME_TO_ENTITY_ID);
+ bTranslationQueryInFlight = false;
}
void UGlobalStateManager::ApplyDeploymentMapData(const Worker_ComponentData& Data)
{
Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type);
-
+
SetDeploymentMapURL(GetStringFromSchema(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_MAP_URL_ID));
bAcceptingPlayers = GetBoolFromSchema(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_ACCEPTING_PLAYERS_ID);
@@ -98,8 +93,10 @@ void UGlobalStateManager::TrySendWorkerReadyToBeginPlay()
// AddComponent. This is important for handling startup Actors correctly in a zoned
// environment.
const bool bHasReceivedStartupActorData = StaticComponentView->HasComponent(GlobalStateManagerEntityId, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID);
- const bool bWorkerEntityCreated = NetDriver->WorkerEntityId != SpatialConstants::INVALID_ENTITY_ID;
- if (bHasSentReadyForVirtualWorkerAssignment || !bHasReceivedStartupActorData || !bWorkerEntityCreated)
+ const bool bWorkerEntityReady = NetDriver->WorkerEntityId != SpatialConstants::INVALID_ENTITY_ID &&
+ StaticComponentView->HasAuthority(NetDriver->WorkerEntityId, SpatialConstants::SERVER_WORKER_COMPONENT_ID);
+
+ if (bHasSentReadyForVirtualWorkerAssignment || !bHasReceivedStartupActorData || !bWorkerEntityReady)
{
return;
}
@@ -114,16 +111,6 @@ void UGlobalStateManager::TrySendWorkerReadyToBeginPlay()
NetDriver->Connection->SendComponentUpdate(NetDriver->WorkerEntityId, &Update);
}
-void UGlobalStateManager::ApplySingletonManagerUpdate(const Worker_ComponentUpdate& Update)
-{
- Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type);
-
- if (Schema_GetObjectCount(ComponentObject, SpatialConstants::SINGLETON_MANAGER_SINGLETON_NAME_TO_ENTITY_ID) > 0)
- {
- SingletonNameToEntityId = GetStringToEntityMapFromSchema(ComponentObject, SpatialConstants::SINGLETON_MANAGER_SINGLETON_NAME_TO_ENTITY_ID);
- }
-}
-
void UGlobalStateManager::ApplyDeploymentMapUpdate(const Worker_ComponentUpdate& Update)
{
Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type);
@@ -162,7 +149,7 @@ void UGlobalStateManager::SendShutdownMultiProcessRequest()
* Standard UnrealEngine behavior is to call TerminateProc on external processes and there is no method to send any messaging
* to those external process.
* The GDK requires shutdown code to be ran for workers to disconnect cleanly so instead of abruptly shutting down the server worker,
- * just send a command to the worker to begin it's shutdown phase.
+ * just send a command to the worker to begin it's shutdown phase.
*/
Worker_CommandRequest CommandRequest = {};
CommandRequest.component_id = SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID;
@@ -177,8 +164,8 @@ void UGlobalStateManager::ReceiveShutdownMultiProcessRequest()
if (NetDriver && NetDriver->GetNetMode() == NM_DedicatedServer)
{
UE_LOG(LogGlobalStateManager, Log, TEXT("Received shutdown multi-process request."));
-
- // Since the server works are shutting down, set reset the accepting_players flag to false to prevent race conditions where the client connects quicker than the server.
+
+ // Since the server works are shutting down, set reset the accepting_players flag to false to prevent race conditions where the client connects quicker than the server.
SetAcceptingPlayers(false);
DeploymentSessionId = 0;
SendSessionIdUpdate();
@@ -237,220 +224,14 @@ void UGlobalStateManager::ApplyStartupActorManagerUpdate(const Worker_ComponentU
bCanSpawnWithAuthority = true;
}
-void UGlobalStateManager::LinkExistingSingletonActor(const UClass* SingletonActorClass)
-{
- const Worker_EntityId* SingletonEntityIdPtr = SingletonNameToEntityId.Find(SingletonActorClass->GetPathName());
- if (SingletonEntityIdPtr == nullptr)
- {
- // No entry in SingletonNameToEntityId for this singleton class type
- UE_LOG(LogGlobalStateManager, Verbose, TEXT("LinkExistingSingletonActor %s failed to find entry"), *SingletonActorClass->GetName());
- return;
- }
-
- const Worker_EntityId SingletonEntityId = *SingletonEntityIdPtr;
- if (SingletonEntityId == SpatialConstants::INVALID_ENTITY_ID)
- {
- // Singleton Entity hasn't been created yet
- UE_LOG(LogGlobalStateManager, Log, TEXT("LinkExistingSingletonActor %s entity id is invalid"), *SingletonActorClass->GetName());
- return;
- }
-
- TPair* ActorChannelPair = SingletonClassPathToActorChannels.Find(SingletonActorClass->GetPathName());
- if (ActorChannelPair == nullptr)
- {
- // Dynamically spawn singleton actor if we have queued up data - ala USpatialReceiver::ReceiveActor - JIRA: 735
-
- // No local actor has registered itself as replicatible on this worker
- UE_LOG(LogGlobalStateManager, Log, TEXT("LinkExistingSingletonActor no actor registered for class %s"), *SingletonActorClass->GetName());
- return;
- }
-
- AActor* SingletonActor = ActorChannelPair->Key;
- USpatialActorChannel*& Channel = ActorChannelPair->Value;
-
- if (Channel != nullptr)
- {
- // Channel has already been setup
- UE_LOG(LogGlobalStateManager, Verbose, TEXT("UGlobalStateManager::LinkExistingSingletonActor channel already setup for %s"), *SingletonActorClass->GetName());
- return;
- }
-
- // If we have previously queued up data for this entity, apply it - UNR-734
-
- // We're now ready to start replicating this actor, create a channel
- USpatialNetConnection* Connection = Cast(NetDriver->ClientConnections[0]);
-
- Channel = Cast(Connection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::OpenedLocally));
-
- if (StaticComponentView->HasAuthority(SingletonEntityId, SpatialConstants::POSITION_COMPONENT_ID))
- {
- SingletonActor->Role = ROLE_Authority;
- SingletonActor->RemoteRole = ROLE_SimulatedProxy;
- }
- else
- {
- SingletonActor->Role = ROLE_SimulatedProxy;
- SingletonActor->RemoteRole = ROLE_Authority;
- }
-
- // Since the entity already exists, we have to handle setting up the PackageMap properly for this Actor
- NetDriver->PackageMap->ResolveEntityActor(SingletonActor, SingletonEntityId);
-
-#if ENGINE_MINOR_VERSION <= 22
- Channel->SetChannelActor(SingletonActor);
-#else
- Channel->SetChannelActor(SingletonActor, ESetChannelActorFlags::None);
-#endif
-
- UE_LOG(LogGlobalStateManager, Log, TEXT("Linked Singleton Actor %s with id %d"), *SingletonActor->GetClass()->GetName(), SingletonEntityId);
-}
-
-void UGlobalStateManager::LinkAllExistingSingletonActors()
-{
- // Early out for clients as they receive Singleton Actors via the normal Unreal replicated actor flow
- if (!NetDriver->IsServer())
- {
- return;
- }
-
- for (const auto& Pair : SingletonNameToEntityId)
- {
- UClass* SingletonActorClass = LoadObject(nullptr, *Pair.Key);
- if (SingletonActorClass == nullptr)
- {
- UE_LOG(LogGlobalStateManager, Error, TEXT("Failed to find Singleton Actor Class: %s"), *Pair.Key);
- continue;
- }
-
- LinkExistingSingletonActor(SingletonActorClass);
- }
-}
-
-USpatialActorChannel* UGlobalStateManager::AddSingleton(AActor* SingletonActor)
-{
- check(SingletonActor->GetIsReplicated());
-
- UClass* SingletonActorClass = SingletonActor->GetClass();
-
- TPair& ActorChannelPair = SingletonClassPathToActorChannels.FindOrAdd(SingletonActorClass->GetPathName());
- USpatialActorChannel*& Channel = ActorChannelPair.Value;
- check(ActorChannelPair.Key == nullptr || ActorChannelPair.Key == SingletonActor);
- ActorChannelPair.Key = SingletonActor;
-
- // Just return the channel if it's already been setup
- if (Channel != nullptr)
- {
- UE_LOG(LogGlobalStateManager, Log, TEXT("AddSingleton called when channel already setup: %s"), *SingletonActor->GetName());
- return Channel;
- }
-
- bool bHasGSMAuthority = NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID);
- if (bHasGSMAuthority)
- {
- // We have control over the GSM, so can safely setup a new channel and let it allocate an entity id
- USpatialNetConnection* Connection = Cast(NetDriver->ClientConnections[0]);
- Channel = Cast(Connection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::OpenedLocally));
-
- // If entity id already exists for this singleton, set the actor to it
- // Otherwise SetChannelActor will issue a new entity id request
- if (const Worker_EntityId* SingletonEntityId = SingletonNameToEntityId.Find(SingletonActorClass->GetPathName()))
- {
- check(NetDriver->PackageMap->GetObjectFromEntityId(*SingletonEntityId) == nullptr);
- NetDriver->PackageMap->ResolveEntityActor(SingletonActor, *SingletonEntityId);
- if (!StaticComponentView->HasAuthority(*SingletonEntityId, SpatialConstants::POSITION_COMPONENT_ID))
- {
- SingletonActor->Role = ROLE_SimulatedProxy;
- SingletonActor->RemoteRole = ROLE_Authority;
- }
- }
-
-#if ENGINE_MINOR_VERSION <= 22
- Channel->SetChannelActor(SingletonActor);
-#else
- Channel->SetChannelActor(SingletonActor, ESetChannelActorFlags::None);
-#endif
- UE_LOG(LogGlobalStateManager, Log, TEXT("Started replication of Singleton Actor %s"), *SingletonActorClass->GetName());
- }
- else
- {
- // We don't have control over the GSM, but we may have received the entity id for this singleton already
- LinkExistingSingletonActor(SingletonActorClass);
- }
-
- return Channel;
-}
-
-void UGlobalStateManager::RemoveSingletonInstance(const AActor* SingletonActor)
-{
- check(SingletonActor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton));
-
- SingletonClassPathToActorChannels.Remove(SingletonActor->GetClass()->GetPathName());
-}
-
-void UGlobalStateManager::RemoveAllSingletons()
-{
- SingletonClassPathToActorChannels.Reset();
-}
-
-void UGlobalStateManager::RegisterSingletonChannel(AActor* SingletonActor, USpatialActorChannel* SingletonChannel)
-{
- TPair& ActorChannelPair = SingletonClassPathToActorChannels.FindOrAdd(SingletonActor->GetClass()->GetPathName());
-
- check(ActorChannelPair.Key == nullptr || ActorChannelPair.Key == SingletonActor);
- check(ActorChannelPair.Value == nullptr || ActorChannelPair.Value == SingletonChannel);
-
- ActorChannelPair.Key = SingletonActor;
- ActorChannelPair.Value = SingletonChannel;
-}
-
-void UGlobalStateManager::ExecuteInitialSingletonActorReplication()
-{
- for (auto& ClassToActorChannel : SingletonClassPathToActorChannels)
- {
- auto& ActorChannelPair = ClassToActorChannel.Value;
- AddSingleton(ActorChannelPair.Key);
- }
-}
-
-void UGlobalStateManager::UpdateSingletonEntityId(const FString& ClassName, const Worker_EntityId SingletonEntityId)
-{
- Worker_EntityId& EntityId = SingletonNameToEntityId.FindOrAdd(ClassName);
- EntityId = SingletonEntityId;
-
- if (!NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID))
- {
- UE_LOG(LogGlobalStateManager, Warning, TEXT("UpdateSingletonEntityId: no authority over the GSM! Update will not be sent. Singleton class: %s, entity: %lld"), *ClassName, SingletonEntityId);
- return;
- }
-
- FWorkerComponentUpdate Update = {};
- Update.component_id = SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID;
- Update.schema_type = Schema_CreateComponentUpdate();
- Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update.schema_type);
-
- AddStringToEntityMapToSchema(UpdateObject, 1, SingletonNameToEntityId);
-
- NetDriver->Connection->SendComponentUpdate(GlobalStateManagerEntityId, &Update);
-}
-
-bool UGlobalStateManager::IsSingletonEntity(Worker_EntityId EntityId) const
-{
- for (const auto& Pair : SingletonNameToEntityId)
- {
- if (Pair.Value == EntityId)
- {
- return true;
- }
- }
- return false;
-}
-
void UGlobalStateManager::SetDeploymentState()
{
check(NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID));
+ UWorld* CurrentWorld = NetDriver->GetWorld();
+
// Send the component update that we can now accept players.
- UE_LOG(LogGlobalStateManager, Log, TEXT("Setting deployment URL to '%s'"), *NetDriver->GetWorld()->URL.Map);
+ UE_LOG(LogGlobalStateManager, Log, TEXT("Setting deployment URL to '%s'"), *CurrentWorld->URL.Map);
UE_LOG(LogGlobalStateManager, Log, TEXT("Setting schema hash to '%u'"), NetDriver->ClassInfoManager->SchemaDatabase->SchemaDescriptorHash);
FWorkerComponentUpdate Update = {};
@@ -459,7 +240,7 @@ void UGlobalStateManager::SetDeploymentState()
Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update.schema_type);
// Set the map URL on the GSM.
- AddStringToSchema(UpdateObject, SpatialConstants::DEPLOYMENT_MAP_MAP_URL_ID, NetDriver->GetWorld()->URL.Map);
+ AddStringToSchema(UpdateObject, SpatialConstants::DEPLOYMENT_MAP_MAP_URL_ID, CurrentWorld->RemovePIEPrefix(CurrentWorld->URL.Map));
// Set the schema hash for connecting workers to check against
Schema_AddUint32(UpdateObject, SpatialConstants::DEPLOYMENT_MAP_SCHEMA_HASH, NetDriver->ClassInfoManager->SchemaDatabase->SchemaDescriptorHash);
@@ -516,11 +297,6 @@ void UGlobalStateManager::AuthorityChanged(const Worker_AuthorityChangeOp& AuthO
SetAcceptingPlayers(true);
break;
}
- case SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID:
- {
- ExecuteInitialSingletonActorReplication();
- break;
- }
case SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID:
{
// The bCanSpawnWithAuthority member determines whether a server-side worker
@@ -548,7 +324,6 @@ bool UGlobalStateManager::HandlesComponent(const Worker_ComponentId ComponentId)
{
switch (ComponentId)
{
- case SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID:
case SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID:
case SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID:
case SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID:
@@ -560,21 +335,12 @@ bool UGlobalStateManager::HandlesComponent(const Worker_ComponentId ComponentId)
void UGlobalStateManager::ResetGSM()
{
- UE_LOG(LogGlobalStateManager, Display, TEXT("GlobalStateManager singletons are being reset. Session restarting."));
+ UE_LOG(LogGlobalStateManager, Display, TEXT("GlobalStateManager not accepting players and resetting BeginPlay lifecycle properties. Session restarting."));
- SingletonNameToEntityId.Empty();
SetAcceptingPlayers(false);
// Reset the BeginPlay flag so Startup Actors are properly managed.
SendCanBeginPlayUpdate(false);
-
- // Reset the Singleton map so Singletons are recreated.
- FWorkerComponentUpdate Update = {};
- Update.component_id = SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID;
- Update.schema_type = Schema_CreateComponentUpdate();
- Schema_AddComponentUpdateClearedField(Update.schema_type, SpatialConstants::SINGLETON_MANAGER_SINGLETON_NAME_TO_ENTITY_ID);
-
- NetDriver->Connection->SendComponentUpdate(GlobalStateManagerEntityId, &Update);
}
void UGlobalStateManager::BeginDestroy()
@@ -590,26 +356,17 @@ void UGlobalStateManager::BeginDestroy()
// Reset the BeginPlay flag so Startup Actors are properly managed.
SendCanBeginPlayUpdate(false);
- // Reset the Singleton map so Singletons are recreated.
- FWorkerComponentUpdate Update = {};
- Update.component_id = SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID;
- Update.schema_type = Schema_CreateComponentUpdate();
- Schema_AddComponentUpdateClearedField(Update.schema_type, SpatialConstants::SINGLETON_MANAGER_SINGLETON_NAME_TO_ENTITY_ID);
-
- NetDriver->Connection->SendComponentUpdate(GlobalStateManagerEntityId, &Update);
+ // Flush the connection and wait a moment to allow the message to propagate.
+ // TODO: UNR-3697 - This needs to be handled more correctly
+ NetDriver->Connection->Flush();
+ FPlatformProcess::Sleep(0.1f);
}
}
#endif
}
-void UGlobalStateManager::BecomeAuthoritativeOverAllActors()
+void UGlobalStateManager::SetAllActorRolesBasedOnLBStrategy()
{
- // This logic is not used in offloading.
- if (USpatialStatics::IsSpatialOffloadingEnabled())
- {
- return;
- }
-
for (TActorIterator It(NetDriver->World); It; ++It)
{
AActor* Actor = *It;
@@ -617,24 +374,9 @@ void UGlobalStateManager::BecomeAuthoritativeOverAllActors()
{
if (Actor->GetIsReplicated())
{
- Actor->Role = ROLE_Authority;
- Actor->RemoteRole = ROLE_SimulatedProxy;
- }
- }
- }
-}
-
-void UGlobalStateManager::BecomeAuthoritativeOverActorsBasedOnLBStrategy()
-{
- for (TActorIterator It(NetDriver->World); It; ++It)
- {
- AActor* Actor = *It;
- if (Actor != nullptr && !Actor->IsPendingKill())
- {
- if (Actor->GetIsReplicated() && NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*Actor))
- {
- Actor->Role = ROLE_Authority;
- Actor->RemoteRole = ROLE_SimulatedProxy;
+ const bool bAuthoritative = NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*Actor);
+ Actor->Role = bAuthoritative ? ROLE_Authority : ROLE_SimulatedProxy;
+ Actor->RemoteRole = bAuthoritative ? ROLE_SimulatedProxy : ROLE_Authority;
}
}
}
@@ -654,14 +396,7 @@ void UGlobalStateManager::TriggerBeginPlay()
// If we're loading from a snapshot, we shouldn't try and call BeginPlay with authority.
if (bCanSpawnWithAuthority)
{
- if (GetDefault()->bEnableUnrealLoadBalancer)
- {
- BecomeAuthoritativeOverActorsBasedOnLBStrategy();
- }
- else
- {
- BecomeAuthoritativeOverAllActors();
- }
+ SetAllActorRolesBasedOnLBStrategy();
}
NetDriver->World->GetWorldSettings()->SetGSMReadyForPlay();
@@ -699,6 +434,7 @@ void UGlobalStateManager::SendCanBeginPlayUpdate(const bool bInCanBeginPlay)
// This is so clients know when to connect to the deployment.
void UGlobalStateManager::QueryGSM(const QueryDelegate& Callback)
{
+ // Build a constraint for the GSM.
Worker_ComponentConstraint GSMComponentConstraint{};
GSMComponentConstraint.component_id = SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID;
@@ -726,10 +462,6 @@ void UGlobalStateManager::QueryGSM(const QueryDelegate& Callback)
}
else
{
- if (NetDriver->VirtualWorkerTranslator.IsValid())
- {
- ApplyVirtualWorkerMappingFromQueryResponse(Op);
- }
ApplyDeploymentMapDataFromQueryResponse(Op);
Callback.ExecuteIfBound(Op);
}
@@ -738,7 +470,53 @@ void UGlobalStateManager::QueryGSM(const QueryDelegate& Callback)
Receiver->AddEntityQueryDelegate(RequestID, GSMQueryDelegate);
}
-void UGlobalStateManager::ApplyVirtualWorkerMappingFromQueryResponse(const Worker_EntityQueryResponseOp& Op)
+void UGlobalStateManager::QueryTranslation()
+{
+ if (bTranslationQueryInFlight)
+ {
+ // Only allow one in flight query. Retries will be handled by the SpatialNetDriver.
+ return;
+ }
+
+ // Build a constraint for the Virtual Worker Translation.
+ Worker_ComponentConstraint TranslationComponentConstraint{};
+ TranslationComponentConstraint.component_id = SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID;
+
+ Worker_Constraint TranslationConstraint{};
+ TranslationConstraint.constraint_type = WORKER_CONSTRAINT_TYPE_COMPONENT;
+ TranslationConstraint.constraint.component_constraint = TranslationComponentConstraint;
+
+ Worker_EntityQuery TranslationQuery{};
+ TranslationQuery.constraint = TranslationConstraint;
+ TranslationQuery.result_type = WORKER_RESULT_TYPE_SNAPSHOT;
+
+ Worker_RequestId RequestID = NetDriver->Connection->SendEntityQueryRequest(&TranslationQuery);
+ bTranslationQueryInFlight = true;
+
+ TWeakObjectPtr WeakGlobalStateManager(this);
+ EntityQueryDelegate TranslationQueryDelegate;
+ TranslationQueryDelegate.BindLambda([WeakGlobalStateManager](const Worker_EntityQueryResponseOp& Op)
+ {
+ if (!WeakGlobalStateManager.IsValid())
+ {
+ // The GSM was destroyed before receiving the response.
+ return;
+ }
+
+ UGlobalStateManager* GlobalStateManager = WeakGlobalStateManager.Get();
+ if (Op.status_code == WORKER_STATUS_CODE_SUCCESS)
+ {
+ if (GlobalStateManager->NetDriver->VirtualWorkerTranslator.IsValid())
+ {
+ GlobalStateManager->ApplyVirtualWorkerMappingFromQueryResponse(Op);
+ }
+ }
+ GlobalStateManager->bTranslationQueryInFlight = false;
+ });
+ Receiver->AddEntityQueryDelegate(RequestID, TranslationQueryDelegate);
+}
+
+void UGlobalStateManager::ApplyVirtualWorkerMappingFromQueryResponse(const Worker_EntityQueryResponseOp& Op) const
{
check(NetDriver->VirtualWorkerTranslator.IsValid());
for (uint32_t i = 0; i < Op.results[0].component_count; i++)
diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp
index 34fa9ee48a..a29d1c0ade 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp
@@ -17,19 +17,17 @@
#include "EngineClasses/SpatialNetDriver.h"
#include "EngineClasses/SpatialPackageMapClient.h"
-#include "Utils/SpatialActorGroupManager.h"
+#include "EngineClasses/SpatialWorldSettings.h"
+#include "LoadBalancing/AbstractLBStrategy.h"
#include "Utils/RepLayoutUtils.h"
DEFINE_LOG_CATEGORY(LogSpatialClassInfoManager);
-bool USpatialClassInfoManager::TryInit(USpatialNetDriver* InNetDriver, SpatialActorGroupManager* InActorGroupManager)
+bool USpatialClassInfoManager::TryInit(USpatialNetDriver* InNetDriver)
{
check(InNetDriver != nullptr);
NetDriver = InNetDriver;
- check(InActorGroupManager != nullptr);
- ActorGroupManager = InActorGroupManager;
-
FSoftObjectPath SchemaDatabasePath = FSoftObjectPath(FPaths::SetExtension(SpatialConstants::SCHEMA_DATABASE_ASSET_PATH, TEXT(".SchemaDatabase")));
SchemaDatabase = Cast(SchemaDatabasePath.TryLoad());
@@ -138,13 +136,12 @@ void USpatialClassInfoManager::CreateClassInfoForClass(UClass* Class)
Info->RPCInfoMap.Add(RemoteFunction, RPCInfo);
}
- const bool bEnableHandover = GetDefault()->bEnableHandover;
-
+ const bool bTrackHandoverProperties = ShouldTrackHandoverProperties();
for (TFieldIterator PropertyIt(Class); PropertyIt; ++PropertyIt)
{
UProperty* Property = *PropertyIt;
- if (bEnableHandover && (Property->PropertyFlags & CPF_Handover))
+ if (bTrackHandoverProperties && (Property->PropertyFlags & CPF_Handover))
{
for (int32 ArrayIdx = 0; ArrayIdx < PropertyIt->ArrayDim; ++ArrayIdx)
{
@@ -187,7 +184,7 @@ void USpatialClassInfoManager::FinishConstructingActorClassInfo(const FString& C
{
Worker_ComponentId ComponentId = SchemaDatabase->ActorClassPathToSchema[ClassPath].SchemaComponents[Type];
- if (!GetDefault()->bEnableHandover && Type == SCHEMA_Handover)
+ if (!ShouldTrackHandoverProperties() && Type == SCHEMA_Handover)
{
return;
}
@@ -221,7 +218,7 @@ void USpatialClassInfoManager::FinishConstructingActorClassInfo(const FString& C
ForAllSchemaComponentTypes([&](ESchemaComponentType Type)
{
- if (!GetDefault()->bEnableHandover && Type == SCHEMA_Handover)
+ if (!ShouldTrackHandoverProperties() && Type == SCHEMA_Handover)
{
return;
}
@@ -238,18 +235,6 @@ void USpatialClassInfoManager::FinishConstructingActorClassInfo(const FString& C
Info->SubobjectInfo.Add(Offset, ActorSubobjectInfo);
}
-
- if (UClass* ActorClass = Info->Class.Get())
- {
- if (ActorClass->IsChildOf())
- {
- Info->ActorGroup = ActorGroupManager->GetActorGroupForClass(TSubclassOf(ActorClass));
- Info->WorkerType = ActorGroupManager->GetWorkerTypeForClass(TSubclassOf(ActorClass));
-
- UE_LOG(LogSpatialClassInfoManager, VeryVerbose, TEXT("[%s] is in ActorGroup [%s], on WorkerType [%s]"),
- *ActorClass->GetPathName(), *Info->ActorGroup.ToString(), *Info->WorkerType.ToString())
- }
- }
}
void USpatialClassInfoManager::FinishConstructingSubobjectClassInfo(const FString& ClassPath, TSharedRef& Info)
@@ -279,6 +264,23 @@ void USpatialClassInfoManager::FinishConstructingSubobjectClassInfo(const FStrin
}
}
+bool USpatialClassInfoManager::ShouldTrackHandoverProperties() 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.
+ if (!NetDriver->IsServer())
+ {
+ return true;
+ }
+
+ const USpatialGDKSettings* Settings = GetDefault();
+
+ const UAbstractLBStrategy* Strategy = NetDriver->LoadBalanceStrategy;
+ check(Strategy != nullptr);
+ return Strategy->RequiresHandoverData() || Settings->bEnableHandover;
+}
+
void USpatialClassInfoManager::TryCreateClassInfoForComponentId(Worker_ComponentId ComponentId)
{
if (FString* ClassPath = SchemaDatabase->ComponentIdToClassPath.Find(ComponentId))
@@ -301,7 +303,7 @@ const FClassInfo& USpatialClassInfoManager::GetOrCreateClassInfoByClass(UClass*
{
CreateClassInfoForClass(Class);
}
-
+
return ClassInfoMap[Class].Get();
}
diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp
index b0d5d3dea2..0d4a7ba1d3 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp
@@ -51,19 +51,20 @@ void USpatialPlayerSpawner::SendPlayerSpawnRequest()
SpatialSpawnerQuery.constraint = SpatialSpawnerConstraint;
SpatialSpawnerQuery.result_type = WORKER_RESULT_TYPE_SNAPSHOT;
- Worker_RequestId RequestID;
- RequestID = NetDriver->Connection->SendEntityQueryRequest(&SpatialSpawnerQuery);
+ const Worker_RequestId RequestID = NetDriver->Connection->SendEntityQueryRequest(&SpatialSpawnerQuery);
EntityQueryDelegate SpatialSpawnerQueryDelegate;
SpatialSpawnerQueryDelegate.BindLambda([this, RequestID](const Worker_EntityQueryResponseOp& Op)
{
+ FString Reason;
+
if (Op.status_code != WORKER_STATUS_CODE_SUCCESS)
{
- UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Entity query for SpatialSpawner failed: %s"), UTF8_TO_TCHAR(Op.message));
+ Reason = FString::Printf(TEXT("Entity query for SpatialSpawner failed: %s"), UTF8_TO_TCHAR(Op.message));
}
else if (Op.result_count == 0)
{
- UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Could not find SpatialSpawner via entity query: %s"), UTF8_TO_TCHAR(Op.message));
+ Reason = FString::Printf(TEXT("Could not find SpatialSpawner via entity query: %s"), UTF8_TO_TCHAR(Op.message));
}
else
{
@@ -73,6 +74,12 @@ void USpatialPlayerSpawner::SendPlayerSpawnRequest()
Worker_CommandRequest SpawnPlayerCommandRequest = PlayerSpawner::CreatePlayerSpawnRequest(SpawnRequest);
NetDriver->Connection->SendCommandRequest(Op.results[0].entity_id, &SpawnPlayerCommandRequest, SpatialConstants::PLAYER_SPAWNER_SPAWN_PLAYER_COMMAND_ID);
}
+
+ if (!Reason.IsEmpty())
+ {
+ UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("%s"), *Reason);
+ OnPlayerSpawnFailed.ExecuteIfBound(Reason);
+ }
});
UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Sending player spawn request"));
@@ -85,7 +92,7 @@ SpatialGDK::SpawnPlayerRequest USpatialPlayerSpawner::ObtainPlayerParams() const
{
FURL LoginURL;
FUniqueNetIdRepl UniqueId;
-
+
const FWorldContext* const WorldContext = GEngine->GetWorldContextFromWorld(NetDriver->GetWorld());
check(WorldContext->OwningGameInstance);
@@ -96,7 +103,7 @@ SpatialGDK::SpawnPlayerRequest USpatialPlayerSpawner::ObtainPlayerParams() const
if (const ULocalPlayer* LocalPlayer = WorldContext->OwningGameInstance->GetFirstGamePlayer())
{
// Send the player nickname if available
- FString OverrideName = LocalPlayer->GetNickname();
+ const FString OverrideName = LocalPlayer->GetNickname();
if (OverrideName.Len() > 0)
{
LoginURL.AddOption(*FString::Printf(TEXT("Name=%s"), *OverrideName));
@@ -131,7 +138,7 @@ SpatialGDK::SpawnPlayerRequest USpatialPlayerSpawner::ObtainPlayerParams() const
UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Couldn't get LocalPlayer data from game instance when trying to spawn player."));
}
- FName OnlinePlatformName = WorldContext->OwningGameInstance->GetOnlinePlatformName();
+ const FName OnlinePlatformName = WorldContext->OwningGameInstance->GetOnlinePlatformName();
return { LoginURL, UniqueId, OnlinePlatformName, bIsSimulatedPlayer };
}
@@ -158,8 +165,10 @@ void USpatialPlayerSpawner::ReceivePlayerSpawnResponseOnClient(const Worker_Comm
}
else
{
- UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Player spawn request failed too many times. (%u attempts)"),
+ FString Reason = FString::Printf(TEXT("Player spawn request failed too many times. (%u attempts)"),
SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS);
+ UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("%s"), *Reason);
+ OnPlayerSpawnFailed.ExecuteIfBound(Reason);
}
}
@@ -167,7 +176,7 @@ void USpatialPlayerSpawner::ReceivePlayerSpawnRequestOnServer(const Worker_Comma
{
UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Received PlayerSpawn request on server"));
- FUTF8ToTCHAR FStringConversion(reinterpret_cast(Op.caller_worker_id), strlen(Op.caller_worker_id));
+ const FUTF8ToTCHAR FStringConversion(reinterpret_cast(Op.caller_worker_id), strlen(Op.caller_worker_id));
FString ClientWorkerId(FStringConversion.Length(), FStringConversion.Get());
// Accept the player if we have not already accepted a player from this worker.
@@ -188,43 +197,68 @@ void USpatialPlayerSpawner::ReceivePlayerSpawnRequestOnServer(const Worker_Comma
void USpatialPlayerSpawner::FindPlayerStartAndProcessPlayerSpawn(Schema_Object* SpawnPlayerRequest, const PhysicalWorkerName& ClientWorkerId)
{
- // If load-balancing is enabled AND the strategy dictates that another worker should have authority over
- // the chosen PlayerStart THEN the spawn request is forwarded to that worker to prevent an initial player
- // migration. Immediate player migrations can still happen if
- // 1) the load-balancing strategy has different rules for PlayerStart Actors and Characters / Controllers /
- // Player States or,
- // 2) the load-balancing strategy can change the authoritative virtual worker ID for a PlayerStart Actor
- // during the lifetime of a deployment.
- if (GetDefault()->bEnableUnrealLoadBalancer)
+ // If the load balancing strategy dictates that this worker should have authority over the chosen PlayerStart THEN the spawn is handled locally,
+ // Else if the the PlayerStart is handled by another worker THEN forward the request to that worker to prevent an initial player migration,
+ // Else if a PlayerStart can't be found THEN we could be on the wrong worker type, so forward to the GameMode authoritative server.
+ //
+ // This implementation depends on:
+ // 1) the load-balancing strategy having the same rules for PlayerStart Actors and Characters / Controllers / Player States or,
+ // 2) the authoritative virtual worker ID for a PlayerStart Actor not changing during the lifetime of a deployment.
+ check (NetDriver->LoadBalanceStrategy != nullptr)
+
+ // We need to specifically extract the URL from the PlayerSpawn request for finding a PlayerStart.
+ const FURL Url = PlayerSpawner::ExtractUrlFromPlayerSpawnParams(SpawnPlayerRequest);
+
+ // Find a PlayerStart Actor on this server.
+ AActor* PlayerStartActor = NetDriver->GetWorld()->GetAuthGameMode()->FindPlayerStart(nullptr, Url.Portal);
+
+ // If the PlayerStart is authoritative locally, spawn the player locally.
+ if (PlayerStartActor != nullptr && NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*PlayerStartActor))
{
- // We need to specifically extract the URL from the PlayerSpawn request for finding a PlayerStart.
- const FURL Url = PlayerSpawner::ExtractUrlFromPlayerSpawnParams(SpawnPlayerRequest);
- AActor* PlayerStartActor = NetDriver->GetWorld()->GetAuthGameMode()->FindPlayerStart(nullptr, Url.Portal);
+ UE_LOG(LogSpatialPlayerSpawner, Verbose, TEXT("Handling SpawnPlayerRequest request locally. Client worker ID: %s."), *ClientWorkerId);
+ PassSpawnRequestToNetDriver(SpawnPlayerRequest, PlayerStartActor);
+ return;
+ }
+
+ VirtualWorkerId VirtualWorkerToForwardTo = SpatialConstants::INVALID_VIRTUAL_WORKER_ID;
- check(NetDriver->LoadBalanceStrategy != nullptr);
- if (!NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*PlayerStartActor))
+ // If we can't find a PlayerStart Actor, the PlayerSpawner authoritative worker may be part of a layer
+ // which has a limited view of the world and/or shouldn't be processing player spawning. In this case,
+ // we attempt to forward to the worker authoritative over the GameMode, as we assume the FindPlayerStart
+ // implementation may depend on authoritative game mode logic. We pass a null object ref so that the
+ // forwarded worker knows to search for a PlayerStart.
+ if (PlayerStartActor == nullptr)
+ {
+ VirtualWorkerToForwardTo = NetDriver->LoadBalanceStrategy->WhoShouldHaveAuthority(*UGameplayStatics::GetGameMode(GetWorld()));
+ if (VirtualWorkerToForwardTo == SpatialConstants::INVALID_VIRTUAL_WORKER_ID)
{
- // If we fail to forward the spawn request, we default to the normal player spawning flow.
- const bool bSuccessfullyForwardedRequest = ForwardSpawnRequestToStrategizedServer(SpawnPlayerRequest, PlayerStartActor, ClientWorkerId);
- if (bSuccessfullyForwardedRequest)
- {
- return;
- }
+ UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("The server authoritative over the GameMode could not locate any PlayerStart, this is unsupported."));
}
- else
+ }
+ else if (!NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*PlayerStartActor))
+ {
+ VirtualWorkerToForwardTo = NetDriver->LoadBalanceStrategy->WhoShouldHaveAuthority(*PlayerStartActor);
+ if (VirtualWorkerToForwardTo == SpatialConstants::INVALID_VIRTUAL_WORKER_ID)
{
- UE_LOG(LogSpatialPlayerSpawner, Verbose, TEXT("Handling SpawnPlayerRequest request locally. Client worker ID: %s."), *ClientWorkerId);
- PassSpawnRequestToNetDriver(SpawnPlayerRequest, PlayerStartActor);
- return;
+ UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Load-balance strategy returned invalid virtual worker ID for selected PlayerStart Actor: %s"),
+ *GetNameSafe(PlayerStartActor));
}
}
- PassSpawnRequestToNetDriver(SpawnPlayerRequest, nullptr);
+ // If the load balancing strategy returns invalid virtual worker IDs for the PlayerStart, we should error.
+ if (VirtualWorkerToForwardTo == SpatialConstants::INVALID_VIRTUAL_WORKER_ID)
+ {
+ UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Defaulting to normal player spawning flow."));
+ PassSpawnRequestToNetDriver(SpawnPlayerRequest, nullptr);
+ return;
+ }
+
+ ForwardSpawnRequestToStrategizedServer(SpawnPlayerRequest, PlayerStartActor, ClientWorkerId, VirtualWorkerToForwardTo);
}
-void USpatialPlayerSpawner::PassSpawnRequestToNetDriver(Schema_Object* PlayerSpawnData, AActor* PlayerStart)
+void USpatialPlayerSpawner::PassSpawnRequestToNetDriver(const Schema_Object* PlayerSpawnData, AActor* PlayerStart)
{
- SpatialGDK::SpawnPlayerRequest SpawnRequest = PlayerSpawner::ExtractPlayerSpawnParams(PlayerSpawnData);
+ const SpatialGDK::SpawnPlayerRequest SpawnRequest = PlayerSpawner::ExtractPlayerSpawnParams(PlayerSpawnData);
AGameModeBase* GameMode = NetDriver->GetWorld()->GetAuthGameMode();
@@ -234,31 +268,29 @@ void USpatialPlayerSpawner::PassSpawnRequestToNetDriver(Schema_Object* PlayerSpa
GameMode->SetPrioritizedPlayerStart(nullptr);
}
-// Copies the fields from the SpawnPlayerRequest argument into a ForwardSpawnPlayerRequest (along with the PlayerStart UnrealObjectRef).
-bool USpatialPlayerSpawner::ForwardSpawnRequestToStrategizedServer(const Schema_Object* OriginalPlayerSpawnRequest, AActor* PlayerStart, const PhysicalWorkerName& ClientWorkerId)
+void USpatialPlayerSpawner::ForwardSpawnRequestToStrategizedServer(const Schema_Object* OriginalPlayerSpawnRequest, AActor* PlayerStart, const PhysicalWorkerName& ClientWorkerId, const VirtualWorkerId SpawningVirtualWorker)
{
- // Find which virtual worker should have authority of the PlayerStart.
- const VirtualWorkerId SpawningVirtualWorker = NetDriver->LoadBalanceStrategy->WhoShouldHaveAuthority(*PlayerStart);
- if (SpawningVirtualWorker == SpatialConstants::INVALID_VIRTUAL_WORKER_ID)
- {
- UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Load-balance strategy returned invalid virtual worker ID for selected PlayerStart Actor: %s. Defaulting to normal player spawning flow."), *GetNameSafe(PlayerStart));
- return false;
- }
+ UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Forwarding player spawn request to strategized worker. Client ID: %s. PlayerStart: %s. Strategeized virtual worker %d"),
+ *ClientWorkerId, *GetNameSafe(PlayerStart), SpawningVirtualWorker);
// Find the server worker entity corresponding to the PlayerStart strategized virtual worker.
const Worker_EntityId ServerWorkerEntity = NetDriver->VirtualWorkerTranslator->GetServerWorkerEntityForVirtualWorker(SpawningVirtualWorker);
if (ServerWorkerEntity == SpatialConstants::INVALID_ENTITY_ID)
{
- UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Virtual worker translator returned invalid server worker entity ID. Virtual worker: %d. Defaulting to normal player spawning flow."), SpawningVirtualWorker);
- return false;
+ UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Player spawning failed. Virtual worker translator returned invalid server worker entity ID. Virtual worker: %d. "
+ "Defaulting to normal player spawning flow."), SpawningVirtualWorker);
+ PassSpawnRequestToNetDriver(OriginalPlayerSpawnRequest, nullptr);
+ return;
}
- UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Forwarding player spawn request to strategized worker. Client ID: %s. PlayerStart: %s. Strategeized virtual worker %d. Forward server worker entity: %lld"),
- *ClientWorkerId, *GetNameSafe(PlayerStart), SpawningVirtualWorker, ServerWorkerEntity);
-
// To pass the PlayerStart Actor to another worker we use a FUnrealObjectRef.
- FNetworkGUID PlayerStartGuid = NetDriver->PackageMap->ResolveStablyNamedObject(PlayerStart);
- FUnrealObjectRef PlayerStartObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromNetGUID(PlayerStartGuid);
+ // The PlayerStartObjectRef can be null if we are trying to just forward the spawn request to the correct worker layer, rather than some specific PlayerStart authoritative worker.
+ FUnrealObjectRef PlayerStartObjectRef = FUnrealObjectRef::NULL_OBJECT_REF;
+ if (PlayerStart != nullptr)
+ {
+ const FNetworkGUID PlayerStartGuid = NetDriver->PackageMap->ResolveStablyNamedObject(PlayerStart);
+ PlayerStartObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromNetGUID(PlayerStartGuid);
+ }
// Create a request using the PlayerStart reference and by copying the data from the PlayerSpawn request from the client.
// The Schema_CommandRequest is constructed separately from the Worker_CommandRequest so we can store it in the outgoing
@@ -267,11 +299,9 @@ bool USpatialPlayerSpawner::ForwardSpawnRequestToStrategizedServer(const Schema_
ServerWorker::CreateForwardPlayerSpawnSchemaRequest(ForwardSpawnPlayerSchemaRequest, PlayerStartObjectRef, OriginalPlayerSpawnRequest, ClientWorkerId);
Worker_CommandRequest ForwardSpawnPlayerRequest = ServerWorker::CreateForwardPlayerSpawnRequest(Schema_CopyCommandRequest(ForwardSpawnPlayerSchemaRequest));
- Worker_RequestId RequestId = NetDriver->Connection->SendCommandRequest(ServerWorkerEntity, &ForwardSpawnPlayerRequest, SpatialConstants::SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND_ID);
+ const Worker_RequestId RequestId = NetDriver->Connection->SendCommandRequest(ServerWorkerEntity, &ForwardSpawnPlayerRequest, SpatialConstants::SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND_ID);
OutgoingForwardPlayerSpawnRequests.Add(RequestId, TUniquePtr(ForwardSpawnPlayerSchemaRequest));
-
- return true;
}
void USpatialPlayerSpawner::ReceiveForwardedPlayerSpawnRequest(const Worker_CommandRequestOp& Op)
@@ -289,20 +319,34 @@ void USpatialPlayerSpawner::ReceiveForwardedPlayerSpawnRequest(const Worker_Comm
return;
}
- FUnrealObjectRef PlayerStartRef = GetObjectRefFromSchema(Payload, SpatialConstants::FORWARD_SPAWN_PLAYER_START_ACTOR_ID);
+ bool bRequestHandledSuccessfully = true;
- bool bUnresolvedRef = false;
- if (AActor* PlayerStart = Cast(FUnrealObjectRef::ToObjectPtr(PlayerStartRef, NetDriver->PackageMap, bUnresolvedRef)))
+ const FUnrealObjectRef PlayerStartRef = GetObjectRefFromSchema(Payload, SpatialConstants::FORWARD_SPAWN_PLAYER_START_ACTOR_ID);
+ if (PlayerStartRef != FUnrealObjectRef::NULL_OBJECT_REF)
{
- UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Received ForwardPlayerSpawn request. Client worker ID: %s. PlayerStart: %s"), *ClientWorkerId, *PlayerStart->GetName());
+ bool bUnresolvedRef = false;
+ AActor* PlayerStart = Cast(FUnrealObjectRef::ToObjectPtr(PlayerStartRef, NetDriver->PackageMap, bUnresolvedRef));
+ bRequestHandledSuccessfully = !bUnresolvedRef;
+
+ if (bRequestHandledSuccessfully)
+ {
+ UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Received ForwardPlayerSpawn request. Client worker ID: %s. PlayerStart: %s"), *ClientWorkerId, *PlayerStart->GetName());
+ }
+ else
+ {
+ UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("PlayerStart Actor UnrealObjectRef was invalid on forwarded player spawn request worker: %s"), *ClientWorkerId);
+ }
+
PassSpawnRequestToNetDriver(PlayerSpawnData, PlayerStart);
}
else
{
- UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("PlayerStart Actor UnrealObjectRef was invalid on forwarded player spawn request worker: %s. Defaulting to normal player spawning flow."), *ClientWorkerId);
+ UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("PlayerStart Actor was null object ref in forward spawn request. This is intentional when handing request to the correct "
+ "load balancing layer. Attempting to find a player start again."));
+ FindPlayerStartAndProcessPlayerSpawn(PlayerSpawnData, ClientWorkerId);
}
- Worker_CommandResponse Response = ServerWorker::CreateForwardPlayerSpawnResponse(!bUnresolvedRef);
+ Worker_CommandResponse Response = ServerWorker::CreateForwardPlayerSpawnResponse(bRequestHandledSuccessfully);
NetDriver->Connection->SendCommandResponse(Op.request_id, &Response);
}
@@ -363,7 +407,7 @@ void USpatialPlayerSpawner::RetryForwardSpawnPlayerRequest(const Worker_EntityId
// Resend the ForwardSpawnPlayer request.
Worker_CommandRequest ForwardSpawnPlayerRequest = ServerWorker::CreateForwardPlayerSpawnRequest(Schema_CopyCommandRequest(OldRequest));
- Worker_RequestId NewRequestId = NetDriver->Connection->SendCommandRequest(EntityId, &ForwardSpawnPlayerRequest, SpatialConstants::SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND_ID);
+ const Worker_RequestId NewRequestId = NetDriver->Connection->SendCommandRequest(EntityId, &ForwardSpawnPlayerRequest, SpatialConstants::SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND_ID);
// Move the request data from the old request ID map entry across to the new ID entry.
OutgoingForwardPlayerSpawnRequests.Add(NewRequestId, TUniquePtr(OldRequest));
diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialRPCService.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialRPCService.cpp
index 63c85219da..eaeaa34b23 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialRPCService.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialRPCService.cpp
@@ -6,40 +6,50 @@
#include "Schema/ClientEndpoint.h"
#include "Schema/MulticastRPCs.h"
#include "Schema/ServerEndpoint.h"
+#include "Utils/SpatialLatencyTracer.h"
DEFINE_LOG_CATEGORY(LogSpatialRPCService);
namespace SpatialGDK
{
-SpatialRPCService::SpatialRPCService(ExtractRPCDelegate ExtractRPCCallback, const USpatialStaticComponentView* View)
+SpatialRPCService::SpatialRPCService(ExtractRPCDelegate ExtractRPCCallback, const USpatialStaticComponentView* View, USpatialLatencyTracer* SpatialLatencyTracer)
: ExtractRPCCallback(ExtractRPCCallback)
, View(View)
+ , SpatialLatencyTracer(SpatialLatencyTracer)
{
}
-EPushRPCResult SpatialRPCService::PushRPC(Worker_EntityId EntityId, ERPCType Type, RPCPayload Payload)
+EPushRPCResult SpatialRPCService::PushRPC(Worker_EntityId EntityId, ERPCType Type, RPCPayload Payload, bool bCreatedEntity)
{
EntityRPCType EntityType = EntityRPCType(EntityId, Type);
+ EPushRPCResult Result = EPushRPCResult::Success;
+
if (RPCRingBufferUtils::ShouldQueueOverflowed(Type) && OverflowedRPCs.Contains(EntityType))
{
// Already has queued RPCs of this type, queue until those are pushed.
AddOverflowedRPC(EntityType, MoveTemp(Payload));
- return EPushRPCResult::QueueOverflowed;
+ Result = EPushRPCResult::QueueOverflowed;
}
-
- EPushRPCResult Result = PushRPCInternal(EntityId, Type, MoveTemp(Payload));
-
- if (Result == EPushRPCResult::QueueOverflowed)
+ else
{
- AddOverflowedRPC(EntityType, MoveTemp(Payload));
+ Result = PushRPCInternal(EntityId, Type, MoveTemp(Payload), bCreatedEntity);
+
+ if (Result == EPushRPCResult::QueueOverflowed)
+ {
+ AddOverflowedRPC(EntityType, MoveTemp(Payload));
+ }
}
+#if TRACE_LIB_ACTIVE
+ ProcessResultToLatencyTrace(Result, Payload.Trace);
+#endif
+
return Result;
}
-EPushRPCResult SpatialRPCService::PushRPCInternal(Worker_EntityId EntityId, ERPCType Type, RPCPayload&& Payload)
+EPushRPCResult SpatialRPCService::PushRPCInternal(Worker_EntityId EntityId, ERPCType Type, RPCPayload&& Payload, bool bCreatedEntity)
{
const Worker_ComponentId RingBufferComponentId = RPCRingBufferUtils::GetRingBufferComponentId(Type);
@@ -52,6 +62,10 @@ EPushRPCResult SpatialRPCService::PushRPCInternal(Worker_EntityId EntityId, ERPC
{
if (!View->HasAuthority(EntityId, RingBufferComponentId))
{
+ if (bCreatedEntity)
+ {
+ return EPushRPCResult::EntityBeingCreated;
+ }
return EPushRPCResult::NoRingBufferAuthority;
}
@@ -75,6 +89,10 @@ EPushRPCResult SpatialRPCService::PushRPCInternal(Worker_EntityId EntityId, ERPC
}
else
{
+ if (bCreatedEntity)
+ {
+ return EPushRPCResult::EntityBeingCreated;
+ }
// If the entity isn't in the view, we assume this RPC was called before
// CreateEntityRequest, so we put it into a component data object.
EndpointObject = Schema_GetComponentDataFields(GetOrCreateComponentData(EntityComponent));
@@ -89,6 +107,20 @@ EPushRPCResult SpatialRPCService::PushRPCInternal(Worker_EntityId EntityId, ERPC
{
RPCRingBufferUtils::WriteRPCToSchema(EndpointObject, Type, NewRPCId, Payload);
+#if TRACE_LIB_ACTIVE
+ if (SpatialLatencyTracer != nullptr && Payload.Trace != InvalidTraceKey)
+ {
+ if (PendingTraces.Find(EntityComponent) == nullptr)
+ {
+ PendingTraces.Add(EntityComponent, Payload.Trace);
+ }
+ else
+ {
+ SpatialLatencyTracer->WriteAndEndTrace(Payload.Trace, TEXT("Multiple rpc updates in single update, ending further stack tracing"), true);
+ }
+ }
+#endif
+
LastSentRPCIds.Add(EntityType, NewRPCId);
}
else
@@ -119,7 +151,7 @@ void SpatialRPCService::PushOverflowedRPCs()
bool bShouldDrop = false;
for (RPCPayload& Payload : OverflowedRPCArray)
{
- EPushRPCResult Result = PushRPCInternal(EntityId, Type, MoveTemp(Payload));
+ const EPushRPCResult Result = PushRPCInternal(EntityId, Type, MoveTemp(Payload), false);
switch (Result)
{
@@ -137,8 +169,14 @@ void SpatialRPCService::PushOverflowedRPCs()
UE_LOG(LogSpatialRPCService, Warning, TEXT("SpatialRPCService::PushOverflowedRPCs: Lost authority over ring buffer component for RPC type that was overflowed. Entity: %lld, RPC type: %s"), EntityId, *SpatialConstants::RPCTypeToString(Type));
bShouldDrop = true;
break;
+ default:
+ checkNoEntry();
}
+#if TRACE_LIB_ACTIVE
+ ProcessResultToLatencyTrace(Result, Payload.Trace);
+#endif
+
// This includes the valid case of RPCs still overflowing (EPushRPCResult::QueueOverflowed), as well as the error cases.
if (Result != EPushRPCResult::Success)
{
@@ -175,6 +213,11 @@ TArray SpatialRPCService::GetRPCsAndAcksToSend(
UpdateToSend.EntityId = It.Key.EntityId;
UpdateToSend.Update.component_id = It.Key.ComponentId;
UpdateToSend.Update.schema_type = It.Value;
+#if TRACE_LIB_ACTIVE
+ TraceKey Trace = InvalidTraceKey;
+ PendingTraces.RemoveAndCopyValue(It.Key, Trace);
+ UpdateToSend.Update.Trace = Trace;
+#endif
}
PendingComponentUpdatesToSend.Empty();
@@ -182,7 +225,7 @@ TArray SpatialRPCService::GetRPCsAndAcksToSend(
return UpdatesToSend;
}
-TArray SpatialRPCService::GetRPCComponentsOnEntityCreation(Worker_EntityId EntityId)
+TArray SpatialRPCService::GetRPCComponentsOnEntityCreation(Worker_EntityId EntityId)
{
static Worker_ComponentId EndpointComponentIds[] = {
SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID,
@@ -190,13 +233,13 @@ TArray SpatialRPCService::GetRPCComponentsOnEntityCreation
SpatialConstants::MULTICAST_RPCS_COMPONENT_ID
};
- TArray Components;
+ TArray Components;
for (Worker_ComponentId EndpointComponentId : EndpointComponentIds)
{
const EntityComponentId EntityComponent = { EntityId, EndpointComponentId };
- Worker_ComponentData& Component = Components.AddZeroed_GetRef();
+ FWorkerComponentData& Component = Components.Emplace_GetRef(FWorkerComponentData{});
Component.component_id = EndpointComponentId;
if (Schema_ComponentData** ComponentData = PendingRPCsOnEntityCreation.Find(EntityComponent))
{
@@ -214,6 +257,11 @@ TArray SpatialRPCService::GetRPCComponentsOnEntityCreation
}
Component.schema_type = *ComponentData;
+#if TRACE_LIB_ACTIVE
+ TraceKey Trace = InvalidTraceKey;
+ PendingTraces.RemoveAndCopyValue(EntityComponent, Trace);
+ Component.Trace = Trace;
+#endif
PendingRPCsOnEntityCreation.Remove(EntityComponent);
}
else
@@ -494,4 +542,50 @@ Schema_ComponentData* SpatialRPCService::GetOrCreateComponentData(EntityComponen
return *ComponentDataPtr;
}
+#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/SpatialReceiver.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp
index 0291887392..ea8fa7baf3 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp
@@ -105,11 +105,54 @@ void USpatialReceiver::LeaveCriticalSection()
{
OnEntityAddedDelegate.Broadcast(PendingAddEntity);
}
+ PendingAddComponents.RemoveAll([PendingAddEntity](const PendingAddComponentWrapper& Component) {return Component.EntityId == PendingAddEntity;});
}
+ // The reason the AuthorityChange processing is split according to authority is to avoid cases
+ // where we receive data while being authoritative, as that could be unintuitive to the game devs.
+ // We process Lose Auth -> Add Components -> Gain Auth. A common thing that happens is that on handover we get
+ // ComponentData -> Gain Auth, and with this split you receive data as if you were a client to get the most up-to-date state,
+ // and then gain authority. Similarly, you first lose authority, and then receive data, in the opposite situation.
for (Worker_AuthorityChangeOp& PendingAuthorityChange : PendingAuthorityChanges)
{
- HandleActorAuthority(PendingAuthorityChange);
+ if (PendingAuthorityChange.authority != WORKER_AUTHORITY_AUTHORITATIVE)
+ {
+ HandleActorAuthority(PendingAuthorityChange);
+ }
+ }
+
+ for (PendingAddComponentWrapper& PendingAddComponent : PendingAddComponents)
+ {
+ if (ClassInfoManager->IsGeneratedQBIMarkerComponent(PendingAddComponent.ComponentId))
+ {
+ continue;
+ }
+ USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(PendingAddComponent.EntityId);
+ if (Channel == nullptr)
+ {
+ UE_LOG(LogSpatialReceiver, Error, TEXT("Got an add component for an entity that doesn't have an associated actor channel."
+ " Entity id: %lld, component id: %d."), PendingAddComponent.EntityId, PendingAddComponent.ComponentId);
+ continue;
+ }
+ if (Channel->bCreatedEntity)
+ {
+ // Allows servers to change state if they are going to be authoritative, without us overwriting it with old data.
+ // TODO: UNR-3457 to remove this workaround.
+ continue;
+ }
+
+ UE_LOG(LogSpatialReceiver, Verbose,
+ TEXT("Add component inside of a critical section, outside of an add entity, being handled: entity id %lld, component id %d."),
+ PendingAddComponent.EntityId, PendingAddComponent.ComponentId);
+ HandleIndividualAddComponent(PendingAddComponent.EntityId, PendingAddComponent.ComponentId, MoveTemp(PendingAddComponent.Data));
+ }
+
+ for (Worker_AuthorityChangeOp& PendingAuthorityChange : PendingAuthorityChanges)
+ {
+ if (PendingAuthorityChange.authority == WORKER_AUTHORITY_AUTHORITATIVE)
+ {
+ HandleActorAuthority(PendingAuthorityChange);
+ }
}
// Mark that we've left the critical section.
@@ -136,6 +179,11 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op)
return;
}
+ if (HasEntityBeenRequestedForDelete(Op.entity_id))
+ {
+ return;
+ }
+
// Remove all RemoveComponentOps that have already been received and have the same entityId and componentId as the AddComponentOp.
// TODO: This can probably be removed when spatial view is added.
QueuedRemoveComponentOps.RemoveAll([&Op](const Worker_RemoveComponentOp& RemoveComponentOp) {
@@ -150,7 +198,6 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op)
case SpatialConstants::PERSISTENCE_COMPONENT_ID:
case SpatialConstants::SPAWN_DATA_COMPONENT_ID:
case SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID:
- case SpatialConstants::SINGLETON_COMPONENT_ID:
case SpatialConstants::INTEREST_COMPONENT_ID:
case SpatialConstants::NOT_STREAMED_COMPONENT_ID:
case SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID:
@@ -165,13 +212,16 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op)
case SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID:
case SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID:
case SpatialConstants::SPATIAL_DEBUGGING_COMPONENT_ID:
- // Ignore static spatial components as they are managed by the SpatialStaticComponentView.
+ case SpatialConstants::SERVER_WORKER_COMPONENT_ID:
+ // We either don't care about processing these components or we only need to store
+ // the data (which is handled by the SpatialStaticComponentView).
return;
case SpatialConstants::UNREAL_METADATA_COMPONENT_ID:
- // The unreal metadata component is used to indicate when an actor needs to be created from the entity.
- // This means we need to be inside a critical section, otherwise we may not have all the requisite information at the point of creating the actor.
+ // The UnrealMetadata component is used to indicate when an Actor needs to be created from the entity.
+ // This means we need to be inside a critical section, otherwise we may not have all the requisite
+ // information at the point of creating the Actor.
check(bInCriticalSection);
- PendingAddActors.Emplace(Op.entity_id);
+ PendingAddActors.AddUnique(Op.entity_id);
return;
case SpatialConstants::ENTITY_ACL_COMPONENT_ID:
case SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID:
@@ -183,7 +233,7 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op)
}
return;
case SpatialConstants::WORKER_COMPONENT_ID:
- if(NetDriver->IsServer())
+ if (NetDriver->IsServer() && !WorkerConnectionEntities.Contains(Op.entity_id))
{
// Register system identity for a worker connection, to know when a player has disconnected.
Worker* WorkerData = StaticComponentView->GetComponentData(Op.entity_id);
@@ -198,10 +248,6 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op)
RPCService->OnCheckoutMulticastRPCComponentOnEntity(Op.entity_id);
}
return;
- case SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID:
- GlobalStateManager->ApplySingletonManagerData(Op.data);
- GlobalStateManager->LinkAllExistingSingletonActors();
- return;
case SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID:
GlobalStateManager->ApplyDeploymentMapData(Op.data);
return;
@@ -243,11 +289,11 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op)
if (bInCriticalSection)
{
- PendingAddComponents.Emplace(Op.entity_id, Op.data.component_id, MakeUnique(Op.data));
+ PendingAddComponents.AddUnique(PendingAddComponentWrapper(Op.entity_id, Op.data.component_id, MakeUnique(Op.data)));
}
else
{
- HandleIndividualAddComponent(Op);
+ HandleIndividualAddComponent(Op.entity_id, Op.data.component_id, MakeUnique(Op.data));
}
}
@@ -255,6 +301,14 @@ void USpatialReceiver::OnRemoveEntity(const Worker_RemoveEntityOp& Op)
{
SCOPE_CYCLE_COUNTER(STAT_ReceiverRemoveEntity);
+ // Stop tracking if the entity was deleted as a result of deleting the actor during creation.
+ // This assumes that authority will be gained before interest is gained and lost.
+ const int32 RetiredActorIndex = EntitiesToRetireOnAuthorityGain.IndexOfByPredicate([Op](const DeferredRetire& Retire) { return Op.entity_id == Retire.EntityId; });
+ if (RetiredActorIndex != INDEX_NONE)
+ {
+ EntitiesToRetireOnAuthorityGain.RemoveAtSwap(RetiredActorIndex);
+ }
+
if (LoadBalanceEnforcer != nullptr)
{
LoadBalanceEnforcer->OnEntityRemoved(Op);
@@ -287,6 +341,17 @@ void USpatialReceiver::OnRemoveEntity(const Worker_RemoveEntityOp& Op)
void USpatialReceiver::OnRemoveComponent(const Worker_RemoveComponentOp& Op)
{
+ // We should exit early if we're receiving a duplicate RemoveComponent op. This can happen with dynamic
+ // components enabled. We detect if the op is a duplicate via the queue of ops to be processed (duplicate
+ // op receive in the same op list).
+ if (QueuedRemoveComponentOps.ContainsByPredicate([&Op](const Worker_RemoveComponentOp& QueuedOp)
+ {
+ return QueuedOp.entity_id == Op.entity_id && QueuedOp.component_id == Op.component_id;
+ }))
+ {
+ return;
+ }
+
if (Op.component_id == SpatialConstants::UNREAL_METADATA_COMPONENT_ID)
{
if (IsEntityWaitingForAsyncLoad(Op.entity_id))
@@ -349,6 +414,10 @@ USpatialActorChannel* USpatialReceiver::GetOrRecreateChannelForDomantActor(AActo
{
// Receive would normally create channel in ReceiveActor - this function is used to recreate the channel after waking up a dormant actor
USpatialActorChannel* Channel = NetDriver->GetOrCreateSpatialActorChannel(Actor);
+ if (Channel == nullptr)
+ {
+ return nullptr;
+ }
check(!Channel->bCreatingNewEntity);
check(Channel->GetEntityId() == EntityID);
@@ -366,6 +435,12 @@ void USpatialReceiver::ProcessRemoveComponent(const Worker_RemoveComponentOp& Op
return;
}
+ // We want to do nothing for RemoveComponent ops for which we never received a corresponding
+ // AddComponent op. This can happen because of the worker SDK generating a RemoveComponent op
+ // when a worker receives authority over a component without having already received the
+ // AddComponent op. The generation is a known part of the worker SDK we need to tolerate for
+ // enabling dynamic components, and having authority ACL entries without having the component
+ // data present on an entity is permitted as part of our Unreal dynamic component implementation.
if (!StaticComponentView->HasComponent(Op.entity_id, Op.component_id))
{
return;
@@ -405,10 +480,25 @@ void USpatialReceiver::UpdateShadowData(Worker_EntityId EntityId)
void USpatialReceiver::OnAuthorityChange(const Worker_AuthorityChangeOp& Op)
{
+ if (HasEntityBeenRequestedForDelete(Op.entity_id))
+ {
+ if (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE && Op.component_id == SpatialConstants::POSITION_COMPONENT_ID)
+ {
+ HandleEntityDeletedAuthority(Op.entity_id);
+ }
+ return;
+ }
+
// Update this worker's view of authority. We do this here as this is when the worker is first notified of the authority change.
// This way systems that depend on having non-stale state can function correctly.
StaticComponentView->OnAuthorityChange(Op);
+ if (Op.component_id == SpatialConstants::SERVER_WORKER_COMPONENT_ID && Op.authority == WORKER_AUTHORITY_AUTHORITATIVE)
+ {
+ GlobalStateManager->TrySendWorkerReadyToBeginPlay();
+ return;
+ }
+
if (Op.component_id == SpatialConstants::ENTITY_ACL_COMPONENT_ID && LoadBalanceEnforcer != nullptr)
{
LoadBalanceEnforcer->OnAclAuthorityChanged(Op);
@@ -421,6 +511,18 @@ void USpatialReceiver::OnAuthorityChange(const Worker_AuthorityChangeOp& Op)
return;
}
+ // Process authority gained event immediately, so if we're in a critical section, the RPCService will
+ // be correctly configured to process RPCs sent during Actor creation
+ if (GetDefault()->UseRPCRingBuffer() && RPCService != nullptr && Op.authority == WORKER_AUTHORITY_AUTHORITATIVE)
+ {
+ if (Op.component_id == SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID ||
+ Op.component_id == SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID ||
+ Op.component_id == SpatialConstants::MULTICAST_RPCS_COMPONENT_ID)
+ {
+ RPCService->OnEndpointAuthorityGained(Op.entity_id, Op.component_id);
+ }
+ }
+
if (bInCriticalSection)
{
// The actor receiving flow requires authority to be handled after all components have been received, so buffer those if we
@@ -482,7 +584,9 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op)
NetDriver->VirtualWorkerTranslationManager->AuthorityChanged(Op);
}
- if (NetDriver->SpatialDebugger != nullptr)
+ if (NetDriver->SpatialDebugger != nullptr
+ && Op.authority == WORKER_AUTHORITY_AUTHORITATIVE
+ && Op.component_id == SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID)
{
NetDriver->SpatialDebugger->ActorAuthorityChanged(Op);
}
@@ -493,7 +597,9 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op)
return;
}
- if (USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(Op.entity_id))
+ USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(Op.entity_id);
+
+ if (Channel != nullptr)
{
if (Op.component_id == SpatialConstants::POSITION_COMPONENT_ID)
{
@@ -546,7 +652,7 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op)
{
const bool bDormantActor = (Actor->NetDormancy >= DORM_DormantAll);
- if (IsValid(NetDriver->GetActorChannelByEntityId(Op.entity_id)) || bDormantActor)
+ if (IsValid(Channel) || bDormantActor)
{
Actor->Role = ROLE_Authority;
Actor->RemoteRole = ROLE_SimulatedProxy;
@@ -585,7 +691,7 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op)
else
{
UE_LOG(LogSpatialReceiver, Verbose, TEXT("Received authority over actor %s, with entity id %lld, which has no channel. This means it attempted to delete it earlier, when it had no authority. Retrying to delete now."), *Actor->GetName(), Op.entity_id);
- Sender->RetireEntity(Op.entity_id);
+ Sender->RetireEntity(Op.entity_id, Actor->IsNetStartupActor());
}
}
else if (Op.authority == WORKER_AUTHORITY_AUTHORITY_LOSS_IMMINENT)
@@ -594,9 +700,9 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op)
}
else if (Op.authority == WORKER_AUTHORITY_NOT_AUTHORITATIVE)
{
- if (USpatialActorChannel* ActorChannel = NetDriver->GetActorChannelByEntityId(Op.entity_id))
+ if (Channel != nullptr)
{
- ActorChannel->bCreatedEntity = false;
+ Channel->bCreatedEntity = false;
}
// With load-balancing enabled, we already set ROLE_SimulatedProxy and trigger OnAuthorityLost when we
@@ -624,9 +730,12 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op)
{
if (UObject* Object = PendingSubobjectAttachment.Subobject.Get())
{
- // TODO: UNR-664 - We should track the bytes sent here and factor them into channel saturation.
- uint32 BytesWritten = 0;
- Sender->SendAddComponentForSubobject(PendingSubobjectAttachment.Channel, Object, *PendingSubobjectAttachment.Info, BytesWritten);
+ if (IsValid(Channel))
+ {
+ // TODO: UNR-664 - We should track the bytes sent here and factor them into channel saturation.
+ uint32 BytesWritten = 0;
+ Sender->SendAddComponentForSubobject(Channel, Object, *PendingSubobjectAttachment.Info, BytesWritten);
+ }
}
}
@@ -635,12 +744,12 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op)
}
else if (Op.component_id == SpatialConstants::GetClientAuthorityComponent(GetDefault()->UseRPCRingBuffer()))
{
- if (USpatialActorChannel* ActorChannel = NetDriver->GetActorChannelByEntityId(Op.entity_id))
+ if (Channel != nullptr)
{
// Soft handover isn't supported currently.
if (Op.authority != WORKER_AUTHORITY_AUTHORITY_LOSS_IMMINENT)
{
- ActorChannel->ClientProcessOwnershipChange(Op.authority == WORKER_AUTHORITY_AUTHORITATIVE);
+ Channel->ClientProcessOwnershipChange(Op.authority == WORKER_AUTHORITY_AUTHORITATIVE);
}
}
@@ -659,10 +768,12 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op)
{
if (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE)
{
- RPCService->OnEndpointAuthorityGained(Op.entity_id, Op.component_id);
if (Op.component_id != SpatialConstants::MULTICAST_RPCS_COMPONENT_ID)
{
- RPCService->ExtractRPCsForEntity(Op.entity_id, Op.component_id);
+ // If we have just received authority over the client endpoint, then we are a client. In that case,
+ // we want to scrape the server endpoint for any server -> client RPCs that are waiting to be called.
+ const Worker_ComponentId ComponentToExtractFrom = Op.component_id == SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID ? SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID : SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID;
+ RPCService->ExtractRPCsForEntity(Op.entity_id, ComponentToExtractFrom);
}
}
else if (Op.authority == WORKER_AUTHORITY_NOT_AUTHORITATIVE)
@@ -742,22 +853,8 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId)
AActor* EntityActor = Cast(PackageMap->GetObjectFromEntityId(EntityId));
if (EntityActor != nullptr)
{
- UE_LOG(LogSpatialReceiver, Verbose, TEXT("%s: Entity for actor %s has been checked out on the worker which spawned it or is a singleton linked on this worker. "
- "Entity ID: %lld"), *NetDriver->Connection->GetWorkerId(), *EntityActor->GetName(), EntityId);
-
- // Assume SimulatedProxy until we've been delegated Authority
- bool bAuthority = StaticComponentView->HasAuthority(EntityId, Position::ComponentId);
- EntityActor->Role = bAuthority ? ROLE_Authority : ROLE_SimulatedProxy;
- EntityActor->RemoteRole = bAuthority ? ROLE_SimulatedProxy : ROLE_Authority;
- if (bAuthority)
- {
- if (EntityActor->GetNetConnection() != nullptr || EntityActor->IsA())
- {
- EntityActor->RemoteRole = ROLE_AutonomousProxy;
- }
- }
-
- // If we're a singleton, apply the data, regardless of authority - JIRA: 736
+ UE_LOG(LogSpatialReceiver, Verbose, TEXT("%s: Entity %lld for Actor %s has been checked out on the worker which spawned it."),
+ *NetDriver->Connection->GetWorkerId(), EntityId, *EntityActor->GetName());
return;
}
@@ -789,7 +886,6 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId)
{
// This could be nullptr if:
// a stably named actor could not be found
- // the Actor is a singleton that has arrived over the wire before it has been created on this worker
// the class couldn't be loaded
return;
}
@@ -844,11 +940,7 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId)
if (Channel->Actor == nullptr)
{
-#if ENGINE_MINOR_VERSION <= 22
- Channel->SetChannelActor(EntityActor);
-#else
Channel->SetChannelActor(EntityActor, ESetChannelActorFlags::None);
-#endif
}
TArray ObjectsToResolvePendingOpsFor;
@@ -878,11 +970,6 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId)
if (!NetDriver->IsServer())
{
// Update interest on the entity's components after receiving initial component data (so Role and RemoteRole are properly set).
- // Don't send dynamic interest for this actor if it is otherwise handled by result types.
- if (!SpatialGDKSettings->bEnableResultTypes)
- {
- Sender->SendComponentInterestForActor(Channel, EntityId, Channel->IsAuthoritativeClient());
- }
// 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.
@@ -909,17 +996,13 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId)
// flow) take care of setting roles correctly.
if (EntityActor->HasAuthority())
{
+ UE_LOG(LogSpatialReceiver, Error, TEXT("Trying to unexpectedly spawn received network Actor with authority. Actor %s. Entity: %lld"), *EntityActor->GetName(), EntityId);
EntityActor->Role = ROLE_SimulatedProxy;
EntityActor->RemoteRole = ROLE_Authority;
}
EntityActor->DispatchBeginPlay();
}
- if (EntityActor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton))
- {
- GlobalStateManager->RegisterSingletonChannel(EntityActor, Channel);
- }
-
EntityActor->UpdateOverlaps();
if (StaticComponentView->HasComponent(EntityId, SpatialConstants::DORMANT_COMPONENT_ID))
@@ -1037,11 +1120,6 @@ void USpatialReceiver::RemoveActor(Worker_EntityId EntityId)
}
}
- if (Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton))
- {
- return;
- }
-
DestroyActor(Actor, EntityId);
}
@@ -1106,6 +1184,13 @@ AActor* USpatialReceiver::TryGetOrCreateActor(UnrealMetadata* UnrealMetadataComp
}
}
+ // Handle linking received unique Actors (e.g. game state, game mode) to instances already spawned on this worker.
+ UClass* ActorClass = UnrealMetadataComp->GetNativeEntityClass();
+ if (FUnrealObjectRef::IsUniqueActorClass(ActorClass) && NetDriver->IsServer())
+ {
+ return PackageMap->GetUniqueActorInstanceByClass(ActorClass);
+ }
+
return CreateActor(UnrealMetadataComp, SpawnDataComp, NetOwningClientWorkerComp);
}
@@ -1120,14 +1205,6 @@ AActor* USpatialReceiver::CreateActor(UnrealMetadata* UnrealMetadataComp, SpawnD
return nullptr;
}
- const bool bIsServer = NetDriver->IsServer();
-
- // Initial Singleton Actor replication is handled with GlobalStateManager::LinkExistingSingletonActors
- if (bIsServer && ActorClass->HasAnySpatialClassFlags(SPATIALCLASS_Singleton))
- {
- return FindSingletonActor(ActorClass);
- }
-
UE_LOG(LogSpatialReceiver, Verbose, TEXT("Spawning a %s whilst checking out an entity."), *ActorClass->GetFullName());
const bool bCreatingPlayerController = ActorClass->IsChildOf(APlayerController::StaticClass());
@@ -1142,7 +1219,7 @@ AActor* USpatialReceiver::CreateActor(UnrealMetadata* UnrealMetadataComp, SpawnD
AActor* NewActor = NetDriver->GetWorld()->SpawnActorAbsolute(ActorClass, FTransform(SpawnDataComp->Rotation, SpawnLocation), SpawnInfo);
check(NewActor);
- if (bIsServer && bCreatingPlayerController)
+ if (NetDriver->IsServer() && bCreatingPlayerController)
{
// If we're spawning a PlayerController, it should definitely have a net-owning client worker ID.
check(NetOwningClientWorkerComp->WorkerId.IsSet());
@@ -1190,7 +1267,7 @@ void USpatialReceiver::ApplyComponentDataOnActorCreation(Worker_EntityId EntityI
bool bFoundOffset = ClassInfoManager->GetOffsetByComponentId(Data.component_id, Offset);
if (!bFoundOffset)
{
- UE_LOG(LogSpatialReceiver, Warning, TEXT("EntityId %lld, ComponentId %d - Could not find offset for component id when applying component data to Actor %s!"), EntityId, Data.component_id, *Actor->GetPathName());
+ UE_LOG(LogSpatialReceiver, Warning, TEXT("Worker: %s EntityId: %lld, ComponentId: %d - Could not find offset for component id when applying component data to Actor %s!"), *NetDriver->Connection->GetWorkerId(), EntityId, Data.component_id, *Actor->GetPathName());
return;
}
@@ -1220,32 +1297,32 @@ void USpatialReceiver::ApplyComponentDataOnActorCreation(Worker_EntityId EntityI
OutObjectsToResolve.Add(ObjectPtrRefPair(TargetObject.Get(), TargetObjectRef));
}
-void USpatialReceiver::HandleIndividualAddComponent(const Worker_AddComponentOp& Op)
+void USpatialReceiver::HandleIndividualAddComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, TUniquePtr Data)
{
uint32 Offset = 0;
- bool bFoundOffset = ClassInfoManager->GetOffsetByComponentId(Op.data.component_id, Offset);
+ bool bFoundOffset = ClassInfoManager->GetOffsetByComponentId(ComponentId, Offset);
if (!bFoundOffset)
{
- UE_LOG(LogSpatialReceiver, Warning, TEXT("EntityId %lld, ComponentId %d - Could not find offset for component id "
- "when receiving dynamic AddComponent."), Op.entity_id, Op.data.component_id);
+ UE_LOG(LogSpatialReceiver, Warning, TEXT("Could not find offset for component id when receiving dynamic AddComponent."
+ " (EntityId %lld, ComponentId %d)"), EntityId, ComponentId);
return;
}
// Object already exists, we can apply data directly.
- if (UObject* Object = PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(Op.entity_id, Offset)).Get())
+ if (UObject* Object = PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(EntityId, Offset)).Get())
{
- if (USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(Op.entity_id))
+ if (USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(EntityId))
{
- ApplyComponentData(*Channel, *Object, Op.data);
+ ApplyComponentData(*Channel, *Object, *Data->ComponentData);
}
return;
}
- const FClassInfo& Info = ClassInfoManager->GetClassInfoByComponentId(Op.data.component_id);
- AActor* Actor = Cast(PackageMap->GetObjectFromEntityId(Op.entity_id).Get());
+ const FClassInfo& Info = ClassInfoManager->GetClassInfoByComponentId(ComponentId);
+ AActor* Actor = Cast(PackageMap->GetObjectFromEntityId(EntityId).Get());
if (Actor == nullptr)
{
- UE_LOG(LogSpatialReceiver, Verbose, TEXT("Received an add component op for subobject of type %s on entity %lld but couldn't find Actor!"), *Info.Class->GetName(), Op.entity_id);
+ UE_LOG(LogSpatialReceiver, Warning, TEXT("Received an add component op for subobject of type %s on entity %lld but couldn't find Actor!"), *Info.Class->GetName(), EntityId);
return;
}
@@ -1254,25 +1331,25 @@ void USpatialReceiver::HandleIndividualAddComponent(const Worker_AddComponentOp&
bool bIsDynamicSubobject = !ActorClassInfo.SubobjectInfo.Contains(Offset);
if (!bIsDynamicSubobject)
{
- UE_LOG(LogSpatialReceiver, Verbose, TEXT("Tried to apply component data on add component for a static subobject that's been deleted, will skip. Entity: %lld, Component: %d, Actor: %s"), Op.entity_id, Op.data.component_id, *Actor->GetPathName());
+ UE_LOG(LogSpatialReceiver, Verbose, TEXT("Tried to apply component data on add component for a static subobject that's been deleted, will skip. Entity: %lld, Component: %d, Actor: %s"), EntityId, ComponentId, *Actor->GetPathName());
return;
}
// Otherwise this is a dynamically attached component. We need to make sure we have all related components before creation.
- PendingDynamicSubobjectComponents.Add(MakeTuple(static_cast(Op.entity_id), Op.data.component_id),
- PendingAddComponentWrapper(Op.entity_id, Op.data.component_id, MakeUnique(Op.data)));
+ PendingDynamicSubobjectComponents.Add(MakeTuple(static_cast(EntityId), ComponentId),
+ PendingAddComponentWrapper(EntityId, ComponentId, MoveTemp(Data)));
bool bReadyToCreate = true;
ForAllSchemaComponentTypes([&](ESchemaComponentType Type)
{
- Worker_ComponentId ComponentId = Info.SchemaComponents[Type];
+ Worker_ComponentId SchemaComponentId = Info.SchemaComponents[Type];
- if (ComponentId == SpatialConstants::INVALID_COMPONENT_ID)
+ if (SchemaComponentId == SpatialConstants::INVALID_COMPONENT_ID)
{
return;
}
- if (!PendingDynamicSubobjectComponents.Contains(MakeTuple(static_cast(Op.entity_id), ComponentId)))
+ if (!PendingDynamicSubobjectComponents.Contains(MakeTuple(static_cast(EntityId), SchemaComponentId)))
{
bReadyToCreate = false;
}
@@ -1280,7 +1357,7 @@ void USpatialReceiver::HandleIndividualAddComponent(const Worker_AddComponentOp&
if (bReadyToCreate)
{
- AttachDynamicSubobject(Actor, Op.entity_id, Info);
+ AttachDynamicSubobject(Actor, EntityId, Info);
}
}
@@ -1320,18 +1397,6 @@ void USpatialReceiver::AttachDynamicSubobject(AActor* Actor, Worker_EntityId Ent
// Resolve things like RepNotify or RPCs after applying component data.
ResolvePendingOperations(Subobject, SubobjectRef);
-
- // Don't send dynamic interest for this subobject if it is otherwise handled by result types.
- if (GetDefault()->bEnableResultTypes)
- {
- return;
- }
-
- // If on a client, we need to set up the proper component interest for the new subobject.
- if (!NetDriver->IsServer())
- {
- Sender->SendComponentInterestForSubobject(Info, EntityId, Channel->IsAuthoritativeClient());
- }
}
struct USpatialReceiver::RepStateUpdateHelper
@@ -1453,7 +1518,6 @@ void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op)
case SpatialConstants::INTEREST_COMPONENT_ID:
case SpatialConstants::SPAWN_DATA_COMPONENT_ID:
case SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID:
- case SpatialConstants::SINGLETON_COMPONENT_ID:
case SpatialConstants::UNREAL_METADATA_COMPONENT_ID:
case SpatialConstants::NOT_STREAMED_COMPONENT_ID:
case SpatialConstants::RPCS_ON_ENTITY_CREATION_ID:
@@ -1470,10 +1534,6 @@ void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op)
case SpatialConstants::HEARTBEAT_COMPONENT_ID:
OnHeartbeatComponentUpdate(Op);
return;
- case SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID:
- GlobalStateManager->ApplySingletonManagerUpdate(Op.update);
- GlobalStateManager->LinkAllExistingSingletonActors();
- return;
case SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID:
NetDriver->GlobalStateManager->ApplyDeploymentMapUpdate(Op.update);
return;
@@ -1553,13 +1613,11 @@ void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op)
}
}
- const FClassInfo& Info = ClassInfoManager->GetClassInfoByComponentId(Op.update.component_id);
-
uint32 Offset;
bool bFoundOffset = ClassInfoManager->GetOffsetByComponentId(Op.update.component_id, Offset);
if (!bFoundOffset)
{
- UE_LOG(LogSpatialReceiver, Warning, TEXT("Entity: %d Component: %d - Couldn't find Offset for component id"), Op.entity_id, Op.update.component_id);
+ UE_LOG(LogSpatialReceiver, Warning, TEXT("Worker: %s EntityId %d ComponentId %d - Could not find offset for component id when receiving a component update."), *NetDriver->Connection->GetWorkerId(), Op.entity_id, Op.update.component_id);
return;
}
@@ -2062,20 +2120,6 @@ TWeakObjectPtr USpatialReceiver::PopPendingActorRequest(Wo
return Channel;
}
-AActor* USpatialReceiver::FindSingletonActor(UClass* SingletonClass)
-{
- TArray FoundActors;
- UGameplayStatics::GetAllActorsOfClass(NetDriver->World, SingletonClass, FoundActors);
-
- // There should be only one singleton actor per class
- if (FoundActors.Num() == 1)
- {
- return FoundActors[0];
- }
-
- return nullptr;
-}
-
void USpatialReceiver::ProcessQueuedActorRPCsOnEntityCreation(Worker_EntityId EntityId, RPCsOnEntityCreation& QueuedRPCs)
{
for (auto& RPC : QueuedRPCs.RPCs)
@@ -2134,7 +2178,20 @@ void USpatialReceiver::ProcessOrQueueIncomingRPC(const FUnrealObjectRef& InTarge
UObject* TargetObject = TargetObjectWeakPtr.Get();
const FClassInfo& ClassInfo = ClassInfoManager->GetOrCreateClassInfoByObject(TargetObject);
+
+ if (InPayload.Index >= static_cast(ClassInfo.RPCs.Num()))
+ {
+ // This should only happen if there's a class layout disagreement between workers, which would indicate incompatible binaries.
+ UE_LOG(LogSpatialReceiver, Error, TEXT("Invalid RPC index (%d) received on %s, dropping the RPC"), InPayload.Index, *TargetObject->GetPathName());
+ return;
+ }
UFunction* Function = ClassInfo.RPCs[InPayload.Index];
+ if (Function == nullptr)
+ {
+ UE_LOG(LogSpatialReceiver, Error, TEXT("Missing function info received on %s, dropping the RPC"), *TargetObject->GetPathName());
+ return;
+ }
+
const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function);
ERPCType Type = RPCInfo.Type;
@@ -2153,16 +2210,18 @@ void USpatialReceiver::ResolvePendingOperations(UObject* Object, const FUnrealOb
UE_LOG(LogSpatialReceiver, Verbose, TEXT("Resolving pending object refs and RPCs which depend on object: %s %s."), *Object->GetName(), *ObjectRef.ToString());
ResolveIncomingOperations(Object, ObjectRef);
- if (Object->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton) && !Object->IsFullNameStableForNetworking())
+
+ // When resolving an Actor that should uniquely exist in a deployment, e.g. GameMode, GameState, LevelScriptActors, we also
+ // resolve using class path (in case any properties were set from a server that hasn't resolved the Actor yet).
+ if (FUnrealObjectRef::ShouldLoadObjectFromClassPath(Object))
{
- // When resolving a singleton, also resolve using class path (in case any properties
- // were set from a server that hasn't resolved the singleton yet)
- FUnrealObjectRef ClassObjectRef = FUnrealObjectRef::GetSingletonClassRef(Object, PackageMap);
+ FUnrealObjectRef ClassObjectRef = FUnrealObjectRef::GetRefFromObjectClassPath(Object, PackageMap);
if (ClassObjectRef.IsValid())
{
ResolveIncomingOperations(Object, ClassObjectRef);
}
}
+
// TODO: UNR-1650 We're trying to resolve all queues, which introduces more overhead.
IncomingRPCs.ProcessRPCs();
}
@@ -2259,7 +2318,7 @@ void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* R
if (AbsOffset >= MaxAbsOffset)
{
- UE_LOG(LogSpatialReceiver, Log, TEXT("ResolveObjectReferences: Removed unresolved reference: AbsOffset >= MaxAbsOffset: %d"), AbsOffset);
+ UE_LOG(LogSpatialReceiver, Error, TEXT("ResolveObjectReferences: Removed unresolved reference: AbsOffset >= MaxAbsOffset: %d"), AbsOffset);
It.RemoveCurrent();
continue;
}
@@ -2445,7 +2504,32 @@ void USpatialReceiver::PeriodicallyProcessIncomingRPCs()
bool USpatialReceiver::NeedToLoadClass(const FString& ClassPath)
{
- return FindObject(nullptr, *ClassPath, false) == nullptr;
+ UObject* ClassObject = FindObject(nullptr, *ClassPath, false);
+ if (ClassObject == nullptr)
+ {
+ return true;
+ }
+
+ FString PackagePath = GetPackagePath(ClassPath);
+ FName PackagePathName = *PackagePath;
+
+ // UNR-3320 The following test checks if the package is currently being processed in the async loading thread.
+ // Without it, we could be using an object loaded in memory, but not completely ready to be used.
+ // Looking through PackageMapClient's code, which handles asset async loading in Native unreal, checking
+ // UPackage::IsFullyLoaded, or UObject::HasAnyInternalFlag(EInternalObjectFlag::AsyncLoading) should tell us if it is the case.
+ // In practice, these tests are not enough to prevent using objects too early (symptom is RF_NeedPostLoad being set, and crash when using them later).
+ // GetAsyncLoadPercentage will actually look through the async loading thread's UAsyncPackage maps to see if there are any entries.
+ // TODO : UNR-3374 This looks like an expensive check, but it does the job. We should investigate further
+ // what is the issue with the other flags and why they do not give us reliable information.
+
+ float Percentage = GetAsyncLoadPercentage(PackagePathName);
+ if (Percentage != -1.0f)
+ {
+ UE_LOG(LogSpatialReceiver, Warning, TEXT("Class %s package is registered in async loading thread."), *ClassPath)
+ return true;
+ }
+
+ return false;
}
FString USpatialReceiver::GetPackagePath(const FString& ClassPath)
@@ -2491,6 +2575,12 @@ void USpatialReceiver::OnAsyncPackageLoaded(const FName& PackageName, UPackage*
return;
}
+ if (Result != EAsyncLoadingResult::Succeeded)
+ {
+ UE_LOG(LogSpatialReceiver, Error, TEXT("USpatialReceiver::OnAsyncPackageLoaded: Package was not loaded successfully. Package: %s"), *PackageName.ToString());
+ return;
+ }
+
for (Worker_EntityId Entity : Entities)
{
if (IsEntityWaitingForAsyncLoad(Entity))
@@ -2530,6 +2620,12 @@ void USpatialReceiver::MoveMappedObjectToUnmapped(const FUnrealObjectRef& Ref)
}
}
+void USpatialReceiver::RetireWhenAuthoritive(Worker_EntityId EntityId, Worker_ComponentId ActorClassId, bool bIsNetStartup, bool bNeedsTearOff)
+{
+ DeferredRetire DeferredObj = { EntityId, ActorClassId, bIsNetStartup, bNeedsTearOff };
+ EntitiesToRetireOnAuthorityGain.Add(DeferredObj);
+}
+
bool USpatialReceiver::IsEntityWaitingForAsyncLoad(Worker_EntityId Entity)
{
return EntitiesWaitingForAsyncLoad.Contains(Entity);
@@ -2539,6 +2635,17 @@ void USpatialReceiver::QueueAddComponentOpForAsyncLoad(const Worker_AddComponent
{
EntityWaitingForAsyncLoad& AsyncLoadEntity = EntitiesWaitingForAsyncLoad.FindChecked(Op.entity_id);
+ // Skip queuing a duplicate AddComponent op.
+ if (AsyncLoadEntity.PendingOps.ContainsByPredicate([&Op](const QueuedOpForAsyncLoad& QueuedOp)
+ {
+ return QueuedOp.Op.op_type == WORKER_OP_TYPE_ADD_COMPONENT
+ && QueuedOp.Op.op.add_component.entity_id == Op.entity_id
+ && QueuedOp.Op.op.add_component.data.component_id && Op.data.component_id;
+ }))
+ {
+ return;
+ }
+
QueuedOpForAsyncLoad NewOp = {};
NewOp.AcquiredData = Worker_AcquireComponentData(&Op.data);
NewOp.Op.op_type = WORKER_OP_TYPE_ADD_COMPONENT;
@@ -2700,3 +2807,30 @@ void USpatialReceiver::CleanupRepStateMap(FSpatialObjectRepState& RepState)
}
}
}
+
+bool USpatialReceiver::HasEntityBeenRequestedForDelete(Worker_EntityId EntityId)
+{
+ return EntitiesToRetireOnAuthorityGain.ContainsByPredicate([EntityId](const DeferredRetire& Retire) { return EntityId == Retire.EntityId; });
+}
+
+void USpatialReceiver::HandleDeferredEntityDeletion(const DeferredRetire& Retire)
+{
+ if (Retire.bNeedsTearOff)
+ {
+ Sender->SendActorTornOffUpdate(Retire.EntityId, Retire.ActorClassId);
+ NetDriver->DelayedRetireEntity(Retire.EntityId, 1.0f, Retire.bIsNetStartupActor);
+ }
+ else
+ {
+ Sender->RetireEntity(Retire.EntityId, Retire.bIsNetStartupActor);
+ }
+}
+
+void USpatialReceiver::HandleEntityDeletedAuthority(Worker_EntityId EntityId)
+{
+ int32 Index = EntitiesToRetireOnAuthorityGain.IndexOfByPredicate([EntityId](const DeferredRetire& Retire) { return Retire.EntityId == EntityId; });
+ if (Index != INDEX_NONE)
+ {
+ HandleDeferredEntityDeletion(EntitiesToRetireOnAuthorityGain[Index]);
+ }
+}
diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp
index 12acb545dc..c00693c626 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp
@@ -14,6 +14,7 @@
#include "Interop/Connection/SpatialWorkerConnection.h"
#include "Interop/GlobalStateManager.h"
#include "Interop/SpatialReceiver.h"
+#include "LoadBalancing/AbstractLBStrategy.h"
#include "Net/NetworkProfiler.h"
#include "Schema/AuthorityIntent.h"
#include "Schema/ClientRPCEndpointLegacy.h"
@@ -29,7 +30,6 @@
#include "Utils/EntityFactory.h"
#include "Utils/InterestFactory.h"
#include "Utils/RepLayoutUtils.h"
-#include "Utils/SpatialActorGroupManager.h"
#include "Utils/SpatialActorUtils.h"
#include "Utils/SpatialDebugger.h"
#include "Utils/SpatialLatencyTracer.h"
@@ -66,17 +66,21 @@ void USpatialSender::Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimer
Receiver = InNetDriver->Receiver;
PackageMap = InNetDriver->PackageMap;
ClassInfoManager = InNetDriver->ClassInfoManager;
- check(InNetDriver->ActorGroupManager != nullptr);
- ActorGroupManager = InNetDriver->ActorGroupManager;
TimerManager = InTimerManager;
RPCService = InRPCService;
OutgoingRPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(this, &USpatialSender::SendRPC));
+
+ // Attempt to send RPCs that might have been queued while waiting for authority over entities this worker created.
+ if (GetDefault()->QueuedOutgoingRPCRetryTime > 0.0f)
+ {
+ PeriodicallyProcessOutgoingRPCs();
+ }
}
Worker_RequestId USpatialSender::CreateEntity(USpatialActorChannel* Channel, uint32& OutBytesWritten)
{
- EntityFactory DataFactory(NetDriver, PackageMap, ClassInfoManager, ActorGroupManager, RPCService);
+ EntityFactory DataFactory(NetDriver, PackageMap, ClassInfoManager, RPCService);
TArray ComponentDatas = DataFactory.CreateEntityComponents(Channel, OutgoingOnCreateEntityRPCs, OutBytesWritten);
// If the Actor was loaded rather than dynamically spawned, associate it with its owning sublevel.
@@ -110,6 +114,18 @@ Worker_ComponentData USpatialSender::CreateLevelComponentData(AActor* Actor)
return ComponentFactory::CreateEmptyComponentData(SpatialConstants::NOT_STREAMED_COMPONENT_ID);
}
+void USpatialSender::PeriodicallyProcessOutgoingRPCs()
+{
+ FTimerHandle Timer;
+ TimerManager->SetTimer(Timer, [WeakThis = TWeakObjectPtr(this)]()
+ {
+ if (USpatialSender* SpatialSender = WeakThis.Get())
+ {
+ SpatialSender->OutgoingRPCs.ProcessRPCs();
+ }
+ }, GetDefault()->QueuedOutgoingRPCRetryTime, true);
+}
+
void USpatialSender::SendAddComponentForSubobject(USpatialActorChannel* Channel, UObject* Subobject, const FClassInfo& SubobjectInfo, uint32& OutBytesWritten)
{
FRepChangeState SubobjectRepChanges = Channel->CreateInitialRepChangeState(Subobject);
@@ -149,7 +165,6 @@ void USpatialSender::GainAuthorityThenAddComponent(USpatialActorChannel* Channel
TSharedRef PendingSubobjectAttachment = MakeShared();
PendingSubobjectAttachment->Subobject = Object;
- PendingSubobjectAttachment->Channel = Channel;
PendingSubobjectAttachment->Info = Info;
// We collect component IDs related to the dynamic subobject being added to gain authority over.
@@ -173,9 +188,7 @@ void USpatialSender::GainAuthorityThenAddComponent(USpatialActorChannel* Channel
// If this worker is EntityACL authoritative, we can directly update the component IDs to gain authority over.
if (StaticComponentView->HasAuthority(EntityId, SpatialConstants::ENTITY_ACL_COMPONENT_ID))
{
- const FClassInfo& ActorInfo = ClassInfoManager->GetOrCreateClassInfoByClass(Channel->Actor->GetClass());
- const WorkerAttributeSet WorkerAttribute = { ActorInfo.WorkerType.ToString() };
- const WorkerRequirementSet AuthoritativeWorkerRequirementSet = { WorkerAttribute };
+ const WorkerRequirementSet AuthoritativeWorkerRequirementSet = { SpatialConstants::UnrealServerAttributeSet };
EntityAcl* EntityACL = StaticComponentView->GetComponentData(Channel->GetEntityId());
for (auto& ComponentId : NewComponentIds)
@@ -227,8 +240,13 @@ void USpatialSender::SendRemoveComponents(Worker_EntityId EntityId, TArrayAllocateEntityId(), 1);
+}
+
// Creates an entity authoritative on this server worker, ensuring it will be able to receive updates for the GSM.
-void USpatialSender::CreateServerWorkerEntity(int AttemptCounter)
+void USpatialSender::RetryServerWorkerEntityCreation(Worker_EntityId EntityId, int AttemptCounter)
{
const WorkerRequirementSet WorkerIdPermission{ { FString::Format(TEXT("workerId:{0}"), { Connection->GetWorkerId() }) } };
@@ -251,10 +269,10 @@ void USpatialSender::CreateServerWorkerEntity(int AttemptCounter)
Components.Add(NetDriver->InterestFactory->CreateServerWorkerInterest(NetDriver->LoadBalanceStrategy).CreateInterestData());
Components.Add(ComponentPresence(EntityFactory::GetComponentPresenceList(Components)).CreateComponentPresenceData());
- const Worker_RequestId RequestId = Connection->SendCreateEntityRequest(MoveTemp(Components), nullptr);
+ const Worker_RequestId RequestId = Connection->SendCreateEntityRequest(MoveTemp(Components), &EntityId);
CreateEntityDelegate OnCreateWorkerEntityResponse;
- OnCreateWorkerEntityResponse.BindLambda([WeakSender = TWeakObjectPtr(this), AttemptCounter](const Worker_CreateEntityResponseOp& Op)
+ OnCreateWorkerEntityResponse.BindLambda([WeakSender = TWeakObjectPtr(this), EntityId, AttemptCounter](const Worker_CreateEntityResponseOp& Op)
{
if (!WeakSender.IsValid())
{
@@ -265,7 +283,13 @@ void USpatialSender::CreateServerWorkerEntity(int AttemptCounter)
if (Op.status_code == WORKER_STATUS_CODE_SUCCESS)
{
Sender->NetDriver->WorkerEntityId = Op.entity_id;
- Sender->NetDriver->GlobalStateManager->TrySendWorkerReadyToBeginPlay();
+ return;
+ }
+
+ // Given the nature of commands, it's possible we have multiple create commands in flight at once. If a command fails where
+ // we've already set the worker entity ID locally, this means we already successfully create the entity, so nothing needs doing.
+ if (Op.status_code != WORKER_STATUS_CODE_SUCCESS && Sender->NetDriver->WorkerEntityId != SpatialConstants::INVALID_ENTITY_ID)
+ {
return;
}
@@ -285,11 +309,11 @@ void USpatialSender::CreateServerWorkerEntity(int AttemptCounter)
UE_LOG(LogSpatialSender, Warning, TEXT("Worker entity creation request timed out and will retry."));
FTimerHandle RetryTimer;
- Sender->TimerManager->SetTimer(RetryTimer, [WeakSender, AttemptCounter]()
+ Sender->TimerManager->SetTimer(RetryTimer, [WeakSender, EntityId, AttemptCounter]()
{
if (USpatialSender* SpatialSender = WeakSender.Get())
{
- SpatialSender->CreateServerWorkerEntity(AttemptCounter + 1);
+ SpatialSender->RetryServerWorkerEntityCreation(EntityId, AttemptCounter + 1);
}
}, SpatialConstants::GetCommandRetryWaitTimeSeconds(AttemptCounter), false);
});
@@ -448,61 +472,17 @@ void USpatialSender::FlushRPCService()
{
RPCService->PushOverflowedRPCs();
- for (const SpatialRPCService::UpdateToSend& Update : RPCService->GetRPCsAndAcksToSend())
+ const TArray RPCs = RPCService->GetRPCsAndAcksToSend();
+ for (const SpatialRPCService::UpdateToSend& Update : RPCs)
{
Connection->SendComponentUpdate(Update.EntityId, &Update.Update);
}
- }
-}
-
-void FillComponentInterests(const FClassInfo& Info, bool bNetOwned, TArray& ComponentInterest)
-{
- if (Info.SchemaComponents[SCHEMA_OwnerOnly] != SpatialConstants::INVALID_COMPONENT_ID)
- {
- Worker_InterestOverride SingleClientInterest = { Info.SchemaComponents[SCHEMA_OwnerOnly], bNetOwned };
- ComponentInterest.Add(SingleClientInterest);
- }
- if (Info.SchemaComponents[SCHEMA_Handover] != SpatialConstants::INVALID_COMPONENT_ID)
- {
- Worker_InterestOverride HandoverInterest = { Info.SchemaComponents[SCHEMA_Handover], false };
- ComponentInterest.Add(HandoverInterest);
- }
-}
-
-TArray USpatialSender::CreateComponentInterestForActor(USpatialActorChannel* Channel, bool bIsNetOwned)
-{
- TArray ComponentInterest;
-
- const FClassInfo& ActorInfo = ClassInfoManager->GetOrCreateClassInfoByClass(Channel->Actor->GetClass());
- FillComponentInterests(ActorInfo, bIsNetOwned, ComponentInterest);
-
- // Statically attached subobjects
- for (auto& SubobjectInfoPair : ActorInfo.SubobjectInfo)
- {
- const FClassInfo& SubobjectInfo = SubobjectInfoPair.Value.Get();
- FillComponentInterests(SubobjectInfo, bIsNetOwned, ComponentInterest);
- }
-
- // Subobjects dynamically created through replication
- for (const auto& Subobject : Channel->CreateSubObjects)
- {
- const FClassInfo& SubobjectInfo = ClassInfoManager->GetOrCreateClassInfoByObject(Subobject);
- FillComponentInterests(SubobjectInfo, bIsNetOwned, ComponentInterest);
- }
-
- if (GetDefault()->UseRPCRingBuffer())
- {
- ComponentInterest.Add({ SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID, bIsNetOwned });
- ComponentInterest.Add({ SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID, bIsNetOwned });
- }
- else
- {
- ComponentInterest.Add({ SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY, bIsNetOwned });
- ComponentInterest.Add({ SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY, bIsNetOwned });
+ if (RPCs.Num())
+ {
+ Connection->MaybeFlush();
+ }
}
-
- return ComponentInterest;
}
RPCPayload USpatialSender::CreateRPCPayloadFromParams(UObject* TargetObject, const FUnrealObjectRef& TargetObjectRef, UFunction* Function, void* Params)
@@ -518,13 +498,6 @@ RPCPayload USpatialSender::CreateRPCPayloadFromParams(UObject* TargetObject, con
#endif
}
-void USpatialSender::SendComponentInterestForActor(USpatialActorChannel* Channel, Worker_EntityId EntityId, bool bNetOwned)
-{
- checkf(!NetDriver->IsServer(), TEXT("Tried to set ComponentInterest on a server-worker. This should never happen!"));
-
- NetDriver->Connection->SendComponentInterest(EntityId, CreateComponentInterestForActor(Channel, bNetOwned));
-}
-
void USpatialSender::SendInterestBucketComponentChange(const Worker_EntityId EntityId, const Worker_ComponentId OldComponent, const Worker_ComponentId NewComponent)
{
if (OldComponent != SpatialConstants::INVALID_COMPONENT_ID)
@@ -552,13 +525,17 @@ void USpatialSender::SendInterestBucketComponentChange(const Worker_EntityId Ent
}
}
-void USpatialSender::SendComponentInterestForSubobject(const FClassInfo& Info, Worker_EntityId EntityId, bool bNetOwned)
+void USpatialSender::SendActorTornOffUpdate(Worker_EntityId EntityId, Worker_ComponentId ComponentId)
{
- checkf(!NetDriver->IsServer(), TEXT("Tried to set ComponentInterest on a server-worker. This should never happen!"));
+ FWorkerComponentUpdate ComponentUpdate = {};
- TArray ComponentInterest;
- FillComponentInterests(Info, bNetOwned, ComponentInterest);
- NetDriver->Connection->SendComponentInterest(EntityId, MoveTemp(ComponentInterest));
+ ComponentUpdate.component_id = ComponentId;
+ ComponentUpdate.schema_type = Schema_CreateComponentUpdate();
+ Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(ComponentUpdate.schema_type);
+
+ Schema_AddBool(ComponentObject, SpatialConstants::ACTOR_TEAROFF_ID, 1);
+
+ Connection->SendComponentUpdate(EntityId, &ComponentUpdate);
}
void USpatialSender::SendPositionUpdate(Worker_EntityId EntityId, const FVector& Location)
@@ -631,7 +608,7 @@ void USpatialSender::SetAclWriteAuthority(const SpatialLoadBalanceEnforcer::AclW
if (ComponentId == SpatialConstants::ENTITY_ACL_COMPONENT_ID)
{
- NewAcl->ComponentWriteAcl.Add(ComponentId, { SpatialConstants::GetLoadBalancerAttributeSet(GetDefault()->LoadBalancingWorkerType.WorkerTypeName) });
+ NewAcl->ComponentWriteAcl.Add(ComponentId, { SpatialConstants::UnrealServerAttributeSet } );
continue;
}
@@ -663,206 +640,194 @@ FRPCErrorInfo USpatialSender::SendRPC(const FPendingRPCParams& Params)
return FRPCErrorInfo{ TargetObject, nullptr, ERPCResult::MissingFunctionInfo, true };
}
- const float TimeDiff = (FDateTime::Now() - Params.Timestamp).GetTotalSeconds();
- if (GetDefault()->QueuedOutgoingRPCWaitTime < TimeDiff)
+ USpatialActorChannel* Channel = NetDriver->GetOrCreateSpatialActorChannel(TargetObject);
+ if (Channel == nullptr)
{
- return FRPCErrorInfo{ TargetObject, Function, ERPCResult::TimedOut, true };
+ return FRPCErrorInfo{ TargetObject, Function, ERPCResult::NoActorChannel, true };
}
- if (AActor* TargetActor = Cast(TargetObject))
+ const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function);
+ bool bUseRPCRingBuffer = GetDefault()->UseRPCRingBuffer();
+
+ if (RPCInfo.Type == ERPCType::CrossServer)
{
- if (TargetActor->IsPendingKillPending())
- {
- return FRPCErrorInfo{ TargetObject, Function, ERPCResult::ActorPendingKill, true };
- }
+ SendCrossServerRPC(TargetObject, Function, Params.Payload, Channel, Params.ObjectRef);
+ return FRPCErrorInfo{ TargetObject, Function, ERPCResult::Success };
}
- ERPCResult Result = SendRPCInternal(TargetObject, Function, Params.Payload);
-
- if (Result == ERPCResult::NoAuthority)
+ if (bUseRPCRingBuffer && RPCService != nullptr)
{
- if (AActor* TargetActor = Cast(TargetObject))
+ if (SendRingBufferedRPC(TargetObject, Function, Params.Payload, Channel, Params.ObjectRef))
{
- bool bShouldDrop = !WillHaveAuthorityOverActor(TargetActor, Params.ObjectRef.Entity);
- return FRPCErrorInfo{ TargetObject, Function, Result, bShouldDrop };
+ return FRPCErrorInfo{ TargetObject, Function, ERPCResult::Success };
}
- }
-
- return FRPCErrorInfo{ TargetObject, Function, Result, false };
-}
-
-#if !UE_BUILD_SHIPPING
-void USpatialSender::TrackRPC(AActor* Actor, UFunction* Function, const RPCPayload& Payload, const ERPCType RPCType)
-{
- NETWORK_PROFILER(GNetworkProfiler.TrackSendRPC(Actor, Function, 0, Payload.CountDataBits(), 0, NetDriver->GetSpatialOSNetConnection()));
- NetDriver->SpatialMetrics->TrackSentRPC(Function, RPCType, Payload.PayloadData.Num());
-}
-#endif
-
-bool USpatialSender::WillHaveAuthorityOverActor(AActor* TargetActor, Worker_EntityId TargetEntity)
-{
- bool WillHaveAuthorityOverActor = true;
-
- if (GetDefault()->bEnableOffloading)
- {
- if (!USpatialStatics::IsActorGroupOwnerForActor(TargetActor))
+ else
{
- WillHaveAuthorityOverActor = false;
+ return FRPCErrorInfo{ TargetObject, Function, ERPCResult::RPCServiceFailure };
}
}
- if (GetDefault()->bEnableUnrealLoadBalancer)
+ if (Channel->bCreatingNewEntity && Function->HasAnyFunctionFlags(FUNC_NetClient))
{
- if (NetDriver->VirtualWorkerTranslator != nullptr)
- {
- if (const SpatialGDK::AuthorityIntent* AuthorityIntentComponent = StaticComponentView->GetComponentData(TargetEntity))
- {
- if (AuthorityIntentComponent->VirtualWorkerId != NetDriver->VirtualWorkerTranslator->GetLocalVirtualWorkerId())
- {
- WillHaveAuthorityOverActor = false;
- }
- }
- }
+ SendOnEntityCreationRPC(TargetObject, Function, Params.Payload, Channel, Params.ObjectRef);
+ return FRPCErrorInfo{ TargetObject, Function, ERPCResult::Success };
}
- return WillHaveAuthorityOverActor;
+ return SendLegacyRPC(TargetObject, Function, Params.Payload, Channel, Params.ObjectRef);
}
-ERPCResult USpatialSender::SendRPCInternal(UObject* TargetObject, UFunction* Function, const RPCPayload& Payload)
+void USpatialSender::SendOnEntityCreationRPC(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef)
{
- USpatialActorChannel* Channel = NetDriver->GetOrCreateSpatialActorChannel(TargetObject);
+ check(NetDriver->IsServer());
- if (!Channel)
- {
- UE_LOG(LogSpatialSender, Warning, TEXT("Failed to create an Actor Channel for %s."), *TargetObject->GetName());
- return ERPCResult::NoActorChannel;
- }
const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function);
- const USpatialGDKSettings* SpatialGDKSettings = GetDefault();
- if (Channel->bCreatingNewEntity && !SpatialGDKSettings->UseRPCRingBuffer())
- {
- if (Function->HasAnyFunctionFlags(FUNC_NetClient))
- {
- check(NetDriver->IsServer());
-
- OutgoingOnCreateEntityRPCs.FindOrAdd(Channel->Actor).RPCs.Add(Payload);
+ OutgoingOnCreateEntityRPCs.FindOrAdd(Channel->Actor).RPCs.Add(Payload);
#if !UE_BUILD_SHIPPING
- TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type);
+ TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type);
#endif // !UE_BUILD_SHIPPING
- return ERPCResult::Success;
- }
- }
+}
- Worker_EntityId EntityId = SpatialConstants::INVALID_ENTITY_ID;
+void USpatialSender::SendCrossServerRPC(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef)
+{
+ const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function);
- switch (RPCInfo.Type)
- {
- case ERPCType::CrossServer:
- {
- Worker_ComponentId ComponentId = SpatialConstants::SERVER_TO_SERVER_COMMAND_ENDPOINT_COMPONENT_ID;
+ Worker_ComponentId ComponentId = SpatialConstants::SERVER_TO_SERVER_COMMAND_ENDPOINT_COMPONENT_ID;
- Worker_CommandRequest CommandRequest = CreateRPCCommandRequest(TargetObject, Payload, ComponentId, RPCInfo.Index, EntityId);
+ Worker_EntityId EntityId = SpatialConstants::INVALID_ENTITY_ID;
+ Worker_CommandRequest CommandRequest = CreateRPCCommandRequest(TargetObject, Payload, ComponentId, RPCInfo.Index, EntityId);
- check(EntityId != SpatialConstants::INVALID_ENTITY_ID);
- Worker_RequestId RequestId = Connection->SendCommandRequest(EntityId, &CommandRequest, SpatialConstants::UNREAL_RPC_ENDPOINT_COMMAND_ID);
+ check(EntityId != SpatialConstants::INVALID_ENTITY_ID);
+ Worker_RequestId RequestId = Connection->SendCommandRequest(EntityId, &CommandRequest, SpatialConstants::UNREAL_RPC_ENDPOINT_COMMAND_ID);
+ if (Function->HasAnyFunctionFlags(FUNC_NetReliable))
+ {
+ UE_LOG(LogSpatialSender, Verbose, TEXT("Sending reliable command request (entity: %lld, component: %d, function: %s, attempt: 1)"),
+ EntityId, CommandRequest.component_id, *Function->GetName());
+ Receiver->AddPendingReliableRPC(RequestId, MakeShared(TargetObject, Function, ComponentId, RPCInfo.Index, Payload.PayloadData, 0));
+ }
+ else
+ {
+ UE_LOG(LogSpatialSender, Verbose, TEXT("Sending unreliable command request (entity: %lld, component: %d, function: %s)"),
+ EntityId, CommandRequest.component_id, *Function->GetName());
+ }
#if !UE_BUILD_SHIPPING
- TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type);
+ TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type);
#endif // !UE_BUILD_SHIPPING
+}
- if (Function->HasAnyFunctionFlags(FUNC_NetReliable))
- {
- UE_LOG(LogSpatialSender, Verbose, TEXT("Sending reliable command request (entity: %lld, component: %d, function: %s, attempt: 1)"),
- EntityId, CommandRequest.component_id, *Function->GetName());
- Receiver->AddPendingReliableRPC(RequestId, MakeShared(TargetObject, Function, ComponentId, RPCInfo.Index, Payload.PayloadData, 0));
- }
- else
- {
- UE_LOG(LogSpatialSender, Verbose, TEXT("Sending unreliable command request (entity: %lld, component: %d, function: %s)"),
- EntityId, CommandRequest.component_id, *Function->GetName());
- }
+FRPCErrorInfo USpatialSender::SendLegacyRPC(UObject* TargetObject, UFunction* Function, const RPCPayload& Payload, USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef)
+{
+ const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function);
- return ERPCResult::Success;
+ // Check if the Channel is listening
+ if ((RPCInfo.Type != ERPCType::NetMulticast) && !Channel->IsListening())
+ {
+ // If the Entity endpoint is not yet ready to receive RPCs -
+ // treat the corresponding object as unresolved and queue RPC
+ // However, it doesn't matter in case of Multicast
+ return FRPCErrorInfo{ TargetObject, Function, ERPCResult::SpatialActorChannelNotListening };
}
- case ERPCType::NetMulticast:
- case ERPCType::ClientReliable:
- case ERPCType::ServerReliable:
- case ERPCType::ClientUnreliable:
- case ERPCType::ServerUnreliable:
+
+ // Check for Authority
+ Worker_EntityId EntityId = TargetObjectRef.Entity;
+ check(EntityId != SpatialConstants::INVALID_ENTITY_ID);
+
+ Worker_ComponentId ComponentId = SpatialConstants::RPCTypeToWorkerComponentIdLegacy(RPCInfo.Type);
+ if (!NetDriver->StaticComponentView->HasAuthority(EntityId, ComponentId))
{
- FUnrealObjectRef TargetObjectRef = PackageMap->GetUnrealObjectRefFromObject(TargetObject);
- if (TargetObjectRef == FUnrealObjectRef::UNRESOLVED_OBJECT_REF)
+ bool bShouldDrop = true;
+ if (AActor* TargetActor = Cast(TargetObject))
{
- return ERPCResult::UnresolvedTargetObject;
+ bShouldDrop = !WillHaveAuthorityOverActor(TargetActor, TargetObjectRef.Entity);
}
- if (SpatialGDKSettings->UseRPCRingBuffer() && RPCService != nullptr)
- {
- EPushRPCResult Result = RPCService->PushRPC(TargetObjectRef.Entity, RPCInfo.Type, Payload);
+ return FRPCErrorInfo{ TargetObject, Function, ERPCResult::NoAuthority, bShouldDrop };
+ }
- if (Result == EPushRPCResult::Success)
- {
- FlushRPCService();
- }
+ FWorkerComponentUpdate ComponentUpdate = CreateRPCEventUpdate(TargetObject, Payload, ComponentId, RPCInfo.Index);
+ Connection->SendComponentUpdate(EntityId, &ComponentUpdate);
+ Connection->MaybeFlush();
#if !UE_BUILD_SHIPPING
- if (Result == EPushRPCResult::Success || Result == EPushRPCResult::QueueOverflowed)
- {
- TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type);
- }
+ TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type);
#endif // !UE_BUILD_SHIPPING
- switch (Result)
- {
- case EPushRPCResult::QueueOverflowed:
- UE_LOG(LogSpatialSender, Log, TEXT("USpatialSender::SendRPCInternal: Ring buffer queue overflowed, queuing RPC locally. Actor: %s, entity: %lld, function: %s"), *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName());
- break;
- case EPushRPCResult::DropOverflowed:
- UE_LOG(LogSpatialSender, Log, TEXT("USpatialSender::SendRPCInternal: Ring buffer queue overflowed, dropping RPC. Actor: %s, entity: %lld, function: %s"), *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName());
- break;
- case EPushRPCResult::HasAckAuthority:
- UE_LOG(LogSpatialSender, Warning, TEXT("USpatialSender::SendRPCInternal: Worker has authority over ack component for RPC it is sending. RPC will not be sent. Actor: %s, entity: %lld, function: %s"), *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName());
- break;
- case EPushRPCResult::NoRingBufferAuthority:
- // TODO: Change engine logic that calls Client RPCs from non-auth servers and change this to error. UNR-2517
- UE_LOG(LogSpatialSender, Log, TEXT("USpatialSender::SendRPCInternal: Failed to send RPC because the worker does not have authority over ring buffer component. Actor: %s, entity: %lld, function: %s"), *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName());
- break;
- }
-
- return ERPCResult::Success;
- }
-
- if (RPCInfo.Type != ERPCType::NetMulticast && !Channel->IsListening())
- {
- // If the Entity endpoint is not yet ready to receive RPCs -
- // treat the corresponding object as unresolved and queue RPC
- // However, it doesn't matter in case of Multicast
- return ERPCResult::SpatialActorChannelNotListening;
- }
-
- EntityId = TargetObjectRef.Entity;
- check(EntityId != SpatialConstants::INVALID_ENTITY_ID);
-
- Worker_ComponentId ComponentId = SpatialConstants::RPCTypeToWorkerComponentIdLegacy(RPCInfo.Type);
+ return FRPCErrorInfo{ TargetObject, Function, ERPCResult::Success };
+}
- if (!NetDriver->StaticComponentView->HasAuthority(EntityId, ComponentId))
- {
- return ERPCResult::NoAuthority;
- }
+bool USpatialSender::SendRingBufferedRPC(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef)
+{
+ const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function);
+ const EPushRPCResult Result = RPCService->PushRPC(TargetObjectRef.Entity, RPCInfo.Type, Payload, Channel->bCreatedEntity);
- FWorkerComponentUpdate ComponentUpdate = CreateRPCEventUpdate(TargetObject, Payload, ComponentId, RPCInfo.Index);
+ if (Result == EPushRPCResult::Success)
+ {
+ FlushRPCService();
+ }
- Connection->SendComponentUpdate(EntityId, &ComponentUpdate);
#if !UE_BUILD_SHIPPING
+ if (Result == EPushRPCResult::Success || Result == EPushRPCResult::QueueOverflowed)
+ {
TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type);
-#endif // !UE_BUILD_SHIPPING
- return ERPCResult::Success;
}
+#endif // !UE_BUILD_SHIPPING
+
+ switch (Result)
+ {
+ case EPushRPCResult::QueueOverflowed:
+ UE_LOG(LogSpatialSender, Log, TEXT("USpatialSender::SendRingBufferedRPC: Ring buffer queue overflowed, queuing RPC locally. Actor: %s, entity: %lld, function: %s"),
+ *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName());
+ return true;
+ case EPushRPCResult::DropOverflowed:
+ UE_LOG(LogSpatialSender, Log, TEXT("USpatialSender::SendRingBufferedRPC: Ring buffer queue overflowed, dropping RPC. Actor: %s, entity: %lld, function: %s"),
+ *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName());
+ return true;
+ case EPushRPCResult::HasAckAuthority:
+ UE_LOG(LogSpatialSender, Warning,
+ TEXT("USpatialSender::SendRingBufferedRPC: Worker has authority over ack component for RPC it is sending. RPC will not be sent. Actor: %s, entity: %lld, function: %s"),
+ *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName());
+ return true;
+ case EPushRPCResult::NoRingBufferAuthority:
+ // TODO: Change engine logic that calls Client RPCs from non-auth servers and change this to error. UNR-2517
+ UE_LOG(LogSpatialSender, Log,
+ TEXT("USpatialSender::SendRingBufferedRPC: Failed to send RPC because the worker does not have authority over ring buffer component. Actor: %s, entity: %lld, function: %s"),
+ *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName());
+ return true;
+ case EPushRPCResult::EntityBeingCreated:
+ UE_LOG(LogSpatialSender, Log,
+ TEXT("USpatialSender::SendRingBufferedRPC: RPC was called between entity creation and initial authority gain, so it will be queued. Actor: %s, entity: %lld, function: %s"),
+ *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName());
+ return false;
default:
- checkNoEntry();
- return ERPCResult::InvalidRPCType;
+ return true;
+ }
+}
+
+#if !UE_BUILD_SHIPPING
+void USpatialSender::TrackRPC(AActor* Actor, UFunction* Function, const RPCPayload& Payload, const ERPCType RPCType)
+{
+ NETWORK_PROFILER(GNetworkProfiler.TrackSendRPC(Actor, Function, 0, Payload.CountDataBits(), 0, NetDriver->GetSpatialOSNetConnection()));
+ NetDriver->SpatialMetrics->TrackSentRPC(Function, RPCType, Payload.PayloadData.Num());
+}
+#endif
+
+bool USpatialSender::WillHaveAuthorityOverActor(AActor* TargetActor, Worker_EntityId TargetEntity)
+{
+ bool WillHaveAuthorityOverActor = true;
+
+ if (NetDriver->VirtualWorkerTranslator != nullptr)
+ {
+ if (const SpatialGDK::AuthorityIntent* AuthorityIntentComponent = StaticComponentView->GetComponentData(TargetEntity))
+ {
+ if (AuthorityIntentComponent->VirtualWorkerId != NetDriver->VirtualWorkerTranslator->GetLocalVirtualWorkerId())
+ {
+ WillHaveAuthorityOverActor = false;
+ }
+ }
}
+
+ return WillHaveAuthorityOverActor;
}
void USpatialSender::EnqueueRetryRPC(TSharedRef RetryRPC)
@@ -1100,31 +1065,29 @@ void USpatialSender::UpdateInterestComponent(AActor* Actor)
Connection->SendComponentUpdate(EntityId, &Update);
}
-void USpatialSender::RetireEntity(const Worker_EntityId EntityId)
+void USpatialSender::RetireEntity(const Worker_EntityId EntityId, bool bIsNetStartupActor)
{
- if (AActor* Actor = Cast(PackageMap->GetObjectFromEntityId(EntityId).Get()))
+ if (bIsNetStartupActor)
{
- if (Actor->IsNetStartupActor())
+ Receiver->RemoveActor(EntityId);
+ // In the case that this is a startup actor, we won't actually delete the entity in SpatialOS. Instead we'll Tombstone it.
+ if (!StaticComponentView->HasComponent(EntityId, SpatialConstants::TOMBSTONE_COMPONENT_ID))
{
- Receiver->RemoveActor(EntityId);
- // In the case that this is a startup actor, we won't actually delete the entity in SpatialOS. Instead we'll Tombstone it.
- if (!StaticComponentView->HasComponent(EntityId, SpatialConstants::TOMBSTONE_COMPONENT_ID))
- {
- AddTombstoneToEntity(EntityId);
- }
- else
- {
- UE_LOG(LogSpatialSender, Verbose, TEXT("RetireEntity called on already retired entity: %lld (actor: %s)"), EntityId, *Actor->GetName());
- }
+ UE_LOG(LogSpatialSender, Log, TEXT("Adding tombstone to entity: %lld"), EntityId);
+ AddTombstoneToEntity(EntityId);
}
else
{
- Connection->SendDeleteEntityRequest(EntityId);
+ UE_LOG(LogSpatialSender, Verbose, TEXT("RetireEntity called on already retired entity: %lld"), EntityId);
}
}
else
{
- UE_LOG(LogSpatialSender, Warning, TEXT("RetireEntity: Couldn't get Actor from PackageMap for EntityId: %lld"), EntityId);
+ // Actor no longer guaranteed to be in package map, but still useful for additional logging info
+ AActor* Actor = Cast(PackageMap->GetObjectFromEntityId(EntityId));
+
+ UE_LOG(LogSpatialSender, Log, TEXT("Sending delete entity request for %s with EntityId %lld, HasAuthority: %d"), *GetPathNameSafe(Actor), EntityId, Actor != nullptr ? Actor->HasAuthority() : false);
+ Connection->SendDeleteEntityRequest(EntityId);
}
}
@@ -1134,7 +1097,7 @@ void USpatialSender::CreateTombstoneEntity(AActor* Actor)
const Worker_EntityId EntityId = NetDriver->PackageMap->AllocateEntityIdAndResolveActor(Actor);
- EntityFactory DataFactory(NetDriver, PackageMap, ClassInfoManager, ActorGroupManager, RPCService);
+ EntityFactory DataFactory(NetDriver, PackageMap, ClassInfoManager, RPCService);
TArray Components = DataFactory.CreateTombstoneEntityComponents(Actor);
Components.Add(CreateLevelComponentData(Actor));
diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSnapshotManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSnapshotManager.cpp
index 08d194b61c..97541dfdf6 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSnapshotManager.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSnapshotManager.cpp
@@ -183,7 +183,7 @@ void SpatialSnapshotManager::LoadSnapshot(const FString& SnapshotName)
// Check if this is the GSM
for (auto& ComponentData : EntityToSpawn)
{
- if (ComponentData.component_id == SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID)
+ if (ComponentData.component_id == SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID)
{
// Save the new GSM Entity ID.
GlobalStateManager->GlobalStateManagerEntityId = ReservedEntityID;
diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp
index c0e3a96716..2481a7d3a2 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp
@@ -14,7 +14,6 @@
#include "Schema/RPCPayload.h"
#include "Schema/ServerEndpoint.h"
#include "Schema/ServerRPCEndpointLegacy.h"
-#include "Schema/Singleton.h"
#include "Schema/SpatialDebugging.h"
#include "Schema/SpawnData.h"
#include "Schema/UnrealMetadata.h"
@@ -49,6 +48,13 @@ bool USpatialStaticComponentView::HasComponent(Worker_EntityId EntityId, Worker_
void USpatialStaticComponentView::OnAddComponent(const Worker_AddComponentOp& Op)
{
+ // With dynamic components enabled, it's possible to get duplicate AddComponent ops which need handling idempotently.
+ // For the sake of efficiency we just exit early here.
+ if (HasComponent(Op.entity_id, Op.data.component_id))
+ {
+ return;
+ }
+
TUniquePtr Data;
switch (Op.data.component_id)
{
@@ -70,9 +76,6 @@ void USpatialStaticComponentView::OnAddComponent(const Worker_AddComponentOp& Op
case SpatialConstants::SPAWN_DATA_COMPONENT_ID:
Data = MakeUnique(Op.data);
break;
- case SpatialConstants::SINGLETON_COMPONENT_ID:
- Data = MakeUnique(Op.data);
- break;
case SpatialConstants::UNREAL_METADATA_COMPONENT_ID:
Data = MakeUnique(Op.data);
break;
diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp
index 3833d0e994..ec00fc501a 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp
@@ -16,6 +16,8 @@ UGridBasedLBStrategy::UGridBasedLBStrategy()
, WorldWidth(1000000.f)
, WorldHeight(1000000.f)
, InterestBorder(0.f)
+ , LocalCellId(0)
+ , bIsStrategyUsedOnLocalWorker(false)
{
}
@@ -25,11 +27,6 @@ void UGridBasedLBStrategy::Init()
UE_LOG(LogGridBasedLBStrategy, Log, TEXT("GridBasedLBStrategy initialized with Rows = %d and Cols = %d."), Rows, Cols);
- for (uint32 i = 1; i <= Rows * Cols; i++)
- {
- VirtualWorkerIds.Add(i);
- }
-
const float WorldWidthMin = -(WorldWidth / 2.f);
const float WorldHeightMin = -(WorldHeight / 2.f);
@@ -63,6 +60,22 @@ void UGridBasedLBStrategy::Init()
}
}
+void UGridBasedLBStrategy::SetLocalVirtualWorkerId(VirtualWorkerId InLocalVirtualWorkerId)
+{
+ if (!VirtualWorkerIds.Contains(InLocalVirtualWorkerId))
+ {
+ // This worker is simulating a layer which is not part of the grid.
+ LocalCellId = WorkerCells.Num();
+ bIsStrategyUsedOnLocalWorker = false;
+ }
+ else
+ {
+ LocalCellId = VirtualWorkerIds.IndexOfByKey(InLocalVirtualWorkerId);
+ bIsStrategyUsedOnLocalWorker = true;
+ }
+ LocalVirtualWorkerId = InLocalVirtualWorkerId;
+}
+
TSet UGridBasedLBStrategy::GetVirtualWorkerIds() const
{
return TSet(VirtualWorkerIds);
@@ -76,8 +89,13 @@ bool UGridBasedLBStrategy::ShouldHaveAuthority(const AActor& Actor) const
return false;
}
+ if (!bIsStrategyUsedOnLocalWorker)
+ {
+ return false;
+ }
+
const FVector2D Actor2DLocation = FVector2D(SpatialGDK::GetActorSpatialPosition(&Actor));
- return IsInside(WorkerCells[LocalVirtualWorkerId - 1], Actor2DLocation);
+ return IsInside(WorkerCells[LocalCellId], Actor2DLocation);
}
VirtualWorkerId UGridBasedLBStrategy::WhoShouldHaveAuthority(const AActor& Actor) const
@@ -90,10 +108,12 @@ VirtualWorkerId UGridBasedLBStrategy::WhoShouldHaveAuthority(const AActor& Actor
const FVector2D Actor2DLocation = FVector2D(SpatialGDK::GetActorSpatialPosition(&Actor));
+ check(VirtualWorkerIds.Num() == WorkerCells.Num());
for (int i = 0; i < WorkerCells.Num(); i++)
{
if (IsInside(WorkerCells[i], Actor2DLocation))
{
+ UE_LOG(LogGridBasedLBStrategy, Log, TEXT("Actor: %s, grid %d, worker %d for position %f, %f"), *AActor::GetDebugName(&Actor), i, VirtualWorkerIds[i], Actor2DLocation.X, Actor2DLocation.Y);
return VirtualWorkerIds[i];
}
}
@@ -105,8 +125,9 @@ SpatialGDK::QueryConstraint UGridBasedLBStrategy::GetWorkerInterestQueryConstrai
{
// For a grid-based strategy, the interest area is the cell that the worker is authoritative over plus some border region.
check(IsReady());
+ check(bIsStrategyUsedOnLocalWorker);
- const FBox2D Interest2D = WorkerCells[LocalVirtualWorkerId - 1].ExpandBy(InterestBorder);
+ const FBox2D Interest2D = WorkerCells[LocalCellId].ExpandBy(InterestBorder);
const FVector2D Center2D = Interest2D.GetCenter();
const FVector Center3D{ Center2D.X, Center2D.Y, 0.0f};
@@ -123,10 +144,25 @@ SpatialGDK::QueryConstraint UGridBasedLBStrategy::GetWorkerInterestQueryConstrai
FVector UGridBasedLBStrategy::GetWorkerEntityPosition() const
{
check(IsReady());
- const FVector2D Centre = WorkerCells[LocalVirtualWorkerId - 1].GetCenter();
+ check(bIsStrategyUsedOnLocalWorker);
+ const FVector2D Centre = WorkerCells[LocalCellId].GetCenter();
return FVector{ Centre.X, Centre.Y, 0.f };
}
+uint32 UGridBasedLBStrategy::GetMinimumRequiredWorkers() const
+{
+ return Rows * Cols;
+}
+
+void UGridBasedLBStrategy::SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtualWorkerId, const VirtualWorkerId& LastVirtualWorkerId)
+{
+ UE_LOG(LogGridBasedLBStrategy, Log, TEXT("Setting VirtualWorkerIds %d to %d"), FirstVirtualWorkerId, LastVirtualWorkerId);
+ for (VirtualWorkerId CurrentVirtualWorkerId = FirstVirtualWorkerId; CurrentVirtualWorkerId <= LastVirtualWorkerId; CurrentVirtualWorkerId++)
+ {
+ VirtualWorkerIds.Add(CurrentVirtualWorkerId);
+ }
+}
+
bool UGridBasedLBStrategy::IsInside(const FBox2D& Box, const FVector2D& Location)
{
return Location.X >= Box.Min.X && Location.Y >= Box.Min.Y
diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/LayeredLBStrategy.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/LayeredLBStrategy.cpp
new file mode 100644
index 0000000000..b5e36da985
--- /dev/null
+++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/LayeredLBStrategy.cpp
@@ -0,0 +1,315 @@
+// Copyright (c) Improbable Worlds Ltd, All Rights Reserved
+
+#include "LoadBalancing/LayeredLBStrategy.h"
+
+#include "EngineClasses/SpatialNetDriver.h"
+#include "EngineClasses/SpatialWorldSettings.h"
+#include "LoadBalancing/GridBasedLBStrategy.h"
+#include "Utils/LayerInfo.h"
+#include "Utils/SpatialActorUtils.h"
+
+#include "Templates/Tuple.h"
+
+DEFINE_LOG_CATEGORY(LogLayeredLBStrategy);
+
+ULayeredLBStrategy::ULayeredLBStrategy()
+ : Super()
+{
+}
+
+void ULayeredLBStrategy::Init()
+{
+ Super::Init();
+
+ VirtualWorkerId CurrentVirtualWorkerId = SpatialConstants::INVALID_VIRTUAL_WORKER_ID + 1;
+
+ const ASpatialWorldSettings* WorldSettings = GetWorld() ? Cast(GetWorld()->GetWorldSettings()) : nullptr;
+ const bool bIsMultiWorkerEnabled = WorldSettings != nullptr && WorldSettings->IsMultiWorkerEnabled();
+
+ if (!bIsMultiWorkerEnabled)
+ {
+ UE_LOG(LogLayeredLBStrategy, Log, TEXT("Multi-Worker has been disabled. Creating LBStrategy for the Default Layer"));
+ UAbstractLBStrategy* DefaultLBStrategy = NewObject(this);
+ AddStrategyForLayer(SpatialConstants::DefaultLayer, DefaultLBStrategy);
+ return;
+ }
+
+ // For each Layer, add a LB Strategy for that layer.
+ for (const TPair& Layer : WorldSettings->WorkerLayers)
+ {
+ const FName& LayerName = Layer.Key;
+ const FLayerInfo& LayerInfo = Layer.Value;
+
+ UAbstractLBStrategy* LBStrategy;
+ if (LayerInfo.LoadBalanceStrategy == nullptr)
+ {
+ UE_LOG(LogLayeredLBStrategy, Error, TEXT("WorkerLayer %s does not specify a loadbalancing strategy (or it cannot be resolved). Using a 1x1 grid."), *LayerName.ToString());
+ LBStrategy = NewObject(this);
+ }
+ else
+ {
+ LBStrategy = NewObject(this, LayerInfo.LoadBalanceStrategy);
+ }
+
+ AddStrategyForLayer(LayerName, LBStrategy);
+
+ UE_LOG(LogLayeredLBStrategy, Log, TEXT("Creating LBStrategy for Layer %s."), *LayerName.ToString());
+ for (const TSoftClassPtr& ClassPtr : LayerInfo.ActorClasses)
+ {
+ UE_LOG(LogLayeredLBStrategy, Log, TEXT(" - Adding class %s."), *ClassPtr.GetAssetName());
+ ClassPathToLayer.Add(ClassPtr, LayerName);
+
+ }
+ }
+
+ // Finally, add the default layer.
+ UE_LOG(LogLayeredLBStrategy, Log, TEXT("Creating LBStrategy for the Default Layer."));
+ if (WorldSettings->DefaultLayerLoadBalanceStrategy == nullptr)
+ {
+ UE_LOG(LogLayeredLBStrategy, Error, TEXT("If EnableMultiWorker is set, there must be a LoadBalancing strategy set. Using a 1x1 grid."));
+ UAbstractLBStrategy* DefaultLBStrategy = NewObject(this);
+ AddStrategyForLayer(SpatialConstants::DefaultLayer, DefaultLBStrategy);
+ }
+ else
+ {
+ UAbstractLBStrategy* DefaultLBStrategy = NewObject(this, WorldSettings->DefaultLayerLoadBalanceStrategy);
+ AddStrategyForLayer(SpatialConstants::DefaultLayer, DefaultLBStrategy);
+
+ // Any class not specified on one of the other layers will be on the default layer. However, some games may have a class hierarchy with
+ // some parts of the hierarchy on different layers. This provides a way to specify that.
+ for (const TSoftClassPtr& ClassPtr : WorldSettings->ExplicitDefaultActorClasses)
+ {
+ UE_LOG(LogLayeredLBStrategy, Log, TEXT(" - Adding class to default layer %s."), *ClassPtr.GetAssetName());
+ ClassPathToLayer.Add(ClassPtr, SpatialConstants::DefaultLayer);
+ }
+ }
+}
+
+void ULayeredLBStrategy::SetLocalVirtualWorkerId(VirtualWorkerId InLocalVirtualWorkerId)
+{
+ if (LocalVirtualWorkerId != SpatialConstants::INVALID_VIRTUAL_WORKER_ID)
+ {
+ UE_LOG(LogLayeredLBStrategy, Error,
+ TEXT("The Local Virtual Worker Id cannot be set twice. Current value: %d Requested new value: %d"),
+ LocalVirtualWorkerId, InLocalVirtualWorkerId);
+ return;
+ }
+
+ LocalVirtualWorkerId = InLocalVirtualWorkerId;
+ for (const auto& Elem : LayerNameToLBStrategy)
+ {
+ Elem.Value->SetLocalVirtualWorkerId(InLocalVirtualWorkerId);
+ }
+}
+
+TSet ULayeredLBStrategy::GetVirtualWorkerIds() const
+{
+ return TSet(VirtualWorkerIds);
+}
+
+bool ULayeredLBStrategy::ShouldHaveAuthority(const AActor& Actor) const
+{
+ if (!IsReady())
+ {
+ UE_LOG(LogLayeredLBStrategy, Warning, TEXT("LayeredLBStrategy not ready to relinquish authority for Actor %s."), *AActor::GetDebugName(&Actor));
+ return false;
+ }
+
+ const AActor* RootOwner = &Actor;
+ while (RootOwner->GetOwner() != nullptr && RootOwner->GetOwner()->GetIsReplicated())
+ {
+ RootOwner = RootOwner->GetOwner();
+ }
+
+ const FName& LayerName = GetLayerNameForActor(*RootOwner);
+ if (!LayerNameToLBStrategy.Contains(LayerName))
+ {
+ UE_LOG(LogLayeredLBStrategy, Error, TEXT("LayeredLBStrategy doesn't have a LBStrategy for Actor %s which is in Layer %s."), *AActor::GetDebugName(RootOwner), *LayerName.ToString());
+ return false;
+ }
+
+ // If this worker is not responsible for the Actor's layer, just return false.
+ if (VirtualWorkerIdToLayerName.Contains(LocalVirtualWorkerId) && VirtualWorkerIdToLayerName[LocalVirtualWorkerId] != LayerName)
+ {
+ return false;
+ }
+
+ return LayerNameToLBStrategy[LayerName]->ShouldHaveAuthority(Actor);
+}
+
+VirtualWorkerId ULayeredLBStrategy::WhoShouldHaveAuthority(const AActor& Actor) const
+{
+ if (!IsReady())
+ {
+ UE_LOG(LogLayeredLBStrategy, Warning, TEXT("LayeredLBStrategy not ready to decide on authority for Actor %s."), *AActor::GetDebugName(&Actor));
+ return SpatialConstants::INVALID_VIRTUAL_WORKER_ID;
+ }
+
+ const AActor* RootOwner = &Actor;
+ while (RootOwner->GetOwner() != nullptr && RootOwner->GetOwner()->GetIsReplicated())
+ {
+ RootOwner = RootOwner->GetOwner();
+ }
+
+ const FName& LayerName = GetLayerNameForActor(*RootOwner);
+ if (!LayerNameToLBStrategy.Contains(LayerName))
+ {
+ UE_LOG(LogLayeredLBStrategy, Error, TEXT("LayeredLBStrategy doesn't have a LBStrategy for Actor %s which is in Layer %s."), *AActor::GetDebugName(RootOwner), *LayerName.ToString());
+ return SpatialConstants::INVALID_VIRTUAL_WORKER_ID;
+ }
+
+ const VirtualWorkerId ReturnedWorkerId = LayerNameToLBStrategy[LayerName]->WhoShouldHaveAuthority(*RootOwner);
+
+ UE_LOG(LogLayeredLBStrategy, Log, TEXT("LayeredLBStrategy returning virtual worker id %d for Actor %s."), ReturnedWorkerId, *AActor::GetDebugName(RootOwner));
+ return ReturnedWorkerId;
+}
+
+SpatialGDK::QueryConstraint ULayeredLBStrategy::GetWorkerInterestQueryConstraint() const
+{
+ check(IsReady());
+ if (!VirtualWorkerIdToLayerName.Contains(LocalVirtualWorkerId))
+ {
+ UE_LOG(LogLayeredLBStrategy, Error, TEXT("LayeredLBStrategy doesn't have a LBStrategy for worker %d."), LocalVirtualWorkerId);
+ SpatialGDK::QueryConstraint Constraint;
+ Constraint.ComponentConstraint = 0;
+ return Constraint;
+ }
+ else
+ {
+ const FName& LayerName = VirtualWorkerIdToLayerName[LocalVirtualWorkerId];
+ check(LayerNameToLBStrategy.Contains(LayerName));
+ return LayerNameToLBStrategy[LayerName]->GetWorkerInterestQueryConstraint();
+ }
+}
+
+FVector ULayeredLBStrategy::GetWorkerEntityPosition() const
+{
+ check(IsReady());
+ 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 };
+ }
+ else
+ {
+ const FName& LayerName = VirtualWorkerIdToLayerName[LocalVirtualWorkerId];
+ check(LayerNameToLBStrategy.Contains(LayerName));
+ return LayerNameToLBStrategy[LayerName]->GetWorkerEntityPosition();
+ }
+}
+
+uint32 ULayeredLBStrategy::GetMinimumRequiredWorkers() const
+{
+ // The MinimumRequiredWorkers for this strategy is a sum of the required workers for each of the wrapped strategies.
+ uint32 MinimumRequiredWorkers = 0;
+ for (const auto& Elem : LayerNameToLBStrategy)
+ {
+ MinimumRequiredWorkers += Elem.Value->GetMinimumRequiredWorkers();
+ }
+
+ UE_LOG(LogLayeredLBStrategy, Verbose, TEXT("LayeredLBStrategy needs %d workers to support all layer strategies."), MinimumRequiredWorkers);
+ return MinimumRequiredWorkers;
+}
+
+void ULayeredLBStrategy::SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtualWorkerId, const VirtualWorkerId& LastVirtualWorkerId)
+{
+ // If the LayeredLBStrategy wraps { SingletonStrategy, 2x2 grid, Singleton } and is given IDs 1 through 6 it will assign:
+ // Singleton : 1
+ // Grid : 2 - 5
+ // Singleton: 6
+ VirtualWorkerId NextWorkerIdToAssign = FirstVirtualWorkerId;
+ for (const auto& Elem : LayerNameToLBStrategy)
+ {
+ UAbstractLBStrategy* LBStrategy = Elem.Value;
+ VirtualWorkerId MinimumRequiredWorkers = LBStrategy->GetMinimumRequiredWorkers();
+
+ VirtualWorkerId LastVirtualWorkerIdToAssign = NextWorkerIdToAssign + MinimumRequiredWorkers - 1;
+ if (LastVirtualWorkerIdToAssign > LastVirtualWorkerId)
+ {
+ UE_LOG(LogLayeredLBStrategy, Error, TEXT("LayeredLBStrategy was not given enough VirtualWorkerIds to meet the demands of the layer strategies."));
+ return;
+ }
+ UE_LOG(LogLayeredLBStrategy, Log, TEXT("LayeredLBStrategy assigning VirtualWorkerIds %d to %d to Layer %s"), NextWorkerIdToAssign, LastVirtualWorkerIdToAssign, *Elem.Key.ToString());
+ LBStrategy->SetVirtualWorkerIds(NextWorkerIdToAssign, LastVirtualWorkerIdToAssign);
+
+ for (VirtualWorkerId id = NextWorkerIdToAssign; id <= LastVirtualWorkerIdToAssign; id++)
+ {
+ VirtualWorkerIdToLayerName.Add(id, Elem.Key);
+ }
+
+ NextWorkerIdToAssign += MinimumRequiredWorkers;
+ }
+
+ // Keep a copy of the VirtualWorkerIds. This is temporary and will be removed in the next PR.
+ for (VirtualWorkerId CurrentVirtualWorkerId = FirstVirtualWorkerId; CurrentVirtualWorkerId <= LastVirtualWorkerId; CurrentVirtualWorkerId++)
+ {
+ VirtualWorkerIds.Add(CurrentVirtualWorkerId);
+ }
+}
+
+// DEPRECATED
+// This is only included because Scavengers uses the function in SpatialStatics that calls this.
+// 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());
+ return *VirtualWorkerIdToLayerName.Find(LocalVirtualWorkerId) == GetLayerNameForClass(Class);
+}
+
+UAbstractLBStrategy* ULayeredLBStrategy::GetLBStrategyForVisualRendering() const
+{
+ // The default strategy is guaranteed to exist as long as the strategy is ready.
+ check(IsReady());
+ return LayerNameToLBStrategy[SpatialConstants::DefaultLayer];
+}
+
+FName ULayeredLBStrategy::GetLayerNameForClass(const TSubclassOf Class) const
+{
+ if (Class == nullptr)
+ {
+ return NAME_None;
+ }
+
+ UClass* FoundClass = Class;
+ TSoftClassPtr ClassPtr = TSoftClassPtr