diff --git a/.buildkite/hooks/pre-command b/.buildkite/hooks/pre-command
new file mode 100755
index 0000000000..29a754c1ba
--- /dev/null
+++ b/.buildkite/hooks/pre-command
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+
+set -e -u -o pipefail
+if [[ -n "${DEBUG-}" ]]; then
+ set -x
+fi
+
+if [[ ${BUILDKITE_AGENT_META_DATA_OS} == darwin* ]]; then
+ ./ci/cleanup.sh
+elif [[ ${BUILDKITE_AGENT_META_DATA_OS} == linux* ]]; then
+ echo "Running a linux agent, no need for any cleanup."
+ exit 0
+else
+ powershell -noprofile -noninteractive -file "./ci/cleanup.ps1"
+fi
diff --git a/.buildkite/hooks/pre-exit.bat b/.buildkite/hooks/pre-exit.bat
deleted file mode 100644
index a60474e46d..0000000000
--- a/.buildkite/hooks/pre-exit.bat
+++ /dev/null
@@ -1,3 +0,0 @@
-@echo off
-echo "Cleaning up symlinks..."
-powershell ".\ci\cleanup.ps1"
\ No newline at end of file
diff --git a/.buildkite/premerge.steps.yaml b/.buildkite/premerge.steps.yaml
index 30ad20d17b..e34cc50887 100755
--- a/.buildkite/premerge.steps.yaml
+++ b/.buildkite/premerge.steps.yaml
@@ -1,9 +1,17 @@
---
+# This is designed to trap and retry failures because agent lost
+# connection. Agent exits with -1 in this case.
agent_transients: &agent_transients
- # This is designed to trap and retry failures because agent lost
- # connection. Agent exits with -1 in this case.
exit_status: -1
limit: 3
+# BK system error
+bk_system_error: &bk_system_error
+ exit_status: 255
+ limit: 3
+# job was interrupted by a signal (e.g. ctrl+c etc)
+bk_interrupted_by_signal: &bk_interrupted_by_signal
+ exit_status: 15
+ limit: 3
script_runner: &script_runner
agents:
@@ -13,33 +21,63 @@ script_runner: &script_runner
- "machine_type=quarter"
- "permission_set=builder"
- "platform=linux"
- - "queue=${CI_LINUX_BUILDER_QUEUE:-v3-1571392077-9a4506d980673dea-------z}"
+ - "queue=${CI_LINUX_BUILDER_QUEUE:-v4-2019-12-12-bk5225-daecba805768d787}"
- "scaler_version=2"
- "working_hours_time_zone=london"
+windows: &windows
+ agents:
+ - "agent_count=1"
+ - "capable_of_building=gdk-for-unreal"
+ - "environment=production"
+ - "machine_type=single-high-cpu" # this name matches to SpatialOS node-size names
+ - "platform=windows"
+ - "permission_set=builder"
+ - "scaler_version=2"
+ - "queue=${CI_WINDOWS_BUILDER_QUEUE:-v4-20-03-26-102432-bk9951-8afe0ffb}"
+ retry:
+ automatic:
+ - <<: *agent_transients
+ - <<: *bk_system_error
+ - <<: *bk_interrupted_by_signal
+ timeout_in_minutes: 60
+ plugins:
+ - ca-johnson/taskkill#v4.1: ~
+
# NOTE: step labels turn into commit-status names like {org}/{repo}/{pipeline}/{step-label}, lower-case and hyphenated.
# These are then relied on to have stable names by other things, so once named, please beware renaming has consequences.
steps:
- - label: "enforce version restrictions"
+ - label: "enforce-version-restrictions"
command: "ci/check-version-file.sh"
<<: *script_runner
# No point in running other steps if the listed versions are invalid
- - wait
+ - wait: ~
# 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"
+ label: "post-merge-example-project-build"
branches: "master preview release"
async: true
build:
env:
+ NIGHTLY_BUILD: "${NIGHTLY_BUILD:-false}"
GDK_BRANCH: "${BUILDKITE_BRANCH}"
- - label: "generate and upload GDK build steps"
+ - label: "generate-pipeline-steps"
commands:
- - "chmod -R +rwx ci" # give the Linux user access to everything in the ci directory
- - "ci/generate_and_upload_build_steps.sh"
- env:
- ENGINE_VERSION: "${ENGINE_VERSION}"
+ - "ci/generate-and-upload-build-steps.sh"
+ <<: *script_runner
+
+ - wait: ~
+ continue_on_failure: true
+
+ - label: "upload-test-metrics"
+ if: build.env("NIGHTLY_BUILD") == "true"
+ command: "ci/upload-test-metrics.sh"
<<: *script_runner
+
+ - label: "slack-notify"
+ if: (build.env("SLACK_NOTIFY") == "true" || build.branch == "master") && build.env("SLACK_NOTIFY") != "false"
+ commands: "powershell ./ci/build-and-send-slack-notification.ps1"
+ <<: *windows
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8fa5c50bad..cb5c6a04b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,113 @@ 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 版本起,其日志提供中英文两个版本。每个日志的中文版本都置于英文版本之后。
-## [Unreleased-`x.y.z`] - 2020-xx-xx
+## [`0.9.0`] - 2020-05-05
+
+### New Known Issues:
+- After you upgrade to Unreal Engine `4.24.3` using `git pull`, you might be left in a state where several `.isph` and `.ispc` files are missing. This state produces [compile errors](https://forums.unrealengine.com/unreal-engine/announcements-and-releases/1695917-unreal-engine-4-24-released?p=1715142#post1715142) when you build the engine. You can fix this by running `git restore .` in the root of your `UnrealEngine` repository.
+
+### Breaking Changes:
+- Simulated player worker configurations now require a development authentication token and deployment flag instead of a login token and player identity token. See the Example Project for an example of how to set this up.
+
+### Features:
+- We now support Unreal Engine `4.24.3`. You can find the `4.24.3` version of our engine fork [here](https://github.com/improbableio/UnrealEngine/tree/4.24-SpatialOSUnrealGDK-release).
+- Added a new variable: `QueuedOutgoingRPCWaitTime`. Outgoing RPCs are now dropped in the following three scenarios: more than `QueuedOutgoingRPCWaitTime` time has passed since the RPC was sent; the worker instance is never expected to have the authority required to receive the RPC (if you're using offloading or zoning); or the Actor that the RPC is sent to is being destroyed.
+- In local deployments of the Example Project, you can now launch simulated players with one click. To launch a single simulated player client, run `LaunchSimPlayerClient.bat`. To launch ten simulated player clients, run `Launch10SimPlayerClients.bat`.
+- Added support for the UE4 Network Profiler to measure relative size of RPC and Actor replication data.
+- Added a `SpatialToggleMetricsDisplay` console command. You must enable `bEnableMetricsDisplay` in order for the metrics display to be available. You must then must call `SpatialToggleMetricsDisplay` on each client that you want the metrics display to be visible for.
+- Enabled compression in the Modular UDP networking stack.
+- Switched off default RPC-packing. You can re-enable this in `SpatialGDKSettings`.
+- When you start a local deployment, we now check to see if the required Runtime port is blocked. If it is, we display a dialog box that asks whether you want to kill the process that is blocking the port.
+- A configurable Actor component `SpatialPingComponent` is now available. This enables PlayerControllers to measure the ping time to the server-worker instances that have authority over them. You can access the latest raw ping value via `GetPing()`, or access the rolling average, which is stored in `PlayerState`.
+- You can invoke the `GenerateSchema`, `GenerateSchemaAndSnapshots`, and `CookAndGenerateSchema` commandlets with the `-AdditionalSchemaCompilerArguments="..."` command line switch to output additional compiled schema formats. If you don't provide this switch, the output contains only the schema descriptor. This switch's value should be a subset of the arguments that you can pass to the schema compiler directly (for example `--bundle_out="path/to/bundle.sb"`). You can see a full list of possible values in the [schema compiler documentation](https://docs.improbable.io/reference/14.2/shared/schema/introduction#schema-compiler-cli-reference)
+- Added the `AllowUnresolvedParameters` function flag. This flag disables warnings that occur during processing of RPCs that have unresolved parameters. To enable this flag, use Blueprints, or add a tag to the `UFUNCTION` macro.
+- There is now a warning if you launch a cloud deployment with the `manual_worker_connection_only` flag set to `true`.
+- We now support server travel for single-server game worlds. We don't support server travel for game worlds that use zoning or offloading.
+- Improved the workflow relating to schema generation issues when you launch local deployments. There is now a warning if you try to launch a local deployment after a schema error.
+- `DeploymentLauncher` can now launch a simulated player deployment independently from the target deployment.
+Usage: `DeploymentLauncher createsim `
+- We now use `HeartbeatTimeoutWithEditorSeconds` if `WITH_EDITOR` is defined. This prevents worker instances from disconnecting when you're running them from the Unreal Editor for debugging.
+- Added the `bAsyncLoadNewClassesOnEntityCheckout` setting to `SpatialGDKSettings`. This allows worker instances to load new classes asynchronously when they are receiving the initial updates about an entity. It is `false` by default.
+- Added `IndexYFromSchema` functions for the `Coordinates`, `WorkerRequirementSet`, `FRotator`, and `FVector` classes. We've remapped the `GetYFromSchema` functions for the same classes to invoke `IndexYFromSchema` internally, in line with other implementations of the pattern.
+- Clients now validate their schema files against the schema files on the server, and log a warning if the files do not match.
+- Entries in the schema database are now sorted to improve efficiency when searching for assets in the Unreal Editor. (DW-Sebastien)
+- `BatchSpatialPositionUpdates` in `SpatialGDKSettings` now defaults to false.
+- Added `bEnableNetCullDistanceInterest` (default `true`) to enable client interest to be exposed through component tagging. This functionality has closer parity to native Unreal client interest.
+- Added `bEnableNetCullDistanceFrequency` (default `false`) to enable client interest queries to use frequency. You can configure this functionality using `InterestRangeFrequencyPairs` and `FullFrequencyNetCullDistanceRatio`.
+- Introduced the feature flag `bEnableResultTypes` (default `true`). This configures interest queries to include only the set of components required for the queries to run. Depending on your game, this might save bandwidth.
+- If you set the `bEnableResultTypes` flag to `true`, this disables dynamic interest overrides.
+- Moved the development authentication settings from the Runtime Settings panel to the Editor Settings panel.
+- Added the option to use the development authentication flow with the command line.
+- Added a button to generate a development authentication token inside the Unreal Editor. To use it, navigate to **Edit** > **Project Setting** > **SpatialOS GDK for Unreal** > **Editor Settings** > **Cloud Connection**.
+- Added a new section where you can configure the launch arguments for running a client on a mobile device. To use it, navigate to **Edit** > **Project Setting** > **SpatialOS GDK for Unreal** > **Editor Settings** > **Mobile**.
+- You can now choose which Runtime version to use (in the Runtime Settings) when you launch a local or cloud deployment.
+- If you set the `--OverrideResultTypes` flag to `true`, server-worker instances no longer receive updates about server RPC components on Actors that they do not own. This should decrease bandwidth for server-worker instances in offloaded and zoned games.
+- The `InstallGDK` scripts now `git clone` the correct version of the `UnrealGDK` and `UnrealGDKExampleProject` for the `UnrealEngine` branch that you have checked out. They read `UnrealGDKVersion.txt` and `UnrealGDKExampleProjectVersion.txt` to determine what the correct branches are.
+- Removed the `bEnableServerQBI` property and the `--OverrideServerInterest` flag.
+- Added custom warning timeouts for each RPC failure condition.
+- `SpatialPingComponent` can now also report average ping measurements over a specified number of recent pings. You can use `PingMeasurementsWindowSize` to specify how many measurements you want to record, and call `GetAverageData` to get the measurement data. There is also a delegate `OnRecordPing` that is broadcast whenever a new ping measurement is recorded.
+- The Spatial Output Log window now displays deployment startup errors.
+- Added `bEnableClientQueriesOnServer` (default false) which makes the same queries on the server as it makes on clients, if the GDK for Unreal's load balancer is enabled. Enable `bEnableClientQueriesOnServer` to avoid a situation in which clients receive updates about entities that the server doesn't receive updates about (which happens if the server's interest query is configured incorrectly).
+- We now log a warning when `AddPendingRPC` fails due to `ControllerChannelNotListening`.
+- When offloading is enabled, Actors have local authority (`ROLE_Authority`) on servers for longer periods of time, to allow more native Unreal functionality to work without problems.
+- When offloading is enabled, if you try to spawn Actors on a server that will not be the Actor Group owner for them, we now log an error and delete the Actors.
+- The GDK now uses SpatialOS Runtime version 14.5.1 by default.
+- Renamed the configuration setting `bPreventAutoConnectWithLocator` to `bPreventClientCloudDeploymentAutoConnect` and moved it to `SpatialGDKSettings`. To use this feature, enable the setting in `SpatialGDKSettings`.
+- Made `USpatialMetrics::WorkerMetricsRecieved` static.
+- You can now connect to a local deployment by selecting **Connect to a local deployment** and specifying the local IP address of your computer in the Launch drop-down menu.
+- Enabled RPC ring buffers by default. We'll remove the legacy RPC mode in a future release.
+- Removed the `bPackRPCs` property and the `--OverrideRPCPacking` flag.
+- Added `OnClientOwnershipGained` and `OnClientOwnershipLost` events on Actors and Actor Components. These events trigger when an Actor is added to or removed from the ownership hierarchy of a client's PlayerController.
+
+## Bug fixes:
+- Queued RPCs no longer spam logs when an entity is deleted.
+- We now take the `OverrideSpatialNetworking` command line argument into account as early as possible (previously, `LocalDeploymentManager` queried `bSpatialNetworking` before the command line was parsed).
+- Servers now maintain interest in `AlwaysRelevant` Actors.
+- `GetActorSpatialPosition` now returns the last spectator sync location while the player is spectating.
+- The default cloud launch configuration is now empty.
+- Fixed a crash that happened when the GDK attempted to read schema from an unloaded class.
+- We now properly handle (and eventually resolve) unresolved object references in replicated arrays of structs.
+- Fixed a tombstone-related assert that could fire and bring down the Unreal Editor.
+- If an Actor that is placed in the level with `bNetLoadOnClient=false` goes out of a worker instance's view, it is now reloaded if it comes back into view.
+- Fixed a crash in `SpatialDebugger` that was caused by the dereference of an invalid weak pointer.
+- Fixed a connection error that occurred when using `spatial cloud connect external`.
+- The command line argument `receptionistHost ` no longer overrides connections to `127.0.0.1`.
+- If you connect a worker instance to a deployment using the Locator, and you initiate a `ClientTravel` using a URL that requires the Receptionist, this now works correctly.
+- You can now access the worker flags via `USpatialStatics::GetWorkerFlag` instead of `USpatialWorkerFlags::GetWorkerFlag`.
+- Fixed a crash in `SpatialDebugger` that occurs when GDK-space load balancing is disabled.
+- The schema database no longer fails to load previous saved state when working in the Unreal Editor.
+- If you attempt to launch a cloud deployment, this now runs the `spatial auth` process as required. Previously the deployment would fail.
+- Made a minor spelling fix to the connection log message.
+- The debug strings in `GlobalStateManager` now display the Actor class name in log files.
+- The server no longer crashes when received RPCs are processed recursively.
+- The GDK no longer crashes when `SoftObjectPointers` are not yet resolved, but instead serializes them as expected after they are resolved.
+- Fixed an issue that occurred when replicating a property for a class that was part of an asynchronously-loaded package, when the package had not finished loading.
+- Fixed component interest constraints that are constructed from schema.
+- The GDK now tracks properties that contain references to replicated Actors, so that it can resolve them again if the Actor that they reference moves out of and back into relevance.
+- PIE sessions no longer occasionally fail to start due to missing schema for the `SpatialDebugger` Blueprint.
+- Fixed an issue where a newly-created subobject had empty state when `RepNotify` was called for a property pointing to that subobject.
+- Fixed an issue where deleted, initially dormant startup Actors would still be present on other worker instances.
+- We now force-activate the RPC ring buffer when load balancing is enabled, to allow RPC handover when authority changes.
+- Fixed a race condition where a client that was leaving the deployment could leave its Actor behind on the server, to be cleaned up after a long timeout.
+- Fixed a crash that was caused by state in `SpatialGameInstance` persisting across a transition from one deployment to another.
+- The GDK no longer crashes when you start and stop PIE clients multiple times.
+- The GDK no longer crashes when shadow data is uninitialized when resolving unresolved objects.
+- Fixed an occasional issue when sending component RPCs on a recently-created Actor.
+
+### 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.
+
+- Enabled the SpatialOS toolbar for MacOS.
+- Added support for Android.
+- `SpatialDebugger` worker regions are now cuboids rather than planes, and can have their `WorkerRegionVerticalScale` adjusted via a setting in the `SpatialDebugger`.
+- Added an `AuthorityIntent` component, a `VirtualWorkerTranslation` component, and a partial framework. We'll use these in the future to control load balancing.
+- Load balancing strategies and locking strategies can be set per-level using `SpatialWorldSettings`.
+- Added a new Runtime Settings flag to enable the GDK for Unreal load balancer. This is a feature that is in development and not yet ready for general use. Enabling the GDK for Unreal load balancer now creates a single query per server-worker instance, depending on the defined load balancing strategy.
+- Extracted the logic responsible for taking an Actor and generating the array of SpatialOS components that represents it as an entity in SpatialOS. This logic is now in `EntityFactory`.
+- `DeploymentLauncher` can now parse a .pb.json launch configuration.
+
+### External contributors:
+@DW-Sebastien
## [`0.8.1`] - 2020-03-17
@@ -24,6 +130,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Bug fixes:
- Replicated references to newly created dynamic subobjects will now be resolved correctly.
- Fixed a bug that caused the local API service to memory leak.
+- Cloud deployment flow will now correctly report errors when a deployment fails to launch due to a missing assembly.
- Errors are now correctly reported when you try to launch a cloud deployment without an assembly.
- The Start deployment button will no longer become greyed out when a `spatial auth login` process times out.
------
@@ -45,11 +152,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 当 `spatial auth login` 进程超时,启动部署的按钮 (Start) 不再显示为灰色。
-## [`0.8.1-preview`] - 2020-03-16
-### Features:
+## [`0.8.1-preview`] - 2020-03-17
+
+### Internal:
+### Adapted from 0.6.5
- **SpatialOS GDK for Unreal** > **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.
- Deployments can now be launched in China, when the **Region where services are located** is set to `CN`.
+
+### Features:
- Updated the version of the local API service used by the UnrealGDK.
- The Spatial output log will now be open by default.
- The GDK now uses SpatialOS 14.5.0.
@@ -57,6 +168,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Bug fixes:
- Replicated references to newly created dynamic subobjects will now be resolved correctly.
- Fixed a bug that caused the local API service to memory leak.
+- Cloud deployment flow will now correctly report errors when a deployment fails to launch due to a missing assembly.
- Errors are now correctly reported when you try to launch a cloud deployment without an assembly.
- The Start deployment button will no longer become greyed out when a `spatial auth login` process times out.
@@ -76,7 +188,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
For more information, check the [Keep your GDK up to date](https://documentation.improbable.io/gdk-for-unreal/docs/keep-your-gdk-up-to-date) SpatialOS documentation.
-
### Features:
- You can now call `SpatialToggleMetricsDisplay` from the console in your Unreal clients in order to view metrics. `bEnableMetricsDisplay` must be enabled on clients where you want to use this feature.
- The modular-udp networking stack now uses compression by default.
@@ -148,8 +259,9 @@ Features listed in the internal section are not ready to use but, in the spirit
## [`0.7.1-preview`] - 2019-12-06
+
### Adapted from 0.6.3
-### Bug fixes:
+### Bug fixes:
- The C Worker SDK now communicates on port 443 instead of 444. This change is intended to protect your cloud deployments from DDoS attacks.
### Internal:
@@ -199,6 +311,10 @@ Features listed in the internal section are not ready to use but, in the spirit
- The GDK no longer generates schema for all UObject subclasses. Schema generation for Actor, ActorComponent and GameplayAbility subclasses is enabled by default, other classes can be enabled using `SpatialType` UCLASS specifier, or by checking the Spatial Type checkbox on blueprints.
- Added new experimental CookAndGenerateSchemaCommandlet that generates required schema during a regular cook.
- Added the `OverrideSpatialOffloading` command line flag. This allows you to toggle offloading at launch time.
+- The initial connection from a worker will attempt to use relevant command line arguments (receptionistHost, locatorHost) to inform the connection. If these are not provided the standard connection flow will be followed. Subsequent connections will not use command line arguments.
+- The command "Open 0.0.0.0" can be used to connect a worker using its command line arguments, simulating initial connection.
+- The command "ConnectToLocator " has been added to allow for explicit connections to deployments.
+- Add SpatialDebugger and associated content. This tool can be enabled via the SpatialToggleDebugger console command. Documentation will be added for this soon.
### Bug fixes:
- Fixed a bug where the spatial daemon started even with spatial networking disabled.
@@ -215,6 +331,13 @@ Features listed in the internal section are not ready to use but, in the spirit
- Muticast RPCs that are sent shortly after an actor is created are now correctly processed by all clients.
- When replicating an actor, the owner's Spatial position will no longer be used if it isn't replicated.
- Fixed a crash upon checking out an actor with a deleted static subobject.
+- Fixed an issue where launching a cloud deployment with an invalid assembly name or deployment name wouldn't show a helpful error message.
+
+## [`0.6.5`] - 2020-01-13
+### Internal:
+Features listed in the internal section are not ready to use but, in the spirit of open development, we detail every change we make to the GDK.
+- **SpatialOS GDK for Unreal** > **Editor Settings** > **Region Settings** has been moved to **SpatialOS GDK for Unreal** > **Runtime Settings** > **Region Settings**.
+- Local deployments can now be launched in China, when the **Region where services are located** is set to `CN`.
## [`0.6.4`] - 2019-12-13
### Bug fixes:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index cd283bb0b7..0cc1fad061 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -13,4 +13,4 @@ We welcome any and all
## Coding standards
-See the [GDK for Unreal C++ coding standards guide](./SpatialGDK/Documentation/contributions/unreal-gdk-coding-standards.md).
\ No newline at end of file
+See the [GDK for Unreal C++ coding standards guide](./SpatialGDK/Documentation/contributions/unreal-gdk-coding-standards.md).
diff --git a/RequireSetup b/RequireSetup
index 6886684295..77d0f13ca4 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.
-40
+53
diff --git a/Setup.bat b/Setup.bat
index 09793ebe8b..35a9fc4200 100644
--- a/Setup.bat
+++ b/Setup.bat
@@ -1,6 +1,8 @@
@echo off
-setlocal
+if not defined NO_SET_LOCAL (
+ setlocal
+)
pushd "%~dp0"
@@ -58,8 +60,10 @@ call :MarkStartOfBlock "Setup variables"
set SCHEMA_STD_COPY_DIR=%~dp0..\..\..\spatial\build\dependencies\schema\standard_library
set SPATIAL_DIR=%~dp0..\..\..\spatial
set DOMAIN_ENVIRONMENT_VAR=
+ set DOWNLOAD_MOBILE=
for %%A in (%*) do (
if "%%A"=="--china" set DOMAIN_ENVIRONMENT_VAR=--environment cn-production
+ if "%%A"=="--mobile" set DOWNLOAD_MOBILE=True
)
call :MarkEndOfBlock "Setup variables"
@@ -81,6 +85,7 @@ call :MarkStartOfBlock "Create folders"
md "%CORE_SDK_DIR%\tools" >nul 2>nul
md "%CORE_SDK_DIR%\worker_sdk" >nul 2>nul
md "%BINARIES_DIR%" >nul 2>nul
+ md "%BINARIES_DIR%\Android" >nul 2>nul
md "%BINARIES_DIR%\Programs" >nul 2>nul
if exist "%SPATIAL_DIR%" (
@@ -96,7 +101,12 @@ call :MarkStartOfBlock "Retrieve dependencies"
spatial package retrieve worker_sdk c-dynamic-x86-vc141_md-win32 %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86-vc141_md-win32.zip"
spatial package retrieve worker_sdk c-dynamic-x86_64-vc141_md-win32 %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-vc141_md-win32.zip"
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 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"
@@ -107,9 +117,15 @@ call :MarkStartOfBlock "Unpack dependencies"
"Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-vc141_md-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Win64\" -Force; "^
"Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-gcc510-linux.zip\" -DestinationPath \"%BINARIES_DIR%\Linux\" -Force; "^
"Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\csharp.zip\" -DestinationPath \"%BINARIES_DIR%\Programs\worker_sdk\csharp\" -Force; "^
- "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%\tools\schema_compiler-x86_64-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Programs\" -Force; "^
"Expand-Archive -Path \"%CORE_SDK_DIR%\schema\standard_library.zip\" -DestinationPath \"%BINARIES_DIR%\Programs\schema\" -Force;"
+
+ if defined DOWNLOAD_MOBILE (
+ 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; "^
+ )
xcopy /s /i /q "%BINARIES_DIR%\Headers\include" "%WORKER_SDK_DIR%"
call :MarkEndOfBlock "Unpack dependencies"
diff --git a/Setup.sh b/Setup.sh
index ffcaa50ec4..633b3a427e 100755
--- a/Setup.sh
+++ b/Setup.sh
@@ -3,7 +3,7 @@
set -e -u -o pipefail
[[ -n "${DEBUG:-}" ]] && set -x
-if [ "$(uname -s)" != "Darwin" ]; then
+if [[ "$(uname -s)" != "Darwin" ]]; then
echo "This script should only be used on OS X. If you are using Windows, please run Setup.bat."
exit 1
fi
@@ -11,6 +11,7 @@ fi
pushd "$(dirname "$0")"
PINNED_CORE_SDK_VERSION=$(cat ./SpatialGDK/Extras/core-sdk.version)
+PINNED_SPOT_VERSION=$(cat ./SpatialGDK/Extras/spot.version)
BUILD_DIR="$(pwd)/SpatialGDK/Build"
CORE_SDK_DIR="${BUILD_DIR}/core_sdk"
WORKER_SDK_DIR="$(pwd)/SpatialGDK/Source/SpatialGDK/Public/WorkerSDK"
@@ -18,19 +19,28 @@ BINARIES_DIR="$(pwd)/SpatialGDK/Binaries/ThirdParty/Improbable"
SCHEMA_COPY_DIR="$(pwd)/../../../spatial/schema/unreal/gdk"
SCHEMA_STD_COPY_DIR="$(pwd)/../../../spatial/build/dependencies/schema/standard_library"
SPATIAL_DIR="$(pwd)/../../../spatial"
-if [[ "${1:-}" == "--china" ]]; then
- DOMAIN_ENVIRONMENT_VAR="--environment cn-production"
-fi
+DOWNLOAD_MOBILE=
+
+while test $# -gt 0
+do
+ case "$1" in
+ --china) DOMAIN_ENVIRONMENT_VAR="--environment cn-production"
+ ;;
+ --mobile) DOWNLOAD_MOBILE=true
+ ;;
+ esac
+ shift
+done
echo "Setup the git hooks"
-if [ -e .git/hooks ]; then
+if [[ -e .git/hooks ]]; then
# Remove the old post-checkout hook.
- if [ -e .git/hooks/post-checkout ]; then
+ if [[ -e .git/hooks/post-checkout ]]; then
rm -f .git/hooks/post-checkout
fi
# Remove the old post-merge hook.
- if [ -e .git/hooks/post-merge ]; then
+ if [[ -e .git/hooks/post-merge ]]; then
rm -f .git/hooks/post-merge
fi
@@ -43,7 +53,7 @@ rm -rf "${CORE_SDK_DIR}"
rm -rf "${WORKER_SDK_DIR}"
rm -rf "${BINARIES_DIR}"
-if [ -d "${SPATIAL_DIR}" ]; then
+if [[ -d "${SPATIAL_DIR}" ]]; then
rm -rf "${SCHEMA_STD_COPY_DIR}"
rm -rf "${SCHEMA_COPY_DIR}"
fi
@@ -53,38 +63,55 @@ mkdir -p "${WORKER_SDK_DIR}"
mkdir -p "${CORE_SDK_DIR}"/schema
mkdir -p "${CORE_SDK_DIR}"/tools
mkdir -p "${CORE_SDK_DIR}"/worker_sdk
+mkdir -p "${BINARIES_DIR}"/Android
mkdir -p "${BINARIES_DIR}"/Programs/worker_sdk
-if [ -d "${SPATIAL_DIR}" ]; then
+if [[ -d "${SPATIAL_DIR}" ]]; then
mkdir -p "${SCHEMA_STD_COPY_DIR}"
mkdir -p "${SCHEMA_COPY_DIR}"
fi
echo "Retrieve dependencies"
-spatial package retrieve tools schema_compiler-x86_64-macos "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/tools/schema_compiler-x86_64-macos.zip
-spatial package retrieve schema standard_library "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/schema/standard_library.zip
-spatial package retrieve worker_sdk c_headers "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c_headers.zip
-spatial package retrieve worker_sdk c-dynamic-x86_64-clang-macos "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang-macos.zip
-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 csharp "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/csharp.zip
-spatial package retrieve spot spot-macos "${PINNED_SPOT_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${BINARIES_DIR}"/Programs/spot
+spatial package retrieve tools schema_compiler-x86_64-macos "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/tools/schema_compiler-x86_64-macos.zip
+spatial package retrieve schema standard_library "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/schema/standard_library.zip
+spatial package retrieve worker_sdk c_headers "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c_headers.zip
+spatial package retrieve worker_sdk c-dynamic-x86_64-clang-macos "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang-macos.zip
+
+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
+fi
+
+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-macos "${PINNED_SPOT_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${BINARIES_DIR}"/Programs/spot
chmod +x "${BINARIES_DIR}"/Programs/spot
echo "Unpack dependencies"
-unzip -oq "${CORE_SDK_DIR}"/tools/schema_compiler-x86_64-macos.zip -d "${BINARIES_DIR}"/Programs/
-unzip -oq "${CORE_SDK_DIR}"/schema/standard_library.zip -d "${BINARIES_DIR}"/Programs/schema/
-unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c_headers.zip -d "${BINARIES_DIR}"/Headers/
-unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang-macos.zip -d "${BINARIES_DIR}"/Mac/
-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/csharp.zip -d "${BINARIES_DIR}"/Programs/worker_sdk/csharp/
+unzip -oq "${CORE_SDK_DIR}"/tools/schema_compiler-x86_64-macos.zip -d "${BINARIES_DIR}"/Programs/
+unzip -oq "${CORE_SDK_DIR}"/schema/standard_library.zip -d "${BINARIES_DIR}"/Programs/schema/
+unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c_headers.zip -d "${BINARIES_DIR}"/Headers/
+unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang-macos.zip -d "${BINARIES_DIR}"/Mac/
+
+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/
+fi
+
+unzip -oq "${CORE_SDK_DIR}"/worker_sdk/csharp.zip -d "${BINARIES_DIR}"/Programs/worker_sdk/csharp/
cp -R "${BINARIES_DIR}"/Headers/include/ "${WORKER_SDK_DIR}"
-if [ -d "${SPATIAL_DIR}" ]; then
+if [[ -d "${SPATIAL_DIR}" ]]; then
echo "Copying standard library schemas to ${SCHEMA_STD_COPY_DIR}"
- cp -R "${BINARIES_DIR}/Programs/schema" "${SCHEMA_STD_COPY_DIR}"
+ cp -R "${BINARIES_DIR}/Programs/schema/." "${SCHEMA_STD_COPY_DIR}"
echo "Copying schemas to ${SCHEMA_COPY_DIR}"
- cp -R SpatialGDK/Extras/schema "${SCHEMA_COPY_DIR}"
+ cp -R SpatialGDK/Extras/schema/. "${SCHEMA_COPY_DIR}"
fi
popd
diff --git a/SetupIncTraceLibs.bat b/SetupIncTraceLibs.bat
new file mode 100644
index 0000000000..b13e731b88
--- /dev/null
+++ b/SetupIncTraceLibs.bat
@@ -0,0 +1,43 @@
+rem **** Warning - Experimental functionality ****
+rem We do not support this functionality currently: Do not use it unless you are Improbable staff.
+rem ****
+
+@echo off
+
+set NO_PAUSE=1
+set NO_SET_LOCAL=1
+
+pushd "%~dp0"
+
+call Setup.bat %*
+
+call :MarkStartOfBlock "%~0"
+
+call :MarkStartOfBlock "Create folders"
+ md "%CORE_SDK_DIR%\trace_lib" >nul 2>nul
+call :MarkEndOfBlock "Create folders"
+
+call :MarkStartOfBlock "Retrieve dependencies"
+ spatial package retrieve internal trace-dynamic-x86_64-vc140_md-win32 14.3.0-b2647-85717ee-WORKER-SNAPSHOT "%CORE_SDK_DIR%\trace_lib\trace-win32.zip"
+ 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"
+
+call :MarkStartOfBlock "Unpack dependencies"
+ powershell -Command "Expand-Archive -Path \"%CORE_SDK_DIR%\trace_lib\trace-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Win64\" -Force;"^
+ "Expand-Archive -Path \"%CORE_SDK_DIR%\trace_lib\trace-linux.zip\" -DestinationPath \"%BINARIES_DIR%\Linux\" -Force;"
+ xcopy /s /i /q "%BINARIES_DIR%\Win64\improbable" "%WORKER_SDK_DIR%\improbable"
+call :MarkEndOfBlock "Unpack dependencies"
+
+call :MarkEndOfBlock "%~0"
+
+popd
+
+exit /b %ERRORLEVEL%
+
+:MarkStartOfBlock
+echo Starting: %~1
+exit /b 0
+
+:MarkEndOfBlock
+echo Finished: %~1
+exit /b 0
diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/LinuxScripts.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/LinuxScripts.cs
index d6380a38bf..f5c6f7a12d 100644
--- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/LinuxScripts.cs
+++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/LinuxScripts.cs
@@ -16,7 +16,7 @@ shift 2
# 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.
mkdir -p /improbable/logs/UnrealWorker/
-useradd $NEW_USER -m -d /improbable/logs/UnrealWorker/Logs 2>/dev/null
+useradd $NEW_USER -m -d /improbable/logs/UnrealWorker 2>/dev/null
chown -R $NEW_USER:$NEW_USER $(pwd) 2>/dev/null
chmod -R o+rw /improbable/logs 2>/dev/null
@@ -55,6 +55,13 @@ shift 1
public const string SimulatedPlayerCoordinatorShellScript =
@"#!/bin/sh
+
+# Some clients are quite large so in order to avoid running out of disk space on the node we attempt to delete the zip
+WORKER_ZIP_DIR=""/tmp/runner_source/""
+if [ -d ""$WORKER_ZIP_DIR"" ]; then
+ rm -rf ""$WORKER_ZIP_DIR""
+fi
+
sleep 5
chmod +x WorkerCoordinator.exe
diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs
index fcf5cb7b1b..d1fb157a51 100644
--- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs
+++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs
@@ -109,14 +109,15 @@ private static PlatformRefreshTokenCredential GetPlatformRefreshTokenCredential(
private static int CreateDeployment(string[] args)
{
- bool launchSimPlayerDeployment = args.Length == 11;
+ bool launchSimPlayerDeployment = args.Length == 12;
var projectName = args[1];
var assemblyName = args[2];
- var mainDeploymentName = args[3];
- var mainDeploymentJsonPath = args[4];
- var mainDeploymentSnapshotPath = args[5];
- var mainDeploymentRegion = args[6];
+ var runtimeVersion = args[3];
+ var mainDeploymentName = args[4];
+ var mainDeploymentJsonPath = args[5];
+ var mainDeploymentSnapshotPath = args[6];
+ var mainDeploymentRegion = args[7];
var simDeploymentName = string.Empty;
var simDeploymentJson = string.Empty;
@@ -125,11 +126,11 @@ private static int CreateDeployment(string[] args)
if (launchSimPlayerDeployment)
{
- simDeploymentName = args[7];
- simDeploymentJson = args[8];
- simDeploymentRegion = args[9];
+ simDeploymentName = args[8];
+ simDeploymentJson = args[9];
+ simDeploymentRegion = args[10];
- if (!Int32.TryParse(args[10], out simNumPlayers))
+ if (!Int32.TryParse(args[11], out simNumPlayers))
{
Console.WriteLine("Cannot parse the number of simulated players to connect.");
return 1;
@@ -145,7 +146,7 @@ private static int CreateDeployment(string[] args)
StopDeploymentByName(deploymentServiceClient, projectName, mainDeploymentName);
}
- var createMainDeploymentOp = CreateMainDeploymentAsync(deploymentServiceClient, launchSimPlayerDeployment, projectName, assemblyName, mainDeploymentName, mainDeploymentJsonPath, mainDeploymentSnapshotPath, mainDeploymentRegion);
+ var createMainDeploymentOp = CreateMainDeploymentAsync(deploymentServiceClient, launchSimPlayerDeployment, projectName, assemblyName, runtimeVersion, mainDeploymentName, mainDeploymentJsonPath, mainDeploymentSnapshotPath, mainDeploymentRegion);
if (!launchSimPlayerDeployment)
{
@@ -167,7 +168,7 @@ private static int CreateDeployment(string[] args)
StopDeploymentByName(deploymentServiceClient, projectName, simDeploymentName);
}
- var createSimDeploymentOp = CreateSimPlayerDeploymentAsync(deploymentServiceClient, projectName, assemblyName, mainDeploymentName, simDeploymentName, simDeploymentJson, simDeploymentRegion, simNumPlayers);
+ 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...");
@@ -223,6 +224,79 @@ private static int CreateDeployment(string[] args)
return 0;
}
+ private static int CreateSimDeployments(string[] args)
+ {
+ var projectName = args[1];
+ var assemblyName = args[2];
+ var runtimeVersion = args[3];
+ var targetDeploymentName = args[4];
+ var simDeploymentName = args[5];
+ var simDeploymentJson = args[6];
+ var simDeploymentRegion = args[7];
+
+ var simNumPlayers = 0;
+ if (!Int32.TryParse(args[8], 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));
+
+ if (DeploymentExists(deploymentServiceClient, projectName, simDeploymentName))
+ {
+ StopDeploymentByName(deploymentServiceClient, projectName, simDeploymentName);
+ }
+
+ var createSimDeploymentOp = CreateSimPlayerDeploymentAsync(deploymentServiceClient, projectName, assemblyName, runtimeVersion, targetDeploymentName, simDeploymentName, simDeploymentJson, simDeploymentRegion, simNumPlayers);
+
+ // Wait for both deployments to be created.
+ Console.WriteLine("Waiting for the simulated player deployment to be ready...");
+ var simPlayerDeployment = createSimDeploymentOp.PollUntilCompleted().GetResultOrNull();
+ if (simPlayerDeployment == null)
+ {
+ Console.WriteLine("Failed to create the simulated player deployment");
+ 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)
+ {
+ 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.");
+ }
+ else
+ {
+ throw;
+ }
+ }
+
+ return 0;
+ }
+
private static bool DeploymentExists(DeploymentServiceClient deploymentServiceClient, string projectName,
string deploymentName)
{
@@ -255,7 +329,7 @@ private static void StopDeploymentByName(DeploymentServiceClient deploymentServi
}
private static Operation CreateMainDeploymentAsync(DeploymentServiceClient deploymentServiceClient,
- bool launchSimPlayerDeployment, string projectName, string assemblyName, string mainDeploymentName, string mainDeploymentJsonPath, string mainDeploymentSnapshotPath, string regionCode)
+ bool launchSimPlayerDeployment, string projectName, string assemblyName, string runtimeVersion, string mainDeploymentName, string mainDeploymentJsonPath, string mainDeploymentSnapshotPath, string regionCode)
{
var snapshotServiceClient = SnapshotServiceClient.Create(GetApiEndpoint(regionCode), GetPlatformRefreshTokenCredential(regionCode));
@@ -279,7 +353,8 @@ private static Operation CreateMainDeploym
Name = mainDeploymentName,
ProjectName = projectName,
StartingSnapshotId = mainSnapshotId,
- RegionCode = regionCode
+ RegionCode = regionCode,
+ RuntimeVersion = runtimeVersion
};
mainDeploymentConfig.Tag.Add(DEPLOYMENT_LAUNCHED_BY_LAUNCHER_TAG);
@@ -303,7 +378,7 @@ private static Operation CreateMainDeploym
}
private static Operation CreateSimPlayerDeploymentAsync(DeploymentServiceClient deploymentServiceClient,
- string projectName, string assemblyName, string mainDeploymentName, string simDeploymentName, string simDeploymentJsonPath, string regionCode, int simNumPlayers)
+ string projectName, string assemblyName, string runtimeVersion, string mainDeploymentName, string simDeploymentName, string simDeploymentJsonPath, string regionCode, int simNumPlayers)
{
var playerAuthServiceClient = PlayerAuthServiceClient.Create(GetApiEndpoint(regionCode), GetPlatformRefreshTokenCredential(regionCode));
@@ -336,31 +411,68 @@ private static Operation CreateSimPlayerDe
var simWorkerConfigJson = File.ReadAllText(simDeploymentJsonPath);
dynamic simWorkerConfig = JObject.Parse(simWorkerConfigJson);
- for (var i = 0; i < simWorkerConfig.workers.Count; ++i)
+ if (simDeploymentJsonPath.EndsWith(".pb.json"))
{
- if (simWorkerConfig.workers[i].worker_type == CoordinatorWorkerName)
+ for (var i = 0; i < simWorkerConfig.worker_flagz.Count; ++i)
{
- simWorkerConfig.workers[i].flags.Add(regionFlag);
- simWorkerConfig.workers[i].flags.Add(devAuthTokenFlag);
- simWorkerConfig.workers[i].flags.Add(targetDeploymentFlag);
- simWorkerConfig.workers[i].flags.Add(numSimulatedPlayersFlag);
+ if (simWorkerConfig.worker_flagz[i].worker_type == CoordinatorWorkerName)
+ {
+ simWorkerConfig.worker_flagz[i].flagz.Add(devAuthTokenFlag);
+ simWorkerConfig.worker_flagz[i].flagz.Add(targetDeploymentFlag);
+ simWorkerConfig.worker_flagz[i].flagz.Add(numSimulatedPlayersFlag);
+ break;
+ }
}
- }
- // Specify the number of managed coordinator workers to start by editing
- // the load balancing options in the launch config. It creates a rectangular
- // launch config of N cols X 1 row, N being the number of coordinators
- // to create.
- // This assumes the launch config contains a rectangular load balancing
- // layer configuration already for the coordinator worker.
- var lbLayerConfigurations = simWorkerConfig.load_balancing.layer_configurations;
- for (var i = 0; i < lbLayerConfigurations.Count; ++i)
+ for (var i = 0; i < simWorkerConfig.flagz.Count; ++i)
+ {
+ if (simWorkerConfig.flagz[i].name == "loadbalancer_v2_config_json")
+ {
+ string layerConfigJson = simWorkerConfig.flagz[i].value;
+ dynamic loadBalanceConfig = JObject.Parse(layerConfigJson);
+ var lbLayerConfigurations = loadBalanceConfig.layerConfigurations;
+ for (var j = 0; j < lbLayerConfigurations.Count; ++j)
+ {
+ if (lbLayerConfigurations[j].layer == CoordinatorWorkerName)
+ {
+ var rectangleGrid = lbLayerConfigurations[j].rectangleGrid;
+ rectangleGrid.cols = simNumPlayers;
+ rectangleGrid.rows = 1;
+ break;
+ }
+ }
+ simWorkerConfig.flagz[i].value = Newtonsoft.Json.JsonConvert.SerializeObject(loadBalanceConfig);
+ break;
+ }
+ }
+ }
+ else // regular non pb.json
{
- if (lbLayerConfigurations[i].layer == CoordinatorWorkerName)
+ for (var i = 0; i < simWorkerConfig.workers.Count; ++i)
{
- var rectangleGrid = lbLayerConfigurations[i].rectangle_grid;
- rectangleGrid.cols = simNumPlayers;
- rectangleGrid.rows = 1;
+ if (simWorkerConfig.workers[i].worker_type == CoordinatorWorkerName)
+ {
+ simWorkerConfig.workers[i].flags.Add(devAuthTokenFlag);
+ simWorkerConfig.workers[i].flags.Add(targetDeploymentFlag);
+ simWorkerConfig.workers[i].flags.Add(numSimulatedPlayersFlag);
+ }
+ }
+
+ // Specify the number of managed coordinator workers to start by editing
+ // the load balancing options in the launch config. It creates a rectangular
+ // launch config of N cols X 1 row, N being the number of coordinators
+ // to create.
+ // This assumes the launch config contains a rectangular load balancing
+ // layer configuration already for the coordinator worker.
+ var lbLayerConfigurations = simWorkerConfig.load_balancing.layer_configurations;
+ for (var i = 0; i < lbLayerConfigurations.Count; ++i)
+ {
+ if (lbLayerConfigurations[i].layer == CoordinatorWorkerName)
+ {
+ var rectangleGrid = lbLayerConfigurations[i].rectangle_grid;
+ rectangleGrid.cols = simNumPlayers;
+ rectangleGrid.rows = 1;
+ }
}
}
@@ -376,7 +488,8 @@ private static Operation CreateSimPlayerDe
},
Name = simDeploymentName,
ProjectName = projectName,
- RegionCode = regionCode
+ RegionCode = regionCode,
+ RuntimeVersion = runtimeVersion
// No snapshot included for the simulated player deployment
};
@@ -505,8 +618,10 @@ 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($" 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(" 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.");
@@ -517,9 +632,10 @@ private static void ShowUsage()
private static int Main(string[] args)
{
if (args.Length == 0 ||
- args[0] == "create" && (args.Length != 11 && args.Length != 7) ||
- args[0] == "stop" && (args.Length != 3 && args.Length != 4) ||
- args[0] == "list" && args.Length != 3)
+ (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))
{
ShowUsage();
return 1;
@@ -532,6 +648,11 @@ private static int Main(string[] args)
return CreateDeployment(args);
}
+ if (args[0] == "createsim")
+ {
+ return CreateSimDeployments(args);
+ }
+
if (args[0] == "stop")
{
return StopDeployments(args);
diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Improbable.Unreal.Scripts.sln b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Improbable.Unreal.Scripts.sln
index 8ba0745a6f..1c76529ea4 100644
--- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Improbable.Unreal.Scripts.sln
+++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Improbable.Unreal.Scripts.sln
@@ -45,18 +45,26 @@ Global
{D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Debug|x86.ActiveCfg = Debug|Any CPU
{D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Debug|x86.Build.0 = Debug|Any CPU
+ {D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Release|x86.ActiveCfg = Release|Any CPU
+ {D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Release|x86.Build.0 = Release|Any CPU
{B879D33B-AA4B-4A13-BCD4-957178C060E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B879D33B-AA4B-4A13-BCD4-957178C060E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B879D33B-AA4B-4A13-BCD4-957178C060E7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B879D33B-AA4B-4A13-BCD4-957178C060E7}.Debug|x86.Build.0 = Debug|Any CPU
{B879D33B-AA4B-4A13-BCD4-957178C060E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B879D33B-AA4B-4A13-BCD4-957178C060E7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B879D33B-AA4B-4A13-BCD4-957178C060E7}.Release|x86.ActiveCfg = Release|Any CPU
+ {B879D33B-AA4B-4A13-BCD4-957178C060E7}.Release|x86.Build.0 = Release|Any CPU
{C41625B0-CDB7-4480-B2E4-AEB27AF3B198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C41625B0-CDB7-4480-B2E4-AEB27AF3B198}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C41625B0-CDB7-4480-B2E4-AEB27AF3B198}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C41625B0-CDB7-4480-B2E4-AEB27AF3B198}.Debug|x86.Build.0 = Debug|Any CPU
{C41625B0-CDB7-4480-B2E4-AEB27AF3B198}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C41625B0-CDB7-4480-B2E4-AEB27AF3B198}.Release|Any CPU.Build.0 = Release|Any CPU
- {D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Release|Any CPU.Build.0 = Release|Any CPU
- {D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Release|x86.ActiveCfg = Release|Any CPU
- {D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Release|x86.Build.0 = Release|Any CPU
+ {C41625B0-CDB7-4480-B2E4-AEB27AF3B198}.Release|x86.ActiveCfg = Release|Any CPU
+ {C41625B0-CDB7-4480-B2E4-AEB27AF3B198}.Release|x86.Build.0 = Release|Any CPU
{B0165BED-C4AF-406C-A652-3DBB3D2E0C52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B0165BED-C4AF-406C-A652-3DBB3D2E0C52}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B0165BED-C4AF-406C-A652-3DBB3D2E0C52}.Debug|x86.ActiveCfg = Debug|Any CPU
diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Authentication.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Authentication.cs
deleted file mode 100644
index 2d2cdf7d42..0000000000
--- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Authentication.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright (c) Improbable Worlds Ltd, All Rights Reserved
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Improbable.Worker;
-using Improbable.Worker.Alpha;
-
-namespace Improbable.WorkerCoordinator
-{
- class Authentication
- {
- private const string LOCATOR_HOST_NAME = "locator.improbable.io";
- private const string LOCATOR_HOST_NAME_CN = "locator.spatialoschina.com";
- private const int LOCATOR_PORT = 444;
-
- public static string GetLocatorHost(string region)
- {
- return region == "CN" ? LOCATOR_HOST_NAME_CN : LOCATOR_HOST_NAME;
- }
-
- public static string GetDevelopmentPlayerIdentityToken(string devAuthToken, string clientName, string region)
- {
- var pitResponse = DevelopmentAuthentication.CreateDevelopmentPlayerIdentityTokenAsync(GetLocatorHost(region), LOCATOR_PORT,
- new PlayerIdentityTokenRequest
- {
- DevelopmentAuthenticationToken = devAuthToken,
- PlayerId = clientName,
- DisplayName = clientName
- }).Get();
-
- if (pitResponse.Status.Code != ConnectionStatusCode.Success)
- {
- throw new Exception($"Failed to retrieve player identity token.\n" +
- $"error code: {pitResponse.Status.Code}\n" +
- $"error message: {pitResponse.Status.Detail}");
- }
-
- return pitResponse.PlayerIdentityToken;
- }
-
- public static List GetDevelopmentLoginTokens(string workerType, string pit, string region)
- {
- var loginTokensResponse = DevelopmentAuthentication.CreateDevelopmentLoginTokensAsync(GetLocatorHost(region), LOCATOR_PORT,
- new LoginTokensRequest
- {
- PlayerIdentityToken = pit,
- WorkerType = workerType,
- UseInsecureConnection = false,
- DurationSeconds = 300
- }).Get();
-
- if (loginTokensResponse.Status.Code != ConnectionStatusCode.Success)
- {
- throw new Exception($"Failed to retrieve any login tokens.\n" +
- $"error code: {loginTokensResponse.Status.Code}\n" +
- $"error message: {loginTokensResponse.Status.Detail}");
- }
-
- return loginTokensResponse.LoginTokens;
- }
-
- public static string SelectLoginToken(List loginTokens, string targetDeployment)
- {
- var selectedLoginToken = loginTokens.FirstOrDefault(token => token.DeploymentName == targetDeployment).LoginToken;
-
- if (selectedLoginToken == null)
- {
- throw new Exception("Failed to launch simulated player. Login token for target deployment was not found in response. Does that deployment have the `dev_auth` tag?");
- }
-
- return selectedLoginToken;
- }
- }
-}
diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/CoordinatorConnection.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/CoordinatorConnection.cs
index 7c70bdea5a..e930aeff86 100644
--- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/CoordinatorConnection.cs
+++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/CoordinatorConnection.cs
@@ -18,7 +18,7 @@ public static Connection ConnectAndKeepAlive(Logger logger, string receptionistH
WorkerType = coordinatorWorkerType,
Network =
{
- ConnectionType = NetworkConnectionType.Tcp,
+ ConnectionType = NetworkConnectionType.ModularTcp,
UseExternalIp = false
}
};
diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs
index 0c201986e3..3e69e42ac9 100644
--- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs
+++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs
@@ -20,7 +20,6 @@ internal class ManagedWorkerCoordinator : AbstractWorkerCoordinator
private const string InitialStartDelayArg = "coordinator_start_delay_millis";
// Worker flags.
- private const string RegionFlag = "simulated_players_region";
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";
@@ -31,11 +30,10 @@ internal class ManagedWorkerCoordinator : AbstractWorkerCoordinator
// Argument placeholders for simulated players - these will be replaced by the coordinator by their actual values.
private const string SimulatedPlayerWorkerNamePlaceholderArg = "";
- private const string PlayerIdentityTokenPlaceholderArg = "";
- private const string LoginTokenPlaceholderArg = "";
+ private const string DevAuthTokenPlaceholderArg = "";
+ private const string TargetDeploymentPlaceholderArg = "";
private const string CoordinatorWorkerType = "SimulatedPlayerCoordinator";
- private const string SimulatedPlayerWorkerType = "UnrealClient";
private const string SimulatedPlayerFilename = "StartSimulatedClient.sh";
private static Random Random;
@@ -120,7 +118,6 @@ public override void Run()
var connection = CoordinatorConnection.ConnectAndKeepAlive(Logger, ReceptionistHost, ReceptionistPort, CoordinatorWorkerId, CoordinatorWorkerType);
// Read worker flags.
- Option region = connection.GetWorkerFlag(RegionFlag);
Option devAuthTokenOpt = connection.GetWorkerFlag(DevAuthTokenWorkerFlag);
Option targetDeploymentOpt = connection.GetWorkerFlag(TargetDeploymentWorkerFlag);
int deploymentTotalNumSimulatedPlayers = int.Parse(GetWorkerFlagOrDefault(connection, DeploymentTotalNumSimulatedPlayersWorkerFlag, "100"));
@@ -154,53 +151,39 @@ public override void Run()
}
Thread.Sleep(timeToSleep);
- StartSimulatedPlayer(clientName, region, devAuthTokenOpt, targetDeploymentOpt);
+ StartSimulatedPlayer(clientName, devAuthTokenOpt, targetDeploymentOpt);
}
// Wait for all clients to exit.
WaitForPlayersToExit();
}
- private void StartSimulatedPlayer(string simulatedPlayerName, Option region, Option devAuthTokenOpt, Option targetDeploymentOpt)
+ private void StartSimulatedPlayer(string simulatedPlayerName, Option devAuthTokenOpt, Option targetDeploymentOpt)
{
try
{
- // Create player identity token and login token
- string pit = "";
- string loginToken = "";
- if (devAuthTokenOpt.HasValue)
+ // Pass in the dev auth token and the target deployment
+ if (devAuthTokenOpt.HasValue && targetDeploymentOpt.HasValue)
{
- pit = Authentication.GetDevelopmentPlayerIdentityToken(devAuthTokenOpt.Value, simulatedPlayerName, region.Value);
-
- if (targetDeploymentOpt.HasValue)
- {
- var loginTokens = Authentication.GetDevelopmentLoginTokens(SimulatedPlayerWorkerType, pit, region.Value);
- loginToken = Authentication.SelectLoginToken(loginTokens, targetDeploymentOpt.Value);
- }
- else
- {
- Logger.WriteLog($"Not generating a login token for player {simulatedPlayerName}, no target deployment provided through worker flag \"{TargetDeploymentWorkerFlag}\".");
- }
+ string[] simulatedPlayerArgs = Util.ReplacePlaceholderArgs(SimulatedPlayerArgs, new Dictionary() {
+ { SimulatedPlayerWorkerNamePlaceholderArg, simulatedPlayerName },
+ { DevAuthTokenPlaceholderArg, devAuthTokenOpt.Value },
+ { TargetDeploymentPlaceholderArg, targetDeploymentOpt.Value }
+ });
+
+ // Prepend the simulated player id as an argument to the start client script.
+ // This argument is consumed by the start client script and will not be passed to the client worker.
+ simulatedPlayerArgs = new string[] { simulatedPlayerName }.Concat(simulatedPlayerArgs).ToArray();
+
+ // Start the client
+ string flattenedArgs = string.Join(" ", simulatedPlayerArgs);
+ Logger.WriteLog($"Starting simulated player {simulatedPlayerName} with args: {flattenedArgs}");
+ CreateSimulatedPlayerProcess(SimulatedPlayerFilename, flattenedArgs); ;
}
else
{
- Logger.WriteLog($"Not generating a player identity token and login token for player {simulatedPlayerName}, no development auth token provided through worker flag \"{DevAuthTokenWorkerFlag}\".");
+ Logger.WriteLog($"No development auth token or target deployment provided through worker flags \"{DevAuthTokenWorkerFlag}\" and \"{TargetDeploymentWorkerFlag}\".");
}
-
- string[] simulatedPlayerArgs = Util.ReplacePlaceholderArgs(SimulatedPlayerArgs, new Dictionary() {
- { SimulatedPlayerWorkerNamePlaceholderArg, simulatedPlayerName },
- { PlayerIdentityTokenPlaceholderArg, pit },
- { LoginTokenPlaceholderArg, loginToken }
- });
-
- // Prepend the simulated player id as an argument to the start client script.
- // This argument is consumed by the start client script and will not be passed to the client worker.
- simulatedPlayerArgs = new string[] { simulatedPlayerName }.Concat(simulatedPlayerArgs).ToArray();
-
- // Start the client
- string flattenedArgs = string.Join(" ", simulatedPlayerArgs);
- Logger.WriteLog($"Starting simulated player {simulatedPlayerName} with args: {flattenedArgs}");
- CreateSimulatedPlayerProcess(SimulatedPlayerFilename, flattenedArgs);;
}
catch (Exception e)
{
diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/WorkerCoordinator.csproj b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/WorkerCoordinator.csproj
index 494f80576c..c85d28a535 100644
--- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/WorkerCoordinator.csproj
+++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/WorkerCoordinator.csproj
@@ -54,7 +54,6 @@
-
diff --git a/SpatialGDK/Build/Scripts/FindMSBuild.bat b/SpatialGDK/Build/Scripts/FindMSBuild.bat
index b5ecf2e1f6..7f4b1fae01 100644
--- a/SpatialGDK/Build/Scripts/FindMSBuild.bat
+++ b/SpatialGDK/Build/Scripts/FindMSBuild.bat
@@ -3,21 +3,28 @@ rem Convenient code to find MSBuild.exe from https://github.com/microsoft/vswher
@echo off
if not defined MSBUILD_EXE (
- set MSBUILD_EXE=
+ set MSBUILD_EXE=
- rem VS 2017 and above always include vswhere
+if exist "%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" (
+ for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe`) do (
+ if exist %%i (
+ set MSBUILD_EXE="%%i"
+ exit /b 0
+ )
+ )
+)
- for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere" -prerelease -latest -requires Microsoft.Component.MSBuild -property installationPath`) do (
- if exist "%%i\MSBuild\Current\Bin\MSBuild.exe" (
- set MSBUILD_EXE="%%i\MSBuild\Current\Bin\MSBuild.exe"
- exit /b 0
- )
+ for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere" -prerelease -latest -requires Microsoft.Component.MSBuild -property installationPath`) do (
+ if exist "%%i\MSBuild\Current\Bin\MSBuild.exe" (
+ set MSBUILD_EXE="%%i\MSBuild\Current\Bin\MSBuild.exe"
+ exit /b 0
+ )
- if exist "%%i\MSBuild\15.0\Bin\MSBuild.exe" (
- set MSBUILD_EXE="%%i\MSBuild\15.0\Bin\MSBuild.exe"
- exit /b 0
- )
- )
+ if exist "%%i\MSBuild\15.0\Bin\MSBuild.exe" (
+ set MSBUILD_EXE="%%i\MSBuild\15.0\Bin\MSBuild.exe"
+ exit /b 0
+ )
+ )
- exit /b 1
+ exit /b 1
)
diff --git a/SpatialGDK/Content/SpatialDebugger/BP_SpatialDebugger.uasset b/SpatialGDK/Content/SpatialDebugger/BP_SpatialDebugger.uasset
new file mode 100644
index 0000000000..aad9ed3100
Binary files /dev/null and b/SpatialGDK/Content/SpatialDebugger/BP_SpatialDebugger.uasset differ
diff --git a/SpatialGDK/Content/SpatialDebugger/Materials/TranslucentWorkerRegion.uasset b/SpatialGDK/Content/SpatialDebugger/Materials/TranslucentWorkerRegion.uasset
new file mode 100644
index 0000000000..24c20d0f35
Binary files /dev/null and b/SpatialGDK/Content/SpatialDebugger/Materials/TranslucentWorkerRegion.uasset differ
diff --git a/SpatialGDK/Content/SpatialDebugger/Textures/Auth.uasset b/SpatialGDK/Content/SpatialDebugger/Textures/Auth.uasset
new file mode 100644
index 0000000000..6bcaf05655
Binary files /dev/null and b/SpatialGDK/Content/SpatialDebugger/Textures/Auth.uasset differ
diff --git a/SpatialGDK/Content/SpatialDebugger/Textures/AuthIntent.uasset b/SpatialGDK/Content/SpatialDebugger/Textures/AuthIntent.uasset
new file mode 100644
index 0000000000..04e7afd2e1
Binary files /dev/null and b/SpatialGDK/Content/SpatialDebugger/Textures/AuthIntent.uasset differ
diff --git a/SpatialGDK/Content/SpatialDebugger/Textures/Box.uasset b/SpatialGDK/Content/SpatialDebugger/Textures/Box.uasset
new file mode 100644
index 0000000000..9a7b8db735
Binary files /dev/null and b/SpatialGDK/Content/SpatialDebugger/Textures/Box.uasset differ
diff --git a/SpatialGDK/Content/SpatialDebugger/Textures/LockClosed.uasset b/SpatialGDK/Content/SpatialDebugger/Textures/LockClosed.uasset
new file mode 100644
index 0000000000..644adb25a1
Binary files /dev/null and b/SpatialGDK/Content/SpatialDebugger/Textures/LockClosed.uasset differ
diff --git a/SpatialGDK/Content/SpatialDebugger/Textures/LockOpen.uasset b/SpatialGDK/Content/SpatialDebugger/Textures/LockOpen.uasset
new file mode 100644
index 0000000000..ed8e7eb8ae
Binary files /dev/null and b/SpatialGDK/Content/SpatialDebugger/Textures/LockOpen.uasset differ
diff --git a/SpatialGDK/Extras/core-sdk.version b/SpatialGDK/Extras/core-sdk.version
index d3be72cbe1..d18453764c 100644
--- a/SpatialGDK/Extras/core-sdk.version
+++ b/SpatialGDK/Extras/core-sdk.version
@@ -1 +1 @@
-14.5.0
\ No newline at end of file
+14.5.0
diff --git a/SpatialGDK/Extras/fastbuild/README.md b/SpatialGDK/Extras/fastbuild/README.md
index a9cf1d3917..046feab5a0 100644
--- a/SpatialGDK/Extras/fastbuild/README.md
+++ b/SpatialGDK/Extras/fastbuild/README.md
@@ -1,15 +1,29 @@
# Introduction
[FASTBuild](http://www.fastbuild.org/docs/home.html) is a distributed build and caching system.
-Improbable have integrated with the Unreal Build Tool, using [Unreal_FASTBuild](https://github.com/liamkf/Unreal_FASTBuild)
+The GDK for Unreal integrates with the Unreal Build Tool, using [Unreal_FASTBuild](https://github.com/liamkf/Unreal_FASTBuild)
as the foundation.
+# Cache location
+
+To use Fastbuild outside Improbable, you need to configure your own caching location, where compilation results can be shared across users.
+
+Follow [these instructions](http://www.fastbuild.org/docs/features/caching.html) to set up your cache and update the `$fileshare` variable in `install.ps1` with your value.
+
+# Cache location (internal to Improbable)
+
+Improbable's cache location is `\\lonv-file-01` which is set as the default value in the installation script. To use it, you first need to authorise access to this network drive:
+1. Enter `\\lonv-file-01` your Windows Explorer address bar
+1. Authenticate with your Windows username and password (not the PIN). Make sure to set `Remember my credentials` so this works after a restart.
+
# Installation
FASTBuild can be installed in two different ways:
* As a service, which is good for build agents.
* As a GUI, which is good for your local machine.
+Installation scripts are provided in this folder: `install.ps1` and `uninstall.ps1`.
+
> All of these must be run from an **administrator** `powershell` or `cmd` prompt.
## As a service
@@ -17,25 +31,24 @@ FASTBuild can be installed as a service, for build agents and other non-interact
**To install**
- `powershell -NoProfile -ExecutionPolicy Bypass -File "\\lonv-file-01\Fastbuild\install.ps1" -service`
+ `powershell -NoProfile -ExecutionPolicy Bypass -File install.ps1 -service`
**To uninstall**
- `powershell -NoProfile -ExecutionPolicy Bypass -File "\\lonv-file-01\Fastbuild\uninstall.ps1" -service`
+ `powershell -NoProfile -ExecutionPolicy Bypass -File uninstall.ps1 -service`
# As a GUI
If you're installing on your workstation, it's recommended to install it in interactive mode.
**To install**
- `powershell -NoProfile -ExecutionPolicy Bypass -File "\\lonv-file-01\Fastbuild\install.ps1"`
+ `powershell -NoProfile -ExecutionPolicy Bypass -File install.ps1`
**To uninstall**
- `powershell -NoProfile -ExecutionPolicy Bypass -File "\\lonv-file-01\Fastbuild\uninstall.ps1"`
+ `powershell -NoProfile -ExecutionPolicy Bypass -File uninstall.ps1`
# Useful tools
* A Visual Studio plugin for monitoring the status of builds: [FASTBuildMonitor](https://github.com/yass007/FASTBuildMonitor)
* An alternative, standalone build monitor: [FASTBuild-Dashboard](https://github.com/hillin/FASTBuild-Dashboard)
-
diff --git a/SpatialGDK/Extras/internal-documentation/release-process.md b/SpatialGDK/Extras/internal-documentation/release-process.md
index 480a9a5350..113e226af3 100644
--- a/SpatialGDK/Extras/internal-documentation/release-process.md
+++ b/SpatialGDK/Extras/internal-documentation/release-process.md
@@ -29,7 +29,7 @@ To check that Xbox-compatible Worker SDK DLLs are available.
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-1275](https://improbableio.atlassian.net/browse/WRK-1275) for an example.
+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.
@@ -59,6 +59,7 @@ If it fails because the DLL is not available, file a WRK ticket for the Worker t
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).
@@ -69,17 +70,11 @@ If it fails because the DLL is not available, file a WRK ticket for the Worker t
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.
-### Serve docs locally
-It is vital that you test using the docs for the release version that you are about to publish, not with the currently live docs that relate to the previous version.
-1. cd `UnrealGDK`
-1. git checkout `docs-release`
-1. `improbadoc serve `
-
## Build your release candidate engine
-1. Open http://localhost:8080/reference/1.0/content/get-started/dependencies.
+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 http://localhost:8080/reference/1.0/content/get-started/dependencies.
+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
@@ -98,42 +93,17 @@ The workflow for this is:
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)
-1. Follow these steps: http://localhost:8080/reference/1.0/content/get-started/gdk-template, bearing in mind the following caveat:
-* When you clone the GDK into the `Plugins` folder, be sure to checkout the release candidate branch, so you're working with the release version.
-2. Launch a local SpatialOS deployment, then a standalone server-worker, and then connect two standalone clients to it. To do this:
-* In your file browser, click `LaunchSpatial.bat` in order to run it.
-* In your file browser, click `LaunchServer.bat` in order to run it.
-* In your file browser, click `LaunchClient.bat` in order to run it.
-* Run the same script again in order to launch the second client
-* Run and shoot eachother with the clients as a smoke test.
-* Open the `UE4 Console` and enter the command `open 127.0.0.1`. The desired effect is that the client disconnect and then re-connects to the map. If you can continue to play after executing the command then you've succesfully tested client travel.
-
-3. Launch a local SpatialOS deployment, then connect two machines as clients using your local network. To do this:
-* Ensure that both machines are on the same network.
-* On your own machine, in your terminal, `cd` to ``.
-* Build out a windows client by running:
-`Game\Plugins\UnrealGDK\SpatialGDK\Build\Scripts\BuildWorker.bat YourProject Win64 Development YourProject.uproject`
-* Send the client you just built to the other machine you'll be using to connect. You can find it at: `\spatial\build\assembly\worker\UnrealClient@Windows.zip`
-* Still on your server machine, discover your local IP address by runing `ipconfig`. It's the one entitled `IPv4 Address`.
-* Still in your server machine, in a terminal window, `cd` to `\spatial\` and run the following command: `spatial local launch default_launch.json --runtime_ip=`
-* Still on your server machine, run `LaunchServer.bat`.
-* On the machine you're going to run your clients on, unzip `UnrealClient@Windows.zip`.
-* On the machine you're going to run your clients on, in a terminal window, `cd` to the unzipped `UnrealClient@Windows` direcory and run the following command: `_YourProject.exe -workerType UnrealClient -useExternalIpForBridge true`
-* Repeat the above step in order to launch the second client
-* Run and shoot eachother with the clients as a smoke test.
-* You can now turn off the machine that's running the client, and return to your own machine.
-
-## Validation (UnrealGDKExampleProject)
-1. Follow these steps: http://localhost:8080/reference/1.0/content/get-started/example-project/exampleproject-intro. All tests must pass.
-
-## Validation (Playtest)
-1. Follow these steps: https://brevi.link/unreal-release-playtests. All tests must pass.
+## 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. Upload docs to docs-testing using Improbadoc.
-1. Validate that Improbadoc reports no linting errors.
-1. Read the docs for five minutes to ensure nothing looks broken.
+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.
## Release
@@ -174,21 +144,17 @@ Copy the latest release notes from `CHANGELOG.md` and paste them into the releas
1. In `UnrealGDK`, merge `release` into `master`.
**Documentation**
-1. Publish the docs to live using Improbadoc commands listed [here](https://improbableio.atlassian.net/wiki/spaces/GBU/pages/327485360/Publishing+GDK+Docs).
-1. Update the [roadmap](https://github.com/spatialos/UnrealGDK/projects/1), moving the release from **Planned** to **Released**, and linking to the release.
-1. Audit the [known issues](https://github.com/spatialos/UnrealGDK/issues), and ensure all fixed issues are updated/removed.
+1. Notify @techwriters in [#docs](https://improbable.slack.com/archives/C0TBQAB5X) that they may publish the new version of the docs.
**Announce**
-Only announce full releases, not `preview` ones.
-
1. Announce the release in:
* Forums
* Discord (`#unreal`, do not `@here`)
* Slack (`#releases`)
-* Email (`spatialos-announce@`)
+* Email (`unreal-interest@`)
-Congratulations, you've done the release!
+Congratulations, you've completed the release process!
## Clean up
diff --git a/SpatialGDK/Extras/schema/component_presence.schema b/SpatialGDK/Extras/schema/component_presence.schema
new file mode 100644
index 0000000000..7465da5e90
--- /dev/null
+++ b/SpatialGDK/Extras/schema/component_presence.schema
@@ -0,0 +1,13 @@
+// Copyright (c) Improbable Worlds Ltd, All Rights Reserved
+package unreal;
+
+// The ComponentPresence component should be present on all entities.
+component ComponentPresence {
+ id = 9972;
+
+ // The component_list is a list of component IDs that should be present on
+ // this entity. This should be useful in future for deducing entity completeness
+ // without critical sections but is used currently just for enabling dynamic
+ // components in a multi-worker environment.
+ list component_list = 1;
+}
diff --git a/SpatialGDK/Extras/schema/global_state_manager.schema b/SpatialGDK/Extras/schema/global_state_manager.schema
index 6f809a480c..5a0e2230ca 100644
--- a/SpatialGDK/Extras/schema/global_state_manager.schema
+++ b/SpatialGDK/Extras/schema/global_state_manager.schema
@@ -19,6 +19,8 @@ component DeploymentMap {
id = 9994;
string map_url = 1;
bool accepting_players = 2;
+ uint32 session_id = 3;
+ uint32 schema_hash = 4;
}
component StartupActorManager {
diff --git a/SpatialGDK/Extras/schema/net_owning_client_worker.schema b/SpatialGDK/Extras/schema/net_owning_client_worker.schema
new file mode 100644
index 0000000000..9d15ce029f
--- /dev/null
+++ b/SpatialGDK/Extras/schema/net_owning_client_worker.schema
@@ -0,0 +1,15 @@
+// Copyright (c) Improbable Worlds Ltd, All Rights Reserved
+package unreal;
+
+// The NetOwningClientWorker component should be present on all entities
+// representing Actors or subobjects which can have owning connections.
+component NetOwningClientWorker {
+ id = 9971;
+
+ // The worker_id is an optional worker ID string that is set by the
+ // simulating worker when the Actor or subobject becomes net-owned by
+ // a client connection. The enforcer uses this value to update the
+ // EntityACL entry for the client RPC endpoint (and Heartbeat component,
+ // if present).
+ option worker_id = 1;
+}
diff --git a/SpatialGDK/Extras/schema/rpc_components.schema b/SpatialGDK/Extras/schema/rpc_components.schema
index 1eca1acd30..f1d85fb574 100644
--- a/SpatialGDK/Extras/schema/rpc_components.schema
+++ b/SpatialGDK/Extras/schema/rpc_components.schema
@@ -2,38 +2,28 @@
package unreal;
import "unreal/gdk/core_types.schema";
+import "unreal/gdk/rpc_payload.schema";
-type UnrealRPCPayload {
- uint32 offset = 1;
- uint32 rpc_index = 2;
- bytes rpc_payload = 3;
-}
-
-type UnrealPackedRPCPayload {
- uint32 offset = 1;
- uint32 rpc_index = 2;
- bytes rpc_payload = 3;
- EntityId entity = 4;
-}
-
-component UnrealClientRPCEndpoint {
+component UnrealClientRPCEndpointLegacy {
id = 9990;
// Set to true when authority is gained, indicating that RPCs can be received
bool ready = 1;
event UnrealRPCPayload client_to_server_rpc_event;
- event UnrealPackedRPCPayload packed_client_to_server_rpc;
}
-component UnrealServerRPCEndPoint {
+component UnrealServerRPCEndpointLegacy {
id = 9989;
// Set to true when authority is gained, indicating that RPCs can be received
bool ready = 1;
event UnrealRPCPayload server_to_client_rpc_event;
- event UnrealPackedRPCPayload packed_server_to_client_rpc;
+}
+
+component UnrealServerToServerCommandEndpoint {
+ id = 9973;
command Void server_to_server_rpc_command(UnrealRPCPayload);
}
-component UnrealMulticastRPCEndpoint {
+component UnrealMulticastRPCEndpointLegacy {
id = 9987;
event UnrealRPCPayload unreliable_multicast_rpc;
}
diff --git a/SpatialGDK/Extras/schema/rpc_payload.schema b/SpatialGDK/Extras/schema/rpc_payload.schema
new file mode 100644
index 0000000000..cd45a4d9ca
--- /dev/null
+++ b/SpatialGDK/Extras/schema/rpc_payload.schema
@@ -0,0 +1,14 @@
+// Copyright (c) Improbable Worlds Ltd, All Rights Reserved
+package unreal;
+
+type TracePayload {
+ bytes trace_id = 1;
+ bytes span_id = 2;
+}
+
+type UnrealRPCPayload {
+ uint32 offset = 1;
+ uint32 rpc_index = 2;
+ bytes rpc_payload = 3;
+ option rpc_trace = 4;
+}
diff --git a/SpatialGDK/Extras/schema/server_worker.schema b/SpatialGDK/Extras/schema/server_worker.schema
new file mode 100644
index 0000000000..d921635897
--- /dev/null
+++ b/SpatialGDK/Extras/schema/server_worker.schema
@@ -0,0 +1,22 @@
+// Copyright (c) Improbable Worlds Ltd, All Rights Reserved
+package unreal;
+
+import "unreal/gdk/core_types.schema";
+import "unreal/gdk/spawner.schema";
+
+type ForwardSpawnPlayerRequest {
+ SpawnPlayerRequest spawn_player_request = 1;
+ UnrealObjectRef player_start = 2;
+ string client_worker_id = 3;
+}
+
+type ForwardSpawnPlayerResponse {
+ bool success = 1;
+}
+
+component ServerWorker {
+ id = 9974;
+ string worker_name = 1;
+ bool ready_to_begin_play = 2;
+ command ForwardSpawnPlayerResponse forward_spawn_player(ForwardSpawnPlayerRequest);
+}
diff --git a/SpatialGDK/Extras/schema/spatial_debugging.schema b/SpatialGDK/Extras/schema/spatial_debugging.schema
new file mode 100644
index 0000000000..096339f947
--- /dev/null
+++ b/SpatialGDK/Extras/schema/spatial_debugging.schema
@@ -0,0 +1,23 @@
+// Copyright (c) Improbable Worlds Ltd, All Rights Reserved
+package unreal;
+
+component SpatialDebugging {
+ id = 9975;
+
+ // Id assigned to the Unreal server worker which is authoritative for this entity.
+ // 0 is reserved as an invalid/unset value.
+ uint32 authoritative_virtual_worker_id = 1;
+
+ // The color for the authoritative virtual worker.
+ uint32 authoritative_color = 2;
+
+ // Id assigned to the Unreal server worker which should be authoritative for this entity.
+ // 0 is reserved as an invalid/unset value.
+ uint32 intent_virtual_worker_id = 3;
+
+ // The color for the intended virtual worker.
+ uint32 intent_color = 4;
+
+ // Whether or not the entity is locked.
+ bool is_locked = 5;
+}
diff --git a/SpatialGDK/Extras/schema/unreal_metadata.schema b/SpatialGDK/Extras/schema/unreal_metadata.schema
index 5abbd60d08..beb648c876 100644
--- a/SpatialGDK/Extras/schema/unreal_metadata.schema
+++ b/SpatialGDK/Extras/schema/unreal_metadata.schema
@@ -6,7 +6,6 @@ import "unreal/gdk/core_types.schema";
component UnrealMetadata {
id = 9996;
option stably_named_ref = 1; // Exists when entity represents a stably named Actor (RF_WasLoaded)
- option owner_worker_attribute = 2;
- string class_path = 3;
- option net_startup = 4; // Exists only when entity has a stably_named_ref
+ string class_path = 2;
+ option net_startup = 3; // Exists only when entity has a stably_named_ref
}
diff --git a/SpatialGDK/Extras/schema/virtual_worker_translation.schema b/SpatialGDK/Extras/schema/virtual_worker_translation.schema
index 5b805e86c7..1bfb48055e 100644
--- a/SpatialGDK/Extras/schema/virtual_worker_translation.schema
+++ b/SpatialGDK/Extras/schema/virtual_worker_translation.schema
@@ -9,9 +9,10 @@ package unreal;
type VirtualWorkerMapping {
uint32 virtual_worker_id = 1;
string physical_worker_name = 2;
+ EntityId server_worker_entity = 3;
}
component VirtualWorkerTranslation {
id = 9979;
- list virtual_worker_mapping = 1;
+ transient list virtual_worker_mapping = 1;
}
diff --git a/SpatialGDK/Extras/templates/WorkerJsonTemplate.json b/SpatialGDK/Extras/templates/WorkerJsonTemplate.json
index 10ec85e060..25374a8baf 100644
--- a/SpatialGDK/Extras/templates/WorkerJsonTemplate.json
+++ b/SpatialGDK/Extras/templates/WorkerJsonTemplate.json
@@ -24,23 +24,6 @@
"{{WorkerTypeName}}"
]
},
- "entity_interest": {
- "range_entity_interest": {
- "radius": 50
- }
- },
- "streaming_query": [
- {
- "global_component_streaming_query": {
- "component_name": "unreal.SingletonManager"
- }
- },
- {
- "global_component_streaming_query": {
- "component_name": "unreal.Singleton"
- }
- }
- ],
"component_delivery": {
"default": "RELIABLE_ORDERED",
"checkout_all_initially": true
diff --git a/SpatialGDK/Raw/SpatialDebugger/Textures/Auth.png b/SpatialGDK/Raw/SpatialDebugger/Textures/Auth.png
new file mode 100644
index 0000000000..5a8b8ee6ed
Binary files /dev/null and b/SpatialGDK/Raw/SpatialDebugger/Textures/Auth.png differ
diff --git a/SpatialGDK/Raw/SpatialDebugger/Textures/AuthIntent.png b/SpatialGDK/Raw/SpatialDebugger/Textures/AuthIntent.png
new file mode 100644
index 0000000000..434f0428a7
Binary files /dev/null and b/SpatialGDK/Raw/SpatialDebugger/Textures/AuthIntent.png differ
diff --git a/SpatialGDK/Raw/SpatialDebugger/Textures/Box.png b/SpatialGDK/Raw/SpatialDebugger/Textures/Box.png
new file mode 100644
index 0000000000..572f2226fa
Binary files /dev/null and b/SpatialGDK/Raw/SpatialDebugger/Textures/Box.png differ
diff --git a/SpatialGDK/Raw/SpatialDebugger/Textures/LockClosed.png b/SpatialGDK/Raw/SpatialDebugger/Textures/LockClosed.png
new file mode 100644
index 0000000000..73cc3ff780
Binary files /dev/null and b/SpatialGDK/Raw/SpatialDebugger/Textures/LockClosed.png differ
diff --git a/SpatialGDK/Raw/SpatialDebugger/Textures/LockOpen.png b/SpatialGDK/Raw/SpatialDebugger/Textures/LockOpen.png
new file mode 100644
index 0000000000..7ef5e530e1
Binary files /dev/null and b/SpatialGDK/Raw/SpatialDebugger/Textures/LockOpen.png differ
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/ActorInterestComponent.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/ActorInterestComponent.cpp
index 778357708d..21a7928c9e 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/ActorInterestComponent.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/ActorInterestComponent.cpp
@@ -5,8 +5,10 @@
#include "Schema/Interest.h"
#include "Interop/SpatialClassInfoManager.h"
-void UActorInterestComponent::CreateQueries(const USpatialClassInfoManager& ClassInfoManager, const SpatialGDK::QueryConstraint& AdditionalConstraints, TArray& OutQueries) const
+void UActorInterestComponent::PopulateFrequencyToConstraintsMap(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::FrequencyToConstraintsMap& OutFrequencyToQueryConstraints) const
{
+ // Loop through the user specified queries to extract the constraints and frequencies.
+ // We don't construct the actual query at this point because the interest factory enforces the result types.
for (const auto& QueryData : Queries)
{
if (!QueryData.Constraint)
@@ -14,27 +16,18 @@ void UActorInterestComponent::CreateQueries(const USpatialClassInfoManager& Clas
continue;
}
- SpatialGDK::Query NewQuery{};
- // Avoid creating an unnecessary AND constraint if there are no AdditionalConstraints to consider.
- if (AdditionalConstraints.IsValid())
- {
- SpatialGDK::QueryConstraint ComponentConstraints;
- QueryData.Constraint->CreateConstraint(ClassInfoManager, ComponentConstraints);
+ SpatialGDK::QueryConstraint NewQueryConstraint{};
+ QueryData.Constraint->CreateConstraint(ClassInfoManager, NewQueryConstraint);
- NewQuery.Constraint.AndConstraint.Add(ComponentConstraints);
- NewQuery.Constraint.AndConstraint.Add(AdditionalConstraints);
- }
- else
+ // If there is already a query defined with this frequency, group them to avoid making too many queries down the line.
+ // This avoids any extra cost due to duplicate result types across the network if they are large.
+ if (OutFrequencyToQueryConstraints.Find(QueryData.Frequency))
{
- QueryData.Constraint->CreateConstraint(ClassInfoManager, NewQuery.Constraint);
+ OutFrequencyToQueryConstraints.Find(QueryData.Frequency)->Add(NewQueryConstraint);
+ continue;
}
- NewQuery.Frequency = QueryData.Frequency;
- NewQuery.FullSnapshotResult = true;
- if (NewQuery.Constraint.IsValid())
- {
- OutQueries.Push(NewQuery);
- }
+ TArray ConstraintList = { NewQueryConstraint };
+ OutFrequencyToQueryConstraints.Add(QueryData.Frequency, ConstraintList);
}
-
}
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/SpatialPingComponent.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/SpatialPingComponent.cpp
index d5571c33ba..399fece084 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/SpatialPingComponent.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/SpatialPingComponent.cpp
@@ -16,7 +16,11 @@ USpatialPingComponent::USpatialPingComponent(const FObjectInitializer& ObjectIni
PrimaryComponentTick.bCanEverTick = false;
PrimaryComponentTick.bStartWithTickEnabled = false;
+#if ENGINE_MINOR_VERSION <= 23
bReplicates = true;
+#else
+ SetIsReplicatedByDefault(true);
+#endif
}
void USpatialPingComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const
@@ -64,7 +68,7 @@ void USpatialPingComponent::SetPingEnabled(bool bSetEnabled)
}
// Only execute on owning local client.
- if (!OwningController->HasAuthority())
+ if (OwningController->IsNetMode(NM_Client) && OwningController->GetLocalRole() == ROLE_AutonomousProxy)
{
if (bSetEnabled && !bIsPingEnabled)
{
@@ -160,6 +164,7 @@ void USpatialPingComponent::OnRep_ReplicatedPingID()
// Calculate the round trip ping
RoundTripPing = static_cast(FPlatformTime::Seconds() - LastSentPingTimestamp);
+ RecordPing(RoundTripPing);
if (RoundTripPing >= MinPingInterval)
{
// If the current ping exceeds the min interval then send a new ping immediately.
@@ -177,6 +182,51 @@ void USpatialPingComponent::OnRep_ReplicatedPingID()
}
}
+FSpatialPingAverageData USpatialPingComponent::GetAverageData() const
+{
+ FSpatialPingAverageData Data = {};
+
+ if (LastPingMeasurements.Num() > 0)
+ {
+ float Total = 0.0f;
+ for (float Ping : LastPingMeasurements)
+ {
+ Total += Ping;
+ }
+ Data.LastMeasurementsWindowAvg = Total / LastPingMeasurements.Num();
+ Data.LastMeasurementsWindowMin = FMath::Min(LastPingMeasurements);
+ Data.LastMeasurementsWindowMax = FMath::Max(LastPingMeasurements);
+ }
+ Data.WindowSize = LastPingMeasurements.Num();
+
+ if (TotalNum > 0)
+ {
+ Data.TotalAvg = TotalPing / TotalNum;
+ Data.TotalMin = TotalMin;
+ Data.TotalMax = TotalMax;
+ Data.TotalNum = TotalNum;
+ }
+
+ return Data;
+}
+
+void USpatialPingComponent::RecordPing(float Ping)
+{
+ LastPingMeasurements.Add(Ping);
+ if (LastPingMeasurements.Num() > PingMeasurementsWindowSize)
+ {
+ LastPingMeasurements.RemoveAt(0);
+ }
+
+ TotalPing += Ping;
+ TotalNum++;
+
+ TotalMin = FMath::Min(TotalMin, Ping);
+ TotalMax = FMath::Max(TotalMax, Ping);
+
+ OnRecordPing.Broadcast(Ping);
+}
+
bool USpatialPingComponent::SendServerWorkerPingID_Validate(uint16 PingID)
{
return true;
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp
index 2431123923..74f05b7b9a 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp
@@ -14,15 +14,18 @@
#include "Settings/LevelEditorPlaySettings.h"
#endif
+#include "EngineStats.h"
#include "EngineClasses/SpatialNetConnection.h"
#include "EngineClasses/SpatialNetDriver.h"
#include "EngineClasses/SpatialPackageMapClient.h"
#include "Interop/GlobalStateManager.h"
#include "Interop/SpatialReceiver.h"
#include "Interop/SpatialSender.h"
-#include "Schema/AlwaysRelevant.h"
-#include "Schema/ClientRPCEndpoint.h"
-#include "Schema/ServerRPCEndpoint.h"
+#include "LoadBalancing/AbstractLBStrategy.h"
+#include "Schema/ClientRPCEndpointLegacy.h"
+#include "Schema/NetOwningClientWorker.h"
+#include "Schema/SpatialDebugging.h"
+#include "Schema/ServerRPCEndpointLegacy.h"
#include "SpatialConstants.h"
#include "SpatialGDKSettings.h"
#include "Utils/RepLayoutUtils.h"
@@ -33,6 +36,11 @@ DEFINE_LOG_CATEGORY(LogSpatialActorChannel);
DECLARE_CYCLE_STAT(TEXT("ReplicateActor"), STAT_SpatialActorChannelReplicateActor, STATGROUP_SpatialNet);
DECLARE_CYCLE_STAT(TEXT("UpdateSpatialPosition"), STAT_SpatialActorChannelUpdateSpatialPosition, STATGROUP_SpatialNet);
DECLARE_CYCLE_STAT(TEXT("ReplicateSubobject"), STAT_SpatialActorChannelReplicateSubobject, STATGROUP_SpatialNet);
+DECLARE_CYCLE_STAT(TEXT("ServerProcessOwnershipChange"), STAT_ServerProcessOwnershipChange, STATGROUP_SpatialNet);
+DECLARE_CYCLE_STAT(TEXT("ClientProcessOwnershipChange"), STAT_ClientProcessOwnershipChange, STATGROUP_SpatialNet);
+DECLARE_CYCLE_STAT(TEXT("CallUpdateEntityACLs"), STAT_CallUpdateEntityACLs, STATGROUP_SpatialNet);
+DECLARE_CYCLE_STAT(TEXT("OnUpdateEntityACLSuccess"), STAT_OnUpdateEntityACLSuccess, STATGROUP_SpatialNet);
+DECLARE_CYCLE_STAT(TEXT("IsAuthoritativeServer"), STAT_IsAuthoritativeServer, STATGROUP_SpatialNet);
namespace
{
@@ -82,6 +90,107 @@ void UpdateChangelistHistory(TUniquePtr& RepState)
}
} // end anonymous namespace
+bool FSpatialObjectRepState::MoveMappedObjectToUnmapped_r(const FUnrealObjectRef& ObjRef, FObjectReferencesMap& ObjectReferencesMap)
+{
+ bool bFoundRef = false;
+
+ for (auto& ObjReferencePair : ObjectReferencesMap)
+ {
+ FObjectReferences& ObjReferences = ObjReferencePair.Value;
+
+ if (ObjReferences.Array != NULL)
+ {
+ if (MoveMappedObjectToUnmapped_r(ObjRef, *ObjReferences.Array))
+ {
+ bFoundRef = true;
+ }
+ continue;
+ }
+
+ if (ObjReferences.MappedRefs.Contains(ObjRef))
+ {
+ ObjReferences.MappedRefs.Remove(ObjRef);
+ ObjReferences.UnresolvedRefs.Add(ObjRef);
+ bFoundRef = true;
+ }
+ }
+
+ return bFoundRef;
+}
+
+bool FSpatialObjectRepState::MoveMappedObjectToUnmapped(const FUnrealObjectRef& ObjRef)
+{
+ if (MoveMappedObjectToUnmapped_r(ObjRef, ReferenceMap))
+ {
+ UnresolvedRefs.Add(ObjRef);
+ return true;
+ }
+ return false;
+}
+
+void FSpatialObjectRepState::GatherObjectRef(TSet& OutReferenced, TSet& OutUnresolved, const FObjectReferences& CurReferences) const
+{
+ if (CurReferences.Array)
+ {
+ for (auto const& Entry : *CurReferences.Array)
+ {
+ GatherObjectRef(OutReferenced, OutUnresolved, Entry.Value);
+ }
+ }
+
+ OutUnresolved.Append(CurReferences.UnresolvedRefs);
+
+ // Add both kind of references to OutReferenced map.
+ // It is simpler to manage the Ref to RepState map that way by not requiring strict partitioning between both sets.
+ OutReferenced.Append(CurReferences.UnresolvedRefs);
+ OutReferenced.Append(CurReferences.MappedRefs);
+}
+
+void FSpatialObjectRepState::UpdateRefToRepStateMap(FObjectToRepStateMap& RepStateMap)
+{
+ // Inspired by FObjectReplicator::UpdateGuidToReplicatorMap
+ UnresolvedRefs.Empty();
+
+ TSet< FUnrealObjectRef > LocalReferencedObj;
+ for (auto& Entry : ReferenceMap)
+ {
+ GatherObjectRef(LocalReferencedObj, UnresolvedRefs, Entry.Value);
+ }
+
+ // TODO : Support references in structures updated by deltas. UNR-2556
+ // Look for the code iterating over LifetimeCustomDeltaProperties in the equivalent FObjectReplicator method.
+
+ // Go over all referenced guids, and make sure we're tracking them in the GuidToReplicatorMap
+ for (const FUnrealObjectRef& Ref : LocalReferencedObj)
+ {
+ if (!ReferencedObj.Contains(Ref))
+ {
+ RepStateMap.FindOrAdd(Ref).Add(ThisObj);
+ }
+ }
+
+ // Remove any guids that we were previously tracking but no longer should
+ for (const FUnrealObjectRef& Ref : ReferencedObj)
+ {
+ if (!LocalReferencedObj.Contains(Ref))
+ {
+ TSet* RepStatesWithRef = RepStateMap.Find(Ref);
+
+ if (ensure(RepStatesWithRef))
+ {
+ RepStatesWithRef->Remove(ThisObj);
+
+ if (RepStatesWithRef->Num() == 0)
+ {
+ RepStateMap.Remove(Ref);
+ }
+ }
+ }
+ }
+
+ ReferencedObj = MoveTemp(LocalReferencedObj);
+}
+
USpatialActorChannel::USpatialActorChannel(const FObjectInitializer& ObjectInitializer /*= FObjectInitializer::Get()*/)
: Super(ObjectInitializer)
, bCreatedEntity(false)
@@ -105,11 +214,14 @@ void USpatialActorChannel::Init(UNetConnection* InConnection, int32 ChannelIndex
EntityId = SpatialConstants::INVALID_ENTITY_ID;
bInterestDirty = false;
bNetOwned = false;
+ bIsAuthClient = false;
+ bIsAuthServer = false;
LastPositionSinceUpdate = FVector::ZeroVector;
TimeWhenPositionLastUpdated = 0.0f;
PendingDynamicSubobjects.Empty();
- SavedOwnerWorkerAttribute.Empty();
+ SavedConnectionOwningWorkerId.Empty();
+ SavedInterestBucketComponentID = SpatialConstants::INVALID_COMPONENT_ID;
FramesTillDormancyAllowed = 0;
@@ -129,7 +241,7 @@ void USpatialActorChannel::DeleteEntityIfAuthoritative()
return;
}
- bool bHasAuthority = NetDriver->IsAuthoritativeDestructionAllowed() && NetDriver->StaticComponentView->GetAuthority(EntityId, SpatialGDK::Position::ComponentId) == WORKER_AUTHORITY_AUTHORITATIVE;
+ 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);
@@ -152,16 +264,11 @@ void USpatialActorChannel::DeleteEntityIfAuthoritative()
}
}
-bool USpatialActorChannel::IsSingletonEntity()
-{
- return NetDriver->GlobalStateManager->IsSingletonEntity(EntityId);
-}
-
bool USpatialActorChannel::CleanUp(const bool bForDestroy, EChannelCloseReason CloseReason)
{
-#if WITH_EDITOR
if (NetDriver != nullptr)
{
+#if WITH_EDITOR
const bool bDeleteDynamicEntities = GetDefault()->GetDeleteDynamicEntities();
if (bDeleteDynamicEntities &&
@@ -172,40 +279,51 @@ bool USpatialActorChannel::CleanUp(const bool bForDestroy, EChannelCloseReason C
// If we're a server worker, and the entity hasn't already been cleaned up, delete it on shutdown.
DeleteEntityIfAuthoritative();
}
- }
-#endif
+#endif // WITH_EDITOR
- if (CloseReason != EChannelCloseReason::Dormancy)
- {
- // Must cleanup actor and subobjects before UActorChannel::Cleanup as it will clear CreateSubObjects.
- NetDriver->PackageMap->RemoveEntityActor(EntityId);
- }
- else
- {
- NetDriver->RegisterDormantEntityId(EntityId);
+ if (CloseReason != EChannelCloseReason::Dormancy)
+ {
+ // Must cleanup actor and subobjects before UActorChannel::Cleanup as it will clear CreateSubObjects.
+ NetDriver->PackageMap->RemoveEntityActor(EntityId);
+ }
+ else
+ {
+ NetDriver->RegisterDormantEntityId(EntityId);
+ }
+
+ if (CloseReason == EChannelCloseReason::Destroyed || CloseReason == EChannelCloseReason::LevelUnloaded)
+ {
+ Receiver->ClearPendingRPCs(EntityId);
+ Sender->ClearPendingRPCs(EntityId);
+ }
+ NetDriver->RemoveActorChannel(EntityId, *this);
}
- NetDriver->RemoveActorChannel(EntityId);
return UActorChannel::CleanUp(bForDestroy, CloseReason);
}
int64 USpatialActorChannel::Close(EChannelCloseReason Reason)
{
- if (Reason != EChannelCloseReason::Dormancy)
- {
- DeleteEntityIfAuthoritative();
- NetDriver->PackageMap->RemoveEntityActor(EntityId);
- }
- else
+ if (Reason == EChannelCloseReason::Dormancy)
{
// Closed for dormancy reasons, ensure we update the component state of this entity.
const bool bMakeDormant = true;
NetDriver->RefreshActorDormancy(Actor, bMakeDormant);
NetDriver->RegisterDormantEntityId(EntityId);
}
+ else if (Reason == EChannelCloseReason::Relevancy)
+ {
+ check(IsAuthoritativeServer());
+ // Do nothing except close actor channel - this should only get processed on auth server
+ }
+ else
+ {
+ DeleteEntityIfAuthoritative();
+ NetDriver->PackageMap->RemoveEntityActor(EntityId);
+ }
- NetDriver->RemoveActorChannel(EntityId);
+ NetDriver->RemoveActorChannel(EntityId, *this);
return Super::Close(Reason);
}
@@ -233,23 +351,13 @@ void USpatialActorChannel::UpdateShadowData()
}
// Refresh shadow data when crossing over servers to prevent stale/out-of-date data.
-#if ENGINE_MINOR_VERSION <= 22
- ActorReplicator->RepLayout->InitShadowData(ActorReplicator->ChangelistMgr->GetRepChangelistState()->StaticBuffer, Actor->GetClass(), reinterpret_cast(Actor));
-#else
- ActorReplicator->ChangelistMgr->GetRepChangelistState()->StaticBuffer.Buffer.Empty();
- ActorReplicator->RepLayout->InitRepStateStaticBuffer(ActorReplicator->ChangelistMgr->GetRepChangelistState()->StaticBuffer, reinterpret_cast(Actor));
-#endif
+ ResetShadowData(*ActorReplicator->RepLayout, ActorReplicator->ChangelistMgr->GetRepChangelistState()->StaticBuffer, Actor);
// Refresh the shadow data for all replicated components of this actor as well.
for (UActorComponent* ActorComponent : Actor->GetReplicatedComponents())
{
FObjectReplicator& ComponentReplicator = FindOrCreateReplicator(ActorComponent).Get();
-#if ENGINE_MINOR_VERSION <= 22
- ComponentReplicator.RepLayout->InitShadowData(ComponentReplicator.ChangelistMgr->GetRepChangelistState()->StaticBuffer, ActorComponent->GetClass(), reinterpret_cast(ActorComponent));
-#else
- ComponentReplicator.ChangelistMgr->GetRepChangelistState()->StaticBuffer.Buffer.Empty();
- ComponentReplicator.RepLayout->InitRepStateStaticBuffer(ComponentReplicator.ChangelistMgr->GetRepChangelistState()->StaticBuffer, reinterpret_cast(ActorComponent));
-#endif
+ ResetShadowData(*ComponentReplicator.RepLayout, ComponentReplicator.ChangelistMgr->GetRepChangelistState()->StaticBuffer, ActorComponent);
}
}
@@ -321,7 +429,7 @@ int64 USpatialActorChannel::ReplicateActor()
{
return 0;
}
-
+
check(Actor);
check(!Closing);
check(Connection);
@@ -338,6 +446,45 @@ int64 USpatialActorChannel::ReplicateActor()
// Group actors by exact class, one level below parent native class.
SCOPE_CYCLE_UOBJECT(ReplicateActor, Actor);
+ const bool bReplay = ActorWorld && ActorWorld->DemoNetDriver == Connection->GetDriver();
+
+ //////////////////////////////////////////////////////////////////////////
+ // Begin - error and stat duplication from DataChannel::ReplicateActor()
+ if (!bReplay)
+ {
+ GNumReplicateActorCalls++;
+ }
+
+ // triggering replication of an Actor while already in the middle of replication can result in invalid data being sent and is therefore illegal
+ if (bIsReplicatingActor)
+ {
+ FString Error(FString::Printf(TEXT("ReplicateActor called while already replicating! %s"), *Describe()));
+ UE_LOG(LogNet, Log, TEXT("%s"), *Error);
+ ensureMsgf(false, TEXT("%s"), *Error);
+ return 0;
+ }
+ else if (bActorIsPendingKill)
+ {
+ // Don't need to do anything, because it should have already been logged.
+ return 0;
+ }
+ // If our Actor is PendingKill, that's bad. It means that somehow it wasn't properly removed
+ // from the NetDriver or ReplicationDriver.
+ // TODO: Maybe notify the NetDriver / RepDriver about this, and have the channel close?
+ 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);
+ return 0;
+ }
+ // End - error and stat duplication from DataChannel::ReplicateActor()
+ //////////////////////////////////////////////////////////////////////////
+
// Create an outgoing bunch (to satisfy some of the functions below).
FOutBunch Bunch(this, 0);
if (Bunch.IsError())
@@ -370,8 +517,12 @@ int64 USpatialActorChannel::ReplicateActor()
}
RepFlags.bNetSimulated = (Actor->GetRemoteRole() == ROLE_SimulatedProxy);
+#if ENGINE_MINOR_VERSION <= 23
RepFlags.bRepPhysics = Actor->ReplicatedMovement.bRepPhysics;
- RepFlags.bReplay = ActorWorld && (ActorWorld->DemoNetDriver == Connection->GetDriver());
+#else
+ RepFlags.bRepPhysics = Actor->GetReplicatedMovement().bRepPhysics;
+#endif
+ RepFlags.bReplay = bReplay;
UE_LOG(LogNetTraffic, Log, TEXT("Replicate %s, bNetInitial: %d, bNetOwner: %d"), *Actor->GetName(), RepFlags.bNetInitial, RepFlags.bNetOwner);
@@ -381,17 +532,16 @@ int64 USpatialActorChannel::ReplicateActor()
// Replicate Actor and Component properties and RPCs
// ----------------------------------------------------------
- // Epic does this at the net driver level, per connection. See UNetDriver::ServerReplicateActors().
- // However, we have many player controllers sharing one connection, so we do it at the actor level before replication.
- if (APlayerController* PlayerController = Cast(Actor))
- {
- PlayerController->SendClientAdjustment();
- }
+#if USE_NETWORK_PROFILER
+ const uint32 ActorReplicateStartTime = GNetworkProfiler.IsTrackingEnabled() ? FPlatformTime::Cycles() : 0;
+#endif
+
+ const USpatialGDKSettings* SpatialGDKSettings = GetDefault();
// Update SpatialOS position.
if (!bCreatingNewEntity)
{
- if (GetDefault()->bBatchSpatialPositionUpdates)
+ if (SpatialGDKSettings->bBatchSpatialPositionUpdates)
{
Sender->RegisterChannelForPositionUpdate(this);
}
@@ -400,10 +550,9 @@ int64 USpatialActorChannel::ReplicateActor()
UpdateSpatialPositionWithFrequencyCheck();
}
}
-
+
// Update the replicated property change list.
FRepChangelistState* ChangelistState = ActorReplicator->ChangelistMgr->GetRepChangelistState();
- bool bWroteSomethingImportant = false;
#if ENGINE_MINOR_VERSION <= 22
ActorReplicator->ChangelistMgr->Update(ActorReplicator->RepState.Get(), Actor, Connection->Driver->ReplicationFrame, RepFlags, bForceCompareProperties);
@@ -445,6 +594,8 @@ int64 USpatialActorChannel::ReplicateActor()
HandoverChangeState = GetHandoverChangeList(*ActorHandoverShadowData, Actor);
}
+ ReplicationBytesWritten = 0;
+
// If any properties have changed, send a component update.
if (bCreatingNewEntity || RepChanged.Num() > 0 || HandoverChangeState.Num() > 0)
{
@@ -454,22 +605,32 @@ int64 USpatialActorChannel::ReplicateActor()
// so we know what subobjects are relevant for replication when creating the entity.
Actor->ReplicateSubobjects(this, &Bunch, &RepFlags);
- Sender->SendCreateEntityRequest(this);
+ Sender->SendCreateEntityRequest(this, ReplicationBytesWritten);
+
bCreatedEntity = true;
- // Since we've tried to create this Actor in Spatial, we no longer have authority over the actor since it hasn't been delegated to us.
- Actor->Role = ROLE_SimulatedProxy;
- Actor->RemoteRole = ROLE_Authority;
+ // 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)))
+ {
+ 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
{
FRepChangeState RepChangeState = { RepChanged, GetObjectRepLayout(Actor) };
- Sender->SendComponentUpdates(Actor, Info, this, &RepChangeState, &HandoverChangeState);
+
+ Sender->SendComponentUpdates(Actor, Info, this, &RepChangeState, &HandoverChangeState, ReplicationBytesWritten);
+
bInterestDirty = false;
}
- bWroteSomethingImportant = true;
-
if (RepChanged.Num() > 0)
{
SendingRepState->HistoryEnd++;
@@ -511,7 +672,7 @@ int64 USpatialActorChannel::ReplicateActor()
// call back into SpatialActorChannel::ReplicateSubobject, as well as issues a call to UActorComponent::ReplicateSubobjects
// on any of its replicating actor components. This allows the component to replicate any of its subobjects directly via
// the same SpatialActorChannel::ReplicateSubobject.
- bWroteSomethingImportant |= Actor->ReplicateSubobjects(this, &DummyOutBunch, &RepFlags);
+ Actor->ReplicateSubobjects(this, &DummyOutBunch, &RepFlags);
for (auto& SubobjectInfoPair : GetHandoverSubobjects())
{
@@ -530,7 +691,7 @@ int64 USpatialActorChannel::ReplicateActor()
FHandoverChangeState SubobjectHandoverChangeState = GetHandoverChangeList(SubobjectHandoverShadowData->Get(), Subobject);
if (SubobjectHandoverChangeState.Num() > 0)
{
- Sender->SendComponentUpdates(Subobject, SubobjectInfo, this, nullptr, &SubobjectHandoverChangeState);
+ Sender->SendComponentUpdates(Subobject, SubobjectInfo, this, nullptr, &SubobjectHandoverChangeState, ReplicationBytesWritten);
}
}
@@ -540,9 +701,12 @@ int64 USpatialActorChannel::ReplicateActor()
if (!RepComp.Value()->GetWeakObjectPtr().IsValid())
{
FUnrealObjectRef ObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromNetGUID(RepComp.Value().Get().ObjectNetGUID);
+
if (ObjectRef.IsValid())
{
- Sender->SendRemoveComponent(EntityId, NetDriver->ClassInfoManager->GetClassInfoByComponentId(ObjectRef.Offset));
+ OnSubobjectDeleted(ObjectRef, RepComp.Key());
+
+ Sender->SendRemoveComponentForClassInfo(EntityId, NetDriver->ClassInfoManager->GetClassInfoByComponentId(ObjectRef.Offset));
}
RepComp.Value()->CleanUp();
@@ -551,6 +715,45 @@ 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 &&
+ 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)
+ {
+ 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();
+ }
+ else
+ {
+ UE_LOG(LogSpatialActorChannel, Error, TEXT("Load Balancing Strategy returned invalid virtual worker for actor %s"), *Actor->GetName());
+ }
+ }
+
+ if (SpatialGDK::SpatialDebugging* DebuggingInfo = NetDriver->StaticComponentView->GetComponentData(EntityId))
+ {
+ const bool bIsLocked = NetDriver->LockingPolicy->IsLocked(Actor);
+ if (DebuggingInfo->IsLocked != bIsLocked)
+ {
+ DebuggingInfo->IsLocked = bIsLocked;
+ FWorkerComponentUpdate DebuggingUpdate = DebuggingInfo->CreateSpatialDebuggingUpdate();
+ NetDriver->Connection->SendComponentUpdate(EntityId, &DebuggingUpdate);
+ }
+ }
+ }
+#if USE_NETWORK_PROFILER
+ NETWORK_PROFILER(GNetworkProfiler.TrackReplicateActor(Actor, RepFlags, FPlatformTime::Cycles() - ActorReplicateStartTime, Connection));
+#endif
+
// If we evaluated everything, mark LastUpdateTime, even if nothing changed.
LastUpdateTime = Connection->Driver->Time;
@@ -560,7 +763,13 @@ int64 USpatialActorChannel::ReplicateActor()
bForceCompareProperties = false; // Only do this once per frame when set
- return (bWroteSomethingImportant) ? 1 : 0; // TODO: return number of bits written (UNR-664)
+ if (ReplicationBytesWritten > 0)
+ {
+ INC_DWORD_STAT_BY(STAT_NumReplicatedActors, 1);
+ }
+ INC_DWORD_STAT_BY(STAT_NumReplicatedActorBytes, ReplicationBytesWritten);
+
+ return ReplicationBytesWritten * 8;
}
void USpatialActorChannel::DynamicallyAttachSubobject(UObject* Object)
@@ -591,7 +800,7 @@ void USpatialActorChannel::DynamicallyAttachSubobject(UObject* Object)
// Check to see if we already have authority over the subobject to be added
if (NetDriver->StaticComponentView->HasAuthority(EntityId, Info->SchemaComponents[SCHEMA_Data]))
{
- Sender->SendAddComponent(this, Object, *Info);
+ Sender->SendAddComponentForSubobject(this, Object, *Info, ReplicationBytesWritten);
}
else
{
@@ -605,14 +814,14 @@ bool USpatialActorChannel::IsListening() const
{
if (NetDriver->IsServer())
{
- if (SpatialGDK::ClientRPCEndpoint* Endpoint = NetDriver->StaticComponentView->GetComponentData(EntityId))
+ if (SpatialGDK::ClientRPCEndpointLegacy* Endpoint = NetDriver->StaticComponentView->GetComponentData(EntityId))
{
return Endpoint->bReady;
}
}
else
{
- if (SpatialGDK::ServerRPCEndpoint* Endpoint = NetDriver->StaticComponentView->GetComponentData(EntityId))
+ if (SpatialGDK::ServerRPCEndpointLegacy* Endpoint = NetDriver->StaticComponentView->GetComponentData(EntityId))
{
return Endpoint->bReady;
}
@@ -701,9 +910,9 @@ bool USpatialActorChannel::ReplicateSubobject(UObject* Object, const FReplicatio
UE_LOG(LogSpatialActorChannel, Verbose, TEXT("Attempted to replicate an invalid ObjectRef. This may be a dynamic component that couldn't attach: %s"), *Object->GetName());
return false;
}
-
+
const FClassInfo& Info = NetDriver->ClassInfoManager->GetOrCreateClassInfoByObject(Object);
- Sender->SendComponentUpdates(Object, Info, this, &RepChangeState, nullptr);
+ Sender->SendComponentUpdates(Object, Info, this, &RepChangeState, nullptr, ReplicationBytesWritten);
SendingRepState->HistoryEnd++;
}
@@ -729,11 +938,11 @@ bool USpatialActorChannel::ReplicateSubobject(UObject* Obj, FOutBunch& Bunch, co
bool USpatialActorChannel::ReadyForDormancy(bool bSuppressLogs /*= false*/)
{
- // Check Receiver doesn't have any pending operations for this channel
- if (Receiver->IsPendingOpsOnChannel(this))
- {
- return false;
- }
+ // Check Receiver doesn't have any pending operations for this channel
+ if (Receiver->IsPendingOpsOnChannel(*this))
+ {
+ return false;
+ }
// Hasn't been waiting for dormancy long enough allow dormancy, soft attempt to prevent dormancy thrashing
if (FramesTillDormancyAllowed > 0)
@@ -884,7 +1093,7 @@ void USpatialActorChannel::SetChannelActor(AActor* InActor, ESetChannelActorFlag
InitializeHandoverShadowData(HandoverShadowDataMap.Add(Subobject, MakeShared>()).Get(), Subobject);
}
- SavedOwnerWorkerAttribute = SpatialGDK::GetOwnerWorkerAttribute(InActor);
+ SavedConnectionOwningWorkerId = SpatialGDK::GetConnectionOwningWorkerId(InActor);
}
bool USpatialActorChannel::TryResolveActor()
@@ -895,7 +1104,7 @@ bool USpatialActorChannel::TryResolveActor()
{
return false;
}
-
+
// If a Singleton was created, update the GSM with the proper Id.
if (Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton))
{
@@ -922,13 +1131,6 @@ FObjectReplicator* USpatialActorChannel::PreReceiveSpatialUpdate(UObject* Target
FObjectReplicator& Replicator = FindOrCreateReplicator(TargetObject).Get();
TargetObject->PreNetReceive();
-#if ENGINE_MINOR_VERSION <= 22
- Replicator.RepLayout->InitShadowData(Replicator.RepState->StaticBuffer, TargetObject->GetClass(), reinterpret_cast(TargetObject));
-#else
- // TODO: UNR-2372 - Investigate not resetting the ShadowData.
- Replicator.RepState->GetReceivingRepState()->StaticBuffer.Buffer.Empty();
- Replicator.RepLayout->InitRepStateStaticBuffer(Replicator.RepState->GetReceivingRepState()->StaticBuffer, reinterpret_cast(TargetObject));
-#endif
return &Replicator;
}
@@ -945,11 +1147,6 @@ void USpatialActorChannel::PostReceiveSpatialUpdate(UObject* TargetObject, const
#endif
Replicator.CallRepNotifies(false);
-
- if (!TargetObject->IsPendingKill())
- {
- TargetObject->PostRepNotifies();
- }
}
void USpatialActorChannel::OnCreateEntityResponse(const Worker_CreateEntityResponseOp& Op)
@@ -964,7 +1161,7 @@ void USpatialActorChannel::OnCreateEntityResponse(const Worker_CreateEntityRespo
// True if the entity is in the worker's view.
// If this is the case then we know the entity was created and do not need to retry if the request timed-out.
- const bool bEntityIsInView = NetDriver->StaticComponentView->HasComponent(Position::ComponentId, GetEntityId());
+ const bool bEntityIsInView = NetDriver->StaticComponentView->HasComponent(SpatialGDK::Position::ComponentId, GetEntityId());
switch (static_cast(Op.status_code))
{
@@ -982,7 +1179,10 @@ void USpatialActorChannel::OnCreateEntityResponse(const Worker_CreateEntityRespo
{
UE_LOG(LogSpatialActorChannel, Warning, TEXT("Create entity request timed out. Retrying. "
"Actor %s, request id: %d, entity id: %lld, message: %s"), *Actor->GetName(), Op.request_id, Op.entity_id, UTF8_TO_TCHAR(Op.message));
- Sender->SendCreateEntityRequest(this);
+
+ // TODO: UNR-664 - Track these bytes written to use in saturation.
+ uint32 BytesWritten = 0;
+ Sender->SendCreateEntityRequest(this, BytesWritten);
}
break;
case WORKER_STATUS_CODE_APPLICATION_ERROR:
@@ -1033,7 +1233,7 @@ void USpatialActorChannel::UpdateSpatialPosition()
// Check that the Actor has moved sufficiently far to be updated
const float SpatialPositionThresholdSquared = FMath::Square(GetDefault()->PositionDistanceThreshold);
- FVector ActorSpatialPosition = GetActorSpatialPosition(Actor);
+ FVector ActorSpatialPosition = SpatialGDK::GetActorSpatialPosition(Actor);
if (FVector::DistSquared(ActorSpatialPosition, LastPositionSinceUpdate) < SpatialPositionThresholdSquared)
{
return;
@@ -1080,8 +1280,14 @@ void USpatialActorChannel::RemoveRepNotifiesWithUnresolvedObjs(TArrayArrayDim > 1;
+ bool bIsArray = RepLayout.Parents[ObjRef.Value.ParentIndex].Property->ArrayDim > 1 || Cast(Property) != nullptr;
if (bIsSameRepNotify && !bIsArray)
{
UE_LOG(LogSpatialActorChannel, Verbose, TEXT("RepNotify %s on %s ignored due to unresolved Actor"), *Property->GetName(), *Object->GetName());
@@ -1094,13 +1300,60 @@ void USpatialActorChannel::RemoveRepNotifiesWithUnresolvedObjs(TArrayStaticComponentView->HasAuthority(EntityId, SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID));
+ SpatialGDK::NetOwningClientWorker* NetOwningClientWorkerData = NetDriver->StaticComponentView->GetComponentData(EntityId);
+ NetOwningClientWorkerData->WorkerId = NewClientConnectionWorkerId;
+ FWorkerComponentUpdate Update = NetOwningClientWorkerData->CreateNetOwningClientWorkerUpdate();
+ NetDriver->Connection->SendComponentUpdate(EntityId, &Update);
+
+ // Update the EntityACL component (if authoritative).
+ if (NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::ENTITY_ACL_COMPONENT_ID))
+ {
+ Sender->UpdateClientAuthoritativeComponentAclEntries(EntityId, NewClientConnectionWorkerId);
+ }
+
+ SavedConnectionOwningWorkerId = NewClientConnectionWorkerId;
+
+ 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)
+ {
+ Sender->SendInterestBucketComponentChange(EntityId, SavedInterestBucketComponentID, NewInterestBucketComponentId);
+
+ SavedInterestBucketComponentID = NewInterestBucketComponentId;
+
+ bUpdatedThisActor = true;
+ }
+
+ // If we haven't updated this Actor, skip attempting to update child Actors.
+ if (!bUpdatedThisActor)
+ {
+ return;
+ }
+
+ // Changes to NetConnection and InterestBucket for an Actor also affect all descendants which we
+ // need to iterate through.
for (AActor* Child : Actor->Children)
{
Worker_EntityId ChildEntityId = NetDriver->PackageMap->GetEntityIdFromObject(Child);
@@ -1112,26 +1365,55 @@ void USpatialActorChannel::ServerProcessOwnershipChange()
}
}
-void USpatialActorChannel::UpdateEntityACLToNewOwner()
+void USpatialActorChannel::ClientProcessOwnershipChange(bool bNewNetOwned)
{
- FString NewOwnerWorkerAttribute = SpatialGDK::GetOwnerWorkerAttribute(Actor);
-
- if (SavedOwnerWorkerAttribute != NewOwnerWorkerAttribute)
+ SCOPE_CYCLE_COUNTER(STAT_ClientProcessOwnershipChange);
+ if (bNewNetOwned != bNetOwned)
{
- bool bSuccess = Sender->UpdateEntityACLs(EntityId, NewOwnerWorkerAttribute);
+ 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);
+ }
- if (bSuccess)
+ Actor->SetIsOwnedByClient(bNetOwned);
+
+ if (bNetOwned)
+ {
+ Actor->OnClientOwnershipGained();
+ }
+ else
{
- SavedOwnerWorkerAttribute = NewOwnerWorkerAttribute;
+ Actor->OnClientOwnershipLost();
}
}
}
-void USpatialActorChannel::ClientProcessOwnershipChange(bool bNewNetOwned)
+void USpatialActorChannel::OnSubobjectDeleted(const FUnrealObjectRef& ObjectRef, UObject* Object)
{
- if (bNewNetOwned != bNetOwned)
+ CreateSubObjects.Remove(Object);
+
+ Receiver->MoveMappedObjectToUnmapped(ObjectRef);
+ if (FSpatialObjectRepState* SubObjectRefMap = ObjectReferenceMap.Find(Object))
{
- bNetOwned = bNewNetOwned;
- Sender->SendComponentInterestForActor(this, GetEntityId(), bNetOwned);
+ Receiver->CleanupRepStateMap(*SubObjectRefMap);
+ ObjectReferenceMap.Remove(Object);
+ }
+}
+
+void USpatialActorChannel::ResetShadowData(FRepLayout& RepLayout, FRepStateStaticBuffer& StaticBuffer, UObject* TargetObject)
+{
+ 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
+ {
+ RepLayout.CopyProperties(StaticBuffer, reinterpret_cast(TargetObject));
}
}
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp
index 80427e81b7..1f9710a6fe 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp
@@ -2,9 +2,10 @@
#include "EngineClasses/SpatialGameInstance.h"
-#include "Engine/Engine.h"
#include "Engine/NetConnection.h"
#include "GeneralProjectSettings.h"
+#include "Misc/Guid.h"
+
#if WITH_EDITOR
#include "Editor/EditorEngine.h"
#include "Settings/LevelEditorPlaySettings.h"
@@ -12,9 +13,15 @@
#include "EngineClasses/SpatialNetDriver.h"
#include "EngineClasses/SpatialPendingNetGame.h"
+#include "Interop/Connection/SpatialConnectionManager.h"
#include "Interop/Connection/SpatialWorkerConnection.h"
+#include "Interop/GlobalStateManager.h"
+#include "Interop/SpatialStaticComponentView.h"
+#include "Utils/SpatialDebugger.h"
+#include "Utils/SpatialLatencyTracer.h"
#include "Utils/SpatialMetrics.h"
#include "Utils/SpatialMetricsDisplay.h"
+#include "Utils/SpatialStatics.h"
DEFINE_LOG_CATEGORY(LogSpatialGameInstance);
@@ -31,7 +38,7 @@ bool USpatialGameInstance::HasSpatialNetDriver() const
if (NetDriver == nullptr)
{
// If Spatial networking is enabled, override the GameNetDriver with the SpatialNetDriver
- if (GetDefault()->bSpatialNetworking)
+ if (GetDefault()->UsesSpatialNetworking())
{
if (FNetDriverDefinition* DriverDefinition = GEngine->NetDriverDefinitions.FindByPredicate([](const FNetDriverDefinition& CurDef)
{
@@ -57,7 +64,7 @@ bool USpatialGameInstance::HasSpatialNetDriver() const
}
}
- if (GetDefault()->bSpatialNetworking && !bHasSpatialNetDriver)
+ if (GetDefault()->UsesSpatialNetworking() && !bHasSpatialNetDriver)
{
UE_LOG(LogSpatialGameInstance, Error, TEXT("Could not find SpatialNetDriver even though Spatial networking is switched on! "
"Please make sure you set up the net driver definitions as specified in the porting "
@@ -67,18 +74,32 @@ bool USpatialGameInstance::HasSpatialNetDriver() const
return bHasSpatialNetDriver;
}
-void USpatialGameInstance::CreateNewSpatialWorkerConnection()
+void USpatialGameInstance::CreateNewSpatialConnectionManager()
{
- SpatialConnection = NewObject(this);
- SpatialConnection->Init(this);
+ SpatialConnectionManager = NewObject(this);
+
+ GlobalStateManager = NewObject();
+ StaticComponentView = NewObject();
}
-void USpatialGameInstance::DestroySpatialWorkerConnection()
+void USpatialGameInstance::DestroySpatialConnectionManager()
{
- if (SpatialConnection != nullptr)
+ if (SpatialConnectionManager != nullptr)
+ {
+ SpatialConnectionManager->DestroyConnection();
+ SpatialConnectionManager = nullptr;
+ }
+
+ if (GlobalStateManager != nullptr)
+ {
+ GlobalStateManager->ConditionalBeginDestroy();
+ GlobalStateManager = nullptr;
+ }
+
+ if (StaticComponentView != nullptr)
{
- SpatialConnection->DestroyConnection();
- SpatialConnection = nullptr;
+ StaticComponentView->ConditionalBeginDestroy();
+ StaticComponentView = nullptr;
}
}
@@ -88,8 +109,16 @@ FGameInstancePIEResult USpatialGameInstance::StartPlayInEditorGameInstance(ULoca
if (HasSpatialNetDriver())
{
// If we are using spatial networking then prepare a spatial connection.
- CreateNewSpatialWorkerConnection();
+ 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));
+ SpatialLatencyTracer->SetWorkerId(WorkerName);
}
+#endif
return Super::StartPlayInEditorGameInstance(LocalPlayer, Params);
}
@@ -100,27 +129,32 @@ void USpatialGameInstance::TryConnectToSpatial()
if (HasSpatialNetDriver())
{
// If we are using spatial networking then prepare a spatial connection.
- CreateNewSpatialWorkerConnection();
+ CreateNewSpatialConnectionManager();
// 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.
- // If a developer wants to use the Launcher and NOT automatically connect they will have to set the `PreventAutoConnectWithLocator` flag to true.
- if (!bPreventAutoConnectWithLocator)
+
+ // Initialize a locator configuration which will parse command line arguments.
+ FLocatorConfig LocatorConfig;
+ if (LocatorConfig.TryLoadCommandLineArgs())
{
- // Initialize a locator configuration which will parse command line arguments.
- FLocatorConfig LocatorConfig;
- if (!LocatorConfig.LoginToken.IsEmpty())
- {
- // Modify the commandline args to have a Host IP to force a NetDriver to be used.
- const TCHAR* CommandLineArgs = FCommandLine::Get();
+ // Modify the commandline args to have a Host IP to force a NetDriver to be used.
+ const TCHAR* CommandLineArgs = FCommandLine::Get();
- FString NewCommandLineArgs = LocatorConfig.LocatorHost + TEXT(" ");
- NewCommandLineArgs.Append(FString(CommandLineArgs));
+ FString NewCommandLineArgs = LocatorConfig.LocatorHost + TEXT(" ");
+ NewCommandLineArgs.Append(FString(CommandLineArgs));
- FCommandLine::Set(*NewCommandLineArgs);
- }
+ 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()
@@ -150,21 +184,101 @@ bool USpatialGameInstance::ProcessConsoleExec(const TCHAR* Cmd, FOutputDevice& A
{
return true;
}
+
+ if (NetDriver->SpatialDebugger && NetDriver->SpatialDebugger->ProcessConsoleExec(Cmd, Ar, Executor))
+ {
+ return true;
+ }
}
}
return false;
}
+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."));
+}
+
void USpatialGameInstance::HandleOnConnected()
{
- UE_LOG(LogSpatialGameInstance, Log, TEXT("Succesfully connected to SpatialOS"));
- SpatialWorkerId = SpatialConnection->GetWorkerId();
+ UE_LOG(LogSpatialGameInstance, Log, TEXT("Successfully connected to SpatialOS"));
+ SpatialWorkerId = SpatialConnectionManager->GetWorkerConnection()->GetWorkerId();
+#if TRACE_LIB_ACTIVE
+ SpatialLatencyTracer->SetWorkerId(SpatialWorkerId);
+
+ USpatialWorkerConnection* WorkerConnection = SpatialConnectionManager->GetWorkerConnection();
+ WorkerConnection->OnEnqueueMessage.AddUObject(SpatialLatencyTracer, &USpatialLatencyTracer::OnEnqueueMessage);
+ WorkerConnection->OnDequeueMessage.AddUObject(SpatialLatencyTracer, &USpatialLatencyTracer::OnDequeueMessage);
+#endif
OnConnected.Broadcast();
}
void USpatialGameInstance::HandleOnConnectionFailed(const FString& Reason)
{
UE_LOG(LogSpatialGameInstance, Error, TEXT("Could not connect to SpatialOS. Reason: %s"), *Reason);
+#if TRACE_LIB_ACTIVE
+ SpatialLatencyTracer->ResetWorkerId();
+#endif
OnConnectionFailed.Broadcast(Reason);
}
+
+void USpatialGameInstance::OnLevelInitializedNetworkActors(ULevel* LoadedLevel, UWorld* OwningWorld)
+{
+ const FString WorkerType = GetSpatialWorkerType().ToString();
+
+ if (OwningWorld != GetWorld()
+ || !OwningWorld->IsServer()
+ || !GetDefault()->UsesSpatialNetworking()
+ || (OwningWorld->WorldType != EWorldType::PIE
+ && OwningWorld->WorldType != EWorldType::Game
+ && OwningWorld->WorldType != EWorldType::GamePreview))
+ {
+ // We only want to do something if this is the correct process and we are on a spatial server, and we are in-game
+ return;
+ }
+
+ for (int32 ActorIndex = 0; ActorIndex < LoadedLevel->Actors.Num(); ActorIndex++)
+ {
+ AActor* Actor = LoadedLevel->Actors[ActorIndex];
+ if (Actor == nullptr)
+ {
+ continue;
+ }
+
+ if (USpatialStatics::IsSpatialOffloadingEnabled())
+ {
+ if (!USpatialStatics::IsActorGroupOwnerForActor(Actor))
+ {
+ if (!Actor->bNetLoadOnNonAuthServer)
+ {
+ Actor->Destroy(true);
+ }
+ else
+ {
+ UE_LOG(LogSpatialGameInstance, Verbose, TEXT("WorkerType %s is not the actor group owner of startup actor %s, exchanging Roles"), *WorkerType, *GetPathNameSafe(Actor));
+ ENetRole Temp = Actor->Role;
+ Actor->Role = Actor->RemoteRole;
+ Actor->RemoteRole = Temp;
+ }
+ }
+ }
+ else
+ {
+ if (Actor->GetIsReplicated())
+ {
+ // Always wait for authority to be delegated down from SpatialOS, if not using offloading
+ Actor->Role = ROLE_SimulatedProxy;
+ Actor->RemoteRole = ROLE_Authority;
+ }
+ }
+ }
+}
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialLoadBalanceEnforcer.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialLoadBalanceEnforcer.cpp
new file mode 100644
index 0000000000..cd03b575d5
--- /dev/null
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialLoadBalanceEnforcer.cpp
@@ -0,0 +1,247 @@
+// Copyright (c) Improbable Worlds Ltd, All Rights Reserved
+
+#include "EngineClasses/SpatialLoadBalanceEnforcer.h"
+#include "EngineClasses/SpatialVirtualWorkerTranslator.h"
+#include "Schema/AuthorityIntent.h"
+#include "Schema/Component.h"
+#include "Schema/ComponentPresence.h"
+#include "Schema/NetOwningClientWorker.h"
+#include "SpatialCommonTypes.h"
+#include "SpatialGDKSettings.h"
+
+DEFINE_LOG_CATEGORY(LogSpatialLoadBalanceEnforcer);
+
+using namespace SpatialGDK;
+
+SpatialLoadBalanceEnforcer::SpatialLoadBalanceEnforcer(const PhysicalWorkerName& InWorkerId, const USpatialStaticComponentView* InStaticComponentView, const SpatialVirtualWorkerTranslator* InVirtualWorkerTranslator)
+ : WorkerId(InWorkerId)
+ , StaticComponentView(InStaticComponentView)
+ , VirtualWorkerTranslator(InVirtualWorkerTranslator)
+{
+ check(InStaticComponentView != nullptr);
+ check(InVirtualWorkerTranslator != nullptr);
+}
+
+void SpatialLoadBalanceEnforcer::OnLoadBalancingComponentAdded(const Worker_AddComponentOp& Op)
+{
+ check(HandlesComponent(Op.data.component_id));
+
+ MaybeQueueAclAssignmentRequest(Op.entity_id);
+}
+
+void SpatialLoadBalanceEnforcer::OnLoadBalancingComponentUpdated(const Worker_ComponentUpdateOp& Op)
+{
+ check(HandlesComponent(Op.update.component_id));
+
+ MaybeQueueAclAssignmentRequest(Op.entity_id);
+}
+
+void SpatialLoadBalanceEnforcer::OnLoadBalancingComponentRemoved(const Worker_RemoveComponentOp& Op)
+{
+ check(HandlesComponent(Op.component_id));
+
+ if (AclAssignmentRequestIsQueued(Op.entity_id))
+ {
+ UE_LOG(LogSpatialLoadBalanceEnforcer, Log,
+ TEXT("Component %d for entity %lld removed. Can no longer enforce the previous request for this entity."),
+ Op.component_id, Op.entity_id);
+ AclWriteAuthAssignmentRequests.Remove(Op.entity_id);
+ }
+}
+
+void SpatialLoadBalanceEnforcer::OnEntityRemoved(const Worker_RemoveEntityOp& Op)
+{
+ if (AclAssignmentRequestIsQueued(Op.entity_id))
+ {
+ UE_LOG(LogSpatialLoadBalanceEnforcer, Log, TEXT("Entity %lld removed. Can no longer enforce the previous request for this entity."),
+ Op.entity_id);
+ AclWriteAuthAssignmentRequests.Remove(Op.entity_id);
+ }
+}
+
+void SpatialLoadBalanceEnforcer::OnAclAuthorityChanged(const Worker_AuthorityChangeOp& AuthOp)
+{
+ // This class should only be informed of ACL authority changes.
+ check(AuthOp.component_id == SpatialConstants::ENTITY_ACL_COMPONENT_ID);
+
+ if (AuthOp.authority != WORKER_AUTHORITY_AUTHORITATIVE)
+ {
+ if (AclAssignmentRequestIsQueued(AuthOp.entity_id))
+ {
+ UE_LOG(LogSpatialLoadBalanceEnforcer, Log,
+ TEXT("ACL authority lost for entity %lld. Can no longer enforce the previous request for this entity."),
+ AuthOp.entity_id);
+ AclWriteAuthAssignmentRequests.Remove(AuthOp.entity_id);
+ }
+ return;
+ }
+
+ MaybeQueueAclAssignmentRequest(AuthOp.entity_id);
+}
+
+// MaybeQueueAclAssignmentRequest is called from three places.
+// 1) AuthorityIntent change - Intent is not authoritative on this worker - ACL is authoritative on this worker.
+// (another worker changed the intent, but this worker is responsible for the ACL, so update it.)
+// 2) ACL change - Intent may be anything - ACL just became authoritative on this worker.
+// (this worker just became responsible, so check to make sure intent and ACL agree.)
+// 3) AuthorityIntent change - Intent is authoritative on this worker but no longer assigned to this worker - ACL is authoritative on this worker.
+// (this worker had responsibility for both and is giving up authority.)
+// Queuing an ACL assignment request may not occur if the assignment is the same as before, or if the request is already queued,
+// or if we don't meet the predicate required to enforce the assignment.
+void SpatialLoadBalanceEnforcer::MaybeQueueAclAssignmentRequest(const Worker_EntityId EntityId)
+{
+ if (!CanEnforce(EntityId))
+ {
+ return;
+ }
+
+ const SpatialGDK::AuthorityIntent* AuthorityIntentComponent = StaticComponentView->GetComponentData(EntityId);
+ const PhysicalWorkerName* OwningWorkerId = VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(AuthorityIntentComponent->VirtualWorkerId);
+
+ check(OwningWorkerId != nullptr);
+ if (OwningWorkerId == nullptr)
+ {
+ UE_LOG(LogSpatialLoadBalanceEnforcer, Error, TEXT("Couldn't find mapped worker for entity %lld. This shouldn't happen! Virtual worker ID: %d"),
+ EntityId, AuthorityIntentComponent->VirtualWorkerId);
+ return;
+ }
+
+ if (*OwningWorkerId == WorkerId && StaticComponentView->HasAuthority(EntityId, SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID))
+ {
+ UE_LOG(LogSpatialLoadBalanceEnforcer, Verbose, TEXT("No need to queue newly authoritative entity because this worker is already authoritative. Entity: %lld. Worker: %s."),
+ EntityId, *WorkerId);
+ return;
+ }
+
+ if (AclAssignmentRequestIsQueued(EntityId))
+ {
+ UE_LOG(LogSpatialLoadBalanceEnforcer, Verbose, TEXT("Avoiding queueing a duplicate ACL assignment request. Entity: %lld. Worker: %s."),
+ EntityId, *WorkerId);
+ return;
+ }
+
+ QueueAclAssignmentRequest(EntityId);
+}
+
+bool SpatialLoadBalanceEnforcer::AclAssignmentRequestIsQueued(const Worker_EntityId EntityId) const
+{
+ return AclWriteAuthAssignmentRequests.Contains(EntityId);
+}
+
+TArray SpatialLoadBalanceEnforcer::ProcessQueuedAclAssignmentRequests()
+{
+ TArray PendingRequests;
+
+ TArray CompletedRequests;
+ CompletedRequests.Reserve(AclWriteAuthAssignmentRequests.Num());
+
+ for (Worker_EntityId EntityId : AclWriteAuthAssignmentRequests)
+ {
+ const SpatialGDK::AuthorityIntent* AuthorityIntentComponent = StaticComponentView->GetComponentData(EntityId);
+ if (AuthorityIntentComponent == nullptr)
+ {
+ // This happens if the authority intent component is removed in the same tick as a request is queued, but the request was not removed from the queue - shouldn't happen.
+ UE_LOG(LogSpatialLoadBalanceEnforcer, Error, TEXT("Cannot process entity as AuthIntent component has been removed since the request was queued. EntityId: %lld"), EntityId);
+ CompletedRequests.Add(EntityId);
+ continue;
+ }
+
+ const SpatialGDK::NetOwningClientWorker* NetOwningClientWorkerComponent = StaticComponentView->GetComponentData(EntityId);
+ if (NetOwningClientWorkerComponent == nullptr)
+ {
+ // This happens if the NetOwningClientWorker component is removed in the same tick as a request is queued, but the request was not removed from the queue - shouldn't happen.
+ UE_LOG(LogSpatialLoadBalanceEnforcer, Error, TEXT("Cannot process entity as NetOwningClientWorker component has been removed since the request was queued. EntityId: %lld"), EntityId);
+ CompletedRequests.Add(EntityId);
+ continue;
+ }
+
+ const SpatialGDK::ComponentPresence* ComponentPresenceComponent = StaticComponentView->GetComponentData(EntityId);
+ if (ComponentPresenceComponent == nullptr)
+ {
+ // This happens if the ComponentPresence component is removed in the same tick as a request is queued, but the request was not removed from the queue - shouldn't happen.
+ UE_LOG(LogSpatialLoadBalanceEnforcer, Error, TEXT("Cannot process entity as ComponentPresence component has been removed since the request was queued. EntityId: %lld"), EntityId);
+ CompletedRequests.Add(EntityId);
+ continue;
+ }
+
+ if (AuthorityIntentComponent->VirtualWorkerId == SpatialConstants::INVALID_VIRTUAL_WORKER_ID)
+ {
+ UE_LOG(LogSpatialLoadBalanceEnforcer, Warning, TEXT("Entity with invalid virtual worker ID assignment will not be processed. EntityId: %lld. This should not happen - investigate if you see this warning."), EntityId);
+ CompletedRequests.Add(EntityId);
+ continue;
+ }
+
+ check(VirtualWorkerTranslator != nullptr);
+ const PhysicalWorkerName* DestinationWorkerId = VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(AuthorityIntentComponent->VirtualWorkerId);
+ if (DestinationWorkerId == nullptr)
+ {
+ UE_LOG(LogSpatialLoadBalanceEnforcer, Error, TEXT("This worker is not assigned a virtual worker. This shouldn't happen! Worker: %s"), *WorkerId);
+ continue;
+ }
+
+ if (!StaticComponentView->HasAuthority(EntityId, SpatialConstants::ENTITY_ACL_COMPONENT_ID))
+ {
+ UE_LOG(LogSpatialLoadBalanceEnforcer, Log, TEXT("Failed to update the EntityACL to match the authority intent; this worker lost authority over the EntityACL since the request was queued."
+ " Source worker ID: %s. Entity ID %lld. Desination worker ID: %s."), *WorkerId, EntityId, **DestinationWorkerId);
+ CompletedRequests.Add(EntityId);
+ continue;
+ }
+
+ TArray ComponentIds;
+
+ EntityAcl* Acl = StaticComponentView->GetComponentData(EntityId);
+ Acl->ComponentWriteAcl.GetKeys(ComponentIds);
+
+ // Ensure that every component ID in ComponentPresence is set in the write ACL.
+ for (const auto& RequiredComponentId : ComponentPresenceComponent->ComponentList)
+ {
+ ComponentIds.AddUnique(RequiredComponentId);
+ }
+
+ // Get the client worker ID net-owning this Actor from the NetOwningClientWorker.
+ PhysicalWorkerName PossessingClientId = NetOwningClientWorkerComponent->WorkerId.IsSet() ?
+ NetOwningClientWorkerComponent->WorkerId.GetValue() :
+ FString();
+
+ PendingRequests.Push(
+ AclWriteAuthorityRequest{
+ EntityId,
+ *DestinationWorkerId,
+ Acl->ReadAcl,
+ { { PossessingClientId } },
+ ComponentIds
+ });
+
+ CompletedRequests.Add(EntityId);
+ }
+
+ AclWriteAuthAssignmentRequests.RemoveAll([CompletedRequests](const Worker_EntityId& EntityId) { return CompletedRequests.Contains(EntityId); });
+
+ return PendingRequests;
+}
+
+void SpatialLoadBalanceEnforcer::QueueAclAssignmentRequest(const Worker_EntityId EntityId)
+{
+ UE_LOG(LogSpatialLoadBalanceEnforcer, Verbose, TEXT("Queueing ACL assignment request for entity %lld on worker %s."), EntityId, *WorkerId);
+ AclWriteAuthAssignmentRequests.Add(EntityId);
+}
+
+bool SpatialLoadBalanceEnforcer::CanEnforce(Worker_EntityId EntityId) const
+{
+ // We need to be able to see the ACL component
+ return StaticComponentView->HasComponent(EntityId, SpatialConstants::ENTITY_ACL_COMPONENT_ID)
+ // and the authority intent component
+ && StaticComponentView->HasComponent(EntityId, SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID)
+ // and the component presence component
+ && StaticComponentView->HasComponent(EntityId, SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID)
+ // and we have to be able to write to the ACL component.
+ && StaticComponentView->HasAuthority(EntityId, SpatialConstants::ENTITY_ACL_COMPONENT_ID);
+}
+
+bool SpatialLoadBalanceEnforcer::HandlesComponent(Worker_ComponentId ComponentId) const
+{
+ return ComponentId == SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID
+ || ComponentId == SpatialConstants::ENTITY_ACL_COMPONENT_ID
+ || ComponentId == SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID
+ || ComponentId == SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID;
+}
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitReader.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitReader.cpp
index fdb7e9e492..d27974e684 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitReader.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitReader.cpp
@@ -9,8 +9,9 @@
DEFINE_LOG_CATEGORY(LogSpatialNetBitReader);
-FSpatialNetBitReader::FSpatialNetBitReader(USpatialPackageMapClient* InPackageMap, uint8* Source, int64 CountBits, TSet& InUnresolvedRefs)
+FSpatialNetBitReader::FSpatialNetBitReader(USpatialPackageMapClient* InPackageMap, uint8* Source, int64 CountBits, TSet& InDynamicRefs, TSet& InUnresolvedRefs)
: FNetBitReader(InPackageMap, Source, CountBits)
+ , DynamicRefs(InDynamicRefs)
, UnresolvedRefs(InUnresolvedRefs) {}
void FSpatialNetBitReader::DeserializeObjectRef(FUnrealObjectRef& ObjectRef)
@@ -42,20 +43,31 @@ void FSpatialNetBitReader::DeserializeObjectRef(FUnrealObjectRef& ObjectRef)
SerializeBits(&ObjectRef.bUseSingletonClassPath, 1);
}
-FArchive& FSpatialNetBitReader::operator<<(UObject*& Value)
+UObject* FSpatialNetBitReader::ReadObject(bool& bUnresolved)
{
FUnrealObjectRef ObjectRef;
DeserializeObjectRef(ObjectRef);
check(ObjectRef != FUnrealObjectRef::UNRESOLVED_OBJECT_REF);
- bool bUnresolved = false;
- Value = FUnrealObjectRef::ToObjectPtr(ObjectRef, Cast(PackageMap), bUnresolved);
+ UObject* Value = FUnrealObjectRef::ToObjectPtr(ObjectRef, Cast(PackageMap), bUnresolved);
if (bUnresolved)
{
UnresolvedRefs.Add(ObjectRef);
}
+ else if (Value && !Value->IsFullNameStableForNetworking())
+ {
+ DynamicRefs.Add(ObjectRef);
+ }
+
+ return Value;
+}
+
+FArchive& FSpatialNetBitReader::operator<<(UObject*& Value)
+{
+ bool bUnresolved = false;
+ Value = ReadObject(bUnresolved);
return *this;
}
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetConnection.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetConnection.cpp
index 06204e69a9..3e39bb6686 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetConnection.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetConnection.cpp
@@ -2,22 +2,24 @@
#include "EngineClasses/SpatialNetConnection.h"
-#include "TimerManager.h"
-
#include "EngineClasses/SpatialNetDriver.h"
#include "EngineClasses/SpatialPackageMapClient.h"
-#include "GameFramework/PlayerController.h"
-#include "GameFramework/Pawn.h"
#include "Interop/Connection/SpatialWorkerConnection.h"
#include "Interop/SpatialReceiver.h"
#include "Interop/SpatialSender.h"
#include "SpatialConstants.h"
#include "SpatialGDKSettings.h"
+#include "GameFramework/PlayerController.h"
+#include "GameFramework/Pawn.h"
+#include "TimerManager.h"
+
#include
DEFINE_LOG_CATEGORY(LogSpatialNetConnection);
+DECLARE_CYCLE_STAT(TEXT("UpdateLevelVisibility"), STAT_SpatialNetConnectionUpdateLevelVisibility, STATGROUP_SpatialNet);
+
USpatialNetConnection::USpatialNetConnection(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
, PlayerControllerEntity(SpatialConstants::INVALID_ENTITY_ID)
@@ -28,10 +30,20 @@ USpatialNetConnection::USpatialNetConnection(const FObjectInitializer& ObjectIni
void USpatialNetConnection::BeginDestroy()
{
DisableHeartbeat();
-
+
Super::BeginDestroy();
}
+void USpatialNetConnection::CleanUp()
+{
+ if (USpatialNetDriver* SpatialNetDriver = Cast(Driver))
+ {
+ SpatialNetDriver->CleanUpClientConnection(this);
+ }
+
+ Super::CleanUp();
+}
+
void USpatialNetConnection::InitBase(UNetDriver* InDriver, class FSocket* InSocket, const FURL& InURL, EConnectionState InState, int32 InMaxPacket /*= 0*/, int32 InPacketOverhead /*= 0*/)
{
Super::InitBase(InDriver, InSocket, InURL, InState, InMaxPacket, InPacketOverhead);
@@ -69,14 +81,25 @@ int32 USpatialNetConnection::IsNetReady(bool Saturate)
return true;
}
+#if ENGINE_MINOR_VERSION <= 23
void USpatialNetConnection::UpdateLevelVisibility(const FName& PackageName, bool bIsVisible)
+#else
+void USpatialNetConnection::UpdateLevelVisibility(const struct FUpdateLevelVisibilityLevelInfo& LevelVisibility)
+#endif
{
+ SCOPE_CYCLE_COUNTER(STAT_SpatialNetConnectionUpdateLevelVisibility);
+
+#if ENGINE_MINOR_VERSION <= 23
UNetConnection::UpdateLevelVisibility(PackageName, bIsVisible);
+#else
+ UNetConnection::UpdateLevelVisibility(LevelVisibility);
+#endif
// We want to update our interest as fast as possible
// So we send an Interest update immediately.
- UpdateActorInterest(Cast(PlayerController));
- UpdateActorInterest(Cast(PlayerController->GetPawn()));
+
+ USpatialSender* Sender = Cast(Driver)->Sender;
+ Sender->UpdateInterestComponent(Cast(PlayerController));
}
void USpatialNetConnection::FlushDormancy(AActor* Actor)
@@ -92,22 +115,6 @@ void USpatialNetConnection::FlushDormancy(AActor* Actor)
}
}
-void USpatialNetConnection::UpdateActorInterest(AActor* Actor)
-{
- if (Actor == nullptr)
- {
- return;
- }
-
- USpatialSender* Sender = Cast(Driver)->Sender;
-
- Sender->UpdateInterestComponent(Actor);
- for (const auto& Child : Actor->Children)
- {
- UpdateActorInterest(Child);
- }
-}
-
void USpatialNetConnection::ClientNotifyClientHasQuit()
{
if (PlayerControllerEntity != SpatialConstants::INVALID_ENTITY_ID)
@@ -118,7 +125,7 @@ void USpatialNetConnection::ClientNotifyClientHasQuit()
return;
}
- Worker_ComponentUpdate Update = {};
+ FWorkerComponentUpdate Update = {};
Update.component_id = SpatialConstants::HEARTBEAT_COMPONENT_ID;
Update.schema_type = Schema_CreateComponentUpdate();
Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type);
@@ -135,6 +142,8 @@ void USpatialNetConnection::ClientNotifyClientHasQuit()
void USpatialNetConnection::InitHeartbeat(FTimerManager* InTimerManager, Worker_EntityId InPlayerControllerEntity)
{
+ UE_LOG(LogSpatialNetConnection, Log, TEXT("Init Heartbeat component: NetConnection %s, PlayerController entity %lld"), *GetName(), InPlayerControllerEntity);
+
checkf(PlayerControllerEntity == SpatialConstants::INVALID_ENTITY_ID, TEXT("InitHeartbeat: PlayerControllerEntity already set: %lld. New entity: %lld"), PlayerControllerEntity, InPlayerControllerEntity);
PlayerControllerEntity = InPlayerControllerEntity;
TimerManager = InTimerManager;
@@ -151,6 +160,11 @@ void USpatialNetConnection::InitHeartbeat(FTimerManager* InTimerManager, Worker_
void USpatialNetConnection::SetHeartbeatTimeoutTimer()
{
+ float Timeout = GetDefault()->HeartbeatTimeoutSeconds;
+#if WITH_EDITOR
+ Timeout = GetDefault()->HeartbeatTimeoutWithEditorSeconds;
+#endif
+
TimerManager->SetTimer(HeartbeatTimer, [WeakThis = TWeakObjectPtr(this)]()
{
if (USpatialNetConnection* Connection = WeakThis.Get())
@@ -158,7 +172,7 @@ void USpatialNetConnection::SetHeartbeatTimeoutTimer()
// This client timed out. Disconnect it and trigger OnDisconnected logic.
Connection->CleanUp();
}
- }, GetDefault()->HeartbeatTimeoutSeconds, false);
+ }, Timeout, false);
}
void USpatialNetConnection::SetHeartbeatEventTimer()
@@ -167,7 +181,7 @@ void USpatialNetConnection::SetHeartbeatEventTimer()
{
if (USpatialNetConnection* Connection = WeakThis.Get())
{
- Worker_ComponentUpdate ComponentUpdate = {};
+ FWorkerComponentUpdate ComponentUpdate = {};
ComponentUpdate.component_id = SpatialConstants::HEARTBEAT_COMPONENT_ID;
ComponentUpdate.schema_type = Schema_CreateComponentUpdate();
@@ -175,7 +189,7 @@ void USpatialNetConnection::SetHeartbeatEventTimer()
Schema_AddObject(EventsObject, SpatialConstants::HEARTBEAT_EVENT_ID);
USpatialWorkerConnection* WorkerConnection = Cast(Connection->Driver)->Connection;
- if (WorkerConnection->IsConnected())
+ if (WorkerConnection != nullptr)
{
WorkerConnection->SendComponentUpdate(Connection->PlayerControllerEntity, &ComponentUpdate);
}
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp
index 8b9a7dd5d6..105a2168b3 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp
@@ -10,6 +10,7 @@
#include "EngineGlobals.h"
#include "GameFramework/GameModeBase.h"
#include "GameFramework/GameNetworkManager.h"
+#include "Misc/MessageDialog.h"
#include "Net/DataReplication.h"
#include "Net/RepLayout.h"
#include "SocketSubsystem.h"
@@ -20,22 +21,27 @@
#include "EngineClasses/SpatialNetConnection.h"
#include "EngineClasses/SpatialPackageMapClient.h"
#include "EngineClasses/SpatialPendingNetGame.h"
+#include "EngineClasses/SpatialWorldSettings.h"
+#include "Interop/Connection/SpatialConnectionManager.h"
#include "Interop/Connection/SpatialWorkerConnection.h"
#include "Interop/GlobalStateManager.h"
-#include "Interop/SnapshotManager.h"
#include "Interop/SpatialClassInfoManager.h"
-#include "Interop/SpatialDispatcher.h"
#include "Interop/SpatialPlayerSpawner.h"
#include "Interop/SpatialReceiver.h"
#include "Interop/SpatialSender.h"
-#include "Schema/AlwaysRelevant.h"
+#include "Interop/SpatialWorkerFlags.h"
+#include "LoadBalancing/AbstractLBStrategy.h"
+#include "LoadBalancing/GridBasedLBStrategy.h"
+#include "LoadBalancing/OwnershipLockingPolicy.h"
#include "SpatialConstants.h"
#include "SpatialGDKSettings.h"
-#include "Utils/ActorGroupManager.h"
+#include "Utils/ComponentFactory.h"
#include "Utils/EntityPool.h"
#include "Utils/ErrorCodeRemapping.h"
#include "Utils/InterestFactory.h"
#include "Utils/OpUtils.h"
+#include "Utils/SpatialActorGroupManager.h"
+#include "Utils/SpatialDebugger.h"
#include "Utils/SpatialMetrics.h"
#include "Utils/SpatialMetricsDisplay.h"
#include "Utils/SpatialStatics.h"
@@ -45,23 +51,34 @@
#include "SpatialGDKServicesModule.h"
#endif
+using SpatialGDK::ComponentFactory;
+using SpatialGDK::FindFirstOpOfType;
+using SpatialGDK::FindFirstOpOfTypeForComponent;
+using SpatialGDK::InterestFactory;
+using SpatialGDK::RPCPayload;
+
DEFINE_LOG_CATEGORY(LogSpatialOSNetDriver);
DECLARE_CYCLE_STAT(TEXT("ServerReplicateActors"), STAT_SpatialServerReplicateActors, STATGROUP_SpatialNet);
DECLARE_CYCLE_STAT(TEXT("ProcessPrioritizedActors"), STAT_SpatialProcessPrioritizedActors, STATGROUP_SpatialNet);
DECLARE_CYCLE_STAT(TEXT("PrioritizeActors"), STAT_SpatialPrioritizeActors, STATGROUP_SpatialNet);
+DECLARE_CYCLE_STAT(TEXT("ProcessOps"), STAT_SpatialProcessOps, STATGROUP_SpatialNet);
+DECLARE_CYCLE_STAT(TEXT("UpdateAuthority"), STAT_SpatialUpdateAuthority, STATGROUP_SpatialNet);
DEFINE_STAT(STAT_SpatialConsiderList);
DEFINE_STAT(STAT_SpatialActorsRelevant);
DEFINE_STAT(STAT_SpatialActorsChanged);
USpatialNetDriver::USpatialNetDriver(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
+ , LoadBalanceStrategy(nullptr)
+ , LoadBalanceEnforcer(nullptr)
, bAuthoritativeDestruction(true)
, bConnectAsClient(false)
, bPersistSpatialConnection(true)
- , bWaitingForAcceptingPlayersToSpawn(false)
+ , bWaitingToSpawn(false)
, bIsReadyToStart(false)
, bMapLoaded(false)
+ , SessionId(0)
, NextRPCIndex(0)
, TimeWhenPositionLastUpdated(0.f)
{
@@ -81,15 +98,17 @@ bool USpatialNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, c
return false;
}
- // This is a temporary measure until we can look into replication graph support, required due to UNR-832
- checkf(!GetReplicationDriver(), TEXT("Replication Driver not supported, please remove it from config"));
-
bConnectAsClient = bInitAsClient;
FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &USpatialNetDriver::OnMapLoaded);
FWorldDelegates::LevelAddedToWorld.AddUObject(this, &USpatialNetDriver::OnLevelAddedToWorld);
+ if (GetWorld() != nullptr)
+ {
+ GetWorld()->AddOnActorSpawnedHandler(FOnActorSpawned::FDelegate::CreateUObject(this, &USpatialNetDriver::OnActorSpawned));
+ }
+
// Make absolutely sure that the actor channel that we are using is our Spatial actor channel
// Copied from what the Engine does with UActorChannel
FChannelDefinition SpatialChannelDefinition{};
@@ -101,8 +120,8 @@ bool USpatialNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, c
ChannelDefinitions[CHTYPE_Actor] = SpatialChannelDefinition;
ChannelDefinitionMap[NAME_Actor] = SpatialChannelDefinition;
- // Extract the snapshot to load (if any) from the map URL so that once we are connected to a deployment we can load that snapshot into the Spatial deployment.
- SnapshotToLoad = URL.GetOption(*SpatialConstants::SnapshotURLOption, TEXT(""));
+ // If no sessionId exists in the URL options, SessionId member will be set to 0.
+ SessionId = FCString::Atoi(URL.GetOption(*SpatialConstants::SpatialSessionIdURLOption, TEXT("0")));
// We do this here straight away to trigger LoadMap.
if (bInitAsClient)
@@ -116,9 +135,7 @@ bool USpatialNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, c
bPersistSpatialConnection = true;
}
- // Initialize ActorGroupManager as it is a depdency of ClassInfoManager (see below)
- ActorGroupManager = NewObject();
- ActorGroupManager->Init();
+ ActorGroupManager = GetGameInstance()->ActorGroupManager.Get();
// Initialize ClassInfoManager here because it needs to load SchemaDatabase.
// We shouldn't do that in CreateAndInitializeCoreClasses because it is called
@@ -134,11 +151,6 @@ bool USpatialNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, c
return false;
}
- if (!bInitAsClient)
- {
- GatherClientInterestDistances();
- }
-
#if WITH_EDITOR
PlayInEditorID = GPlayInEditorID;
@@ -207,65 +219,74 @@ void USpatialNetDriver::InitiateConnectionToSpatialOS(const FURL& URL)
return;
}
- if (!bPersistSpatialConnection)
+ if (bConnectAsClient)
{
- // Destroy the old connection
- GameInstance->DestroySpatialWorkerConnection();
-
- // Create a new SpatialWorkerConnection in the SpatialGameInstance.
- GameInstance->CreateNewSpatialWorkerConnection();
+ bPersistSpatialConnection = URL.HasOption(*SpatialConstants::ClientsStayConnectedURLOption);
}
- Connection = GameInstance->GetSpatialWorkerConnection();
- Connection->OnConnectedCallback.BindUObject(this, &USpatialNetDriver::OnConnectionToSpatialOSSucceeded);
- Connection->OnFailedToConnectCallback.BindUObject(this, &USpatialNetDriver::OnConnectionToSpatialOSFailed);
-
- if (URL.HasOption(TEXT("locator")))
+ if (!bPersistSpatialConnection)
{
- // Obtain PIT and LT.
- Connection->LocatorConfig.PlayerIdentityToken = URL.GetOption(TEXT("playeridentity="), TEXT(""));
- Connection->LocatorConfig.LoginToken = URL.GetOption(TEXT("login="), TEXT(""));
- Connection->LocatorConfig.UseExternalIp = true;
- Connection->LocatorConfig.WorkerType = GameInstance->GetSpatialWorkerType().ToString();
+ GameInstance->DestroySpatialConnectionManager();
+ GameInstance->CreateNewSpatialConnectionManager();
}
- else // Using Receptionist
+ else
{
- Connection->ReceptionistConfig.WorkerType = GameInstance->GetSpatialWorkerType().ToString();
+ UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Getting existing connection, not creating a new one"));
+ }
- // Check for overrides in the travel URL.
- if (!URL.Host.IsEmpty() && URL.Host.Compare(SpatialConstants::LOCAL_HOST) != 0)
- {
- Connection->ReceptionistConfig.ReceptionistHost = URL.Host;
- }
+ ConnectionManager = GameInstance->GetSpatialConnectionManager();
+ ConnectionManager->OnConnectedCallback.BindUObject(this, &USpatialNetDriver::OnConnectionToSpatialOSSucceeded);
+ ConnectionManager->OnFailedToConnectCallback.BindUObject(this, &USpatialNetDriver::OnConnectionToSpatialOSFailed);
- bool bHasUseExternalIpOption = URL.HasOption(TEXT("useExternalIpForBridge"));
+ // 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.
- if (bHasUseExternalIpOption)
+ FString SpatialWorkerType = GetGameInstance()->GetSpatialWorkerType().ToString();
+
+ if (!GameInstance->GetFirstConnectionToSpatialOSAttempted())
+ {
+ GameInstance->SetFirstConnectionToSpatialOSAttempted();
+ if (GetDefault()->GetPreventClientCloudDeploymentAutoConnect(bConnectAsClient))
{
- FString UseExternalIpOption = URL.GetOption(TEXT("useExternalIpForBridge"), TEXT(""));
- if (UseExternalIpOption.Equals(TEXT("false"), ESearchCase::IgnoreCase))
- {
- Connection->ReceptionistConfig.UseExternalIp = false;
- }
- else
- {
- Connection->ReceptionistConfig.UseExternalIp = true;
- }
+ // 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))
+ {
+ // If the command line arguments can not be used, use the input URL to setup connection config.
+ ConnectionManager->SetupConnectionConfigFromURL(URL, SpatialWorkerType);
}
}
+ else if (URL.Host == SpatialConstants::RECONNECT_USING_COMMANDLINE_ARGUMENTS)
+ {
+ if (!ConnectionManager->TrySetupConnectionConfigFromCommandLine(SpatialWorkerType))
+ {
+ ConnectionManager->SetConnectionType(ESpatialConnectionType::Receptionist);
+ ConnectionManager->ReceptionistConfig.LoadDefaults();
+ ConnectionManager->ReceptionistConfig.WorkerType = SpatialWorkerType;
+ }
+ }
+ else
+ {
+ ConnectionManager->SetupConnectionConfigFromURL(URL, SpatialWorkerType);
+ }
#if WITH_EDITOR
- Connection->Connect(bConnectAsClient, PlayInEditorID);
+ ConnectionManager->Connect(bConnectAsClient, PlayInEditorID);
#else
- Connection->Connect(bConnectAsClient, 0);
+ ConnectionManager->Connect(bConnectAsClient, 0);
#endif
}
void USpatialNetDriver::OnConnectionToSpatialOSSucceeded()
{
+ Connection = ConnectionManager->GetWorkerConnection();
+ check(Connection);
+
// If we're the server, we will spawn the special Spatial connection that will route all updates to SpatialOS.
// There may be more than one of these connections in the future for different replication conditions.
- if (IsServer())
+ if (!bConnectAsClient)
{
CreateServerSpatialOSNetConnection();
}
@@ -273,26 +294,30 @@ void USpatialNetDriver::OnConnectionToSpatialOSSucceeded()
CreateAndInitializeCoreClasses();
// Query the GSM to figure out what map to load
- if (!IsServer())
+ if (bConnectAsClient)
{
QueryGSMToLoadMap();
}
-
- if (IsServer())
+ else
{
Sender->CreateServerWorkerEntity();
- HandleOngoingServerTravel();
}
+
+ USpatialGameInstance* GameInstance = GetGameInstance();
+ check(GameInstance != nullptr);
+ GameInstance->HandleOnConnected();
}
void USpatialNetDriver::OnConnectionToSpatialOSFailed(uint8_t ConnectionStatusCode, const FString& ErrorMessage)
{
- if (const USpatialGameInstance* GameInstance = GetGameInstance())
+ if (USpatialGameInstance* GameInstance = GetGameInstance())
{
if (GEngine != nullptr && GameInstance->GetWorld() != nullptr)
{
GEngine->BroadcastNetworkFailure(GameInstance->GetWorld(), this, ENetworkFailure::FromDisconnectOpStatusCode(ConnectionStatusCode), *ErrorMessage);
}
+
+ GameInstance->HandleOnConnectionFailed(ErrorMessage);
}
}
@@ -300,7 +325,7 @@ void USpatialNetDriver::InitializeSpatialOutputDevice()
{
int32 PIEIndex = -1; // -1 is Unreal's default index when not using PIE
#if WITH_EDITOR
- if (IsServer())
+ if (!bConnectAsClient)
{
PIEIndex = GEngine->GetWorldContextFromWorldChecked(GetWorld()).PIEInstance;
}
@@ -324,41 +349,63 @@ void USpatialNetDriver::CreateAndInitializeCoreClasses()
{
InitializeSpatialOutputDevice();
- Dispatcher = NewObject();
+ Dispatcher = MakeUnique();
Sender = NewObject();
Receiver = NewObject();
- GlobalStateManager = NewObject();
+
+ // TODO: UNR-2452
+ // Ideally the GlobalStateManager and StaticComponentView would be created as part of USpatialWorkerConnection::Init
+ // however, this causes a crash upon the second instance of running PIE due to a destroyed USpatialNetDriver still being reference.
+ // Why the destroyed USpatialNetDriver is referenced is unknown.
+ USpatialGameInstance* GameInstance = GetGameInstance();
+ check(GameInstance != nullptr);
+
+ GlobalStateManager = GameInstance->GetGlobalStateManager();
+ check(GlobalStateManager != nullptr);
+
+ StaticComponentView = GameInstance->GetStaticComponentView();
+ check(StaticComponentView != nullptr);
+
PlayerSpawner = NewObject();
- StaticComponentView = NewObject();
- SnapshotManager = NewObject();
+ SnapshotManager = MakeUnique();
SpatialMetrics = NewObject();
- if (GetDefault()->bEnableUnrealLoadBalancer)
- {
- VirtualWorkerTranslator = MakeUnique();
- }
+ SpatialWorkerFlags = NewObject();
+ 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 (IsServer() && GetDefault()->bEnableMetricsDisplay)
+ if (IsServer())
{
- SpatialMetricsDisplay = GetWorld()->SpawnActor();
+ if (SpatialSettings->bEnableMetricsDisplay)
+ {
+ SpatialMetricsDisplay = GetWorld()->SpawnActor();
+ }
+
+ if (SpatialSettings->SpatialDebugger != nullptr)
+ {
+ SpatialDebugger = GetWorld()->SpawnActor(SpatialSettings->SpatialDebugger);
+ }
}
#endif
- Dispatcher->Init(Receiver, StaticComponentView, SpatialMetrics);
- Sender->Init(this, &TimerManager);
- Receiver->Init(this, &TimerManager);
- GlobalStateManager->Init(this, &TimerManager);
- if (GetDefault()->bEnableUnrealLoadBalancer)
+ if (SpatialSettings->bEnableUnrealLoadBalancer)
{
- VirtualWorkerTranslator->Init(this);
- // TODO(zoning): This currently hard codes the desired number of virtual workers. This should be retrieved
- // from the configuration.
- VirtualWorkerTranslator->SetDesiredVirtualWorkerCount(2);
+ CreateAndInitializeLoadBalancingClasses();
}
- SnapshotManager->Init(this);
+
+ if (SpatialSettings->UseRPCRingBuffer())
+ {
+ RPCService = MakeUnique(ExtractRPCDelegate::CreateUObject(Receiver, &USpatialReceiver::OnExtractIncomingRPC), StaticComponentView);
+ }
+
+ Dispatcher->Init(Receiver, StaticComponentView, SpatialMetrics, SpatialWorkerFlags);
+ Sender->Init(this, &TimerManager, RPCService.Get());
+ Receiver->Init(this, &TimerManager, RPCService.Get());
+ GlobalStateManager->Init(this);
+ SnapshotManager->Init(Connection, GlobalStateManager, Receiver);
PlayerSpawner->Init(this, &TimerManager);
- SpatialMetrics->Init(this);
+ SpatialMetrics->Init(Connection, NetServerMaxTickRate, IsServer());
+ SpatialMetrics->ControllerRefProvider.BindUObject(this, &USpatialNetDriver::GetCurrentPlayerControllerRef);
// PackageMap value has been set earlier in USpatialNetConnection::InitBase
// Making sure the value is the same
@@ -366,8 +413,55 @@ void USpatialNetDriver::CreateAndInitializeCoreClasses()
check(NewPackageMap == PackageMap);
PackageMap->Init(this, &TimerManager);
+
+ // The interest factory depends on the package map, so is created last.
+ InterestFactory = MakeUnique(ClassInfoManager, PackageMap);
}
+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->Init();
+ }
+
+ VirtualWorkerTranslator = MakeUnique(LoadBalanceStrategy, Connection->GetWorkerId());
+
+ if (IsServer())
+ {
+ LoadBalanceEnforcer = MakeUnique(Connection->GetWorkerId(), StaticComponentView, VirtualWorkerTranslator.Get());
+
+ if (WorldSettings == nullptr || WorldSettings->LockingPolicy == nullptr)
+ {
+ UE_LOG(LogSpatialOSNetDriver, Error, TEXT("If EnableUnrealLoadBalancer is set, there must be a Locking Policy set. Using default policy."));
+ LockingPolicy = NewObject(this);
+ }
+ else
+ {
+ LockingPolicy = NewObject(this, WorldSettings->LockingPolicy);
+ }
+ LockingPolicy->Init(AcquireLockDelegate, ReleaseLockDelegate);
+ }
+}
+
+
void USpatialNetDriver::CreateServerSpatialOSNetConnection()
{
check(!bConnectAsClient);
@@ -401,30 +495,141 @@ void USpatialNetDriver::CreateServerSpatialOSNetConnection()
GetWorld()->SpatialProcessServerTravelDelegate.BindStatic(SpatialProcessServerTravel);
}
+bool USpatialNetDriver::ClientCanSendPlayerSpawnRequests()
+{
+ return GlobalStateManager->GetAcceptingPlayers() && SessionId == GlobalStateManager->GetSessionId();
+}
+
+void USpatialNetDriver::OnGSMQuerySuccess()
+{
+ // If the deployment is now accepting players and we are waiting to spawn. Spawn.
+ if (bWaitingToSpawn && ClientCanSendPlayerSpawnRequests())
+ {
+ uint32 ServerHash = GlobalStateManager->GetSchemaHash();
+ if (ClassInfoManager->SchemaDatabase->SchemaDescriptorHash != ServerHash) // Are we running with the same schema hash as the server?
+ {
+ UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Your client's schema does not match your deployment's schema. Client hash: '%u' Server hash: '%u'"), ClassInfoManager->SchemaDatabase->SchemaDescriptorHash, ServerHash);
+ }
+
+ UWorld* CurrentWorld = GetWorld();
+ const FString& DeploymentMapURL = GlobalStateManager->GetDeploymentMapURL();
+ if (CurrentWorld == nullptr || CurrentWorld->RemovePIEPrefix(DeploymentMapURL) != CurrentWorld->RemovePIEPrefix(CurrentWorld->URL.Map))
+ {
+ // Load the correct map based on the GSM URL
+ UE_LOG(LogSpatial, Log, TEXT("Welcomed by SpatialOS (Level: %s)"), *DeploymentMapURL);
+
+ // Extract map name and options
+ FWorldContext& WorldContext = GEngine->GetWorldContextFromPendingNetGameNetDriverChecked(this);
+ FURL LastURL = WorldContext.PendingNetGame->URL;
+
+ FURL RedirectURL = FURL(&LastURL, *DeploymentMapURL, (ETravelType)WorldContext.TravelType);
+ RedirectURL.Host = LastURL.Host;
+ RedirectURL.Port = LastURL.Port;
+ RedirectURL.Portal = LastURL.Portal;
+
+ // Usually the LastURL options are added to the RedirectURL in the FURL constructor.
+ // However this is not the case when TravelType = TRAVEL_Absolute so we must do it explicitly here.
+ if (WorldContext.TravelType == ETravelType::TRAVEL_Absolute)
+ {
+ RedirectURL.Op.Append(LastURL.Op);
+ }
+
+ RedirectURL.AddOption(*SpatialConstants::ClientsStayConnectedURLOption);
+
+ 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
+ {
+ MakePlayerSpawnRequest();
+ }
+ }
+}
+
+void USpatialNetDriver::RetryQueryGSM()
+{
+ float RetryTimerDelay = SpatialConstants::ENTITY_QUERY_RETRY_WAIT_SECONDS;
+
+ // In PIE we want to retry the entity query as soon as possible.
+#if WITH_EDITOR
+ RetryTimerDelay = 0.1f;
+#endif
+
+ UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Retrying query for GSM in %f seconds"), RetryTimerDelay);
+ FTimerHandle RetryTimer;
+ TimerManager.SetTimer(RetryTimer, [WeakThis = TWeakObjectPtr(this)]()
+ {
+ if (WeakThis.IsValid())
+ {
+ if (UGlobalStateManager* GSM = WeakThis.Get()->GlobalStateManager)
+ {
+ UGlobalStateManager::QueryDelegate QueryDelegate;
+ QueryDelegate.BindUObject(WeakThis.Get(), &USpatialNetDriver::GSMQueryDelegateFunction);
+ GSM->QueryGSM(QueryDelegate);
+ }
+ }
+ }, RetryTimerDelay, false);
+}
+
+void USpatialNetDriver::GSMQueryDelegateFunction(const Worker_EntityQueryResponseOp& Op)
+{
+ bool bNewAcceptingPlayers = false;
+ int32 QuerySessionId = 0;
+ bool bQueryResponseSuccess = GlobalStateManager->GetAcceptingPlayersAndSessionIdFromQueryResponse(Op, bNewAcceptingPlayers, QuerySessionId);
+
+ if (!bQueryResponseSuccess)
+ {
+ UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Failed to extract AcceptingPlayers and SessionId from GSM query response. Will retry query for GSM."));
+ RetryQueryGSM();
+ return;
+ }
+ else if (bNewAcceptingPlayers != true ||
+ QuerySessionId != SessionId)
+ {
+ UE_LOG(LogSpatialOSNetDriver, Log, TEXT("GlobalStateManager did not match expected state. Will retry query for GSM."));
+ RetryQueryGSM();
+ return;
+ }
+
+ OnGSMQuerySuccess();
+}
+
void USpatialNetDriver::QueryGSMToLoadMap()
{
check(bConnectAsClient);
// Register our interest in spawning.
- bWaitingForAcceptingPlayersToSpawn = true;
+ bWaitingToSpawn = true;
+
+ UGlobalStateManager::QueryDelegate QueryDelegate;
+ QueryDelegate.BindUObject(this, &USpatialNetDriver::GSMQueryDelegateFunction);
- // Begin querying the state of the GSM so we know the state of AcceptingPlayers.
- GlobalStateManager->QueryGSM(true /*bRetryUntilAcceptingPlayers*/);
+ // Begin querying the state of the GSM so we know the state of AcceptingPlayers and SessionId.
+ GlobalStateManager->QueryGSM(QueryDelegate);
}
-void USpatialNetDriver::HandleOngoingServerTravel()
+void USpatialNetDriver::OnActorSpawned(AActor* Actor)
{
- check(!bConnectAsClient);
-
- // Here if we are a server and this is server travel (there is a snapshot to load) we want to load the snapshot.
- if (!ServerConnection && !SnapshotToLoad.IsEmpty() && Cast(GetWorld()->GetGameInstance())->bResponsibleForSnapshotLoading)
+ if (!Actor->GetIsReplicated() ||
+ Actor->GetLocalRole() != ROLE_Authority ||
+ Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton) ||
+ !Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_SpatialType) ||
+ USpatialStatics::IsActorGroupOwnerForActor(Actor))
{
- UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Worker authoriative over the GSM is loading snapshot: %s"), *SnapshotToLoad);
- SnapshotManager->LoadSnapshot(SnapshotToLoad);
-
- // Once we've finished loading the snapshot we must update our bResponsibleForSnapshotLoading in-case we do not gain authority over the new GSM.
- Cast(GetWorld()->GetGameInstance())->bResponsibleForSnapshotLoading = false;
+ // We only want to delete actors which are replicated and we somehow gain local authority over, while not the actor group owner.
+ 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());
+ // 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);
}
void USpatialNetDriver::OnMapLoaded(UWorld* LoadedWorld)
@@ -442,82 +647,84 @@ void USpatialNetDriver::OnMapLoaded(UWorld* LoadedWorld)
return;
}
- // If we're the client, we can now ask the server to spawn our controller.
- if (!IsServer())
+ if (IsServer())
{
- // If we know the GSM is already accepting players, simply spawn.
- if (GlobalStateManager->bAcceptingPlayers && GetWorld()->RemovePIEPrefix(GlobalStateManager->DeploymentMapURL) == GetWorld()->RemovePIEPrefix(GetWorld()->URL.Map))
+ if (GlobalStateManager != nullptr &&
+ !GlobalStateManager->GetCanBeginPlay() &&
+ StaticComponentView->HasAuthority(GlobalStateManager->GlobalStateManagerEntityId, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID))
{
- PlayerSpawner->SendPlayerSpawnRequest();
- bWaitingForAcceptingPlayersToSpawn = false;
+ // ServerTravel - Increment the session id, so users don't rejoin the old game.
+ GlobalStateManager->TriggerBeginPlay();
+ GlobalStateManager->SetDeploymentState();
+ GlobalStateManager->SetAcceptingPlayers(true);
+ GlobalStateManager->IncrementSessionID();
+ }
+ }
+ else
+ {
+ if (ClientCanSendPlayerSpawnRequests())
+ {
+ MakePlayerSpawnRequest();
}
else
{
- checkNoEntry();
+ UE_LOG(LogSpatial, Warning, TEXT("Client map finished loading but could not send player spawn request. Will requery the GSM for the correct map to load."));
+ QueryGSMToLoadMap();
}
}
bMapLoaded = true;
}
+void USpatialNetDriver::MakePlayerSpawnRequest()
+{
+ if (bWaitingToSpawn)
+ {
+ PlayerSpawner->SendPlayerSpawnRequest();
+ bWaitingToSpawn = false;
+ bPersistSpatialConnection = false;
+ }
+}
+
void USpatialNetDriver::OnLevelAddedToWorld(ULevel* LoadedLevel, UWorld* OwningWorld)
{
- // Callback got called on a World that's not associated with this NetDriver.
- // Don't do anything.
- if (OwningWorld != World)
+ UE_LOG(LogSpatialOSNetDriver, Log, TEXT("OnLevelAddedToWorld: Level (%s) OwningWorld (%s) World (%s)"),
+ *GetNameSafe(LoadedLevel), *GetNameSafe(OwningWorld), *GetNameSafe(World));
+
+ if (OwningWorld != World
+ || !IsServer()
+ || GlobalStateManager == nullptr
+ || USpatialStatics::IsSpatialOffloadingEnabled())
{
+ // 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.
return;
}
- // If we have authority over the GSM when loading a sublevel, make sure we have authority
- // over the actors in the sublevel.
- if (GlobalStateManager != nullptr)
+ 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 (GlobalStateManager->HasAuthority())
- {
- for (auto Actor : LoadedLevel->Actors)
- {
- if (Actor->GetIsReplicated())
- {
- Actor->Role = ROLE_Authority;
- Actor->RemoteRole = ROLE_SimulatedProxy;
- }
- }
- }
+ // If load balancing is disabled and this worker is not GSM authoritative then exit early.
+ return;
}
-}
-void USpatialNetDriver::OnAcceptingPlayersChanged(bool bAcceptingPlayers)
-{
- // If the deployment is now accepting players and we are waiting to spawn. Spawn.
- if (bWaitingForAcceptingPlayersToSpawn && bAcceptingPlayers)
+ if (bLoadBalancingEnabled && !LoadBalanceStrategy->IsReady())
{
- // If we have the correct map loaded then ask to spawn.
- if (GetWorld() != nullptr && GetWorld()->RemovePIEPrefix(GlobalStateManager->DeploymentMapURL) == GetWorld()->RemovePIEPrefix(GetWorld()->URL.Map))
- {
- PlayerSpawner->SendPlayerSpawnRequest();
+ // Load balancer isn't ready, this should only occur when servers are loading composition levels on startup, before connecting to spatial.
+ return;
+ }
- // Unregister our interest in spawning on accepting players changing again.
- bWaitingForAcceptingPlayersToSpawn = false;
- }
- else
+ for (auto Actor : LoadedLevel->Actors)
+ {
+ // 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)))
{
- // Load the correct map based on the GSM URL
- UE_LOG(LogSpatial, Log, TEXT("Welcomed by SpatialOS (Level: %s)"), *GlobalStateManager->DeploymentMapURL);
-
- // Extract map name and options
- FWorldContext& WorldContext = GEngine->GetWorldContextFromPendingNetGameNetDriverChecked(this);
- FURL LastURL = WorldContext.PendingNetGame->URL;
-
- FURL RedirectURL = FURL(&LastURL, *GlobalStateManager->DeploymentMapURL, (ETravelType)WorldContext.TravelType);
- RedirectURL.Host = LastURL.Host;
- RedirectURL.Port = LastURL.Port;
- RedirectURL.Op.Append(LastURL.Op);
- RedirectURL.AddOption(*SpatialConstants::ClientsStayConnectedURLOption);
-
- WorldContext.PendingNetGame->bSuccessfullyConnected = true;
- WorldContext.PendingNetGame->bSentJoinRequest = false;
- WorldContext.PendingNetGame->URL = RedirectURL;
+ Actor->Role = ROLE_Authority;
+ Actor->RemoteRole = ROLE_SimulatedProxy;
}
}
}
@@ -531,15 +738,14 @@ void USpatialNetDriver::SpatialProcessServerTravel(const FString& URL, bool bAbs
UWorld* World = GameMode->GetWorld();
USpatialNetDriver* NetDriver = Cast(World->GetNetDriver());
- if (!NetDriver->StaticComponentView->HasAuthority(NetDriver->GlobalStateManager->GlobalStateManagerEntityId, SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID))
+ if (!NetDriver->StaticComponentView->HasAuthority(SpatialConstants::INITIAL_GLOBAL_STATE_MANAGER_ENTITY_ID, SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID))
{
// TODO: UNR-678 Send a command to the GSM to initiate server travel on the correct server.
UE_LOG(LogGameMode, Warning, TEXT("Trying to server travel on a server which is not authoritative over the GSM."));
return;
}
- // Register that this server will be responsible for loading the snapshot once it has finished wiping the world + loading the new map.
- Cast(World->GetGameInstance())->bResponsibleForSnapshotLoading = true;
+ NetDriver->GlobalStateManager->ResetGSM();
GameMode->StartToLeaveMap();
@@ -568,15 +774,10 @@ void USpatialNetDriver::SpatialProcessServerTravel(const FString& URL, bool bAbs
FString NewURL = URL;
- bool SnapshotOption = NewURL.Contains(TEXT("snapshot="));
- if (!SnapshotOption)
+ if (!NewURL.Contains(SpatialConstants::SpatialSessionIdURLOption))
{
- // In the case that we don't have a snapshot option, we assume the map name will be the snapshot name.
- // Remove any leading path before the map name.
- FString Path;
- FString MapName;
- NextMap.Split(TEXT("/"), &Path, &MapName, ESearchCase::IgnoreCase, ESearchDir::FromEnd);
- NewURL.Append(FString::Printf(TEXT("?snapshot=%s"), *MapName));
+ int32 NextSessionId = NetDriver->GlobalStateManager->GetSessionId() + 1;
+ NewURL.Append(FString::Printf(TEXT("?spatialSessionId=%d"), NextSessionId));
}
// Notify clients we're switching level and give them time to receive.
@@ -592,7 +793,7 @@ void USpatialNetDriver::SpatialProcessServerTravel(const FString& URL, bool bAbs
ENetMode NetMode = GameMode->GetNetMode();
// FinishServerTravel - Allows Unreal to finish it's normal server travel.
- USpatialNetDriver::PostWorldWipeDelegate FinishServerTravel;
+ PostWorldWipeDelegate FinishServerTravel;
FinishServerTravel.BindLambda([World, NetDriver, NewURL, NetMode, bSeamless, bAbsolute]
{
UE_LOG(LogGameMode, Log, TEXT("SpatialServerTravel - Finishing Server Travel : %s"), *NewURL);
@@ -627,13 +828,13 @@ void USpatialNetDriver::BeginDestroy()
{
Connection->SendDeleteEntityRequest(WorkerEntityId);
}
-
+
// Destroy the connection to disconnect from SpatialOS if we aren't meant to persist it.
if (!bPersistSpatialConnection)
{
if (UWorld* LocalWorld = GetWorld())
{
- Cast(LocalWorld->GetGameInstance())->DestroySpatialWorkerConnection();
+ Cast(LocalWorld->GetGameInstance())->DestroySpatialConnectionManager();
}
Connection = nullptr;
}
@@ -646,6 +847,8 @@ void USpatialNetDriver::BeginDestroy()
GDKServices->GetLocalDeploymentManager()->OnDeploymentStart.Remove(SpatialDeploymentStartHandle);
}
#endif
+
+ ActorGroupManager = nullptr;
}
void USpatialNetDriver::PostInitProperties()
@@ -671,13 +874,54 @@ void USpatialNetDriver::NotifyActorDestroyed(AActor* ThisActor, bool IsSeamlessT
// The native UNetDriver would normally store destruction info here for "StartupActors" - replicated actors
// placed in the level, but we handle this flow differently in the GDK
+ // In single process PIE sessions this can be called on the server with actors from a client when the client unloads a level.
+ // Such actors will not have a valid entity ID.
+ // As only clients unload a level, if an actor has an entity ID and authority then it can not be such a spurious entity.
+
// Remove the actor from the property tracker map
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
+ if (PackageMap != nullptr && World != nullptr)
+ {
+ 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())
+ {
+ UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Creating a tombstone entity for initially dormant statup actor. "
+ "Actor: %s."), *ThisActor->GetName());
+ Sender->CreateTombstoneEntity(ThisActor);
+ }
+ else if (IsDormantEntity(EntityId) && ThisActor->HasAuthority())
+ {
+ // Deliberately don't unregister the dormant entity, but let it get cleaned up in the entity remove op process
+ if (!StaticComponentView->HasAuthority(EntityId, SpatialGDK::Position::ComponentId))
+ {
+ UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Retiring dormant entity that we don't have spatial authority over [%lld][%s]"), EntityId, *ThisActor->GetName());
+ }
+ Sender->RetireEntity(EntityId);
+ }
+ }
+
for (int32 i = ClientConnections.Num() - 1; i >= 0; i--)
{
UNetConnection* ClientConnection = ClientConnections[i];
@@ -697,21 +941,6 @@ void USpatialNetDriver::NotifyActorDestroyed(AActor* ThisActor, bool IsSeamlessT
// Remove it from any dormancy lists
ClientConnection->DormantReplicatorMap.Remove(ThisActor);
}
-
- // Check if this is a dormant entity, and if so retire the entity
- if (PackageMap != nullptr)
- {
- Worker_EntityId EntityId = PackageMap->GetEntityIdFromObject(ThisActor);
- if (IsDormantEntity(EntityId) && ThisActor->HasAuthority())
- {
- // Deliberately don't unregister the dormant entity, but let it get cleaned up in the entity remove op process
- if (StaticComponentView->GetAuthority(EntityId, SpatialGDK::Position::ComponentId) != WORKER_AUTHORITY_AUTHORITATIVE)
- {
- UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Retiring dormant entity that we don't have spatial authority over [%lld][%s]"), EntityId, *ThisActor->GetName());
- }
- Sender->RetireEntity(EntityId);
- }
- }
}
// Remove this actor from the network object list
@@ -732,6 +961,8 @@ void USpatialNetDriver::Shutdown()
}
}
+ SpatialOutputDevice = nullptr;
+
Super::Shutdown();
// This is done after Super::Shutdown so the NetDriver is given an opportunity to shutdown all open channels, and those
@@ -743,7 +974,7 @@ void USpatialNetDriver::Shutdown()
{
for (const Worker_EntityId EntityId : DormantEntities)
{
- if (StaticComponentView->GetAuthority(EntityId, SpatialGDK::Position::ComponentId) == WORKER_AUTHORITY_AUTHORITATIVE)
+ if (StaticComponentView->HasAuthority(EntityId, SpatialGDK::Position::ComponentId))
{
Connection->SendDeleteEntityRequest(EntityId);
}
@@ -751,7 +982,7 @@ void USpatialNetDriver::Shutdown()
for (const Worker_EntityId EntityId : TombstonedEntities)
{
- if (StaticComponentView->GetAuthority(EntityId, SpatialGDK::Position::ComponentId) == WORKER_AUTHORITY_AUTHORITATIVE)
+ if (StaticComponentView->HasAuthority(EntityId, SpatialGDK::Position::ComponentId))
{
Connection->SendDeleteEntityRequest(EntityId);
}
@@ -766,16 +997,26 @@ void USpatialNetDriver::NotifyActorFullyDormantForConnection(AActor* Actor, UNet
const int NumConnections = 1;
GetNetworkObjectList().MarkDormant(Actor, NetConnection, NumConnections, this);
+ if (UReplicationDriver* RepDriver = GetReplicationDriver())
+ {
+ RepDriver->NotifyActorFullyDormantForConnection(Actor, NetConnection);
+ }
+
// Intentionally don't call Super::NotifyActorFullyDormantForConnection
}
-void USpatialNetDriver::OnOwnerUpdated(AActor* Actor)
+void USpatialNetDriver::OnOwnerUpdated(AActor* Actor, AActor* OldOwner)
{
if (!IsServer())
{
return;
}
+ if (LockingPolicy != nullptr)
+ {
+ LockingPolicy->OnOwnerUpdated(Actor, OldOwner);
+ }
+
// If PackageMap doesn't exist, we haven't connected yet, which means
// we don't need to update the interest at this point
if (PackageMap == nullptr)
@@ -930,7 +1171,7 @@ int32 USpatialNetDriver::ServerReplicateActors_PrioritizeActors(UNetConnection*
AGameNetworkManager* const NetworkManager = World->NetworkManager;
const bool bLowNetBandwidth = NetworkManager ? NetworkManager->IsInLowBandwidthMode() : false;
- const bool bNetRelevancyEnabled = GetDefault()->UseIsActorRelevantForConnection;
+ const bool bNetRelevancyEnabled = GetDefault()->bUseIsActorRelevantForConnection;
for (FNetworkObjectInfo* ActorInfo : ConsiderList)
{
@@ -1217,22 +1458,13 @@ void USpatialNetDriver::ProcessRPC(AActor* Actor, UObject* SubObject, UFunction*
return;
}
- int ReliableRPCIndex = 0;
- if (GetDefault()->bCheckRPCOrder)
- {
- if (Function->HasAnyFunctionFlags(FUNC_NetReliable) && !Function->HasAnyFunctionFlags(FUNC_NetMulticast))
- {
- ReliableRPCIndex = GetNextReliableRPCId(Actor, FunctionFlagsToRPCSchemaType(Function->FunctionFlags), CallingObject);
- }
- }
-
FUnrealObjectRef CallingObjectRef = PackageMap->GetUnrealObjectRefFromObject(CallingObject);
if (!CallingObjectRef.IsValid())
{
UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("The target object %s is unresolved; RPC %s will be dropped."), *CallingObject->GetFullName(), *Function->GetName());
return;
}
- RPCPayload Payload = Sender->CreateRPCPayloadFromParams(CallingObject, CallingObjectRef, Function, ReliableRPCIndex, Parameters);
+ RPCPayload Payload = Sender->CreateRPCPayloadFromParams(CallingObject, CallingObjectRef, Function, Parameters);
Sender->ProcessOrQueueOutgoingRPC(CallingObjectRef, MoveTemp(Payload));
}
@@ -1243,6 +1475,8 @@ void USpatialNetDriver::ProcessRPC(AActor* Actor, UObject* SubObject, UFunction*
int32 USpatialNetDriver::ServerReplicateActors(float DeltaSeconds)
{
SCOPE_CYCLE_COUNTER(STAT_SpatialServerReplicateActors);
+ SET_DWORD_STAT(STAT_NumReplicatedActorBytes, 0);
+ SET_DWORD_STAT(STAT_NumReplicatedActors, 0);
#if WITH_SERVER_CODE
// Only process the stand-in client connection, which is the connection to the runtime itself.
@@ -1252,7 +1486,13 @@ int32 USpatialNetDriver::ServerReplicateActors(float DeltaSeconds)
{
return 0;
}
- check(SpatialConnection && SpatialConnection->bReliableSpatialConnection);
+ check(SpatialConnection->bReliableSpatialConnection);
+
+ if (UReplicationDriver* RepDriver = GetReplicationDriver())
+ {
+ return RepDriver->ServerReplicateActors(DeltaSeconds);
+ }
+
check(World);
int32 Updated = 0;
@@ -1310,6 +1550,13 @@ int32 USpatialNetDriver::ServerReplicateActors(float DeltaSeconds)
{
new(ConnectionViewers)FNetViewer(ClientConnection, DeltaSeconds);
+ // send ClientAdjustment if necessary
+ // we do this here so that we send a maximum of one per packet to that client; there is no value in stacking additional corrections
+ if (ClientConnection->PlayerController != nullptr)
+ {
+ ClientConnection->PlayerController->SendClientAdjustment();
+ }
+
if (ClientConnection->Children.Num() > 0)
{
UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Child connections present on Spatial client connection %s! We don't support splitscreen yet, so this will not function correctly."), *ClientConnection->GetName());
@@ -1363,6 +1610,12 @@ void USpatialNetDriver::TickDispatch(float DeltaTime)
if (Connection != nullptr)
{
+ const USpatialGDKSettings* SpatialGDKSettings = GetDefault();
+ if (SpatialGDKSettings->bRunSpatialWorkerConnectionOnGameThread)
+ {
+ Connection->QueueLatestOpList();
+ }
+
TArray OpLists = Connection->GetOpList();
// Servers will queue ops at startup until we've extracted necessary information from the op stream
@@ -1372,16 +1625,28 @@ void USpatialNetDriver::TickDispatch(float DeltaTime)
return;
}
- for (Worker_OpList* OpList : OpLists)
{
- Dispatcher->ProcessOps(OpList);
+ SCOPE_CYCLE_COUNTER(STAT_SpatialProcessOps);
+ for (Worker_OpList* OpList : OpLists)
+ {
+ Dispatcher->ProcessOps(OpList);
- Worker_OpList_Destroy(OpList);
+ Worker_OpList_Destroy(OpList);
+ }
}
- if (SpatialMetrics != nullptr && GetDefault()->bEnableMetrics)
+ if (SpatialMetrics != nullptr && SpatialGDKSettings->bEnableMetrics)
{
- SpatialMetrics->TickMetrics();
+ SpatialMetrics->TickMetrics(Time);
+ }
+
+ if (LoadBalanceEnforcer.IsValid())
+ {
+ SCOPE_CYCLE_COUNTER(STAT_SpatialUpdateAuthority);
+ for (const auto& AclAssignmentRequest : LoadBalanceEnforcer->ProcessQueuedAclAssignmentRequests())
+ {
+ Sender->SetAclWriteAuthority(AclAssignmentRequest);
+ }
}
}
}
@@ -1466,30 +1731,50 @@ void USpatialNetDriver::ProcessRemoteFunction(
}
}
+void USpatialNetDriver::PollPendingLoads()
+{
+ if (PackageMap == nullptr)
+ {
+ return;
+ }
+
+ for (auto IterPending = PackageMap->PendingReferences.CreateIterator(); IterPending; ++IterPending)
+ {
+ if (PackageMap->IsGUIDPending(*IterPending))
+ {
+ continue;
+ }
+
+ FUnrealObjectRef ObjectReference = PackageMap->GetUnrealObjectRefFromNetGUID(*IterPending);
+
+ bool bOutUnresolved = false;
+ UObject* ResolvedObject = FUnrealObjectRef::ToObjectPtr(ObjectReference, PackageMap, bOutUnresolved);
+ if (ResolvedObject)
+ {
+ Receiver->ResolvePendingOperations(ResolvedObject, ObjectReference);
+ }
+ else
+ {
+ UE_LOG(LogSpatialPackageMap, Warning, TEXT("Object %s which was being asynchronously loaded was not found after loading has completed."), *ObjectReference.ToString());
+ }
+
+ IterPending.RemoveCurrent();
+ }
+}
+
void USpatialNetDriver::TickFlush(float DeltaTime)
{
- // Super::TickFlush() will not call ReplicateActors() because Spatial connections have InternalAck set to true.
- // In our case, our Spatial actor interop is triggered through ReplicateActors() so we want to call it regardless.
+ const USpatialGDKSettings* SpatialGDKSettings = GetDefault();
-#if USE_SERVER_PERF_COUNTERS
- double ServerReplicateActorsTimeMs = 0.0f;
-#endif // USE_SERVER_PERF_COUNTERS
+ PollPendingLoads();
- if (IsServer() && GetSpatialOSNetConnection() != nullptr && PackageMap->IsEntityPoolReady())
+ if (IsServer() && GetSpatialOSNetConnection() != nullptr && PackageMap->IsEntityPoolReady() && bIsReadyToStart)
{
// Update all clients.
#if WITH_SERVER_CODE
-#if USE_SERVER_PERF_COUNTERS
- double ServerReplicateActorsTimeStart = FPlatformTime::Seconds();
-#endif // USE_SERVER_PERF_COUNTERS
-
int32 Updated = ServerReplicateActors(DeltaTime);
-#if USE_SERVER_PERF_COUNTERS
- ServerReplicateActorsTimeMs = (FPlatformTime::Seconds() - ServerReplicateActorsTimeStart) * 1000.0;
-#endif // USE_SERVER_PERF_COUNTERS
-
static int32 LastUpdateCount = 0;
// Only log the zero replicated actors once after replicating an actor
if ((LastUpdateCount && !Updated) || Updated)
@@ -1498,8 +1783,6 @@ void USpatialNetDriver::TickFlush(float DeltaTime)
}
LastUpdateCount = Updated;
- const USpatialGDKSettings* SpatialGDKSettings = GetDefault();
-
if (SpatialGDKSettings->bBatchSpatialPositionUpdates && Sender != nullptr)
{
if ((Time - TimeWhenPositionLastUpdated) >= (1.0f / SpatialGDKSettings->PositionUpdateFrequency))
@@ -1513,15 +1796,25 @@ void USpatialNetDriver::TickFlush(float DeltaTime)
#endif // WITH_SERVER_CODE
}
- if (GetDefault()->bPackRPCs && Sender != nullptr)
+ if (SpatialGDKSettings->UseRPCRingBuffer() && Sender != nullptr)
{
- Sender->FlushPackedRPCs();
+ Sender->FlushRPCService();
}
ProcessPendingDormancy();
TimerManager.Tick(DeltaTime);
+ if (SpatialGDKSettings->bRunSpatialWorkerConnectionOnGameThread)
+ {
+ if (Connection != nullptr)
+ {
+ Connection->ProcessOutgoingMessages();
+ }
+ }
+
+ // Super::TickFlush() will not call ReplicateActors() because Spatial connections have InternalAck set to true.
+ // In our case, our Spatial actor interop is triggered through ReplicateActors() so we want to call it regardless.
Super::TickFlush(DeltaTime);
}
@@ -1541,6 +1834,23 @@ USpatialNetConnection * USpatialNetDriver::GetSpatialOSNetConnection() const
}
}
+namespace
+{
+ TOptional ExtractWorkerIDFromAttribute(const FString& WorkerAttribute)
+ {
+ const FString WorkerIdAttr = TEXT("workerId:");
+ int32 AttrOffset = WorkerAttribute.Find(WorkerIdAttr);
+
+ if (AttrOffset < 0)
+ {
+ UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Error : Worker attribute does not contain workerId : %s"), *WorkerAttribute);
+ return {};
+ }
+
+ return WorkerAttribute.RightChop(AttrOffset + WorkerIdAttr.Len());
+ }
+}
+
bool USpatialNetDriver::CreateSpatialNetConnection(const FURL& InUrl, const FUniqueNetIdRepl& UniqueId, const FName& OnlinePlatformName, USpatialNetConnection** OutConn)
{
check(*OutConn == nullptr);
@@ -1570,7 +1880,15 @@ bool USpatialNetDriver::CreateSpatialNetConnection(const FURL& InUrl, const FUni
// Get the worker attribute.
const TCHAR* WorkerAttributeOption = InUrl.GetOption(TEXT("workerAttribute"), nullptr);
check(WorkerAttributeOption);
- SpatialConnection->WorkerAttribute = FString(WorkerAttributeOption).Mid(1); // Trim off the = at the beginning.
+ SpatialConnection->ConnectionOwningWorkerId = FString(WorkerAttributeOption).Mid(1); // Trim off the = at the beginning.
+
+ // Register workerId and its connection.
+ if (TOptional WorkerId = ExtractWorkerIDFromAttribute(SpatialConnection->ConnectionOwningWorkerId))
+ {
+ UE_LOG(LogSpatialOSNetDriver, Verbose, TEXT("Worker %s 's NetConnection created."), *WorkerId.GetValue());
+
+ WorkerConnections.Add(WorkerId.GetValue(), SpatialConnection);
+ }
// We will now ask GameMode/GameSession if it's ok for this user to join.
// Note that in the initial implementation, we carry over no data about the user here (such as a unique player id, or the real IP)
@@ -1603,6 +1921,27 @@ bool USpatialNetDriver::CreateSpatialNetConnection(const FURL& InUrl, const FUni
return true;
}
+void USpatialNetDriver::CleanUpClientConnection(USpatialNetConnection* ConnectionCleanedUp)
+{
+ if (!ConnectionCleanedUp->ConnectionOwningWorkerId.IsEmpty())
+ {
+ if (TOptional WorkerId = ExtractWorkerIDFromAttribute(*ConnectionCleanedUp->ConnectionOwningWorkerId))
+ {
+ WorkerConnections.Remove(WorkerId.GetValue());
+ }
+ }
+}
+
+TWeakObjectPtr USpatialNetDriver::FindClientConnectionFromWorkerId(const FString& WorkerId)
+{
+ if (TWeakObjectPtr* ClientConnectionPtr = WorkerConnections.Find(WorkerId))
+ {
+ return *ClientConnectionPtr;
+ }
+
+ return {};
+}
+
void USpatialNetDriver::ProcessPendingDormancy()
{
TSet> RemainingChannels;
@@ -1613,7 +1952,7 @@ void USpatialNetDriver::ProcessPendingDormancy()
USpatialActorChannel* Channel = PendingDormantChannel.Get();
if (Channel->Actor != nullptr)
{
- if (Receiver->IsPendingOpsOnChannel(Channel))
+ if (Receiver->IsPendingOpsOnChannel(*Channel))
{
RemainingChannels.Emplace(PendingDormantChannel);
continue;
@@ -1650,15 +1989,15 @@ void USpatialNetDriver::AcceptNewPlayer(const FURL& InUrl, const FUniqueNetIdRep
}
// This function is called for server workers who received the PC over the wire
-void USpatialNetDriver::PostSpawnPlayerController(APlayerController* PlayerController, const FString& WorkerAttribute)
+void USpatialNetDriver::PostSpawnPlayerController(APlayerController* PlayerController, const FString& ClientWorkerId)
{
check(PlayerController != nullptr);
- checkf(!WorkerAttribute.IsEmpty(), TEXT("A player controller entity must have an owner worker attribute."));
+ checkf(!ClientWorkerId.IsEmpty(), TEXT("A player controller entity must have an owning client worker ID."));
PlayerController->SetFlags(GetFlags() | RF_Transient);
FString URLString = FURL().ToString();
- URLString += TEXT("?workerAttribute=") + WorkerAttribute;
+ URLString += TEXT("?workerAttribute=") + ClientWorkerId;
// We create a connection here so that any code that searches for owning connection, etc on the server
// resolves ownership correctly
@@ -1827,8 +2166,14 @@ void USpatialNetDriver::AddActorChannel(Worker_EntityId EntityId, USpatialActorC
EntityToActorChannel.Add(EntityId, Channel);
}
-void USpatialNetDriver::RemoveActorChannel(Worker_EntityId EntityId)
+void USpatialNetDriver::RemoveActorChannel(Worker_EntityId EntityId, USpatialActorChannel& Channel)
{
+ for (auto& ChannelRefs : Channel.ObjectReferenceMap)
+ {
+ Receiver->CleanupRepStateMap(ChannelRefs.Value);
+ }
+ Channel.ObjectReferenceMap.Empty();
+
if (!EntityToActorChannel.Contains(EntityId))
{
UE_LOG(LogSpatialOSNetDriver, Verbose, TEXT("RemoveActorChannel: Failed to find entity/channel mapping for entity %lld."), EntityId);
@@ -1907,8 +2252,8 @@ void USpatialNetDriver::RefreshActorDormancy(AActor* Actor, bool bMakeDormant)
{
Worker_AddComponentOp AddComponentOp{};
AddComponentOp.entity_id = EntityId;
- AddComponentOp.data = SpatialGDK::Dormant().CreateData();
- Connection->SendAddComponent(AddComponentOp.entity_id, &AddComponentOp.data);
+ AddComponentOp.data = ComponentFactory::CreateEmptyComponentData(SpatialConstants::DORMANT_COMPONENT_ID);
+ Sender->SendAddComponents(AddComponentOp.entity_id, { AddComponentOp.data });
StaticComponentView->OnAddComponent(AddComponentOp);
}
}
@@ -1919,7 +2264,7 @@ void USpatialNetDriver::RefreshActorDormancy(AActor* Actor, bool bMakeDormant)
Worker_RemoveComponentOp RemoveComponentOp{};
RemoveComponentOp.entity_id = EntityId;
RemoveComponentOp.component_id = SpatialConstants::DORMANT_COMPONENT_ID;
- Connection->SendRemoveComponent(EntityId, SpatialConstants::DORMANT_COMPONENT_ID);
+ Sender->SendRemoveComponents(EntityId, { SpatialConstants::DORMANT_COMPONENT_ID });
StaticComponentView->OnRemoveComponent(RemoveComponentOp);
}
}
@@ -1930,6 +2275,11 @@ void USpatialNetDriver::AddPendingDormantChannel(USpatialActorChannel* Channel)
PendingDormantChannels.Emplace(Channel);
}
+void USpatialNetDriver::RemovePendingDormantChannel(USpatialActorChannel* Channel)
+{
+ PendingDormantChannels.Remove(Channel);
+}
+
void USpatialNetDriver::RegisterDormantEntityId(Worker_EntityId EntityId)
{
// Register dormant entities when their actor channel has been closed, but their entity is still alive.
@@ -1954,7 +2304,10 @@ USpatialActorChannel* USpatialNetDriver::CreateSpatialActorChannel(AActor* Actor
check(Actor != nullptr);
check(PackageMap != nullptr);
- check(GetActorChannelByEntityId(PackageMap->GetEntityIdFromObject(Actor)) == nullptr);
+
+ Worker_EntityId EntityId = PackageMap->GetEntityIdFromObject(Actor);
+
+ check(GetActorChannelByEntityId(EntityId) == nullptr);
USpatialNetConnection* NetConnection = GetSpatialOSNetConnection();
check(NetConnection != nullptr);
@@ -1981,117 +2334,17 @@ USpatialActorChannel* USpatialNetDriver::CreateSpatialActorChannel(AActor* Actor
}
}
- return Channel;
-}
-
-void USpatialNetDriver::WipeWorld(const USpatialNetDriver::PostWorldWipeDelegate& LoadSnapshotAfterWorldWipe)
-{
- if (Cast(GetWorld()->GetGameInstance())->bResponsibleForSnapshotLoading)
- {
- SnapshotManager->WorldWipe(LoadSnapshotAfterWorldWipe);
- }
-}
-
-uint32 USpatialNetDriver::GetNextReliableRPCId(AActor* Actor, ESchemaComponentType RPCType, UObject* TargetObject)
-{
- if (!ReliableRPCIdMap.Contains(Actor))
- {
- ReliableRPCIdMap.Add(Actor);
- }
- FRPCTypeToReliableRPCIdMap& ReliableRPCIds = ReliableRPCIdMap[Actor];
-
- if (FReliableRPCId* RPCIdEntry = ReliableRPCIds.Find(RPCType))
- {
- if (!RPCIdEntry->WorkerId.IsEmpty())
- {
- // We previously used to receive RPCs of this type, now we're about to send one, so we reset the reliable RPC index.
- // This should only be possible for CrossServer RPCs.
- check(RPCType == SCHEMA_CrossServerRPC);
- UE_LOG(LogSpatialOSNetDriver, Verbose, TEXT("Actor %s, object %s: Used to receive reliable CrossServer RPCs from worker %s, now about to send one. The entity must have crossed boundary."),
- *Actor->GetName(), *TargetObject->GetName(), *RPCIdEntry->WorkerId);
- RPCIdEntry->WorkerId = FString();
- RPCIdEntry->RPCId = 0;
- }
- }
- else
- {
- // Add an entry for this RPC type with empty WorkerId and RPCId = 0
- ReliableRPCIds.Add(RPCType);
- }
-
- return ++ReliableRPCIds[RPCType].RPCId;
-}
-
-void USpatialNetDriver::OnReceivedReliableRPC(AActor* Actor, ESchemaComponentType RPCType, FString WorkerId, uint32 RPCId, UObject* TargetObject, UFunction* Function)
-{
- if (!ReliableRPCIdMap.Contains(Actor))
+ if (Channel != nullptr)
{
- ReliableRPCIdMap.Add(Actor);
+ Channel->RefreshAuthority();
}
- FRPCTypeToReliableRPCIdMap& ReliableRPCIds = ReliableRPCIdMap[Actor];
- if (FReliableRPCId* RPCIdEntry = ReliableRPCIds.Find(RPCType))
- {
- if (WorkerId != RPCIdEntry->WorkerId)
- {
- if (RPCIdEntry->WorkerId.IsEmpty())
- {
- // We previously used to send RPCs of this type, now we received one. This should only be possible for CrossServer RPCs.
- check(RPCType == SCHEMA_CrossServerRPC);
- UE_LOG(LogSpatialOSNetDriver, Verbose, TEXT("Actor %s, object %s: Used to send reliable CrossServer RPCs, now received one from worker %s. The entity must have crossed boundary."),
- *Actor->GetName(), *TargetObject->GetName(), *WorkerId);
- }
- else
- {
- // We received an RPC from a different worker than the one we used to receive RPCs of this type from.
- UE_LOG(LogSpatialOSNetDriver, Verbose, TEXT("Actor %s, object %s: Received a reliable %s RPC from a different worker %s. Previously received from worker %s."),
- *Actor->GetName(), *TargetObject->GetName(), *RPCSchemaTypeToString(RPCType), *WorkerId, *RPCIdEntry->WorkerId);
- }
- RPCIdEntry->WorkerId = WorkerId;
- }
- else if (RPCId != RPCIdEntry->RPCId + 1)
- {
- if (RPCId < RPCIdEntry->RPCId)
- {
- UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Actor %s: Reliable %s RPC received out of order! Previously received RPC: %s, target %s, index %d. Now received: %s, target %s, index %d. Sender: %s"),
- *Actor->GetName(), *RPCSchemaTypeToString(RPCType), *RPCIdEntry->LastRPCName, *RPCIdEntry->LastRPCTarget, RPCIdEntry->RPCId, *Function->GetName(), *TargetObject->GetName(), RPCId, *WorkerId);
- }
- else if (RPCId == RPCIdEntry->RPCId)
- {
- UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Actor %s: Reliable %s RPC index duplicated! Previously received RPC: %s, target %s, index %d. Now received: %s, target %s, index %d. Sender: %s"),
- *Actor->GetName(), *RPCSchemaTypeToString(RPCType), *RPCIdEntry->LastRPCName, *RPCIdEntry->LastRPCTarget, RPCIdEntry->RPCId, *Function->GetName(), *TargetObject->GetName(), RPCId, *WorkerId);
- }
- else
- {
- UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Actor %s: One or more reliable %s RPCs skipped! Previously received RPC: %s, target %s, index %d. Now received: %s, target %s, index %d. Sender: %s"),
- *Actor->GetName(), *RPCSchemaTypeToString(RPCType), *RPCIdEntry->LastRPCName, *RPCIdEntry->LastRPCTarget, RPCIdEntry->RPCId, *Function->GetName(), *TargetObject->GetName(), RPCId, *WorkerId);
- }
- }
-
- RPCIdEntry->RPCId = RPCId;
- RPCIdEntry->LastRPCTarget = TargetObject->GetName();
- RPCIdEntry->LastRPCName = Function->GetName();
- }
- else
- {
- ReliableRPCIds.Add(RPCType, FReliableRPCId(WorkerId, RPCId, TargetObject->GetName(), Function->GetName()));
- }
+ return Channel;
}
-void USpatialNetDriver::OnRPCAuthorityGained(AActor* Actor, ESchemaComponentType RPCType)
+void USpatialNetDriver::WipeWorld(const PostWorldWipeDelegate& LoadSnapshotAfterWorldWipe)
{
- // When we gain authority on an RPC component of an actor that we previously received RPCs for, reset the reliable RPC counter.
- // This is to account for the case where the actor crosses to another worker, receives a couple of reliable RPCs, and comes back
- // to the original worker.
- if (FRPCTypeToReliableRPCIdMap* ReliableRPCIds = ReliableRPCIdMap.Find(Actor))
- {
- if (ReliableRPCIds->Contains(RPCType))
- {
- UE_LOG(LogSpatialOSNetDriver, Verbose, TEXT("Actor %s: Gained authority over %s RPC component. Resetting previous reliable RPC counter."),
- *Actor->GetName(), *RPCSchemaTypeToString(RPCType));
- ReliableRPCIds->Remove(RPCType);
- }
- }
+ SnapshotManager->WorldWipe(LoadSnapshotAfterWorldWipe);
}
void USpatialNetDriver::DelayedSendDeleteEntityRequest(Worker_EntityId EntityId, float Delay)
@@ -2117,6 +2370,12 @@ 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();
+ }
+
// We've found and dispatched all ops we need for startup,
// trigger BeginPlay() on the GSM and process the queued ops.
// Note that FindAndDispatchStartupOps() will have notified the Dispatcher
@@ -2131,7 +2390,7 @@ void USpatialNetDriver::HandleStartupOpQueueing(const TArray& In
if (!bIsReadyToStart)
{
- return;
+ return;
}
for (Worker_OpList* OpList : QueuedStartupOpLists)
@@ -2151,6 +2410,24 @@ bool USpatialNetDriver::FindAndDispatchStartupOpsServer(const TArray FoundOps;
+ Worker_Op* EntityQueryResponseOp = nullptr;
+ FindFirstOpOfType(InOpLists, WORKER_OP_TYPE_ENTITY_QUERY_RESPONSE, &EntityQueryResponseOp);
+
+ if (EntityQueryResponseOp != nullptr)
+ {
+ FoundOps.Add(EntityQueryResponseOp);
+ }
+
+ // 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);
+
+ if (CreateEntityResponseOp != nullptr)
+ {
+ FoundOps.Add(CreateEntityResponseOp);
+ }
+
// Search for entity id reservation response and process it. The entity id reservation
// can fail to reserve entity ids. In that case, the EntityPool will not be marked ready,
// a new query will be sent, and we will process the new response here when it arrives.
@@ -2166,7 +2443,7 @@ bool USpatialNetDriver::FindAndDispatchStartupOpsServer(const TArrayIsReadyToCallBeginPlay())
+ if (!GlobalStateManager->IsReady())
{
Worker_Op* AddComponentOp = nullptr;
FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_ADD_COMPONENT, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID, &AddComponentOp);
@@ -2193,14 +2470,48 @@ bool USpatialNetDriver::FindAndDispatchStartupOpsServer(const TArrayIsReady())
+ {
+ Worker_Op* AddComponentOp = nullptr;
+ FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_ADD_COMPONENT, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID, &AddComponentOp);
+
+ Worker_Op* AuthorityChangedOp = nullptr;
+ FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_AUTHORITY_CHANGE, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID, &AuthorityChangedOp);
+
+ Worker_Op* ComponentUpdateOp = nullptr;
+ FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_COMPONENT_UPDATE, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID, &ComponentUpdateOp);
+
+ if (AddComponentOp != nullptr)
+ {
+ UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Processing Translation component add to bootstrap SpatialVirtualWorkerTranslator."));
+ FoundOps.Add(AddComponentOp);
+ }
+
+ if (AuthorityChangedOp != nullptr)
+ {
+ UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Processing Translation component authority change to bootstrap SpatialVirtualWorkerTranslator."));
+ FoundOps.Add(AuthorityChangedOp);
+ }
+
+ if (ComponentUpdateOp != nullptr)
+ {
+ UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Processing Translation component update to bootstrap SpatialVirtualWorkerTranslator."));
+ FoundOps.Add(ComponentUpdateOp);
+ }
+ }
+
SelectiveProcessOps(FoundOps);
- if (PackageMap->IsEntityPoolReady() && GlobalStateManager->IsReadyToCallBeginPlay())
+ if (PackageMap->IsEntityPoolReady() &&
+ GlobalStateManager->IsReady() &&
+ (!VirtualWorkerTranslator.IsValid() || VirtualWorkerTranslator->IsReady()))
{
// 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("Not yet ready to begin processing, still processing startup ops."));
return false;
}
@@ -2259,3 +2570,40 @@ void USpatialNetDriver::TrackTombstone(const Worker_EntityId EntityId)
TombstonedEntities.Add(EntityId);
}
#endif
+
+// 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());
+ if (SpatialDebugger != nullptr)
+ {
+ UE_LOG(LogSpatialOSNetDriver, Error, TEXT("SpatialDebugger should only be set once on each client!"));
+ return;
+ }
+
+ SpatialDebugger = InSpatialDebugger;
+}
+
+FUnrealObjectRef USpatialNetDriver::GetCurrentPlayerControllerRef()
+{
+ if (USpatialNetConnection* NetConnection = GetSpatialOSNetConnection())
+ {
+ if (APlayerController* PlayerController = Cast(NetConnection->OwningActor))
+ {
+ if (PackageMap)
+ {
+ return PackageMap->GetUnrealObjectRefFromObject(PlayerController);
+ }
+ }
+ }
+ return FUnrealObjectRef::NULL_OBJECT_REF;
+}
+
+// This is only called if this worker has been selected by SpatialOS to be authoritative
+// for the TranslationManager, otherwise the manager will never be instantiated.
+void USpatialNetDriver::InitializeVirtualWorkerTranslationManager()
+{
+ VirtualWorkerTranslationManager = MakeUnique(Receiver, Connection, VirtualWorkerTranslator.Get());
+ VirtualWorkerTranslationManager->AddVirtualWorkerIds(LoadBalanceStrategy->GetVirtualWorkerIds());
+}
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp
index 5bf68f30ae..a96e8fa76c 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp
@@ -9,6 +9,7 @@
#include "EngineClasses/SpatialActorChannel.h"
#include "EngineClasses/SpatialNetDriver.h"
+#include "EngineClasses/SpatialNetBitReader.h"
#include "Interop/Connection/SpatialWorkerConnection.h"
#include "Interop/SpatialReceiver.h"
#include "Interop/SpatialSender.h"
@@ -31,10 +32,10 @@ void USpatialPackageMapClient::Init(USpatialNetDriver* NetDriver, FTimerManager*
}
}
-void GetSubobjects(UObject* Object, TArray& InSubobjects)
+void GetSubobjects(UObject* ParentObject, TArray& InSubobjects)
{
InSubobjects.Empty();
- ForEachObjectWithOuter(Object, [&InSubobjects](UObject* Object)
+ ForEachObjectWithOuter(ParentObject, [&InSubobjects](UObject* Object)
{
// Objects can only be allocated NetGUIDs if this is true.
if (Object->IsSupportedForNetworking() && !Object->IsPendingKill() && !Object->IsEditorOnly())
@@ -68,6 +69,12 @@ Worker_EntityId USpatialPackageMapClient::AllocateEntityIdAndResolveActor(AActor
check(Actor);
checkf(bIsServer, TEXT("Tried to allocate an Entity ID on the client, this shouldn't happen."));
+ if (!IsEntityPoolReady())
+ {
+ UE_LOG(LogSpatialPackageMap, Error, TEXT("EntityPool must be ready when resolving an Actor: %s"), *Actor->GetName());
+ return SpatialConstants::INVALID_ENTITY_ID;
+ }
+
Worker_EntityId EntityId = EntityPool->GetNextEntityId();
if (EntityId == SpatialConstants::INVALID_ENTITY_ID)
{
@@ -76,7 +83,11 @@ Worker_EntityId USpatialPackageMapClient::AllocateEntityIdAndResolveActor(AActor
}
// Register Actor with package map since we know what the entity id is.
- ResolveEntityActor(Actor, EntityId);
+ if (!ResolveEntityActor(Actor, EntityId))
+ {
+ UE_LOG(LogSpatialPackageMap, Error, TEXT("Unable to resolve an Entity for Actor: %s"), *Actor->GetName());
+ return SpatialConstants::INVALID_ENTITY_ID;
+ }
return EntityId;
}
@@ -133,7 +144,7 @@ void USpatialPackageMapClient::RemovePendingCreationEntityId(Worker_EntityId Ent
PendingCreationEntityIds.Remove(EntityId);
}
-FNetworkGUID USpatialPackageMapClient::ResolveEntityActor(AActor* Actor, Worker_EntityId EntityId)
+bool USpatialPackageMapClient::ResolveEntityActor(AActor* Actor, Worker_EntityId EntityId)
{
FSpatialNetGUIDCache* SpatialGuidCache = static_cast(GuidCache.Get());
FNetworkGUID NetGUID = SpatialGuidCache->GetNetGUIDFromEntityId(EntityId);
@@ -143,7 +154,14 @@ FNetworkGUID USpatialPackageMapClient::ResolveEntityActor(AActor* Actor, Worker_
{
NetGUID = SpatialGuidCache->AssignNewEntityActorNetGUID(Actor, EntityId);
}
- return NetGUID;
+
+ if (GetEntityIdFromObject(Actor) != EntityId)
+ {
+ UE_LOG(LogSpatialPackageMap, Error, TEXT("ResolveEntityActor failed for Actor: %s with NetGUID: %s and passed entity ID: %lld"), *Actor->GetName(), *NetGUID.ToString(), EntityId);
+ return false;
+ }
+
+ return NetGUID.IsValid();
}
void USpatialPackageMapClient::ResolveSubobject(UObject* Object, const FUnrealObjectRef& ObjectRef)
@@ -224,7 +242,7 @@ TWeakObjectPtr USpatialPackageMapClient::GetObjectFromEntityId(const Wo
return GetObjectFromUnrealObjectRef(FUnrealObjectRef(EntityId, 0));
}
-FUnrealObjectRef USpatialPackageMapClient::GetUnrealObjectRefFromObject(UObject* Object)
+FUnrealObjectRef USpatialPackageMapClient::GetUnrealObjectRefFromObject(const UObject* Object)
{
if (Object == nullptr)
{
@@ -290,9 +308,17 @@ bool USpatialPackageMapClient::IsEntityPoolReady() const
bool USpatialPackageMapClient::SerializeObject(FArchive& Ar, UClass* InClass, UObject*& Obj, FNetworkGUID *OutNetGUID)
{
// Super::SerializeObject is not called here on purpose
- Ar << Obj;
+ if (Ar.IsSaving())
+ {
+ Ar << Obj;
+ return true;
+ }
- return true;
+ FSpatialNetBitReader& Reader = static_cast(Ar);
+ bool bUnresolved = false;
+ Obj = Reader.ReadObject(bUnresolved);
+
+ return !bUnresolved;
}
const FClassInfo* USpatialPackageMapClient::TryResolveNewDynamicSubobjectAndGetClassInfo(UObject* Object)
@@ -367,14 +393,6 @@ FNetworkGUID FSpatialNetGUIDCache::AssignNewEntityActorNetGUID(AActor* Actor, Wo
UE_LOG(LogSpatialPackageMap, Verbose, TEXT("Registered new object ref for actor: %s. NetGUID: %s, entity ID: %lld"),
*Actor->GetName(), *NetGUID.ToString(), EntityId);
- // This will be null when being used in the snapshot generator
-#if WITH_EDITOR
- if (Receiver != nullptr)
-#endif
- {
- Receiver->ResolvePendingOperations(Actor, EntityObjectRef);
- }
-
const FClassInfo& Info = SpatialNetDriver->ClassInfoManager->GetOrCreateClassInfoByClass(Actor->GetClass());
const SubobjectToOffsetMap& SubobjectToOffset = SpatialGDK::CreateOffsetMapFromActor(Actor, Info);
@@ -412,14 +430,6 @@ FNetworkGUID FSpatialNetGUIDCache::AssignNewEntityActorNetGUID(AActor* Actor, Wo
UE_LOG(LogSpatialPackageMap, Verbose, TEXT("Registered new object ref for subobject %s inside actor %s. NetGUID: %s, object ref: %s"),
*Subobject->GetName(), *Actor->GetName(), *SubobjectNetGUID.ToString(), *EntityIdSubobjectRef.ToString());
-
- // This will be null when being used in the snapshot generator
-#if WITH_EDITOR
- if (Receiver != nullptr)
-#endif
- {
- Receiver->ResolvePendingOperations(Subobject, EntityIdSubobjectRef);
- }
}
return NetGUID;
@@ -429,8 +439,6 @@ void FSpatialNetGUIDCache::AssignNewSubobjectNetGUID(UObject* Subobject, const F
{
FNetworkGUID SubobjectNetGUID = GetOrAssignNetGUID_SpatialGDK(Subobject);
RegisterObjectRef(SubobjectNetGUID, SubobjectRef);
-
- Cast(Driver)->Receiver->ResolvePendingOperations(Subobject, SubobjectRef);
}
// Recursively assign netguids to the outer chain of a UObject. Then associate them with their Spatial representation (FUnrealObjectRef)
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialReplicationGraph.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialReplicationGraph.cpp
new file mode 100644
index 0000000000..931d5de054
--- /dev/null
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialReplicationGraph.cpp
@@ -0,0 +1,21 @@
+// Copyright (c) Improbable Worlds Ltd, All Rights Reserved
+
+#include "EngineClasses/SpatialReplicationGraph.h"
+
+#include "EngineClasses/SpatialActorChannel.h"
+#include "EngineClasses/SpatialNetDriver.h"
+
+UActorChannel* USpatialReplicationGraph::GetOrCreateSpatialActorChannel(UObject* TargetObject)
+{
+ if (TargetObject != nullptr)
+ {
+ if (USpatialNetDriver* SpatialNetDriver = Cast(NetDriver))
+ {
+ return SpatialNetDriver->GetOrCreateSpatialActorChannel(TargetObject);
+ }
+
+ checkNoEntry();
+ }
+
+ return nullptr;
+}
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp
new file mode 100644
index 0000000000..d376bf6ff2
--- /dev/null
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp
@@ -0,0 +1,206 @@
+// Copyright (c) Improbable Worlds Ltd, All Rights Reserved
+
+#include "EngineClasses/SpatialVirtualWorkerTranslationManager.h"
+#include "EngineClasses/SpatialVirtualWorkerTranslator.h"
+#include "Interop/Connection/SpatialWorkerConnection.h"
+#include "Interop/SpatialOSDispatcherInterface.h"
+#include "SpatialConstants.h"
+#include "Utils/SchemaUtils.h"
+
+DEFINE_LOG_CATEGORY(LogSpatialVirtualWorkerTranslationManager);
+
+SpatialVirtualWorkerTranslationManager::SpatialVirtualWorkerTranslationManager(
+ SpatialOSDispatcherInterface* InReceiver,
+ SpatialOSWorkerInterface* InConnection,
+ SpatialVirtualWorkerTranslator* InTranslator)
+ : Receiver(InReceiver)
+ , Connection(InConnection)
+ , Translator(InTranslator)
+ , bWorkerEntityQueryInFlight(false)
+{}
+
+void SpatialVirtualWorkerTranslationManager::AddVirtualWorkerIds(const TSet& InVirtualWorkerIds)
+{
+ // 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)
+ {
+ UnassignedVirtualWorkers.Enqueue(VirtualWorkerId);
+ }
+}
+
+void SpatialVirtualWorkerTranslationManager::AuthorityChanged(const Worker_AuthorityChangeOp& AuthOp)
+{
+ check(AuthOp.component_id == SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID);
+
+ const bool bAuthoritative = AuthOp.authority == WORKER_AUTHORITY_AUTHORITATIVE;
+
+ if (!bAuthoritative)
+ {
+ UE_LOG(LogSpatialVirtualWorkerTranslationManager, Error, TEXT("Lost authority over the translation mapping. This is not supported."));
+ return;
+ }
+
+ UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT("This worker now has authority over the VirtualWorker translation."));
+
+ // TODO(zoning): The prototype had an unassigned workers list. Need to follow up with Tim/Chris about whether
+ // that is necessary or we can continue to use the (possibly) stale list until we receive the query response.
+
+ // Query for all connection entities, so we can detect if some worker has died and needs to be updated in
+ // the mapping.
+ QueryForServerWorkerEntities();
+}
+
+// For each entry in the map, write a VirtualWorkerMapping type object to the Schema object.
+void SpatialVirtualWorkerTranslationManager::WriteMappingToSchema(Schema_Object* Object) const
+{
+ for (const auto& Entry : VirtualToPhysicalWorkerMapping)
+ {
+ Schema_Object* EntryObject = Schema_AddObject(Object, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_MAPPING_ID);
+ Schema_AddUint32(EntryObject, SpatialConstants::MAPPING_VIRTUAL_WORKER_ID, Entry.Key);
+ SpatialGDK::AddStringToSchema(EntryObject, SpatialConstants::MAPPING_PHYSICAL_WORKER_NAME, Entry.Value.Key);
+ Schema_AddEntityId(EntryObject, SpatialConstants::MAPPING_SERVER_WORKER_ENTITY_ID, Entry.Value.Value);
+ }
+}
+
+// This method is called on the worker who is authoritative over the translation mapping. Based on the results of the
+// system entity query, assign the VirtualWorkerIds to the workers represented by the system entities.
+void SpatialVirtualWorkerTranslationManager::ConstructVirtualWorkerMappingFromQueryResponse(const Worker_EntityQueryResponseOp& Op)
+{
+ // The query response is an array of entities. Each of these represents a worker.
+ for (uint32_t i = 0; i < Op.result_count; ++i)
+ {
+ const Worker_Entity& Entity = Op.results[i];
+ for (uint32_t j = 0; j < Entity.component_count; j++)
+ {
+ const Worker_ComponentData& Data = Entity.components[j];
+ // System entities which represent workers have a component on them which specifies the SpatialOS worker ID,
+ // which is the string we use to refer to them as a physical worker ID.
+ if (Data.component_id == SpatialConstants::SERVER_WORKER_COMPONENT_ID)
+ {
+ const Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type);
+
+ // The translator should only acknowledge workers that are ready to begin play. This means we can make
+ // guarantees based on where non-GSM-authoritative servers canBeginPlay=true as an AddComponent
+ // or ComponentUpdate op. This affects how startup Actors are treated in a zoned environment.
+ const bool bWorkerIsReadyToBeginPlay = SpatialGDK::GetBoolFromSchema(ComponentObject, SpatialConstants::SERVER_WORKER_READY_TO_BEGIN_PLAY_ID);
+ if (!bWorkerIsReadyToBeginPlay)
+ {
+ continue;
+ }
+
+ // If we didn't find all our server worker entities the first time, future query responses should
+ // ignore workers that we have already assigned a virtual worker ID.
+ if (!UnassignedVirtualWorkers.IsEmpty())
+ {
+ // TODO(zoning): Currently, this only works if server workers never die. Once we want to support replacing
+ // workers, this will need to process UnassignWorker before processing AssignWorker.
+ AssignWorker(SpatialGDK::GetStringFromSchema(ComponentObject, SpatialConstants::SERVER_WORKER_NAME_ID), Entity.entity_id);
+ }
+ }
+ }
+ }
+}
+
+// This will be called on the worker authoritative for the translation mapping to push the new version of the map
+// to the SpatialOS storage.
+void SpatialVirtualWorkerTranslationManager::SendVirtualWorkerMappingUpdate()
+{
+ // Construct the mapping update based on the local virtual worker to physical worker mapping.
+ FWorkerComponentUpdate Update = {};
+ Update.component_id = SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID;
+ Update.schema_type = Schema_CreateComponentUpdate();
+ Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update.schema_type);
+
+ WriteMappingToSchema(UpdateObject);
+
+ check(Connection != nullptr);
+ Connection->SendComponentUpdate(SpatialConstants::INITIAL_VIRTUAL_WORKER_TRANSLATOR_ENTITY_ID, &Update);
+
+ // The Translator on the worker which hosts the manager won't get the component update notification,
+ // so send it across directly.
+ check(Translator != nullptr);
+ Translator->ApplyVirtualWorkerManagerData(UpdateObject);
+}
+
+void SpatialVirtualWorkerTranslationManager::QueryForServerWorkerEntities()
+{
+ UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT("Sending query for WorkerEntities"));
+
+ if (bWorkerEntityQueryInFlight)
+ {
+ UE_LOG(LogSpatialVirtualWorkerTranslationManager, Warning, TEXT("Trying to query for worker entities while a previous query is still in flight!"));
+ return;
+ }
+
+ // Create a query for all the server worker entities. This will be used
+ // to find physical workers which the virtual workers will map to.
+ Worker_ComponentConstraint WorkerEntityComponentConstraint{};
+ WorkerEntityComponentConstraint.component_id = SpatialConstants::SERVER_WORKER_COMPONENT_ID;
+
+ Worker_Constraint WorkerEntityConstraint{};
+ WorkerEntityConstraint.constraint_type = WORKER_CONSTRAINT_TYPE_COMPONENT;
+ WorkerEntityConstraint.constraint.component_constraint = WorkerEntityComponentConstraint;
+
+ Worker_EntityQuery WorkerEntityQuery{};
+ WorkerEntityQuery.constraint = WorkerEntityConstraint;
+ WorkerEntityQuery.result_type = WORKER_RESULT_TYPE_SNAPSHOT;
+
+ // Make the query.
+ check(Connection != nullptr);
+ Worker_RequestId RequestID = Connection->SendEntityQueryRequest(&WorkerEntityQuery);
+ bWorkerEntityQueryInFlight = true;
+
+ // Register a method to handle the query response.
+ EntityQueryDelegate ServerWorkerEntityQueryDelegate;
+ ServerWorkerEntityQueryDelegate.BindRaw(this, &SpatialVirtualWorkerTranslationManager::ServerWorkerEntityQueryDelegate);
+ check(Receiver != nullptr);
+ Receiver->AddEntityQueryDelegate(RequestID, ServerWorkerEntityQueryDelegate);
+}
+
+// This method allows the translation manager to deal with the returned list of server worker entities when they are received.
+// Note that this worker may have lost authority for the translation mapping in the meantime, so it's possible the
+// returned information will be thrown away.
+void SpatialVirtualWorkerTranslationManager::ServerWorkerEntityQueryDelegate(const Worker_EntityQueryResponseOp& Op)
+{
+ bWorkerEntityQueryInFlight = false;
+
+ if (Op.status_code != WORKER_STATUS_CODE_SUCCESS)
+ {
+ UE_LOG(LogSpatialVirtualWorkerTranslationManager, Warning, TEXT("Could not find ServerWorker Entities via entity query: %s, retrying."), UTF8_TO_TCHAR(Op.message));
+ }
+ else
+ {
+ UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT(" Processing ServerWorker Entity query response"));
+ ConstructVirtualWorkerMappingFromQueryResponse(Op);
+ }
+
+ // If the translation mapping is complete, publish it. Otherwise retry the server worker entity query.
+ if (UnassignedVirtualWorkers.IsEmpty())
+ {
+ SendVirtualWorkerMappingUpdate();
+ }
+ else
+ {
+ UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT("Waiting for all virtual workers to be assigned before publishing translation update."));
+ QueryForServerWorkerEntities();
+ }
+}
+
+void SpatialVirtualWorkerTranslationManager::AssignWorker(const PhysicalWorkerName& Name, const Worker_EntityId& ServerWorkerEntityId)
+{
+ if (PhysicalToVirtualWorkerMapping.Contains(Name))
+ {
+ return;
+ }
+
+ // Get a VirtualWorkerId from the list of unassigned work.
+ VirtualWorkerId Id;
+ UnassignedVirtualWorkers.Dequeue(Id);
+
+ VirtualToPhysicalWorkerMapping.Add(Id, MakeTuple(Name, ServerWorkerEntityId));
+ PhysicalToVirtualWorkerMapping.Add(Name, Id);
+
+ UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT("Assigned VirtualWorker %d to simulate on Worker %s"), Id, *Name);
+}
diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslator.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslator.cpp
index 248ede17e6..7debc8eb46 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslator.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslator.cpp
@@ -1,105 +1,88 @@
// Copyright (c) Improbable Worlds Ltd, All Rights Reserved
#include "EngineClasses/SpatialVirtualWorkerTranslator.h"
-#include "EngineClasses/SpatialNetDriver.h"
-#include "Interop/Connection/SpatialWorkerConnection.h"
-#include "Interop/SpatialReceiver.h"
-#include "Interop/SpatialStaticComponentView.h"
+#include "LoadBalancing/AbstractLBStrategy.h"
#include "SpatialConstants.h"
#include "Utils/SchemaUtils.h"
DEFINE_LOG_CATEGORY(LogSpatialVirtualWorkerTranslator);
-SpatialVirtualWorkerTranslator::SpatialVirtualWorkerTranslator()
- : NetDriver(nullptr)
- , bWorkerEntityQueryInFlight(false)
+SpatialVirtualWorkerTranslator::SpatialVirtualWorkerTranslator(UAbstractLBStrategy* InLoadBalanceStrategy,
+ PhysicalWorkerName InPhysicalWorkerName)
+ : LoadBalanceStrategy(InLoadBalanceStrategy)
, bIsReady(false)
+ , LocalPhysicalWorkerName(InPhysicalWorkerName)
+ , LocalVirtualWorkerId(SpatialConstants::INVALID_VIRTUAL_WORKER_ID)
{}
-void SpatialVirtualWorkerTranslator::Init(USpatialNetDriver* InNetDriver)
+const PhysicalWorkerName* SpatialVirtualWorkerTranslator::GetPhysicalWorkerForVirtualWorker(VirtualWorkerId Id) const
{
- NetDriver = InNetDriver;
- // If this is being run from tests, NetDriver will be null.
- WorkerId = (NetDriver != nullptr && NetDriver->Connection != nullptr) ? NetDriver->Connection->GetWorkerId() : "InvalidWorkerId";
-}
-
-void SpatialVirtualWorkerTranslator::SetDesiredVirtualWorkerCount(uint32 NumberOfVirtualWorkers)
-{
- // Currently, this should only be called once on startup. In the future we may allow for more
- // flexibility.
- if (bIsReady)
- {
- UE_LOG(LogSpatialVirtualWorkerTranslator, Warning, TEXT("(%s) SetDesiredVirtualWorkerCount called after the translator is ready, ignoring."), *WorkerId);
- }
-
- UnassignedVirtualWorkers.Empty();
- for (uint32 i = 0; i < NumberOfVirtualWorkers; i++)
+ if (const TPair* PhysicalWorkerInfo = VirtualToPhysicalWorkerMapping.Find(Id))
{
- UnassignedVirtualWorkers.Enqueue(i);
+ return &PhysicalWorkerInfo->Key;
}
- // TODO(zoning): We likely want to not declare ready until we have received the connection entity query and have
- // synchronized with the strategy and enforcer.
- bIsReady = true;
+ return nullptr;
}
-
-const FString* SpatialVirtualWorkerTranslator::GetPhysicalWorkerForVirtualWorker(VirtualWorkerId id)
+Worker_EntityId SpatialVirtualWorkerTranslator::GetServerWorkerEntityForVirtualWorker(VirtualWorkerId Id) const
{
- return VirtualToPhysicalWorkerMapping.Find(id);
+ if (const TPair* PhysicalWorkerInfo = VirtualToPhysicalWorkerMapping.Find(Id))
+ {
+ return PhysicalWorkerInfo->Value;
+ }
+
+ return SpatialConstants::INVALID_ENTITY_ID;
}
void SpatialVirtualWorkerTranslator::ApplyVirtualWorkerManagerData(Schema_Object* ComponentObject)
{
- UE_LOG(LogSpatialVirtualWorkerTranslator, Log, TEXT("(%s) ApplyVirtualWorkerManagerData"), *WorkerId);
+ UE_LOG(LogSpatialVirtualWorkerTranslator, Log, TEXT("(%d) ApplyVirtualWorkerManagerData"), LocalVirtualWorkerId);
// The translation schema is a list of Mappings, where each entry has a virtual and physical worker ID.
ApplyMappingFromSchema(ComponentObject);
for (const auto& Entry : VirtualToPhysicalWorkerMapping)
{
- UE_LOG(LogSpatialVirtualWorkerTranslator, Log, TEXT("(%s) assignment: %d - %s"), *WorkerId, Entry.Key, *(Entry.Value));
+ UE_LOG(LogSpatialVirtualWorkerTranslator, Log, TEXT("Translator assignment: Virtual Worker %d to %s with server worker entity: %lld"), Entry.Key, *(Entry.Value.Key), Entry.Value.Value);
}
}
-void SpatialVirtualWorkerTranslator::OnComponentUpdated(const Worker_ComponentUpdateOp& Op)
+// Check to see if this worker's physical worker name is in the mapping. If it isn't, it's possibly an old mapping.
+// This is needed to give good behaviour across restarts. It's not very efficient, but it should happen only a few times
+// after a PiE restart.
+bool SpatialVirtualWorkerTranslator::IsValidMapping(Schema_Object* Object) const
{
- if (Op.update.component_id == SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID)
- {
- // TODO(zoning): Check for whether the ACL should be updated.
- }
-}
-
-void SpatialVirtualWorkerTranslator::AuthorityChanged(const Worker_AuthorityChangeOp& AuthOp)
-{
- const bool bAuthoritative = AuthOp.authority == WORKER_AUTHORITY_AUTHORITATIVE;
+ int32 TranslationCount = (int32)Schema_GetObjectCount(Object, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_MAPPING_ID);
- if (AuthOp.component_id == SpatialConstants::VIRTUAL_WORKER_TRANSLATION_MAPPING_ID)
+ for (int32 i = 0; i < TranslationCount; i++)
{
- UE_LOG(LogSpatialVirtualWorkerTranslator, Log, TEXT("(%s) Authority over the VirtualWorkerTranslator has changed. This worker %s authority."), *WorkerId, bAuthoritative ? TEXT("now has") : TEXT("does not have"));
-
- if (bAuthoritative)
+ // Get each entry of the list and then unpack the virtual and physical IDs from the entry.
+ Schema_Object* MappingObject = Schema_IndexObject(Object, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_MAPPING_ID, i);
+ if (SpatialGDK::GetStringFromSchema(MappingObject, SpatialConstants::MAPPING_PHYSICAL_WORKER_NAME) == LocalPhysicalWorkerName)
{
- // TODO(zoning): The prototype had an unassigned workers list. Need to follow up with Tim/Chris about whether
- // that is necessary or we can continue to use the (possibly) stale list until we receive the query response.
-
- // Query for all connection entities, so we can detect if some worker has died and needs to be updated in
- // the mapping.
- QueryForWorkerEntities();
+ VirtualWorkerId ReceivedVirtualWorkerId = Schema_GetUint32(MappingObject, SpatialConstants::MAPPING_VIRTUAL_WORKER_ID);
+ if (LocalVirtualWorkerId != SpatialConstants::INVALID_VIRTUAL_WORKER_ID && LocalVirtualWorkerId != ReceivedVirtualWorkerId)
+ {
+ UE_LOG(LogSpatialVirtualWorkerTranslator, Error, TEXT("Received mapping containing a new and updated virtual worker ID, this shouldn't happen."));
+ return false;
+ }
+ return true;
}
}
- // TODO(zoning): If authority is received on the ACL component, we may need to update it.
+
+ return false;
}
// The translation schema is a list of Mappings, where each entry has a virtual and physical worker ID.
-// This method should only be called on workers who are not authoritative over the mapping.
-// TODO(harkness): Is this true? I think this will be called once the first time we get authority?
+// This method should only be called on workers who are not authoritative over the mapping and also when
+// a worker first becomes authoritative for the mapping.
void SpatialVirtualWorkerTranslator::ApplyMappingFromSchema(Schema_Object* Object)
{
- if (NetDriver != nullptr &&
- NetDriver->StaticComponentView->HasAuthority(SpatialConstants::INITIAL_VIRTUAL_WORKER_TRANSLATOR_ENTITY_ID, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID))
+ if (!IsValidMapping(Object))
{
- UE_LOG(LogSpatialVirtualWorkerTranslator, Log, TEXT("(%s) ApplyMappingFromSchema called, but this worker is authoritative, ignoring"), *WorkerId);
+ UE_LOG(LogSpatialVirtualWorkerTranslator, Log, TEXT("Received invalid mapping, likely due to PiE restart, will wait for a valid version."));
+ return;
}
// Resize the map to accept the new data.
@@ -111,162 +94,31 @@ void SpatialVirtualWorkerTranslator::ApplyMappingFromSchema(Schema_Object* Objec
{
// Get each entry of the list and then unpack the virtual and physical IDs from the entry.
Schema_Object* MappingObject = Schema_IndexObject(Object, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_MAPPING_ID, i);
- uint32 VirtualWorkerId = Schema_GetUint32(MappingObject, SpatialConstants::MAPPING_VIRTUAL_WORKER_ID);
- FString PhysicalWorkerName = SpatialGDK::GetStringFromSchema(MappingObject, SpatialConstants::MAPPING_PHYSICAL_WORKER_NAME);
+ VirtualWorkerId VirtualWorkerId = Schema_GetUint32(MappingObject, SpatialConstants::MAPPING_VIRTUAL_WORKER_ID);
+ PhysicalWorkerName PhysicalWorkerName = SpatialGDK::GetStringFromSchema(MappingObject, SpatialConstants::MAPPING_PHYSICAL_WORKER_NAME);
+ Worker_EntityId ServerWorkerEntityId = Schema_GetEntityId(MappingObject, SpatialConstants::MAPPING_SERVER_WORKER_ENTITY_ID);
// Insert each into the provided map.
- VirtualToPhysicalWorkerMapping.Add(VirtualWorkerId, PhysicalWorkerName);
+ UpdateMapping(VirtualWorkerId, PhysicalWorkerName, ServerWorkerEntityId);
}
}
-// For each entry in the map, write a VirtualWorkerMapping type object to the Schema object.
-void SpatialVirtualWorkerTranslator::WriteMappingToSchema(Schema_Object* Object)
+void SpatialVirtualWorkerTranslator::UpdateMapping(VirtualWorkerId Id, PhysicalWorkerName Name, Worker_EntityId ServerWorkerEntityId)
{
- for (auto& Entry : VirtualToPhysicalWorkerMapping)
- {
- Schema_Object* EntryObject = Schema_AddObject(Object, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_MAPPING_ID);
- Schema_AddUint32(EntryObject, SpatialConstants::MAPPING_VIRTUAL_WORKER_ID, Entry.Key);
- SpatialGDK::AddStringToSchema(EntryObject, SpatialConstants::MAPPING_PHYSICAL_WORKER_NAME, Entry.Value);
- }
-}
+ VirtualToPhysicalWorkerMapping.Add(Id, MakeTuple(Name, ServerWorkerEntityId));
-// This method is called on the worker who is authoritative over the translation mapping. Based on the results of the
-// system entity query, assign the VirtualWorkerIds to the workers represented by the system entities.
-void SpatialVirtualWorkerTranslator::ConstructVirtualWorkerMappingFromQueryResponse(const Worker_EntityQueryResponseOp& Op)
-{
- // The query response is an array of entities. Each of these represents a worker.
- for (uint32_t i = 0; i < Op.result_count; ++i)
+ if (LocalVirtualWorkerId == SpatialConstants::INVALID_VIRTUAL_WORKER_ID && Name == LocalPhysicalWorkerName)
{
- const Worker_Entity& Entity = Op.results[i];
- for (uint32_t j = 0; j < Entity.component_count; j++)
- {
- const Worker_ComponentData& Data = Entity.components[j];
- // System entities which represent workers have a component on them which specifies the SpatialOS worker ID,
- // which is the string we use to refer to them as a physical worker ID.
- if (Data.component_id == SpatialConstants::WORKER_COMPONENT_ID)
- {
- const Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type);
-
- const FString& WorkerType = SpatialGDK::GetStringFromSchema(ComponentObject, SpatialConstants::WORKER_TYPE_ID);
+ LocalVirtualWorkerId = Id;
+ bIsReady = true;
- // TODO(zoning): Currently, this only works if server workers never die. Once we want to support replacing
- // workers, this will need to process UnassignWorker before processing AssignWorker.
- if (WorkerType.Equals(SpatialConstants::DefaultServerWorkerType.ToString()) &&
- !UnassignedVirtualWorkers.IsEmpty())
- {
- AssignWorker(SpatialGDK::GetStringFromSchema(ComponentObject, SpatialConstants::WORKER_ID_ID));
- }
- }
+ // Tell the strategy about the local virtual worker id. This is an "if" and not a "check" to allow unit tests which don't
+ // provide a strategy.
+ if (LoadBalanceStrategy.IsValid())
+ {
+ LoadBalanceStrategy->SetLocalVirtualWorkerId(LocalVirtualWorkerId);
}
- }
-}
-
-// This will be called on the worker authoritative for the translation mapping to push the new version of the map
-// to the spatialOS storage.
-void SpatialVirtualWorkerTranslator::SendVirtualWorkerMappingUpdate()
-{
- if (!bIsReady || NetDriver == nullptr)
- {
- return;
- }
-
- UE_LOG(LogSpatialVirtualWorkerTranslator, Log, TEXT("(%s) SendVirtualWorkerMappingUpdate"), *WorkerId);
-
- check(NetDriver->StaticComponentView->HasAuthority(SpatialConstants::INITIAL_VIRTUAL_WORKER_TRANSLATOR_ENTITY_ID, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID));
-
- // Construct the mapping update based on the local virtual worker to physical worker mapping.
- Worker_ComponentUpdate Update = {};
- Update.component_id = SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID;
- Update.schema_type = Schema_CreateComponentUpdate();
- Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update.schema_type);
-
- WriteMappingToSchema(UpdateObject);
-
- NetDriver->Connection->SendComponentUpdate(SpatialConstants::INITIAL_VIRTUAL_WORKER_TRANSLATOR_ENTITY_ID, &Update);
-
- // Broadcast locally since we won't receive the ComponentUpdate on this worker.
- // This is disabled until the Enforcer is available to update ACLs.
- // OnWorkerAssignmentChanged.Broadcast(VirtualWorkerAssignment);
-}
-
-void SpatialVirtualWorkerTranslator::QueryForWorkerEntities()
-{
- if (!bIsReady || NetDriver == nullptr)
- {
- return;
- }
-
- UE_LOG(LogSpatialVirtualWorkerTranslator, Log, TEXT("(%s) Sending query for WorkerEntities"), *WorkerId);
-
- checkf(!bWorkerEntityQueryInFlight, TEXT("(%s) Trying to query for worker entities while a previous query is still in flight!"), *WorkerId);
-
- if (!NetDriver->StaticComponentView->HasAuthority(SpatialConstants::INITIAL_VIRTUAL_WORKER_TRANSLATOR_ENTITY_ID, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_MAPPING_ID))
- {
- UE_LOG(LogSpatialVirtualWorkerTranslator, Log, TEXT("(%s) Trying QueryForWorkerEntities, but don't have authority over VIRTUAL_WORKER_MANAGER_COMPONENT. Aborting processing."), *WorkerId);
- return;
- }
-
- // Create a query for all the system entities which represent workers. This will be used
- // to find physical workers which the virtual workers will map to.
- Worker_ComponentConstraint WorkerEntityComponentConstraint{};
- WorkerEntityComponentConstraint.component_id = SpatialConstants::WORKER_COMPONENT_ID;
- Worker_Constraint WorkerEntityConstraint{};
- WorkerEntityConstraint.constraint_type = WORKER_CONSTRAINT_TYPE_COMPONENT;
- WorkerEntityConstraint.constraint.component_constraint = WorkerEntityComponentConstraint;
-
- Worker_EntityQuery WorkerEntityQuery{};
- WorkerEntityQuery.constraint = WorkerEntityConstraint;
- WorkerEntityQuery.result_type = WORKER_RESULT_TYPE_SNAPSHOT;
-
- // Make the query.
- Worker_RequestId RequestID;
- RequestID = NetDriver->Connection->SendEntityQueryRequest(&WorkerEntityQuery);
- bWorkerEntityQueryInFlight = true;
-
- // Register a method to handle the query response.
- EntityQueryDelegate WorkerEntityQueryDelegate;
- WorkerEntityQueryDelegate.BindRaw(this, &SpatialVirtualWorkerTranslator::WorkerEntityQueryDelegate);
- NetDriver->Receiver->AddEntityQueryDelegate(RequestID, WorkerEntityQueryDelegate);
-}
-
-// This method allows the translator to deal with the returned list of connection entities when they are received.
-// Note that this worker may have lost authority for the translation mapping in the meantime, so it's possible the
-// returned information will be thrown away.
-void SpatialVirtualWorkerTranslator::WorkerEntityQueryDelegate(const Worker_EntityQueryResponseOp& Op)
-{
- if (!NetDriver->StaticComponentView->HasAuthority(SpatialConstants::INITIAL_VIRTUAL_WORKER_TRANSLATOR_ENTITY_ID, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_MAPPING_ID))
- {
- UE_LOG(LogSpatialVirtualWorkerTranslator, Log, TEXT("(%s) Received response to WorkerEntityQuery, but don't have authority over VIRTUAL_WORKER_MANAGER_COMPONENT. Aborting processing."), *WorkerId);
- }
- else if (Op.status_code != WORKER_STATUS_CODE_SUCCESS)
- {
- UE_LOG(LogSpatialVirtualWorkerTranslator, Log, TEXT("(%s) Could not find Worker Entities via entity query: %s"), *WorkerId, UTF8_TO_TCHAR(Op.message));
- }
- else if (Op.result_count == 0)
- {
- UE_LOG(LogSpatialVirtualWorkerTranslator, Log, TEXT("(%s) Worker Entity query shows that Worker Entities do not yet exist in the world."), *WorkerId);
- }
- else
- {
- UE_LOG(LogSpatialVirtualWorkerTranslator, Log, TEXT("(%s) Processing Worker Entity query response"), *WorkerId);
- ConstructVirtualWorkerMappingFromQueryResponse(Op);
- SendVirtualWorkerMappingUpdate();
+ UE_LOG(LogSpatialVirtualWorkerTranslator, Log, TEXT("VirtualWorkerTranslator is now ready for loadbalancing."));
}
-
- bWorkerEntityQueryInFlight = false;
-}
-
-void SpatialVirtualWorkerTranslator::AssignWorker(const PhysicalWorkerName& Name)
-{
- check(!UnassignedVirtualWorkers.IsEmpty());
- check(NetDriver->StaticComponentView->HasAuthority(SpatialConstants::INITIAL_VIRTUAL_WORKER_TRANSLATOR_ENTITY_ID, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_MAPPING_ID));
-
- // Get a VirtualWorkerId from the list of unassigned work.
- VirtualWorkerId Id;
- UnassignedVirtualWorkers.Dequeue(Id);
-
- VirtualToPhysicalWorkerMapping.Add(Id, Name);
-
- UE_LOG(LogSpatialVirtualWorkerTranslator, Log, TEXT("(%s) Assigned VirtualWorker %d to simulate on Worker %s"), *WorkerId, Id, *Name);
}
diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp
new file mode 100644
index 0000000000..935c573e45
--- /dev/null
+++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp
@@ -0,0 +1,480 @@
+// Copyright (c) Improbable Worlds Ltd, All Rights Reserved
+
+#include "Interop/Connection/SpatialConnectionManager.h"
+#if WITH_EDITOR
+#include "Interop/Connection/EditorWorkerController.h"
+#endif
+
+#include "Async/Async.h"
+#include "Improbable/SpatialEngineConstants.h"
+#include "Misc/Paths.h"
+
+#include "Interop/Connection/SpatialWorkerConnection.h"
+#include "SpatialGDKSettings.h"
+#include "Utils/ErrorCodeRemapping.h"
+
+DEFINE_LOG_CATEGORY(LogSpatialConnectionManager);
+
+using namespace SpatialGDK;
+
+struct ConfigureConnection
+{
+ ConfigureConnection(const FConnectionConfig& InConfig, const bool bConnectAsClient)
+ : Config(InConfig)
+ , Params()
+ , WorkerType(*Config.WorkerType)
+ , ProtocolLogPrefix(*FormatProtocolPrefix())
+ {
+ Params = Worker_DefaultConnectionParameters();
+
+ Params.worker_type = WorkerType.Get();
+
+ Params.enable_protocol_logging_at_startup = Config.EnableProtocolLoggingAtStartup;
+ Params.protocol_logging.log_prefix = ProtocolLogPrefix.Get();
+
+ Params.component_vtable_count = 0;
+ Params.default_component_vtable = &DefaultVtable;
+
+ Params.network.connection_type = Config.LinkProtocol;
+ Params.network.use_external_ip = Config.UseExternalIp;
+ Params.network.modular_tcp.multiplex_level = Config.TcpMultiplexLevel;
+ if (Config.TcpNoDelay)
+ {
+ Params.network.modular_tcp.downstream_tcp.flush_delay_millis = 0;
+ Params.network.modular_tcp.upstream_tcp.flush_delay_millis = 0;
+ }
+
+ // We want the bridge to worker messages to be compressed; not the worker to bridge messages.
+ Params.network.modular_kcp.upstream_compression = nullptr;
+ Params.network.modular_kcp.downstream_compression = &EnableCompressionParams;
+
+ Params.network.modular_kcp.upstream_kcp.flush_interval_millis = Config.UdpUpstreamIntervalMS;
+ Params.network.modular_kcp.downstream_kcp.flush_interval_millis = Config.UdpDownstreamIntervalMS;
+
+#if WITH_EDITOR
+ Params.network.modular_tcp.downstream_heartbeat = &HeartbeatParams;
+ Params.network.modular_tcp.upstream_heartbeat = &HeartbeatParams;
+ Params.network.modular_kcp.downstream_heartbeat = &HeartbeatParams;
+ 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)
+ {
+ 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;
+ }
+
+ Params.enable_dynamic_components = true;
+ }
+
+ FString FormatProtocolPrefix() const
+ {
+ FString FinalProtocolLoggingPrefix = FPaths::ConvertRelativePathToFull(FPaths::ProjectLogDir());
+ if (!Config.ProtocolLoggingPrefix.IsEmpty())
+ {
+ FinalProtocolLoggingPrefix += Config.ProtocolLoggingPrefix;
+ }
+ else
+ {
+ FinalProtocolLoggingPrefix += Config.WorkerId;
+ }
+ return FinalProtocolLoggingPrefix;
+ }
+
+ const FConnectionConfig& Config;
+ Worker_ConnectionParameters Params;
+ FTCHARToUTF8 WorkerType;
+ FTCHARToUTF8 ProtocolLogPrefix;
+ Worker_ComponentVtable DefaultVtable{};
+ Worker_CompressionParameters EnableCompressionParams{};
+
+#if WITH_EDITOR
+ Worker_HeartbeatParameters HeartbeatParams{ WORKER_DEFAULTS_HEARTBEAT_INTERVAL_MILLIS, MAX_int64 };
+#endif
+};
+
+void USpatialConnectionManager::FinishDestroy()
+{
+ UE_LOG(LogSpatialConnectionManager, Log, TEXT("Destroying SpatialConnectionManager."));
+
+ DestroyConnection();
+
+ Super::FinishDestroy();
+}
+
+void USpatialConnectionManager::DestroyConnection()
+{
+ if (WorkerLocator)
+ {
+ AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [WorkerLocator = WorkerLocator]
+ {
+ Worker_Locator_Destroy(WorkerLocator);
+ });
+
+ WorkerLocator = nullptr;
+ }
+
+ if (WorkerConnection != nullptr)
+ {
+ WorkerConnection->DestroyConnection();
+ WorkerConnection = nullptr;
+ }
+
+ bIsConnected = false;
+}
+
+void USpatialConnectionManager::Connect(bool bInitAsClient, uint32 PlayInEditorID)
+{
+ if (bIsConnected)
+ {
+ check(bInitAsClient == bConnectAsClient);
+ AsyncTask(ENamedThreads::GameThread, [WeakThis = TWeakObjectPtr(this)]
+ {
+ if (WeakThis.IsValid())
+ {
+ WeakThis->OnConnectionSuccess();
+ }
+ else
+ {
+ UE_LOG(LogSpatialConnectionManager, Error, TEXT("SpatialConnectionManager is not valid but was already connected."));
+ }
+ });
+ return;
+ }
+
+ bConnectAsClient = bInitAsClient;
+
+ const USpatialGDKSettings* SpatialGDKSettings = GetDefault();
+ if (SpatialGDKSettings->bUseDevelopmentAuthenticationFlow && bInitAsClient)
+ {
+ DevAuthConfig.Deployment = SpatialGDKSettings->DevelopmentDeploymentToConnect;
+ DevAuthConfig.WorkerType = SpatialConstants::DefaultClientWorkerType.ToString();
+ DevAuthConfig.UseExternalIp = true;
+ StartDevelopmentAuth(SpatialGDKSettings->DevelopmentAuthenticationToken);
+ return;
+ }
+
+ switch (GetConnectionType())
+ {
+ case ESpatialConnectionType::Receptionist:
+ ConnectToReceptionist(PlayInEditorID);
+ break;
+ case ESpatialConnectionType::Locator:
+ ConnectToLocator(&LocatorConfig);
+ break;
+ case ESpatialConnectionType::DevAuthFlow:
+ StartDevelopmentAuth(DevAuthConfig.DevelopmentAuthToken);
+ break;
+ }
+}
+
+void USpatialConnectionManager::OnLoginTokens(void* UserData, const Worker_Alpha_LoginTokensResponse* LoginTokens)
+{
+ if (LoginTokens->status.code != WORKER_CONNECTION_STATUS_CODE_SUCCESS)
+ {
+ UE_LOG(LogSpatialWorkerConnection, Error, TEXT("Failed to get login token, StatusCode: %d, Error: %s"), LoginTokens->status.code, UTF8_TO_TCHAR(LoginTokens->status.detail));
+ return;
+ }
+
+ if (LoginTokens->login_token_count == 0)
+ {
+ UE_LOG(LogSpatialWorkerConnection, Warning, TEXT("No deployment found to connect to. Did you add the 'dev_login' tag to the deployment you want to connect to?"));
+ return;
+ }
+
+ UE_LOG(LogSpatialWorkerConnection, Verbose, TEXT("Successfully received LoginTokens, Count: %d"), LoginTokens->login_token_count);
+ USpatialConnectionManager* ConnectionManager = static_cast(UserData);
+ ConnectionManager->ProcessLoginTokensResponse(LoginTokens);
+}
+
+void USpatialConnectionManager::ProcessLoginTokensResponse(const Worker_Alpha_LoginTokensResponse* LoginTokens)
+{
+ // If LoginTokenResCallback is callable and returns true, return early.
+ if (LoginTokenResCallback && LoginTokenResCallback(LoginTokens))
+ {
+ return;
+ }
+
+ const 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);
+ }
+ else
+ {
+ for (uint32 i = 0; i < LoginTokens->login_token_count; i++)
+ {
+ FString DeploymentName = FString(LoginTokens->login_tokens[i].deployment_name);
+ if (DeploymentToConnect.Compare(DeploymentName) == 0)
+ {
+ DevAuthConfig.LoginToken = FString(LoginTokens->login_tokens[i].login_token);
+ break;
+ }
+ }
+ }
+ ConnectToLocator(&DevAuthConfig);
+}
+
+void USpatialConnectionManager::RequestDeploymentLoginTokens()
+{
+ Worker_Alpha_LoginTokensRequest LTParams{};
+ FTCHARToUTF8 PlayerIdentityToken(*DevAuthConfig.PlayerIdentityToken);
+ LTParams.player_identity_token = PlayerIdentityToken.Get();
+ FTCHARToUTF8 WorkerType(*DevAuthConfig.WorkerType);
+ LTParams.worker_type = WorkerType.Get();
+ LTParams.use_insecure_connection = false;
+
+ if (Worker_Alpha_LoginTokensResponseFuture* LTFuture = Worker_Alpha_CreateDevelopmentLoginTokensAsync(TCHAR_TO_UTF8(*DevAuthConfig.LocatorHost), SpatialConstants::LOCATOR_PORT, <Params))
+ {
+ Worker_Alpha_LoginTokensResponseFuture_Get(LTFuture, nullptr, this, &USpatialConnectionManager::OnLoginTokens);
+ }
+}
+
+void USpatialConnectionManager::OnPlayerIdentityToken(void* UserData, const Worker_Alpha_PlayerIdentityTokenResponse* PIToken)
+{
+ if (PIToken->status.code != WORKER_CONNECTION_STATUS_CODE_SUCCESS)
+ {
+ UE_LOG(LogSpatialWorkerConnection, Error, TEXT("Failed to get PlayerIdentityToken, StatusCode: %d, Error: %s"), PIToken->status.code, UTF8_TO_TCHAR(PIToken->status.detail));
+ return;
+ }
+
+ UE_LOG(LogSpatialWorkerConnection, Log, TEXT("Successfully received PIToken: %s"), UTF8_TO_TCHAR(PIToken->player_identity_token));
+ USpatialConnectionManager* ConnectionManager = static_cast(UserData);
+ ConnectionManager->DevAuthConfig.PlayerIdentityToken = UTF8_TO_TCHAR(PIToken->player_identity_token);
+
+ ConnectionManager->RequestDeploymentLoginTokens();
+}
+
+void USpatialConnectionManager::StartDevelopmentAuth(const FString& DevAuthToken)
+{
+ FTCHARToUTF8 DAToken(*DevAuthToken);
+ FTCHARToUTF8 PlayerId(*DevAuthConfig.PlayerId);
+ FTCHARToUTF8 DisplayName(*DevAuthConfig.DisplayName);
+ FTCHARToUTF8 MetaData(*DevAuthConfig.MetaData);
+
+ Worker_Alpha_PlayerIdentityTokenRequest PITParams{};
+ PITParams.development_authentication_token = DAToken.Get();
+ PITParams.player_id = PlayerId.Get();
+ PITParams.display_name = DisplayName.Get();
+ PITParams.metadata = MetaData.Get();
+ PITParams.use_insecure_connection = false;
+
+ if (Worker_Alpha_PlayerIdentityTokenResponseFuture* PITFuture = Worker_Alpha_CreateDevelopmentPlayerIdentityTokenAsync(TCHAR_TO_UTF8(*DevAuthConfig.LocatorHost), SpatialConstants::LOCATOR_PORT, &PITParams))
+ {
+ Worker_Alpha_PlayerIdentityTokenResponseFuture_Get(PITFuture, nullptr, this, &USpatialConnectionManager::OnPlayerIdentityToken);
+ }
+}
+
+void USpatialConnectionManager::ConnectToReceptionist(uint32 PlayInEditorID)
+{
+#if WITH_EDITOR
+ SpatialGDKServices::InitWorkers(bConnectAsClient, PlayInEditorID, ReceptionistConfig.WorkerId);
+#endif
+
+ ReceptionistConfig.PreConnectInit(bConnectAsClient);
+
+ ConfigureConnection ConnectionConfig(ReceptionistConfig, bConnectAsClient);
+
+ Worker_ConnectionFuture* ConnectionFuture = Worker_ConnectAsync(
+ TCHAR_TO_UTF8(*ReceptionistConfig.GetReceptionistHost()), ReceptionistConfig.ReceptionistPort,
+ TCHAR_TO_UTF8(*ReceptionistConfig.WorkerId), &ConnectionConfig.Params);
+
+ FinishConnecting(ConnectionFuture);
+}
+
+void USpatialConnectionManager::ConnectToLocator(FLocatorConfig* InLocatorConfig)
+{
+ if (InLocatorConfig == nullptr)
+ {
+ UE_LOG(LogSpatialWorkerConnection, Error, TEXT("Trying to connect to locator with invalid locator config"));
+ return;
+ }
+
+ InLocatorConfig->PreConnectInit(bConnectAsClient);
+
+ ConfigureConnection ConnectionConfig(*InLocatorConfig, bConnectAsClient);
+
+ FTCHARToUTF8 PlayerIdentityTokenCStr(*InLocatorConfig->PlayerIdentityToken);
+ FTCHARToUTF8 LoginTokenCStr(*InLocatorConfig->LoginToken);
+
+ Worker_LocatorParameters LocatorParams = {};
+ FString ProjectName;
+ FParse::Value(FCommandLine::Get(), TEXT("projectName"), ProjectName);
+ LocatorParams.project_name = TCHAR_TO_UTF8(*ProjectName);
+ LocatorParams.credentials_type = Worker_LocatorCredentialsTypes::WORKER_LOCATOR_PLAYER_IDENTITY_CREDENTIALS;
+ LocatorParams.player_identity.player_identity_token = PlayerIdentityTokenCStr.Get();
+ LocatorParams.player_identity.login_token = LoginTokenCStr.Get();
+
+ // Connect to the locator on the default port(0 will choose the default)
+ WorkerLocator = Worker_Locator_Create(TCHAR_TO_UTF8(*InLocatorConfig->LocatorHost), SpatialConstants::LOCATOR_PORT, &LocatorParams);
+
+ Worker_ConnectionFuture* ConnectionFuture = Worker_Locator_ConnectAsync(WorkerLocator, &ConnectionConfig.Params);
+
+ FinishConnecting(ConnectionFuture);
+}
+
+void USpatialConnectionManager::FinishConnecting(Worker_ConnectionFuture* ConnectionFuture)
+{
+ TWeakObjectPtr WeakSpatialConnectionManager(this);
+
+ AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [ConnectionFuture, WeakSpatialConnectionManager]
+ {
+ Worker_Connection* NewCAPIWorkerConnection = Worker_ConnectionFuture_Get(ConnectionFuture, nullptr);
+ Worker_ConnectionFuture_Destroy(ConnectionFuture);
+
+ AsyncTask(ENamedThreads::GameThread, [WeakSpatialConnectionManager, NewCAPIWorkerConnection]
+ {
+ if (!WeakSpatialConnectionManager.IsValid())
+ {
+ // The game instance was destroyed before the connection finished, so just clean up the connection.
+ Worker_Connection_Destroy(NewCAPIWorkerConnection);
+ return;
+ }
+
+ USpatialConnectionManager* SpatialConnectionManager = WeakSpatialConnectionManager.Get();
+
+ if (Worker_Connection_IsConnected(NewCAPIWorkerConnection))
+ {
+ SpatialConnectionManager->WorkerConnection = NewObject();
+ SpatialConnectionManager->WorkerConnection->SetConnection(NewCAPIWorkerConnection);
+ SpatialConnectionManager->OnConnectionSuccess();
+ }
+ else
+ {
+ uint8_t ConnectionStatusCode = Worker_Connection_GetConnectionStatusCode(NewCAPIWorkerConnection);
+ const FString ErrorMessage(UTF8_TO_TCHAR(Worker_Connection_GetConnectionStatusDetailString(NewCAPIWorkerConnection)));
+
+ // TODO: Try to reconnect - UNR-576
+ SpatialConnectionManager->OnConnectionFailure(ConnectionStatusCode, ErrorMessage);
+ }
+ });
+ });
+}
+
+ESpatialConnectionType USpatialConnectionManager::GetConnectionType() const
+{
+ return ConnectionType;
+}
+
+void USpatialConnectionManager::SetConnectionType(ESpatialConnectionType InConnectionType)
+{
+ // The locator config may not have been initialized
+ check(!(InConnectionType == ESpatialConnectionType::Locator && LocatorConfig.LocatorHost.IsEmpty()))
+
+ ConnectionType = InConnectionType;
+}
+
+bool USpatialConnectionManager::TrySetupConnectionConfigFromCommandLine(const FString& SpatialWorkerType)
+{
+ bool bSuccessfullyLoaded = LocatorConfig.TryLoadCommandLineArgs();
+ if (bSuccessfullyLoaded)
+ {
+ SetConnectionType(ESpatialConnectionType::Locator);
+ LocatorConfig.WorkerType = SpatialWorkerType;
+ }
+ else
+ {
+ bSuccessfullyLoaded = DevAuthConfig.TryLoadCommandLineArgs();
+ if (bSuccessfullyLoaded)
+ {
+ SetConnectionType(ESpatialConnectionType::DevAuthFlow);
+ DevAuthConfig.WorkerType = SpatialWorkerType;
+ }
+ else
+ {
+ bSuccessfullyLoaded = ReceptionistConfig.TryLoadCommandLineArgs();
+ SetConnectionType(ESpatialConnectionType::Receptionist);
+ ReceptionistConfig.WorkerType = SpatialWorkerType;
+ }
+ }
+
+ return bSuccessfullyLoaded;
+}
+
+void USpatialConnectionManager::SetupConnectionConfigFromURL(const FURL& URL, const FString& SpatialWorkerType)
+{
+ if (URL.HasOption(TEXT("locator")) || URL.HasOption(TEXT("devauth")))
+ {
+ FString LocatorHostOverride;
+ if (URL.HasOption(TEXT("customLocator")))
+ {
+ LocatorHostOverride = URL.Host;
+ }
+ else
+ {
+ FParse::Value(FCommandLine::Get(), TEXT("locatorHost"), LocatorHostOverride);
+ }
+
+ if (URL.HasOption(TEXT("devauth")))
+ {
+ // Use devauth login flow.
+ SetConnectionType(ESpatialConnectionType::DevAuthFlow);
+ if (LocatorHostOverride != "")
+ {
+ DevAuthConfig.LocatorHost = LocatorHostOverride;
+ }
+ DevAuthConfig.DevelopmentAuthToken = URL.GetOption(*SpatialConstants::URL_DEV_AUTH_TOKEN_OPTION, TEXT(""));
+ DevAuthConfig.Deployment = URL.GetOption(*SpatialConstants::URL_TARGET_DEPLOYMENT_OPTION, TEXT(""));
+ DevAuthConfig.PlayerId = URL.GetOption(*SpatialConstants::URL_PLAYER_ID_OPTION, *SpatialConstants::DEVELOPMENT_AUTH_PLAYER_ID);
+ DevAuthConfig.DisplayName = URL.GetOption(*SpatialConstants::URL_DISPLAY_NAME_OPTION, TEXT(""));
+ DevAuthConfig.MetaData = URL.GetOption(*SpatialConstants::URL_METADATA_OPTION, TEXT(""));
+ DevAuthConfig.WorkerType = SpatialWorkerType;
+ }
+ else
+ {
+ // Use locator login flow.
+ SetConnectionType(ESpatialConnectionType::Locator);
+ if (LocatorHostOverride != "")
+ {
+ LocatorConfig.LocatorHost = LocatorHostOverride;
+ }
+ LocatorConfig.PlayerIdentityToken = URL.GetOption(*SpatialConstants::URL_PLAYER_IDENTITY_OPTION, TEXT(""));
+ LocatorConfig.LoginToken = URL.GetOption(*SpatialConstants::URL_LOGIN_OPTION, TEXT(""));
+ LocatorConfig.WorkerType = SpatialWorkerType;
+ }
+ }
+ else
+ {
+ 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.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);
+ }
+ }
+}
+
+void USpatialConnectionManager::OnConnectionSuccess()
+{
+ bIsConnected = true;
+
+ OnConnectedCallback.ExecuteIfBound();
+}
+
+void USpatialConnectionManager::OnConnectionFailure(uint8_t ConnectionStatusCode, const FString& ErrorMessage)
+{
+ bIsConnected = false;
+
+ OnFailedToConnectCallback.ExecuteIfBound(ConnectionStatusCode, ErrorMessage);
+}
diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp
index 7e1629476b..d49a443b21 100644
--- a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp
+++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp
@@ -1,32 +1,34 @@
// Copyright (c) Improbable Worlds Ltd, All Rights Reserved
#include "Interop/Connection/SpatialWorkerConnection.h"
-#if WITH_EDITOR
-#include "Interop/Connection/EditorWorkerController.h"
-#endif
-#include "EngineClasses/SpatialGameInstance.h"
-#include "Engine/World.h"
-#include "UnrealEngine.h"
#include "Async/Async.h"
-#include "Engine/Engine.h"
-#include "Engine/World.h"
-#include "Misc/Paths.h"
-
#include "SpatialGDKSettings.h"
-#include "Utils/ErrorCodeRemapping.h"
DEFINE_LOG_CATEGORY(LogSpatialWorkerConnection);
using namespace SpatialGDK;
-void USpatialWorkerConnection::Init(USpatialGameInstance* InGameInstance)
+void USpatialWorkerConnection::SetConnection(Worker_Connection* WorkerConnectionIn)
{
- GameInstance = InGameInstance;
+ WorkerConnection = WorkerConnectionIn;
+
+ CacheWorkerAttributes();
+
+ const USpatialGDKSettings* SpatialGDKSettings = GetDefault();
+ if (!SpatialGDKSettings->bRunSpatialWorkerConnectionOnGameThread)
+ {
+ if (OpsProcessingThread == nullptr)
+ {
+ InitializeOpsProcessingThread();
+ }
+ }
}
void USpatialWorkerConnection::FinishDestroy()
{
+ UE_LOG(LogSpatialWorkerConnection, Log, TEXT("Destroying SpatialWorkerconnection."));
+
DestroyConnection();
Super::FinishDestroy();
@@ -51,305 +53,10 @@ void USpatialWorkerConnection::DestroyConnection()
WorkerConnection = nullptr;
}
- if (WorkerLocator)
- {
- AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [WorkerLocator = WorkerLocator]
- {
- Worker_Locator_Destroy(WorkerLocator);
- });
-
- WorkerLocator = nullptr;
- }
-
- bIsConnected = false;
NextRequestId = 0;
KeepRunning.AtomicSet(true);
}
-void USpatialWorkerConnection::Connect(bool bInitAsClient, uint32 PlayInEditorID)
-{
- if (bIsConnected)
- {
- OnConnectionSuccess();
- return;
- }
-
- const USpatialGDKSettings* SpatialGDKSettings = GetDefault();
- if (SpatialGDKSettings->bUseDevelopmentAuthenticationFlow && bInitAsClient)
- {
- LocatorConfig.WorkerType = SpatialConstants::DefaultClientWorkerType.ToString();
- LocatorConfig.UseExternalIp = true;
- StartDevelopmentAuth(SpatialGDKSettings->DevelopmentAuthenticationToken);
- return;
- }
-
- switch (GetConnectionType())
- {
- case SpatialConnectionType::Receptionist:
- ConnectToReceptionist(bInitAsClient, PlayInEditorID);
- break;
- case SpatialConnectionType::Locator:
- ConnectToLocator();
- break;
- }
-}
-
-void USpatialWorkerConnection::OnLoginTokens(void* UserData, const Worker_Alpha_LoginTokensResponse* LoginTokens)
-{
- if (LoginTokens->status.code != WORKER_CONNECTION_STATUS_CODE_SUCCESS)
- {
- UE_LOG(LogSpatialWorkerConnection, Error, TEXT("Failed to get login token, StatusCode: %d, Error: %s"), LoginTokens->status.code, UTF8_TO_TCHAR(LoginTokens->status.detail));
- return;
- }
-
- if (LoginTokens->login_token_count == 0)
- {
- UE_LOG(LogSpatialWorkerConnection, Warning, TEXT("No deployment found to connect to. Did you add the 'dev_login' tag to the deployment you want to connect to?"));
- return;
- }
-
- UE_LOG(LogSpatialWorkerConnection, Verbose, TEXT("Successfully received LoginTokens, Count: %d"), LoginTokens->login_token_count);
- USpatialWorkerConnection* Connection = static_cast(UserData);
- Connection->ProcessLoginTokensResponse(LoginTokens);
-}
-
-void USpatialWorkerConnection::ProcessLoginTokensResponse(const Worker_Alpha_LoginTokensResponse* LoginTokens)
-{
- // If LoginTokenResCallback is callable and returns true, return early.
- if (LoginTokenResCallback && LoginTokenResCallback(LoginTokens))
- {
- return;
- }
-
- const FString& DeploymentToConnect = GetDefault()->DevelopmentDeploymentToConnect;
- // 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())
- {
- LocatorConfig.LoginToken = FString(LoginTokens->login_tokens[0].login_token);
- }
- else
- {
- for (uint32 i = 0; i < LoginTokens->login_token_count; i++)
- {
- FString DeploymentName = FString(LoginTokens->login_tokens[i].deployment_name);
- if (DeploymentToConnect.Compare(DeploymentName) == 0)
- {
- LocatorConfig.LoginToken = FString(LoginTokens->login_tokens[i].login_token);
- break;
- }
- }
- }
- ConnectToLocator();
-}
-
-void USpatialWorkerConnection::RequestDeploymentLoginTokens()
-{
- Worker_Alpha_LoginTokensRequest LTParams{};
- FTCHARToUTF8 PlayerIdentityToken(*LocatorConfig.PlayerIdentityToken);
- LTParams.player_identity_token = PlayerIdentityToken.Get();
- FTCHARToUTF8 WorkerType(*LocatorConfig.WorkerType);
- LTParams.worker_type = WorkerType.Get();
- LTParams.use_insecure_connection = false;
-
- if (Worker_Alpha_LoginTokensResponseFuture* LTFuture = Worker_Alpha_CreateDevelopmentLoginTokensAsync(TCHAR_TO_UTF8(*LocatorConfig.LocatorHost), SpatialConstants::LOCATOR_PORT, <Params))
- {
- Worker_Alpha_LoginTokensResponseFuture_Get(LTFuture, nullptr, this, &USpatialWorkerConnection::OnLoginTokens);
- }
-}
-
-void USpatialWorkerConnection::OnPlayerIdentityToken(void* UserData, const Worker_Alpha_PlayerIdentityTokenResponse* PIToken)
-{
- if (PIToken->status.code != WORKER_CONNECTION_STATUS_CODE_SUCCESS)
- {
- UE_LOG(LogSpatialWorkerConnection, Error, TEXT("Failed to get PlayerIdentityToken, StatusCode: %d, Error: %s"), PIToken->status.code, UTF8_TO_TCHAR(PIToken->status.detail));
- return;
- }
-
- UE_LOG(LogSpatialWorkerConnection, Log, TEXT("Successfully received PIToken: %s"), UTF8_TO_TCHAR(PIToken->player_identity_token));
- USpatialWorkerConnection* Connection = static_cast(UserData);
- Connection->LocatorConfig.PlayerIdentityToken = UTF8_TO_TCHAR(PIToken->player_identity_token);
-
- Connection->RequestDeploymentLoginTokens();
-}
-
-void USpatialWorkerConnection::StartDevelopmentAuth(FString DevAuthToken)
-{
- Worker_Alpha_PlayerIdentityTokenRequest PITParams{};
- FTCHARToUTF8 DAToken(*DevAuthToken);
- FTCHARToUTF8 PlayerId(*SpatialConstants::DEVELOPMENT_AUTH_PLAYER_ID);
- PITParams.development_authentication_token = DAToken.Get();
- PITParams.player_id = PlayerId.Get();
- PITParams.display_name = "";
- PITParams.metadata = "";
- PITParams.use_insecure_connection = false;
-
- if (Worker_Alpha_PlayerIdentityTokenResponseFuture* PITFuture = Worker_Alpha_CreateDevelopmentPlayerIdentityTokenAsync(TCHAR_TO_UTF8(*LocatorConfig.LocatorHost), SpatialConstants::LOCATOR_PORT, &PITParams))
- {
- Worker_Alpha_PlayerIdentityTokenResponseFuture_Get(PITFuture, nullptr, this, &USpatialWorkerConnection::OnPlayerIdentityToken);
- }
-}
-
-void USpatialWorkerConnection::ConnectToReceptionist(bool bConnectAsClient, uint32 PlayInEditorID)
-{
- if (ReceptionistConfig.WorkerType.IsEmpty())
- {
- ReceptionistConfig.WorkerType = bConnectAsClient ? SpatialConstants::DefaultClientWorkerType.ToString() : SpatialConstants::DefaultServerWorkerType.ToString();
- UE_LOG(LogSpatialWorkerConnection, Warning, TEXT("No worker type specified through commandline, defaulting to %s"), *ReceptionistConfig.WorkerType);
- }
-
-#if WITH_EDITOR
- SpatialGDKServices::InitWorkers(bConnectAsClient, PlayInEditorID, ReceptionistConfig.WorkerId);
-#endif
-
- if (ReceptionistConfig.WorkerId.IsEmpty())
- {
- ReceptionistConfig.WorkerId = ReceptionistConfig.WorkerType + FGuid::NewGuid().ToString();
- }
-
- // TODO UNR-1271: Move creation of connection parameters into a function somehow
- Worker_ConnectionParameters ConnectionParams = Worker_DefaultConnectionParameters();
- FTCHARToUTF8 WorkerTypeCStr(*ReceptionistConfig.WorkerType);
- ConnectionParams.worker_type = WorkerTypeCStr.Get();
- ConnectionParams.enable_protocol_logging_at_startup = ReceptionistConfig.EnableProtocolLoggingAtStartup;
-
- FString FinalProtocolLoggingPrefix;
- if (!ReceptionistConfig.ProtocolLoggingPrefix.IsEmpty())
- {
- FinalProtocolLoggingPrefix = ReceptionistConfig.ProtocolLoggingPrefix;
- }
- else
- {
- FinalProtocolLoggingPrefix = ReceptionistConfig.WorkerId;
- }
- FTCHARToUTF8 ProtocolLoggingPrefixCStr(*FinalProtocolLoggingPrefix);
- ConnectionParams.protocol_logging.log_prefix = ProtocolLoggingPrefixCStr.Get();
-
- Worker_ComponentVtable DefaultVtable = {};
- ConnectionParams.component_vtable_count = 0;
- ConnectionParams.default_component_vtable = &DefaultVtable;
-
- ConnectionParams.network.connection_type = ReceptionistConfig.LinkProtocol;
- ConnectionParams.network.use_external_ip = ReceptionistConfig.UseExternalIp;
- ConnectionParams.network.tcp.multiplex_level = ReceptionistConfig.TcpMultiplexLevel;
-
- // We want the bridge to worker messages to be compressed; not the worker to bridge messages.
- Worker_CompressionParameters EnableCompressionParams{};
- ConnectionParams.network.modular_kcp.upstream_compression = nullptr;
- ConnectionParams.network.modular_kcp.downstream_compression = &EnableCompressionParams;
-
- ConnectionParams.enable_dynamic_components = true;
- // end TODO
-
- Worker_ConnectionFuture* ConnectionFuture = Worker_ConnectAsync(
- TCHAR_TO_UTF8(*ReceptionistConfig.ReceptionistHost), ReceptionistConfig.ReceptionistPort,
- TCHAR_TO_UTF8(*ReceptionistConfig.WorkerId), &ConnectionParams);
-
- FinishConnecting(ConnectionFuture);
-}
-
-void USpatialWorkerConnection::ConnectToLocator()
-{
- if (LocatorConfig.WorkerType.IsEmpty())
- {
- LocatorConfig.WorkerType = SpatialConstants::DefaultClientWorkerType.ToString();
- UE_LOG(LogSpatialWorkerConnection, Warning, TEXT("No worker type specified through commandline, defaulting to %s"), *LocatorConfig.WorkerType);
- }
-
- if (LocatorConfig.WorkerId.IsEmpty())
- {
- LocatorConfig.WorkerId = LocatorConfig.WorkerType + FGuid::NewGuid().ToString();
- }
-
- FTCHARToUTF8 PlayerIdentityTokenCStr(*LocatorConfig.PlayerIdentityToken);
- FTCHARToUTF8 LoginTokenCStr(*LocatorConfig.LoginToken);
-
- Worker_LocatorParameters LocatorParams = {};
- FString ProjectName;
- FParse::Value(FCommandLine::Get(), TEXT("projectName"), ProjectName);
- LocatorParams.project_name = TCHAR_TO_UTF8(*ProjectName);
- LocatorParams.credentials_type = Worker_LocatorCredentialsTypes::WORKER_LOCATOR_PLAYER_IDENTITY_CREDENTIALS;
- LocatorParams.player_identity.player_identity_token = PlayerIdentityTokenCStr.Get();
- LocatorParams.player_identity.login_token = LoginTokenCStr.Get();
-
- // Connect to the locator on the default port(0 will choose the default)
- WorkerLocator = Worker_Locator_Create(TCHAR_TO_UTF8(*LocatorConfig.LocatorHost), SpatialConstants::LOCATOR_PORT, &LocatorParams);
-
- // TODO UNR-1271: Move creation of connection parameters into a function somehow
- Worker_ConnectionParameters ConnectionParams = Worker_DefaultConnectionParameters();
- FTCHARToUTF8 WorkerTypeCStr(*LocatorConfig.WorkerType);
- ConnectionParams.worker_type = WorkerTypeCStr.Get();
- ConnectionParams.enable_protocol_logging_at_startup = LocatorConfig.EnableProtocolLoggingAtStartup;
-
- Worker_ComponentVtable DefaultVtable = {};
- ConnectionParams.component_vtable_count = 0;
- ConnectionParams.default_component_vtable = &DefaultVtable;
-
- ConnectionParams.network.connection_type = LocatorConfig.LinkProtocol;
- ConnectionParams.network.use_external_ip = LocatorConfig.UseExternalIp;
- ConnectionParams.network.tcp.multiplex_level = LocatorConfig.TcpMultiplexLevel;
-
- // We want the bridge to worker messages to be compressed; not the worker to bridge messages.
- Worker_CompressionParameters EnableCompressionParams{};
- ConnectionParams.network.modular_kcp.upstream_compression = nullptr;
- ConnectionParams.network.modular_kcp.downstream_compression = &EnableCompressionParams;
-
- FString ProtocolLogDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectLogDir()) + TEXT("protocol-log-");
- ConnectionParams.protocol_logging.log_prefix = TCHAR_TO_UTF8(*ProtocolLogDir);
-
- ConnectionParams.enable_dynamic_components = true;
- // end TODO
-
- Worker_ConnectionFuture* ConnectionFuture = Worker_Locator_ConnectAsync(WorkerLocator, &ConnectionParams);
-
- FinishConnecting(ConnectionFuture);
-}
-
-void USpatialWorkerConnection::FinishConnecting(Worker_ConnectionFuture* ConnectionFuture)
-{
- TWeakObjectPtr WeakSpatialWorkerConnection(this);
-
- AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [ConnectionFuture, WeakSpatialWorkerConnection]
- {
- Worker_Connection* NewCAPIWorkerConnection = Worker_ConnectionFuture_Get(ConnectionFuture, nullptr);
- Worker_ConnectionFuture_Destroy(ConnectionFuture);
-
- AsyncTask(ENamedThreads::GameThread, [WeakSpatialWorkerConnection, NewCAPIWorkerConnection]
- {
- USpatialWorkerConnection* SpatialWorkerConnection = WeakSpatialWorkerConnection.Get();
-
- if (SpatialWorkerConnection == nullptr)
- {
- return;
- }
-
- SpatialWorkerConnection->WorkerConnection = NewCAPIWorkerConnection;
-
- if (Worker_Connection_IsConnected(NewCAPIWorkerConnection))
- {
- SpatialWorkerConnection->CacheWorkerAttributes();
- SpatialWorkerConnection->OnConnectionSuccess();
- }
- else
- {
- // TODO: Try to reconnect - UNR-576
- SpatialWorkerConnection->OnConnectionFailure();
- }
- });
- });
-}
-
-SpatialConnectionType USpatialWorkerConnection::GetConnectionType() const
-{
- if (!LocatorConfig.PlayerIdentityToken.IsEmpty())
- {
- return SpatialConnectionType::Locator;
- }
- else
- {
- return SpatialConnectionType::Receptionist;
- }
-}
-
TArray USpatialWorkerConnection::GetOpList()
{
TArray OpLists;
@@ -369,7 +76,7 @@ Worker_RequestId USpatialWorkerConnection::SendReserveEntityIdsRequest(uint32_t
return NextRequestId++;
}
-Worker_RequestId USpatialWorkerConnection::SendCreateEntityRequest(TArray&& Components, const Worker_EntityId* EntityId)
+Worker_RequestId USpatialWorkerConnection::SendCreateEntityRequest(TArray&& Components, const Worker_EntityId* EntityId)
{
QueueOutgoingMessage(MoveTemp(Components), EntityId);
return NextRequestId++;
@@ -381,7 +88,7 @@ Worker_RequestId USpatialWorkerConnection::SendDeleteEntityRequest(Worker_Entity
return NextRequestId++;
}
-void USpatialWorkerConnection::SendAddComponent(Worker_EntityId EntityId, Worker_ComponentData* ComponentData)
+void USpatialWorkerConnection::SendAddComponent(Worker_EntityId EntityId, FWorkerComponentData* ComponentData)
{
QueueOutgoingMessage(EntityId, *ComponentData);
}
@@ -391,7 +98,7 @@ void USpatialWorkerConnection::SendRemoveComponent(Worker_EntityId EntityId, Wor
QueueOutgoingMessage(EntityId, ComponentId);
}
-void USpatialWorkerConnection::SendComponentUpdate(Worker_EntityId EntityId, const Worker_ComponentUpdate* ComponentUpdate)
+void USpatialWorkerConnection::SendComponentUpdate(Worker_EntityId EntityId, const FWorkerComponentUpdate* ComponentUpdate)
{
QueueOutgoingMessage(EntityId, *ComponentUpdate);
}
@@ -433,9 +140,9 @@ void USpatialWorkerConnection::SendMetrics(const SpatialMetrics& Metrics)
QueueOutgoingMessage(Metrics);
}
-FString USpatialWorkerConnection::GetWorkerId() const
+PhysicalWorkerName USpatialWorkerConnection::GetWorkerId() const
{
- return FString(UTF8_TO_TCHAR(Worker_Connection_GetWorkerId(WorkerConnection)));
+ return PhysicalWorkerName(UTF8_TO_TCHAR(Worker_Connection_GetWorkerId(WorkerConnection)));
}
const TArray& USpatialWorkerConnection::GetWorkerAttributes() const
@@ -460,37 +167,6 @@ void USpatialWorkerConnection::CacheWorkerAttributes()
}
}
-void USpatialWorkerConnection::OnConnectionSuccess()
-{
- bIsConnected = true;
-
- if (OpsProcessingThread == nullptr)
- {
- InitializeOpsProcessingThread();
- }
-
- OnConnectedCallback.ExecuteIfBound();
- GameInstance->HandleOnConnected();
-}
-
-void USpatialWorkerConnection::OnPreConnectionFailure(const FString& Reason)
-{
- bIsConnected = false;
- GameInstance->HandleOnConnectionFailed(Reason);
-}
-
-void USpatialWorkerConnection::OnConnectionFailure()
-{
- bIsConnected = false;
-
- if (GEngine != nullptr && GameInstance->GetWorld() != nullptr)
- {
- uint8_t ConnectionStatusCode = Worker_Connection_GetConnectionStatusCode(WorkerConnection);
- const FString ErrorMessage(UTF8_TO_TCHAR(Worker_Connection_GetConnectionStatusDetailString(WorkerConnection)));
- OnFailedToConnectCallback.ExecuteIfBound(ConnectionStatusCode, ErrorMessage);
- }
-}
-
bool USpatialWorkerConnection::Init()
{
OpsUpdateInterval = 1.0f / GetDefault()->OpsUpdateRate;
@@ -500,12 +176,13 @@ bool USpatialWorkerConnection::Init()
uint32 USpatialWorkerConnection::Run()
{
+ const USpatialGDKSettings* SpatialGDKSettings = GetDefault();
+ check(!SpatialGDKSettings->bRunSpatialWorkerConnectionOnGameThread);
+
while (KeepRunning)
{
FPlatformProcess::Sleep(OpsUpdateInterval);
-
QueueLatestOpList();
-
ProcessOutgoingMessages();
}
@@ -545,6 +222,8 @@ void USpatialWorkerConnection::ProcessOutgoingMessages()
TUniquePtr OutgoingMessage;
OutgoingMessagesQueue.Dequeue(OutgoingMessage);
+ OnDequeueMessage.Broadcast(OutgoingMessage.Get());
+
static const Worker_UpdateParameters DisableLoopback{ /*loopback*/ WORKER_COMPONENT_UPDATE_LOOPBACK_NONE };
switch (OutgoingMessage->Type)
@@ -562,9 +241,23 @@ void USpatialWorkerConnection::ProcessOutgoingMessages()
{
FCreateEntityRequest* Message = static_cast(OutgoingMessage.Get());
+#if TRACE_LIB_ACTIVE
+ // We have to unpack these as Worker_ComponentData is not the same as FWorkerComponentData
+ TArray