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 UnpackedComponentData; + UnpackedComponentData.SetNum(Message->Components.Num()); + for (int i = 0, Num = Message->Components.Num(); i < Num; i++) + { + UnpackedComponentData[i] = Message->Components[i]; + } + Worker_ComponentData* ComponentData = UnpackedComponentData.GetData(); + uint32 ComponentCount = UnpackedComponentData.Num(); +#else + Worker_ComponentData* ComponentData = Message->Components.GetData(); + uint32 ComponentCount = Message->Components.Num(); +#endif Worker_Connection_SendCreateEntityRequest(WorkerConnection, - Message->Components.Num(), - Message->Components.GetData(), + ComponentCount, + ComponentData, Message->EntityId.IsSet() ? &(Message->EntityId.GetValue()) : nullptr, nullptr); break; @@ -606,6 +299,7 @@ void USpatialWorkerConnection::ProcessOutgoingMessages() Message->EntityId, &Message->Update, &DisableLoopback); + break; } case EOutgoingMessageType::CommandRequest: @@ -730,5 +424,7 @@ void USpatialWorkerConnection::QueueOutgoingMessage(ArgsType&&... Args) { // TODO UNR-1271: As later optimization, we can change the queue to hold a union // of all outgoing message types, rather than having a pointer. - OutgoingMessagesQueue.Enqueue(MakeUnique(Forward(Args)...)); + auto Message = MakeUnique(Forward(Args)...); + OnEnqueueMessage.Broadcast(Message.Get()); + OutgoingMessagesQueue.Enqueue(MoveTemp(Message)); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp index 6ec395fb71..51a792d0ec 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp @@ -18,24 +18,25 @@ #include "Interop/Connection/SpatialWorkerConnection.h" #include "Interop/SpatialReceiver.h" #include "Interop/SpatialSender.h" +#include "LoadBalancing/AbstractLBStrategy.h" #include "Kismet/GameplayStatics.h" -#include "Runtime/Engine/Public/TimerManager.h" +#include "Schema/ServerWorker.h" #include "Schema/UnrealMetadata.h" #include "SpatialConstants.h" #include "UObject/UObjectGlobals.h" #include "Utils/EntityPool.h" +#include "Utils/SpatialStatics.h" DEFINE_LOG_CATEGORY(LogGlobalStateManager); using namespace SpatialGDK; -void UGlobalStateManager::Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimerManager) +void UGlobalStateManager::Init(USpatialNetDriver* InNetDriver) { NetDriver = InNetDriver; StaticComponentView = InNetDriver->StaticComponentView; Sender = InNetDriver->Sender; Receiver = InNetDriver->Receiver; - TimerManager = InTimerManager; GlobalStateManagerEntityId = SpatialConstants::INITIAL_GLOBAL_STATE_MANAGER_ENTITY_ID; #if WITH_EDITOR @@ -47,15 +48,17 @@ void UGlobalStateManager::Init(USpatialNetDriver* InNetDriver, FTimerManager* In bool bRunUnderOneProcess = true; PlayInSettings->GetRunUnderOneProcess(bRunUnderOneProcess); - if (!bRunUnderOneProcess) + if (!bRunUnderOneProcess && !PrePIEEndedHandle.IsValid()) { - FEditorDelegates::PrePIEEnded.AddUObject(this, &UGlobalStateManager::OnPrePIEEnded); + PrePIEEndedHandle = FEditorDelegates::PrePIEEnded.AddUObject(this, &UGlobalStateManager::OnPrePIEEnded); } } #endif // WITH_EDITOR bAcceptingPlayers = false; + bHasSentReadyForVirtualWorkerAssignment = false; bCanBeginPlay = false; + bCanSpawnWithAuthority = false; } void UGlobalStateManager::ApplySingletonManagerData(const Worker_ComponentData& Data) @@ -68,20 +71,47 @@ void UGlobalStateManager::ApplyDeploymentMapData(const Worker_ComponentData& Dat { Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); - // Set the Deployment Map URL. SetDeploymentMapURL(GetStringFromSchema(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_MAP_URL_ID)); - // Set the AcceptingPlayers state. - bool bDataAcceptingPlayers = GetBoolFromSchema(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_ACCEPTING_PLAYERS_ID); - ApplyAcceptingPlayersUpdate(bDataAcceptingPlayers); + bAcceptingPlayers = GetBoolFromSchema(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_ACCEPTING_PLAYERS_ID); + + DeploymentSessionId = Schema_GetInt32(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_SESSION_ID); + + SchemaHash = Schema_GetUint32(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_SCHEMA_HASH); } void UGlobalStateManager::ApplyStartupActorManagerData(const Worker_ComponentData& Data) { Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); - const bool bCanBeginPlayData = GetBoolFromSchema(ComponentObject, SpatialConstants::STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_ID); - ApplyCanBeginPlayUpdate(bCanBeginPlayData); + bCanBeginPlay = GetBoolFromSchema(ComponentObject, SpatialConstants::STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_ID); + + TrySendWorkerReadyToBeginPlay(); +} + +void UGlobalStateManager::TrySendWorkerReadyToBeginPlay() +{ + // Once a worker has received the StartupActorManager AddComponent op, we say that a + // worker is ready to begin play. This means if the GSM-authoritative worker then sets + // canBeginPlay=true it will be received as a ComponentUpdate and so we can differentiate + // from when canBeginPlay=true was loaded from the snapshot and was received as an + // AddComponent. This is important for handling startup Actors correctly in a zoned + // environment. + const bool bHasReceivedStartupActorData = StaticComponentView->HasComponent(GlobalStateManagerEntityId, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID); + const bool bWorkerEntityCreated = NetDriver->WorkerEntityId != SpatialConstants::INVALID_ENTITY_ID; + if (bHasSentReadyForVirtualWorkerAssignment || !bHasReceivedStartupActorData || !bWorkerEntityCreated) + { + return; + } + + FWorkerComponentUpdate Update = {}; + Update.component_id = SpatialConstants::SERVER_WORKER_COMPONENT_ID; + Update.schema_type = Schema_CreateComponentUpdate(); + Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update.schema_type); + Schema_AddBool(UpdateObject, SpatialConstants::SERVER_WORKER_READY_TO_BEGIN_PLAY_ID, true); + + bHasSentReadyForVirtualWorkerAssignment = true; + NetDriver->Connection->SendComponentUpdate(NetDriver->WorkerEntityId, &Update); } void UGlobalStateManager::ApplySingletonManagerUpdate(const Worker_ComponentUpdate& Update) @@ -105,20 +135,17 @@ void UGlobalStateManager::ApplyDeploymentMapUpdate(const Worker_ComponentUpdate& if (Schema_GetBoolCount(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_ACCEPTING_PLAYERS_ID) == 1) { - bool bUpdateAcceptingPlayers = GetBoolFromSchema(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_ACCEPTING_PLAYERS_ID); - ApplyAcceptingPlayersUpdate(bUpdateAcceptingPlayers); + bAcceptingPlayers = GetBoolFromSchema(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_ACCEPTING_PLAYERS_ID); } -} -void UGlobalStateManager::ApplyAcceptingPlayersUpdate(bool bAcceptingPlayersUpdate) -{ - if (bAcceptingPlayersUpdate != bAcceptingPlayers) + if (Schema_GetObjectCount(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_SESSION_ID) == 1) { - UE_LOG(LogGlobalStateManager, Log, TEXT("GlobalStateManager Update - AcceptingPlayers: %s"), bAcceptingPlayersUpdate ? TEXT("true") : TEXT("false")); - bAcceptingPlayers = bAcceptingPlayersUpdate; + DeploymentSessionId = Schema_GetInt32(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_SESSION_ID); + } - // Tell the SpatialNetDriver that AcceptingPlayers has changed. - NetDriver->OnAcceptingPlayersChanged(bAcceptingPlayersUpdate); + if (Schema_GetObjectCount(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_SCHEMA_HASH) == 1) + { + SchemaHash = Schema_GetUint32(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_SCHEMA_HASH); } } @@ -126,6 +153,7 @@ void UGlobalStateManager::ApplyAcceptingPlayersUpdate(bool bAcceptingPlayersUpda void UGlobalStateManager::OnPrePIEEnded(bool bValue) { SendShutdownMultiProcessRequest(); + FEditorDelegates::PrePIEEnded.Remove(PrePIEEndedHandle); } void UGlobalStateManager::SendShutdownMultiProcessRequest() @@ -152,6 +180,8 @@ void UGlobalStateManager::ReceiveShutdownMultiProcessRequest() // Since the server works are shutting down, set reset the accepting_players flag to false to prevent race conditions where the client connects quicker than the server. SetAcceptingPlayers(false); + DeploymentSessionId = 0; + SendSessionIdUpdate(); // If we have multiple servers, they need to be informed of PIE session ending. SendShutdownAdditionalServersEvent(); @@ -188,7 +218,7 @@ void UGlobalStateManager::SendShutdownAdditionalServersEvent() return; } - Worker_ComponentUpdate ComponentUpdate = {}; + FWorkerComponentUpdate ComponentUpdate = {}; ComponentUpdate.component_id = SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID; ComponentUpdate.schema_type = Schema_CreateComponentUpdate(); @@ -203,16 +233,8 @@ void UGlobalStateManager::ApplyStartupActorManagerUpdate(const Worker_ComponentU { Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); - if (Schema_GetBoolCount(ComponentObject, SpatialConstants::STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_ID) == 1) - { - const bool bCanBeginPlayUpdate = GetBoolFromSchema(ComponentObject, SpatialConstants::STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_ID); - ApplyCanBeginPlayUpdate(bCanBeginPlayUpdate); - } -} - -void UGlobalStateManager::ApplyCanBeginPlayUpdate(const bool bCanBeginPlayUpdate) -{ - bCanBeginPlay = bCanBeginPlayUpdate; + bCanBeginPlay = GetBoolFromSchema(ComponentObject, SpatialConstants::STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_ID); + bCanSpawnWithAuthority = true; } void UGlobalStateManager::LinkExistingSingletonActor(const UClass* SingletonActorClass) @@ -233,13 +255,13 @@ void UGlobalStateManager::LinkExistingSingletonActor(const UClass* SingletonActo return; } - TPair* ActorChannelPair = NetDriver->SingletonActorChannels.Find(SingletonActorClass); + TPair* ActorChannelPair = SingletonClassPathToActorChannels.Find(SingletonActorClass->GetPathName()); if (ActorChannelPair == nullptr) { // Dynamically spawn singleton actor if we have queued up data - ala USpatialReceiver::ReceiveActor - JIRA: 735 // No local actor has registered itself as replicatible on this worker - UE_LOG(LogGlobalStateManager, Log, TEXT("LinkExistingSingletonActor no actor registered"), *SingletonActorClass->GetName()); + UE_LOG(LogGlobalStateManager, Log, TEXT("LinkExistingSingletonActor no actor registered for class %s"), *SingletonActorClass->GetName()); return; } @@ -249,7 +271,7 @@ void UGlobalStateManager::LinkExistingSingletonActor(const UClass* SingletonActo if (Channel != nullptr) { // Channel has already been setup - UE_LOG(LogGlobalStateManager, Verbose, TEXT("UGlobalStateManager::LinkExistingSingletonActor channel already setup"), *SingletonActorClass->GetName()); + UE_LOG(LogGlobalStateManager, Verbose, TEXT("UGlobalStateManager::LinkExistingSingletonActor channel already setup for %s"), *SingletonActorClass->GetName()); return; } @@ -260,7 +282,7 @@ void UGlobalStateManager::LinkExistingSingletonActor(const UClass* SingletonActo Channel = Cast(Connection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::OpenedLocally)); - if (StaticComponentView->GetAuthority(SingletonEntityId, SpatialConstants::POSITION_COMPONENT_ID) == WORKER_AUTHORITY_AUTHORITATIVE) + if (StaticComponentView->HasAuthority(SingletonEntityId, SpatialConstants::POSITION_COMPONENT_ID)) { SingletonActor->Role = ROLE_Authority; SingletonActor->RemoteRole = ROLE_SimulatedProxy; @@ -310,7 +332,7 @@ USpatialActorChannel* UGlobalStateManager::AddSingleton(AActor* SingletonActor) UClass* SingletonActorClass = SingletonActor->GetClass(); - TPair& ActorChannelPair = NetDriver->SingletonActorChannels.FindOrAdd(SingletonActorClass); + TPair& ActorChannelPair = SingletonClassPathToActorChannels.FindOrAdd(SingletonActorClass->GetPathName()); USpatialActorChannel*& Channel = ActorChannelPair.Value; check(ActorChannelPair.Key == nullptr || ActorChannelPair.Key == SingletonActor); ActorChannelPair.Key = SingletonActor; @@ -335,7 +357,7 @@ USpatialActorChannel* UGlobalStateManager::AddSingleton(AActor* SingletonActor) { check(NetDriver->PackageMap->GetObjectFromEntityId(*SingletonEntityId) == nullptr); NetDriver->PackageMap->ResolveEntityActor(SingletonActor, *SingletonEntityId); - if (StaticComponentView->GetAuthority(*SingletonEntityId, SpatialConstants::POSITION_COMPONENT_ID) != WORKER_AUTHORITY_AUTHORITATIVE) + if (!StaticComponentView->HasAuthority(*SingletonEntityId, SpatialConstants::POSITION_COMPONENT_ID)) { SingletonActor->Role = ROLE_SimulatedProxy; SingletonActor->RemoteRole = ROLE_Authority; @@ -358,9 +380,21 @@ USpatialActorChannel* UGlobalStateManager::AddSingleton(AActor* SingletonActor) return Channel; } +void UGlobalStateManager::RemoveSingletonInstance(const AActor* SingletonActor) +{ + check(SingletonActor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)); + + SingletonClassPathToActorChannels.Remove(SingletonActor->GetClass()->GetPathName()); +} + +void UGlobalStateManager::RemoveAllSingletons() +{ + SingletonClassPathToActorChannels.Reset(); +} + void UGlobalStateManager::RegisterSingletonChannel(AActor* SingletonActor, USpatialActorChannel* SingletonChannel) { - TPair& ActorChannelPair = NetDriver->SingletonActorChannels.FindOrAdd(SingletonActor->GetClass()); + TPair& ActorChannelPair = SingletonClassPathToActorChannels.FindOrAdd(SingletonActor->GetClass()->GetPathName()); check(ActorChannelPair.Key == nullptr || ActorChannelPair.Key == SingletonActor); check(ActorChannelPair.Value == nullptr || ActorChannelPair.Value == SingletonChannel); @@ -371,7 +405,7 @@ void UGlobalStateManager::RegisterSingletonChannel(AActor* SingletonActor, USpat void UGlobalStateManager::ExecuteInitialSingletonActorReplication() { - for (auto& ClassToActorChannel : NetDriver->SingletonActorChannels) + for (auto& ClassToActorChannel : SingletonClassPathToActorChannels) { auto& ActorChannelPair = ClassToActorChannel.Value; AddSingleton(ActorChannelPair.Key); @@ -389,7 +423,7 @@ void UGlobalStateManager::UpdateSingletonEntityId(const FString& ClassName, cons return; } - Worker_ComponentUpdate Update = {}; + FWorkerComponentUpdate Update = {}; Update.component_id = SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID; Update.schema_type = Schema_CreateComponentUpdate(); Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update.schema_type); @@ -411,13 +445,15 @@ bool UGlobalStateManager::IsSingletonEntity(Worker_EntityId EntityId) const return false; } -void UGlobalStateManager::SetAcceptingPlayers(bool bInAcceptingPlayers) +void UGlobalStateManager::SetDeploymentState() { check(NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID)); // Send the component update that we can now accept players. - UE_LOG(LogGlobalStateManager, Log, TEXT("Setting accepting players to '%s'"), bInAcceptingPlayers ? TEXT("true") : TEXT("false")); - Worker_ComponentUpdate Update = {}; + UE_LOG(LogGlobalStateManager, Log, TEXT("Setting deployment URL to '%s'"), *NetDriver->GetWorld()->URL.Map); + UE_LOG(LogGlobalStateManager, Log, TEXT("Setting schema hash to '%u'"), NetDriver->ClassInfoManager->SchemaDatabase->SchemaDescriptorHash); + + FWorkerComponentUpdate Update = {}; Update.component_id = SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID; Update.schema_type = Schema_CreateComponentUpdate(); Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update.schema_type); @@ -425,26 +461,39 @@ void UGlobalStateManager::SetAcceptingPlayers(bool bInAcceptingPlayers) // Set the map URL on the GSM. AddStringToSchema(UpdateObject, SpatialConstants::DEPLOYMENT_MAP_MAP_URL_ID, NetDriver->GetWorld()->URL.Map); - // Set the AcceptingPlayers state on the GSM - Schema_AddBool(UpdateObject, SpatialConstants::DEPLOYMENT_MAP_ACCEPTING_PLAYERS_ID, static_cast(bInAcceptingPlayers)); + // Set the schema hash for connecting workers to check against + Schema_AddUint32(UpdateObject, SpatialConstants::DEPLOYMENT_MAP_SCHEMA_HASH, NetDriver->ClassInfoManager->SchemaDatabase->SchemaDescriptorHash); // Component updates are short circuited so we set the updated state here and then send the component update. - bAcceptingPlayers = bInAcceptingPlayers; NetDriver->Connection->SendComponentUpdate(GlobalStateManagerEntityId, &Update); } -void UGlobalStateManager::SetCanBeginPlay(const bool bInCanBeginPlay) +void UGlobalStateManager::SetAcceptingPlayers(bool bInAcceptingPlayers) { - check(NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID)); + // We should only be able to change whether we're accepting players if: + // - we're authoritative over the DeploymentMap which has the acceptingPlayers property, + // - we've called BeginPlay (so startup Actors can do initialization before any spawn requests are received), + // - we aren't duplicating the current state. + const bool bHasDeploymentMapAuthority = NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID); + const bool bHasBegunPlay = NetDriver->GetWorld()->HasBegunPlay(); + const bool bIsDuplicatingCurrentState = bAcceptingPlayers == bInAcceptingPlayers; + if (!bHasDeploymentMapAuthority || !bHasBegunPlay || bIsDuplicatingCurrentState) + { + return; + } - Worker_ComponentUpdate Update = {}; - Update.component_id = SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID; + // Send the component update that we can now accept players. + UE_LOG(LogGlobalStateManager, Log, TEXT("Setting accepting players to '%s'"), bInAcceptingPlayers ? TEXT("true") : TEXT("false")); + FWorkerComponentUpdate Update = {}; + Update.component_id = SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID; Update.schema_type = Schema_CreateComponentUpdate(); Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update.schema_type); - Schema_AddBool(UpdateObject, SpatialConstants::STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_ID, static_cast(bInCanBeginPlay)); + // Set the AcceptingPlayers state on the GSM + Schema_AddBool(UpdateObject, SpatialConstants::DEPLOYMENT_MAP_ACCEPTING_PLAYERS_ID, static_cast(bInAcceptingPlayers)); - bCanBeginPlay = bInCanBeginPlay; + // Component updates are short circuited so we set the updated state here and then send the component update. + bAcceptingPlayers = bInAcceptingPlayers; NetDriver->Connection->SendComponentUpdate(GlobalStateManagerEntityId, &Update); } @@ -463,6 +512,7 @@ void UGlobalStateManager::AuthorityChanged(const Worker_AuthorityChangeOp& AuthO case SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID: { GlobalStateManagerEntityId = AuthOp.entity_id; + SetDeploymentState(); SetAcceptingPlayers(true); break; } @@ -473,14 +523,18 @@ void UGlobalStateManager::AuthorityChanged(const Worker_AuthorityChangeOp& AuthO } case SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID: { - // We can reach this point with bCanBeginPlay==true if the server - // that was authoritative over the GSM restarts. - if (!bCanBeginPlay) - { - BecomeAuthoritativeOverAllActors(); - SetCanBeginPlay(true); - } - + // The bCanSpawnWithAuthority member determines whether a server-side worker + // should consider calling BeginPlay on startup Actors if the load-balancing + // strategy dictates that the worker should have authority over the Actor + // (providing Unreal load balancing is enabled). This should only happen for + // workers launching for fresh deployments, since for restarted workers and + // when deployments are launched from a snapshot, the entities representing + // startup Actors should already exist. If bCanBeginPlay is set to false, this + // means it's a fresh deployment, so bCanSpawnWithAuthority should be true. + // Conversely, if bCanBeginPlay is set to true, this worker is either a restarted + // crashed worker or in a deployment loaded from snapshot, so bCanSpawnWithAuthority + // should be false. + bCanSpawnWithAuthority = !bCanBeginPlay; break; } default: @@ -504,6 +558,25 @@ bool UGlobalStateManager::HandlesComponent(const Worker_ComponentId ComponentId) } } +void UGlobalStateManager::ResetGSM() +{ + UE_LOG(LogGlobalStateManager, Display, TEXT("GlobalStateManager singletons are being reset. Session restarting.")); + + SingletonNameToEntityId.Empty(); + SetAcceptingPlayers(false); + + // Reset the BeginPlay flag so Startup Actors are properly managed. + SendCanBeginPlayUpdate(false); + + // Reset the Singleton map so Singletons are recreated. + FWorkerComponentUpdate Update = {}; + Update.component_id = SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID; + Update.schema_type = Schema_CreateComponentUpdate(); + Schema_AddComponentUpdateClearedField(Update.schema_type, SpatialConstants::SINGLETON_MANAGER_SINGLETON_NAME_TO_ENTITY_ID); + + NetDriver->Connection->SendComponentUpdate(GlobalStateManagerEntityId, &Update); +} + void UGlobalStateManager::BeginDestroy() { Super::BeginDestroy(); @@ -515,10 +588,10 @@ void UGlobalStateManager::BeginDestroy() if (GetDefault()->GetDeleteDynamicEntities()) { // Reset the BeginPlay flag so Startup Actors are properly managed. - SetCanBeginPlay(false); + SendCanBeginPlayUpdate(false); // Reset the Singleton map so Singletons are recreated. - Worker_ComponentUpdate Update = {}; + FWorkerComponentUpdate Update = {}; Update.component_id = SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID; Update.schema_type = Schema_CreateComponentUpdate(); Schema_AddComponentUpdateClearedField(Update.schema_type, SpatialConstants::SINGLETON_MANAGER_SINGLETON_NAME_TO_ENTITY_ID); @@ -529,19 +602,36 @@ void UGlobalStateManager::BeginDestroy() #endif } -bool UGlobalStateManager::HasAuthority() +void UGlobalStateManager::BecomeAuthoritativeOverAllActors() { - return NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID); + // This logic is not used in offloading. + if (USpatialStatics::IsSpatialOffloadingEnabled()) + { + return; + } + + for (TActorIterator It(NetDriver->World); It; ++It) + { + AActor* Actor = *It; + if (Actor != nullptr && !Actor->IsPendingKill()) + { + if (Actor->GetIsReplicated()) + { + Actor->Role = ROLE_Authority; + Actor->RemoteRole = ROLE_SimulatedProxy; + } + } + } } -void UGlobalStateManager::BecomeAuthoritativeOverAllActors() +void UGlobalStateManager::BecomeAuthoritativeOverActorsBasedOnLBStrategy() { for (TActorIterator It(NetDriver->World); It; ++It) { AActor* Actor = *It; if (Actor != nullptr && !Actor->IsPendingKill()) { - if (Actor->GetIsReplicated()) + if (Actor->GetIsReplicated() && NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*Actor)) { Actor->Role = ROLE_Authority; Actor->RemoteRole = ROLE_SimulatedProxy; @@ -552,15 +642,62 @@ void UGlobalStateManager::BecomeAuthoritativeOverAllActors() void UGlobalStateManager::TriggerBeginPlay() { - check(IsReadyToCallBeginPlay()); + const bool bHasStartupActorAuthority = NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID); + if (bHasStartupActorAuthority) + { + SendCanBeginPlayUpdate(true); + } + + // This method has early exits internally to ensure the logic is only executed on the correct worker. + SetAcceptingPlayers(true); + + // If we're loading from a snapshot, we shouldn't try and call BeginPlay with authority. + if (bCanSpawnWithAuthority) + { + if (GetDefault()->bEnableUnrealLoadBalancer) + { + BecomeAuthoritativeOverActorsBasedOnLBStrategy(); + } + else + { + BecomeAuthoritativeOverAllActors(); + } + } NetDriver->World->GetWorldSettings()->SetGSMReadyForPlay(); NetDriver->World->GetWorldSettings()->NotifyBeginPlay(); } +bool UGlobalStateManager::GetCanBeginPlay() const +{ + return bCanBeginPlay; +} + +bool UGlobalStateManager::IsReady() const +{ + return GetCanBeginPlay() || NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID); +} + +void UGlobalStateManager::SendCanBeginPlayUpdate(const bool bInCanBeginPlay) +{ + check(NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID)); + + bCanBeginPlay = bInCanBeginPlay; + + FWorkerComponentUpdate Update = {}; + Update.component_id = SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID; + Update.schema_type = Schema_CreateComponentUpdate(); + Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update.schema_type); + + Schema_AddBool(UpdateObject, SpatialConstants::STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_ID, static_cast(bCanBeginPlay)); + + NetDriver->Connection->SendComponentUpdate(GlobalStateManagerEntityId, &Update); +} + // Queries for the GlobalStateManager in the deployment. -// bRetryUntilAcceptingPlayers will continue querying until the state of AcceptingPlayers is true, this is so clients know when to connect to the deployment. -void UGlobalStateManager::QueryGSM(bool bRetryUntilAcceptingPlayers) +// bRetryUntilRecievedExpectedValues will continue querying until the state of AcceptingPlayers and SessionId are the same as the given arguments +// This is so clients know when to connect to the deployment. +void UGlobalStateManager::QueryGSM(const QueryDelegate& Callback) { Worker_ComponentConstraint GSMComponentConstraint{}; GSMComponentConstraint.component_id = SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID; @@ -577,7 +714,7 @@ void UGlobalStateManager::QueryGSM(bool bRetryUntilAcceptingPlayers) RequestID = NetDriver->Connection->SendEntityQueryRequest(&GSMQuery); EntityQueryDelegate GSMQueryDelegate; - GSMQueryDelegate.BindLambda([this, bRetryUntilAcceptingPlayers](const Worker_EntityQueryResponseOp& Op) + GSMQueryDelegate.BindLambda([this, Callback](const Worker_EntityQueryResponseOp& Op) { if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) { @@ -589,30 +726,32 @@ void UGlobalStateManager::QueryGSM(bool bRetryUntilAcceptingPlayers) } else { - bool bNewAcceptingPlayers = GetAcceptingPlayersFromQueryResponse(Op); - - if (!bNewAcceptingPlayers && bRetryUntilAcceptingPlayers) + if (NetDriver->VirtualWorkerTranslator.IsValid()) { - UE_LOG(LogGlobalStateManager, Log, TEXT("Not yet accepting new players. Will retry query for GSM.")); - RetryQueryGSM(bRetryUntilAcceptingPlayers); + ApplyVirtualWorkerMappingFromQueryResponse(Op); } - else - { - ApplyDeploymentMapDataFromQueryResponse(Op); - } - - return; - } - - if (bRetryUntilAcceptingPlayers) - { - RetryQueryGSM(bRetryUntilAcceptingPlayers); + ApplyDeploymentMapDataFromQueryResponse(Op); + Callback.ExecuteIfBound(Op); } }); Receiver->AddEntityQueryDelegate(RequestID, GSMQueryDelegate); } +void UGlobalStateManager::ApplyVirtualWorkerMappingFromQueryResponse(const Worker_EntityQueryResponseOp& Op) +{ + check(NetDriver->VirtualWorkerTranslator.IsValid()); + for (uint32_t i = 0; i < Op.results[0].component_count; i++) + { + Worker_ComponentData Data = Op.results[0].components[i]; + if (Data.component_id == SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID) + { + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + NetDriver->VirtualWorkerTranslator->ApplyVirtualWorkerManagerData(ComponentObject); + } + } +} + void UGlobalStateManager::ApplyDeploymentMapDataFromQueryResponse(const Worker_EntityQueryResponseOp& Op) { for (uint32_t i = 0; i < Op.results[0].component_count; i++) @@ -625,10 +764,13 @@ void UGlobalStateManager::ApplyDeploymentMapDataFromQueryResponse(const Worker_E } } -bool UGlobalStateManager::GetAcceptingPlayersFromQueryResponse(const Worker_EntityQueryResponseOp& Op) +bool UGlobalStateManager::GetAcceptingPlayersAndSessionIdFromQueryResponse(const Worker_EntityQueryResponseOp& Op, bool& OutAcceptingPlayers, int32& OutSessionId) { checkf(Op.result_count == 1, TEXT("There should never be more than one GSM")); + bool AcceptingPlayersFound = false; + bool SessionIdFound = false; + // Iterate over each component on the GSM until we get the DeploymentMap component. for (uint32_t i = 0; i < Op.results[0].component_count; i++) { @@ -639,43 +781,48 @@ bool UGlobalStateManager::GetAcceptingPlayersFromQueryResponse(const Worker_Enti if (Schema_GetBoolCount(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_ACCEPTING_PLAYERS_ID) == 1) { - bool bDataAcceptingPlayers = GetBoolFromSchema(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_ACCEPTING_PLAYERS_ID); - return bDataAcceptingPlayers; + OutAcceptingPlayers = GetBoolFromSchema(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_ACCEPTING_PLAYERS_ID); + AcceptingPlayersFound = true; + } + + if (Schema_GetUint32Count(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_SESSION_ID) == 1) + { + OutSessionId = Schema_GetInt32(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_SESSION_ID); + SessionIdFound = true; + } + + if (AcceptingPlayersFound && SessionIdFound) + { + return true; } } } - UE_LOG(LogGlobalStateManager, Warning, TEXT("Entity query response for the GSM did not contain an AcceptingPlayers state.")); + UE_LOG(LogGlobalStateManager, Warning, TEXT("Entity query response for the GSM did not contain both AcceptingPlayers and SessionId states.")); return false; } -void UGlobalStateManager::RetryQueryGSM(bool bRetryUntilAcceptingPlayers) +void UGlobalStateManager::SetDeploymentMapURL(const FString& MapURL) { - // TODO: UNR-656 - TLDR: Hack to get around runtime not giving data on streaming queries unless you have write authority. - // There is currently a bug in runtime which prevents clients from being able to have read access on the component via the streaming query. - // This means that the clients never actually receive updates or data on the GSM. To get around this we are making timed entity queries to - // find the state of the GSM and the accepting players. Remove this work-around when the runtime bug is fixed. - 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(LogGlobalStateManager, Log, TEXT("Setting DeploymentMapURL: %s"), *MapURL); + DeploymentMapURL = MapURL; +} - UE_LOG(LogGlobalStateManager, Log, TEXT("Retrying query for GSM in %f seconds"), RetryTimerDelay); - FTimerHandle RetryTimer; - TimerManager->SetTimer(RetryTimer, [WeakThis = TWeakObjectPtr(this), bRetryUntilAcceptingPlayers]() - { - if (UGlobalStateManager* GSM = WeakThis.Get()) - { - GSM->QueryGSM(bRetryUntilAcceptingPlayers); - } - }, RetryTimerDelay, false); +void UGlobalStateManager::IncrementSessionID() +{ + DeploymentSessionId++; + SendSessionIdUpdate(); } -void UGlobalStateManager::SetDeploymentMapURL(const FString& MapURL) +void UGlobalStateManager::SendSessionIdUpdate() { - UE_LOG(LogGlobalStateManager, Log, TEXT("Setting DeploymentMapURL: %s"), *MapURL); - DeploymentMapURL = MapURL; + FWorkerComponentUpdate Update = {}; + Update.component_id = SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID; + Update.schema_type = Schema_CreateComponentUpdate(); + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); + + Schema_AddInt32(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_SESSION_ID, DeploymentSessionId); + + NetDriver->Connection->SendComponentUpdate(GlobalStateManagerEntityId, &Update); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp index e305204d84..34fa9ee48a 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp @@ -17,14 +17,17 @@ #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" -#include "Utils/ActorGroupManager.h" +#include "Utils/SpatialActorGroupManager.h" #include "Utils/RepLayoutUtils.h" DEFINE_LOG_CATEGORY(LogSpatialClassInfoManager); -bool USpatialClassInfoManager::TryInit(USpatialNetDriver* InNetDriver, UActorGroupManager* InActorGroupManager) +bool USpatialClassInfoManager::TryInit(USpatialNetDriver* InNetDriver, SpatialActorGroupManager* InActorGroupManager) { + check(InNetDriver != nullptr); NetDriver = InNetDriver; + + check(InActorGroupManager != nullptr); ActorGroupManager = InActorGroupManager; FSoftObjectPath SchemaDatabasePath = FSoftObjectPath(FPaths::SetExtension(SpatialConstants::SCHEMA_DATABASE_ASSET_PATH, TEXT(".SchemaDatabase"))); @@ -67,40 +70,40 @@ FORCEINLINE UClass* ResolveClass(FString& ClassPath) return Class; } -ESchemaComponentType GetRPCType(UFunction* RemoteFunction) +ERPCType GetRPCType(UFunction* RemoteFunction) { if (RemoteFunction->HasAnyFunctionFlags(FUNC_NetMulticast)) { - return SCHEMA_NetMulticastRPC; + return ERPCType::NetMulticast; } else if (RemoteFunction->HasAnyFunctionFlags(FUNC_NetCrossServer)) { - return SCHEMA_CrossServerRPC; + return ERPCType::CrossServer; } else if (RemoteFunction->HasAnyFunctionFlags(FUNC_NetReliable)) { if (RemoteFunction->HasAnyFunctionFlags(FUNC_NetClient)) { - return SCHEMA_ClientReliableRPC; + return ERPCType::ClientReliable; } else if (RemoteFunction->HasAnyFunctionFlags(FUNC_NetServer)) { - return SCHEMA_ServerReliableRPC; + return ERPCType::ServerReliable; } } else { if (RemoteFunction->HasAnyFunctionFlags(FUNC_NetClient)) { - return SCHEMA_ClientUnreliableRPC; + return ERPCType::ClientUnreliable; } else if (RemoteFunction->HasAnyFunctionFlags(FUNC_NetServer)) { - return SCHEMA_ServerUnreliableRPC; + return ERPCType::ServerUnreliable; } } - return SCHEMA_Invalid; + return ERPCType::Invalid; } void USpatialClassInfoManager::CreateClassInfoForClass(UClass* Class) @@ -122,8 +125,8 @@ void USpatialClassInfoManager::CreateClassInfoForClass(UClass* Class) for (UFunction* RemoteFunction : RelevantClassFunctions) { - ESchemaComponentType RPCType = GetRPCType(RemoteFunction); - checkf(RPCType != SCHEMA_Invalid, TEXT("Could not determine RPCType for RemoteFunction: %s"), *GetPathNameSafe(RemoteFunction)); + ERPCType RPCType = GetRPCType(RemoteFunction); + checkf(RPCType != ERPCType::Invalid, TEXT("Could not determine RPCType for RemoteFunction: %s"), *GetPathNameSafe(RemoteFunction)); FRPCInfo RPCInfo; RPCInfo.Type = RPCType; @@ -345,7 +348,17 @@ UClass* USpatialClassInfoManager::GetClassByComponentId(Worker_ComponentId Compo // The weak pointer to the class stored in the FClassInfo will be the same as the one used as the key in ClassInfoMap, so we can use it to clean up the old entry. ClassInfoMap.Remove(Info->Class); - // The old references in the other maps (ComponentToClassInfoMap etc) will be replaced by reloading the info (as a part of LoadClassForComponent). + // The old references in the other maps (ComponentToClassInfoMap etc) will be replaced by reloading the info (as a part of TryCreateClassInfoForComponentId). + TryCreateClassInfoForComponentId(ComponentId); + TSharedRef NewInfo = ComponentToClassInfoMap.FindChecked(ComponentId); + if (UClass* NewClass = NewInfo->Class.Get()) + { + return NewClass; + } + else + { + UE_LOG(LogSpatialClassInfoManager, Error, TEXT("Could not reload class for component %d!"), ComponentId); + } } return nullptr; @@ -452,22 +465,45 @@ const FRPCInfo& USpatialClassInfoManager::GetRPCInfo(UObject* Object, UFunction* return *RPCInfoPtr; } -uint32 USpatialClassInfoManager::GetComponentIdFromLevelPath(const FString& LevelPath) +Worker_ComponentId USpatialClassInfoManager::GetComponentIdFromLevelPath(const FString& LevelPath) const { FString CleanLevelPath = UWorld::RemovePIEPrefix(LevelPath); - if (const uint32* ComponentId = SchemaDatabase->LevelPathToComponentId.Find(CleanLevelPath)) + if (const Worker_ComponentId* ComponentId = SchemaDatabase->LevelPathToComponentId.Find(CleanLevelPath)) { return *ComponentId; } return SpatialConstants::INVALID_COMPONENT_ID; } -bool USpatialClassInfoManager::IsSublevelComponent(Worker_ComponentId ComponentId) +bool USpatialClassInfoManager::IsSublevelComponent(Worker_ComponentId ComponentId) const { return SchemaDatabase->LevelComponentIds.Contains(ComponentId); } -const FClassInfo* USpatialClassInfoManager::GetClassInfoForNewSubobject(const UObject * Object, Worker_EntityId EntityId, USpatialPackageMapClient* PackageMapClient) +const TMap& USpatialClassInfoManager::GetNetCullDistanceToComponentIds() const +{ + return SchemaDatabase->NetCullDistanceToComponentId; +} + +const TArray& USpatialClassInfoManager::GetComponentIdsForComponentType(const ESchemaComponentType ComponentType) const +{ + switch (ComponentType) + { + case ESchemaComponentType::SCHEMA_Data: + return SchemaDatabase->DataComponentIds; + case ESchemaComponentType::SCHEMA_OwnerOnly: + return SchemaDatabase->OwnerOnlyComponentIds; + case ESchemaComponentType::SCHEMA_Handover: + return SchemaDatabase->HandoverComponentIds; + default: + UE_LOG(LogSpatialClassInfoManager, Error, TEXT("Component type %d not recognised."), ComponentType); + checkNoEntry(); + static const TArray EmptyArray; + return EmptyArray; + } +} + +const FClassInfo* USpatialClassInfoManager::GetClassInfoForNewSubobject(const UObject* Object, Worker_EntityId EntityId, USpatialPackageMapClient* PackageMapClient) { const FClassInfo* Info = nullptr; @@ -495,6 +531,25 @@ const FClassInfo* USpatialClassInfoManager::GetClassInfoForNewSubobject(const UO return Info; } +Worker_ComponentId USpatialClassInfoManager::GetComponentIdForNetCullDistance(float NetCullDistance) const +{ + if (const uint32* ComponentId = SchemaDatabase->NetCullDistanceToComponentId.Find(NetCullDistance)) + { + return *ComponentId; + } + return SpatialConstants::INVALID_COMPONENT_ID; +} + +bool USpatialClassInfoManager::IsNetCullDistanceComponent(Worker_ComponentId ComponentId) const +{ + return SchemaDatabase->NetCullDistanceComponentIds.Contains(ComponentId); +} + +bool USpatialClassInfoManager::IsGeneratedQBIMarkerComponent(Worker_ComponentId ComponentId) const +{ + return IsSublevelComponent(ComponentId) || IsNetCullDistanceComponent(ComponentId); +} + void USpatialClassInfoManager::QuitGame() { #if WITH_EDITOR @@ -506,3 +561,43 @@ void USpatialClassInfoManager::QuitGame() FGenericPlatformMisc::RequestExit(false); #endif } + +Worker_ComponentId USpatialClassInfoManager::ComputeActorInterestComponentId(const AActor* Actor) const +{ + check(Actor); + const AActor* ActorForRelevancy = Actor; + // bAlwaysRelevant takes precedence over bNetUseOwnerRelevancy - see AActor::IsNetRelevantFor + while (!ActorForRelevancy->bAlwaysRelevant && ActorForRelevancy->bNetUseOwnerRelevancy && ActorForRelevancy->GetOwner() != nullptr) + { + ActorForRelevancy = ActorForRelevancy->GetOwner(); + } + + if (ActorForRelevancy->bAlwaysRelevant) + { + return SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID; + } + + if (GetDefault()->bEnableNetCullDistanceInterest) + { + Worker_ComponentId NCDComponentId = GetComponentIdForNetCullDistance(ActorForRelevancy->NetCullDistanceSquared); + if (NCDComponentId != SpatialConstants::INVALID_COMPONENT_ID) + { + return NCDComponentId; + } + + const AActor* DefaultActor = ActorForRelevancy->GetClass()->GetDefaultObject(); + if (ActorForRelevancy->NetCullDistanceSquared != DefaultActor->NetCullDistanceSquared) + { + UE_LOG(LogSpatialClassInfoManager, Error, TEXT("Could not find Net Cull Distance Component for distance %f, processing Actor %s via %s, because its Net Cull Distance is different from its default one."), + ActorForRelevancy->NetCullDistanceSquared, *Actor->GetPathName(), *ActorForRelevancy->GetPathName()); + + return ComputeActorInterestComponentId(DefaultActor); + } + else + { + UE_LOG(LogSpatialClassInfoManager, Error, TEXT("Could not find Net Cull Distance Component for distance %f, processing Actor %s via %s. Have you generated schema?"), + ActorForRelevancy->NetCullDistanceSquared, *Actor->GetPathName(), *ActorForRelevancy->GetPathName()); + } + } + return SpatialConstants::INVALID_COMPONENT_ID; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialDispatcher.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialDispatcher.cpp index c09c02dcce..62346a6273 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialDispatcher.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialDispatcher.cpp @@ -13,15 +13,24 @@ DEFINE_LOG_CATEGORY(LogSpatialView); -void USpatialDispatcher::Init(USpatialReceiver* InReceiver, USpatialStaticComponentView* InStaticComponentView, USpatialMetrics* InSpatialMetrics) +void SpatialDispatcher::Init(USpatialReceiver* InReceiver, USpatialStaticComponentView* InStaticComponentView, USpatialMetrics* InSpatialMetrics, USpatialWorkerFlags* InSpatialWorkerFlags) { + check(InReceiver != nullptr); Receiver = InReceiver; + + check(InStaticComponentView != nullptr); StaticComponentView = InStaticComponentView; + + check(InSpatialMetrics != nullptr); SpatialMetrics = InSpatialMetrics; + SpatialWorkerFlags = InSpatialWorkerFlags; } -void USpatialDispatcher::ProcessOps(Worker_OpList* OpList) +void SpatialDispatcher::ProcessOps(Worker_OpList* OpList) { + check(Receiver.IsValid()); + check(StaticComponentView.IsValid()); + for (size_t i = 0; i < OpList->op_count; ++i) { Worker_Op* Op = &OpList->ops[i]; @@ -53,7 +62,7 @@ void USpatialDispatcher::ProcessOps(Worker_OpList* OpList) case WORKER_OP_TYPE_REMOVE_ENTITY: Receiver->OnRemoveEntity(Op->op.remove_entity); StaticComponentView->OnRemoveEntity(Op->op.remove_entity.entity_id); - Receiver->RemoveComponentOpsForEntity(Op->op.remove_entity.entity_id); + Receiver->DropQueuedRemoveComponentOpsForEntity(Op->op.remove_entity.entity_id); break; // Components @@ -96,13 +105,14 @@ void USpatialDispatcher::ProcessOps(Worker_OpList* OpList) break; case WORKER_OP_TYPE_FLAG_UPDATE: - USpatialWorkerFlags::ApplyWorkerFlagUpdate(Op->op.flag_update); + SpatialWorkerFlags->ApplyWorkerFlagUpdate(Op->op.flag_update); break; case WORKER_OP_TYPE_LOG_MESSAGE: UE_LOG(LogSpatialView, Log, TEXT("SpatialOS Worker Log: %s"), UTF8_TO_TCHAR(Op->op.log_message.message)); break; case WORKER_OP_TYPE_METRICS: #if !UE_BUILD_SHIPPING + check(SpatialMetrics.IsValid()); SpatialMetrics->HandleWorkerMetrics(Op); #endif break; @@ -119,16 +129,17 @@ void USpatialDispatcher::ProcessOps(Worker_OpList* OpList) Receiver->FlushRetryRPCs(); } -bool USpatialDispatcher::IsExternalSchemaOp(Worker_Op* Op) const +bool SpatialDispatcher::IsExternalSchemaOp(Worker_Op* Op) const { Worker_ComponentId ComponentId = SpatialGDK::GetComponentId(Op); return SpatialConstants::MIN_EXTERNAL_SCHEMA_ID <= ComponentId && ComponentId <= SpatialConstants::MAX_EXTERNAL_SCHEMA_ID; } -void USpatialDispatcher::ProcessExternalSchemaOp(Worker_Op* Op) +void SpatialDispatcher::ProcessExternalSchemaOp(Worker_Op* Op) { Worker_ComponentId ComponentId = SpatialGDK::GetComponentId(Op); check(ComponentId != SpatialConstants::INVALID_COMPONENT_ID); + check(StaticComponentView.IsValid()); switch (Op->op_type) { @@ -150,7 +161,7 @@ void USpatialDispatcher::ProcessExternalSchemaOp(Worker_Op* Op) } } -USpatialDispatcher::FCallbackId USpatialDispatcher::OnAddComponent(Worker_ComponentId ComponentId, const TFunction& Callback) +SpatialDispatcher::FCallbackId SpatialDispatcher::OnAddComponent(Worker_ComponentId ComponentId, const TFunction& Callback) { return AddGenericOpCallback(ComponentId, WORKER_OP_TYPE_ADD_COMPONENT, [Callback](const Worker_Op* Op) { @@ -158,7 +169,7 @@ USpatialDispatcher::FCallbackId USpatialDispatcher::OnAddComponent(Worker_Compon }); } -USpatialDispatcher::FCallbackId USpatialDispatcher::OnRemoveComponent(Worker_ComponentId ComponentId, const TFunction& Callback) +SpatialDispatcher::FCallbackId SpatialDispatcher::OnRemoveComponent(Worker_ComponentId ComponentId, const TFunction& Callback) { return AddGenericOpCallback(ComponentId, WORKER_OP_TYPE_REMOVE_COMPONENT, [Callback](const Worker_Op* Op) { @@ -166,14 +177,15 @@ USpatialDispatcher::FCallbackId USpatialDispatcher::OnRemoveComponent(Worker_Com }); } -USpatialDispatcher::FCallbackId USpatialDispatcher::OnAuthorityChange(Worker_ComponentId ComponentId, const TFunction& Callback) +SpatialDispatcher::FCallbackId SpatialDispatcher::OnAuthorityChange(Worker_ComponentId ComponentId, const TFunction& Callback) { return AddGenericOpCallback(ComponentId, WORKER_OP_TYPE_AUTHORITY_CHANGE, [Callback](const Worker_Op* Op) { Callback(Op->op.authority_change); }); } -USpatialDispatcher::FCallbackId USpatialDispatcher::OnComponentUpdate(Worker_ComponentId ComponentId, const TFunction& Callback) + +SpatialDispatcher::FCallbackId SpatialDispatcher::OnComponentUpdate(Worker_ComponentId ComponentId, const TFunction& Callback) { return AddGenericOpCallback(ComponentId, WORKER_OP_TYPE_COMPONENT_UPDATE, [Callback](const Worker_Op* Op) { @@ -181,7 +193,7 @@ USpatialDispatcher::FCallbackId USpatialDispatcher::OnComponentUpdate(Worker_Com }); } -USpatialDispatcher::FCallbackId USpatialDispatcher::OnCommandRequest(Worker_ComponentId ComponentId, const TFunction& Callback) +SpatialDispatcher::FCallbackId SpatialDispatcher::OnCommandRequest(Worker_ComponentId ComponentId, const TFunction& Callback) { return AddGenericOpCallback(ComponentId, WORKER_OP_TYPE_COMMAND_REQUEST, [Callback](const Worker_Op* Op) { @@ -189,7 +201,7 @@ USpatialDispatcher::FCallbackId USpatialDispatcher::OnCommandRequest(Worker_Comp }); } -USpatialDispatcher::FCallbackId USpatialDispatcher::OnCommandResponse(Worker_ComponentId ComponentId, const TFunction& Callback) +SpatialDispatcher::FCallbackId SpatialDispatcher::OnCommandResponse(Worker_ComponentId ComponentId, const TFunction& Callback) { return AddGenericOpCallback(ComponentId, WORKER_OP_TYPE_COMMAND_RESPONSE, [Callback](const Worker_Op* Op) { @@ -197,7 +209,7 @@ USpatialDispatcher::FCallbackId USpatialDispatcher::OnCommandResponse(Worker_Com }); } -USpatialDispatcher::FCallbackId USpatialDispatcher::AddGenericOpCallback(Worker_ComponentId ComponentId, Worker_OpType OpType, const TFunction& Callback) +SpatialDispatcher::FCallbackId SpatialDispatcher::AddGenericOpCallback(Worker_ComponentId ComponentId, Worker_OpType OpType, const TFunction& Callback) { check(SpatialConstants::MIN_EXTERNAL_SCHEMA_ID <= ComponentId && ComponentId <= SpatialConstants::MAX_EXTERNAL_SCHEMA_ID); const FCallbackId NewCallbackId = NextCallbackId++; @@ -206,7 +218,7 @@ USpatialDispatcher::FCallbackId USpatialDispatcher::AddGenericOpCallback(Worker_ return NewCallbackId; } -bool USpatialDispatcher::RemoveOpCallback(FCallbackId CallbackId) +bool SpatialDispatcher::RemoveOpCallback(FCallbackId CallbackId) { CallbackIdData* CallbackData = CallbackIdToDataMap.Find(CallbackId); if (CallbackData == nullptr) @@ -251,7 +263,7 @@ bool USpatialDispatcher::RemoveOpCallback(FCallbackId CallbackId) return true; } -void USpatialDispatcher::RunCallbacks(Worker_ComponentId ComponentId, const Worker_Op* Op) +void SpatialDispatcher::RunCallbacks(Worker_ComponentId ComponentId, const Worker_Op* Op) { OpTypeToCallbacksMap* OpTypeCallbacks = ComponentOpTypeToCallbacksMap.Find(ComponentId); if (OpTypeCallbacks == nullptr) @@ -271,12 +283,12 @@ void USpatialDispatcher::RunCallbacks(Worker_ComponentId ComponentId, const Work } } -void USpatialDispatcher::MarkOpToSkip(const Worker_Op* Op) +void SpatialDispatcher::MarkOpToSkip(const Worker_Op* Op) { OpsToSkip.Add(Op); } -int USpatialDispatcher::GetNumOpsToSkip() const +int SpatialDispatcher::GetNumOpsToSkip() const { return OpsToSkip.Num(); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialOutputDevice.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialOutputDevice.cpp index 908cd390a1..e4201ecf28 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialOutputDevice.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialOutputDevice.cpp @@ -30,7 +30,7 @@ void FSpatialOutputDevice::Serialize(const TCHAR* InData, ELogVerbosity::Type Ve return; } - if (bLogToSpatial && Connection->IsConnected()) + if (bLogToSpatial && Connection != nullptr) { #if WITH_EDITOR if (GPlayInEditorID != PIEIndex) diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp index 787684df66..b0d5d3dea2 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp @@ -2,20 +2,32 @@ #include "Interop/SpatialPlayerSpawner.h" -#include "Engine/Engine.h" -#include "Engine/LocalPlayer.h" -#include "Kismet/GameplayStatics.h" -#include "TimerManager.h" - #include "EngineClasses/SpatialNetDriver.h" #include "Interop/Connection/SpatialWorkerConnection.h" #include "Interop/SpatialReceiver.h" +#include "LoadBalancing/AbstractLBStrategy.h" +#include "Schema/ServerWorker.h" +#include "Schema/UnrealObjectRef.h" +#include "SpatialCommonTypes.h" #include "SpatialConstants.h" +#include "SpatialGDKSettings.h" #include "Utils/SchemaUtils.h" +#include "Engine/Engine.h" +#include "Engine/LocalPlayer.h" +#include "Containers/StringConv.h" +#include "GameFramework/GameModeBase.h" +#include "GameFramework/PlayerStart.h" +#include "HAL/Platform.h" +#include "Kismet/GameplayStatics.h" +#include "TimerManager.h" +#include "UObject/SoftObjectPath.h" + #include #include +#include + DEFINE_LOG_CATEGORY(LogSpatialPlayerSpawner); using namespace SpatialGDK; @@ -28,46 +40,6 @@ void USpatialPlayerSpawner::Init(USpatialNetDriver* InNetDriver, FTimerManager* NumberOfAttempts = 0; } -void USpatialPlayerSpawner::ReceivePlayerSpawnRequest(Schema_Object* Payload, const char* CallerAttribute, Worker_RequestId RequestId ) -{ - FString Attributes = FString{ UTF8_TO_TCHAR(CallerAttribute) }; - - bool bAlreadyHasPlayer; - WorkersWithPlayersSpawned.Emplace(Attributes, &bAlreadyHasPlayer); - - // Accept the player if we have not already accepted a player from this worker. - if (!bAlreadyHasPlayer) - { - // Extract spawn parameters. - FString URLString = GetStringFromSchema(Payload, 1); - - FUniqueNetIdRepl UniqueId; - TArray UniqueIdBytes = GetBytesFromSchema(Payload, 2); - FNetBitReader UniqueIdReader(nullptr, UniqueIdBytes.GetData(), UniqueIdBytes.Num() * 8); - UniqueIdReader << UniqueId; - - FName OnlinePlatformName = FName(*GetStringFromSchema(Payload, 3)); - bool bSimulatedPlayer = GetBoolFromSchema(Payload, 4); - - URLString.Append(TEXT("?workerAttribute=")).Append(Attributes); - if (bSimulatedPlayer) - { - URLString += TEXT("?simulatedPlayer=1"); - } - - NetDriver->AcceptNewPlayer(FURL(nullptr, *URLString, TRAVEL_Absolute), UniqueId, OnlinePlatformName); - } - - // Send a successful response if the player has been accepted, either from this request or one in the past. - Worker_CommandResponse CommandResponse = {}; - CommandResponse.component_id = SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID; - CommandResponse.command_index = 1; - CommandResponse.schema_type = Schema_CreateCommandResponse(); - Schema_Object* ResponseObject = Schema_GetCommandResponseObject(CommandResponse.schema_type); - - NetDriver->Connection->SendCommandResponse(RequestId, &CommandResponse); -} - void USpatialPlayerSpawner::SendPlayerSpawnRequest() { // Send an entity query for the SpatialSpawner and bind a delegate so that once it's found, we send a spawn command. @@ -97,29 +69,9 @@ void USpatialPlayerSpawner::SendPlayerSpawnRequest() { checkf(Op.result_count == 1, TEXT("There should never be more than one SpatialSpawner entity.")); - // Construct and send the player spawn request. - FURL LoginURL; - FUniqueNetIdRepl UniqueId; - FName OnlinePlatformName; - ObtainPlayerParams(LoginURL, UniqueId, OnlinePlatformName); - - Worker_CommandRequest CommandRequest = {}; - CommandRequest.component_id = SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID; - CommandRequest.command_index = 1; - CommandRequest.schema_type = Schema_CreateCommandRequest(); - Schema_Object* RequestObject = Schema_GetCommandRequestObject(CommandRequest.schema_type); - AddStringToSchema(RequestObject, 1, LoginURL.ToString(true)); - - // Write player identity information. - FNetBitWriter UniqueIdWriter(0); - UniqueIdWriter << UniqueId; - AddBytesToSchema(RequestObject, 2, UniqueIdWriter); - AddStringToSchema(RequestObject, 3, OnlinePlatformName.ToString()); - UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(NetDriver); - bool bSimulatedPlayer = GameInstance ? GameInstance->IsSimulatedPlayer() : false; - Schema_AddBool(RequestObject, 4, bSimulatedPlayer); - - NetDriver->Connection->SendCommandRequest(Op.results[0].entity_id, &CommandRequest, 1); + SpatialGDK::SpawnPlayerRequest SpawnRequest = ObtainPlayerParams(); + Worker_CommandRequest SpawnPlayerCommandRequest = PlayerSpawner::CreatePlayerSpawnRequest(SpawnRequest); + NetDriver->Connection->SendCommandRequest(Op.results[0].entity_id, &SpawnPlayerCommandRequest, SpatialConstants::PLAYER_SPAWNER_SPAWN_PLAYER_COMMAND_ID); } }); @@ -129,11 +81,66 @@ void USpatialPlayerSpawner::SendPlayerSpawnRequest() ++NumberOfAttempts; } -void USpatialPlayerSpawner::ReceivePlayerSpawnResponse(const Worker_CommandResponseOp& Op) +SpatialGDK::SpawnPlayerRequest USpatialPlayerSpawner::ObtainPlayerParams() const +{ + FURL LoginURL; + FUniqueNetIdRepl UniqueId; + + const FWorldContext* const WorldContext = GEngine->GetWorldContextFromWorld(NetDriver->GetWorld()); + check(WorldContext->OwningGameInstance); + + const UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(NetDriver); + const bool bIsSimulatedPlayer = GameInstance ? GameInstance->IsSimulatedPlayer() : false; + + // This code is adapted from PendingNetGame.cpp:242 + if (const ULocalPlayer* LocalPlayer = WorldContext->OwningGameInstance->GetFirstGamePlayer()) + { + // Send the player nickname if available + FString OverrideName = LocalPlayer->GetNickname(); + if (OverrideName.Len() > 0) + { + LoginURL.AddOption(*FString::Printf(TEXT("Name=%s"), *OverrideName)); + } + + LoginURL.AddOption(*FString::Printf(TEXT("workerAttribute=%s"), *FString::Format(TEXT("workerId:{0}"), { NetDriver->Connection->GetWorkerId() }))); + + if (bIsSimulatedPlayer) + { + LoginURL.AddOption(*FString::Printf(TEXT("simulatedPlayer=1"))); + } + + // Send any game-specific url options for this player + const FString GameUrlOptions = LocalPlayer->GetGameLoginOptions(); + if (GameUrlOptions.Len() > 0) + { + LoginURL.AddOption(*FString::Printf(TEXT("%s"), *GameUrlOptions)); + } + // Pull in options from the current world URL (to preserve options added to a travel URL) + const TArray& LastURLOptions = WorldContext->LastURL.Op; + for (const FString& Op : LastURLOptions) + { + LoginURL.AddOption(*Op); + } + LoginURL.Portal = WorldContext->LastURL.Portal; + + // Send the player unique Id at login + UniqueId = LocalPlayer->GetPreferredUniqueNetId(); + } + else + { + UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Couldn't get LocalPlayer data from game instance when trying to spawn player.")); + } + + FName OnlinePlatformName = WorldContext->OwningGameInstance->GetOnlinePlatformName(); + + return { LoginURL, UniqueId, OnlinePlatformName, bIsSimulatedPlayer }; +} + +void USpatialPlayerSpawner::ReceivePlayerSpawnResponseOnClient(const Worker_CommandResponseOp& Op) { if (Op.status_code == WORKER_STATUS_CODE_SUCCESS) { - UE_LOG(LogSpatialPlayerSpawner, Display, TEXT("Player spawned sucessfully")); + UE_LOG(LogSpatialPlayerSpawner, Display, TEXT("PlayerSpawn returned from server sucessfully")); } else if (NumberOfAttempts < SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS) { @@ -156,37 +163,208 @@ void USpatialPlayerSpawner::ReceivePlayerSpawnResponse(const Worker_CommandRespo } } -void USpatialPlayerSpawner::ObtainPlayerParams(FURL& LoginURL, FUniqueNetIdRepl& OutUniqueId, FName& OutOnlinePlatformName) +void USpatialPlayerSpawner::ReceivePlayerSpawnRequestOnServer(const Worker_CommandRequestOp& Op) { - const FWorldContext* const WorldContext = GEngine->GetWorldContextFromWorld(NetDriver->GetWorld()); - check(WorldContext->OwningGameInstance); + UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Received PlayerSpawn request on server")); - // This code is adapted from PendingNetGame.cpp:242 - if (ULocalPlayer* LocalPlayer = WorldContext->OwningGameInstance->GetFirstGamePlayer()) + FUTF8ToTCHAR FStringConversion(reinterpret_cast(Op.caller_worker_id), strlen(Op.caller_worker_id)); + FString ClientWorkerId(FStringConversion.Length(), FStringConversion.Get()); + + // Accept the player if we have not already accepted a player from this worker. + bool bAlreadyHasPlayer; + WorkersWithPlayersSpawned.Emplace(ClientWorkerId, &bAlreadyHasPlayer); + if (bAlreadyHasPlayer) { - // Send the player nickname if available - FString OverrideName = LocalPlayer->GetNickname(); - if (OverrideName.Len() > 0) + UE_LOG(LogSpatialPlayerSpawner, Verbose, TEXT("Ignoring duplicate PlayerSpawn request. Client worker ID: %s"), *ClientWorkerId); + return; + } + + Schema_Object* RequestPayload = Schema_GetCommandRequestObject(Op.request.schema_type); + FindPlayerStartAndProcessPlayerSpawn(RequestPayload, ClientWorkerId); + + const Worker_CommandResponse Response = PlayerSpawner::CreatePlayerSpawnResponse(); + NetDriver->Connection->SendCommandResponse(Op.request_id, &Response); +} + +void USpatialPlayerSpawner::FindPlayerStartAndProcessPlayerSpawn(Schema_Object* SpawnPlayerRequest, const PhysicalWorkerName& ClientWorkerId) +{ + // If load-balancing is enabled AND the strategy dictates that another worker should have authority over + // the chosen PlayerStart THEN the spawn request is forwarded to that worker to prevent an initial player + // migration. Immediate player migrations can still happen if + // 1) the load-balancing strategy has different rules for PlayerStart Actors and Characters / Controllers / + // Player States or, + // 2) the load-balancing strategy can change the authoritative virtual worker ID for a PlayerStart Actor + // during the lifetime of a deployment. + if (GetDefault()->bEnableUnrealLoadBalancer) + { + // We need to specifically extract the URL from the PlayerSpawn request for finding a PlayerStart. + const FURL Url = PlayerSpawner::ExtractUrlFromPlayerSpawnParams(SpawnPlayerRequest); + AActor* PlayerStartActor = NetDriver->GetWorld()->GetAuthGameMode()->FindPlayerStart(nullptr, Url.Portal); + + check(NetDriver->LoadBalanceStrategy != nullptr); + if (!NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*PlayerStartActor)) { - LoginURL.AddOption(*FString::Printf(TEXT("Name=%s"), *OverrideName)); + // If we fail to forward the spawn request, we default to the normal player spawning flow. + const bool bSuccessfullyForwardedRequest = ForwardSpawnRequestToStrategizedServer(SpawnPlayerRequest, PlayerStartActor, ClientWorkerId); + if (bSuccessfullyForwardedRequest) + { + return; + } } + else + { + UE_LOG(LogSpatialPlayerSpawner, Verbose, TEXT("Handling SpawnPlayerRequest request locally. Client worker ID: %s."), *ClientWorkerId); + PassSpawnRequestToNetDriver(SpawnPlayerRequest, PlayerStartActor); + return; + } + } - // Send any game-specific url options for this player - FString GameUrlOptions = LocalPlayer->GetGameLoginOptions(); - if (GameUrlOptions.Len() > 0) + PassSpawnRequestToNetDriver(SpawnPlayerRequest, nullptr); +} + +void USpatialPlayerSpawner::PassSpawnRequestToNetDriver(Schema_Object* PlayerSpawnData, AActor* PlayerStart) +{ + SpatialGDK::SpawnPlayerRequest SpawnRequest = PlayerSpawner::ExtractPlayerSpawnParams(PlayerSpawnData); + + AGameModeBase* GameMode = NetDriver->GetWorld()->GetAuthGameMode(); + + // Set a prioritized PlayerStart for the new player to spawn at. Passing nullptr is a no-op. + GameMode->SetPrioritizedPlayerStart(PlayerStart); + NetDriver->AcceptNewPlayer(SpawnRequest.LoginURL, SpawnRequest.UniqueId, SpawnRequest.OnlinePlatformName); + GameMode->SetPrioritizedPlayerStart(nullptr); +} + +// Copies the fields from the SpawnPlayerRequest argument into a ForwardSpawnPlayerRequest (along with the PlayerStart UnrealObjectRef). +bool USpatialPlayerSpawner::ForwardSpawnRequestToStrategizedServer(const Schema_Object* OriginalPlayerSpawnRequest, AActor* PlayerStart, const PhysicalWorkerName& ClientWorkerId) +{ + // Find which virtual worker should have authority of the PlayerStart. + const VirtualWorkerId SpawningVirtualWorker = NetDriver->LoadBalanceStrategy->WhoShouldHaveAuthority(*PlayerStart); + if (SpawningVirtualWorker == SpatialConstants::INVALID_VIRTUAL_WORKER_ID) + { + UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Load-balance strategy returned invalid virtual worker ID for selected PlayerStart Actor: %s. Defaulting to normal player spawning flow."), *GetNameSafe(PlayerStart)); + return false; + } + + // Find the server worker entity corresponding to the PlayerStart strategized virtual worker. + const Worker_EntityId ServerWorkerEntity = NetDriver->VirtualWorkerTranslator->GetServerWorkerEntityForVirtualWorker(SpawningVirtualWorker); + if (ServerWorkerEntity == SpatialConstants::INVALID_ENTITY_ID) + { + UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Virtual worker translator returned invalid server worker entity ID. Virtual worker: %d. Defaulting to normal player spawning flow."), SpawningVirtualWorker); + return false; + } + + UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Forwarding player spawn request to strategized worker. Client ID: %s. PlayerStart: %s. Strategeized virtual worker %d. Forward server worker entity: %lld"), + *ClientWorkerId, *GetNameSafe(PlayerStart), SpawningVirtualWorker, ServerWorkerEntity); + + // To pass the PlayerStart Actor to another worker we use a FUnrealObjectRef. + FNetworkGUID PlayerStartGuid = NetDriver->PackageMap->ResolveStablyNamedObject(PlayerStart); + FUnrealObjectRef PlayerStartObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromNetGUID(PlayerStartGuid); + + // Create a request using the PlayerStart reference and by copying the data from the PlayerSpawn request from the client. + // The Schema_CommandRequest is constructed separately from the Worker_CommandRequest so we can store it in the outgoing + // map for future retries. + Schema_CommandRequest* ForwardSpawnPlayerSchemaRequest = Schema_CreateCommandRequest(); + ServerWorker::CreateForwardPlayerSpawnSchemaRequest(ForwardSpawnPlayerSchemaRequest, PlayerStartObjectRef, OriginalPlayerSpawnRequest, ClientWorkerId); + Worker_CommandRequest ForwardSpawnPlayerRequest = ServerWorker::CreateForwardPlayerSpawnRequest(Schema_CopyCommandRequest(ForwardSpawnPlayerSchemaRequest)); + + Worker_RequestId RequestId = NetDriver->Connection->SendCommandRequest(ServerWorkerEntity, &ForwardSpawnPlayerRequest, SpatialConstants::SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND_ID); + + OutgoingForwardPlayerSpawnRequests.Add(RequestId, TUniquePtr(ForwardSpawnPlayerSchemaRequest)); + + return true; +} + +void USpatialPlayerSpawner::ReceiveForwardedPlayerSpawnRequest(const Worker_CommandRequestOp& Op) +{ + Schema_Object* Payload = Schema_GetCommandRequestObject(Op.request.schema_type); + Schema_Object* PlayerSpawnData = Schema_GetObject(Payload, SpatialConstants::FORWARD_SPAWN_PLAYER_DATA_ID); + FString ClientWorkerId = GetStringFromSchema(Payload, SpatialConstants::FORWARD_SPAWN_PLAYER_CLIENT_WORKER_ID); + + // Accept the player if we have not already accepted a player from this worker. + bool bAlreadyHasPlayer; + WorkersWithPlayersSpawned.Emplace(ClientWorkerId, &bAlreadyHasPlayer); + if (bAlreadyHasPlayer) + { + UE_LOG(LogSpatialPlayerSpawner, Verbose, TEXT("Ignoring duplicate forward player spawn request. Client worker ID: %s"), *ClientWorkerId); + return; + } + + FUnrealObjectRef PlayerStartRef = GetObjectRefFromSchema(Payload, SpatialConstants::FORWARD_SPAWN_PLAYER_START_ACTOR_ID); + + bool bUnresolvedRef = false; + if (AActor* PlayerStart = Cast(FUnrealObjectRef::ToObjectPtr(PlayerStartRef, NetDriver->PackageMap, bUnresolvedRef))) + { + UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Received ForwardPlayerSpawn request. Client worker ID: %s. PlayerStart: %s"), *ClientWorkerId, *PlayerStart->GetName()); + PassSpawnRequestToNetDriver(PlayerSpawnData, PlayerStart); + } + else + { + UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("PlayerStart Actor UnrealObjectRef was invalid on forwarded player spawn request worker: %s. Defaulting to normal player spawning flow."), *ClientWorkerId); + } + + Worker_CommandResponse Response = ServerWorker::CreateForwardPlayerSpawnResponse(!bUnresolvedRef); + NetDriver->Connection->SendCommandResponse(Op.request_id, &Response); +} + +void USpatialPlayerSpawner::ReceiveForwardPlayerSpawnResponse(const Worker_CommandResponseOp& Op) +{ + if (Op.status_code == WORKER_STATUS_CODE_SUCCESS) + { + const bool bForwardingSucceeding = GetBoolFromSchema(Schema_GetCommandResponseObject(Op.response.schema_type), SpatialConstants::FORWARD_SPAWN_PLAYER_RESPONSE_SUCCESS_ID); + if (bForwardingSucceeding) { - LoginURL.AddOption(*FString::Printf(TEXT("%s"), *GameUrlOptions)); + // If forwarding the player spawn request succeeded, clean up our outgoing request map. + UE_LOG(LogSpatialPlayerSpawner, Display, TEXT("Forwarding player spawn succeeded")); + OutgoingForwardPlayerSpawnRequests.Remove(Op.request_id); } - // Pull in options from the current world URL (to preserve options added to a travel URL) - const TArray& LastURLOptions = WorldContext->LastURL.Op; - for (const FString& Op : LastURLOptions) + else { - LoginURL.AddOption(*Op); + // If the forwarding failed, e.g. if the chosen PlayerStart Actor was deleted on the other server, + // then try spawning again. + RetryForwardSpawnPlayerRequest(Op.entity_id, Op.request_id, true); } + return; + } - // Send the player unique Id at login - OutUniqueId = LocalPlayer->GetPreferredUniqueNetId(); + UE_LOG(LogSpatialPlayerSpawner, Warning, TEXT("ForwardPlayerSpawn request failed: \"%s\". Retrying"), UTF8_TO_TCHAR(Op.message)); + + FTimerHandle RetryTimer; + TimerManager->SetTimer(RetryTimer, [EntityId = Op.entity_id, RequestId = Op.request_id, WeakThis = TWeakObjectPtr(this)]() + { + if (USpatialPlayerSpawner* Spawner = WeakThis.Get()) + { + Spawner->RetryForwardSpawnPlayerRequest(EntityId, RequestId); + } + }, SpatialConstants::GetCommandRetryWaitTimeSeconds(SpatialConstants::FORWARD_PLAYER_SPAWN_COMMAND_WAIT_SECONDS), false); +} + +void USpatialPlayerSpawner::RetryForwardSpawnPlayerRequest(const Worker_EntityId EntityId, const Worker_RequestId RequestId, const bool bShouldTryDifferentPlayerStart) +{ + // If the forward request data doesn't exist, we assume the command actually succeeded previously and this failure is spurious. + if (!OutgoingForwardPlayerSpawnRequests.Contains(RequestId)) + { + return; } - OutOnlinePlatformName = WorldContext->OwningGameInstance->GetOnlinePlatformName(); + Schema_CommandRequest* OldRequest = OutgoingForwardPlayerSpawnRequests.FindAndRemoveChecked(RequestId).Get(); + Schema_Object* OldRequestPayload = Schema_GetCommandRequestObject(OldRequest); + + // If the chosen PlayerStart is deleted or being deleted, we will pick another. + const FUnrealObjectRef PlayerStartRef = GetObjectRefFromSchema(OldRequestPayload, SpatialConstants::FORWARD_SPAWN_PLAYER_START_ACTOR_ID); + const TWeakObjectPtr PlayerStart = NetDriver->PackageMap->GetObjectFromUnrealObjectRef(PlayerStartRef); + if (bShouldTryDifferentPlayerStart || !PlayerStart.IsValid() || PlayerStart->IsPendingKill()) + { + UE_LOG(LogSpatialPlayerSpawner, Warning, TEXT("Target PlayerStart to spawn player was no longer valid after forwarding failed. Finding another PlayerStart.")); + Schema_Object* SpawnPlayerData = Schema_GetObject(OldRequestPayload, SpatialConstants::FORWARD_SPAWN_PLAYER_DATA_ID); + const PhysicalWorkerName& ClientWorkerId = GetStringFromSchema(OldRequestPayload, SpatialConstants::FORWARD_SPAWN_PLAYER_CLIENT_WORKER_ID); + FindPlayerStartAndProcessPlayerSpawn(SpawnPlayerData, ClientWorkerId); + return; + } + + // Resend the ForwardSpawnPlayer request. + Worker_CommandRequest ForwardSpawnPlayerRequest = ServerWorker::CreateForwardPlayerSpawnRequest(Schema_CopyCommandRequest(OldRequest)); + Worker_RequestId NewRequestId = NetDriver->Connection->SendCommandRequest(EntityId, &ForwardSpawnPlayerRequest, SpatialConstants::SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND_ID); + + // Move the request data from the old request ID map entry across to the new ID entry. + OutgoingForwardPlayerSpawnRequests.Add(NewRequestId, TUniquePtr(OldRequest)); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialRPCService.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialRPCService.cpp new file mode 100644 index 0000000000..63c85219da --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialRPCService.cpp @@ -0,0 +1,497 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Interop/SpatialRPCService.h" + +#include "Interop/SpatialStaticComponentView.h" +#include "Schema/ClientEndpoint.h" +#include "Schema/MulticastRPCs.h" +#include "Schema/ServerEndpoint.h" + +DEFINE_LOG_CATEGORY(LogSpatialRPCService); + +namespace SpatialGDK +{ + +SpatialRPCService::SpatialRPCService(ExtractRPCDelegate ExtractRPCCallback, const USpatialStaticComponentView* View) + : ExtractRPCCallback(ExtractRPCCallback) + , View(View) +{ +} + +EPushRPCResult SpatialRPCService::PushRPC(Worker_EntityId EntityId, ERPCType Type, RPCPayload Payload) +{ + EntityRPCType EntityType = EntityRPCType(EntityId, Type); + + if (RPCRingBufferUtils::ShouldQueueOverflowed(Type) && OverflowedRPCs.Contains(EntityType)) + { + // Already has queued RPCs of this type, queue until those are pushed. + AddOverflowedRPC(EntityType, MoveTemp(Payload)); + return EPushRPCResult::QueueOverflowed; + } + + EPushRPCResult Result = PushRPCInternal(EntityId, Type, MoveTemp(Payload)); + + if (Result == EPushRPCResult::QueueOverflowed) + { + AddOverflowedRPC(EntityType, MoveTemp(Payload)); + } + + return Result; +} + +EPushRPCResult SpatialRPCService::PushRPCInternal(Worker_EntityId EntityId, ERPCType Type, RPCPayload&& Payload) +{ + const Worker_ComponentId RingBufferComponentId = RPCRingBufferUtils::GetRingBufferComponentId(Type); + + const EntityComponentId EntityComponent = { EntityId, RingBufferComponentId }; + const EntityRPCType EntityType = EntityRPCType(EntityId, Type); + + Schema_Object* EndpointObject; + uint64 LastAckedRPCId; + if (View->HasComponent(EntityId, RingBufferComponentId)) + { + if (!View->HasAuthority(EntityId, RingBufferComponentId)) + { + return EPushRPCResult::NoRingBufferAuthority; + } + + EndpointObject = Schema_GetComponentUpdateFields(GetOrCreateComponentUpdate(EntityComponent)); + + if (Type == ERPCType::NetMulticast) + { + // Assume all multicast RPCs are auto-acked. + LastAckedRPCId = LastSentRPCIds.FindRef(EntityType); + } + else + { + // We shouldn't have authority over the component that has the acks. + if (View->HasAuthority(EntityId, RPCRingBufferUtils::GetAckComponentId(Type))) + { + return EPushRPCResult::HasAckAuthority; + } + + LastAckedRPCId = GetAckFromView(EntityId, Type); + } + } + else + { + // If the entity isn't in the view, we assume this RPC was called before + // CreateEntityRequest, so we put it into a component data object. + EndpointObject = Schema_GetComponentDataFields(GetOrCreateComponentData(EntityComponent)); + + LastAckedRPCId = 0; + } + + uint64 NewRPCId = LastSentRPCIds.FindRef(EntityType) + 1; + + // Check capacity. + if (LastAckedRPCId + RPCRingBufferUtils::GetRingBufferSize(Type) >= NewRPCId) + { + RPCRingBufferUtils::WriteRPCToSchema(EndpointObject, Type, NewRPCId, Payload); + + LastSentRPCIds.Add(EntityType, NewRPCId); + } + else + { + // Overflowed + if (RPCRingBufferUtils::ShouldQueueOverflowed(Type)) + { + return EPushRPCResult::QueueOverflowed; + } + else + { + return EPushRPCResult::DropOverflowed; + } + } + + return EPushRPCResult::Success; +} + +void SpatialRPCService::PushOverflowedRPCs() +{ + for (auto It = OverflowedRPCs.CreateIterator(); It; ++It) + { + Worker_EntityId EntityId = It.Key().EntityId; + ERPCType Type = It.Key().Type; + TArray& OverflowedRPCArray = It.Value(); + + int NumProcessed = 0; + bool bShouldDrop = false; + for (RPCPayload& Payload : OverflowedRPCArray) + { + EPushRPCResult Result = PushRPCInternal(EntityId, Type, MoveTemp(Payload)); + + switch (Result) + { + case EPushRPCResult::Success: + NumProcessed++; + break; + case EPushRPCResult::DropOverflowed: + checkf(false, TEXT("Shouldn't be able to drop on overflow for RPC type that was previously queued.")); + break; + case EPushRPCResult::HasAckAuthority: + UE_LOG(LogSpatialRPCService, Warning, TEXT("SpatialRPCService::PushOverflowedRPCs: Gained authority over ack component for RPC type that was overflowed. Entity: %lld, RPC type: %s"), EntityId, *SpatialConstants::RPCTypeToString(Type)); + bShouldDrop = true; + break; + case EPushRPCResult::NoRingBufferAuthority: + UE_LOG(LogSpatialRPCService, Warning, TEXT("SpatialRPCService::PushOverflowedRPCs: Lost authority over ring buffer component for RPC type that was overflowed. Entity: %lld, RPC type: %s"), EntityId, *SpatialConstants::RPCTypeToString(Type)); + bShouldDrop = true; + break; + } + + // This includes the valid case of RPCs still overflowing (EPushRPCResult::QueueOverflowed), as well as the error cases. + if (Result != EPushRPCResult::Success) + { + break; + } + } + + if (NumProcessed == OverflowedRPCArray.Num() || bShouldDrop) + { + It.RemoveCurrent(); + } + else + { + OverflowedRPCArray.RemoveAt(0, NumProcessed); + } + } +} + +void SpatialRPCService::ClearOverflowedRPCs(Worker_EntityId EntityId) +{ + for (uint8 RPCType = static_cast(ERPCType::ClientReliable); RPCType <= static_cast(ERPCType::NetMulticast); RPCType++) + { + OverflowedRPCs.Remove(EntityRPCType(EntityId, static_cast(RPCType))); + } +} + +TArray SpatialRPCService::GetRPCsAndAcksToSend() +{ + TArray UpdatesToSend; + + for (auto& It : PendingComponentUpdatesToSend) + { + SpatialRPCService::UpdateToSend& UpdateToSend = UpdatesToSend.AddZeroed_GetRef(); + UpdateToSend.EntityId = It.Key.EntityId; + UpdateToSend.Update.component_id = It.Key.ComponentId; + UpdateToSend.Update.schema_type = It.Value; + } + + PendingComponentUpdatesToSend.Empty(); + + return UpdatesToSend; +} + +TArray SpatialRPCService::GetRPCComponentsOnEntityCreation(Worker_EntityId EntityId) +{ + static Worker_ComponentId EndpointComponentIds[] = { + SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID, + SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID, + SpatialConstants::MULTICAST_RPCS_COMPONENT_ID + }; + + TArray Components; + + for (Worker_ComponentId EndpointComponentId : EndpointComponentIds) + { + const EntityComponentId EntityComponent = { EntityId, EndpointComponentId }; + + Worker_ComponentData& Component = Components.AddZeroed_GetRef(); + Component.component_id = EndpointComponentId; + if (Schema_ComponentData** ComponentData = PendingRPCsOnEntityCreation.Find(EntityComponent)) + { + // When sending initial multicast RPCs, write the number of RPCs into a separate field instead of + // last sent RPC ID field. When the server gains authority for the first time, it will copy the + // value over to last sent RPC ID, so the clients that checked out the entity process the initial RPCs. + if (EndpointComponentId == SpatialConstants::MULTICAST_RPCS_COMPONENT_ID) + { + RPCRingBufferUtils::MoveLastSentIdToInitiallyPresentCount(Schema_GetComponentDataFields(*ComponentData), LastSentRPCIds[EntityRPCType(EntityId, ERPCType::NetMulticast)]); + } + + if (EndpointComponentId == SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID) + { + UE_LOG(LogSpatialRPCService, Error, TEXT("SpatialRPCService::GetRPCComponentsOnEntityCreation: Initial RPCs present on ClientEndpoint! EntityId: %lld"), EntityId); + } + + Component.schema_type = *ComponentData; + PendingRPCsOnEntityCreation.Remove(EntityComponent); + } + else + { + Component.schema_type = Schema_CreateComponentData(); + } + } + + return Components; +} + +void SpatialRPCService::ExtractRPCsForEntity(Worker_EntityId EntityId, Worker_ComponentId ComponentId) +{ + switch (ComponentId) + { + case SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID: + if (View->HasAuthority(EntityId, SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID)) + { + ExtractRPCsForType(EntityId, ERPCType::ServerReliable); + ExtractRPCsForType(EntityId, ERPCType::ServerUnreliable); + } + break; + case SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID: + if (View->HasAuthority(EntityId, SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID)) + { + ExtractRPCsForType(EntityId, ERPCType::ClientReliable); + ExtractRPCsForType(EntityId, ERPCType::ClientUnreliable); + } + break; + case SpatialConstants::MULTICAST_RPCS_COMPONENT_ID: + ExtractRPCsForType(EntityId, ERPCType::NetMulticast); + break; + default: + checkNoEntry(); + break; + } +} + +void SpatialRPCService::OnCheckoutMulticastRPCComponentOnEntity(Worker_EntityId EntityId) +{ + const MulticastRPCs* Component = View->GetComponentData(EntityId); + + if (!ensure(Component != nullptr)) + { + UE_LOG(LogSpatialRPCService, Error, TEXT("Multicast RPC component for entity with ID %lld was not present at point of checking out the component."), EntityId); + return; + } + + // When checking out entity, ignore multicast RPCs that are already on the component. + LastSeenMulticastRPCIds.Add(EntityId, Component->MulticastRPCBuffer.LastSentRPCId); +} + +void SpatialRPCService::OnRemoveMulticastRPCComponentForEntity(Worker_EntityId EntityId) +{ + LastSeenMulticastRPCIds.Remove(EntityId); +} + +void SpatialRPCService::OnEndpointAuthorityGained(Worker_EntityId EntityId, Worker_ComponentId ComponentId) +{ + switch (ComponentId) + { + case SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID: + { + const ClientEndpoint* Endpoint = View->GetComponentData(EntityId); + LastAckedRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientReliable), Endpoint->ReliableRPCAck); + LastAckedRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientUnreliable), Endpoint->UnreliableRPCAck); + LastSentRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerReliable), Endpoint->ReliableRPCBuffer.LastSentRPCId); + LastSentRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerUnreliable), Endpoint->UnreliableRPCBuffer.LastSentRPCId); + break; + } + case SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID: + { + const ServerEndpoint* Endpoint = View->GetComponentData(EntityId); + LastAckedRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerReliable), Endpoint->ReliableRPCAck); + LastAckedRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerUnreliable), Endpoint->UnreliableRPCAck); + LastSentRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientReliable), Endpoint->ReliableRPCBuffer.LastSentRPCId); + LastSentRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientUnreliable), Endpoint->UnreliableRPCBuffer.LastSentRPCId); + break; + } + case SpatialConstants::MULTICAST_RPCS_COMPONENT_ID: + { + const MulticastRPCs* Component = View->GetComponentData(EntityId); + + if (Component->MulticastRPCBuffer.LastSentRPCId == 0 && Component->InitiallyPresentMulticastRPCsCount > 0) + { + // Update last sent ID to the number of initially present RPCs so the clients who check out this entity + // as it's created can process the initial multicast RPCs. + LastSentRPCIds.Add(EntityRPCType(EntityId, ERPCType::NetMulticast), Component->InitiallyPresentMulticastRPCsCount); + + RPCRingBufferDescriptor Descriptor = RPCRingBufferUtils::GetRingBufferDescriptor(ERPCType::NetMulticast); + Schema_Object* SchemaObject = Schema_GetComponentUpdateFields(GetOrCreateComponentUpdate(EntityComponentId{ EntityId, ComponentId })); + Schema_AddUint64(SchemaObject, Descriptor.LastSentRPCFieldId, Component->InitiallyPresentMulticastRPCsCount); + } + else + { + LastSentRPCIds.Add(EntityRPCType(EntityId, ERPCType::NetMulticast), Component->MulticastRPCBuffer.LastSentRPCId); + } + + break; + } + default: + checkNoEntry(); + break; + } +} + +void SpatialRPCService::OnEndpointAuthorityLost(Worker_EntityId EntityId, Worker_ComponentId ComponentId) +{ + switch (ComponentId) + { + case SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID: + { + LastAckedRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ClientReliable)); + LastAckedRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ClientUnreliable)); + LastSentRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerReliable)); + LastSentRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerUnreliable)); + ClearOverflowedRPCs(EntityId); + break; + } + case SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID: + { + LastAckedRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerReliable)); + LastAckedRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerUnreliable)); + LastSentRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ClientReliable)); + LastSentRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ClientUnreliable)); + ClearOverflowedRPCs(EntityId); + break; + } + case SpatialConstants::MULTICAST_RPCS_COMPONENT_ID: + { + // Set last seen to last sent, so we don't process own RPCs after crossing the boundary. + LastSeenMulticastRPCIds.Add(EntityId, LastSentRPCIds[EntityRPCType(EntityId, ERPCType::NetMulticast)]); + LastSentRPCIds.Remove(EntityRPCType(EntityId, ERPCType::NetMulticast)); + break; + } + default: + checkNoEntry(); + break; + } +} + +void SpatialRPCService::ExtractRPCsForType(Worker_EntityId EntityId, ERPCType Type) +{ + uint64 LastSeenRPCId; + EntityRPCType EntityTypePair = EntityRPCType(EntityId, Type); + + if (Type == ERPCType::NetMulticast) + { + LastSeenRPCId = LastSeenMulticastRPCIds[EntityId]; + } + else + { + LastSeenRPCId = LastAckedRPCIds[EntityTypePair]; + } + + const RPCRingBuffer& Buffer = GetBufferFromView(EntityId, Type); + + uint64 LastProcessedRPCId = LastSeenRPCId; + if (Buffer.LastSentRPCId >= LastSeenRPCId) + { + uint64 FirstRPCIdToRead = LastSeenRPCId + 1; + + uint32 BufferSize = RPCRingBufferUtils::GetRingBufferSize(Type); + if (Buffer.LastSentRPCId > LastSeenRPCId + BufferSize) + { + UE_LOG(LogSpatialRPCService, Warning, TEXT("SpatialRPCService::ExtractRPCsForType: RPCs were overwritten without being processed! Entity: %lld, RPC type: %s, last seen RPC ID: %d, last sent ID: %d, buffer size: %d"), + EntityId, *SpatialConstants::RPCTypeToString(Type), LastSeenRPCId, Buffer.LastSentRPCId, BufferSize); + FirstRPCIdToRead = Buffer.LastSentRPCId - BufferSize + 1; + } + + for (uint64 RPCId = FirstRPCIdToRead; RPCId <= Buffer.LastSentRPCId; RPCId++) + { + const TOptional& Element = Buffer.GetRingBufferElement(RPCId); + if (Element.IsSet()) + { + bool bKeepExtracting = ExtractRPCCallback.Execute(EntityId, Type, Element.GetValue()); + if (!bKeepExtracting) + { + break; + } + LastProcessedRPCId = RPCId; + } + else + { + UE_LOG(LogSpatialRPCService, Warning, TEXT("SpatialRPCService::ExtractRPCsForType: Ring buffer element empty. Entity: %lld, RPC type: %s, empty element RPC id: %d"), EntityId, *SpatialConstants::RPCTypeToString(Type), RPCId); + } + } + } + else + { + UE_LOG(LogSpatialRPCService, Warning, TEXT("SpatialRPCService::ExtractRPCsForType: Last sent RPC has smaller ID than last seen RPC. Entity: %lld, RPC type: %s, last sent ID: %d, last seen ID: %d"), + EntityId, *SpatialConstants::RPCTypeToString(Type), Buffer.LastSentRPCId, LastSeenRPCId); + } + + if (LastProcessedRPCId > LastSeenRPCId) + { + if (Type == ERPCType::NetMulticast) + { + LastSeenMulticastRPCIds[EntityId] = LastProcessedRPCId; + } + else + { + LastAckedRPCIds[EntityTypePair] = LastProcessedRPCId; + const EntityComponentId EntityComponentPair = { EntityId, RPCRingBufferUtils::GetAckComponentId(Type) }; + + Schema_Object* EndpointObject = Schema_GetComponentUpdateFields(GetOrCreateComponentUpdate(EntityComponentPair)); + + RPCRingBufferUtils::WriteAckToSchema(EndpointObject, Type, LastProcessedRPCId); + } + } +} + +void SpatialRPCService::AddOverflowedRPC(EntityRPCType EntityType, RPCPayload&& Payload) +{ + OverflowedRPCs.FindOrAdd(EntityType).Add(MoveTemp(Payload)); +} + +uint64 SpatialRPCService::GetAckFromView(Worker_EntityId EntityId, ERPCType Type) +{ + switch (Type) + { + case ERPCType::ClientReliable: + return View->GetComponentData(EntityId)->ReliableRPCAck; + case ERPCType::ClientUnreliable: + return View->GetComponentData(EntityId)->UnreliableRPCAck; + case ERPCType::ServerReliable: + return View->GetComponentData(EntityId)->ReliableRPCAck; + case ERPCType::ServerUnreliable: + return View->GetComponentData(EntityId)->UnreliableRPCAck; + } + + checkNoEntry(); + return 0; +} + +const RPCRingBuffer& SpatialRPCService::GetBufferFromView(Worker_EntityId EntityId, ERPCType Type) +{ + switch (Type) + { + // Server sends Client RPCs, so ClientReliable & ClientUnreliable buffers live on ServerEndpoint. + case ERPCType::ClientReliable: + return View->GetComponentData(EntityId)->ReliableRPCBuffer; + case ERPCType::ClientUnreliable: + return View->GetComponentData(EntityId)->UnreliableRPCBuffer; + + // Client sends Server RPCs, so ServerReliable & ServerUnreliable buffers live on ClientEndpoint. + case ERPCType::ServerReliable: + return View->GetComponentData(EntityId)->ReliableRPCBuffer; + case ERPCType::ServerUnreliable: + return View->GetComponentData(EntityId)->UnreliableRPCBuffer; + + case ERPCType::NetMulticast: + return View->GetComponentData(EntityId)->MulticastRPCBuffer; + } + + checkNoEntry(); + static const RPCRingBuffer DummyBuffer(ERPCType::Invalid); + return DummyBuffer; +} + +Schema_ComponentUpdate* SpatialRPCService::GetOrCreateComponentUpdate(EntityComponentId EntityComponentIdPair) +{ + Schema_ComponentUpdate** ComponentUpdatePtr = PendingComponentUpdatesToSend.Find(EntityComponentIdPair); + if (ComponentUpdatePtr == nullptr) + { + ComponentUpdatePtr = &PendingComponentUpdatesToSend.Add(EntityComponentIdPair, Schema_CreateComponentUpdate()); + } + return *ComponentUpdatePtr; +} + +Schema_ComponentData* SpatialRPCService::GetOrCreateComponentData(EntityComponentId EntityComponentIdPair) +{ + Schema_ComponentData** ComponentDataPtr = PendingRPCsOnEntityCreation.Find(EntityComponentIdPair); + if (ComponentDataPtr == nullptr) + { + ComponentDataPtr = &PendingRPCsOnEntityCreation.Add(EntityComponentIdPair, Schema_CreateComponentData()); + } + return *ComponentDataPtr; +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp index 238ef4d15f..0291887392 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp @@ -11,19 +11,17 @@ #include "EngineClasses/SpatialActorChannel.h" #include "EngineClasses/SpatialFastArrayNetSerialize.h" -#include "EngineClasses/SpatialGameInstance.h" #include "EngineClasses/SpatialNetConnection.h" #include "EngineClasses/SpatialPackageMapClient.h" #include "EngineClasses/SpatialVirtualWorkerTranslator.h" +#include "EngineClasses/SpatialLoadBalanceEnforcer.h" #include "Interop/Connection/SpatialWorkerConnection.h" #include "Interop/GlobalStateManager.h" #include "Interop/SpatialPlayerSpawner.h" #include "Interop/SpatialSender.h" -#include "Schema/AlwaysRelevant.h" -#include "Schema/ClientRPCEndpoint.h" +#include "Schema/AuthorityIntent.h" #include "Schema/DynamicComponent.h" #include "Schema/RPCPayload.h" -#include "Schema/ServerRPCEndpoint.h" #include "Schema/SpawnData.h" #include "Schema/Tombstone.h" #include "Schema/UnrealMetadata.h" @@ -31,15 +29,34 @@ #include "Utils/ComponentReader.h" #include "Utils/ErrorCodeRemapping.h" #include "Utils/RepLayoutUtils.h" +#include "Utils/SpatialDebugger.h" #include "Utils/SpatialMetrics.h" DEFINE_LOG_CATEGORY(LogSpatialReceiver); DECLARE_CYCLE_STAT(TEXT("PendingOpsOnChannel"), STAT_SpatialPendingOpsOnChannel, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Receiver LeaveCritSection"), STAT_ReceiverLeaveCritSection, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Receiver RemoveEntity"), STAT_ReceiverRemoveEntity, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Receiver AddComponent"), STAT_ReceiverAddComponent, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Receiver ComponentUpdate"), STAT_ReceiverComponentUpdate, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Receiver ApplyData"), STAT_ReceiverApplyData, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Receiver ApplyHandover"), STAT_ReceiverApplyHandover, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Receiver HandleRPC"), STAT_ReceiverHandleRPC, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Receiver HandleRPCLegacy"), STAT_ReceiverHandleRPCLegacy, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Receiver CommandRequest"), STAT_ReceiverCommandRequest, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Receiver CommandResponse"), STAT_ReceiverCommandResponse, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Receiver AuthorityChange"), STAT_ReceiverAuthChange, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Receiver ReserveEntityIds"), STAT_ReceiverReserveEntityIds, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Receiver CreateEntityResponse"), STAT_ReceiverCreateEntityResponse, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Receiver EntityQueryResponse"), STAT_ReceiverEntityQueryResponse, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Receiver FlushRemoveComponents"), STAT_ReceiverFlushRemoveComponents, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Receiver ReceiveActor"), STAT_ReceiverReceiveActor, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Receiver RemoveActor"), STAT_ReceiverRemoveActor, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Receiver ApplyRPC"), STAT_ReceiverApplyRPC, STATGROUP_SpatialNet); using namespace SpatialGDK; -void USpatialReceiver::Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimerManager) +void USpatialReceiver::Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimerManager, SpatialGDK::SpatialRPCService* InRPCService) { NetDriver = InNetDriver; StaticComponentView = InNetDriver->StaticComponentView; @@ -47,7 +64,9 @@ void USpatialReceiver::Init(USpatialNetDriver* InNetDriver, FTimerManager* InTim PackageMap = InNetDriver->PackageMap; ClassInfoManager = InNetDriver->ClassInfoManager; GlobalStateManager = InNetDriver->GlobalStateManager; + LoadBalanceEnforcer = InNetDriver->LoadBalanceEnforcer.Get(); TimerManager = InTimerManager; + RPCService = InRPCService; IncomingRPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(this, &USpatialReceiver::ApplyRPC)); PeriodicallyProcessIncomingRPCs(); @@ -74,12 +93,18 @@ void USpatialReceiver::EnterCriticalSection() void USpatialReceiver::LeaveCriticalSection() { + SCOPE_CYCLE_COUNTER(STAT_ReceiverLeaveCritSection); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Leaving critical section.")); check(bInCriticalSection); - for (Worker_EntityId& PendingAddEntity : PendingAddEntities) + for (Worker_EntityId& PendingAddEntity : PendingAddActors) { ReceiveActor(PendingAddEntity); + if (!IsEntityWaitingForAsyncLoad(PendingAddEntity)) + { + OnEntityAddedDelegate.Broadcast(PendingAddEntity); + } } for (Worker_AuthorityChangeOp& PendingAuthorityChange : PendingAuthorityChanges) @@ -89,50 +114,90 @@ void USpatialReceiver::LeaveCriticalSection() // Mark that we've left the critical section. bInCriticalSection = false; - PendingAddEntities.Empty(); + PendingAddActors.Empty(); PendingAddComponents.Empty(); PendingAuthorityChanges.Empty(); - - ProcessQueuedResolvedObjects(); } void USpatialReceiver::OnAddEntity(const Worker_AddEntityOp& Op) { UE_LOG(LogSpatialReceiver, Verbose, TEXT("AddEntity: %lld"), Op.entity_id); - - check(bInCriticalSection); - - PendingAddEntities.Emplace(Op.entity_id); } void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op) { + SCOPE_CYCLE_COUNTER(STAT_ReceiverAddComponent); UE_LOG(LogSpatialReceiver, Verbose, TEXT("AddComponent component ID: %u entity ID: %lld"), Op.data.component_id, Op.entity_id); + if (IsEntityWaitingForAsyncLoad(Op.entity_id)) + { + QueueAddComponentOpForAsyncLoad(Op); + return; + } + + // Remove all RemoveComponentOps that have already been received and have the same entityId and componentId as the AddComponentOp. + // TODO: This can probably be removed when spatial view is added. + QueuedRemoveComponentOps.RemoveAll([&Op](const Worker_RemoveComponentOp& RemoveComponentOp) { + return RemoveComponentOp.entity_id == Op.entity_id && + RemoveComponentOp.component_id == Op.data.component_id; + }); + switch (Op.data.component_id) { - case SpatialConstants::ENTITY_ACL_COMPONENT_ID: case SpatialConstants::METADATA_COMPONENT_ID: case SpatialConstants::POSITION_COMPONENT_ID: case SpatialConstants::PERSISTENCE_COMPONENT_ID: case SpatialConstants::SPAWN_DATA_COMPONENT_ID: case SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID: case SpatialConstants::SINGLETON_COMPONENT_ID: - case SpatialConstants::UNREAL_METADATA_COMPONENT_ID: case SpatialConstants::INTEREST_COMPONENT_ID: case SpatialConstants::NOT_STREAMED_COMPONENT_ID: case SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID: case SpatialConstants::HEARTBEAT_COMPONENT_ID: - case SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID: + case SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID_LEGACY: case SpatialConstants::RPCS_ON_ENTITY_CREATION_ID: case SpatialConstants::DEBUG_METRICS_COMPONENT_ID: case SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID: - case SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID: - case SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID: - case SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID: + case SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY: + case SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY: + case SpatialConstants::SERVER_TO_SERVER_COMMAND_ENDPOINT_COMPONENT_ID: + case SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID: + case SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID: + case SpatialConstants::SPATIAL_DEBUGGING_COMPONENT_ID: // Ignore static spatial components as they are managed by the SpatialStaticComponentView. return; + case SpatialConstants::UNREAL_METADATA_COMPONENT_ID: + // The unreal metadata component is used to indicate when an actor needs to be created from the entity. + // This means we need to be inside a critical section, otherwise we may not have all the requisite information at the point of creating the actor. + check(bInCriticalSection); + PendingAddActors.Emplace(Op.entity_id); + return; + case SpatialConstants::ENTITY_ACL_COMPONENT_ID: + case SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID: + case SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID: + case SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID: + if (LoadBalanceEnforcer != nullptr) + { + LoadBalanceEnforcer->OnLoadBalancingComponentAdded(Op); + } + return; + case SpatialConstants::WORKER_COMPONENT_ID: + if(NetDriver->IsServer()) + { + // Register system identity for a worker connection, to know when a player has disconnected. + Worker* WorkerData = StaticComponentView->GetComponentData(Op.entity_id); + WorkerConnectionEntities.Add(Op.entity_id, WorkerData->WorkerId); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Worker %s 's system identity was checked out."), *WorkerData->WorkerId); + } + return; + case SpatialConstants::MULTICAST_RPCS_COMPONENT_ID: + // The RPC service needs to be informed when a multi-cast RPC component is added. + if (GetDefault()->UseRPCRingBuffer() && RPCService != nullptr) + { + RPCService->OnCheckoutMulticastRPCComponentOnEntity(Op.entity_id); + } + return; case SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID: GlobalStateManager->ApplySingletonManagerData(Op.data); GlobalStateManager->LinkAllExistingSingletonActors(); @@ -158,7 +223,7 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op) } return; case SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID: - if (NetDriver->VirtualWorkerTranslator != nullptr) + if (NetDriver->VirtualWorkerTranslator.IsValid()) { Schema_Object* ComponentObject = Schema_GetComponentDataFields(Op.data.schema_type); NetDriver->VirtualWorkerTranslator->ApplyVirtualWorkerManagerData(ComponentObject); @@ -171,7 +236,7 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op) return; } - if (ClassInfoManager->IsSublevelComponent(Op.data.component_id)) + if (ClassInfoManager->IsGeneratedQBIMarkerComponent(Op.data.component_id)) { return; } @@ -188,11 +253,64 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op) void USpatialReceiver::OnRemoveEntity(const Worker_RemoveEntityOp& Op) { - RemoveActor(Op.entity_id); + SCOPE_CYCLE_COUNTER(STAT_ReceiverRemoveEntity); + + if (LoadBalanceEnforcer != nullptr) + { + LoadBalanceEnforcer->OnEntityRemoved(Op); + } + + OnEntityRemovedDelegate.Broadcast(Op.entity_id); + + if (NetDriver->IsServer()) + { + // Check to see if we are removing a system entity for a worker connection. If so clean up the ClientConnection to delete any and all actors for this connection's controller. + if (FString* WorkerName = WorkerConnectionEntities.Find(Op.entity_id)) + { + TWeakObjectPtr ClientConnectionPtr = NetDriver->FindClientConnectionFromWorkerId(*WorkerName); + if (USpatialNetConnection* ClientConnection = ClientConnectionPtr.Get()) + { + if (APlayerController* Controller = ClientConnection->GetPlayerController(/*InWorld*/ nullptr)) + { + Worker_EntityId PCEntity = PackageMap->GetEntityIdFromObject(Controller); + if (AuthorityPlayerControllerConnectionMap.Find(PCEntity)) + { + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Worker %s disconnected after its system identity was removed."), *(*WorkerName)); + CloseClientConnection(ClientConnection, PCEntity); + } + } + } + WorkerConnectionEntities.Remove(Op.entity_id); + } + } } void USpatialReceiver::OnRemoveComponent(const Worker_RemoveComponentOp& Op) { + if (Op.component_id == SpatialConstants::UNREAL_METADATA_COMPONENT_ID) + { + if (IsEntityWaitingForAsyncLoad(Op.entity_id)) + { + // Pretend we never saw this actor. + EntitiesWaitingForAsyncLoad.Remove(Op.entity_id); + } + else + { + RemoveActor(Op.entity_id); + } + } + + if (GetDefault()->UseRPCRingBuffer() && RPCService != nullptr && Op.component_id == SpatialConstants::MULTICAST_RPCS_COMPONENT_ID) + { + // If this is a multi-cast RPC component, the RPC service should be informed to handle it. + RPCService->OnRemoveMulticastRPCComponentForEntity(Op.entity_id); + } + + if (LoadBalanceEnforcer != nullptr && LoadBalanceEnforcer->HandlesComponent(Op.component_id)) + { + LoadBalanceEnforcer->OnLoadBalancingComponentRemoved(Op); + } + // We are queuing here because if an Actor is removed from your view, remove component ops will be // generated and sent first, and then the RemoveEntityOp will be sent. In this case, we only want // to delete the Actor and not delete the subobjects that the RemoveComponent relate to. @@ -204,6 +322,8 @@ void USpatialReceiver::OnRemoveComponent(const Worker_RemoveComponentOp& Op) void USpatialReceiver::FlushRemoveComponentOps() { + SCOPE_CYCLE_COUNTER(STAT_ReceiverFlushRemoveComponents); + for (const auto& Op : QueuedRemoveComponentOps) { ProcessRemoveComponent(Op); @@ -212,8 +332,9 @@ void USpatialReceiver::FlushRemoveComponentOps() QueuedRemoveComponentOps.Empty(); } -void USpatialReceiver::RemoveComponentOpsForEntity(Worker_EntityId EntityId) +void USpatialReceiver::DropQueuedRemoveComponentOpsForEntity(Worker_EntityId EntityId) { + // Drop any remove components ops for a removed entity because we cannot process them once we no longer see the entity. for (auto& RemoveComponentOp : QueuedRemoveComponentOps) { if (RemoveComponentOp.entity_id == EntityId) @@ -224,13 +345,14 @@ void USpatialReceiver::RemoveComponentOpsForEntity(Worker_EntityId EntityId) } } -USpatialActorChannel* USpatialReceiver::RecreateDormantSpatialChannel(AActor* Actor, Worker_EntityId EntityID) +USpatialActorChannel* USpatialReceiver::GetOrRecreateChannelForDomantActor(AActor* Actor, Worker_EntityId EntityID) { // Receive would normally create channel in ReceiveActor - this function is used to recreate the channel after waking up a dormant actor USpatialActorChannel* Channel = NetDriver->GetOrCreateSpatialActorChannel(Actor); check(!Channel->bCreatingNewEntity); check(Channel->GetEntityId() == EntityID); + NetDriver->RemovePendingDormantChannel(Channel); NetDriver->UnregisterDormantEntityId(EntityID); return Channel; @@ -238,6 +360,12 @@ USpatialActorChannel* USpatialReceiver::RecreateDormantSpatialChannel(AActor* Ac void USpatialReceiver::ProcessRemoveComponent(const Worker_RemoveComponentOp& Op) { + if (IsEntityWaitingForAsyncLoad(Op.entity_id)) + { + QueueRemoveComponentOpForAsyncLoad(Op); + return; + } + if (!StaticComponentView->HasComponent(Op.entity_id, Op.component_id)) { return; @@ -245,15 +373,16 @@ void USpatialReceiver::ProcessRemoveComponent(const Worker_RemoveComponentOp& Op if (AActor* Actor = Cast(PackageMap->GetObjectFromEntityId(Op.entity_id).Get())) { + FUnrealObjectRef ObjectRef(Op.entity_id, Op.component_id); if (Op.component_id == SpatialConstants::DORMANT_COMPONENT_ID) { - RecreateDormantSpatialChannel(Actor, Op.entity_id); + GetOrRecreateChannelForDomantActor(Actor, Op.entity_id); } - else if (UObject* Object = PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(Op.entity_id, Op.component_id)).Get()) + else if (UObject* Object = PackageMap->GetObjectFromUnrealObjectRef(ObjectRef).Get()) { if (USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(Op.entity_id)) { - Channel->CreateSubObjects.Remove(Object); + Channel->OnSubobjectDeleted(ObjectRef, Object); Actor->OnSubobjectDestroyFromReplication(Object); @@ -276,8 +405,26 @@ void USpatialReceiver::UpdateShadowData(Worker_EntityId EntityId) void USpatialReceiver::OnAuthorityChange(const Worker_AuthorityChangeOp& Op) { + // Update this worker's view of authority. We do this here as this is when the worker is first notified of the authority change. + // This way systems that depend on having non-stale state can function correctly. + StaticComponentView->OnAuthorityChange(Op); + + if (Op.component_id == SpatialConstants::ENTITY_ACL_COMPONENT_ID && LoadBalanceEnforcer != nullptr) + { + LoadBalanceEnforcer->OnAclAuthorityChanged(Op); + } + + SCOPE_CYCLE_COUNTER(STAT_ReceiverAuthChange); + if (IsEntityWaitingForAsyncLoad(Op.entity_id)) + { + QueueAuthorityOpForAsyncLoad(Op); + return; + } + if (bInCriticalSection) { + // The actor receiving flow requires authority to be handled after all components have been received, so buffer those if we + // are in a critical section to be handled later. PendingAuthorityChanges.Add(Op); return; } @@ -287,6 +434,8 @@ void USpatialReceiver::OnAuthorityChange(const Worker_AuthorityChangeOp& Op) void USpatialReceiver::HandlePlayerLifecycleAuthority(const Worker_AuthorityChangeOp& Op, APlayerController* PlayerController) { + UE_LOG(LogSpatialReceiver, Verbose, TEXT("HandlePlayerLifecycleAuthority for PlayerController %d."), *AActor::GetDebugName(PlayerController)); + // Server initializes heartbeat logic based on its authority over the position component, // client does the same for heartbeat component if ((NetDriver->IsServer() && Op.component_id == SpatialConstants::POSITION_COMPONENT_ID) || @@ -319,21 +468,44 @@ void USpatialReceiver::HandlePlayerLifecycleAuthority(const Worker_AuthorityChan void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) { - StaticComponentView->OnAuthorityChange(Op); - if (GlobalStateManager->HandlesComponent(Op.component_id)) { GlobalStateManager->AuthorityChanged(Op); return; } + if (NetDriver->VirtualWorkerTranslator != nullptr + && Op.component_id == SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID + && Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) + { + NetDriver->InitializeVirtualWorkerTranslationManager(); + NetDriver->VirtualWorkerTranslationManager->AuthorityChanged(Op); + } + + if (NetDriver->SpatialDebugger != nullptr) + { + NetDriver->SpatialDebugger->ActorAuthorityChanged(Op); + } + AActor* Actor = Cast(NetDriver->PackageMap->GetObjectFromEntityId(Op.entity_id)); if (Actor == nullptr) { return; } - if (Op.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID + if (USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(Op.entity_id)) + { + if (Op.component_id == SpatialConstants::POSITION_COMPONENT_ID) + { + Channel->SetServerAuthority(Op.authority == WORKER_AUTHORITY_AUTHORITATIVE); + } + else if (Op.component_id == SpatialConstants::GetClientAuthorityComponent(GetDefault()->UseRPCRingBuffer())) + { + Channel->SetClientAuthority(Op.authority == WORKER_AUTHORITY_AUTHORITATIVE); + } + } + + if (Op.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY && Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) { check(!NetDriver->IsServer()); @@ -341,7 +513,7 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) { if (QueuedRPCs->HasRPCPayloadData()) { - ProcessQueuedActorRPCsOnEntityCreation(Actor, *QueuedRPCs); + ProcessQueuedActorRPCsOnEntityCreation(Op.entity_id, *QueuedRPCs); } Sender->SendRequestToClearRPCsOnEntityCreation(Op.entity_id); @@ -396,8 +568,7 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) // The following check will return false on non-authoritative servers if the Pawn hasn't been received yet. if (APawn* PawnFromPlayerState = PlayerState->GetPawn()) { - check(PlayerState->bIsABot || PawnFromPlayerState->IsPlayerControlled()); - if (PawnFromPlayerState->IsPlayerControlled()) + if (PawnFromPlayerState->IsPlayerControlled() && PawnFromPlayerState->HasAuthority()) { PawnFromPlayerState->RemoteRole = ROLE_AutonomousProxy; } @@ -428,10 +599,16 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) ActorChannel->bCreatedEntity = false; } - Actor->Role = ROLE_SimulatedProxy; - Actor->RemoteRole = ROLE_Authority; + // With load-balancing enabled, we already set ROLE_SimulatedProxy and trigger OnAuthorityLost when we + // set AuthorityIntent to another worker. This conditional exists to dodge calling OnAuthorityLost + // twice. + if (Actor->Role != ROLE_SimulatedProxy) + { + Actor->Role = ROLE_SimulatedProxy; + Actor->RemoteRole = ROLE_Authority; - Actor->OnAuthorityLost(); + Actor->OnAuthorityLost(); + } } } @@ -447,51 +624,67 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) { if (UObject* Object = PendingSubobjectAttachment.Subobject.Get()) { - Sender->SendAddComponent(PendingSubobjectAttachment.Channel, Object, *PendingSubobjectAttachment.Info); + // TODO: UNR-664 - We should track the bytes sent here and factor them into channel saturation. + uint32 BytesWritten = 0; + Sender->SendAddComponentForSubobject(PendingSubobjectAttachment.Channel, Object, *PendingSubobjectAttachment.Info, BytesWritten); } } PendingEntitySubobjectDelegations.Remove(EntityComponentPair); } } - else + else if (Op.component_id == SpatialConstants::GetClientAuthorityComponent(GetDefault()->UseRPCRingBuffer())) { - if (Op.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID) + if (USpatialActorChannel* ActorChannel = NetDriver->GetActorChannelByEntityId(Op.entity_id)) { - if (USpatialActorChannel* ActorChannel = NetDriver->GetActorChannelByEntityId(Op.entity_id)) + // Soft handover isn't supported currently. + if (Op.authority != WORKER_AUTHORITY_AUTHORITY_LOSS_IMMINENT) { ActorChannel->ClientProcessOwnershipChange(Op.authority == WORKER_AUTHORITY_AUTHORITATIVE); } + } - // If we are a Pawn or PlayerController, our local role should be ROLE_AutonomousProxy. Otherwise ROLE_SimulatedProxy - if ((Actor->IsA() || Actor->IsA()) && Op.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID) - { - Actor->Role = (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) ? ROLE_AutonomousProxy : ROLE_SimulatedProxy; - } + // If we are a Pawn or PlayerController, our local role should be ROLE_AutonomousProxy. Otherwise ROLE_SimulatedProxy + if (Actor->IsA() || Actor->IsA()) + { + Actor->Role = (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) ? ROLE_AutonomousProxy : ROLE_SimulatedProxy; } } - if (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) + if (Op.component_id == SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID || + Op.component_id == SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID || + Op.component_id == SpatialConstants::MULTICAST_RPCS_COMPONENT_ID) { - if (Op.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID) + if (GetDefault()->UseRPCRingBuffer() && RPCService != nullptr) { - Sender->SendClientEndpointReadyUpdate(Op.entity_id); + if (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) + { + RPCService->OnEndpointAuthorityGained(Op.entity_id, Op.component_id); + if (Op.component_id != SpatialConstants::MULTICAST_RPCS_COMPONENT_ID) + { + RPCService->ExtractRPCsForEntity(Op.entity_id, Op.component_id); + } + } + else if (Op.authority == WORKER_AUTHORITY_NOT_AUTHORITATIVE) + { + RPCService->OnEndpointAuthorityLost(Op.entity_id, Op.component_id); + } } - if (Op.component_id == SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID) + else { - Sender->SendServerEndpointReadyUpdate(Op.entity_id); + UE_LOG(LogSpatialReceiver, Error, TEXT("USpatialReceiver::HandleActorAuthority: Gained authority over ring buffer endpoint but ring buffers not enabled! Entity: %lld, Component: %d"), Op.entity_id, Op.component_id); } } - if (GetDefault()->bCheckRPCOrder && Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) + if (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) { - ESchemaComponentType ComponentType = ClassInfoManager->GetCategoryByComponentId(Op.component_id); - if (Op.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID || - Op.component_id == SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID || - Op.component_id == SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID) + if (Op.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY) + { + Sender->SendClientEndpointReadyUpdate(Op.entity_id); + } + if (Op.component_id == SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY) { - // This will be called multiple times on each RPC component. - NetDriver->OnRPCAuthorityGained(Actor, ComponentType); + Sender->SendServerEndpointReadyUpdate(Op.entity_id); } } } @@ -523,25 +716,37 @@ bool USpatialReceiver::IsReceivedEntityTornOff(Worker_EntityId EntityId) void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) { + SCOPE_CYCLE_COUNTER(STAT_ReceiverReceiveActor); + checkf(NetDriver, TEXT("We should have a NetDriver whilst processing ops.")); checkf(NetDriver->GetWorld(), TEXT("We should have a World whilst processing ops.")); SpawnData* SpawnDataComp = StaticComponentView->GetComponentData(EntityId); UnrealMetadata* UnrealMetadataComp = StaticComponentView->GetComponentData(EntityId); + NetOwningClientWorker* NetOwningClientWorkerComp = StaticComponentView->GetComponentData(EntityId); + + // This function should only ever be called if we have received an unreal metadata component. + check(UnrealMetadataComp != nullptr); + + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); - if (UnrealMetadataComp == nullptr) + // Check if actor's class is loaded. If not, start async loading it and extract all data and + // authority ops into a separate entry that will get processed once the loading is finished. + const FString& ClassPath = UnrealMetadataComp->ClassPath; + if (SpatialGDKSettings->bAsyncLoadNewClassesOnEntityCheckout && NeedToLoadClass(ClassPath)) { - // Not an Unreal entity + StartAsyncLoadingClass(ClassPath, EntityId); return; } - if (AActor* EntityActor = Cast(PackageMap->GetObjectFromEntityId(EntityId))) + AActor* EntityActor = Cast(PackageMap->GetObjectFromEntityId(EntityId)); + if (EntityActor != nullptr) { - UE_LOG(LogSpatialReceiver, Log, TEXT("Entity for actor %s has been checked out on the worker which spawned it or is a singleton linked on this worker. " - "Entity id: %lld"), *EntityActor->GetName(), EntityId); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("%s: Entity for actor %s has been checked out on the worker which spawned it or is a singleton linked on this worker. " + "Entity ID: %lld"), *NetDriver->Connection->GetWorkerId(), *EntityActor->GetName(), EntityId); // Assume SimulatedProxy until we've been delegated Authority - bool bAuthority = StaticComponentView->GetAuthority(EntityId, Position::ComponentId) == WORKER_AUTHORITY_AUTHORITATIVE; + bool bAuthority = StaticComponentView->HasAuthority(EntityId, Position::ComponentId); EntityActor->Role = bAuthority ? ROLE_Authority : ROLE_SimulatedProxy; EntityActor->RemoteRole = bAuthority ? ROLE_SimulatedProxy : ROLE_Authority; if (bAuthority) @@ -553,151 +758,183 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) } // If we're a singleton, apply the data, regardless of authority - JIRA: 736 + return; } - else + + UE_LOG(LogSpatialReceiver, Verbose, TEXT("%s: Entity has been checked out on a worker which didn't spawn it. " + "Entity ID: %lld"), *NetDriver->Connection->GetWorkerId(), EntityId); + + UClass* Class = UnrealMetadataComp->GetNativeEntityClass(); + if (Class == nullptr) { - UClass* Class = UnrealMetadataComp->GetNativeEntityClass(); - if (Class == nullptr) - { - UE_LOG(LogSpatialReceiver, Warning, TEXT("The received actor with entity id %lld couldn't be loaded. The actor (%s) will not be spawned."), - EntityId, *UnrealMetadataComp->ClassPath); - return; - } + UE_LOG(LogSpatialReceiver, Warning, TEXT("The received actor with entity ID %lld couldn't be loaded. The actor (%s) will not be spawned."), + EntityId, *UnrealMetadataComp->ClassPath); + return; + } - // Make sure ClassInfo exists - const FClassInfo& ActorClassInfo = ClassInfoManager->GetOrCreateClassInfoByClass(Class); + // Make sure ClassInfo exists + const FClassInfo& ActorClassInfo = ClassInfoManager->GetOrCreateClassInfoByClass(Class); - // If the received actor is torn off, don't bother spawning it. - // (This is only needed due to the delay between tearoff and deleting the entity. See https://improbableio.atlassian.net/browse/UNR-841) - if (IsReceivedEntityTornOff(EntityId)) - { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("The received actor with entity id %lld was already torn off. The actor will not be spawned."), EntityId); - return; - } + // If the received actor is torn off, don't bother spawning it. + // (This is only needed due to the delay between tearoff and deleting the entity. See https://improbableio.atlassian.net/browse/UNR-841) + if (IsReceivedEntityTornOff(EntityId)) + { + UE_LOG(LogSpatialReceiver, Verbose, TEXT("The received actor with entity ID %lld was already torn off. The actor will not be spawned."), EntityId); + return; + } - EntityActor = TryGetOrCreateActor(UnrealMetadataComp, SpawnDataComp); + EntityActor = TryGetOrCreateActor(UnrealMetadataComp, SpawnDataComp, NetOwningClientWorkerComp); - if (EntityActor == nullptr) - { - // This could be nullptr if: - // a stably named actor could not be found - // the Actor is a singleton that has arrived over the wire before it has been created on this worker - // the class couldn't be loaded - return; - } + if (EntityActor == nullptr) + { + // This could be nullptr if: + // a stably named actor could not be found + // the Actor is a singleton that has arrived over the wire before it has been created on this worker + // the class couldn't be loaded + return; + } - // RemoveActor immediately if we've received the tombstone component. - if (NetDriver->StaticComponentView->HasComponent(EntityId, SpatialConstants::TOMBSTONE_COMPONENT_ID)) - { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("The received actor with entity id %lld was tombstoned. The actor will not be spawned."), EntityId); - // We must first Resolve the EntityId to the Actor in order for RemoveActor to succeed. - PackageMap->ResolveEntityActor(EntityActor, EntityId); - RemoveActor(EntityId); - return; - } + // RemoveActor immediately if we've received the tombstone component. + if (NetDriver->StaticComponentView->HasComponent(EntityId, SpatialConstants::TOMBSTONE_COMPONENT_ID)) + { + UE_LOG(LogSpatialReceiver, Verbose, TEXT("The received actor with entity ID %lld was tombstoned. The actor will not be spawned."), EntityId); + // We must first Resolve the EntityId to the Actor in order for RemoveActor to succeed. + PackageMap->ResolveEntityActor(EntityActor, EntityId); + RemoveActor(EntityId); + return; + } - UNetConnection* Connection = NetDriver->GetSpatialOSNetConnection(); + UNetConnection* Connection = NetDriver->GetSpatialOSNetConnection(); - if (NetDriver->IsServer()) + if (NetDriver->IsServer()) + { + if (APlayerController* PlayerController = Cast(EntityActor)) { - if (APlayerController* PlayerController = Cast(EntityActor)) - { - // If entity is a PlayerController, create channel on the PlayerController's connection. - Connection = PlayerController->NetConnection; - } + // If entity is a PlayerController, create channel on the PlayerController's connection. + Connection = PlayerController->NetConnection; } + } - if (Connection == nullptr) - { - UE_LOG(LogSpatialReceiver, Error, TEXT("Unable to find SpatialOSNetConnection! Has this worker been disconnected from SpatialOS due to a timeout?")); - return; - } + if (Connection == nullptr) + { + UE_LOG(LogSpatialReceiver, Error, TEXT("Unable to find SpatialOSNetConnection! Has this worker been disconnected from SpatialOS due to a timeout?")); + return; + } - // Set up actor channel. - USpatialActorChannel* Channel = Cast(Connection->CreateChannelByName(NAME_Actor, NetDriver->IsServer() ? EChannelCreateFlags::OpenedLocally : EChannelCreateFlags::None)); + if (!PackageMap->ResolveEntityActor(EntityActor, EntityId)) + { + UE_LOG(LogSpatialReceiver, Warning, TEXT("Failed to resolve entity actor when receiving entity %lld. The actor (%s) will not be spawned."), EntityId, *EntityActor->GetName()); + EntityActor->Destroy(true); + return; + } - if (!Channel) - { - UE_LOG(LogSpatialReceiver, Warning, TEXT("Failed to create an actor channel when receiving entity %lld. The actor will not be spawned."), EntityId); - EntityActor->Destroy(true); - return; - } + // Set up actor channel. + USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(EntityId); + if (Channel == nullptr) + { + Channel = Cast(Connection->CreateChannelByName(NAME_Actor, NetDriver->IsServer() ? EChannelCreateFlags::OpenedLocally : EChannelCreateFlags::None)); + } - PackageMap->ResolveEntityActor(EntityActor, EntityId); + if (Channel == nullptr) + { + UE_LOG(LogSpatialReceiver, Warning, TEXT("Failed to create an actor channel when receiving entity %lld. The actor (%s) will not be spawned."), EntityId, *EntityActor->GetName()); + EntityActor->Destroy(true); + return; + } + if (Channel->Actor == nullptr) + { #if ENGINE_MINOR_VERSION <= 22 Channel->SetChannelActor(EntityActor); #else Channel->SetChannelActor(EntityActor, ESetChannelActorFlags::None); #endif + } - // Apply initial replicated properties. - // This was moved to after FinishingSpawning because components existing only in blueprints aren't added until spawning is complete - // Potentially we could split out the initial actor state and the initial component state - for (PendingAddComponentWrapper& PendingAddComponent : PendingAddComponents) - { - if (ClassInfoManager->IsSublevelComponent(PendingAddComponent.ComponentId)) - { - continue; - } + TArray ObjectsToResolvePendingOpsFor; - if (PendingAddComponent.EntityId == EntityId) - { - ApplyComponentDataOnActorCreation(EntityId, *PendingAddComponent.Data->ComponentData, Channel, ActorClassInfo); - } + // Apply initial replicated properties. + // This was moved to after FinishingSpawning because components existing only in blueprints aren't added until spawning is complete + // Potentially we could split out the initial actor state and the initial component state + for (PendingAddComponentWrapper& PendingAddComponent : PendingAddComponents) + { + if (ClassInfoManager->IsGeneratedQBIMarkerComponent(PendingAddComponent.ComponentId)) + { + continue; } - if (!NetDriver->IsServer()) + if (PendingAddComponent.EntityId == EntityId) { - // Update interest on the entity's components after receiving initial component data (so Role and RemoteRole are properly set). - Sender->SendComponentInterestForActor(Channel, EntityId, Channel->IsOwnedByWorker()); + ApplyComponentDataOnActorCreation(EntityId, *PendingAddComponent.Data->ComponentData, *Channel, ActorClassInfo, ObjectsToResolvePendingOpsFor); + } + } - // This is a bit of a hack unfortunately, among the core classes only PlayerController implements this function and it requires - // a player index. For now we don't support split screen, so the number is always 0. - if (EntityActor->IsA(APlayerController::StaticClass())) - { - uint8 PlayerIndex = 0; - // FInBunch takes size in bits not bytes - FInBunch Bunch(NetDriver->ServerConnection, &PlayerIndex, sizeof(PlayerIndex) * 8); - EntityActor->OnActorChannelOpen(Bunch, NetDriver->ServerConnection); - } - else - { - FInBunch Bunch(NetDriver->ServerConnection); - EntityActor->OnActorChannelOpen(Bunch, NetDriver->ServerConnection); - } + // Resolve things like RepNotify or RPCs after applying component data. + for (const ObjectPtrRefPair& ObjectToResolve : ObjectsToResolvePendingOpsFor) + { + ResolvePendingOperations(ObjectToResolve.Key, ObjectToResolve.Value); + } + if (!NetDriver->IsServer()) + { + // Update interest on the entity's components after receiving initial component data (so Role and RemoteRole are properly set). + // Don't send dynamic interest for this actor if it is otherwise handled by result types. + if (!SpatialGDKSettings->bEnableResultTypes) + { + Sender->SendComponentInterestForActor(Channel, EntityId, Channel->IsAuthoritativeClient()); } - // Taken from PostNetInit - if (NetDriver->GetWorld()->HasBegunPlay() && !EntityActor->HasActorBegunPlay()) + // This is a bit of a hack unfortunately, among the core classes only PlayerController implements this function and it requires + // a player index. For now we don't support split screen, so the number is always 0. + if (EntityActor->IsA(APlayerController::StaticClass())) { - // Whenever we receive an actor over the wire, the expectation is that it is not in an authoritative - // state. This is because it should already have had authoritative BeginPlay() called. If we have - // authority here, we are calling BeginPlay() with authority on this actor a 2nd time, which is always incorrect. - check(!EntityActor->HasAuthority()); - EntityActor->DispatchBeginPlay(); + uint8 PlayerIndex = 0; + // FInBunch takes size in bits not bytes + FInBunch Bunch(NetDriver->ServerConnection, &PlayerIndex, sizeof(PlayerIndex) * 8); + EntityActor->OnActorChannelOpen(Bunch, NetDriver->ServerConnection); } - - if (EntityActor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)) + else { - GlobalStateManager->RegisterSingletonChannel(EntityActor, Channel); + FInBunch Bunch(NetDriver->ServerConnection); + EntityActor->OnActorChannelOpen(Bunch, NetDriver->ServerConnection); } + } - EntityActor->UpdateOverlaps(); - - if (StaticComponentView->HasComponent(EntityId, SpatialConstants::DORMANT_COMPONENT_ID)) + // Taken from PostNetInit + if (NetDriver->GetWorld()->HasBegunPlay() && !EntityActor->HasActorBegunPlay()) + { + // The Actor role can be authority here if a PendingAddComponent processed above set the role. + // Calling BeginPlay() with authority a 2nd time globally is always incorrect, so we set role + // to SimulatedProxy and let the processing of authority ops (later in the LeaveCriticalSection + // flow) take care of setting roles correctly. + if (EntityActor->HasAuthority()) { - NetDriver->AddPendingDormantChannel(Channel); + EntityActor->Role = ROLE_SimulatedProxy; + EntityActor->RemoteRole = ROLE_Authority; } + EntityActor->DispatchBeginPlay(); + } + + if (EntityActor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)) + { + GlobalStateManager->RegisterSingletonChannel(EntityActor, Channel); + } + + EntityActor->UpdateOverlaps(); + + if (StaticComponentView->HasComponent(EntityId, SpatialConstants::DORMANT_COMPONENT_ID)) + { + NetDriver->AddPendingDormantChannel(Channel); } } void USpatialReceiver::RemoveActor(Worker_EntityId EntityId) { + SCOPE_CYCLE_COUNTER(STAT_ReceiverRemoveActor); + TWeakObjectPtr WeakActor = PackageMap->GetObjectFromEntityId(EntityId); - // Actor has been destroyed already. Clean up surrounding bookkeeping. + // Actor has not been resolved yet or has already been destroyed. Clean up surrounding bookkeeping. if (!WeakActor.IsValid()) { DestroyActor(nullptr, EntityId); @@ -736,14 +973,38 @@ void USpatialReceiver::RemoveActor(Worker_EntityId EntityId) return; } - // If the entity is to be deleted after having been torn off, ignore the request (but clean up the channel if it has not been cleaned up already). - if (Actor->GetTearOff()) + if (USpatialActorChannel* ActorChannel = NetDriver->GetActorChannelByEntityId(EntityId)) { - if (USpatialActorChannel* ActorChannel = NetDriver->GetActorChannelByEntityId(EntityId)) + if (NetDriver->GetWorld() != nullptr && !NetDriver->GetWorld()->IsPendingKillOrUnreachable()) + { + for (UObject* SubObject : ActorChannel->CreateSubObjects) + { + if (SubObject) + { + FUnrealObjectRef ObjectRef = FUnrealObjectRef::FromObjectPtr(SubObject, Cast(PackageMap)); + // Unmap this object so we can remap it if it becomes relevant again in the future + MoveMappedObjectToUnmapped(ObjectRef); + } + } + + FUnrealObjectRef ObjectRef = FUnrealObjectRef::FromObjectPtr(Actor, Cast(PackageMap)); + // Unmap this object so we can remap it if it becomes relevant again in the future + MoveMappedObjectToUnmapped(ObjectRef); + } + + for (auto& ChannelRefs : ActorChannel->ObjectReferenceMap) + { + CleanupRepStateMap(ChannelRefs.Value); + } + + ActorChannel->ObjectReferenceMap.Empty(); + + // If the entity is to be deleted after having been torn off, ignore the request (but clean up the channel if it has not been cleaned up already). + if (Actor->GetTearOff()) { ActorChannel->ConditionalCleanUp(false, EChannelCloseReason::TearOff); + return; } - return; } // Actor is a startup actor that is a part of the level. If it's not Tombstone'd, then it @@ -810,13 +1071,9 @@ void USpatialReceiver::DestroyActor(AActor* Actor, Worker_EntityId EntityId) { PackageMap->RemoveEntityActor(EntityId); } - else if (Actor == nullptr) - { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Removing actor as a result of a remove entity op, which has a missing actor channel. EntityId: %lld"), EntityId); - } else { - UE_LOG(LogSpatialReceiver, Warning, TEXT("Removing actor as a result of a remove entity op, which has a missing actor channel. Actor: %s EntityId: %lld"), *Actor->GetName(), EntityId); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Removing actor as a result of a remove entity op, which has a missing actor channel. Actor: %s EntityId: %lld"), *GetNameSafe(Actor), EntityId); } } @@ -830,7 +1087,7 @@ void USpatialReceiver::DestroyActor(AActor* Actor, Worker_EntityId EntityId) check(PackageMap->GetObjectFromEntityId(EntityId) == nullptr); } -AActor* USpatialReceiver::TryGetOrCreateActor(UnrealMetadata* UnrealMetadataComp, SpawnData* SpawnDataComp) +AActor* USpatialReceiver::TryGetOrCreateActor(UnrealMetadata* UnrealMetadataComp, SpawnData* SpawnDataComp, NetOwningClientWorker* NetOwningClientWorkerComp) { if (UnrealMetadataComp->StablyNamedRef.IsSet()) { @@ -849,11 +1106,11 @@ AActor* USpatialReceiver::TryGetOrCreateActor(UnrealMetadata* UnrealMetadataComp } } - return CreateActor(UnrealMetadataComp, SpawnDataComp); + return CreateActor(UnrealMetadataComp, SpawnDataComp, NetOwningClientWorkerComp); } // This function is only called for client and server workers who did not spawn the Actor -AActor* USpatialReceiver::CreateActor(UnrealMetadata* UnrealMetadataComp, SpawnData* SpawnDataComp) +AActor* USpatialReceiver::CreateActor(UnrealMetadata* UnrealMetadataComp, SpawnData* SpawnDataComp, NetOwningClientWorker* NetOwningClientWorkerComp) { UClass* ActorClass = UnrealMetadataComp->GetNativeEntityClass(); @@ -887,7 +1144,9 @@ AActor* USpatialReceiver::CreateActor(UnrealMetadata* UnrealMetadataComp, SpawnD if (bIsServer && bCreatingPlayerController) { - NetDriver->PostSpawnPlayerController(Cast(NewActor), UnrealMetadataComp->OwnerWorkerAttribute); + // If we're spawning a PlayerController, it should definitely have a net-owning client worker ID. + check(NetOwningClientWorkerComp->WorkerId.IsSet()); + NetDriver->PostSpawnPlayerController(Cast(NewActor), *NetOwningClientWorkerComp->WorkerId); } // Imitate the behavior in UPackageMapClient::SerializeNewActor. @@ -923,9 +1182,9 @@ FTransform USpatialReceiver::GetRelativeSpawnTransform(UClass* ActorClass, FTran return NewTransform; } -void USpatialReceiver::ApplyComponentDataOnActorCreation(Worker_EntityId EntityId, const Worker_ComponentData& Data, USpatialActorChannel* Channel, const FClassInfo& ActorClassInfo) +void USpatialReceiver::ApplyComponentDataOnActorCreation(Worker_EntityId EntityId, const Worker_ComponentData& Data, USpatialActorChannel& Channel, const FClassInfo& ActorClassInfo, TArray& OutObjectsToResolve) { - AActor* Actor = Channel->GetActor(); + AActor* Actor = Channel.GetActor(); uint32 Offset = 0; bool bFoundOffset = ClassInfoManager->GetOffsetByComponentId(Data.component_id, Offset); @@ -935,7 +1194,8 @@ void USpatialReceiver::ApplyComponentDataOnActorCreation(Worker_EntityId EntityI return; } - TWeakObjectPtr TargetObject = PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(EntityId, Offset)); + FUnrealObjectRef TargetObjectRef(EntityId, Offset); + TWeakObjectPtr TargetObject = PackageMap->GetObjectFromUnrealObjectRef(TargetObjectRef); if (!TargetObject.IsValid()) { bool bIsDynamicSubobject = !ActorClassInfo.SubobjectInfo.Contains(Offset); @@ -950,12 +1210,14 @@ void USpatialReceiver::ApplyComponentDataOnActorCreation(Worker_EntityId EntityI Actor->OnSubobjectCreatedFromReplication(TargetObject.Get()); - PackageMap->ResolveSubobject(TargetObject.Get(), FUnrealObjectRef(EntityId, Offset)); + PackageMap->ResolveSubobject(TargetObject.Get(), TargetObjectRef); - Channel->CreateSubObjects.Add(TargetObject.Get()); + Channel.CreateSubObjects.Add(TargetObject.Get()); } - ApplyComponentData(TargetObject.Get(), Channel, Data); + ApplyComponentData(Channel, *TargetObject, Data); + + OutObjectsToResolve.Add(ObjectPtrRefPair(TargetObject.Get(), TargetObjectRef)); } void USpatialReceiver::HandleIndividualAddComponent(const Worker_AddComponentOp& Op) @@ -972,7 +1234,10 @@ void USpatialReceiver::HandleIndividualAddComponent(const Worker_AddComponentOp& // Object already exists, we can apply data directly. if (UObject* Object = PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(Op.entity_id, Offset)).Get()) { - ApplyComponentData(Object, NetDriver->GetActorChannelByEntityId(Op.entity_id), Op.data); + if (USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(Op.entity_id)) + { + ApplyComponentData(*Channel, *Object, Op.data); + } return; } @@ -1021,7 +1286,6 @@ void USpatialReceiver::HandleIndividualAddComponent(const Worker_AddComponentOp& void USpatialReceiver::AttachDynamicSubobject(AActor* Actor, Worker_EntityId EntityId, const FClassInfo& Info) { - USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(EntityId); if (Channel == nullptr) { @@ -1033,7 +1297,8 @@ void USpatialReceiver::AttachDynamicSubobject(AActor* Actor, Worker_EntityId Ent Actor->OnSubobjectCreatedFromReplication(Subobject); - PackageMap->ResolveSubobject(Subobject, FUnrealObjectRef(EntityId, Info.SchemaComponents[SCHEMA_Data])); + FUnrealObjectRef SubobjectRef(EntityId, Info.SchemaComponents[SCHEMA_Data]); + PackageMap->ResolveSubobject(Subobject, SubobjectRef); Channel->CreateSubObjects.Add(Subobject); @@ -1049,29 +1314,95 @@ void USpatialReceiver::AttachDynamicSubobject(AActor* Actor, Worker_EntityId Ent TPair EntityComponentPair = MakeTuple(static_cast(EntityId), ComponentId); PendingAddComponentWrapper& AddComponent = PendingDynamicSubobjectComponents[EntityComponentPair]; - ApplyComponentData(Subobject, NetDriver->GetActorChannelByEntityId(EntityId), *AddComponent.Data->ComponentData); + ApplyComponentData(*Channel, *Subobject, *AddComponent.Data->ComponentData); PendingDynamicSubobjectComponents.Remove(EntityComponentPair); }); + // Resolve things like RepNotify or RPCs after applying component data. + ResolvePendingOperations(Subobject, SubobjectRef); + + // Don't send dynamic interest for this subobject if it is otherwise handled by result types. + if (GetDefault()->bEnableResultTypes) + { + return; + } + // If on a client, we need to set up the proper component interest for the new subobject. if (!NetDriver->IsServer()) { - Sender->SendComponentInterestForSubobject(Info, EntityId, Channel->IsOwnedByWorker()); + Sender->SendComponentInterestForSubobject(Info, EntityId, Channel->IsAuthoritativeClient()); } } -void USpatialReceiver::ApplyComponentData(UObject* TargetObject, USpatialActorChannel* Channel, const Worker_ComponentData& Data) +struct USpatialReceiver::RepStateUpdateHelper +{ + RepStateUpdateHelper(USpatialActorChannel& Channel, UObject& TargetObject) + : ObjectPtr(MakeWeakObjectPtr(&TargetObject)) + , ObjectRepState(Channel.ObjectReferenceMap.Find(ObjectPtr)) + { + } + + ~RepStateUpdateHelper() + { + check(bUpdatePerfomed); + } + + FObjectReferencesMap& GetRefMap() + { + if (ObjectRepState) + { + return ObjectRepState->ReferenceMap; + } + return TempRefMap; + } + + void Update(USpatialReceiver& Receiver, USpatialActorChannel& Channel, UObject& TargetObject, bool bReferencesChanged) + { + check(!bUpdatePerfomed); + + if (bReferencesChanged) + { + if (ObjectRepState == nullptr && TempRefMap.Num() > 0) + { + ObjectRepState = &Channel.ObjectReferenceMap.Add(ObjectPtr, FSpatialObjectRepState(FChannelObjectPair(&Channel, ObjectPtr))); + ObjectRepState->ReferenceMap = MoveTemp(TempRefMap); + } + + if (ObjectRepState) + { + ObjectRepState->UpdateRefToRepStateMap(Receiver.ObjectRefToRepStateMap); + + if (ObjectRepState->ReferencedObj.Num() == 0) + { + Channel.ObjectReferenceMap.Remove(ObjectPtr); + } + } + } +#if DO_CHECK + bUpdatePerfomed = true; +#endif + } + + //TSet UnresolvedRefs; +private: + FObjectReferencesMap TempRefMap; + TWeakObjectPtr ObjectPtr; + FSpatialObjectRepState* ObjectRepState; +#if DO_CHECK + bool bUpdatePerfomed = false; +#endif +}; + +void USpatialReceiver::ApplyComponentData(USpatialActorChannel& Channel, UObject& TargetObject, const Worker_ComponentData& Data) { UClass* Class = ClassInfoManager->GetClassByComponentId(Data.component_id); checkf(Class, TEXT("Component %d isn't hand-written and not present in ComponentToClassMap."), Data.component_id); - FChannelObjectPair ChannelObjectPair(Channel, TargetObject); - ESchemaComponentType ComponentType = ClassInfoManager->GetCategoryByComponentId(Data.component_id); if (ComponentType == SCHEMA_Data || ComponentType == SCHEMA_OwnerOnly) { - if (ComponentType == SCHEMA_Data && TargetObject->IsA()) + if (ComponentType == SCHEMA_Data && TargetObject.IsA()) { Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); bool bReplicates = !!Schema_IndexBool(ComponentObject, SpatialConstants::ACTOR_COMPONENT_REPLICATES_ID, 0); @@ -1080,33 +1411,39 @@ void USpatialReceiver::ApplyComponentData(UObject* TargetObject, USpatialActorCh return; } } + RepStateUpdateHelper RepStateHelper(Channel, TargetObject); - FObjectReferencesMap& ObjectReferencesMap = UnresolvedRefsMap.FindOrAdd(ChannelObjectPair); - TSet UnresolvedRefs; + ComponentReader Reader(NetDriver, RepStateHelper.GetRefMap()); + bool bOutReferencesChanged = false; + Reader.ApplyComponentData(Data, TargetObject, Channel, /* bIsHandover */ false, bOutReferencesChanged); - ComponentReader Reader(NetDriver, ObjectReferencesMap, UnresolvedRefs); - Reader.ApplyComponentData(Data, TargetObject, Channel, /* bIsHandover */ false); - - QueueIncomingRepUpdates(ChannelObjectPair, ObjectReferencesMap, UnresolvedRefs); + RepStateHelper.Update(*this, Channel, TargetObject, bOutReferencesChanged); } else if (ComponentType == SCHEMA_Handover) { - FObjectReferencesMap& ObjectReferencesMap = UnresolvedRefsMap.FindOrAdd(ChannelObjectPair); - TSet UnresolvedRefs; + RepStateUpdateHelper RepStateHelper(Channel, TargetObject); - ComponentReader Reader(NetDriver, ObjectReferencesMap, UnresolvedRefs); - Reader.ApplyComponentData(Data, TargetObject, Channel, /* bIsHandover */ true); + ComponentReader Reader(NetDriver, RepStateHelper.GetRefMap()); + bool bOutReferencesChanged = false; + Reader.ApplyComponentData(Data, TargetObject, Channel, /* bIsHandover */ true, bOutReferencesChanged); - QueueIncomingRepUpdates(ChannelObjectPair, ObjectReferencesMap, UnresolvedRefs); + RepStateHelper.Update(*this, Channel, TargetObject, bOutReferencesChanged); } else { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Entity: %d Component: %d - Skipping because RPC components don't have actual data."), Channel->GetEntityId(), Data.component_id); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Entity: %d Component: %d - Skipping because RPC components don't have actual data."), Channel.GetEntityId(), Data.component_id); } } void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) { + SCOPE_CYCLE_COUNTER(STAT_ReceiverComponentUpdate); + if (IsEntityWaitingForAsyncLoad(Op.entity_id)) + { + QueueComponentUpdateOpForAsyncLoad(Op); + return; + } + switch (Op.update.component_id) { case SpatialConstants::ENTITY_ACL_COMPONENT_ID: @@ -1122,6 +1459,7 @@ void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) case SpatialConstants::RPCS_ON_ENTITY_CREATION_ID: case SpatialConstants::DEBUG_METRICS_COMPONENT_ID: case SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID: + case SpatialConstants::SPATIAL_DEBUGGING_COMPONENT_ID: UE_LOG(LogSpatialReceiver, Verbose, TEXT("Entity: %d Component: %d - Skipping because this is hand-written Spatial component"), Op.entity_id, Op.update.component_id); return; case SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID: @@ -1142,21 +1480,31 @@ void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) case SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID: NetDriver->GlobalStateManager->ApplyStartupActorManagerUpdate(Op.update); return; - case SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID: - case SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID: - case SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID: - HandleRPC(Op); + case SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY: + case SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY: + case SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID_LEGACY: + HandleRPCLegacy(Op); return; case SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID: - check(false); // TODO(zoning): Handle updates to the entity's authority intent. - break; + case SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID: + case SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID: + if (LoadBalanceEnforcer != nullptr) + { + LoadBalanceEnforcer->OnLoadBalancingComponentUpdated(Op); + } + return; case SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID: - if (NetDriver->VirtualWorkerTranslator != nullptr) + if (NetDriver->VirtualWorkerTranslator.IsValid()) { Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Op.update.schema_type); NetDriver->VirtualWorkerTranslator->ApplyVirtualWorkerManagerData(ComponentObject); } return; + case SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID: + case SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID: + case SpatialConstants::MULTICAST_RPCS_COMPONENT_ID: + HandleRPC(Op); + return; } if (Op.update.component_id < SpatialConstants::MAX_RESERVED_SPATIAL_SYSTEM_COMPONENT_ID) @@ -1172,7 +1520,7 @@ void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) return; } - if (ClassInfoManager->IsSublevelComponent(Op.update.component_id)) + if (ClassInfoManager->IsGeneratedQBIMarkerComponent(Op.update.component_id)) { return; } @@ -1181,11 +1529,16 @@ void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) if (Channel == nullptr) { // If there is no actor channel as a result of the actor being dormant, then assume the actor is about to become active. - if (const Dormant* DormantComponent = StaticComponentView->GetComponentData(Op.entity_id)) + if (StaticComponentView->HasComponent(Op.entity_id, SpatialConstants::DORMANT_COMPONENT_ID)) { if (AActor* Actor = Cast(PackageMap->GetObjectFromEntityId(Op.entity_id))) { - Channel = RecreateDormantSpatialChannel(Actor, Op.entity_id); + Channel = GetOrRecreateChannelForDomantActor(Actor, Op.entity_id); + + // As we haven't removed the dormant component just yet, this might be a single replication update where the actor + // remains dormant. Add it back to pending dormancy so the local worker can clean up the channel. If we do process + // a dormant component removal later in this frame, we'll clear the channel from pending dormancy channel then. + NetDriver->AddPendingDormantChannel(Channel); } else { @@ -1231,17 +1584,19 @@ void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) if (Category == ESchemaComponentType::SCHEMA_Data || Category == ESchemaComponentType::SCHEMA_OwnerOnly) { - ApplyComponentUpdate(Op.update, TargetObject, Channel, /* bIsHandover */ false); + SCOPE_CYCLE_COUNTER(STAT_ReceiverApplyData); + ApplyComponentUpdate(Op.update, *TargetObject, *Channel, /* bIsHandover */ false); } else if (Category == ESchemaComponentType::SCHEMA_Handover) { + SCOPE_CYCLE_COUNTER(STAT_ReceiverApplyHandover); if (!NetDriver->IsServer()) { UE_LOG(LogSpatialReceiver, Verbose, TEXT("Entity: %d Component: %d - Skipping Handover component because we're a client."), Op.entity_id, Op.update.component_id); return; } - ApplyComponentUpdate(Op.update, TargetObject, Channel, /* bIsHandover */ true); + ApplyComponentUpdate(Op.update, *TargetObject, *Channel, /* bIsHandover */ true); } else { @@ -1249,38 +1604,32 @@ void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) } } -void USpatialReceiver::HandleRPC(const Worker_ComponentUpdateOp& Op) +void USpatialReceiver::HandleRPCLegacy(const Worker_ComponentUpdateOp& Op) { + SCOPE_CYCLE_COUNTER(STAT_ReceiverHandleRPCLegacy); Worker_EntityId EntityId = Op.entity_id; // If the update is to the client rpc endpoint, then the handler should have authority over the server rpc endpoint component and vice versa // Ideally these events are never delivered to workers which are not able to handle them with clever interest management - const Worker_ComponentId RPCEndpointComponentId = Op.update.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID - ? SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID : SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID; + const Worker_ComponentId RPCEndpointComponentId = Op.update.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY + ? SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY : SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY; // Multicast RPCs should be executed by whoever receives them. - if (Op.update.component_id != SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID) + if (Op.update.component_id != SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID_LEGACY) { - if (StaticComponentView->GetAuthority(Op.entity_id, RPCEndpointComponentId) != WORKER_AUTHORITY_AUTHORITATIVE) + if (!StaticComponentView->HasAuthority(Op.entity_id, RPCEndpointComponentId)) { return; } } - // Always process unpacked RPCs since some cannot be packed. - ProcessRPCEventField(EntityId, Op, RPCEndpointComponentId, /* bPacked */ false); - - if (GetDefault()->bPackRPCs) - { - // Only process packed RPCs if packing is enabled - ProcessRPCEventField(EntityId, Op, RPCEndpointComponentId, /* bPacked */ true); - } + ProcessRPCEventField(EntityId, Op, RPCEndpointComponentId); } -void USpatialReceiver::ProcessRPCEventField(Worker_EntityId EntityId, const Worker_ComponentUpdateOp& Op, Worker_ComponentId RPCEndpointComponentId, bool bPacked) +void USpatialReceiver::ProcessRPCEventField(Worker_EntityId EntityId, const Worker_ComponentUpdateOp& Op, Worker_ComponentId RPCEndpointComponentId) { Schema_Object* EventsObject = Schema_GetComponentUpdateEvents(Op.update.schema_type); - const Schema_FieldId EventId = bPacked ? SpatialConstants::UNREAL_RPC_ENDPOINT_PACKED_EVENT_ID : SpatialConstants::UNREAL_RPC_ENDPOINT_EVENT_ID; + const Schema_FieldId EventId = SpatialConstants::UNREAL_RPC_ENDPOINT_EVENT_ID; uint32 EventCount = Schema_GetObjectCount(EventsObject, EventId); for (uint32 i = 0; i < EventCount; i++) @@ -1291,46 +1640,67 @@ void USpatialReceiver::ProcessRPCEventField(Worker_EntityId EntityId, const Work FUnrealObjectRef ObjectRef(EntityId, Payload.Offset); - if (bPacked) + if (UObject* TargetObject = PackageMap->GetObjectFromUnrealObjectRef(ObjectRef).Get()) { - // When packing unreliable RPCs into one update, they also always go through the PlayerController. - // This means we need to retrieve the actual target Entity ID from the payload. - if (Op.update.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID || - Op.update.component_id == SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID) - { - ObjectRef.Entity = Schema_GetEntityId(EventData, SpatialConstants::UNREAL_PACKED_RPC_PAYLOAD_ENTITY_ID); + ProcessOrQueueIncomingRPC(ObjectRef, MoveTemp(Payload)); + } + } +} - // In a zoned multiworker scenario we might not have gained authority over the current entity in this bundle in time - // before processing so don't ApplyRPCs to an entity that we don't have authority over. - if (StaticComponentView->GetAuthority(ObjectRef.Entity, RPCEndpointComponentId) != WORKER_AUTHORITY_AUTHORITATIVE) - { - continue; - } - } - } +void USpatialReceiver::HandleRPC(const Worker_ComponentUpdateOp& Op) +{ + SCOPE_CYCLE_COUNTER(STAT_ReceiverHandleRPC); + if (!GetDefault()->UseRPCRingBuffer() || RPCService == nullptr) + { + UE_LOG(LogSpatialReceiver, Error, TEXT("Received component update on ring buffer component but ring buffers not enabled! Entity: %lld, Component: %d"), Op.entity_id, Op.update.component_id); + return; + } - if (UObject* TargetObject = PackageMap->GetObjectFromUnrealObjectRef(ObjectRef).Get()) + // When migrating an Actor to another worker, we preemptively change the role to SimulatedProxy when updating authority intent. + // This can happen while this worker still has ServerEndpoint authority, and attempting to process a server RPC causes the engine + // to print errors if the role isn't Authority. Instead, we exit here, and the RPC will be processed by the server that receives + // authority. + const bool bIsServerRpc = Op.update.component_id == SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID; + if (bIsServerRpc && StaticComponentView->HasAuthority(Op.entity_id, SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID)) + { + const TWeakObjectPtr ActorReceivingRPC = PackageMap->GetObjectFromEntityId(Op.entity_id); + if (!ActorReceivingRPC.IsValid()) { - ProcessOrQueueIncomingRPC(ObjectRef, MoveTemp(Payload)); + UE_LOG(LogSpatialReceiver, Error, TEXT("Entity receiving ring buffer RPC does not exist in PackageMap! Entity: %lld, Component: %d"), Op.entity_id, Op.update.component_id); + return; } + const bool bActorRoleIsSimulatedProxy = Cast(ActorReceivingRPC.Get())->Role == ROLE_SimulatedProxy; + if (bActorRoleIsSimulatedProxy) + { + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Will not process server RPC, Actor role changed to SimulatedProxy. This happens on migration. Entity: %lld"), Op.entity_id); + return; + } } + RPCService->ExtractRPCsForEntity(Op.entity_id, Op.update.component_id); } void USpatialReceiver::OnCommandRequest(const Worker_CommandRequestOp& Op) { + SCOPE_CYCLE_COUNTER(STAT_ReceiverCommandRequest); Schema_FieldId CommandIndex = Op.request.command_index; - if (Op.request.component_id == SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID && CommandIndex == SpatialConstants::PLAYER_SPAWNER_SPAWN_PLAYER_COMMAND_ID) + if (IsEntityWaitingForAsyncLoad(Op.entity_id)) { - Schema_Object* Payload = Schema_GetCommandRequestObject(Op.request.schema_type); + UE_LOG(LogSpatialReceiver, Warning, TEXT("USpatialReceiver::OnCommandRequest: Actor class async loading, cannot handle command. Entity %lld, Class %s"), Op.entity_id, *EntitiesWaitingForAsyncLoad[Op.entity_id].ClassPath); + Sender->SendCommandFailure(Op.request_id, TEXT("Target actor async loading.")); + return; + } - // Op.caller_attribute_set has two attributes. - // 1. The attribute of the worker type - // 2. The attribute of the specific worker that sent the request - // We want to give authority to the specific worker, so we grab the second element from the attribute set. - NetDriver->PlayerSpawner->ReceivePlayerSpawnRequest(Payload, Op.caller_attribute_set.attributes[1], Op.request_id); + if (Op.request.component_id == SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID && CommandIndex == SpatialConstants::PLAYER_SPAWNER_SPAWN_PLAYER_COMMAND_ID) + { + NetDriver->PlayerSpawner->ReceivePlayerSpawnRequestOnServer(Op); + return; + } + else if (Op.request.component_id == SpatialConstants::SERVER_WORKER_COMPONENT_ID && CommandIndex == SpatialConstants::SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND_ID) + { + NetDriver->PlayerSpawner->ReceiveForwardedPlayerSpawnRequest(Op); return; } else if (Op.request.component_id == SpatialConstants::RPCS_ON_ENTITY_CREATION_ID && CommandIndex == SpatialConstants::CLEAR_RPCS_ON_ENTITY_CREATION) @@ -1398,9 +1768,15 @@ void USpatialReceiver::OnCommandRequest(const Worker_CommandRequestOp& Op) void USpatialReceiver::OnCommandResponse(const Worker_CommandResponseOp& Op) { + SCOPE_CYCLE_COUNTER(STAT_ReceiverCommandResponse); if (Op.response.component_id == SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID) { - NetDriver->PlayerSpawner->ReceivePlayerSpawnResponse(Op); + NetDriver->PlayerSpawner->ReceivePlayerSpawnResponseOnClient(Op); + return; + } + else if (Op.response.component_id == SpatialConstants::SERVER_WORKER_COMPONENT_ID) + { + NetDriver->PlayerSpawner->ReceiveForwardPlayerSpawnResponse(Op); return; } @@ -1470,29 +1846,27 @@ void USpatialReceiver::ReceiveCommandResponse(const Worker_CommandResponseOp& Op } } -void USpatialReceiver::ApplyComponentUpdate(const Worker_ComponentUpdate& ComponentUpdate, UObject* TargetObject, USpatialActorChannel* Channel, bool bIsHandover) +void USpatialReceiver::ApplyComponentUpdate(const Worker_ComponentUpdate& ComponentUpdate, UObject& TargetObject, USpatialActorChannel& Channel, bool bIsHandover) { - FChannelObjectPair ChannelObjectPair(Channel, TargetObject); + RepStateUpdateHelper RepStateHelper(Channel, TargetObject); - FObjectReferencesMap& ObjectReferencesMap = UnresolvedRefsMap.FindOrAdd(ChannelObjectPair); - TSet UnresolvedRefs; - ComponentReader Reader(NetDriver, ObjectReferencesMap, UnresolvedRefs); - Reader.ApplyComponentUpdate(ComponentUpdate, TargetObject, Channel, bIsHandover); + ComponentReader Reader(NetDriver, RepStateHelper.GetRefMap()); + bool bOutReferencesChanged = false; + Reader.ApplyComponentUpdate(ComponentUpdate, TargetObject, Channel, bIsHandover, bOutReferencesChanged); + RepStateHelper.Update(*this, Channel, TargetObject, bOutReferencesChanged); // This is a temporary workaround, see UNR-841: // If the update includes tearoff, close the channel and clean up the entity. - if (TargetObject->IsA() && ClassInfoManager->GetCategoryByComponentId(ComponentUpdate.component_id) == SCHEMA_Data) + if (TargetObject.IsA() && ClassInfoManager->GetCategoryByComponentId(ComponentUpdate.component_id) == SCHEMA_Data) { Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(ComponentUpdate.schema_type); // Check if bTearOff has been set to true if (GetBoolFromSchema(ComponentObject, SpatialConstants::ACTOR_TEAROFF_ID)) { - Channel->ConditionalCleanUp(false, EChannelCloseReason::TearOff); + Channel.ConditionalCleanUp(false, EChannelCloseReason::TearOff); } } - - QueueIncomingRepUpdates(ChannelObjectPair, ObjectReferencesMap, UnresolvedRefs); } ERPCResult USpatialReceiver::ApplyRPCInternal(UObject* TargetObject, UFunction* Function, const RPCPayload& Payload, const FString& SenderWorkerId, bool bApplyWithUnresolvedRefs /* = false */) @@ -1503,38 +1877,15 @@ ERPCResult USpatialReceiver::ApplyRPCInternal(UObject* TargetObject, UFunction* FMemory::Memzero(Parms, Function->ParmsSize); TSet UnresolvedRefs; - + TSet MappedRefs; RPCPayload PayloadCopy = Payload; - FSpatialNetBitReader PayloadReader(PackageMap, PayloadCopy.PayloadData.GetData(), PayloadCopy.CountDataBits(), UnresolvedRefs); - - int ReliableRPCId = 0; - if (GetDefault()->bCheckRPCOrder) - { - if (Function->HasAnyFunctionFlags(FUNC_NetReliable) && !Function->HasAnyFunctionFlags(FUNC_NetMulticast)) - { - PayloadReader << ReliableRPCId; - } - } + FSpatialNetBitReader PayloadReader(PackageMap, PayloadCopy.PayloadData.GetData(), PayloadCopy.CountDataBits(), MappedRefs, UnresolvedRefs); TSharedPtr RepLayout = NetDriver->GetFunctionRepLayout(Function); RepLayout_ReceivePropertiesForRPC(*RepLayout, PayloadReader, Parms); if ((UnresolvedRefs.Num() == 0) || bApplyWithUnresolvedRefs) { - if (GetDefault()->bCheckRPCOrder) - { - if (Function->HasAnyFunctionFlags(FUNC_NetReliable) && !Function->HasAnyFunctionFlags(FUNC_NetMulticast)) - { - AActor* Actor = Cast(TargetObject); - if (Actor == nullptr) - { - Actor = Cast(TargetObject->GetOuter()); - check(Actor); - } - NetDriver->OnReceivedReliableRPC(Actor, FunctionFlagsToRPCSchemaType(Function->FunctionFlags), SenderWorkerId, ReliableRPCId, TargetObject, Function); - } - } - TargetObject->ProcessEvent(Function, Parms); Result = ERPCResult::Success; } @@ -1554,10 +1905,12 @@ ERPCResult USpatialReceiver::ApplyRPCInternal(UObject* TargetObject, UFunction* FRPCErrorInfo USpatialReceiver::ApplyRPC(const FPendingRPCParams& Params) { + SCOPE_CYCLE_COUNTER(STAT_ReceiverApplyRPC); + TWeakObjectPtr TargetObjectWeakPtr = PackageMap->GetObjectFromUnrealObjectRef(Params.ObjectRef); if (!TargetObjectWeakPtr.IsValid()) { - return FRPCErrorInfo{ nullptr, nullptr, NetDriver->IsServer(), ERPCQueueType::Receive, ERPCResult::UnresolvedTargetObject }; + return FRPCErrorInfo{ nullptr, nullptr, ERPCResult::UnresolvedTargetObject }; } UObject* TargetObject = TargetObjectWeakPtr.Get(); @@ -1565,7 +1918,7 @@ FRPCErrorInfo USpatialReceiver::ApplyRPC(const FPendingRPCParams& Params) UFunction* Function = ClassInfo.RPCs[Params.Payload.Index]; if (Function == nullptr) { - return FRPCErrorInfo{ TargetObject, nullptr, NetDriver->IsServer(), ERPCQueueType::Receive, ERPCResult::MissingFunctionInfo }; + return FRPCErrorInfo{ TargetObject, nullptr, ERPCResult::MissingFunctionInfo }; } bool bApplyWithUnresolvedRefs = false; @@ -1580,11 +1933,13 @@ FRPCErrorInfo USpatialReceiver::ApplyRPC(const FPendingRPCParams& Params) } ERPCResult Result = ApplyRPCInternal(TargetObject, Function, Params.Payload, FString{}, bApplyWithUnresolvedRefs); - return FRPCErrorInfo{ TargetObject, Function, NetDriver->IsServer(), ERPCQueueType::Receive, Result }; + + return FRPCErrorInfo{ TargetObject, Function, Result }; } void USpatialReceiver::OnReserveEntityIdsResponse(const Worker_ReserveEntityIdsResponseOp& Op) { + SCOPE_CYCLE_COUNTER(STAT_ReceiverReserveEntityIds); if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) { UE_LOG(LogSpatialReceiver, Warning, TEXT("ReserveEntityIds request failed: request id: %d, message: %s"), Op.request_id, UTF8_TO_TCHAR(Op.message)); @@ -1608,6 +1963,7 @@ void USpatialReceiver::OnReserveEntityIdsResponse(const Worker_ReserveEntityIdsR void USpatialReceiver::OnCreateEntityResponse(const Worker_CreateEntityResponseOp& Op) { + SCOPE_CYCLE_COUNTER(STAT_ReceiverCreateEntityResponse); switch (static_cast(Op.status_code)) { case WORKER_STATUS_CODE_SUCCESS: @@ -1652,6 +2008,7 @@ void USpatialReceiver::OnCreateEntityResponse(const Worker_CreateEntityResponseO void USpatialReceiver::OnEntityQueryResponse(const Worker_EntityQueryResponseOp& Op) { + SCOPE_CYCLE_COUNTER(STAT_ReceiverEntityQueryResponse); if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) { UE_LOG(LogSpatialReceiver, Error, TEXT("EntityQuery failed: request id: %d, message: %s"), Op.request_id, UTF8_TO_TCHAR(Op.message)); @@ -1680,17 +2037,17 @@ void USpatialReceiver::AddPendingReliableRPC(Worker_RequestId RequestId, TShared void USpatialReceiver::AddEntityQueryDelegate(Worker_RequestId RequestId, EntityQueryDelegate Delegate) { - EntityQueryDelegates.Add(RequestId, Delegate); + EntityQueryDelegates.Add(RequestId, MoveTemp(Delegate)); } void USpatialReceiver::AddReserveEntityIdsDelegate(Worker_RequestId RequestId, ReserveEntityIDsDelegate Delegate) { - ReserveEntityIDsDelegates.Add(RequestId, Delegate); + ReserveEntityIDsDelegates.Add(RequestId, MoveTemp(Delegate)); } -void USpatialReceiver::AddCreateEntityDelegate(Worker_RequestId RequestId, const CreateEntityDelegate& Delegate) +void USpatialReceiver::AddCreateEntityDelegate(Worker_RequestId RequestId, CreateEntityDelegate Delegate) { - CreateEntityDelegates.Add(RequestId, Delegate); + CreateEntityDelegates.Add(RequestId, MoveTemp(Delegate)); } TWeakObjectPtr USpatialReceiver::PopPendingActorRequest(Worker_RequestId RequestId) @@ -1719,42 +2076,15 @@ AActor* USpatialReceiver::FindSingletonActor(UClass* SingletonClass) return nullptr; } -void USpatialReceiver::ProcessQueuedResolvedObjects() -{ - for (TPair& It : ResolvedObjectQueue) - { - ResolvePendingOperations_Internal(It.Key, It.Value); - } - ResolvedObjectQueue.Empty(); -} - -void USpatialReceiver::ProcessQueuedActorRPCsOnEntityCreation(AActor* Actor, RPCsOnEntityCreation& QueuedRPCs) +void USpatialReceiver::ProcessQueuedActorRPCsOnEntityCreation(Worker_EntityId EntityId, RPCsOnEntityCreation& QueuedRPCs) { - const FClassInfo& Info = ClassInfoManager->GetOrCreateClassInfoByClass(Actor->GetClass()); - for (auto& RPC : QueuedRPCs.RPCs) { - UFunction* Function = Info.RPCs[RPC.Index]; - const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(Actor, Function); - const FUnrealObjectRef ObjectRef = PackageMap->GetUnrealObjectRefFromObject(Actor); - check(ObjectRef != FUnrealObjectRef::UNRESOLVED_OBJECT_REF); - + const FUnrealObjectRef ObjectRef(EntityId, RPC.Offset); ProcessOrQueueIncomingRPC(ObjectRef, MoveTemp(RPC)); } } -void USpatialReceiver::ResolvePendingOperations(UObject* Object, const FUnrealObjectRef& ObjectRef) -{ - if (bInCriticalSection) - { - ResolvedObjectQueue.Add(TPair{ Object, ObjectRef }); - } - else - { - ResolvePendingOperations_Internal(Object, ObjectRef); - } -} - void USpatialReceiver::OnDisconnect(Worker_DisconnectOp& Op) { if (GEngine != nullptr) @@ -1763,16 +2093,15 @@ void USpatialReceiver::OnDisconnect(Worker_DisconnectOp& Op) } } -bool USpatialReceiver::IsPendingOpsOnChannel(USpatialActorChannel* Channel) +bool USpatialReceiver::IsPendingOpsOnChannel(USpatialActorChannel& Channel) { SCOPE_CYCLE_COUNTER(STAT_SpatialPendingOpsOnChannel); // Don't allow Actors to go dormant if they have any pending operations waiting on their channel - check(Channel); - for (const auto& UnresolvedRef : UnresolvedRefsMap) + for (const auto& RefMap : Channel.ObjectReferenceMap) { - if (UnresolvedRef.Key.Key == Channel) + if (RefMap.Value.HasUnresolved()) { return true; } @@ -1780,7 +2109,7 @@ bool USpatialReceiver::IsPendingOpsOnChannel(USpatialActorChannel* Channel) for (const auto& ActorRequest : PendingActorRequests) { - if (ActorRequest.Value == Channel) + if (ActorRequest.Value == &Channel) { return true; } @@ -1789,21 +2118,12 @@ bool USpatialReceiver::IsPendingOpsOnChannel(USpatialActorChannel* Channel) return false; } -void USpatialReceiver::QueueIncomingRepUpdates(FChannelObjectPair ChannelObjectPair, const FObjectReferencesMap& ObjectReferencesMap, const TSet& UnresolvedRefs) +void USpatialReceiver::ClearPendingRPCs(Worker_EntityId EntityId) { - for (const FUnrealObjectRef& UnresolvedRef : UnresolvedRefs) - { - UE_LOG(LogSpatialReceiver, Log, TEXT("Added pending incoming property for object ref: %s, target object: %s"), *UnresolvedRef.ToString(), *ChannelObjectPair.Value->GetName()); - IncomingRefsMap.FindOrAdd(UnresolvedRef).Add(ChannelObjectPair); - } - - if (ObjectReferencesMap.Num() == 0) - { - UnresolvedRefsMap.Remove(ChannelObjectPair); - } + IncomingRPCs.DropForEntity(EntityId); } -void USpatialReceiver::ProcessOrQueueIncomingRPC(const FUnrealObjectRef& InTargetObjectRef, SpatialGDK::RPCPayload&& InPayload) +void USpatialReceiver::ProcessOrQueueIncomingRPC(const FUnrealObjectRef& InTargetObjectRef, SpatialGDK::RPCPayload InPayload) { TWeakObjectPtr TargetObjectWeakPtr = PackageMap->GetObjectFromUnrealObjectRef(InTargetObjectRef); if (!TargetObjectWeakPtr.IsValid()) @@ -1816,12 +2136,19 @@ void USpatialReceiver::ProcessOrQueueIncomingRPC(const FUnrealObjectRef& InTarge const FClassInfo& ClassInfo = ClassInfoManager->GetOrCreateClassInfoByObject(TargetObject); UFunction* Function = ClassInfo.RPCs[InPayload.Index]; const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); - ESchemaComponentType Type = RPCInfo.Type; + ERPCType Type = RPCInfo.Type; IncomingRPCs.ProcessOrQueueRPC(InTargetObjectRef, Type, MoveTemp(InPayload)); } -void USpatialReceiver::ResolvePendingOperations_Internal(UObject* Object, const FUnrealObjectRef& ObjectRef) +bool USpatialReceiver::OnExtractIncomingRPC(Worker_EntityId EntityId, ERPCType RPCType, const SpatialGDK::RPCPayload& Payload) +{ + ProcessOrQueueIncomingRPC(FUnrealObjectRef(EntityId, Payload.Offset), Payload); + + return true; +} + +void USpatialReceiver::ResolvePendingOperations(UObject* Object, const FUnrealObjectRef& ObjectRef) { UE_LOG(LogSpatialReceiver, Verbose, TEXT("Resolving pending object refs and RPCs which depend on object: %s %s."), *Object->GetName(), *ObjectRef.ToString()); @@ -1845,7 +2172,7 @@ void USpatialReceiver::ResolveIncomingOperations(UObject* Object, const FUnrealO // TODO: queue up resolved objects since they were resolved during process ops // and then resolve all of them at the end of process ops - UNR:582 - TSet* TargetObjectSet = IncomingRefsMap.Find(ObjectRef); + TSet* TargetObjectSet = ObjectRefToRepStateMap.Find(ObjectRef); if (!TargetObjectSet) { return; @@ -1853,22 +2180,32 @@ void USpatialReceiver::ResolveIncomingOperations(UObject* Object, const FUnrealO UE_LOG(LogSpatialReceiver, Verbose, TEXT("Resolving incoming operations depending on object ref %s, resolved object: %s"), *ObjectRef.ToString(), *Object->GetName()); - for (FChannelObjectPair& ChannelObjectPair : *TargetObjectSet) + for (auto ChannelObjectIter = TargetObjectSet->CreateIterator(); ChannelObjectIter; ++ChannelObjectIter) { - FObjectReferencesMap* UnresolvedRefs = UnresolvedRefsMap.Find(ChannelObjectPair); - if (!UnresolvedRefs) + USpatialActorChannel* DependentChannel = ChannelObjectIter->Key.Get(); + if (!DependentChannel) { + ChannelObjectIter.RemoveCurrent(); continue; } - if (!ChannelObjectPair.Key.IsValid() || !ChannelObjectPair.Value.IsValid()) + UObject* ReplicatingObject = ChannelObjectIter->Value.Get(); + + if (!ReplicatingObject) { - UnresolvedRefsMap.Remove(ChannelObjectPair); + if (DependentChannel->ObjectReferenceMap.Find(ChannelObjectIter->Value)) + { + DependentChannel->ObjectReferenceMap.Remove(ChannelObjectIter->Value); + ChannelObjectIter.RemoveCurrent(); + } continue; } - USpatialActorChannel* DependentChannel = ChannelObjectPair.Key.Get(); - UObject* ReplicatingObject = ChannelObjectPair.Value.Get(); + FSpatialObjectRepState* RepState = DependentChannel->ObjectReferenceMap.Find(ChannelObjectIter->Value); + if (!RepState || !RepState->UnresolvedRefs.Contains(ObjectRef)) + { + continue; + } // Check whether the resolved object has been torn off, or is on an actor that has been torn off. if (AActor* AsActor = Cast(ReplicatingObject)) @@ -1876,7 +2213,7 @@ void USpatialReceiver::ResolveIncomingOperations(UObject* Object, const FUnrealO if (AsActor->GetTearOff()) { UE_LOG(LogSpatialActorChannel, Log, TEXT("Actor to be resolved was torn off, so ignoring incoming operations. Object ref: %s, resolved object: %s"), *ObjectRef.ToString(), *Object->GetName()); - UnresolvedRefsMap.Remove(ChannelObjectPair); + DependentChannel->ObjectReferenceMap.Remove(ChannelObjectIter->Value); continue; } } @@ -1885,38 +2222,36 @@ void USpatialReceiver::ResolveIncomingOperations(UObject* Object, const FUnrealO if (OuterActor->GetTearOff()) { UE_LOG(LogSpatialActorChannel, Log, TEXT("Owning Actor of the object to be resolved was torn off, so ignoring incoming operations. Object ref: %s, resolved object: %s"), *ObjectRef.ToString(), *Object->GetName()); - UnresolvedRefsMap.Remove(ChannelObjectPair); + DependentChannel->ObjectReferenceMap.Remove(ChannelObjectIter->Value); continue; } } - bool bStillHasUnresolved = false; bool bSomeObjectsWereMapped = false; TArray RepNotifies; FRepLayout& RepLayout = DependentChannel->GetObjectRepLayout(ReplicatingObject); FRepStateStaticBuffer& ShadowData = DependentChannel->GetObjectStaticBuffer(ReplicatingObject); + if (ShadowData.Num() == 0) + { + DependentChannel->ResetShadowData(RepLayout, ShadowData, ReplicatingObject); + } - ResolveObjectReferences(RepLayout, ReplicatingObject, *UnresolvedRefs, ShadowData.GetData(), (uint8*)ReplicatingObject, ReplicatingObject->GetClass()->GetPropertiesSize(), RepNotifies, bSomeObjectsWereMapped, bStillHasUnresolved); + ResolveObjectReferences(RepLayout, ReplicatingObject, *RepState, RepState->ReferenceMap, ShadowData.GetData(), (uint8*)ReplicatingObject, ReplicatingObject->GetClass()->GetPropertiesSize(), RepNotifies, bSomeObjectsWereMapped); if (bSomeObjectsWereMapped) { - DependentChannel->RemoveRepNotifiesWithUnresolvedObjs(RepNotifies, RepLayout, *UnresolvedRefs, ReplicatingObject); + DependentChannel->RemoveRepNotifiesWithUnresolvedObjs(RepNotifies, RepLayout, RepState->ReferenceMap, ReplicatingObject); UE_LOG(LogSpatialReceiver, Verbose, TEXT("Resolved for target object %s"), *ReplicatingObject->GetName()); DependentChannel->PostReceiveSpatialUpdate(ReplicatingObject, RepNotifies); } - if (!bStillHasUnresolved) - { - UnresolvedRefsMap.Remove(ChannelObjectPair); - } + RepState->UnresolvedRefs.Remove(ObjectRef); } - - IncomingRefsMap.Remove(ObjectRef); } -void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* ReplicatedObject, FObjectReferencesMap& ObjectReferencesMap, uint8* RESTRICT StoredData, uint8* RESTRICT Data, int32 MaxAbsOffset, TArray& RepNotifies, bool& bOutSomeObjectsWereMapped, bool& bOutStillHasUnresolved) +void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* ReplicatedObject, FSpatialObjectRepState& RepState, FObjectReferencesMap& ObjectReferencesMap, uint8* RESTRICT StoredData, uint8* RESTRICT Data, int32 MaxAbsOffset, TArray& RepNotifies, bool& bOutSomeObjectsWereMapped) { for (auto It = ObjectReferencesMap.CreateIterator(); It; ++It) { @@ -1941,7 +2276,8 @@ void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* R if (ObjectReferences.Array) { - check(Property->IsA()); + UArrayProperty* ArrayProperty = Cast(Property); + check(ArrayProperty != nullptr); if (!bIsHandover) { @@ -1951,23 +2287,15 @@ void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* R FScriptArray* StoredArray = bIsHandover ? nullptr : (FScriptArray*)(StoredData + StoredDataOffset); FScriptArray* Array = (FScriptArray*)(Data + AbsOffset); - int32 NewMaxOffset = Array->Num() * Property->ElementSize; + int32 NewMaxOffset = Array->Num() * ArrayProperty->Inner->ElementSize; - bool bArrayHasUnresolved = false; - ResolveObjectReferences(RepLayout, ReplicatedObject, *ObjectReferences.Array, bIsHandover ? nullptr : (uint8*)StoredArray->GetData(), (uint8*)Array->GetData(), NewMaxOffset, RepNotifies, bOutSomeObjectsWereMapped, bArrayHasUnresolved); - if (!bArrayHasUnresolved) - { - It.RemoveCurrent(); - } - else - { - bOutStillHasUnresolved = true; - } + ResolveObjectReferences(RepLayout, ReplicatedObject, RepState, *ObjectReferences.Array, bIsHandover ? nullptr : (uint8*)StoredArray->GetData(), (uint8*)Array->GetData(), NewMaxOffset, RepNotifies, bOutSomeObjectsWereMapped); continue; } bool bResolvedSomeRefs = false; UObject* SinglePropObject = nullptr; + FUnrealObjectRef SinglePropRef = FUnrealObjectRef::NULL_OBJECT_REF; for (auto UnresolvedIt = ObjectReferences.UnresolvedRefs.CreateIterator(); UnresolvedIt; ++UnresolvedIt) { @@ -1981,13 +2309,15 @@ void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* R UE_LOG(LogSpatialReceiver, Verbose, TEXT("ResolveObjectReferences: Resolved object ref: Offset: %d, Object ref: %s, PropName: %s, ObjName: %s"), AbsOffset, *ObjectRef.ToString(), *Property->GetNameCPP(), *Object->GetName()); - UnresolvedIt.RemoveCurrent(); - bResolvedSomeRefs = true; - if (ObjectReferences.bSingleProp) { SinglePropObject = Object; + SinglePropRef = ObjectRef; } + + UnresolvedIt.RemoveCurrent(); + + bResolvedSomeRefs = true; } } @@ -2010,28 +2340,32 @@ void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* R check(ObjectProperty); ObjectProperty->SetObjectPropertyValue(Data + AbsOffset, SinglePropObject); + ObjectReferences.MappedRefs.Add(SinglePropRef); } else if (ObjectReferences.bFastArrayProp) { + TSet NewMappedRefs; TSet NewUnresolvedRefs; - FSpatialNetBitReader ValueDataReader(PackageMap, ObjectReferences.Buffer.GetData(), ObjectReferences.NumBufferBits, NewUnresolvedRefs); + FSpatialNetBitReader ValueDataReader(PackageMap, ObjectReferences.Buffer.GetData(), ObjectReferences.NumBufferBits, NewMappedRefs, NewUnresolvedRefs); check(Property->IsA()); UScriptStruct* NetDeltaStruct = GetFastArraySerializerProperty(Cast(Property)); FSpatialNetDeltaSerializeInfo::DeltaSerializeRead(NetDriver, ValueDataReader, ReplicatedObject, Parent->ArrayIndex, Parent->Property, NetDeltaStruct); - if (NewUnresolvedRefs.Num() > 0) - { - bOutStillHasUnresolved = true; - } + ObjectReferences.MappedRefs.Append(NewMappedRefs); } else { + TSet NewMappedRefs; TSet NewUnresolvedRefs; - FSpatialNetBitReader BitReader(PackageMap, ObjectReferences.Buffer.GetData(), ObjectReferences.NumBufferBits, NewUnresolvedRefs); + FSpatialNetBitReader BitReader(PackageMap, ObjectReferences.Buffer.GetData(), ObjectReferences.NumBufferBits, NewMappedRefs, NewUnresolvedRefs); check(Property->IsA()); - ReadStructProperty(BitReader, Cast(Property), NetDriver, Data + AbsOffset, bOutStillHasUnresolved); + + bool bHasUnresolved = false; + ReadStructProperty(BitReader, Cast(Property), NetDriver, Data + AbsOffset, bHasUnresolved); + + ObjectReferences.MappedRefs.Append(NewMappedRefs); } if (Parent && Parent->Property->HasAnyPropertyFlags(CPF_RepNotify)) @@ -2042,15 +2376,6 @@ void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* R } } } - - if (ObjectReferences.UnresolvedRefs.Num() > 0) - { - bOutStillHasUnresolved = true; - } - else - { - It.RemoveCurrent(); - } } } @@ -2096,11 +2421,16 @@ void USpatialReceiver::OnHeartbeatComponentUpdate(const Worker_ComponentUpdateOp GetBoolFromSchema(FieldsObject, SpatialConstants::HEARTBEAT_CLIENT_HAS_QUIT_ID)) { // Client has disconnected, let's clean up their connection. - NetConnection->CleanUp(); - AuthorityPlayerControllerConnectionMap.Remove(Op.entity_id); + CloseClientConnection(NetConnection, Op.entity_id); } } +void USpatialReceiver::CloseClientConnection(USpatialNetConnection* ClientConnection, Worker_EntityId PlayerControllerEntityId) +{ + ClientConnection->CleanUp(); + AuthorityPlayerControllerConnectionMap.Remove(PlayerControllerEntityId); +} + void USpatialReceiver::PeriodicallyProcessIncomingRPCs() { FTimerHandle IncomingRPCsPeriodicProcessTimer; @@ -2112,3 +2442,261 @@ void USpatialReceiver::PeriodicallyProcessIncomingRPCs() } }, GetDefault()->QueuedIncomingRPCWaitTime, true); } + +bool USpatialReceiver::NeedToLoadClass(const FString& ClassPath) +{ + return FindObject(nullptr, *ClassPath, false) == nullptr; +} + +FString USpatialReceiver::GetPackagePath(const FString& ClassPath) +{ + return FSoftObjectPath(ClassPath).GetLongPackageName(); +} + +void USpatialReceiver::StartAsyncLoadingClass(const FString& ClassPath, Worker_EntityId EntityId) +{ + FString PackagePath = GetPackagePath(ClassPath); + FName PackagePathName = *PackagePath; + + bool bAlreadyLoading = AsyncLoadingPackages.Contains(PackagePathName); + + if (IsEntityWaitingForAsyncLoad(EntityId)) + { + // This shouldn't happen because even if the entity goes out and comes back into view, + // we would've received a RemoveEntity op that would remove the entry from the map. + UE_LOG(LogSpatialReceiver, Error, TEXT("USpatialReceiver::ReceiveActor: Checked out entity but it's already waiting for async load! Entity: %lld"), EntityId); + } + + EntityWaitingForAsyncLoad AsyncLoadEntity; + AsyncLoadEntity.ClassPath = ClassPath; + AsyncLoadEntity.InitialPendingAddComponents = ExtractAddComponents(EntityId); + AsyncLoadEntity.PendingOps = ExtractAuthorityOps(EntityId); + + EntitiesWaitingForAsyncLoad.Emplace(EntityId, MoveTemp(AsyncLoadEntity)); + AsyncLoadingPackages.FindOrAdd(PackagePathName).Add(EntityId); + + UE_LOG(LogSpatialReceiver, Log, TEXT("Async loading package %s for entity %lld. Already loading: %s"), *PackagePath, EntityId, bAlreadyLoading ? TEXT("true") : TEXT("false")); + if (!bAlreadyLoading) + { + LoadPackageAsync(PackagePath, FLoadPackageAsyncDelegate::CreateUObject(this, &USpatialReceiver::OnAsyncPackageLoaded)); + } +} + +void USpatialReceiver::OnAsyncPackageLoaded(const FName& PackageName, UPackage* Package, EAsyncLoadingResult::Type Result) +{ + TArray Entities; + if (!AsyncLoadingPackages.RemoveAndCopyValue(PackageName, Entities)) + { + UE_LOG(LogSpatialReceiver, Error, TEXT("USpatialReceiver::OnAsyncPackageLoaded: Package loaded but no entry in AsyncLoadingPackages. Package: %s"), *PackageName.ToString()); + return; + } + + for (Worker_EntityId Entity : Entities) + { + if (IsEntityWaitingForAsyncLoad(Entity)) + { + UE_LOG(LogSpatialReceiver, Log, TEXT("Finished async loading package %s for entity %lld."), *PackageName.ToString(), Entity); + + // Save critical section if we're in one and restore upon leaving this scope. + CriticalSectionSaveState CriticalSectionState(*this); + + EntityWaitingForAsyncLoad AsyncLoadEntity = EntitiesWaitingForAsyncLoad.FindAndRemoveChecked(Entity); + PendingAddActors.Add(Entity); + PendingAddComponents = MoveTemp(AsyncLoadEntity.InitialPendingAddComponents); + LeaveCriticalSection(); + + for (QueuedOpForAsyncLoad& Op : AsyncLoadEntity.PendingOps) + { + HandleQueuedOpForAsyncLoad(Op); + } + } + } +} + +void USpatialReceiver::MoveMappedObjectToUnmapped(const FUnrealObjectRef& Ref) +{ + if (TSet* RepStatesWithMappedRef = ObjectRefToRepStateMap.Find(Ref)) + { + for (const FChannelObjectPair& ChannelObject : *RepStatesWithMappedRef) + { + if (USpatialActorChannel* Channel = ChannelObject.Key.Get()) + { + if (FSpatialObjectRepState* RepState = Channel->ObjectReferenceMap.Find(ChannelObject.Value)) + { + RepState->MoveMappedObjectToUnmapped(Ref); + } + } + } + } +} + +bool USpatialReceiver::IsEntityWaitingForAsyncLoad(Worker_EntityId Entity) +{ + return EntitiesWaitingForAsyncLoad.Contains(Entity); +} + +void USpatialReceiver::QueueAddComponentOpForAsyncLoad(const Worker_AddComponentOp& Op) +{ + EntityWaitingForAsyncLoad& AsyncLoadEntity = EntitiesWaitingForAsyncLoad.FindChecked(Op.entity_id); + + QueuedOpForAsyncLoad NewOp = {}; + NewOp.AcquiredData = Worker_AcquireComponentData(&Op.data); + NewOp.Op.op_type = WORKER_OP_TYPE_ADD_COMPONENT; + NewOp.Op.op.add_component.entity_id = Op.entity_id; + NewOp.Op.op.add_component.data = *NewOp.AcquiredData; + AsyncLoadEntity.PendingOps.Add(NewOp); +} + +void USpatialReceiver::QueueRemoveComponentOpForAsyncLoad(const Worker_RemoveComponentOp& Op) +{ + EntityWaitingForAsyncLoad& AsyncLoadEntity = EntitiesWaitingForAsyncLoad.FindChecked(Op.entity_id); + + QueuedOpForAsyncLoad NewOp = {}; + NewOp.Op.op_type = WORKER_OP_TYPE_REMOVE_COMPONENT; + NewOp.Op.op.remove_component = Op; + AsyncLoadEntity.PendingOps.Add(NewOp); +} + +void USpatialReceiver::QueueAuthorityOpForAsyncLoad(const Worker_AuthorityChangeOp& Op) +{ + EntityWaitingForAsyncLoad& AsyncLoadEntity = EntitiesWaitingForAsyncLoad.FindChecked(Op.entity_id); + + QueuedOpForAsyncLoad NewOp = {}; + NewOp.Op.op_type = WORKER_OP_TYPE_AUTHORITY_CHANGE; + NewOp.Op.op.authority_change = Op; + AsyncLoadEntity.PendingOps.Add(NewOp); +} + +void USpatialReceiver::QueueComponentUpdateOpForAsyncLoad(const Worker_ComponentUpdateOp& Op) +{ + EntityWaitingForAsyncLoad& AsyncLoadEntity = EntitiesWaitingForAsyncLoad.FindChecked(Op.entity_id); + + QueuedOpForAsyncLoad NewOp = {}; + NewOp.AcquiredUpdate = Worker_AcquireComponentUpdate(&Op.update); + NewOp.Op.op_type = WORKER_OP_TYPE_COMPONENT_UPDATE; + NewOp.Op.op.component_update.entity_id = Op.entity_id; + NewOp.Op.op.component_update.update = *NewOp.AcquiredUpdate; + AsyncLoadEntity.PendingOps.Add(NewOp); +} + +TArray USpatialReceiver::ExtractAddComponents(Worker_EntityId Entity) +{ + TArray ExtractedAddComponents; + TArray RemainingAddComponents; + + for (PendingAddComponentWrapper& AddComponent : PendingAddComponents) + { + if (AddComponent.EntityId == Entity) + { + ExtractedAddComponents.Add(MoveTemp(AddComponent)); + } + else + { + RemainingAddComponents.Add(MoveTemp(AddComponent)); + } + } + PendingAddComponents = MoveTemp(RemainingAddComponents); + return ExtractedAddComponents; +} + +TArray USpatialReceiver::ExtractAuthorityOps(Worker_EntityId Entity) +{ + TArray ExtractedOps; + TArray RemainingOps; + + for (const Worker_AuthorityChangeOp& Op : PendingAuthorityChanges) + { + if (Op.entity_id == Entity) + { + QueuedOpForAsyncLoad NewOp = {}; + NewOp.Op.op_type = WORKER_OP_TYPE_AUTHORITY_CHANGE; + NewOp.Op.op.authority_change = Op; + ExtractedOps.Add(NewOp); + } + else + { + RemainingOps.Add(Op); + } + } + PendingAuthorityChanges = MoveTemp(RemainingOps); + return ExtractedOps; +} + +void USpatialReceiver::HandleQueuedOpForAsyncLoad(QueuedOpForAsyncLoad& Op) +{ + switch (Op.Op.op_type) + { + case WORKER_OP_TYPE_ADD_COMPONENT: + OnAddComponent(Op.Op.op.add_component); + Worker_ReleaseComponentData(Op.AcquiredData); + break; + case WORKER_OP_TYPE_REMOVE_COMPONENT: + ProcessRemoveComponent(Op.Op.op.remove_component); + break; + case WORKER_OP_TYPE_AUTHORITY_CHANGE: + HandleActorAuthority(Op.Op.op.authority_change); + break; + case WORKER_OP_TYPE_COMPONENT_UPDATE: + OnComponentUpdate(Op.Op.op.component_update); + Worker_ReleaseComponentUpdate(Op.AcquiredUpdate); + break; + default: + checkNoEntry(); + } +} + +USpatialReceiver::CriticalSectionSaveState::CriticalSectionSaveState(USpatialReceiver& InReceiver) + : Receiver(InReceiver) + , bInCriticalSection(InReceiver.bInCriticalSection) +{ + if (bInCriticalSection) + { + PendingAddActors = MoveTemp(Receiver.PendingAddActors); + PendingAuthorityChanges = MoveTemp(Receiver.PendingAuthorityChanges); + PendingAddComponents = MoveTemp(Receiver.PendingAddComponents); + Receiver.PendingAddActors.Empty(); + Receiver.PendingAuthorityChanges.Empty(); + Receiver.PendingAddComponents.Empty(); + } + Receiver.bInCriticalSection = true; +} + +USpatialReceiver::CriticalSectionSaveState::~CriticalSectionSaveState() +{ + if (bInCriticalSection) + { + Receiver.PendingAddActors = MoveTemp(PendingAddActors); + Receiver.PendingAuthorityChanges = MoveTemp(PendingAuthorityChanges); + Receiver.PendingAddComponents = MoveTemp(PendingAddComponents); + } + Receiver.bInCriticalSection = bInCriticalSection; +} + +namespace +{ + FString GetObjectNameFromRepState(const FSpatialObjectRepState& RepState) + { + if (UObject* Obj = RepState.GetChannelObjectPair().Value.Get()) + { + return Obj->GetName(); + } + return TEXT(""); + } +} + +void USpatialReceiver::CleanupRepStateMap(FSpatialObjectRepState& RepState) +{ + for (const FUnrealObjectRef& Ref : RepState.ReferencedObj) + { + TSet* RepStatesWithMappedRef = ObjectRefToRepStateMap.Find(Ref); + if (ensureMsgf(RepStatesWithMappedRef, TEXT("Ref to entity %lld on object %s is missing its referenced entry in the Ref/RepState map"), Ref.Entity, *GetObjectNameFromRepState(RepState))) + { + checkf(RepStatesWithMappedRef->Contains(RepState.GetChannelObjectPair()), TEXT("Ref to entity %lld on object %s is missing its referenced entry in the Ref/RepState map"), Ref.Entity, *GetObjectNameFromRepState(RepState)); + RepStatesWithMappedRef->Remove(RepState.GetChannelObjectPair()); + if (RepStatesWithMappedRef->Num() == 0) + { + ObjectRefToRepStateMap.Remove(Ref); + } + } + } +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp index 9a1c4253aa..12acb545dc 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp @@ -10,36 +10,42 @@ #include "EngineClasses/SpatialNetConnection.h" #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" +#include "EngineClasses/SpatialLoadBalanceEnforcer.h" #include "Interop/Connection/SpatialWorkerConnection.h" -#include "Interop/SpatialDispatcher.h" +#include "Interop/GlobalStateManager.h" #include "Interop/SpatialReceiver.h" -#include "Schema/AlwaysRelevant.h" +#include "Net/NetworkProfiler.h" #include "Schema/AuthorityIntent.h" -#include "Schema/ClientRPCEndpoint.h" -#include "Schema/Heartbeat.h" +#include "Schema/ClientRPCEndpointLegacy.h" +#include "Schema/ComponentPresence.h" #include "Schema/Interest.h" #include "Schema/RPCPayload.h" -#include "Schema/ServerRPCEndpoint.h" -#include "Schema/Singleton.h" -#include "Schema/SpawnData.h" +#include "Schema/ServerRPCEndpointLegacy.h" +#include "Schema/ServerWorker.h" #include "Schema/StandardLibrary.h" #include "Schema/Tombstone.h" -#include "Schema/UnrealMetadata.h" #include "SpatialConstants.h" -#include "Utils/ActorGroupManager.h" #include "Utils/ComponentFactory.h" +#include "Utils/EntityFactory.h" #include "Utils/InterestFactory.h" #include "Utils/RepLayoutUtils.h" +#include "Utils/SpatialActorGroupManager.h" #include "Utils/SpatialActorUtils.h" +#include "Utils/SpatialDebugger.h" +#include "Utils/SpatialLatencyTracer.h" #include "Utils/SpatialMetrics.h" +#include "Utils/SpatialStatics.h" DEFINE_LOG_CATEGORY(LogSpatialSender); using namespace SpatialGDK; -DECLARE_CYCLE_STAT(TEXT("SendComponentUpdates"), STAT_SpatialSenderSendComponentUpdates, STATGROUP_SpatialNet); -DECLARE_CYCLE_STAT(TEXT("ResetOutgoingUpdate"), STAT_SpatialSenderResetOutgoingUpdate, STATGROUP_SpatialNet); -DECLARE_CYCLE_STAT(TEXT("QueueOutgoingUpdate"), STAT_SpatialSenderQueueOutgoingUpdate, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Sender SendComponentUpdates"), STAT_SpatialSenderSendComponentUpdates, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Sender ResetOutgoingUpdate"), STAT_SpatialSenderResetOutgoingUpdate, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Sender QueueOutgoingUpdate"), STAT_SpatialSenderQueueOutgoingUpdate, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Sender UpdateInterestComponent"), STAT_SpatialSenderUpdateInterestComponent, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Sender FlushRetryRPCs"), STAT_SpatialSenderFlushRetryRPCs, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Sender SendRPC"), STAT_SpatialSenderSendRPC, STATGROUP_SpatialNet); FReliableRPCForRetry::FReliableRPCForRetry(UObject* InTargetObject, UFunction* InFunction, Worker_ComponentId InComponentId, Schema_FieldId InRPCIndex, const TArray& InPayload, int InRetryIndex) : TargetObject(InTargetObject) @@ -52,15 +58,7 @@ FReliableRPCForRetry::FReliableRPCForRetry(UObject* InTargetObject, UFunction* I { } -FPendingRPC::FPendingRPC(FPendingRPC&& Other) - : Offset(Other.Offset) - , Index(Other.Index) - , Data(MoveTemp(Other.Data)) - , Entity(Other.Entity) -{ -} - -void USpatialSender::Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimerManager) +void USpatialSender::Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimerManager, SpatialGDK::SpatialRPCService* InRPCService) { NetDriver = InNetDriver; StaticComponentView = InNetDriver->StaticComponentView; @@ -68,286 +66,23 @@ void USpatialSender::Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimer Receiver = InNetDriver->Receiver; PackageMap = InNetDriver->PackageMap; ClassInfoManager = InNetDriver->ClassInfoManager; + check(InNetDriver->ActorGroupManager != nullptr); ActorGroupManager = InNetDriver->ActorGroupManager; TimerManager = InTimerManager; + RPCService = InRPCService; OutgoingRPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(this, &USpatialSender::SendRPC)); } -Worker_RequestId USpatialSender::CreateEntity(USpatialActorChannel* Channel) +Worker_RequestId USpatialSender::CreateEntity(USpatialActorChannel* Channel, uint32& OutBytesWritten) { - AActor* Actor = Channel->Actor; - UClass* Class = Actor->GetClass(); - - FString ClientWorkerAttribute = GetOwnerWorkerAttribute(Actor); - - WorkerRequirementSet AnyServerRequirementSet; - WorkerRequirementSet AnyServerOrClientRequirementSet = { SpatialConstants::UnrealClientAttributeSet }; - - WorkerAttributeSet OwningClientAttributeSet = { ClientWorkerAttribute }; - - WorkerRequirementSet AnyServerOrOwningClientRequirementSet = { OwningClientAttributeSet }; - WorkerRequirementSet OwningClientOnlyRequirementSet = { OwningClientAttributeSet }; - - for (const FName& WorkerType : GetDefault()->ServerWorkerTypes) - { - WorkerAttributeSet ServerWorkerAttributeSet = { WorkerType.ToString() }; - - AnyServerRequirementSet.Add(ServerWorkerAttributeSet); - AnyServerOrClientRequirementSet.Add(ServerWorkerAttributeSet); - AnyServerOrOwningClientRequirementSet.Add(ServerWorkerAttributeSet); - } - - WorkerRequirementSet ReadAcl; - if (Class->HasAnySpatialClassFlags(SPATIALCLASS_ServerOnly)) - { - ReadAcl = AnyServerRequirementSet; - } - else if (Actor->IsA()) - { - ReadAcl = AnyServerOrOwningClientRequirementSet; - } - else - { - ReadAcl = AnyServerOrClientRequirementSet; - } - - const FClassInfo& Info = ClassInfoManager->GetOrCreateClassInfoByClass(Class); - - const WorkerAttributeSet WorkerAttribute{ Info.WorkerType.ToString() }; - const WorkerRequirementSet AuthoritativeWorkerRequirementSet = { WorkerAttribute }; - - WriteAclMap ComponentWriteAcl; - ComponentWriteAcl.Add(SpatialConstants::POSITION_COMPONENT_ID, AuthoritativeWorkerRequirementSet); - ComponentWriteAcl.Add(SpatialConstants::INTEREST_COMPONENT_ID, AuthoritativeWorkerRequirementSet); - ComponentWriteAcl.Add(SpatialConstants::SPAWN_DATA_COMPONENT_ID, AuthoritativeWorkerRequirementSet); - ComponentWriteAcl.Add(SpatialConstants::ENTITY_ACL_COMPONENT_ID, AuthoritativeWorkerRequirementSet); - ComponentWriteAcl.Add(SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID, AuthoritativeWorkerRequirementSet); - ComponentWriteAcl.Add(SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID, AuthoritativeWorkerRequirementSet); - ComponentWriteAcl.Add(SpatialConstants::DORMANT_COMPONENT_ID, AuthoritativeWorkerRequirementSet); - ComponentWriteAcl.Add(SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID, OwningClientOnlyRequirementSet); - ComponentWriteAcl.Add(SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID, AuthoritativeWorkerRequirementSet); - - if (Actor->IsNetStartupActor()) - { - ComponentWriteAcl.Add(SpatialConstants::TOMBSTONE_COMPONENT_ID, AuthoritativeWorkerRequirementSet); - } - - // If there are pending RPCs, add this component. - if (OutgoingOnCreateEntityRPCs.Contains(Actor)) - { - ComponentWriteAcl.Add(SpatialConstants::RPCS_ON_ENTITY_CREATION_ID, AuthoritativeWorkerRequirementSet); - } - - // If Actor is a PlayerController, add the heartbeat component. - if (Actor->IsA()) - { -#if !UE_BUILD_SHIPPING - ComponentWriteAcl.Add(SpatialConstants::DEBUG_METRICS_COMPONENT_ID, AuthoritativeWorkerRequirementSet); -#endif // !UE_BUILD_SHIPPING - ComponentWriteAcl.Add(SpatialConstants::HEARTBEAT_COMPONENT_ID, OwningClientOnlyRequirementSet); - } - - ComponentWriteAcl.Add(SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID, AuthoritativeWorkerRequirementSet); - - ForAllSchemaComponentTypes([&](ESchemaComponentType Type) - { - Worker_ComponentId ComponentId = Info.SchemaComponents[Type]; - if (ComponentId == SpatialConstants::INVALID_COMPONENT_ID) - { - return; - } - - ComponentWriteAcl.Add(ComponentId, AuthoritativeWorkerRequirementSet); - }); - - for (auto& SubobjectInfoPair : Info.SubobjectInfo) - { - const FClassInfo& SubobjectInfo = SubobjectInfoPair.Value.Get(); - - // Static subobjects aren't guaranteed to exist on actor instances, check they are present before adding write acls - TWeakObjectPtr Subobject = PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(Channel->GetEntityId(), SubobjectInfoPair.Key)); - if (!Subobject.IsValid()) - { - continue; - } - - ForAllSchemaComponentTypes([&](ESchemaComponentType Type) - { - Worker_ComponentId ComponentId = SubobjectInfo.SchemaComponents[Type]; - if (ComponentId == SpatialConstants::INVALID_COMPONENT_ID) - { - return; - } - - ComponentWriteAcl.Add(ComponentId, AuthoritativeWorkerRequirementSet); - }); - } - - // We want to have a stably named ref if this is a loaded Actor. - // We use this to indicate if a new Actor should be created or to link a pre-existing Actor when receiving an AddEntityOp. - // Previously, IsFullNameStableForNetworking was used but this was only true if bNetLoadOnClient was true. - TSchemaOption StablyNamedObjectRef; - TSchemaOption bNetStartup; - if (Actor->HasAnyFlags(RF_WasLoaded) || Actor->bNetStartup) - { - // Since we've already received the EntityId for this Actor. It is guaranteed to be resolved - // with the package map by this point - FUnrealObjectRef OuterObjectRef = PackageMap->GetUnrealObjectRefFromObject(Actor->GetOuter()); - if (OuterObjectRef == FUnrealObjectRef::UNRESOLVED_OBJECT_REF) - { - FNetworkGUID NetGUID = PackageMap->ResolveStablyNamedObject(Actor->GetOuter()); - OuterObjectRef = PackageMap->GetUnrealObjectRefFromNetGUID(NetGUID); - } - - // No path in SpatialOS should contain a PIE prefix. - FString TempPath = Actor->GetFName().ToString(); - GEngine->NetworkRemapPath(NetDriver, TempPath, false /*bIsReading*/); - - StablyNamedObjectRef = FUnrealObjectRef(0, 0, TempPath, OuterObjectRef, true); - bNetStartup = Actor->bNetStartup; - } - - TArray ComponentDatas; - ComponentDatas.Add(Position(Coordinates::FromFVector(GetActorSpatialPosition(Actor))).CreatePositionData()); - ComponentDatas.Add(Metadata(Class->GetName()).CreateMetadataData()); - ComponentDatas.Add(SpawnData(Actor).CreateSpawnDataData()); - ComponentDatas.Add(UnrealMetadata(StablyNamedObjectRef, ClientWorkerAttribute, Class->GetPathName(), bNetStartup).CreateUnrealMetadataData()); - // TODO(zoning): For now, setting AuthorityIntent to an invalid value. - ComponentDatas.Add(AuthorityIntent(SpatialConstants::INVALID_VIRTUAL_WORKER_ID).CreateAuthorityIntentData()); - - if (!Class->HasAnySpatialClassFlags(SPATIALCLASS_NotPersistent)) - { - ComponentDatas.Add(Persistence().CreatePersistenceData()); - } - - if (RPCsOnEntityCreation* QueuedRPCs = OutgoingOnCreateEntityRPCs.Find(Actor)) - { - if (QueuedRPCs->HasRPCPayloadData()) - { - ComponentDatas.Add(QueuedRPCs->CreateRPCPayloadData()); - } - OutgoingOnCreateEntityRPCs.Remove(Actor); - } - - if (Class->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)) - { - ComponentDatas.Add(Singleton().CreateSingletonData()); - } - - if (Actor->bAlwaysRelevant) - { - ComponentDatas.Add(AlwaysRelevant().CreateData()); - } - - if (Actor->NetDormancy >= DORM_DormantAll) - { - ComponentDatas.Add(Dormant().CreateData()); - } + EntityFactory DataFactory(NetDriver, PackageMap, ClassInfoManager, ActorGroupManager, RPCService); + TArray ComponentDatas = DataFactory.CreateEntityComponents(Channel, OutgoingOnCreateEntityRPCs, OutBytesWritten); // If the Actor was loaded rather than dynamically spawned, associate it with its owning sublevel. - ComponentDatas.Add(CreateLevelComponentData(Actor)); - - if (Actor->IsA()) - { -#if !UE_BUILD_SHIPPING - ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::DEBUG_METRICS_COMPONENT_ID)); -#endif // !UE_BUILD_SHIPPING - ComponentDatas.Add(Heartbeat().CreateHeartbeatData()); - } + ComponentDatas.Add(CreateLevelComponentData(Channel->Actor)); - ComponentFactory DataFactory(false, NetDriver); - - FRepChangeState InitialRepChanges = Channel->CreateInitialRepChangeState(Actor); - FHandoverChangeState InitialHandoverChanges = Channel->CreateInitialHandoverChangeState(Info); - - TArray DynamicComponentDatas = DataFactory.CreateComponentDatas(Actor, Info, InitialRepChanges, InitialHandoverChanges); - ComponentDatas.Append(DynamicComponentDatas); - - InterestFactory InterestDataFactory(Actor, Info, NetDriver->ClassInfoManager, NetDriver->PackageMap); - ComponentDatas.Add(InterestDataFactory.CreateInterestData()); - - ComponentDatas.Add(ClientRPCEndpoint().CreateRPCEndpointData()); - ComponentDatas.Add(ServerRPCEndpoint().CreateRPCEndpointData()); - ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID)); - - // Only add subobjects which are replicating - for (auto RepSubobject = Channel->ReplicationMap.CreateIterator(); RepSubobject; ++RepSubobject) - { - if (UObject* Subobject = RepSubobject.Value()->GetWeakObjectPtr().Get()) - { - if (Subobject == Actor) - { - // Actor's replicator is also contained in ReplicationMap. - continue; - } - - // If this object is not in the PackageMap, it has been dynamically created. - if (!PackageMap->GetUnrealObjectRefFromObject(Subobject).IsValid()) - { - const FClassInfo* SubobjectInfo = PackageMap->TryResolveNewDynamicSubobjectAndGetClassInfo(Subobject); - - if (SubobjectInfo == nullptr) - { - // This is a failure but there is already a log inside TryResolveNewDynamicSubbojectAndGetClassInfo - continue; - } - } - - const FClassInfo& SubobjectInfo = ClassInfoManager->GetOrCreateClassInfoByObject(Subobject); - - FRepChangeState SubobjectRepChanges = Channel->CreateInitialRepChangeState(Subobject); - FHandoverChangeState SubobjectHandoverChanges = Channel->CreateInitialHandoverChangeState(SubobjectInfo); - - ForAllSchemaComponentTypes([&](ESchemaComponentType Type) - { - if (SubobjectInfo.SchemaComponents[Type] != SpatialConstants::INVALID_COMPONENT_ID) - { - ComponentWriteAcl.Add(SubobjectInfo.SchemaComponents[Type], AuthoritativeWorkerRequirementSet); - } - }); - - TArray ActorSubobjectDatas = DataFactory.CreateComponentDatas(Subobject, SubobjectInfo, SubobjectRepChanges, SubobjectHandoverChanges); - ComponentDatas.Append(ActorSubobjectDatas); - } - } - - // Or if the subobject has handover properties, add it as well. - // NOTE: this is only for subobjects that are a part of the CDO. - // NOT dynamic subobjects which have been added before entity creation. - for (auto& SubobjectInfoPair : Info.SubobjectInfo) - { - const FClassInfo& SubobjectInfo = SubobjectInfoPair.Value.Get(); - - // Static subobjects aren't guaranteed to exist on actor instances, check they are present before adding write acls - TWeakObjectPtr WeakSubobject = PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(Channel->GetEntityId(), SubobjectInfoPair.Key)); - if (!WeakSubobject.IsValid()) - { - continue; - } - - UObject* Subobject = WeakSubobject.Get(); - - if (SubobjectInfo.SchemaComponents[SCHEMA_Handover] == SpatialConstants::INVALID_COMPONENT_ID) - { - continue; - } - - // If it contains it, we've already created handover data for it. - if (Channel->ReplicationMap.Contains(Subobject)) - { - continue; - } - - FHandoverChangeState SubobjectHandoverChanges = Channel->CreateInitialHandoverChangeState(SubobjectInfo); - - Worker_ComponentData SubobjectHandoverData = DataFactory.CreateHandoverComponentData(SubobjectInfo.SchemaComponents[SCHEMA_Handover], Subobject, SubobjectInfo, SubobjectHandoverChanges); - ComponentDatas.Add(SubobjectHandoverData); - - ComponentWriteAcl.Add(SubobjectInfo.SchemaComponents[SCHEMA_Handover], AuthoritativeWorkerRequirementSet); - } - - ComponentDatas.Add(EntityAcl(ReadAcl, ComponentWriteAcl).CreateEntityAclData()); + ComponentDatas.Add(ComponentPresence(EntityFactory::GetComponentPresenceList(ComponentDatas)).CreateComponentPresenceData()); Worker_EntityId EntityId = Channel->GetEntityId(); Worker_RequestId CreateEntityRequestId = Connection->SendCreateEntityRequest(MoveTemp(ComponentDatas), &EntityId); @@ -375,36 +110,50 @@ Worker_ComponentData USpatialSender::CreateLevelComponentData(AActor* Actor) return ComponentFactory::CreateEmptyComponentData(SpatialConstants::NOT_STREAMED_COMPONENT_ID); } -void USpatialSender::SendAddComponent(USpatialActorChannel* Channel, UObject* Subobject, const FClassInfo& SubobjectInfo) +void USpatialSender::SendAddComponentForSubobject(USpatialActorChannel* Channel, UObject* Subobject, const FClassInfo& SubobjectInfo, uint32& OutBytesWritten) { FRepChangeState SubobjectRepChanges = Channel->CreateInitialRepChangeState(Subobject); FHandoverChangeState SubobjectHandoverChanges = Channel->CreateInitialHandoverChangeState(SubobjectInfo); - ComponentFactory DataFactory(false, NetDriver); + ComponentFactory DataFactory(false, NetDriver, USpatialLatencyTracer::GetTracer(Subobject)); + + TArray SubobjectDatas = DataFactory.CreateComponentDatas(Subobject, SubobjectInfo, SubobjectRepChanges, SubobjectHandoverChanges, OutBytesWritten); + SendAddComponents(Channel->GetEntityId(), SubobjectDatas); - TArray SubobjectDatas = DataFactory.CreateComponentDatas(Subobject, SubobjectInfo, SubobjectRepChanges, SubobjectHandoverChanges); + Channel->PendingDynamicSubobjects.Remove(TWeakObjectPtr(Subobject)); +} - for (Worker_ComponentData& ComponentData : SubobjectDatas) +void USpatialSender::SendAddComponents(Worker_EntityId EntityId, TArray ComponentDatas) +{ + if (ComponentDatas.Num() == 0) { - Connection->SendAddComponent(Channel->GetEntityId(), &ComponentData); + return; } - Channel->PendingDynamicSubobjects.Remove(TWeakObjectPtr(Subobject)); + // Update ComponentPresence. + check(StaticComponentView->HasAuthority(EntityId, SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID)); + ComponentPresence* Presence = StaticComponentView->GetComponentData(EntityId); + Presence->AddComponentDataIds(ComponentDatas); + FWorkerComponentUpdate Update = Presence->CreateComponentPresenceUpdate(); + Connection->SendComponentUpdate(EntityId, &Update); + + for (FWorkerComponentData& ComponentData : ComponentDatas) + { + Connection->SendAddComponent(EntityId, &ComponentData); + } } void USpatialSender::GainAuthorityThenAddComponent(USpatialActorChannel* Channel, UObject* Object, const FClassInfo* Info) { - const FClassInfo& ActorInfo = ClassInfoManager->GetOrCreateClassInfoByClass(Channel->Actor->GetClass()); - const WorkerAttributeSet WorkerAttribute{ ActorInfo.WorkerType.ToString() }; - const WorkerRequirementSet AuthoritativeWorkerRequirementSet = { WorkerAttribute }; - - EntityAcl* EntityACL = StaticComponentView->GetComponentData(Channel->GetEntityId()); + Worker_EntityId EntityId = Channel->GetEntityId(); TSharedRef PendingSubobjectAttachment = MakeShared(); PendingSubobjectAttachment->Subobject = Object; PendingSubobjectAttachment->Channel = Channel; PendingSubobjectAttachment->Info = Info; + // We collect component IDs related to the dynamic subobject being added to gain authority over. + TArray NewComponentIds; ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { Worker_ComponentId ComponentId = Info->SchemaComponents[Type]; @@ -414,30 +163,70 @@ void USpatialSender::GainAuthorityThenAddComponent(USpatialActorChannel* Channel // adding the subobject. PendingSubobjectAttachment->PendingAuthorityDelegations.Add(ComponentId); Receiver->PendingEntitySubobjectDelegations.Add( - MakeTuple(static_cast(Channel->GetEntityId()), ComponentId), + MakeTuple(static_cast(EntityId), ComponentId), PendingSubobjectAttachment); - EntityACL->ComponentWriteAcl.Add(Info->SchemaComponents[Type], AuthoritativeWorkerRequirementSet); + NewComponentIds.Add(ComponentId); } }); - Worker_ComponentUpdate Update = EntityACL->CreateEntityAclUpdate(); + // If this worker is EntityACL authoritative, we can directly update the component IDs to gain authority over. + if (StaticComponentView->HasAuthority(EntityId, SpatialConstants::ENTITY_ACL_COMPONENT_ID)) + { + const FClassInfo& ActorInfo = ClassInfoManager->GetOrCreateClassInfoByClass(Channel->Actor->GetClass()); + const WorkerAttributeSet WorkerAttribute = { ActorInfo.WorkerType.ToString() }; + const WorkerRequirementSet AuthoritativeWorkerRequirementSet = { WorkerAttribute }; + + EntityAcl* EntityACL = StaticComponentView->GetComponentData(Channel->GetEntityId()); + for (auto& ComponentId : NewComponentIds) + { + EntityACL->ComponentWriteAcl.Add(ComponentId, AuthoritativeWorkerRequirementSet); + } + + FWorkerComponentUpdate Update = EntityACL->CreateEntityAclUpdate(); + Connection->SendComponentUpdate(Channel->GetEntityId(), &Update); + } + + // Update the ComponentPresence component with the new component IDs. If this worker does not have EntityACL + // authority, this component is used to inform the enforcer of the component IDs to add to the EntityACL. + check(StaticComponentView->HasAuthority(EntityId, SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID)); + ComponentPresence* ComponentPresenceData = StaticComponentView->GetComponentData(EntityId); + ComponentPresenceData->AddComponentIds(NewComponentIds); + FWorkerComponentUpdate Update = ComponentPresenceData->CreateComponentPresenceUpdate(); Connection->SendComponentUpdate(Channel->GetEntityId(), &Update); } -void USpatialSender::SendRemoveComponent(Worker_EntityId EntityId, const FClassInfo& Info) +void USpatialSender::SendRemoveComponentForClassInfo(Worker_EntityId EntityId, const FClassInfo& Info) { + TArray ComponentsToRemove; + ComponentsToRemove.Reserve(SCHEMA_Count); for (Worker_ComponentId SubobjectComponentId : Info.SchemaComponents) { if (SubobjectComponentId != SpatialConstants::INVALID_COMPONENT_ID) { - NetDriver->Connection->SendRemoveComponent(EntityId, SubobjectComponentId); + ComponentsToRemove.Add(SubobjectComponentId); } } + SendRemoveComponents(EntityId, ComponentsToRemove); + PackageMap->RemoveSubobject(FUnrealObjectRef(EntityId, Info.SchemaComponents[SCHEMA_Data])); } +void USpatialSender::SendRemoveComponents(Worker_EntityId EntityId, TArray ComponentIds) +{ + check(StaticComponentView->HasAuthority(EntityId, SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID)); + ComponentPresence* ComponentPresenceData = StaticComponentView->GetComponentData(EntityId); + ComponentPresenceData->RemoveComponentIds(ComponentIds); + FWorkerComponentUpdate Update = ComponentPresenceData->CreateComponentPresenceUpdate(); + Connection->SendComponentUpdate(EntityId, &Update); + + for (auto ComponentId : ComponentIds) + { + Connection->SendRemoveComponent(EntityId, ComponentId); + } +} + // Creates an entity authoritative on this server worker, ensuring it will be able to receive updates for the GSM. void USpatialSender::CreateServerWorkerEntity(int AttemptCounter) { @@ -448,14 +237,21 @@ void USpatialSender::CreateServerWorkerEntity(int AttemptCounter) ComponentWriteAcl.Add(SpatialConstants::METADATA_COMPONENT_ID, WorkerIdPermission); ComponentWriteAcl.Add(SpatialConstants::ENTITY_ACL_COMPONENT_ID, WorkerIdPermission); ComponentWriteAcl.Add(SpatialConstants::INTEREST_COMPONENT_ID, WorkerIdPermission); + ComponentWriteAcl.Add(SpatialConstants::SERVER_WORKER_COMPONENT_ID, WorkerIdPermission); + ComponentWriteAcl.Add(SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID, WorkerIdPermission); - TArray Components; + TArray Components; Components.Add(Position().CreatePositionData()); Components.Add(Metadata(FString::Format(TEXT("WorkerEntity:{0}"), { Connection->GetWorkerId() })).CreateMetadataData()); Components.Add(EntityAcl(WorkerIdPermission, ComponentWriteAcl).CreateEntityAclData()); - Components.Add(InterestFactory::CreateServerWorkerInterest().CreateInterestData()); + Components.Add(ServerWorker(Connection->GetWorkerId(), false).CreateServerWorkerData()); + check(NetDriver != nullptr); + // It is unlikely the load balance strategy would be set up at this point, but we call this function again later when it is ready in order + // to set the interest of the server worker according to the strategy. + Components.Add(NetDriver->InterestFactory->CreateServerWorkerInterest(NetDriver->LoadBalanceStrategy).CreateInterestData()); + Components.Add(ComponentPresence(EntityFactory::GetComponentPresenceList(Components)).CreateComponentPresenceData()); - Worker_RequestId RequestId = Connection->SendCreateEntityRequest(MoveTemp(Components), nullptr); + const Worker_RequestId RequestId = Connection->SendCreateEntityRequest(MoveTemp(Components), nullptr); CreateEntityDelegate OnCreateWorkerEntityResponse; OnCreateWorkerEntityResponse.BindLambda([WeakSender = TWeakObjectPtr(this), AttemptCounter](const Worker_CreateEntityResponseOp& Op) @@ -469,6 +265,7 @@ void USpatialSender::CreateServerWorkerEntity(int AttemptCounter) if (Op.status_code == WORKER_STATUS_CODE_SUCCESS) { Sender->NetDriver->WorkerEntityId = Op.entity_id; + Sender->NetDriver->GlobalStateManager->TrySendWorkerReadyToBeginPlay(); return; } @@ -490,14 +287,19 @@ void USpatialSender::CreateServerWorkerEntity(int AttemptCounter) FTimerHandle RetryTimer; Sender->TimerManager->SetTimer(RetryTimer, [WeakSender, AttemptCounter]() { - if (USpatialSender* Sender = WeakSender.Get()) + if (USpatialSender* SpatialSender = WeakSender.Get()) { - Sender->CreateServerWorkerEntity(AttemptCounter + 1); + SpatialSender->CreateServerWorkerEntity(AttemptCounter + 1); } }, SpatialConstants::GetCommandRetryWaitTimeSeconds(AttemptCounter), false); }); - Receiver->AddCreateEntityDelegate(RequestId, OnCreateWorkerEntityResponse); + Receiver->AddCreateEntityDelegate(RequestId, MoveTemp(OnCreateWorkerEntityResponse)); +} + +void USpatialSender::ClearPendingRPCs(const Worker_EntityId EntityId) +{ + OutgoingRPCs.DropForEntity(EntityId); } bool USpatialSender::ValidateOrExit_IsSupportedClass(const FString& PathName) @@ -509,19 +311,100 @@ bool USpatialSender::ValidateOrExit_IsSupportedClass(const FString& PathName) return ClassInfoManager->ValidateOrExit_IsSupportedClass(RemappedPathName); } -void USpatialSender::SendComponentUpdates(UObject* Object, const FClassInfo& Info, USpatialActorChannel* Channel, const FRepChangeState* RepChanges, const FHandoverChangeState* HandoverChanges) +void USpatialSender::DeleteEntityComponentData(TArray& EntityComponents) +{ + for (FWorkerComponentData& Component : EntityComponents) + { + Schema_DestroyComponentData(Component.schema_type); + } + + EntityComponents.Empty(); +} + +TArray USpatialSender::CopyEntityComponentData(const TArray& EntityComponents) +{ + TArray Copy; + Copy.Reserve(EntityComponents.Num()); + for (const FWorkerComponentData& Component : EntityComponents) + { + Copy.Emplace(Worker_ComponentData{ + Component.reserved, + Component.component_id, + Schema_CopyComponentData(Component.schema_type), + nullptr + }); + } + + return Copy; +} + +void USpatialSender::CreateEntityWithRetries(Worker_EntityId EntityId, FString EntityName, TArray EntityComponents) +{ + const Worker_RequestId RequestId = Connection->SendCreateEntityRequest(CopyEntityComponentData(EntityComponents), &EntityId); + + CreateEntityDelegate Delegate; + + Delegate.BindLambda([this, EntityId, Name = MoveTemp(EntityName), Components = MoveTemp(EntityComponents)](const Worker_CreateEntityResponseOp& Op) mutable + { + switch (Op.status_code) + { + case WORKER_STATUS_CODE_SUCCESS: + UE_LOG(LogSpatialSender, Log, TEXT("Created entity. " + "Entity name: %s, entity id: %lld"), *Name, EntityId); + DeleteEntityComponentData(Components); + break; + case WORKER_STATUS_CODE_TIMEOUT: + UE_LOG(LogSpatialSender, Log, TEXT("Timed out creating entity. Retrying. " + "Entity name: %s, entity id: %lld"), *Name, EntityId); + CreateEntityWithRetries(EntityId, MoveTemp(Name), MoveTemp(Components)); + break; + default: + UE_LOG(LogSpatialSender, Log, TEXT("Failed to create entity. It might already be created. Not retrying. " + "Entity name: %s, entity id: %lld"), *Name, EntityId); + DeleteEntityComponentData(Components); + break; + } + }); + + Receiver->AddCreateEntityDelegate(RequestId, MoveTemp(Delegate)); +} + +void USpatialSender::UpdateServerWorkerEntityInterestAndPosition() +{ + check(Connection != nullptr); + check(NetDriver != nullptr); + if (NetDriver->WorkerEntityId == SpatialConstants::INVALID_ENTITY_ID) + { + // No worker entity to update. + return; + } + + // Update the interest. If it's ready and not null, also adds interest according to the load balancing strategy. + FWorkerComponentUpdate InterestUpdate = NetDriver->InterestFactory->CreateServerWorkerInterest(NetDriver->LoadBalanceStrategy).CreateInterestUpdate(); + Connection->SendComponentUpdate(NetDriver->WorkerEntityId, &InterestUpdate); + + if (NetDriver->LoadBalanceStrategy != nullptr && NetDriver->LoadBalanceStrategy->IsReady()) + { + // Also update the position of the worker entity to the centre of the load balancing region. + SendPositionUpdate(NetDriver->WorkerEntityId, NetDriver->LoadBalanceStrategy->GetWorkerEntityPosition()); + } +} + +void USpatialSender::SendComponentUpdates(UObject* Object, const FClassInfo& Info, USpatialActorChannel* Channel, const FRepChangeState* RepChanges, const FHandoverChangeState* HandoverChanges, uint32& OutBytesWritten) { SCOPE_CYCLE_COUNTER(STAT_SpatialSenderSendComponentUpdates); Worker_EntityId EntityId = Channel->GetEntityId(); UE_LOG(LogSpatialSender, Verbose, TEXT("Sending component update (object: %s, entity: %lld)"), *Object->GetName(), EntityId); - ComponentFactory UpdateFactory(Channel->GetInterestDirty(), NetDriver); + USpatialLatencyTracer* Tracer = USpatialLatencyTracer::GetTracer(Object); + ComponentFactory UpdateFactory(Channel->GetInterestDirty(), NetDriver, Tracer); - TArray ComponentUpdates = UpdateFactory.CreateComponentUpdates(Object, Info, EntityId, RepChanges, HandoverChanges); + TArray ComponentUpdates = UpdateFactory.CreateComponentUpdates(Object, Info, EntityId, RepChanges, HandoverChanges, OutBytesWritten); - for (Worker_ComponentUpdate& Update : ComponentUpdates) + for(int i = 0; i < ComponentUpdates.Num(); i++) { + FWorkerComponentUpdate& Update = ComponentUpdates[i]; if (!NetDriver->StaticComponentView->HasAuthority(EntityId, Update.component_id)) { UE_LOG(LogSpatialSender, Verbose, TEXT("Trying to send component update but don't have authority! Update will be queued and sent when authority gained. Component Id: %d, entity: %lld"), Update.component_id, EntityId); @@ -529,7 +412,7 @@ void USpatialSender::SendComponentUpdates(UObject* Object, const FClassInfo& Inf // This is a temporary fix. A task to improve this has been created: UNR-955 // It may be the case that upon resolving a component, we do not have authority to send the update. In this case, we queue the update, to send upon receiving authority. // Note: This will break in a multi-worker context, if we try to create an entity that we don't intend to have authority over. For this reason, this fix is only temporary. - TArray& UpdatesQueuedUntilAuthority = UpdatesQueuedUntilAuthorityMap.FindOrAdd(EntityId); + TArray& UpdatesQueuedUntilAuthority = UpdatesQueuedUntilAuthorityMap.FindOrAdd(EntityId); UpdatesQueuedUntilAuthority.Add(Update); continue; } @@ -541,7 +424,7 @@ void USpatialSender::SendComponentUpdates(UObject* Object, const FClassInfo& Inf // Apply (and clean up) any updates queued, due to being sent previously when they didn't have authority. void USpatialSender::ProcessUpdatesQueuedUntilAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId) { - if (TArray* UpdatesQueuedUntilAuthority = UpdatesQueuedUntilAuthorityMap.Find(EntityId)) + if (TArray* UpdatesQueuedUntilAuthority = UpdatesQueuedUntilAuthorityMap.Find(EntityId)) { for (auto It = UpdatesQueuedUntilAuthority->CreateIterator(); It; It++) { @@ -559,41 +442,17 @@ void USpatialSender::ProcessUpdatesQueuedUntilAuthority(Worker_EntityId EntityId } } -void USpatialSender::FlushPackedRPCs() +void USpatialSender::FlushRPCService() { - if (RPCsToPack.Num() == 0) + if (RPCService != nullptr) { - return; - } - - // TODO: This could be further optimized for the case when there's only 1 RPC to be sent during this frame - // by sending it directly to the corresponding entity, without including the EntityId in the payload - UNR-1563. - for (const auto& It : RPCsToPack) - { - Worker_EntityId PlayerControllerEntityId = It.Key; - const TArray& PendingRPCArray = It.Value; + RPCService->PushOverflowedRPCs(); - Worker_ComponentUpdate ComponentUpdate = {}; - - Worker_ComponentId ComponentId = NetDriver->IsServer() ? SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID : SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID; - ComponentUpdate.component_id = ComponentId; - ComponentUpdate.schema_type = Schema_CreateComponentUpdate(); - Schema_Object* EventsObject = Schema_GetComponentUpdateEvents(ComponentUpdate.schema_type); - - for (const FPendingRPC& RPC : PendingRPCArray) + for (const SpatialRPCService::UpdateToSend& Update : RPCService->GetRPCsAndAcksToSend()) { - Schema_Object* EventData = Schema_AddObject(EventsObject, SpatialConstants::UNREAL_RPC_ENDPOINT_PACKED_EVENT_ID); - - Schema_AddUint32(EventData, SpatialConstants::UNREAL_RPC_PAYLOAD_OFFSET_ID, RPC.Offset); - Schema_AddUint32(EventData, SpatialConstants::UNREAL_RPC_PAYLOAD_RPC_INDEX_ID, RPC.Index); - SpatialGDK::AddBytesToSchema(EventData, SpatialConstants::UNREAL_RPC_PAYLOAD_RPC_PAYLOAD_ID, RPC.Data.GetData(), RPC.Data.Num()); - Schema_AddEntityId(EventData, SpatialConstants::UNREAL_PACKED_RPC_PAYLOAD_ENTITY_ID, RPC.Entity); + Connection->SendComponentUpdate(Update.EntityId, &Update.Update); } - - Connection->SendComponentUpdate(PlayerControllerEntityId, &ComponentUpdate); } - - RPCsToPack.Empty(); } void FillComponentInterests(const FClassInfo& Info, bool bNetOwned, TArray& ComponentInterest) @@ -632,19 +491,31 @@ TArray USpatialSender::CreateComponentInterestForActor( FillComponentInterests(SubobjectInfo, bIsNetOwned, ComponentInterest); } - ComponentInterest.Add({ SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID, bIsNetOwned }); - ComponentInterest.Add({ SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID, bIsNetOwned }); + if (GetDefault()->UseRPCRingBuffer()) + { + ComponentInterest.Add({ SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID, bIsNetOwned }); + ComponentInterest.Add({ SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID, bIsNetOwned }); + } + else + { + ComponentInterest.Add({ SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY, bIsNetOwned }); + ComponentInterest.Add({ SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY, bIsNetOwned }); + } return ComponentInterest; } -RPCPayload USpatialSender::CreateRPCPayloadFromParams(UObject* TargetObject, const FUnrealObjectRef& TargetObjectRef, UFunction* Function, int ReliableRPCIndex, void* Params) +RPCPayload USpatialSender::CreateRPCPayloadFromParams(UObject* TargetObject, const FUnrealObjectRef& TargetObjectRef, UFunction* Function, void* Params) { const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); - FSpatialNetBitWriter PayloadWriter = PackRPCDataToSpatialNetBitWriter(Function, Params, ReliableRPCIndex); + FSpatialNetBitWriter PayloadWriter = PackRPCDataToSpatialNetBitWriter(Function, Params); +#if TRACE_LIB_ACTIVE + return RPCPayload(TargetObjectRef.Offset, RPCInfo.Index, TArray(PayloadWriter.GetData(), PayloadWriter.GetNumBytes()), USpatialLatencyTracer::GetTracer(TargetObject)->RetrievePendingTrace(TargetObject, Function)); +#else return RPCPayload(TargetObjectRef.Offset, RPCInfo.Index, TArray(PayloadWriter.GetData(), PayloadWriter.GetNumBytes())); +#endif } void USpatialSender::SendComponentInterestForActor(USpatialActorChannel* Channel, Worker_EntityId EntityId, bool bNetOwned) @@ -654,6 +525,33 @@ void USpatialSender::SendComponentInterestForActor(USpatialActorChannel* Channel NetDriver->Connection->SendComponentInterest(EntityId, CreateComponentInterestForActor(Channel, bNetOwned)); } +void USpatialSender::SendInterestBucketComponentChange(const Worker_EntityId EntityId, const Worker_ComponentId OldComponent, const Worker_ComponentId NewComponent) +{ + if (OldComponent != SpatialConstants::INVALID_COMPONENT_ID) + { + // No loopback, so simulate the operations locally. + Worker_RemoveComponentOp RemoveOp{}; + RemoveOp.entity_id = EntityId; + RemoveOp.component_id = OldComponent; + StaticComponentView->OnRemoveComponent(RemoveOp); + + SendRemoveComponents(EntityId, { OldComponent }); + } + + if (NewComponent != SpatialConstants::INVALID_COMPONENT_ID) + { + Worker_AddComponentOp AddOp{}; + AddOp.entity_id = EntityId; + AddOp.data.component_id = NewComponent; + AddOp.data.schema_type = nullptr; + AddOp.data.user_handle = nullptr; + + StaticComponentView->OnAddComponent(AddOp); + + SendAddComponents(EntityId, { ComponentFactory::CreateEmptyComponentData(NewComponent) }); + } +} + void USpatialSender::SendComponentInterestForSubobject(const FClassInfo& Info, Worker_EntityId EntityId, bool bNetOwned) { checkf(!NetDriver->IsServer(), TEXT("Tried to set ComponentInterest on a server-worker. This should never happen!")); @@ -673,17 +571,88 @@ void USpatialSender::SendPositionUpdate(Worker_EntityId EntityId, const FVector& } #endif - Worker_ComponentUpdate Update = Position::CreatePositionUpdate(Coordinates::FromFVector(Location)); + FWorkerComponentUpdate Update = Position::CreatePositionUpdate(Coordinates::FromFVector(Location)); + Connection->SendComponentUpdate(EntityId, &Update); +} + +void USpatialSender::SendAuthorityIntentUpdate(const AActor& Actor, VirtualWorkerId NewAuthoritativeVirtualWorkerId) +{ + const Worker_EntityId EntityId = PackageMap->GetEntityIdFromObject(&Actor); + check(EntityId != SpatialConstants::INVALID_ENTITY_ID); + check(NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID)); + + AuthorityIntent* AuthorityIntentComponent = StaticComponentView->GetComponentData(EntityId); + check(AuthorityIntentComponent != nullptr); + + if (AuthorityIntentComponent->VirtualWorkerId == NewAuthoritativeVirtualWorkerId) + { + // There may be multiple intent updates triggered by a server worker before the Runtime + // notifies this worker that the authority has changed. Ignore the extra calls here. + return; + } + + AuthorityIntentComponent->VirtualWorkerId = NewAuthoritativeVirtualWorkerId; + UE_LOG(LogSpatialSender, Log, TEXT("(%s) Sending authority intent update for entity id %d. Virtual worker '%d' should become authoritative over %s"), + *NetDriver->Connection->GetWorkerId(), EntityId, NewAuthoritativeVirtualWorkerId, *GetNameSafe(&Actor)); + + // If the SpatialDebugger is enabled, also update the authority intent virtual worker ID and color. + if (NetDriver->SpatialDebugger != nullptr) + { + NetDriver->SpatialDebugger->ActorAuthorityIntentChanged(EntityId, NewAuthoritativeVirtualWorkerId); + } + + FWorkerComponentUpdate Update = AuthorityIntentComponent->CreateAuthorityIntentUpdate(); Connection->SendComponentUpdate(EntityId, &Update); + + // Also notify the enforcer directly on the worker that sends the component update, as the update will short circuit + NetDriver->LoadBalanceEnforcer->MaybeQueueAclAssignmentRequest(EntityId); +} + +void USpatialSender::SetAclWriteAuthority(const SpatialLoadBalanceEnforcer::AclWriteAuthorityRequest& Request) +{ + check(NetDriver); + check(StaticComponentView->HasComponent(Request.EntityId, SpatialConstants::ENTITY_ACL_COMPONENT_ID)); + + const FString& WriteWorkerId = FString::Printf(TEXT("workerId:%s"), *Request.OwningWorkerId); + + const WorkerAttributeSet OwningServerWorkerAttributeSet = { WriteWorkerId }; + + EntityAcl* NewAcl = StaticComponentView->GetComponentData(Request.EntityId); + NewAcl->ReadAcl = Request.ReadAcl; + + for (const Worker_ComponentId& ComponentId : Request.ComponentIds) + { + if (ComponentId == SpatialConstants::HEARTBEAT_COMPONENT_ID + || ComponentId == SpatialConstants::GetClientAuthorityComponent(GetDefault()->UseRPCRingBuffer())) + { + NewAcl->ComponentWriteAcl.Add(ComponentId, Request.ClientRequirementSet); + continue; + } + + if (ComponentId == SpatialConstants::ENTITY_ACL_COMPONENT_ID) + { + NewAcl->ComponentWriteAcl.Add(ComponentId, { SpatialConstants::GetLoadBalancerAttributeSet(GetDefault()->LoadBalancingWorkerType.WorkerTypeName) }); + continue; + } + + NewAcl->ComponentWriteAcl.Add(ComponentId, { OwningServerWorkerAttributeSet }); + } + + UE_LOG(LogSpatialLoadBalanceEnforcer, Verbose, TEXT("(%s) Setting Acl WriteAuth for entity %lld to %s"), *NetDriver->Connection->GetWorkerId(), Request.EntityId, *Request.OwningWorkerId); + + FWorkerComponentUpdate Update = NewAcl->CreateEntityAclUpdate(); + NetDriver->Connection->SendComponentUpdate(Request.EntityId, &Update); } FRPCErrorInfo USpatialSender::SendRPC(const FPendingRPCParams& Params) { + SCOPE_CYCLE_COUNTER(STAT_SpatialSenderSendRPC); + TWeakObjectPtr TargetObjectWeakPtr = PackageMap->GetObjectFromUnrealObjectRef(Params.ObjectRef); if (!TargetObjectWeakPtr.IsValid()) { // Target object was destroyed before the RPC could be (re)sent - return FRPCErrorInfo{ nullptr, nullptr, NetDriver->IsServer(), ERPCQueueType::Send, ERPCResult::UnresolvedTargetObject }; + return FRPCErrorInfo{ nullptr, nullptr, ERPCResult::UnresolvedTargetObject, true }; } UObject* TargetObject = TargetObjectWeakPtr.Get(); @@ -691,12 +660,72 @@ FRPCErrorInfo USpatialSender::SendRPC(const FPendingRPCParams& Params) UFunction* Function = ClassInfo.RPCs[Params.Payload.Index]; if (Function == nullptr) { - return FRPCErrorInfo{ TargetObject, nullptr, NetDriver->IsServer(), ERPCQueueType::Send, ERPCResult::MissingFunctionInfo }; + return FRPCErrorInfo{ TargetObject, nullptr, ERPCResult::MissingFunctionInfo, true }; + } + + const float TimeDiff = (FDateTime::Now() - Params.Timestamp).GetTotalSeconds(); + if (GetDefault()->QueuedOutgoingRPCWaitTime < TimeDiff) + { + return FRPCErrorInfo{ TargetObject, Function, ERPCResult::TimedOut, true }; + } + + if (AActor* TargetActor = Cast(TargetObject)) + { + if (TargetActor->IsPendingKillPending()) + { + return FRPCErrorInfo{ TargetObject, Function, ERPCResult::ActorPendingKill, true }; + } } ERPCResult Result = SendRPCInternal(TargetObject, Function, Params.Payload); - return FRPCErrorInfo{ TargetObject, Function, NetDriver->IsServer(), ERPCQueueType::Send, Result }; + if (Result == ERPCResult::NoAuthority) + { + if (AActor* TargetActor = Cast(TargetObject)) + { + bool bShouldDrop = !WillHaveAuthorityOverActor(TargetActor, Params.ObjectRef.Entity); + return FRPCErrorInfo{ TargetObject, Function, Result, bShouldDrop }; + } + } + + return FRPCErrorInfo{ TargetObject, Function, Result, false }; +} + +#if !UE_BUILD_SHIPPING +void USpatialSender::TrackRPC(AActor* Actor, UFunction* Function, const RPCPayload& Payload, const ERPCType RPCType) +{ + NETWORK_PROFILER(GNetworkProfiler.TrackSendRPC(Actor, Function, 0, Payload.CountDataBits(), 0, NetDriver->GetSpatialOSNetConnection())); + NetDriver->SpatialMetrics->TrackSentRPC(Function, RPCType, Payload.PayloadData.Num()); +} +#endif + +bool USpatialSender::WillHaveAuthorityOverActor(AActor* TargetActor, Worker_EntityId TargetEntity) +{ + bool WillHaveAuthorityOverActor = true; + + if (GetDefault()->bEnableOffloading) + { + if (!USpatialStatics::IsActorGroupOwnerForActor(TargetActor)) + { + WillHaveAuthorityOverActor = false; + } + } + + if (GetDefault()->bEnableUnrealLoadBalancer) + { + if (NetDriver->VirtualWorkerTranslator != nullptr) + { + if (const SpatialGDK::AuthorityIntent* AuthorityIntentComponent = StaticComponentView->GetComponentData(TargetEntity)) + { + if (AuthorityIntentComponent->VirtualWorkerId != NetDriver->VirtualWorkerTranslator->GetLocalVirtualWorkerId()) + { + WillHaveAuthorityOverActor = false; + } + } + } + } + + return WillHaveAuthorityOverActor; } ERPCResult USpatialSender::SendRPCInternal(UObject* TargetObject, UFunction* Function, const RPCPayload& Payload) @@ -709,16 +738,17 @@ ERPCResult USpatialSender::SendRPCInternal(UObject* TargetObject, UFunction* Fun return ERPCResult::NoActorChannel; } const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); - if (Channel->bCreatingNewEntity) + if (Channel->bCreatingNewEntity && !SpatialGDKSettings->UseRPCRingBuffer()) { if (Function->HasAnyFunctionFlags(FUNC_NetClient)) { check(NetDriver->IsServer()); - OutgoingOnCreateEntityRPCs.FindOrAdd(TargetObject).RPCs.Add(Payload); + OutgoingOnCreateEntityRPCs.FindOrAdd(Channel->Actor).RPCs.Add(Payload); #if !UE_BUILD_SHIPPING - NetDriver->SpatialMetrics->TrackSentRPC(Function, RPCInfo.Type, Payload.PayloadData.Num()); + TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type); #endif // !UE_BUILD_SHIPPING return ERPCResult::Success; } @@ -728,9 +758,9 @@ ERPCResult USpatialSender::SendRPCInternal(UObject* TargetObject, UFunction* Fun switch (RPCInfo.Type) { - case SCHEMA_CrossServerRPC: + case ERPCType::CrossServer: { - Worker_ComponentId ComponentId = SchemaComponentTypeToWorkerComponentId(RPCInfo.Type); + Worker_ComponentId ComponentId = SpatialConstants::SERVER_TO_SERVER_COMMAND_ENDPOINT_COMPONENT_ID; Worker_CommandRequest CommandRequest = CreateRPCCommandRequest(TargetObject, Payload, ComponentId, RPCInfo.Index, EntityId); @@ -738,7 +768,7 @@ ERPCResult USpatialSender::SendRPCInternal(UObject* TargetObject, UFunction* Fun Worker_RequestId RequestId = Connection->SendCommandRequest(EntityId, &CommandRequest, SpatialConstants::UNREAL_RPC_ENDPOINT_COMMAND_ID); #if !UE_BUILD_SHIPPING - NetDriver->SpatialMetrics->TrackSentRPC(Function, RPCInfo.Type, Payload.PayloadData.Num()); + TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type); #endif // !UE_BUILD_SHIPPING if (Function->HasAnyFunctionFlags(FUNC_NetReliable)) @@ -755,11 +785,11 @@ ERPCResult USpatialSender::SendRPCInternal(UObject* TargetObject, UFunction* Fun return ERPCResult::Success; } - case SCHEMA_NetMulticastRPC: - case SCHEMA_ClientReliableRPC: - case SCHEMA_ServerReliableRPC: - case SCHEMA_ClientUnreliableRPC: - case SCHEMA_ServerUnreliableRPC: + case ERPCType::NetMulticast: + case ERPCType::ClientReliable: + case ERPCType::ServerReliable: + case ERPCType::ClientUnreliable: + case ERPCType::ServerUnreliable: { FUnrealObjectRef TargetObjectRef = PackageMap->GetUnrealObjectRefFromObject(TargetObject); if (TargetObjectRef == FUnrealObjectRef::UNRESOLVED_OBJECT_REF) @@ -767,7 +797,43 @@ ERPCResult USpatialSender::SendRPCInternal(UObject* TargetObject, UFunction* Fun return ERPCResult::UnresolvedTargetObject; } - if (RPCInfo.Type != SCHEMA_NetMulticastRPC && !Channel->IsListening()) + if (SpatialGDKSettings->UseRPCRingBuffer() && RPCService != nullptr) + { + EPushRPCResult Result = RPCService->PushRPC(TargetObjectRef.Entity, RPCInfo.Type, Payload); + + if (Result == EPushRPCResult::Success) + { + FlushRPCService(); + } + +#if !UE_BUILD_SHIPPING + if (Result == EPushRPCResult::Success || Result == EPushRPCResult::QueueOverflowed) + { + TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type); + } +#endif // !UE_BUILD_SHIPPING + + switch (Result) + { + case EPushRPCResult::QueueOverflowed: + UE_LOG(LogSpatialSender, Log, TEXT("USpatialSender::SendRPCInternal: Ring buffer queue overflowed, queuing RPC locally. Actor: %s, entity: %lld, function: %s"), *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); + break; + case EPushRPCResult::DropOverflowed: + UE_LOG(LogSpatialSender, Log, TEXT("USpatialSender::SendRPCInternal: Ring buffer queue overflowed, dropping RPC. Actor: %s, entity: %lld, function: %s"), *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); + break; + case EPushRPCResult::HasAckAuthority: + UE_LOG(LogSpatialSender, Warning, TEXT("USpatialSender::SendRPCInternal: Worker has authority over ack component for RPC it is sending. RPC will not be sent. Actor: %s, entity: %lld, function: %s"), *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); + break; + case EPushRPCResult::NoRingBufferAuthority: + // TODO: Change engine logic that calls Client RPCs from non-auth servers and change this to error. UNR-2517 + UE_LOG(LogSpatialSender, Log, TEXT("USpatialSender::SendRPCInternal: Failed to send RPC because the worker does not have authority over ring buffer component. Actor: %s, entity: %lld, function: %s"), *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); + break; + } + + return ERPCResult::Success; + } + + if (RPCInfo.Type != ERPCType::NetMulticast && !Channel->IsListening()) { // If the Entity endpoint is not yet ready to receive RPCs - // treat the corresponding object as unresolved and queue RPC @@ -778,62 +844,20 @@ ERPCResult USpatialSender::SendRPCInternal(UObject* TargetObject, UFunction* Fun EntityId = TargetObjectRef.Entity; check(EntityId != SpatialConstants::INVALID_ENTITY_ID); - Worker_ComponentId ComponentId = SchemaComponentTypeToWorkerComponentId(RPCInfo.Type); + Worker_ComponentId ComponentId = SpatialConstants::RPCTypeToWorkerComponentIdLegacy(RPCInfo.Type); - bool bCanPackRPC = GetDefault()->bPackRPCs; - if (bCanPackRPC && RPCInfo.Type == SCHEMA_NetMulticastRPC) + if (!NetDriver->StaticComponentView->HasAuthority(EntityId, ComponentId)) { - bCanPackRPC = false; + return ERPCResult::NoAuthority; } - if (bCanPackRPC && GetDefault()->bEnableOffloading) - { - if (const AActor* TargetActor = Cast(PackageMap->GetObjectFromEntityId(TargetObjectRef.Entity).Get())) - { - if (const UNetConnection* OwningConnection = TargetActor->GetNetConnection()) - { - if (const AActor* ConnectionOwner = OwningConnection->OwningActor) - { - if (!ActorGroupManager->IsSameWorkerType(TargetActor, ConnectionOwner)) - { - UE_LOG(LogSpatialSender, Verbose, TEXT("RPC %s Cannot be packed as TargetActor (%s) and Connection Owner (%s) are on different worker types."), - *Function->GetName(), - *TargetActor->GetName(), - *ConnectionOwner->GetName() - ) - bCanPackRPC = false; - } - } - } - } - } + FWorkerComponentUpdate ComponentUpdate = CreateRPCEventUpdate(TargetObject, Payload, ComponentId, RPCInfo.Index); - if (bCanPackRPC) - { - ERPCResult Result = AddPendingRPC(TargetObject, Function, Payload, ComponentId, RPCInfo.Index); + Connection->SendComponentUpdate(EntityId, &ComponentUpdate); #if !UE_BUILD_SHIPPING - if (Result == ERPCResult::Success) - { - NetDriver->SpatialMetrics->TrackSentRPC(Function, RPCInfo.Type, Payload.PayloadData.Num()); - } + TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type); #endif // !UE_BUILD_SHIPPING - return Result; - } - else - { - if (!NetDriver->StaticComponentView->HasAuthority(EntityId, ComponentId)) - { - return ERPCResult::NoAuthority; - } - - Worker_ComponentUpdate ComponentUpdate = CreateRPCEventUpdate(TargetObject, Payload, ComponentId, RPCInfo.Index); - - Connection->SendComponentUpdate(EntityId, &ComponentUpdate); -#if !UE_BUILD_SHIPPING - NetDriver->SpatialMetrics->TrackSentRPC(Function, RPCInfo.Type, Payload.PayloadData.Num()); -#endif // !UE_BUILD_SHIPPING - return ERPCResult::Success; - } + return ERPCResult::Success; } default: checkNoEntry(); @@ -848,6 +872,8 @@ void USpatialSender::EnqueueRetryRPC(TSharedRef RetryRPC) void USpatialSender::FlushRetryRPCs() { + SCOPE_CYCLE_COUNTER(STAT_SpatialSenderFlushRetryRPCs); + // Retried RPCs are sorted by their index. RetryRPCs.Sort([](const TSharedRef& A, const TSharedRef& B) { return A->RetryIndex < B->RetryIndex; }); for (auto& RetryRPC : RetryRPCs) @@ -901,11 +927,12 @@ void USpatialSender::ProcessPositionUpdates() ChannelsToUpdatePosition.Empty(); } -void USpatialSender::SendCreateEntityRequest(USpatialActorChannel* Channel) +void USpatialSender::SendCreateEntityRequest(USpatialActorChannel* Channel, uint32& OutBytesWritten) { - UE_LOG(LogSpatialSender, Log, TEXT("Sending create entity request for %s with EntityId %lld"), *Channel->Actor->GetName(), Channel->GetEntityId()); + UE_LOG(LogSpatialSender, Log, TEXT("Sending create entity request for %s with EntityId %lld, HasAuthority: %d"), *Channel->Actor->GetName(), Channel->GetEntityId(), Channel->Actor->HasAuthority()); + + Worker_RequestId RequestId = CreateEntity(Channel, OutBytesWritten); - Worker_RequestId RequestId = CreateEntity(Channel); Receiver->AddPendingActorRequest(RequestId, Channel); } @@ -918,23 +945,23 @@ void USpatialSender::SendRequestToClearRPCsOnEntityCreation(Worker_EntityId Enti void USpatialSender::ClearRPCsOnEntityCreation(Worker_EntityId EntityId) { check(NetDriver->IsServer()); - Worker_ComponentUpdate Update = RPCsOnEntityCreation::CreateClearFieldsUpdate(); + FWorkerComponentUpdate Update = RPCsOnEntityCreation::CreateClearFieldsUpdate(); NetDriver->Connection->SendComponentUpdate(EntityId, &Update); } void USpatialSender::SendClientEndpointReadyUpdate(Worker_EntityId EntityId) { - ClientRPCEndpoint Endpoint; + ClientRPCEndpointLegacy Endpoint; Endpoint.bReady = true; - Worker_ComponentUpdate Update = Endpoint.CreateRPCEndpointUpdate(); + FWorkerComponentUpdate Update = Endpoint.CreateRPCEndpointUpdate(); NetDriver->Connection->SendComponentUpdate(EntityId, &Update); } void USpatialSender::SendServerEndpointReadyUpdate(Worker_EntityId EntityId) { - ServerRPCEndpoint Endpoint; + ServerRPCEndpointLegacy Endpoint; Endpoint.bReady = true; - Worker_ComponentUpdate Update = Endpoint.CreateRPCEndpointUpdate(); + FWorkerComponentUpdate Update = Endpoint.CreateRPCEndpointUpdate(); NetDriver->Connection->SendComponentUpdate(EntityId, &Update); } @@ -958,18 +985,10 @@ void USpatialSender::ProcessOrQueueOutgoingRPC(const FUnrealObjectRef& InTargetO OutgoingRPCs.ProcessRPCs(); } -FSpatialNetBitWriter USpatialSender::PackRPCDataToSpatialNetBitWriter(UFunction* Function, void* Parameters, int ReliableRPCId) const +FSpatialNetBitWriter USpatialSender::PackRPCDataToSpatialNetBitWriter(UFunction* Function, void* Parameters) const { FSpatialNetBitWriter PayloadWriter(PackageMap); - if (GetDefault()->bCheckRPCOrder) - { - if (Function->HasAnyFunctionFlags(FUNC_NetReliable) && !Function->HasAnyFunctionFlags(FUNC_NetMulticast)) - { - PayloadWriter << ReliableRPCId; - } - } - TSharedPtr RepLayout = NetDriver->GetFunctionRepLayout(Function); RepLayout_SendPropertiesForRPC(*RepLayout, PayloadWriter, Parameters); @@ -1007,9 +1026,9 @@ Worker_CommandRequest USpatialSender::CreateRetryRPCCommandRequest(const FReliab return CommandRequest; } -Worker_ComponentUpdate USpatialSender::CreateRPCEventUpdate(UObject* TargetObject, const RPCPayload& Payload, Worker_ComponentId ComponentId, Schema_FieldId EventIndex) +FWorkerComponentUpdate USpatialSender::CreateRPCEventUpdate(UObject* TargetObject, const RPCPayload& Payload, Worker_ComponentId ComponentId, Schema_FieldId EventIndex) { - Worker_ComponentUpdate ComponentUpdate = {}; + FWorkerComponentUpdate ComponentUpdate = {}; ComponentUpdate.component_id = ComponentId; ComponentUpdate.schema_type = Schema_CreateComponentUpdate(); @@ -1019,62 +1038,18 @@ Worker_ComponentUpdate USpatialSender::CreateRPCEventUpdate(UObject* TargetObjec FUnrealObjectRef TargetObjectRef(PackageMap->GetUnrealObjectRefFromNetGUID(PackageMap->GetNetGUIDFromObject(TargetObject))); ensure(TargetObjectRef != FUnrealObjectRef::UNRESOLVED_OBJECT_REF); - RPCPayload::WriteToSchemaObject(EventData, Payload.Offset, Payload.Index, Payload.PayloadData.GetData(), Payload.PayloadData.Num()); + Payload.WriteToSchemaObject(EventData); - return ComponentUpdate; -} -ERPCResult USpatialSender::AddPendingRPC(UObject* TargetObject, UFunction* Function, const RPCPayload& Payload, Worker_ComponentId ComponentId, Schema_FieldId RPCIndex) -{ - FUnrealObjectRef TargetObjectRef(PackageMap->GetUnrealObjectRefFromNetGUID(PackageMap->GetNetGUIDFromObject(TargetObject))); - ensure(TargetObjectRef != FUnrealObjectRef::UNRESOLVED_OBJECT_REF); - - AActor* TargetActor = Cast(PackageMap->GetObjectFromEntityId(TargetObjectRef.Entity).Get()); - check(TargetActor != nullptr); - UNetConnection* OwningConnection = TargetActor->GetNetConnection(); - if (OwningConnection == nullptr) - { - UE_LOG(LogSpatialSender, Warning, TEXT("AddPendingRPC: No connection for object %s (RPC %s, actor %s, entity %lld)"), - *TargetObject->GetName(), *Function->GetName(), *TargetActor->GetName(), TargetObjectRef.Entity); - return ERPCResult::NoNetConnection; - } - - APlayerController* Controller = Cast(OwningConnection->OwningActor); - if (Controller == nullptr) - { - UE_LOG(LogSpatialSender, Warning, TEXT("AddPendingRPC: Connection's owner is not a player controller for object %s (RPC %s, actor %s, entity %lld): connection owner %s"), - *TargetObject->GetName(), *Function->GetName(), *TargetActor->GetName(), TargetObjectRef.Entity, *OwningConnection->OwningActor->GetName()); - return ERPCResult::NoOwningController; - } - - USpatialActorChannel* ControllerChannel = NetDriver->GetOrCreateSpatialActorChannel(Controller); - if (ControllerChannel == nullptr) - { - return ERPCResult::NoControllerChannel; - } - - if (!ControllerChannel->IsListening()) - { - return ERPCResult::ControllerChannelNotListening; - } - - FUnrealObjectRef ControllerObjectRef = PackageMap->GetUnrealObjectRefFromObject(Controller); - ensure(ControllerObjectRef != FUnrealObjectRef::UNRESOLVED_OBJECT_REF); - - TSet> UnresolvedObjects; +#if TRACE_LIB_ACTIVE + ComponentUpdate.Trace = Payload.Trace; +#endif - FPendingRPC RPC; - RPC.Offset = TargetObjectRef.Offset; - RPC.Index = RPCIndex; - RPC.Data.SetNumUninitialized(Payload.PayloadData.Num()); - FMemory::Memcpy(RPC.Data.GetData(), Payload.PayloadData.GetData(), Payload.PayloadData.Num()); - RPC.Entity = TargetObjectRef.Entity; - RPCsToPack.FindOrAdd(ControllerObjectRef.Entity).Emplace(MoveTemp(RPC)); - return ERPCResult::Success; + return ComponentUpdate; } -void USpatialSender::SendCommandResponse(Worker_RequestId request_id, Worker_CommandResponse& Response) +void USpatialSender::SendCommandResponse(Worker_RequestId RequestId, Worker_CommandResponse& Response) { - Connection->SendCommandResponse(request_id, &Response); + Connection->SendCommandResponse(RequestId, &Response); } void USpatialSender::SendEmptyCommandResponse(Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, Worker_RequestId RequestId) @@ -1087,44 +1062,40 @@ void USpatialSender::SendEmptyCommandResponse(Worker_ComponentId ComponentId, Sc Connection->SendCommandResponse(RequestId, &Response); } -// Authority over the ClientRPC Schema component is dictated by the owning connection of a client. -// This function updates the authority of that component as the owning connection can change. -bool USpatialSender::UpdateEntityACLs(Worker_EntityId EntityId, const FString& OwnerWorkerAttribute) +void USpatialSender::SendCommandFailure(Worker_RequestId RequestId, const FString& Message) { - EntityAcl* EntityACL = StaticComponentView->GetComponentData(EntityId); - - if (EntityACL == nullptr) - { - return false; - } + Connection->SendCommandFailure(RequestId, Message); +} - if (!NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::ENTITY_ACL_COMPONENT_ID)) - { - UE_LOG(LogSpatialSender, Warning, TEXT("Trying to update EntityACL but don't have authority! Update will not be sent. Entity: %lld"), EntityId); - return false; - } +// Authority over the ClientRPC Schema component and the Heartbeat component are dictated by the owning connection of a client. +// This function updates the authority of that component as the owning connection can change. +void USpatialSender::UpdateClientAuthoritativeComponentAclEntries(Worker_EntityId EntityId, const FString& OwnerWorkerAttribute) +{ + check(StaticComponentView->HasAuthority(EntityId, SpatialConstants::ENTITY_ACL_COMPONENT_ID)); WorkerAttributeSet OwningClientAttribute = { OwnerWorkerAttribute }; WorkerRequirementSet OwningClientOnly = { OwningClientAttribute }; - EntityACL->ComponentWriteAcl.Add(SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID, OwningClientOnly); - Worker_ComponentUpdate Update = EntityACL->CreateEntityAclUpdate(); + EntityAcl* EntityACL = StaticComponentView->GetComponentData(EntityId); + EntityACL->ComponentWriteAcl.Add(SpatialConstants::GetClientAuthorityComponent(GetDefault()->UseRPCRingBuffer()), OwningClientOnly); + EntityACL->ComponentWriteAcl.Add(SpatialConstants::HEARTBEAT_COMPONENT_ID, OwningClientOnly); + FWorkerComponentUpdate Update = EntityACL->CreateEntityAclUpdate(); Connection->SendComponentUpdate(EntityId, &Update); - return true; } void USpatialSender::UpdateInterestComponent(AActor* Actor) { + SCOPE_CYCLE_COUNTER(STAT_SpatialSenderUpdateInterestComponent); + Worker_EntityId EntityId = PackageMap->GetEntityIdFromObject(Actor); if (EntityId == SpatialConstants::INVALID_ENTITY_ID) { - UE_LOG(LogSpatialSender, Verbose, TEXT("Attempted to update interest for non replicated actor: %s"), *Actor->GetName()); + UE_LOG(LogSpatialSender, Verbose, TEXT("Attempted to update interest for non replicated actor: %s"), *GetNameSafe(Actor)); return; } - InterestFactory InterestUpdateFactory(Actor, ClassInfoManager->GetOrCreateClassInfoByObject(Actor), NetDriver->ClassInfoManager, NetDriver->PackageMap); - Worker_ComponentUpdate Update = InterestUpdateFactory.CreateInterestUpdate(); + FWorkerComponentUpdate Update = NetDriver->InterestFactory->CreateInterestUpdate(Actor, ClassInfoManager->GetOrCreateClassInfoByObject(Actor), EntityId); Connection->SendComponentUpdate(EntityId, &Update); } @@ -1135,10 +1106,16 @@ void USpatialSender::RetireEntity(const Worker_EntityId EntityId) { if (Actor->IsNetStartupActor()) { - check(StaticComponentView->HasComponent(EntityId, SpatialConstants::TOMBSTONE_COMPONENT_ID) == false); - // In the case that this is a startup actor, we won't actually delete the entity in SpatialOS. Instead we'll Tombstone it. Receiver->RemoveActor(EntityId); - AddTombstoneToEntity(EntityId); + // In the case that this is a startup actor, we won't actually delete the entity in SpatialOS. Instead we'll Tombstone it. + if (!StaticComponentView->HasComponent(EntityId, SpatialConstants::TOMBSTONE_COMPONENT_ID)) + { + AddTombstoneToEntity(EntityId); + } + else + { + UE_LOG(LogSpatialSender, Verbose, TEXT("RetireEntity called on already retired entity: %lld (actor: %s)"), EntityId, *Actor->GetName()); + } } else { @@ -1151,6 +1128,29 @@ void USpatialSender::RetireEntity(const Worker_EntityId EntityId) } } +void USpatialSender::CreateTombstoneEntity(AActor* Actor) +{ + check(Actor->IsNetStartupActor()); + + const Worker_EntityId EntityId = NetDriver->PackageMap->AllocateEntityIdAndResolveActor(Actor); + + EntityFactory DataFactory(NetDriver, PackageMap, ClassInfoManager, ActorGroupManager, RPCService); + TArray Components = DataFactory.CreateTombstoneEntityComponents(Actor); + + Components.Add(CreateLevelComponentData(Actor)); + + Components.Add(ComponentPresence(EntityFactory::GetComponentPresenceList(Components)).CreateComponentPresenceData()); + + CreateEntityWithRetries(EntityId, Actor->GetName(), MoveTemp(Components)); + + UE_LOG(LogSpatialSender, Log, TEXT("Creating tombstone entity for actor. " + "Actor: %s. Entity ID: %d."), *Actor->GetName(), EntityId); + +#if WITH_EDITOR + NetDriver->TrackTombstone(EntityId); +#endif +} + void USpatialSender::AddTombstoneToEntity(const Worker_EntityId EntityId) { check(NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::TOMBSTONE_COMPONENT_ID)); @@ -1158,7 +1158,7 @@ void USpatialSender::AddTombstoneToEntity(const Worker_EntityId EntityId) Worker_AddComponentOp AddComponentOp{}; AddComponentOp.entity_id = EntityId; AddComponentOp.data = Tombstone().CreateData(); - Connection->SendAddComponent(EntityId, &AddComponentOp.data); + SendAddComponents(EntityId, { AddComponentOp.data }); StaticComponentView->OnAddComponent(AddComponentOp); #if WITH_EDITOR diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SnapshotManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSnapshotManager.cpp similarity index 69% rename from SpatialGDK/Source/SpatialGDK/Private/Interop/SnapshotManager.cpp rename to SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSnapshotManager.cpp index 4d30798448..08d194b61c 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SnapshotManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSnapshotManager.cpp @@ -1,8 +1,7 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved -#include "Interop/SnapshotManager.h" +#include "Interop/SpatialSnapshotManager.h" -#include "EngineClasses/SpatialNetDriver.h" #include "Interop/Connection/SpatialWorkerConnection.h" #include "Interop/GlobalStateManager.h" #include "Interop/SpatialReceiver.h" @@ -13,41 +12,46 @@ DEFINE_LOG_CATEGORY(LogSnapshotManager); using namespace SpatialGDK; -void USnapshotManager::Init(USpatialNetDriver* InNetDriver) +SpatialSnapshotManager::SpatialSnapshotManager() + : Connection(nullptr) + , GlobalStateManager(nullptr) + , Receiver(nullptr) +{} + +void SpatialSnapshotManager::Init(USpatialWorkerConnection* InConnection, UGlobalStateManager* InGlobalStateManager, USpatialReceiver* InReceiver) { - NetDriver = InNetDriver; - Receiver = InNetDriver->Receiver; - GlobalStateManager = InNetDriver->GlobalStateManager; + check(InConnection != nullptr); + Connection = InConnection; + + check(InReceiver != nullptr); + Receiver = InReceiver; + + check(InGlobalStateManager != nullptr); + GlobalStateManager = InGlobalStateManager; } // WorldWipe will send out an expensive entity query for every entity in the deployment. -// It does this by having an entity query for all entities that are not the GSM (workaround for not having the ability to make a query for all entities). -// Once it has the response to this query, it will send deletion requests for all found entities and then one for the GSM itself. +// It does this by sending an entity query for all entities with the Unreal Metadata Component +// Once it has the response to this query, it will send deletion requests for all found entities. // Should only be triggered by the worker which is authoritative over the GSM. -void USnapshotManager::WorldWipe(const USpatialNetDriver::PostWorldWipeDelegate& PostWorldWipeDelegate) +void SpatialSnapshotManager::WorldWipe(const PostWorldWipeDelegate& PostWorldWipeDelegate) { - UE_LOG(LogSnapshotManager, Log, TEXT("World wipe for deployment has been triggered. All entities will be deleted!")); + UE_LOG(LogSnapshotManager, Log, TEXT("World wipe for deployment has been triggered. All entities with the UnrealMetaData component will be deleted!")); - Worker_Constraint GSMConstraint; - GSMConstraint.constraint_type = WORKER_CONSTRAINT_TYPE_ENTITY_ID; - GSMConstraint.constraint.entity_id_constraint.entity_id = GlobalStateManager->GlobalStateManagerEntityId; - - Worker_NotConstraint NotGSMConstraint; - NotGSMConstraint.constraint = &GSMConstraint; - - Worker_Constraint WorldConstraint; - WorldConstraint.constraint_type = WORKER_CONSTRAINT_TYPE_NOT; - WorldConstraint.constraint.not_constraint = NotGSMConstraint; + Worker_Constraint UnrealMetadataConstraint; + UnrealMetadataConstraint.constraint_type = WORKER_CONSTRAINT_TYPE_COMPONENT; + UnrealMetadataConstraint.constraint.component_constraint.component_id = SpatialConstants::UNREAL_METADATA_COMPONENT_ID; Worker_EntityQuery WorldQuery{}; - WorldQuery.constraint = WorldConstraint; + WorldQuery.constraint = UnrealMetadataConstraint; WorldQuery.result_type = WORKER_RESULT_TYPE_SNAPSHOT; Worker_RequestId RequestID; - RequestID = NetDriver->Connection->SendEntityQueryRequest(&WorldQuery); + check(Connection.IsValid()); + RequestID = Connection->SendEntityQueryRequest(&WorldQuery); EntityQueryDelegate WorldQueryDelegate; - WorldQueryDelegate.BindLambda([this, PostWorldWipeDelegate](const Worker_EntityQueryResponseOp& Op) + WorldQueryDelegate.BindLambda([Connection = this->Connection, PostWorldWipeDelegate](const Worker_EntityQueryResponseOp& Op) { if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) { @@ -60,27 +64,26 @@ void USnapshotManager::WorldWipe(const USpatialNetDriver::PostWorldWipeDelegate& else { // Send deletion requests for all entities found in the world entity query. - DeleteEntities(Op); - - // Also make sure that we kill the GSM. - NetDriver->Connection->SendDeleteEntityRequest(GlobalStateManager->GlobalStateManagerEntityId); + DeleteEntities(Op, Connection); // The world is now ready to finish ServerTravel which means loading in a new map. PostWorldWipeDelegate.ExecuteIfBound(); } }); + check(Receiver.IsValid()); Receiver->AddEntityQueryDelegate(RequestID, WorldQueryDelegate); } -void USnapshotManager::DeleteEntities(const Worker_EntityQueryResponseOp& Op) +void SpatialSnapshotManager::DeleteEntities(const Worker_EntityQueryResponseOp& Op, TWeakObjectPtr Connection) { UE_LOG(LogSnapshotManager, Log, TEXT("Deleting %u entities."), Op.result_count); for (uint32_t i = 0; i < Op.result_count; i++) { UE_LOG(LogSnapshotManager, Verbose, TEXT("Sending delete request for: %i"), Op.results[i].entity_id); - NetDriver->Connection->SendDeleteEntityRequest(Op.results[i].entity_id); + check(Connection.IsValid()); + Connection->SendDeleteEntityRequest(Op.results[i].entity_id); } } @@ -99,7 +102,7 @@ FString GetSnapshotPath(const FString& SnapshotName) // LoadSnapshot will take a snapshot name which should be on disk and attempt to read and spawn all of the entities in that snapshot. // This should only be called from the worker which has authority over the GSM. -void USnapshotManager::LoadSnapshot(const FString& SnapshotName) +void SpatialSnapshotManager::LoadSnapshot(const FString& SnapshotName) { FString SnapshotPath = GetSnapshotPath(SnapshotName); @@ -119,7 +122,7 @@ void USnapshotManager::LoadSnapshot(const FString& SnapshotName) return; } - TArray> EntitiesToSpawn; + TArray> EntitiesToSpawn; // Get all of the entities from the snapshot. while (Worker_SnapshotInputStream_HasNext(Snapshot) > 0) @@ -137,12 +140,12 @@ void USnapshotManager::LoadSnapshot(const FString& SnapshotName) Error = Worker_SnapshotInputStream_GetState(Snapshot).error_message; if (Error.IsEmpty()) { - TArray EntityComponents; + TArray EntityComponents; for (uint32_t i = 0; i < EntityToSpawn->component_count; ++i) { // Entity component data must be deep copied so that it can be used for CreateEntityRequest. Schema_ComponentData* CopySchemaData = Schema_CopyComponentData(EntityToSpawn->components[i].schema_type); - Worker_ComponentData EntityComponentData{}; + FWorkerComponentData EntityComponentData{}; EntityComponentData.component_id = EntityToSpawn->components[i].component_id; EntityComponentData.schema_type = CopySchemaData; EntityComponents.Add(EntityComponentData); @@ -162,17 +165,19 @@ void USnapshotManager::LoadSnapshot(const FString& SnapshotName) // Set up reserve IDs delegate ReserveEntityIDsDelegate SpawnEntitiesDelegate; - SpawnEntitiesDelegate.BindLambda([EntitiesToSpawn, this](const Worker_ReserveEntityIdsResponseOp& Op) + SpawnEntitiesDelegate.BindLambda([Connection = this->Connection, GlobalStateManager = this->GlobalStateManager, EntitiesToSpawn](const Worker_ReserveEntityIdsResponseOp& Op) { UE_LOG(LogSnapshotManager, Log, TEXT("Creating entities in snapshot, number of entities to spawn: %i"), Op.number_of_entity_ids); // Ensure we have the same number of reserved IDs as we have entities to spawn check(EntitiesToSpawn.Num() == Op.number_of_entity_ids); + check(GlobalStateManager.IsValid()); + check(Connection.IsValid()); for (uint32_t i = 0; i < Op.number_of_entity_ids; i++) { // Get an entity to spawn and a reserved EntityID - TArray EntityToSpawn = EntitiesToSpawn[i]; + TArray EntityToSpawn = EntitiesToSpawn[i]; Worker_EntityId ReservedEntityID = Op.first_entity_id + i; // Check if this is the GSM @@ -186,18 +191,21 @@ void USnapshotManager::LoadSnapshot(const FString& SnapshotName) } UE_LOG(LogSnapshotManager, Log, TEXT("Sending entity create request for: %i"), ReservedEntityID); - NetDriver->Connection->SendCreateEntityRequest(MoveTemp(EntityToSpawn), &ReservedEntityID); + Connection->SendCreateEntityRequest(MoveTemp(EntityToSpawn), &ReservedEntityID); } + GlobalStateManager->SetDeploymentState(); GlobalStateManager->SetAcceptingPlayers(true); }); // Reserve the Entity IDs - Worker_RequestId ReserveRequestID = NetDriver->Connection->SendReserveEntityIdsRequest(EntitiesToSpawn.Num()); + check(Connection.IsValid()); + Worker_RequestId ReserveRequestID = Connection->SendReserveEntityIdsRequest(EntitiesToSpawn.Num()); // TODO: UNR-654 // References to entities that are stored within the snapshot need remapping once we know the new entity IDs. // Add the spawn delegate + check(Receiver.IsValid()); Receiver->AddReserveEntityIdsDelegate(ReserveRequestID, SpawnEntitiesDelegate); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp index 8104297b7a..c0e3a96716 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp @@ -3,20 +3,27 @@ #include "Interop/SpatialStaticComponentView.h" #include "Schema/AuthorityIntent.h" -#include "Schema/ClientRPCEndpoint.h" +#include "Schema/ClientEndpoint.h" +#include "Schema/ClientRPCEndpointLegacy.h" #include "Schema/Component.h" +#include "Schema/ComponentPresence.h" #include "Schema/Heartbeat.h" #include "Schema/Interest.h" +#include "Schema/MulticastRPCs.h" +#include "Schema/NetOwningClientWorker.h" #include "Schema/RPCPayload.h" -#include "Schema/ServerRPCEndpoint.h" +#include "Schema/ServerEndpoint.h" +#include "Schema/ServerRPCEndpointLegacy.h" #include "Schema/Singleton.h" +#include "Schema/SpatialDebugging.h" #include "Schema/SpawnData.h" +#include "Schema/UnrealMetadata.h" -Worker_Authority USpatialStaticComponentView::GetAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId) +Worker_Authority USpatialStaticComponentView::GetAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const { - if (TMap* ComponentAuthorityMap = EntityComponentAuthorityMap.Find(EntityId)) + if (const TMap* ComponentAuthorityMap = EntityComponentAuthorityMap.Find(EntityId)) { - if (Worker_Authority* Authority = ComponentAuthorityMap->Find(ComponentId)) + if (const Worker_Authority* Authority = ComponentAuthorityMap->Find(ComponentId)) { return *Authority; } @@ -25,13 +32,12 @@ Worker_Authority USpatialStaticComponentView::GetAuthority(Worker_EntityId Entit return WORKER_AUTHORITY_NOT_AUTHORITATIVE; } -// TODO UNR-640 - Need to fix for authority loss imminent -bool USpatialStaticComponentView::HasAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId) +bool USpatialStaticComponentView::HasAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const { return GetAuthority(EntityId, ComponentId) == WORKER_AUTHORITY_AUTHORITATIVE; } -bool USpatialStaticComponentView::HasComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) +bool USpatialStaticComponentView::HasComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const { if (auto* EntityComponentStorage = EntityComponentMap.Find(EntityId)) { @@ -43,53 +49,74 @@ bool USpatialStaticComponentView::HasComponent(Worker_EntityId EntityId, Worker_ void USpatialStaticComponentView::OnAddComponent(const Worker_AddComponentOp& Op) { - TUniquePtr Data; + TUniquePtr Data; switch (Op.data.component_id) { case SpatialConstants::ENTITY_ACL_COMPONENT_ID: - Data = MakeUnique>(Op.data); + Data = MakeUnique(Op.data); break; case SpatialConstants::METADATA_COMPONENT_ID: - Data = MakeUnique>(Op.data); + Data = MakeUnique(Op.data); break; case SpatialConstants::POSITION_COMPONENT_ID: - Data = MakeUnique>(Op.data); + Data = MakeUnique(Op.data); break; case SpatialConstants::PERSISTENCE_COMPONENT_ID: - Data = MakeUnique>(Op.data); + Data = MakeUnique(Op.data); + break; + case SpatialConstants::WORKER_COMPONENT_ID: + Data = MakeUnique(Op.data); break; case SpatialConstants::SPAWN_DATA_COMPONENT_ID: - Data = MakeUnique>(Op.data); + Data = MakeUnique(Op.data); break; case SpatialConstants::SINGLETON_COMPONENT_ID: - Data = MakeUnique>(Op.data); + Data = MakeUnique(Op.data); break; case SpatialConstants::UNREAL_METADATA_COMPONENT_ID: - Data = MakeUnique>(Op.data); + Data = MakeUnique(Op.data); break; case SpatialConstants::INTEREST_COMPONENT_ID: - Data = MakeUnique>(Op.data); + Data = MakeUnique(Op.data); break; case SpatialConstants::HEARTBEAT_COMPONENT_ID: - Data = MakeUnique>(Op.data); + Data = MakeUnique(Op.data); break; case SpatialConstants::RPCS_ON_ENTITY_CREATION_ID: - Data = MakeUnique>(Op.data); + Data = MakeUnique(Op.data); break; - case SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID: - Data = MakeUnique>(Op.data); + case SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY: + Data = MakeUnique(Op.data); break; - case SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID: - Data = MakeUnique>(Op.data); + case SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY: + Data = MakeUnique(Op.data); break; case SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID: - Data = MakeUnique>(Op.data); + Data = MakeUnique(Op.data); + break; + case SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID: + Data = MakeUnique(Op.data); + break; + case SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID: + Data = MakeUnique(Op.data); + break; + case SpatialConstants::MULTICAST_RPCS_COMPONENT_ID: + Data = MakeUnique(Op.data); + break; + case SpatialConstants::SPATIAL_DEBUGGING_COMPONENT_ID: + Data = MakeUnique(Op.data); + break; + case SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID: + Data = MakeUnique(Op.data); + break; + case SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID: + Data = MakeUnique(Op.data); break; default: // Component is not hand written, but we still want to know the existence of it on this entity. Data = nullptr; } - EntityComponentMap.FindOrAdd(Op.entity_id).FindOrAdd(Op.data.component_id) = std::move(Data); + EntityComponentMap.FindOrAdd(Op.entity_id).FindOrAdd(Op.data.component_id) = MoveTemp(Data); } void USpatialStaticComponentView::OnRemoveComponent(const Worker_RemoveComponentOp& Op) @@ -118,15 +145,33 @@ void USpatialStaticComponentView::OnComponentUpdate(const Worker_ComponentUpdate case SpatialConstants::POSITION_COMPONENT_ID: Component = GetComponentData(Op.entity_id); break; - case SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID: - Component = GetComponentData(Op.entity_id); + case SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY: + Component = GetComponentData(Op.entity_id); break; - case SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID: - Component = GetComponentData(Op.entity_id); + case SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY: + Component = GetComponentData(Op.entity_id); break; case SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID: Component = GetComponentData(Op.entity_id); break; + case SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID: + Component = GetComponentData(Op.entity_id); + break; + case SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID: + Component = GetComponentData(Op.entity_id); + break; + case SpatialConstants::MULTICAST_RPCS_COMPONENT_ID: + Component = GetComponentData(Op.entity_id); + break; + case SpatialConstants::SPATIAL_DEBUGGING_COMPONENT_ID: + Component = GetComponentData(Op.entity_id); + break; + case SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID: + Component = GetComponentData(Op.entity_id); + break; + case SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID: + Component = GetComponentData(Op.entity_id); + break; default: return; } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialWorkerFlags.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialWorkerFlags.cpp index 1cd9dc8607..c3929129ed 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialWorkerFlags.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialWorkerFlags.cpp @@ -2,14 +2,11 @@ #include "Interop/SpatialWorkerFlags.h" -TMap USpatialWorkerFlags::WorkerFlags; -FOnWorkerFlagsUpdated USpatialWorkerFlags::OnWorkerFlagsUpdated; - -bool USpatialWorkerFlags::GetWorkerFlag(const FString& Name, FString& OutValue) +bool USpatialWorkerFlags::GetWorkerFlag(const FString& InFlagName, FString& OutFlagValue) const { - if (FString* ValuePtr = WorkerFlags.Find(Name)) + if (const FString* ValuePtr = WorkerFlags.Find(InFlagName)) { - OutValue = *ValuePtr; + OutFlagValue = *ValuePtr; return true; } @@ -32,10 +29,6 @@ void USpatialWorkerFlags::ApplyWorkerFlagUpdate(const Worker_FlagUpdateOp& Op) WorkerFlags.Remove(NewName); } } -FOnWorkerFlagsUpdated& USpatialWorkerFlags::GetOnWorkerFlagsUpdated() -{ - return OnWorkerFlagsUpdated; -} void USpatialWorkerFlags::BindToOnWorkerFlagsUpdated(const FOnWorkerFlagsUpdatedBP& InDelegate) { diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/AbstractLBStrategy.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/AbstractLBStrategy.cpp index de317849bb..3aabc36e22 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/AbstractLBStrategy.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/AbstractLBStrategy.cpp @@ -10,11 +10,7 @@ UAbstractLBStrategy::UAbstractLBStrategy() { } -void UAbstractLBStrategy::SetLocalVirtualWorkerId(uint32 InLocalVirtualWorkerId) +void UAbstractLBStrategy::SetLocalVirtualWorkerId(VirtualWorkerId InLocalVirtualWorkerId) { LocalVirtualWorkerId = InLocalVirtualWorkerId; } - -void UAbstractLBStrategy::Init(const USpatialNetDriver* InNetDriver) -{ -} diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp index d7486f234b..3833d0e994 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp @@ -5,18 +5,25 @@ #include "EngineClasses/SpatialNetDriver.h" #include "Utils/SpatialActorUtils.h" +#include "Templates/Tuple.h" + +DEFINE_LOG_CATEGORY(LogGridBasedLBStrategy); + UGridBasedLBStrategy::UGridBasedLBStrategy() : Super() , Rows(1) , Cols(1) - , WorldWidth(10000.f) - , WorldHeight(10000.f) + , WorldWidth(1000000.f) + , WorldHeight(1000000.f) + , InterestBorder(0.f) { } -void UGridBasedLBStrategy::Init(const USpatialNetDriver* InNetDriver) +void UGridBasedLBStrategy::Init() { - Super::Init(InNetDriver); + Super::Init(); + + UE_LOG(LogGridBasedLBStrategy, Log, TEXT("GridBasedLBStrategy initialized with Rows = %d and Cols = %d."), Rows, Cols); for (uint32 i = 1; i <= Rows * Cols; i++) { @@ -29,53 +36,55 @@ void UGridBasedLBStrategy::Init(const USpatialNetDriver* InNetDriver) const float ColumnWidth = WorldWidth / Cols; const float RowHeight = WorldHeight / Rows; - float XMin = WorldWidthMin; - float YMin = WorldHeightMin; + // We would like the inspector's representation of the load balancing strategy to match our intuition. + // +x is forward, so rows are perpendicular to the x-axis and columns are perpendicular to the y-axis. + float XMin = WorldHeightMin; + float YMin = WorldWidthMin; float XMax, YMax; for (uint32 Col = 0; Col < Cols; ++Col) { - XMax = XMin + ColumnWidth; + YMax = YMin + ColumnWidth; for (uint32 Row = 0; Row < Rows; ++Row) { - YMax = YMin + RowHeight; + XMax = XMin + RowHeight; FVector2D Min(XMin, YMin); FVector2D Max(XMax, YMax); FBox2D Cell(Min, Max); WorkerCells.Add(Cell); - YMin = YMax; + XMin = XMax; } - YMin = WorldHeightMin; - XMin = XMax; + XMin = WorldHeightMin; + YMin = YMax; } } -TSet UGridBasedLBStrategy::GetVirtualWorkerIds() const +TSet UGridBasedLBStrategy::GetVirtualWorkerIds() const { - return TSet(VirtualWorkerIds); + return TSet(VirtualWorkerIds); } -bool UGridBasedLBStrategy::ShouldRelinquishAuthority(const AActor& Actor) const +bool UGridBasedLBStrategy::ShouldHaveAuthority(const AActor& Actor) const { if (!IsReady()) { + UE_LOG(LogGridBasedLBStrategy, Warning, TEXT("GridBasedLBStrategy not ready to relinquish authority for Actor %s."), *AActor::GetDebugName(&Actor)); return false; } const FVector2D Actor2DLocation = FVector2D(SpatialGDK::GetActorSpatialPosition(&Actor)); - - - return !IsInside(WorkerCells[LocalVirtualWorkerId - 1], Actor2DLocation); + return IsInside(WorkerCells[LocalVirtualWorkerId - 1], Actor2DLocation); } -uint32 UGridBasedLBStrategy::WhoShouldHaveAuthority(const AActor& Actor) const +VirtualWorkerId UGridBasedLBStrategy::WhoShouldHaveAuthority(const AActor& Actor) const { if (!IsReady()) { + UE_LOG(LogGridBasedLBStrategy, Warning, TEXT("GridBasedLBStrategy not ready to decide on authority for Actor %s."), *AActor::GetDebugName(&Actor)); return SpatialConstants::INVALID_VIRTUAL_WORKER_ID; } @@ -92,8 +101,46 @@ uint32 UGridBasedLBStrategy::WhoShouldHaveAuthority(const AActor& Actor) const return SpatialConstants::INVALID_VIRTUAL_WORKER_ID; } +SpatialGDK::QueryConstraint UGridBasedLBStrategy::GetWorkerInterestQueryConstraint() const +{ + // For a grid-based strategy, the interest area is the cell that the worker is authoritative over plus some border region. + check(IsReady()); + + const FBox2D Interest2D = WorkerCells[LocalVirtualWorkerId - 1].ExpandBy(InterestBorder); + + const FVector2D Center2D = Interest2D.GetCenter(); + const FVector Center3D{ Center2D.X, Center2D.Y, 0.0f}; + + const FVector2D EdgeLengths2D = Interest2D.GetSize(); + check(EdgeLengths2D.X > 0.0f && EdgeLengths2D.Y > 0.0f); + const FVector EdgeLengths3D{ EdgeLengths2D.X, EdgeLengths2D.Y, FLT_MAX}; + + SpatialGDK::QueryConstraint Constraint; + Constraint.BoxConstraint = SpatialGDK::BoxConstraint{ SpatialGDK::Coordinates::FromFVector(Center3D), SpatialGDK::EdgeLength::FromFVector(EdgeLengths3D) }; + return Constraint; +} + +FVector UGridBasedLBStrategy::GetWorkerEntityPosition() const +{ + check(IsReady()); + const FVector2D Centre = WorkerCells[LocalVirtualWorkerId - 1].GetCenter(); + return FVector{ Centre.X, Centre.Y, 0.f }; +} + bool UGridBasedLBStrategy::IsInside(const FBox2D& Box, const FVector2D& Location) { return Location.X >= Box.Min.X && Location.Y >= Box.Min.Y && Location.X < Box.Max.X && Location.Y < Box.Max.Y; } + +UGridBasedLBStrategy::LBStrategyRegions UGridBasedLBStrategy::GetLBStrategyRegions() const +{ + LBStrategyRegions VirtualWorkerToCell; + VirtualWorkerToCell.SetNum(WorkerCells.Num()); + + for (int i = 0; i < WorkerCells.Num(); i++) + { + VirtualWorkerToCell[i] = MakeTuple(VirtualWorkerIds[i], WorkerCells[i]); + } + return VirtualWorkerToCell; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/OwnershipLockingPolicy.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/OwnershipLockingPolicy.cpp new file mode 100644 index 0000000000..a2c0fcde99 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/OwnershipLockingPolicy.cpp @@ -0,0 +1,271 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "LoadBalancing/OwnershipLockingPolicy.h" + +#include "Schema/AuthorityIntent.h" +#include "Schema/Component.h" +#include "Utils/SpatialActorUtils.h" + +#include "Improbable/SpatialEngineDelegates.h" +#include "UObject/UObjectGlobals.h" + +DEFINE_LOG_CATEGORY(LogOwnershipLockingPolicy); + +bool UOwnershipLockingPolicy::CanAcquireLock(const AActor* Actor) const +{ + if (Actor == nullptr) + { + UE_LOG(LogOwnershipLockingPolicy, Error, TEXT("Failed to lock nullptr actor")); + return false; + } + + return Actor->Role == ROLE_Authority; +} + +ActorLockToken UOwnershipLockingPolicy::AcquireLock(AActor* Actor, FString DebugString) +{ + if (!CanAcquireLock(Actor)) + { + UE_LOG(LogOwnershipLockingPolicy, Error, TEXT("Called AcquireLock when CanAcquireLock returned false. Actor: %s."), *GetNameSafe(Actor)); + return SpatialConstants::INVALID_ACTOR_LOCK_TOKEN; + } + + if (MigrationLockElement* ActorLockingState = ActorToLockingState.Find(Actor)) + { + ++ActorLockingState->LockCount; + } + else + { + // We want to avoid memory leak if a locked actor is deleted. + // To do this, we register with the Actor OnDestroyed delegate with a function that cleans up the internal map. + if (!Actor->OnDestroyed.IsAlreadyBound(this, &UOwnershipLockingPolicy::OnExplicitlyLockedActorDeleted)) + { + Actor->OnDestroyed.AddDynamic(this, &UOwnershipLockingPolicy::OnExplicitlyLockedActorDeleted); + } + + AActor* OwnershipHierarchyRoot = SpatialGDK::GetHierarchyRoot(Actor); + AddOwnershipHierarchyRootInformation(OwnershipHierarchyRoot, Actor); + + ActorToLockingState.Add(Actor, MigrationLockElement{ 1, OwnershipHierarchyRoot }); + } + + UE_LOG(LogOwnershipLockingPolicy, Log, TEXT("Acquiring migration lock. " + "Actor: %s. Lock name: %s. Token %d: Locks held: %d."), *GetNameSafe(Actor), *DebugString, NextToken, ActorToLockingState.Find(Actor)->LockCount); + TokenToNameAndActor.Emplace(NextToken, LockNameAndActor{ MoveTemp(DebugString), Actor }); + return NextToken++; +} + +bool UOwnershipLockingPolicy::ReleaseLock(const ActorLockToken Token) +{ + const LockNameAndActor* NameAndActor = TokenToNameAndActor.Find(Token); + if (NameAndActor == nullptr) + { + UE_LOG(LogOwnershipLockingPolicy, Error, TEXT("Called ReleaseLock for unidentified Actor lock token. Token: %d."), Token); + return false; + } + + AActor* Actor = NameAndActor->Actor; + const FString& Name = NameAndActor->LockName; + UE_LOG(LogOwnershipLockingPolicy, Log, TEXT("Releasing Actor migration lock. Actor: %s. Token: %d. Lock name: %s"), *Actor->GetName(), Token, *Name); + + check(ActorToLockingState.Contains(Actor)); + + { + // Reduce the reference count and erase the entry if reduced to 0. + auto CountIt = ActorToLockingState.CreateKeyIterator(Actor); + MigrationLockElement& ActorLockingState = CountIt.Value(); + if (ActorLockingState.LockCount == 1) + { + UE_LOG(LogOwnershipLockingPolicy, Log, TEXT("Actor migration no longer locked. Actor: %s"), *Actor->GetName()); + Actor->OnDestroyed.RemoveDynamic(this, &UOwnershipLockingPolicy::OnExplicitlyLockedActorDeleted); + RemoveOwnershipHierarchyRootInformation(ActorLockingState.HierarchyRoot, Actor); + CountIt.RemoveCurrent(); + } + else + { + --ActorLockingState.LockCount; + } + } + + TokenToNameAndActor.Remove(Token); + + return true; +} + +bool UOwnershipLockingPolicy::IsLocked(const AActor* Actor) const +{ + if (Actor == nullptr) + { + UE_LOG(LogOwnershipLockingPolicy, Warning, TEXT("IsLocked called for nullptr")); + return false; + } + + // Is this Actor explicitly locked or on a locked hierarchy ownership path. + if (IsExplicitlyLocked(Actor) || IsLockedHierarchyRoot(Actor)) + { + return true; + } + + // Is the hierarchy root of this Actor explicitly locked or on a locked hierarchy ownership path. + if (AActor* HierarchyRoot = SpatialGDK::GetHierarchyRoot(Actor)) + { + return IsExplicitlyLocked(HierarchyRoot) || IsLockedHierarchyRoot(HierarchyRoot); + } + + return false; +} + +bool UOwnershipLockingPolicy::IsExplicitlyLocked(const AActor* Actor) const +{ + return ActorToLockingState.Contains(Actor); +} + +bool UOwnershipLockingPolicy::IsLockedHierarchyRoot(const AActor* Actor) const +{ + return LockedOwnershipRootActorToExplicitlyLockedActors.Contains(Actor); +} + +bool UOwnershipLockingPolicy::AcquireLockFromDelegate(AActor* ActorToLock, const FString& DelegateLockIdentifier) +{ + ActorLockToken LockToken = AcquireLock(ActorToLock, DelegateLockIdentifier); + if (LockToken == SpatialConstants::INVALID_ACTOR_LOCK_TOKEN) + { + UE_LOG(LogOwnershipLockingPolicy, Error, TEXT("AcquireLock called from engine delegate returned an invalid token")); + return false; + } + + check(!DelegateLockingIdentifierToActorLockToken.Contains(DelegateLockIdentifier)); + DelegateLockingIdentifierToActorLockToken.Add(DelegateLockIdentifier, LockToken); + return true; +} + +bool UOwnershipLockingPolicy::ReleaseLockFromDelegate(AActor* ActorToRelease, const FString& DelegateLockIdentifier) +{ + if (!DelegateLockingIdentifierToActorLockToken.Contains(DelegateLockIdentifier)) + { + UE_LOG(LogOwnershipLockingPolicy, Error, TEXT("Executed ReleaseLockDelegate for unidentified delegate lock identifier. Token: %s."), *DelegateLockIdentifier); + return false; + } + ActorLockToken LockToken = DelegateLockingIdentifierToActorLockToken.FindAndRemoveChecked(DelegateLockIdentifier); + bool bReleaseSucceeded = ReleaseLock(LockToken); + return bReleaseSucceeded; +} + +void UOwnershipLockingPolicy::OnOwnerUpdated(const AActor* Actor, const AActor* OldOwner) +{ + check(Actor != nullptr); + + // If an explicitly locked Actor is changing owner. + if (IsExplicitlyLocked(Actor)) + { + RecalculateLockedActorOwnershipHierarchyInformation(Actor); + } + + // If a locked hierarchy root is changing owner. + if (IsLockedHierarchyRoot(Actor)) + { + RecalculateAllExplicitlyLockedActorsInThisHierarchy(Actor); + } + // If an Actor in a locked hierarchy is changing owner (i.e. either the old owner or + // the root hierarchy of the old owner is the root of a locked hierarchy), we need to + // recalculate ownership hierarchies of all explicitly locked Actors in that hierarchy. + else if (OldOwner != nullptr) + { + const AActor* OldHierarchyRoot = OldOwner->GetOwner() != nullptr ? SpatialGDK::GetHierarchyRoot(OldOwner) : OldOwner; + if (IsLockedHierarchyRoot(OldHierarchyRoot)) + { + RecalculateAllExplicitlyLockedActorsInThisHierarchy(OldHierarchyRoot); + } + } + } + +void UOwnershipLockingPolicy::OnExplicitlyLockedActorDeleted(AActor* DestroyedActor) +{ + // Find all tokens for this Actor and unlock. + for (auto TokenNameActorIterator = TokenToNameAndActor.CreateIterator(); TokenNameActorIterator; ++TokenNameActorIterator) + { + if (TokenNameActorIterator->Value.Actor == DestroyedActor) + { + TokenNameActorIterator.RemoveCurrent(); + } + } + + // Delete Actor from local mapping. + MigrationLockElement ActorLockingState = ActorToLockingState.FindAndRemoveChecked(DestroyedActor); + + // Update ownership path Actor mapping to remove this Actor. + RemoveOwnershipHierarchyRootInformation(ActorLockingState.HierarchyRoot, DestroyedActor); +} + +void UOwnershipLockingPolicy::OnHierarchyRootActorDeleted(AActor* DeletedHierarchyRoot) +{ + check(LockedOwnershipRootActorToExplicitlyLockedActors.Contains(DeletedHierarchyRoot)); + + // For all explicitly locked Actors where this Actor is on the ownership path, recalculate the + // ownership path information to account for this Actor's deletion. + RecalculateAllExplicitlyLockedActorsInThisHierarchy(DeletedHierarchyRoot); + LockedOwnershipRootActorToExplicitlyLockedActors.Remove(DeletedHierarchyRoot); +} + +void UOwnershipLockingPolicy::RecalculateAllExplicitlyLockedActorsInThisHierarchy(const AActor* HierarchyRoot) +{ + TArray ExplicitlyLockedActorsWithThisActorInOwnershipPath = LockedOwnershipRootActorToExplicitlyLockedActors.FindChecked(HierarchyRoot); + for (const AActor* ExplicitlyLockedActor : ExplicitlyLockedActorsWithThisActorInOwnershipPath) + { + RecalculateLockedActorOwnershipHierarchyInformation(ExplicitlyLockedActor); + } +} + +void UOwnershipLockingPolicy::RecalculateLockedActorOwnershipHierarchyInformation(const AActor* ExplicitlyLockedActor) +{ + // For the old ownership path, update ownership path Actor mapping to explicitly locked Actors to remove this Actor. + AActor* OldHierarchyRoot = ActorToLockingState.FindChecked(ExplicitlyLockedActor).HierarchyRoot; + RemoveOwnershipHierarchyRootInformation(OldHierarchyRoot, ExplicitlyLockedActor); + + // For the new ownership path, update ownership path Actor mapping to explicitly locked Actors to include this Actor. + AActor* NewOwnershipHierarchyRoot = SpatialGDK::GetHierarchyRoot(ExplicitlyLockedActor); + ActorToLockingState.FindChecked(ExplicitlyLockedActor).HierarchyRoot = NewOwnershipHierarchyRoot; + AddOwnershipHierarchyRootInformation(NewOwnershipHierarchyRoot, ExplicitlyLockedActor); +} + +void UOwnershipLockingPolicy::RemoveOwnershipHierarchyRootInformation(AActor* HierarchyRoot, const AActor* ExplicitlyLockedActor) +{ + if (HierarchyRoot == nullptr) + { + return; + } + + // Find Actors in this root Actor's hierarchy which are explicitly locked. + TArray& ExplicitlyLockedActorsWithThisActorOnPath = LockedOwnershipRootActorToExplicitlyLockedActors.FindChecked(HierarchyRoot); + check(ExplicitlyLockedActorsWithThisActorOnPath.Num() > 0); + + // If there's only one explicitly locked Actor in the hierarchy, we're removing the only Actor with this root, + // so we can stop caring about the root itself. Otherwise, just remove the specific Actor entry in the root's list. + if (ExplicitlyLockedActorsWithThisActorOnPath.Num() == 1) + { + LockedOwnershipRootActorToExplicitlyLockedActors.Remove(HierarchyRoot); + HierarchyRoot->OnDestroyed.RemoveDynamic(this, &UOwnershipLockingPolicy::OnHierarchyRootActorDeleted); + } + else + { + ExplicitlyLockedActorsWithThisActorOnPath.Remove(ExplicitlyLockedActor); + } +} + +void UOwnershipLockingPolicy::AddOwnershipHierarchyRootInformation(AActor* HierarchyRoot, const AActor* ExplicitlyLockedActor) +{ + if (HierarchyRoot == nullptr) + { + return; + } + + // For the hierarchy root of an explicitly locked Actor, we store a reference from the hierarchy root Actor back to + // the explicitly locked Actor, as well as binding a deletion delegate to the hierarchy root Actor. + TArray& ExplicitlyLockedActorsWithThisActorOnPath = LockedOwnershipRootActorToExplicitlyLockedActors.FindOrAdd(HierarchyRoot); + ExplicitlyLockedActorsWithThisActorOnPath.AddUnique(ExplicitlyLockedActor); + + if (!HierarchyRoot->OnDestroyed.IsAlreadyBound(this, &UOwnershipLockingPolicy::OnHierarchyRootActorDeleted)) + { + HierarchyRoot->OnDestroyed.AddDynamic(this, &UOwnershipLockingPolicy::OnHierarchyRootActorDeleted); + } +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/WorkerRegion.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/WorkerRegion.cpp new file mode 100644 index 0000000000..513bbce254 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/WorkerRegion.cpp @@ -0,0 +1,71 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "LoadBalancing/WorkerRegion.h" + +#include "Materials/MaterialInstanceDynamic.h" +#include "UObject/ConstructorHelpers.h" +#include "UObject/UObjectGlobals.h" + +namespace +{ + const float DEFAULT_WORKER_REGION_HEIGHT = 30.0f; + const float DEFAULT_WORKER_REGION_OPACITY = 0.7f; + const FString WORKER_REGION_ACTOR_NAME = TEXT("WorkerRegionCuboid"); + const FName WORKER_REGION_MATERIAL_OPACITY_PARAM = TEXT("Opacity"); + const FName WORKER_REGION_MATERIAL_COLOR_PARAM = TEXT("Color"); + const FString CUBE_MESH_PATH = TEXT("/Engine/BasicShapes/Cube.Cube"); +} + +AWorkerRegion::AWorkerRegion(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + Mesh = ObjectInitializer.CreateDefaultSubobject(this, *WORKER_REGION_ACTOR_NAME); + static ConstructorHelpers::FObjectFinder CubeAsset(*CUBE_MESH_PATH); + Mesh->SetStaticMesh(CubeAsset.Object); + SetRootComponent(Mesh); +} + +void AWorkerRegion::Init(UMaterial* Material, const FColor& Color, const FBox2D& Extents, const float VerticalScale) +{ + SetHeight(DEFAULT_WORKER_REGION_HEIGHT); + + MaterialInstance = UMaterialInstanceDynamic::Create(Material, nullptr); + Mesh->SetMaterial(0, MaterialInstance); + SetOpacity(DEFAULT_WORKER_REGION_OPACITY); + SetColor(Color); + SetPositionAndScale(Extents, VerticalScale); +} + +void AWorkerRegion::SetHeight(const float Height) +{ + const FVector CurrentLocation = GetActorLocation(); + SetActorLocation(FVector(CurrentLocation.X, CurrentLocation.Y, Height)); +} + +void AWorkerRegion::SetOpacity(const float Opacity) +{ + MaterialInstance->SetScalarParameterValue(WORKER_REGION_MATERIAL_OPACITY_PARAM, Opacity); +} + +void AWorkerRegion::SetPositionAndScale(const FBox2D& Extents, const float VerticalScale) +{ + const FVector CurrentLocation = GetActorLocation(); + + const float MinX = Extents.Min.X; + const float MaxX = Extents.Max.X; + const float MinY = Extents.Min.Y; + const float MaxY = Extents.Max.Y; + + const float CenterX = MinX + (MaxX - MinX) / 2; + const float CenterY = MinY + (MaxY - MinY) / 2; + const float ScaleX = (MaxX - MinX) / 100; + const float ScaleY = (MaxY - MinY) / 100; + + SetActorLocation(FVector(CenterX, CenterY, CurrentLocation.Z)); + SetActorScale3D(FVector(ScaleX, ScaleY, VerticalScale)); +} + +void AWorkerRegion::SetColor(const FColor& Color) +{ + MaterialInstance->SetVectorParameterValue(WORKER_REGION_MATERIAL_COLOR_PARAM, Color); +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Schema/ClientEndpoint.cpp b/SpatialGDK/Source/SpatialGDK/Private/Schema/ClientEndpoint.cpp new file mode 100644 index 0000000000..29ab6fc3da --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Schema/ClientEndpoint.cpp @@ -0,0 +1,28 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Schema/ClientEndpoint.h" + +namespace SpatialGDK +{ + +ClientEndpoint::ClientEndpoint(const Worker_ComponentData& Data) + : ReliableRPCBuffer(ERPCType::ServerReliable) + , UnreliableRPCBuffer(ERPCType::ServerUnreliable) +{ + ReadFromSchema(Schema_GetComponentDataFields(Data.schema_type)); +} + +void ClientEndpoint::ApplyComponentUpdate(const Worker_ComponentUpdate& Update) +{ + ReadFromSchema(Schema_GetComponentUpdateFields(Update.schema_type)); +} + +void ClientEndpoint::ReadFromSchema(Schema_Object* SchemaObject) +{ + RPCRingBufferUtils::ReadBufferFromSchema(SchemaObject, ReliableRPCBuffer); + RPCRingBufferUtils::ReadBufferFromSchema(SchemaObject, UnreliableRPCBuffer); + RPCRingBufferUtils::ReadAckFromSchema(SchemaObject, ERPCType::ClientReliable, ReliableRPCAck); + RPCRingBufferUtils::ReadAckFromSchema(SchemaObject, ERPCType::ClientUnreliable, UnreliableRPCAck); +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Schema/MulticastRPCs.cpp b/SpatialGDK/Source/SpatialGDK/Private/Schema/MulticastRPCs.cpp new file mode 100644 index 0000000000..b8d424fc93 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Schema/MulticastRPCs.cpp @@ -0,0 +1,33 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Schema/MulticastRPCs.h" + +namespace SpatialGDK +{ + +MulticastRPCs::MulticastRPCs(const Worker_ComponentData& Data) + : MulticastRPCBuffer(ERPCType::NetMulticast) +{ + ReadFromSchema(Schema_GetComponentDataFields(Data.schema_type)); +} + +void MulticastRPCs::ApplyComponentUpdate(const Worker_ComponentUpdate& Update) +{ + ReadFromSchema(Schema_GetComponentUpdateFields(Update.schema_type)); +} + +void MulticastRPCs::ReadFromSchema(Schema_Object* SchemaObject) +{ + RPCRingBufferUtils::ReadBufferFromSchema(SchemaObject, MulticastRPCBuffer); + + // This is a special field that is set when creating a MulticastRPCs component with initial RPCs. + // The server that first gains authority over the component will set last sent RPC ID to be equal + // to this so the clients that already checked out this entity can execute initial RPCs. + Schema_FieldId FieldId = RPCRingBufferUtils::GetInitiallyPresentMulticastRPCsCountFieldId(); + if (Schema_GetUint32Count(SchemaObject, FieldId) > 0) + { + InitiallyPresentMulticastRPCsCount = Schema_GetUint32(SchemaObject, FieldId); + } +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Schema/ServerEndpoint.cpp b/SpatialGDK/Source/SpatialGDK/Private/Schema/ServerEndpoint.cpp new file mode 100644 index 0000000000..fe0ceddd19 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Schema/ServerEndpoint.cpp @@ -0,0 +1,28 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Schema/ServerEndpoint.h" + +namespace SpatialGDK +{ + +ServerEndpoint::ServerEndpoint(const Worker_ComponentData& Data) + : ReliableRPCBuffer(ERPCType::ClientReliable) + , UnreliableRPCBuffer(ERPCType::ClientUnreliable) +{ + ReadFromSchema(Schema_GetComponentDataFields(Data.schema_type)); +} + +void ServerEndpoint::ApplyComponentUpdate(const Worker_ComponentUpdate& Update) +{ + ReadFromSchema(Schema_GetComponentUpdateFields(Update.schema_type)); +} + +void ServerEndpoint::ReadFromSchema(Schema_Object* SchemaObject) +{ + RPCRingBufferUtils::ReadBufferFromSchema(SchemaObject, ReliableRPCBuffer); + RPCRingBufferUtils::ReadBufferFromSchema(SchemaObject, UnreliableRPCBuffer); + RPCRingBufferUtils::ReadAckFromSchema(SchemaObject, ERPCType::ServerReliable, ReliableRPCAck); + RPCRingBufferUtils::ReadAckFromSchema(SchemaObject, ERPCType::ServerUnreliable, UnreliableRPCAck); +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.cpp b/SpatialGDK/Source/SpatialGDK/Private/Schema/UnrealObjectRef.cpp similarity index 79% rename from SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.cpp rename to SpatialGDK/Source/SpatialGDK/Private/Schema/UnrealObjectRef.cpp index aa6436805c..6a4a486ad5 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Schema/UnrealObjectRef.cpp @@ -3,12 +3,13 @@ #include "Schema/UnrealObjectRef.h" #include "EngineClasses/SpatialPackageMapClient.h" +#include "SpatialConstants.h" #include "Utils/SchemaUtils.h" DEFINE_LOG_CATEGORY_STATIC(LogUnrealObjectRef, Log, All); -const FUnrealObjectRef FUnrealObjectRef::NULL_OBJECT_REF = FUnrealObjectRef(0, 0); -const FUnrealObjectRef FUnrealObjectRef::UNRESOLVED_OBJECT_REF = FUnrealObjectRef(0, 1); +const FUnrealObjectRef FUnrealObjectRef::NULL_OBJECT_REF = FUnrealObjectRef(SpatialConstants::INVALID_ENTITY_ID, 0); +const FUnrealObjectRef FUnrealObjectRef::UNRESOLVED_OBJECT_REF = FUnrealObjectRef(SpatialConstants::INVALID_ENTITY_ID, 1); UObject* FUnrealObjectRef::ToObjectPtr(const FUnrealObjectRef& ObjectRef, USpatialPackageMapClient* PackageMap, bool& bOutUnresolved) { @@ -39,6 +40,14 @@ UObject* FUnrealObjectRef::ToObjectPtr(const FUnrealObjectRef& ObjectRef, USpati UObject* Value = PackageMap->GetObjectFromNetGUID(NetGUID, true); if (Value == nullptr) { + // Check if the object we are looking for is in a package being loaded. + if (PackageMap->IsGUIDPending(NetGUID)) + { + PackageMap->PendingReferences.Add(NetGUID); + bOutUnresolved = true; + return nullptr; + } + // At this point, we're unable to resolve a stably-named actor by path. This likely means either the actor doesn't exist, or // it's part of a streaming level that hasn't been streamed in. Native Unreal networking sets reference to nullptr and continues. // So we do the same. @@ -143,6 +152,48 @@ FUnrealObjectRef FUnrealObjectRef::FromObjectPtr(UObject* ObjectValue, USpatialP return ObjectRef; } +FUnrealObjectRef FUnrealObjectRef::FromSoftObjectPath(const FSoftObjectPath& ObjectPath) +{ + FUnrealObjectRef PackageRef; + + PackageRef.Path = ObjectPath.GetLongPackageName(); + + FUnrealObjectRef ObjectRef; + ObjectRef.Outer = PackageRef; + ObjectRef.Path = ObjectPath.GetAssetName(); + + return ObjectRef; +} + +FSoftObjectPath FUnrealObjectRef::ToSoftObjectPath(const FUnrealObjectRef& ObjectRef) +{ + if (!ObjectRef.Path.IsSet()) + { + return FSoftObjectPath(); + } + + bool bSubObjectName = true; + FString FullPackagePath; + const FUnrealObjectRef* CurRef = &ObjectRef; + while (CurRef) + { + if (CurRef->Path.IsSet()) + { + FString Path = *CurRef->Path; + if (!FullPackagePath.IsEmpty()) + { + Path.Append(bSubObjectName ? TEXT(".") : TEXT("/")); + Path.Append(FullPackagePath); + bSubObjectName = false; + } + FullPackagePath = MoveTemp(Path); + } + CurRef = CurRef->Outer.IsSet() ? &(*CurRef->Outer) : nullptr; + } + + return FSoftObjectPath(MoveTemp(FullPackagePath)); +} + FUnrealObjectRef FUnrealObjectRef::GetSingletonClassRef(UObject* SingletonObject, USpatialPackageMapClient* PackageMap) { FUnrealObjectRef ClassObjectRef = FromObjectPtr(SingletonObject->GetClass(), PackageMap); diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKConsoleCommands.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKConsoleCommands.cpp new file mode 100644 index 0000000000..089e8e2158 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKConsoleCommands.cpp @@ -0,0 +1,37 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialGDKConsoleCommands.h" + +#include "SpatialConstants.h" +#include "Engine/Engine.h" + +DEFINE_LOG_CATEGORY(LogSpatialGDKConsoleCommands) + +namespace SpatialGDKConsoleCommands +{ + void ConsoleCommand_ConnectToLocator(const TArray& Args, UWorld* World) + { + if (Args.Num() != 2) + { + UE_LOG(LogSpatialGDKConsoleCommands, Log, TEXT("ConsoleCommand_ConnectToLocator takes 2 arguments (login, playerToken). Only %d given."), Args.Num()); + return; + } + + FURL URL; + URL.Host = SpatialConstants::LOCATOR_HOST; + FString Login = SpatialConstants::URL_LOGIN_OPTION + Args[0]; + FString PlayerIdentity = SpatialConstants::URL_PLAYER_IDENTITY_OPTION + Args[1]; + URL.AddOption(*PlayerIdentity); + URL.AddOption(*Login); + + FString Error; + FWorldContext &WorldContext = GEngine->GetWorldContextFromWorldChecked(World); + GEngine->Browse(WorldContext, URL, Error); + } + + FAutoConsoleCommandWithWorldAndArgs ConnectToLocatorCommand = FAutoConsoleCommandWithWorldAndArgs( + TEXT("ConnectToLocator"), + TEXT("Usage: ConnectToLocator "), + FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&ConsoleCommand_ConnectToLocator) + ); +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp index 916a512339..2da7e83a1b 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp @@ -10,6 +10,28 @@ #include "Settings/LevelEditorPlaySettings.h" #endif +DEFINE_LOG_CATEGORY(LogSpatialGDKSettings); + +namespace +{ + void CheckCmdLineOverrideBool(const TCHAR* CommandLine, const TCHAR* Parameter, const TCHAR* PrettyName, bool& bOutValue) + { + if(FParse::Param(CommandLine, Parameter)) + { + bOutValue = true; + } + else + { + TCHAR TempStr[16]; + if (FParse::Value(CommandLine, Parameter, TempStr, 16) && TempStr[0] == '=') + { + bOutValue = FCString::ToBool(TempStr + 1); // + 1 to skip = + } + } + UE_LOG(LogSpatialGDKSettings, Log, TEXT("%s is %s."), PrettyName, bOutValue ? TEXT("enabled") : TEXT("disabled")); + } +} + USpatialGDKSettings::USpatialGDKSettings(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , EntityPoolInitialReservationCount(3000) @@ -17,31 +39,51 @@ USpatialGDKSettings::USpatialGDKSettings(const FObjectInitializer& ObjectInitial , EntityPoolRefreshCount(2000) , HeartbeatIntervalSeconds(2.0f) , HeartbeatTimeoutSeconds(10.0f) + , HeartbeatTimeoutWithEditorSeconds(10000.0f) , ActorReplicationRateLimit(0) , EntityCreationRateLimit(0) - , UseIsActorRelevantForConnection(false) + , bUseIsActorRelevantForConnection(false) , OpsUpdateRate(1000.0f) , bEnableHandover(true) - , MaxNetCullDistanceSquared(900000000.0f) // Set to twice the default Actor NetCullDistanceSquared (300m) + , MaxNetCullDistanceSquared(0.0f) // Default disabled , QueuedIncomingRPCWaitTime(1.0f) + , QueuedOutgoingRPCWaitTime(30.0f) , PositionUpdateFrequency(1.0f) , PositionDistanceThreshold(100.0f) // 1m (100cm) , bEnableMetrics(true) , bEnableMetricsDisplay(false) , MetricsReportRate(2.0f) , bUseFrameTimeAsLoad(false) - , bCheckRPCOrder(false) - , bBatchSpatialPositionUpdates(true) + , bBatchSpatialPositionUpdates(false) , MaxDynamicallyAttachedSubobjectsPerClass(3) - , bEnableServerQBI(true) - , bPackRPCs(false) - , bUseDevelopmentAuthenticationFlow(false) + , bEnableResultTypes(true) , ServicesRegion(EServicesRegion::Default) , DefaultWorkerType(FWorkerType(SpatialConstants::DefaultServerWorkerType)) , bEnableOffloading(false) , ServerWorkerTypes({ SpatialConstants::DefaultServerWorkerType }) , WorkerLogLevel(ESettingsWorkerLogVerbosity::Warning) , bEnableUnrealLoadBalancer(false) + , bRunSpatialWorkerConnectionOnGameThread(false) + , bUseRPCRingBuffers(true) + , DefaultRPCRingBufferSize(32) + , MaxRPCRingBufferSize(32) + // TODO - UNR 2514 - These defaults are not necessarily optimal - readdress when we have better data + , bTcpNoDelay(false) + , UdpServerUpstreamUpdateIntervalMS(1) + , UdpServerDownstreamUpdateIntervalMS(1) + , UdpClientUpstreamUpdateIntervalMS(1) + , UdpClientDownstreamUpdateIntervalMS(1) + // TODO - end + , bAsyncLoadNewClassesOnEntityCheckout(false) + , RPCQueueWarningDefaultTimeout(2.0f) + , bEnableNetCullDistanceInterest(true) + , bEnableNetCullDistanceFrequency(false) + , FullFrequencyNetCullDistanceRatio(1.0f) + , bUseSecureClientConnection(false) + , bUseSecureServerConnection(false) + , bEnableClientQueriesOnServer(false) + , bUseSpatialView(false) + , bUseDevelopmentAuthenticationFlow(false) { DefaultReceptionistHost = SpatialConstants::LOCAL_HOST; } @@ -52,14 +94,23 @@ void USpatialGDKSettings::PostInitProperties() // Check any command line overrides for using QBI, Offloading (after reading the config value): const TCHAR* CommandLine = FCommandLine::Get(); + CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideSpatialOffloading"), TEXT("Offloading"), bEnableOffloading); + CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideHandover"), TEXT("Handover"), bEnableHandover); + CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideLoadBalancer"), TEXT("Load balancer"), bEnableUnrealLoadBalancer); + CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideRPCRingBuffers"), TEXT("RPC ring buffers"), bUseRPCRingBuffers); + CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideSpatialWorkerConnectionOnGameThread"), TEXT("Spatial worker connection on game thread"), bRunSpatialWorkerConnectionOnGameThread); + CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideResultTypes"), TEXT("Result types"), bEnableResultTypes); + CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideNetCullDistanceInterest"), TEXT("Net cull distance interest"), bEnableNetCullDistanceInterest); + CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideNetCullDistanceInterestFrequency"), TEXT("Net cull distance interest frequency"), bEnableNetCullDistanceFrequency); + CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideActorRelevantForConnection"), TEXT("Actor relevant for connection"), bUseIsActorRelevantForConnection); + CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideBatchSpatialPositionUpdates"), TEXT("Batch spatial position updates"), bBatchSpatialPositionUpdates); - if (FParse::Param(CommandLine, TEXT("OverrideSpatialOffloading"))) + if (bEnableUnrealLoadBalancer) { - bEnableOffloading = true; - } - else - { - FParse::Bool(CommandLine, TEXT("OverrideSpatialOffloading="), bEnableOffloading); + if (bEnableHandover == false) + { + UE_LOG(LogSpatialGDKSettings, Warning, TEXT("Unreal load balancing is enabled, but handover is disabled.")); + } } #if WITH_EDITOR @@ -92,4 +143,65 @@ void USpatialGDKSettings::PostEditChangeProperty(struct FPropertyChangedEvent& P "Failing to do will result in unintended behavior or crashes!")))); } } + +bool USpatialGDKSettings::CanEditChange(const UProperty* InProperty) const +{ + if (!InProperty) + { + return false; + } + + const FName Name = InProperty->GetFName(); + + if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, bUseRPCRingBuffers)) + { + return !bEnableUnrealLoadBalancer; + } + + if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, DefaultRPCRingBufferSize) + || Name == GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, RPCRingBufferSizeMap) + || Name == GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, MaxRPCRingBufferSize)) + { + return UseRPCRingBuffer(); + } + + return true; +} + +#endif + +uint32 USpatialGDKSettings::GetRPCRingBufferSize(ERPCType RPCType) const +{ + if (const uint32* Size = RPCRingBufferSizeMap.Find(RPCType)) + { + return *Size; + } + + return DefaultRPCRingBufferSize; +} + + +bool USpatialGDKSettings::UseRPCRingBuffer() const +{ + // RPC Ring buffer are necessary in order to do RPC handover, something legacy RPC does not handle. + return bUseRPCRingBuffers || bEnableUnrealLoadBalancer; +} + +float USpatialGDKSettings::GetSecondsBeforeWarning(const ERPCResult Result) const +{ + if (const float* CustomSecondsBeforeWarning = RPCQueueWarningTimeouts.Find(Result)) + { + return *CustomSecondsBeforeWarning; + } + + return RPCQueueWarningDefaultTimeout; +} + +bool USpatialGDKSettings::GetPreventClientCloudDeploymentAutoConnect(bool bIsClient) const +{ +#if WITH_EDITOR + return false; +#else + return bIsClient && bPreventClientCloudDeploymentAutoConnect; #endif +}; diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/AuthorityRecord.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/AuthorityRecord.cpp new file mode 100644 index 0000000000..495bf431da --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/AuthorityRecord.cpp @@ -0,0 +1,59 @@ +#include "SpatialView/AuthorityRecord.h" + +namespace SpatialGDK +{ + +void AuthorityRecord::SetAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId, Worker_Authority Authority) +{ + const EntityComponentId Id = {EntityId, ComponentId}; + + switch (Authority) + { + case WORKER_AUTHORITY_NOT_AUTHORITATIVE: + // If the entity-component as recorded as authority-gained then remove it. + // If not then ensure it's only recorded as authority lost. + if (!AuthorityGained.RemoveSingleSwap(Id)) + { + AuthorityLossTemporary.RemoveSingleSwap(Id); + AuthorityLost.Push(Id); + } + break; + case WORKER_AUTHORITY_AUTHORITATIVE: + if (AuthorityLost.RemoveSingleSwap(Id)) + { + AuthorityLossTemporary.Push(Id); + } + else + { + AuthorityGained.Push(Id); + } + break; + case WORKER_AUTHORITY_AUTHORITY_LOSS_IMMINENT: + // Deliberately ignore loss imminent. + break; + } +} + +void AuthorityRecord::Clear() +{ + AuthorityGained.Empty(); + AuthorityLost.Empty(); + AuthorityLossTemporary.Empty(); +} + +const TArray& AuthorityRecord::GetAuthorityGained() const +{ + return AuthorityGained; +} + +const TArray& AuthorityRecord::GetAuthorityLost() const +{ + return AuthorityLost; +} + +const TArray& AuthorityRecord::GetAuthorityLostTemporarily() const +{ + return AuthorityLossTemporary; +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewCoordinator.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewCoordinator.cpp new file mode 100644 index 0000000000..3ef57076c2 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewCoordinator.cpp @@ -0,0 +1,55 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialView/ViewCoordinator.h" + +namespace SpatialGDK +{ + +ViewCoordinator::ViewCoordinator(TUniquePtr ConnectionHandler) +: ConnectionHandler(MoveTemp(ConnectionHandler)) +{ + Delta = View.GenerateViewDelta(); +} + +void ViewCoordinator::Advance() +{ + ConnectionHandler->Advance(); + const uint32 OpListCount = ConnectionHandler->GetOpListCount(); + for (uint32 i = 0; i < OpListCount; ++i) + { + View.EnqueueOpList(ConnectionHandler->GetNextOpList()); + } + Delta = View.GenerateViewDelta(); +} + +void ViewCoordinator::FlushMessagesToSend() +{ + ConnectionHandler->SendMessages(View.FlushLocalChanges()); +} + +const TArray& ViewCoordinator::GetCreateEntityResponses() const +{ + return Delta->GetCreateEntityResponses(); +} + +const TArray& ViewCoordinator::GetAuthorityGained() const +{ + return Delta->GetAuthorityGained(); +} + +const TArray& ViewCoordinator::GetAuthorityLost() const +{ + return Delta->GetAuthorityLost(); +} + +const TArray& ViewCoordinator::GetAuthorityLostTemporarily() const +{ + return Delta->GetAuthorityLostTemporarily(); +} + +TUniquePtr ViewCoordinator::GenerateLegacyOpList() const +{ + return Delta->GenerateLegacyOpList(); +} + +} // SpatialView diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewDelta.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewDelta.cpp new file mode 100644 index 0000000000..aef4e1d6bb --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewDelta.cpp @@ -0,0 +1,119 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialView/ViewDelta.h" +#include "SpatialView/OpList/ViewDeltaLegacyOpList.h" +#include "Containers/StringConv.h" + +namespace SpatialGDK +{ + +void ViewDelta::AddCreateEntityResponse(CreateEntityResponse Response) +{ + CreateEntityResponses.Push(MoveTemp(Response)); +} + +void ViewDelta::SetAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId, Worker_Authority Authority) +{ + AuthorityChanges.SetAuthority(EntityId, ComponentId, Authority); +} + +const TArray& ViewDelta::GetCreateEntityResponses() const +{ + return CreateEntityResponses; +} + +const TArray& ViewDelta::GetAuthorityGained() const +{ + return AuthorityChanges.GetAuthorityGained(); +} + +const TArray& ViewDelta::GetAuthorityLost() const +{ + return AuthorityChanges.GetAuthorityLost(); +} + +const TArray& ViewDelta::GetAuthorityLostTemporarily() const +{ + return AuthorityChanges.GetAuthorityLostTemporarily(); +} + +TUniquePtr ViewDelta::GenerateLegacyOpList() const +{ + // Todo - refactor individual op creation to an oplist type. + TArray OpList; + OpList.Reserve(CreateEntityResponses.Num()); + + // todo Entity added ops get created here. + + // todo Component Added ops get created here. + + for (const EntityComponentId& Id : AuthorityChanges.GetAuthorityLost()) + { + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_AUTHORITY_CHANGE; + Op.op.authority_change.entity_id = Id.EntityId; + Op.op.authority_change.component_id = Id.ComponentId; + Op.op.authority_change.authority = WORKER_AUTHORITY_NOT_AUTHORITATIVE; + OpList.Push(Op); + } + + for (const EntityComponentId& Id : AuthorityChanges.GetAuthorityLostTemporarily()) + { + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_AUTHORITY_CHANGE; + Op.op.authority_change.entity_id = Id.EntityId; + Op.op.authority_change.component_id = Id.ComponentId; + Op.op.authority_change.authority = WORKER_AUTHORITY_NOT_AUTHORITATIVE; + OpList.Push(Op); + } + + // todo Component update and remove ops get created here. + + // todo Entity removed ops get created here or below. + + for (const EntityComponentId& Id : AuthorityChanges.GetAuthorityLostTemporarily()) + { + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_AUTHORITY_CHANGE; + Op.op.authority_change.entity_id = Id.EntityId; + Op.op.authority_change.component_id = Id.ComponentId; + Op.op.authority_change.authority = WORKER_AUTHORITY_AUTHORITATIVE; + OpList.Push(Op); + } + + for (const EntityComponentId& Id : AuthorityChanges.GetAuthorityGained()) + { + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_AUTHORITY_CHANGE; + Op.op.authority_change.entity_id = Id.EntityId; + Op.op.authority_change.component_id = Id.ComponentId; + Op.op.authority_change.authority = WORKER_AUTHORITY_AUTHORITATIVE; + OpList.Push(Op); + } + + // todo Command requests ops are created here. + + // The following ops do not have ordering constraints. + + for (const CreateEntityResponse& Response : CreateEntityResponses) + { + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_CREATE_ENTITY_RESPONSE; + Op.op.create_entity_response.request_id = Response.RequestId; + Op.op.create_entity_response.status_code = Response.StatusCode; + // TODO: UNR-3163 - the string is located on a stack and gets corrupted + Op.op.create_entity_response.message = TCHAR_TO_UTF8(Response.Message.GetCharArray().GetData()); + Op.op.create_entity_response.entity_id = Response.EntityId; + OpList.Push(Op); + } + + return MakeUnique(MoveTemp(OpList)); +} + +void ViewDelta::Clear() +{ + CreateEntityResponses.Empty(); + AuthorityChanges.Clear(); +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/WorkerView.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/WorkerView.cpp new file mode 100644 index 0000000000..caa3acb2d3 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/WorkerView.cpp @@ -0,0 +1,104 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialView/WorkerView.h" +#include "SpatialView/MessagesToSend.h" + +namespace SpatialGDK +{ + +WorkerView::WorkerView() +: LocalChanges(MakeUnique()) +{ +} + +const ViewDelta* WorkerView::GenerateViewDelta() +{ + Delta.Clear(); + for (const auto& OpList : QueuedOps) + { + const uint32 OpCount = OpList->GetCount(); + for (uint32 i = 0; i < OpCount; ++i) + { + ProcessOp((*OpList)[i]); + } + } + + return Δ +} + +void WorkerView::EnqueueOpList(TUniquePtr OpList) +{ + QueuedOps.Push(MoveTemp(OpList)); +} + +TUniquePtr WorkerView::FlushLocalChanges() +{ + TUniquePtr OutgoingMessages = MoveTemp(LocalChanges); + LocalChanges = MakeUnique(); + return OutgoingMessages; +} + +void WorkerView::SendCreateEntityRequest(CreateEntityRequest Request) +{ + LocalChanges->CreateEntityRequests.Push(MoveTemp(Request)); +} + +void WorkerView::ProcessOp(const Worker_Op& Op) +{ + switch (static_cast(Op.op_type)) + { + case WORKER_OP_TYPE_DISCONNECT: + break; + case WORKER_OP_TYPE_FLAG_UPDATE: + break; + case WORKER_OP_TYPE_LOG_MESSAGE: + break; + case WORKER_OP_TYPE_METRICS: + break; + case WORKER_OP_TYPE_CRITICAL_SECTION: + break; + case WORKER_OP_TYPE_ADD_ENTITY: + break; + case WORKER_OP_TYPE_REMOVE_ENTITY: + break; + case WORKER_OP_TYPE_RESERVE_ENTITY_IDS_RESPONSE: + break; + case WORKER_OP_TYPE_CREATE_ENTITY_RESPONSE: + HandleCreateEntityResponse(Op.op.create_entity_response); + break; + case WORKER_OP_TYPE_DELETE_ENTITY_RESPONSE: + break; + case WORKER_OP_TYPE_ENTITY_QUERY_RESPONSE: + break; + case WORKER_OP_TYPE_ADD_COMPONENT: + break; + case WORKER_OP_TYPE_REMOVE_COMPONENT: + break; + case WORKER_OP_TYPE_AUTHORITY_CHANGE: + HandleAuthorityChange(Op.op.authority_change); + break; + case WORKER_OP_TYPE_COMPONENT_UPDATE: + break; + case WORKER_OP_TYPE_COMMAND_REQUEST: + break; + case WORKER_OP_TYPE_COMMAND_RESPONSE: + break; + } +} + +void WorkerView::HandleAuthorityChange(const Worker_AuthorityChangeOp& AuthorityChange) +{ + Delta.SetAuthority(AuthorityChange.entity_id, AuthorityChange.component_id, static_cast(AuthorityChange.authority)); +} + +void WorkerView::HandleCreateEntityResponse(const Worker_CreateEntityResponseOp& Response) +{ + Delta.AddCreateEntityResponse(CreateEntityResponse{ + Response.request_id, + static_cast(Response.status_code), + FString{Response.message}, + Response.entity_id + }); +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/RPCServiceTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/RPCServiceTest.cpp new file mode 100644 index 0000000000..7536941964 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/RPCServiceTest.cpp @@ -0,0 +1,498 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "CoreMinimal.h" +#include "Interop/SpatialRPCService.h" +#include "Interop/SpatialStaticComponentView.h" +#include "Schema/RPCPayload.h" +#include "SpatialConstants.h" +#include "SpatialGDKSettings.h" +#include "Tests/TestingComponentViewHelpers.h" +#include "Tests/TestDefinitions.h" +#include "Utils/RPCRingBuffer.h" + +#define RPC_SERVICE_TEST(TestName) \ + GDK_TEST(Core, SpatialRPCService, TestName) + +// Test Globals +namespace +{ + +enum ERPCEndpointType : uint8_t +{ + SERVER_AUTH, + CLIENT_AUTH, + SERVER_AND_CLIENT_AUTH, + NO_AUTH +}; + +struct EntityPayload +{ + EntityPayload(Worker_EntityId InEntityID, const SpatialGDK::RPCPayload& InPayload) + : EntityId(InEntityID) + , Payload(InPayload) + {} + + Worker_EntityId EntityId; + SpatialGDK::RPCPayload Payload; +}; + +constexpr Worker_EntityId RPCTestEntityId_1 = 201; +constexpr Worker_EntityId RPCTestEntityId_2 = 42; + +const SpatialGDK::RPCPayload SimplePayload = SpatialGDK::RPCPayload(1, 0, TArray({ 1 }, 1)); + +ExtractRPCDelegate DefaultRPCDelegate = ExtractRPCDelegate::CreateLambda([](Worker_EntityId EntityId, ERPCType RPCType, const SpatialGDK::RPCPayload& Payload) { + return true; +}); + + +Worker_Authority GetClientAuthorityFromRPCEndpointType(ERPCEndpointType RPCEndpointType) +{ + switch (RPCEndpointType) + { + case CLIENT_AUTH: + case SERVER_AND_CLIENT_AUTH: + return WORKER_AUTHORITY_AUTHORITATIVE; + break; + case SERVER_AUTH: + default: + return WORKER_AUTHORITY_NOT_AUTHORITATIVE; + break; + } +} + +Worker_Authority GetServerAuthorityFromRPCEndpointType(ERPCEndpointType RPCEndpointType) +{ + switch (RPCEndpointType) + { + case SERVER_AUTH: + case SERVER_AND_CLIENT_AUTH: + return WORKER_AUTHORITY_AUTHORITATIVE; + break; + case CLIENT_AUTH: + default: + return WORKER_AUTHORITY_NOT_AUTHORITATIVE; + break; + } +} + +Worker_Authority GetMulticastAuthorityFromRPCEndpointType(ERPCEndpointType RPCEndpointType) +{ + return GetServerAuthorityFromRPCEndpointType(RPCEndpointType); +} + +void AddEntityToStaticComponentView(USpatialStaticComponentView& StaticComponentView, Worker_EntityId EntityId, ERPCEndpointType RPCEndpointType) +{ + TestingComponentViewHelpers::AddEntityComponentToStaticComponentView(StaticComponentView, + EntityId, SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID, + GetClientAuthorityFromRPCEndpointType(RPCEndpointType)); + + TestingComponentViewHelpers::AddEntityComponentToStaticComponentView(StaticComponentView, + EntityId, SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID, + GetServerAuthorityFromRPCEndpointType(RPCEndpointType)); + + TestingComponentViewHelpers::AddEntityComponentToStaticComponentView(StaticComponentView, + EntityId, SpatialConstants::MULTICAST_RPCS_COMPONENT_ID, + GetMulticastAuthorityFromRPCEndpointType(RPCEndpointType)); +}; + +USpatialStaticComponentView* CreateStaticComponentView(const TArray& EntityIdArray, ERPCEndpointType RPCEndpointType) +{ + USpatialStaticComponentView* StaticComponentView = NewObject(); + for (Worker_EntityId EntityId : EntityIdArray) + { + AddEntityToStaticComponentView(*StaticComponentView, EntityId, RPCEndpointType); + } + return StaticComponentView; +} + +SpatialGDK::SpatialRPCService CreateRPCService(const TArray& EntityIdArray, + ERPCEndpointType RPCEndpointType, + ExtractRPCDelegate RPCDelegate = DefaultRPCDelegate, + USpatialStaticComponentView* StaticComponentView = nullptr) +{ + if (StaticComponentView == nullptr) + { + StaticComponentView = CreateStaticComponentView(EntityIdArray, RPCEndpointType); + } + + SpatialGDK::SpatialRPCService RPCService = SpatialGDK::SpatialRPCService(RPCDelegate, StaticComponentView); + + for (Worker_EntityId EntityId : EntityIdArray) + { + if (GetClientAuthorityFromRPCEndpointType(RPCEndpointType) == WORKER_AUTHORITY_AUTHORITATIVE) + { + RPCService.OnEndpointAuthorityGained(EntityId, SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID); + } + + if (GetServerAuthorityFromRPCEndpointType(RPCEndpointType) == WORKER_AUTHORITY_AUTHORITATIVE) + { + RPCService.OnEndpointAuthorityGained(EntityId, SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID); + } + } + + return RPCService; +} + +bool CompareRPCPayload(const SpatialGDK::RPCPayload& Payload1, const SpatialGDK::RPCPayload& Payload2) +{ + return Payload1.Index == Payload2.Index && + Payload1.Offset == Payload2.Offset && + Payload1.PayloadData == Payload2.PayloadData; +} + +bool CompareSchemaObjectToSendAndPayload(Schema_Object* SchemaObject, const SpatialGDK::RPCPayload& Payload, ERPCType RPCType, uint64 RPCId) +{ + SpatialGDK::RPCRingBufferDescriptor Descriptor = SpatialGDK::RPCRingBufferUtils::GetRingBufferDescriptor(RPCType); + Schema_Object* RPCObject = Schema_GetObject(SchemaObject, Descriptor.GetRingBufferElementFieldId(RPCId)); + return CompareRPCPayload(SpatialGDK::RPCPayload(RPCObject), Payload); +} + +bool CompareUpdateToSendAndEntityPayload(SpatialGDK::SpatialRPCService::UpdateToSend& Update, const EntityPayload& EntityPayloadItem, ERPCType RPCType, uint64 RPCId) +{ + return CompareSchemaObjectToSendAndPayload(Schema_GetComponentUpdateFields(Update.Update.schema_type), EntityPayloadItem.Payload, RPCType, RPCId) && + Update.EntityId == EntityPayloadItem.EntityId; +} + +bool CompareComponentDataAndEntityPayload(const Worker_ComponentData& ComponentData, const EntityPayload& EntityPayloadItem, ERPCType RPCType, uint64 RPCId) +{ + return CompareSchemaObjectToSendAndPayload(Schema_GetComponentDataFields(ComponentData.schema_type), EntityPayloadItem.Payload, RPCType, RPCId); +} + +Worker_ComponentData GetComponentDataOnEntityCreationFromRPCService(SpatialGDK::SpatialRPCService& RPCService, Worker_EntityId EntityID, ERPCType RPCType) +{ + Worker_ComponentId ExpectedUpdateComponentId = SpatialGDK::RPCRingBufferUtils::GetRingBufferComponentId(RPCType); + TArray ComponentDataArray = RPCService.GetRPCComponentsOnEntityCreation(EntityID); + + const Worker_ComponentData* ComponentData = ComponentDataArray.FindByPredicate([ExpectedUpdateComponentId](const Worker_ComponentData& CompData) { + return CompData.component_id == ExpectedUpdateComponentId; + }); + + if (ComponentData == nullptr) + { + return {}; + } + return *ComponentData; +} + +} // anonymous namespace + +RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_client_reliable_rpcs_to_the_service_THEN_rpc_push_result_success) +{ + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload); + TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::Success)); + return true; +} + +RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_client_unreliable_rpcs_to_the_service_THEN_rpc_push_result_success) +{ + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientUnreliable, SimplePayload); + TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::Success)); + return true; +} + +RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_server_reliable_rpcs_to_the_service_THEN_rpc_push_result_no_buffer_authority) +{ + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerReliable, SimplePayload); + TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority)); + return true; +} + +RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_server_unreliable_rpcs_to_the_service_THEN_rpc_push_result_no_buffer_authority) +{ + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerUnreliable, SimplePayload); + TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority)); + return true; +} + +RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_client_reliable_rpcs_to_the_service_THEN_rpc_push_result_no_buffer_authority) +{ + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload); + TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority)); + return true; +} + +RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_client_unreliable_rpcs_to_the_service_THEN_rpc_push_result_no_buffer_authority) +{ + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientUnreliable, SimplePayload); + TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority)); + return true; +} + +RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_server_reliable_rpcs_to_the_service_THEN_rpc_push_result_success) +{ + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerReliable, SimplePayload); + TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::Success)); + return true; +} + +RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_server_unreliable_rpcs_to_the_service_THEN_rpc_push_result_success) +{ + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerUnreliable, SimplePayload); + TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::Success)); + return true; +} + +RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_multicast_rpcs_to_the_service_THEN_rpc_push_result_no_buffer_authority) +{ + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload); + TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority)); + return true; +} + +RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_multicast_rpcs_to_the_service_THEN_rpc_push_result_success) +{ + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload); + TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::Success)); + return true; +} + +RPC_SERVICE_TEST(GIVEN_authority_over_server_and_client_endpoint_WHEN_push_rpcs_to_the_service_THEN_rpc_push_result_has_ack_authority) +{ + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AND_CLIENT_AUTH); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload); + TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::HasAckAuthority)); + return true; +} + +RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_overflow_client_reliable_rpcs_to_the_service_THEN_rpc_push_result_queue_overflowed) +{ + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH); + + // Send RPCs to the point where we will overflow + uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ClientReliable); + for (uint32 i = 0; i < RPCsToSend; ++i) + { + RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload); + } + + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload); + TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::QueueOverflowed)); + return true; +} + +RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_overflow_client_unreliable_rpcs_to_the_service_THEN_rpc_push_result_drop_overflow) +{ + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH); + + // Send RPCs to the point where we will overflow + uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ClientUnreliable); + for (uint32 i = 0; i < RPCsToSend; ++i) + { + RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientUnreliable, SimplePayload); + } + + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientUnreliable, SimplePayload); + TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::DropOverflowed)); + return true; +} + +RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_overflow_client_reliable_rpcs_to_the_service_THEN_rpc_push_result_queue_overflowed) +{ + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH); + + // Send RPCs to the point where we will overflow + uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ServerReliable); + for (uint32 i = 0; i < RPCsToSend; ++i) + { + RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerReliable, SimplePayload); + } + + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerReliable, SimplePayload); + TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::QueueOverflowed)); + return true; +} + +RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_overflow_client_unreliable_rpcs_to_the_service_THEN_rpc_push_result_drop_overflow) +{ + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH); + + // Send RPCs to the point where we will overflow + uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ServerUnreliable); + for (uint32 i = 0; i < RPCsToSend; ++i) + { + RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerUnreliable, SimplePayload); + } + + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerUnreliable, SimplePayload); + TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::DropOverflowed)); + return true; +} + +RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_overflow_multicast_rpcs_to_the_service_THEN_rpc_push_result_success) +{ + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH); + + // Send RPCs to the point where we will overflow + uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::NetMulticast); + for (uint32 i = 0; i < RPCsToSend; ++i) + { + RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload); + } + + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload); + TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::Success)); + return true; +} + +RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_server_unreliable_rpcs_to_the_service_THEN_payloads_are_writen_correctly_to_component_updates) +{ + TArray EntityPayloads; + EntityPayloads.Add(EntityPayload(RPCTestEntityId_1, SimplePayload)); + EntityPayloads.Add(EntityPayload(RPCTestEntityId_2, SimplePayload)); + + TArray EntityIdArray; + EntityIdArray.Add(RPCTestEntityId_1); + EntityIdArray.Add(RPCTestEntityId_2); + + SpatialGDK::SpatialRPCService RPCService = CreateRPCService(EntityIdArray, CLIENT_AUTH); + for (const EntityPayload& EntityPayloadItem : EntityPayloads) + { + RPCService.PushRPC(EntityPayloadItem.EntityId, ERPCType::ServerUnreliable, EntityPayloadItem.Payload); + } + + TArray UpdateToSendArray = RPCService.GetRPCsAndAcksToSend(); + + bool bTestPassed = true; + if (UpdateToSendArray.Num() != EntityPayloads.Num()) + { + bTestPassed = false; + } + else + { + for (int i = 0; i < EntityPayloads.Num(); ++i) + { + if (!CompareUpdateToSendAndEntityPayload(UpdateToSendArray[i], EntityPayloads[i], ERPCType::ServerUnreliable, 1)) + { + bTestPassed = false; + break; + } + } + } + + TestTrue("UpdateToSend have expected payloads", bTestPassed); + return true; +} + +RPC_SERVICE_TEST(GIVEN_no_authority_over_rpc_endpoint_WHEN_push_client_reliable_rpcs_to_the_service_THEN_component_data_matches_payload) +{ + // Create RPCService with empty component view + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({}, NO_AUTH); + + RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload); + + Worker_ComponentData ComponentData = GetComponentDataOnEntityCreationFromRPCService(RPCService, RPCTestEntityId_1, ERPCType::ClientReliable); + bool bTestPassed = CompareComponentDataAndEntityPayload(ComponentData, EntityPayload(RPCTestEntityId_1, SimplePayload), ERPCType::ClientReliable, 1); + TestTrue("Entity creation test returned expected results", bTestPassed); + return true; +} + +RPC_SERVICE_TEST(GIVEN_no_authority_over_rpc_endpoint_WHEN_push_multicast_rpcs_to_the_service_THEN_initially_present_set) +{ + // Create RPCService with empty component view + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({}, NO_AUTH); + + RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload); + RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload); + + Worker_ComponentData ComponentData = GetComponentDataOnEntityCreationFromRPCService(RPCService, RPCTestEntityId_1, ERPCType::NetMulticast); + const Schema_Object* SchemaObject = Schema_GetComponentDataFields(ComponentData.schema_type); + uint32 InitiallyPresent = Schema_GetUint32(SchemaObject, SpatialGDK::RPCRingBufferUtils::GetInitiallyPresentMulticastRPCsCountFieldId()); + TestTrue("Entity creation multicast test returned expected results", (InitiallyPresent == 2)); + return true; +} + +RPC_SERVICE_TEST(GIVEN_client_endpoint_with_rpcs_in_view_and_authority_over_server_endpoint_WHEN_extract_rpcs_from_the_service_THEN_extracted_payloads_match_pushed_payloads) +{ + USpatialStaticComponentView* StaticComponentView = NewObject(); + + Schema_ComponentData* ClientComponentData = Schema_CreateComponentData(); + Schema_Object* ClientSchemaObject = Schema_GetComponentDataFields(ClientComponentData); + SpatialGDK::RPCRingBufferUtils::WriteRPCToSchema(ClientSchemaObject, ERPCType::ClientReliable, 1, SimplePayload); + SpatialGDK::RPCRingBufferUtils::WriteRPCToSchema(ClientSchemaObject, ERPCType::ClientReliable, 2, SimplePayload); + + TestingComponentViewHelpers::AddEntityComponentToStaticComponentView(*StaticComponentView, + RPCTestEntityId_1, SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID, + ClientComponentData, + GetClientAuthorityFromRPCEndpointType(SERVER_AUTH)); + + TestingComponentViewHelpers::AddEntityComponentToStaticComponentView(*StaticComponentView, + RPCTestEntityId_1, SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID, + GetServerAuthorityFromRPCEndpointType(SERVER_AUTH)); + + int RPCsExtracted = 0; + bool bPayloadsMatch = true; + ExtractRPCDelegate RPCDelegate = ExtractRPCDelegate::CreateLambda([&RPCsExtracted, &bPayloadsMatch](Worker_EntityId EntityId, ERPCType RPCType, const SpatialGDK::RPCPayload& Payload) { + RPCsExtracted++; + bPayloadsMatch &= CompareRPCPayload(Payload, SimplePayload); + bPayloadsMatch &= EntityId == RPCTestEntityId_1; + return true; + }); + + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH, RPCDelegate, StaticComponentView); + RPCService.ExtractRPCsForEntity(RPCTestEntityId_1, SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID); + + TestTrue("Extracted RPCs match expected payloads", (RPCsExtracted == 2 && bPayloadsMatch)); + return true; +} + +RPC_SERVICE_TEST(GIVEN_receiving_an_rpc_WHEN_return_false_from_extract_callback_THEN_some_rpcs_persist_on_component) +{ + USpatialStaticComponentView* StaticComponentView = NewObject(); + + Schema_ComponentData* ClientComponentData = Schema_CreateComponentData(); + Schema_Object* ClientSchemaObject = Schema_GetComponentDataFields(ClientComponentData); + SpatialGDK::RPCRingBufferUtils::WriteRPCToSchema(ClientSchemaObject, ERPCType::ClientReliable, 1, SimplePayload); + SpatialGDK::RPCRingBufferUtils::WriteRPCToSchema(ClientSchemaObject, ERPCType::ClientReliable, 2, SimplePayload); + SpatialGDK::RPCRingBufferUtils::WriteRPCToSchema(ClientSchemaObject, ERPCType::ClientReliable, 3, SimplePayload); + SpatialGDK::RPCRingBufferUtils::WriteRPCToSchema(ClientSchemaObject, ERPCType::ClientReliable, 4, SimplePayload); + SpatialGDK::RPCRingBufferUtils::WriteRPCToSchema(ClientSchemaObject, ERPCType::ClientReliable, 5, SimplePayload); + + TestingComponentViewHelpers::AddEntityComponentToStaticComponentView(*StaticComponentView, + RPCTestEntityId_1, SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID, + ClientComponentData, + GetClientAuthorityFromRPCEndpointType(SERVER_AUTH)); + + TestingComponentViewHelpers::AddEntityComponentToStaticComponentView(*StaticComponentView, + RPCTestEntityId_1, SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID, + GetServerAuthorityFromRPCEndpointType(SERVER_AUTH)); + + constexpr int MaxRPCsToProccess = 2; + int RPCsToProcess = MaxRPCsToProccess; + ExtractRPCDelegate RPCDelegate = ExtractRPCDelegate::CreateLambda([&RPCsToProcess](Worker_EntityId EntityId, ERPCType RPCType, const SpatialGDK::RPCPayload& Payload) { + --RPCsToProcess; + return RPCsToProcess >= 0; + }); + + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH, RPCDelegate, StaticComponentView); + + RPCService.ExtractRPCsForEntity(RPCTestEntityId_1, SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID); + + TArray UpdateToSendArray = RPCService.GetRPCsAndAcksToSend(); + + bool bTestPassed = false; + SpatialGDK::SpatialRPCService::UpdateToSend* Update = UpdateToSendArray.FindByPredicate([](const SpatialGDK::SpatialRPCService::UpdateToSend& UpdateToSend) { + return (UpdateToSend.Update.component_id == SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID); + }); + + if (Update != nullptr) + { + const Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update->Update.schema_type); + uint64 Ack = 0; + SpatialGDK::RPCRingBufferUtils::ReadAckFromSchema(ComponentObject, ERPCType::ClientReliable, Ack); + bTestPassed = MaxRPCsToProccess == Ack; + } + + TestTrue("Returning false in extraction callback correctly stopped processing RPCs", bTestPassed); + return true; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/AuthorityRecordTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/AuthorityRecordTest.cpp new file mode 100644 index 0000000000..ce62590273 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/AuthorityRecordTest.cpp @@ -0,0 +1,145 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/TestDefinitions.h" + +#include "SpatialView/AuthorityRecord.h" + +#define AUTHORITYRECORD_TEST(TestName) \ + GDK_TEST(Core, AuthorityRecord, TestName) + +using namespace SpatialGDK; + +namespace +{ + class AuthorityChangeRecordFixture + { + public: + const Worker_EntityId kTestEntityId = 1337; + const Worker_ComponentId kTestComponentId = 1338; + + const EntityComponentId kEntityComponentId{ kTestEntityId, kTestComponentId }; + + AuthorityRecord Record; + + TArray ExpectedAuthorityGained; + TArray ExpectedAuthorityLost; + TArray ExpectedAuthorityLostTemporarily; + }; +} // anonymous namespace + +AUTHORITYRECORD_TEST(GIVEN_EmptyAuthorityRecord_WHEN_set_to_authoritative_THEN_AuthorityRecord_has_AuthorityGainedRecord) +{ + // GIVEN + AuthorityChangeRecordFixture Fixture; + + // WHEN + Fixture.Record.SetAuthority(Fixture.kTestEntityId, Fixture.kTestComponentId, WORKER_AUTHORITY_AUTHORITATIVE); + + // THEN + Fixture.ExpectedAuthorityGained.Push(Fixture.kEntityComponentId); + TestTrue(TEXT("Comparing AuthorityGained"), Fixture.Record.GetAuthorityGained() == Fixture.ExpectedAuthorityGained); + TestTrue(TEXT("Comparing AuthorityLost"), Fixture.Record.GetAuthorityLost() == Fixture.ExpectedAuthorityLost); + TestTrue(TEXT("Comparing AuthorityLostTemporarily"), Fixture.Record.GetAuthorityLostTemporarily() == Fixture.ExpectedAuthorityLostTemporarily); + + return true; +} + +AUTHORITYRECORD_TEST(GIVEN_AuthorityRecord_with_AuthoritativeRecord_WHEN_set_to_NonAuthoritative_THEN_AuthorityRecord_has_no_records) +{ + // GIVEN + AuthorityChangeRecordFixture Fixture; + Fixture.Record.SetAuthority(Fixture.kTestEntityId, Fixture.kTestComponentId, WORKER_AUTHORITY_AUTHORITATIVE); + + // WHEN + Fixture.Record.SetAuthority(Fixture.kTestEntityId, Fixture.kTestComponentId, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + + // THEN + TestTrue(TEXT("Comparing AuthorityGained"), Fixture.Record.GetAuthorityGained() == Fixture.ExpectedAuthorityGained); + TestTrue(TEXT("Comparing AuthorityLost"), Fixture.Record.GetAuthorityLost() == Fixture.ExpectedAuthorityLost); + TestTrue(TEXT("Comparing AuthorityLostTemporarily"), Fixture.Record.GetAuthorityLostTemporarily() == Fixture.ExpectedAuthorityLostTemporarily); + + return true; +} + +AUTHORITYRECORD_TEST(GIVEN_empty_AuthorityRecord_WHEN_set_to_NonAuthoritative_THEN_has_AuthorityLostRecord) +{ + // GIVEN + AuthorityChangeRecordFixture Fixture; + + // WHEN + Fixture.Record.SetAuthority(Fixture.kTestEntityId, Fixture.kTestComponentId, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + + // THEN + Fixture.ExpectedAuthorityLost.Push(Fixture.kEntityComponentId); + TestTrue(TEXT("Comparing AuthorityGained"), Fixture.Record.GetAuthorityGained() == Fixture.ExpectedAuthorityGained); + TestTrue(TEXT("Comparing AuthorityLost"), Fixture.Record.GetAuthorityLost() == Fixture.ExpectedAuthorityLost); + TestTrue(TEXT("Comparing AuthorityLostTemporarily"), Fixture.Record.GetAuthorityLostTemporarily() == Fixture.ExpectedAuthorityLostTemporarily); + + return true; +} + +AUTHORITYRECORD_TEST(GIVEN_AuthorityRecord_with_NonAuthoritativeRecord_WHEN_set_to_Authoritative_THEN_has_AuthorityLostTemporarilyRecord) +{ + // GIVEN + AuthorityChangeRecordFixture Fixture; + Fixture.Record.SetAuthority(Fixture.kTestEntityId, Fixture.kTestComponentId, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + + // WHEN + Fixture.Record.SetAuthority(Fixture.kTestEntityId, Fixture.kTestComponentId, WORKER_AUTHORITY_AUTHORITATIVE); + + // THEN + Fixture.ExpectedAuthorityLostTemporarily.Push(Fixture.kEntityComponentId); + TestTrue(TEXT("Comparing AuthorityGained"), Fixture.Record.GetAuthorityGained() == Fixture.ExpectedAuthorityGained); + TestTrue(TEXT("Comparing AuthorityLost"), Fixture.Record.GetAuthorityLost() == Fixture.ExpectedAuthorityLost); + TestTrue(TEXT("Comparing AuthorityLostTemporarily"), Fixture.Record.GetAuthorityLostTemporarily() == Fixture.ExpectedAuthorityLostTemporarily); + + return true; +} + +AUTHORITYRECORD_TEST(GIVEN_AuthorityRecord_with_NonAuthoritativeRecord_WHEN_set_to_Authoritative_and_NonAuthoritative_THEN_has_AuthorityLostRecord) +{ + // GIVEN + AuthorityChangeRecordFixture Fixture; + Fixture.Record.SetAuthority(Fixture.kTestEntityId, Fixture.kTestComponentId, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + + // WHEN + Fixture.Record.SetAuthority(Fixture.kTestEntityId, Fixture.kTestComponentId, WORKER_AUTHORITY_AUTHORITATIVE); + Fixture.Record.SetAuthority(Fixture.kTestEntityId, Fixture.kTestComponentId, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + + // THEN + Fixture.ExpectedAuthorityLost.Push(Fixture.kEntityComponentId); + TestTrue(TEXT("Comparing AuthorityGained"), Fixture.Record.GetAuthorityGained() == Fixture.ExpectedAuthorityGained); + TestTrue(TEXT("Comparing AuthorityLost"), Fixture.Record.GetAuthorityLost() == Fixture.ExpectedAuthorityLost); + TestTrue(TEXT("Comparing AuthorityLostTemporarily"), Fixture.Record.GetAuthorityLostTemporarily() == Fixture.ExpectedAuthorityLostTemporarily); + + return true; +} + +AUTHORITYRECORD_TEST(GIVEN_AuthorityRecord_with_AuthoritativeRecord_NonAuthoritativeRecord_and_AuthorityLostTemporarilyRecorde_WHEN_Cleared_THEN_has_no_records) +{ + // GIVEN + AuthorityChangeRecordFixture Fixture; + Fixture.Record.SetAuthority(Fixture.kTestEntityId, 1, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + Fixture.Record.SetAuthority(Fixture.kTestEntityId, 2, WORKER_AUTHORITY_AUTHORITATIVE); + Fixture.Record.SetAuthority(Fixture.kTestEntityId, 3, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + Fixture.Record.SetAuthority(Fixture.kTestEntityId, 3, WORKER_AUTHORITY_AUTHORITATIVE); + Fixture.ExpectedAuthorityLost.Push(EntityComponentId{ Fixture.kTestEntityId, 1 }); + Fixture.ExpectedAuthorityGained.Push(EntityComponentId{ Fixture.kTestEntityId, 2 }); + Fixture.ExpectedAuthorityLostTemporarily.Push(EntityComponentId{ Fixture.kTestEntityId, 3 }); + TestTrue(TEXT("Comparing AuthorityGained"), Fixture.Record.GetAuthorityGained() == Fixture.ExpectedAuthorityGained); + TestTrue(TEXT("Comparing AuthorityLost"), Fixture.Record.GetAuthorityLost() == Fixture.ExpectedAuthorityLost); + TestTrue(TEXT("Comparing AuthorityLostTemporarily"), Fixture.Record.GetAuthorityLostTemporarily() == Fixture.ExpectedAuthorityLostTemporarily); + + // WHEN + Fixture.Record.Clear(); + + // THEN + Fixture.ExpectedAuthorityLost.Empty(); + Fixture.ExpectedAuthorityGained.Empty(); + Fixture.ExpectedAuthorityLostTemporarily.Empty(); + TestTrue(TEXT("Comparing AuthorityGained"), Fixture.Record.GetAuthorityGained() == Fixture.ExpectedAuthorityGained); + TestTrue(TEXT("Comparing AuthorityLost"), Fixture.Record.GetAuthorityLost() == Fixture.ExpectedAuthorityLost); + TestTrue(TEXT("Comparing AuthorityLostTemporarily"), Fixture.Record.GetAuthorityLostTemporarily() == Fixture.ExpectedAuthorityLostTemporarily); + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/ViewDeltaTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/ViewDeltaTest.cpp new file mode 100644 index 0000000000..f88339aa03 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/ViewDeltaTest.cpp @@ -0,0 +1,76 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/TestDefinitions.h" + +#include "SpatialView/ViewDelta.h" + +#define VIEWDELTA_TEST(TestName) \ + GDK_TEST(Core, ViewDelta, TestName) + +using namespace SpatialGDK; + +VIEWDELTA_TEST(GIVEN_ViewDelta_with_multiple_CreateEntityResponse_added_WHEN_GetCreateEntityResponse_called_THEN_multiple_CreateEntityResponses_returned) +{ + // GIVEN + ViewDelta Delta; + CreateEntityResponse Response{}; + Delta.AddCreateEntityResponse(Response); + Delta.AddCreateEntityResponse(Response); + + // WHEN + auto Responses = Delta.GetCreateEntityResponses(); + + // THEN + TestTrue("Multiple Responses returned", Responses.Num() > 1); + return true; +} + +VIEWDELTA_TEST(GIVEN_ViewDelta_with_multiple_CreateEntityResponse_added_WHEN_GenerateLegacyOpList_called_THEN_multiple_ops_returned) +{ + // GIVEN + ViewDelta Delta; + CreateEntityResponse Response{}; + Delta.AddCreateEntityResponse(Response); + Delta.AddCreateEntityResponse(Response); + + // WHEN + auto Responses = Delta.GenerateLegacyOpList(); + + // THEN + TestTrue("Multiple Responses returned", Responses->GetCount() > 1); + return true; +} + +VIEWDELTA_TEST(GIVEN_non_empty_ViewDelta_WHEN_Clear_called_THEN_GetCreateEntityResponse_returns_no_items) +{ + // GIVEN + ViewDelta Delta; + CreateEntityResponse Response{}; + Delta.AddCreateEntityResponse(Response); + + // WHEN + Delta.Clear(); + + // THEN + auto Responses = Delta.GetCreateEntityResponses(); + TestTrue("No Responses returned", Responses.Num() == 0); + + return true; +} + +VIEWDELTA_TEST(GIVEN_non_empty_ViewDelta_WHEN_Clear_called_THEN_GenerateLegacyOpList_returns_no_items) +{ + // GIVEN + ViewDelta Delta; + CreateEntityResponse Response{}; + Delta.AddCreateEntityResponse(Response); + + // WHEN + Delta.Clear(); + + // THEN + auto Responses = Delta.GenerateLegacyOpList(); + TestTrue("No Responses returned", Responses->GetCount() == 0); + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/WorkerViewTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/WorkerViewTest.cpp new file mode 100644 index 0000000000..d1186810d2 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/WorkerViewTest.cpp @@ -0,0 +1,92 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/TestDefinitions.h" + +#include "SpatialView/WorkerView.h" +#include "SpatialView/OpList/ViewDeltaLegacyOpList.h" + +#define WORKERVIEW_TEST(TestName) \ + GDK_TEST(Core, WorkerView, TestName) + +using namespace SpatialGDK; + +namespace +{ + Worker_Op CreateEmptyCreateEntityResponseOp() + { + Worker_Op Op{}; + Op.op_type = WORKER_OP_TYPE_CREATE_ENTITY_RESPONSE; + Op.op.create_entity_response = Worker_CreateEntityResponseOp{}; + return Op; + } + +} // anonymous namespace + +WORKERVIEW_TEST(GIVEN_WorkerView_with_one_CreateEntityRequest_WHEN_FlushLocalChanges_called_THEN_one_CreateEntityRequest_returned) +{ + // GIVEN + WorkerView View; + CreateEntityRequest Request; + View.SendCreateEntityRequest(Request); + + // WHEN + auto Messages = View.FlushLocalChanges(); + + // THEN + TestTrue("WorkerView has one CreateEntityRequest", Messages->CreateEntityRequests.Num() == 1); + + return true; +} + +WORKERVIEW_TEST(GIVEN_WorkerView_with_multiple_CreateEntityRequest_WHEN_FlushLocalChanges_called_THEN_mutliple_CreateEntityRequests_returned) +{ + // GIVEN + WorkerView View; + CreateEntityRequest Request; + View.SendCreateEntityRequest(Request); + View.SendCreateEntityRequest(Request); + + auto Messages = View.FlushLocalChanges(); + + // THEN + TestTrue("WorkerView has multiple CreateEntityRequest", Messages->CreateEntityRequests.Num() > 1); + + return true; +} + +WORKERVIEW_TEST(GIVEN_WorkerView_with_one_op_enqued_WHEN_GenerateViewDelta_called_THEN_ViewDelta_with_one_op_returned) +{ + // GIVEN + WorkerView View; + TArray Ops; + Ops.Push(CreateEmptyCreateEntityResponseOp()); + auto OpList = MakeUnique(Ops); + View.EnqueueOpList(MoveTemp(OpList)); + + // WHEN + auto ViewDelta = View.GenerateViewDelta(); + + // THEN + TestTrue("ViewDelta has one op", ViewDelta->GenerateLegacyOpList()->GetCount() == 1); + + return true; +} + +WORKERVIEW_TEST(GIVEN_WorkerView_with_multiple_ops_engued_WHEN_GenerateViewDelta_called_THEN_ViewDelta_with_multiple_ops_returned) +{ + // GIVEN + WorkerView View; + TArray Ops; + Ops.Push(CreateEmptyCreateEntityResponseOp()); + Ops.Push(CreateEmptyCreateEntityResponseOp()); + auto OpList = MakeUnique(Ops); + View.EnqueueOpList(MoveTemp(OpList)); + + // WHEN + auto ViewDelta = View.GenerateViewDelta(); + + // THEN + TestTrue("ViewDelta has multiple ops", ViewDelta->GenerateLegacyOpList()->GetCount() > 1); + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/TestingComponentViewHelpers.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/TestingComponentViewHelpers.cpp new file mode 100644 index 0000000000..043cf4003b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/TestingComponentViewHelpers.cpp @@ -0,0 +1,32 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/TestingComponentViewHelpers.h" + +#include "CoreMinimal.h" + +void TestingComponentViewHelpers::AddEntityComponentToStaticComponentView(USpatialStaticComponentView& StaticComponentView, + const Worker_EntityId EntityId, + const Worker_ComponentId ComponentId, + Schema_ComponentData* ComponentData, + const Worker_Authority Authority) +{ + Worker_AddComponentOp AddComponentOp; + AddComponentOp.entity_id = EntityId; + AddComponentOp.data.component_id = ComponentId; + AddComponentOp.data.schema_type = ComponentData; + StaticComponentView.OnAddComponent(AddComponentOp); + + Worker_AuthorityChangeOp AuthorityChangeOp; + AuthorityChangeOp.entity_id = EntityId; + AuthorityChangeOp.component_id = ComponentId; + AuthorityChangeOp.authority = Authority; + StaticComponentView.OnAuthorityChange(AuthorityChangeOp); +} + +void TestingComponentViewHelpers::AddEntityComponentToStaticComponentView(USpatialStaticComponentView& StaticComponentView, + const Worker_EntityId EntityId, + const Worker_ComponentId ComponentId, + const Worker_Authority Authority) +{ + AddEntityComponentToStaticComponentView(StaticComponentView, EntityId, ComponentId, Schema_CreateComponentData(), Authority); +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/TestingSchemaHelpers.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/TestingSchemaHelpers.cpp new file mode 100644 index 0000000000..a3c0938909 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/TestingSchemaHelpers.cpp @@ -0,0 +1,24 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/TestingSchemaHelpers.h" + +#include "CoreMinimal.h" +#include "SpatialConstants.h" +#include "Utils/SchemaUtils.h" + +#include + +Schema_Object* TestingSchemaHelpers::CreateTranslationComponentDataFields() +{ + Worker_ComponentData Data = {}; + Data.component_id = SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID; + Data.schema_type = Schema_CreateComponentData(); + return Schema_GetComponentDataFields(Data.schema_type); +} + +void TestingSchemaHelpers::AddTranslationComponentDataMapping(Schema_Object* ComponentDataFields, VirtualWorkerId VWId, const PhysicalWorkerName& WorkerName) +{ + Schema_Object* SchemaObject = Schema_AddObject(ComponentDataFields, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_MAPPING_ID); + Schema_AddUint32(SchemaObject, SpatialConstants::MAPPING_VIRTUAL_WORKER_ID, VWId); + SpatialGDK::AddStringToSchema(SchemaObject, SpatialConstants::MAPPING_PHYSICAL_WORKER_NAME, WorkerName); +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp index f8e7733843..db2f18ce86 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp @@ -11,26 +11,46 @@ #include "EngineClasses/SpatialNetBitWriter.h" #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" +#include "Net/NetworkProfiler.h" #include "Schema/Interest.h" #include "SpatialConstants.h" -#include "Utils/RepLayoutUtils.h" #include "Utils/InterestFactory.h" +#include "Utils/RepLayoutUtils.h" +#include "Utils/SpatialLatencyTracer.h" DEFINE_LOG_CATEGORY(LogComponentFactory); +DECLARE_CYCLE_STAT(TEXT("Factory ProcessPropertyUpdates"), STAT_FactoryProcessPropertyUpdates, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Factory ProcessFastArrayUpdate"), STAT_FactoryProcessFastArrayUpdate, STATGROUP_SpatialNet); + +namespace +{ + template + TraceKey* GetTraceKeyFromComponentObject(T& Obj) + { +#if TRACE_LIB_ACTIVE + return &Obj.Trace; +#else + return nullptr; +#endif + } +} namespace SpatialGDK { -ComponentFactory::ComponentFactory(bool bInterestDirty, USpatialNetDriver* InNetDriver) +ComponentFactory::ComponentFactory(bool bInterestDirty, USpatialNetDriver* InNetDriver, USpatialLatencyTracer* InLatencyTracer) : NetDriver(InNetDriver) , PackageMap(InNetDriver->PackageMap) , ClassInfoManager(InNetDriver->ClassInfoManager) , bInterestHasChanged(bInterestDirty) + , LatencyTracer(InLatencyTracer) { } -bool ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FRepChangeState& Changes, ESchemaComponentType PropertyGroup, bool bIsInitialData, TArray* ClearedIds /*= nullptr*/) +uint32 ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FRepChangeState& Changes, ESchemaComponentType PropertyGroup, bool bIsInitialData, TraceKey* OutLatencyTraceId, TArray* ClearedIds /*= nullptr*/) { - bool bWroteSomething = false; + SCOPE_CYCLE_COUNTER(STAT_FactoryProcessPropertyUpdates); + + const uint32 BytesStart = Schema_GetWriteBufferLength(ComponentObject); // Populate the replicated data component updates from the replicated property changelist. if (Changes.RepChanged.Num() > 0) @@ -46,12 +66,38 @@ bool ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObject* const FRepLayoutCmd& Cmd = Changes.RepLayout.Cmds[HandleIterator.CmdIndex]; const FRepParentCmd& Parent = Changes.RepLayout.Parents[Cmd.ParentIndex]; +#if TRACE_LIB_ACTIVE + if (LatencyTracer != nullptr && OutLatencyTraceId != nullptr) + { + TraceKey PropertyKey = InvalidTraceKey; + PropertyKey = LatencyTracer->RetrievePendingTrace(Object, Cmd.Property); + if (PropertyKey == InvalidTraceKey) + { + // Check for sending a nested property + PropertyKey = LatencyTracer->RetrievePendingTrace(Object, Parent.Property); + } + if (PropertyKey != InvalidTraceKey) + { + // If we have already got a trace for this actor/component, we will end one of them here + if (*OutLatencyTraceId != InvalidTraceKey) + { + UE_LOG(LogComponentFactory, Warning, TEXT("%s property trace being dropped because too many active on this actor (%s)"), *Cmd.Property->GetName(), *Object->GetName()); + LatencyTracer->WriteAndEndTraceIfRemote(*OutLatencyTraceId, TEXT("Multiple actor component traces not supported")); + } + *OutLatencyTraceId = PropertyKey; + } + } +#endif if (GetGroupFromCondition(Parent.Condition) == PropertyGroup) { const uint8* Data = (uint8*)Object + Cmd.Offset; bool bProcessedFastArrayProperty = false; +#if USE_NETWORK_PROFILER + const uint32 ProfilerBytesStart = Schema_GetWriteBufferLength(ComponentObject); +#endif + if (Cmd.Type == ERepLayoutCmdType::DynamicArray) { UArrayProperty* ArrayProperty = Cast(Cmd.Property); @@ -59,6 +105,8 @@ bool ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObject* // Check if this is a FastArraySerializer array and if so, call our custom delta serialization if (UScriptStruct* NetDeltaStruct = GetFastArraySerializerProperty(ArrayProperty)) { + SCOPE_CYCLE_COUNTER(STAT_FactoryProcessFastArrayUpdate); + FSpatialNetBitWriter ValueDataWriter(PackageMap); if (FSpatialNetDeltaSerializeInfo::DeltaSerializeWrite(NetDriver, ValueDataWriter, Object, Parent.ArrayIndex, Parent.Property, NetDeltaStruct) || bIsInitialData) @@ -75,7 +123,17 @@ bool ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObject* AddProperty(ComponentObject, HandleIterator.Handle, Cmd.Property, Data, ClearedIds); } - bWroteSomething = true; +#if USE_NETWORK_PROFILER + /** + * a good proxy for how many bits are being sent for a property. Reasons for why it might not be fully accurate: + - the serialized size of a message is just the body contents. Typically something will send the message with the length prefixed, which might be varint encoded, and you pushing the size over some size can cause the encoding of the length be bigger + - similarly, if you push the message over some size it can cause fragmentation which means you now have to pay for the headers again + - if there is any compression or anything else going on, the number of bytes actually transferred because of this data can differ + - lastly somewhat philosophical question of who pays for the overhead of a packet and whether you attribute a part of it to each field or attribute it to the update itself, but I assume you care a bit less about this + */ + const uint32 ProfilerBytesEnd = Schema_GetWriteBufferLength(ComponentObject); + NETWORK_PROFILER(GNetworkProfiler.TrackReplicateProperty(Cmd.Property, (ProfilerBytesEnd - ProfilerBytesStart) * CHAR_BIT, nullptr)); +#endif } if (Cmd.Type == ERepLayoutCmdType::DynamicArray) @@ -88,12 +146,14 @@ bool ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObject* } } - return bWroteSomething; + const uint32 BytesEnd = Schema_GetWriteBufferLength(ComponentObject); + + return BytesEnd - BytesStart; } -bool ComponentFactory::FillHandoverSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes, bool bIsInitialData, TArray* ClearedIds /* = nullptr */) +uint32 ComponentFactory::FillHandoverSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes, bool bIsInitialData, TraceKey* OutLatencyTraceId, TArray* ClearedIds /* = nullptr */) { - bool bWroteSomething = false; + const uint32 BytesStart = Schema_GetWriteBufferLength(ComponentObject); for (uint16 ChangedHandle : Changes) { @@ -102,12 +162,24 @@ bool ComponentFactory::FillHandoverSchemaObject(Schema_Object* ComponentObject, const uint8* Data = (uint8*)Object + PropertyInfo.Offset; +#if TRACE_LIB_ACTIVE + if (LatencyTracer != nullptr && OutLatencyTraceId != nullptr) + { + // If we have already got a trace for this actor/component, we will end one of them here + if (*OutLatencyTraceId != InvalidTraceKey) + { + UE_LOG(LogComponentFactory, Warning, TEXT("%s handover trace being dropped because too many active on this actor (%s)"), *PropertyInfo.Property->GetName(), *Object->GetName()); + LatencyTracer->WriteAndEndTraceIfRemote(*OutLatencyTraceId, TEXT("Multiple actor component traces not supported")); + } + *OutLatencyTraceId = LatencyTracer->RetrievePendingTrace(Object, PropertyInfo.Property); + } +#endif AddProperty(ComponentObject, ChangedHandle, PropertyInfo.Property, Data, ClearedIds); - - bWroteSomething = true; } - return bWroteSomething; + const uint32 BytesEnd = Schema_GetWriteBufferLength(ComponentObject); + + return BytesEnd - BytesStart; } void ComponentFactory::AddProperty(Schema_Object* Object, Schema_FieldId FieldId, UProperty* Property, const uint8* Data, TArray* ClearedIds) @@ -131,7 +203,7 @@ void ComponentFactory::AddProperty(Schema_Object* Object, Schema_FieldId FieldId // Check the success of the serialization and print a warning if it failed. This is how native handles failed serialization. if (!bSuccess) { - UE_LOG(LogSpatialNetSerialize, Warning, TEXT("AddProperty: NetSerialize %s failed."), *Struct->GetFullName()); + UE_LOG(LogComponentFactory, Warning, TEXT("AddProperty: NetSerialize %s failed."), *Struct->GetFullName()); return; } } @@ -190,14 +262,22 @@ void ComponentFactory::AddProperty(Schema_Object* Object, Schema_FieldId FieldId } else if (UObjectPropertyBase* ObjectProperty = Cast(Property)) { - UObject* ObjectValue = ObjectProperty->GetObjectPropertyValue(Data); - - if (ObjectProperty->PropertyFlags & CPF_AlwaysInterested) + if (Cast(Property)) { - bInterestHasChanged = true; + const FSoftObjectPtr* ObjectPtr = reinterpret_cast(Data); + + AddObjectRefToSchema(Object, FieldId, FUnrealObjectRef::FromSoftObjectPath(ObjectPtr->ToSoftObjectPath())); } + else + { + UObject* ObjectValue = ObjectProperty->GetObjectPropertyValue(Data); - AddObjectRefToSchema(Object, FieldId, FUnrealObjectRef::FromObjectPtr(ObjectValue, PackageMap)); + if (ObjectProperty->PropertyFlags & CPF_AlwaysInterested) + { + bInterestHasChanged = true; + } + AddObjectRefToSchema(Object, FieldId, FUnrealObjectRef::FromObjectPtr(ObjectValue, PackageMap)); + } } else if (UNameProperty* NameProperty = Cast(Property)) { @@ -253,84 +333,86 @@ void ComponentFactory::AddProperty(Schema_Object* Object, Schema_FieldId FieldId } } -TArray ComponentFactory::CreateComponentDatas(UObject* Object, const FClassInfo& Info, const FRepChangeState& RepChangeState, const FHandoverChangeState& HandoverChangeState) +TArray ComponentFactory::CreateComponentDatas(UObject* Object, const FClassInfo& Info, const FRepChangeState& RepChangeState, const FHandoverChangeState& HandoverChangeState, uint32& OutBytesWritten) { - TArray ComponentDatas; + TArray ComponentDatas; if (Info.SchemaComponents[SCHEMA_Data] != SpatialConstants::INVALID_COMPONENT_ID) { - ComponentDatas.Add(CreateComponentData(Info.SchemaComponents[SCHEMA_Data], Object, RepChangeState, SCHEMA_Data)); + ComponentDatas.Add(CreateComponentData(Info.SchemaComponents[SCHEMA_Data], Object, RepChangeState, SCHEMA_Data, OutBytesWritten)); } if (Info.SchemaComponents[SCHEMA_OwnerOnly] != SpatialConstants::INVALID_COMPONENT_ID) { - ComponentDatas.Add(CreateComponentData(Info.SchemaComponents[SCHEMA_OwnerOnly], Object, RepChangeState, SCHEMA_OwnerOnly)); + ComponentDatas.Add(CreateComponentData(Info.SchemaComponents[SCHEMA_OwnerOnly], Object, RepChangeState, SCHEMA_OwnerOnly, OutBytesWritten)); } if (Info.SchemaComponents[SCHEMA_Handover] != SpatialConstants::INVALID_COMPONENT_ID) { - ComponentDatas.Add(CreateHandoverComponentData(Info.SchemaComponents[SCHEMA_Handover], Object, Info, HandoverChangeState)); + ComponentDatas.Add(CreateHandoverComponentData(Info.SchemaComponents[SCHEMA_Handover], Object, Info, HandoverChangeState, OutBytesWritten)); } return ComponentDatas; } -Worker_ComponentData ComponentFactory::CreateComponentData(Worker_ComponentId ComponentId, UObject* Object, const FRepChangeState& Changes, ESchemaComponentType PropertyGroup) +FWorkerComponentData ComponentFactory::CreateComponentData(Worker_ComponentId ComponentId, UObject* Object, const FRepChangeState& Changes, ESchemaComponentType PropertyGroup, uint32& OutBytesWritten) { - Worker_ComponentData ComponentData = {}; + FWorkerComponentData ComponentData = {}; ComponentData.component_id = ComponentId; ComponentData.schema_type = Schema_CreateComponentData(); Schema_Object* ComponentObject = Schema_GetComponentDataFields(ComponentData.schema_type); // We're currently ignoring ClearedId fields, which is problematic if the initial replicated state // is different to what the default state is (the client will have the incorrect data). UNR:959 - FillSchemaObject(ComponentObject, Object, Changes, PropertyGroup, true); + OutBytesWritten += FillSchemaObject(ComponentObject, Object, Changes, PropertyGroup, true, GetTraceKeyFromComponentObject(ComponentData)); return ComponentData; } -Worker_ComponentData ComponentFactory::CreateEmptyComponentData(Worker_ComponentId ComponentId) +FWorkerComponentData ComponentFactory::CreateEmptyComponentData(Worker_ComponentId ComponentId) { - Worker_ComponentData ComponentData = {}; + FWorkerComponentData ComponentData = {}; ComponentData.component_id = ComponentId; ComponentData.schema_type = Schema_CreateComponentData(); return ComponentData; } -Worker_ComponentData ComponentFactory::CreateHandoverComponentData(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes) +FWorkerComponentData ComponentFactory::CreateHandoverComponentData(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes, uint32& OutBytesWritten) { - Worker_ComponentData ComponentData = CreateEmptyComponentData(ComponentId); + FWorkerComponentData ComponentData = CreateEmptyComponentData(ComponentId); Schema_Object* ComponentObject = Schema_GetComponentDataFields(ComponentData.schema_type); - FillHandoverSchemaObject(ComponentObject, Object, Info, Changes, true); + OutBytesWritten += FillHandoverSchemaObject(ComponentObject, Object, Info, Changes, true, GetTraceKeyFromComponentObject(ComponentData)); return ComponentData; } -TArray ComponentFactory::CreateComponentUpdates(UObject* Object, const FClassInfo& Info, Worker_EntityId EntityId, const FRepChangeState* RepChangeState, const FHandoverChangeState* HandoverChangeState) +TArray ComponentFactory::CreateComponentUpdates(UObject* Object, const FClassInfo& Info, Worker_EntityId EntityId, const FRepChangeState* RepChangeState, const FHandoverChangeState* HandoverChangeState, uint32& OutBytesWritten) { - TArray ComponentUpdates; + TArray ComponentUpdates; if (RepChangeState) { if (Info.SchemaComponents[SCHEMA_Data] != SpatialConstants::INVALID_COMPONENT_ID) { - bool bWroteSomething = false; - Worker_ComponentUpdate MultiClientUpdate = CreateComponentUpdate(Info.SchemaComponents[SCHEMA_Data], Object, *RepChangeState, SCHEMA_Data, bWroteSomething); - if (bWroteSomething) + uint32 BytesWritten = 0; + FWorkerComponentUpdate MultiClientUpdate = CreateComponentUpdate(Info.SchemaComponents[SCHEMA_Data], Object, *RepChangeState, SCHEMA_Data, BytesWritten); + if (BytesWritten > 0) { ComponentUpdates.Add(MultiClientUpdate); + OutBytesWritten += BytesWritten; } } if (Info.SchemaComponents[SCHEMA_OwnerOnly] != SpatialConstants::INVALID_COMPONENT_ID) { - bool bWroteSomething = false; - Worker_ComponentUpdate SingleClientUpdate = CreateComponentUpdate(Info.SchemaComponents[SCHEMA_OwnerOnly], Object, *RepChangeState, SCHEMA_OwnerOnly, bWroteSomething); - if (bWroteSomething) + uint32 BytesWritten = 0; + FWorkerComponentUpdate SingleClientUpdate = CreateComponentUpdate(Info.SchemaComponents[SCHEMA_OwnerOnly], Object, *RepChangeState, SCHEMA_OwnerOnly, BytesWritten); + if (BytesWritten > 0) { ComponentUpdates.Add(SingleClientUpdate); + OutBytesWritten += BytesWritten; } } } @@ -339,11 +421,12 @@ TArray ComponentFactory::CreateComponentUpdates(UObject* { if (Info.SchemaComponents[SCHEMA_Handover] != SpatialConstants::INVALID_COMPONENT_ID) { - bool bWroteSomething = false; - Worker_ComponentUpdate HandoverUpdate = CreateHandoverComponentUpdate(Info.SchemaComponents[SCHEMA_Handover], Object, Info, *HandoverChangeState, bWroteSomething); - if (bWroteSomething) + uint32 BytesWritten = 0; + FWorkerComponentUpdate HandoverUpdate = CreateHandoverComponentUpdate(Info.SchemaComponents[SCHEMA_Handover], Object, Info, *HandoverChangeState, BytesWritten); + if (BytesWritten > 0) { ComponentUpdates.Add(HandoverUpdate); + OutBytesWritten += BytesWritten; } } } @@ -351,16 +434,15 @@ TArray ComponentFactory::CreateComponentUpdates(UObject* // Only support Interest for Actors for now. if (Object->IsA() && bInterestHasChanged) { - InterestFactory InterestUpdateFactory(Cast(Object), Info, NetDriver->ClassInfoManager, NetDriver->PackageMap); - ComponentUpdates.Add(InterestUpdateFactory.CreateInterestUpdate()); + ComponentUpdates.Add(NetDriver->InterestFactory->CreateInterestUpdate((AActor*)Object, Info, EntityId)); } return ComponentUpdates; } -Worker_ComponentUpdate ComponentFactory::CreateComponentUpdate(Worker_ComponentId ComponentId, UObject* Object, const FRepChangeState& Changes, ESchemaComponentType PropertyGroup, bool& bWroteSomething) +FWorkerComponentUpdate ComponentFactory::CreateComponentUpdate(Worker_ComponentId ComponentId, UObject* Object, const FRepChangeState& Changes, ESchemaComponentType PropertyGroup, uint32& OutBytesWritten) { - Worker_ComponentUpdate ComponentUpdate = {}; + FWorkerComponentUpdate ComponentUpdate = {}; ComponentUpdate.component_id = ComponentId; ComponentUpdate.schema_type = Schema_CreateComponentUpdate(); @@ -368,24 +450,27 @@ Worker_ComponentUpdate ComponentFactory::CreateComponentUpdate(Worker_ComponentI TArray ClearedIds; - bWroteSomething = FillSchemaObject(ComponentObject, Object, Changes, PropertyGroup, false, &ClearedIds); + uint32 BytesWritten = FillSchemaObject(ComponentObject, Object, Changes, PropertyGroup, false, GetTraceKeyFromComponentObject(ComponentUpdate), &ClearedIds); for (Schema_FieldId Id : ClearedIds) { Schema_AddComponentUpdateClearedField(ComponentUpdate.schema_type, Id); + BytesWritten++; // Workaround so we don't drop updates that *only* contain cleared fields - JIRA UNR-3371 } - if (!bWroteSomething) + if (BytesWritten == 0) { Schema_DestroyComponentUpdate(ComponentUpdate.schema_type); } + OutBytesWritten += BytesWritten; + return ComponentUpdate; } -Worker_ComponentUpdate ComponentFactory::CreateHandoverComponentUpdate(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes, bool& bWroteSomething) +FWorkerComponentUpdate ComponentFactory::CreateHandoverComponentUpdate(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes, uint32& OutBytesWritten) { - Worker_ComponentUpdate ComponentUpdate = {}; + FWorkerComponentUpdate ComponentUpdate = {}; ComponentUpdate.component_id = ComponentId; ComponentUpdate.schema_type = Schema_CreateComponentUpdate(); @@ -393,18 +478,21 @@ Worker_ComponentUpdate ComponentFactory::CreateHandoverComponentUpdate(Worker_Co TArray ClearedIds; - bWroteSomething = FillHandoverSchemaObject(ComponentObject, Object, Info, Changes, false, &ClearedIds); + uint32 BytesWritten = FillHandoverSchemaObject(ComponentObject, Object, Info, Changes, false, GetTraceKeyFromComponentObject(ComponentUpdate), &ClearedIds); for (Schema_FieldId Id : ClearedIds) { Schema_AddComponentUpdateClearedField(ComponentUpdate.schema_type, Id); + BytesWritten++; // Workaround so we don't drop updates that *only* contain cleared fields - JIRA UNR-3371 } - if (!bWroteSomething) + if (BytesWritten == 0) { Schema_DestroyComponentUpdate(ComponentUpdate.schema_type); } + OutBytesWritten += BytesWritten; + return ComponentUpdate; } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp index 342bba7c44..dbf690d7cc 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp @@ -16,21 +16,85 @@ DEFINE_LOG_CATEGORY(LogSpatialComponentReader); +DECLARE_CYCLE_STAT(TEXT("Reader ApplyPropertyUpdates"), STAT_ReaderApplyPropertyUpdates, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Reader ApplyHandoverPropertyUpdates"), STAT_ReaderApplyHandoverPropertyUpdates, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Reader ApplyFastArrayUpdate"), STAT_ReaderApplyFastArrayUpdate, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Reader ApplyProperty"), STAT_ReaderApplyProperty, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("Reader ApplyArray"), STAT_ReaderApplyArray, STATGROUP_SpatialNet); + +namespace +{ + bool FORCEINLINE ObjectRefSetsAreSame(const TSet< FUnrealObjectRef >& A, const TSet< FUnrealObjectRef >& B) + { + if (A.Num() != B.Num()) + { + return false; + } + + for (const FUnrealObjectRef& CompareRef : A) + { + if (!B.Contains(CompareRef)) + { + return false; + } + } + + return true; + } + + bool ReferencesChanged(FObjectReferencesMap& InObjectReferencesMap, int32 Offset, bool bHasReferences, const TSet& NewDynamicRefs, const TSet NewUnresolvedRefs) + { + FObjectReferences* CurEntry = InObjectReferencesMap.Find(Offset); + + if (bHasReferences ^ (CurEntry != nullptr)) + { + return true; + } + if (CurEntry && bHasReferences) + { + return !ObjectRefSetsAreSame(NewDynamicRefs, CurEntry->MappedRefs) || !ObjectRefSetsAreSame(NewUnresolvedRefs, CurEntry->UnresolvedRefs); + } + return false; + } + + bool ReferencesChanged(FObjectReferencesMap& InObjectReferencesMap, int32 Offset, bool bHasReferences, const FUnrealObjectRef& ObjectRef, bool bUnresolved) + { + FObjectReferences* CurEntry = InObjectReferencesMap.Find(Offset); + + if (bHasReferences ^ (CurEntry != nullptr)) + { + return true; + } + if (CurEntry && bHasReferences) + { + if (!bUnresolved) + { + return CurEntry->MappedRefs.Num() != 1 || CurEntry->UnresolvedRefs.Num() != 0 || *CurEntry->MappedRefs.begin() != ObjectRef; + } + else + { + return CurEntry->MappedRefs.Num() != 0 || CurEntry->UnresolvedRefs.Num() != 1 || *CurEntry->UnresolvedRefs.begin() != ObjectRef; + } + + } + return false; + } +} + namespace SpatialGDK { -ComponentReader::ComponentReader(USpatialNetDriver* InNetDriver, FObjectReferencesMap& InObjectReferencesMap, TSet& InUnresolvedRefs) +ComponentReader::ComponentReader(USpatialNetDriver* InNetDriver, FObjectReferencesMap& InObjectReferencesMap/*, TSet& InUnresolvedRefs*/) : PackageMap(InNetDriver->PackageMap) , NetDriver(InNetDriver) , ClassInfoManager(InNetDriver->ClassInfoManager) , RootObjectReferencesMap(InObjectReferencesMap) - , UnresolvedRefs(InUnresolvedRefs) { } -void ComponentReader::ApplyComponentData(const Worker_ComponentData& ComponentData, UObject* Object, USpatialActorChannel* Channel, bool bIsHandover) +void ComponentReader::ApplyComponentData(const Worker_ComponentData& ComponentData, UObject& Object, USpatialActorChannel& Channel, bool bIsHandover, bool& bOutReferencesChanged) { - if (Object->IsPendingKill()) + if (Object.IsPendingKill()) { return; } @@ -43,17 +107,17 @@ void ComponentReader::ApplyComponentData(const Worker_ComponentData& ComponentDa if (bIsHandover) { - ApplyHandoverSchemaObject(ComponentObject, Object, Channel, true, UpdatedIds, ComponentData.component_id); + ApplyHandoverSchemaObject(ComponentObject, Object, Channel, true, UpdatedIds, ComponentData.component_id, bOutReferencesChanged); } else { - ApplySchemaObject(ComponentObject, Object, Channel, true, UpdatedIds, ComponentData.component_id); + ApplySchemaObject(ComponentObject, Object, Channel, true, UpdatedIds, ComponentData.component_id, bOutReferencesChanged); } } -void ComponentReader::ApplyComponentUpdate(const Worker_ComponentUpdate& ComponentUpdate, UObject* Object, USpatialActorChannel* Channel, bool bIsHandover) +void ComponentReader::ApplyComponentUpdate(const Worker_ComponentUpdate& ComponentUpdate, UObject& Object, USpatialActorChannel& Channel, bool bIsHandover, bool& bOutReferencesChanged) { - if (Object->IsPendingKill()) + if (Object.IsPendingKill()) { return; } @@ -77,18 +141,18 @@ void ComponentReader::ApplyComponentUpdate(const Worker_ComponentUpdate& Compone { if (bIsHandover) { - ApplyHandoverSchemaObject(ComponentObject, Object, Channel, false, UpdatedIds, ComponentUpdate.component_id); + ApplyHandoverSchemaObject(ComponentObject, Object, Channel, false, UpdatedIds, ComponentUpdate.component_id, bOutReferencesChanged); } else { - ApplySchemaObject(ComponentObject, Object, Channel, false, UpdatedIds, ComponentUpdate.component_id); + ApplySchemaObject(ComponentObject, Object, Channel, false, UpdatedIds, ComponentUpdate.component_id, bOutReferencesChanged); } } } -void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject* Object, USpatialActorChannel* Channel, bool bIsInitialData, const TArray& UpdatedIds, Worker_ComponentId ComponentId) +void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject& Object, USpatialActorChannel& Channel, bool bIsInitialData, const TArray& UpdatedIds, Worker_ComponentId ComponentId, bool& bOutReferencesChanged) { - FObjectReplicator* Replicator = Channel->PreReceiveSpatialUpdate(Object); + FObjectReplicator* Replicator = Channel.PreReceiveSpatialUpdate(&Object); if (Replicator == nullptr) { // Can't apply this schema object. Error printed from PreReceiveSpatialUpdate. @@ -100,179 +164,220 @@ void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject* TArray& BaseHandleToCmdIndex = Replicator->RepLayout->BaseHandleToCmdIndex; TArray& Parents = Replicator->RepLayout->Parents; - bool bIsAuthServer = Channel->IsAuthoritativeServer(); - bool bAutonomousProxy = Channel->IsClientAutonomousProxy(); + bool bIsAuthServer = Channel.IsAuthoritativeServer(); + bool bAutonomousProxy = Channel.IsClientAutonomousProxy(); bool bIsClient = NetDriver->GetNetMode() == NM_Client; - FSpatialConditionMapFilter ConditionMap(Channel, bIsClient); + FSpatialConditionMapFilter ConditionMap(&Channel, bIsClient); TArray RepNotifies; - for (uint32 FieldId : UpdatedIds) { - // FieldId is the same as rep handle - if (FieldId == 0 || (int)FieldId - 1 >= BaseHandleToCmdIndex.Num()) - { - UE_LOG(LogSpatialComponentReader, Error, TEXT("ApplySchemaObject: Encountered an invalid field Id while applying schema. Object: %s, Field: %d, Entity: %lld, Component: %d"), *Object->GetPathName(), FieldId, Channel->GetEntityId(), ComponentId); - continue; - } + // Scoped to exclude OnRep callbacks which are already tracked per OnRep function + SCOPE_CYCLE_COUNTER(STAT_ReaderApplyPropertyUpdates); - int32 CmdIndex = BaseHandleToCmdIndex[FieldId - 1].CmdIndex; - const FRepLayoutCmd& Cmd = Cmds[CmdIndex]; - const FRepParentCmd& Parent = Parents[Cmd.ParentIndex]; - int32 ShadowOffset = Cmd.ShadowOffset; - - if (NetDriver->IsServer() || ConditionMap.IsRelevant(Parent.Condition)) + for (uint32 FieldId : UpdatedIds) { - // This swaps Role/RemoteRole as we write it - const FRepLayoutCmd& SwappedCmd = (!bIsAuthServer && Parent.RoleSwapIndex != -1) ? Cmds[Parents[Parent.RoleSwapIndex].CmdStart] : Cmd; + // FieldId is the same as rep handle + if (FieldId == 0 || (int)FieldId - 1 >= BaseHandleToCmdIndex.Num()) + { + UE_LOG(LogSpatialComponentReader, Error, TEXT("ApplySchemaObject: Encountered an invalid field Id while applying schema. Object: %s, Field: %d, Entity: %lld, Component: %d"), *Object.GetPathName(), FieldId, Channel.GetEntityId(), ComponentId); + continue; + } - uint8* Data = (uint8*)Object + SwappedCmd.Offset; + int32 CmdIndex = BaseHandleToCmdIndex[FieldId - 1].CmdIndex; + const FRepLayoutCmd& Cmd = Cmds[CmdIndex]; + const FRepParentCmd& Parent = Parents[Cmd.ParentIndex]; + int32 ShadowOffset = Cmd.ShadowOffset; - if (Cmd.Type == ERepLayoutCmdType::DynamicArray) + if (NetDriver->IsServer() || ConditionMap.IsRelevant(Parent.Condition)) { - UArrayProperty* ArrayProperty = Cast(Cmd.Property); - if (ArrayProperty == nullptr) + // This swaps Role/RemoteRole as we write it + const FRepLayoutCmd& SwappedCmd = (!bIsAuthServer && Parent.RoleSwapIndex != -1) ? Cmds[Parents[Parent.RoleSwapIndex].CmdStart] : Cmd; + + uint8* Data = (uint8*)&Object + SwappedCmd.Offset; + + // If the property has RepNotifies, update with local data and possibly initialize the shadow data + if (Parent.Property->HasAnyPropertyFlags(CPF_RepNotify)) { - UE_LOG(LogSpatialComponentReader, Error, TEXT("Failed to apply Schema Object %s. One of it's properties is null"), *Object->GetName()); - continue; +#if ENGINE_MINOR_VERSION <= 22 + FRepStateStaticBuffer& ShadowData = RepState->StaticBuffer; +#else + FRepStateStaticBuffer& ShadowData = RepState->GetReceivingRepState()->StaticBuffer; +#endif + if (ShadowData.Num() == 0) + { + Channel.ResetShadowData(*Replicator->RepLayout.Get(), ShadowData, &Object); + } + else + { + Cmd.Property->CopySingleValue(ShadowData.GetData() + SwappedCmd.ShadowOffset, Data); + } } - // Check if this is a FastArraySerializer array and if so, call our custom delta serialization - if (UScriptStruct* NetDeltaStruct = GetFastArraySerializerProperty(ArrayProperty)) + if (Cmd.Type == ERepLayoutCmdType::DynamicArray) { - TArray ValueData = GetBytesFromSchema(ComponentObject, FieldId); - int64 CountBits = ValueData.Num() * 8; - TSet NewUnresolvedRefs; - FSpatialNetBitReader ValueDataReader(PackageMap, ValueData.GetData(), CountBits, NewUnresolvedRefs); - - if (ValueData.Num() > 0) + UArrayProperty* ArrayProperty = Cast(Cmd.Property); + if (ArrayProperty == nullptr) { - FSpatialNetDeltaSerializeInfo::DeltaSerializeRead(NetDriver, ValueDataReader, Object, Parent.ArrayIndex, Parent.Property, NetDeltaStruct); + UE_LOG(LogSpatialComponentReader, Error, TEXT("Failed to apply Schema Object %s. One of it's properties is null"), *Object.GetName()); + continue; } - if (NewUnresolvedRefs.Num() > 0) + // Check if this is a FastArraySerializer array and if so, call our custom delta serialization + if (UScriptStruct* NetDeltaStruct = GetFastArraySerializerProperty(ArrayProperty)) { - RootObjectReferencesMap.Add(SwappedCmd.Offset, FObjectReferences(ValueData, CountBits, NewUnresolvedRefs, ShadowOffset, Cmd.ParentIndex, ArrayProperty, /* bFastArrayProp */ true)); - UnresolvedRefs.Append(NewUnresolvedRefs); + SCOPE_CYCLE_COUNTER(STAT_ReaderApplyFastArrayUpdate); + + TArray ValueData = GetBytesFromSchema(ComponentObject, FieldId); + int64 CountBits = ValueData.Num() * 8; + TSet NewMappedRefs; + TSet NewUnresolvedRefs; + FSpatialNetBitReader ValueDataReader(PackageMap, ValueData.GetData(), CountBits, NewMappedRefs, NewUnresolvedRefs); + + if (ValueData.Num() > 0) + { + FSpatialNetDeltaSerializeInfo::DeltaSerializeRead(NetDriver, ValueDataReader, &Object, Parent.ArrayIndex, Parent.Property, NetDeltaStruct); + } + + FObjectReferences* CurEntry = RootObjectReferencesMap.Find(SwappedCmd.Offset); + const bool bHasReferences = NewUnresolvedRefs.Num() > 0 || NewMappedRefs.Num() > 0; + + if (ReferencesChanged(RootObjectReferencesMap, SwappedCmd.Offset, bHasReferences, NewMappedRefs, NewUnresolvedRefs)) + { + if (bHasReferences) + { + RootObjectReferencesMap.Add(SwappedCmd.Offset, FObjectReferences(ValueData, CountBits, MoveTemp(NewMappedRefs), MoveTemp(NewUnresolvedRefs), ShadowOffset, Cmd.ParentIndex, ArrayProperty, /* bFastArrayProp */ true)); + } + else + { + RootObjectReferencesMap.Remove(SwappedCmd.Offset); + } + bOutReferencesChanged = true; + } } - else if (RootObjectReferencesMap.Find(SwappedCmd.Offset)) + else { - RootObjectReferencesMap.Remove(SwappedCmd.Offset); + ApplyArray(ComponentObject, FieldId, RootObjectReferencesMap, ArrayProperty, Data, SwappedCmd.Offset, ShadowOffset, Cmd.ParentIndex, bOutReferencesChanged); } } else { - ApplyArray(ComponentObject, FieldId, RootObjectReferencesMap, ArrayProperty, Data, SwappedCmd.Offset, ShadowOffset, Cmd.ParentIndex); + ApplyProperty(ComponentObject, FieldId, RootObjectReferencesMap, 0, Cmd.Property, Data, SwappedCmd.Offset, ShadowOffset, Cmd.ParentIndex, bOutReferencesChanged); } - } - else - { - ApplyProperty(ComponentObject, FieldId, RootObjectReferencesMap, 0, Cmd.Property, Data, SwappedCmd.Offset, ShadowOffset, Cmd.ParentIndex); - } - if (Cmd.Property->GetFName() == NAME_RemoteRole) - { - // Downgrade role from AutonomousProxy to SimulatedProxy if we aren't authoritative over - // the client RPCs component. - UByteProperty* ByteProperty = Cast(Cmd.Property); - if (!bIsAuthServer && !bAutonomousProxy && ByteProperty->GetPropertyValue(Data) == ROLE_AutonomousProxy) + if (Cmd.Property->GetFName() == NAME_RemoteRole) { - ByteProperty->SetPropertyValue(Data, ROLE_SimulatedProxy); + // Downgrade role from AutonomousProxy to SimulatedProxy if we aren't authoritative over + // the client RPCs component. + UByteProperty* ByteProperty = Cast(Cmd.Property); + if (!bIsAuthServer && !bAutonomousProxy && ByteProperty->GetPropertyValue(Data) == ROLE_AutonomousProxy) + { + ByteProperty->SetPropertyValue(Data, ROLE_SimulatedProxy); + } } - } - // Parent.Property is the "root" replicated property, e.g. if a struct property was flattened - if (Parent.Property->HasAnyPropertyFlags(CPF_RepNotify)) - { -#if ENGINE_MINOR_VERSION <= 22 - bool bIsIdentical = Cmd.Property->Identical(RepState->StaticBuffer.GetData() + SwappedCmd.ShadowOffset, Data); -#else - bool bIsIdentical = Cmd.Property->Identical(RepState->GetReceivingRepState()->StaticBuffer.GetData() + SwappedCmd.ShadowOffset, Data); -#endif - - // Only call RepNotify for REPNOTIFY_Always if we are not applying initial data. - if (bIsInitialData) + // Parent.Property is the "root" replicated property, e.g. if a struct property was flattened + if (Parent.Property->HasAnyPropertyFlags(CPF_RepNotify)) { - if (!bIsIdentical) + #if ENGINE_MINOR_VERSION <= 22 + bool bIsIdentical = Cmd.Property->Identical(RepState->StaticBuffer.GetData() + SwappedCmd.ShadowOffset, Data); + #else + bool bIsIdentical = Cmd.Property->Identical(RepState->GetReceivingRepState()->StaticBuffer.GetData() + SwappedCmd.ShadowOffset, Data); + #endif + + // Only call RepNotify for REPNOTIFY_Always if we are not applying initial data. + if (bIsInitialData) { - RepNotifies.AddUnique(Parent.Property); + if (!bIsIdentical) + { + RepNotifies.AddUnique(Parent.Property); + } } - } - else - { - if (Parent.RepNotifyCondition == REPNOTIFY_Always || !bIsIdentical) + else { - RepNotifies.AddUnique(Parent.Property); + if (Parent.RepNotifyCondition == REPNOTIFY_Always || !bIsIdentical) + { + RepNotifies.AddUnique(Parent.Property); + } } } - } } } - Channel->RemoveRepNotifiesWithUnresolvedObjs(RepNotifies, *Replicator->RepLayout, RootObjectReferencesMap, Object); + Channel.RemoveRepNotifiesWithUnresolvedObjs(RepNotifies, *Replicator->RepLayout, RootObjectReferencesMap, &Object); - Channel->PostReceiveSpatialUpdate(Object, RepNotifies); + Channel.PostReceiveSpatialUpdate(&Object, RepNotifies); } -void ComponentReader::ApplyHandoverSchemaObject(Schema_Object* ComponentObject, UObject* Object, USpatialActorChannel* Channel, bool bIsInitialData, const TArray& UpdatedIds, Worker_ComponentId ComponentId) +void ComponentReader::ApplyHandoverSchemaObject(Schema_Object* ComponentObject, UObject& Object, USpatialActorChannel& Channel, bool bIsInitialData, const TArray& UpdatedIds, Worker_ComponentId ComponentId, bool& bOutReferencesChanged) { - FObjectReplicator* Replicator = Channel->PreReceiveSpatialUpdate(Object); + SCOPE_CYCLE_COUNTER(STAT_ReaderApplyHandoverPropertyUpdates); + + FObjectReplicator* Replicator = Channel.PreReceiveSpatialUpdate(&Object); if (Replicator == nullptr) { // Can't apply this schema object. Error printed from PreReceiveSpatialUpdate. return; } - const FClassInfo& ClassInfo = ClassInfoManager->GetOrCreateClassInfoByClass(Object->GetClass()); + const FClassInfo& ClassInfo = ClassInfoManager->GetOrCreateClassInfoByClass(Object.GetClass()); for (uint32 FieldId : UpdatedIds) { // FieldId is the same as handover handle if (FieldId == 0 || (int)FieldId - 1 >= ClassInfo.HandoverProperties.Num()) { - UE_LOG(LogSpatialComponentReader, Error, TEXT("ApplyHandoverSchemaObject: Encountered an invalid field Id while applying schema. Object: %s, Field: %d, Entity: %lld, Component: %d"), *Object->GetPathName(), FieldId, Channel->GetEntityId(), ComponentId); + UE_LOG(LogSpatialComponentReader, Error, TEXT("ApplyHandoverSchemaObject: Encountered an invalid field Id while applying schema. Object: %s, Field: %d, Entity: %lld, Component: %d"), *Object.GetPathName(), FieldId, Channel.GetEntityId(), ComponentId); continue; } const FHandoverPropertyInfo& PropertyInfo = ClassInfo.HandoverProperties[FieldId - 1]; - uint8* Data = (uint8*)Object + PropertyInfo.Offset; + uint8* Data = (uint8*)&Object + PropertyInfo.Offset; if (UArrayProperty* ArrayProperty = Cast(PropertyInfo.Property)) { - ApplyArray(ComponentObject, FieldId, RootObjectReferencesMap, ArrayProperty, Data, PropertyInfo.Offset, -1, -1); + ApplyArray(ComponentObject, FieldId, RootObjectReferencesMap, ArrayProperty, Data, PropertyInfo.Offset, -1, -1, bOutReferencesChanged); } else { - ApplyProperty(ComponentObject, FieldId, RootObjectReferencesMap, 0, PropertyInfo.Property, Data, PropertyInfo.Offset, -1, -1); + ApplyProperty(ComponentObject, FieldId, RootObjectReferencesMap, 0, PropertyInfo.Property, Data, PropertyInfo.Offset, -1, -1, bOutReferencesChanged); } } - Channel->PostReceiveSpatialUpdate(Object, TArray()); + Channel.PostReceiveSpatialUpdate(&Object, TArray()); } -void ComponentReader::ApplyProperty(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, uint32 Index, UProperty* Property, uint8* Data, int32 Offset, int32 ShadowOffset, int32 ParentIndex) +void ComponentReader::ApplyProperty(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, uint32 Index, UProperty* Property, uint8* Data, int32 Offset, int32 ShadowOffset, int32 ParentIndex, bool& bOutReferencesChanged) { + SCOPE_CYCLE_COUNTER(STAT_ReaderApplyProperty); + if (UStructProperty* StructProperty = Cast(Property)) { TArray ValueData = IndexBytesFromSchema(Object, FieldId, Index); // A bit hacky, we should probably include the number of bits with the data instead. int64 CountBits = ValueData.Num() * 8; + TSet NewDynamicRefs; TSet NewUnresolvedRefs; - FSpatialNetBitReader ValueDataReader(PackageMap, ValueData.GetData(), CountBits, NewUnresolvedRefs); + FSpatialNetBitReader ValueDataReader(PackageMap, ValueData.GetData(), CountBits, NewDynamicRefs, NewUnresolvedRefs); bool bHasUnmapped = false; ReadStructProperty(ValueDataReader, StructProperty, NetDriver, Data, bHasUnmapped); + const bool bHasReferences = NewDynamicRefs.Num() > 0 || NewUnresolvedRefs.Num() > 0; - if (bHasUnmapped) - { - InObjectReferencesMap.Add(Offset, FObjectReferences(ValueData, CountBits, NewUnresolvedRefs, ShadowOffset, ParentIndex, Property)); - UnresolvedRefs.Append(NewUnresolvedRefs); - } - else if (InObjectReferencesMap.Find(Offset)) + if (ReferencesChanged(InObjectReferencesMap, Offset, bHasReferences, NewDynamicRefs, NewUnresolvedRefs)) { - InObjectReferencesMap.Remove(Offset); + if (bHasReferences) + { + InObjectReferencesMap.Add(Offset, FObjectReferences(ValueData, CountBits, MoveTemp(NewDynamicRefs), MoveTemp(NewUnresolvedRefs), ShadowOffset, ParentIndex, Property)); + } + else + { + InObjectReferencesMap.Remove(Offset); + } + + bOutReferencesChanged = true; } } else if (UBoolProperty* BoolProperty = Cast(Property)) @@ -323,28 +428,40 @@ void ComponentReader::ApplyProperty(Schema_Object* Object, Schema_FieldId FieldI { FUnrealObjectRef ObjectRef = IndexObjectRefFromSchema(Object, FieldId, Index); check(ObjectRef != FUnrealObjectRef::UNRESOLVED_OBJECT_REF); - bool bUnresolved = false; - - UObject* ObjectValue = FUnrealObjectRef::ToObjectPtr(ObjectRef, PackageMap, bUnresolved); - if (bUnresolved) + if (Cast(Property)) { - InObjectReferencesMap.Add(Offset, FObjectReferences(ObjectRef, ShadowOffset, ParentIndex, Property)); - UnresolvedRefs.Add(ObjectRef); + FSoftObjectPtr* ObjectPtr = reinterpret_cast(Data); + *ObjectPtr = FUnrealObjectRef::ToSoftObjectPath(ObjectRef); } else { - ObjectProperty->SetObjectPropertyValue(Data, ObjectValue); - if (ObjectValue != nullptr) + bool bUnresolved = false; + UObject* ObjectValue = FUnrealObjectRef::ToObjectPtr(ObjectRef, PackageMap, bUnresolved); + + const bool bHasReferences = bUnresolved || (ObjectValue && !ObjectValue->IsFullNameStableForNetworking()); + + if (ReferencesChanged(InObjectReferencesMap, Offset, bHasReferences, ObjectRef, bUnresolved)) + { + if (bHasReferences) + { + InObjectReferencesMap.Add(Offset, FObjectReferences(ObjectRef, bUnresolved, ShadowOffset, ParentIndex, Property)); + } + else + { + InObjectReferencesMap.Remove(Offset); + } + bOutReferencesChanged = true; + } + if(!bUnresolved) { - checkf(ObjectValue->IsA(ObjectProperty->PropertyClass), TEXT("Object ref %s maps to object %s with the wrong class."), *ObjectRef.ToString(), *ObjectValue->GetFullName()); + ObjectProperty->SetObjectPropertyValue(Data, ObjectValue); + if (ObjectValue != nullptr) + { + checkf(ObjectValue->IsA(ObjectProperty->PropertyClass), TEXT("Object ref %s maps to object %s with the wrong class."), *ObjectRef.ToString(), *ObjectValue->GetFullName()); + } } } - - if (!bUnresolved && InObjectReferencesMap.Find(Offset)) - { - InObjectReferencesMap.Remove(Offset); - } } else if (UNameProperty* NameProperty = Cast(Property)) { @@ -366,7 +483,7 @@ void ComponentReader::ApplyProperty(Schema_Object* Object, Schema_FieldId FieldI } else { - ApplyProperty(Object, FieldId, InObjectReferencesMap, Index, EnumProperty->GetUnderlyingProperty(), Data, Offset, ShadowOffset, ParentIndex); + ApplyProperty(Object, FieldId, InObjectReferencesMap, Index, EnumProperty->GetUnderlyingProperty(), Data, Offset, ShadowOffset, ParentIndex, bOutReferencesChanged); } } else @@ -375,8 +492,10 @@ void ComponentReader::ApplyProperty(Schema_Object* Object, Schema_FieldId FieldI } } -void ComponentReader::ApplyArray(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, UArrayProperty* Property, uint8* Data, int32 Offset, int32 ShadowOffset, int32 ParentIndex) +void ComponentReader::ApplyArray(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, UArrayProperty* Property, uint8* Data, int32 Offset, int32 ShadowOffset, int32 ParentIndex, bool& bOutReferencesChanged) { + SCOPE_CYCLE_COUNTER(STAT_ReaderApplyArray); + FObjectReferencesMap* ArrayObjectReferences; bool bNewArrayMap = false; if (FObjectReferences* ExistingEntry = InObjectReferencesMap.Find(Offset)) @@ -399,7 +518,7 @@ void ComponentReader::ApplyArray(Schema_Object* Object, Schema_FieldId FieldId, for (int i = 0; i < Count; i++) { int32 ElementOffset = i * Property->Inner->ElementSize; - ApplyProperty(Object, FieldId, *ArrayObjectReferences, i, Property->Inner, ArrayHelper.GetRawPtr(i), ElementOffset, ElementOffset, ParentIndex); + ApplyProperty(Object, FieldId, *ArrayObjectReferences, i, Property->Inner, ArrayHelper.GetRawPtr(i), ElementOffset, ElementOffset, ParentIndex, bOutReferencesChanged); } if (ArrayObjectReferences->Num() > 0) diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityFactory.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityFactory.cpp new file mode 100644 index 0000000000..63f9b890c7 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityFactory.cpp @@ -0,0 +1,503 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Utils/EntityFactory.h" + +#include "EngineClasses/SpatialActorChannel.h" +#include "EngineClasses/SpatialNetDriver.h" +#include "EngineClasses/SpatialPackageMapClient.h" +#include "EngineClasses/SpatialVirtualWorkerTranslator.h" +#include "Interop/SpatialRPCService.h" +#include "LoadBalancing/AbstractLBStrategy.h" +#include "Schema/AuthorityIntent.h" +#include "Schema/ComponentPresence.h" +#include "Schema/Heartbeat.h" +#include "Schema/ClientRPCEndpointLegacy.h" +#include "Schema/ServerRPCEndpointLegacy.h" +#include "Schema/NetOwningClientWorker.h" +#include "Schema/RPCPayload.h" +#include "Schema/Singleton.h" +#include "Schema/SpatialDebugging.h" +#include "Schema/SpawnData.h" +#include "Schema/Tombstone.h" +#include "SpatialCommonTypes.h" +#include "SpatialConstants.h" +#include "Utils/ComponentFactory.h" +#include "Utils/InspectionColors.h" +#include "Utils/InterestFactory.h" +#include "Utils/SpatialActorGroupManager.h" +#include "Utils/SpatialActorUtils.h" +#include "Utils/SpatialDebugger.h" + +#include "Engine/Engine.h" + +DEFINE_LOG_CATEGORY(LogEntityFactory); + +namespace SpatialGDK +{ + +EntityFactory::EntityFactory(USpatialNetDriver* InNetDriver, USpatialPackageMapClient* InPackageMap, USpatialClassInfoManager* InClassInfoManager, SpatialActorGroupManager* InActorGroupManager, SpatialRPCService* InRPCService) + : NetDriver(InNetDriver) + , PackageMap(InPackageMap) + , ClassInfoManager(InClassInfoManager) + , ActorGroupManager(InActorGroupManager) + , RPCService(InRPCService) +{ } + +TArray EntityFactory::CreateEntityComponents(USpatialActorChannel* Channel, FRPCsOnEntityCreationMap& OutgoingOnCreateEntityRPCs, uint32& OutBytesWritten) +{ + AActor* Actor = Channel->Actor; + UClass* Class = Actor->GetClass(); + Worker_EntityId EntityId = Channel->GetEntityId(); + + FString ClientWorkerAttribute = GetConnectionOwningWorkerId(Actor); + + WorkerRequirementSet AnyServerRequirementSet; + WorkerRequirementSet AnyServerOrClientRequirementSet = { SpatialConstants::UnrealClientAttributeSet }; + + WorkerAttributeSet OwningClientAttributeSet = { ClientWorkerAttribute }; + + WorkerRequirementSet AnyServerOrOwningClientRequirementSet = { OwningClientAttributeSet }; + WorkerRequirementSet OwningClientOnlyRequirementSet = { OwningClientAttributeSet }; + + for (const FName& WorkerType : GetDefault()->ServerWorkerTypes) + { + WorkerAttributeSet ServerWorkerAttributeSet = { WorkerType.ToString() }; + + AnyServerRequirementSet.Add(ServerWorkerAttributeSet); + AnyServerOrClientRequirementSet.Add(ServerWorkerAttributeSet); + AnyServerOrOwningClientRequirementSet.Add(ServerWorkerAttributeSet); + } + + const FClassInfo& Info = ClassInfoManager->GetOrCreateClassInfoByClass(Class); + + const USpatialGDKSettings* SpatialSettings = GetDefault(); + + const FName AclAuthoritativeWorkerType = SpatialSettings->bEnableOffloading ? + ActorGroupManager->GetWorkerTypeForActorGroup(USpatialStatics::GetActorGroupForActor(Actor)) : + Info.WorkerType; + + WorkerAttributeSet WorkerAttributeOrSpecificWorker{ AclAuthoritativeWorkerType.ToString() }; + VirtualWorkerId IntendedVirtualWorkerId = SpatialConstants::INVALID_VIRTUAL_WORKER_ID; + + // Add Load Balancer Attribute if we are using the load balancer. + if (SpatialSettings->bEnableUnrealLoadBalancer) + { + AnyServerRequirementSet.Add(SpatialConstants::GetLoadBalancerAttributeSet(SpatialSettings->LoadBalancingWorkerType.WorkerTypeName)); + AnyServerOrClientRequirementSet.Add(SpatialConstants::GetLoadBalancerAttributeSet(SpatialSettings->LoadBalancingWorkerType.WorkerTypeName)); + AnyServerOrOwningClientRequirementSet.Add(SpatialConstants::GetLoadBalancerAttributeSet(SpatialSettings->LoadBalancingWorkerType.WorkerTypeName)); + + const UAbstractLBStrategy* LBStrategy = NetDriver->LoadBalanceStrategy; + check(LBStrategy != nullptr); + IntendedVirtualWorkerId = LBStrategy->WhoShouldHaveAuthority(*Actor); + if (IntendedVirtualWorkerId == SpatialConstants::INVALID_VIRTUAL_WORKER_ID) + { + UE_LOG(LogEntityFactory, Error, TEXT("Load balancing strategy provided invalid virtual worker ID to spawn actor with. Actor: %s. Strategy: %s"), *Actor->GetName(), *LBStrategy->GetName()); + } + else + { + const PhysicalWorkerName* IntendedAuthoritativePhysicalWorkerName = NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(IntendedVirtualWorkerId); + WorkerAttributeOrSpecificWorker = { FString::Format(TEXT("workerId:{0}"), { *IntendedAuthoritativePhysicalWorkerName }) }; + } + } + + const WorkerRequirementSet AuthoritativeWorkerRequirementSet = { WorkerAttributeOrSpecificWorker }; + + WorkerRequirementSet ReadAcl; + if (Class->HasAnySpatialClassFlags(SPATIALCLASS_ServerOnly)) + { + ReadAcl = AnyServerRequirementSet; + } + else if (Actor->IsA()) + { + ReadAcl = AnyServerOrOwningClientRequirementSet; + } + else + { + ReadAcl = AnyServerOrClientRequirementSet; + } + + WriteAclMap ComponentWriteAcl; + ComponentWriteAcl.Add(SpatialConstants::POSITION_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + ComponentWriteAcl.Add(SpatialConstants::INTEREST_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + ComponentWriteAcl.Add(SpatialConstants::SPAWN_DATA_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + ComponentWriteAcl.Add(SpatialConstants::DORMANT_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + ComponentWriteAcl.Add(SpatialConstants::SERVER_TO_SERVER_COMMAND_ENDPOINT_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + ComponentWriteAcl.Add(SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + ComponentWriteAcl.Add(SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + + if (SpatialSettings->UseRPCRingBuffer() && RPCService != nullptr) + { + ComponentWriteAcl.Add(SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID, OwningClientOnlyRequirementSet); + ComponentWriteAcl.Add(SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + ComponentWriteAcl.Add(SpatialConstants::MULTICAST_RPCS_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + } + else + { + ComponentWriteAcl.Add(SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY, AuthoritativeWorkerRequirementSet); + ComponentWriteAcl.Add(SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID_LEGACY, AuthoritativeWorkerRequirementSet); + ComponentWriteAcl.Add(SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY, OwningClientOnlyRequirementSet); + + // If there are pending RPCs, add this component. + if (OutgoingOnCreateEntityRPCs.Contains(Actor)) + { + ComponentWriteAcl.Add(SpatialConstants::RPCS_ON_ENTITY_CREATION_ID, AuthoritativeWorkerRequirementSet); + } + } + + if (SpatialSettings->bEnableUnrealLoadBalancer) + { + const WorkerRequirementSet ACLRequirementSet = { SpatialConstants::GetLoadBalancerAttributeSet(SpatialSettings->LoadBalancingWorkerType.WorkerTypeName) }; + ComponentWriteAcl.Add(SpatialConstants::ENTITY_ACL_COMPONENT_ID, ACLRequirementSet); + ComponentWriteAcl.Add(SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + } + else + { + const WorkerAttributeSet ACLAttributeSet = { AclAuthoritativeWorkerType.ToString() }; + const WorkerRequirementSet ACLRequirementSet = { ACLAttributeSet }; + ComponentWriteAcl.Add(SpatialConstants::ENTITY_ACL_COMPONENT_ID, ACLRequirementSet); + } + + if (Actor->IsNetStartupActor()) + { + ComponentWriteAcl.Add(SpatialConstants::TOMBSTONE_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + } + + // If Actor is a PlayerController, add the heartbeat component. + if (Actor->IsA()) + { +#if !UE_BUILD_SHIPPING + ComponentWriteAcl.Add(SpatialConstants::DEBUG_METRICS_COMPONENT_ID, AuthoritativeWorkerRequirementSet); +#endif // !UE_BUILD_SHIPPING + ComponentWriteAcl.Add(SpatialConstants::HEARTBEAT_COMPONENT_ID, OwningClientOnlyRequirementSet); + } + + // Add all Interest component IDs to allow us to change it if needed. + ComponentWriteAcl.Add(SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + for (const auto ComponentId : ClassInfoManager->SchemaDatabase->NetCullDistanceComponentIds) + { + ComponentWriteAcl.Add(ComponentId, AuthoritativeWorkerRequirementSet); + } + + Worker_ComponentId ActorInterestComponentId = ClassInfoManager->ComputeActorInterestComponentId(Actor); + + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) + { + Worker_ComponentId ComponentId = Info.SchemaComponents[Type]; + if (ComponentId == SpatialConstants::INVALID_COMPONENT_ID) + { + return; + } + + ComponentWriteAcl.Add(ComponentId, AuthoritativeWorkerRequirementSet); + }); + + for (auto& SubobjectInfoPair : Info.SubobjectInfo) + { + const FClassInfo& SubobjectInfo = SubobjectInfoPair.Value.Get(); + + // Static subobjects aren't guaranteed to exist on actor instances, check they are present before adding write acls + TWeakObjectPtr Subobject = PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(EntityId, SubobjectInfoPair.Key)); + if (!Subobject.IsValid()) + { + continue; + } + + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) + { + Worker_ComponentId ComponentId = SubobjectInfo.SchemaComponents[Type]; + if (ComponentId == SpatialConstants::INVALID_COMPONENT_ID) + { + return; + } + + ComponentWriteAcl.Add(ComponentId, AuthoritativeWorkerRequirementSet); + }); + } + + // We want to have a stably named ref if this is an Actor placed in the world. + // We use this to indicate if a new Actor should be created, or to link a pre-existing Actor when receiving an AddEntityOp. + // Previously, IsFullNameStableForNetworking was used but this was only true if bNetLoadOnClient=true. + // Actors with bNetLoadOnClient=false also need a StablyNamedObjectRef for linking in the case of loading from a snapshot or the server crashes and restarts. + TSchemaOption StablyNamedObjectRef; + TSchemaOption bNetStartup; + if (Actor->HasAnyFlags(RF_WasLoaded) || Actor->bNetStartup) + { + // Since we've already received the EntityId for this Actor. It is guaranteed to be resolved + // with the package map by this point + FUnrealObjectRef OuterObjectRef = PackageMap->GetUnrealObjectRefFromObject(Actor->GetOuter()); + if (OuterObjectRef == FUnrealObjectRef::UNRESOLVED_OBJECT_REF) + { + FNetworkGUID NetGUID = PackageMap->ResolveStablyNamedObject(Actor->GetOuter()); + OuterObjectRef = PackageMap->GetUnrealObjectRefFromNetGUID(NetGUID); + } + + // No path in SpatialOS should contain a PIE prefix. + FString TempPath = Actor->GetFName().ToString(); + GEngine->NetworkRemapPath(NetDriver, TempPath, false /*bIsReading*/); + + StablyNamedObjectRef = FUnrealObjectRef(0, 0, TempPath, OuterObjectRef, true); + bNetStartup = Actor->bNetStartup; + } + + TArray ComponentDatas; + ComponentDatas.Add(Position(Coordinates::FromFVector(GetActorSpatialPosition(Actor))).CreatePositionData()); + ComponentDatas.Add(Metadata(Class->GetName()).CreateMetadataData()); + ComponentDatas.Add(SpawnData(Actor).CreateSpawnDataData()); + ComponentDatas.Add(UnrealMetadata(StablyNamedObjectRef, Class->GetPathName(), bNetStartup).CreateUnrealMetadataData()); + ComponentDatas.Add(NetOwningClientWorker(GetConnectionOwningWorkerId(Channel->Actor)).CreateNetOwningClientWorkerData()); + + if (!Class->HasAnySpatialClassFlags(SPATIALCLASS_NotPersistent)) + { + ComponentDatas.Add(Persistence().CreatePersistenceData()); + } + + if (SpatialSettings->bEnableUnrealLoadBalancer) + { + ComponentDatas.Add(AuthorityIntent::CreateAuthorityIntentData(IntendedVirtualWorkerId)); + } + + if (NetDriver->SpatialDebugger != nullptr) + { + if (SpatialSettings->bEnableUnrealLoadBalancer) + { + check(NetDriver->VirtualWorkerTranslator != nullptr); + + VirtualWorkerId IntentVirtualWorkerId = NetDriver->VirtualWorkerTranslator->GetLocalVirtualWorkerId(); + + const PhysicalWorkerName* PhysicalWorkerName = NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(IntentVirtualWorkerId); + FColor InvalidServerTintColor = NetDriver->SpatialDebugger->InvalidServerTintColor; + FColor IntentColor = PhysicalWorkerName == nullptr ? InvalidServerTintColor : SpatialGDK::GetColorForWorkerName(*PhysicalWorkerName); + + SpatialDebugging DebuggingInfo(SpatialConstants::INVALID_VIRTUAL_WORKER_ID, InvalidServerTintColor, IntentVirtualWorkerId, IntentColor, false); + ComponentDatas.Add(DebuggingInfo.CreateSpatialDebuggingData()); + } + + ComponentWriteAcl.Add(SpatialConstants::SPATIAL_DEBUGGING_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + } + + if (Class->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)) + { + ComponentDatas.Add(Singleton().CreateSingletonData()); + } + + if (ActorInterestComponentId != SpatialConstants::INVALID_COMPONENT_ID) + { + ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(ActorInterestComponentId)); + } + + if (Actor->NetDormancy >= DORM_DormantAll) + { + ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::DORMANT_COMPONENT_ID)); + } + + if (Actor->IsA()) + { +#if !UE_BUILD_SHIPPING + ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::DEBUG_METRICS_COMPONENT_ID)); +#endif // !UE_BUILD_SHIPPING + ComponentDatas.Add(Heartbeat().CreateHeartbeatData()); + } + + USpatialLatencyTracer* Tracer = USpatialLatencyTracer::GetTracer(Actor); + + ComponentFactory DataFactory(false, NetDriver, Tracer); + + FRepChangeState InitialRepChanges = Channel->CreateInitialRepChangeState(Actor); + FHandoverChangeState InitialHandoverChanges = Channel->CreateInitialHandoverChangeState(Info); + + TArray DynamicComponentDatas = DataFactory.CreateComponentDatas(Actor, Info, InitialRepChanges, InitialHandoverChanges, OutBytesWritten); + + ComponentDatas.Append(DynamicComponentDatas); + + ComponentDatas.Add(NetDriver->InterestFactory->CreateInterestData(Actor, Info, EntityId)); + + ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::SERVER_TO_SERVER_COMMAND_ENDPOINT_COMPONENT_ID)); + + if (SpatialSettings->UseRPCRingBuffer() && RPCService != nullptr) + { + ComponentDatas.Append(RPCService->GetRPCComponentsOnEntityCreation(EntityId)); + } + else + { + ComponentDatas.Add(ClientRPCEndpointLegacy().CreateRPCEndpointData()); + ComponentDatas.Add(ServerRPCEndpointLegacy().CreateRPCEndpointData()); + ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID_LEGACY)); + + if (RPCsOnEntityCreation* QueuedRPCs = OutgoingOnCreateEntityRPCs.Find(Actor)) + { + if (QueuedRPCs->HasRPCPayloadData()) + { + ComponentDatas.Add(QueuedRPCs->CreateRPCPayloadData()); + } + OutgoingOnCreateEntityRPCs.Remove(Actor); + } + } + + // Only add subobjects which are replicating + for (auto RepSubobject = Channel->ReplicationMap.CreateIterator(); RepSubobject; ++RepSubobject) + { + if (UObject* Subobject = RepSubobject.Value()->GetWeakObjectPtr().Get()) + { + if (Subobject == Actor) + { + // Actor's replicator is also contained in ReplicationMap. + continue; + } + + // If this object is not in the PackageMap, it has been dynamically created. + if (!PackageMap->GetUnrealObjectRefFromObject(Subobject).IsValid()) + { + const FClassInfo* SubobjectInfo = PackageMap->TryResolveNewDynamicSubobjectAndGetClassInfo(Subobject); + + if (SubobjectInfo == nullptr) + { + // This is a failure but there is already a log inside TryResolveNewDynamicSubbojectAndGetClassInfo + continue; + } + } + + const FClassInfo& SubobjectInfo = ClassInfoManager->GetOrCreateClassInfoByObject(Subobject); + + FRepChangeState SubobjectRepChanges = Channel->CreateInitialRepChangeState(Subobject); + FHandoverChangeState SubobjectHandoverChanges = Channel->CreateInitialHandoverChangeState(SubobjectInfo); + + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) + { + if (SubobjectInfo.SchemaComponents[Type] != SpatialConstants::INVALID_COMPONENT_ID) + { + ComponentWriteAcl.Add(SubobjectInfo.SchemaComponents[Type], AuthoritativeWorkerRequirementSet); + } + }); + + TArray ActorSubobjectDatas = DataFactory.CreateComponentDatas(Subobject, SubobjectInfo, SubobjectRepChanges, SubobjectHandoverChanges, OutBytesWritten); + + ComponentDatas.Append(ActorSubobjectDatas); + } + } + + // Or if the subobject has handover properties, add it as well. + // NOTE: this is only for subobjects that are a part of the CDO. + // NOT dynamic subobjects which have been added before entity creation. + for (auto& SubobjectInfoPair : Info.SubobjectInfo) + { + const FClassInfo& SubobjectInfo = SubobjectInfoPair.Value.Get(); + + // Static subobjects aren't guaranteed to exist on actor instances, check they are present before adding write acls + TWeakObjectPtr WeakSubobject = PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(Channel->GetEntityId(), SubobjectInfoPair.Key)); + if (!WeakSubobject.IsValid()) + { + continue; + } + + UObject* Subobject = WeakSubobject.Get(); + + if (SubobjectInfo.SchemaComponents[SCHEMA_Handover] == SpatialConstants::INVALID_COMPONENT_ID) + { + continue; + } + + // If it contains it, we've already created handover data for it. + if (Channel->ReplicationMap.Contains(Subobject)) + { + continue; + } + + FHandoverChangeState SubobjectHandoverChanges = Channel->CreateInitialHandoverChangeState(SubobjectInfo); + + FWorkerComponentData SubobjectHandoverData = DataFactory.CreateHandoverComponentData(SubobjectInfo.SchemaComponents[SCHEMA_Handover], Subobject, SubobjectInfo, SubobjectHandoverChanges, OutBytesWritten); + + ComponentDatas.Add(SubobjectHandoverData); + + ComponentWriteAcl.Add(SubobjectInfo.SchemaComponents[SCHEMA_Handover], AuthoritativeWorkerRequirementSet); + } + + ComponentDatas.Add(EntityAcl(ReadAcl, ComponentWriteAcl).CreateEntityAclData()); + + return ComponentDatas; +} + +// This method should be called once all the components besides ComponentPresence have been added to the +// ComponentDatas list. +TArray EntityFactory::GetComponentPresenceList(const TArray& ComponentDatas) +{ + TArray ComponentPresenceList; + ComponentPresenceList.SetNum(ComponentDatas.Num() + 1); + for (int i = 0; i < ComponentDatas.Num(); i++) + { + ComponentPresenceList[i] = ComponentDatas[i].component_id; + } + ComponentPresenceList[ComponentDatas.Num()] = SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID; + return ComponentPresenceList; +} + +TArray EntityFactory::CreateTombstoneEntityComponents(AActor* Actor) +{ + check(Actor->IsNetStartupActor()); + + const UClass* Class = Actor->GetClass(); + + // Construct an ACL for a read-only entity. + WorkerRequirementSet AnyServerRequirementSet; + WorkerRequirementSet AnyServerOrClientRequirementSet = { SpatialConstants::UnrealClientAttributeSet }; + + for (const FName& WorkerType : GetDefault()->ServerWorkerTypes) + { + WorkerAttributeSet ServerWorkerAttributeSet = { WorkerType.ToString() }; + + AnyServerRequirementSet.Add(ServerWorkerAttributeSet); + AnyServerOrClientRequirementSet.Add(ServerWorkerAttributeSet); + } + + // Add Zoning Attribute if we are using the load balancer. + const USpatialGDKSettings* SpatialSettings = GetDefault(); + if (SpatialSettings->bEnableUnrealLoadBalancer) + { + AnyServerRequirementSet.Add(SpatialConstants::GetLoadBalancerAttributeSet(SpatialSettings->LoadBalancingWorkerType.WorkerTypeName)); + AnyServerOrClientRequirementSet.Add(SpatialConstants::GetLoadBalancerAttributeSet(SpatialSettings->LoadBalancingWorkerType.WorkerTypeName)); + } + + WorkerRequirementSet ReadAcl; + if (Class->HasAnySpatialClassFlags(SPATIALCLASS_ServerOnly)) + { + ReadAcl = AnyServerRequirementSet; + } + else + { + ReadAcl = AnyServerOrClientRequirementSet; + } + + // Get a stable object ref. + FUnrealObjectRef OuterObjectRef = PackageMap->GetUnrealObjectRefFromObject(Actor->GetOuter()); + if (OuterObjectRef == FUnrealObjectRef::UNRESOLVED_OBJECT_REF) + { + const FNetworkGUID NetGUID = PackageMap->ResolveStablyNamedObject(Actor->GetOuter()); + OuterObjectRef = PackageMap->GetUnrealObjectRefFromNetGUID(NetGUID); + } + + // No path in SpatialOS should contain a PIE prefix. + FString TempPath = Actor->GetFName().ToString(); + GEngine->NetworkRemapPath(NetDriver, TempPath, false /*bIsReading*/); + const TSchemaOption StablyNamedObjectRef = FUnrealObjectRef(0, 0, TempPath, OuterObjectRef, true); + + TArray Components; + Components.Add(Position(Coordinates::FromFVector(GetActorSpatialPosition(Actor))).CreatePositionData()); + Components.Add(Metadata(Class->GetName()).CreateMetadataData()); + Components.Add(UnrealMetadata(StablyNamedObjectRef, Class->GetPathName(), true).CreateUnrealMetadataData()); + Components.Add(Tombstone().CreateData()); + Components.Add(EntityAcl(ReadAcl, WriteAclMap()).CreateEntityAclData()); + + Worker_ComponentId ActorInterestComponentId = ClassInfoManager->ComputeActorInterestComponentId(Actor); + if (ActorInterestComponentId != SpatialConstants::INVALID_COMPONENT_ID) + { + Components.Add(ComponentFactory::CreateEmptyComponentData(ActorInterestComponentId)); + } + + if (!Class->HasAnySpatialClassFlags(SPATIALCLASS_NotPersistent)) + { + Components.Add(Persistence().CreatePersistenceData()); + } + + return Components; +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/InspectionColors.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/InspectionColors.cpp new file mode 100644 index 0000000000..515e8b9806 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/InspectionColors.cpp @@ -0,0 +1,114 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Utils/InspectionColors.h" + +#include "Containers/UnrealString.h" +#include "Math/Color.h" +#include "Math/UnrealMathUtility.h" +#include "Misc/Char.h" + +#include "cstring" + +namespace SpatialGDK +{ + +namespace +{ + const int32 MIN_HUE = 10; + const int32 MAX_HUE = 350; + const int32 MIN_SATURATION = 60; + const int32 MAX_SATURATION = 100; + const int32 MIN_LIGHTNESS = 25; + const int32 MAX_LIGHTNESS = 60; + + int64 GenerateValueFromThresholds(int64 Hash, int32 Min, int32 Max) + { + return Hash % FMath::Abs(Max - Min) + Min; + } + + FColor HSLtoRGB(double Hue, double Saturation, double Lightness) + { + // const[h, s, l] = hsl; + // Must be fractions of 1 + + const double c = (1 - FMath::Abs(2 * Lightness / 100 - 1)) * Saturation / 100; + const double x = c * (1 - FMath::Abs(FMath::Fmod((Hue / 60), 2) - 1)); + const double m = Lightness / 100 - c / 2; + + double r = 0; + double g = 0; + double b = 0; + + if (0 <= Hue && Hue < 60) { + r = c; + g = x; + b = 0; + } + else if (60 <= Hue && Hue < 120) { + r = x; + g = c; + b = 0; + } + else if (120 <= Hue && Hue < 180) { + r = 0; + g = c; + b = x; + } + else if (180 <= Hue && Hue < 240) { + r = 0; + g = x; + b = c; + } + else if (240 <= Hue && Hue < 300) { + r = x; + g = 0; + b = c; + } + else if (300 <= Hue && Hue <= 360) { + r = c; + g = 0; + b = x; + } + r = (r + m) * 255; + g = (g + m) * 255; + b = (b + m) * 255; + + return FColor{ static_cast(r), static_cast(g), static_cast(b) }; + } + + int64 DJBReverseHash(const PhysicalWorkerName& WorkerName) { + const int32 StringLength = WorkerName.Len(); + int64 Hash = 5381; + for (int32 i = StringLength - 1; i > 0; --i) { + // We're mimicking the Inspector logic which is in JS. In JavaScript, + // a number is stored as a 64-bit floating point number but the bit-wise + // operation is performed on a 32-bit integer i.e. to perform a + // bit-operation JavaScript converts the number into a 32-bit binary + // number (signed) and perform the operation and convert back the result + // to a 64-bit number. + // Ideally, this would just be ((static_cast(Hash)) << 5) but left + // shifting a signed int with overflow is undefined so we have to memcpy + // to an unsigned. + uint64 BitShiftingScratchRegister; + std::memcpy(&BitShiftingScratchRegister, &Hash, sizeof(int64)); + int32 BitShiftedHash = static_cast((BitShiftingScratchRegister << 5) & 0xFFFFFFFF); + Hash = BitShiftedHash + Hash + static_cast(WorkerName[i]); + } + return FMath::Abs(Hash); + } +} + +FColor GetColorForWorkerName(const PhysicalWorkerName& WorkerName) +{ + int64 Hash = DJBReverseHash(WorkerName); + + const double Lightness = GenerateValueFromThresholds(Hash, MIN_LIGHTNESS, MAX_LIGHTNESS); + const double Saturation = GenerateValueFromThresholds(Hash, MIN_SATURATION, MAX_SATURATION); + // Provides additional color variance for potentially sequential hashes + auto abs = FMath::Abs((double)Hash / Saturation + Lightness); + Hash = FMath::FloorToInt(abs); + const double Hue = GenerateValueFromThresholds(Hash, MIN_HUE, MAX_HUE); + + return SpatialGDK::HSLtoRGB(Hue, Saturation, Lightness); +} +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/Interest/NetCullDistanceInterest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/Interest/NetCullDistanceInterest.cpp new file mode 100644 index 0000000000..52209ce8a9 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/Interest/NetCullDistanceInterest.cpp @@ -0,0 +1,335 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Utils/Interest/NetCullDistanceInterest.h" + +#include "UObject/UObjectIterator.h" + +#include "SpatialGDKSettings.h" + +DEFINE_LOG_CATEGORY(LogNetCullDistanceInterest); + +// Use 0 to represent "full" frequency here. Zero actually represents "never" when set in spatial, so this will be converted +// to an empty optional later. +const float FullFrequencyHz = 0.f; + +namespace SpatialGDK +{ + +// And this the empty optional type it will be translated to. +const TSchemaOption FullFrequencyOptional = TSchemaOption(); + +FrequencyConstraints NetCullDistanceInterest::CreateCheckoutRadiusConstraints(USpatialClassInfoManager* InClassInfoManager) +{ + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + + if (!SpatialGDKSettings->bEnableNetCullDistanceInterest) + { + return NetCullDistanceInterest::CreateLegacyNetCullDistanceConstraint(InClassInfoManager); + } + + if (!SpatialGDKSettings->bEnableNetCullDistanceFrequency) + { + return NetCullDistanceInterest::CreateNetCullDistanceConstraint(InClassInfoManager); + } + + return NetCullDistanceInterest::CreateNetCullDistanceConstraintWithFrequency(InClassInfoManager); +} + +FrequencyConstraints NetCullDistanceInterest::CreateLegacyNetCullDistanceConstraint(USpatialClassInfoManager* InClassInfoManager) +{ + // Checkout Radius constraints are defined by the NetCullDistanceSquared property on actors. + // - Checkout radius is a RelativeCylinder constraint on the player controller. + // - NetCullDistanceSquared on AActor is used to define the default checkout radius with no other constraints. + // - NetCullDistanceSquared on other actor types is used to define additional constraints if needed. + // - If a subtype defines a radius smaller than a parent type, then its requirements are already captured. + // - If a subtype defines a radius larger than all parent types, then it needs an additional constraint. + // - Other than the default from AActor, all radius constraints also include Component constraints to + // capture specific types, including all derived types of that actor. + + QueryConstraint CheckoutRadiusConstraint; + + CheckoutRadiusConstraint.OrConstraint.Add(NetCullDistanceInterest::GetDefaultCheckoutRadiusConstraint()); + + // Get interest distances for each actor. + TMap ActorComponentSetToRadius = NetCullDistanceInterest::GetActorTypeToRadius(); + + // For every interest distance that we still want, build a map from radius to list of actor type components that match that radius. + TMap> DistanceToActorTypeComponents = NetCullDistanceInterest::DedupeDistancesAcrossActorTypes( + ActorComponentSetToRadius); + + // The previously built map removes duplicates of spatial constraints. Now the actual query constraints can be built of the form: + // OR(AND(cylinder(radius), OR(actor 1 components, actor 2 components, ...)), ...) + // which is equivalent to having a separate spatial query for each actor type if the radius is the same. + TArray CheckoutRadiusConstraints = NetCullDistanceInterest::BuildNonDefaultActorCheckoutConstraints( + DistanceToActorTypeComponents, InClassInfoManager); + + // Add all the different actor queries to the overall checkout constraint. + for (auto& ActorCheckoutConstraint : CheckoutRadiusConstraints) + { + CheckoutRadiusConstraint.OrConstraint.Add(ActorCheckoutConstraint); + } + + return { { FullFrequencyOptional, CheckoutRadiusConstraint } }; +} + +FrequencyConstraints NetCullDistanceInterest::CreateNetCullDistanceConstraint(USpatialClassInfoManager* InClassInfoManager) +{ + QueryConstraint CheckoutRadiusConstraintRoot; + + const TMap& NetCullDistancesToComponentIds = InClassInfoManager->GetNetCullDistanceToComponentIds(); + + for (const auto& DistanceComponentPair : NetCullDistancesToComponentIds) + { + const float MaxCheckoutRadiusMeters = NetCullDistanceInterest::NetCullDistanceSquaredToSpatialDistance(DistanceComponentPair.Key); + + QueryConstraint ComponentConstraint; + ComponentConstraint.ComponentConstraint = DistanceComponentPair.Value; + + QueryConstraint RadiusConstraint; + RadiusConstraint.RelativeCylinderConstraint = RelativeCylinderConstraint{ MaxCheckoutRadiusMeters }; + + QueryConstraint CheckoutRadiusConstraint; + CheckoutRadiusConstraint.AndConstraint.Add(RadiusConstraint); + CheckoutRadiusConstraint.AndConstraint.Add(ComponentConstraint); + + CheckoutRadiusConstraintRoot.OrConstraint.Add(CheckoutRadiusConstraint); + } + + return { { FullFrequencyOptional, CheckoutRadiusConstraintRoot } }; +} + +FrequencyConstraints NetCullDistanceInterest::CreateNetCullDistanceConstraintWithFrequency(USpatialClassInfoManager* InClassInfoManager) +{ + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + const TMap& NetCullDistancesToComponentIds = InClassInfoManager->GetNetCullDistanceToComponentIds(); + + FrequencyToConstraintsMap FrequencyToConstraints; + + for (const auto& DistanceComponentPair : NetCullDistancesToComponentIds) + { + const float MaxCheckoutRadiusMeters = NetCullDistanceInterest::NetCullDistanceSquaredToSpatialDistance(DistanceComponentPair.Key); + + QueryConstraint ComponentConstraint; + ComponentConstraint.ComponentConstraint = DistanceComponentPair.Value; + + float FullFrequencyCheckoutRadius = MaxCheckoutRadiusMeters * SpatialGDKSettings->FullFrequencyNetCullDistanceRatio; + + QueryConstraint RadiusConstraint; + RadiusConstraint.RelativeCylinderConstraint = RelativeCylinderConstraint{ FullFrequencyCheckoutRadius }; + + QueryConstraint CheckoutRadiusConstraint; + CheckoutRadiusConstraint.AndConstraint.Add(RadiusConstraint); + CheckoutRadiusConstraint.AndConstraint.Add(ComponentConstraint); + + AddToFrequencyConstraintMap(FullFrequencyHz, CheckoutRadiusConstraint, FrequencyToConstraints); + + // Add interest query for specified distance/frequency pairs + for (const auto& DistanceFrequencyPair : SpatialGDKSettings->InterestRangeFrequencyPairs) + { + float CheckoutRadius = MaxCheckoutRadiusMeters * DistanceFrequencyPair.DistanceRatio; + + QueryConstraint FrequencyRadiusConstraint; + FrequencyRadiusConstraint.RelativeCylinderConstraint = RelativeCylinderConstraint{ CheckoutRadius }; + + QueryConstraint FrequencyCheckoutRadiusConstraint; + FrequencyCheckoutRadiusConstraint.AndConstraint.Add(FrequencyRadiusConstraint); + FrequencyCheckoutRadiusConstraint.AndConstraint.Add(ComponentConstraint); + + AddToFrequencyConstraintMap(DistanceFrequencyPair.Frequency, FrequencyCheckoutRadiusConstraint, FrequencyToConstraints); + } + } + + FrequencyConstraints CheckoutConstraints; + + // De dupe across frequencies. + for (auto& FrequencyConstraintsPair : FrequencyToConstraints) + { + TSchemaOption SpatialFrequency = FrequencyConstraintsPair.Key == FullFrequencyHz ? FullFrequencyOptional : TSchemaOption(FrequencyConstraintsPair.Key); + if (FrequencyConstraintsPair.Value.Num() == 1) + { + CheckoutConstraints.Add({ SpatialFrequency, FrequencyConstraintsPair.Value[0] }); + continue; + } + QueryConstraint RadiusDisjunct; + RadiusDisjunct.OrConstraint.Append(FrequencyConstraintsPair.Value); + CheckoutConstraints.Add({ SpatialFrequency, RadiusDisjunct }); + } + + return CheckoutConstraints; +} + +void NetCullDistanceInterest::AddToFrequencyConstraintMap(const float Frequency, const QueryConstraint& Constraint, FrequencyToConstraintsMap& OutFrequencyToConstraints) +{ + // 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. + TArray& ConstraintList = OutFrequencyToConstraints.FindOrAdd(Frequency); + ConstraintList.Add(Constraint); +} + +QueryConstraint NetCullDistanceInterest::GetDefaultCheckoutRadiusConstraint() +{ + const float MaxDistanceSquared = GetDefault()->MaxNetCullDistanceSquared; + + // Use AActor's ClientInterestDistance for the default radius (all actors in that radius will be checked out) + const AActor* DefaultActor = Cast(AActor::StaticClass()->GetDefaultObject()); + + float DefaultDistanceSquared = DefaultActor->NetCullDistanceSquared; + + if (MaxDistanceSquared > FLT_EPSILON && DefaultDistanceSquared > MaxDistanceSquared) + { + UE_LOG(LogNetCullDistanceInterest, Warning, TEXT("Default NetCullDistanceSquared is too large, clamping from %f to %f"), + DefaultDistanceSquared, MaxDistanceSquared); + + DefaultDistanceSquared = MaxDistanceSquared; + } + + const float DefaultCheckoutRadius = NetCullDistanceSquaredToSpatialDistance(DefaultDistanceSquared); + + QueryConstraint DefaultCheckoutRadiusConstraint; + DefaultCheckoutRadiusConstraint.RelativeCylinderConstraint = RelativeCylinderConstraint{ DefaultCheckoutRadius }; + + return DefaultCheckoutRadiusConstraint; +} + +TMap NetCullDistanceInterest::GetActorTypeToRadius() +{ + const AActor* DefaultActor = Cast(AActor::StaticClass()->GetDefaultObject()); + const float DefaultDistanceSquared = DefaultActor->NetCullDistanceSquared; + const float MaxDistanceSquared = GetDefault()->MaxNetCullDistanceSquared; + + // Gather ClientInterestDistance settings, and add any larger than the default radius to a list for processing. + TMap DiscoveredInterestDistancesSquared; + for (TObjectIterator It; It; ++It) + { + if (It->HasAnySpatialClassFlags(SPATIALCLASS_ServerOnly)) + { + continue; + } + if (!It->HasAnySpatialClassFlags(SPATIALCLASS_SpatialType)) + { + continue; + } + if (It->HasAnyClassFlags(CLASS_NewerVersionExists)) + { + // This skips classes generated for hot reload etc (i.e. REINST_, SKEL_, TRASHCLASS_) + continue; + } + if (!It->IsChildOf()) + { + continue; + } + + const AActor* IteratedDefaultActor = Cast(It->GetDefaultObject()); + if (IteratedDefaultActor->NetCullDistanceSquared > DefaultDistanceSquared) + { + float ActorNetCullDistanceSquared = IteratedDefaultActor->NetCullDistanceSquared; + + if (MaxDistanceSquared > FLT_EPSILON && IteratedDefaultActor->NetCullDistanceSquared > MaxDistanceSquared) + { + UE_LOG(LogNetCullDistanceInterest, Warning, TEXT("NetCullDistanceSquared for %s too large, clamping from %f to %f"), + *It->GetName(), ActorNetCullDistanceSquared, MaxDistanceSquared); + + ActorNetCullDistanceSquared = MaxDistanceSquared; + } + + DiscoveredInterestDistancesSquared.Add(*It, ActorNetCullDistanceSquared); + } + } + + // Sort the map for iteration so that parent classes are seen before derived classes. This lets us skip + // derived classes that have a smaller interest distance than a parent class. + DiscoveredInterestDistancesSquared.KeySort([](const UClass& LHS, const UClass& RHS) { + return LHS.IsChildOf(&RHS); + }); + + TMap ActorTypeToDistance; + + // If an actor's interest distance is smaller than that of a parent class, there's no need to add interest for that actor. + // Can't do inline removal since the sorted order is only guaranteed when the map isn't changed. + for (const auto& ActorInterestDistance : DiscoveredInterestDistancesSquared) + { + check(ActorInterestDistance.Key); + + // Spatial distance works in meters, whereas unreal distance works in cm^2. Here we do the dimensionally strange conversion between the two. + float SpatialDistance = NetCullDistanceSquaredToSpatialDistance(ActorInterestDistance.Value); + + bool bShouldAdd = true; + for (auto& OptimizedInterestDistance : ActorTypeToDistance) + { + if (ActorInterestDistance.Key->IsChildOf(OptimizedInterestDistance.Key) && SpatialDistance <= OptimizedInterestDistance.Value) + { + // No need to add this interest distance since it's captured in the optimized map already. + bShouldAdd = false; + break; + } + } + if (bShouldAdd) + { + ActorTypeToDistance.Add(ActorInterestDistance.Key, SpatialDistance); + } + } + + return ActorTypeToDistance; +} + +TMap> NetCullDistanceInterest::DedupeDistancesAcrossActorTypes(TMap ActorTypeToRadius) +{ + TMap> RadiusToActorTypes; + for (const auto& InterestDistance : ActorTypeToRadius) + { + if (!RadiusToActorTypes.Contains(InterestDistance.Value)) + { + TArray NewActorTypes; + RadiusToActorTypes.Add(InterestDistance.Value, NewActorTypes); + } + + auto& ActorTypes = RadiusToActorTypes[InterestDistance.Value]; + ActorTypes.Add(InterestDistance.Key); + } + return RadiusToActorTypes; +} + +TArray NetCullDistanceInterest::BuildNonDefaultActorCheckoutConstraints(TMap> DistanceToActorTypes, USpatialClassInfoManager* ClassInfoManager) +{ + TArray CheckoutConstraints; + for (const auto& DistanceActorsPair : DistanceToActorTypes) + { + QueryConstraint CheckoutRadiusConstraint; + + QueryConstraint RadiusConstraint; + RadiusConstraint.RelativeCylinderConstraint = RelativeCylinderConstraint{ DistanceActorsPair.Key }; + CheckoutRadiusConstraint.AndConstraint.Add(RadiusConstraint); + + QueryConstraint ActorTypesConstraint; + for (const auto ActorType : DistanceActorsPair.Value) + { + AddTypeHierarchyToConstraint(*ActorType, ActorTypesConstraint, ClassInfoManager); + } + CheckoutRadiusConstraint.AndConstraint.Add(ActorTypesConstraint); + + CheckoutConstraints.Add(CheckoutRadiusConstraint); + } + return CheckoutConstraints; +} + +float NetCullDistanceInterest::NetCullDistanceSquaredToSpatialDistance(float NetCullDistanceSquared) +{ + // Spatial distance works in meters, whereas unreal distance works in cm^2. Here we do the dimensionally strange conversion between the two. + return FMath::Sqrt(NetCullDistanceSquared / (100.f * 100.f)); +} + +// The type hierarchy added here are the component IDs that represent the actor type hierarchy. These are added to the given constraint as: +// OR(actor type component IDs...) +void NetCullDistanceInterest::AddTypeHierarchyToConstraint(const UClass& BaseType, QueryConstraint& OutConstraint, USpatialClassInfoManager* ClassInfoManager) +{ + check(ClassInfoManager); + TArray ComponentIds = ClassInfoManager->GetComponentIdsForClassHierarchy(BaseType); + for (Worker_ComponentId ComponentId : ComponentIds) + { + QueryConstraint ComponentTypeConstraint; + ComponentTypeConstraint.ComponentConstraint = ComponentId; + OutConstraint.OrConstraint.Add(ComponentTypeConstraint); + } +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp index 2612b44e7d..f0ecc7659c 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp @@ -2,366 +2,487 @@ #include "Utils/InterestFactory.h" -#include "Engine/World.h" -#include "Engine/Classes/GameFramework/Actor.h" -#include "GameFramework/PlayerController.h" -#include "UObject/UObjectIterator.h" - #include "EngineClasses/Components/ActorInterestComponent.h" #include "EngineClasses/SpatialNetConnection.h" +#include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" +#include "LoadBalancing/AbstractLBStrategy.h" #include "SpatialGDKSettings.h" #include "SpatialConstants.h" +#include "Utils/Interest/NetCullDistanceInterest.h" + +#include "Engine/World.h" +#include "Engine/Classes/GameFramework/Actor.h" +#include "GameFramework/PlayerController.h" #include "UObject/UObjectIterator.h" DEFINE_LOG_CATEGORY(LogInterestFactory); -namespace +DECLARE_STATS_GROUP(TEXT("InterestFactory"), STATGROUP_SpatialInterestFactory, STATCAT_Advanced); +DECLARE_CYCLE_STAT(TEXT("AddUserDefinedQueries"), STAT_InterestFactoryAddUserDefinedQueries, STATGROUP_SpatialInterestFactory); + +namespace SpatialGDK +{ + +InterestFactory::InterestFactory(USpatialClassInfoManager* InClassInfoManager, USpatialPackageMapClient* InPackageMap) + : ClassInfoManager(InClassInfoManager) + , PackageMap(InPackageMap) { -static TMap ClientInterestDistancesSquared; + CreateAndCacheInterestState(); } -namespace SpatialGDK +void InterestFactory::CreateAndCacheInterestState() { -void GatherClientInterestDistances() + ClientCheckoutRadiusConstraint = NetCullDistanceInterest::CreateCheckoutRadiusConstraints(ClassInfoManager); + ClientNonAuthInterestResultType = CreateClientNonAuthInterestResultType(ClassInfoManager); + ClientAuthInterestResultType = CreateClientAuthInterestResultType(ClassInfoManager); + ServerNonAuthInterestResultType = CreateServerNonAuthInterestResultType(ClassInfoManager); + ServerAuthInterestResultType = CreateServerAuthInterestResultType(); +} + +ResultType InterestFactory::CreateClientNonAuthInterestResultType(USpatialClassInfoManager* InClassInfoManager) { - ClientInterestDistancesSquared.Empty(); + ResultType ClientNonAuthResultType; - const AActor* DefaultActor = Cast(AActor::StaticClass()->GetDefaultObject()); - const float DefaultDistanceSquared = DefaultActor->NetCullDistanceSquared; - const float MaxDistanceSquared = GetDefault()->MaxNetCullDistanceSquared; + // Add the required unreal components + ClientNonAuthResultType.Append(SpatialConstants::REQUIRED_COMPONENTS_FOR_NON_AUTH_CLIENT_INTEREST); - // Gather ClientInterestDistance settings, and add any larger than the default radius to a list for processing. - TMap DiscoveredInterestDistancesSquared; - for (TObjectIterator It; It; ++It) - { - if (It->HasAnySpatialClassFlags(SPATIALCLASS_ServerOnly)) - { - continue; - } - if (!It->HasAnySpatialClassFlags(SPATIALCLASS_SpatialType)) - { - continue; - } - if (It->HasAnyClassFlags(CLASS_NewerVersionExists)) - { - // This skips classes generated for hot reload etc (i.e. REINST_, SKEL_, TRASHCLASS_) - continue; - } - if (!It->IsChildOf()) - { - continue; - } + // Add all data components- clients don't need to see handover or owner only components on other entities. + ClientNonAuthResultType.Append(InClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_Data)); - const AActor* IteratedDefaultActor = Cast(It->GetDefaultObject()); - if (IteratedDefaultActor->NetCullDistanceSquared > DefaultDistanceSquared) - { - float ActorNetCullDistanceSquared = IteratedDefaultActor->NetCullDistanceSquared; + // In direct disagreement with the above comment, we add the owner only components as well. + // This is because GDK workers currently make assumptions about information being available at the point of possession. + // TODO(jacques): fix (unr-2865) + ClientNonAuthResultType.Append(InClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_OwnerOnly)); - if (MaxDistanceSquared != 0.f && IteratedDefaultActor->NetCullDistanceSquared > MaxDistanceSquared) - { - UE_LOG(LogInterestFactory, Warning, TEXT("NetCullDistanceSquared for %s too large, clamping from %f to %f"), - *It->GetName(), ActorNetCullDistanceSquared, MaxDistanceSquared); + return ClientNonAuthResultType; +} - ActorNetCullDistanceSquared = MaxDistanceSquared; - } +ResultType InterestFactory::CreateClientAuthInterestResultType(USpatialClassInfoManager* InClassInfoManager) +{ + ResultType ClientAuthResultType; - DiscoveredInterestDistancesSquared.Add(*It, ActorNetCullDistanceSquared); - } - } + // Add the required known components + ClientAuthResultType.Append(SpatialConstants::REQUIRED_COMPONENTS_FOR_AUTH_CLIENT_INTEREST); + ClientAuthResultType.Append(SpatialConstants::REQUIRED_COMPONENTS_FOR_NON_AUTH_CLIENT_INTEREST); - // Sort the map for iteration so that parent classes are seen before derived classes. This lets us skip - // derived classes that have a smaller interest distance than a parent class. - DiscoveredInterestDistancesSquared.KeySort([](const UClass& LHS, const UClass& RHS) { - return LHS.IsChildOf(&RHS); - }); + // Add all the generated unreal components + ClientAuthResultType.Append(ClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_Data)); + ClientAuthResultType.Append(ClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_OwnerOnly)); - // If an actor's interest distance is smaller than that of a parent class, there's no need to add interest for that actor. - // Can't do inline removal since the sorted order is only guaranteed when the map isn't changed. - for (const auto& ActorInterestDistance : DiscoveredInterestDistancesSquared) - { - bool bShouldAdd = true; - for (auto& OptimizedInterestDistance : ClientInterestDistancesSquared) - { - if (ActorInterestDistance.Key->IsChildOf(OptimizedInterestDistance.Key) && ActorInterestDistance.Value <= OptimizedInterestDistance.Value) - { - // No need to add this interest distance since it's captured in the optimized map already. - bShouldAdd = false; - break; - } - } - if (bShouldAdd) - { - ClientInterestDistancesSquared.Add(ActorInterestDistance.Key, ActorInterestDistance.Value); - } - } + return ClientAuthResultType; } -InterestFactory::InterestFactory(AActor* InActor, const FClassInfo& InInfo, USpatialClassInfoManager* InClassInfoManager, USpatialPackageMapClient* InPackageMap) - : Actor(InActor) - , Info(InInfo) - , ClassInfoManager(InClassInfoManager) - , PackageMap(InPackageMap) +ResultType InterestFactory::CreateServerNonAuthInterestResultType(USpatialClassInfoManager* InClassInfoManager) { + ResultType ServerNonAuthResultType; + + // Add the required unreal components + ServerNonAuthResultType.Append(SpatialConstants::REQUIRED_COMPONENTS_FOR_NON_AUTH_SERVER_INTEREST); + + // Add all data, owner only, and handover components + ServerNonAuthResultType.Append(InClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_Data)); + ServerNonAuthResultType.Append(InClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_OwnerOnly)); + ServerNonAuthResultType.Append(InClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_Handover)); + + return ServerNonAuthResultType; } -Worker_ComponentData InterestFactory::CreateInterestData() const +ResultType InterestFactory::CreateServerAuthInterestResultType() { - return CreateInterest().CreateInterestData(); + // Just the components that we won't have already checked out through authority + return SpatialConstants::REQUIRED_COMPONENTS_FOR_AUTH_SERVER_INTEREST; } -Worker_ComponentUpdate InterestFactory::CreateInterestUpdate() const +Worker_ComponentData InterestFactory::CreateInterestData(AActor* InActor, const FClassInfo& InInfo, const Worker_EntityId InEntityId) const { - return CreateInterest().CreateInterestUpdate(); + return CreateInterest(InActor, InInfo, InEntityId).CreateInterestData(); } -Interest InterestFactory::CreateServerWorkerInterest() +Worker_ComponentUpdate InterestFactory::CreateInterestUpdate(AActor* InActor, const FClassInfo& InInfo, const Worker_EntityId InEntityId) const { - QueryConstraint Constraint; + return CreateInterest(InActor, InInfo, InEntityId).CreateInterestUpdate(); +} +Interest InterestFactory::CreateServerWorkerInterest(const UAbstractLBStrategy* LBStrategy) +{ const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); - if (SpatialGDKSettings->bEnableServerQBI && SpatialGDKSettings->bEnableOffloading) + + // Build the Interest component as we go by updating the component-> query list mappings. + Interest ServerInterest; + ComponentInterest ServerComponentInterest; + Query ServerQuery; + QueryConstraint Constraint; + + // Set the result type of the query + if (SpatialGDKSettings->bEnableResultTypes) + { + ServerQuery.ResultComponentIds = ServerNonAuthInterestResultType; + } + else { - UE_LOG(LogInterestFactory, Warning, TEXT("For performance reasons, it's recommended to disable server QBI when using offloading")); + ServerQuery.FullSnapshotResult = true; } - if (!SpatialGDKSettings->bEnableServerQBI && SpatialGDKSettings->bEnableOffloading) + if (SpatialGDKSettings->bEnableOffloading) { // In offloading scenarios, hijack the server worker entity to ensure each server has interest in all entities Constraint.ComponentConstraint = SpatialConstants::POSITION_COMPONENT_ID; + ServerQuery.Constraint = Constraint; + + // No need to add any further interest as we are already interested in everything + AddComponentQueryPairToInterestComponent(ServerInterest, SpatialConstants::POSITION_COMPONENT_ID, ServerQuery); + return ServerInterest; } - else + + // If we aren't offloading, the server gets more granular interest. + + // Ensure server worker receives always relevant entities + QueryConstraint AlwaysRelevantConstraint = CreateAlwaysRelevantConstraint(); + + Constraint = AlwaysRelevantConstraint; + + // If we are using the unreal load balancer, we also add the server worker interest defined by the load balancing strategy. + if (SpatialGDKSettings->bEnableUnrealLoadBalancer) { - // Ensure server worker receives the GSM entity - Constraint.EntityIdConstraint = SpatialConstants::INITIAL_GLOBAL_STATE_MANAGER_ENTITY_ID; + check(LBStrategy != nullptr); + + // The load balancer won't be ready when the worker initially connects to SpatialOS. It needs + // to wait for the virtual worker mappings to be replicated. + // This function will be called again when that is the case in order to update the interest on the server entity. + if (LBStrategy->IsReady()) + { + QueryConstraint LoadBalancerConstraint = LBStrategy->GetWorkerInterestQueryConstraint(); + + // Rather than adding the load balancer constraint at the end, reorder the constraints to have the large spatial + // constraint at the front. This is more likely to be efficient. + QueryConstraint NewConstraint; + NewConstraint.OrConstraint.Add(LoadBalancerConstraint); + NewConstraint.OrConstraint.Add(AlwaysRelevantConstraint); + Constraint = NewConstraint; + } } - Query Query; - Query.Constraint = Constraint; - Query.FullSnapshotResult = true; + ServerQuery.Constraint = Constraint; + AddComponentQueryPairToInterestComponent(ServerInterest, SpatialConstants::POSITION_COMPONENT_ID, ServerQuery); - ComponentInterest Queries; - Queries.Queries.Add(Query); + // Add another query to get the worker system entities. + // It allows us to know when a client has disconnected. + // TODO UNR-3042 : Migrate the VirtualWorkerTranslationManager to use the checked-out worker components instead of making a query. - Interest ServerInterest; - ServerInterest.ComponentInterestMap.Add(SpatialConstants::POSITION_COMPONENT_ID, Queries); + ServerQuery = Query(); + SetResultType(ServerQuery, ResultType{ SpatialConstants::WORKER_COMPONENT_ID }); + ServerQuery.Constraint.ComponentConstraint = SpatialConstants::WORKER_COMPONENT_ID; + AddComponentQueryPairToInterestComponent(ServerInterest, SpatialConstants::POSITION_COMPONENT_ID, ServerQuery); return ServerInterest; } -Interest InterestFactory::CreateInterest() const +Interest InterestFactory::CreateInterest(AActor* InActor, const FClassInfo& InInfo, const Worker_EntityId InEntityId) const { - if (GetDefault()->bEnableServerQBI) + const USpatialGDKSettings* Settings = GetDefault(); + + // The interest is built progressively by adding the different component query pairs to build the full map. + Interest ResultInterest; + + if (InActor->IsA(APlayerController::StaticClass())) { - if (Actor->GetNetConnection() != nullptr) - { - return CreatePlayerOwnedActorInterest(); - } - else - { - return CreateActorInterest(); - } + // Put the "main" interest queries on the player controller + AddPlayerControllerActorInterest(ResultInterest, InActor, InInfo); } - else + + if (Settings->bEnableResultTypes) { - if (Actor->IsA(APlayerController::StaticClass())) + if (InActor->GetNetConnection() != nullptr) { - return CreatePlayerOwnedActorInterest(); - } - else - { - return Interest{}; + // Clients need to see owner only and server RPC components on entities they have authority over + AddClientSelfInterest(ResultInterest, InEntityId); } + + // Every actor needs a self query for the server to the client RPC endpoint + AddServerSelfInterest(ResultInterest, InEntityId); } + + return ResultInterest; } -Interest InterestFactory::CreateActorInterest() const +void InterestFactory::AddPlayerControllerActorInterest(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo) const { - Interest NewInterest; + QueryConstraint LevelConstraint = CreateLevelConstraints(InActor); + + AddAlwaysRelevantAndInterestedQuery(OutInterest, InActor, InInfo, LevelConstraint); - QueryConstraint SystemConstraints = CreateSystemDefinedConstraints(); + AddUserDefinedQueries(OutInterest, InActor, LevelConstraint); - if (!SystemConstraints.IsValid()) + // Either add the NCD interest because there are no user interest queries, or because the user interest specified we should. + if (ShouldAddNetCullDistanceInterest(InActor)) { - return NewInterest; + AddNetCullDistanceQueries(OutInterest, LevelConstraint); } +} +void InterestFactory::AddClientSelfInterest(Interest& OutInterest, const Worker_EntityId& EntityId) const +{ Query NewQuery; - NewQuery.Constraint = SystemConstraints; - // TODO: Make result type handle components certain workers shouldn't see - // e.g. Handover, OwnerOnly, etc. - NewQuery.FullSnapshotResult = true; + // Just an entity ID constraint is fine, as clients should not become authoritative over entities outside their loaded levels + NewQuery.Constraint.EntityIdConstraint = EntityId; - ComponentInterest NewComponentInterest; - NewComponentInterest.Queries.Add(NewQuery); + NewQuery.ResultComponentIds = ClientAuthInterestResultType; - // Server Interest - NewInterest.ComponentInterestMap.Add(SpatialConstants::POSITION_COMPONENT_ID, NewComponentInterest); - - return NewInterest; + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::GetClientAuthorityComponent(GetDefault()->UseRPCRingBuffer()), NewQuery); } -Interest InterestFactory::CreatePlayerOwnedActorInterest() const +void InterestFactory::AddServerSelfInterest(Interest& OutInterest, const Worker_EntityId& EntityId) const { - QueryConstraint SystemConstraints = CreateSystemDefinedConstraints(); + // Add a query for components all servers need to read client data + Query ClientQuery; + ClientQuery.Constraint.EntityIdConstraint = EntityId; + ClientQuery.ResultComponentIds = ServerAuthInterestResultType; + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::POSITION_COMPONENT_ID, ClientQuery); + + // Add a query for the load balancing worker (whoever is delegated the ACL) to read the authority intent + Query LoadBalanceQuery; + LoadBalanceQuery.Constraint.EntityIdConstraint = EntityId; + LoadBalanceQuery.ResultComponentIds = ResultType{ SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID, SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID }; + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::ENTITY_ACL_COMPONENT_ID, LoadBalanceQuery); +} - // Servers only need the defined constraints - Query ServerQuery; - ServerQuery.Constraint = SystemConstraints; - ServerQuery.FullSnapshotResult = true; +void InterestFactory::AddAlwaysRelevantAndInterestedQuery(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo, const QueryConstraint& LevelConstraint) const +{ + const USpatialGDKSettings* Settings = GetDefault(); - ComponentInterest ServerComponentInterest; - ServerComponentInterest.Queries.Add(ServerQuery); + QueryConstraint AlwaysInterestedConstraint = CreateAlwaysInterestedConstraint(InActor, InInfo); + QueryConstraint AlwaysRelevantConstraint = CreateAlwaysRelevantConstraint(); - // Clients should only check out entities that are in loaded sublevels - QueryConstraint LevelConstraints = CreateLevelConstraints(); + QueryConstraint SystemDefinedConstraints; - QueryConstraint ClientConstraint; + if (AlwaysInterestedConstraint.IsValid()) + { + SystemDefinedConstraints.OrConstraint.Add(AlwaysInterestedConstraint); + } - if (SystemConstraints.IsValid()) + if (AlwaysRelevantConstraint.IsValid()) { - ClientConstraint.AndConstraint.Add(SystemConstraints); + SystemDefinedConstraints.OrConstraint.Add(AlwaysRelevantConstraint); } - if (LevelConstraints.IsValid()) + // Add the level constraint here as all client queries need to make sure they don't check out anything outside their loaded levels. + QueryConstraint SystemAndLevelConstraint; + SystemAndLevelConstraint.AndConstraint.Add(SystemDefinedConstraints); + SystemAndLevelConstraint.AndConstraint.Add(LevelConstraint); + + Query ClientSystemQuery; + ClientSystemQuery.Constraint = SystemAndLevelConstraint; + + SetResultType(ClientSystemQuery, ClientNonAuthInterestResultType); + + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::GetClientAuthorityComponent(Settings->UseRPCRingBuffer()), ClientSystemQuery); + + // Add always interested constraint to the server as well to make sure the server sees the same as the client. + // The always relevant constraint is added as part of the server worker query, so leave that out here. + // Servers also don't need to be level constrained. + if (Settings->bEnableClientQueriesOnServer) { - ClientConstraint.AndConstraint.Add(LevelConstraints); + Query ServerSystemQuery; + QueryConstraint ServerSystemConstraint; + ServerSystemConstraint.OrConstraint.Add(AlwaysInterestedConstraint); + ServerSystemQuery.Constraint = ServerSystemConstraint; + + SetResultType(ServerSystemQuery, ServerNonAuthInterestResultType); + + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::POSITION_COMPONENT_ID, ServerSystemQuery); } +} - Query ClientQuery; - ClientQuery.Constraint = ClientConstraint; - ClientQuery.FullSnapshotResult = true; +void InterestFactory::AddUserDefinedQueries(Interest& OutInterest, const AActor* InActor, const QueryConstraint& LevelConstraint) const +{ + SCOPE_CYCLE_COUNTER(STAT_InterestFactoryAddUserDefinedQueries); + const USpatialGDKSettings* Settings = GetDefault(); + + FrequencyToConstraintsMap FrequencyConstraintsMap = GetUserDefinedFrequencyToConstraintsMap(InActor); - ComponentInterest ClientComponentInterest; - ClientComponentInterest.Queries.Add(ClientQuery); + for (const auto& FrequencyToConstraints : FrequencyConstraintsMap) + { + Query UserQuery; + QueryConstraint UserConstraint; - AddUserDefinedQueries(LevelConstraints, ClientComponentInterest.Queries); + UserQuery.Frequency = FrequencyToConstraints.Key; - Interest NewInterest; - // Server Interest - if (SystemConstraints.IsValid() && GetDefault()->bEnableServerQBI) + // If there is only one constraint, don't make the constraint an OR. + if (FrequencyToConstraints.Value.Num() == 1) + { + UserConstraint = FrequencyToConstraints.Value[0]; + } + else + { + UserConstraint.OrConstraint.Append(FrequencyToConstraints.Value); + } + + if (!UserConstraint.IsValid()) + { + continue; + } + + // All constraints have to be limited to the checked out levels, so create an AND constraint with the level. + UserQuery.Constraint.AndConstraint.Add(UserConstraint); + UserQuery.Constraint.AndConstraint.Add(LevelConstraint); + + // We enforce result type even for user defined queries. Here we are assuming what a user wants from their defined + // queries are for their players to check out more actors than they normally would, so use the client non auth result type, + // which includes all components required for a client to see non-authoritative actors. + SetResultType(UserQuery, ClientNonAuthInterestResultType); + + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::GetClientAuthorityComponent(Settings->UseRPCRingBuffer()), UserQuery); + + // Add the user interest to the server as well if load balancing is enabled and the client queries on server flag is flipped + // Need to check if load balancing is enabled otherwise there is not chance the client could see and entity the server can't, + // which is what the client queries on server flag is to avoid. + if (Settings->bEnableClientQueriesOnServer) + { + Query ServerUserQuery; + ServerUserQuery.Constraint = UserConstraint; + ServerUserQuery.Frequency = FrequencyToConstraints.Key; + + SetResultType(ServerUserQuery, ServerNonAuthInterestResultType); + + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::POSITION_COMPONENT_ID, ServerUserQuery); + } + } +} + +FrequencyToConstraintsMap InterestFactory::GetUserDefinedFrequencyToConstraintsMap(const AActor* InActor) const +{ + // This function builds a frequency to constraint map rather than queries. It does this for two reasons: + // - We need to set the result type later + // - The map implicitly removes duplicates queries that have the same constraint. Result types are set for each query and these are large, + // so worth simplifying as much as possible. + FrequencyToConstraintsMap FrequencyToConstraints; + + if (const APlayerController* PlayerController = Cast(InActor)) { - NewInterest.ComponentInterestMap.Add(SpatialConstants::POSITION_COMPONENT_ID, ServerComponentInterest); + // If this is for a player controller, loop through the pawns of the controller as well, because we only add interest to + // the player controller entity but interest can be specified on the pawn of the controller as well. + GetActorUserDefinedQueryConstraints(InActor, FrequencyToConstraints, true); + GetActorUserDefinedQueryConstraints(PlayerController->GetPawn(), FrequencyToConstraints, true); } - // Client Interest - if (ClientConstraint.IsValid()) + else { - NewInterest.ComponentInterestMap.Add(SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID, ClientComponentInterest); + GetActorUserDefinedQueryConstraints(InActor, FrequencyToConstraints, false); } - return NewInterest; + return FrequencyToConstraints; } -void InterestFactory::AddUserDefinedQueries(const QueryConstraint& LevelConstraints, TArray& OutQueries) const +void InterestFactory::GetActorUserDefinedQueryConstraints(const AActor* InActor, FrequencyToConstraintsMap& OutFrequencyToConstraints, bool bRecurseChildren) const { - check(Actor); check(ClassInfoManager); + if (InActor == nullptr) + { + return; + } + + // The defined actor interest component populates the frequency to constraints map with the user defined queries. TArray ActorInterestComponents; - Actor->GetComponents(ActorInterestComponents); + InActor->GetComponents(ActorInterestComponents); if (ActorInterestComponents.Num() == 1) { - ActorInterestComponents[0]->CreateQueries(*ClassInfoManager, LevelConstraints, OutQueries); + ActorInterestComponents[0]->PopulateFrequencyToConstraintsMap(*ClassInfoManager, OutFrequencyToConstraints); } else if (ActorInterestComponents.Num() > 1) { - UE_LOG(LogInterestFactory, Error, TEXT("%s has more than one ActorInterestQueryComponent"), *Actor->GetPathName()); + UE_LOG(LogInterestFactory, Error, TEXT("%s has more than one ActorInterestComponent"), *InActor->GetPathName()); + checkNoEntry() + } + + if (bRecurseChildren) + { + for (const auto& Child : InActor->Children) + { + GetActorUserDefinedQueryConstraints(Child, OutFrequencyToConstraints, true); + } } } -QueryConstraint InterestFactory::CreateSystemDefinedConstraints() const +void InterestFactory::AddNetCullDistanceQueries(Interest& OutInterest, const QueryConstraint& LevelConstraint) const { - QueryConstraint CheckoutRadiusConstraint = CreateCheckoutRadiusConstraints(); - QueryConstraint AlwaysInterestedConstraint = CreateAlwaysInterestedConstraint(); - QueryConstraint AlwaysRelevantConstraint = CreateAlwaysRelevantConstraint(); - - QueryConstraint SystemDefinedConstraints; + const USpatialGDKSettings* Settings = GetDefault(); - if (CheckoutRadiusConstraint.IsValid()) + // The CheckoutConstraints list contains items with a constraint and a frequency. + // They are then converted to queries by adding a result type to them, and the constraints are conjoined with the level constraint. + for (const auto& CheckoutRadiusConstraintFrequencyPair : ClientCheckoutRadiusConstraint) { - SystemDefinedConstraints.OrConstraint.Add(CheckoutRadiusConstraint); - } + if (!CheckoutRadiusConstraintFrequencyPair.Constraint.IsValid()) + { + continue; + } - if (AlwaysInterestedConstraint.IsValid()) - { - SystemDefinedConstraints.OrConstraint.Add(AlwaysInterestedConstraint); + Query NewQuery; + + NewQuery.Constraint.AndConstraint.Add(CheckoutRadiusConstraintFrequencyPair.Constraint); + + if (LevelConstraint.IsValid()) + { + NewQuery.Constraint.AndConstraint.Add(LevelConstraint); + } + + NewQuery.Frequency = CheckoutRadiusConstraintFrequencyPair.Frequency; + + SetResultType(NewQuery, ClientNonAuthInterestResultType); + + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::GetClientAuthorityComponent(Settings->UseRPCRingBuffer()), NewQuery); + + // Add the queries to the server as well to ensure that all entities checked out on the client will be present on the server. + if (Settings->bEnableClientQueriesOnServer) + { + Query ServerQuery; + ServerQuery.Constraint = CheckoutRadiusConstraintFrequencyPair.Constraint; + ServerQuery.Frequency = CheckoutRadiusConstraintFrequencyPair.Frequency; + + SetResultType(ServerQuery, ServerNonAuthInterestResultType); + + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::POSITION_COMPONENT_ID, ServerQuery); + } } +} - if (AlwaysRelevantConstraint.IsValid()) +void InterestFactory::AddComponentQueryPairToInterestComponent(Interest& OutInterest, const Worker_ComponentId ComponentId, const Query& QueryToAdd) const +{ + if (!OutInterest.ComponentInterestMap.Contains(ComponentId)) { - SystemDefinedConstraints.OrConstraint.Add(AlwaysRelevantConstraint); + ComponentInterest NewComponentInterest; + OutInterest.ComponentInterestMap.Add(ComponentId, NewComponentInterest); } - - return SystemDefinedConstraints; + OutInterest.ComponentInterestMap[ComponentId].Queries.Add(QueryToAdd); } -QueryConstraint InterestFactory::CreateCheckoutRadiusConstraints() const +bool InterestFactory::ShouldAddNetCullDistanceInterest(const AActor* InActor) const { - // If the actor has a component to specify interest and that indicates that we shouldn't generate + // If the actor has a component to specify interest and that indicates that we shouldn't add // constraints based on NetCullDistanceSquared, abort. There is a check elsewhere to ensure that // there is at most one ActorInterestQueryComponent. TArray ActorInterestComponents; - Actor->GetComponents(ActorInterestComponents); + InActor->GetComponents(ActorInterestComponents); if (ActorInterestComponents.Num() == 1) { const UActorInterestComponent* ActorInterest = ActorInterestComponents[0]; check(ActorInterest); if (!ActorInterest->bUseNetCullDistanceSquaredForCheckoutRadius) { - return QueryConstraint{}; - } - } - - // Checkout Radius constraints are defined by the NetCullDistanceSquared property on actors. - // - Checkout radius is a RelativeCylinder constraint on the player controller. - // - NetCullDistanceSquared on AActor is used to define the default checkout radius with no other constraints. - // - NetCullDistanceSquared on other actor types is used to define additional constraints if needed. - // - If a subtype defines a radius smaller than a parent type, then its requirements are already captured. - // - If a subtype defines a radius larger than all parent types, then it needs an additional constraint. - // - Other than the default from AActor, all radius constraints also include Component constraints to - // capture specific types, including all derived types of that actor. - - const AActor* DefaultActor = Cast(AActor::StaticClass()->GetDefaultObject()); - const float DefaultDistanceSquared = DefaultActor->NetCullDistanceSquared; - - QueryConstraint CheckoutRadiusConstraints; - - // Use AActor's ClientInterestDistance for the default radius (all actors in that radius will be checked out) - const float DefaultCheckoutRadiusMeters = FMath::Sqrt(DefaultDistanceSquared / (100.0f * 100.0f)); - QueryConstraint DefaultCheckoutRadiusConstraint; - DefaultCheckoutRadiusConstraint.RelativeCylinderConstraint = RelativeCylinderConstraint{ DefaultCheckoutRadiusMeters }; - CheckoutRadiusConstraints.OrConstraint.Add(DefaultCheckoutRadiusConstraint); - - // For every interest distance that we still want, add a constraint with the distance for the actor type and all of its derived types. - for (const auto& InterestDistanceSquared: ClientInterestDistancesSquared) - { - QueryConstraint CheckoutRadiusConstraint; - - QueryConstraint RadiusConstraint; - const float CheckoutRadiusMeters = FMath::Sqrt(InterestDistanceSquared.Value / (100.0f * 100.0f)); - RadiusConstraint.RelativeCylinderConstraint = RelativeCylinderConstraint{ CheckoutRadiusMeters }; - CheckoutRadiusConstraint.AndConstraint.Add(RadiusConstraint); - - QueryConstraint ActorTypeConstraint; - check(InterestDistanceSquared.Key); - AddTypeHierarchyToConstraint(*InterestDistanceSquared.Key, ActorTypeConstraint); - if (ActorTypeConstraint.IsValid()) - { - CheckoutRadiusConstraint.AndConstraint.Add(ActorTypeConstraint); - CheckoutRadiusConstraints.OrConstraint.Add(CheckoutRadiusConstraint); + return false; } } - return CheckoutRadiusConstraints; + return true; } -QueryConstraint InterestFactory::CreateAlwaysInterestedConstraint() const +QueryConstraint InterestFactory::CreateAlwaysInterestedConstraint(const AActor* InActor, const FClassInfo& InInfo) const { QueryConstraint AlwaysInterestedConstraint; - for (const FInterestPropertyInfo& PropertyInfo : Info.InterestProperties) + for (const FInterestPropertyInfo& PropertyInfo : InInfo.InterestProperties) { - uint8* Data = (uint8*)Actor + PropertyInfo.Offset; + uint8* Data = (uint8*)InActor + PropertyInfo.Offset; if (UObjectPropertyBase* ObjectProperty = Cast(PropertyInfo.Property)) { AddObjectToConstraint(ObjectProperty, Data, AlwaysInterestedConstraint); @@ -383,7 +504,6 @@ QueryConstraint InterestFactory::CreateAlwaysInterestedConstraint() const return AlwaysInterestedConstraint; } - QueryConstraint InterestFactory::CreateAlwaysRelevantConstraint() const { QueryConstraint AlwaysRelevantConstraint; @@ -391,6 +511,8 @@ QueryConstraint InterestFactory::CreateAlwaysRelevantConstraint() const Worker_ComponentId ComponentIds[] = { SpatialConstants::SINGLETON_COMPONENT_ID, SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID, + SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID, + SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID, SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID }; @@ -404,40 +526,7 @@ QueryConstraint InterestFactory::CreateAlwaysRelevantConstraint() const return AlwaysRelevantConstraint; } -void InterestFactory::AddObjectToConstraint(UObjectPropertyBase* Property, uint8* Data, QueryConstraint& OutConstraint) const -{ - UObject* ObjectOfInterest = Property->GetObjectPropertyValue(Data); - - if (ObjectOfInterest == nullptr) - { - return; - } - - FUnrealObjectRef UnrealObjectRef = PackageMap->GetUnrealObjectRefFromObject(ObjectOfInterest); - - if (!UnrealObjectRef.IsValid()) - { - return; - } - - QueryConstraint EntityIdConstraint; - EntityIdConstraint.EntityIdConstraint = UnrealObjectRef.Entity; - OutConstraint.OrConstraint.Add(EntityIdConstraint); -} - -void InterestFactory::AddTypeHierarchyToConstraint(const UClass& BaseType, QueryConstraint& OutConstraint) const -{ - check(ClassInfoManager); - TArray ComponentIds = ClassInfoManager->GetComponentIdsForClassHierarchy(BaseType); - for (Worker_ComponentId ComponentId : ComponentIds) - { - QueryConstraint ComponentTypeConstraint; - ComponentTypeConstraint.ComponentConstraint = ComponentId; - OutConstraint.OrConstraint.Add(ComponentTypeConstraint); - } -} - -QueryConstraint InterestFactory::CreateLevelConstraints() const +QueryConstraint InterestFactory::CreateLevelConstraints(const AActor* InActor) const { QueryConstraint LevelConstraint; @@ -445,17 +534,17 @@ QueryConstraint InterestFactory::CreateLevelConstraints() const DefaultConstraint.ComponentConstraint = SpatialConstants::NOT_STREAMED_COMPONENT_ID; LevelConstraint.OrConstraint.Add(DefaultConstraint); - UNetConnection* Connection = Actor->GetNetConnection(); + UNetConnection* Connection = InActor->GetNetConnection(); check(Connection); APlayerController* PlayerController = Connection->GetPlayerController(nullptr); check(PlayerController); const TSet& LoadedLevels = PlayerController->NetConnection->ClientVisibleLevelNames; - // Create component constraints for every loaded sublevel + // Create component constraints for every loaded sub-level for (const auto& LevelPath : LoadedLevels) { - const uint32 ComponentId = ClassInfoManager->GetComponentIdFromLevelPath(LevelPath.ToString()); + const Worker_ComponentId ComponentId = ClassInfoManager->GetComponentIdFromLevelPath(LevelPath.ToString()); if (ComponentId != SpatialConstants::INVALID_COMPONENT_ID) { QueryConstraint SpecificLevelConstraint; @@ -465,11 +554,44 @@ QueryConstraint InterestFactory::CreateLevelConstraints() const else { UE_LOG(LogInterestFactory, Error, TEXT("Error creating query constraints for Actor %s. " - "Could not find Streaming Level Component for Level %s. Have you generated schema?"), *Actor->GetName(), *LevelPath.ToString()); + "Could not find Streaming Level Component for Level %s. Have you generated schema?"), *InActor->GetName(), *LevelPath.ToString()); } } return LevelConstraint; } +void InterestFactory::AddObjectToConstraint(UObjectPropertyBase* Property, uint8* Data, QueryConstraint& OutConstraint) const +{ + UObject* ObjectOfInterest = Property->GetObjectPropertyValue(Data); + + if (ObjectOfInterest == nullptr) + { + return; + } + + FUnrealObjectRef UnrealObjectRef = PackageMap->GetUnrealObjectRefFromObject(ObjectOfInterest); + + if (!UnrealObjectRef.IsValid()) + { + return; + } + + QueryConstraint EntityIdConstraint; + EntityIdConstraint.EntityIdConstraint = UnrealObjectRef.Entity; + OutConstraint.OrConstraint.Add(EntityIdConstraint); +} + +void InterestFactory::SetResultType(Query& OutQuery, const ResultType& InResultType) const +{ + if (GetDefault()->bEnableResultTypes) + { + OutQuery.ResultComponentIds = InResultType; + } + else + { + OutQuery.FullSnapshotResult = true; + } +} + } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp index 3e98739f38..b3fe925f10 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp @@ -3,13 +3,12 @@ #include "Utils/RPCContainer.h" #include "Schema/UnrealObjectRef.h" +#include "SpatialGDKSettings.h" DEFINE_LOG_CATEGORY(LogRPCContainer); using namespace SpatialGDK; -const double FRPCContainer::SECONDS_BEFORE_WARNING = 2.0; - namespace { FString ERPCResultToString(ERPCResult Result) @@ -28,6 +27,12 @@ namespace case ERPCResult::UnresolvedParameters: return TEXT("Unresolved Parameters"); + case ERPCResult::ActorPendingKill: + return TEXT("Actor Pending Kill"); + + case ERPCResult::TimedOut: + return TEXT("Timed Out"); + case ERPCResult::NoActorChannel: return TEXT("No Actor Channel"); @@ -57,21 +62,24 @@ namespace } } - void LogRPCError(const FRPCErrorInfo& ErrorInfo, const FPendingRPCParams& Params) + void LogRPCError(const FRPCErrorInfo& ErrorInfo, ERPCQueueType QueueType, const FPendingRPCParams& Params) { const FTimespan TimeDiff = FDateTime::Now() - Params.Timestamp; // The format is expected to be: - // Function :: sending/execution queued on server/client for . Reason: - FString OutputLog = FString::Printf(TEXT("Function %s::%s %s queued on %s for %s. Reason: %s"), + // Function :: sending/execution dropped/queued for . Reason: + FString OutputLog = FString::Printf(TEXT("Function %s::%s %s %s for %s. Reason: %s"), ErrorInfo.TargetObject.IsValid() ? *ErrorInfo.TargetObject->GetName() : TEXT("UNKNOWN"), ErrorInfo.Function.IsValid() ? *ErrorInfo.Function->GetName() : TEXT("UNKNOWN"), - ErrorInfo.QueueType == ERPCQueueType::Send ? TEXT("sending") : ErrorInfo.QueueType == ERPCQueueType::Receive ? TEXT("execution") : TEXT("UNKNOWN"), - ErrorInfo.bIsServer ? TEXT("server") : TEXT("client"), + QueueType == ERPCQueueType::Send ? TEXT("sending") : QueueType == ERPCQueueType::Receive ? TEXT("execution") : TEXT("UNKNOWN"), + ErrorInfo.bShouldDrop ? TEXT("dropped") : TEXT("queued"), *TimeDiff.ToString(), *ERPCResultToString(ErrorInfo.ErrorCode)); - if (TimeDiff.GetTotalSeconds() > FRPCContainer::SECONDS_BEFORE_WARNING) + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + check(SpatialGDKSettings != nullptr); + + if (TimeDiff.GetTotalSeconds() > SpatialGDKSettings->GetSecondsBeforeWarning(ErrorInfo.ErrorCode)) { UE_LOG(LogRPCContainer, Warning, TEXT("%s"), *OutputLog); } @@ -82,7 +90,7 @@ namespace } } -FPendingRPCParams::FPendingRPCParams(const FUnrealObjectRef& InTargetObjectRef, ESchemaComponentType InType, RPCPayload&& InPayload) +FPendingRPCParams::FPendingRPCParams(const FUnrealObjectRef& InTargetObjectRef, ERPCType InType, RPCPayload&& InPayload) : ObjectRef(InTargetObjectRef) , Payload(MoveTemp(InPayload)) , Timestamp(FDateTime::Now()) @@ -90,7 +98,7 @@ FPendingRPCParams::FPendingRPCParams(const FUnrealObjectRef& InTargetObjectRef, { } -void FRPCContainer::ProcessOrQueueRPC(const FUnrealObjectRef& TargetObjectRef, ESchemaComponentType Type, RPCPayload&& Payload) +void FRPCContainer::ProcessOrQueueRPC(const FUnrealObjectRef& TargetObjectRef, ERPCType Type, RPCPayload&& Payload) { FPendingRPCParams Params {TargetObjectRef, Type, MoveTemp(Payload)}; @@ -126,10 +134,18 @@ void FRPCContainer::ProcessRPCs(FArrayOfParams& RPCList) void FRPCContainer::ProcessRPCs() { + if (bAlreadyProcessingRPCs) + { + UE_LOG(LogRPCContainer, Log, TEXT("Calling ProcessRPCs recursively, ignoring the call")); + return; + } + + bAlreadyProcessingRPCs = true; + for (auto& RPCs : QueuedRPCs) { FRPCMap& MapOfQueues = RPCs.Value; - for(auto It = MapOfQueues.CreateIterator(); It; ++It) + for (auto It = MapOfQueues.CreateIterator(); It; ++It) { FArrayOfParams& RPCList = It.Value(); ProcessRPCs(RPCList); @@ -139,9 +155,19 @@ void FRPCContainer::ProcessRPCs() } } } + + bAlreadyProcessingRPCs = false; +} + +void FRPCContainer::DropForEntity(const Worker_EntityId& EntityId) +{ + for (auto& RpcMap : QueuedRPCs) + { + RpcMap.Value.Remove(EntityId); + } } -bool FRPCContainer::ObjectHasRPCsQueuedOfType(const Worker_EntityId& EntityId, ESchemaComponentType Type) const +bool FRPCContainer::ObjectHasRPCsQueuedOfType(const Worker_EntityId& EntityId, ERPCType Type) const { if(const FRPCMap* MapOfQueues = QueuedRPCs.Find(Type)) { @@ -154,6 +180,11 @@ bool FRPCContainer::ObjectHasRPCsQueuedOfType(const Worker_EntityId& EntityId, E return false; } +FRPCContainer::FRPCContainer(ERPCQueueType InQueueType) + : QueueType(InQueueType) +{ +} + void FRPCContainer::BindProcessingFunction(const FProcessRPCDelegate& Function) { ProcessingFunction = Function; @@ -171,9 +202,8 @@ bool FRPCContainer::ApplyFunction(FPendingRPCParams& Params) else { #if !UE_BUILD_SHIPPING - LogRPCError(ErrorInfo, Params); + LogRPCError(ErrorInfo, QueueType, Params); #endif - - return false; + return ErrorInfo.bShouldDrop; } } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCRingBuffer.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCRingBuffer.cpp new file mode 100644 index 0000000000..4b3356b629 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCRingBuffer.cpp @@ -0,0 +1,197 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Utils/RPCRingBuffer.h" + +#include "SpatialGDKSettings.h" + +namespace SpatialGDK +{ + +RPCRingBuffer::RPCRingBuffer(ERPCType InType) + : Type(InType) +{ + RingBuffer.SetNum(RPCRingBufferUtils::GetRingBufferSize(Type)); +} + +namespace RPCRingBufferUtils +{ + +Worker_ComponentId GetRingBufferComponentId(ERPCType Type) +{ + switch (Type) + { + case ERPCType::ClientReliable: + case ERPCType::ClientUnreliable: + return SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID; + case ERPCType::ServerReliable: + case ERPCType::ServerUnreliable: + return SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID; + case ERPCType::NetMulticast: + return SpatialConstants::MULTICAST_RPCS_COMPONENT_ID; + default: + checkNoEntry(); + return SpatialConstants::INVALID_COMPONENT_ID; + } +} + +RPCRingBufferDescriptor GetRingBufferDescriptor(ERPCType Type) +{ + RPCRingBufferDescriptor Descriptor; + Descriptor.RingBufferSize = GetRingBufferSize(Type); + + uint32 MaxRingBufferSize = GetDefault()->MaxRPCRingBufferSize; + // In schema, the client and server endpoints will first have a + // Reliable ring buffer, starting from 1 and containing MaxRingBufferSize elements, then + // Last sent reliable RPC, + // Unreliable ring buffer, containing MaxRingBufferSize elements, + // Last sent unreliable RPC, + // followed by reliable and unreliable RPC acks. + // MulticastRPCs component will only have one buffer that looks like the reliable buffer above. + // The numbers below are based on this structure, and have to match the component generated in SchemaGenerator (GenerateRPCEndpointsSchema). + switch (Type) + { + case ERPCType::ClientReliable: + case ERPCType::ServerReliable: + case ERPCType::NetMulticast: + Descriptor.SchemaFieldStart = 1; + Descriptor.LastSentRPCFieldId = 1 + MaxRingBufferSize; + break; + case ERPCType::ClientUnreliable: + case ERPCType::ServerUnreliable: + Descriptor.SchemaFieldStart = 1 + MaxRingBufferSize + 1; + Descriptor.LastSentRPCFieldId = 1 + MaxRingBufferSize + 1 + MaxRingBufferSize; + break; + default: + checkNoEntry(); + break; + } + + return Descriptor; +} + +uint32 GetRingBufferSize(ERPCType Type) +{ + return GetDefault()->GetRPCRingBufferSize(Type); +} + +Worker_ComponentId GetAckComponentId(ERPCType Type) +{ + switch (Type) + { + case ERPCType::ClientReliable: + case ERPCType::ClientUnreliable: + return SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID; + case ERPCType::ServerReliable: + case ERPCType::ServerUnreliable: + return SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID; + default: + checkNoEntry(); + return SpatialConstants::INVALID_COMPONENT_ID; + } +} + +Schema_FieldId GetAckFieldId(ERPCType Type) +{ + uint32 MaxRingBufferSize = GetDefault()->MaxRPCRingBufferSize; + + switch (Type) + { + case ERPCType::ClientReliable: + case ERPCType::ServerReliable: + // In the generated schema components, acks will follow two ring buffers, each containing MaxRingBufferSize elements as well as a last sent ID. + return 1 + 2 * (MaxRingBufferSize + 1); + case ERPCType::ClientUnreliable: + case ERPCType::ServerUnreliable: + return 1 + 2 * (MaxRingBufferSize + 1) + 1; + default: + checkNoEntry(); + return 0; + } +} + +Schema_FieldId GetInitiallyPresentMulticastRPCsCountFieldId() +{ + uint32 MaxRingBufferSize = GetDefault()->MaxRPCRingBufferSize; + // This field directly follows the ring buffer + last sent id. + return 1 + MaxRingBufferSize + 1; +} + +bool ShouldQueueOverflowed(ERPCType Type) +{ + switch (Type) + { + case ERPCType::ClientReliable: + case ERPCType::ServerReliable: + return true; + case ERPCType::ClientUnreliable: + case ERPCType::ServerUnreliable: + case ERPCType::NetMulticast: + return false; + default: + checkNoEntry(); + return false; + } +} + +void ReadBufferFromSchema(Schema_Object* SchemaObject, RPCRingBuffer& OutBuffer) +{ + RPCRingBufferDescriptor Descriptor = GetRingBufferDescriptor(OutBuffer.Type); + + for (uint32 RingBufferIndex = 0; RingBufferIndex < Descriptor.RingBufferSize; RingBufferIndex++) + { + Schema_FieldId FieldId = Descriptor.SchemaFieldStart + RingBufferIndex; + if (Schema_GetObjectCount(SchemaObject, FieldId) > 0) + { + OutBuffer.RingBuffer[RingBufferIndex].Emplace(Schema_GetObject(SchemaObject, FieldId)); + } + } + + if (Schema_GetUint64Count(SchemaObject, Descriptor.LastSentRPCFieldId) > 0) + { + OutBuffer.LastSentRPCId = Schema_GetUint64(SchemaObject, Descriptor.LastSentRPCFieldId); + } +} + +void ReadAckFromSchema(const Schema_Object* SchemaObject, ERPCType Type, uint64& OutAck) +{ + Schema_FieldId AckFieldId = GetAckFieldId(Type); + + if (Schema_GetUint64Count(SchemaObject, AckFieldId) > 0) + { + OutAck = Schema_GetUint64(SchemaObject, AckFieldId); + } +} + +void WriteRPCToSchema(Schema_Object* SchemaObject, ERPCType Type, uint64 RPCId, const RPCPayload& Payload) +{ + RPCRingBufferDescriptor Descriptor = GetRingBufferDescriptor(Type); + + Schema_Object* RPCObject = Schema_AddObject(SchemaObject, Descriptor.GetRingBufferElementFieldId(RPCId)); + Payload.WriteToSchemaObject(RPCObject); + + Schema_ClearField(SchemaObject, Descriptor.LastSentRPCFieldId); + Schema_AddUint64(SchemaObject, Descriptor.LastSentRPCFieldId, RPCId); +} + +void WriteAckToSchema(Schema_Object* SchemaObject, ERPCType Type, uint64 Ack) +{ + Schema_FieldId AckFieldId = GetAckFieldId(Type); + + Schema_ClearField(SchemaObject, AckFieldId); + Schema_AddUint64(SchemaObject, AckFieldId, Ack); +} + +void MoveLastSentIdToInitiallyPresentCount(Schema_Object* SchemaObject, uint64 LastSentId) +{ + // This is a special field that is set when creating a MulticastRPCs component with initial RPCs. + // Last sent RPC Id is cleared so the clients don't ignore the initial RPCs. + // The server that first gains authority over the component will set last sent RPC ID to be equal + // to the initial count so the clients that already checked out this entity can execute initial RPCs. + RPCRingBufferDescriptor Descriptor = GetRingBufferDescriptor(ERPCType::NetMulticast); + Schema_ClearField(SchemaObject, Descriptor.LastSentRPCFieldId); + Schema_AddUint32(SchemaObject, GetInitiallyPresentMulticastRPCsCountFieldId(), static_cast(LastSentId)); +} + +} // namespace RPCRingBufferUtils + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/ActorGroupManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialActorGroupManager.cpp similarity index 73% rename from SpatialGDK/Source/SpatialGDK/Private/Utils/ActorGroupManager.cpp rename to SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialActorGroupManager.cpp index 1bdacb3aad..2fa19baa31 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/ActorGroupManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialActorGroupManager.cpp @@ -1,7 +1,9 @@ -#include "Utils/ActorGroupManager.h" +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Utils/SpatialActorGroupManager.h" #include "SpatialGDKSettings.h" -void UActorGroupManager::Init() +void SpatialActorGroupManager::Init() { if (const USpatialGDKSettings* Settings = GetDefault()) { @@ -21,7 +23,7 @@ void UActorGroupManager::Init() } } -FName UActorGroupManager::GetActorGroupForClass(const TSubclassOf Class) +FName SpatialActorGroupManager::GetActorGroupForClass(const TSubclassOf Class) { if (Class == nullptr) { @@ -35,11 +37,12 @@ FName UActorGroupManager::GetActorGroupForClass(const TSubclassOf Class) { if (const FName* ActorGroup = ClassPathToActorGroup.Find(ClassPtr)) { + FName ActorGroupHolder = *ActorGroup; if (FoundClass != Class) { - ClassPathToActorGroup.Add(TSoftClassPtr(Class), *ActorGroup); + ClassPathToActorGroup.Add(TSoftClassPtr(Class), ActorGroupHolder); } - return *ActorGroup; + return ActorGroupHolder; } FoundClass = FoundClass->GetSuperClass(); @@ -51,7 +54,7 @@ FName UActorGroupManager::GetActorGroupForClass(const TSubclassOf Class) return SpatialConstants::DefaultActorGroup; } -FName UActorGroupManager::GetWorkerTypeForClass(const TSubclassOf Class) +FName SpatialActorGroupManager::GetWorkerTypeForClass(const TSubclassOf Class) { const FName ActorGroup = GetActorGroupForClass(Class); @@ -63,7 +66,7 @@ FName UActorGroupManager::GetWorkerTypeForClass(const TSubclassOf Class) return DefaultWorkerType; } -FName UActorGroupManager::GetWorkerTypeForActorGroup(const FName& ActorGroup) const +FName SpatialActorGroupManager::GetWorkerTypeForActorGroup(const FName& ActorGroup) const { if (const FName* WorkerType = ActorGroupToWorkerType.Find(ActorGroup)) { @@ -73,7 +76,7 @@ FName UActorGroupManager::GetWorkerTypeForActorGroup(const FName& ActorGroup) co return DefaultWorkerType; } -bool UActorGroupManager::IsSameWorkerType(const AActor* ActorA, const AActor* ActorB) +bool SpatialActorGroupManager::IsSameWorkerType(const AActor* ActorA, const AActor* ActorB) { if (ActorA == nullptr || ActorB == nullptr) { diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebugger.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebugger.cpp new file mode 100644 index 0000000000..b5a45663dc --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebugger.cpp @@ -0,0 +1,516 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Utils/SpatialDebugger.h" + +#include "EngineClasses/SpatialNetDriver.h" +#include "Interop/SpatialReceiver.h" +#include "Interop/SpatialSender.h" +#include "Interop/SpatialStaticComponentView.h" +#include "LoadBalancing/WorkerRegion.h" +#include "Schema/AuthorityIntent.h" +#include "Schema/SpatialDebugging.h" +#include "SpatialCommonTypes.h" +#include "Utils/InspectionColors.h" + +#include "Debug/DebugDrawService.h" +#include "Engine/Engine.h" +#include "GameFramework/Pawn.h" +#include "GameFramework/PlayerController.h" +#include "GameFramework/PlayerState.h" +#include "GenericPlatform/GenericPlatformMath.h" +#include "Kismet/GameplayStatics.h" +#include "Net/UnrealNetwork.h" + +using namespace SpatialGDK; + +DEFINE_LOG_CATEGORY(LogSpatialDebugger); + +namespace +{ + const FString DEFAULT_WORKER_REGION_MATERIAL = TEXT("/SpatialGDK/SpatialDebugger/Materials/TranslucentWorkerRegion.TranslucentWorkerRegion"); +} + +ASpatialDebugger::ASpatialDebugger(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.bStartWithTickEnabled = true; + PrimaryActorTick.TickInterval = 1.f; + + bAlwaysRelevant = true; + bNetLoadOnClient = false; + bReplicates = true; + + NetUpdateFrequency = 1.f; + + NetDriver = Cast(GetNetDriver()); + + // For GDK design reasons, this is the approach chosen to get a pointer + // on the net driver to the client ASpatialDebugger. Various alternatives + // were considered and this is the best of a bad bunch. + if (NetDriver != nullptr && GetNetMode() == NM_Client) + { + NetDriver->SetSpatialDebugger(this); + } +} + +void ASpatialDebugger::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME_CONDITION(ASpatialDebugger, WorkerRegions, COND_SimulatedOnly); +} + +void ASpatialDebugger::Tick(float DeltaSeconds) +{ + Super::Tick(DeltaSeconds); + + check(NetDriver != nullptr); + + if (!NetDriver->IsServer()) + { + for (TMap>::TIterator It = EntityActorMapping.CreateIterator(); It; ++It) + { + if (!It->Value.IsValid()) + { + It.RemoveCurrent(); + } + } + + // Since we have no guarantee on the order we'll receive the PC/Pawn/PlayerState + // over the wire, we check here once per tick (currently 1 Hz tick rate) to setup our local pointers. + // Note that we can capture the PC in OnEntityAdded() since we know we will only receive one of those. + if (LocalPawn.IsValid() == false && LocalPlayerController.IsValid()) + { + LocalPawn = LocalPlayerController->GetPawn(); + } + + if (LocalPlayerState.IsValid() == false && LocalPawn.IsValid()) + { + LocalPlayerState = LocalPawn->GetPlayerState(); + } + + if (LocalPawn.IsValid()) + { + SCOPE_CYCLE_COUNTER(STAT_SortingActors); + const FVector& PlayerLocation = LocalPawn->GetActorLocation(); + + EntityActorMapping.ValueSort([PlayerLocation](const TWeakObjectPtr& A, const TWeakObjectPtr& B) { + return FVector::Dist(PlayerLocation, A->GetActorLocation()) > FVector::Dist(PlayerLocation, B->GetActorLocation()); + }); + } + } +} + +void ASpatialDebugger::BeginPlay() +{ + Super::BeginPlay(); + + check(NetDriver != nullptr); + + if (!NetDriver->IsServer()) + { + EntityActorMapping.Reserve(ENTITY_ACTOR_MAP_RESERVATION_COUNT); + + LoadIcons(); + + TArray EntityIds; + NetDriver->StaticComponentView->GetEntityIds(EntityIds); + + // Capture any entities that are already present on this client (ie they came over the wire before the SpatialDebugger did). + for (const Worker_EntityId_Key EntityId : EntityIds) + { + OnEntityAdded(EntityId); + } + + // Register callbacks to get notified of all future entity arrivals / deletes. + OnEntityAddedHandle = NetDriver->Receiver->OnEntityAddedDelegate.AddUObject(this, &ASpatialDebugger::OnEntityAdded); + OnEntityRemovedHandle = NetDriver->Receiver->OnEntityRemovedDelegate.AddUObject(this, &ASpatialDebugger::OnEntityRemoved); + + FontRenderInfo.bClipText = true; + FontRenderInfo.bEnableShadow = true; + + RenderFont = GEngine->GetSmallFont(); + + if (bAutoStart) + { + SpatialToggleDebugger(); + } + } +} + +void ASpatialDebugger::OnAuthorityGained() +{ + if (NetDriver->LoadBalanceStrategy) + { + if (const UGridBasedLBStrategy* GridBasedLBStrategy = Cast(NetDriver->LoadBalanceStrategy)) + { + const UGridBasedLBStrategy::LBStrategyRegions LBStrategyRegions = GridBasedLBStrategy->GetLBStrategyRegions(); + WorkerRegions.SetNum(LBStrategyRegions.Num()); + for (int i = 0; i < LBStrategyRegions.Num(); i++) + { + const TPair& LBStrategyRegion = LBStrategyRegions[i]; + const PhysicalWorkerName* WorkerName = NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(LBStrategyRegion.Key); + FWorkerRegionInfo WorkerRegionInfo; + WorkerRegionInfo.Color = (WorkerName == nullptr) ? InvalidServerTintColor : SpatialGDK::GetColorForWorkerName(*WorkerName); + WorkerRegionInfo.Extents = LBStrategyRegion.Value; + WorkerRegions[i] = WorkerRegionInfo; + } + } + } +} + +void ASpatialDebugger::CreateWorkerRegions() +{ + UMaterial* WorkerRegionMaterial = LoadObject(nullptr, *DEFAULT_WORKER_REGION_MATERIAL); + if (WorkerRegionMaterial == nullptr) + { + UE_LOG(LogSpatialDebugger, Error, TEXT("Worker regions were not rendered. Could not find default material: %s"), + *DEFAULT_WORKER_REGION_MATERIAL); + return; + } + + // Create new actors for all new worker regions + FActorSpawnParameters SpawnParams; + SpawnParams.bNoFail = true; + SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + for (const FWorkerRegionInfo& WorkerRegionData : WorkerRegions) + { + AWorkerRegion* WorkerRegion = GetWorld()->SpawnActor(SpawnParams); + WorkerRegion->Init(WorkerRegionMaterial, WorkerRegionData.Color, WorkerRegionData.Extents, WorkerRegionVerticalScale); + WorkerRegion->SetActorEnableCollision(false); + } +} + +void ASpatialDebugger::DestroyWorkerRegions() +{ + TArray WorkerRegionsToRemove; + UGameplayStatics::GetAllActorsOfClass(this, AWorkerRegion::StaticClass(), WorkerRegionsToRemove); + for (AActor* WorkerRegion : WorkerRegionsToRemove) + { + WorkerRegion->Destroy(); + } +} + +void ASpatialDebugger::OnRep_SetWorkerRegions() +{ + if (NetDriver != nullptr && !NetDriver->IsServer() && DrawDebugDelegateHandle.IsValid() && bShowWorkerRegions) + { + DestroyWorkerRegions(); + CreateWorkerRegions(); + } +} + +void ASpatialDebugger::Destroyed() +{ + if (NetDriver != nullptr && NetDriver->Receiver != nullptr) + { + if (OnEntityAddedHandle.IsValid()) + { + NetDriver->Receiver->OnEntityAddedDelegate.Remove(OnEntityAddedHandle); + } + + if (OnEntityRemovedHandle.IsValid()) + { + NetDriver->Receiver->OnEntityRemovedDelegate.Remove(OnEntityRemovedHandle); + } + } + + if (DrawDebugDelegateHandle.IsValid()) + { + UDebugDrawService::Unregister(DrawDebugDelegateHandle); + DestroyWorkerRegions(); + } + + Super::Destroyed(); +} + +void ASpatialDebugger::LoadIcons() +{ + check(NetDriver != nullptr && !NetDriver->IsServer()); + + UTexture2D* DefaultTexture = DefaultTexture = LoadObject(nullptr, TEXT("/Engine/EngineResources/DefaultTexture.DefaultTexture")); + + const float IconWidth = 16.0f; + const float IconHeight = 16.0f; + + Icons[ICON_AUTH] = UCanvas::MakeIcon(AuthTexture != nullptr ? AuthTexture : DefaultTexture, 0.0f, 0.0f, IconWidth, IconHeight); + Icons[ICON_AUTH_INTENT] = UCanvas::MakeIcon(AuthIntentTexture != nullptr ? AuthIntentTexture : DefaultTexture, 0.0f, 0.0f, IconWidth, IconHeight); + Icons[ICON_UNLOCKED] = UCanvas::MakeIcon(UnlockedTexture != nullptr ? UnlockedTexture : DefaultTexture, 0.0f, 0.0f, IconWidth, IconHeight); + Icons[ICON_LOCKED] = UCanvas::MakeIcon(LockedTexture != nullptr ? LockedTexture : DefaultTexture, 0.0f, 0.0f, IconWidth, IconHeight); + Icons[ICON_BOX] = UCanvas::MakeIcon(BoxTexture != nullptr ? BoxTexture : DefaultTexture, 0.0f, 0.0f, IconWidth, IconHeight); +} + +void ASpatialDebugger::OnEntityAdded(const Worker_EntityId EntityId) +{ + check(NetDriver != nullptr && !NetDriver->IsServer()); + + TWeakObjectPtr* ExistingActor = EntityActorMapping.Find(EntityId); + + if (ExistingActor != nullptr) + { + return; + } + + if (AActor* Actor = Cast(NetDriver->PackageMap->GetObjectFromEntityId(EntityId).Get())) + { + EntityActorMapping.Add(EntityId, Actor); + + // Each client will only receive a PlayerController once. + if (Actor->IsA()) + { + LocalPlayerController = Cast(Actor); + } + } +} + +void ASpatialDebugger::OnEntityRemoved(const Worker_EntityId EntityId) +{ + check(NetDriver != nullptr && !NetDriver->IsServer()); + + EntityActorMapping.Remove(EntityId); +} + +void ASpatialDebugger::ActorAuthorityChanged(const Worker_AuthorityChangeOp& AuthOp) const +{ + const bool bAuthoritative = AuthOp.authority == WORKER_AUTHORITY_AUTHORITATIVE; + + if (bAuthoritative && AuthOp.component_id == SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID) + { + if (NetDriver->VirtualWorkerTranslator == nullptr) + { + // Currently, there's nothing to display in the debugger other than load balancing information. + return; + } + + VirtualWorkerId LocalVirtualWorkerId = NetDriver->VirtualWorkerTranslator->GetLocalVirtualWorkerId(); + FColor LocalVirtualWorkerColor = SpatialGDK::GetColorForWorkerName(NetDriver->VirtualWorkerTranslator->GetLocalPhysicalWorkerName()); + + SpatialDebugging* DebuggingInfo = NetDriver->StaticComponentView->GetComponentData(AuthOp.entity_id); + if (DebuggingInfo == nullptr) + { + // Some entities won't have debug info, so create it now. + SpatialDebugging NewDebuggingInfo(LocalVirtualWorkerId, LocalVirtualWorkerColor, SpatialConstants::INVALID_VIRTUAL_WORKER_ID, InvalidServerTintColor, false); + NetDriver->Sender->SendAddComponents(AuthOp.entity_id, { NewDebuggingInfo.CreateSpatialDebuggingData() }); + return; + } + else + { + DebuggingInfo->AuthoritativeVirtualWorkerId = LocalVirtualWorkerId; + DebuggingInfo->AuthoritativeColor = LocalVirtualWorkerColor; + FWorkerComponentUpdate DebuggingUpdate = DebuggingInfo->CreateSpatialDebuggingUpdate(); + NetDriver->Connection->SendComponentUpdate(AuthOp.entity_id, &DebuggingUpdate); + } + } +} + +void ASpatialDebugger::ActorAuthorityIntentChanged(Worker_EntityId EntityId, VirtualWorkerId NewIntentVirtualWorkerId) const +{ + SpatialDebugging* DebuggingInfo = NetDriver->StaticComponentView->GetComponentData(EntityId); + check(DebuggingInfo != nullptr); + DebuggingInfo->IntentVirtualWorkerId = NewIntentVirtualWorkerId; + + const PhysicalWorkerName* NewAuthoritativePhysicalWorkerName = NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(NewIntentVirtualWorkerId); + check(NewAuthoritativePhysicalWorkerName != nullptr); + + DebuggingInfo->IntentColor = SpatialGDK::GetColorForWorkerName(*NewAuthoritativePhysicalWorkerName); + FWorkerComponentUpdate DebuggingUpdate = DebuggingInfo->CreateSpatialDebuggingUpdate(); + NetDriver->Connection->SendComponentUpdate(EntityId, &DebuggingUpdate); +} + +void ASpatialDebugger::DrawTag(UCanvas* Canvas, const FVector2D& ScreenLocation, const Worker_EntityId EntityId, const FString& ActorName) +{ + SCOPE_CYCLE_COUNTER(STAT_DrawTag); + + // TODO: Smarter positioning of elements so they're centered no matter how many are enabled https://improbableio.atlassian.net/browse/UNR-2360. + int32 HorizontalOffset = -32.0f; + + check(NetDriver != nullptr && !NetDriver->IsServer()); + if (!NetDriver->StaticComponentView->HasComponent(EntityId, SpatialConstants::SPATIAL_DEBUGGING_COMPONENT_ID)) + { + return; + } + + const SpatialDebugging* DebuggingInfo = NetDriver->StaticComponentView->GetComponentData(EntityId); + + static const float BaseHorizontalOffset(16.0f); + + if (bShowLock) + { + SCOPE_CYCLE_COUNTER(STAT_DrawIcons); + const bool bIsLocked = DebuggingInfo->IsLocked; + const EIcon LockIcon = bIsLocked ? ICON_LOCKED : ICON_UNLOCKED; + + Canvas->SetDrawColor(FColor::White); + Canvas->DrawIcon(Icons[LockIcon], ScreenLocation.X + HorizontalOffset, ScreenLocation.Y, 1.0f); + HorizontalOffset += BaseHorizontalOffset; + } + + if (bShowAuth) + { + SCOPE_CYCLE_COUNTER(STAT_DrawIcons); + const FColor& ServerWorkerColor = DebuggingInfo->AuthoritativeColor; + Canvas->SetDrawColor(FColor::White); + Canvas->DrawIcon(Icons[ICON_AUTH], ScreenLocation.X + HorizontalOffset, ScreenLocation.Y, 1.0f); + HorizontalOffset += BaseHorizontalOffset; + Canvas->SetDrawColor(ServerWorkerColor); + const float BoxScaleBasedOnNumberSize = 0.75f * GetNumberOfDigitsIn(DebuggingInfo->AuthoritativeVirtualWorkerId); + Canvas->DrawScaledIcon(Icons[ICON_BOX], ScreenLocation.X + HorizontalOffset, ScreenLocation.Y, FVector(BoxScaleBasedOnNumberSize, 1.f, 1.f)); + Canvas->SetDrawColor(GetTextColorForBackgroundColor(ServerWorkerColor)); + Canvas->DrawText(RenderFont, FString::FromInt(DebuggingInfo->AuthoritativeVirtualWorkerId), ScreenLocation.X + HorizontalOffset + 1, ScreenLocation.Y, 1.1f, 1.1f, FontRenderInfo); + HorizontalOffset += (BaseHorizontalOffset * BoxScaleBasedOnNumberSize); + } + + if (bShowAuthIntent) + { + SCOPE_CYCLE_COUNTER(STAT_DrawIcons); + const FColor& VirtualWorkerColor = DebuggingInfo->IntentColor; + Canvas->SetDrawColor(FColor::White); + Canvas->DrawIcon(Icons[ICON_AUTH_INTENT], ScreenLocation.X + HorizontalOffset, ScreenLocation.Y, 1.0f); + HorizontalOffset += 16.0f; + Canvas->SetDrawColor(VirtualWorkerColor); + const float BoxScaleBasedOnNumberSize = 0.75f * GetNumberOfDigitsIn(DebuggingInfo->IntentVirtualWorkerId); + Canvas->DrawScaledIcon(Icons[ICON_BOX], ScreenLocation.X + HorizontalOffset, ScreenLocation.Y, FVector(BoxScaleBasedOnNumberSize, 1.f, 1.f)); + Canvas->SetDrawColor(GetTextColorForBackgroundColor(VirtualWorkerColor)); + Canvas->DrawText(RenderFont, FString::FromInt(DebuggingInfo->IntentVirtualWorkerId), ScreenLocation.X + HorizontalOffset + 1, ScreenLocation.Y, 1.1f, 1.1f, FontRenderInfo); + HorizontalOffset += (BaseHorizontalOffset * BoxScaleBasedOnNumberSize); + } + + FString Label; + if (bShowEntityId) + { + SCOPE_CYCLE_COUNTER(STAT_BuildText); + Label += FString::Printf(TEXT("%lld "), EntityId); + } + + if (bShowActorName) + { + SCOPE_CYCLE_COUNTER(STAT_BuildText); + Label += FString::Printf(TEXT("(%s)"), *ActorName); + } + + if (bShowEntityId || bShowActorName) + { + SCOPE_CYCLE_COUNTER(STAT_DrawText); + Canvas->SetDrawColor(FColor::Green); + Canvas->DrawText(RenderFont, Label, ScreenLocation.X + HorizontalOffset, ScreenLocation.Y, 1.0f, 1.0f, FontRenderInfo); + } +} + +FColor ASpatialDebugger::GetTextColorForBackgroundColor(const FColor& BackgroundColor) const +{ + return BackgroundColor.ReinterpretAsLinear().ComputeLuminance() > 0.5 ? FColor::Black : FColor::White; +} + +// This will break once we have more than 10,000 workers, happily kicking that can down the road. +int32 ASpatialDebugger::GetNumberOfDigitsIn(int32 SomeNumber) const +{ + SomeNumber = FMath::Abs(SomeNumber); + return (SomeNumber < 10 ? 1 : (SomeNumber < 100 ? 2 : (SomeNumber < 1000 ? 3 : 4))); +} + +void ASpatialDebugger::DrawDebug(UCanvas* Canvas, APlayerController* /* Controller */) // Controller is invalid. +{ + SCOPE_CYCLE_COUNTER(STAT_DrawDebug); + + check(NetDriver != nullptr && !NetDriver->IsServer()); + +#if WITH_EDITOR + // Prevent one client's data rendering in another client's view in PIE when using UDebugDrawService. Lifted from EQSRenderingComponent. + if (Canvas && Canvas->SceneView && Canvas->SceneView->Family && Canvas->SceneView->Family->Scene && Canvas->SceneView->Family->Scene->GetWorld() != GetWorld()) + { + return; + } +#endif + + DrawDebugLocalPlayer(Canvas); + + FVector PlayerLocation = FVector::ZeroVector; + + if (LocalPawn.IsValid()) + { + PlayerLocation = LocalPawn->GetActorLocation(); + } + + for (TPair>& EntityActorPair : EntityActorMapping) + { + const TWeakObjectPtr Actor = EntityActorPair.Value; + const Worker_EntityId EntityId = EntityActorPair.Key; + + if (Actor != nullptr) + { + FVector ActorLocation = Actor->GetActorLocation(); + + if (ActorLocation.IsZero()) + { + continue; + } + + if (FVector::Dist(PlayerLocation, ActorLocation) > MaxRange) + { + continue; + } + + FVector2D ScreenLocation = FVector2D::ZeroVector; + if (LocalPlayerController.IsValid()) + { + SCOPE_CYCLE_COUNTER(STAT_Projection); + UGameplayStatics::ProjectWorldToScreen(LocalPlayerController.Get(), ActorLocation + WorldSpaceActorTagOffset, ScreenLocation, false); + } + + if (ScreenLocation.IsZero()) + { + continue; + } + + DrawTag(Canvas, ScreenLocation, EntityId, Actor->GetName()); + } + } +} + +void ASpatialDebugger::DrawDebugLocalPlayer(UCanvas* Canvas) +{ + if (LocalPawn == nullptr || LocalPlayerController == nullptr || LocalPlayerState == nullptr) + { + return; + } + + const TArray> LocalPlayerActors = + { + LocalPawn, + LocalPlayerController, + LocalPlayerState + }; + + FVector2D ScreenLocation(PlayerPanelStartX, PlayerPanelStartY); + + for (int32 i = 0; i < LocalPlayerActors.Num(); ++i) + { + if (LocalPlayerActors[i].IsValid()) + { + const Worker_EntityId EntityId = NetDriver->PackageMap->GetEntityIdFromObject(LocalPlayerActors[i].Get()); + DrawTag(Canvas, ScreenLocation, EntityId, LocalPlayerActors[i]->GetName()); + ScreenLocation.Y -= PLAYER_TAG_VERTICAL_OFFSET; + } + } +} + +void ASpatialDebugger::SpatialToggleDebugger() +{ + check(NetDriver != nullptr && !NetDriver->IsServer()); + + if (DrawDebugDelegateHandle.IsValid()) + { + UDebugDrawService::Unregister(DrawDebugDelegateHandle); + DrawDebugDelegateHandle.Reset(); + DestroyWorkerRegions(); + } + else + { + DrawDebugDelegateHandle = UDebugDrawService::Register(TEXT("Game"), FDebugDrawDelegate::CreateUObject(this, &ASpatialDebugger::DrawDebug)); + if (bShowWorkerRegions) + { + CreateWorkerRegions(); + } + } +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLatencyTracer.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLatencyTracer.cpp new file mode 100644 index 0000000000..8b3fe20d9f --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLatencyTracer.cpp @@ -0,0 +1,626 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Utils/SpatialLatencyTracer.h" + +#include "Async/Async.h" +#include "Engine/World.h" +#include "EngineClasses/SpatialGameInstance.h" +#include "GeneralProjectSettings.h" +#include "Interop/Connection/OutgoingMessages.h" +#include "Utils/SchemaUtils.h" + +#include + +DEFINE_LOG_CATEGORY(LogSpatialLatencyTracing); + +DECLARE_CYCLE_STAT(TEXT("ContinueLatencyTraceRPC_Internal"), STAT_ContinueLatencyTraceRPC_Internal, STATGROUP_SpatialNet); +DECLARE_CYCLE_STAT(TEXT("BeginLatencyTraceRPC_Internal"), STAT_BeginLatencyTraceRPC_Internal, STATGROUP_SpatialNet); + +namespace +{ + // Stream for piping trace lib output to UE output + class UEStream : public std::stringbuf + { + int sync() override + { + UE_LOG(LogSpatialLatencyTracing, Verbose, TEXT("%s"), *FString(str().c_str())); + str(""); + return std::stringbuf::sync(); + } + + public: + virtual ~UEStream() override + { + sync(); + } + }; + + UEStream Stream; + +#if TRACE_LIB_ACTIVE + improbable::trace::SpanContext ReadSpanContext(const void* TraceBytes, const void* SpanBytes) + { + improbable::trace::TraceId _TraceId; + memcpy(&_TraceId[0], TraceBytes, sizeof(improbable::trace::TraceId)); + + improbable::trace::SpanId _SpanId; + memcpy(&_SpanId[0], SpanBytes, sizeof(improbable::trace::SpanId)); + + return improbable::trace::SpanContext(_TraceId, _SpanId); + } +#endif +} // anonymous namespace + +USpatialLatencyTracer::USpatialLatencyTracer() +{ +#if TRACE_LIB_ACTIVE + ResetWorkerId(); + FParse::Value(FCommandLine::Get(), TEXT("traceMetadata"), TraceMetadata); +#endif +} + +void USpatialLatencyTracer::RegisterProject(UObject* WorldContextObject, const FString& ProjectId) +{ +#if TRACE_LIB_ACTIVE + using namespace improbable::exporters::trace; + + StackdriverExporter::Register({ TCHAR_TO_UTF8(*ProjectId) }); + + std::cout.rdbuf(&Stream); + std::cerr.rdbuf(&Stream); + + StdoutExporter::Register(); +#endif // TRACE_LIB_ACTIVE +} + +bool USpatialLatencyTracer::SetTraceMetadata(UObject* WorldContextObject, const FString& NewTraceMetadata) +{ +#if TRACE_LIB_ACTIVE + if (USpatialLatencyTracer* Tracer = GetTracer(WorldContextObject)) + { + Tracer->TraceMetadata = NewTraceMetadata; + return true; + } +#endif // TRACE_LIB_ACTIVE + return false; +} + +bool USpatialLatencyTracer::BeginLatencyTrace(UObject* WorldContextObject, const FString& TraceDesc, FSpatialLatencyPayload& OutLatencyPayload) +{ +#if TRACE_LIB_ACTIVE + if (USpatialLatencyTracer* Tracer = GetTracer(WorldContextObject)) + { + return Tracer->BeginLatencyTrace_Internal(TraceDesc, OutLatencyPayload); + } +#endif // TRACE_LIB_ACTIVE + return false; +} + +bool USpatialLatencyTracer::ContinueLatencyTraceRPC(UObject* WorldContextObject, const AActor* Actor, const FString& FunctionName, const FString& TraceDesc, const FSpatialLatencyPayload& LatencyPayload, FSpatialLatencyPayload& OutContinuedLatencyPayload) +{ +#if TRACE_LIB_ACTIVE + if (USpatialLatencyTracer* Tracer = GetTracer(WorldContextObject)) + { + return Tracer->ContinueLatencyTrace_Internal(Actor, FunctionName, ETraceType::RPC, TraceDesc, LatencyPayload, OutContinuedLatencyPayload); + } +#endif // TRACE_LIB_ACTIVE + return false; +} + +bool USpatialLatencyTracer::ContinueLatencyTraceProperty(UObject* WorldContextObject, const AActor* Actor, const FString& PropertyName, const FString& TraceDesc, const FSpatialLatencyPayload& LatencyPayload, FSpatialLatencyPayload& OutContinuedLatencyPayload) +{ +#if TRACE_LIB_ACTIVE + if (USpatialLatencyTracer* Tracer = GetTracer(WorldContextObject)) + { + return Tracer->ContinueLatencyTrace_Internal(Actor, PropertyName, ETraceType::Property, TraceDesc, LatencyPayload, OutContinuedLatencyPayload); + } +#endif // TRACE_LIB_ACTIVE + return false; +} + +bool USpatialLatencyTracer::ContinueLatencyTraceTagged(UObject* WorldContextObject, const AActor* Actor, const FString& Tag, const FString& TraceDesc, const FSpatialLatencyPayload& LatencyPayload, FSpatialLatencyPayload& OutContinuedLatencyPayload) +{ +#if TRACE_LIB_ACTIVE + if (USpatialLatencyTracer* Tracer = GetTracer(WorldContextObject)) + { + return Tracer->ContinueLatencyTrace_Internal(Actor, Tag, ETraceType::Tagged, TraceDesc, LatencyPayload, OutContinuedLatencyPayload); + } +#endif // TRACE_LIB_ACTIVE + return false; +} + +bool USpatialLatencyTracer::EndLatencyTrace(UObject* WorldContextObject, const FSpatialLatencyPayload& LatencyPayload) +{ +#if TRACE_LIB_ACTIVE + if (USpatialLatencyTracer* Tracer = GetTracer(WorldContextObject)) + { + return Tracer->EndLatencyTrace_Internal(LatencyPayload); + } +#endif // TRACE_LIB_ACTIVE + return false; +} + +FSpatialLatencyPayload USpatialLatencyTracer::RetrievePayload(UObject* WorldContextObject, const AActor* Actor, const FString& Tag) +{ +#if TRACE_LIB_ACTIVE + if (USpatialLatencyTracer* Tracer = GetTracer(WorldContextObject)) + { + return Tracer->RetrievePayload_Internal(Actor, Tag); + } +#endif + return FSpatialLatencyPayload{}; +} + +USpatialLatencyTracer* USpatialLatencyTracer::GetTracer(UObject* WorldContextObject) +{ +#if TRACE_LIB_ACTIVE + UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull); + if (World == nullptr) + { + World = GWorld; + } + + if (USpatialGameInstance* GameInstance = World->GetGameInstance()) + { + return GameInstance->GetSpatialLatencyTracer(); + } +#endif + return nullptr; +} + +#if TRACE_LIB_ACTIVE +bool USpatialLatencyTracer::IsValidKey(const TraceKey Key) +{ + FScopeLock Lock(&Mutex); + return (TraceMap.Find(Key) != nullptr); +} + +TraceKey USpatialLatencyTracer::RetrievePendingTrace(const UObject* Obj, const UFunction* Function) +{ + FScopeLock Lock(&Mutex); + + ActorFuncKey FuncKey{ Cast(Obj), Function }; + TraceKey ReturnKey = InvalidTraceKey; + TrackingRPCs.RemoveAndCopyValue(FuncKey, ReturnKey); + return ReturnKey; +} + +TraceKey USpatialLatencyTracer::RetrievePendingTrace(const UObject* Obj, const UProperty* Property) +{ + FScopeLock Lock(&Mutex); + + ActorPropertyKey PropKey{ Cast(Obj), Property }; + TraceKey ReturnKey = InvalidTraceKey; + TrackingProperties.RemoveAndCopyValue(PropKey, ReturnKey); + return ReturnKey; +} + +TraceKey USpatialLatencyTracer::RetrievePendingTrace(const UObject* Obj, const FString& Tag) +{ + FScopeLock Lock(&Mutex); + + ActorTagKey EventKey{ Cast(Obj), Tag }; + TraceKey ReturnKey = InvalidTraceKey; + TrackingTags.RemoveAndCopyValue(EventKey, ReturnKey); + return ReturnKey; +} + +void USpatialLatencyTracer::WriteToLatencyTrace(const TraceKey Key, const FString& TraceDesc) +{ + FScopeLock Lock(&Mutex); + + if (TraceSpan* Trace = TraceMap.Find(Key)) + { + WriteKeyFrameToTrace(Trace, TraceDesc); + } +} + +void USpatialLatencyTracer::WriteAndEndTraceIfRemote(const TraceKey Key, const FString& TraceDesc) +{ + FScopeLock Lock(&Mutex); + + if (TraceSpan* Trace = TraceMap.Find(Key)) + { + WriteKeyFrameToTrace(Trace, TraceDesc); + + // Check RootTraces to verify if this trace was started locally. If it was, we don't End the trace yet, but + // wait for an explicit call to EndLatencyTrace. + if (RootTraces.Find(Key) == nullptr) + { + Trace->End(); + TraceMap.Remove(Key); + } + } +} + +void USpatialLatencyTracer::WriteTraceToSchemaObject(const TraceKey Key, Schema_Object* Obj, const Schema_FieldId FieldId) +{ + FScopeLock Lock(&Mutex); + + if (TraceSpan* Trace = TraceMap.Find(Key)) + { + Schema_Object* TraceObj = Schema_AddObject(Obj, FieldId); + + const improbable::trace::SpanContext& TraceContext = Trace->context(); + improbable::trace::TraceId _TraceId = TraceContext.trace_id(); + improbable::trace::SpanId _SpanId = TraceContext.span_id(); + + SpatialGDK::AddBytesToSchema(TraceObj, SpatialConstants::UNREAL_RPC_TRACE_ID, &_TraceId[0], _TraceId.size()); + SpatialGDK::AddBytesToSchema(TraceObj, SpatialConstants::UNREAL_RPC_SPAN_ID, &_SpanId[0], _SpanId.size()); + } +} + +TraceKey USpatialLatencyTracer::ReadTraceFromSchemaObject(Schema_Object* Obj, const Schema_FieldId FieldId) +{ + FScopeLock Lock(&Mutex); + + if (Schema_GetObjectCount(Obj, FieldId) > 0) + { + Schema_Object* TraceData = Schema_IndexObject(Obj, FieldId, 0); + + const uint8* TraceBytes = Schema_GetBytes(TraceData, SpatialConstants::UNREAL_RPC_TRACE_ID); + const uint8* SpanBytes = Schema_GetBytes(TraceData, SpatialConstants::UNREAL_RPC_SPAN_ID); + + improbable::trace::SpanContext DestContext = ReadSpanContext(TraceBytes, SpanBytes); + + TraceKey Key = InvalidTraceKey; + + for (const auto& TracePair : TraceMap) + { + const TraceKey& _Key = TracePair.Key; + const TraceSpan& Span = TracePair.Value; + + if (Span.context().trace_id() == DestContext.trace_id()) + { + Key = _Key; + break; + } + } + + if (Key != InvalidTraceKey) + { + TraceSpan* Span = TraceMap.Find(Key); + + WriteKeyFrameToTrace(Span, TEXT("Local Trace - Schema Obj Read")); + } + else + { + FString SpanMsg = FormatMessage(TEXT("Remote Parent Trace - Schema Obj Read")); + TraceSpan RetrieveTrace = improbable::trace::Span::StartSpanWithRemoteParent(TCHAR_TO_UTF8(*SpanMsg), DestContext); + + Key = GenerateNewTraceKey(); + TraceMap.Add(Key, MoveTemp(RetrieveTrace)); + } + + return Key; + } + + return InvalidTraceKey; +} + +FSpatialLatencyPayload USpatialLatencyTracer::RetrievePayload_Internal(const UObject* Obj, const FString& Tag) +{ + FScopeLock Lock(&Mutex); + + TraceKey Key = RetrievePendingTrace(Obj, Tag); + if (Key != InvalidTraceKey) + { + if (const TraceSpan* Span = TraceMap.Find(Key)) + { + const improbable::trace::SpanContext& TraceContext = Span->context(); + + TArray TraceBytes = TArray((const uint8_t*)&TraceContext.trace_id()[0], sizeof(improbable::trace::TraceId)); + TArray SpanBytes = TArray((const uint8_t*)&TraceContext.span_id()[0], sizeof(improbable::trace::SpanId)); + return FSpatialLatencyPayload(MoveTemp(TraceBytes), MoveTemp(SpanBytes), Key); + } + } + return {}; +} + +void USpatialLatencyTracer::ResetWorkerId() +{ + WorkerId = TEXT("DeviceId_") + FPlatformMisc::GetDeviceId(); +} + +void USpatialLatencyTracer::OnEnqueueMessage(const SpatialGDK::FOutgoingMessage* Message) +{ + if (Message->Type == SpatialGDK::EOutgoingMessageType::ComponentUpdate) + { + const SpatialGDK::FComponentUpdate* ComponentUpdate = static_cast(Message); + WriteToLatencyTrace(ComponentUpdate->Update.Trace, TEXT("Moved componentUpdate to Worker queue")); + } + else if (Message->Type == SpatialGDK::EOutgoingMessageType::AddComponent) + { + const SpatialGDK::FAddComponent* ComponentAdd = static_cast(Message); + WriteToLatencyTrace(ComponentAdd->Data.Trace, TEXT("Moved componentAdd to Worker queue")); + } + else if (Message->Type == SpatialGDK::EOutgoingMessageType::CreateEntityRequest) + { + const SpatialGDK::FCreateEntityRequest* CreateEntityRequest = static_cast(Message); + for (auto& Component : CreateEntityRequest->Components) + { + WriteToLatencyTrace(Component.Trace, TEXT("Moved createEntityRequest to Worker queue")); + } + } +} + +void USpatialLatencyTracer::OnDequeueMessage(const SpatialGDK::FOutgoingMessage* Message) +{ + if (Message->Type == SpatialGDK::EOutgoingMessageType::ComponentUpdate) + { + const SpatialGDK::FComponentUpdate* ComponentUpdate = static_cast(Message); + WriteAndEndTraceIfRemote(ComponentUpdate->Update.Trace, TEXT("Sent componentUpdate to Worker SDK")); + } + else if (Message->Type == SpatialGDK::EOutgoingMessageType::AddComponent) + { + const SpatialGDK::FAddComponent* ComponentAdd = static_cast(Message); + WriteAndEndTraceIfRemote(ComponentAdd->Data.Trace, TEXT("Sent componentAdd to Worker SDK")); + } + else if (Message->Type == SpatialGDK::EOutgoingMessageType::CreateEntityRequest) + { + const SpatialGDK::FCreateEntityRequest* CreateEntityRequest = static_cast(Message); + for (auto& Component : CreateEntityRequest->Components) + { + WriteAndEndTraceIfRemote(Component.Trace, TEXT("Sent createEntityRequest to Worker SDK")); + } + } +} + +bool USpatialLatencyTracer::BeginLatencyTrace_Internal(const FString& TraceDesc, FSpatialLatencyPayload& OutLatencyPayload) +{ + // TODO: UNR-2787 - Improve mutex-related latency + // This functions might spike because of the Mutex below + SCOPE_CYCLE_COUNTER(STAT_BeginLatencyTraceRPC_Internal); + FScopeLock Lock(&Mutex); + + FString SpanMsg = FormatMessage(TraceDesc, true); + TraceSpan NewTrace = improbable::trace::Span::StartSpan(TCHAR_TO_UTF8(*SpanMsg), nullptr); + + // Construct payload data from trace + const improbable::trace::SpanContext& TraceContext = NewTrace.context(); + + { + TArray TraceBytes = TArray((const uint8_t*)&TraceContext.trace_id()[0], sizeof(improbable::trace::TraceId)); + TArray SpanBytes = TArray((const uint8_t*)&TraceContext.span_id()[0], sizeof(improbable::trace::SpanId)); + OutLatencyPayload = FSpatialLatencyPayload(MoveTemp(TraceBytes), MoveTemp(SpanBytes), GenerateNewTraceKey()); + } + + // Add to internal tracking + TraceMap.Add(OutLatencyPayload.Key, MoveTemp(NewTrace)); + + // Store traces started on this worker, so we can persist them until they've been round trip returned. + RootTraces.Add(OutLatencyPayload.Key); + + return true; +} + +bool USpatialLatencyTracer::ContinueLatencyTrace_Internal(const AActor* Actor, const FString& Target, ETraceType::Type Type, const FString& TraceDesc, const FSpatialLatencyPayload& LatencyPayload, FSpatialLatencyPayload& OutLatencyPayload) +{ + // TODO: UNR-2787 - Improve mutex-related latency + // This functions might spike because of the Mutex below + SCOPE_CYCLE_COUNTER(STAT_ContinueLatencyTraceRPC_Internal); + if (Actor == nullptr) + { + return false; + } + + // We do minimal internal tracking for native rpcs/properties + const bool bInternalTracking = GetDefault()->UsesSpatialNetworking() || Type == ETraceType::Tagged; + + FScopeLock Lock(&Mutex); + + OutLatencyPayload = LatencyPayload; + if (OutLatencyPayload.Key == InvalidTraceKey) + { + ResolveKeyInLatencyPayload(OutLatencyPayload); + } + + const TraceKey Key = OutLatencyPayload.Key; + const TraceSpan* ActiveTrace = TraceMap.Find(Key); + if (ActiveTrace == nullptr) + { + UE_LOG(LogSpatialLatencyTracing, Warning, TEXT("(%s) : No active trace to continue (%s)"), *WorkerId, *TraceDesc); + return false; + } + + if (bInternalTracking) + { + if (!AddTrackingInfo(Actor, Target, Type, Key)) + { + UE_LOG(LogSpatialLatencyTracing, Warning, TEXT("(%s) : Failed to create Actor/Func trace (%s)"), *WorkerId, *TraceDesc); + return false; + } + } + + WriteKeyFrameToTrace(ActiveTrace, FString::Printf(TEXT("Continue [%s] %s - %s"), *TraceDesc, *UEnum::GetValueAsString(Type), *Target)); + + // If we're not doing any further tracking, end the trace + if (!bInternalTracking) + { + WriteAndEndTraceIfRemote(Key, TEXT("Native - End of Tracking")); + } + + return true; +} + +bool USpatialLatencyTracer::EndLatencyTrace_Internal(const FSpatialLatencyPayload& LatencyPayload) +{ + FScopeLock Lock(&Mutex); + + // Create temp payload to resolve key + FSpatialLatencyPayload LocalLatencyPayload = LatencyPayload; + if (LocalLatencyPayload.Key == InvalidTraceKey) + { + ResolveKeyInLatencyPayload(LocalLatencyPayload); + } + + const TraceKey Key = LocalLatencyPayload.Key; + const TraceSpan* ActiveTrace = TraceMap.Find(Key); + if (ActiveTrace == nullptr) + { + UE_LOG(LogSpatialLatencyTracing, Warning, TEXT("(%s) : No active trace to end"), *WorkerId); + return false; + } + + WriteKeyFrameToTrace(ActiveTrace, TEXT("End")); + + ActiveTrace->End(); + + TraceMap.Remove(Key); + RootTraces.Remove(Key); + + return true; +} + +bool USpatialLatencyTracer::AddTrackingInfo(const AActor* Actor, const FString& Target, const ETraceType::Type Type, const TraceKey Key) +{ + if (Actor == nullptr) + { + return false; + } + + if (UClass* ActorClass = Actor->GetClass()) + { + switch (Type) + { + case ETraceType::RPC: + if (const UFunction* Function = ActorClass->FindFunctionByName(*Target)) + { + ActorFuncKey AFKey{ Actor, Function }; + if (TrackingRPCs.Find(AFKey) == nullptr) + { + TrackingRPCs.Add(AFKey, Key); + return true; + } + UE_LOG(LogSpatialLatencyTracing, Warning, TEXT("(%s) : ActorFunc already exists for trace"), *WorkerId); + } + break; + case ETraceType::Property: + if (const UProperty* Property = ActorClass->FindPropertyByName(*Target)) + { + ActorPropertyKey APKey{ Actor, Property }; + if (TrackingProperties.Find(APKey) == nullptr) + { + TrackingProperties.Add(APKey, Key); + return true; + } + UE_LOG(LogSpatialLatencyTracing, Warning, TEXT("(%s) : ActorProperty already exists for trace"), *WorkerId); + } + break; + case ETraceType::Tagged: + { + ActorTagKey ATKey{ Actor, Target }; + if (TrackingTags.Find(ATKey) == nullptr) + { + TrackingTags.Add(ATKey, Key); + return true; + } + UE_LOG(LogSpatialLatencyTracing, Warning, TEXT("(%s) : ActorProperty already exists for trace"), *WorkerId); + } + break; + } + } + + return false; +} + +TraceKey USpatialLatencyTracer::GenerateNewTraceKey() +{ + return NextTraceKey++; +} + +void USpatialLatencyTracer::ResolveKeyInLatencyPayload(FSpatialLatencyPayload& Payload) +{ + // Key isn't set, so attempt to find it in the trace map + for (const auto& TracePair : TraceMap) + { + const TraceKey& Key = TracePair.Key; + const TraceSpan& Span = TracePair.Value; + + if (memcmp(Span.context().trace_id().data(), Payload.TraceId.GetData(), sizeof(Payload.TraceId)) == 0) + { + WriteKeyFrameToTrace(&Span, TEXT("Local Trace - Payload Obj Read")); + Payload.Key = Key; + break; + } + } + + if (Payload.Key == InvalidTraceKey) + { + // Uninitialized key, generate and add to map + Payload.Key = GenerateNewTraceKey(); + + improbable::trace::SpanContext DestContext = ReadSpanContext(Payload.TraceId.GetData(), Payload.SpanId.GetData()); + + FString SpanMsg = FormatMessage(TEXT("Remote Parent Trace - Payload Obj Read")); + TraceSpan RetrieveTrace = improbable::trace::Span::StartSpanWithRemoteParent(TCHAR_TO_UTF8(*SpanMsg), DestContext); + + TraceMap.Add(Payload.Key, MoveTemp(RetrieveTrace)); + } +} + +void USpatialLatencyTracer::WriteKeyFrameToTrace(const TraceSpan* Trace, const FString& TraceDesc) +{ + if (Trace != nullptr) + { + FString TraceMsg = FormatMessage(TraceDesc); + improbable::trace::Span::StartSpan(TCHAR_TO_UTF8(*TraceMsg), Trace).End(); + } +} + +FString USpatialLatencyTracer::FormatMessage(const FString& Message, bool bIncludeMetadata) const +{ + if (bIncludeMetadata) + { + return FString::Printf(TEXT("%s (%s : %s)"), *Message, *TraceMetadata, *WorkerId.Left(18)); + } + else + { + return FString::Printf(TEXT("%s (%s)"), *Message, *WorkerId.Left(18)); + } +} + +#endif // TRACE_LIB_ACTIVE + +void USpatialLatencyTracer::Debug_SendTestTrace() +{ +#if TRACE_LIB_ACTIVE + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [] + { + using namespace improbable::trace; + + std::cout << "Sending test trace" << std::endl; + + Span RootSpan = Span::StartSpan("Example Span", nullptr); + + { + Span SubSpan1 = Span::StartSpan("Sub span 1", &RootSpan); + FPlatformProcess::Sleep(1); + SubSpan1.End(); + } + + { + Span SubSpan2 = Span::StartSpan("Sub span 2", &RootSpan); + FPlatformProcess::Sleep(1); + SubSpan2.End(); + } + + FPlatformProcess::Sleep(1); + + // recreate Span from context + const SpanContext& SourceContext = RootSpan.context(); + auto TraceId = SourceContext.trace_id(); + auto SpanId = SourceContext.span_id(); + RootSpan.End(); + + SpanContext DestContext(TraceId, SpanId); + + { + Span SubSpan3 = Span::StartSpanWithRemoteParent("SubSpan 3", DestContext); + SubSpan3.AddAnnotation("Starting sub span"); + FPlatformProcess::Sleep(1); + SubSpan3.End(); + } + }); +#endif // TRACE_LIB_ACTIVE +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp index a977d6e3cd..6d587f1346 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp @@ -4,20 +4,21 @@ #include "Engine/Engine.h" #include "EngineGlobals.h" -#include "GameFramework/PlayerController.h" -#include "EngineClasses/SpatialNetConnection.h" -#include "EngineClasses/SpatialNetDriver.h" -#include "EngineClasses/SpatialPackageMapClient.h" #include "Interop/Connection/SpatialWorkerConnection.h" #include "SpatialGDKSettings.h" #include "Utils/SchemaUtils.h" DEFINE_LOG_CATEGORY(LogSpatialMetrics); -void USpatialMetrics::Init(USpatialNetDriver* InNetDriver) +USpatialMetrics::WorkerMetricsDelegate USpatialMetrics::WorkerMetricsRecieved; + +void USpatialMetrics::Init(USpatialWorkerConnection* InConnection, float InNetServerMaxTickRate, bool bInIsServer) { - NetDriver = InNetDriver; + Connection = InConnection; + bIsServer = bInIsServer; + NetServerMaxTickRate = InNetServerMaxTickRate; + TimeBetweenMetricsReports = GetDefault()->MetricsReportRate; FramesSinceLastReport = 0; TimeOfLastReport = 0.0f; @@ -26,11 +27,11 @@ void USpatialMetrics::Init(USpatialNetDriver* InNetDriver) RPCTrackingStartTime = 0.0f; } -void USpatialMetrics::TickMetrics() +void USpatialMetrics::TickMetrics(float NetDriverTime) { FramesSinceLastReport++; - TimeSinceLastReport = NetDriver->Time - TimeOfLastReport; + TimeSinceLastReport = NetDriverTime - TimeOfLastReport; // Check that there has been a sufficient amount of time since the last report. if (TimeSinceLastReport > 0.f && TimeSinceLastReport < TimeBetweenMetricsReports) @@ -49,10 +50,10 @@ void USpatialMetrics::TickMetrics() DynamicFPSMetrics.GaugeMetrics.Add(DynamicFPSGauge); DynamicFPSMetrics.Load = WorkerLoad; - TimeOfLastReport = NetDriver->Time; + TimeOfLastReport = NetDriverTime; FramesSinceLastReport = 0; - NetDriver->Connection->SendMetrics(DynamicFPSMetrics); + Connection->SendMetrics(DynamicFPSMetrics); } // Load defined as performance relative to target frame time or just frame time based on config value. @@ -65,7 +66,7 @@ double USpatialMetrics::CalculateLoad() const return AverageFrameTime; } - float TargetFrameTime = 1.0f / NetDriver->NetServerMaxTickRate; + float TargetFrameTime = 1.0f / NetServerMaxTickRate; return AverageFrameTime / TargetFrameTime; } @@ -84,9 +85,9 @@ void USpatialMetrics::SpatialStartRPCMetrics() RPCTrackingStartTime = FPlatformTime::Seconds(); // If RPC tracking is activated on a client, send a command to the server to start tracking. - if (!NetDriver->IsServer()) + if (!bIsServer && ControllerRefProvider.IsBound()) { - FUnrealObjectRef PCObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromObject(Cast(NetDriver->GetSpatialOSNetConnection()->OwningActor)); + FUnrealObjectRef PCObjectRef = ControllerRefProvider.Execute(); Worker_EntityId ControllerEntityId = PCObjectRef.Entity; if (ControllerEntityId != SpatialConstants::INVALID_ENTITY_ID) @@ -95,7 +96,7 @@ void USpatialMetrics::SpatialStartRPCMetrics() Request.component_id = SpatialConstants::DEBUG_METRICS_COMPONENT_ID; Request.command_index = SpatialConstants::DEBUG_METRICS_START_RPC_METRICS_ID; Request.schema_type = Schema_CreateCommandRequest(); - NetDriver->Connection->SendCommandRequest(ControllerEntityId, &Request, SpatialConstants::DEBUG_METRICS_START_RPC_METRICS_ID); + Connection->SendCommandRequest(ControllerEntityId, &Request, SpatialConstants::DEBUG_METRICS_START_RPC_METRICS_ID); } else { @@ -147,18 +148,18 @@ void USpatialMetrics::SpatialStopRPCMetrics() int TotalPayload = 0; UE_LOG(LogSpatialMetrics, Log, TEXT("---------------------------")); - UE_LOG(LogSpatialMetrics, Log, TEXT("Recently sent RPCs - %s:"), NetDriver->IsServer() ? TEXT("Server") : TEXT("Client")); + UE_LOG(LogSpatialMetrics, Log, TEXT("Recently sent RPCs - %s:"), bIsServer ? TEXT("Server") : TEXT("Client")); UE_LOG(LogSpatialMetrics, Log, TEXT("RPC Type | %s | # of calls | Calls/sec | Total payload | Avg. payload | Payload/sec"), *FString(TEXT("RPC Name")).RightPad(MaxRPCNameLen)); FString SeparatorLine = FString::Printf(TEXT("-------------------+-%s-+------------+------------+---------------+--------------+------------"), *FString::ChrN(MaxRPCNameLen, '-')); - ESchemaComponentType PrevType = SCHEMA_Invalid; + ERPCType PrevType = ERPCType::Invalid; for (RPCStat& Stat : RecentRPCArray) { FString RPCTypeField; if (Stat.Type != PrevType) { - RPCTypeField = RPCSchemaTypeToString(Stat.Type); + RPCTypeField = SpatialConstants::RPCTypeToString(Stat.Type); PrevType = Stat.Type; UE_LOG(LogSpatialMetrics, Log, TEXT("%s"), *SeparatorLine); } @@ -175,9 +176,9 @@ void USpatialMetrics::SpatialStopRPCMetrics() bRPCTrackingEnabled = false; // If RPC tracking is stopped on a client, send a command to the server to stop tracking. - if (!NetDriver->IsServer()) + if (!bIsServer && ControllerRefProvider.IsBound()) { - FUnrealObjectRef PCObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromObject(Cast(NetDriver->GetSpatialOSNetConnection()->OwningActor)); + FUnrealObjectRef PCObjectRef = ControllerRefProvider.Execute(); Worker_EntityId ControllerEntityId = PCObjectRef.Entity; if (ControllerEntityId != SpatialConstants::INVALID_ENTITY_ID) @@ -186,7 +187,7 @@ void USpatialMetrics::SpatialStopRPCMetrics() Request.component_id = SpatialConstants::DEBUG_METRICS_COMPONENT_ID; Request.command_index = SpatialConstants::DEBUG_METRICS_STOP_RPC_METRICS_ID; Request.schema_type = Schema_CreateCommandRequest(); - NetDriver->Connection->SendCommandRequest(ControllerEntityId, &Request, SpatialConstants::DEBUG_METRICS_STOP_RPC_METRICS_ID); + Connection->SendCommandRequest(ControllerEntityId, &Request, SpatialConstants::DEBUG_METRICS_STOP_RPC_METRICS_ID); } else { @@ -202,9 +203,9 @@ void USpatialMetrics::OnStopRPCMetricsCommand() void USpatialMetrics::SpatialModifySetting(const FString& Name, float Value) { - if (!NetDriver->IsServer()) + if (!bIsServer && ControllerRefProvider.IsBound()) { - FUnrealObjectRef PCObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromObject(Cast(NetDriver->GetSpatialOSNetConnection()->OwningActor)); + FUnrealObjectRef PCObjectRef = ControllerRefProvider.Execute(); Worker_EntityId ControllerEntityId = PCObjectRef.Entity; if (ControllerEntityId != SpatialConstants::INVALID_ENTITY_ID) @@ -218,7 +219,7 @@ void USpatialMetrics::SpatialModifySetting(const FString& Name, float Value) SpatialGDK::AddStringToSchema(RequestObject, SpatialConstants::MODIFY_SETTING_PAYLOAD_NAME_ID, Name); Schema_AddFloat(RequestObject, SpatialConstants::MODIFY_SETTING_PAYLOAD_VALUE_ID, Value); - NetDriver->Connection->SendCommandRequest(ControllerEntityId, &Request, SpatialConstants::DEBUG_METRICS_MODIFY_SETTINGS_ID); + Connection->SendCommandRequest(ControllerEntityId, &Request, SpatialConstants::DEBUG_METRICS_MODIFY_SETTINGS_ID); } else { @@ -268,7 +269,7 @@ void USpatialMetrics::OnModifySettingCommand(Schema_Object* CommandPayload) SpatialModifySetting(Name, Value); } -void USpatialMetrics::TrackSentRPC(UFunction* Function, ESchemaComponentType RPCType, int PayloadSize) +void USpatialMetrics::TrackSentRPC(UFunction* Function, ERPCType RPCType, int PayloadSize) { if (!bRPCTrackingEnabled) { diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp index 559932d46e..7df399b35d 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp @@ -4,26 +4,31 @@ #include "Engine/World.h" #include "EngineClasses/SpatialNetDriver.h" +#include "EngineClasses/SpatialPackageMapClient.h" #include "GeneralProjectSettings.h" +#include "Interop/SpatialWorkerFlags.h" #include "Kismet/KismetSystemLibrary.h" #include "SpatialConstants.h" +#include "EngineClasses/SpatialGameInstance.h" #include "SpatialGDKSettings.h" -#include "Utils/ActorGroupManager.h" +#include "Utils/InspectionColors.h" +#include "Utils/SpatialActorGroupManager.h" DEFINE_LOG_CATEGORY(LogSpatial); bool USpatialStatics::IsSpatialNetworkingEnabled() { - return GetDefault()->bSpatialNetworking; + return GetDefault()->UsesSpatialNetworking(); } -UActorGroupManager* USpatialStatics::GetActorGroupManager(const UObject* WorldContext) +SpatialActorGroupManager* USpatialStatics::GetActorGroupManager(const UObject* WorldContext) { if (const UWorld* World = WorldContext->GetWorld()) { - if (const USpatialNetDriver* SpatialNetDriver = Cast(World->GetNetDriver())) + if (const USpatialGameInstance* SpatialGameInstance = Cast(World->GetGameInstance())) { - return SpatialNetDriver->ActorGroupManager; + check(SpatialGameInstance->ActorGroupManager.IsValid()); + return SpatialGameInstance->ActorGroupManager.Get(); } } return nullptr; @@ -42,6 +47,37 @@ FName USpatialStatics::GetCurrentWorkerType(const UObject* WorldContext) return NAME_None; } +bool USpatialStatics::GetWorkerFlag(const UObject* WorldContext, const FString& InFlagName, FString& OutFlagValue) +{ + if (const UWorld* World = WorldContext->GetWorld()) + { + if (const USpatialNetDriver* SpatialNetDriver = Cast(World->GetNetDriver())) + { + if (const USpatialWorkerFlags* SpatialWorkerFlags = SpatialNetDriver->SpatialWorkerFlags) + { + return SpatialWorkerFlags->GetWorkerFlag(InFlagName, OutFlagValue); + } + } + } + + return false; +} + +TArray USpatialStatics::GetNCDDistanceRatios() +{ + return GetDefault()->InterestRangeFrequencyPairs; +} + +float USpatialStatics::GetFullFrequencyNetCullDistanceRatio() +{ + return GetDefault()->FullFrequencyNetCullDistanceRatio; +} + +FColor USpatialStatics::GetInspectorColorForWorkerName(const FString& WorkerName) +{ + return SpatialGDK::GetColorForWorkerName(WorkerName); +} + bool USpatialStatics::IsSpatialOffloadingEnabled() { return IsSpatialNetworkingEnabled() && GetDefault()->bEnableOffloading; @@ -54,12 +90,18 @@ bool USpatialStatics::IsActorGroupOwnerForActor(const AActor* Actor) return false; } - return IsActorGroupOwnerForClass(Actor, Actor->GetClass()); + const AActor* EffectiveActor = Actor; + while (EffectiveActor->bUseNetOwnerActorGroup && EffectiveActor->GetOwner() != nullptr) + { + EffectiveActor = EffectiveActor->GetOwner(); + } + + return IsActorGroupOwnerForClass(EffectiveActor, EffectiveActor->GetClass()); } bool USpatialStatics::IsActorGroupOwnerForClass(const UObject* WorldContextObject, const TSubclassOf ActorClass) { - if (UActorGroupManager* ActorGroupManager = GetActorGroupManager(WorldContextObject)) + if (SpatialActorGroupManager* ActorGroupManager = GetActorGroupManager(WorldContextObject)) { const FName ClassWorkerType = ActorGroupManager->GetWorkerTypeForClass(ActorClass); const FName CurrentWorkerType = GetCurrentWorkerType(WorldContextObject); @@ -76,7 +118,7 @@ bool USpatialStatics::IsActorGroupOwnerForClass(const UObject* WorldContextObjec bool USpatialStatics::IsActorGroupOwner(const UObject* WorldContextObject, const FName ActorGroup) { - if (UActorGroupManager* ActorGroupManager = GetActorGroupManager(WorldContextObject)) + if (SpatialActorGroupManager* ActorGroupManager = GetActorGroupManager(WorldContextObject)) { const FName ActorGroupWorkerType = ActorGroupManager->GetWorkerTypeForActorGroup(ActorGroup); const FName CurrentWorkerType = GetCurrentWorkerType(WorldContextObject); @@ -93,10 +135,15 @@ bool USpatialStatics::IsActorGroupOwner(const UObject* WorldContextObject, const FName USpatialStatics::GetActorGroupForActor(const AActor* Actor) { - if (UActorGroupManager* ActorGroupManager = GetActorGroupManager(Actor)) + if (SpatialActorGroupManager* ActorGroupManager = GetActorGroupManager(Actor)) { - UClass* ActorClass = Actor->GetClass(); - return ActorGroupManager->GetActorGroupForClass(ActorClass); + const AActor* EffectiveActor = Actor; + while (EffectiveActor->bUseNetOwnerActorGroup && EffectiveActor->GetOwner() != nullptr) + { + EffectiveActor = EffectiveActor->GetOwner(); + } + + return ActorGroupManager->GetActorGroupForClass(EffectiveActor->GetClass()); } return SpatialConstants::DefaultActorGroup; @@ -104,7 +151,7 @@ FName USpatialStatics::GetActorGroupForActor(const AActor* Actor) FName USpatialStatics::GetActorGroupForClass(const UObject* WorldContextObject, const TSubclassOf ActorClass) { - if (UActorGroupManager* ActorGroupManager = GetActorGroupManager(WorldContextObject)) + if (SpatialActorGroupManager* ActorGroupManager = GetActorGroupManager(WorldContextObject)) { return ActorGroupManager->GetActorGroupForClass(ActorClass); } @@ -125,3 +172,28 @@ void USpatialStatics::PrintTextSpatial(UObject* WorldContextObject, const FText { PrintStringSpatial(WorldContextObject, InText.ToString(), bPrintToScreen, TextColor, Duration); } + +int64 USpatialStatics::GetActorEntityId(const AActor* Actor) +{ + check(Actor); + if (const USpatialNetDriver* SpatialNetDriver = Cast(Actor->GetNetDriver())) + { + return static_cast(SpatialNetDriver->PackageMap->GetEntityIdFromObject(Actor)); + } + return 0; +} + +FString USpatialStatics::EntityIdToString(int64 EntityId) +{ + if (EntityId <= SpatialConstants::INVALID_ENTITY_ID) + { + return FString("Invalid"); + } + + return FString::Printf(TEXT("%lld"), EntityId); +} + +FString USpatialStatics::GetActorEntityIdAsString(const AActor* Actor) +{ + return EntityIdToString(GetActorEntityId(Actor)); +} diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/ActorInterestComponent.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/ActorInterestComponent.h index d18ee7abd0..dbc19e379c 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/ActorInterestComponent.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/ActorInterestComponent.h @@ -5,13 +5,10 @@ #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "Interop/SpatialInterestConstraints.h" +#include "Schema/Interest.h" #include "ActorInterestComponent.generated.h" -namespace SpatialGDK -{ -struct Query; -} class USpatialClassInfoManager; /** @@ -26,7 +23,7 @@ class SPATIALGDK_API UActorInterestComponent final : public UActorComponent UActorInterestComponent() = default; ~UActorInterestComponent() = default; - void CreateQueries(const USpatialClassInfoManager& ClassInfoManager, const SpatialGDK::QueryConstraint& AdditionalConstraints, TArray& OutQueries) const; + void PopulateFrequencyToConstraintsMap(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::FrequencyToConstraintsMap& OutFrequencyToQueryConstraints) const; /** * Whether to use NetCullDistanceSquared to generate constraints relative to the Actor that this component is attached to. diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/SpatialPingComponent.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/SpatialPingComponent.h index 336df6172a..b0b66c258a 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/SpatialPingComponent.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/SpatialPingComponent.h @@ -9,6 +9,32 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialPingComponent, Log, All); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRecordPing, float, Ping); + +USTRUCT(BlueprintType) +struct FSpatialPingAverageData +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadWrite, Category = SpatialPing) + float LastMeasurementsWindowAvg; + UPROPERTY(BlueprintReadWrite, Category = SpatialPing) + float LastMeasurementsWindowMin; + UPROPERTY(BlueprintReadWrite, Category = SpatialPing) + float LastMeasurementsWindowMax; + UPROPERTY(BlueprintReadWrite, Category = SpatialPing) + int WindowSize; + + UPROPERTY(BlueprintReadWrite, Category = SpatialPing) + float TotalAvg; + UPROPERTY(BlueprintReadWrite, Category = SpatialPing) + float TotalMin; + UPROPERTY(BlueprintReadWrite, Category = SpatialPing) + float TotalMax; + UPROPERTY(BlueprintReadWrite, Category = SpatialPing) + int TotalNum; +}; + /* Offers a configurable means of measuring round-trip latency in SpatialOS deployments. This component should be attached to a player controller. @@ -31,6 +57,10 @@ class SPATIALGDK_API USpatialPingComponent : public UActorComponent UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = SpatialPing) float TimeoutLimit = 4.0f; + // The number of ping measurements recorded for the rolling average. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = SpatialPing) + int PingMeasurementsWindowSize = 20; + virtual void BeginPlay() override; virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; @@ -46,6 +76,14 @@ class SPATIALGDK_API USpatialPingComponent : public UActorComponent UFUNCTION(BlueprintCallable, Category = "SpatialGDK|Ping") float GetPing() const; + // Returns the average, min, and max values for the last PingMeasurementsWindowSize measurements as well as the total average, min, and max. + UFUNCTION(BlueprintCallable, Category = "SpatialGDK|Ping") + FSpatialPingAverageData GetAverageData() const; + + // Multicast delegate that will be broadcast whenever a new ping measurement is recorded. + UPROPERTY(BlueprintAssignable, Category = "SpatialGDK|Ping") + FOnRecordPing OnRecordPing; + private: float RoundTripPing; @@ -79,4 +117,13 @@ class SPATIALGDK_API USpatialPingComponent : public UActorComponent UFUNCTION(Server, Unreliable, WithValidation) virtual void SendServerWorkerPingID(uint16 PingID); + + void RecordPing(float Ping); + + TArray LastPingMeasurements; + + float TotalPing = 0.0f; + float TotalMin = 1.0f; + float TotalMax = 0.0f; + int TotalNum = 0; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h index c4f414243b..27a568c797 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h @@ -10,8 +10,11 @@ #include "Interop/SpatialStaticComponentView.h" #include "Runtime/Launch/Resources/Version.h" #include "Schema/StandardLibrary.h" +#include "Schema/RPCPayload.h" #include "SpatialCommonTypes.h" +#include "SpatialGDKSettings.h" #include "Utils/RepDataUtils.h" +#include "Utils/SpatialStatics.h" #include @@ -19,6 +22,92 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialActorChannel, Log, All); +struct FObjectReferences +{ + FObjectReferences() = default; + FObjectReferences(FObjectReferences&& Other) + : MappedRefs(MoveTemp(Other.MappedRefs)) + , UnresolvedRefs(MoveTemp(Other.UnresolvedRefs)) + , bSingleProp(Other.bSingleProp) + , bFastArrayProp(Other.bFastArrayProp) + , Buffer(MoveTemp(Other.Buffer)) + , NumBufferBits(Other.NumBufferBits) + , Array(MoveTemp(Other.Array)) + , ShadowOffset(Other.ShadowOffset) + , ParentIndex(Other.ParentIndex) + , Property(Other.Property) {} + + // Single property constructor + FObjectReferences(const FUnrealObjectRef& InObjectRef, bool bUnresolved, int32 InCmdIndex, int32 InParentIndex, UProperty* InProperty) + : bSingleProp(true), bFastArrayProp(false), ShadowOffset(InCmdIndex), ParentIndex(InParentIndex), Property(InProperty) + { + if (bUnresolved) + { + UnresolvedRefs.Add(InObjectRef); + } + else + { + MappedRefs.Add(InObjectRef); + } + } + + // Struct (memory stream) constructor + FObjectReferences(const TArray& InBuffer, int32 InNumBufferBits, TSet&& InDynamicRefs, TSet&& InUnresolvedRefs, int32 InCmdIndex, int32 InParentIndex, UProperty* InProperty, bool InFastArrayProp = false) + : MappedRefs(MoveTemp(InDynamicRefs)), UnresolvedRefs(MoveTemp(InUnresolvedRefs)), bSingleProp(false), bFastArrayProp(InFastArrayProp), Buffer(InBuffer), NumBufferBits(InNumBufferBits), ShadowOffset(InCmdIndex), ParentIndex(InParentIndex), Property(InProperty) {} + + // Array constructor + FObjectReferences(FObjectReferencesMap* InArray, int32 InCmdIndex, int32 InParentIndex, UProperty* InProperty) + : bSingleProp(false), bFastArrayProp(false), Array(InArray), ShadowOffset(InCmdIndex), ParentIndex(InParentIndex), Property(InProperty) {} + + TSet MappedRefs; + TSet UnresolvedRefs; + + bool bSingleProp; + bool bFastArrayProp; + TArray Buffer; + int32 NumBufferBits; + + TUniquePtr Array; + int32 ShadowOffset; + int32 ParentIndex; + UProperty* Property; +}; + +struct FPendingSubobjectAttachment +{ + USpatialActorChannel* Channel; + const FClassInfo* Info; + TWeakObjectPtr Subobject; + + TSet PendingAuthorityDelegations; +}; + +// Utility class to manage mapped and unresolved references. +// Reproduces what is happening with FRepState::GuidReferencesMap, but with FUnrealObjectRef instead of FNetworkGUID +class FSpatialObjectRepState +{ +public: + + FSpatialObjectRepState(FChannelObjectPair InThisObj) : ThisObj(InThisObj) {} + + void UpdateRefToRepStateMap(FObjectToRepStateMap& ReplicatorMap); + bool MoveMappedObjectToUnmapped(const FUnrealObjectRef& ObjRef); + bool HasUnresolved() const { return UnresolvedRefs.Num() == 0; } + + const FChannelObjectPair& GetChannelObjectPair() const { return ThisObj; } + + FObjectReferencesMap ReferenceMap; + TSet< FUnrealObjectRef > ReferencedObj; + TSet< FUnrealObjectRef > UnresolvedRefs; + +private: + bool MoveMappedObjectToUnmapped_r(const FUnrealObjectRef& ObjRef, FObjectReferencesMap& ObjectReferencesMap); + void GatherObjectRef(TSet& OutReferenced, TSet& OutUnresolved, const FObjectReferences& References) const; + + FChannelObjectPair ThisObj; +}; + + UCLASS(Transient) class SPATIALGDK_API USpatialActorChannel : public UActorChannel { @@ -48,6 +137,12 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel if (EntityId != SpatialConstants::INVALID_ENTITY_ID) { + // If the entity already exists, make sure we have spatial authority before we replicate with Offloading, because we pretend to have local authority + if (USpatialStatics::IsSpatialOffloadingEnabled() && !bCreatingNewEntity && !NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::POSITION_COMPONENT_ID)) + { + return false; + } + return true; } @@ -64,18 +159,30 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel return false; } - return NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID); + return NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::GetClientAuthorityComponent(GetDefault()->UseRPCRingBuffer())); + } + + inline void SetClientAuthority(const bool IsAuth) + { + bIsAuthClient = IsAuth; } + // Indicates whether this client worker has "ownership" (authority over Client endpoint) over the entity corresponding to this channel. - FORCEINLINE bool IsOwnedByWorker() const + inline bool IsAuthoritativeClient() const { - const TArray& WorkerAttributes = NetDriver->Connection->GetWorkerAttributes(); + if (GetDefault()->bEnableResultTypes) + { + return bIsAuthClient; + } + // If we aren't using result types, we have to actually look at the ACL to see if we should be authoritative or not to guess if we are going to receive authority + // in order to send dynamic interest overrides correctly for this client. If we don't do this there's a good chance we will see that there is no server RPC endpoint + // on this entity when we try to send any RPCs immediately after checking out the entity, which can lead to inconsistent state. + const TArray& WorkerAttributes = NetDriver->Connection->GetWorkerAttributes(); if (const SpatialGDK::EntityAcl* EntityACL = NetDriver->StaticComponentView->GetComponentData(EntityId)) { - if (const WorkerRequirementSet* WorkerRequirementsSet = EntityACL->ComponentWriteAcl.Find(SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID)) - { + if (const WorkerRequirementSet* WorkerRequirementsSet = EntityACL->ComponentWriteAcl.Find(SpatialConstants::GetClientAuthorityComponent(GetDefault()->UseRPCRingBuffer()))) { for (const WorkerAttributeSet& AttributeSet : *WorkerRequirementsSet) { for (const FString& Attribute : AttributeSet) @@ -88,13 +195,31 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel } } } - + return false; } - FORCEINLINE bool IsAuthoritativeServer() + // Sets the server and client authorities for this SpatialActorChannel based on the StaticComponentView + inline void RefreshAuthority() { - return NetDriver->IsServer() && NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::POSITION_COMPONENT_ID); + if (NetDriver->IsServer()) + { + SetServerAuthority(NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::POSITION_COMPONENT_ID)); + } + else + { + SetClientAuthority(NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::GetClientAuthorityComponent(GetDefault()->UseRPCRingBuffer()))); + } + } + + inline void SetServerAuthority(const bool IsAuth) + { + bIsAuthServer = IsAuth; + } + + inline bool IsAuthoritativeServer() const + { + return bIsAuthServer; } FORCEINLINE FRepLayout& GetObjectRepLayout(UObject* Object) @@ -118,7 +243,7 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel virtual int64 Close(EChannelCloseReason Reason) override; // End UChannel interface - // Begin UActorChannel inteface + // Begin UActorChannel interface virtual int64 ReplicateActor() override; #if ENGINE_MINOR_VERSION <= 22 virtual void SetChannelActor(AActor* InActor) override; @@ -160,6 +285,11 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel bool IsListening() const; + // Call when a subobject is deleted to unmap its references and cleanup its cached informations. + void OnSubobjectDeleted(const FUnrealObjectRef& ObjectRef, UObject* Object); + + static void ResetShadowData(FRepLayout& RepLayout, FRepStateStaticBuffer& StaticBuffer, UObject* TargetObject); + protected: // Begin UChannel interface virtual bool CleanUp(const bool bForDestroy, EChannelCloseReason CloseReason) override; @@ -169,15 +299,12 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel void DynamicallyAttachSubobject(UObject* Object); void DeleteEntityIfAuthoritative(); - bool IsSingletonEntity(); void SendPositionUpdate(AActor* InActor, Worker_EntityId InEntityId, const FVector& NewPosition); void InitializeHandoverShadowData(TArray& ShadowData, UObject* Object); FHandoverChangeState GetHandoverChangeList(TArray& ShadowData, UObject* Object); - void UpdateEntityACLToNewOwner(); - public: // If this actor channel is responsible for creating a new entity, this will be set to true once the entity creation request is issued. bool bCreatedEntity; @@ -187,14 +314,26 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel TSet> PendingDynamicSubobjects; + TMap, FSpatialObjectRepState> ObjectReferenceMap; + private: Worker_EntityId EntityId; bool bInterestDirty; + bool bIsAuthServer; + bool bIsAuthClient; + // Used on the client to track gaining/losing ownership. bool bNetOwned; - // Used on the server to track when the owner changes. - FString SavedOwnerWorkerAttribute; + + // Used on the server + // Tracks the client worker ID corresponding to the owning connection. + // If no owning client connection exists, this will be an empty string. + FString SavedConnectionOwningWorkerId; + + // Used on the server + // Tracks the interest bucket component ID for the relevant Actor. + Worker_ComponentId SavedInterestBucketComponentID; UPROPERTY(transient) USpatialNetDriver* NetDriver; @@ -210,6 +349,10 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel uint8 FramesTillDormancyAllowed = 0; + // This is incremented in ReplicateActor. It represents how many bytes are sent per call to ReplicateActor. + // ReplicationBytesWritten is reset back to 0 at the start of ReplicateActor. + uint32 ReplicationBytesWritten = 0; + // Shadow data for Handover properties. // For each object with handover properties, we store a blob of memory which contains // the state of those properties at the last time we sent them, and is used to detect diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialGameInstance.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialGameInstance.h index 8c794a1665..2ccf09d741 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialGameInstance.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialGameInstance.h @@ -4,15 +4,19 @@ #include "CoreMinimal.h" #include "Engine/GameInstance.h" +#include "Utils/SpatialActorGroupManager.h" #include "SpatialGameInstance.generated.h" -class USpatialWorkerConnection; +class USpatialLatencyTracer; +class USpatialConnectionManager; +class UGlobalStateManager; +class USpatialStaticComponentView; DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGameInstance, Log, All); -DECLARE_EVENT(USpatialWorkerConnection, FOnConnectedEvent); -DECLARE_EVENT_OneParam(USpatialWorkerConnection, FOnConnectionFailedEvent, const FString&); +DECLARE_EVENT(USpatialGameInstance, FOnConnectedEvent); +DECLARE_EVENT_OneParam(USpatialGameInstance, FOnConnectionFailedEvent, const FString&); UCLASS(config = Engine) class SPATIALGDK_API USpatialGameInstance : public UGameInstance @@ -32,16 +36,20 @@ class SPATIALGDK_API USpatialGameInstance : public UGameInstance virtual bool ProcessConsoleExec(const TCHAR* Cmd, FOutputDevice& Ar, UObject* Executor) override; //~ End UObject Interface - // bResponsibleForSnapshotLoading exists to have persistent knowledge if this worker has authority over the GSM during ServerTravel. - bool bResponsibleForSnapshotLoading = false; + //~ Begin UGameInstance Interface + virtual void Init() override; + //~ End UGameInstance Interface - // The SpatialWorkerConnection must always be owned by the SpatialGameInstance and so must be created here to prevent TrimMemory from deleting it during Browse. - void CreateNewSpatialWorkerConnection(); + // The SpatiaConnectionManager must always be owned by the SpatialGameInstance and so must be created here to prevent TrimMemory from deleting it during Browse. + void CreateNewSpatialConnectionManager(); - // Destroying the SpatialWorkerConnection disconnects us from SpatialOS. - void DestroySpatialWorkerConnection(); + // Destroying the SpatialConnectionManager disconnects us from SpatialOS. + void DestroySpatialConnectionManager(); - FORCEINLINE USpatialWorkerConnection* GetSpatialWorkerConnection() { return SpatialConnection; } + FORCEINLINE USpatialConnectionManager* GetSpatialConnectionManager() { return SpatialConnectionManager; } + FORCEINLINE USpatialLatencyTracer* GetSpatialLatencyTracer() { return SpatialLatencyTracer; } + FORCEINLINE UGlobalStateManager* GetGlobalStateManager() { return GlobalStateManager; }; + FORCEINLINE USpatialStaticComponentView* GetStaticComponentView() { return StaticComponentView; }; void HandleOnConnected(); void HandleOnConnectionFailed(const FString& Reason); @@ -51,18 +59,34 @@ class SPATIALGDK_API USpatialGameInstance : public UGameInstance // Invoked when this worker fails to initiate a connection to SpatialOS FOnConnectionFailedEvent OnConnectionFailed; + void SetFirstConnectionToSpatialOSAttempted() { bFirstConnectionToSpatialOSAttempted = true; }; + bool GetFirstConnectionToSpatialOSAttempted() const { return bFirstConnectionToSpatialOSAttempted; }; + + TUniquePtr ActorGroupManager; + protected: // Checks whether the current net driver is a USpatialNetDriver. // Can be used to decide whether to use Unreal networking or SpatialOS networking. bool HasSpatialNetDriver() const; private: - // SpatialConnection is stored here for persistence between map travels. UPROPERTY() - USpatialWorkerConnection* SpatialConnection; + USpatialConnectionManager* SpatialConnectionManager; + + bool bFirstConnectionToSpatialOSAttempted = false; + + UPROPERTY() + USpatialLatencyTracer* SpatialLatencyTracer = nullptr; + + // GlobalStateManager must persist when server traveling + UPROPERTY() + UGlobalStateManager* GlobalStateManager; + + // StaticComponentView must persist when server traveling + UPROPERTY() + USpatialStaticComponentView* StaticComponentView; - // If this flag is set to true standalone clients will not attempt to connect to a deployment automatically if a 'loginToken' exists in arguments. - UPROPERTY(Config) - bool bPreventAutoConnectWithLocator; + UFUNCTION() + void OnLevelInitializedNetworkActors(ULevel* Level, UWorld* OwningWorld); }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialLoadBalanceEnforcer.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialLoadBalanceEnforcer.h new file mode 100644 index 0000000000..d6335b8186 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialLoadBalanceEnforcer.h @@ -0,0 +1,53 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Interop/SpatialStaticComponentView.h" +#include "SpatialCommonTypes.h" + +#include + +#include "CoreMinimal.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialLoadBalanceEnforcer, Log, All) + +class SpatialVirtualWorkerTranslator; + +class SPATIALGDK_API SpatialLoadBalanceEnforcer +{ +public: + struct AclWriteAuthorityRequest + { + Worker_EntityId EntityId = 0; + PhysicalWorkerName OwningWorkerId; + WorkerRequirementSet ReadAcl; + WorkerRequirementSet ClientRequirementSet; + TArray ComponentIds; + }; + + SpatialLoadBalanceEnforcer(const PhysicalWorkerName& InWorkerId, const USpatialStaticComponentView* InStaticComponentView, const SpatialVirtualWorkerTranslator* InVirtualWorkerTranslator); + + bool HandlesComponent(Worker_ComponentId ComponentId) const; + + void OnLoadBalancingComponentAdded(const Worker_AddComponentOp& Op); + void OnLoadBalancingComponentUpdated(const Worker_ComponentUpdateOp& Op); + void OnLoadBalancingComponentRemoved(const Worker_RemoveComponentOp& Op); + void OnEntityRemoved(const Worker_RemoveEntityOp& Op); + void OnAclAuthorityChanged(const Worker_AuthorityChangeOp& AuthOp); + + void MaybeQueueAclAssignmentRequest(const Worker_EntityId EntityId); + // Visible for testing + bool AclAssignmentRequestIsQueued(const Worker_EntityId EntityId) const; + + TArray ProcessQueuedAclAssignmentRequests(); + +private: + void QueueAclAssignmentRequest(const Worker_EntityId EntityId); + bool CanEnforce(Worker_EntityId EntityId) const; + + const PhysicalWorkerName WorkerId; + TWeakObjectPtr StaticComponentView; + const SpatialVirtualWorkerTranslator* VirtualWorkerTranslator; + + TArray AclWriteAuthAssignmentRequests; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetBitReader.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetBitReader.h index 5edc208c7f..e2e7569d41 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetBitReader.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetBitReader.h @@ -14,7 +14,7 @@ class USpatialPackageMapClient; class SPATIALGDK_API FSpatialNetBitReader : public FNetBitReader { public: - FSpatialNetBitReader(USpatialPackageMapClient* InPackageMap, uint8* Source, int64 CountBits, TSet& InUnresolvedRefs); + FSpatialNetBitReader(USpatialPackageMapClient* InPackageMap, uint8* Source, int64 CountBits, TSet& InDynamicRefs, TSet& InUnresolvedRefs); using FArchive::operator<<; // For visibility of the overloads we don't override @@ -22,8 +22,11 @@ class SPATIALGDK_API FSpatialNetBitReader : public FNetBitReader virtual FArchive& operator<<(struct FWeakObjectPtr& Value) override; + UObject* ReadObject(bool& bUnresolved); + protected: void DeserializeObjectRef(FUnrealObjectRef& ObjectRef); + TSet& DynamicRefs; TSet& UnresolvedRefs; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetConnection.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetConnection.h index 6fa432f881..5af0b43459 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetConnection.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetConnection.h @@ -2,12 +2,13 @@ #pragma once +#include "Schema/Interest.h" + #include "CoreMinimal.h" +#include "Misc/Optional.h" #include "IpConnection.h" #include "Runtime/Launch/Resources/Version.h" -#include "Schema/Interest.h" - #include #include "SpatialNetConnection.generated.h" @@ -30,7 +31,11 @@ class SPATIALGDK_API USpatialNetConnection : public UIpConnection virtual int32 IsNetReady(bool Saturate) override; /** Called by PlayerController to tell connection about client level visibility change */ +#if ENGINE_MINOR_VERSION <= 23 virtual void UpdateLevelVisibility(const FName& PackageName, bool bIsVisible) override; +#else + virtual void UpdateLevelVisibility(const struct FUpdateLevelVisibilityLevelInfo& LevelVisibility) override; +#endif virtual void FlushDormancy(class AActor* Actor) override; @@ -40,6 +45,7 @@ class SPATIALGDK_API USpatialNetConnection : public UIpConnection virtual FString LowLevelGetRemoteAddress(bool bAppendPort = false) override { return TEXT(""); } virtual FString LowLevelDescribe() override { return TEXT(""); } virtual FString RemoteAddressToString() override { return TEXT(""); } + virtual void CleanUp() override; /////// // End NetConnection Interface @@ -50,15 +56,14 @@ class SPATIALGDK_API USpatialNetConnection : public UIpConnection void DisableHeartbeat(); void OnHeartbeat(); - void UpdateActorInterest(AActor* Actor); void ClientNotifyClientHasQuit(); UPROPERTY() bool bReliableSpatialConnection; - UPROPERTY() - FString WorkerAttribute; + // Only used on the server for client connections. + FString ConnectionOwningWorkerId; class FTimerManager* TimerManager; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h index 9b60b5e143..b73acd97d5 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h @@ -2,45 +2,50 @@ #pragma once -#include "CoreMinimal.h" -#include "GameFramework/OnlineReplStructs.h" -#include "IpNetDriver.h" -#include "OnlineSubsystemNames.h" -#include "TimerManager.h" -#include "UObject/CoreOnline.h" - +#include "EngineClasses/SpatialLoadBalanceEnforcer.h" +#include "EngineClasses/SpatialVirtualWorkerTranslationManager.h" #include "EngineClasses/SpatialVirtualWorkerTranslator.h" #include "Interop/Connection/ConnectionConfig.h" +#include "Interop/SpatialDispatcher.h" #include "Interop/SpatialOutputDevice.h" +#include "Interop/SpatialRPCService.h" +#include "Interop/SpatialSnapshotManager.h" +#include "Utils/SpatialActorGroupManager.h" +#include "Utils/InterestFactory.h" + +#include "LoadBalancing/AbstractLockingPolicy.h" #include "SpatialConstants.h" #include "SpatialGDKSettings.h" -#include +#include "CoreMinimal.h" +#include "GameFramework/OnlineReplStructs.h" +#include "IpNetDriver.h" +#include "TimerManager.h" #include "SpatialNetDriver.generated.h" +class ASpatialDebugger; +class ASpatialMetricsDisplay; +class SpatialActorGroupManager; +class UAbstractLBStrategy; +class UEntityPool; +class UGlobalStateManager; class USpatialActorChannel; +class USpatialClassInfoManager; +class USpatialConnectionManager; +class USpatialGameInstance; +class USpatialMetrics; class USpatialNetConnection; class USpatialPackageMapClient; - -class USpatialWorkerConnection; -class USpatialDispatcher; -class USpatialSender; -class USpatialReceiver; -class UActorGroupManager; -class USpatialClassInfoManager; -class UGlobalStateManager; class USpatialPlayerSpawner; +class USpatialReceiver; +class USpatialSender; class USpatialStaticComponentView; -class USnapshotManager; -class USpatialMetrics; -class ASpatialMetricsDisplay; - -class UEntityPool; +class USpatialWorkerConnection; +class USpatialWorkerFlags; DECLARE_LOG_CATEGORY_EXTERN(LogSpatialOSNetDriver, Log, All); -DECLARE_STATS_GROUP(TEXT("SpatialNet"), STATGROUP_SpatialNet, STATCAT_Advanced); DECLARE_DWORD_ACCUMULATOR_STAT_EXTERN(TEXT("Consider List Size"), STAT_SpatialConsiderList, STATGROUP_SpatialNet,); DECLARE_DWORD_ACCUMULATOR_STAT_EXTERN(TEXT("Num Relevant Actors"), STAT_SpatialActorsRelevant, STATGROUP_SpatialNet,); DECLARE_DWORD_ACCUMULATOR_STAT_EXTERN(TEXT("Num Changed Relevant Actors"), STAT_SpatialActorsChanged, STATGROUP_SpatialNet,); @@ -73,15 +78,12 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver virtual void NotifyActorDestroyed(AActor* Actor, bool IsSeamlessTravel = false) override; virtual void Shutdown() override; virtual void NotifyActorFullyDormantForConnection(AActor* Actor, UNetConnection* NetConnection) override; + virtual void OnOwnerUpdated(AActor* Actor, AActor* OldOwner) override; // End UNetDriver interface. - virtual void OnOwnerUpdated(AActor* Actor); - void OnConnectionToSpatialOSSucceeded(); void OnConnectionToSpatialOSFailed(uint8_t ConnectionStatusCode, const FString& ErrorMessage); - void OnConnectedToSpatialOS(); - #if !UE_BUILD_SHIPPING bool HandleNetDumpCrossServerRPCCommand(const TCHAR* Cmd, FOutputDevice& Ar); #endif @@ -93,15 +95,17 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver // You can check if we connected by calling GetSpatialOS()->IsConnected() USpatialNetConnection* GetSpatialOSNetConnection() const; - // When the AcceptingPlayers state on the GSM has changed this method will be called. - void OnAcceptingPlayersChanged(bool bAcceptingPlayers); + // When the AcceptingPlayers/SessionID state on the GSM has changed this method will be called. + void OnGSMQuerySuccess(); + void RetryQueryGSM(); + void GSMQueryDelegateFunction(const Worker_EntityQueryResponseOp& Op); // Used by USpatialSpawner (when new players join the game) and USpatialInteropPipelineBlock (when player controllers are migrated). void AcceptNewPlayer(const FURL& InUrl, const FUniqueNetIdRepl& UniqueId, const FName& OnlinePlatformName); - void PostSpawnPlayerController(APlayerController* PlayerController, const FString& WorkerAttribute); + void PostSpawnPlayerController(APlayerController* PlayerController, const FString& ConnectionOwningWorkerId); void AddActorChannel(Worker_EntityId EntityId, USpatialActorChannel* Channel); - void RemoveActorChannel(Worker_EntityId EntityId); + void RemoveActorChannel(Worker_EntityId EntityId, USpatialActorChannel& Channel); TMap& GetEntityToActorChannelMap(); USpatialActorChannel* GetOrCreateSpatialActorChannel(UObject* TargetObject); @@ -110,27 +114,27 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver void RefreshActorDormancy(AActor* Actor, bool bMakeDormant); void AddPendingDormantChannel(USpatialActorChannel* Channel); + void RemovePendingDormantChannel(USpatialActorChannel* Channel); void RegisterDormantEntityId(Worker_EntityId EntityId); void UnregisterDormantEntityId(Worker_EntityId EntityId); bool IsDormantEntity(Worker_EntityId EntityId) const; - DECLARE_DELEGATE(PostWorldWipeDelegate); - - void WipeWorld(const USpatialNetDriver::PostWorldWipeDelegate& LoadSnapshotAfterWorldWipe); + void WipeWorld(const PostWorldWipeDelegate& LoadSnapshotAfterWorldWipe); void SetSpatialMetricsDisplay(ASpatialMetricsDisplay* InSpatialMetricsDisplay); + void SetSpatialDebugger(ASpatialDebugger* InSpatialDebugger); + TWeakObjectPtr FindClientConnectionFromWorkerId(const FString& WorkerId); + void CleanUpClientConnection(USpatialNetConnection* ClientConnection); UPROPERTY() USpatialWorkerConnection* Connection; UPROPERTY() - USpatialDispatcher* Dispatcher; + USpatialConnectionManager* ConnectionManager; UPROPERTY() USpatialSender* Sender; UPROPERTY() USpatialReceiver* Receiver; UPROPERTY() - UActorGroupManager* ActorGroupManager; - UPROPERTY() USpatialClassInfoManager* ClassInfoManager; UPROPERTY() UGlobalStateManager* GlobalStateManager; @@ -141,17 +145,28 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver UPROPERTY() USpatialStaticComponentView* StaticComponentView; UPROPERTY() - USnapshotManager* SnapshotManager; - UPROPERTY() USpatialMetrics* SpatialMetrics; UPROPERTY() ASpatialMetricsDisplay* SpatialMetricsDisplay; + UPROPERTY() + ASpatialDebugger* SpatialDebugger; + UPROPERTY() + UAbstractLBStrategy* LoadBalanceStrategy; + UPROPERTY() + UAbstractLockingPolicy* LockingPolicy; + UPROPERTY() + USpatialWorkerFlags* SpatialWorkerFlags; - Worker_EntityId WorkerEntityId = SpatialConstants::INVALID_ENTITY_ID; - + SpatialActorGroupManager* ActorGroupManager; + TUniquePtr InterestFactory; + TUniquePtr LoadBalanceEnforcer; TUniquePtr VirtualWorkerTranslator; - TMap> SingletonActorChannels; + Worker_EntityId WorkerEntityId = SpatialConstants::INVALID_ENTITY_ID; + + // If this worker is authoritative over the translation, the manager will be instantiated. + TUniquePtr VirtualWorkerTranslationManager; + void InitializeVirtualWorkerTranslationManager(); bool IsAuthoritativeDestructionAllowed() const { return bAuthoritativeDestruction; } void StartIgnoringAuthoritativeDestruction() { bAuthoritativeDestruction = false; } @@ -161,24 +176,6 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver int32 GetConsiderListSize() const { return ConsiderListSize; } #endif - uint32 GetNextReliableRPCId(AActor* Actor, ESchemaComponentType RPCType, UObject* TargetObject); - void OnReceivedReliableRPC(AActor* Actor, ESchemaComponentType RPCType, FString WorkerId, uint32 RPCId, UObject* TargetObject, UFunction* Function); - void OnRPCAuthorityGained(AActor* Actor, ESchemaComponentType RPCType); - - struct FReliableRPCId - { - FReliableRPCId() = default; - FReliableRPCId(FString InWorkerId, uint32 InRPCId, FString InRPCTarget, FString InRPCName) : WorkerId(InWorkerId), RPCId(InRPCId), LastRPCTarget(InRPCTarget), LastRPCName(InRPCName) {} - - FString WorkerId; - uint32 RPCId = 0; - FString LastRPCTarget; - FString LastRPCName; - }; - using FRPCTypeToReliableRPCIdMap = TMap; - // Per actor, maps from RPC type to the reliable RPC index used to detect if reliable RPCs go out of order. - TMap, FRPCTypeToReliableRPCIdMap> ReliableRPCIdMap; - void DelayedSendDeleteEntityRequest(Worker_EntityId EntityId, float Delay); #if WITH_EDITOR @@ -189,38 +186,48 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver #endif private: + + TUniquePtr Dispatcher; + TUniquePtr SnapshotManager; TUniquePtr SpatialOutputDevice; + TUniquePtr RPCService; + TMap EntityToActorChannel; TArray QueuedStartupOpLists; TSet DormantEntities; TSet> PendingDormantChannels; + TMap> WorkerConnections; + FTimerManager TimerManager; bool bAuthoritativeDestruction; bool bConnectAsClient; bool bPersistSpatialConnection; - bool bWaitingForAcceptingPlayersToSpawn; + bool bWaitingToSpawn; bool bIsReadyToStart; bool bMapLoaded; FString SnapshotToLoad; + // Client variable which stores the SessionId given to us by the server in the URL options. + // Used to compare against the GSM SessionId to ensure the the server is ready to spawn players. + int32 SessionId; + class USpatialGameInstance* GetGameInstance() const; void InitiateConnectionToSpatialOS(const FURL& URL); void InitializeSpatialOutputDevice(); void CreateAndInitializeCoreClasses(); + void CreateAndInitializeLoadBalancingClasses(); void CreateServerSpatialOSNetConnection(); USpatialActorChannel* CreateSpatialActorChannel(AActor* Actor); void QueryGSMToLoadMap(); - void HandleOngoingServerTravel(); - void HandleStartupOpQueueing(const TArray& InOpLists); bool FindAndDispatchStartupOpsServer(const TArray& InOpLists); bool FindAndDispatchStartupOpsClient(const TArray& InOpLists); @@ -232,6 +239,8 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver UFUNCTION() void OnLevelAddedToWorld(ULevel* LoadedLevel, UWorld* OwningWorld); + void OnActorSpawned(AActor* Actor); + static void SpatialProcessServerTravel(const FString& URL, bool bAbsolute, AGameModeBase* GameMode); #if WITH_SERVER_CODE @@ -246,9 +255,7 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver bool CreateSpatialNetConnection(const FURL& InUrl, const FUniqueNetIdRepl& UniqueId, const FName& OnlinePlatformName, USpatialNetConnection** OutConn); void ProcessPendingDormancy(); - - friend USpatialNetConnection; - friend USpatialWorkerConnection; + void PollPendingLoads(); // This index is incremented and assigned to every new RPC in ProcessRemoteFunction. // The SpatialSender uses these indexes to retry any failed reliable RPCs @@ -272,4 +279,12 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver static const int32 EDITOR_TOMBSTONED_ENTITY_TRACKING_RESERVATION_COUNT = 256; TArray TombstonedEntities; #endif + + void MakePlayerSpawnRequest(); + + FUnrealObjectRef GetCurrentPlayerControllerRef(); + + // Checks the GSM is acceptingPlayers and that the SessionId on the GSM matches the SessionId on the net-driver. + // The SessionId on the net-driver is set by looking at the sessionId option in the URL sent to the client for ServerTravel. + bool ClientCanSendPlayerSpawnRequests(); }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h index 70b1265c28..48de674d43 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h @@ -31,7 +31,7 @@ class SPATIALGDK_API USpatialPackageMapClient : public UPackageMapClient bool IsEntityIdPendingCreation(Worker_EntityId EntityId) const; void RemovePendingCreationEntityId(Worker_EntityId EntityId); - FNetworkGUID ResolveEntityActor(AActor* Actor, Worker_EntityId EntityId); + bool ResolveEntityActor(AActor* Actor, Worker_EntityId EntityId); void ResolveSubobject(UObject* Object, const FUnrealObjectRef& ObjectRef); void RemoveEntityActor(Worker_EntityId EntityId); @@ -49,7 +49,7 @@ class SPATIALGDK_API USpatialPackageMapClient : public UPackageMapClient TWeakObjectPtr GetObjectFromUnrealObjectRef(const FUnrealObjectRef& ObjectRef); TWeakObjectPtr GetObjectFromEntityId(const Worker_EntityId& EntityId); - FUnrealObjectRef GetUnrealObjectRefFromObject(UObject* Object); + FUnrealObjectRef GetUnrealObjectRefFromObject(const UObject* Object); Worker_EntityId GetEntityIdFromObject(const UObject* Object); AActor* GetSingletonByClassRef(const FUnrealObjectRef& SingletonClassRef); @@ -63,6 +63,9 @@ class SPATIALGDK_API USpatialPackageMapClient : public UPackageMapClient const FClassInfo* TryResolveNewDynamicSubobjectAndGetClassInfo(UObject* Object); + // Pending object references, being asynchronously loaded. + TSet PendingReferences; + private: UPROPERTY() UEntityPool* EntityPool; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialReplicationGraph.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialReplicationGraph.h new file mode 100644 index 0000000000..86bb6fb63e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialReplicationGraph.h @@ -0,0 +1,23 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "ReplicationGraph.h" + +#include "SpatialReplicationGraph.generated.h" + +class UActorChannel; +class UObject; + +UCLASS(Transient) +class SPATIALGDK_API USpatialReplicationGraph : public UReplicationGraph +{ + GENERATED_BODY() + +public: + + //~ Begin UReplicationGraph Interface + virtual UActorChannel* GetOrCreateSpatialActorChannel(UObject* TargetObject) override; + //~ End UReplicationGraph Interface + +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslationManager.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslationManager.h new file mode 100644 index 0000000000..8bc70fa516 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslationManager.h @@ -0,0 +1,70 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Containers/Queue.h" +#include "SpatialCommonTypes.h" +#include "SpatialConstants.h" + +#include +#include + +#include "CoreMinimal.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialVirtualWorkerTranslationManager, Log, All) + +class SpatialVirtualWorkerTranslator; +class SpatialOSDispatcherInterface; +class SpatialOSWorkerInterface; + +// +// The Translation Manager is responsible for querying SpatialOS for all UnrealWorker worker +// entities and querying the LBStrategy for the number of Virtual Workers needed for load +// balancing. Then the Translation manager creates a mapping from physical worker name +// to virtual worker ID, and writes that to the single Translation entity. +// +// One UnrealWorker is arbitrarily chosen by SpatialOS to be authoritative for the Translation +// entity. This class will execute on that worker and will be idle on all other workers. +// +// This class is currently implemented in the UnrealWorker, but none of the logic must be in +// Unreal. It could be moved to an independent worker in the future in cloud deployments. It +// lives here now for convenience and for fast iteration on local deployments. +// + +class SPATIALGDK_API SpatialVirtualWorkerTranslationManager +{ +public: + SpatialVirtualWorkerTranslationManager(SpatialOSDispatcherInterface* InReceiver, + SpatialOSWorkerInterface* InConnection, + SpatialVirtualWorkerTranslator* InTranslator); + + void AddVirtualWorkerIds(const TSet& InVirtualWorkerIds); + + // The translation manager only cares about changes to the authority of the translation mapping. + void AuthorityChanged(const Worker_AuthorityChangeOp& AuthChangeOp); + +private: + SpatialOSDispatcherInterface* Receiver; + SpatialOSWorkerInterface* Connection; + + SpatialVirtualWorkerTranslator* Translator; + + TMap> VirtualToPhysicalWorkerMapping; + TMap PhysicalToVirtualWorkerMapping; + TQueue UnassignedVirtualWorkers; + + bool bWorkerEntityQueryInFlight; + + // Serialization and deserialization of the mapping. + void WriteMappingToSchema(Schema_Object* Object) const; + + // The following methods are used to query the Runtime for all worker entities and update the mapping + // based on the response. + void QueryForServerWorkerEntities(); + void ServerWorkerEntityQueryDelegate(const Worker_EntityQueryResponseOp& Op); + void ConstructVirtualWorkerMappingFromQueryResponse(const Worker_EntityQueryResponseOp& Op); + void SendVirtualWorkerMappingUpdate(); + + void AssignWorker(const PhysicalWorkerName& WorkerId, const Worker_EntityId& ServerWorkerEntityId); +}; + diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslator.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslator.h index d94c82b773..b07f1f2d4d 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslator.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslator.h @@ -2,71 +2,57 @@ #pragma once -#include "CoreMinimal.h" +#include "SpatialCommonTypes.h" +#include "SpatialConstants.h" #include #include -DECLARE_LOG_CATEGORY_EXTERN(LogSpatialVirtualWorkerTranslator, Log, All) +#include "Containers/Queue.h" +#include "CoreMinimal.h" -class USpatialNetDriver; +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialVirtualWorkerTranslator, Log, All) -typedef uint32 VirtualWorkerId; -typedef FString PhysicalWorkerName; +class UAbstractLBStrategy; class SPATIALGDK_API SpatialVirtualWorkerTranslator { public: - SpatialVirtualWorkerTranslator(); - - void Init(USpatialNetDriver* InNetDriver); + SpatialVirtualWorkerTranslator() = delete; + SpatialVirtualWorkerTranslator(UAbstractLBStrategy* InLoadBalanceStrategy, + PhysicalWorkerName InPhysicalWorkerName); // Returns true if the Translator has received the information needed to map virtual workers to physical workers. // Currently that is only the number of virtual workers desired. bool IsReady() const { return bIsReady; } - void SetDesiredVirtualWorkerCount(uint32 NumberOfVirtualWorkers); + VirtualWorkerId GetLocalVirtualWorkerId() const { return LocalVirtualWorkerId; } + PhysicalWorkerName GetLocalPhysicalWorkerName() const { return LocalPhysicalWorkerName; } // Returns the name of the worker currently assigned to VirtualWorkerId id or nullptr if there is // no worker assigned. // TODO(harkness): Do we want to copy this data? Otherwise it's only guaranteed to be valid until // the next mapping update. - const FString* GetPhysicalWorkerForVirtualWorker(VirtualWorkerId id); + const PhysicalWorkerName* GetPhysicalWorkerForVirtualWorker(VirtualWorkerId Id) const; + Worker_EntityId GetServerWorkerEntityForVirtualWorker(VirtualWorkerId Id) const; // On receiving a version of the translation state, apply that to the internal mapping. void ApplyVirtualWorkerManagerData(Schema_Object* ComponentObject); - void OnComponentUpdated(const Worker_ComponentUpdateOp& Op); - - // Authority may change on one of two components we care about: - // 1) The translation component, in which case this worker is now authoritative on the virtual to physical worker translation. - // 2) The ACL component for some entity, in which case this worker is now authoritative for the entity and will be - // responsible for updating the ACL in the future if this worker loses authority. - void AuthorityChanged(const Worker_AuthorityChangeOp& AuthChangeOp); - private: - USpatialNetDriver* NetDriver; + TWeakObjectPtr LoadBalanceStrategy; - TMap VirtualToPhysicalWorkerMapping; - TQueue UnassignedVirtualWorkers; - - bool bWorkerEntityQueryInFlight; + TMap> VirtualToPhysicalWorkerMapping; bool bIsReady; // The WorkerId of this worker, for logging purposes. - FString WorkerId; + PhysicalWorkerName LocalPhysicalWorkerName; + VirtualWorkerId LocalVirtualWorkerId; // Serialization and deserialization of the mapping. void ApplyMappingFromSchema(Schema_Object* Object); - void WriteMappingToSchema(Schema_Object* Object); - - // The following methods are used to query the Runtime for all worker entities and update the mapping - // based on the response. - void QueryForWorkerEntities(); - void WorkerEntityQueryDelegate(const Worker_EntityQueryResponseOp& Op); - void ConstructVirtualWorkerMappingFromQueryResponse(const Worker_EntityQueryResponseOp& Op); - void SendVirtualWorkerMappingUpdate(); + bool IsValidMapping(Schema_Object* Object) const; - void AssignWorker(const FString& WorkerId); + void UpdateMapping(VirtualWorkerId Id, PhysicalWorkerName Name, Worker_EntityId ServerWorkerEntityId); }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialWorldSettings.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialWorldSettings.h new file mode 100644 index 0000000000..58340fed2c --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialWorldSettings.h @@ -0,0 +1,25 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/WorldSettings.h" + +#include "SpatialWorldSettings.generated.h" + +class UAbstractLBStrategy; +class UAbstractLockingPolicy; + +UCLASS() +class SPATIALGDK_API ASpatialWorldSettings : public AWorldSettings +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, Config, Category = "Load Balancing") + TSubclassOf LoadBalanceStrategy; + + UPROPERTY(EditAnywhere, Config, Category = "Load Balancing") + TSubclassOf LockingPolicy; + +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/ConnectionConfig.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/ConnectionConfig.h index 8d17d19a6b..8ce0da4e6b 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/ConnectionConfig.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/ConnectionConfig.h @@ -18,6 +18,10 @@ struct FConnectionConfig , EnableProtocolLoggingAtStartup(false) , LinkProtocol(WORKER_NETWORK_CONNECTION_TYPE_MODULAR_KCP) , TcpMultiplexLevel(2) // This is a "finger-in-the-air" number. + // These settings will be overridden by Spatial GDK settings before connection applied (see PreConnectInit) + , TcpNoDelay(0) + , UdpUpstreamIntervalMS(0) + , UdpDownstreamIntervalMS(0) { const TCHAR* CommandLine = FCommandLine::Get(); @@ -26,11 +30,6 @@ struct FConnectionConfig FParse::Bool(CommandLine, TEXT("enableProtocolLogging"), EnableProtocolLoggingAtStartup); FParse::Value(CommandLine, TEXT("protocolLoggingPrefix"), ProtocolLoggingPrefix); -#if PLATFORM_IOS || PLATFORM_ANDROID - // On a mobile platform, you can only be a client worker, and therefore use the external IP. - WorkerType = SpatialConstants::DefaultClientWorkerType.ToString(); - UseExternalIp = true; -#endif FString LinkProtocolString; FParse::Value(CommandLine, TEXT("linkProtocol"), LinkProtocolString); if (LinkProtocolString == TEXT("Tcp")) @@ -47,6 +46,27 @@ struct FConnectionConfig } } + void PreConnectInit(const bool bConnectAsClient) + { + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + + if (WorkerType.IsEmpty()) + { + WorkerType = bConnectAsClient ? SpatialConstants::DefaultClientWorkerType.ToString() : SpatialConstants::DefaultServerWorkerType.ToString(); + UE_LOG(LogTemp, Warning, TEXT("No worker type specified through commandline, defaulting to %s"), *WorkerType); + } + + if (WorkerId.IsEmpty()) + { + WorkerId = WorkerType + FGuid::NewGuid().ToString(); + } + + TcpNoDelay = (SpatialGDKSettings->bTcpNoDelay ? 1 : 0); + + UdpUpstreamIntervalMS = (bConnectAsClient ? SpatialGDKSettings->UdpClientUpstreamUpdateIntervalMS : SpatialGDKSettings->UdpServerUpstreamUpdateIntervalMS); + UdpDownstreamIntervalMS = (bConnectAsClient ? SpatialGDKSettings->UdpClientDownstreamUpdateIntervalMS : SpatialGDKSettings->UdpServerDownstreamUpdateIntervalMS); + } + FString WorkerId; FString WorkerType; bool UseExternalIp; @@ -55,64 +75,143 @@ struct FConnectionConfig Worker_NetworkConnectionType LinkProtocol; Worker_ConnectionParameters ConnectionParams; uint8 TcpMultiplexLevel; + uint8 TcpNoDelay; + uint8 UdpUpstreamIntervalMS; + uint8 UdpDownstreamIntervalMS; +}; + +class FLocatorConfig : public FConnectionConfig +{ +public: + FLocatorConfig() + { + LoadDefaults(); + } + + void LoadDefaults() + { + UseExternalIp = true; + + if (GetDefault()->IsRunningInChina()) + { + LocatorHost = SpatialConstants::LOCATOR_HOST_CN; + } + else + { + LocatorHost = SpatialConstants::LOCATOR_HOST; + } + } + + bool TryLoadCommandLineArgs() + { + bool bSuccess = true; + const TCHAR* CommandLine = FCommandLine::Get(); + FParse::Value(CommandLine, TEXT("locatorHost"), LocatorHost); + bSuccess &= FParse::Value(CommandLine, TEXT("playerIdentityToken"), PlayerIdentityToken); + bSuccess &= FParse::Value(CommandLine, TEXT("loginToken"), LoginToken); + return bSuccess; + } + + FString LocatorHost; + FString PlayerIdentityToken; + FString LoginToken; +}; + +class FDevAuthConfig : public FLocatorConfig +{ +public: + FDevAuthConfig() + { + LoadDefaults(); + } + + void LoadDefaults() + { + UseExternalIp = true; + PlayerId = SpatialConstants::DEVELOPMENT_AUTH_PLAYER_ID; + + if (GetDefault()->IsRunningInChina()) + { + LocatorHost = SpatialConstants::LOCATOR_HOST_CN; + } + else + { + LocatorHost = SpatialConstants::LOCATOR_HOST; + } + } + + bool TryLoadCommandLineArgs() + { + bool bSuccess = true; + const TCHAR* CommandLine = FCommandLine::Get(); + FParse::Value(CommandLine, TEXT("locatorHost"), LocatorHost); + FParse::Value(CommandLine, TEXT("deployment"), Deployment); + FParse::Value(CommandLine, TEXT("playerId"), PlayerId); + FParse::Value(CommandLine, TEXT("displayName"), DisplayName); + FParse::Value(CommandLine, TEXT("metaData"), MetaData); + bSuccess = FParse::Value(CommandLine, TEXT("devAuthToken"), DevelopmentAuthToken); + return bSuccess; + } + + FString DevelopmentAuthToken; + FString Deployment; + FString PlayerId; + FString DisplayName; + FString MetaData; }; -struct FReceptionistConfig : public FConnectionConfig +class FReceptionistConfig : public FConnectionConfig { +public: FReceptionistConfig() - : ReceptionistPort(SpatialConstants::DEFAULT_PORT) { + LoadDefaults(); + } + + void LoadDefaults() + { + ReceptionistPort = SpatialConstants::DEFAULT_PORT; + SetReceptionistHost(GetDefault()->DefaultReceptionistHost); + } + + bool TryLoadCommandLineArgs() + { + bool bSuccess = true; const TCHAR* CommandLine = FCommandLine::Get(); - // Parse the commandline for receptionistHost, if it exists then use this as the host IP. + // Parse the command line for receptionistHost, if it exists then use this as the host IP. if (!FParse::Value(CommandLine, TEXT("receptionistHost"), ReceptionistHost)) { // If a receptionistHost is not specified then parse for an IP address as the first argument and use this instead. // This is how native Unreal handles connecting to other IPs, a map name can also be specified, in this case we use the default IP. - FParse::Token(CommandLine, ReceptionistHost, 0); - + FString URLAddress; + FParse::Token(CommandLine, URLAddress, 0); FRegexPattern Ipv4RegexPattern(TEXT("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$")); - - FRegexMatcher IpV4RegexMatcher(Ipv4RegexPattern, *ReceptionistHost); - if (!IpV4RegexMatcher.FindNext()) + FRegexMatcher IpV4RegexMatcher(Ipv4RegexPattern, *URLAddress); + bSuccess = IpV4RegexMatcher.FindNext(); + if (bSuccess) { - // If an IP is not specified then use default. - ReceptionistHost = GetDefault()->DefaultReceptionistHost; - if (ReceptionistHost.Compare(SpatialConstants::LOCAL_HOST) != 0) - { - UseExternalIp = true; - } + SetReceptionistHost(URLAddress); } } FParse::Value(CommandLine, TEXT("receptionistPort"), ReceptionistPort); + return bSuccess; } - FString ReceptionistHost; - uint16 ReceptionistPort; -}; - -struct FLocatorConfig : public FConnectionConfig -{ - FLocatorConfig() + void SetReceptionistHost(const FString& host) { - const TCHAR* CommandLine = FCommandLine::Get(); - if (!FParse::Value(CommandLine, TEXT("locatorHost"), LocatorHost)) + ReceptionistHost = host; + if (ReceptionistHost.Compare(SpatialConstants::LOCAL_HOST) != 0) { - if (GetDefault()->IsRunningInChina()) - { - LocatorHost = SpatialConstants::LOCATOR_HOST_CN; - } - else - { - LocatorHost = SpatialConstants::LOCATOR_HOST; - } + UseExternalIp = true; } - FParse::Value(CommandLine, TEXT("playerIdentityToken"), PlayerIdentityToken); - FParse::Value(CommandLine, TEXT("loginToken"), LoginToken); } - FString LocatorHost; - FString PlayerIdentityToken; - FString LoginToken; + FString GetReceptionistHost() const { return ReceptionistHost; } + + uint16 ReceptionistPort; + +private: + FString ReceptionistHost; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/OutgoingMessages.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/OutgoingMessages.h index e07c319fc3..f28091041d 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/OutgoingMessages.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/OutgoingMessages.h @@ -1,4 +1,5 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved + #pragma once #include "Containers/Array.h" @@ -8,6 +9,7 @@ #include "Templates/UnrealTemplate.h" #include "Templates/UniquePtr.h" #include "UObject/NameTypes.h" +#include "Utils/SpatialLatencyTracer.h" #include @@ -53,13 +55,13 @@ struct FReserveEntityIdsRequest : FOutgoingMessage struct FCreateEntityRequest : FOutgoingMessage { - FCreateEntityRequest(TArray&& InComponents, const Worker_EntityId* InEntityId) + FCreateEntityRequest(TArray&& InComponents, const Worker_EntityId* InEntityId) : FOutgoingMessage(EOutgoingMessageType::CreateEntityRequest) , Components(MoveTemp(InComponents)) , EntityId(InEntityId != nullptr ? *InEntityId : TOptional()) {} - TArray Components; + TArray Components; TOptional EntityId; }; @@ -75,14 +77,14 @@ struct FDeleteEntityRequest : FOutgoingMessage struct FAddComponent : FOutgoingMessage { - FAddComponent(Worker_EntityId InEntityId, const Worker_ComponentData& InData) + FAddComponent(Worker_EntityId InEntityId, const FWorkerComponentData& InData) : FOutgoingMessage(EOutgoingMessageType::AddComponent) , EntityId(InEntityId) , Data(InData) {} Worker_EntityId EntityId; - Worker_ComponentData Data; + FWorkerComponentData Data; }; struct FRemoveComponent : FOutgoingMessage @@ -99,14 +101,14 @@ struct FRemoveComponent : FOutgoingMessage struct FComponentUpdate : FOutgoingMessage { - FComponentUpdate(Worker_EntityId InEntityId, const Worker_ComponentUpdate& InComponentUpdate) + FComponentUpdate(Worker_EntityId InEntityId, const FWorkerComponentUpdate& InComponentUpdate) : FOutgoingMessage(EOutgoingMessageType::ComponentUpdate) , EntityId(InEntityId) , Update(InComponentUpdate) {} Worker_EntityId EntityId; - Worker_ComponentUpdate Update; + FWorkerComponentUpdate Update; }; struct FCommandRequest : FOutgoingMessage diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialConnectionManager.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialConnectionManager.h new file mode 100644 index 0000000000..ca7d790d86 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialConnectionManager.h @@ -0,0 +1,90 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Interop/Connection/SpatialOSWorkerInterface.h" +#include "Interop/Connection/ConnectionConfig.h" +#include "SpatialCommonTypes.h" +#include "SpatialGDKSettings.h" + +#include "SpatialConnectionManager.generated.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialConnectionManager, Log, All); + +class USpatialWorkerConnection; + +enum class ESpatialConnectionType +{ + Receptionist, + LegacyLocator, + Locator, + DevAuthFlow +}; + +UCLASS() +class SPATIALGDK_API USpatialConnectionManager : public UObject +{ + GENERATED_BODY() + +public: + virtual void FinishDestroy() override; + void DestroyConnection(); + + using LoginTokenResponseCallback = TFunction; + + /// Register a callback using this function. + /// It will be triggered when receiving login tokens using the development authentication flow inside SpatialWorkerConnection. + /// @param Callback - callback function. + void RegisterOnLoginTokensCallback(const LoginTokenResponseCallback& Callback) {LoginTokenResCallback = Callback;} + + void Connect(bool bConnectAsClient, uint32 PlayInEditorID); + + FORCEINLINE bool IsConnected() { return bIsConnected; } + + void SetConnectionType(ESpatialConnectionType InConnectionType); + + // TODO: UNR-2753 + FReceptionistConfig ReceptionistConfig; + FLocatorConfig LocatorConfig; + FDevAuthConfig DevAuthConfig; + + DECLARE_DELEGATE(OnConnectionToSpatialOSSucceededDelegate) + OnConnectionToSpatialOSSucceededDelegate OnConnectedCallback; + + DECLARE_DELEGATE_TwoParams(OnConnectionToSpatialOSFailedDelegate, uint8_t, const FString&); + OnConnectionToSpatialOSFailedDelegate OnFailedToConnectCallback; + + bool TrySetupConnectionConfigFromCommandLine(const FString& SpatialWorkerType); + void SetupConnectionConfigFromURL(const FURL& URL, const FString& SpatialWorkerType); + + USpatialWorkerConnection* GetWorkerConnection() { return WorkerConnection; } + + void RequestDeploymentLoginTokens(); + +private: + void ConnectToReceptionist(uint32 PlayInEditorID); + void ConnectToLocator(FLocatorConfig* InLocatorConfig); + void FinishConnecting(Worker_ConnectionFuture* ConnectionFuture); + + void OnConnectionSuccess(); + void OnConnectionFailure(uint8_t ConnectionStatusCode, const FString& ErrorMessage); + + ESpatialConnectionType GetConnectionType() const; + + void StartDevelopmentAuth(const FString& DevAuthToken); + static void OnPlayerIdentityToken(void* UserData, const Worker_Alpha_PlayerIdentityTokenResponse* PIToken); + static void OnLoginTokens(void* UserData, const Worker_Alpha_LoginTokensResponse* LoginTokens); + void ProcessLoginTokensResponse(const Worker_Alpha_LoginTokensResponse* LoginTokens); + +private: + UPROPERTY() + USpatialWorkerConnection* WorkerConnection; + + Worker_Locator* WorkerLocator; + + bool bIsConnected; + bool bConnectAsClient = false; + + ESpatialConnectionType ConnectionType = ESpatialConnectionType::Receptionist; + LoginTokenResponseCallback LoginTokenResCallback; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialOSWorkerInterface.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialOSWorkerInterface.h new file mode 100644 index 0000000000..bed9a32e79 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialOSWorkerInterface.h @@ -0,0 +1,33 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Interop/Connection/OutgoingMessages.h" +#include "SpatialCommonTypes.h" +#include "Utils/SpatialLatencyTracer.h" + +#include +#include + +class SPATIALGDK_API SpatialOSWorkerInterface +{ +public: +// FORCEINLINE bool IsConnected() { return bIsConnected; } + + // Worker Connection Interface + virtual TArray GetOpList() PURE_VIRTUAL(AbstractSpatialWorkerConnection::GetOpList, return TArray();); + virtual Worker_RequestId SendReserveEntityIdsRequest(uint32_t NumOfEntities) PURE_VIRTUAL(AbstractSpatialWorkerConnection::SendReserveEntityIdsRequest, return 0;); + virtual Worker_RequestId SendCreateEntityRequest(TArray&& Components, const Worker_EntityId* EntityId) PURE_VIRTUAL(AbstractSpatialWorkerConnection::SendCreateEntityRequest, return 0;); + virtual Worker_RequestId SendDeleteEntityRequest(Worker_EntityId EntityId) PURE_VIRTUAL(AbstractSpatialWorkerConnection::SendDeleteEntityRequest, return 0;); + virtual void SendAddComponent(Worker_EntityId EntityId, FWorkerComponentData* ComponentData) PURE_VIRTUAL(AbstractSpatialWorkerConnection::SendAddComponent, return;); + virtual void SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) PURE_VIRTUAL(AbstractSpatialWorkerConnection::SendRemoveComponent, return;); + virtual void SendComponentUpdate(Worker_EntityId EntityId, const FWorkerComponentUpdate* ComponentUpdate) PURE_VIRTUAL(AbstractSpatialWorkerConnection::SendComponentUpdate, return;); + virtual Worker_RequestId SendCommandRequest(Worker_EntityId EntityId, const Worker_CommandRequest* Request, uint32_t CommandId) PURE_VIRTUAL(AbstractSpatialWorkerConnection::SendCommandRequest, return 0;); + virtual void SendCommandResponse(Worker_RequestId RequestId, const Worker_CommandResponse* Response) PURE_VIRTUAL(AbstractSpatialWorkerConnection::SendCommandResponse, return;); + virtual void SendCommandFailure(Worker_RequestId RequestId, const FString& Message) PURE_VIRTUAL(AbstractSpatialWorkerConnection::SendCommandFailure, return;); + virtual void SendLogMessage(uint8_t Level, const FName& LoggerName, const TCHAR* Message) PURE_VIRTUAL(AbstractSpatialWorkerConnection::SendLogMessage, return;); + virtual void SendComponentInterest(Worker_EntityId EntityId, TArray&& ComponentInterest) PURE_VIRTUAL(AbstractSpatialWorkerConnection::SendEntityQueryRequest, return;); + virtual Worker_RequestId SendEntityQueryRequest(const Worker_EntityQuery* EntityQuery) PURE_VIRTUAL(AbstractSpatialWorkerConnection::SendEntityQueryRequest, return 0;); + virtual void SendMetrics(const SpatialGDK::SpatialMetrics& Metrics) PURE_VIRTUAL(AbstractSpatialWorkerConnection::SendMetrics, return;); +}; + diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h index 6eaf805eea..9fdef3e471 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h @@ -1,13 +1,14 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved + #pragma once #include "Containers/Queue.h" #include "HAL/Runnable.h" #include "HAL/ThreadSafeBool.h" -#include "Interop/Connection/ConnectionConfig.h" +#include "Interop/Connection/SpatialOSWorkerInterface.h" #include "Interop/Connection/OutgoingMessages.h" -#include "SpatialGDKSettings.h" +#include "SpatialCommonTypes.h" #include "UObject/WeakObjectPtr.h" #include @@ -17,79 +18,45 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialWorkerConnection, Log, All); -class USpatialGameInstance; -class UWorld; - -enum class SpatialConnectionType -{ - Receptionist, - LegacyLocator, - Locator -}; - UCLASS() -class SPATIALGDK_API USpatialWorkerConnection : public UObject, public FRunnable +class SPATIALGDK_API USpatialWorkerConnection : public UObject, public FRunnable, public SpatialOSWorkerInterface { GENERATED_BODY() public: - void Init(USpatialGameInstance* InGameInstance); - + void SetConnection(Worker_Connection* WorkerConnectionIn); virtual void FinishDestroy() override; void DestroyConnection(); - using LoginTokenResponseCallback = TFunction; - - /// Register a callback using this function. - /// It will be triggered when receiving login tokens using the development authentication flow inside SpatialWorkerConnection. - /// @param Callback - callback function. - void RegisterOnLoginTokensCallback(const LoginTokenResponseCallback& Callback) {LoginTokenResCallback = Callback;} - - void Connect(bool bConnectAsClient, uint32 PlayInEditorID); - - FORCEINLINE bool IsConnected() { return bIsConnected; } - // Worker Connection Interface - TArray GetOpList(); - Worker_RequestId SendReserveEntityIdsRequest(uint32_t NumOfEntities); - Worker_RequestId SendCreateEntityRequest(TArray&& Components, const Worker_EntityId* EntityId); - Worker_RequestId SendDeleteEntityRequest(Worker_EntityId EntityId); - void SendAddComponent(Worker_EntityId EntityId, Worker_ComponentData* ComponentData); - void SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId); - void SendComponentUpdate(Worker_EntityId EntityId, const Worker_ComponentUpdate* ComponentUpdate); - Worker_RequestId SendCommandRequest(Worker_EntityId EntityId, const Worker_CommandRequest* Request, uint32_t CommandId); - void SendCommandResponse(Worker_RequestId RequestId, const Worker_CommandResponse* Response); - void SendCommandFailure(Worker_RequestId RequestId, const FString& Message); - void SendLogMessage(uint8_t Level, const FName& LoggerName, const TCHAR* Message); - void SendComponentInterest(Worker_EntityId EntityId, TArray&& ComponentInterest); - Worker_RequestId SendEntityQueryRequest(const Worker_EntityQuery* EntityQuery); - void SendMetrics(const SpatialGDK::SpatialMetrics& Metrics); - - FString GetWorkerId() const; + virtual TArray GetOpList() override; + virtual Worker_RequestId SendReserveEntityIdsRequest(uint32_t NumOfEntities) override; + virtual Worker_RequestId SendCreateEntityRequest(TArray&& Components, const Worker_EntityId* EntityId) override; + virtual Worker_RequestId SendDeleteEntityRequest(Worker_EntityId EntityId) override; + virtual void SendAddComponent(Worker_EntityId EntityId, FWorkerComponentData* ComponentData) override; + virtual void SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) override; + virtual void SendComponentUpdate(Worker_EntityId EntityId, const FWorkerComponentUpdate* ComponentUpdate) override; + virtual Worker_RequestId SendCommandRequest(Worker_EntityId EntityId, const Worker_CommandRequest* Request, uint32_t CommandId) override; + virtual void SendCommandResponse(Worker_RequestId RequestId, const Worker_CommandResponse* Response) override; + virtual void SendCommandFailure(Worker_RequestId RequestId, const FString& Message) override; + virtual void SendLogMessage(uint8_t Level, const FName& LoggerName, const TCHAR* Message) override; + virtual void SendComponentInterest(Worker_EntityId EntityId, TArray&& ComponentInterest) override; + virtual Worker_RequestId SendEntityQueryRequest(const Worker_EntityQuery* EntityQuery) override; + virtual void SendMetrics(const SpatialGDK::SpatialMetrics& Metrics) override; + + PhysicalWorkerName GetWorkerId() const; const TArray& GetWorkerAttributes() const; - FReceptionistConfig ReceptionistConfig; - FLocatorConfig LocatorConfig; - - DECLARE_DELEGATE(OnConnectionToSpatialOSSucceededDelegate) - OnConnectionToSpatialOSSucceededDelegate OnConnectedCallback; + DECLARE_MULTICAST_DELEGATE_OneParam(FOnEnqueueMessage, const SpatialGDK::FOutgoingMessage*); + FOnEnqueueMessage OnEnqueueMessage; - DECLARE_DELEGATE_TwoParams(OnConnectionToSpatialOSFailedDelegate, uint8_t, const FString&); - OnConnectionToSpatialOSFailedDelegate OnFailedToConnectCallback; + DECLARE_MULTICAST_DELEGATE_OneParam(FOnDequeueMessage, const SpatialGDK::FOutgoingMessage*); + FOnDequeueMessage OnDequeueMessage; - void RequestDeploymentLoginTokens(); + void QueueLatestOpList(); + void ProcessOutgoingMessages(); private: - void ConnectToReceptionist(bool bConnectAsClient, uint32 PlayInEditorID); - void ConnectToLocator(); - void FinishConnecting(Worker_ConnectionFuture* ConnectionFuture); - - void OnConnectionSuccess(); - void OnPreConnectionFailure(const FString& Reason); - void OnConnectionFailure(); - - SpatialConnectionType GetConnectionType() const; - void CacheWorkerAttributes(); // Begin FRunnable Interface @@ -99,24 +66,12 @@ class SPATIALGDK_API USpatialWorkerConnection : public UObject, public FRunnable // End FRunnable Interface void InitializeOpsProcessingThread(); - void QueueLatestOpList(); - void ProcessOutgoingMessages(); - - void StartDevelopmentAuth(FString DevAuthToken); - static void OnPlayerIdentityToken(void* UserData, const Worker_Alpha_PlayerIdentityTokenResponse* PIToken); - static void OnLoginTokens(void* UserData, const Worker_Alpha_LoginTokensResponse* LoginTokens); - void ProcessLoginTokensResponse(const Worker_Alpha_LoginTokensResponse* LoginTokens); template void QueueOutgoingMessage(ArgsType&&... Args); private: Worker_Connection* WorkerConnection; - Worker_Locator* WorkerLocator; - - TWeakObjectPtr GameInstance; - - bool bIsConnected; TArray CachedWorkerAttributes; @@ -129,5 +84,4 @@ class SPATIALGDK_API USpatialWorkerConnection : public UObject, public FRunnable // RequestIds per worker connection start at 0 and incrementally go up each command sent. Worker_RequestId NextRequestId = 0; - LoginTokenResponseCallback LoginTokenResCallback; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/GlobalStateManager.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/GlobalStateManager.h index e3e4ffefa4..994524eadf 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/GlobalStateManager.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/GlobalStateManager.h @@ -3,7 +3,6 @@ #pragma once #include "CoreMinimal.h" -#include "TimerManager.h" #include "UObject/NoExportTypes.h" #include "Utils/SchemaUtils.h" @@ -27,7 +26,7 @@ class SPATIALGDK_API UGlobalStateManager : public UObject GENERATED_BODY() public: - void Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimerManager); + void Init(USpatialNetDriver* InNetDriver); void ApplySingletonManagerData(const Worker_ComponentData& Data); void ApplyDeploymentMapData(const Worker_ComponentData& Data); @@ -42,44 +41,57 @@ class SPATIALGDK_API UGlobalStateManager : public UObject void ExecuteInitialSingletonActorReplication(); void UpdateSingletonEntityId(const FString& ClassName, const Worker_EntityId SingletonEntityId); - void QueryGSM(bool bRetryUntilAcceptingPlayers); - void RetryQueryGSM(bool bRetryUntilAcceptingPlayers); - bool GetAcceptingPlayersFromQueryResponse(const Worker_EntityQueryResponseOp& Op); + DECLARE_DELEGATE_OneParam(QueryDelegate, const Worker_EntityQueryResponseOp&); + void QueryGSM(const QueryDelegate& Callback); + bool GetAcceptingPlayersAndSessionIdFromQueryResponse(const Worker_EntityQueryResponseOp& Op, bool& OutAcceptingPlayers, int32& OutSessionId); + void ApplyVirtualWorkerMappingFromQueryResponse(const Worker_EntityQueryResponseOp& Op); void ApplyDeploymentMapDataFromQueryResponse(const Worker_EntityQueryResponseOp& Op); - void SetDeploymentMapURL(const FString& MapURL); + void SetDeploymentState(); void SetAcceptingPlayers(bool bAcceptingPlayers); - void SetCanBeginPlay(const bool bInCanBeginPlay); + void IncrementSessionID(); + + FORCEINLINE FString GetDeploymentMapURL() const { return DeploymentMapURL; } + FORCEINLINE bool GetAcceptingPlayers() const { return bAcceptingPlayers; } + FORCEINLINE int32 GetSessionId() const { return DeploymentSessionId; } + FORCEINLINE uint32 GetSchemaHash() const { return SchemaHash; } void AuthorityChanged(const Worker_AuthorityChangeOp& AuthChangeOp); bool HandlesComponent(const Worker_ComponentId ComponentId) const; - void BeginDestroy() override; + void ResetGSM(); - bool HasAuthority(); + void BeginDestroy() override; + void TrySendWorkerReadyToBeginPlay(); void TriggerBeginPlay(); + bool GetCanBeginPlay() const; - FORCEINLINE bool IsReadyToCallBeginPlay() const - { - return bCanBeginPlay; - } + bool IsReady() const; USpatialActorChannel* AddSingleton(AActor* SingletonActor); void RegisterSingletonChannel(AActor* SingletonActor, USpatialActorChannel* SingletonChannel); + void RemoveSingletonInstance(const AActor* SingletonActor); + void RemoveAllSingletons(); Worker_EntityId GlobalStateManagerEntityId; // Singleton Manager Component StringToEntityMap SingletonNameToEntityId; +private: // Deployment Map Component FString DeploymentMapURL; bool bAcceptingPlayers; + int32 DeploymentSessionId = 0; + uint32 SchemaHash; // Startup Actor Manager Component + bool bHasSentReadyForVirtualWorkerAssignment; bool bCanBeginPlay; + bool bCanSpawnWithAuthority; +public: #if WITH_EDITOR void OnPrePIEEnded(bool bValue); void ReceiveShutdownMultiProcessRequest(); @@ -88,11 +100,13 @@ class SPATIALGDK_API UGlobalStateManager : public UObject void ReceiveShutdownAdditionalServersEvent(); #endif // WITH_EDITOR private: + void SetDeploymentMapURL(const FString& MapURL); + void SendSessionIdUpdate(); void LinkExistingSingletonActor(const UClass* SingletonClass); - void ApplyAcceptingPlayersUpdate(bool bAcceptingPlayersUpdate); - void ApplyCanBeginPlayUpdate(const bool bCanBeginPlayUpdate); void BecomeAuthoritativeOverAllActors(); + void BecomeAuthoritativeOverActorsBasedOnLBStrategy(); + void SendCanBeginPlayUpdate(const bool bInCanBeginPlay); #if WITH_EDITOR void SendShutdownMultiProcessRequest(); @@ -112,5 +126,7 @@ class SPATIALGDK_API UGlobalStateManager : public UObject UPROPERTY() USpatialReceiver* Receiver; - FTimerManager* TimerManager; + FDelegateHandle PrePIEEndedHandle; + + TMap> SingletonClassPathToActorChannels; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SnapshotManager.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SnapshotManager.h deleted file mode 100644 index 97fedf6288..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SnapshotManager.h +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#pragma once - -#include "CoreMinimal.h" -#include "UObject/NoExportTypes.h" - -#include "EngineClasses/SpatialNetDriver.h" -#include "Utils/SchemaUtils.h" - -#include -#include - -#include "SnapshotManager.generated.h" - -class UGlobalStateManager; -class USpatialReceiver; - -DECLARE_LOG_CATEGORY_EXTERN(LogSnapshotManager, Log, All) - -UCLASS() -class SPATIALGDK_API USnapshotManager : public UObject -{ - GENERATED_BODY() - -public: - void Init(USpatialNetDriver* InNetDriver); - - void WorldWipe(const USpatialNetDriver::PostWorldWipeDelegate& Delegate); - void DeleteEntities(const Worker_EntityQueryResponseOp& Op); - void LoadSnapshot(const FString& SnapshotName); - -private: - UPROPERTY() - USpatialNetDriver* NetDriver; - - UPROPERTY() - UGlobalStateManager* GlobalStateManager; - - UPROPERTY() - USpatialReceiver* Receiver; -}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h index 2351eb975c..6a0bc64eaf 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h @@ -31,7 +31,7 @@ FORCEINLINE ESchemaComponentType GetGroupFromCondition(ELifetimeCondition Condit struct FRPCInfo { - ESchemaComponentType Type; + ERPCType Type; uint32 Index; }; @@ -78,7 +78,7 @@ struct FClassInfo FName WorkerType; }; -class UActorGroupManager; +class SpatialActorGroupManager; class USpatialNetDriver; DECLARE_LOG_CATEGORY_EXTERN(LogSpatialClassInfoManager, Log, All) @@ -90,7 +90,7 @@ class SPATIALGDK_API USpatialClassInfoManager : public UObject public: - bool TryInit(USpatialNetDriver* NetDriver, UActorGroupManager* ActorGroupManager); + bool TryInit(USpatialNetDriver* InNetDriver, SpatialActorGroupManager* InActorGroupManager); // Checks whether a class is supported and quits the game if not. This is to avoid crashing // when running with an out-of-date schema database. @@ -113,8 +113,20 @@ class SPATIALGDK_API USpatialClassInfoManager : public UObject const FRPCInfo& GetRPCInfo(UObject* Object, UFunction* Function); - uint32 GetComponentIdFromLevelPath(const FString& LevelPath); - bool IsSublevelComponent(Worker_ComponentId ComponentId); + Worker_ComponentId GetComponentIdFromLevelPath(const FString& LevelPath) const; + bool IsSublevelComponent(Worker_ComponentId ComponentId) const; + + const TMap& GetNetCullDistanceToComponentIds() const; + + Worker_ComponentId GetComponentIdForNetCullDistance(float NetCullDistance) const; + Worker_ComponentId ComputeActorInterestComponentId(const AActor* Actor) const; + + bool IsNetCullDistanceComponent(Worker_ComponentId ComponentId) const; + + const TArray& GetComponentIdsForComponentType(const ESchemaComponentType ComponentType) const; + + // Used to check if component is used for qbi tracking only + bool IsGeneratedQBIMarkerComponent(Worker_ComponentId ComponentId) const; // Tries to find ClassInfo corresponding to an unused dynamic subobject on the given entity const FClassInfo* GetClassInfoForNewSubobject(const UObject* Object, Worker_EntityId EntityId, USpatialPackageMapClient* PackageMapClient); @@ -122,6 +134,8 @@ class SPATIALGDK_API USpatialClassInfoManager : public UObject UPROPERTY() USchemaDatabase* SchemaDatabase; + void QuitGame(); + private: void CreateClassInfoForClass(UClass* Class); void TryCreateClassInfoForComponentId(Worker_ComponentId ComponentId); @@ -129,14 +143,11 @@ class SPATIALGDK_API USpatialClassInfoManager : public UObject void FinishConstructingActorClassInfo(const FString& ClassPath, TSharedRef& Info); void FinishConstructingSubobjectClassInfo(const FString& ClassPath, TSharedRef& Info); - void QuitGame(); - private: UPROPERTY() USpatialNetDriver* NetDriver; - UPROPERTY() - UActorGroupManager* ActorGroupManager; + SpatialActorGroupManager* ActorGroupManager; TMap, TSharedRef> ClassInfoMap; TMap> ComponentToClassInfoMap; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialConditionMapFilter.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialConditionMapFilter.h index df6099f21e..4b715a5a94 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialConditionMapFilter.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialConditionMapFilter.h @@ -18,8 +18,12 @@ class FSpatialConditionMapFilter RepFlags.bReplay = 0; RepFlags.bNetInitial = 1; // The server will only ever send one update for bNetInitial, so just let them through here. RepFlags.bNetSimulated = ActorChannel->Actor->Role == ROLE_SimulatedProxy; - RepFlags.bNetOwner = bIsClient && ActorChannel->IsOwnedByWorker(); + RepFlags.bNetOwner = bIsClient && ActorChannel->IsAuthoritativeClient(); +#if ENGINE_MINOR_VERSION <= 23 RepFlags.bRepPhysics = ActorChannel->Actor->ReplicatedMovement.bRepPhysics; +#else + RepFlags.bRepPhysics = ActorChannel->Actor->GetReplicatedMovement().bRepPhysics; +#endif #if 0 UE_LOG(LogTemp, Verbose, TEXT("CMF Actor %s (%lld) NetOwner %d Simulated %d RepPhysics %d Client %s"), diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialDispatcher.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialDispatcher.h index 9c5dfe6c97..1429007c6a 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialDispatcher.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialDispatcher.h @@ -13,23 +13,19 @@ #include #include -#include "SpatialDispatcher.generated.h" - DECLARE_LOG_CATEGORY_EXTERN(LogSpatialView, Log, All); class USpatialMetrics; class USpatialReceiver; class USpatialStaticComponentView; +class USpatialWorkerFlags; -UCLASS() -class SPATIALGDK_API USpatialDispatcher : public UObject +class SPATIALGDK_API SpatialDispatcher { - GENERATED_BODY() - public: using FCallbackId = uint32; - void Init(USpatialReceiver* InReceiver, USpatialStaticComponentView* InStaticComponentView, USpatialMetrics* InSpatialMetrics); + void Init(USpatialReceiver* InReceiver, USpatialStaticComponentView* InStaticComponentView, USpatialMetrics* InSpatialMetrics, USpatialWorkerFlags* InSpatialWorkerFlags); void ProcessOps(Worker_OpList* OpList); // The following 2 methods should *only* be used by the Startup OpList Queueing flow @@ -68,14 +64,12 @@ class SPATIALGDK_API USpatialDispatcher : public UObject FCallbackId AddGenericOpCallback(Worker_ComponentId ComponentId, Worker_OpType OpType, const TFunction& Callback); void RunCallbacks(Worker_ComponentId ComponentId, const Worker_Op* Op); - UPROPERTY() - USpatialReceiver* Receiver; - - UPROPERTY() - USpatialStaticComponentView* StaticComponentView; + TWeakObjectPtr Receiver; + TWeakObjectPtr StaticComponentView; + TWeakObjectPtr SpatialMetrics; UPROPERTY() - USpatialMetrics* SpatialMetrics; + USpatialWorkerFlags* SpatialWorkerFlags; // This index is incremented and returned every time an AddOpCallback function is called. // CallbackIds enable you to deregister callbacks using the RemoveOpCallback function. diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialInterestConstraints.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialInterestConstraints.h index 47f4a3b1e2..d1cadc43e4 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialInterestConstraints.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialInterestConstraints.h @@ -32,8 +32,6 @@ struct SPATIALGDK_API FQueryData class UAbstractQueryConstraint* Constraint; /** - * Not currently supported. - * * Used for frequency-based rate limiting. Represents the maximum frequency * of updates for this particular query. An empty option represents no * rate-limiting (ie. updates are received as soon as possible). Frequency @@ -53,7 +51,7 @@ struct SPATIALGDK_API FQueryData * If multiple queries match the same Entity-Component then the highest of * all frequencies is used. */ - UPROPERTY() + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Meta = (ClampMin = 0.0), Category = "SpatialGDK") float Frequency; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialOSDispatcherInterface.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialOSDispatcherInterface.h new file mode 100644 index 0000000000..8f9a2e48e2 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialOSDispatcherInterface.h @@ -0,0 +1,45 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" + +#include "EngineClasses/SpatialActorChannel.h" +#include "Schema/RPCPayload.h" + +#include +#include + +DECLARE_DELEGATE_OneParam(EntityQueryDelegate, const Worker_EntityQueryResponseOp&); +DECLARE_DELEGATE_OneParam(ReserveEntityIDsDelegate, const Worker_ReserveEntityIdsResponseOp&); +DECLARE_DELEGATE_OneParam(CreateEntityDelegate, const Worker_CreateEntityResponseOp&); + +DECLARE_MULTICAST_DELEGATE_OneParam(FOnEntityAddedDelegate, const Worker_EntityId); +DECLARE_MULTICAST_DELEGATE_OneParam(FOnEntityRemovedDelegate, const Worker_EntityId); + +class SpatialOSDispatcherInterface +{ +public: + // Dispatcher Calls + virtual void OnCriticalSection(bool InCriticalSection) PURE_VIRTUAL(SpatialOSDispatcherInterface::OnCriticalSection, return;); + virtual void OnAddEntity(const Worker_AddEntityOp& Op) PURE_VIRTUAL(SpatialOSDispatcherInterface::OnAddEntity, return;); + virtual void OnAddComponent(const Worker_AddComponentOp& Op) PURE_VIRTUAL(SpatialOSDispatcherInterface::OnAddComponent, return;); + virtual void OnRemoveEntity(const Worker_RemoveEntityOp& Op) PURE_VIRTUAL(SpatialOSDispatcherInterface::OnRemoveEntity, return;); + virtual void OnRemoveComponent(const Worker_RemoveComponentOp& Op) PURE_VIRTUAL(SpatialOSDispatcherInterface::OnRemoveComponent, return;); + virtual void FlushRemoveComponentOps() PURE_VIRTUAL(SpatialOSDispatcherInterface::FlushRemoveComponentOps, return;); + virtual void DropQueuedRemoveComponentOpsForEntity(Worker_EntityId EntityId) PURE_VIRTUAL(SpatialOSDispatcherInterface::DropQueuedRemoveComponentOpsForEntity, return;); + virtual void OnAuthorityChange(const Worker_AuthorityChangeOp& Op) PURE_VIRTUAL(SpatialOSDispatcherInterface::OnAuthorityChange, return;); + virtual void OnComponentUpdate(const Worker_ComponentUpdateOp& Op) PURE_VIRTUAL(SpatialOSDispatcherInterface::OnComponentUpdate, return;); + virtual void OnEntityQueryResponse(const Worker_EntityQueryResponseOp& Op) PURE_VIRTUAL(SpatialOSDispatcherInterface::OnEntityQueryResponse, return;); + virtual bool OnExtractIncomingRPC(Worker_EntityId EntityId, ERPCType RPCType, const SpatialGDK::RPCPayload& Payload) PURE_VIRTUAL(SpatialOSDispatcherInterface::OnExtractIncomingRPC, return false;); + virtual void OnCommandRequest(const Worker_CommandRequestOp& Op) PURE_VIRTUAL(SpatialOSDispatcherInterface::OnCommandRequest, return;); + virtual void OnCommandResponse(const Worker_CommandResponseOp& Op) PURE_VIRTUAL(SpatialOSDispatcherInterface::OnCommandResponse, return;); + virtual void OnReserveEntityIdsResponse(const Worker_ReserveEntityIdsResponseOp& Op) PURE_VIRTUAL(SpatialOSDispatcherInterface::OnReserveEntityIdsResponse, return;); + virtual void OnCreateEntityResponse(const Worker_CreateEntityResponseOp& Op) PURE_VIRTUAL(SpatialOSDispatcherInterface::OnCreateEntityResponse, return;); + + virtual void AddPendingActorRequest(Worker_RequestId RequestId, USpatialActorChannel* Channel) PURE_VIRTUAL(SpatialOSDispatcherInterface::AddPendingActorRequest, return;); + virtual void AddPendingReliableRPC(Worker_RequestId RequestId, TSharedRef ReliableRPC) PURE_VIRTUAL(SpatialOSDispatcherInterface::AddPendingReliableRPC, return;); + virtual void AddEntityQueryDelegate(Worker_RequestId RequestId, EntityQueryDelegate Delegate) PURE_VIRTUAL(SpatialOSDispatcherInterface::AddEntityQueryDelegate, return;); + virtual void AddReserveEntityIdsDelegate(Worker_RequestId RequestId, ReserveEntityIDsDelegate Delegate) PURE_VIRTUAL(SpatialOSDispatcherInterface::AddReserveEntityIdsDelegate, return;); + virtual void AddCreateEntityDelegate(Worker_RequestId RequestId, CreateEntityDelegate Delegate) PURE_VIRTUAL(SpatialOSDispatcherInterface::AddCreateEntityDelegate, return;); +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialPlayerSpawner.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialPlayerSpawner.h index ce904f6711..233622db27 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialPlayerSpawner.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialPlayerSpawner.h @@ -2,7 +2,11 @@ #pragma once +#include "Schema/PlayerSpawner.h" +#include "SpatialCommonTypes.h" + #include "GameFramework/OnlineReplStructs.h" +#include "Templates/UniquePtr.h" #include "UObject/NoExportTypes.h" #include @@ -24,21 +28,47 @@ class SPATIALGDK_API USpatialPlayerSpawner : public UObject void Init(USpatialNetDriver* NetDriver, FTimerManager* TimerManager); - // Server - void ReceivePlayerSpawnRequest(Schema_Object* Payload, const char* CallerAttribute, Worker_RequestId RequestId); - // Client void SendPlayerSpawnRequest(); - void ReceivePlayerSpawnResponse(const Worker_CommandResponseOp& Op); + void ReceivePlayerSpawnResponseOnClient(const Worker_CommandResponseOp& Op); + + // Authoritative server worker + void ReceivePlayerSpawnRequestOnServer(const Worker_CommandRequestOp& Op); + void ReceiveForwardPlayerSpawnResponse(const Worker_CommandResponseOp& Op); + + // Non-authoritative server worker + void ReceiveForwardedPlayerSpawnRequest(const Worker_CommandRequestOp& Op); private: - void ObtainPlayerParams(struct FURL& LoginURL, FUniqueNetIdRepl& OutUniqueId, FName& OutOnlinePlatformName); + struct ForwardSpawnRequestDeleter + { + void operator()(Schema_CommandRequest* Request) const noexcept + { + if (Request == nullptr) + { + return; + } + Schema_DestroyCommandRequest(Request); + } + }; + + // Client + SpatialGDK::SpawnPlayerRequest ObtainPlayerParams() const; + + // Authoritative server worker + void FindPlayerStartAndProcessPlayerSpawn(Schema_Object* Request, const PhysicalWorkerName& ClientWorkerId); + bool ForwardSpawnRequestToStrategizedServer(const Schema_Object* OriginalPlayerSpawnRequest, AActor* PlayerStart, const PhysicalWorkerName& ClientWorkerId); + void RetryForwardSpawnPlayerRequest(const Worker_EntityId EntityId, const Worker_RequestId RequestId, const bool bShouldTryDifferentPlayerStart = false); + + // Any server + void PassSpawnRequestToNetDriver(Schema_Object* PlayerSpawnData, AActor* PlayerStart); UPROPERTY() USpatialNetDriver* NetDriver; FTimerManager* TimerManager; int NumberOfAttempts; + TMap> OutgoingForwardPlayerSpawnRequests; TSet WorkersWithPlayersSpawned; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialRPCService.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialRPCService.h new file mode 100644 index 0000000000..7888ab6da5 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialRPCService.h @@ -0,0 +1,116 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" + +#include "Schema/RPCPayload.h" +#include "SpatialView/EntityComponentId.h" +#include "Utils/RPCRingBuffer.h" + +#include +#include + +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialRPCService, Log, All); + +class USpatialStaticComponentView; +struct RPCRingBuffer; + +DECLARE_DELEGATE_RetVal_ThreeParams(bool, ExtractRPCDelegate, Worker_EntityId, ERPCType, const SpatialGDK::RPCPayload&); + +namespace SpatialGDK +{ + +struct EntityRPCType +{ + EntityRPCType(Worker_EntityId EntityId, ERPCType Type) + : EntityId(EntityId) + , Type(Type) + {} + + Worker_EntityId EntityId; + ERPCType Type; + + friend bool operator==(const EntityRPCType& Lhs, const EntityRPCType& Rhs) + { + return Lhs.EntityId == Rhs.EntityId && Lhs.Type == Rhs.Type; + } + + friend uint32 GetTypeHash(EntityRPCType Value) + { + return HashCombine(::GetTypeHash(static_cast(Value.EntityId)), ::GetTypeHash(static_cast(Value.Type))); + } +}; + +enum class EPushRPCResult : uint8 +{ + Success, + + QueueOverflowed, + DropOverflowed, + HasAckAuthority, + NoRingBufferAuthority +}; + +class SPATIALGDK_API SpatialRPCService +{ +public: + SpatialRPCService(ExtractRPCDelegate ExtractRPCCallback, const USpatialStaticComponentView* View); + + EPushRPCResult PushRPC(Worker_EntityId EntityId, ERPCType Type, RPCPayload Payload); + void PushOverflowedRPCs(); + + struct UpdateToSend + { + Worker_EntityId EntityId; + FWorkerComponentUpdate Update; + }; + TArray GetRPCsAndAcksToSend(); + TArray GetRPCComponentsOnEntityCreation(Worker_EntityId EntityId); + + // Will also store acked IDs locally. + // Calls ExtractRPCCallback for each RPC it extracts from a given component. If the callback returns false, + // stops retrieving RPCs. + void ExtractRPCsForEntity(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + + void OnCheckoutMulticastRPCComponentOnEntity(Worker_EntityId EntityId); + void OnRemoveMulticastRPCComponentForEntity(Worker_EntityId EntityId); + + void OnEndpointAuthorityGained(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + void OnEndpointAuthorityLost(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + +private: + // For now, we should drop overflowed RPCs when entity crosses the boundary. + // When locking works as intended, we should re-evaluate how this will work (drop after some time?). + void ClearOverflowedRPCs(Worker_EntityId EntityId); + + EPushRPCResult PushRPCInternal(Worker_EntityId EntityId, ERPCType Type, RPCPayload&& Payload); + + void ExtractRPCsForType(Worker_EntityId EntityId, ERPCType Type); + + void AddOverflowedRPC(EntityRPCType EntityType, RPCPayload&& Payload); + + uint64 GetAckFromView(Worker_EntityId EntityId, ERPCType Type); + const RPCRingBuffer& GetBufferFromView(Worker_EntityId EntityId, ERPCType Type); + + Schema_ComponentUpdate* GetOrCreateComponentUpdate(EntityComponentId EntityComponentIdPair); + Schema_ComponentData* GetOrCreateComponentData(EntityComponentId EntityComponentIdPair); + +private: + ExtractRPCDelegate ExtractRPCCallback; + const USpatialStaticComponentView* View; + + // This is local, not written into schema. + TMap LastSeenMulticastRPCIds; + + // Stored here for things we have authority over. + TMap LastAckedRPCIds; + TMap LastSentRPCIds; + + TMap PendingRPCsOnEntityCreation; + + TMap PendingComponentUpdatesToSend; + TMap> OverflowedRPCs; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialReceiver.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialReceiver.h index 55b8563f85..8a23a34336 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialReceiver.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialReceiver.h @@ -8,7 +8,10 @@ #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" #include "Interop/SpatialClassInfoManager.h" +#include "Interop/SpatialOSDispatcherInterface.h" +#include "Interop/SpatialRPCService.h" #include "Schema/DynamicComponent.h" +#include "Schema/NetOwningClientWorker.h" #include "Schema/RPCPayload.h" #include "Schema/SpawnData.h" #include "Schema/StandardLibrary.h" @@ -26,6 +29,7 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialReceiver, Log, All); class USpatialNetConnection; class USpatialSender; class UGlobalStateManager; +class SpatialLoadBalanceEnforcer; struct PendingAddComponentWrapper { @@ -38,112 +42,43 @@ struct PendingAddComponentWrapper TUniquePtr Data; }; -struct FObjectReferences -{ - FObjectReferences() = default; - FObjectReferences(FObjectReferences&& Other) - : UnresolvedRefs(MoveTemp(Other.UnresolvedRefs)) - , bSingleProp(Other.bSingleProp) - , bFastArrayProp(Other.bFastArrayProp) - , Buffer(MoveTemp(Other.Buffer)) - , NumBufferBits(Other.NumBufferBits) - , Array(MoveTemp(Other.Array)) - , ShadowOffset(Other.ShadowOffset) - , ParentIndex(Other.ParentIndex) - , Property(Other.Property) {} - - // Single property constructor - FObjectReferences(const FUnrealObjectRef& InUnresolvedRef, int32 InCmdIndex, int32 InParentIndex, UProperty* InProperty) - : bSingleProp(true), bFastArrayProp(false), ShadowOffset(InCmdIndex), ParentIndex(InParentIndex), Property(InProperty) - { - UnresolvedRefs.Add(InUnresolvedRef); - } - - // Struct (memory stream) constructor - FObjectReferences(const TArray& InBuffer, int32 InNumBufferBits, const TSet& InUnresolvedRefs, int32 InCmdIndex, int32 InParentIndex, UProperty* InProperty, bool InFastArrayProp = false) - : UnresolvedRefs(InUnresolvedRefs), bSingleProp(false), bFastArrayProp(InFastArrayProp), Buffer(InBuffer), NumBufferBits(InNumBufferBits), ShadowOffset(InCmdIndex), ParentIndex(InParentIndex), Property(InProperty) {} - - // Array constructor - FObjectReferences(FObjectReferencesMap* InArray, int32 InCmdIndex, int32 InParentIndex, UProperty* InProperty) - : bSingleProp(false), bFastArrayProp(false), Array(InArray), ShadowOffset(InCmdIndex), ParentIndex(InParentIndex), Property(InProperty) {} - - TSet UnresolvedRefs; - - bool bSingleProp; - bool bFastArrayProp; - TArray Buffer; - int32 NumBufferBits; - - TUniquePtr Array; - int32 ShadowOffset; - int32 ParentIndex; - UProperty* Property; -}; - -struct FPendingIncomingRPC -{ - FPendingIncomingRPC(const TSet& InUnresolvedRefs, UObject* InTargetObject, UFunction* InFunction, const SpatialGDK::RPCPayload& InPayload) - : UnresolvedRefs(InUnresolvedRefs), TargetObject(InTargetObject), Function(InFunction), Payload(InPayload) {} - - TSet UnresolvedRefs; - TWeakObjectPtr TargetObject; - UFunction* Function; - SpatialGDK::RPCPayload Payload; - FString SenderWorkerId; -}; - -struct FPendingSubobjectAttachment -{ - USpatialActorChannel* Channel; - const FClassInfo* Info; - TWeakObjectPtr Subobject; - - TSet PendingAuthorityDelegations; -}; - -using FIncomingRPCArray = TArray>; - -DECLARE_DELEGATE_OneParam(EntityQueryDelegate, const Worker_EntityQueryResponseOp&); -DECLARE_DELEGATE_OneParam(ReserveEntityIDsDelegate, const Worker_ReserveEntityIdsResponseOp&); -DECLARE_DELEGATE_OneParam(CreateEntityDelegate, const Worker_CreateEntityResponseOp&); - UCLASS() -class USpatialReceiver : public UObject +class USpatialReceiver : public UObject, public SpatialOSDispatcherInterface { GENERATED_BODY() public: - void Init(USpatialNetDriver* NetDriver, FTimerManager* InTimerManager); + void Init(USpatialNetDriver* NetDriver, FTimerManager* InTimerManager, SpatialGDK::SpatialRPCService* InRPCService); // Dispatcher Calls - void OnCriticalSection(bool InCriticalSection); - void OnAddEntity(const Worker_AddEntityOp& Op); - void OnAddComponent(const Worker_AddComponentOp& Op); - void OnRemoveEntity(const Worker_RemoveEntityOp& Op); - void OnRemoveComponent(const Worker_RemoveComponentOp& Op); - void FlushRemoveComponentOps(); - void RemoveComponentOpsForEntity(Worker_EntityId EntityId); - void OnAuthorityChange(const Worker_AuthorityChangeOp& Op); - - void OnComponentUpdate(const Worker_ComponentUpdateOp& Op); - void HandleRPC(const Worker_ComponentUpdateOp& Op); + virtual void OnCriticalSection(bool InCriticalSection) override; + virtual void OnAddEntity(const Worker_AddEntityOp& Op) override; + virtual void OnAddComponent(const Worker_AddComponentOp& Op) override; + virtual void OnRemoveEntity(const Worker_RemoveEntityOp& Op) override; + virtual void OnRemoveComponent(const Worker_RemoveComponentOp& Op) override; + virtual void FlushRemoveComponentOps() override; + virtual void DropQueuedRemoveComponentOpsForEntity(Worker_EntityId EntityId) override; + virtual void OnAuthorityChange(const Worker_AuthorityChangeOp& Op) override; - void ProcessRPCEventField(Worker_EntityId EntityId, const Worker_ComponentUpdateOp &Op, const Worker_ComponentId RPCEndpointComponentId, bool bPacked); + virtual void OnComponentUpdate(const Worker_ComponentUpdateOp& Op) override; - void OnCommandRequest(const Worker_CommandRequestOp& Op); - void OnCommandResponse(const Worker_CommandResponseOp& Op); + // This gets bound to a delegate in SpatialRPCService and is called for each RPC extracted when calling SpatialRPCService::ExtractRPCsForEntity. + virtual bool OnExtractIncomingRPC(Worker_EntityId EntityId, ERPCType RPCType, const SpatialGDK::RPCPayload& Payload) override; - void OnReserveEntityIdsResponse(const Worker_ReserveEntityIdsResponseOp& Op); - void OnCreateEntityResponse(const Worker_CreateEntityResponseOp& Op); + virtual void OnCommandRequest(const Worker_CommandRequestOp& Op) override; + virtual void OnCommandResponse(const Worker_CommandResponseOp& Op) override; - void AddPendingActorRequest(Worker_RequestId RequestId, USpatialActorChannel* Channel); - void AddPendingReliableRPC(Worker_RequestId RequestId, TSharedRef ReliableRPC); + virtual void OnReserveEntityIdsResponse(const Worker_ReserveEntityIdsResponseOp& Op) override; + virtual void OnCreateEntityResponse(const Worker_CreateEntityResponseOp& Op) override; - void AddEntityQueryDelegate(Worker_RequestId RequestId, EntityQueryDelegate Delegate); - void AddReserveEntityIdsDelegate(Worker_RequestId RequestId, ReserveEntityIDsDelegate Delegate); - void AddCreateEntityDelegate(Worker_RequestId RequestId, const CreateEntityDelegate& Delegate); + virtual void AddPendingActorRequest(Worker_RequestId RequestId, USpatialActorChannel* Channel) override; + virtual void AddPendingReliableRPC(Worker_RequestId RequestId, TSharedRef ReliableRPC) override; - void OnEntityQueryResponse(const Worker_EntityQueryResponseOp& Op); + virtual void AddEntityQueryDelegate(Worker_RequestId RequestId, EntityQueryDelegate Delegate) override; + virtual void AddReserveEntityIdsDelegate(Worker_RequestId RequestId, ReserveEntityIDsDelegate Delegate) override; + virtual void AddCreateEntityDelegate(Worker_RequestId RequestId, CreateEntityDelegate Delegate) override; + + virtual void OnEntityQueryResponse(const Worker_EntityQueryResponseOp& Op) override; void ResolvePendingOperations(UObject* Object, const FUnrealObjectRef& ObjectRef); void FlushRetryRPCs(); @@ -151,7 +86,12 @@ class USpatialReceiver : public UObject void OnDisconnect(Worker_DisconnectOp& Op); void RemoveActor(Worker_EntityId EntityId); - bool IsPendingOpsOnChannel(USpatialActorChannel* Channel); + bool IsPendingOpsOnChannel(USpatialActorChannel& Channel); + + void ClearPendingRPCs(Worker_EntityId EntityId); + + void CleanupRepStateMap(FSpatialObjectRepState& Replicator); + void MoveMappedObjectToUnmapped(const FUnrealObjectRef&); private: void EnterCriticalSection(); @@ -160,10 +100,10 @@ class USpatialReceiver : public UObject void ReceiveActor(Worker_EntityId EntityId); void DestroyActor(AActor* Actor, Worker_EntityId EntityId); - AActor* TryGetOrCreateActor(SpatialGDK::UnrealMetadata* UnrealMetadata, SpatialGDK::SpawnData* SpawnData); - AActor* CreateActor(SpatialGDK::UnrealMetadata* UnrealMetadata, SpatialGDK::SpawnData* SpawnData); + AActor* TryGetOrCreateActor(SpatialGDK::UnrealMetadata* UnrealMetadata, SpatialGDK::SpawnData* SpawnData, SpatialGDK::NetOwningClientWorker* NetOwningClientWorkerData); + AActor* CreateActor(SpatialGDK::UnrealMetadata* UnrealMetadata, SpatialGDK::SpawnData* SpawnData, SpatialGDK::NetOwningClientWorker* NetOwningClientWorkerData); - USpatialActorChannel* RecreateDormantSpatialChannel(AActor* Actor, Worker_EntityId EntityID); + USpatialActorChannel* GetOrRecreateChannelForDomantActor(AActor* Actor, Worker_EntityId EntityID); void ProcessRemoveComponent(const Worker_RemoveComponentOp& Op); static FTransform GetRelativeSpawnTransform(UClass* ActorClass, FTransform SpawnTransform); @@ -171,13 +111,18 @@ class USpatialReceiver : public UObject void HandlePlayerLifecycleAuthority(const Worker_AuthorityChangeOp& Op, class APlayerController* PlayerController); void HandleActorAuthority(const Worker_AuthorityChangeOp& Op); - void ApplyComponentDataOnActorCreation(Worker_EntityId EntityId, const Worker_ComponentData& Data, USpatialActorChannel* Channel, const FClassInfo& ActorClassInfo); - void ApplyComponentData(UObject* TargetObject, USpatialActorChannel* Channel, const Worker_ComponentData& Data); + void HandleRPCLegacy(const Worker_ComponentUpdateOp& Op); + void ProcessRPCEventField(Worker_EntityId EntityId, const Worker_ComponentUpdateOp &Op, const Worker_ComponentId RPCEndpointComponentId); + void HandleRPC(const Worker_ComponentUpdateOp& Op); + + void ApplyComponentDataOnActorCreation(Worker_EntityId EntityId, const Worker_ComponentData& Data, USpatialActorChannel& Channel, const FClassInfo& ActorClassInfo, TArray& OutObjectsToResolve); + void ApplyComponentData(USpatialActorChannel& Channel, UObject& TargetObject, const Worker_ComponentData& Data); + // This is called for AddComponentOps not in a critical section, which means they are not a part of the initial entity creation. void HandleIndividualAddComponent(const Worker_AddComponentOp& Op); void AttachDynamicSubobject(AActor* Actor, Worker_EntityId EntityId, const FClassInfo& Info); - void ApplyComponentUpdate(const Worker_ComponentUpdate& ComponentUpdate, UObject* TargetObject, USpatialActorChannel* Channel, bool bIsHandover); + void ApplyComponentUpdate(const Worker_ComponentUpdate& ComponentUpdate, UObject& TargetObject, USpatialActorChannel& Channel, bool bIsHandover); FRPCErrorInfo ApplyRPC(const FPendingRPCParams& Params); ERPCResult ApplyRPCInternal(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, const FString& SenderWorkerId, bool bApplyWithUnresolvedRefs = false); @@ -186,31 +131,68 @@ class USpatialReceiver : public UObject bool IsReceivedEntityTornOff(Worker_EntityId EntityId); - void QueueIncomingRepUpdates(FChannelObjectPair ChannelObjectPair, const FObjectReferencesMap& ObjectReferencesMap, const TSet& UnresolvedRefs); + void ProcessOrQueueIncomingRPC(const FUnrealObjectRef& InTargetObjectRef, SpatialGDK::RPCPayload InPayload); - void ProcessOrQueueIncomingRPC(const FUnrealObjectRef& InTargetObjectRef, SpatialGDK::RPCPayload&& InPayload); - - void ResolvePendingOperations_Internal(UObject* Object, const FUnrealObjectRef& ObjectRef); void ResolveIncomingOperations(UObject* Object, const FUnrealObjectRef& ObjectRef); - void ResolveObjectReferences(FRepLayout& RepLayout, UObject* ReplicatedObject, FObjectReferencesMap& ObjectReferencesMap, uint8* RESTRICT StoredData, uint8* RESTRICT Data, int32 MaxAbsOffset, TArray& RepNotifies, bool& bOutSomeObjectsWereMapped, bool& bOutStillHasUnresolved); + void ResolveObjectReferences(FRepLayout& RepLayout, UObject* ReplicatedObject, FSpatialObjectRepState& RepState, FObjectReferencesMap& ObjectReferencesMap, uint8* RESTRICT StoredData, uint8* RESTRICT Data, int32 MaxAbsOffset, TArray& RepNotifies, bool& bOutSomeObjectsWereMapped); - void ProcessQueuedResolvedObjects(); - void ProcessQueuedActorRPCsOnEntityCreation(AActor* Actor, SpatialGDK::RPCsOnEntityCreation& QueuedRPCs); + void ProcessQueuedActorRPCsOnEntityCreation(Worker_EntityId EntityId, SpatialGDK::RPCsOnEntityCreation& QueuedRPCs); void UpdateShadowData(Worker_EntityId EntityId); TWeakObjectPtr PopPendingActorRequest(Worker_RequestId RequestId); AActor* FindSingletonActor(UClass* SingletonClass); void OnHeartbeatComponentUpdate(const Worker_ComponentUpdateOp& Op); + void CloseClientConnection(USpatialNetConnection* ClientConnection, Worker_EntityId PlayerControllerEntityId); void PeriodicallyProcessIncomingRPCs(); -public: - TMap> IncomingRefsMap; + // TODO: Refactor into a separate class so we can add automated tests for this. UNR-2649 + static bool NeedToLoadClass(const FString& ClassPath); + static FString GetPackagePath(const FString& ClassPath); + + void StartAsyncLoadingClass(const FString& ClassPath, Worker_EntityId EntityId); + void OnAsyncPackageLoaded(const FName& PackageName, UPackage* Package, EAsyncLoadingResult::Type Result); + + bool IsEntityWaitingForAsyncLoad(Worker_EntityId Entity); + + struct QueuedOpForAsyncLoad + { + Worker_Op Op; + Worker_ComponentData* AcquiredData; + Worker_ComponentUpdate* AcquiredUpdate; + }; + void QueueAddComponentOpForAsyncLoad(const Worker_AddComponentOp& Op); + void QueueRemoveComponentOpForAsyncLoad(const Worker_RemoveComponentOp& Op); + void QueueAuthorityOpForAsyncLoad(const Worker_AuthorityChangeOp& Op); + void QueueComponentUpdateOpForAsyncLoad(const Worker_ComponentUpdateOp& Op); + + TArray ExtractAddComponents(Worker_EntityId Entity); + TArray ExtractAuthorityOps(Worker_EntityId Entity); + + struct CriticalSectionSaveState + { + CriticalSectionSaveState(USpatialReceiver& InReceiver); + ~CriticalSectionSaveState(); + USpatialReceiver& Receiver; + + bool bInCriticalSection; + TArray PendingAddActors; + TArray PendingAuthorityChanges; + TArray PendingAddComponents; + }; + + void HandleQueuedOpForAsyncLoad(QueuedOpForAsyncLoad& Op); + // END TODO + +public: TMap, TSharedRef> PendingEntitySubobjectDelegations; + FOnEntityAddedDelegate OnEntityAddedDelegate; + FOnEntityRemovedDelegate OnEntityRemovedDelegate; + private: UPROPERTY() USpatialNetDriver* NetDriver; @@ -230,17 +212,23 @@ class USpatialReceiver : public UObject UPROPERTY() UGlobalStateManager* GlobalStateManager; + SpatialLoadBalanceEnforcer* LoadBalanceEnforcer; + FTimerManager* TimerManager; - // TODO: Figure out how to remove entries when Channel/Actor gets deleted - UNR:100 - TMap UnresolvedRefsMap; - TArray> ResolvedObjectQueue; + SpatialGDK::SpatialRPCService* RPCService; + + // Helper struct to manage FSpatialObjectRepState update cycle. + struct RepStateUpdateHelper; + + // Map from references to replicated objects to properties using these references. + // Useful to manage entities going in and out of interest, in order to recover references to actors. + FObjectToRepStateMap ObjectRefToRepStateMap; - TMap IncomingRPCMap; - FRPCContainer IncomingRPCs; + FRPCContainer IncomingRPCs{ ERPCQueueType::Receive }; bool bInCriticalSection; - TArray PendingAddEntities; + TArray PendingAddActors; TArray PendingAuthorityChanges; TArray PendingAddComponents; TArray QueuedRemoveComponentOps; @@ -258,4 +246,16 @@ class USpatialReceiver : public UObject TMap> AuthorityPlayerControllerConnectionMap; TMap, PendingAddComponentWrapper> PendingDynamicSubobjectComponents; + TMap WorkerConnectionEntities; + + // TODO: Refactor into a separate class so we can add automated tests for this. UNR-2649 + struct EntityWaitingForAsyncLoad + { + FString ClassPath; + TArray InitialPendingAddComponents; + TArray PendingOps; + }; + TMap EntitiesWaitingForAsyncLoad; + TMap> AsyncLoadingPackages; + // END TODO }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSender.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSender.h index 32d52f20ce..42b92ab5da 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSender.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSender.h @@ -4,8 +4,10 @@ #include "CoreMinimal.h" +#include "EngineClasses/SpatialLoadBalanceEnforcer.h" #include "EngineClasses/SpatialNetBitWriter.h" #include "Interop/SpatialClassInfoManager.h" +#include "Interop/SpatialRPCService.h" #include "Schema/RPCPayload.h" #include "TimerManager.h" #include "Utils/RepDataUtils.h" @@ -16,18 +18,16 @@ #include "SpatialSender.generated.h" -using namespace SpatialGDK; - DECLARE_LOG_CATEGORY_EXTERN(LogSpatialSender, Log, All); class USpatialActorChannel; -class USpatialDispatcher; +class SpatialDispatcher; class USpatialNetDriver; class USpatialPackageMapClient; class USpatialReceiver; class USpatialStaticComponentView; class USpatialClassInfoManager; -class UActorGroupManager; +class SpatialActorGroupManager; class USpatialWorkerConnection; struct FReliableRPCForRetry @@ -47,7 +47,7 @@ struct FReliableRPCForRetry struct FPendingRPC { FPendingRPC() = default; - FPendingRPC(FPendingRPC&& Other); + FPendingRPC(FPendingRPC&& Other) = default; uint32 Offset; Schema_FieldId Index; @@ -58,8 +58,8 @@ struct FPendingRPC // TODO: Clear TMap entries when USpatialActorChannel gets deleted - UNR:100 // care for actor getting deleted before actor channel using FChannelObjectPair = TPair, TWeakObjectPtr>; -using FRPCsOnEntityCreationMap = TMap, RPCsOnEntityCreation>; -using FUpdatesQueuedUntilAuthority = TMap>; +using FRPCsOnEntityCreationMap = TMap, SpatialGDK::RPCsOnEntityCreation>; +using FUpdatesQueuedUntilAuthority = TMap>; using FChannelsToUpdatePosition = TSet>; UCLASS() @@ -68,23 +68,32 @@ class SPATIALGDK_API USpatialSender : public UObject GENERATED_BODY() public: - void Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimerManager); + void Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimerManager, SpatialGDK::SpatialRPCService* InRPCService); // Actor Updates - void SendComponentUpdates(UObject* Object, const FClassInfo& Info, USpatialActorChannel* Channel, const FRepChangeState* RepChanges, const FHandoverChangeState* HandoverChanges); + void SendComponentUpdates(UObject* Object, const FClassInfo& Info, USpatialActorChannel* Channel, const FRepChangeState* RepChanges, const FHandoverChangeState* HandoverChanges, uint32& OutBytesWritten); void SendComponentInterestForActor(USpatialActorChannel* Channel, Worker_EntityId EntityId, bool bNetOwned); void SendComponentInterestForSubobject(const FClassInfo& Info, Worker_EntityId EntityId, bool bNetOwned); void SendPositionUpdate(Worker_EntityId EntityId, const FVector& Location); + void SendAuthorityIntentUpdate(const AActor& Actor, VirtualWorkerId NewAuthoritativeVirtualWorkerId); + void SetAclWriteAuthority(const SpatialLoadBalanceEnforcer::AclWriteAuthorityRequest& Request); FRPCErrorInfo SendRPC(const FPendingRPCParams& Params); - ERPCResult SendRPCInternal(UObject* TargetObject, UFunction* Function, const RPCPayload& Payload); - void SendCommandResponse(Worker_RequestId request_id, Worker_CommandResponse& Response); + ERPCResult SendRPCInternal(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload); + void SendCommandResponse(Worker_RequestId RequestId, Worker_CommandResponse& Response); void SendEmptyCommandResponse(Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, Worker_RequestId RequestId); - void SendAddComponent(USpatialActorChannel* Channel, UObject* Subobject, const FClassInfo& Info); - void SendRemoveComponent(Worker_EntityId EntityId, const FClassInfo& Info); - - void SendCreateEntityRequest(USpatialActorChannel* Channel); + void SendCommandFailure(Worker_RequestId RequestId, const FString& Message); + void SendAddComponentForSubobject(USpatialActorChannel* Channel, UObject* Subobject, const FClassInfo& Info, uint32& OutBytesWritten); + void SendAddComponents(Worker_EntityId EntityId, TArray ComponentDatas); + void SendRemoveComponentForClassInfo(Worker_EntityId EntityId, const FClassInfo& Info); + void SendRemoveComponents(Worker_EntityId EntityId, TArray ComponentIds); + void SendInterestBucketComponentChange(const Worker_EntityId EntityId, const Worker_ComponentId OldComponent, const Worker_ComponentId NewComponent); + + void SendCreateEntityRequest(USpatialActorChannel* Channel, uint32& OutBytesWritten); void RetireEntity(const Worker_EntityId EntityId); + // Creates an entity containing just a tombstone component and the minimal data to resolve an actor. + void CreateTombstoneEntity(AActor* Actor); + void SendRequestToClearRPCsOnEntityCreation(Worker_EntityId EntityId); void ClearRPCsOnEntityCreation(Worker_EntityId EntityId); @@ -98,39 +107,56 @@ class SPATIALGDK_API USpatialSender : public UObject void RegisterChannelForPositionUpdate(USpatialActorChannel* Channel); void ProcessPositionUpdates(); - bool UpdateEntityACLs(Worker_EntityId EntityId, const FString& OwnerWorkerAttribute); + void UpdateClientAuthoritativeComponentAclEntries(Worker_EntityId EntityId, const FString& OwnerWorkerAttribute); void UpdateInterestComponent(AActor* Actor); void ProcessOrQueueOutgoingRPC(const FUnrealObjectRef& InTargetObjectRef, SpatialGDK::RPCPayload&& InPayload); void ProcessUpdatesQueuedUntilAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId); - void FlushPackedRPCs(); + void FlushRPCService(); - RPCPayload CreateRPCPayloadFromParams(UObject* TargetObject, const FUnrealObjectRef& TargetObjectRef, UFunction* Function, int ReliableRPCIndex, void* Params); + SpatialGDK::RPCPayload CreateRPCPayloadFromParams(UObject* TargetObject, const FUnrealObjectRef& TargetObjectRef, UFunction* Function, void* Params); void GainAuthorityThenAddComponent(USpatialActorChannel* Channel, UObject* Object, const FClassInfo* Info); // Creates an entity authoritative on this server worker, ensuring it will be able to receive updates for the GSM. void CreateServerWorkerEntity(int AttemptCounter = 1); + void UpdateServerWorkerEntityInterestAndPosition(); + + void ClearPendingRPCs(const Worker_EntityId EntityId); bool ValidateOrExit_IsSupportedClass(const FString& PathName); private: + // Create a copy of an array of components. Deep copies all Schema_ComponentData. + static TArray CopyEntityComponentData(const TArray& EntityComponents); + // Create a copy of an array of components. Deep copies all Schema_ComponentData. + static void DeleteEntityComponentData(TArray& EntityComponents); + + // Create an entity given a set of components and an ID. Retries with the same component data and entity ID on timeout. + void CreateEntityWithRetries(Worker_EntityId EntityId, FString EntityName, TArray Components); + // Actor Lifecycle - Worker_RequestId CreateEntity(USpatialActorChannel* Channel); + Worker_RequestId CreateEntity(USpatialActorChannel* Channel, uint32& OutBytesWritten); Worker_ComponentData CreateLevelComponentData(AActor* Actor); void AddTombstoneToEntity(const Worker_EntityId EntityId); // RPC Construction - FSpatialNetBitWriter PackRPCDataToSpatialNetBitWriter(UFunction* Function, void* Parameters, int ReliableRPCId) const; + FSpatialNetBitWriter PackRPCDataToSpatialNetBitWriter(UFunction* Function, void* Parameters) const; - Worker_CommandRequest CreateRPCCommandRequest(UObject* TargetObject, const RPCPayload& Payload, Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, Worker_EntityId& OutEntityId); + Worker_CommandRequest CreateRPCCommandRequest(UObject* TargetObject, const SpatialGDK::RPCPayload& Payload, Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, Worker_EntityId& OutEntityId); Worker_CommandRequest CreateRetryRPCCommandRequest(const FReliableRPCForRetry& RPC, uint32 TargetObjectOffset); - Worker_ComponentUpdate CreateRPCEventUpdate(UObject* TargetObject, const RPCPayload& Payload, Worker_ComponentId ComponentId, Schema_FieldId EventIndext); - ERPCResult AddPendingRPC(UObject* TargetObject, UFunction* Function, const RPCPayload& Payload, Worker_ComponentId ComponentId, Schema_FieldId RPCIndext); + FWorkerComponentUpdate CreateRPCEventUpdate(UObject* TargetObject, const SpatialGDK::RPCPayload& Payload, Worker_ComponentId ComponentId, Schema_FieldId EventIndext); TArray CreateComponentInterestForActor(USpatialActorChannel* Channel, bool bIsNetOwned); + // RPC Tracking +#if !UE_BUILD_SHIPPING + void TrackRPC(AActor* Actor, UFunction* Function, const SpatialGDK::RPCPayload& Payload, const ERPCType RPCType); +#endif + + bool WillHaveAuthorityOverActor(AActor* TargetActor, Worker_EntityId TargetEntity); + private: UPROPERTY() USpatialNetDriver* NetDriver; @@ -150,12 +176,13 @@ class SPATIALGDK_API USpatialSender : public UObject UPROPERTY() USpatialClassInfoManager* ClassInfoManager; - UPROPERTY() - UActorGroupManager* ActorGroupManager; + SpatialActorGroupManager* ActorGroupManager; FTimerManager* TimerManager; - FRPCContainer OutgoingRPCs; + SpatialGDK::SpatialRPCService* RPCService; + + FRPCContainer OutgoingRPCs{ ERPCQueueType::Send }; FRPCsOnEntityCreationMap OutgoingOnCreateEntityRPCs; TArray> RetryRPCs; @@ -163,6 +190,4 @@ class SPATIALGDK_API USpatialSender : public UObject FUpdatesQueuedUntilAuthority UpdatesQueuedUntilAuthorityMap; FChannelsToUpdatePosition ChannelsToUpdatePosition; - - TMap> RPCsToPack; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSnapshotManager.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSnapshotManager.h new file mode 100644 index 0000000000..74a5433b3b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSnapshotManager.h @@ -0,0 +1,36 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Utils/SchemaUtils.h" + +#include +#include + +#include "CoreMinimal.h" + +class UGlobalStateManager; +class USpatialReceiver; +class USpatialWorkerConnection; + +DECLARE_LOG_CATEGORY_EXTERN(LogSnapshotManager, Log, All) + +DECLARE_DELEGATE(PostWorldWipeDelegate); + +class SPATIALGDK_API SpatialSnapshotManager +{ +public: + SpatialSnapshotManager(); + + void Init(USpatialWorkerConnection* InConnection, UGlobalStateManager* InGlobalStateManager, USpatialReceiver* InReceiver); + + void WorldWipe(const PostWorldWipeDelegate& Delegate); + void LoadSnapshot(const FString& SnapshotName); + +private: + static void DeleteEntities(const Worker_EntityQueryResponseOp& Op, TWeakObjectPtr Connection); + + TWeakObjectPtr Connection; + TWeakObjectPtr GlobalStateManager; + TWeakObjectPtr Receiver; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialStaticComponentView.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialStaticComponentView.h index 06a3bfbe5b..f533d30668 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialStaticComponentView.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialStaticComponentView.h @@ -2,16 +2,17 @@ #pragma once -#include "CoreMinimal.h" - #include "Schema/Component.h" #include "Schema/StandardLibrary.h" -#include "Schema/UnrealMetadata.h" #include "SpatialConstants.h" #include #include +#include "Containers/Map.h" +#include "Templates/UniquePtr.h" +#include "UObject/Object.h" + #include "SpatialStaticComponentView.generated.h" UCLASS() @@ -20,23 +21,23 @@ class SPATIALGDK_API USpatialStaticComponentView : public UObject GENERATED_BODY() public: - Worker_Authority GetAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId); - bool HasAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + bool HasAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const; template - T* GetComponentData(Worker_EntityId EntityId) + T* GetComponentData(Worker_EntityId EntityId) const { - if (TMap>* ComponentStorageMap = EntityComponentMap.Find(EntityId)) + if (const auto* ComponentStorageMap = EntityComponentMap.Find(EntityId)) { - if (TUniquePtr* Component = ComponentStorageMap->Find(T::ComponentId)) + if (const TUniquePtr* Component = ComponentStorageMap->Find(T::ComponentId)) { - return &(static_cast*>(Component->Get())->Get()); + return static_cast(Component->Get()); } } return nullptr; } - bool HasComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + + bool HasComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const; void OnAddComponent(const Worker_AddComponentOp& Op); void OnRemoveComponent(const Worker_RemoveComponentOp& Op); @@ -44,7 +45,11 @@ class SPATIALGDK_API USpatialStaticComponentView : public UObject void OnComponentUpdate(const Worker_ComponentUpdateOp& Op); void OnAuthorityChange(const Worker_AuthorityChangeOp& Op); + void GetEntityIds(TArray& OutEntityIds) const { EntityComponentMap.GetKeys(OutEntityIds); } + private: + Worker_Authority GetAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const; + TMap> EntityComponentAuthorityMap; - TMap>> EntityComponentMap; + TMap>> EntityComponentMap; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialWorkerFlags.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialWorkerFlags.h index 35603cfc35..ae5f60df7d 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialWorkerFlags.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialWorkerFlags.h @@ -2,40 +2,36 @@ #pragma once -#include "Kismet/BlueprintFunctionLibrary.h" #include #include "SpatialWorkerFlags.generated.h" -DECLARE_DYNAMIC_DELEGATE_TwoParams(FOnWorkerFlagsUpdatedBP, FString, FlagName, FString, FlagValue); -DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnWorkerFlagsUpdated, FString, FlagName, FString, FlagValue); +DECLARE_DYNAMIC_DELEGATE_TwoParams(FOnWorkerFlagsUpdatedBP, const FString&, FlagName, const FString&, FlagValue); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnWorkerFlagsUpdated, const FString&, FlagName, const FString&, FlagValue); UCLASS() -class SPATIALGDK_API USpatialWorkerFlags : public UBlueprintFunctionLibrary +class SPATIALGDK_API USpatialWorkerFlags : public UObject { GENERATED_BODY() public: /** Gets value of a worker flag. Must be connected to SpatialOS to properly work. - * @param Name - Name of worker flag - * @param OutValue - Value of worker flag + * @param InFlagName - Name of worker flag + * @param OutFlagValue - Value of worker flag * @return - If worker flag was found. */ - UFUNCTION(BlueprintCallable, Category="SpatialOS") - static bool GetWorkerFlag(const FString& Name, FString& OutValue); - - static FOnWorkerFlagsUpdated& GetOnWorkerFlagsUpdated(); - + bool GetWorkerFlag(const FString& InFlagName, FString& OutFlagValue) const; + UFUNCTION(BlueprintCallable, Category = "SpatialOS") - static void BindToOnWorkerFlagsUpdated(const FOnWorkerFlagsUpdatedBP& InDelegate); + void BindToOnWorkerFlagsUpdated(const FOnWorkerFlagsUpdatedBP& InDelegate); UFUNCTION(BlueprintCallable, Category = "SpatialOS") - static void UnbindFromOnWorkerFlagsUpdated(const FOnWorkerFlagsUpdatedBP& InDelegate); + void UnbindFromOnWorkerFlagsUpdated(const FOnWorkerFlagsUpdatedBP& InDelegate); + + void ApplyWorkerFlagUpdate(const Worker_FlagUpdateOp& Op); - static FOnWorkerFlagsUpdated OnWorkerFlagsUpdated; private: - static void ApplyWorkerFlagUpdate(const Worker_FlagUpdateOp& Op); - static TMap WorkerFlags; + FOnWorkerFlagsUpdated OnWorkerFlagsUpdated; - friend class USpatialDispatcher; + TMap WorkerFlags; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLBStrategy.h b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLBStrategy.h index 41186965f7..5d3e2ee537 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLBStrategy.h +++ b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLBStrategy.h @@ -2,12 +2,14 @@ #pragma once -#include "CoreMinimal.h" +#include "SpatialCommonTypes.h" #include "SpatialConstants.h" + +#include "CoreMinimal.h" +#include "Schema/Interest.h" #include "UObject/NoExportTypes.h" -#include "AbstractLBStrategy.generated.h" -class USpatialNetDriver; +#include "AbstractLBStrategy.generated.h" /** * This class can be used to define a load balancing strategy. @@ -18,7 +20,7 @@ class USpatialNetDriver; * VirtualWorkerIds from GetVirtualWorkerIds() and begin assinging workers. * (Other Workers): SetLocalVirtualWorkerId when assigned a VirtualWorkerId. * 4. For each Actor being replicated: - * a) Check if authority should be relinquished by calling ShouldRelinquishAuthority + * a) Check if authority should be relinquished by calling ShouldHaveAuthority * b) If true: Send authority change request to Translator/Enforcer passing in new * VirtualWorkerId returned by WhoShouldHaveAuthority */ @@ -30,18 +32,28 @@ class SPATIALGDK_API UAbstractLBStrategy : public UObject public: UAbstractLBStrategy(); - virtual void Init(const USpatialNetDriver* InNetDriver); + virtual void Init() {} bool IsReady() const { return LocalVirtualWorkerId != SpatialConstants::INVALID_VIRTUAL_WORKER_ID; } - void SetLocalVirtualWorkerId(uint32 LocalVirtualWorkerId); + void SetLocalVirtualWorkerId(VirtualWorkerId LocalVirtualWorkerId); + + virtual TSet GetVirtualWorkerIds() const PURE_VIRTUAL(UAbstractLBStrategy::GetVirtualWorkerIds, return {};) + + virtual bool ShouldHaveAuthority(const AActor& Actor) const { return false; } + virtual VirtualWorkerId WhoShouldHaveAuthority(const AActor& Actor) const PURE_VIRTUAL(UAbstractLBStrategy::WhoShouldHaveAuthority, return SpatialConstants::INVALID_VIRTUAL_WORKER_ID;) - virtual TSet GetVirtualWorkerIds() const PURE_VIRTUAL(UAbstractLBStrategy::GetVirtualWorkerIds, return {};) + /** + * Get the query constraints required by this worker based on the load balancing strategy used. + */ + virtual SpatialGDK::QueryConstraint GetWorkerInterestQueryConstraint() const PURE_VIRTUAL(UAbstractLBStrategy::GetWorkerInterestQueryConstraint, return {};) - virtual bool ShouldRelinquishAuthority(const AActor& Actor) const { return false; } - virtual uint32 WhoShouldHaveAuthority(const AActor& Actor) const PURE_VIRTUAL(UAbstractLBStrategy::WhoShouldHaveAuthority, return SpatialConstants::INVALID_VIRTUAL_WORKER_ID; ) + /** + * Get a logical worker entity position for this strategy. For example, the centre of a grid square in a grid-based strategy. Optional- otherwise returns the origin. + */ + virtual FVector GetWorkerEntityPosition() const { return FVector::ZeroVector; } protected: - uint32 LocalVirtualWorkerId; + VirtualWorkerId LocalVirtualWorkerId; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLockingPolicy.h b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLockingPolicy.h new file mode 100644 index 0000000000..ce1e43b65b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLockingPolicy.h @@ -0,0 +1,33 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialConstants.h" + +#include "GameFramework/Actor.h" +#include "Improbable/SpatialEngineDelegates.h" +#include "Templates/SharedPointer.h" +#include "UObject/WeakObjectPtrTemplates.h" + +#include "AbstractLockingPolicy.generated.h" + +UCLASS(abstract) +class SPATIALGDK_API UAbstractLockingPolicy : public UObject +{ + GENERATED_BODY() + +public: + virtual void Init(SpatialDelegates::FAcquireLockDelegate& AcquireLockDelegate, SpatialDelegates::FReleaseLockDelegate& ReleaseLockDelegate) + { + AcquireLockDelegate.BindUObject(this, &UAbstractLockingPolicy::AcquireLockFromDelegate); + ReleaseLockDelegate.BindUObject(this, &UAbstractLockingPolicy::ReleaseLockFromDelegate); + }; + virtual ActorLockToken AcquireLock(AActor* Actor, FString LockName = TEXT("")) PURE_VIRTUAL(UAbstractLockingPolicy::AcquireLock, return SpatialConstants::INVALID_ACTOR_LOCK_TOKEN;); + virtual bool ReleaseLock(const ActorLockToken Token) PURE_VIRTUAL(UAbstractLockingPolicy::ReleaseLock, return false;); + virtual bool IsLocked(const AActor* Actor) const PURE_VIRTUAL(UAbstractLockingPolicy::IsLocked, return false;); + virtual void OnOwnerUpdated(const AActor* Actor, const AActor* OldOwner) PURE_VIRTUAL(UAbstractLockingPolicy::OnOwnerUpdated, return;); + +private: + virtual bool AcquireLockFromDelegate(AActor* ActorToLock, const FString& DelegateLockIdentifier) PURE_VIRTUAL(UAbstractLockingPolicy::AcquireLockFromDelegate, return false;); + virtual bool ReleaseLockFromDelegate(AActor* ActorToRelease, const FString& DelegateLockIdentifier) PURE_VIRTUAL(UAbstractLockingPolicy::ReleaseLockFromDelegate, return false;); +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GridBasedLBStrategy.h b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GridBasedLBStrategy.h index bdb9b7af9b..f15974764c 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GridBasedLBStrategy.h +++ b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GridBasedLBStrategy.h @@ -2,10 +2,18 @@ #pragma once -#include "CoreMinimal.h" #include "LoadBalancing/AbstractLBStrategy.h" + +#include "CoreMinimal.h" +#include "Math/Box2D.h" +#include "Math/Vector2D.h" + #include "GridBasedLBStrategy.generated.h" +class SpatialVirtualWorkerTranslator; + +DECLARE_LOG_CATEGORY_EXTERN(LogGridBasedLBStrategy, Log, All) + /** * A load balancing strategy that divides the world into a grid. * Divides the load between Rows * Cols number of workers, each handling a @@ -25,15 +33,23 @@ class SPATIALGDK_API UGridBasedLBStrategy : public UAbstractLBStrategy public: UGridBasedLBStrategy(); + using LBStrategyRegions = TArray>; + /* UAbstractLBStrategy Interface */ - virtual void Init(const class USpatialNetDriver* InNetDriver) override; + virtual void Init() override; + + virtual TSet GetVirtualWorkerIds() const override; + + virtual bool ShouldHaveAuthority(const AActor& Actor) const override; + virtual VirtualWorkerId WhoShouldHaveAuthority(const AActor& Actor) const override; - virtual TSet GetVirtualWorkerIds() const; + virtual SpatialGDK::QueryConstraint GetWorkerInterestQueryConstraint() const override; - virtual bool ShouldRelinquishAuthority(const AActor& Actor) const override; - virtual uint32 WhoShouldHaveAuthority(const AActor& Actor) const override; + virtual FVector GetWorkerEntityPosition() const override; /* End UAbstractLBStrategy Interface */ + LBStrategyRegions GetLBStrategyRegions() const; + protected: UPROPERTY(EditDefaultsOnly, meta = (ClampMin = "1"), Category = "Grid Based Load Balancing") uint32 Rows; @@ -47,9 +63,12 @@ class SPATIALGDK_API UGridBasedLBStrategy : public UAbstractLBStrategy UPROPERTY(EditDefaultsOnly, meta = (ClampMin = "1"), Category = "Grid Based Load Balancing") float WorldHeight; + UPROPERTY(EditDefaultsOnly, meta = (ClampMin = "0"), Category = "Grid Based Load Balancing") + float InterestBorder; + private: - TArray VirtualWorkerIds; + TArray VirtualWorkerIds; TArray WorkerCells; diff --git a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/OwnershipLockingPolicy.h b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/OwnershipLockingPolicy.h new file mode 100644 index 0000000000..c594aa01d4 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/OwnershipLockingPolicy.h @@ -0,0 +1,68 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "AbstractLockingPolicy.h" + +#include "Containers/Map.h" +#include "Containers/UnrealString.h" +#include "GameFramework/Actor.h" +#include "UObject/WeakObjectPtr.h" + +#include "OwnershipLockingPolicy.generated.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogOwnershipLockingPolicy, Log, All) + +UCLASS() +class SPATIALGDK_API UOwnershipLockingPolicy : public UAbstractLockingPolicy +{ + GENERATED_BODY() + +public: + virtual ActorLockToken AcquireLock(AActor* Actor, FString DebugString = "") override; + + // This should only be called during the lifetime of the locked Actor + virtual bool ReleaseLock(const ActorLockToken Token) override; + + virtual bool IsLocked(const AActor* Actor) const override; + + virtual void OnOwnerUpdated(const AActor* Actor, const AActor* OldOwner) override; + +private: + struct MigrationLockElement + { + int32 LockCount; + AActor* HierarchyRoot; + }; + + struct LockNameAndActor + { + const FString LockName; + AActor* Actor; + }; + + bool CanAcquireLock(const AActor* Actor) const; + bool IsExplicitlyLocked(const AActor* Actor) const; + bool IsLockedHierarchyRoot(const AActor* Actor) const; + + UFUNCTION() + void OnExplicitlyLockedActorDeleted(AActor* DestroyedActor); + + UFUNCTION() + void OnHierarchyRootActorDeleted(AActor* DestroyedActorRoot); + + virtual bool AcquireLockFromDelegate(AActor* ActorToLock, const FString& DelegateLockIdentifier) override; + virtual bool ReleaseLockFromDelegate(AActor* ActorToRelease, const FString& DelegateLockIdentifier) override; + + void RecalculateAllExplicitlyLockedActorsInThisHierarchy(const AActor* HierarchyRoot); + void RecalculateLockedActorOwnershipHierarchyInformation(const AActor* ExplicitlyLockedActor); + void AddOwnershipHierarchyRootInformation(AActor* HierarchyRoot, const AActor* ExplicitlyLockedActor); + void RemoveOwnershipHierarchyRootInformation(AActor* HierarchyRoot, const AActor* ExplicitlyLockedActor); + + TMap ActorToLockingState; + TMap TokenToNameAndActor; + TMap DelegateLockingIdentifierToActorLockToken; + TMap> LockedOwnershipRootActorToExplicitlyLockedActors; + + ActorLockToken NextToken = 1; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/WorkerRegion.h b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/WorkerRegion.h new file mode 100644 index 0000000000..51efd28a7a --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/WorkerRegion.h @@ -0,0 +1,33 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Components/StaticMeshComponent.h" +#include "GameFramework/Actor.h" +#include "Math/Box2D.h" +#include "Math/Color.h" + +#include "WorkerRegion.generated.h" + +UCLASS(NotPlaceable, NotBlueprintable) +class SPATIALGDK_API AWorkerRegion : public AActor +{ + GENERATED_BODY() + +public: + AWorkerRegion(const FObjectInitializer& ObjectInitializer); + + void Init(UMaterial* Material, const FColor& Color, const FBox2D& Extents, const float VerticalScale); + + UPROPERTY() + UStaticMeshComponent *Mesh; + + UPROPERTY() + UMaterialInstanceDynamic *MaterialInstance; + +private: + void SetOpacity(const float Opacity); + void SetHeight(const float Height); + void SetPositionAndScale(const FBox2D& Extents, const float VerticalScale); + void SetColor(const FColor& Color); +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/AlwaysRelevant.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/AlwaysRelevant.h deleted file mode 100644 index f11349fba2..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/AlwaysRelevant.h +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#pragma once - -#include "Schema/Component.h" -#include "SpatialConstants.h" - -#include -#include - -namespace SpatialGDK -{ - -struct AlwaysRelevant : Component -{ - static const Worker_ComponentId ComponentId = SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID; - - AlwaysRelevant() = default; - - FORCEINLINE Worker_ComponentData CreateData() - { - Worker_ComponentData Data = {}; - Data.component_id = ComponentId; - Data.schema_type = Schema_CreateComponentData(); - - return Data; - } -}; - -struct Dormant : Component -{ - static const Worker_ComponentId ComponentId = SpatialConstants::DORMANT_COMPONENT_ID; - - Dormant() = default; - - FORCEINLINE Worker_ComponentData CreateData() - { - Worker_ComponentData Data = {}; - Data.component_id = ComponentId; - Data.schema_type = Schema_CreateComponentData(); - - return Data; - } -}; - -} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/AuthorityIntent.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/AuthorityIntent.h index 852865ab80..90d14ad365 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/AuthorityIntent.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/AuthorityIntent.h @@ -3,6 +3,7 @@ #pragma once #include "Schema/Component.h" +#include "SpatialCommonTypes.h" #include "Utils/SchemaUtils.h" #include @@ -18,10 +19,13 @@ struct AuthorityIntent : Component { static const Worker_ComponentId ComponentId = SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID; - AuthorityIntent() = default; + AuthorityIntent() + : VirtualWorkerId(SpatialConstants::AUTHORITY_INTENT_VIRTUAL_WORKER_ID) + {} - AuthorityIntent(uint32 InVirtualWorkerId) - : VirtualWorkerId(InVirtualWorkerId) {} + AuthorityIntent(VirtualWorkerId InVirtualWorkerId) + : VirtualWorkerId(InVirtualWorkerId) + {} AuthorityIntent(const Worker_ComponentData& Data) { @@ -31,25 +35,35 @@ struct AuthorityIntent : Component } Worker_ComponentData CreateAuthorityIntentData() + { + return CreateAuthorityIntentData(VirtualWorkerId); + } + + static Worker_ComponentData CreateAuthorityIntentData(VirtualWorkerId InVirtualWorkerId) { Worker_ComponentData Data = {}; Data.component_id = ComponentId; Data.schema_type = Schema_CreateComponentData(); Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); - Schema_AddUint32(ComponentObject, SpatialConstants::AUTHORITY_INTENT_VIRTUAL_WORKER_ID, VirtualWorkerId); + Schema_AddUint32(ComponentObject, SpatialConstants::AUTHORITY_INTENT_VIRTUAL_WORKER_ID, InVirtualWorkerId); return Data; } Worker_ComponentUpdate CreateAuthorityIntentUpdate() + { + return CreateAuthorityIntentUpdate(VirtualWorkerId); + } + + static Worker_ComponentUpdate CreateAuthorityIntentUpdate(VirtualWorkerId InVirtualWorkerId) { Worker_ComponentUpdate Update = {}; Update.component_id = ComponentId; Update.schema_type = Schema_CreateComponentUpdate(); Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); - Schema_AddUint32(ComponentObject, SpatialConstants::AUTHORITY_INTENT_VIRTUAL_WORKER_ID, VirtualWorkerId); + Schema_AddUint32(ComponentObject, SpatialConstants::AUTHORITY_INTENT_VIRTUAL_WORKER_ID, InVirtualWorkerId); return Update; } @@ -62,7 +76,7 @@ struct AuthorityIntent : Component // Id of the Unreal server worker which should be authoritative for the entity. // 0 is reserved as an invalid/unset value. - uint32 VirtualWorkerId; + VirtualWorkerId VirtualWorkerId; }; } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/ClientEndpoint.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/ClientEndpoint.h new file mode 100644 index 0000000000..7e6dffb52b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/ClientEndpoint.h @@ -0,0 +1,32 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Schema/Component.h" +#include "SpatialConstants.h" +#include "Utils/RPCRingBuffer.h" + +#include +#include + +namespace SpatialGDK +{ + +struct ClientEndpoint : Component +{ + static const Worker_ComponentId ComponentId = SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID; + + ClientEndpoint(const Worker_ComponentData& Data); + + void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) override; + + RPCRingBuffer ReliableRPCBuffer; + RPCRingBuffer UnreliableRPCBuffer; + uint64 ReliableRPCAck = 0; + uint64 UnreliableRPCAck = 0; + +private: + void ReadFromSchema(Schema_Object* SchemaObject); +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/ClientRPCEndpoint.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/ClientRPCEndpointLegacy.h similarity index 90% rename from SpatialGDK/Source/SpatialGDK/Public/Schema/ClientRPCEndpoint.h rename to SpatialGDK/Source/SpatialGDK/Public/Schema/ClientRPCEndpointLegacy.h index 42a6d40ea3..b3a69f1e68 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/ClientRPCEndpoint.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/ClientRPCEndpointLegacy.h @@ -12,13 +12,13 @@ namespace SpatialGDK { -struct ClientRPCEndpoint : Component +struct ClientRPCEndpointLegacy : Component { - static const Worker_ComponentId ComponentId = SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID; + static const Worker_ComponentId ComponentId = SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY; - ClientRPCEndpoint() = default; + ClientRPCEndpointLegacy() = default; - ClientRPCEndpoint(const Worker_ComponentData& Data) + ClientRPCEndpointLegacy(const Worker_ComponentData& Data) { Schema_Object* EndpointObject = Schema_GetComponentDataFields(Data.schema_type); bReady = GetBoolFromSchema(EndpointObject, SpatialConstants::UNREAL_RPC_ENDPOINT_READY_ID); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/Component.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/Component.h index 39e5d91ea1..9eb68a6f9a 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/Component.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/Component.h @@ -14,33 +14,4 @@ struct Component virtual void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) {} }; -class ComponentStorageBase -{ -public: - virtual ~ComponentStorageBase(){}; - virtual TUniquePtr Copy() const = 0; -}; - -template -class ComponentStorage : public ComponentStorageBase -{ -public: - explicit ComponentStorage(const T& data) : data{data} {} - explicit ComponentStorage(T&& data) : data{MoveTemp(data)} {} - ~ComponentStorage() override {} - - TUniquePtr Copy() const override - { - return TUniquePtr{new ComponentStorage{data}}; - } - - T& Get() - { - return data; - } - -private: - T data; -}; - } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/ComponentPresence.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/ComponentPresence.h new file mode 100644 index 0000000000..0413518353 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/ComponentPresence.h @@ -0,0 +1,116 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Schema/Component.h" +#include "SpatialCommonTypes.h" +#include "SpatialConstants.h" +#include "Utils/SchemaUtils.h" + +#include "Containers/Array.h" +#include "Templates/UnrealTemplate.h" + +#include +#include + +namespace SpatialGDK +{ + +struct ComponentPresence : Component +{ + static const Worker_ComponentId ComponentId = SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID; + + ComponentPresence() = default; + + ComponentPresence(TArray&& InComponentList) + : ComponentList(MoveTemp(InComponentList)) {} + + ComponentPresence(const Worker_ComponentData& Data) + { + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + CopyListFromComponentObject(ComponentObject); + } + + Worker_ComponentData CreateComponentPresenceData() + { + return CreateComponentPresenceData(ComponentList); + } + + static Worker_ComponentData CreateComponentPresenceData(const TArray& ComponentList) + { + Worker_ComponentData Data = {}; + Data.component_id = ComponentId; + Data.schema_type = Schema_CreateComponentData(); + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + + for (const Worker_ComponentId& InComponentId : ComponentList) + { + Schema_AddUint32(ComponentObject, SpatialConstants::COMPONENT_PRESENCE_COMPONENT_LIST_ID, InComponentId); + } + + return Data; + } + + Worker_ComponentUpdate CreateComponentPresenceUpdate() + { + return CreateComponentPresenceUpdate(ComponentList); + } + + static Worker_ComponentUpdate CreateComponentPresenceUpdate(const TArray& ComponentList) + { + Worker_ComponentUpdate Update = {}; + Update.component_id = ComponentId; + Update.schema_type = Schema_CreateComponentUpdate(); + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); + + Schema_AddUint32List(ComponentObject, SpatialConstants::COMPONENT_PRESENCE_COMPONENT_LIST_ID, + ComponentList.GetData(), ComponentList.Num()); + + return Update; + } + + void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) + { + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); + CopyListFromComponentObject(ComponentObject); + } + + void CopyListFromComponentObject(Schema_Object* ComponentObject) + { + ComponentList.SetNum(Schema_GetUint32Count(ComponentObject, SpatialConstants::COMPONENT_PRESENCE_COMPONENT_LIST_ID), true); + Schema_GetUint32List(ComponentObject, SpatialConstants::COMPONENT_PRESENCE_COMPONENT_LIST_ID, ComponentList.GetData()); + } + + void AddComponentDataIds(const TArray& ComponentDatas) + { + TArray ComponentIds; + ComponentIds.Reserve(ComponentDatas.Num()); + for (const FWorkerComponentData& ComponentData : ComponentDatas) + { + ComponentIds.Add(ComponentData.component_id); + } + + AddComponentIds(ComponentIds); + } + + void AddComponentIds(const TArray& ComponentsToAdd) + { + for (const Worker_ComponentId& NewComponentId : ComponentsToAdd) + { + ComponentList.AddUnique(NewComponentId); + } + } + + void RemoveComponentIds(const TArray& ComponentsToRemove) + { + ComponentList.RemoveAll([&](Worker_ComponentId PresentComponent) + { + return ComponentsToRemove.Contains(PresentComponent); + }); + } + + // List of component IDs that exist on an entity. + TArray ComponentList; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/Interest.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/Interest.h index f5c5869345..121e02eb13 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/Interest.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/Interest.h @@ -6,8 +6,8 @@ namespace SpatialGDK { - using EdgeLength = Coordinates; +using ResultType = TArray; struct SphereConstraint { @@ -112,7 +112,7 @@ struct Query // Either full_snapshot_result or a list of result_component_id should be provided. Providing both is invalid. TSchemaOption FullSnapshotResult; // Whether all components should be included or none. - TArray ResultComponentId; // Which components should be included. + ResultType ResultComponentIds; // Which components should be included. // Used for frequency-based rate limiting. Represents the maximum frequency of updates for this // particular query. An empty option represents no rate-limiting (ie. updates are received @@ -132,6 +132,19 @@ struct Query TSchemaOption Frequency; }; +// Constraints are typically linked to a corresponding frequency in the GDK use case, but without the result set yet. +struct FrequencyConstraint +{ + TSchemaOption Frequency; + QueryConstraint Constraint; +}; + +// Used for deduping queries across frequencies +using FrequencyToConstraintsMap = TMap>; + +// A common type for lists of frequency constraints to be converted into queries later +using FrequencyConstraints = TArray; + struct ComponentInterest { TArray Queries; @@ -222,7 +235,7 @@ inline void AddQueryConstraintToQuerySchema(Schema_Object* QueryObject, Schema_F inline void AddQueryToComponentInterestSchema(Schema_Object* ComponentInterestObject, Schema_FieldId Id, const Query& Query) { - checkf(!(Query.FullSnapshotResult.IsSet() && Query.ResultComponentId.Num() > 0), TEXT("Either full_snapshot_result or a list of result_component_id should be provided. Providing both is invalid.")); + checkf(!(Query.FullSnapshotResult.IsSet() && Query.ResultComponentIds.Num() > 0), TEXT("Either full_snapshot_result or a list of result_component_id should be provided. Providing both is invalid.")); Schema_Object* QueryObject = Schema_AddObject(ComponentInterestObject, Id); @@ -233,7 +246,7 @@ inline void AddQueryToComponentInterestSchema(Schema_Object* ComponentInterestOb Schema_AddBool(QueryObject, 2, *Query.FullSnapshotResult); } - for (uint32 ComponentId : Query.ResultComponentId) + for (uint32 ComponentId : Query.ResultComponentIds) { Schema_AddUint32(QueryObject, 3, ComponentId); } @@ -324,7 +337,7 @@ inline QueryConstraint IndexQueryConstraintFromSchema(Schema_Object* Object, Sch { Schema_Object* ComponentConstraintObject = Schema_GetObject(QueryConstraintObject, 8); - NewQueryConstraint.EntityIdConstraint = Schema_GetUint32(ComponentConstraintObject, 1); + NewQueryConstraint.ComponentConstraint = Schema_GetUint32(ComponentConstraintObject, 1); } // list and_constraint = 9; @@ -367,10 +380,10 @@ inline Query IndexQueryFromSchema(Schema_Object* Object, Schema_FieldId Id, uint } uint32 ResultComponentIdCount = Schema_GetObjectCount(QueryObject, 3); - NewQuery.ResultComponentId.Reserve(ResultComponentIdCount); + NewQuery.ResultComponentIds.Reserve(ResultComponentIdCount); for (uint32 ComponentIdIndex = 0; ComponentIdIndex < ResultComponentIdCount; ComponentIdIndex++) { - NewQuery.ResultComponentId.Add(Schema_IndexUint32(QueryObject, 3, ComponentIdIndex)); + NewQuery.ResultComponentIds.Add(Schema_IndexUint32(QueryObject, 3, ComponentIdIndex)); } if (Schema_GetObjectCount(QueryObject, 4) > 0) diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/MulticastRPCs.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/MulticastRPCs.h new file mode 100644 index 0000000000..820cc993b6 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/MulticastRPCs.h @@ -0,0 +1,30 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Schema/Component.h" +#include "SpatialConstants.h" +#include "Utils/RPCRingBuffer.h" + +#include +#include + +namespace SpatialGDK +{ + +struct MulticastRPCs : Component +{ + static const Worker_ComponentId ComponentId = SpatialConstants::MULTICAST_RPCS_COMPONENT_ID; + + MulticastRPCs(const Worker_ComponentData& Data); + + void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) override; + + RPCRingBuffer MulticastRPCBuffer; + uint32 InitiallyPresentMulticastRPCsCount = 0; + +private: + void ReadFromSchema(Schema_Object* SchemaObject); +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/NetOwningClientWorker.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/NetOwningClientWorker.h new file mode 100644 index 0000000000..46b2e29bb8 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/NetOwningClientWorker.h @@ -0,0 +1,95 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Schema/Component.h" +#include "SpatialCommonTypes.h" +#include "SpatialConstants.h" +#include "Utils/SchemaOption.h" +#include "Utils/SchemaUtils.h" + +#include +#include + +namespace SpatialGDK +{ + struct NetOwningClientWorker : Component + { + static const Worker_ComponentId ComponentId = SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID; + + NetOwningClientWorker() = default; + + NetOwningClientWorker(const TSchemaOption& InWorkerId) + : WorkerId(InWorkerId) {} + + NetOwningClientWorker(const Worker_ComponentData& Data) + { + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + if (Schema_GetBytesCount(ComponentObject, SpatialConstants::NET_OWNING_CLIENT_WORKER_FIELD_ID) == 1) + { + WorkerId = GetStringFromSchema(ComponentObject, SpatialConstants::NET_OWNING_CLIENT_WORKER_FIELD_ID); + } + } + + Worker_ComponentData CreateNetOwningClientWorkerData() + { + return CreateNetOwningClientWorkerData(WorkerId); + } + + static Worker_ComponentData CreateNetOwningClientWorkerData(const TSchemaOption& WorkerId) + { + Worker_ComponentData Data = {}; + Data.component_id = ComponentId; + Data.schema_type = Schema_CreateComponentData(); + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + + if (WorkerId.IsSet()) + { + AddStringToSchema(ComponentObject, SpatialConstants::NET_OWNING_CLIENT_WORKER_FIELD_ID, *WorkerId); + } + + return Data; + } + + Worker_ComponentUpdate CreateNetOwningClientWorkerUpdate() + { + return CreateNetOwningClientWorkerUpdate(WorkerId); + } + + static Worker_ComponentUpdate CreateNetOwningClientWorkerUpdate(const TSchemaOption& WorkerId) + { + Worker_ComponentUpdate Update = {}; + Update.component_id = ComponentId; + Update.schema_type = Schema_CreateComponentUpdate(); + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); + + if (WorkerId.IsSet()) + { + AddStringToSchema(ComponentObject, SpatialConstants::NET_OWNING_CLIENT_WORKER_FIELD_ID, *WorkerId); + } + else + { + Schema_AddComponentUpdateClearedField(Update.schema_type, SpatialConstants::NET_OWNING_CLIENT_WORKER_FIELD_ID); + } + + return Update; + } + + void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) + { + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); + if (Schema_GetBytesCount(ComponentObject, SpatialConstants::NET_OWNING_CLIENT_WORKER_FIELD_ID) == 1) + { + WorkerId = GetStringFromSchema(ComponentObject, SpatialConstants::NET_OWNING_CLIENT_WORKER_FIELD_ID); + } + else if (Schema_IsComponentUpdateFieldCleared(Update.schema_type, SpatialConstants::NET_OWNING_CLIENT_WORKER_FIELD_ID)) + { + WorkerId = TSchemaOption(); + } + } + + // Client worker ID corresponding to the owning net connection (if exists). + TSchemaOption WorkerId; + }; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/PlayerSpawner.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/PlayerSpawner.h new file mode 100644 index 0000000000..76ddbc94c3 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/PlayerSpawner.h @@ -0,0 +1,101 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Schema/Component.h" +#include "SpatialConstants.h" +#include "Utils/SchemaUtils.h" + +#include "Containers/UnrealString.h" +#include "Engine/EngineBaseTypes.h" +#include "Engine/GameInstance.h" +#include "GameFramework/OnlineReplStructs.h" +#include "Kismet/GameplayStatics.h" +#include "UObject/CoreNet.h" + +#include +#include + +namespace SpatialGDK +{ + +struct SpawnPlayerRequest +{ + FURL LoginURL; + FUniqueNetIdRepl UniqueId; + FName OnlinePlatformName; + bool bIsSimulatedPlayer; +}; + +struct PlayerSpawner : Component +{ + static const Worker_ComponentId ComponentId = SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID; + + PlayerSpawner() = default; + + static Worker_CommandRequest CreatePlayerSpawnRequest(SpawnPlayerRequest& SpawnRequest) + { + Worker_CommandRequest CommandRequest = {}; + CommandRequest.component_id = SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID; + CommandRequest.command_index = SpatialConstants::PLAYER_SPAWNER_SPAWN_PLAYER_COMMAND_ID; + CommandRequest.schema_type = Schema_CreateCommandRequest(); + + Schema_Object* RequestFields = Schema_GetCommandRequestObject(CommandRequest.schema_type); + AddSpawnPlayerData(RequestFields, SpawnRequest); + + return CommandRequest; + } + + static Worker_CommandResponse CreatePlayerSpawnResponse() + { + Worker_CommandResponse CommandResponse = {}; + CommandResponse.component_id = SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID; + CommandResponse.command_index = SpatialConstants::PLAYER_SPAWNER_SPAWN_PLAYER_COMMAND_ID; + CommandResponse.schema_type = Schema_CreateCommandResponse(); + return CommandResponse; + } + + static void AddSpawnPlayerData(Schema_Object* RequestObject, SpawnPlayerRequest& SpawnRequest) + { + AddStringToSchema(RequestObject, SpatialConstants::SPAWN_PLAYER_URL_ID, SpawnRequest.LoginURL.ToString(true)); + + // Write player identity information. + FNetBitWriter UniqueIdWriter(0); + UniqueIdWriter << SpawnRequest.UniqueId; + AddBytesToSchema(RequestObject, SpatialConstants::SPAWN_PLAYER_UNIQUE_ID, UniqueIdWriter); + AddStringToSchema(RequestObject, SpatialConstants::SPAWN_PLAYER_PLATFORM_NAME_ID, SpawnRequest.OnlinePlatformName.ToString()); + Schema_AddBool(RequestObject, SpatialConstants::SPAWN_PLAYER_IS_SIMULATED_ID, SpawnRequest.bIsSimulatedPlayer); + } + + static FURL ExtractUrlFromPlayerSpawnParams(const Schema_Object* Payload) + { + return FURL(nullptr, *GetStringFromSchema(Payload, SpatialConstants::SPAWN_PLAYER_URL_ID), TRAVEL_Absolute); + } + + static SpawnPlayerRequest ExtractPlayerSpawnParams(const Schema_Object* CommandRequestPayload) + { + const FURL LoginURL = ExtractUrlFromPlayerSpawnParams(CommandRequestPayload); + + FUniqueNetIdRepl UniqueId; + TArray UniqueIdBytes = GetBytesFromSchema(CommandRequestPayload, SpatialConstants::SPAWN_PLAYER_UNIQUE_ID); + FNetBitReader UniqueIdReader(nullptr, UniqueIdBytes.GetData(), UniqueIdBytes.Num() * 8); + UniqueIdReader << UniqueId; + + const FName OnlinePlatformName = FName(*GetStringFromSchema(CommandRequestPayload, SpatialConstants::SPAWN_PLAYER_PLATFORM_NAME_ID)); + + const bool bIsSimulated = GetBoolFromSchema(CommandRequestPayload, SpatialConstants::SPAWN_PLAYER_IS_SIMULATED_ID); + + return { LoginURL, UniqueId, OnlinePlatformName, bIsSimulated }; + } + + static void CopySpawnDataBetweenObjects(const Schema_Object* SpawnPlayerDataSource, Schema_Object* SpawnPlayerDataDestination) + { + AddStringToSchema(SpawnPlayerDataDestination, SpatialConstants::SPAWN_PLAYER_URL_ID, GetStringFromSchema(SpawnPlayerDataSource, SpatialConstants::SPAWN_PLAYER_URL_ID)); + TArray UniqueId = GetBytesFromSchema(SpawnPlayerDataSource, SpatialConstants::SPAWN_PLAYER_UNIQUE_ID); + AddBytesToSchema(SpawnPlayerDataDestination, SpatialConstants::SPAWN_PLAYER_UNIQUE_ID, UniqueId.GetData(), UniqueId.Num()); + AddStringToSchema(SpawnPlayerDataDestination, SpatialConstants::SPAWN_PLAYER_PLATFORM_NAME_ID, GetStringFromSchema(SpawnPlayerDataSource, SpatialConstants::SPAWN_PLAYER_PLATFORM_NAME_ID)); + Schema_AddBool(SpawnPlayerDataDestination, SpatialConstants::SPAWN_PLAYER_IS_SIMULATED_ID, GetBoolFromSchema(SpawnPlayerDataSource, SpatialConstants::SPAWN_PLAYER_IS_SIMULATED_ID)); + } +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/RPCPayload.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/RPCPayload.h index eaa8ab1df4..42a89c2316 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/RPCPayload.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/RPCPayload.h @@ -5,6 +5,7 @@ #include "Schema/Component.h" #include "SpatialConstants.h" #include "Utils/SchemaUtils.h" +#include "Utils/SpatialLatencyTracer.h" #include #include @@ -16,14 +17,25 @@ struct RPCPayload { RPCPayload() = delete; - RPCPayload(uint32 InOffset, uint32 InIndex, TArray&& Data) : Offset(InOffset), Index(InIndex), PayloadData(MoveTemp(Data)) + RPCPayload(uint32 InOffset, uint32 InIndex, TArray&& Data, TraceKey InTraceKey = InvalidTraceKey) + : Offset(InOffset) + , Index(InIndex) + , PayloadData(MoveTemp(Data)) + , Trace(InTraceKey) {} - RPCPayload(const Schema_Object* RPCObject) + RPCPayload(Schema_Object* RPCObject) { Offset = Schema_GetUint32(RPCObject, SpatialConstants::UNREAL_RPC_PAYLOAD_OFFSET_ID); Index = Schema_GetUint32(RPCObject, SpatialConstants::UNREAL_RPC_PAYLOAD_RPC_INDEX_ID); PayloadData = SpatialGDK::GetBytesFromSchema(RPCObject, SpatialConstants::UNREAL_RPC_PAYLOAD_RPC_PAYLOAD_ID); + +#if TRACE_LIB_ACTIVE + if (USpatialLatencyTracer* Tracer = USpatialLatencyTracer::GetTracer(nullptr)) + { + Trace = Tracer->ReadTraceFromSchemaObject(RPCObject, SpatialConstants::UNREAL_RPC_PAYLOAD_TRACE_ID); + } +#endif } int64 CountDataBits() const @@ -31,6 +43,18 @@ struct RPCPayload return PayloadData.Num() * 8; } + void WriteToSchemaObject(Schema_Object* RPCObject) const + { + WriteToSchemaObject(RPCObject, Offset, Index, PayloadData.GetData(), PayloadData.Num()); + +#if TRACE_LIB_ACTIVE + if (USpatialLatencyTracer* Tracer = USpatialLatencyTracer::GetTracer(nullptr)) + { + Tracer->WriteTraceToSchemaObject(Trace, RPCObject, SpatialConstants::UNREAL_RPC_PAYLOAD_TRACE_ID); + } +#endif + } + static void WriteToSchemaObject(Schema_Object* RPCObject, uint32 Offset, uint32 Index, const uint8* Data, int32 NumElems) { Schema_AddUint32(RPCObject, SpatialConstants::UNREAL_RPC_PAYLOAD_OFFSET_ID, Offset); @@ -41,6 +65,7 @@ struct RPCPayload uint32 Offset; uint32 Index; TArray PayloadData; + TraceKey Trace = InvalidTraceKey; }; struct RPCsOnEntityCreation : Component diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerEndpoint.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerEndpoint.h new file mode 100644 index 0000000000..3a4255069d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerEndpoint.h @@ -0,0 +1,32 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Schema/Component.h" +#include "SpatialConstants.h" +#include "Utils/RPCRingBuffer.h" + +#include +#include + +namespace SpatialGDK +{ + +struct ServerEndpoint : Component +{ + static const Worker_ComponentId ComponentId = SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID; + + ServerEndpoint(const Worker_ComponentData& Data); + + void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) override; + + RPCRingBuffer ReliableRPCBuffer; + RPCRingBuffer UnreliableRPCBuffer; + uint64 ReliableRPCAck = 0; + uint64 UnreliableRPCAck = 0; + +private: + void ReadFromSchema(Schema_Object* SchemaObject); +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerRPCEndpoint.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerRPCEndpointLegacy.h similarity index 90% rename from SpatialGDK/Source/SpatialGDK/Public/Schema/ServerRPCEndpoint.h rename to SpatialGDK/Source/SpatialGDK/Public/Schema/ServerRPCEndpointLegacy.h index e9a1641e75..7f8fc0be9b 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerRPCEndpoint.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerRPCEndpointLegacy.h @@ -12,13 +12,13 @@ namespace SpatialGDK { -struct ServerRPCEndpoint : Component +struct ServerRPCEndpointLegacy : Component { - static const Worker_ComponentId ComponentId = SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID; + static const Worker_ComponentId ComponentId = SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY; - ServerRPCEndpoint() = default; + ServerRPCEndpointLegacy() = default; - ServerRPCEndpoint(const Worker_ComponentData& Data) + ServerRPCEndpointLegacy(const Worker_ComponentData& Data) { Schema_Object* EndpointObject = Schema_GetComponentDataFields(Data.schema_type); bReady = GetBoolFromSchema(EndpointObject, SpatialConstants::UNREAL_RPC_ENDPOINT_READY_ID); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerWorker.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerWorker.h new file mode 100644 index 0000000000..a3a7ffbe71 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerWorker.h @@ -0,0 +1,118 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Schema/Component.h" +#include "Schema/PlayerSpawner.h" +#include "SpatialCommonTypes.h" +#include "SpatialConstants.h" +#include "Utils/SchemaUtils.h" + +#include "Containers/UnrealString.h" + +#include +#include + +namespace SpatialGDK +{ + +// The ServerWorker component exists to hold the physical worker name corresponding to a +// server worker entity. This is so that the translator can make virtual workers to physical +// worker names using the server worker entities. +struct ServerWorker : Component +{ + static const Worker_ComponentId ComponentId = SpatialConstants::SERVER_WORKER_COMPONENT_ID; + + ServerWorker() + : WorkerName(SpatialConstants::INVALID_WORKER_NAME) + , bReadyToBeginPlay(false) + {} + + ServerWorker(const PhysicalWorkerName& InWorkerName, const bool bInReadyToBeginPlay) + { + WorkerName = InWorkerName; + bReadyToBeginPlay = bInReadyToBeginPlay; + } + + ServerWorker(const Worker_ComponentData& Data) + { + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + + WorkerName = GetStringFromSchema(ComponentObject, SpatialConstants::SERVER_WORKER_NAME_ID); + bReadyToBeginPlay = GetBoolFromSchema(ComponentObject, SpatialConstants::SERVER_WORKER_READY_TO_BEGIN_PLAY_ID); + } + + Worker_ComponentData CreateServerWorkerData() + { + Worker_ComponentData Data = {}; + Data.component_id = ComponentId; + Data.schema_type = Schema_CreateComponentData(); + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + + AddStringToSchema(ComponentObject, SpatialConstants::SERVER_WORKER_NAME_ID, WorkerName); + Schema_AddBool(ComponentObject, SpatialConstants::SERVER_WORKER_READY_TO_BEGIN_PLAY_ID, bReadyToBeginPlay); + + return Data; + } + + Worker_ComponentUpdate CreateServerWorkerUpdate() + { + Worker_ComponentUpdate Update = {}; + Update.component_id = ComponentId; + Update.schema_type = Schema_CreateComponentUpdate(); + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); + + AddStringToSchema(ComponentObject, SpatialConstants::SERVER_WORKER_NAME_ID, WorkerName); + Schema_AddBool(ComponentObject, SpatialConstants::SERVER_WORKER_READY_TO_BEGIN_PLAY_ID, bReadyToBeginPlay); + + return Update; + } + + void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) + { + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); + + WorkerName = GetStringFromSchema(ComponentObject, SpatialConstants::SERVER_WORKER_NAME_ID); + bReadyToBeginPlay = GetBoolFromSchema(ComponentObject, SpatialConstants::SERVER_WORKER_READY_TO_BEGIN_PLAY_ID); + } + + static Worker_CommandRequest CreateForwardPlayerSpawnRequest(Schema_CommandRequest* SchemaCommandRequest) + { + Worker_CommandRequest CommandRequest = {}; + CommandRequest.component_id = SpatialConstants::SERVER_WORKER_COMPONENT_ID; + CommandRequest.command_index = SpatialConstants::SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND_ID; + CommandRequest.schema_type = SchemaCommandRequest; + return CommandRequest; + } + + static Worker_CommandResponse CreateForwardPlayerSpawnResponse(const bool bSuccess) + { + Worker_CommandResponse CommandResponse = {}; + CommandResponse.component_id = SpatialConstants::SERVER_WORKER_COMPONENT_ID; + CommandResponse.command_index = SpatialConstants::SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND_ID; + CommandResponse.schema_type = Schema_CreateCommandResponse(); + Schema_Object* ResponseObject = Schema_GetCommandResponseObject(CommandResponse.schema_type); + + Schema_AddBool(ResponseObject, SpatialConstants::FORWARD_SPAWN_PLAYER_RESPONSE_SUCCESS_ID, bSuccess); + + return CommandResponse; + } + + static void CreateForwardPlayerSpawnSchemaRequest(Schema_CommandRequest* Request, const FUnrealObjectRef& PlayerStartObjectRef, const Schema_Object* OriginalPlayerSpawnRequest, const PhysicalWorkerName& ClientWorkerID) + { + Schema_Object* RequestFields = Schema_GetCommandRequestObject(Request); + + AddObjectRefToSchema(RequestFields, SpatialConstants::FORWARD_SPAWN_PLAYER_START_ACTOR_ID, PlayerStartObjectRef); + + Schema_Object* PlayerSpawnData = Schema_AddObject(RequestFields, SpatialConstants::FORWARD_SPAWN_PLAYER_DATA_ID); + PlayerSpawner::CopySpawnDataBetweenObjects(OriginalPlayerSpawnRequest, PlayerSpawnData); + + AddStringToSchema(RequestFields, SpatialConstants::FORWARD_SPAWN_PLAYER_CLIENT_WORKER_ID, ClientWorkerID); + } + + PhysicalWorkerName WorkerName; + bool bReadyToBeginPlay; +}; + +} // namespace SpatialGDK + diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/SpatialDebugging.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/SpatialDebugging.h new file mode 100644 index 0000000000..9173405943 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/SpatialDebugging.h @@ -0,0 +1,110 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Schema/Component.h" +#include "SpatialCommonTypes.h" +#include "Utils/SchemaUtils.h" + +#include + +namespace SpatialGDK +{ + +// The SpatialDebugging component exists to hold information which needs to be displayed by the +// SpatialDebugger on clients but which would not normally be available to clients. +struct SpatialDebugging : Component +{ + static const Worker_ComponentId ComponentId = SpatialConstants::SPATIAL_DEBUGGING_COMPONENT_ID; + + SpatialDebugging() + : AuthoritativeVirtualWorkerId(SpatialConstants::INVALID_VIRTUAL_WORKER_ID) + , AuthoritativeColor() + , IntentVirtualWorkerId(SpatialConstants::INVALID_VIRTUAL_WORKER_ID) + , IntentColor() + , IsLocked(false) + {} + + SpatialDebugging(const VirtualWorkerId AuthoritativeVirtualWorkerIdIn, const FColor& AuthoritativeColorIn, const VirtualWorkerId IntentVirtualWorkerIdIn, const FColor& IntentColorIn, bool IsLockedIn) + { + AuthoritativeVirtualWorkerId = AuthoritativeVirtualWorkerIdIn; + AuthoritativeColor = AuthoritativeColorIn; + IntentVirtualWorkerId = IntentVirtualWorkerIdIn; + IntentColor = IntentColorIn; + IsLocked = IsLockedIn; + } + + SpatialDebugging(const Worker_ComponentData& Data) + { + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + + AuthoritativeVirtualWorkerId = Schema_GetUint32(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_AUTHORITATIVE_VIRTUAL_WORKER_ID); + AuthoritativeColor = FColor(Schema_GetUint32(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_AUTHORITATIVE_COLOR)); + IntentVirtualWorkerId = Schema_GetUint32(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_INTENT_VIRTUAL_WORKER_ID); + IntentColor = FColor(Schema_GetUint32(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_INTENT_COLOR)); + IsLocked = Schema_GetBool(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_IS_LOCKED) != 0; + } + + Worker_ComponentData CreateSpatialDebuggingData() + { + Worker_ComponentData Data = {}; + Data.component_id = ComponentId; + Data.schema_type = Schema_CreateComponentData(); + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + + Schema_AddUint32(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_AUTHORITATIVE_VIRTUAL_WORKER_ID, AuthoritativeVirtualWorkerId); + Schema_AddUint32(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_AUTHORITATIVE_COLOR, AuthoritativeColor.DWColor()); + Schema_AddUint32(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_INTENT_VIRTUAL_WORKER_ID, IntentVirtualWorkerId); + Schema_AddUint32(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_INTENT_COLOR, IntentColor.DWColor()); + Schema_AddBool(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_IS_LOCKED, IsLocked); + + return Data; + } + + Worker_ComponentUpdate CreateSpatialDebuggingUpdate() + { + Worker_ComponentUpdate Update = {}; + Update.component_id = ComponentId; + Update.schema_type = Schema_CreateComponentUpdate(); + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); + + Schema_AddUint32(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_AUTHORITATIVE_VIRTUAL_WORKER_ID, AuthoritativeVirtualWorkerId); + Schema_AddUint32(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_AUTHORITATIVE_COLOR, AuthoritativeColor.DWColor()); + Schema_AddUint32(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_INTENT_VIRTUAL_WORKER_ID, IntentVirtualWorkerId); + Schema_AddUint32(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_INTENT_COLOR, IntentColor.DWColor()); + Schema_AddBool(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_IS_LOCKED, IsLocked); + + return Update; + } + + void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) + { + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); + + AuthoritativeVirtualWorkerId = Schema_GetUint32(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_AUTHORITATIVE_VIRTUAL_WORKER_ID); + AuthoritativeColor = FColor(Schema_GetUint32(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_AUTHORITATIVE_COLOR)); + IntentVirtualWorkerId = Schema_GetUint32(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_INTENT_VIRTUAL_WORKER_ID); + IntentColor = FColor(Schema_GetUint32(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_INTENT_COLOR)); + IsLocked = Schema_GetBool(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_IS_LOCKED) != 0; + } + + // Id of the Unreal server worker which is authoritative for the entity. + // 0 is reserved as an invalid/unset value. + VirtualWorkerId AuthoritativeVirtualWorkerId; + + // The color for the authoritative virtual worker. + FColor AuthoritativeColor; + + // Id of the Unreal server worker which should be authoritative for the entity. + // 0 is reserved as an invalid/unset value. + VirtualWorkerId IntentVirtualWorkerId; + + // The color for the intended virtual worker. + FColor IntentColor; + + // Whether or not the entity is locked. + bool IsLocked; +}; + +} // namespace SpatialGDK + diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/StandardLibrary.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/StandardLibrary.h index f405442d71..0f2df9b9f5 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/StandardLibrary.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/StandardLibrary.h @@ -42,9 +42,14 @@ struct Coordinates return Location; } + + inline bool operator!=(const Coordinates& Right) const + { + return X != Right.X || Y != Right.Y || Z != Right.Z; + } }; -static const Coordinates Origin{ 0, 0, 0 }; +static const Coordinates DeploymentOrigin{ 0, 0, 0 }; inline void AddCoordinateToSchema(Schema_Object* Object, Schema_FieldId Id, const Coordinates& Coordinate) { @@ -55,9 +60,9 @@ inline void AddCoordinateToSchema(Schema_Object* Object, Schema_FieldId Id, cons Schema_AddDouble(CoordsObject, 3, Coordinate.Z); } -inline Coordinates GetCoordinateFromSchema(Schema_Object* Object, Schema_FieldId Id) +inline Coordinates IndexCoordinateFromSchema(Schema_Object* Object, Schema_FieldId Id, uint32 Index) { - Schema_Object* CoordsObject = Schema_GetObject(Object, Id); + Schema_Object* CoordsObject = Schema_IndexObject(Object, Id, Index); Coordinates Coordinate; Coordinate.X = Schema_GetDouble(CoordsObject, 1); @@ -67,6 +72,11 @@ inline Coordinates GetCoordinateFromSchema(Schema_Object* Object, Schema_FieldId return Coordinate; } +inline Coordinates GetCoordinateFromSchema(Schema_Object* Object, Schema_FieldId Id) +{ + return IndexCoordinateFromSchema(Object, Id, 0); +} + struct EntityAcl : Component { static const Worker_ComponentId ComponentId = SpatialConstants::ENTITY_ACL_COMPONENT_ID; @@ -262,4 +272,48 @@ struct Persistence : Component } }; +struct Connection +{ + enum ConnectionStatus + { + UNKNOWN = 0, + // The worker requested a bridge from the receptionist, but the bridge has not yet had the worker connect to it. + AWAITING_WORKER_CONNECTION = 1, + // The worker is connected to the bridge as normal. + CONNECTED = 2, + // A worker was connected at one point, but is no longer connected. Currently, reconnecting is unsupported. + DISCONNECTED = 3 + }; + + void ReadConnectionData(Schema_Object* Object) + { + Status = (ConnectionStatus)Schema_GetUint32(Object, 1); + DataLatencyMs = Schema_GetUint32(Object, 2); + ConnectedSinceUtc = Schema_GetUint64(Object, 3); + } + + ConnectionStatus Status; + uint32 DataLatencyMs; + uint64 ConnectedSinceUtc; +}; + +struct Worker : Component +{ + static const Worker_ComponentId ComponentId = SpatialConstants::WORKER_COMPONENT_ID; + + Worker() = default; + Worker(const Worker_ComponentData& Data) + { + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + + WorkerId = GetStringFromSchema(ComponentObject, 1); + WorkerType = GetStringFromSchema(ComponentObject, 2); + Connection.ReadConnectionData(Schema_GetObject(ComponentObject, 3)); + } + + FString WorkerId; + FString WorkerType; + Connection Connection; +}; + } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealMetadata.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealMetadata.h index 0971c1fb39..ab9f7ab6d1 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealMetadata.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealMetadata.h @@ -2,18 +2,22 @@ #pragma once -#include "GameFramework/Actor.h" #include "Interop/SpatialClassInfoManager.h" #include "Schema/Component.h" #include "Schema/UnrealObjectRef.h" #include "SpatialConstants.h" -#include "UObject/Package.h" -#include "UObject/UObjectHash.h" +#include "SpatialGDKSettings.h" #include "Utils/SchemaUtils.h" +#include "GameFramework/Actor.h" +#include "UObject/UObjectHash.h" +#include "UObject/Package.h" + #include #include +DEFINE_LOG_CATEGORY_STATIC(LogSpatialUnrealMetadata, Warning, All); + using SubobjectToOffsetMap = TMap; namespace SpatialGDK @@ -25,23 +29,22 @@ struct UnrealMetadata : Component UnrealMetadata() = default; - UnrealMetadata(const TSchemaOption& InStablyNamedRef, const FString& InOwnerWorkerAttribute, const FString& InClassPath, const TSchemaOption& InbNetStartup) - : StablyNamedRef(InStablyNamedRef), OwnerWorkerAttribute(InOwnerWorkerAttribute), ClassPath(InClassPath), bNetStartup(InbNetStartup) {} + UnrealMetadata(const TSchemaOption& InStablyNamedRef, const FString& InClassPath, const TSchemaOption& InbNetStartup) + : StablyNamedRef(InStablyNamedRef), ClassPath(InClassPath), bNetStartup(InbNetStartup) {} UnrealMetadata(const Worker_ComponentData& Data) { Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); - if (Schema_GetObjectCount(ComponentObject, 1) == 1) + if (Schema_GetObjectCount(ComponentObject, SpatialConstants::UNREAL_METADATA_STABLY_NAMED_REF_ID) == 1) { - StablyNamedRef = GetObjectRefFromSchema(ComponentObject, 1); + StablyNamedRef = GetObjectRefFromSchema(ComponentObject, SpatialConstants::UNREAL_METADATA_STABLY_NAMED_REF_ID); } - OwnerWorkerAttribute = GetStringFromSchema(ComponentObject, 2); - ClassPath = GetStringFromSchema(ComponentObject, 3); + ClassPath = GetStringFromSchema(ComponentObject, SpatialConstants::UNREAL_METADATA_CLASS_PATH_ID); - if (Schema_GetBoolCount(ComponentObject, 4) == 1) + if (Schema_GetBoolCount(ComponentObject, SpatialConstants::UNREAL_METADATA_NET_STARTUP_ID) == 1) { - bNetStartup = GetBoolFromSchema(ComponentObject, 4); + bNetStartup = GetBoolFromSchema(ComponentObject, SpatialConstants::UNREAL_METADATA_NET_STARTUP_ID); } } @@ -54,13 +57,12 @@ struct UnrealMetadata : Component if (StablyNamedRef.IsSet()) { - AddObjectRefToSchema(ComponentObject, 1, StablyNamedRef.GetValue()); + AddObjectRefToSchema(ComponentObject, SpatialConstants::UNREAL_METADATA_STABLY_NAMED_REF_ID, StablyNamedRef.GetValue()); } - AddStringToSchema(ComponentObject, 2, OwnerWorkerAttribute); - AddStringToSchema(ComponentObject, 3, ClassPath); + AddStringToSchema(ComponentObject, SpatialConstants::UNREAL_METADATA_CLASS_PATH_ID, ClassPath); if (bNetStartup.IsSet()) { - Schema_AddBool(ComponentObject, 4, bNetStartup.GetValue()); + Schema_AddBool(ComponentObject, SpatialConstants::UNREAL_METADATA_NET_STARTUP_ID, bNetStartup.GetValue()); } return Data; @@ -76,17 +78,20 @@ struct UnrealMetadata : Component #if !UE_BUILD_SHIPPING if (NativeClass.IsStale()) { - UE_LOG(LogSpatialClassInfoManager, Warning, TEXT("UnrealMetadata native class %s unloaded whilst entity in view."), *ClassPath); + UE_LOG(LogSpatialUnrealMetadata, Warning, TEXT("UnrealMetadata native class %s unloaded whilst entity in view."), *ClassPath); } #endif - UClass* Class = nullptr; + UClass* Class = FindObject(nullptr, *ClassPath, false); - if (StablyNamedRef.IsSet()) - { - Class = FindObject(nullptr, *ClassPath, false); - } - else + // Unfortunately StablyNameRef doesn't mean NameStableForNetworking as we add a StablyNameRef for every startup actor (see USpatialSender::CreateEntity) + // TODO: UNR-2537 Investigate why FindObject can be used the first time the actor comes into view for a client but not subsequent loads. + if (Class == nullptr && !(StablyNamedRef.IsSet() && bNetStartup.IsSet() && bNetStartup.GetValue())) { + if (GetDefault()->bAsyncLoadNewClassesOnEntityCheckout) + { + UE_LOG(LogSpatialUnrealMetadata, Warning, TEXT("Class couldn't be found even though async loading on entity checkout is enabled. Will attempt to load it synchronously. Class: %s"), *ClassPath); + } + Class = LoadObject(nullptr, *ClassPath); } @@ -100,7 +105,6 @@ struct UnrealMetadata : Component } TSchemaOption StablyNamedRef; - FString OwnerWorkerAttribute; FString ClassPath; TSchemaOption bNetStartup; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h index 71fb71887d..a1ac848edd 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h @@ -1,3 +1,5 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + #pragma once #include "CoreMinimal.h" @@ -70,7 +72,9 @@ struct SPATIALGDK_API FUnrealObjectRef } static UObject* ToObjectPtr(const FUnrealObjectRef& ObjectRef, USpatialPackageMapClient* PackageMap, bool& bOutUnresolved); + static FSoftObjectPath ToSoftObjectPath(const FUnrealObjectRef& ObjectRef); static FUnrealObjectRef FromObjectPtr(UObject* ObjectValue, USpatialPackageMapClient* PackageMap); + static FUnrealObjectRef FromSoftObjectPath(const FSoftObjectPath& ObjectPath); static FUnrealObjectRef GetSingletonClassRef(UObject* SingletonObject, USpatialPackageMapClient* PackageMap); static const FUnrealObjectRef NULL_OBJECT_REF; @@ -95,3 +99,5 @@ inline uint32 GetTypeHash(const FUnrealObjectRef& ObjectRef) Result = (Result * 977u) + GetTypeHash(ObjectRef.bUseSingletonClassPath ? 1 : 0); return Result; } + +using ObjectPtrRefPair = TPair; diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialCommonTypes.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialCommonTypes.h index ebbc1da3b5..8c1f67c497 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialCommonTypes.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialCommonTypes.h @@ -12,11 +12,37 @@ using Worker_EntityId_Key = int64; using Worker_RequestId_Key = int64; +using VirtualWorkerId = uint32; +using PhysicalWorkerName = FString; +using ActorLockToken = int64; +using TraceKey = int32; +constexpr TraceKey InvalidTraceKey{ -1 }; + using WorkerAttributeSet = TArray; using WorkerRequirementSet = TArray; using WriteAclMap = TMap; using FChannelObjectPair = TPair, TWeakObjectPtr>; -struct FObjectReferences; -using FObjectReferencesMap = TMap; +using FObjectReferencesMap = TMap; using FReliableRPCMap = TMap>; + +using FObjectToRepStateMap = TMap >; + +template +struct FTrackableWorkerType : public T +{ + FTrackableWorkerType() = default; + + FTrackableWorkerType(const T& Update) + : T(Update) {} + + FTrackableWorkerType(T&& Update) + : T(MoveTemp(Update)) {} + +#if TRACE_LIB_ACTIVE + TraceKey Trace{ InvalidTraceKey }; +#endif +}; + +using FWorkerComponentUpdate = FTrackableWorkerType; +using FWorkerComponentData = FTrackableWorkerType; diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h index 2506c6b5b4..8a675b9f72 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h @@ -10,6 +10,20 @@ #include #include +#include "SpatialConstants.generated.h" + +UENUM() +enum class ERPCType : uint8 +{ + Invalid, + ClientReliable, + ClientUnreliable, + ServerReliable, + ServerUnreliable, + NetMulticast, + CrossServer +}; + enum ESchemaComponentType : int32 { SCHEMA_Invalid = -1, @@ -21,58 +35,28 @@ enum ESchemaComponentType : int32 SCHEMA_Count, - // RPCs - SCHEMA_ClientReliableRPC, - SCHEMA_ClientUnreliableRPC, - SCHEMA_ServerReliableRPC, - SCHEMA_ServerUnreliableRPC, - SCHEMA_NetMulticastRPC, - SCHEMA_CrossServerRPC, - - // Iteration helpers SCHEMA_Begin = SCHEMA_Data, }; -FORCEINLINE ESchemaComponentType FunctionFlagsToRPCSchemaType(EFunctionFlags FunctionFlags) +namespace SpatialConstants { - if (FunctionFlags & FUNC_NetClient) - { - return SCHEMA_ClientReliableRPC; - } - else if (FunctionFlags & FUNC_NetServer) - { - return SCHEMA_ServerReliableRPC; - } - else if (FunctionFlags & FUNC_NetMulticast) - { - return SCHEMA_NetMulticastRPC; - } - else if (FunctionFlags & FUNC_NetCrossServer) - { - return SCHEMA_CrossServerRPC; - } - else - { - return SCHEMA_Invalid; - } -} -FORCEINLINE FString RPCSchemaTypeToString(ESchemaComponentType RPCType) +inline FString RPCTypeToString(ERPCType RPCType) { switch (RPCType) { - case SCHEMA_ClientReliableRPC: + case ERPCType::ClientReliable: return TEXT("Client, Reliable"); - case SCHEMA_ClientUnreliableRPC: + case ERPCType::ClientUnreliable: return TEXT("Client, Unreliable"); - case SCHEMA_ServerReliableRPC: + case ERPCType::ServerReliable: return TEXT("Server, Reliable"); - case SCHEMA_ServerUnreliableRPC: + case ERPCType::ServerUnreliable: return TEXT("Server, Unreliable"); - case SCHEMA_NetMulticastRPC: + case ERPCType::NetMulticast: return TEXT("Multicast"); - case SCHEMA_CrossServerRPC: + case ERPCType::CrossServer: return TEXT("CrossServer"); } @@ -80,189 +64,339 @@ FORCEINLINE FString RPCSchemaTypeToString(ESchemaComponentType RPCType) return FString(); } -namespace SpatialConstants +enum EntityIds { - enum EntityIds - { - INVALID_ENTITY_ID = 0, - INITIAL_SPAWNER_ENTITY_ID = 1, - INITIAL_GLOBAL_STATE_MANAGER_ENTITY_ID = 2, - INITIAL_VIRTUAL_WORKER_TRANSLATOR_ENTITY_ID = 3, - FIRST_AVAILABLE_ENTITY_ID = 4, - }; - - const Worker_ComponentId INVALID_COMPONENT_ID = 0; - - const Worker_ComponentId ENTITY_ACL_COMPONENT_ID = 50; - const Worker_ComponentId METADATA_COMPONENT_ID = 53; - const Worker_ComponentId POSITION_COMPONENT_ID = 54; - const Worker_ComponentId PERSISTENCE_COMPONENT_ID = 55; - const Worker_ComponentId INTEREST_COMPONENT_ID = 58; - // This is a component on per-worker system entities. - const Worker_ComponentId WORKER_COMPONENT_ID = 60; - - const Worker_ComponentId MAX_RESERVED_SPATIAL_SYSTEM_COMPONENT_ID = 100; - - const Worker_ComponentId SPAWN_DATA_COMPONENT_ID = 9999; - const Worker_ComponentId PLAYER_SPAWNER_COMPONENT_ID = 9998; - const Worker_ComponentId SINGLETON_COMPONENT_ID = 9997; - const Worker_ComponentId UNREAL_METADATA_COMPONENT_ID = 9996; - const Worker_ComponentId SINGLETON_MANAGER_COMPONENT_ID = 9995; - const Worker_ComponentId DEPLOYMENT_MAP_COMPONENT_ID = 9994; - const Worker_ComponentId STARTUP_ACTOR_MANAGER_COMPONENT_ID = 9993; - const Worker_ComponentId GSM_SHUTDOWN_COMPONENT_ID = 9992; - const Worker_ComponentId HEARTBEAT_COMPONENT_ID = 9991; - const Worker_ComponentId CLIENT_RPC_ENDPOINT_COMPONENT_ID = 9990; - const Worker_ComponentId SERVER_RPC_ENDPOINT_COMPONENT_ID = 9989; - const Worker_ComponentId NETMULTICAST_RPCS_COMPONENT_ID = 9987; - const Worker_ComponentId NOT_STREAMED_COMPONENT_ID = 9986; - const Worker_ComponentId RPCS_ON_ENTITY_CREATION_ID = 9985; - const Worker_ComponentId DEBUG_METRICS_COMPONENT_ID = 9984; - const Worker_ComponentId ALWAYS_RELEVANT_COMPONENT_ID = 9983; - const Worker_ComponentId TOMBSTONE_COMPONENT_ID = 9982; - const Worker_ComponentId DORMANT_COMPONENT_ID = 9981; - const Worker_ComponentId AUTHORITY_INTENT_COMPONENT_ID = 9980; - const Worker_ComponentId VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID = 9979; - - const Worker_ComponentId STARTING_GENERATED_COMPONENT_ID = 10000; - - const Schema_FieldId SINGLETON_MANAGER_SINGLETON_NAME_TO_ENTITY_ID = 1; - - const Schema_FieldId DEPLOYMENT_MAP_MAP_URL_ID = 1; - const Schema_FieldId DEPLOYMENT_MAP_ACCEPTING_PLAYERS_ID = 2; - - const Schema_FieldId STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_ID = 1; - - const Schema_FieldId ACTOR_COMPONENT_REPLICATES_ID = 1; - const Schema_FieldId ACTOR_TEAROFF_ID = 3; - - const Schema_FieldId HEARTBEAT_EVENT_ID = 1; - const Schema_FieldId HEARTBEAT_CLIENT_HAS_QUIT_ID = 1; - - const Schema_FieldId SHUTDOWN_MULTI_PROCESS_REQUEST_ID = 1; - const Schema_FieldId SHUTDOWN_ADDITIONAL_SERVERS_EVENT_ID = 1; - - const Schema_FieldId CLEAR_RPCS_ON_ENTITY_CREATION = 1; - - // DebugMetrics command IDs - const Schema_FieldId DEBUG_METRICS_START_RPC_METRICS_ID = 1; - const Schema_FieldId DEBUG_METRICS_STOP_RPC_METRICS_ID = 2; - const Schema_FieldId DEBUG_METRICS_MODIFY_SETTINGS_ID = 3; - - // ModifySettingPayload Field IDs - const Schema_FieldId MODIFY_SETTING_PAYLOAD_NAME_ID = 1; - const Schema_FieldId MODIFY_SETTING_PAYLOAD_VALUE_ID = 2; - - // UnrealObjectRef Field IDs - const Schema_FieldId UNREAL_OBJECT_REF_ENTITY_ID = 1; - const Schema_FieldId UNREAL_OBJECT_REF_OFFSET_ID = 2; - const Schema_FieldId UNREAL_OBJECT_REF_PATH_ID = 3; - const Schema_FieldId UNREAL_OBJECT_REF_NO_LOAD_ON_CLIENT_ID = 4; - const Schema_FieldId UNREAL_OBJECT_REF_OUTER_ID = 5; - const Schema_FieldId UNREAL_OBJECT_REF_USE_SINGLETON_CLASS_PATH_ID = 6; - - // UnrealRPCPayload Field IDs - const Schema_FieldId UNREAL_RPC_PAYLOAD_OFFSET_ID = 1; - const Schema_FieldId UNREAL_RPC_PAYLOAD_RPC_INDEX_ID = 2; - const Schema_FieldId UNREAL_RPC_PAYLOAD_RPC_PAYLOAD_ID = 3; - // UnrealPackedRPCPayload additional Field ID - const Schema_FieldId UNREAL_PACKED_RPC_PAYLOAD_ENTITY_ID = 4; - - // Unreal(Client|Server|Multicast)RPCEndpoint Field IDs - const Schema_FieldId UNREAL_RPC_ENDPOINT_READY_ID = 1; - const Schema_FieldId UNREAL_RPC_ENDPOINT_EVENT_ID = 1; - const Schema_FieldId UNREAL_RPC_ENDPOINT_PACKED_EVENT_ID = 2; - const Schema_FieldId UNREAL_RPC_ENDPOINT_COMMAND_ID = 1; - - const Schema_FieldId PLAYER_SPAWNER_SPAWN_PLAYER_COMMAND_ID = 1; - - // AuthorityIntent codes and Field IDs. - const Schema_FieldId AUTHORITY_INTENT_VIRTUAL_WORKER_ID = 1; - const uint32 INVALID_VIRTUAL_WORKER_ID = 0; - - // VirtualWorkerTranslation Field IDs. - const Schema_FieldId VIRTUAL_WORKER_TRANSLATION_MAPPING_ID = 1; - const Schema_FieldId MAPPING_VIRTUAL_WORKER_ID = 1; - const Schema_FieldId MAPPING_PHYSICAL_WORKER_NAME = 2; - - // WorkerEntity Field IDs. - const Schema_FieldId WORKER_ID_ID = 1; - const Schema_FieldId WORKER_TYPE_ID = 2; - - // Reserved entity IDs expire in 5 minutes, we will refresh them every 3 minutes to be safe. - const float ENTITY_RANGE_EXPIRATION_INTERVAL_SECONDS = 180.0f; - - const float FIRST_COMMAND_RETRY_WAIT_SECONDS = 0.2f; - const uint32 MAX_NUMBER_COMMAND_ATTEMPTS = 5u; - - static const FName DefaultActorGroup = FName(TEXT("Default")); - - const WorkerAttributeSet UnrealServerAttributeSet = TArray{DefaultServerWorkerType.ToString()}; - const WorkerAttributeSet UnrealClientAttributeSet = TArray{DefaultClientWorkerType.ToString()}; - - const WorkerRequirementSet UnrealServerPermission{ {UnrealServerAttributeSet} }; - const WorkerRequirementSet UnrealClientPermission{ {UnrealClientAttributeSet} }; - const WorkerRequirementSet ClientOrServerPermission{ {UnrealClientAttributeSet, UnrealServerAttributeSet} }; - - static const FString ClientsStayConnectedURLOption = TEXT("clientsStayConnected"); - static const FString SnapshotURLOption = TEXT("snapshot="); - - static const FString AssemblyPattern = TEXT("^[a-zA-Z0-9_.-]{5,64}$"); - static const FString ProjectPattern = TEXT("^[a-z0-9_]{3,32}$"); - static const FString DeploymentPattern = TEXT("^[a-z0-9_]{2,32}$"); + INVALID_ENTITY_ID = 0, + INITIAL_SPAWNER_ENTITY_ID = 1, + INITIAL_GLOBAL_STATE_MANAGER_ENTITY_ID = 2, + INITIAL_VIRTUAL_WORKER_TRANSLATOR_ENTITY_ID = 3, + FIRST_AVAILABLE_ENTITY_ID = 4, +}; - inline float GetCommandRetryWaitTimeSeconds(uint32 NumAttempts) - { - // Double the time to wait on each failure. - uint32 WaitTimeExponentialFactor = 1u << (NumAttempts - 1); - return FIRST_COMMAND_RETRY_WAIT_SECONDS * WaitTimeExponentialFactor; - } +const Worker_ComponentId INVALID_COMPONENT_ID = 0; + +const Worker_ComponentId ENTITY_ACL_COMPONENT_ID = 50; +const Worker_ComponentId METADATA_COMPONENT_ID = 53; +const Worker_ComponentId POSITION_COMPONENT_ID = 54; +const Worker_ComponentId PERSISTENCE_COMPONENT_ID = 55; +const Worker_ComponentId INTEREST_COMPONENT_ID = 58; +// This is a component on per-worker system entities. +const Worker_ComponentId WORKER_COMPONENT_ID = 60; +const Worker_ComponentId PLAYERIDENTITY_COMPONENT_ID = 61; + +const Worker_ComponentId MAX_RESERVED_SPATIAL_SYSTEM_COMPONENT_ID = 100; + +const Worker_ComponentId SPAWN_DATA_COMPONENT_ID = 9999; +const Worker_ComponentId PLAYER_SPAWNER_COMPONENT_ID = 9998; +const Worker_ComponentId SINGLETON_COMPONENT_ID = 9997; +const Worker_ComponentId UNREAL_METADATA_COMPONENT_ID = 9996; +const Worker_ComponentId SINGLETON_MANAGER_COMPONENT_ID = 9995; +const Worker_ComponentId DEPLOYMENT_MAP_COMPONENT_ID = 9994; +const Worker_ComponentId STARTUP_ACTOR_MANAGER_COMPONENT_ID = 9993; +const Worker_ComponentId GSM_SHUTDOWN_COMPONENT_ID = 9992; +const Worker_ComponentId HEARTBEAT_COMPONENT_ID = 9991; +// Marking the event-based RPC components as legacy while the ring buffer +// implementation is under a feature flag. +const Worker_ComponentId CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY = 9990; +const Worker_ComponentId SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY = 9989; +const Worker_ComponentId NETMULTICAST_RPCS_COMPONENT_ID_LEGACY = 9987; + +const Worker_ComponentId NOT_STREAMED_COMPONENT_ID = 9986; +const Worker_ComponentId RPCS_ON_ENTITY_CREATION_ID = 9985; +const Worker_ComponentId DEBUG_METRICS_COMPONENT_ID = 9984; +const Worker_ComponentId ALWAYS_RELEVANT_COMPONENT_ID = 9983; +const Worker_ComponentId TOMBSTONE_COMPONENT_ID = 9982; +const Worker_ComponentId DORMANT_COMPONENT_ID = 9981; +const Worker_ComponentId AUTHORITY_INTENT_COMPONENT_ID = 9980; +const Worker_ComponentId VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID = 9979; + +const Worker_ComponentId CLIENT_ENDPOINT_COMPONENT_ID = 9978; +const Worker_ComponentId SERVER_ENDPOINT_COMPONENT_ID = 9977; +const Worker_ComponentId MULTICAST_RPCS_COMPONENT_ID = 9976; +const Worker_ComponentId SPATIAL_DEBUGGING_COMPONENT_ID = 9975; +const Worker_ComponentId SERVER_WORKER_COMPONENT_ID = 9974; +const Worker_ComponentId SERVER_TO_SERVER_COMMAND_ENDPOINT_COMPONENT_ID = 9973; +const Worker_ComponentId COMPONENT_PRESENCE_COMPONENT_ID = 9972; +const Worker_ComponentId NET_OWNING_CLIENT_WORKER_COMPONENT_ID = 9971; + +const Worker_ComponentId STARTING_GENERATED_COMPONENT_ID = 10000; + +const Schema_FieldId SINGLETON_MANAGER_SINGLETON_NAME_TO_ENTITY_ID = 1; + +const Schema_FieldId DEPLOYMENT_MAP_MAP_URL_ID = 1; +const Schema_FieldId DEPLOYMENT_MAP_ACCEPTING_PLAYERS_ID = 2; +const Schema_FieldId DEPLOYMENT_MAP_SESSION_ID = 3; +const Schema_FieldId DEPLOYMENT_MAP_SCHEMA_HASH = 4; + +const Schema_FieldId STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_ID = 1; + +const Schema_FieldId ACTOR_COMPONENT_REPLICATES_ID = 1; +const Schema_FieldId ACTOR_TEAROFF_ID = 3; + +const Schema_FieldId HEARTBEAT_EVENT_ID = 1; +const Schema_FieldId HEARTBEAT_CLIENT_HAS_QUIT_ID = 1; + +const Schema_FieldId SHUTDOWN_MULTI_PROCESS_REQUEST_ID = 1; +const Schema_FieldId SHUTDOWN_ADDITIONAL_SERVERS_EVENT_ID = 1; + +const Schema_FieldId CLEAR_RPCS_ON_ENTITY_CREATION = 1; + +// DebugMetrics command IDs +const Schema_FieldId DEBUG_METRICS_START_RPC_METRICS_ID = 1; +const Schema_FieldId DEBUG_METRICS_STOP_RPC_METRICS_ID = 2; +const Schema_FieldId DEBUG_METRICS_MODIFY_SETTINGS_ID = 3; + +// ModifySettingPayload Field IDs +const Schema_FieldId MODIFY_SETTING_PAYLOAD_NAME_ID = 1; +const Schema_FieldId MODIFY_SETTING_PAYLOAD_VALUE_ID = 2; + +// UnrealObjectRef Field IDs +const Schema_FieldId UNREAL_OBJECT_REF_ENTITY_ID = 1; +const Schema_FieldId UNREAL_OBJECT_REF_OFFSET_ID = 2; +const Schema_FieldId UNREAL_OBJECT_REF_PATH_ID = 3; +const Schema_FieldId UNREAL_OBJECT_REF_NO_LOAD_ON_CLIENT_ID = 4; +const Schema_FieldId UNREAL_OBJECT_REF_OUTER_ID = 5; +const Schema_FieldId UNREAL_OBJECT_REF_USE_SINGLETON_CLASS_PATH_ID = 6; + +// UnrealRPCPayload Field IDs +const Schema_FieldId UNREAL_RPC_PAYLOAD_OFFSET_ID = 1; +const Schema_FieldId UNREAL_RPC_PAYLOAD_RPC_INDEX_ID = 2; +const Schema_FieldId UNREAL_RPC_PAYLOAD_RPC_PAYLOAD_ID = 3; +const Schema_FieldId UNREAL_RPC_PAYLOAD_TRACE_ID = 4; + +const Schema_FieldId UNREAL_RPC_TRACE_ID = 1; +const Schema_FieldId UNREAL_RPC_SPAN_ID = 2; + +// Unreal(Client|Server|Multicast)RPCEndpoint Field IDs +const Schema_FieldId UNREAL_RPC_ENDPOINT_READY_ID = 1; +const Schema_FieldId UNREAL_RPC_ENDPOINT_EVENT_ID = 1; +const Schema_FieldId UNREAL_RPC_ENDPOINT_COMMAND_ID = 1; + +const Schema_FieldId PLAYER_SPAWNER_SPAWN_PLAYER_COMMAND_ID = 1; + +// AuthorityIntent codes and Field IDs. +const Schema_FieldId AUTHORITY_INTENT_VIRTUAL_WORKER_ID = 1; + +// VirtualWorkerTranslation Field IDs. +const Schema_FieldId VIRTUAL_WORKER_TRANSLATION_MAPPING_ID = 1; +const Schema_FieldId MAPPING_VIRTUAL_WORKER_ID = 1; +const Schema_FieldId MAPPING_PHYSICAL_WORKER_NAME = 2; +const Schema_FieldId MAPPING_SERVER_WORKER_ENTITY_ID = 3; +const PhysicalWorkerName TRANSLATOR_UNSET_PHYSICAL_NAME = FString("UnsetWorkerName"); + +// WorkerEntity Field IDs. +const Schema_FieldId WORKER_ID_ID = 1; +const Schema_FieldId WORKER_TYPE_ID = 2; + +// SpatialDebugger Field IDs. +const Schema_FieldId SPATIAL_DEBUGGING_AUTHORITATIVE_VIRTUAL_WORKER_ID = 1; +const Schema_FieldId SPATIAL_DEBUGGING_AUTHORITATIVE_COLOR = 2; +const Schema_FieldId SPATIAL_DEBUGGING_INTENT_VIRTUAL_WORKER_ID = 3; +const Schema_FieldId SPATIAL_DEBUGGING_INTENT_COLOR = 4; +const Schema_FieldId SPATIAL_DEBUGGING_IS_LOCKED = 5; + +// ServerWorker Field IDs. +const Schema_FieldId SERVER_WORKER_NAME_ID = 1; +const Schema_FieldId SERVER_WORKER_READY_TO_BEGIN_PLAY_ID = 2; +const Schema_FieldId SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND_ID = 1; + +// SpawnPlayerRequest type IDs. +const Schema_FieldId SPAWN_PLAYER_URL_ID = 1; +const Schema_FieldId SPAWN_PLAYER_UNIQUE_ID = 2; +const Schema_FieldId SPAWN_PLAYER_PLATFORM_NAME_ID = 3; +const Schema_FieldId SPAWN_PLAYER_IS_SIMULATED_ID = 4; + +// ForwardSpawnPlayerRequest type IDs. +const Schema_FieldId FORWARD_SPAWN_PLAYER_START_ACTOR_ID = 1; +const Schema_FieldId FORWARD_SPAWN_PLAYER_DATA_ID = 2; +const Schema_FieldId FORWARD_SPAWN_PLAYER_CLIENT_WORKER_ID = 3; +const Schema_FieldId FORWARD_SPAWN_PLAYER_RESPONSE_SUCCESS_ID = 1; + +// ComponentPresence Field IDs. +const Schema_FieldId COMPONENT_PRESENCE_COMPONENT_LIST_ID = 1; - const FString LOCAL_HOST = TEXT("127.0.0.1"); - const uint16 DEFAULT_PORT = 7777; +// NetOwningClientWorker Field IDs. +const Schema_FieldId NET_OWNING_CLIENT_WORKER_FIELD_ID = 1; - const float ENTITY_QUERY_RETRY_WAIT_SECONDS = 3.0f; +// UnrealMetadata Field IDs. +const Schema_FieldId UNREAL_METADATA_STABLY_NAMED_REF_ID = 1; +const Schema_FieldId UNREAL_METADATA_CLASS_PATH_ID = 2; +const Schema_FieldId UNREAL_METADATA_NET_STARTUP_ID = 3; - const Worker_ComponentId MIN_EXTERNAL_SCHEMA_ID = 1000; - const Worker_ComponentId MAX_EXTERNAL_SCHEMA_ID = 2000; +// Reserved entity IDs expire in 5 minutes, we will refresh them every 3 minutes to be safe. +const float ENTITY_RANGE_EXPIRATION_INTERVAL_SECONDS = 180.0f; - const FString SPATIALOS_METRICS_DYNAMIC_FPS = TEXT("Dynamic.FPS"); +const float FIRST_COMMAND_RETRY_WAIT_SECONDS = 0.2f; +const uint32 MAX_NUMBER_COMMAND_ATTEMPTS = 5u; +const float FORWARD_PLAYER_SPAWN_COMMAND_WAIT_SECONDS = 0.2f; - const FString LOCATOR_HOST = TEXT("locator.improbable.io"); - const FString LOCATOR_HOST_CN = TEXT("locator.spatialoschina.com"); - const uint16 LOCATOR_PORT = 443; +const FName DefaultActorGroup = FName(TEXT("Default")); + +const VirtualWorkerId INVALID_VIRTUAL_WORKER_ID = 0; +const ActorLockToken INVALID_ACTOR_LOCK_TOKEN = 0; +const FString INVALID_WORKER_NAME = TEXT(""); + +const WorkerAttributeSet UnrealServerAttributeSet = TArray{DefaultServerWorkerType.ToString()}; +const WorkerAttributeSet UnrealClientAttributeSet = TArray{DefaultClientWorkerType.ToString()}; + +const WorkerRequirementSet UnrealServerPermission{ {UnrealServerAttributeSet} }; +const WorkerRequirementSet UnrealClientPermission{ {UnrealClientAttributeSet} }; +const WorkerRequirementSet ClientOrServerPermission{ {UnrealClientAttributeSet, UnrealServerAttributeSet} }; + +const FString ClientsStayConnectedURLOption = TEXT("clientsStayConnected"); +const FString SpatialSessionIdURLOption = TEXT("spatialSessionId="); + +const FString LOCATOR_HOST = TEXT("locator.improbable.io"); +const FString LOCATOR_HOST_CN = TEXT("locator.spatialoschina.com"); +const uint16 LOCATOR_PORT = 443; + +const FString AssemblyPattern = TEXT("^[a-zA-Z0-9_.-]{5,64}$"); +const FString ProjectPattern = TEXT("^[a-z0-9_]{3,32}$"); +const FString DeploymentPattern = TEXT("^[a-z0-9_]{2,32}$"); + +inline float GetCommandRetryWaitTimeSeconds(uint32 NumAttempts) +{ + // Double the time to wait on each failure. + uint32 WaitTimeExponentialFactor = 1u << (NumAttempts - 1); + return FIRST_COMMAND_RETRY_WAIT_SECONDS * WaitTimeExponentialFactor; +} - const FString DEVELOPMENT_AUTH_PLAYER_ID = TEXT("Player Id"); +const FString LOCAL_HOST = TEXT("127.0.0.1"); +const uint16 DEFAULT_PORT = 7777; - const FString SCHEMA_DATABASE_FILE_PATH = TEXT("Spatial/SchemaDatabase"); - const FString SCHEMA_DATABASE_ASSET_PATH = TEXT("/Game/Spatial/SchemaDatabase"); +const float ENTITY_QUERY_RETRY_WAIT_SECONDS = 3.0f; -} // ::SpatialConstants +const Worker_ComponentId MIN_EXTERNAL_SCHEMA_ID = 1000; +const Worker_ComponentId MAX_EXTERNAL_SCHEMA_ID = 2000; + +const FString SPATIALOS_METRICS_DYNAMIC_FPS = TEXT("Dynamic.FPS"); + +// URL that can be used to reconnect using the command line arguments. +const FString RECONNECT_USING_COMMANDLINE_ARGUMENTS = TEXT("0.0.0.0"); +const FString URL_LOGIN_OPTION = TEXT("login="); +const FString URL_PLAYER_IDENTITY_OPTION = TEXT("playeridentity="); +const FString URL_DEV_AUTH_TOKEN_OPTION = TEXT("devauthtoken="); +const FString URL_TARGET_DEPLOYMENT_OPTION = TEXT("deployment="); +const FString URL_PLAYER_ID_OPTION = TEXT("playerid="); +const FString URL_DISPLAY_NAME_OPTION = TEXT("displayname="); +const FString URL_METADATA_OPTION = TEXT("metadata="); -FORCEINLINE Worker_ComponentId SchemaComponentTypeToWorkerComponentId(ESchemaComponentType SchemaType) +const FString DEVELOPMENT_AUTH_PLAYER_ID = TEXT("Player Id"); + +const FString SCHEMA_DATABASE_FILE_PATH = TEXT("Spatial/SchemaDatabase"); +const FString SCHEMA_DATABASE_ASSET_PATH = TEXT("/Game/Spatial/SchemaDatabase"); + +// A list of components clients require on top of any generated data components in order to handle non-authoritative actors correctly. +const TArray REQUIRED_COMPONENTS_FOR_NON_AUTH_CLIENT_INTEREST = TArray +{ + // Actor components + UNREAL_METADATA_COMPONENT_ID, + SPAWN_DATA_COMPONENT_ID, + RPCS_ON_ENTITY_CREATION_ID, + TOMBSTONE_COMPONENT_ID, + DORMANT_COMPONENT_ID, + + // Multicast RPCs + MULTICAST_RPCS_COMPONENT_ID, + NETMULTICAST_RPCS_COMPONENT_ID_LEGACY, + + // Global state components + SINGLETON_MANAGER_COMPONENT_ID, + DEPLOYMENT_MAP_COMPONENT_ID, + STARTUP_ACTOR_MANAGER_COMPONENT_ID, + GSM_SHUTDOWN_COMPONENT_ID, + + // Debugging information + DEBUG_METRICS_COMPONENT_ID, + SPATIAL_DEBUGGING_COMPONENT_ID +}; + +// A list of components clients require on entities they are authoritative over on top of the components already checked out by the interest query. +const TArray REQUIRED_COMPONENTS_FOR_AUTH_CLIENT_INTEREST = TArray +{ + // RPCs from the server + SERVER_ENDPOINT_COMPONENT_ID, + SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY +}; + +// A list of components servers require on top of any generated data and handover components in order to handle non-authoritative actors correctly. +const TArray REQUIRED_COMPONENTS_FOR_NON_AUTH_SERVER_INTEREST = TArray +{ + // Actor components + UNREAL_METADATA_COMPONENT_ID, + SPAWN_DATA_COMPONENT_ID, + RPCS_ON_ENTITY_CREATION_ID, + TOMBSTONE_COMPONENT_ID, + DORMANT_COMPONENT_ID, + NET_OWNING_CLIENT_WORKER_COMPONENT_ID, + + // Multicast RPCs + MULTICAST_RPCS_COMPONENT_ID, + NETMULTICAST_RPCS_COMPONENT_ID_LEGACY, + + // Global state components + SINGLETON_MANAGER_COMPONENT_ID, + DEPLOYMENT_MAP_COMPONENT_ID, + STARTUP_ACTOR_MANAGER_COMPONENT_ID, + GSM_SHUTDOWN_COMPONENT_ID, + + // Unreal load balancing components + VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID +}; + +// A list of components servers require on entities they are authoritative over on top of the components already checked out by the interest query. +const TArray REQUIRED_COMPONENTS_FOR_AUTH_SERVER_INTEREST = TArray { - switch (SchemaType) + // RPCs from clients + CLIENT_ENDPOINT_COMPONENT_ID, + CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY, + + // Heartbeat + HEARTBEAT_COMPONENT_ID +}; + +inline Worker_ComponentId RPCTypeToWorkerComponentIdLegacy(ERPCType RPCType) +{ + switch (RPCType) { - case SCHEMA_CrossServerRPC: + case ERPCType::CrossServer: { - return SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID; + return SpatialConstants::SERVER_TO_SERVER_COMMAND_ENDPOINT_COMPONENT_ID; } - case SCHEMA_NetMulticastRPC: + case ERPCType::NetMulticast: { - return SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID; + return SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID_LEGACY; } - case SCHEMA_ClientReliableRPC: - case SCHEMA_ClientUnreliableRPC: + case ERPCType::ClientReliable: + case ERPCType::ClientUnreliable: { - return SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID; + return SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY; } - case SCHEMA_ServerReliableRPC: - case SCHEMA_ServerUnreliableRPC: + case ERPCType::ServerReliable: + case ERPCType::ServerUnreliable: { - return SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID; + return SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY; } default: checkNoEntry(); return SpatialConstants::INVALID_COMPONENT_ID; } } + +inline Worker_ComponentId GetClientAuthorityComponent(bool bUsingRingBuffers) +{ + return bUsingRingBuffers ? CLIENT_ENDPOINT_COMPONENT_ID : CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY; +} + +inline WorkerAttributeSet GetLoadBalancerAttributeSet(FName LoadBalancingWorkerType) +{ + if (LoadBalancingWorkerType == "") + { + return { DefaultServerWorkerType.ToString() }; + } + return { LoadBalancingWorkerType.ToString() }; +} + +} // ::SpatialConstants + +DECLARE_STATS_GROUP(TEXT("SpatialNet"), STATGROUP_SpatialNet, STATCAT_Advanced); diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKConsoleCommands.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKConsoleCommands.h new file mode 100644 index 0000000000..fc9adf3721 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKConsoleCommands.h @@ -0,0 +1,14 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +#pragma once + +#include "SpatialCommonTypes.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKConsoleCommands, Log, All); + +class UWorld; + +namespace SpatialGDKConsoleCommands +{ + void ConsoleCommand_ConnectToLocator(const TArray& Args, UWorld* World); +} +// namespace diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKLoader.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKLoader.h index 5871693ca3..9668b475bf 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKLoader.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKLoader.h @@ -21,13 +21,25 @@ class FSpatialGDKLoader Path = Path / TEXT("Win64"); #else Path = Path / TEXT("Win32"); -#endif - Path = Path / TEXT("improbable_worker.dll"); - WorkerLibraryHandle = FPlatformProcess::GetDllHandle(*Path); +#endif // PLATFORM_64BITS + FString WorkerFilePath = Path / TEXT("improbable_worker.dll"); + WorkerLibraryHandle = FPlatformProcess::GetDllHandle(*WorkerFilePath); if (WorkerLibraryHandle == nullptr) { - UE_LOG(LogTemp, Fatal, TEXT("Failed to load %s. Have you run `UnrealGDK/Setup.bat`?"), *Path); + UE_LOG(LogTemp, Fatal, TEXT("Failed to load %s. Have you run `UnrealGDK/Setup.bat`?"), *WorkerFilePath); + } + +#if TRACE_LIB_ACTIVE + + FString TraceFilePath = Path / TEXT("trace_dynamic.dll"); + TraceLibraryHandle = FPlatformProcess::GetDllHandle(*TraceFilePath); + if (TraceLibraryHandle == nullptr) + { + UE_LOG(LogTemp, Fatal, TEXT("Failed to load %s. Have you run `UnrealGDK/SetupIncTraceLibs.bat`?"), *TraceFilePath); } + +#endif // TRACE_LIB_ACTIVE + #elif PLATFORM_PS4 WorkerLibraryHandle = FPlatformProcess::GetDllHandle(TEXT("libworker.prx")); #endif @@ -40,6 +52,12 @@ class FSpatialGDKLoader FPlatformProcess::FreeDllHandle(WorkerLibraryHandle); WorkerLibraryHandle = nullptr; } + + if (TraceLibraryHandle != nullptr) + { + FPlatformProcess::FreeDllHandle(TraceLibraryHandle); + TraceLibraryHandle = nullptr; + } } FSpatialGDKLoader(const FSpatialGDKLoader& rhs) = delete; @@ -47,4 +65,5 @@ class FSpatialGDKLoader private: void* WorkerLibraryHandle = nullptr; + void* TraceLibraryHandle = nullptr; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h index 73152963c4..e6229be567 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h @@ -2,13 +2,19 @@ #pragma once +#include "Utils/SpatialActorGroupManager.h" + #include "CoreMinimal.h" #include "Engine/EngineTypes.h" #include "Misc/Paths.h" -#include "Utils/ActorGroupManager.h" +#include "Utils/RPCContainer.h" #include "SpatialGDKSettings.generated.h" +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKSettings, Log, All); + +class ASpatialDebugger; + /** * Enum that maps Unreal's log verbosity to allow use in settings. **/ @@ -37,6 +43,18 @@ namespace EServicesRegion }; } +USTRUCT(BlueprintType) +struct FDistanceFrequencyPair +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "SpatialGDK") + float DistanceRatio; + + UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "SpatialGDK") + float Frequency; +}; + UCLASS(config = SpatialGDKSettings, defaultconfig) class SPATIALGDK_API USpatialGDKSettings : public UObject { @@ -55,95 +73,108 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject * The number of entity IDs to be reserved when the entity pool is first created. Ensure that the number of entity IDs * reserved is greater than the number of Actors that you expect the server-worker instances to spawn at game deployment */ - UPROPERTY(EditAnywhere, config, Category = "Entity Pool", meta = (ConfigRestartRequired = false, DisplayName = "Initial Entity ID Reservation Count")) + UPROPERTY(EditAnywhere, config, Category = "Entity Pool", meta = (DisplayName = "Initial Entity ID Reservation Count")) uint32 EntityPoolInitialReservationCount; /** * Specifies when the SpatialOS Runtime should reserve a new batch of entity IDs: the value is the number of un-used entity * IDs left in the entity pool which triggers the SpatialOS Runtime to reserve new entity IDs */ - UPROPERTY(EditAnywhere, config, Category = "Entity Pool", meta = (ConfigRestartRequired = false, DisplayName = "Pool Refresh Threshold")) + UPROPERTY(EditAnywhere, config, Category = "Entity Pool", meta = (DisplayName = "Pool Refresh Threshold")) uint32 EntityPoolRefreshThreshold; /** * Specifies the number of new entity IDs the SpatialOS Runtime reserves when `Pool refresh threshold` triggers a new batch. */ - UPROPERTY(EditAnywhere, config, Category = "Entity Pool", meta = (ConfigRestartRequired = false, DisplayName = "Refresh Count")) + UPROPERTY(EditAnywhere, config, Category = "Entity Pool", meta = (DisplayName = "Refresh Count")) uint32 EntityPoolRefreshCount; /** Specifies the amount of time, in seconds, between heartbeat events sent from a game client to notify the server-worker instances that it's connected. */ - UPROPERTY(EditAnywhere, config, Category = "Heartbeat", meta = (ConfigRestartRequired = false, DisplayName = "Heartbeat Interval (seconds)")) + UPROPERTY(EditAnywhere, config, Category = "Heartbeat", meta = (DisplayName = "Heartbeat Interval (seconds)")) float HeartbeatIntervalSeconds; /** * Specifies the maximum amount of time, in seconds, that the server-worker instances wait for a game client to send heartbeat events. * (If the timeout expires, the game client has disconnected.) */ - UPROPERTY(EditAnywhere, config, Category = "Heartbeat", meta = (ConfigRestartRequired = false, DisplayName = "Heartbeat Timeout (seconds)")) + UPROPERTY(EditAnywhere, config, Category = "Heartbeat", meta = (DisplayName = "Heartbeat Timeout (seconds)")) float HeartbeatTimeoutSeconds; /** - * Specifies the maximum number of Actors replicated per tick. + * Same as HeartbeatTimeoutSeconds, but used if WITH_EDITOR is defined. + */ + UPROPERTY(EditAnywhere, config, Category = "Heartbeat", meta = (DisplayName = "Heartbeat Timeout With Editor (seconds)")) + float HeartbeatTimeoutWithEditorSeconds; + + /** + * Specifies the maximum number of Actors replicated per tick. Not respected when using the Replication Graph. * Default: `0` per tick (no limit) * (If you set the value to ` 0`, the SpatialOS Runtime replicates every Actor per tick; this forms a large SpatialOS world, affecting the performance of both game clients and server-worker instances.) * You can use the `stat Spatial` flag when you run project builds to find the number of calls to `ReplicateActor`, and then use this number for reference. */ - UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (ConfigRestartRequired = false, DisplayName = "Maximum Actors replicated per tick")) + UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (DisplayName = "Maximum Actors replicated per tick")) uint32 ActorReplicationRateLimit; /** - * Specifies the maximum number of entities created by the SpatialOS Runtime per tick. + * Specifies the maximum number of entities created by the SpatialOS Runtime per tick. Not respected when using the Replication Graph. * (The SpatialOS Runtime handles entity creation separately from Actor replication to ensure it can handle entity creation requests under load.) * Note: if you set the value to 0, there is no limit to the number of entities created per tick. However, too many entities created at the same time might overload the SpatialOS Runtime, which can negatively affect your game. * Default: `0` per tick (no limit) */ - UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (ConfigRestartRequired = false, DisplayName = "Maximum entities created per tick")) + UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (DisplayName = "Maximum entities created per tick")) uint32 EntityCreationRateLimit; /** - * When enabled, only entities which are in the net relevancy range of player controllers will be replicated to SpatialOS. + * When enabled, only entities which are in the net relevancy range of player controllers will be replicated to SpatialOS. Not respected when using the Replication Graph. * This should only be used in single server configurations. The state of the world in the inspector will no longer be up to date. */ - UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (ConfigRestartRequired = false, DisplayName = "Only Replicate Net Relevant Actors")) - bool UseIsActorRelevantForConnection; + UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (DisplayName = "Only Replicate Net Relevant Actors")) + bool bUseIsActorRelevantForConnection; /** * Specifies the rate, in number of times per second, at which server-worker instance updates are sent to and received from the SpatialOS Runtime. * Default:1000/s */ - UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (ConfigRestartRequired = false, DisplayName = "SpatialOS Network Update Rate")) + UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (DisplayName = "SpatialOS Network Update Rate")) float OpsUpdateRate; /** Replicate handover properties between servers, required for zoned worker deployments.*/ - UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (ConfigRestartRequired = false)) + UPROPERTY(EditAnywhere, config, Category = "Replication") bool bEnableHandover; - /** Maximum NetCullDistanceSquared value used in Spatial networking. Set to 0.0 to disable. This is temporary and will be removed when the runtime issue is resolved.*/ - UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (ConfigRestartRequired = false)) + /** + * Maximum NetCullDistanceSquared value used in Spatial networking. Not respected when using the Replication Graph. + * Set to 0.0 to disable. This is temporary and will be removed when the runtime issue is resolved. + */ + UPROPERTY(EditAnywhere, config, Category = "Replication") float MaxNetCullDistanceSquared; /** Seconds to wait before executing a received RPC substituting nullptr for unresolved UObjects*/ - UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (ConfigRestartRequired = false, DisplayName = "Wait Time Before Processing Received RPC With Unresolved Refs")) + UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (DisplayName = "Wait Time Before Processing Received RPC With Unresolved Refs")) float QueuedIncomingRPCWaitTime; + /** Seconds to wait before dropping an outgoing RPC.*/ + UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (DisplayName = "Wait Time Before Dropping Outgoing RPC")) + float QueuedOutgoingRPCWaitTime; + /** Frequency for updating an Actor's SpatialOS Position. Updating position should have a low update rate since it is expensive.*/ - UPROPERTY(EditAnywhere, config, Category = "SpatialOS Position Updates", meta = (ConfigRestartRequired = false)) + UPROPERTY(EditAnywhere, config, Category = "SpatialOS Position Updates") float PositionUpdateFrequency; /** Threshold an Actor needs to move, in centimeters, before its SpatialOS Position is updated.*/ - UPROPERTY(EditAnywhere, config, Category = "SpatialOS Position Updates", meta = (ConfigRestartRequired = false)) + UPROPERTY(EditAnywhere, config, Category = "SpatialOS Position Updates") float PositionDistanceThreshold; /** Metrics about client and server performance can be reported to SpatialOS to monitor a deployments health.*/ - UPROPERTY(EditAnywhere, config, Category = "Metrics", meta = (ConfigRestartRequired = false)) + UPROPERTY(EditAnywhere, config, Category = "Metrics") bool bEnableMetrics; /** Display server metrics on clients.*/ - UPROPERTY(EditAnywhere, config, Category = "Metrics", meta = (ConfigRestartRequired = false)) + UPROPERTY(EditAnywhere, config, Category = "Metrics") bool bEnableMetricsDisplay; /** Frequency that metrics are reported to SpatialOS.*/ - UPROPERTY(EditAnywhere, config, Category = "Metrics", meta = (ConfigRestartRequired = false), DisplayName = "Metrics Report Rate (seconds)") + UPROPERTY(EditAnywhere, config, Category = "Metrics", meta = (DisplayName = "Metrics Report Rate (seconds)")) float MetricsReportRate; /** @@ -151,46 +182,36 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject * Select this to switch so it reports as seconds per frame. * This value is visible as 'Load' in the Inspector, next to each worker. */ - UPROPERTY(EditAnywhere, config, Category = "Metrics", meta = (ConfigRestartRequired = false)) + UPROPERTY(EditAnywhere, config, Category = "Metrics") bool bUseFrameTimeAsLoad; - // TODO: UNR-1653 Redesign bCheckRPCOrder Tests functionality - /** Include an order index with reliable RPCs and warn if they are executed out of order.*/ - UPROPERTY(config, meta = (ConfigRestartRequired = false)) - bool bCheckRPCOrder; - /** Batch entity position updates to be processed on a single frame.*/ - UPROPERTY(config, meta = (ConfigRestartRequired = false)) + UPROPERTY(config) bool bBatchSpatialPositionUpdates; /** Maximum number of ActorComponents/Subobjects of the same class that can be attached to an Actor.*/ - UPROPERTY(EditAnywhere, config, Category = "Schema Generation", meta = (ConfigRestartRequired = false), DisplayName = "Maximum Dynamically Attached Subobjects Per Class") + UPROPERTY(EditAnywhere, config, Category = "Schema Generation", meta = (DisplayName = "Maximum Dynamically Attached Subobjects Per Class")) uint32 MaxDynamicallyAttachedSubobjectsPerClass; - /** EXPERIMENTAL - This is a stop-gap until we can better define server interest on system entities. - Disabling this is not supported in any type of multi-server environment*/ - UPROPERTY(config, meta = (ConfigRestartRequired = false)) - bool bEnableServerQBI; - - /** Pack RPCs sent during the same frame into a single update. */ - UPROPERTY(config, meta = (ConfigRestartRequired = false)) - bool bPackRPCs; + /** + * Adds granular result types for queries. + * Granular here means specifically the required Unreal components for spawning other actors and all data type components. + */ + UPROPERTY(config) + bool bEnableResultTypes; /** The receptionist host to use if no 'receptionistHost' argument is passed to the command line. */ - UPROPERTY(EditAnywhere, config, Category = "Local Connection", meta = (ConfigRestartRequired = false)) + UPROPERTY(EditAnywhere, config, Category = "Local Connection") FString DefaultReceptionistHost; - /** If the Development Authentication Flow is used, the client will try to connect to the cloud rather than local deployment. */ - UPROPERTY(EditAnywhere, config, Category = "Cloud Connection", meta = (ConfigRestartRequired = false)) - bool bUseDevelopmentAuthenticationFlow; +private: + /** Will stop a non editor client auto connecting via command line args to a cloud deployment */ + UPROPERTY(EditAnywhere, config, Category = "Cloud Connection") + bool bPreventClientCloudDeploymentAutoConnect; - /** The token created using 'spatial project auth dev-auth-token' */ - UPROPERTY(EditAnywhere, config, Category = "Cloud Connection", meta = (ConfigRestartRequired = false)) - FString DevelopmentAuthenticationToken; +public: - /** The deployment to connect to when using the Development Authentication Flow. If left empty, it uses the first available one (order not guaranteed when there are multiple items). The deployment needs to be tagged with 'dev_login'. */ - UPROPERTY(EditAnywhere, config, Category = "Cloud Connection", meta = (ConfigRestartRequired = false)) - FString DevelopmentDeploymentToConnect; + bool GetPreventClientCloudDeploymentAutoConnect(bool bIsClient) const; UPROPERTY(EditAnywhere, Config, Category = "Region settings", meta = (ConfigRestartRequired = true, DisplayName = "Region where services are located")) TEnumAsByte ServicesRegion; @@ -212,16 +233,122 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject TSet ServerWorkerTypes; /** Controls the verbosity of worker logs which are sent to SpatialOS. These logs will appear in the Spatial Output and launch.log */ - UPROPERTY(EditAnywhere, config, Category = "Logging", meta = (ConfigRestartRequired = false, DisplayName = "Worker Log Level")) + UPROPERTY(EditAnywhere, config, Category = "Logging", meta = (DisplayName = "Worker Log Level")) TEnumAsByte WorkerLogLevel; + UPROPERTY(EditAnywhere, config, Category = "Debug", meta = (MetaClass = "SpatialDebugger")) + TSubclassOf SpatialDebugger; + /** EXPERIMENTAL: Disable runtime load balancing and use a worker to do it instead. */ UPROPERTY(EditAnywhere, Config, Category = "Load Balancing") - bool bEnableUnrealLoadBalancer; + bool bEnableUnrealLoadBalancer; /** EXPERIMENTAL: Worker type to assign for load balancing. */ UPROPERTY(EditAnywhere, Config, Category = "Load Balancing", meta = (EditCondition = "bEnableUnrealLoadBalancer")) - FWorkerType LoadBalancingWorkerType; + FWorkerType LoadBalancingWorkerType; + + /** EXPERIMENTAL: Run SpatialWorkerConnection on Game Thread. */ + UPROPERTY(Config) + bool bRunSpatialWorkerConnectionOnGameThread; + + /** RPC ring buffers is enabled when either the matching setting is set, or load balancing is enabled */ + bool UseRPCRingBuffer() const; + +private: +#if WITH_EDITOR + bool CanEditChange(const UProperty* InProperty) const override; +#endif + + UPROPERTY(EditAnywhere, Config, Category = "Replication", meta = (DisplayName = "Use RPC Ring Buffers")) + bool bUseRPCRingBuffers; + + UPROPERTY(EditAnywhere, Config, Category = "Replication", meta = (DisplayName = "Default RPC Ring Buffer Size")) + uint32 DefaultRPCRingBufferSize; + + /** Overrides default ring buffer size. */ + UPROPERTY(EditAnywhere, Config, Category = "Replication", meta = (DisplayName = "RPC Ring Buffer Size Map")) + TMap RPCRingBufferSizeMap; + +public: + uint32 GetRPCRingBufferSize(ERPCType RPCType) const; + + float GetSecondsBeforeWarning(const ERPCResult Result) const; + + /** The number of fields that the endpoint schema components are generated with. Changing this will require schema to be regenerated and break snapshot compatibility. */ + UPROPERTY(EditAnywhere, Config, Category = "Replication", meta = (DisplayName = "Max RPC Ring Buffer Size")) + uint32 MaxRPCRingBufferSize; + + /** Only valid on Tcp connections - indicates if we should enable TCP_NODELAY - see c_worker.h */ + UPROPERTY(Config) + bool bTcpNoDelay; + + /** Only valid on Udp connections - specifies server upstream flush interval - see c_worker.h */ + UPROPERTY(Config) + uint32 UdpServerUpstreamUpdateIntervalMS; + + /** Only valid on Udp connections - specifies server downstream flush interval - see c_worker.h */ + UPROPERTY(Config) + uint32 UdpServerDownstreamUpdateIntervalMS; + + /** Only valid on Udp connections - specifies client upstream flush interval - see c_worker.h */ + UPROPERTY(Config) + uint32 UdpClientUpstreamUpdateIntervalMS; + + /** Only valid on Udp connections - specifies client downstream flush interval - see c_worker.h */ + UPROPERTY(Config) + uint32 UdpClientDownstreamUpdateIntervalMS; + + /** Do async loading for new classes when checking out entities. */ + UPROPERTY(Config) + bool bAsyncLoadNewClassesOnEntityCheckout; + + UPROPERTY(EditAnywhere, config, Category = "Queued RPC Warning Timeouts", AdvancedDisplay, meta = (DisplayName = "For a given RPC failure type, the time it will queue before reporting warnings to the logs.")) + TMap RPCQueueWarningTimeouts; + + UPROPERTY(EditAnywhere, config, Category = "Queued RPC Warning Timeouts", AdvancedDisplay, meta = (DisplayName = "Default time before a queued RPC will start reporting warnings to the logs.")) + float RPCQueueWarningDefaultTimeout; FORCEINLINE bool IsRunningInChina() const { return ServicesRegion == EServicesRegion::CN; } + + /** Enable to use the new net cull distance component tagging form of interest */ + UPROPERTY(EditAnywhere, Config, Category = "Interest") + bool bEnableNetCullDistanceInterest; + + /** Enable to use interest frequency with bEnableNetCullDistanceInterest*/ + UPROPERTY(EditAnywhere, Config, Category = "Interest", meta = (EditCondition = "bEnableNetCullDistanceInterest")) + bool bEnableNetCullDistanceFrequency; + + /** Full update frequency ratio of actor's net cull distance */ + UPROPERTY(EditAnywhere, Config, Category = "Interest", meta = (EditCondition = "bEnableNetCullDistanceFrequency")) + float FullFrequencyNetCullDistanceRatio; + + /** QBI pairs for ratio of - net cull distance : update frequency */ + UPROPERTY(EditAnywhere, Config, Category = "Interest", meta = (EditCondition = "bEnableNetCullDistanceFrequency")) + TArray InterestRangeFrequencyPairs; + + /** Use TLS encryption for UnrealClient workers connection. May impact performance. */ + UPROPERTY(EditAnywhere, Config, Category = "Connection") + bool bUseSecureClientConnection; + + /** Use TLS encryption for UnrealWorker (server) workers connection. May impact performance. */ + UPROPERTY(EditAnywhere, Config, Category = "Connection") + bool bUseSecureServerConnection; + + /** + * Enable to ensure server workers always express interest such that any server is interested in a super set of + * client interest. This will cause servers to make most of the same queries as their delegated client queries. + * Intended to be used in development before interest due to the LB strategy ensures correct functionality. + */ + UPROPERTY(EditAnywhere, Config, Category = "Interest") + bool bEnableClientQueriesOnServer; + + /** Experimental feature to use SpatialView layer when communicating with the Worker */ + UPROPERTY(Config) + bool bUseSpatialView; + +public: + // UI Hidden settings passed through from SpatialGDKEditorSettings + bool bUseDevelopmentAuthenticationFlow; + FString DevelopmentAuthenticationToken; + FString DevelopmentDeploymentToConnect; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/AuthorityRecord.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/AuthorityRecord.h new file mode 100644 index 0000000000..32b2fbbe3b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/AuthorityRecord.h @@ -0,0 +1,50 @@ +#pragma once + +#include "SpatialView/EntityComponentId.h" +#include "Containers/Array.h" + +namespace SpatialGDK +{ + + // A record of authority changes to entity-components. + // Authority for an entity-component can be in at most one of the following states: + // Recorded as gained. + // Recorded as lost. + // Recorded as lost-temporarily. +class AuthorityRecord +{ +public: + // Record an authority change for an entity-component. + // + // The following values of `authority` will cause these respective state transitions: + // WORKER_AUTHORITY_NOT_AUTHORITATIVE + // not recorded -> lost + // gained -> not recorded + // lost -> UNDEFINED + // lost-temporarily -> lost + // WORKER_AUTHORITY_AUTHORITATIVE + // not recorded -> gained + // gained -> UNDEFINED + // lost -> lost-temporarily + // lost-temporarily -> UNDEFINED + // WORKER_AUTHORITY_AUTHORITY_LOSS_IMMINENT + // ignored + void SetAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId, Worker_Authority Authority); + + // Remove all records. + void Clear(); + + // Get all entity-components with an authority change recorded as gained. + const TArray& GetAuthorityGained() const; + // Get all entity-components with an authority change recorded as lost. + const TArray& GetAuthorityLost() const; + // Get all entity-components with an authority change recorded as lost-temporarily. + const TArray& GetAuthorityLostTemporarily() const; + +private: + TArray AuthorityGained; + TArray AuthorityLost; + TArray AuthorityLossTemporary; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandMessages.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandMessages.h new file mode 100644 index 0000000000..0efa2cf885 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandMessages.h @@ -0,0 +1,29 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once +#include "Misc/Optional.h" +#include "Containers/UnrealString.h" +#include + +namespace SpatialGDK +{ + +struct CreateEntityRequest +{ + Worker_RequestId RequestId; + // todo this should be a owning entity state type. + Worker_ComponentData* EntityComponents; + uint32 ComponentCount; + TOptional EntityId; + TOptional TimeoutMillis; +}; + +struct CreateEntityResponse +{ + Worker_RequestId RequestId; + Worker_StatusCode StatusCode; + FString Message; + Worker_EntityId EntityId; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandlers/AbstractConnectionHandler.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandlers/AbstractConnectionHandler.h new file mode 100644 index 0000000000..bb15635816 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandlers/AbstractConnectionHandler.h @@ -0,0 +1,39 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once +#include "SpatialView/MessagesToSend.h" +#include "SpatialView/OpList/AbstractOpList.h" +#include "Templates/UniquePtr.h" +#include + +namespace SpatialGDK +{ + +class AbstractConnectionHandler +{ +public: + virtual ~AbstractConnectionHandler() = default; + + // Should be called to indicate a new tick has started. + // Ensures all external messages, up to this, point have been received. + virtual void Advance() = 0; + + // The number of OpList instances queued. + virtual uint32 GetOpListCount() = 0; + + // Gets the next queued OpList. If there is no OpList queued then an empty one is returned. + virtual TUniquePtr GetNextOpList() = 0; + + // Consumes messages and sends them to the deployment. + virtual void SendMessages(TUniquePtr Messages) = 0; + + // todo implement this once spatial view can be used without the legacy worker connection. + // Return the unique ID for the worker. + // virtual const FString& GetWorkerId() const = 0; + + // todo implement this once spatial view can be used without the legacy worker connection. + // Returns the attributes for the worker. + // virtual const TArray& GetWorkerAttributes() const = 0; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandlers/QueuedOpListConnectionHandler.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandlers/QueuedOpListConnectionHandler.h new file mode 100644 index 0000000000..e3fc664eed --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandlers/QueuedOpListConnectionHandler.h @@ -0,0 +1,55 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once +#include "SpatialView/ConnectionHandlers/AbstractConnectionHandler.h" +#include "SpatialView/OpList/AbstractOpList.h" +#include "Containers/Array.h" + +namespace SpatialGDK +{ + +class QueuedOpListConnectionHandler : public AbstractConnectionHandler +{ +public: + explicit QueuedOpListConnectionHandler(Worker_Connection* Connection) + : Connection(Connection) + { + } + + void Advance() override + { + } + + uint32 GetOpListCount() override + { + return OpLists.Num(); + } + + TUniquePtr GetNextOpList() override + { + TUniquePtr NextOpList = MoveTemp(OpLists[0]); + OpLists.RemoveAt(0); + return NextOpList; + } + + void EnqueueOpList(TUniquePtr OpList) + { + OpLists.Push(MoveTemp(OpList)); + } + + void SendMessages(TUniquePtr Messages) override + { + for (auto& Request : Messages->CreateEntityRequests) + { + Worker_EntityId* EntityId = Request.EntityId.IsSet() ? &Request.EntityId.GetValue() : nullptr; + uint32* TimeoutMillis = Request.TimeoutMillis.IsSet() ? &Request.TimeoutMillis.GetValue() : nullptr; + Worker_Connection_SendCreateEntityRequest(Connection, Request.ComponentCount, Request.EntityComponents, EntityId, TimeoutMillis); + } + } + +private: + Worker_Connection* Connection; + TArray> OpLists; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentId.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentId.h new file mode 100644 index 0000000000..39265791f6 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentId.h @@ -0,0 +1,25 @@ +#pragma once + +#include "Templates/TypeHash.h" +#include + +namespace SpatialGDK +{ + +struct EntityComponentId +{ + Worker_EntityId EntityId; + Worker_ComponentId ComponentId; + + friend bool operator==(const EntityComponentId& Lhs, const EntityComponentId& Rhs) + { + return Lhs.EntityId == Rhs.EntityId && Lhs.ComponentId == Rhs.ComponentId; + } + + friend uint32 GetTypeHash(EntityComponentId Value) + { + return HashCombine(::GetTypeHash(static_cast(Value.EntityId)), ::GetTypeHash(static_cast(Value.ComponentId))); + } +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/MessagesToSend.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/MessagesToSend.h new file mode 100644 index 0000000000..c3613e9be4 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/MessagesToSend.h @@ -0,0 +1,16 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once +#include "SpatialView/CommandMessages.h" +#include "Containers/Array.h" + +namespace SpatialGDK +{ + +// todo Placeholder for vertical slice. This should be revisited when we have more complicated messages. +struct MessagesToSend +{ + TArray CreateEntityRequests; +}; + +} // SpatialView diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/AbstractOpList.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/AbstractOpList.h new file mode 100644 index 0000000000..c1edc33018 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/AbstractOpList.h @@ -0,0 +1,19 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once +#include + +namespace SpatialGDK +{ + +class AbstractOpList +{ +public: + virtual ~AbstractOpList() = default; + + virtual uint32 GetCount() const = 0; + virtual Worker_Op& operator[](uint32 Index) = 0; + virtual const Worker_Op& operator[](uint32 Index) const = 0; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/ViewDeltaLegacyOpList.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/ViewDeltaLegacyOpList.h new file mode 100644 index 0000000000..806583d8fa --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/ViewDeltaLegacyOpList.h @@ -0,0 +1,38 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once +#include "SpatialView/OpList/AbstractOpList.h" +#include "Containers/Array.h" +#include + +namespace SpatialGDK +{ + +class ViewDeltaLegacyOpList : public AbstractOpList +{ +public: + explicit ViewDeltaLegacyOpList(TArray OpList) + : OpList(MoveTemp(OpList)) + { + } + + virtual uint32 GetCount() const override + { + return OpList.Num(); + } + + virtual Worker_Op& operator[](uint32 Index) override + { + return OpList[Index]; + } + + virtual const Worker_Op& operator[](uint32 Index) const override + { + return OpList[Index]; + } + +private: + TArray OpList; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/WorkerConnectionOpList.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/WorkerConnectionOpList.h new file mode 100644 index 0000000000..15fc17f2d3 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/WorkerConnectionOpList.h @@ -0,0 +1,46 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once +#include "SpatialView/OpList/AbstractOpList.h" +#include "UniquePtr.h" +#include + +namespace SpatialGDK +{ + +class WorkerConnectionOpList : public AbstractOpList +{ +public: + explicit WorkerConnectionOpList(Worker_OpList* OpList) + : OpList(OpList) + { + } + + virtual uint32 GetCount() const override + { + return OpList->op_count; + } + + virtual Worker_Op& operator[](uint32 Index) override + { + return OpList->ops[Index]; + } + + virtual const Worker_Op& operator[](uint32 Index) const override + { + return OpList->ops[Index]; + } + +private: + struct Deleter + { + void operator()(Worker_OpList* Ops) const noexcept + { + Worker_OpList_Destroy(Ops); + } + }; + + TUniquePtr OpList; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewCoordinator.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewCoordinator.h new file mode 100644 index 0000000000..6084e1ff91 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewCoordinator.h @@ -0,0 +1,32 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once +#include "SpatialView/WorkerView.h" +#include "SpatialView/ConnectionHandlers/AbstractConnectionHandler.h" +#include "Templates/UniquePtr.h" + +namespace SpatialGDK +{ + +class ViewCoordinator +{ +public: + explicit ViewCoordinator(TUniquePtr ConnectionHandler); + + void Advance(); + void FlushMessagesToSend(); + + const TArray& GetCreateEntityResponses() const; + const TArray& GetAuthorityGained() const; + const TArray& GetAuthorityLost() const; + const TArray& GetAuthorityLostTemporarily() const; + + TUniquePtr GenerateLegacyOpList() const; + +private: + const ViewDelta* Delta; + WorkerView View; + TUniquePtr ConnectionHandler; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewDelta.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewDelta.h new file mode 100644 index 0000000000..3dc71d2c4b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewDelta.h @@ -0,0 +1,40 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once +#include "SpatialView/CommandMessages.h" +#include "SpatialView/OpList/AbstractOpList.h" +#include "SpatialView/AuthorityRecord.h" +#include "Containers/Array.h" +#include "Templates/UniquePtr.h" +#include + +namespace SpatialGDK +{ + +class ViewDelta +{ +public: + void AddCreateEntityResponse(CreateEntityResponse Response); + + void SetAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId, Worker_Authority Authority); + + const TArray& GetCreateEntityResponses() const; + const TArray& GetAuthorityGained() const; + const TArray& GetAuthorityLost() const; + const TArray& GetAuthorityLostTemporarily() const; + + // Returns an array of ops equivalent to the current state of the view delta. + // It is expected that Clear should be called between calls to GenerateLegacyOpList. + // todo Remove this once the view delta is not read via a legacy op list. + TUniquePtr GenerateLegacyOpList() const; + + void Clear(); + +private: + // todo wrap world command responses in their own record? + TArray CreateEntityResponses; + + AuthorityRecord AuthorityChanges; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/WorkerView.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/WorkerView.h new file mode 100644 index 0000000000..52f7085448 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/WorkerView.h @@ -0,0 +1,41 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once +#include "MessagesToSend.h" +#include "ViewDelta.h" +#include "Templates/UniquePtr.h" +#include + +namespace SpatialGDK +{ + +class WorkerView +{ +public: + WorkerView(); + + // Process queued op lists to create a new view delta. + // The view delta will exist until the next call to advance. + const ViewDelta* GenerateViewDelta(); + + // Add an OpList to generate the next ViewDelta. + void EnqueueOpList(TUniquePtr OpList); + + // Ensure all local changes have been applied and return the resulting MessagesToSend. + TUniquePtr FlushLocalChanges(); + + void SendCreateEntityRequest(CreateEntityRequest Request); + +private: + void ProcessOp(const Worker_Op& Op); + + void HandleAuthorityChange(const Worker_AuthorityChangeOp& AuthorityChange); + void HandleCreateEntityResponse(const Worker_CreateEntityResponseOp& Response); + + TArray> QueuedOps; + + ViewDelta Delta; + TUniquePtr LocalChanges; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Tests/TestDefinitions.h b/SpatialGDK/Source/SpatialGDK/Public/Tests/TestDefinitions.h new file mode 100644 index 0000000000..31a48dfe66 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Tests/TestDefinitions.h @@ -0,0 +1,12 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Misc/AutomationTest.h" + +#define GDK_TEST(ModuleName, ComponentName, TestName) \ + IMPLEMENT_SIMPLE_AUTOMATION_TEST(TestName, "SpatialGDK."#ModuleName"."#ComponentName"."#TestName, EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::EngineFilter) \ + bool TestName::RunTest(const FString& Parameters) + +#define GDK_COMPLEX_TEST(ModuleName, ComponentName, TestName) \ + IMPLEMENT_COMPLEX_AUTOMATION_TEST(TestName, "SpatialGDK."#ModuleName"."#ComponentName"."#TestName, EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::EngineFilter) diff --git a/SpatialGDK/Source/SpatialGDK/Public/Tests/TestingComponentViewHelpers.h b/SpatialGDK/Source/SpatialGDK/Public/Tests/TestingComponentViewHelpers.h new file mode 100644 index 0000000000..fc42a46cff --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Tests/TestingComponentViewHelpers.h @@ -0,0 +1,24 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" + +#include "Interop/SpatialStaticComponentView.h" + +#include + +struct SPATIALGDK_API TestingComponentViewHelpers +{ + // Can be used add components to a component view for a given entity. + static void AddEntityComponentToStaticComponentView(USpatialStaticComponentView& StaticComponentView, + const Worker_EntityId EntityId, + const Worker_ComponentId ComponentId, + Schema_ComponentData* ComponentData, + const Worker_Authority Authority); + + static void AddEntityComponentToStaticComponentView(USpatialStaticComponentView& StaticComponentView, + const Worker_EntityId EntityId, + const Worker_ComponentId ComponentId, + const Worker_Authority Authority); +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Tests/TestingSchemaHelpers.h b/SpatialGDK/Source/SpatialGDK/Public/Tests/TestingSchemaHelpers.h new file mode 100644 index 0000000000..04680334dd --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Tests/TestingSchemaHelpers.h @@ -0,0 +1,17 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" + +#include "SpatialCommonTypes.h" + +#include + +struct SPATIALGDK_API TestingSchemaHelpers +{ + // Can be used to create a Schema_Object to be passed to VirtualWorkerTranslator. + static Schema_Object* CreateTranslationComponentDataFields(); + // Can be used to add a mapping between virtual work id and physical worker name. + static void AddTranslationComponentDataMapping(Schema_Object* ComponentDataFields, VirtualWorkerId VWId, const PhysicalWorkerName& WorkerName); +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentFactory.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentFactory.h index da63899b75..e291d65292 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentFactory.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentFactory.h @@ -14,6 +14,7 @@ DECLARE_LOG_CATEGORY_EXTERN(LogComponentFactory, Log, All); class USpatialNetDriver; class USpatialPackageMap; class USpatialClassInfoManager; +class USpatialLatencyTracer; class USpatialPackageMapClient; class UNetDriver; @@ -27,24 +28,24 @@ namespace SpatialGDK class SPATIALGDK_API ComponentFactory { public: - ComponentFactory(bool bInterestDirty, USpatialNetDriver* InNetDriver); + ComponentFactory(bool bInterestDirty, USpatialNetDriver* InNetDriver, USpatialLatencyTracer* LatencyTracer); - TArray CreateComponentDatas(UObject* Object, const FClassInfo& Info, const FRepChangeState& RepChangeState, const FHandoverChangeState& HandoverChangeState); - TArray CreateComponentUpdates(UObject* Object, const FClassInfo& Info, Worker_EntityId EntityId, const FRepChangeState* RepChangeState, const FHandoverChangeState* HandoverChangeState); + TArray CreateComponentDatas(UObject* Object, const FClassInfo& Info, const FRepChangeState& RepChangeState, const FHandoverChangeState& HandoverChangeState, uint32& OutBytesWritten); + TArray CreateComponentUpdates(UObject* Object, const FClassInfo& Info, Worker_EntityId EntityId, const FRepChangeState* RepChangeState, const FHandoverChangeState* HandoverChangeState, uint32& OutBytesWritten); - Worker_ComponentData CreateHandoverComponentData(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes); + FWorkerComponentData CreateHandoverComponentData(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes, uint32& OutBytesWritten); - static Worker_ComponentData CreateEmptyComponentData(Worker_ComponentId ComponentId); + static FWorkerComponentData CreateEmptyComponentData(Worker_ComponentId ComponentId); private: - Worker_ComponentData CreateComponentData(Worker_ComponentId ComponentId, UObject* Object, const FRepChangeState& Changes, ESchemaComponentType PropertyGroup); - Worker_ComponentUpdate CreateComponentUpdate(Worker_ComponentId ComponentId, UObject* Object, const FRepChangeState& Changes, ESchemaComponentType PropertyGroup, bool& bWroteSomething); + FWorkerComponentData CreateComponentData(Worker_ComponentId ComponentId, UObject* Object, const FRepChangeState& Changes, ESchemaComponentType PropertyGroup, uint32& OutBytesWritten); + FWorkerComponentUpdate CreateComponentUpdate(Worker_ComponentId ComponentId, UObject* Object, const FRepChangeState& Changes, ESchemaComponentType PropertyGroup, uint32& OutBytesWritten); - bool FillSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FRepChangeState& Changes, ESchemaComponentType PropertyGroup, bool bIsInitialData, TArray* ClearedIds = nullptr); + uint32 FillSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FRepChangeState& Changes, ESchemaComponentType PropertyGroup, bool bIsInitialData, TraceKey* OutLatencyTraceId, TArray* ClearedIds = nullptr); - Worker_ComponentUpdate CreateHandoverComponentUpdate(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes, bool& bWroteSomething); + FWorkerComponentUpdate CreateHandoverComponentUpdate(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes, uint32& OutBytesWritten); - bool FillHandoverSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes, bool bIsInitialData, TArray* ClearedIds = nullptr); + uint32 FillHandoverSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes, bool bIsInitialData, TraceKey* OutLatencyTraceId, TArray* ClearedIds = nullptr); void AddProperty(Schema_Object* Object, Schema_FieldId FieldId, UProperty* Property, const uint8* Data, TArray* ClearedIds); @@ -53,6 +54,8 @@ class SPATIALGDK_API ComponentFactory USpatialClassInfoManager* ClassInfoManager; bool bInterestHasChanged; + + USpatialLatencyTracer* LatencyTracer; }; } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentReader.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentReader.h index 51115de6f3..f7e6c570c0 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentReader.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentReader.h @@ -13,17 +13,17 @@ namespace SpatialGDK class ComponentReader { public: - ComponentReader(class USpatialNetDriver* InNetDriver, FObjectReferencesMap& InObjectReferencesMap, TSet& InUnresolvedRefs); + ComponentReader(class USpatialNetDriver* InNetDriver, FObjectReferencesMap& InObjectReferencesMap); - void ApplyComponentData(const Worker_ComponentData& ComponentData, UObject* Object, USpatialActorChannel* Channel, bool bIsHandover); - void ApplyComponentUpdate(const Worker_ComponentUpdate& ComponentUpdate, UObject* Object, USpatialActorChannel* Channel, bool bIsHandover); + void ApplyComponentData(const Worker_ComponentData& ComponentData, UObject& Object, USpatialActorChannel& Channel, bool bIsHandover, bool& bOutReferencesChanged); + void ApplyComponentUpdate(const Worker_ComponentUpdate& ComponentUpdate, UObject& Object, USpatialActorChannel& Channel, bool bIsHandover, bool& bOutReferencesChanged); private: - void ApplySchemaObject(Schema_Object* ComponentObject, UObject* Object, USpatialActorChannel* Channel, bool bIsInitialData, const TArray& UpdatedIds, Worker_ComponentId ComponentId); - void ApplyHandoverSchemaObject(Schema_Object* ComponentObject, UObject* Object, USpatialActorChannel* Channel, bool bIsInitialData, const TArray& UpdatedIds, Worker_ComponentId ComponentId); + void ApplySchemaObject(Schema_Object* ComponentObject, UObject& Object, USpatialActorChannel& Channel, bool bIsInitialData, const TArray& UpdatedIds, Worker_ComponentId ComponentId, bool& bOutReferencesChanged); + void ApplyHandoverSchemaObject(Schema_Object* ComponentObject, UObject& Object, USpatialActorChannel& Channel, bool bIsInitialData, const TArray& UpdatedIds, Worker_ComponentId ComponentId, bool& bOutReferencesChanged); - void ApplyProperty(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, uint32 Index, UProperty* Property, uint8* Data, int32 Offset, int32 CmdIndex, int32 ParentIndex); - void ApplyArray(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, UArrayProperty* Property, uint8* Data, int32 Offset, int32 CmdIndex, int32 ParentIndex); + void ApplyProperty(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, uint32 Index, UProperty* Property, uint8* Data, int32 Offset, int32 CmdIndex, int32 ParentIndex, bool& bOutReferencesChanged); + void ApplyArray(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, UArrayProperty* Property, uint8* Data, int32 Offset, int32 CmdIndex, int32 ParentIndex, bool& bOutReferencesChanged); uint32 GetPropertyCount(const Schema_Object* Object, Schema_FieldId Id, UProperty* Property); @@ -32,7 +32,6 @@ class ComponentReader class USpatialNetDriver* NetDriver; class USpatialClassInfoManager* ClassInfoManager; FObjectReferencesMap& RootObjectReferencesMap; - TSet& UnresolvedRefs; }; } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/EngineVersionCheck.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/EngineVersionCheck.h index 1bed2379f6..a0839d3849 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/EngineVersionCheck.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/EngineVersionCheck.h @@ -7,7 +7,7 @@ // GDK Version to be updated with SPATIAL_ENGINE_VERSION // when breaking changes are made to the engine that requires // changes to the GDK to remain compatible -#define SPATIAL_GDK_VERSION 11 +#define SPATIAL_GDK_VERSION 19 // Check if GDK is compatible with the current version of Unreal Engine // SPATIAL_ENGINE_VERSION is incremented in engine when breaking changes diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityFactory.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityFactory.h new file mode 100644 index 0000000000..1fb285c7f1 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityFactory.h @@ -0,0 +1,45 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialCommonTypes.h" +#include "Utils/SpatialStatics.h" + +#include +#include + +DECLARE_LOG_CATEGORY_EXTERN(LogEntityFactory, Log, All); + +class AActor; +class USpatialActorChannel; +class USpatialNetDriver; +class USpatialPackageMap; +class USpatialClassInfoManager; +class USpatialPackageMapClient; +class SpatialActorGroupManager; + +namespace SpatialGDK +{ +class SpatialRPCService; + +struct RPCsOnEntityCreation; +using FRPCsOnEntityCreationMap = TMap, RPCsOnEntityCreation>; + +class SPATIALGDK_API EntityFactory +{ +public: + EntityFactory(USpatialNetDriver* InNetDriver, USpatialPackageMapClient* InPackageMap, USpatialClassInfoManager* InClassInfoManager, SpatialActorGroupManager* InActorGroupManager, SpatialRPCService* InRPCService); + + TArray CreateEntityComponents(USpatialActorChannel* Channel, FRPCsOnEntityCreationMap& OutgoingOnCreateEntityRPCs, uint32& OutBytesWritten); + TArray CreateTombstoneEntityComponents(AActor* Actor); + + static TArray GetComponentPresenceList(const TArray& ComponentDatas); + +private: + USpatialNetDriver* NetDriver; + USpatialPackageMapClient* PackageMap; + USpatialClassInfoManager* ClassInfoManager; + SpatialActorGroupManager* ActorGroupManager; + SpatialRPCService* RPCService; +}; +} // namepsace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/InspectionColors.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/InspectionColors.h new file mode 100644 index 0000000000..de6c7e249d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/InspectionColors.h @@ -0,0 +1,14 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialCommonTypes.h" +#include "Math/Color.h" + +// Mimicking Inspector V2 coloring from platform/js/console/src/inspector-v2/styles/colors.ts + +namespace SpatialGDK +{ + // Argument expected in the form: UnrealWorker1a2s3d4f... + FColor GetColorForWorkerName(const PhysicalWorkerName& WorkerName); +} diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/Interest/NetCullDistanceInterest.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/Interest/NetCullDistanceInterest.h new file mode 100644 index 0000000000..3934ea979b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/Interest/NetCullDistanceInterest.h @@ -0,0 +1,58 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Interop/SpatialClassInfoManager.h" +#include "Schema/Interest.h" + +/** + * This class gives static functionality which returns spatial interest constraints mirroring the net cull distance + * functionality of Unreal given the spatial class info manager. + * + * There are three different ways to generate the checkout radius constraint. The default is legacy NCD interest. + * This generates a disjunct of radius bucket queries where each spatial bucket is conjoined with all the components representing the actors with that + * net cull distance. There is also a minimum radius constraint which is not conjoined with any actor components. This is + * set to the default NCD. + * + * If bEnableNetCullDistanceInterest is true, instead each radius bucket generated will only be conjoined with a single + * marker component representing that net cull distance interest. These marker components are added to entities which represent + * actors with that defined net cull distance. An important distinction between this and legacy NCD is that + * all radius buckets now have a conjoined component. + * + * Further if also bEnableNetCullDistanceFrequency is true, then for each NCD, multiple queries will be generated. + * Inside each NCD, there will be further radius buckets all conjoined with the same NCD marker component, but only a small radius + * will receive the full frequency. More queries will be added representing bigger circles with lower frequencies depending on the + * configured frequency <-> distance ratio pairs, until the final circle will be at the configured NCD. This approach will generate + * n queries per client in total where n is the number of configured frequency buckets. + */ + +DECLARE_LOG_CATEGORY_EXTERN(LogNetCullDistanceInterest, Log, All); + +namespace SpatialGDK +{ + +class SPATIALGDK_API NetCullDistanceInterest +{ +public: + + static FrequencyConstraints CreateCheckoutRadiusConstraints(USpatialClassInfoManager* InClassInfoManager); + + // visible for testing + static TMap> DedupeDistancesAcrossActorTypes(const TMap ComponentSetToRadius); + +private: + + static FrequencyConstraints CreateLegacyNetCullDistanceConstraint(USpatialClassInfoManager* InClassInfoManager); + static FrequencyConstraints CreateNetCullDistanceConstraint(USpatialClassInfoManager* InClassInfoManager); + static FrequencyConstraints CreateNetCullDistanceConstraintWithFrequency(USpatialClassInfoManager* InClassInfoManager); + + static QueryConstraint GetDefaultCheckoutRadiusConstraint(); + static TMap GetActorTypeToRadius(); + static TArray BuildNonDefaultActorCheckoutConstraints(const TMap> DistanceToActorTypes, USpatialClassInfoManager* ClassInfoManager); + static float NetCullDistanceSquaredToSpatialDistance(float NetCullDistanceSquared); + + static void AddToFrequencyConstraintMap(const float Frequency, const QueryConstraint& Constraint, FrequencyToConstraintsMap& OutFrequencyToConstraints); + static void AddTypeHierarchyToConstraint(const UClass& BaseType, QueryConstraint& OutConstraint, USpatialClassInfoManager* ClassInfoManager); +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/InterestFactory.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/InterestFactory.h index 470e4691d8..c5ca3bc390 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/InterestFactory.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/InterestFactory.h @@ -7,55 +7,106 @@ #include +/** + * The InterestFactory is responsible for creating spatial Interest component state and updates for a GDK game. + * + * It has two dependencies: + * - the class info manager for finding level components and for creating user defined queries from ActorInterestComponents + * - the package map, for finding unreal object references as part of creating AlwaysInterested constraints + * (TODO) remove this dependency when/if we drop support for the AlwaysInterested constraint + * + * The interest factory is initialized within and has its lifecycle tied to the spatial net driver. + * + * There are two public types of functionality for this class. + * + * The first is actor interest. The factory takes information about an actor (the object, info and corresponding entity ID) + * and produces an interest data/update for that entity. This interest contains anything specific to that actor, such as self constraints + * for servers and clients, and if the actor is a player controller, the client worker's interest is also built for that actor. + * + * The other is server worker interest. Given a load balancing strategy, the factory will take the strategy's defined query constraint + * and produce an interest component to exist on the server's worker entity. This interest component contains the primary interest query made + * by that server worker. + */ + +class UAbstractLBStrategy; class USpatialClassInfoManager; class USpatialPackageMapClient; -class AActor; DECLARE_LOG_CATEGORY_EXTERN(LogInterestFactory, Log, All); namespace SpatialGDK { -void GatherClientInterestDistances(); - class SPATIALGDK_API InterestFactory { public: - InterestFactory(AActor* InActor, const FClassInfo& InInfo, USpatialClassInfoManager* InClassInfoManager, USpatialPackageMapClient* InPackageMap); + InterestFactory(USpatialClassInfoManager* InClassInfoManager, USpatialPackageMapClient* InPackageMap); - Worker_ComponentData CreateInterestData() const; - Worker_ComponentUpdate CreateInterestUpdate() const; + Worker_ComponentData CreateInterestData(AActor* InActor, const FClassInfo& InInfo, const Worker_EntityId InEntityId) const; + Worker_ComponentUpdate CreateInterestUpdate(AActor* InActor, const FClassInfo& InInfo, const Worker_EntityId InEntityId) const; - static Interest CreateServerWorkerInterest(); + Interest CreateServerWorkerInterest(const UAbstractLBStrategy* LBStrategy); private: - Interest CreateInterest() const; + // Shared constraints and result types are created at initialization and reused throughout the lifetime of the factory. + void CreateAndCacheInterestState(); + + // Build the checkout radius constraints for client workers + FrequencyConstraints CreateClientCheckoutRadiusConstraint(USpatialClassInfoManager* ClassInfoManager); + + // Builds the result types of necessary components for clients + // TODO: create and pull out into result types class + ResultType CreateClientNonAuthInterestResultType(USpatialClassInfoManager* ClassInfoManager); + ResultType CreateClientAuthInterestResultType(USpatialClassInfoManager* ClassInfoManager); + ResultType CreateServerNonAuthInterestResultType(USpatialClassInfoManager* ClassInfoManager); + ResultType CreateServerAuthInterestResultType(); + + Interest CreateInterest(AActor* InActor, const FClassInfo& InInfo, const Worker_EntityId InEntityId) const; - // Only uses Defined Constraint - Interest CreateActorInterest() const; // Defined Constraint AND Level Constraint - Interest CreatePlayerOwnedActorInterest() const; + void AddPlayerControllerActorInterest(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo) const; + // Self interests require the entity ID to know which entity is "self". This would no longer be required if there was a first class self constraint. + // The components clients need to see on entities they are have authority over that they don't already see through authority. + void AddClientSelfInterest(Interest& OutInterest, const Worker_EntityId& EntityId) const; + // The components servers need to see on entities they have authority over that they don't already see through authority. + void AddServerSelfInterest(Interest& OutInterest, const Worker_EntityId& EntityId) const; + + // Add the always relevant and the always interested query. + void AddAlwaysRelevantAndInterestedQuery(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo, const QueryConstraint& LevelConstraint) const; - void AddUserDefinedQueries(const QueryConstraint& LevelConstraints, TArray& OutQueries) const; + void AddUserDefinedQueries(Interest& OutInterest, const AActor* InActor, const QueryConstraint& LevelConstraint) const; + FrequencyToConstraintsMap GetUserDefinedFrequencyToConstraintsMap(const AActor* InActor) const; + void GetActorUserDefinedQueryConstraints(const AActor* InActor, FrequencyToConstraintsMap& OutFrequencyToConstraints, bool bRecurseChildren) const; - // Checkout Constraint OR AlwaysInterested Constraint - QueryConstraint CreateSystemDefinedConstraints() const; + void AddNetCullDistanceQueries(Interest& OutInterest, const QueryConstraint& LevelConstraint) const; + + void AddComponentQueryPairToInterestComponent(Interest& OutInterest, const Worker_ComponentId ComponentId, const Query& QueryToAdd) const; // System Defined Constraints - QueryConstraint CreateCheckoutRadiusConstraints() const; - QueryConstraint CreateAlwaysInterestedConstraint() const; + bool ShouldAddNetCullDistanceInterest(const AActor* InActor) const; + QueryConstraint CreateAlwaysInterestedConstraint(const AActor* InActor, const FClassInfo& InInfo) const; QueryConstraint CreateAlwaysRelevantConstraint() const; - // Only checkout entities that are in loaded sublevels - QueryConstraint CreateLevelConstraints() const; + // Only checkout entities that are in loaded sub-levels + QueryConstraint CreateLevelConstraints(const AActor* InActor) const; void AddObjectToConstraint(UObjectPropertyBase* Property, uint8* Data, QueryConstraint& OutConstraint) const; - void AddTypeHierarchyToConstraint(const UClass& BaseType, QueryConstraint& OutConstraint) const; - AActor* Actor; - const FClassInfo& Info; + // If the result types flag is flipped, set the specified result type. + void SetResultType(Query& OutQuery, const ResultType& InResultType) const; + USpatialClassInfoManager* ClassInfoManager; USpatialPackageMapClient* PackageMap; + + // The checkout radius constraint is built once for all actors in CreateCheckoutRadiusConstraint as it is equivalent for all actors. + // It is built once per net driver initialization. + FrequencyConstraints ClientCheckoutRadiusConstraint; + + // Cache the result types of queries. + ResultType ClientNonAuthInterestResultType; + ResultType ClientAuthInterestResultType; + ResultType ServerNonAuthInterestResultType; + ResultType ServerAuthInterestResultType; }; } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/RPCContainer.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/RPCContainer.h index 8c5774a457..3f8d385a84 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/RPCContainer.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/RPCContainer.h @@ -18,7 +18,8 @@ struct FPendingRPCParams; struct FRPCErrorInfo; DECLARE_DELEGATE_RetVal_OneParam(FRPCErrorInfo, FProcessRPCDelegate, const FPendingRPCParams&) -enum class ERPCResult : uint8_t +UENUM() +enum class ERPCResult : uint8 { Success, @@ -26,6 +27,8 @@ enum class ERPCResult : uint8_t UnresolvedTargetObject, MissingFunctionInfo, UnresolvedParameters, + ActorPendingKill, + TimedOut, // Sender specific NoActorChannel, @@ -58,14 +61,13 @@ struct FRPCErrorInfo TWeakObjectPtr TargetObject = nullptr; TWeakObjectPtr Function = nullptr; - bool bIsServer = false; - ERPCQueueType QueueType = ERPCQueueType::Unknown; ERPCResult ErrorCode = ERPCResult::Unknown; + bool bShouldDrop = false; }; struct SPATIALGDK_API FPendingRPCParams { - FPendingRPCParams(const FUnrealObjectRef& InTargetObjectRef, ESchemaComponentType InType, SpatialGDK::RPCPayload&& InPayload); + FPendingRPCParams(const FUnrealObjectRef& InTargetObjectRef, ERPCType InType, SpatialGDK::RPCPayload&& InPayload); // Moveable, not copyable. FPendingRPCParams() = delete; @@ -79,14 +81,15 @@ struct SPATIALGDK_API FPendingRPCParams SpatialGDK::RPCPayload Payload; FDateTime Timestamp; - ESchemaComponentType Type; + ERPCType Type; }; class SPATIALGDK_API FRPCContainer { public: // Moveable, not copyable. - FRPCContainer() = default; + FRPCContainer(ERPCQueueType QueueType); + FRPCContainer() = delete; FRPCContainer(const FRPCContainer&) = delete; FRPCContainer(FRPCContainer&&) = default; FRPCContainer& operator=(const FRPCContainer&) = delete; @@ -94,20 +97,22 @@ class SPATIALGDK_API FRPCContainer ~FRPCContainer() = default; void BindProcessingFunction(const FProcessRPCDelegate& Function); - void ProcessOrQueueRPC(const FUnrealObjectRef& InTargetObjectRef, ESchemaComponentType InType, SpatialGDK::RPCPayload&& InPayload); + void ProcessOrQueueRPC(const FUnrealObjectRef& InTargetObjectRef, ERPCType InType, SpatialGDK::RPCPayload&& InPayload); void ProcessRPCs(); + void DropForEntity(const Worker_EntityId& EntityId); - bool ObjectHasRPCsQueuedOfType(const Worker_EntityId& EntityId, ESchemaComponentType Type) const; - - static const double SECONDS_BEFORE_WARNING; + bool ObjectHasRPCsQueuedOfType(const Worker_EntityId& EntityId, ERPCType Type) const; private: using FArrayOfParams = TArray; using FRPCMap = TMap; - using RPCContainerType = TMap; + using RPCContainerType = TMap; void ProcessRPCs(FArrayOfParams& RPCList); bool ApplyFunction(FPendingRPCParams& Params); RPCContainerType QueuedRPCs; FProcessRPCDelegate ProcessingFunction; + bool bAlreadyProcessingRPCs = false; + + ERPCQueueType QueueType = ERPCQueueType::Unknown; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/RPCRingBuffer.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/RPCRingBuffer.h new file mode 100644 index 0000000000..45a32f07f0 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/RPCRingBuffer.h @@ -0,0 +1,70 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Misc/Optional.h" + +#include "Schema/RPCPayload.h" + +#include +#include + +namespace SpatialGDK +{ + +struct RPCRingBuffer +{ + RPCRingBuffer(ERPCType InType); + + const TOptional& GetRingBufferElement(uint64 RPCId) const + { + return RingBuffer[(RPCId - 1) % RingBuffer.Num()]; + } + + ERPCType Type; + TArray> RingBuffer; + uint64 LastSentRPCId = 0; +}; + +struct RPCRingBufferDescriptor +{ + uint32 GetRingBufferElementIndex(uint64 RPCId) const + { + return (RPCId - 1) % RingBufferSize; + } + + Schema_FieldId GetRingBufferElementFieldId(uint64 RPCId) const + { + return SchemaFieldStart + GetRingBufferElementIndex(RPCId); + } + + uint32 RingBufferSize; + Schema_FieldId SchemaFieldStart; + Schema_FieldId LastSentRPCFieldId; +}; + +namespace RPCRingBufferUtils +{ + +Worker_ComponentId GetRingBufferComponentId(ERPCType Type); +RPCRingBufferDescriptor GetRingBufferDescriptor(ERPCType Type); +uint32 GetRingBufferSize(ERPCType Type); + +Worker_ComponentId GetAckComponentId(ERPCType Type); +Schema_FieldId GetAckFieldId(ERPCType Type); + +Schema_FieldId GetInitiallyPresentMulticastRPCsCountFieldId(); + +bool ShouldQueueOverflowed(ERPCType Type); + +void ReadBufferFromSchema(Schema_Object* SchemaObject, RPCRingBuffer& OutBuffer); +void ReadAckFromSchema(const Schema_Object* SchemaObject, ERPCType Type, uint64& OutAck); + +void WriteRPCToSchema(Schema_Object* SchemaObject, ERPCType Type, uint64 RPCId, const RPCPayload& Payload); +void WriteAckToSchema(Schema_Object* SchemaObject, ERPCType Type, uint64 Ack); + +void MoveLastSentIdToInitiallyPresentCount(Schema_Object* SchemaObject, uint64 LastSentId); + +} // namespace RPCRingBufferUtils + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaDatabase.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaDatabase.h index c919b88828..fc4610fd7c 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaDatabase.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaDatabase.h @@ -1,3 +1,5 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + #pragma once #include "Containers/StaticArray.h" @@ -90,13 +92,32 @@ class SPATIALGDK_API USchemaDatabase : public UDataAsset UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) TMap LevelPathToComponentId; + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) + TMap NetCullDistanceToComponentId; + + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) + TSet NetCullDistanceComponentIds; + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) TMap ComponentIdToClassPath; + // These component ID lists for each data type are stored separately as you cannot have nested maps in a UPROPERTY + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) + TArray DataComponentIds; + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) - TSet LevelComponentIds; + TArray OwnerOnlyComponentIds; + + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) + TArray HandoverComponentIds; + + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) + TArray LevelComponentIds; UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) uint32 NextAvailableComponentId; + + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) + uint32 SchemaDescriptorHash; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaOption.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaOption.h index c6e26eb5fd..c70074238d 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaOption.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaOption.h @@ -1,3 +1,5 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + #pragma once #include "Templates/UniquePtr.h" diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaUtils.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaUtils.h index e3d05650bd..ba94c9ec7e 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaUtils.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaUtils.h @@ -80,9 +80,9 @@ inline void AddWorkerRequirementSetToSchema(Schema_Object* Object, Schema_FieldI } } -inline WorkerRequirementSet GetWorkerRequirementSetFromSchema(Schema_Object* Object, Schema_FieldId Id) +inline WorkerRequirementSet IndexWorkerRequirementSetFromSchema(Schema_Object* Object, Schema_FieldId Id, uint32 Index) { - Schema_Object* RequirementSetObject = Schema_GetObject(Object, Id); + Schema_Object* RequirementSetObject = Schema_IndexObject(Object, Id, Index); int32 AttributeSetCount = (int32)Schema_GetObjectCount(RequirementSetObject, 1); WorkerRequirementSet RequirementSet; @@ -107,6 +107,11 @@ inline WorkerRequirementSet GetWorkerRequirementSetFromSchema(Schema_Object* Obj return RequirementSet; } +inline WorkerRequirementSet GetWorkerRequirementSetFromSchema(Schema_Object* Object, Schema_FieldId Id) +{ + return IndexWorkerRequirementSetFromSchema(Object, Id, 0); +} + inline void AddObjectRefToSchema(Schema_Object* Object, Schema_FieldId Id, const FUnrealObjectRef& ObjectRef) { using namespace SpatialConstants; @@ -204,11 +209,11 @@ inline void AddRotatorToSchema(Schema_Object* Object, Schema_FieldId Id, FRotato Schema_AddFloat(RotatorObject, 3, Rotator.Roll); } -inline FRotator GetRotatorFromSchema(Schema_Object* Object, Schema_FieldId Id) +inline FRotator IndexRotatorFromSchema(Schema_Object* Object, Schema_FieldId Id, uint32 Index) { FRotator Rotator; - Schema_Object* RotatorObject = Schema_GetObject(Object, Id); + Schema_Object* RotatorObject = Schema_IndexObject(Object, Id, Index); Rotator.Pitch = Schema_GetFloat(RotatorObject, 1); Rotator.Yaw = Schema_GetFloat(RotatorObject, 2); @@ -217,6 +222,11 @@ inline FRotator GetRotatorFromSchema(Schema_Object* Object, Schema_FieldId Id) return Rotator; } +inline FRotator GetRotatorFromSchema(Schema_Object* Object, Schema_FieldId Id) +{ + return IndexRotatorFromSchema(Object, Id, 0); +} + inline void AddVectorToSchema(Schema_Object* Object, Schema_FieldId Id, FVector Vector) { Schema_Object* VectorObject = Schema_AddObject(Object, Id); @@ -226,11 +236,11 @@ inline void AddVectorToSchema(Schema_Object* Object, Schema_FieldId Id, FVector Schema_AddFloat(VectorObject, 3, Vector.Z); } -inline FVector GetVectorFromSchema(Schema_Object* Object, Schema_FieldId Id) +inline FVector IndexVectorFromSchema(Schema_Object* Object, Schema_FieldId Id, uint32 Index) { FVector Vector; - Schema_Object* VectorObject = Schema_GetObject(Object, Id); + Schema_Object* VectorObject = Schema_IndexObject(Object, Id, Index); Vector.X = Schema_GetFloat(VectorObject, 1); Vector.Y = Schema_GetFloat(VectorObject, 2); @@ -239,6 +249,11 @@ inline FVector GetVectorFromSchema(Schema_Object* Object, Schema_FieldId Id) return Vector; } +inline FVector GetVectorFromSchema(Schema_Object* Object, Schema_FieldId Id) +{ + return IndexVectorFromSchema(Object, Id, 0); +} + // Generates the full path from an ObjectRef, if it has paths. Writes the result to OutPath. // Does not clear OutPath first. void GetFullPathFromUnrealObjectReference(const FUnrealObjectRef& ObjectRef, FString& OutPath); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/ActorGroupManager.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialActorGroupManager.h similarity index 92% rename from SpatialGDK/Source/SpatialGDK/Public/Utils/ActorGroupManager.h rename to SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialActorGroupManager.h index 60bc525b6d..05bddd1f59 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/ActorGroupManager.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialActorGroupManager.h @@ -1,10 +1,12 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + #pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "SpatialConstants.h" -#include "ActorGroupManager.generated.h" +#include "SpatialActorGroupManager.generated.h" USTRUCT() struct FWorkerType @@ -45,11 +47,8 @@ struct FActorGroupInfo } }; -UCLASS(Config=SpatialGDKSettings) -class SPATIALGDK_API UActorGroupManager : public UObject +class SPATIALGDK_API SpatialActorGroupManager { - GENERATED_BODY() - private: TMap, FName> ClassPathToActorGroup; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialActorUtils.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialActorUtils.h index 234f46a906..283be0b1c1 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialActorUtils.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialActorUtils.h @@ -2,22 +2,43 @@ #pragma once -#include "CoreMinimal.h" +#include "EngineClasses/SpatialNetConnection.h" #include "Components/SceneComponent.h" +#include "Containers/Array.h" +#include "Containers/UnrealString.h" #include "Engine/EngineTypes.h" -#include "EngineClasses/SpatialNetConnection.h" #include "GameFramework/Actor.h" #include "GameFramework/Controller.h" +#include "GameFramework/PlayerController.h" +#include "Math/Vector.h" namespace SpatialGDK { -inline FString GetOwnerWorkerAttribute(AActor* Actor) +inline AActor* GetHierarchyRoot(const AActor* Actor) +{ + check(Actor != nullptr); + + AActor* Owner = Actor->GetOwner(); + if (Owner == nullptr || Owner->IsPendingKillPending()) + { + return nullptr; + } + + while (Owner->GetOwner() != nullptr && !Owner->GetOwner()->IsPendingKillPending()) + { + Owner = Owner->GetOwner(); + } + + return Owner; +} + +inline FString GetConnectionOwningWorkerId(const AActor* Actor) { if (const USpatialNetConnection* NetConnection = Cast(Actor->GetNetConnection())) { - return NetConnection->WorkerAttribute; + return NetConnection->ConnectionOwningWorkerId; } return FString(); @@ -28,6 +49,7 @@ inline FVector GetActorSpatialPosition(const AActor* InActor) FVector Location = FVector::ZeroVector; // If the Actor is a Controller, use its Pawn's position, + // Otherwise if the Actor is a PlayerController, use its last spectator sync location, otherwise its focal point // Otherwise if the Actor has an Owner, use its position. // Otherwise if the Actor has a well defined location then use that // Otherwise use the origin @@ -37,6 +59,17 @@ inline FVector GetActorSpatialPosition(const AActor* InActor) USceneComponent* PawnRootComponent = Controller->GetPawn()->GetRootComponent(); Location = PawnRootComponent ? PawnRootComponent->GetComponentLocation() : FVector::ZeroVector; } + else if (const APlayerController* PlayerController = Cast(Controller)) + { + if (PlayerController->IsInState(NAME_Spectating)) + { + Location = PlayerController->LastSpectatorSyncLocation; + } + else + { + Location = PlayerController->GetFocalLocation(); + } + } else if (InActor->GetOwner() != nullptr && InActor->GetOwner()->GetIsReplicated()) { return GetActorSpatialPosition(InActor->GetOwner()); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialDebugger.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialDebugger.h new file mode 100644 index 0000000000..095dca0b6b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialDebugger.h @@ -0,0 +1,184 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "LoadBalancing/GridBasedLBStrategy.h" +#include "LoadBalancing/WorkerRegion.h" +#include "SpatialCommonTypes.h" + +#include "Containers/Map.h" +#include "CoreMinimal.h" +#include "Engine/Canvas.h" +#include "GameFramework/Info.h" +#include "Materials/Material.h" +#include "Math/Box2D.h" +#include "Math/Color.h" +#include "Templates/Tuple.h" + +#include +#include "SpatialDebugger.generated.h" + +class APawn; +class APlayerController; +class APlayerState; +class USpatialNetDriver; +class UFont; +class UTexture2D; + +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialDebugger, Log, All); + +DECLARE_STATS_GROUP(TEXT("SpatialDebugger"), STATGROUP_SpatialDebugger, STATCAT_Advanced); + +DECLARE_CYCLE_STAT(TEXT("DrawDebug"), STAT_DrawDebug, STATGROUP_SpatialDebugger); +DECLARE_CYCLE_STAT(TEXT("DrawTag"), STAT_DrawTag, STATGROUP_SpatialDebugger); +DECLARE_CYCLE_STAT(TEXT("Projection"), STAT_Projection, STATGROUP_SpatialDebugger); +DECLARE_CYCLE_STAT(TEXT("DrawIcons"), STAT_DrawIcons, STATGROUP_SpatialDebugger); +DECLARE_CYCLE_STAT(TEXT("DrawText"), STAT_DrawText, STATGROUP_SpatialDebugger); +DECLARE_CYCLE_STAT(TEXT("BuildText"), STAT_BuildText, STATGROUP_SpatialDebugger); +DECLARE_CYCLE_STAT(TEXT("SortingActors"), STAT_SortingActors, STATGROUP_SpatialDebugger); + +USTRUCT() +struct FWorkerRegionInfo +{ + GENERATED_BODY() + + UPROPERTY() + FColor Color; + + UPROPERTY() + FBox2D Extents; +}; + +UCLASS(SpatialType=(Singleton, NotPersistent), Blueprintable, NotPlaceable) +class SPATIALGDK_API ASpatialDebugger : + public AInfo +{ + GENERATED_UCLASS_BODY() + +public: + + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + virtual void Tick(float DeltaSeconds) override; + virtual void BeginPlay() override; + virtual void Destroyed() override; + + virtual void OnAuthorityGained() override; + + UFUNCTION(Exec, Category = "SpatialGDK", BlueprintCallable) + void SpatialToggleDebugger(); + + // TODO: Expose these through a runtime UI: https://improbableio.atlassian.net/browse/UNR-2359. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = LocalPlayer, meta = (ToolTip = "X location of player data panel")) + int PlayerPanelStartX = 64; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = LocalPlayer, meta = (ToolTip = "Y location of player data panel")) + int PlayerPanelStartY = 128; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = General, meta = (ToolTip = "Maximum range from local player that tags will be drawn out to")) + float MaxRange = 100.0f * 100.0f; // 100m + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Visualization, meta = (ToolTip = "Show server authority for every entity in range")) + bool bShowAuth = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Visualization, meta = (ToolTip = "Show authority intent for every entity in range")) + bool bShowAuthIntent = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Visualization, meta = (ToolTip = "Show lock status for every entity in range")) + bool bShowLock = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Visualization, meta = (ToolTip = "Show EntityId for every entity in range")) + bool bShowEntityId = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Visualization, meta = (ToolTip = "Show Actor Name for every entity in range")) + bool bShowActorName = false; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = StartUp, meta = (ToolTip = "Show the Spatial Debugger automatically at startup")) + bool bAutoStart = false; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Visualization, meta = (ToolTip = "Show a transparent Worker Region cuboid representing the area of authority for each server worker")) + bool bShowWorkerRegions = false; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Visualization, meta = (ToolTip = "Texture to use for the Auth Icon")) + UTexture2D *AuthTexture; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Visualization, meta = (ToolTip = "Texture to use for the Auth Intent Icon")) + UTexture2D *AuthIntentTexture; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Visualization, meta = (ToolTip = "Texture to use for the Unlocked Icon")) + UTexture2D *UnlockedTexture; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Visualization, meta = (ToolTip = "Texture to use for the Locked Icon")) + UTexture2D *LockedTexture; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Visualization, meta = (ToolTip = "Texture to use for the Box Icon")) + UTexture2D *BoxTexture; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Visualization, meta = (ToolTip = "WorldSpace offset of tag from actor pivot")) + FVector WorldSpaceActorTagOffset = FVector(0.0f, 0.0f, 200.0f); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Visualization, meta = (ToolTip = "Color used for any server with an unresolved name")) + FColor InvalidServerTintColor = FColor::Magenta; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Visualization, meta = (ToolTip = "Vertical scale to apply to each worker region cuboid")) + float WorkerRegionVerticalScale = 1.0f; + + UPROPERTY(ReplicatedUsing = OnRep_SetWorkerRegions) + TArray WorkerRegions; + + UFUNCTION() + virtual void OnRep_SetWorkerRegions(); + + void ActorAuthorityChanged(const Worker_AuthorityChangeOp& AuthOp) const; + void ActorAuthorityIntentChanged(Worker_EntityId EntityId, VirtualWorkerId NewIntentVirtualWorkerId) const; + +private: + + void LoadIcons(); + + // FOnEntityAdded/FOnEntityRemoved Delegates + void OnEntityAdded(const Worker_EntityId EntityId); + void OnEntityRemoved(const Worker_EntityId EntityId); + + // FDebugDrawDelegate + void DrawDebug(UCanvas* Canvas, APlayerController* Controller); + + void DrawTag(UCanvas* Canvas, const FVector2D& ScreenLocation, const Worker_EntityId EntityId, const FString& ActorName); + void DrawDebugLocalPlayer(UCanvas* Canvas); + + void CreateWorkerRegions(); + void DestroyWorkerRegions(); + + FColor GetTextColorForBackgroundColor(const FColor& BackgroundColor) const; + int32 GetNumberOfDigitsIn(int32 SomeNumber) const; + + static const int ENTITY_ACTOR_MAP_RESERVATION_COUNT = 512; + static const int PLAYER_TAG_VERTICAL_OFFSET = 18; + + enum EIcon + { + ICON_AUTH, + ICON_AUTH_INTENT, + ICON_UNLOCKED, + ICON_LOCKED, + ICON_BOX, + ICON_MAX + }; + + USpatialNetDriver* NetDriver; + + // These mappings are maintained independently on each client + // Mapping of the entities a client has checked out + TMap> EntityActorMapping; + + FDelegateHandle DrawDebugDelegateHandle; + FDelegateHandle OnEntityAddedHandle; + FDelegateHandle OnEntityRemovedHandle; + + TWeakObjectPtr LocalPawn; + TWeakObjectPtr LocalPlayerController; + TWeakObjectPtr LocalPlayerState; + UFont* RenderFont; + + FFontRenderInfo FontRenderInfo; + FCanvasIcon Icons[ICON_MAX]; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyPayload.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyPayload.h new file mode 100644 index 0000000000..f9769f4588 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyPayload.h @@ -0,0 +1,44 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "Containers/Array.h" +#include "Hash/CityHash.h" +#include "SpatialCommonTypes.h" + +#include "SpatialLatencyPayload.generated.h" + +USTRUCT(BlueprintType) +struct SPATIALGDK_API FSpatialLatencyPayload +{ + GENERATED_BODY() + + FSpatialLatencyPayload() {} + + FSpatialLatencyPayload(TArray&& TraceBytes, TArray&& SpanBytes, TraceKey InKey) + : TraceId(MoveTemp(TraceBytes)) + , SpanId(MoveTemp(SpanBytes)) + , Key(InKey) + {} + + UPROPERTY() + TArray TraceId; + + UPROPERTY() + TArray SpanId; + + UPROPERTY(NotReplicated) + int32 Key = InvalidTraceKey; + + // Required for TMap hash + bool operator == (const FSpatialLatencyPayload& Other) const + { + return TraceId == Other.TraceId && SpanId == Other.SpanId; + } + + friend uint32 GetTypeHash(const FSpatialLatencyPayload& Obj) + { + return CityHash32((const char*)Obj.TraceId.GetData(), Obj.TraceId.Num()) ^ CityHash32((const char*)Obj.SpanId.GetData(), Obj.SpanId.Num()); + } +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyTracer.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyTracer.h new file mode 100644 index 0000000000..d5c8ad9d49 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyTracer.h @@ -0,0 +1,187 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" + +#include "SpatialConstants.h" +#include "Containers/Map.h" +#include "Containers/StaticArray.h" +#include "SpatialLatencyPayload.h" + +#if TRACE_LIB_ACTIVE +#include "WorkerSDK/improbable/trace.h" +#endif + +#include "SpatialLatencyTracer.generated.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialLatencyTracing, Log, All); + +class AActor; +class UFunction; +class USpatialGameInstance; + +namespace SpatialGDK +{ + struct FOutgoingMessage; +} // namespace SpatialGDK + +/** + * Enum that maps Unreal's log verbosity to allow use in settings. +**/ +UENUM() +namespace ETraceType +{ + enum Type + { + RPC, + Property, + Tagged + }; +} + +UCLASS() +class SPATIALGDK_API USpatialLatencyTracer : public UObject +{ + GENERATED_BODY() + +public: + + ////////////////////////////////////////////////////////////////////////// + // + // EXPERIMENTAL: We do not support this functionality currently: Do not use it unless you are Improbable staff. + // + // USpatialLatencyTracer allows for tracing of gameplay events across multiple workers, from their user + // instigation, to their observed results. Each of these multi-worker events are tracked through `traces` + // which allow the user to see collected timings of these events in a single location. Key timings related + // to these events are logged throughout the Unreal GDK networking stack. This API makes the assumption + // that the distributed workers have had their clocks synced by some time syncing protocol (eg. NTP). To + // give accurate timings, the trace payload is embedded directly within the relevant networking component + // updates. This framework also assumes that the worker that calls BeginLatencyTrace will also eventually + // call EndLatencyTrace on the trace. This allows accurate end-to-end timings. + // + // These timings are logged to Google's Stackdriver (https://cloud.google.com/stackdriver/) + // + // Setup: + // 1. Run UnrealGDK SetupIncTraceLibs.bat to include latency tracking libraries. + // 2. Setup a Google project with access to Stackdriver. + // 3. Create and download a service-account certificate + // 4. Set an environment variable GOOGLE_APPLICATION_CREDENTIALS to certificate path + // 5. Set an environment variable GRPC_DEFAULT_SSL_ROOTS_FILE_PATH to your `roots.pem` gRPC path + // + // Usage: + // 1. Register your Google's project id with `RegisterProject` + // 2. Start a latency trace using `BeginLatencyTrace` and store the returned payload. + // 3. Pass this payload to a variant of `ContinueLatencyTrace` depending on how you want to continue the trace (rpc/property/tag) + // - If continuing via an RPC include the FSpatialLatencyPayload as an RPC parameter + // - If continuing via a Property ensure the property is of type FSpatialLatencyPayload + // 4. Repeat (3) until the trace is returned to the originating worker. + // 5. Call `EndLatencyTrace` on the returned payload. + // + ////////////////////////////////////////////////////////////////////////// + + USpatialLatencyTracer(); + + // Front-end exposed, allows users to register, start, continue, and end traces + + // Call with your google project id. This must be called before latency trace calls are made. + UFUNCTION(BlueprintCallable, Category = "SpatialOS", meta = (WorldContext = "WorldContextObject")) + static void RegisterProject(UObject* WorldContextObject, const FString& ProjectId); + + // Set metadata string to be included in all span names. Resulting uploaded span names are of the format "USER_SPECIFIED_NAME (METADATA : WORKER_ID)". + UFUNCTION(BlueprintCallable, Category = "SpatialOS", meta = (WorldContext = "WorldContextObject")) + static bool SetTraceMetadata(UObject* WorldContextObject, const FString& NewTraceMetadata); + + // Start a latency trace. This will start the latency timer and return you a LatencyPayload object. This payload can then be "continued" via a ContinueLatencyTrace call. + UFUNCTION(BlueprintCallable, Category = "SpatialOS", meta = (WorldContext = "WorldContextObject")) + static bool BeginLatencyTrace(UObject* WorldContextObject, const FString& TraceDesc, FSpatialLatencyPayload& OutLatencyPayload); + + // Attach a LatencyPayload to an RPC/Actor pair. The next time that RPC is executed on that Actor, the timings will be measured. + // You must also send the OutContinuedLatencyPayload as a parameter in the RPC. + UFUNCTION(BlueprintCallable, Category = "SpatialOS", meta = (WorldContext = "WorldContextObject")) + static bool ContinueLatencyTraceRPC(UObject* WorldContextObject, const AActor* Actor, const FString& Function, const FString& TraceDesc, const FSpatialLatencyPayload& LatencyPayload, FSpatialLatencyPayload& OutContinuedLatencyPayload); + + // Attach a LatencyPayload to an Property/Actor pair. The next time that Property is executed on that Actor, the timings will be measured. + // The property being measured should be a FSpatialLatencyPayload and should be set to OutContinuedLatencyPayload. + UFUNCTION(BlueprintCallable, Category = "SpatialOS", meta = (WorldContext = "WorldContextObject")) + static bool ContinueLatencyTraceProperty(UObject* WorldContextObject, const AActor* Actor, const FString& Property, const FString& TraceDesc, const FSpatialLatencyPayload& LatencyPayload, FSpatialLatencyPayload& OutContinuedLatencyPayload); + + // Store a LatencyPayload to an Tag/Actor pair. This payload will be stored internally until the user is ready to retrieve it. + // Use RetrievePayload to retrieve the Payload + UFUNCTION(BlueprintCallable, Category = "SpatialOS", meta = (WorldContext = "WorldContextObject")) + static bool ContinueLatencyTraceTagged(UObject* WorldContextObject, const AActor* Actor, const FString& Tag, const FString& TraceDesc, const FSpatialLatencyPayload& LatencyPayload, FSpatialLatencyPayload& OutContinuedLatencyPayload); + + // End a latency trace. This will terminate the trace, and can be called on multiple workers all operating on the same trace but the worker + // that called BeginLatencyTrace must call this at some point to ensure correct e2e latency timings. + UFUNCTION(BlueprintCallable, Category = "SpatialOS", meta = (WorldContext = "WorldContextObject")) + static bool EndLatencyTrace(UObject* WorldContextObject, const FSpatialLatencyPayload& LatencyPayLoad); + + // Returns a previously saved payload from ContinueLatencyTraceTagged + UFUNCTION(BlueprintCallable, Category = "SpatialOS", meta = (WorldContext = "WorldContextObject")) + static FSpatialLatencyPayload RetrievePayload(UObject* WorldContextObject, const AActor* Actor, const FString& Tag); + + // Internal GDK usage, shouldn't be used by game code + static USpatialLatencyTracer* GetTracer(UObject* WorldContextObject); + +#if TRACE_LIB_ACTIVE + + bool IsValidKey(TraceKey Key); + TraceKey RetrievePendingTrace(const UObject* Obj, const UFunction* Function); + TraceKey RetrievePendingTrace(const UObject* Obj, const UProperty* Property); + TraceKey RetrievePendingTrace(const UObject* Obj, const FString& Tag); + + void WriteToLatencyTrace(const TraceKey Key, const FString& TraceDesc); + void WriteAndEndTraceIfRemote(const TraceKey Key, const FString& TraceDesc); + + void WriteTraceToSchemaObject(const TraceKey Key, Schema_Object* Obj, const Schema_FieldId FieldId); + TraceKey ReadTraceFromSchemaObject(Schema_Object* Obj, const Schema_FieldId FieldId); + + void SetWorkerId(const FString& NewWorkerId) { WorkerId = NewWorkerId; } + void ResetWorkerId(); + + void OnEnqueueMessage(const SpatialGDK::FOutgoingMessage*); + void OnDequeueMessage(const SpatialGDK::FOutgoingMessage*); + +private: + + using ActorFuncKey = TPair; + using ActorPropertyKey = TPair; + using ActorTagKey = TPair; + using TraceSpan = improbable::trace::Span; + + bool BeginLatencyTrace_Internal(const FString& TraceDesc, FSpatialLatencyPayload& OutLatencyPayload); + bool ContinueLatencyTrace_Internal(const AActor* Actor, const FString& Target, ETraceType::Type Type, const FString& TraceDesc, const FSpatialLatencyPayload& LatencyPayload, FSpatialLatencyPayload& OutLatencyPayload); + bool EndLatencyTrace_Internal(const FSpatialLatencyPayload& LatencyPayload); + + FSpatialLatencyPayload RetrievePayload_Internal(const UObject* Actor, const FString& Key); + + bool AddTrackingInfo(const AActor* Actor, const FString& Target, const ETraceType::Type Type, const TraceKey Key); + + TraceKey GenerateNewTraceKey(); + void ResolveKeyInLatencyPayload(FSpatialLatencyPayload& Payload); + + void WriteKeyFrameToTrace(const TraceSpan* Trace, const FString& TraceDesc); + FString FormatMessage(const FString& Message, bool bIncludeMetadata = false) const; + + FString WorkerId; + FString TraceMetadata; + + TraceKey NextTraceKey = 1; + + FCriticalSection Mutex; // This mutex is to protect modifications to the containers below + + TMap TrackingRPCs; + TMap TrackingProperties; + TMap TrackingTags; + TMap TraceMap; + + TSet RootTraces; + +public: + +#endif // TRACE_LIB_ACTIVE + + // Used for testing trace functionality, will send a debug trace in three parts from this worker + UFUNCTION(BlueprintCallable, Category = "SpatialOS") + static void Debug_SendTestTrace(); +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h index eb8f8f195a..aa3e0920b8 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h @@ -11,20 +11,19 @@ #include "SpatialMetrics.generated.h" -class USpatialNetDriver; class USpatialWorkerConnection; DECLARE_LOG_CATEGORY_EXTERN(LogSpatialMetrics, Log, All); UCLASS() -class USpatialMetrics : public UObject +class SPATIALGDK_API USpatialMetrics : public UObject { GENERATED_BODY() public: - void Init(USpatialNetDriver* InNetDriver); + void Init(USpatialWorkerConnection* Connection, float MaxServerTickRate, bool bIsServer); - void TickMetrics(); + void TickMetrics(float NetDriverTime); double CalculateLoad() const; @@ -43,18 +42,26 @@ class USpatialMetrics : public UObject void SpatialModifySetting(const FString& Name, float Value); void OnModifySettingCommand(Schema_Object* CommandPayload); - void TrackSentRPC(UFunction* Function, ESchemaComponentType RPCType, int PayloadSize); + void TrackSentRPC(UFunction* Function, ERPCType RPCType, int PayloadSize); void HandleWorkerMetrics(Worker_Op* Op); // The user can bind their own delegate to handle worker metrics. typedef TMap WorkerMetrics; - DECLARE_MULTICAST_DELEGATE_OneParam(WorkerMetricsDelegate, WorkerMetrics) - WorkerMetricsDelegate WorkerMetricsRecieved; + DECLARE_MULTICAST_DELEGATE_OneParam(WorkerMetricsDelegate, WorkerMetrics); + static WorkerMetricsDelegate WorkerMetricsRecieved; + + // Delegate used to poll for the current player controller's reference + DECLARE_DELEGATE_RetVal(FUnrealObjectRef, FControllerRefProviderDelegate); + FControllerRefProviderDelegate ControllerRefProvider; private: + UPROPERTY() - USpatialNetDriver* NetDriver; + USpatialWorkerConnection* Connection; + + bool bIsServer; + float NetServerMaxTickRate; float TimeOfLastReport; float TimeSinceLastReport; @@ -70,7 +77,7 @@ class USpatialMetrics : public UObject // tracking on the server. struct RPCStat { - ESchemaComponentType Type; + ERPCType Type; FString Name; int Calls; int TotalPayload; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialStatics.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialStatics.h index f6a8023750..3506523682 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialStatics.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialStatics.h @@ -8,9 +8,12 @@ #include "Templates/SubclassOf.h" #include "UObject/TextProperty.h" +#include "SpatialGDKSettings.h" + #include "SpatialStatics.generated.h" class AActor; +class SpatialActorGroupManager; // This log category will always log to the spatial runtime and thus also be printed in the SpatialOutput. DECLARE_LOG_CATEGORY_EXTERN(LogSpatial, Log, All); @@ -79,8 +82,53 @@ class SPATIALGDK_API USpatialStatics : public UBlueprintFunctionLibrary UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject", CallableWithoutWorldContext, Keywords = "log spatial", AdvancedDisplay = "2", DevelopmentOnly), Category = "Utilities|Text") static void PrintTextSpatial(UObject* WorldContextObject, const FText InText = INVTEXT("Hello"), bool bPrintToScreen = true, FLinearColor TextColor = FLinearColor(0.0, 0.66, 1.0), float Duration = 2.f); + /** + * Returns true if worker flag with the given name was found. + * Gets value of a worker flag. + */ + UFUNCTION(BlueprintCallable, Category = "SpatialOS", meta = (WorldContext = "WorldContextObject")) + static bool GetWorkerFlag(const UObject* WorldContextObject, const FString& InFlagName, FString& OutFlagValue); + + /** + * Returns the Net Cull Distance distance/frequency pairs used in client qbi-f + */ + UFUNCTION(BlueprintCallable, Category = "SpatialOS") + static TArray GetNCDDistanceRatios(); + + /** + * Returns the full frequency net cull distance ratio used in client qbi-f + */ + UFUNCTION(BlueprintCallable, Category = "SpatialOS") + static float GetFullFrequencyNetCullDistanceRatio(); + + /** + * Returns the inspector colour for the given worker name. + * Argument expected in the form: UnrealWorker1a2s3d4f... + */ + UFUNCTION(BlueprintCallable, Category = "SpatialOS") + static FColor GetInspectorColorForWorkerName(const FString& WorkerName); + + /** + * Returns the entity ID of a given actor, or 0 if we are not using spatial networking or Actor is nullptr. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "SpatialOS") + static int64 GetActorEntityId(const AActor* Actor); + + /** + * Returns the entity ID as a string if the ID is valid, or "Invalid" if not + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "SpatialOS") + static FString EntityIdToString(int64 EntityId); + + /** + * Returns the entity ID of a given actor as a string, or "Invalid" if we are not using spatial networking or Actor is nullptr. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "SpatialOS") + static FString GetActorEntityIdAsString(const AActor* Actor); + + private: - static class UActorGroupManager* GetActorGroupManager(const UObject* WorldContext); + static SpatialActorGroupManager* GetActorGroupManager(const UObject* WorldContext); static FName GetCurrentWorkerType(const UObject* WorldContext); }; diff --git a/SpatialGDK/Source/SpatialGDK/SpatialGDK.Build.cs b/SpatialGDK/Source/SpatialGDK/SpatialGDK.Build.cs index 896ed5d431..af7beb57db 100644 --- a/SpatialGDK/Source/SpatialGDK/SpatialGDK.Build.cs +++ b/SpatialGDK/Source/SpatialGDK/SpatialGDK.Build.cs @@ -6,17 +6,30 @@ using System.Text; using System.IO; using System.Diagnostics; +using Tools.DotNETCommon; using UnrealBuildTool; public class SpatialGDK : ModuleRules { public SpatialGDK(ReadOnlyTargetRules Target) : base(Target) { + bLegacyPublicIncludePaths = false; PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; - bFasterWithoutUnity = true; +#pragma warning disable 0618 + bFasterWithoutUnity = true; // Deprecated in 4.24, replace with bUseUnity = false; once we drop support for 4.23 + if (Target.Version.MinorVersion == 24) // Due to a bug in 4.24, bFasterWithoutUnity is inversed, fixed in master, so should hopefully roll into the next release, remove this once it does + { + bFasterWithoutUnity = false; + } +#pragma warning restore 0618 PrivateIncludePaths.Add("SpatialGDK/Private"); + var WorkerSDKPath = Path.GetFullPath(Path.Combine(ModuleDirectory, "Public", "WorkerSDK")); + + PublicIncludePaths.Add(WorkerSDKPath); // Worker SDK uses a different include format + PrivateIncludePaths.Add(WorkerSDKPath); + PublicDependencyModuleNames.AddRange( new string[] { @@ -28,13 +41,14 @@ public SpatialGDK(ReadOnlyTargetRules Target) : base(Target) "OnlineSubsystemUtils", "InputCore", "Sockets", + "ReplicationGraph" }); - if (Target.bBuildEditor) - { - PublicDependencyModuleNames.Add("UnrealEd"); - PublicDependencyModuleNames.Add("SpatialGDKServices"); - } + if (Target.bBuildEditor) + { + PublicDependencyModuleNames.Add("UnrealEd"); + PublicDependencyModuleNames.Add("SpatialGDKServices"); + } if (Target.bWithPerfCounters) { @@ -43,6 +57,11 @@ public SpatialGDK(ReadOnlyTargetRules Target) : base(Target) var WorkerLibraryDir = Path.GetFullPath(Path.Combine(ModuleDirectory, "..", "..", "Binaries", "ThirdParty", "Improbable", Target.Platform.ToString())); + var WorkerLibraryPaths = new List + { + WorkerLibraryDir, + }; + string LibPrefix = "improbable_"; string ImportLibSuffix = ""; string SharedLibSuffix = ""; @@ -83,6 +102,19 @@ public SpatialGDK(ReadOnlyTargetRules Target) : base(Target) LibPrefix = "libimprobable_"; ImportLibSuffix = SharedLibSuffix = "_static.a"; } + else if (Target.Platform == UnrealTargetPlatform.Android) + { + LibPrefix = "improbable_"; + WorkerLibraryPaths.AddRange(new string[] + { + Path.Combine(WorkerLibraryDir, "arm64-v8a"), + Path.Combine(WorkerLibraryDir, "armeabi-v7a"), + Path.Combine(WorkerLibraryDir, "x86_64"), + }); + + string PluginPath = Utils.MakePathRelativeTo(ModuleDirectory, Target.RelativeEnginePath); + AdditionalPropertiesForReceipt.Add("AndroidPlugin", Path.Combine(PluginPath, "SpatialGDK_APL.xml")); + } else { throw new System.Exception(System.String.Format("Unsupported platform {0}", Target.Platform.ToString())); @@ -91,12 +123,56 @@ public SpatialGDK(ReadOnlyTargetRules Target) : base(Target) string WorkerImportLib = System.String.Format("{0}worker{1}", LibPrefix, ImportLibSuffix); string WorkerSharedLib = System.String.Format("{0}worker{1}", LibPrefix, SharedLibSuffix); - PublicAdditionalLibraries.AddRange(new[] { Path.Combine(WorkerLibraryDir, WorkerImportLib) }); - PublicLibraryPaths.Add(WorkerLibraryDir); - RuntimeDependencies.Add(Path.Combine(WorkerLibraryDir, WorkerSharedLib), StagedFileType.NonUFS); - if (bAddDelayLoad) + if (Target.Platform != UnrealTargetPlatform.Android) + { + RuntimeDependencies.Add(Path.Combine(WorkerLibraryDir, WorkerSharedLib), StagedFileType.NonUFS); + if (bAddDelayLoad) + { + PublicDelayLoadDLLs.Add(WorkerSharedLib); + } + + WorkerImportLib = Path.Combine(WorkerLibraryDir, WorkerImportLib); + } + + PublicAdditionalLibraries.Add(WorkerImportLib); +#pragma warning disable 0618 + PublicLibraryPaths.AddRange(WorkerLibraryPaths); // Deprecated in 4.24, replace with PublicRuntimeLibraryPaths or move the full path into PublicAdditionalLibraries once we drop support for 4.23 +#pragma warning restore 0618 + + // Detect existence of trace library, if present add preprocessor + string TraceStaticLibPath = ""; + string TraceDynamicLib = ""; + string TraceDynamicLibPath = ""; + if (Target.Platform == UnrealTargetPlatform.Win32 || Target.Platform == UnrealTargetPlatform.Win64) + { + TraceStaticLibPath = Path.Combine(WorkerLibraryDir, "trace_dynamic.lib"); + TraceDynamicLib = "trace_dynamic.dll"; + TraceDynamicLibPath = Path.Combine(WorkerLibraryDir, TraceDynamicLib); + } + else if (Target.Platform == UnrealTargetPlatform.Linux) + { + TraceStaticLibPath = Path.Combine(WorkerLibraryDir, "libtrace_dynamic.so"); + TraceDynamicLib = "libtrace_dynamic.so"; + TraceDynamicLibPath = Path.Combine(WorkerLibraryDir, TraceDynamicLib); + } + + if (File.Exists(TraceStaticLibPath) && File.Exists(TraceDynamicLibPath)) + { + Log.TraceInformation("Detection of trace libraries found at {0} and {1}, enabling trace functionality.", TraceStaticLibPath, TraceDynamicLibPath); + PublicDefinitions.Add("TRACE_LIB_ACTIVE=1"); + + PublicAdditionalLibraries.Add(TraceStaticLibPath); + + RuntimeDependencies.Add(TraceDynamicLibPath, StagedFileType.NonUFS); + if (bAddDelayLoad) + { + PublicDelayLoadDLLs.Add(TraceDynamicLib); + } + } + else { - PublicDelayLoadDLLs.Add(WorkerSharedLib); + Log.TraceInformation("Didn't find trace libraries at {0} and {1}, disabling trace functionality.", TraceStaticLibPath, TraceDynamicLibPath); + PublicDefinitions.Add("TRACE_LIB_ACTIVE=0"); } - } + } } diff --git a/SpatialGDK/Source/SpatialGDK/SpatialGDK_APL.xml b/SpatialGDK/Source/SpatialGDK/SpatialGDK_APL.xml new file mode 100644 index 0000000000..c946ce37cd --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/SpatialGDK_APL.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/GridLBStrategyEditorExtension.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/GridLBStrategyEditorExtension.cpp new file mode 100644 index 0000000000..25af3a89c3 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/GridLBStrategyEditorExtension.cpp @@ -0,0 +1,27 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "GridLBStrategyEditorExtension.h" +#include "SpatialGDKEditorSettings.h" + +class UGridBasedLBStrategy_Spy : public UGridBasedLBStrategy +{ +public: + using UGridBasedLBStrategy::WorldWidth; + using UGridBasedLBStrategy::WorldHeight; + using UGridBasedLBStrategy::Rows; + using UGridBasedLBStrategy::Cols; +}; + +bool FGridLBStrategyEditorExtension::GetDefaultLaunchConfiguration(const UGridBasedLBStrategy* Strategy, FWorkerTypeLaunchSection& OutConfiguration, FIntPoint& OutWorldDimensions) const +{ + const UGridBasedLBStrategy_Spy* StrategySpy = static_cast(Strategy); + + OutConfiguration.Rows = StrategySpy->Rows; + OutConfiguration.Columns = StrategySpy->Cols; + OutConfiguration.NumEditorInstances = StrategySpy->Rows * StrategySpy->Cols; + + // Convert from cm to m. + OutWorldDimensions = FIntPoint(StrategySpy->WorldWidth / 100, StrategySpy->WorldHeight / 100); + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/GridLBStrategyEditorExtension.h b/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/GridLBStrategyEditorExtension.h new file mode 100644 index 0000000000..ab3e53cec2 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/GridLBStrategyEditorExtension.h @@ -0,0 +1,12 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "EditorExtension/LBStrategyEditorExtension.h" +#include "LoadBalancing/GridBasedLBStrategy.h" + +class FGridLBStrategyEditorExtension : public FLBStrategyEditorExtensionTemplate +{ +public: + bool GetDefaultLaunchConfiguration(const UGridBasedLBStrategy* Strategy, FWorkerTypeLaunchSection& OutConfiguration, FIntPoint& OutWorldDimensions) const; +}; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/LBStrategyEditorExtension.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/LBStrategyEditorExtension.cpp new file mode 100644 index 0000000000..c1d383ce0e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/LBStrategyEditorExtension.cpp @@ -0,0 +1,67 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "EditorExtension/LBStrategyEditorExtension.h" +#include "LoadBalancing/AbstractLBStrategy.h" + +namespace +{ + +bool InheritFromClosest(UClass* Derived, UClass* PotentialBase, uint32& InOutPreviousDistance) +{ + uint32 InheritanceDistance = 0; + for (const UStruct* TempStruct = Derived; TempStruct; TempStruct = TempStruct->GetSuperStruct()) + { + if (TempStruct == PotentialBase) + { + break; + } + ++InheritanceDistance; + if (InheritanceDistance > InOutPreviousDistance) + { + return false; + } + } + + InOutPreviousDistance = InheritanceDistance; + return true; +} + +} // anonymous namespace + +bool FLBStrategyEditorExtensionManager::GetDefaultLaunchConfiguration(const UAbstractLBStrategy* Strategy, FWorkerTypeLaunchSection& OutConfiguration, FIntPoint& OutWorldDimensions) const +{ + if (!Strategy) + { + return false; + } + + UClass* StrategyClass = Strategy->GetClass(); + + FLBStrategyEditorExtensionInterface* StrategyInterface = nullptr; + uint32 InheritanceDistance = UINT32_MAX; + + for (auto& Extension : Extensions) + { + if (InheritFromClosest(StrategyClass, Extension.Key, InheritanceDistance)) + { + StrategyInterface = Extension.Value.Get(); + } + } + + if (StrategyInterface) + { + return StrategyInterface->GetDefaultLaunchConfiguration_Virtual(Strategy, OutConfiguration, OutWorldDimensions); + } + + return false; +} + +void FLBStrategyEditorExtensionManager::RegisterExtension(UClass* StrategyClass, TUniquePtr StrategyExtension) +{ + Extensions.Push(ExtensionArray::ElementType(StrategyClass, MoveTemp(StrategyExtension))); +} + +void FLBStrategyEditorExtensionManager::Cleanup() +{ + Extensions.Empty(); +} diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.cpp index 93befdf609..fa8eba5f4f 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.cpp @@ -18,6 +18,9 @@ using namespace SpatialGDKEditor::Schema; DEFINE_LOG_CATEGORY(LogSchemaGenerator); +namespace +{ + ESchemaComponentType PropertyGroupToSchemaComponentType(EReplicatedPropertyGroup Group) { if (Group == REP_MultiClient) @@ -134,6 +137,220 @@ void WriteSchemaHandoverField(FCodeWriter& Writer, const TSharedPtr& TypeInfo, UClass* ComponentClass, UClass* ActorClass, int MapIndex, const FActorSpecificSubobjectSchemaData* ExistingSchemaData) +{ + FUnrealFlatRepData RepData = GetFlatRepData(TypeInfo); + + FActorSpecificSubobjectSchemaData SubobjectData; + SubobjectData.ClassPath = ComponentClass->GetPathName(); + + for (EReplicatedPropertyGroup Group : GetAllReplicatedPropertyGroups()) + { + // Since it is possible to replicate subobjects which have no replicated properties. + // We need to generate a schema component for every subobject. So if we have no replicated + // properties, we only don't generate a schema component if we are REP_SingleClient + if (RepData[Group].Num() == 0 && Group == REP_SingleClient) + { + continue; + } + + Worker_ComponentId ComponentId = 0; + if (ExistingSchemaData != nullptr && ExistingSchemaData->SchemaComponents[PropertyGroupToSchemaComponentType(Group)] != 0) + { + ComponentId = ExistingSchemaData->SchemaComponents[PropertyGroupToSchemaComponentType(Group)]; + } + else + { + ComponentId = IdGenerator.Next(); + } + + Writer.PrintNewLine(); + + FString ComponentName = PropertyName + GetReplicatedPropertyGroupName(Group); + Writer.Printf("component {0} {", *ComponentName); + Writer.Indent(); + Writer.Printf("id = {0};", ComponentId); + Writer.Printf("data unreal.generated.{0};", *SchemaReplicatedDataName(Group, ComponentClass)); + Writer.Outdent().Print("}"); + + AddComponentId(ComponentId, SubobjectData.SchemaComponents, PropertyGroupToSchemaComponentType(Group)); + } + + FCmdHandlePropertyMap HandoverData = GetFlatHandoverData(TypeInfo); + if (HandoverData.Num() > 0) + { + Worker_ComponentId ComponentId = 0; + if (ExistingSchemaData != nullptr && ExistingSchemaData->SchemaComponents[ESchemaComponentType::SCHEMA_Handover] != 0) + { + ComponentId = ExistingSchemaData->SchemaComponents[ESchemaComponentType::SCHEMA_Handover]; + } + else + { + ComponentId = IdGenerator.Next(); + } + + Writer.PrintNewLine(); + + // Handover (server to server) replicated properties. + Writer.Printf("component {0} {", *(PropertyName + TEXT("Handover"))); + Writer.Indent(); + Writer.Printf("id = {0};", ComponentId); + Writer.Printf("data unreal.generated.{0};", *SchemaHandoverDataName(ComponentClass)); + Writer.Outdent().Print("}"); + + AddComponentId(ComponentId, SubobjectData.SchemaComponents, ESchemaComponentType::SCHEMA_Handover); + } + + return SubobjectData; +} + +// Output the includes required by this schema file. +void GenerateSubobjectSchemaForActorIncludes(FCodeWriter& Writer, TSharedPtr& TypeInfo) +{ + TSet AlreadyImported; + + for (auto& PropertyPair : TypeInfo->Properties) + { + UProperty* Property = PropertyPair.Key; + UObjectProperty* ObjectProperty = Cast(Property); + + TSharedPtr& PropertyTypeInfo = PropertyPair.Value->Type; + + if (ObjectProperty && PropertyTypeInfo.IsValid()) + { + UObject* Value = PropertyTypeInfo->Object; + + if (Value != nullptr && IsSupportedClass(Value->GetClass())) + { + UClass* Class = Value->GetClass(); + if (!AlreadyImported.Contains(Class) && SchemaGeneratedClasses.Contains(Class)) + { + Writer.Printf("import \"unreal/generated/Subobjects/{0}.schema\";", *ClassPathToSchemaName[Class->GetPathName()]); + AlreadyImported.Add(Class); + } + } + } + } +} + +// Generates schema for all statically attached subobjects on an Actor. +void GenerateSubobjectSchemaForActor(FComponentIdGenerator& IdGenerator, UClass* ActorClass, TSharedPtr TypeInfo, FString SchemaPath, FActorSchemaData& ActorSchemaData, const FActorSchemaData* ExistingSchemaData) +{ + FCodeWriter Writer; + + Writer.Printf(R"""( + // Copyright (c) Improbable Worlds Ltd, All Rights Reserved + // Note that this file has been generated automatically + package unreal.generated.{0}.subobjects;)""", + *ClassPathToSchemaName[ActorClass->GetPathName()].ToLower()); + + Writer.PrintNewLine(); + + GenerateSubobjectSchemaForActorIncludes(Writer, TypeInfo); + + FSubobjectMap Subobjects = GetAllSubobjects(TypeInfo); + + bool bHasComponents = false; + + for (auto& It : Subobjects) + { + TSharedPtr& SubobjectTypeInfo = It.Value; + UClass* SubobjectClass = Cast(SubobjectTypeInfo->Type); + + FActorSpecificSubobjectSchemaData SubobjectData; + + if (SchemaGeneratedClasses.Contains(SubobjectClass)) + { + bHasComponents = true; + + const FActorSpecificSubobjectSchemaData* ExistingSubobjectSchemaData = nullptr; + if (ExistingSchemaData != nullptr) + { + for (auto& SubobjectIt : ExistingSchemaData->SubobjectData) + { + if (SubobjectIt.Value.Name == SubobjectTypeInfo->Name) + { + ExistingSubobjectSchemaData = &SubobjectIt.Value; + break; + } + } + } + SubobjectData = GenerateSchemaForStaticallyAttachedSubobject(Writer, IdGenerator, UnrealNameToSchemaComponentName(SubobjectTypeInfo->Name.ToString()), SubobjectTypeInfo, SubobjectClass, ActorClass, 0, ExistingSubobjectSchemaData); + } + else + { + continue; + } + + SubobjectData.Name = SubobjectTypeInfo->Name; + uint32 SubobjectOffset = SubobjectData.SchemaComponents[SCHEMA_Data]; + check(SubobjectOffset != 0); + ActorSchemaData.SubobjectData.Add(SubobjectOffset, SubobjectData); + } + + if (bHasComponents) + { + Writer.WriteToFile(FString::Printf(TEXT("%s%sComponents.schema"), *SchemaPath, *ClassPathToSchemaName[ActorClass->GetPathName()])); + } +} + +FString GetRPCFieldPrefix(ERPCType RPCType) +{ + switch (RPCType) + { + case ERPCType::ClientReliable: + return TEXT("server_to_client_reliable"); + case ERPCType::ClientUnreliable: + return TEXT("server_to_client_unreliable"); + case ERPCType::ServerReliable: + return TEXT("client_to_server_reliable"); + case ERPCType::ServerUnreliable: + return TEXT("client_to_server_unreliable"); + case ERPCType::NetMulticast: + return TEXT("multicast"); + default: + checkNoEntry(); + } + + return FString(); +} + +void GenerateRPCEndpoint(FCodeWriter& Writer, FString EndpointName, Worker_ComponentId ComponentId, TArray SentRPCTypes, TArray AckedRPCTypes) +{ + Writer.PrintNewLine(); + Writer.Printf("component Unreal{0} {", *EndpointName).Indent(); + Writer.Printf("id = {0};", ComponentId); + + Schema_FieldId FieldId = 1; + for (ERPCType SentRPCType : SentRPCTypes) + { + uint32 RingBufferSize = GetDefault()->MaxRPCRingBufferSize; + + for (uint32 RingBufferIndex = 0; RingBufferIndex < RingBufferSize; RingBufferIndex++) + { + Writer.Printf("option {0}_rpc_{1} = {2};", GetRPCFieldPrefix(SentRPCType), RingBufferIndex, FieldId++); + } + Writer.Printf("uint64 last_sent_{0}_rpc_id = {1};", GetRPCFieldPrefix(SentRPCType), FieldId++); + } + + for (ERPCType AckedRPCType : AckedRPCTypes) + { + Writer.Printf("uint64 last_acked_{0}_rpc_id = {1};", GetRPCFieldPrefix(AckedRPCType), FieldId++); + } + + if (ComponentId == SpatialConstants::MULTICAST_RPCS_COMPONENT_ID) + { + // This counter is used to let clients execute initial multicast RPCs when entity is just getting created, + // while ignoring existing multicast RPCs when an entity enters the interest range. + Writer.Printf("uint32 initially_present_multicast_rpc_count = {0};", FieldId++); + } + + Writer.Outdent().Print("}"); +} + +} // anonymous namespace + void GenerateSubobjectSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSharedPtr TypeInfo, FString SchemaPath) { FCodeWriter Writer; @@ -277,7 +494,7 @@ void GenerateSubobjectSchema(FComponentIdGenerator& IdGenerator, UClass* Class, Writer.Printf("data {0};", *SchemaReplicatedDataName(Group, Class)); Writer.Outdent().Print("}"); - DynamicSubobjectComponents.SchemaComponents[PropertyGroupToSchemaComponentType(Group)] = ComponentId; + AddComponentId(ComponentId, DynamicSubobjectComponents.SchemaComponents, PropertyGroupToSchemaComponentType(Group)); } if (HandoverData.Num() > 0) @@ -302,7 +519,7 @@ void GenerateSubobjectSchema(FComponentIdGenerator& IdGenerator, UClass* Class, Writer.Printf("data {0};", *SchemaHandoverDataName(Class)); Writer.Outdent().Print("}"); - DynamicSubobjectComponents.SchemaComponents[SCHEMA_Handover] = ComponentId; + AddComponentId(ComponentId, DynamicSubobjectComponents.SchemaComponents, ESchemaComponentType::SCHEMA_Handover); } SubobjectSchemaData.DynamicSubobjectComponents.Add(MoveTemp(DynamicSubobjectComponents)); @@ -372,7 +589,7 @@ void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSha Writer.Indent(); Writer.Printf("id = {0};", ComponentId); - ActorSchemaData.SchemaComponents[PropertyGroupToSchemaComponentType(Group)] = ComponentId; + AddComponentId(ComponentId, ActorSchemaData.SchemaComponents, PropertyGroupToSchemaComponentType(Group)); int FieldCounter = 0; for (auto& RepProp : RepData[Group]) @@ -406,7 +623,7 @@ void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSha Writer.Indent(); Writer.Printf("id = {0};", ComponentId); - ActorSchemaData.SchemaComponents[ESchemaComponentType::SCHEMA_Handover] = ComponentId; + AddComponentId(ComponentId, ActorSchemaData.SchemaComponents, ESchemaComponentType::SCHEMA_Handover); int FieldCounter = 0; for (auto& Prop : HandoverData) @@ -423,160 +640,47 @@ void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSha ActorClassPathToSchema.Add(Class->GetPathName(), ActorSchemaData); - Writer.WriteToFile(FString::Printf(TEXT("%s%s.schema"), *SchemaPath, *ClassPathToSchemaName[Class->GetPathName()])); -} - -FActorSpecificSubobjectSchemaData GenerateSchemaForStaticallyAttachedSubobject(FCodeWriter& Writer, FComponentIdGenerator& IdGenerator, FString PropertyName, TSharedPtr& TypeInfo, UClass* ComponentClass, UClass* ActorClass, int MapIndex, const FActorSpecificSubobjectSchemaData* ExistingSchemaData) -{ - FUnrealFlatRepData RepData = GetFlatRepData(TypeInfo); - - FActorSpecificSubobjectSchemaData SubobjectData; - SubobjectData.ClassPath = ComponentClass->GetPathName(); - - for (EReplicatedPropertyGroup Group : GetAllReplicatedPropertyGroups()) + // Cache the NCD for this Actor + if (AActor* CDO = Class->GetDefaultObject()) { - // Since it is possible to replicate subobjects which have no replicated properties. - // We need to generate a schema component for every subobject. So if we have no replicated - // properties, we only don't generate a schema component if we are REP_SingleClient - if (RepData[Group].Num() == 0 && Group == REP_SingleClient) + const float NCD = CDO->NetCullDistanceSquared; + if (NetCullDistanceToComponentId.Find(NCD) == nullptr) { - continue; - } + if (FMath::FloorToFloat(NCD) != NCD) + { + UE_LOG(LogSchemaGenerator, Warning, TEXT("Fractional Net Cull Distance values are not supported and may result in incorrect behaviour. " + "Please modify class's (%s) Net Cull Distance Squared value (%f)"), *Class->GetPathName(), NCD); + } - Worker_ComponentId ComponentId = 0; - if (ExistingSchemaData != nullptr && ExistingSchemaData->SchemaComponents[PropertyGroupToSchemaComponentType(Group)] != 0) - { - ComponentId = ExistingSchemaData->SchemaComponents[PropertyGroupToSchemaComponentType(Group)]; - } - else - { - ComponentId = IdGenerator.Next(); + NetCullDistanceToComponentId.Add(NCD, 0); } - - Writer.PrintNewLine(); - - FString ComponentName = PropertyName + GetReplicatedPropertyGroupName(Group); - Writer.Printf("component {0} {", *ComponentName); - Writer.Indent(); - Writer.Printf("id = {0};", ComponentId); - Writer.Printf("data unreal.generated.{0};", *SchemaReplicatedDataName(Group, ComponentClass)); - Writer.Outdent().Print("}"); - - SubobjectData.SchemaComponents[PropertyGroupToSchemaComponentType(Group)] = ComponentId; } - FCmdHandlePropertyMap HandoverData = GetFlatHandoverData(TypeInfo); - if (HandoverData.Num() > 0) - { - Worker_ComponentId ComponentId = 0; - if (ExistingSchemaData != nullptr && ExistingSchemaData->SchemaComponents[ESchemaComponentType::SCHEMA_Handover] != 0) - { - ComponentId = ExistingSchemaData->SchemaComponents[ESchemaComponentType::SCHEMA_Handover]; - } - else - { - ComponentId = IdGenerator.Next(); - } - - Writer.PrintNewLine(); - - // Handover (server to server) replicated properties. - Writer.Printf("component {0} {", *(PropertyName + TEXT("Handover"))); - Writer.Indent(); - Writer.Printf("id = {0};", ComponentId); - Writer.Printf("data unreal.generated.{0};", *SchemaHandoverDataName(ComponentClass)); - Writer.Outdent().Print("}"); - - SubobjectData.SchemaComponents[ESchemaComponentType::SCHEMA_Handover] = ComponentId; - } - - return SubobjectData; + Writer.WriteToFile(FString::Printf(TEXT("%s%s.schema"), *SchemaPath, *ClassPathToSchemaName[Class->GetPathName()])); } -void GenerateSubobjectSchemaForActor(FComponentIdGenerator& IdGenerator, UClass* ActorClass, TSharedPtr TypeInfo, FString SchemaPath, FActorSchemaData& ActorSchemaData, const FActorSchemaData* ExistingSchemaData) +void GenerateRPCEndpointsSchema(FString SchemaPath) { FCodeWriter Writer; - Writer.Printf(R"""( + Writer.Print(R"""( // Copyright (c) Improbable Worlds Ltd, All Rights Reserved // Note that this file has been generated automatically - package unreal.generated.{0}.subobjects;)""", - *ClassPathToSchemaName[ActorClass->GetPathName()].ToLower()); - + package unreal.generated;)"""); Writer.PrintNewLine(); + Writer.Print("import \"unreal/gdk/core_types.schema\";"); + Writer.Print("import \"unreal/gdk/rpc_payload.schema\";"); - GenerateSubobjectSchemaForActorIncludes(Writer, TypeInfo); - - FSubobjectMap Subobjects = GetAllSubobjects(TypeInfo); - - bool bHasComponents = false; - - for (auto& It : Subobjects) - { - TSharedPtr& SubobjectTypeInfo = It.Value; - UClass* SubobjectClass = Cast(SubobjectTypeInfo->Type); - - FActorSpecificSubobjectSchemaData SubobjectData; - - if (SchemaGeneratedClasses.Contains(SubobjectClass)) - { - bHasComponents = true; + GenerateRPCEndpoint(Writer, TEXT("ClientEndpoint"), SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID, { ERPCType::ServerReliable, ERPCType::ServerUnreliable }, { ERPCType::ClientReliable, ERPCType::ClientUnreliable }); + GenerateRPCEndpoint(Writer, TEXT("ServerEndpoint"), SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID, { ERPCType::ClientReliable, ERPCType::ClientUnreliable }, { ERPCType::ServerReliable, ERPCType::ServerUnreliable }); + GenerateRPCEndpoint(Writer, TEXT("MulticastRPCs"), SpatialConstants::MULTICAST_RPCS_COMPONENT_ID, { ERPCType::NetMulticast }, {}); - const FActorSpecificSubobjectSchemaData* ExistingSubobjectSchemaData = nullptr; - if (ExistingSchemaData != nullptr) - { - for (auto& SubobjectIt : ExistingSchemaData->SubobjectData) - { - if (SubobjectIt.Value.Name == SubobjectTypeInfo->Name) - { - ExistingSubobjectSchemaData = &SubobjectIt.Value; - break; - } - } - } - SubobjectData = GenerateSchemaForStaticallyAttachedSubobject(Writer, IdGenerator, UnrealNameToSchemaComponentName(SubobjectTypeInfo->Name.ToString()), SubobjectTypeInfo, SubobjectClass, ActorClass, 0, ExistingSubobjectSchemaData); - } - else - { - continue; - } - - SubobjectData.Name = SubobjectTypeInfo->Name; - uint32 SubobjectOffset = SubobjectData.SchemaComponents[SCHEMA_Data]; - check(SubobjectOffset != 0); - ActorSchemaData.SubobjectData.Add(SubobjectOffset, SubobjectData); - } - - if (bHasComponents) - { - Writer.WriteToFile(FString::Printf(TEXT("%s%sComponents.schema"), *SchemaPath, *ClassPathToSchemaName[ActorClass->GetPathName()])); - } + Writer.WriteToFile(FString::Printf(TEXT("%srpc_endpoints.schema"), *SchemaPath)); } -void GenerateSubobjectSchemaForActorIncludes(FCodeWriter& Writer, TSharedPtr& TypeInfo) +// Add the component ID to the passed schema components array and the set of components of that type. +void AddComponentId(const Worker_ComponentId ComponentId, ComponentIdPerType& SchemaComponents, const ESchemaComponentType ComponentType) { - TSet AlreadyImported; - - for (auto& PropertyPair : TypeInfo->Properties) - { - UProperty* Property = PropertyPair.Key; - UObjectProperty* ObjectProperty = Cast(Property); - - TSharedPtr& PropertyTypeInfo = PropertyPair.Value->Type; - - if (ObjectProperty && PropertyTypeInfo.IsValid()) - { - UObject* Value = PropertyTypeInfo->Object; - - if (Value != nullptr && IsSupportedClass(Value->GetClass())) - { - UClass* Class = Value->GetClass(); - if (!AlreadyImported.Contains(Class) && SchemaGeneratedClasses.Contains(Class)) - { - Writer.Printf("import \"unreal/generated/Subobjects/{0}.schema\";", *ClassPathToSchemaName[Class->GetPathName()]); - AlreadyImported.Add(Class); - } - } - } - } + SchemaComponents[ComponentType] = ComponentId; + SchemaComponentTypeToComponents[ComponentType].Add(ComponentId); } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.h b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.h index 3357f5c470..c7b0d8d4e2 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.h @@ -5,6 +5,8 @@ #include "TypeStructure.h" #include "Utils/SchemaDatabase.h" +using ComponentIdPerType = Worker_ComponentId[ESchemaComponentType::SCHEMA_Count]; + DECLARE_LOG_CATEGORY_EXTERN(LogSchemaGenerator, Log, All); class FCodeWriter; @@ -13,17 +15,16 @@ struct FComponentIdGenerator; extern TArray SchemaGeneratedClasses; extern TMap ActorClassPathToSchema; extern TMap SubobjectClassPathToSchema; -extern TMap LevelPathToComponentId; +extern TMap LevelPathToComponentId; +extern TMap> SchemaComponentTypeToComponents; +extern TMap NetCullDistanceToComponentId; // Generates schema for an Actor void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSharedPtr TypeInfo, FString SchemaPath); // Generates schema for a Subobject class - the schema type and the dynamic schema components void GenerateSubobjectSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSharedPtr TypeInfo, FString SchemaPath); -// Generates schema for all statically attached subobjects on an Actor. -void GenerateSubobjectSchemaForActor(FComponentIdGenerator& IdGenerator, UClass* ActorClass, TSharedPtr TypeInfo, - FString SchemaPath, FActorSchemaData& ActorSchemaData, const FActorSchemaData* ExistingSchemaData); -// Generates schema for a statically attached subobject on an Actor - called by GenerateSubobjectSchemaForActor. -FActorSpecificSubobjectSchemaData GenerateSchemaForStaticallyAttachedSubobject(FCodeWriter& Writer, FComponentIdGenerator& IdGenerator, - FString PropertyName, TSharedPtr& TypeInfo, UClass* ComponentClass, UClass* ActorClass, int MapIndex, const FActorSpecificSubobjectSchemaData* ExistingSchemaData); -// Output the includes required by this schema file. -void GenerateSubobjectSchemaForActorIncludes(FCodeWriter& Writer, TSharedPtr& TypeInfo); + +// Generates schema for RPC endpoints. +void GenerateRPCEndpointsSchema(FString SchemaPath); + +void AddComponentId(const Worker_ComponentId ComponentId, ComponentIdPerType& SchemaComponents, const ESchemaComponentType ComponentType); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp index 33b45daf42..d1acefaa97 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp @@ -2,7 +2,6 @@ #include "SpatialGDKEditorSchemaGenerator.h" -#include "Abilities/GameplayAbility.h" #include "AssetRegistryModule.h" #include "Async/Async.h" #include "Components/SceneComponent.h" @@ -14,6 +13,7 @@ #include "GenericPlatform/GenericPlatformFile.h" #include "GenericPlatform/GenericPlatformProcess.h" #include "HAL/PlatformFilemanager.h" +#include "Hash/CityHash.h" #include "Misc/ConfigCacheIni.h" #include "Misc/FileHelper.h" #include "Misc/MessageDialog.h" @@ -28,6 +28,7 @@ #include "Settings/ProjectPackagingSettings.h" #include "SpatialConstants.h" #include "SpatialGDKEditorSettings.h" +#include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesModule.h" #include "TypeStructure.h" #include "UObject/StrongObjectPtr.h" @@ -42,24 +43,28 @@ DEFINE_LOG_CATEGORY(LogSpatialGDKSchemaGenerator); TArray SchemaGeneratedClasses; TMap ActorClassPathToSchema; TMap SubobjectClassPathToSchema; -uint32 NextAvailableComponentId = SpatialConstants::STARTING_GENERATED_COMPONENT_ID; +Worker_ComponentId NextAvailableComponentId = SpatialConstants::STARTING_GENERATED_COMPONENT_ID; + +// Sets of data/owner only/handover components +TMap> SchemaComponentTypeToComponents; // LevelStreaming -TMap LevelPathToComponentId; -TSet LevelComponentIds; +TMap LevelPathToComponentId; // Prevent name collisions. TMap ClassPathToSchemaName; TMap SchemaNameToClassPath; TMap> PotentialSchemaNameCollisions; +// QBI +TMap NetCullDistanceToComponentId; + const FString RelativeSchemaDatabaseFilePath = FPaths::SetExtension(FPaths::Combine(FPaths::ProjectContentDir(), SpatialConstants::SCHEMA_DATABASE_FILE_PATH), FPackageName::GetAssetPackageExtension()); namespace SpatialGDKEditor { namespace Schema { - void AddPotentialNameCollision(const FString& DesiredSchemaName, const FString& ClassPath, const FString& GeneratedSchemaName) { PotentialSchemaNameCollisions.FindOrAdd(DesiredSchemaName).Add(FString::Printf(TEXT("%s(%s)"), *ClassPath, *GeneratedSchemaName)); @@ -251,7 +256,7 @@ void GenerateSchemaFromClasses(const TArray>& TypeInfos, } } -void WriteLevelComponent(FCodeWriter& Writer, FString LevelName, uint32 ComponentId, FString ClassPath) +void WriteLevelComponent(FCodeWriter& Writer, FString LevelName, Worker_ComponentId ComponentId, FString ClassPath) { Writer.PrintNewLine(); Writer.Printf("// {0}", *ClassPath); @@ -290,7 +295,7 @@ void GenerateSchemaForSublevels() GenerateSchemaForSublevels(SchemaOutputPath, LevelNamesToPaths); } -SPATIALGDKEDITOR_API void GenerateSchemaForSublevels(const FString& SchemaOutputPath, const TMultiMap& LevelNamesToPaths) +void GenerateSchemaForSublevels(const FString& SchemaOutputPath, const TMultiMap& LevelNamesToPaths) { FCodeWriter Writer; Writer.Printf(R"""( @@ -314,12 +319,11 @@ SPATIALGDKEDITOR_API void GenerateSchemaForSublevels(const FString& SchemaOutput for (int i = 0; i < LevelPaths.Num(); i++) { - uint32 ComponentId = LevelPathToComponentId.FindRef(LevelPaths[i].ToString()); + Worker_ComponentId ComponentId = LevelPathToComponentId.FindRef(LevelPaths[i].ToString()); if (ComponentId == 0) { ComponentId = IdGenerator.Next(); LevelPathToComponentId.Add(LevelPaths[i].ToString(), ComponentId); - LevelComponentIds.Add(ComponentId); } WriteLevelComponent(Writer, FString::Printf(TEXT("%sInd%d"), *LevelNameString, i), ComponentId, LevelPaths[i].ToString()); @@ -329,12 +333,11 @@ SPATIALGDKEDITOR_API void GenerateSchemaForSublevels(const FString& SchemaOutput { // Write a single component. FString LevelPath = LevelNamesToPaths.FindRef(LevelName).ToString(); - uint32 ComponentId = LevelPathToComponentId.FindRef(LevelPath); + Worker_ComponentId ComponentId = LevelPathToComponentId.FindRef(LevelPath); if (ComponentId == 0) { ComponentId = IdGenerator.Next(); LevelPathToComponentId.Add(LevelPath, ComponentId); - LevelComponentIds.Add(ComponentId); } WriteLevelComponent(Writer, LevelName.ToString(), ComponentId, LevelPath); } @@ -345,6 +348,52 @@ SPATIALGDKEDITOR_API void GenerateSchemaForSublevels(const FString& SchemaOutput Writer.WriteToFile(FString::Printf(TEXT("%sSublevels/sublevels.schema"), *SchemaOutputPath)); } +void GenerateSchemaForRPCEndpoints() +{ + GenerateSchemaForRPCEndpoints(GetDefault()->GetGeneratedSchemaOutputFolder()); +} + +void GenerateSchemaForRPCEndpoints(const FString& SchemaOutputPath) +{ + GenerateRPCEndpointsSchema(SchemaOutputPath); +} + +void GenerateSchemaForNCDs() +{ + GenerateSchemaForNCDs(GetDefault()->GetGeneratedSchemaOutputFolder()); +} + +void GenerateSchemaForNCDs(const FString& SchemaOutputPath) +{ + FCodeWriter Writer; + Writer.Printf(R"""( + // Copyright (c) Improbable Worlds Ltd, All Rights Reserved + // Note that this file has been generated automatically + package unreal.ncdcomponents;)"""); + + FComponentIdGenerator IdGenerator = FComponentIdGenerator(NextAvailableComponentId); + + for (auto& NCDComponent : NetCullDistanceToComponentId) + { + const FString ComponentName = FString::Printf(TEXT("NetCullDistanceSquared%lld"), static_cast(NCDComponent.Key)); + if (NCDComponent.Value == 0) + { + NCDComponent.Value = IdGenerator.Next(); + } + + Writer.PrintNewLine(); + Writer.Printf("// distance {0}", NCDComponent.Key); + Writer.Printf("component {0} {", *UnrealNameToSchemaComponentName(ComponentName)); + Writer.Indent(); + Writer.Printf("id = {0};", NCDComponent.Value); + Writer.Outdent().Print("}"); + } + + NextAvailableComponentId = IdGenerator.Peek(); + + Writer.WriteToFile(FString::Printf(TEXT("%sNetCullDistance/ncdcomponents.schema"), *SchemaOutputPath)); +} + FString GenerateIntermediateDirectory() { const FString CombinedIntermediatePath = FPaths::Combine(*FPaths::GetPath(FPaths::GetProjectFilePath()), TEXT("Intermediate/Improbable/"), *FGuid::NewGuid().ToString(), TEXT("/")); @@ -354,9 +403,9 @@ FString GenerateIntermediateDirectory() return AbsoluteCombinedIntermediatePath; } -TMap CreateComponentIdToClassPathMap() +TMap CreateComponentIdToClassPathMap() { - TMap ComponentIdToClassPath; + TMap ComponentIdToClassPath; for (const auto& ActorSchemaData : ActorClassPathToSchema) { @@ -394,13 +443,61 @@ bool SaveSchemaDatabase(const FString& PackagePath) { UPackage *Package = CreatePackage(nullptr, *PackagePath); + ActorClassPathToSchema.KeySort([](const FString& LHS, const FString& RHS) { return LHS < RHS; }); + SubobjectClassPathToSchema.KeySort([](const FString& LHS, const FString& RHS) { return LHS < RHS; }); + LevelPathToComponentId.KeySort([](const FString& LHS, const FString& RHS) { return LHS < RHS; }); + USchemaDatabase* SchemaDatabase = NewObject(Package, USchemaDatabase::StaticClass(), FName("SchemaDatabase"), EObjectFlags::RF_Public | EObjectFlags::RF_Standalone); SchemaDatabase->NextAvailableComponentId = NextAvailableComponentId; SchemaDatabase->ActorClassPathToSchema = ActorClassPathToSchema; SchemaDatabase->SubobjectClassPathToSchema = SubobjectClassPathToSchema; SchemaDatabase->LevelPathToComponentId = LevelPathToComponentId; + SchemaDatabase->NetCullDistanceToComponentId = NetCullDistanceToComponentId; SchemaDatabase->ComponentIdToClassPath = CreateComponentIdToClassPathMap(); - SchemaDatabase->LevelComponentIds = LevelComponentIds; + SchemaDatabase->DataComponentIds = SchemaComponentTypeToComponents[ESchemaComponentType::SCHEMA_Data].Array(); + SchemaDatabase->OwnerOnlyComponentIds = SchemaComponentTypeToComponents[ESchemaComponentType::SCHEMA_OwnerOnly].Array(); + SchemaDatabase->HandoverComponentIds = SchemaComponentTypeToComponents[ESchemaComponentType::SCHEMA_Handover].Array(); + + SchemaDatabase->NetCullDistanceComponentIds.Reset(); + TArray NetCullDistanceComponentIds; + NetCullDistanceComponentIds.Reserve(NetCullDistanceToComponentId.Num()); + NetCullDistanceToComponentId.GenerateValueArray(NetCullDistanceComponentIds); + SchemaDatabase->NetCullDistanceComponentIds.Append(NetCullDistanceComponentIds); + + SchemaDatabase->LevelComponentIds.Reset(LevelPathToComponentId.Num()); + LevelPathToComponentId.GenerateValueArray(SchemaDatabase->LevelComponentIds); + + + FString CompiledSchemaDir = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("build/assembly/schema")); + + // Generate hash + { + SchemaDatabase->SchemaDescriptorHash = 0; + FString DescriptorPath = FPaths::Combine(CompiledSchemaDir, TEXT("schema.descriptor")); + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + + TUniquePtr FileHandle(PlatformFile.OpenRead(DescriptorPath.GetCharArray().GetData())); + if (FileHandle) + { + // Create our byte buffer + int64 FileSize = FileHandle->Size(); + TUniquePtr ByteArray(new uint8[FileSize]); + bool Result = FileHandle->Read(ByteArray.Get(), FileSize); + if (Result) + { + SchemaDatabase->SchemaDescriptorHash = CityHash32(reinterpret_cast(ByteArray.Get()), FileSize); + UE_LOG(LogSpatialGDKSchemaGenerator, Display, TEXT("Generated schema hash for database %u"), SchemaDatabase->SchemaDescriptorHash); + } + else + { + UE_LOG(LogSpatialGDKSchemaGenerator, Warning, TEXT("Failed to fully read schema.descriptor. Schema not saved. Location: %s"), *DescriptorPath); + } + } + else + { + UE_LOG(LogSpatialGDKSchemaGenerator, Warning, TEXT("Failed to open schema.descriptor generated by the schema compiler! Location: %s"), *DescriptorPath); + } + } FAssetRegistryModule::AssetCreated(SchemaDatabase); SchemaDatabase->MarkPackageDirty(); @@ -443,26 +540,13 @@ bool IsSupportedClass(const UClass* SupportedClass) if (SupportedClass->HasAnySpatialClassFlags(SPATIALCLASS_NotSpatialType)) { UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("[%s] Has NotSpatialType flag, not supported for schema gen."), *GetPathNameSafe(SupportedClass)); - return false; } - - // Need to check if super class is supported here because some blueprints don't appear to inherit SpatialFlags correctly until - // recompiled and saved. See [UNR-2172]. - UClass* Class = SupportedClass->GetSuperClass(); - while (Class != nullptr) + else { - if (Class->HasAnySpatialClassFlags(SPATIALCLASS_SpatialType | SPATIALCLASS_NotSpatialType)) - { - break; - } - Class = Class->GetSuperClass(); + UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("[%s] Has neither a SpatialType or NotSpatialType flag."), *GetPathNameSafe(SupportedClass)); } - if (Class == nullptr || !Class->HasAnySpatialClassFlags(SPATIALCLASS_SpatialType)) - { - UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("[%s] No SpatialType flag, not supported for schema gen."), *GetPathNameSafe(SupportedClass)); - return false; - } + return false; } if (SupportedClass->HasAnyClassFlags(CLASS_LayoutChanging)) @@ -565,10 +649,15 @@ void ResetSchemaGeneratorState() { ActorClassPathToSchema.Empty(); SubobjectClassPathToSchema.Empty(); - LevelComponentIds.Empty(); + SchemaComponentTypeToComponents.Empty(); + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) + { + SchemaComponentTypeToComponents.Add(Type, TSet()); + }); LevelPathToComponentId.Empty(); NextAvailableComponentId = SpatialConstants::STARTING_GENERATED_COMPONENT_ID; SchemaGeneratedClasses.Empty(); + NetCullDistanceToComponentId.Empty(); } void ResetSchemaGeneratorStateAndCleanupFolders() @@ -578,8 +667,7 @@ void ResetSchemaGeneratorState() } bool LoadGeneratorStateFromSchemaDatabase(const FString& FileName) - { - +{ FString RelativeFileName = FPaths::Combine(FPaths::ProjectContentDir(), FileName); RelativeFileName = FPaths::SetExtension(RelativeFileName, FPackageName::GetAssetPackageExtension()); @@ -606,9 +694,13 @@ bool LoadGeneratorStateFromSchemaDatabase(const FString& FileName) ActorClassPathToSchema = SchemaDatabase->ActorClassPathToSchema; SubobjectClassPathToSchema = SchemaDatabase->SubobjectClassPathToSchema; - LevelComponentIds = SchemaDatabase->LevelComponentIds; + SchemaComponentTypeToComponents.Empty(); + SchemaComponentTypeToComponents.Add(ESchemaComponentType::SCHEMA_Data, TSet(SchemaDatabase->DataComponentIds)); + SchemaComponentTypeToComponents.Add(ESchemaComponentType::SCHEMA_OwnerOnly, TSet(SchemaDatabase->OwnerOnlyComponentIds)); + SchemaComponentTypeToComponents.Add(ESchemaComponentType::SCHEMA_Handover, TSet(SchemaDatabase->HandoverComponentIds)); LevelPathToComponentId = SchemaDatabase->LevelPathToComponentId; NextAvailableComponentId = SchemaDatabase->NextAvailableComponentId; + NetCullDistanceToComponentId = SchemaDatabase->NetCullDistanceToComponentId; // Component Id generation was updated to be non-destructive, if we detect an old schema database, delete it. if (ActorClassPathToSchema.Num() > 0 && NextAvailableComponentId == SpatialConstants::STARTING_GENERATED_COMPONENT_ID) @@ -723,39 +815,70 @@ bool RunSchemaCompiler() // Get the schema_compiler path and arguments FString SchemaCompilerExe = FPaths::Combine(PluginDir, TEXT("SpatialGDK/Binaries/ThirdParty/Improbable/Programs/schema_compiler.exe")); - FString SchemaDir = FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("schema")); - FString CoreSDKSchemaDir = FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("build/dependencies/schema/standard_library")); - FString SchemaDescriptorDir = FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("build/assembly/schema")); - FString SchemaDescriptorOutput = FPaths::Combine(SchemaDescriptorDir, TEXT("schema.descriptor")); + FString SchemaDir = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("schema")); + FString CoreSDKSchemaDir = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("build/dependencies/schema/standard_library")); + FString CompiledSchemaDir = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("build/assembly/schema")); + FString CompiledSchemaASTDir = FPaths::Combine(CompiledSchemaDir, TEXT("ast")); + FString SchemaDescriptorOutput = FPaths::Combine(CompiledSchemaDir, TEXT("schema.descriptor")); - // The schema_compiler cannot create folders. - if (!FPaths::DirectoryExists(SchemaDescriptorDir)) + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + + const FString& SchemaCompilerBaseArgs = FString::Printf(TEXT("--schema_path=\"%s\" --schema_path=\"%s\" --descriptor_set_out=\"%s\" --load_all_schema_on_schema_path "), *SchemaDir, *CoreSDKSchemaDir, *SchemaDescriptorOutput); + + // If there's already a compiled schema dir, blow it away so we don't have lingering artifacts from previous generation runs. + if (FPaths::DirectoryExists(CompiledSchemaDir)) { - IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); - if (!PlatformFile.CreateDirectoryTree(*SchemaDescriptorDir)) + if (!PlatformFile.DeleteDirectoryRecursively(*CompiledSchemaDir)) { - UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Could not create schema descriptor directory '%s'! Please make sure the parent directory is writeable."), *SchemaDescriptorDir); + UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Could not delete pre-existing compiled schema directory '%s'! Please make sure the directory is writeable."), *CompiledSchemaDir); return false; } } - FString SchemaCompilerArgs = FString::Printf(TEXT("--schema_path=\"%s\" --schema_path=\"%s\" --descriptor_set_out=\"%s\" --load_all_schema_on_schema_path"), *SchemaDir, *CoreSDKSchemaDir, *SchemaDescriptorOutput); + // schema_compiler cannot create folders, so we need to set them up beforehand. + if (!PlatformFile.CreateDirectoryTree(*CompiledSchemaDir)) + { + UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Could not create compiled schema directory '%s'! Please make sure the parent directory is writeable."), *CompiledSchemaDir); + return false; + } - UE_LOG(LogSpatialGDKSchemaGenerator, Log, TEXT("Starting '%s' with `%s` arguments."), *SchemaCompilerExe, *SchemaCompilerArgs); + FString AdditionalSchemaCompilerArgs; + + TArray Tokens; + TArray Switches; + FCommandLine::Parse(FCommandLine::Get(), Tokens, Switches); + + if (const FString* SchemaCompileArgsCLSwitchPtr = Switches.FindByPredicate([](const FString& ClSwitch) { return ClSwitch.StartsWith(FString{ TEXT("AdditionalSchemaCompilerArgs") }); })) + { + FString SwitchName; + SchemaCompileArgsCLSwitchPtr->Split(FString{ TEXT("=") }, &SwitchName, &AdditionalSchemaCompilerArgs); + if (AdditionalSchemaCompilerArgs.Contains(FString{ TEXT("ast_proto_out") }) || AdditionalSchemaCompilerArgs.Contains(FString{ TEXT("ast_json_out") })) + { + if (!PlatformFile.CreateDirectoryTree(*CompiledSchemaASTDir)) + { + UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Could not create compiled schema AST directory '%s'! Please make sure the parent directory is writeable."), *CompiledSchemaASTDir); + return false; + } + } + } + + FString SchemaCompilerArgs = FString::Printf(TEXT("%s %s"), *SchemaCompilerBaseArgs, *AdditionalSchemaCompilerArgs); + + UE_LOG(LogSpatialGDKSchemaGenerator, Log, TEXT("Starting '%s' with `%s` arguments."), *SpatialGDKServicesConstants::SchemaCompilerExe, *SchemaCompilerArgs); int32 ExitCode = 1; FString SchemaCompilerOut; FString SchemaCompilerErr; - FPlatformProcess::ExecProcess(*SchemaCompilerExe, *SchemaCompilerArgs, &ExitCode, &SchemaCompilerOut, &SchemaCompilerErr); + FPlatformProcess::ExecProcess(*SpatialGDKServicesConstants::SchemaCompilerExe, *SchemaCompilerArgs, &ExitCode, &SchemaCompilerOut, &SchemaCompilerErr); if (ExitCode == 0) { - UE_LOG(LogSpatialGDKSchemaGenerator, Log, TEXT("schema_compiler successfully generated schema descriptor: %s"), *SchemaCompilerOut); + UE_LOG(LogSpatialGDKSchemaGenerator, Log, TEXT("schema_compiler successfully generated compiled schema with arguments `%s`: %s"), *SchemaCompilerArgs, *SchemaCompilerOut); return true; } else { - UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("schema_compiler failed to generate schema descriptor: %s"), *SchemaCompilerErr); + UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("schema_compiler failed to generate compiled schema for arguments `%s`: %s"), *SchemaCompilerArgs, *SchemaCompilerErr); return false; } } @@ -774,13 +897,20 @@ bool SpatialGDKGenerateSchema() } GenerateSchemaForSublevels(); + GenerateSchemaForRPCEndpoints(); + GenerateSchemaForNCDs(); - if (!SaveSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_ASSET_PATH)) + if (!RunSchemaCompiler()) { return false; } - return RunSchemaCompiler(); + if (!SaveSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_ASSET_PATH)) // This requires RunSchemaCompiler to run first + { + return false; + } + + return true; } bool SpatialGDKGenerateSchemaForClasses(TSet Classes, FString SchemaOutputPath /*= ""*/) @@ -839,7 +969,7 @@ bool SpatialGDKGenerateSchemaForClasses(TSet Classes, FString SchemaOut } #if ENGINE_MINOR_VERSION <= 22 - check(GetDefault()->bSpatialNetworking); + check(GetDefault()->UsesSpatialNetworking()); #endif FComponentIdGenerator IdGenerator = FComponentIdGenerator(NextAvailableComponentId); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp index 11e4de3082..9e8351705f 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp @@ -3,10 +3,8 @@ #include "SpatialGDKEditorSnapshotGenerator.h" #include "Engine/LevelScriptActor.h" -#include "EngineClasses/SpatialActorChannel.h" -#include "EngineClasses/SpatialNetConnection.h" -#include "EngineClasses/SpatialNetDriver.h" #include "Interop/SpatialClassInfoManager.h" +#include "Schema/ComponentPresence.h" #include "Schema/Interest.h" #include "Schema/SpawnData.h" #include "Schema/StandardLibrary.h" @@ -14,6 +12,7 @@ #include "SpatialConstants.h" #include "SpatialGDKEditorSettings.h" #include "SpatialGDKSettings.h" +#include "Utils/EntityFactory.h" #include "Utils/ComponentFactory.h" #include "Utils/RepDataUtils.h" #include "Utils/RepLayoutUtils.h" @@ -32,6 +31,26 @@ using namespace SpatialGDK; DEFINE_LOG_CATEGORY(LogSpatialGDKSnapshot); +TArray UnpackedComponentData; + +void SetEntityData(Worker_Entity& Entity, const TArray& Components) +{ + Entity.component_count = Components.Num(); + +#if TRACE_LIB_ACTIVE + // We have to unpack these as Worker_ComponentData is not the same as FWorkerComponentData + UnpackedComponentData.Empty(); + UnpackedComponentData.SetNum(Components.Num()); + for (int i = 0, Num = Components.Num(); i < Num; i++) + { + UnpackedComponentData[i] = Components[i]; + } + Entity.components = UnpackedComponentData.GetData(); +#else + Entity.components = Components.GetData(); +#endif +} + bool CreateSpawnerEntity(Worker_SnapshotOutputStream* OutputStream) { Worker_Entity SpawnerEntity; @@ -41,7 +60,7 @@ bool CreateSpawnerEntity(Worker_SnapshotOutputStream* OutputStream) PlayerSpawnerData.component_id = SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID; PlayerSpawnerData.schema_type = Schema_CreateComponentData(); - TArray Components; + TArray Components; WriteAclMap ComponentWriteAcl; ComponentWriteAcl.Add(SpatialConstants::POSITION_COMPONENT_ID, SpatialConstants::UnrealServerPermission); @@ -49,15 +68,17 @@ bool CreateSpawnerEntity(Worker_SnapshotOutputStream* OutputStream) ComponentWriteAcl.Add(SpatialConstants::PERSISTENCE_COMPONENT_ID, SpatialConstants::UnrealServerPermission); ComponentWriteAcl.Add(SpatialConstants::ENTITY_ACL_COMPONENT_ID, SpatialConstants::UnrealServerPermission); ComponentWriteAcl.Add(SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID, SpatialConstants::UnrealServerPermission); + ComponentWriteAcl.Add(SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID, SpatialConstants::UnrealServerPermission); + ComponentWriteAcl.Add(SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID, SpatialConstants::UnrealServerPermission); - Components.Add(Position(Origin).CreatePositionData()); + Components.Add(Position(DeploymentOrigin).CreatePositionData()); Components.Add(Metadata(TEXT("SpatialSpawner")).CreateMetadataData()); Components.Add(Persistence().CreatePersistenceData()); Components.Add(EntityAcl(SpatialConstants::ClientOrServerPermission, ComponentWriteAcl).CreateEntityAclData()); Components.Add(PlayerSpawnerData); + Components.Add(ComponentPresence(EntityFactory::GetComponentPresenceList(Components)).CreateComponentPresenceData()); - SpawnerEntity.component_count = Components.Num(); - SpawnerEntity.components = Components.GetData(); + SetEntityData(SpawnerEntity, Components); Worker_SnapshotOutputStream_WriteEntity(OutputStream, &SpawnerEntity); return Worker_SnapshotOutputStream_GetState(OutputStream).stream_state == WORKER_STREAM_STATE_GOOD; @@ -84,10 +105,10 @@ Worker_ComponentData CreateDeploymentData() DeploymentData.schema_type = Schema_CreateComponentData(); Schema_Object* DeploymentDataObject = Schema_GetComponentDataFields(DeploymentData.schema_type); - Schema_Object* MapURLObject = Schema_AddObject(DeploymentDataObject, SpatialConstants::DEPLOYMENT_MAP_MAP_URL_ID); - AddStringToSchema(MapURLObject, 1, TEXT("default")); // TODO: Fill this with the map name of the map the snapshot is being generated for. - + AddStringToSchema(DeploymentDataObject, SpatialConstants::DEPLOYMENT_MAP_MAP_URL_ID, ""); Schema_AddBool(DeploymentDataObject, SpatialConstants::DEPLOYMENT_MAP_ACCEPTING_PLAYERS_ID, false); + Schema_AddInt32(DeploymentDataObject, SpatialConstants::DEPLOYMENT_MAP_SESSION_ID, 0); + Schema_AddUint32(DeploymentDataObject, SpatialConstants::DEPLOYMENT_MAP_SCHEMA_HASH, 0); return DeploymentData; } @@ -112,12 +133,26 @@ Worker_ComponentData CreateStartupActorManagerData() return StartupActorManagerData; } +WorkerRequirementSet CreateReadACLForAlwaysRelevantEntities() +{ + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + + WorkerRequirementSet ReadACL; + for (const FName& WorkerType : SpatialGDKSettings->ServerWorkerTypes) + { + const WorkerAttributeSet WorkerTypeAttributeSet{ { WorkerType.ToString() } }; + ReadACL.Add(WorkerTypeAttributeSet); + } + + return ReadACL; +} + bool CreateGlobalStateManager(Worker_SnapshotOutputStream* OutputStream) { Worker_Entity GSM; GSM.entity_id = SpatialConstants::INITIAL_GLOBAL_STATE_MANAGER_ENTITY_ID; - TArray Components; + TArray Components; WriteAclMap ComponentWriteAcl; ComponentWriteAcl.Add(SpatialConstants::POSITION_COMPONENT_ID, SpatialConstants::UnrealServerPermission); @@ -128,30 +163,58 @@ bool CreateGlobalStateManager(Worker_SnapshotOutputStream* OutputStream) ComponentWriteAcl.Add(SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID, SpatialConstants::UnrealServerPermission); ComponentWriteAcl.Add(SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID, SpatialConstants::UnrealServerPermission); ComponentWriteAcl.Add(SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID, SpatialConstants::UnrealServerPermission); + ComponentWriteAcl.Add(SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID, SpatialConstants::UnrealServerPermission); + ComponentWriteAcl.Add(SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID, SpatialConstants::UnrealServerPermission); - Components.Add(Position(Origin).CreatePositionData()); + Components.Add(Position(DeploymentOrigin).CreatePositionData()); Components.Add(Metadata(TEXT("GlobalStateManager")).CreateMetadataData()); Components.Add(Persistence().CreatePersistenceData()); Components.Add(CreateSingletonManagerData()); Components.Add(CreateDeploymentData()); Components.Add(CreateGSMShutdownData()); Components.Add(CreateStartupActorManagerData()); + Components.Add(EntityAcl(CreateReadACLForAlwaysRelevantEntities(), ComponentWriteAcl).CreateEntityAclData()); + Components.Add(ComponentPresence(EntityFactory::GetComponentPresenceList(Components)).CreateComponentPresenceData()); - const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + SetEntityData(GSM, Components); - WorkerRequirementSet ReadACL; - for (const FName& WorkerType : SpatialGDKSettings->ServerWorkerTypes) - { - const WorkerAttributeSet WorkerTypeAttributeSet{ { WorkerType.ToString() } }; - ReadACL.Add(WorkerTypeAttributeSet); - } + Worker_SnapshotOutputStream_WriteEntity(OutputStream, &GSM); + return Worker_SnapshotOutputStream_GetState(OutputStream).stream_state == WORKER_STREAM_STATE_GOOD; +} - Components.Add(EntityAcl(ReadACL, ComponentWriteAcl).CreateEntityAclData()); +Worker_ComponentData CreateVirtualWorkerTranslatorData() +{ + Worker_ComponentData VirtualWorkerTranslatorData{}; + VirtualWorkerTranslatorData.component_id = SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID; + VirtualWorkerTranslatorData.schema_type = Schema_CreateComponentData(); + return VirtualWorkerTranslatorData; +} + +bool CreateVirtualWorkerTranslator(Worker_SnapshotOutputStream* OutputStream) +{ + Worker_Entity VirtualWorkerTranslator; + VirtualWorkerTranslator.entity_id = SpatialConstants::INITIAL_VIRTUAL_WORKER_TRANSLATOR_ENTITY_ID; - GSM.component_count = Components.Num(); - GSM.components = Components.GetData(); + TArray Components; - Worker_SnapshotOutputStream_WriteEntity(OutputStream, &GSM); + WriteAclMap ComponentWriteAcl; + ComponentWriteAcl.Add(SpatialConstants::POSITION_COMPONENT_ID, SpatialConstants::UnrealServerPermission); + ComponentWriteAcl.Add(SpatialConstants::METADATA_COMPONENT_ID, SpatialConstants::UnrealServerPermission); + ComponentWriteAcl.Add(SpatialConstants::PERSISTENCE_COMPONENT_ID, SpatialConstants::UnrealServerPermission); + ComponentWriteAcl.Add(SpatialConstants::ENTITY_ACL_COMPONENT_ID, SpatialConstants::UnrealServerPermission); + ComponentWriteAcl.Add(SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID, SpatialConstants::UnrealServerPermission); + ComponentWriteAcl.Add(SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID, SpatialConstants::UnrealServerPermission); + + Components.Add(Position(DeploymentOrigin).CreatePositionData()); + Components.Add(Metadata(TEXT("VirtualWorkerTranslator")).CreateMetadataData()); + Components.Add(Persistence().CreatePersistenceData()); + Components.Add(CreateVirtualWorkerTranslatorData()); + Components.Add(EntityAcl(CreateReadACLForAlwaysRelevantEntities(), ComponentWriteAcl).CreateEntityAclData()); + Components.Add(ComponentPresence(EntityFactory::GetComponentPresenceList(Components)).CreateComponentPresenceData()); + + SetEntityData(VirtualWorkerTranslator, Components); + + Worker_SnapshotOutputStream_WriteEntity(OutputStream, &VirtualWorkerTranslator); return Worker_SnapshotOutputStream_GetState(OutputStream).stream_state == WORKER_STREAM_STATE_GOOD; } @@ -208,6 +271,12 @@ bool FillSnapshot(Worker_SnapshotOutputStream* OutputStream, UWorld* World) return false; } + if (!CreateVirtualWorkerTranslator(OutputStream)) + { + UE_LOG(LogSpatialGDKSnapshot, Error, TEXT("Error generating VirtualWorkerTranslator in snapshot: %s"), UTF8_TO_TCHAR(Worker_SnapshotOutputStream_GetState(OutputStream).error_message)); + return false; + } + Worker_EntityId NextAvailableEntityID = SpatialConstants::FIRST_AVAILABLE_ENTITY_ID; if (!RunUserSnapshotGenerationOverrides(OutputStream, NextAvailableEntityID)) { diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultLaunchConfigGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultLaunchConfigGenerator.cpp index cf4162d208..80a197c5fe 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultLaunchConfigGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultLaunchConfigGenerator.cpp @@ -7,6 +7,11 @@ #include "Serialization/JsonWriter.h" #include "Misc/FileHelper.h" +#include "Misc/MessageDialog.h" + +#include "ISettingsModule.h" +#include "SpatialGDKSettings.h" + DEFINE_LOG_CATEGORY(LogSpatialGDKDefaultLaunchConfigGenerator); #define LOCTEXT_NAMESPACE "SpatialGDKDefaultLaunchConfigGenerator" @@ -168,4 +173,86 @@ bool GenerateDefaultLaunchConfig(const FString& LaunchConfigPath, const FSpatial return false; } +bool ValidateGeneratedLaunchConfig(const FSpatialLaunchConfigDescription& LaunchConfigDesc) +{ + const USpatialGDKSettings* SpatialGDKRuntimeSettings = GetDefault(); + + if (const FString* EnableChunkInterest = LaunchConfigDesc.World.LegacyFlags.Find(TEXT("enable_chunk_interest"))) + { + if (*EnableChunkInterest == TEXT("true")) + { + const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(TEXT("The legacy flag \"enable_chunk_interest\" is set to true in the generated launch configuration. Chunk interest is not supported and this flag needs to be set to false.\n\nDo you want to configure your launch config settings now?"))); + + if (Result == EAppReturnType::Yes) + { + FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Editor Settings"); + } + + return false; + } + } + + if (!SpatialGDKRuntimeSettings->bEnableHandover && LaunchConfigDesc.ServerWorkers.ContainsByPredicate([](const FWorkerTypeLaunchSection& Section) + { + return (Section.Rows * Section.Columns) > 1; + })) + { + const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(TEXT("Property handover is disabled and a zoned deployment is specified.\nThis is not supported.\n\nDo you want to configure your project settings now?"))); + + if (Result == EAppReturnType::Yes) + { + FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Runtime Settings"); + } + + return false; + } + + if (LaunchConfigDesc.ServerWorkers.ContainsByPredicate([](const FWorkerTypeLaunchSection& Section) + { + return (Section.Rows * Section.Columns) < Section.NumEditorInstances; + })) + { + const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(TEXT("Attempting to launch too many servers for load balance configuration.\nThis is not supported.\n\nDo you want to configure your project settings now?"))); + + if (Result == EAppReturnType::Yes) + { + FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Editor Settings"); + } + + return false; + } + + if (!SpatialGDKRuntimeSettings->ServerWorkerTypes.Contains(SpatialGDKRuntimeSettings->DefaultWorkerType.WorkerTypeName)) + { + const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(TEXT("Default Worker Type is invalid, please choose a valid worker type as the default.\n\nDo you want to configure your project settings now?"))); + + if (Result == EAppReturnType::Yes) + { + FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Runtime Settings"); + } + + return false; + } + + if (SpatialGDKRuntimeSettings->bEnableOffloading) + { + for (const TPair& ActorGroup : SpatialGDKRuntimeSettings->ActorGroups) + { + if (!SpatialGDKRuntimeSettings->ServerWorkerTypes.Contains(ActorGroup.Value.OwningWorkerType.WorkerTypeName)) + { + const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(FString::Printf(TEXT("Actor Group '%s' has an invalid Owning Worker Type, please choose a valid worker type.\n\nDo you want to configure your project settings now?"), *ActorGroup.Key.ToString()))); + + if (Result == EAppReturnType::Yes) + { + FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Runtime Settings"); + } + + return false; + } + } + } + + return true; +} + #undef LOCTEXT_NAMESPACE diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultWorkerJsonGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultWorkerJsonGenerator.cpp index 7377994be7..59409e5bee 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultWorkerJsonGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultWorkerJsonGenerator.cpp @@ -3,6 +3,7 @@ #include "SpatialGDKDefaultWorkerJsonGenerator.h" #include "SpatialGDKEditorSettings.h" +#include "SpatialGDKServicesConstants.h" #include "Misc/FileHelper.h" @@ -39,7 +40,7 @@ bool GenerateDefaultWorkerJson(const FString& JsonPath, const FString& WorkerTyp bool GenerateAllDefaultWorkerJsons(bool& bOutRedeployRequired) { - const FString WorkerJsonDir = FSpatialGDKServicesModule::GetSpatialOSDirectory(TEXT("workers/unreal")); + const FString WorkerJsonDir = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("workers/unreal")); bool bAllJsonsGeneratedSuccessfully = true; if (const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault()) diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp index 6365c56be4..2b2ac37705 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp @@ -10,13 +10,15 @@ #include "Editor.h" #include "FileHelpers.h" -#include "AssetRegistryModule.h" #include "AssetDataTagMap.h" +#include "AssetRegistryModule.h" #include "GeneralProjectSettings.h" +#include "Internationalization/Regex.h" #include "Misc/ScopedSlowTask.h" +#include "Settings/ProjectPackagingSettings.h" #include "SpatialGDKEditorSettings.h" +#include "SpatialGDKServicesConstants.h" #include "UObject/StrongObjectPtr.h" -#include "Settings/ProjectPackagingSettings.h" using namespace SpatialGDKEditor; @@ -57,8 +59,8 @@ bool FSpatialGDKEditor::GenerateSchema(bool bFullScan) #if ENGINE_MINOR_VERSION <= 22 // Force spatial networking so schema layouts are correct UGeneralProjectSettings* GeneralProjectSettings = GetMutableDefault(); - bool bCachedSpatialNetworking = GeneralProjectSettings->bSpatialNetworking; - GeneralProjectSettings->bSpatialNetworking = true; + bool bCachedSpatialNetworking = GeneralProjectSettings->UsesSpatialNetworking(); + GeneralProjectSettings->SetUsesSpatialNetworking(true); #endif RemoveEditorAssetLoadedCallback(); @@ -69,7 +71,7 @@ bool FSpatialGDKEditor::GenerateSchema(bool bFullScan) return false; } - if (!Schema::LoadGeneratorStateFromSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_ASSET_PATH)) + if (!Schema::LoadGeneratorStateFromSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_FILE_PATH)) { Schema::ResetSchemaGeneratorStateAndCleanupFolders(); } @@ -98,8 +100,8 @@ bool FSpatialGDKEditor::GenerateSchema(bool bFullScan) if (bFullScan) { // UNR-1610 - This copy is a workaround to enable schema_compiler usage until FPL is ready. Without this prepare_for_run checks crash local launch and cloud upload. - FString GDKSchemaCopyDir = FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("schema/unreal/gdk")); - FString CoreSDKSchemaCopyDir = FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("build/dependencies/schema/standard_library")); + FString GDKSchemaCopyDir = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("schema/unreal/gdk")); + FString CoreSDKSchemaCopyDir = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("build/dependencies/schema/standard_library")); Schema::CopyWellKnownSchemaFiles(GDKSchemaCopyDir, CoreSDKSchemaCopyDir); Schema::RefreshSchemaFiles(GetDefault()->GetGeneratedSchemaOutputFolder()); } @@ -125,7 +127,7 @@ bool FSpatialGDKEditor::GenerateSchema(bool bFullScan) } #if ENGINE_MINOR_VERSION <= 22 - GetMutableDefault()->bSpatialNetworking = bCachedSpatialNetworking; + GetMutableDefault()->SetUsesSpatialNetworking(bCachedSpatialNetworking); #endif bSchemaGeneratorRunning = false; @@ -165,10 +167,7 @@ bool FSpatialGDKEditor::LoadPotentialAssets(TArray>& O return false; } const FString PackagePath = Data.PackagePath.ToString(); - if (!PackagePath.StartsWith("/Game")) - { - return false; - } + for (const auto& Directory : DirectoriesToNeverCook) { if (PackagePath.StartsWith(Directory.Path)) diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCloudLauncher.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCloudLauncher.cpp index 8de15eb7d1..b1995678a6 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCloudLauncher.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCloudLauncher.cpp @@ -17,13 +17,13 @@ bool SpatialGDKCloudLaunch() { const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); - FString LauncherCreateArguments = FString::Printf( - TEXT("create %s %s %s \"%s\" \"%s\" %s"), + TEXT("create %s %s %s %s \"%s\" \"%s\" %s"), *FSpatialGDKServicesModule::GetProjectName(), *SpatialGDKSettings->GetAssemblyName(), + *SpatialGDKSettings->GetSpatialOSRuntimeVersionForCloud(), *SpatialGDKSettings->GetPrimaryDeploymentName(), - *SpatialGDKSettings->GetPrimaryLanchConfigPath(), + *SpatialGDKSettings->GetPrimaryLaunchConfigPath(), *SpatialGDKSettings->GetSnapshotPath(), *SpatialGDKSettings->GetPrimaryRegionCode().ToString() ); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorLayoutDetails.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorLayoutDetails.cpp new file mode 100644 index 0000000000..140f7fc202 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorLayoutDetails.cpp @@ -0,0 +1,298 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialGDKEditorLayoutDetails.h" + +#include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "DetailCategoryBuilder.h" +#include "HAL/PlatformFilemanager.h" +#include "IOSRuntimeSettings.h" +#include "Misc/App.h" +#include "Misc/FileHelper.h" +#include "Misc/MessageDialog.h" +#include "Serialization/JsonSerializer.h" +#include "SpatialGDKSettings.h" +#include "SpatialGDKEditorSettings.h" +#include "SpatialGDKServicesConstants.h" +#include "SpatialGDKServicesModule.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/Input/SButton.h" + +DEFINE_LOG_CATEGORY(LogSpatialGDKEditorLayoutDetails); + +TSharedRef FSpatialGDKEditorLayoutDetails::MakeInstance() +{ + return MakeShareable(new FSpatialGDKEditorLayoutDetails); +} + +void FSpatialGDKEditorLayoutDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) +{ + TSharedPtr UsePinnedVersionProperty = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, bUseGDKPinnedRuntimeVersion)); + + IDetailPropertyRow* CustomRow = DetailBuilder.EditDefaultProperty(UsePinnedVersionProperty); + + FString PinnedVersionDisplay = FString::Printf(TEXT("GDK Pinned Version : %s"), *SpatialGDKServicesConstants::SpatialOSRuntimePinnedVersion); + + CustomRow->CustomWidget() + .NameContent() + [ + UsePinnedVersionProperty->CreatePropertyNameWidget() + ] + .ValueContent() + [ + SNew(SHorizontalBox) + +SHorizontalBox::Slot() + .HAlign(HAlign_Left) + .AutoWidth() + [ + UsePinnedVersionProperty->CreatePropertyValueWidget() + ] + +SHorizontalBox::Slot() + .Padding(5) + .HAlign(HAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(FText::FromString(PinnedVersionDisplay)) + ] + ]; + + IDetailCategoryBuilder& CloudConnectionCategory = DetailBuilder.EditCategory("Cloud Connection"); + CloudConnectionCategory.AddCustomRow(FText::FromString("Generate Development Authentication Token")) + .ValueContent() + .VAlign(VAlign_Center) + .MinDesiredWidth(250) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .OnClicked(this, &FSpatialGDKEditorLayoutDetails::GenerateDevAuthToken) + .Content() + [ + SNew(STextBlock).Text(FText::FromString("Generate Dev Auth Token")) + ] + ]; + + IDetailCategoryBuilder& MobileCategory = DetailBuilder.EditCategory("Mobile"); + MobileCategory.AddCustomRow(FText::FromString("Push SpatialOS settings to Android device")) + .ValueContent() + .VAlign(VAlign_Center) + .MinDesiredWidth(250) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .OnClicked(this, &FSpatialGDKEditorLayoutDetails::PushCommandLineArgsToAndroidDevice) + .Content() + [ + SNew(STextBlock).Text(FText::FromString("Push SpatialOS settings to Android device")) + ] + ]; + + MobileCategory.AddCustomRow(FText::FromString("Push SpatialOS settings to iOS device")) + .ValueContent() + .VAlign(VAlign_Center) + .MinDesiredWidth(250) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .OnClicked(this, &FSpatialGDKEditorLayoutDetails::PushCommandLineArgsToIOSDevice) + .Content() + [ + SNew(STextBlock).Text(FText::FromString("Push SpatialOS settings to iOS device")) + ] + ]; +} + +FReply FSpatialGDKEditorLayoutDetails::GenerateDevAuthToken() +{ + FString Arguments = TEXT("project auth dev-auth-token create --description=\"Unreal GDK Token\" --json_output"); + if (GetDefault()->IsRunningInChina()) + { + Arguments += TEXT(" --environment cn-production"); + } + + FString CreateDevAuthTokenResult; + int32 ExitCode; + FSpatialGDKServicesModule::ExecuteAndReadOutput(SpatialGDKServicesConstants::SpatialExe, Arguments, SpatialGDKServicesConstants::SpatialOSDirectory, CreateDevAuthTokenResult, ExitCode); + + if (ExitCode != 0) + { + UE_LOG(LogSpatialGDKEditorLayoutDetails, Error, TEXT("Unable to generate a development authentication token. Result: %s"), *CreateDevAuthTokenResult); + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(TEXT("Unable to generate a development authentication token. Result: %s"), *CreateDevAuthTokenResult))); + return FReply::Unhandled(); + }; + + FString AuthResult; + FString DevAuthTokenResult; + bool bFoundNewline = CreateDevAuthTokenResult.TrimEnd().Split(TEXT("\n"), &AuthResult, &DevAuthTokenResult, ESearchCase::IgnoreCase, ESearchDir::FromEnd); + if (!bFoundNewline || DevAuthTokenResult.IsEmpty()) + { + // This is necessary because depending on whether you are already authenticated against spatial, it will either return two json structs or one. + DevAuthTokenResult = CreateDevAuthTokenResult; + } + + TSharedRef> JsonReader = TJsonReaderFactory::Create(DevAuthTokenResult); + TSharedPtr JsonRootObject; + if (!(FJsonSerializer::Deserialize(JsonReader, JsonRootObject) && JsonRootObject.IsValid())) + { + UE_LOG(LogSpatialGDKEditorLayoutDetails, Error, TEXT("Unable to parse the received development authentication token. Result: %s"), *DevAuthTokenResult); + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(TEXT("Unable to parse the received development authentication token. Result: %s"), *DevAuthTokenResult))); + return FReply::Unhandled(); + } + + // We need a pointer to a shared pointer due to how the JSON API works. + const TSharedPtr* JsonDataObject; + if (!(JsonRootObject->TryGetObjectField("json_data", JsonDataObject))) + { + UE_LOG(LogSpatialGDKEditorLayoutDetails, Error, TEXT("Unable to parse the received json data. Result: %s"), *DevAuthTokenResult); + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(TEXT("Unable to parse the received json data. Result: %s"), *DevAuthTokenResult))); + return FReply::Unhandled(); + } + + FString TokenSecret; + if (!(*JsonDataObject)->TryGetStringField("token_secret", TokenSecret)) + { + UE_LOG(LogSpatialGDKEditorLayoutDetails, Error, TEXT("Unable to parse the token_secret field inside the received json data. Result: %s"), *DevAuthTokenResult); + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(TEXT("Unable to parse the token_secret field inside the received json data. Result: %s"), *DevAuthTokenResult))); + return FReply::Unhandled(); + } + + if (USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetMutableDefault()) + { + SpatialGDKEditorSettings->DevelopmentAuthenticationToken = TokenSecret; + SpatialGDKEditorSettings->SaveConfig(); + SpatialGDKEditorSettings->SetRuntimeDevelopmentAuthenticationToken(); + } + + return FReply::Handled(); +} + +bool FSpatialGDKEditorLayoutDetails::TryConstructMobileCommandLineArgumentsFile(FString& CommandLineArgsFile) +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + const FString ProjectName = FApp::GetProjectName(); + + // The project path is based on this: https://github.com/improbableio/UnrealEngine/blob/4.22-SpatialOSUnrealGDK-release/Engine/Source/Programs/AutomationTool/AutomationUtils/DeploymentContext.cs#L408 + const FString MobileProjectPath = FString::Printf(TEXT("../../../%s/%s.uproject"), *ProjectName, *ProjectName); + FString TravelUrl; + FString SpatialOSOptions = FString::Printf(TEXT("-workerType %s"), *(SpatialGDKSettings->MobileWorkerType)); + if (SpatialGDKSettings->bMobileConnectToLocalDeployment) + { + if (SpatialGDKSettings->MobileRuntimeIP.IsEmpty()) + { + UE_LOG(LogSpatialGDKEditorLayoutDetails, Error, TEXT("The Runtime IP is currently not set. Please make sure to specify a Runtime IP.")); + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(TEXT("The Runtime IP is currently not set. Please make sure to specify a Runtime IP.")))); + return false; + } + + TravelUrl = SpatialGDKSettings->MobileRuntimeIP; + } + else + { + TravelUrl = TEXT("connect.to.spatialos"); + + if (SpatialGDKSettings->DevelopmentAuthenticationToken.IsEmpty()) + { + FReply GeneratedTokenReply = GenerateDevAuthToken(); + if (!GeneratedTokenReply.IsEventHandled()) + { + return false; + } + } + + SpatialOSOptions += FString::Printf(TEXT(" +devauthToken %s"), *(SpatialGDKSettings->DevelopmentAuthenticationToken)); + if (!SpatialGDKSettings->DevelopmentDeploymentToConnect.IsEmpty()) + { + SpatialOSOptions += FString::Printf(TEXT(" +deployment %s"), *(SpatialGDKSettings->DevelopmentDeploymentToConnect)); + } + } + + const FString SpatialOSCommandLineArgs = FString::Printf(TEXT("%s %s %s %s"), *MobileProjectPath, *TravelUrl, *SpatialOSOptions, *(SpatialGDKSettings->MobileExtraCommandLineArgs)); + CommandLineArgsFile = FPaths::ConvertRelativePathToFull(FPaths::Combine(*FPaths::ProjectLogDir(), TEXT("ue4commandline.txt"))); + + if (!FFileHelper::SaveStringToFile(SpatialOSCommandLineArgs, *CommandLineArgsFile, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) + { + UE_LOG(LogSpatialGDKEditorLayoutDetails, Error, TEXT("Failed to write command line args to file: %s"), *CommandLineArgsFile); + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(TEXT("Failed to write command line args to file: %s"), *CommandLineArgsFile))); + return false; + } + + return true; +} + +bool FSpatialGDKEditorLayoutDetails::TryPushCommandLineArgsToDevice(const FString& Executable, const FString& ExeArguments, const FString& CommandLineArgsFile) +{ + FString ExeOutput; + FString StdErr; + int32 ExitCode; + + FPlatformProcess::ExecProcess(*Executable, *ExeArguments, &ExitCode, &ExeOutput, &StdErr); + if (ExitCode != 0) + { + UE_LOG(LogSpatialGDKEditorLayoutDetails, Error, TEXT("Failed to update the mobile client. %s %s"), *ExeOutput, *StdErr); + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(TEXT("Failed to update the mobile client. See the Output log for more information."))); + return false; + } + + UE_LOG(LogSpatialGDKEditorLayoutDetails, Log, TEXT("Successfully stored command line args on device: %s"), *ExeOutput); + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + if (!PlatformFile.DeleteFile(*CommandLineArgsFile)) + { + UE_LOG(LogSpatialGDKEditorLayoutDetails, Error, TEXT("Failed to delete file %s"), *CommandLineArgsFile); + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(TEXT("Failed to delete file %s"), *CommandLineArgsFile))); + return false; + } + + return true; +} + +FReply FSpatialGDKEditorLayoutDetails::PushCommandLineArgsToAndroidDevice() +{ + FString AndroidHome = FPlatformMisc::GetEnvironmentVariable(TEXT("ANDROID_HOME")); + if (AndroidHome.IsEmpty()) + { + UE_LOG(LogSpatialGDKEditorLayoutDetails, Error, TEXT("Environment variable ANDROID_HOME is not set. Please make sure to configure this.")); + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(TEXT("Environment variable ANDROID_HOME is not set. Please make sure to configure this."))); + return FReply::Unhandled(); + } + + FString OutCommandLineArgsFile; + + if (!TryConstructMobileCommandLineArgumentsFile(OutCommandLineArgsFile)) + { + return FReply::Unhandled(); + } + + const FString AndroidCommandLineFile = FString::Printf(TEXT("/mnt/sdcard/UE4Game/%s/UE4CommandLine.txt"), *FString(FApp::GetProjectName())); + const FString AdbArguments = FString::Printf(TEXT("push \"%s\" \"%s\""), *OutCommandLineArgsFile, *AndroidCommandLineFile); + +#if PLATFORM_WINDOWS + const FString AdbExe = FPaths::ConvertRelativePathToFull(FPaths::Combine(AndroidHome, TEXT("platform-tools/adb.exe"))); +#else + const FString AdbExe = FPaths::ConvertRelativePathToFull(FPaths::Combine(AndroidHome, TEXT("platform-tools/adb"))); +#endif + + TryPushCommandLineArgsToDevice(AdbExe, AdbArguments, OutCommandLineArgsFile); + return FReply::Handled(); +} + +FReply FSpatialGDKEditorLayoutDetails::PushCommandLineArgsToIOSDevice() +{ + const UIOSRuntimeSettings* IOSRuntimeSettings = GetDefault(); + FString OutCommandLineArgsFile; + + if (!TryConstructMobileCommandLineArgumentsFile(OutCommandLineArgsFile)) + { + return FReply::Unhandled(); + } + + FString Executable = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::EngineDir(), TEXT("Binaries/DotNET/IOS/deploymentserver.exe"))); + FString DeploymentServerArguments = FString::Printf(TEXT("copyfile -bundle \"%s\" -file \"%s\" -file \"/Documents/ue4commandline.txt\""), *(IOSRuntimeSettings->BundleIdentifier.Replace(TEXT("[PROJECT_NAME]"), FApp::GetProjectName())), *OutCommandLineArgsFile); + +#if PLATFORM_MAC + DeploymentServerArguments = FString::Printf(TEXT("%s %s"), *Executable, *DeploymentServerArguments); + Executable = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::EngineDir(), TEXT("Binaries/ThirdParty/Mono/Mac/bin/mono"))); +#endif + + TryPushCommandLineArgsToDevice(Executable, DeploymentServerArguments, OutCommandLineArgsFile); + return FReply::Handled(); +} diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorModule.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorModule.cpp index 5c17008101..46dbe03e5f 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorModule.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorModule.cpp @@ -4,6 +4,7 @@ #include "SpatialGDKSettings.h" #include "SpatialGDKEditorSettings.h" +#include "SpatialGDKEditorLayoutDetails.h" #include "ISettingsModule.h" #include "ISettingsContainer.h" @@ -11,15 +12,27 @@ #include "PropertyEditor/Public/PropertyEditorModule.h" #include "WorkerTypeCustomization.h" +#include "EditorExtension/GridLBStrategyEditorExtension.h" + #define LOCTEXT_NAMESPACE "FSpatialGDKEditorModule" +FSpatialGDKEditorModule::FSpatialGDKEditorModule() + : ExtensionManager(MakeUnique()) +{ + +} + void FSpatialGDKEditorModule::StartupModule() { RegisterSettings(); + + ExtensionManager->RegisterExtension(); } void FSpatialGDKEditorModule::ShutdownModule() { + ExtensionManager->Cleanup(); + if (UObjectInitialized()) { UnregisterSettings(); @@ -58,6 +71,7 @@ void FSpatialGDKEditorModule::RegisterSettings() FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); PropertyModule.RegisterCustomPropertyTypeLayout("WorkerType", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FWorkerTypeCustomization::MakeInstance)); + PropertyModule.RegisterCustomClassLayout(USpatialGDKEditorSettings::StaticClass()->GetFName(), FOnGetDetailCustomizationInstance::CreateStatic(&FSpatialGDKEditorLayoutDetails::MakeInstance)); } void FSpatialGDKEditorModule::UnregisterSettings() diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorSettings.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorSettings.cpp index ee17c7ffee..9b177a734f 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorSettings.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorSettings.cpp @@ -4,6 +4,7 @@ #include "Internationalization/Regex.h" #include "ISettingsModule.h" +#include "Misc/FileHelper.h" #include "Misc/MessageDialog.h" #include "Modules/ModuleManager.h" #include "Settings/LevelEditorPlaySettings.h" @@ -11,19 +12,36 @@ #include "SpatialConstants.h" #include "SpatialGDKSettings.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonSerializer.h" + DEFINE_LOG_CATEGORY(LogSpatialEditorSettings); +#define LOCTEXT_NAMESPACE "USpatialGDKEditorSettings" + +void FSpatialLaunchConfigDescription::SetLevelEditorPlaySettingsWorkerTypes() +{ + ULevelEditorPlaySettings* PlayInSettings = GetMutableDefault(); + + PlayInSettings->WorkerTypesToLaunch.Empty(ServerWorkers.Num()); + for (const FWorkerTypeLaunchSection& WorkerLaunch : ServerWorkers) + { + PlayInSettings->WorkerTypesToLaunch.Add(WorkerLaunch.WorkerTypeName, WorkerLaunch.NumEditorInstances); + } +} USpatialGDKEditorSettings::USpatialGDKEditorSettings(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , bShowSpatialServiceButton(false) , bDeleteDynamicEntities(true) , bGenerateDefaultLaunchConfig(true) + , bUseGDKPinnedRuntimeVersion(true) , bExposeRuntimeIP(false) , ExposedRuntimeIP(TEXT("")) , bStopSpatialOnExit(false) , bAutoStartLocalDeployment(true) , PrimaryDeploymentRegionCode(ERegionCode::US) , SimulatedPlayerLaunchConfigPath(FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(TEXT("SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/SpatialConfig/cloud_launch_sim_player_deployment.json"))) + , bUseDevelopmentAuthenticationFlow(false) , SimulatedPlayerDeploymentRegionCode(ERegionCode::US) { SpatialOSLaunchConfig.FilePath = GetSpatialOSLaunchConfig(); @@ -31,6 +49,24 @@ USpatialGDKEditorSettings::USpatialGDKEditorSettings(const FObjectInitializer& O SpatialOSSnapshotToLoad = GetSpatialOSSnapshotToLoad(); } +const FString& USpatialGDKEditorSettings::GetSpatialOSRuntimeVersionForLocal() const +{ + if (bUseGDKPinnedRuntimeVersion || LocalRuntimeVersion.IsEmpty()) + { + return SpatialGDKServicesConstants::SpatialOSRuntimePinnedVersion; + } + return LocalRuntimeVersion; +} + +const FString& USpatialGDKEditorSettings::GetSpatialOSRuntimeVersionForCloud() const +{ + if (bUseGDKPinnedRuntimeVersion || CloudRuntimeVersion.IsEmpty()) + { + return SpatialGDKServicesConstants::SpatialOSRuntimePinnedVersion; + } + return CloudRuntimeVersion; +} + void USpatialGDKEditorSettings::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) { Super::PostEditChangeProperty(PropertyChangedEvent); @@ -49,7 +85,18 @@ void USpatialGDKEditorSettings::PostEditChangeProperty(struct FPropertyChangedEv else if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, LaunchConfigDesc)) { SetRuntimeWorkerTypes(); - SetLevelEditorPlaySettingsWorkerTypes(); + } + else if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, bUseDevelopmentAuthenticationFlow)) + { + SetRuntimeUseDevelopmentAuthenticationFlow(); + } + else if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, DevelopmentAuthenticationToken)) + { + SetRuntimeDevelopmentAuthenticationToken(); + } + else if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, DevelopmentDeploymentToConnect)) + { + SetRuntimeDevelopmentDeploymentToConnect(); } } @@ -63,13 +110,15 @@ void USpatialGDKEditorSettings::PostInitProperties() PlayInSettings->SaveConfig(); SetRuntimeWorkerTypes(); - SetLevelEditorPlaySettingsWorkerTypes(); + SetRuntimeUseDevelopmentAuthenticationFlow(); + SetRuntimeDevelopmentAuthenticationToken(); + SetRuntimeDevelopmentDeploymentToConnect(); } void USpatialGDKEditorSettings::SetRuntimeWorkerTypes() { TSet WorkerTypes; - + for (const FWorkerTypeLaunchSection& WorkerLaunch : LaunchConfigDesc.ServerWorkers) { if (WorkerLaunch.WorkerTypeName != NAME_None) @@ -84,19 +133,26 @@ void USpatialGDKEditorSettings::SetRuntimeWorkerTypes() RuntimeSettings->ServerWorkerTypes.Empty(WorkerTypes.Num()); RuntimeSettings->ServerWorkerTypes.Append(WorkerTypes); RuntimeSettings->PostEditChange(); - RuntimeSettings->SaveConfig(CPF_Config, *RuntimeSettings->GetDefaultConfigFilename()); + RuntimeSettings->UpdateSinglePropertyInConfigFile(RuntimeSettings->GetClass()->FindPropertyByName(GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, ServerWorkerTypes)), RuntimeSettings->GetDefaultConfigFilename()); } } -void USpatialGDKEditorSettings::SetLevelEditorPlaySettingsWorkerTypes() +void USpatialGDKEditorSettings::SetRuntimeUseDevelopmentAuthenticationFlow() { - ULevelEditorPlaySettings* PlayInSettings = GetMutableDefault(); + USpatialGDKSettings* RuntimeSettings = GetMutableDefault(); + RuntimeSettings->bUseDevelopmentAuthenticationFlow = bUseDevelopmentAuthenticationFlow; +} - PlayInSettings->WorkerTypesToLaunch.Empty(LaunchConfigDesc.ServerWorkers.Num()); - for (const FWorkerTypeLaunchSection& WorkerLaunch : LaunchConfigDesc.ServerWorkers) - { - PlayInSettings->WorkerTypesToLaunch.Add(WorkerLaunch.WorkerTypeName, WorkerLaunch.NumEditorInstances); - } +void USpatialGDKEditorSettings::SetRuntimeDevelopmentAuthenticationToken() +{ + USpatialGDKSettings* RuntimeSettings = GetMutableDefault(); + RuntimeSettings->DevelopmentAuthenticationToken = DevelopmentAuthenticationToken; +} + +void USpatialGDKEditorSettings::SetRuntimeDevelopmentDeploymentToConnect() +{ + USpatialGDKSettings* RuntimeSettings = GetMutableDefault(); + RuntimeSettings->DevelopmentDeploymentToConnect = DevelopmentDeploymentToConnect; } bool USpatialGDKEditorSettings::IsAssemblyNameValid(const FString& Name) @@ -144,7 +200,15 @@ void USpatialGDKEditorSettings::SetAssemblyName(const FString& Name) void USpatialGDKEditorSettings::SetPrimaryLaunchConfigPath(const FString& Path) { - PrimaryLaunchConfigPath.FilePath = FPaths::ConvertRelativePathToFull(Path); + // If the path is empty don't try to convert it to a full path. + if (Path.IsEmpty()) + { + PrimaryLaunchConfigPath.FilePath = Path; + } + else + { + PrimaryLaunchConfigPath.FilePath = FPaths::ConvertRelativePathToFull(Path); + } SaveConfig(); } @@ -170,6 +234,18 @@ void USpatialGDKEditorSettings::SetSimulatedPlayersEnabledState(bool IsEnabled) SaveConfig(); } +void USpatialGDKEditorSettings::SetUseGDKPinnedRuntimeVersion(bool Use) +{ + bUseGDKPinnedRuntimeVersion = Use; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetCustomCloudSpatialOSRuntimeVersion(const FString& Version) +{ + CloudRuntimeVersion = Version; + SaveConfig(); +} + void USpatialGDKEditorSettings::SetSimulatedPlayerDeploymentName(const FString& Name) { SimulatedPlayerDeploymentName = Name; @@ -182,6 +258,71 @@ void USpatialGDKEditorSettings::SetNumberOfSimulatedPlayers(uint32 Number) SaveConfig(); } +bool USpatialGDKEditorSettings::IsManualWorkerConnectionSet(const FString& LaunchConfigPath, TArray& OutWorkersManuallyLaunched) +{ + TSharedPtr LaunchConfigJson; + { + TUniquePtr ConfigFile(IFileManager::Get().CreateFileReader(*LaunchConfigPath)); + + if (!ConfigFile) + { + UE_LOG(LogSpatialEditorSettings, Error, TEXT("Could not open configuration file %s"), *LaunchConfigPath); + return false; + } + + TSharedRef> JsonReader = TJsonReader::Create(ConfigFile.Get()); + + FJsonSerializer::Deserialize(*JsonReader, LaunchConfigJson); + } + + const TSharedPtr* LaunchConfigJsonRootObject; + + if (!LaunchConfigJson || !LaunchConfigJson->TryGetObject(LaunchConfigJsonRootObject)) + { + UE_LOG(LogSpatialEditorSettings, Error, TEXT("Invalid configuration file %s"), *LaunchConfigPath); + return false; + } + + const TSharedPtr* LoadBalancingField; + if (!(*LaunchConfigJsonRootObject)->TryGetObjectField("load_balancing", LoadBalancingField)) + { + return false; + } + + const TArray>* LayerConfigurations; + if (!(*LoadBalancingField)->TryGetArrayField("layer_configurations", LayerConfigurations)) + { + return false; + } + + for (const auto& LayerConfigurationValue : *LayerConfigurations) + { + if (const TSharedPtr LayerConfiguration = LayerConfigurationValue->AsObject()) + { + const TSharedPtr* OptionsField; + bool ManualWorkerConnectionFlag; + + // Check manual_worker_connection flag, if it exists. + if (LayerConfiguration->TryGetObjectField("options", OptionsField) + && (*OptionsField)->TryGetBoolField("manual_worker_connection_only", ManualWorkerConnectionFlag) + && ManualWorkerConnectionFlag) + { + FString WorkerName; + if (LayerConfiguration->TryGetStringField("layer", WorkerName)) + { + OutWorkersManuallyLaunched.Add(WorkerName); + } + else + { + UE_LOG(LogSpatialEditorSettings, Error, TEXT("Invalid configuration file %s, Layer configuration missing its layer field"), *LaunchConfigPath); + } + } + } + } + + return OutWorkersManuallyLaunched.Num() != 0; +} + bool USpatialGDKEditorSettings::IsDeploymentConfigurationValid() const { bool bValid = true; @@ -205,7 +346,7 @@ bool USpatialGDKEditorSettings::IsDeploymentConfigurationValid() const UE_LOG(LogSpatialEditorSettings, Error, TEXT("Snapshot path cannot be empty.")); bValid = false; } - if (GetPrimaryLanchConfigPath().IsEmpty()) + if (GetPrimaryLaunchConfigPath().IsEmpty()) { UE_LOG(LogSpatialEditorSettings, Error, TEXT("Launch config path cannot be empty.")); bValid = false; @@ -230,5 +371,21 @@ bool USpatialGDKEditorSettings::IsDeploymentConfigurationValid() const } } + TArray WorkersManuallyLaunched; + if (IsManualWorkerConnectionSet(GetPrimaryLaunchConfigPath(), WorkersManuallyLaunched)) + { + FString WorkersReportString (LOCTEXT("AllowManualWorkerConnection", "Chosen launch configuration will not automatically launch the following worker types. Do you want to continue?\n").ToString()); + + for (const FString& Worker : WorkersManuallyLaunched) + { + WorkersReportString.Append(FString::Printf(TEXT(" - %s\n"), *Worker)); + } + + if (FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(WorkersReportString)) != EAppReturnType::Yes) + { + return false; + } + } + return bValid; } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/LaunchConfigEditor.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/LaunchConfigEditor.cpp new file mode 100644 index 0000000000..f172539eef --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/LaunchConfigEditor.cpp @@ -0,0 +1,30 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Utils/LaunchConfigEditor.h" + +#include "DesktopPlatformModule.h" +#include "Framework/Application/SlateApplication.h" +#include "IDesktopPlatform.h" +#include "SpatialGDKDefaultLaunchConfigGenerator.h" + +void ULaunchConfigurationEditor::SaveConfiguration() +{ + IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get(); + + FString DefaultOutPath = SpatialGDKServicesConstants::SpatialOSDirectory; + TArray Filenames; + + bool bSaved = DesktopPlatform->SaveFileDialog( + FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr), + TEXT("Save launch configuration"), + DefaultOutPath, + TEXT(""), + TEXT("JSON Configuration|*.json"), + EFileDialogFlags::None, + Filenames); + + if (bSaved && Filenames.Num() > 0) + { + GenerateDefaultLaunchConfig(Filenames[0], &LaunchConfiguration); + } +} diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/TransientUObjectEditor.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/TransientUObjectEditor.cpp new file mode 100644 index 0000000000..14a097072a --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/TransientUObjectEditor.cpp @@ -0,0 +1,160 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Utils/TransientUObjectEditor.h" + +#include "MainFrame/Public/Interfaces/IMainFrameModule.h" +#include "PropertyEditor/Public/PropertyEditorModule.h" + +#include "Framework/Application/SlateApplication.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Layout/SBorder.h" + + +namespace +{ + + void OnTransientUObjectEditorWindowClosed(const TSharedRef& Window, UTransientUObjectEditor* Instance) + { + Instance->RemoveFromRoot(); + } + + // Copied from FPropertyEditorModule::CreateFloatingDetailsView. + bool ShouldShowProperty(const FPropertyAndParent& PropertyAndParent, bool bHaveTemplate) + { + const UProperty& Property = PropertyAndParent.Property; + + if (bHaveTemplate) + { + const UClass* PropertyOwnerClass = Cast(Property.GetOuter()); + const bool bDisableEditOnTemplate = PropertyOwnerClass + && PropertyOwnerClass->IsNative() + && Property.HasAnyPropertyFlags(CPF_DisableEditOnTemplate); + + if (bDisableEditOnTemplate) + { + return false; + } + } + return true; + } + + FReply ExecuteEditorCommand(UTransientUObjectEditor* Instance, UFunction* MethodToExecute) + { + Instance->CallFunctionByNameWithArguments(*MethodToExecute->GetName(), *GLog, nullptr, true); + + return FReply::Handled(); + } +} + +// Rewrite of FPropertyEditorModule::CreateFloatingDetailsView to use the detail property view in a new window. +void UTransientUObjectEditor::LaunchTransientUObjectEditor(const FString& EditorName, UClass* ObjectClass) +{ + if (!ObjectClass) + { + return; + } + + if (!ObjectClass->IsChildOf()) + { + return; + } + + UTransientUObjectEditor* ObjectInstance = NewObject(GetTransientPackage(), ObjectClass); + ObjectInstance->AddToRoot(); + + FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked("PropertyEditor"); + + TArray ObjectsToView; + ObjectsToView.Add(ObjectInstance); + + FDetailsViewArgs Args; + Args.bHideSelectionTip = true; + Args.bLockable = false; + Args.bAllowSearch = false; + Args.bShowPropertyMatrixButton = false; + + TSharedRef DetailView = PropertyEditorModule.CreateDetailView(Args); + + bool bHaveTemplate = false; + for (int32 i = 0; i < ObjectsToView.Num(); i++) + { + if (ObjectsToView[i] != NULL && ObjectsToView[i]->IsTemplate()) + { + bHaveTemplate = true; + break; + } + } + + DetailView->SetIsPropertyVisibleDelegate(FIsPropertyVisible::CreateStatic(&ShouldShowProperty, bHaveTemplate)); + + DetailView->SetObjects(ObjectsToView); + + TSharedRef VBoxBuilder = SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + .FillHeight(1.0) + [ + DetailView + ]; + + // Add UFunction marked Exec as buttons in the editor's window + for (TFieldIterator FuncIt(ObjectClass); FuncIt; ++FuncIt) + { + UFunction* Function = *FuncIt; + if (Function->HasAnyFunctionFlags(FUNC_Exec) && (Function->NumParms == 0)) + { + const FText ButtonCaption = Function->GetDisplayNameText(); + + VBoxBuilder->AddSlot() + .AutoHeight() + .VAlign(VAlign_Bottom) + .HAlign(HAlign_Right) + .Padding(2.0) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0) + [ + SNew(SButton) + .Text(ButtonCaption) + .OnClicked(FOnClicked::CreateStatic(&ExecuteEditorCommand, ObjectInstance, Function)) + ] + ]; + } + } + + TSharedRef NewSlateWindow = SNew(SWindow) + .Title(FText::FromString(EditorName)) + [ + SNew(SBorder) + .BorderImage(FEditorStyle::GetBrush(TEXT("PropertyWindow.WindowBorder"))) + [ + VBoxBuilder + ] + ]; + + // If the main frame exists parent the window to it + TSharedPtr ParentWindow; + if (FModuleManager::Get().IsModuleLoaded("MainFrame")) + { + IMainFrameModule& MainFrame = FModuleManager::GetModuleChecked("MainFrame"); + ParentWindow = MainFrame.GetParentWindow(); + } + + if (ParentWindow.IsValid()) + { + // Parent the window to the main frame + FSlateApplication::Get().AddWindowAsNativeChild(NewSlateWindow, ParentWindow.ToSharedRef()); + } + else + { + FSlateApplication::Get().AddWindow(NewSlateWindow); + } + + NewSlateWindow->RegisterActiveTimer(0.5, FWidgetActiveTimerDelegate::CreateLambda([NewSlateWindow](double, float) + { + NewSlateWindow->Resize(NewSlateWindow->GetDesiredSize()); + return EActiveTimerReturnType::Stop; + })); +} diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/WorkerTypeCustomization.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/WorkerTypeCustomization.cpp index 15c3f3a0c8..da61005167 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/WorkerTypeCustomization.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/WorkerTypeCustomization.cpp @@ -1,3 +1,5 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + #include "WorkerTypeCustomization.h" #include "SpatialGDKSettings.h" diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/EditorExtension/LBStrategyEditorExtension.h b/SpatialGDK/Source/SpatialGDKEditor/Public/EditorExtension/LBStrategyEditorExtension.h new file mode 100644 index 0000000000..1006fb0b4b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/EditorExtension/LBStrategyEditorExtension.h @@ -0,0 +1,52 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" + +class UAbstractLBStrategy; +class FLBStrategyEditorExtensionManager; +struct FWorkerTypeLaunchSection; + +class FLBStrategyEditorExtensionInterface +{ +public: + virtual ~FLBStrategyEditorExtensionInterface() {} +private: + friend FLBStrategyEditorExtensionManager; + virtual bool GetDefaultLaunchConfiguration_Virtual(const UAbstractLBStrategy* Strategy, FWorkerTypeLaunchSection& OutConfiguration, FIntPoint& OutWorldDimensions) const = 0; +}; + +template +class FLBStrategyEditorExtensionTemplate : public FLBStrategyEditorExtensionInterface +{ +public: + using ExtendedStrategy = StrategyImpl; + +private: + bool GetDefaultLaunchConfiguration_Virtual(const UAbstractLBStrategy* Strategy, FWorkerTypeLaunchSection& OutConfiguration, FIntPoint& OutWorldDimensions) const override + { + return static_cast(this)->GetDefaultLaunchConfiguration(static_cast(Strategy), OutConfiguration, OutWorldDimensions); + } +}; + +class FLBStrategyEditorExtensionManager +{ +public: + SPATIALGDKEDITOR_API bool GetDefaultLaunchConfiguration(const UAbstractLBStrategy* Strategy, FWorkerTypeLaunchSection& OutConfiguration, FIntPoint& OutWorldDimensions) const; + + template + void RegisterExtension() + { + RegisterExtension(Extension::ExtendedStrategy::StaticClass(), MakeUnique()); + } + + void Cleanup(); + +private: + void RegisterExtension(UClass* StrategyClass, TUniquePtr StrategyExtension); + + using ExtensionArray = TArray>>; + + ExtensionArray Extensions; +}; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDefaultLaunchConfigGenerator.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDefaultLaunchConfigGenerator.h index 5066df62e6..ac093de32b 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDefaultLaunchConfigGenerator.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDefaultLaunchConfigGenerator.h @@ -9,3 +9,5 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKDefaultLaunchConfigGenerator, Log, All) struct FSpatialLaunchConfigDescription; bool SPATIALGDKEDITOR_API GenerateDefaultLaunchConfig(const FString& LaunchConfigPath, const FSpatialLaunchConfigDescription* InLaunchConfigDescription); + +bool SPATIALGDKEDITOR_API ValidateGeneratedLaunchConfig(const FSpatialLaunchConfigDescription& LaunchConfigDesc); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorLayoutDetails.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorLayoutDetails.h new file mode 100644 index 0000000000..0dea03656b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorLayoutDetails.h @@ -0,0 +1,24 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "IDetailCustomization.h" +#include "Input/Reply.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKEditorLayoutDetails, Log, All); + +class FSpatialGDKEditorLayoutDetails : public IDetailCustomization +{ +private: + bool TryConstructMobileCommandLineArgumentsFile(FString& CommandLineArgsFile); + bool TryPushCommandLineArgsToDevice(const FString& Executable, const FString& ExeArguments, const FString& CommandLineArgsFile); + + FReply GenerateDevAuthToken(); + FReply PushCommandLineArgsToIOSDevice(); + FReply PushCommandLineArgsToAndroidDevice(); + +public: + static TSharedRef MakeInstance(); + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; +}; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorModule.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorModule.h index 67b3b177c1..b5a7ae3936 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorModule.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorModule.h @@ -3,9 +3,16 @@ #include "Modules/ModuleInterface.h" #include "Modules/ModuleManager.h" +class FLBStrategyEditorExtensionManager; + class FSpatialGDKEditorModule : public IModuleInterface { public: + + FSpatialGDKEditorModule(); + + SPATIALGDKEDITOR_API const FLBStrategyEditorExtensionManager& GetLBStrategyExtensionManager() { return *ExtensionManager; } + virtual void StartupModule() override; virtual void ShutdownModule() override; @@ -20,4 +27,6 @@ class FSpatialGDKEditorModule : public IModuleInterface bool HandleEditorSettingsSaved(); bool HandleRuntimeSettingsSaved(); bool HandleCloudLauncherSettingsSaved(); + + TUniquePtr ExtensionManager; }; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSchemaGenerator.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSchemaGenerator.h index b36cd3650a..e8b044faac 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSchemaGenerator.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSchemaGenerator.h @@ -21,6 +21,14 @@ namespace SpatialGDKEditor SPATIALGDKEDITOR_API void GenerateSchemaForSublevels(); SPATIALGDKEDITOR_API void GenerateSchemaForSublevels(const FString& SchemaOutputPath, const TMultiMap& LevelNamesToPaths); + + SPATIALGDKEDITOR_API void GenerateSchemaForRPCEndpoints(); + + SPATIALGDKEDITOR_API void GenerateSchemaForRPCEndpoints(const FString& SchemaOutputPath); + + SPATIALGDKEDITOR_API void GenerateSchemaForNCDs(); + + SPATIALGDKEDITOR_API void GenerateSchemaForNCDs(const FString& SchemaOutputPath); SPATIALGDKEDITOR_API bool LoadGeneratorStateFromSchemaDatabase(const FString& FileName); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h index c94d3a98e4..524ddc04e3 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h @@ -1,4 +1,5 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved + #pragma once #include "CoreMinimal.h" @@ -6,6 +7,7 @@ #include "Misc/Paths.h" #include "SpatialConstants.h" #include "UObject/Package.h" +#include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesModule.h" #include "SpatialGDKEditorSettings.generated.h" @@ -24,26 +26,27 @@ struct FWorldLaunchSection { LegacyFlags.Add(TEXT("bridge_qos_max_timeout"), TEXT("0")); LegacyFlags.Add(TEXT("bridge_soft_handover_enabled"), TEXT("false")); + LegacyFlags.Add(TEXT("bridge_single_port_max_heartbeat_timeout_ms"), TEXT("3600000")); } /** The size of the simulation, in meters, for the auto-generated launch configuration file. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Simulation dimensions in meters")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Simulation dimensions in meters")) FIntPoint Dimensions; /** The size of the grid squares that the world is divided into, in “world units” (an arbitrary unit that worker instances can interpret as they choose). */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Chunk edge length in meters")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Chunk edge length in meters")) int32 ChunkEdgeLengthMeters; /** The frequency in seconds to write snapshots of the simulated world. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Snapshot write period in seconds")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Snapshot write period in seconds")) int32 SnapshotWritePeriodSeconds; /** Legacy non-worker flag configurations. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false)) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config) TMap LegacyFlags; /** Legacy JVM configurations. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Legacy Java parameters")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Legacy Java parameters")) TMap LegacyJavaParams; }; @@ -62,23 +65,23 @@ struct FWorkerPermissionsSection } /** Gives all permissions to a worker instance. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "All")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "All")) bool bAllPermissions; /** Enables a worker instance to create new entities. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "!bAllPermissions", ConfigRestartRequired = false, DisplayName = "Allow entity creation")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "!bAllPermissions", DisplayName = "Allow entity creation")) bool bAllowEntityCreation; /** Enables a worker instance to delete entities. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "!bAllPermissions", ConfigRestartRequired = false, DisplayName = "Allow entity deletion")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "!bAllPermissions", DisplayName = "Allow entity deletion")) bool bAllowEntityDeletion; /** Controls which components can be returned from entity queries that the worker instance performs. If an entity query specifies other components to be returned, the query will fail. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "!bAllPermissions", ConfigRestartRequired = false, DisplayName = "Allow entity query")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "!bAllPermissions", DisplayName = "Allow entity query")) bool bAllowEntityQuery; /** Specifies which components can be returned in the query result. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "!bAllPermissions", ConfigRestartRequired = false, DisplayName = "Component queries")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "!bAllPermissions", DisplayName = "Component queries")) TArray Components; }; @@ -94,11 +97,11 @@ struct FLoginRateLimitSection } /** The duration for which worker connection requests will be limited. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false)) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config) FString Duration; /** The connection request limit for the duration. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, ClampMin = "1", UIMin = "1")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ClampMin = "1", UIMin = "1")) int32 RequestsPerDuration; }; @@ -121,43 +124,43 @@ struct FWorkerTypeLaunchSection } /** The name of the worker type, defined in the filename of its spatialos..worker.json file. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false)) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config) FName WorkerTypeName; /** Defines the worker instance's permissions. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false)) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config) FWorkerPermissionsSection WorkerPermissions; /** Defines the maximum number of worker instances that can connect. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Max connection capacity limit (0 = unlimited capacity)", ClampMin = "0", UIMin = "0")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Max connection capacity limit (0 = unlimited capacity)", ClampMin = "0", UIMin = "0")) int32 MaxConnectionCapacityLimit; /** Enable connection rate limiting. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Login rate limit enabled")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Login rate limit enabled")) bool bLoginRateLimitEnabled; /** Login rate limiting configuration. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "bLoginRateLimitEnabled", ConfigRestartRequired = false)) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "bLoginRateLimitEnabled")) FLoginRateLimitSection LoginRateLimit; /** Number of columns in the rectangle grid load balancing config. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Rectangle grid column count", ClampMin = "1", UIMin = "1")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Rectangle grid column count", ClampMin = "1", UIMin = "1")) int32 Columns; /** Number of rows in the rectangle grid load balancing config. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Rectangle grid row count", ClampMin = "1", UIMin = "1")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Rectangle grid row count", ClampMin = "1", UIMin = "1")) int32 Rows; /** Number of instances to launch when playing in editor. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Instances to launch in editor", ClampMin = "0", UIMin = "0")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Instances to launch in editor", ClampMin = "0", UIMin = "0")) int32 NumEditorInstances; /** Flags defined for a worker instance. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Flags")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Flags")) TMap Flags; /** Determines if the worker instance is launched manually or by SpatialOS. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Manual worker connection only")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Manual worker connection only")) bool bManualWorkerConnectionOnly; }; @@ -192,16 +195,20 @@ struct FSpatialLaunchConfigDescription ServerWorkers.Add(UnrealWorkerDefaultSetting); } + + /** Set WorkerTypesToLaunch in level editor play settings. */ + SPATIALGDKEDITOR_API void SetLevelEditorPlaySettingsWorkerTypes(); + /** Deployment template. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false)) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config) FString Template; /** Configuration for the simulated world. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false)) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config) FWorldLaunchSection World; /** Worker-specific configuration parameters. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false)) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (TitleProperty = "WorkerTypeName")) TArray ServerWorkers; }; @@ -236,101 +243,150 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject /** Set WorkerTypes in runtime settings. */ void SetRuntimeWorkerTypes(); - /** Set WorkerTypesToLaunch in level editor play settings. */ - void SetLevelEditorPlaySettingsWorkerTypes(); + /** Set DAT in runtime settings. */ + void SetRuntimeUseDevelopmentAuthenticationFlow(); + void SetRuntimeDevelopmentDeploymentToConnect(); public: + /** If checked, show the Spatial service button on the GDK toolbar which can be used to turn the Spatial service on and off. */ - UPROPERTY(EditAnywhere, config, Category = "General", meta = (ConfigRestartRequired = false, DisplayName = "Show Spatial service button")) + UPROPERTY(EditAnywhere, config, Category = "General", meta = (DisplayName = "Show Spatial service button")) bool bShowSpatialServiceButton; /** Select to delete all a server-worker instance’s dynamically-spawned entities when the server-worker instance shuts down. If NOT selected, a new server-worker instance has all of these entities from the former server-worker instance’s session. */ - UPROPERTY(EditAnywhere, config, Category = "Play in editor settings", meta = (ConfigRestartRequired = false, DisplayName = "Delete dynamically spawned entities")) + UPROPERTY(EditAnywhere, config, Category = "Play in editor settings", meta = (DisplayName = "Delete dynamically spawned entities")) bool bDeleteDynamicEntities; /** Select the check box for the GDK to auto-generate a launch configuration file for your game when you launch a deployment session. If NOT selected, you must specify a launch configuration `.json` file. */ - UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (ConfigRestartRequired = false, DisplayName = "Auto-generate launch configuration file")) + UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (DisplayName = "Auto-generate launch configuration file")) bool bGenerateDefaultLaunchConfig; + /** Returns the Runtime version to use for cloud deployments, either the pinned one, or the user-specified one depending of the settings. */ + const FString& GetSpatialOSRuntimeVersionForCloud() const; + + /** Returns the Runtime version to use for local deployments, either the pinned one, or the user-specified one depending of the settings. */ + const FString& GetSpatialOSRuntimeVersionForLocal() const; + + /** Whether to use the GDK-associated SpatialOS runtime version, or to use the one specified in the RuntimeVersion field. */ + UPROPERTY(EditAnywhere, config, Category = "Runtime", meta = (DisplayName = "Use GDK pinned runtime version")) + bool bUseGDKPinnedRuntimeVersion; + + /** Runtime version to use for local deployments, if not using the GDK pinned version. */ + UPROPERTY(EditAnywhere, config, Category = "Runtime", meta = (EditCondition = "!bUseGDKPinnedRuntimeVersion")) + FString LocalRuntimeVersion; + + /** Runtime version to use for cloud deployments, if not using the GDK pinned version. */ + UPROPERTY(EditAnywhere, config, Category = "Runtime", meta = (EditCondition = "!bUseGDKPinnedRuntimeVersion")) + FString CloudRuntimeVersion; + private: + /** If you are not using auto-generate launch configuration file, specify a launch configuration `.json` file and location here. */ - UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (EditCondition = "!bGenerateDefaultLaunchConfig", ConfigRestartRequired = false, DisplayName = "Launch configuration file path")) + UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (EditCondition = "!bGenerateDefaultLaunchConfig", DisplayName = "Launch configuration file path")) FFilePath SpatialOSLaunchConfig; public: /** Expose the runtime on a particular IP address when it is running on this machine. Changes are applied on next local deployment startup. */ - UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (ConfigRestartRequired = false, DisplayName = "Expose local runtime")) + UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (DisplayName = "Expose local runtime")) bool bExposeRuntimeIP; /** If the runtime is set to be exposed, specify on which IP address it should be reachable. Changes are applied on next local deployment startup. */ - UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (EditCondition = "bExposeRuntimeIP", ConfigRestartRequired = false, DisplayName = "Exposed local runtime IP address")) + UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (EditCondition = "bExposeRuntimeIP", DisplayName = "Exposed local runtime IP address")) FString ExposedRuntimeIP; /** Select the check box to stop your game’s local deployment when you shut down Unreal Editor. */ - UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (ConfigRestartRequired = false, DisplayName = "Stop local deployment on exit")) + UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (DisplayName = "Stop local deployment on exit")) bool bStopSpatialOnExit; /** Start a local SpatialOS deployment when clicking 'Play'. */ - UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (ConfigRestartRequired = false, DisplayName = "Auto-start local deployment")) + UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (DisplayName = "Auto-start local deployment")) bool bAutoStartLocalDeployment; private: /** Name of your SpatialOS snapshot file that will be generated. */ - UPROPERTY(EditAnywhere, config, Category = "Snapshots", meta = (ConfigRestartRequired = false, DisplayName = "Snapshot to save")) + UPROPERTY(EditAnywhere, config, Category = "Snapshots", meta = (DisplayName = "Snapshot to save")) FString SpatialOSSnapshotToSave; /** Name of your SpatialOS snapshot file that will be loaded during deployment. */ - UPROPERTY(EditAnywhere, config, Category = "Snapshots", meta = (ConfigRestartRequired = false, DisplayName = "Snapshot to load")) + UPROPERTY(EditAnywhere, config, Category = "Snapshots", meta = (DisplayName = "Snapshot to load")) FString SpatialOSSnapshotToLoad; /** Add flags to the `spatial local launch` command; they alter the deployment’s behavior. Select the trash icon to remove all the flags.*/ - UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (ConfigRestartRequired = false, DisplayName = "Command line flags for local launch")) + UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (DisplayName = "Command line flags for local launch")) TArray SpatialOSCommandLineLaunchFlags; private: - UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (ConfigRestartRequired = false, DisplayName = "Assembly name")) + UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (DisplayName = "Assembly name")) FString AssemblyName; - UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (ConfigRestartRequired = false, DisplayName = "Deployment name")) + UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (DisplayName = "Deployment name")) FString PrimaryDeploymentName; - UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (ConfigRestartRequired = false, DisplayName = "Cloud launch configuration path")) + UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (DisplayName = "Cloud launch configuration path")) FFilePath PrimaryLaunchConfigPath; - UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (ConfigRestartRequired = false, DisplayName = "Snapshot path")) + UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (DisplayName = "Snapshot path")) FFilePath SnapshotPath; - UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (ConfigRestartRequired = false, DisplayName = "Region")) + UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (DisplayName = "Region")) TEnumAsByte PrimaryDeploymentRegionCode; const FString SimulatedPlayerLaunchConfigPath; - UPROPERTY(EditAnywhere, config, Category = "Simulated Players", meta = (EditCondition = "bSimulatedPlayersIsEnabled", ConfigRestartRequired = false, DisplayName = "Region")) +public: + /** If the Development Authentication Flow is used, the client will try to connect to the cloud rather than local deployment. */ + UPROPERTY(EditAnywhere, config, Category = "Cloud Connection") + bool bUseDevelopmentAuthenticationFlow; + + /** The token created using 'spatial project auth dev-auth-token' */ + UPROPERTY(EditAnywhere, config, Category = "Cloud Connection") + FString DevelopmentAuthenticationToken; + + /** The deployment to connect to when using the Development Authentication Flow. If left empty, it uses the first available one (order not guaranteed when there are multiple items). The deployment needs to be tagged with 'dev_login'. */ + UPROPERTY(EditAnywhere, config, Category = "Cloud Connection") + FString DevelopmentDeploymentToConnect; + +private: + UPROPERTY(EditAnywhere, config, Category = "Simulated Players", meta = (EditCondition = "bSimulatedPlayersIsEnabled", DisplayName = "Region")) TEnumAsByte SimulatedPlayerDeploymentRegionCode; - UPROPERTY(EditAnywhere, config, Category = "Simulated Players", meta = (ConfigRestartRequired = false, DisplayName = "Include simulated players")) + UPROPERTY(EditAnywhere, config, Category = "Simulated Players", meta = (DisplayName = "Include simulated players")) bool bSimulatedPlayersIsEnabled; - UPROPERTY(EditAnywhere, config, Category = "Simulated Players", meta = (EditCondition = "bSimulatedPlayersIsEnabled", ConfigRestartRequired = false, DisplayName = "Deployment name")) + UPROPERTY(EditAnywhere, config, Category = "Simulated Players", meta = (EditCondition = "bSimulatedPlayersIsEnabled", DisplayName = "Deployment name")) FString SimulatedPlayerDeploymentName; - UPROPERTY(EditAnywhere, config, Category = "Simulated Players", meta = (EditCondition = "bSimulatedPlayersIsEnabled", ConfigRestartRequired = false, DisplayName = "Number of simulated players")) + UPROPERTY(EditAnywhere, config, Category = "Simulated Players", meta = (EditCondition = "bSimulatedPlayersIsEnabled", DisplayName = "Number of simulated players")) uint32 NumberOfSimulatedPlayers; static bool IsAssemblyNameValid(const FString& Name); static bool IsProjectNameValid(const FString& Name); static bool IsDeploymentNameValid(const FString& Name); static bool IsRegionCodeValid(const ERegionCode::Type RegionCode); + static bool IsManualWorkerConnectionSet(const FString& LaunchConfigPath, TArray& OutWorkersManuallyLaunched); + +public: + UPROPERTY(EditAnywhere, config, Category = "Mobile", meta = (DisplayName = "Connect to a local deployment")) + bool bMobileConnectToLocalDeployment; + + UPROPERTY(EditAnywhere, config, Category = "Mobile", meta = (EditCondition = "bMobileConnectToLocalDeployment", DisplayName = "Runtime IP to local deployment")) + FString MobileRuntimeIP; + + UPROPERTY(EditAnywhere, config, Category = "Mobile", meta = (DisplayName = "Mobile Client Worker Type")) + FString MobileWorkerType = SpatialConstants::DefaultClientWorkerType.ToString(); + + UPROPERTY(EditAnywhere, config, Category = "Mobile", meta = (DisplayName = "Extra Command Line Arguments")) + FString MobileExtraCommandLineArgs; public: /** If you have selected **Auto-generate launch configuration file**, you can change the default options in the file from the drop-down menu. */ - UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (EditCondition = "bGenerateDefaultLaunchConfig", ConfigRestartRequired = false, DisplayName = "Launch configuration file options")) + UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (EditCondition = "bGenerateDefaultLaunchConfig", DisplayName = "Launch configuration file options")) FSpatialLaunchConfigDescription LaunchConfigDesc; FORCEINLINE FString GetSpatialOSLaunchConfig() const { return SpatialOSLaunchConfig.FilePath.IsEmpty() - ? FSpatialGDKServicesModule::GetSpatialOSDirectory(TEXT("default_launch.json")) + ? FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("default_launch.json")) : SpatialOSLaunchConfig.FilePath; } @@ -360,12 +416,17 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject FORCEINLINE FString GetSpatialOSSnapshotFolderPath() const { - return FPaths::ConvertRelativePathToFull(FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("snapshots"))); + return FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("snapshots")); } FORCEINLINE FString GetGeneratedSchemaOutputFolder() const { - return FPaths::ConvertRelativePathToFull(FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("schema/unreal/generated/"))); + return FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("schema/unreal/generated/")); + } + + FORCEINLINE FString GetBuiltWorkerFolder() const + { + return FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("build/assembly/worker/")); } FORCEINLINE FString GetSpatialOSCommandLineLaunchFlags() const @@ -394,12 +455,10 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject } void SetPrimaryLaunchConfigPath(const FString& Path); - FORCEINLINE FString GetPrimaryLanchConfigPath() const + FORCEINLINE FString GetPrimaryLaunchConfigPath() const { - const USpatialGDKEditorSettings* SpatialEditorSettings = GetDefault(); - return PrimaryLaunchConfigPath.FilePath.IsEmpty() - ? SpatialEditorSettings->GetSpatialOSLaunchConfig() - : PrimaryLaunchConfigPath.FilePath; + + return PrimaryLaunchConfigPath.FilePath; } void SetSnapshotPath(const FString& Path); @@ -443,6 +502,18 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject return bSimulatedPlayersIsEnabled; } + void SetUseGDKPinnedRuntimeVersion(bool IsEnabled); + FORCEINLINE bool GetUseGDKPinnedRuntimeVersion() const + { + return bUseGDKPinnedRuntimeVersion; + } + + void SetCustomCloudSpatialOSRuntimeVersion(const FString& Version); + FORCEINLINE const FString& GetCustomCloudSpatialOSRuntimeVersion() const + { + return CloudRuntimeVersion; + } + void SetSimulatedPlayerDeploymentName(const FString& Name); FORCEINLINE FString GetSimulatedPlayerDeploymentName() const { @@ -466,4 +537,6 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject } bool IsDeploymentConfigurationValid() const; + + void SetRuntimeDevelopmentAuthenticationToken(); }; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/LaunchConfigEditor.h b/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/LaunchConfigEditor.h new file mode 100644 index 0000000000..71e5e88f8a --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/LaunchConfigEditor.h @@ -0,0 +1,20 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialGDKEditorSettings.h" +#include "Utils/TransientUObjectEditor.h" + +#include "LaunchConfigEditor.generated.h" + +UCLASS() +class SPATIALGDKEDITOR_API ULaunchConfigurationEditor : public UTransientUObjectEditor +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, Category = "Launch Configuration") + FSpatialLaunchConfigDescription LaunchConfiguration; + + UFUNCTION(Exec) + void SaveConfiguration(); +}; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/TransientUObjectEditor.h b/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/TransientUObjectEditor.h new file mode 100644 index 0000000000..1b8a1055f8 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/TransientUObjectEditor.h @@ -0,0 +1,25 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" + +#include "TransientUObjectEditor.generated.h" + +// Utility class to create Editor tools exposing a UObject Field and automatically adding Exec UFUNCTION as buttons. +UCLASS(Blueprintable, Abstract) +class SPATIALGDKEDITOR_API UTransientUObjectEditor : public UObject +{ + GENERATED_BODY() +public: + + template + static void LaunchTransientUObjectEditor(const FString& EditorName) + { + LaunchTransientUObjectEditor(EditorName, T::StaticClass()); + } + +private: + static void LaunchTransientUObjectEditor(const FString& EditorName, UClass* ObjectClass); +}; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/WorkerTypeCustomization.h b/SpatialGDK/Source/SpatialGDKEditor/Public/WorkerTypeCustomization.h index 062f6457ec..06b4baea1e 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/WorkerTypeCustomization.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/WorkerTypeCustomization.h @@ -1,8 +1,9 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + #pragma once #include "CoreMinimal.h" #include "IPropertyTypeCustomization.h" -#include "Utils/ActorGroupManager.h" class FWorkerTypeCustomization : public IPropertyTypeCustomization { diff --git a/SpatialGDK/Source/SpatialGDKEditor/SpatialGDKEditor.Build.cs b/SpatialGDK/Source/SpatialGDKEditor/SpatialGDKEditor.Build.cs index 0adefffc8e..d908fd08d0 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/SpatialGDKEditor.Build.cs +++ b/SpatialGDK/Source/SpatialGDKEditor/SpatialGDKEditor.Build.cs @@ -6,25 +6,33 @@ public class SpatialGDKEditor : ModuleRules { public SpatialGDKEditor(ReadOnlyTargetRules Target) : base(Target) { + bLegacyPublicIncludePaths = false; PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; - bFasterWithoutUnity = true; +#pragma warning disable 0618 + bFasterWithoutUnity = true; // Deprecated in 4.24, replace with bUseUnity = false; once we drop support for 4.23 + if (Target.Version.MinorVersion == 24) // Due to a bug in 4.24, bFasterWithoutUnity is inversed, fixed in master, so should hopefully roll into the next release, remove this once it does + { + bFasterWithoutUnity = false; + } +#pragma warning restore 0618 - PrivateDependencyModuleNames.AddRange( + PrivateDependencyModuleNames.AddRange( new string[] { "Core", "CoreUObject", "EditorStyle", "Engine", "EngineSettings", - "Json", + "IOSRuntimeSettings", + "Json", "PropertyEditor", "Slate", "SlateCore", "SpatialGDK", "SpatialGDKServices", "UnrealEd", - "GameplayAbilities" - }); + "DesktopPlatform" + }); PrivateIncludePaths.AddRange( new string[] diff --git a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/CookAndGenerateSchemaCommandlet.cpp b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/CookAndGenerateSchemaCommandlet.cpp index bfd2ad171b..d5cf86737e 100644 --- a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/CookAndGenerateSchemaCommandlet.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/CookAndGenerateSchemaCommandlet.cpp @@ -67,10 +67,15 @@ UCookAndGenerateSchemaCommandlet::UCookAndGenerateSchemaCommandlet() int32 UCookAndGenerateSchemaCommandlet::Main(const FString& CmdLineParams) { + UE_LOG(LogCookAndGenerateSchemaCommandlet, Display, TEXT("Cook and Generate Schema Started.")); + + TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, GIsRunningUnattendedScript || IsRunningCommandlet()); + +#if ENGINE_MINOR_VERSION <= 22 // Force spatial networking - GetMutableDefault()->bSpatialNetworking = true; + GetMutableDefault()->SetUsesSpatialNetworking(true); +#endif - UE_LOG(LogCookAndGenerateSchemaCommandlet, Display, TEXT("Cook and Generate Schema Started.")); FObjectListener ObjectListener; TSet ReferencedClasses; ObjectListener.StartListening(&ReferencedClasses); @@ -138,21 +143,23 @@ int32 UCookAndGenerateSchemaCommandlet::Main(const FString& CmdLineParams) SpatialGDKGenerateSchemaForClasses(Classes); GenerateSchemaForSublevels(); + GenerateSchemaForRPCEndpoints(); + GenerateSchemaForNCDs(); FTimespan Duration = FDateTime::Now() - StartTime; UE_LOG(LogCookAndGenerateSchemaCommandlet, Display, TEXT("Schema Generation Finished in %.2f seconds"), Duration.GetTotalSeconds()); - - if (!SaveSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_ASSET_PATH)) - { - UE_LOG(LogCookAndGenerateSchemaCommandlet, Error, TEXT("Failed to save schema database.")); - return 0; - } if (!RunSchemaCompiler()) { UE_LOG(LogCookAndGenerateSchemaCommandlet, Error, TEXT("Failed to run schema compiler.")); return 0; + } + + if (!SaveSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_ASSET_PATH)) + { + UE_LOG(LogCookAndGenerateSchemaCommandlet, Error, TEXT("Failed to save schema database.")); + return 0; } return CookResult; diff --git a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaAndSnapshotsCommandlet.cpp b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaAndSnapshotsCommandlet.cpp index a11f58fac9..a46cd819a9 100644 --- a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaAndSnapshotsCommandlet.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaAndSnapshotsCommandlet.cpp @@ -25,6 +25,8 @@ int32 UGenerateSchemaAndSnapshotsCommandlet::Main(const FString& Args) { UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Schema & Snapshot Generation Commandlet Started")); + TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, GIsRunningUnattendedScript || IsRunningCommandlet()); + TArray Tokens; TArray Switches; TMap Params; diff --git a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaCommandlet.cpp b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaCommandlet.cpp index 7f2c59c21c..64e80777c6 100644 --- a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaCommandlet.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaCommandlet.cpp @@ -37,6 +37,8 @@ int32 UGenerateSchemaCommandlet::Main(const FString& Args) { UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Schema Generation Commandlet Started")); + TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, GIsRunningUnattendedScript || IsRunningCommandlet()); + TArray Tokens; TArray Switches; TMap Params; diff --git a/SpatialGDK/Source/SpatialGDKEditorCommandlet/SpatialGDKEditorCommandlet.Build.cs b/SpatialGDK/Source/SpatialGDKEditorCommandlet/SpatialGDKEditorCommandlet.Build.cs index 641e2508d0..e64708a4a4 100644 --- a/SpatialGDK/Source/SpatialGDKEditorCommandlet/SpatialGDKEditorCommandlet.Build.cs +++ b/SpatialGDK/Source/SpatialGDKEditorCommandlet/SpatialGDKEditorCommandlet.Build.cs @@ -6,8 +6,15 @@ public class SpatialGDKEditorCommandlet : ModuleRules { public SpatialGDKEditorCommandlet(ReadOnlyTargetRules Target) : base(Target) { + bLegacyPublicIncludePaths = false; PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; - bFasterWithoutUnity = true; +#pragma warning disable 0618 + bFasterWithoutUnity = true; // Deprecated in 4.24, replace with bUseUnity = false; once we drop support for 4.23 + if (Target.Version.MinorVersion == 24) // Due to a bug in 4.24, bFasterWithoutUnity is inversed, fixed in master, so should hopefully roll into the next release, remove this once it does + { + bFasterWithoutUnity = false; + } +#pragma warning restore 0618 PrivateDependencyModuleNames.AddRange( new string[] { diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp index df14b5e1b3..7a1de185be 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp @@ -9,6 +9,9 @@ #include "Framework/Application/SlateApplication.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Framework/Notifications/NotificationManager.h" +#include "HAL/PlatformFilemanager.h" +#include "Interfaces/IProjectManager.h" +#include "IOSRuntimeSettings.h" #include "ISettingsContainer.h" #include "ISettingsModule.h" #include "ISettingsSection.h" @@ -26,9 +29,11 @@ #include "SpatialGDKEditor.h" #include "SpatialGDKEditorSchemaGenerator.h" #include "SpatialGDKEditorSettings.h" +#include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesModule.h" #include "SpatialGDKSettings.h" #include "SpatialGDKSimulatedPlayerDeployment.h" +#include "Utils/LaunchConfigEditor.h" #include "Editor/EditorEngine.h" #include "HAL/FileManager.h" @@ -38,6 +43,10 @@ #include "GeneralProjectSettings.h" #include "LevelEditor.h" #include "Misc/FileHelper.h" +#include "EngineClasses/SpatialWorldSettings.h" +#include "EditorExtension/LBStrategyEditorExtension.h" +#include "LoadBalancing/AbstractLBStrategy.h" +#include "SpatialGDKEditorModule.h" DEFINE_LOG_CATEGORY(LogSpatialGDKEditorToolbar); @@ -45,6 +54,7 @@ DEFINE_LOG_CATEGORY(LogSpatialGDKEditorToolbar); FSpatialGDKEditorToolbarModule::FSpatialGDKEditorToolbarModule() : bStopSpatialOnExit(false) +, bSchemaBuildError(false) { } @@ -88,6 +98,25 @@ void FSpatialGDKEditorToolbarModule::StartupModule() }); } + FEditorDelegates::PreBeginPIE.AddLambda([this](bool bIsSimulatingInEditor) + { + if (GIsAutomationTesting && GetDefault()->UsesSpatialNetworking()) + { + LocalDeploymentManager->IsServiceRunningAndInCorrectDirectory(); + LocalDeploymentManager->GetLocalDeploymentStatus(); + + VerifyAndStartDeployment(); + } + }); + + FEditorDelegates::EndPIE.AddLambda([this](bool bIsSimulatingInEditor) + { + if (GIsAutomationTesting && GetDefault()->UsesSpatialNetworking()) + { + LocalDeploymentManager->TryStopLocalDeployment(); + } + }); + LocalDeploymentManager->Init(GetOptionalExposedRuntimeIP()); } @@ -192,6 +221,11 @@ void FSpatialGDKEditorToolbarModule::MapActions(TSharedPtr FSpatialGDKEditorToolbarCommands::Get().OpenSimulatedPlayerConfigurationWindowAction, FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::ShowSimulatedPlayerDeploymentDialog), FCanExecuteAction()); + + InPluginCommands->MapAction( + FSpatialGDKEditorToolbarCommands::Get().OpenLaunchConfigurationEditorAction, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::OpenLaunchConfigurationEditor), + FCanExecuteAction()); InPluginCommands->MapAction( FSpatialGDKEditorToolbarCommands::Get().StartSpatialService, @@ -241,7 +275,9 @@ void FSpatialGDKEditorToolbarModule::AddMenuExtension(FMenuBuilder& Builder) Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StartSpatialDeployment); Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StopSpatialDeployment); Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().LaunchInspectorWebPageAction); +#if PLATFORM_WINDOWS Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().OpenSimulatedPlayerConfigurationWindowAction); +#endif Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StartSpatialService); Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StopSpatialService); } @@ -264,14 +300,24 @@ void FSpatialGDKEditorToolbarModule::AddToolbarExtension(FToolBarBuilder& Builde Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StartSpatialDeployment); Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StopSpatialDeployment); Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().LaunchInspectorWebPageAction); +#if PLATFORM_WINDOWS Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().OpenSimulatedPlayerConfigurationWindowAction); + Builder.AddComboButton( + FUIAction(), + FOnGetContent::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CreateLaunchDeploymentMenuContent), + LOCTEXT("GDKDeploymentCombo_Label", "Deployment Tools"), + TAttribute(), + FSlateIcon(FEditorStyle::GetStyleSetName(), "GDK.Cloud"), + true + ); +#endif Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StartSpatialService); Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StopSpatialService); } TSharedRef FSpatialGDKEditorToolbarModule::CreateGenerateSchemaMenuContent() { - FMenuBuilder MenuBuilder(true, PluginCommands); + FMenuBuilder MenuBuilder(true /*bInShouldCloseWindowAfterMenuSelection*/, PluginCommands); MenuBuilder.BeginSection(NAME_None, LOCTEXT("GDKSchemaOptionsHeader", "Schema Generation")); { MenuBuilder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSchemaFull); @@ -282,6 +328,18 @@ TSharedRef FSpatialGDKEditorToolbarModule::CreateGenerateSchemaMenuCont return MenuBuilder.MakeWidget(); } +TSharedRef FSpatialGDKEditorToolbarModule::CreateLaunchDeploymentMenuContent() +{ + FMenuBuilder MenuBuilder(true /*bInShouldCloseWindowAfterMenuSelection*/, PluginCommands); + MenuBuilder.BeginSection(NAME_None, LOCTEXT("GDKDeploymentOptionsHeader", "Deployment Tools")); + { + MenuBuilder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().OpenLaunchConfigurationEditorAction); + } + MenuBuilder.EndSection(); + + return MenuBuilder.MakeWidget(); +} + void FSpatialGDKEditorToolbarModule::CreateSnapshotButtonClicked() { OnShowTaskStartNotification("Started snapshot generation"); @@ -418,90 +476,6 @@ void FSpatialGDKEditorToolbarModule::ShowFailedNotification(const FString& Notif } } -bool FSpatialGDKEditorToolbarModule::ValidateGeneratedLaunchConfig() const -{ - const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); - const USpatialGDKSettings* SpatialGDKRuntimeSettings = GetDefault(); - const FSpatialLaunchConfigDescription& LaunchConfigDescription = SpatialGDKEditorSettings->LaunchConfigDesc; - - if (const FString* EnableChunkInterest = LaunchConfigDescription.World.LegacyFlags.Find(TEXT("enable_chunk_interest"))) - { - if (*EnableChunkInterest == TEXT("true")) - { - const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(TEXT("The legacy flag \"enable_chunk_interest\" is set to true in the generated launch configuration. Chunk interest is not supported and this flag needs to be set to false.\n\nDo you want to configure your launch config settings now?"))); - - if (Result == EAppReturnType::Yes) - { - FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Editor Settings"); - } - - return false; - } - } - - if (!SpatialGDKRuntimeSettings->bEnableHandover && SpatialGDKEditorSettings->LaunchConfigDesc.ServerWorkers.ContainsByPredicate([](const FWorkerTypeLaunchSection& Section) - { - return (Section.Rows * Section.Columns) > 1; - })) - { - const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(TEXT("Property handover is disabled and a zoned deployment is specified.\nThis is not supported.\n\nDo you want to configure your project settings now?"))); - - if (Result == EAppReturnType::Yes) - { - FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Runtime Settings"); - } - - return false; - } - - if (SpatialGDKEditorSettings->LaunchConfigDesc.ServerWorkers.ContainsByPredicate([](const FWorkerTypeLaunchSection& Section) - { - return (Section.Rows * Section.Columns) < Section.NumEditorInstances; - })) - { - const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(TEXT("Attempting to launch too many servers for load balance configuration.\nThis is not supported.\n\nDo you want to configure your project settings now?"))); - - if (Result == EAppReturnType::Yes) - { - FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Editor Settings"); - } - - return false; - } - - if (!SpatialGDKRuntimeSettings->ServerWorkerTypes.Contains(SpatialGDKRuntimeSettings->DefaultWorkerType.WorkerTypeName)) - { - const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(TEXT("Default Worker Type is invalid, please choose a valid worker type as the default.\n\nDo you want to configure your project settings now?"))); - - if (Result == EAppReturnType::Yes) - { - FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Runtime Settings"); - } - - return false; - } - - if (SpatialGDKRuntimeSettings->bEnableOffloading) - { - for (const TPair& ActorGroup : SpatialGDKRuntimeSettings->ActorGroups) - { - if (!SpatialGDKRuntimeSettings->ServerWorkerTypes.Contains(ActorGroup.Value.OwningWorkerType.WorkerTypeName)) - { - const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(FString::Printf(TEXT("Actor Group '%s' has an invalid Owning Worker Type, please choose a valid worker type.\n\nDo you want to configure your project settings now?"), *ActorGroup.Key.ToString()))); - - if (Result == EAppReturnType::Yes) - { - FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Runtime Settings"); - } - - return false; - } - } - } - - return true; -} - void FSpatialGDKEditorToolbarModule::StartSpatialServiceButtonClicked() { AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] @@ -547,10 +521,37 @@ void FSpatialGDKEditorToolbarModule::StopSpatialServiceButtonClicked() }); } +bool FSpatialGDKEditorToolbarModule::FillWorkerLaunchConfigFromWorldSettings(UWorld& World, FWorkerTypeLaunchSection& OutLaunchConfig, FIntPoint& OutWorldDimension) +{ + const ASpatialWorldSettings* WorldSettings = Cast(World.GetWorldSettings()); + + if (!WorldSettings) + { + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Missing SpatialWorldSettings on map %s"), *World.GetMapName()); + return false; + } + + if (!WorldSettings->LoadBalanceStrategy) + { + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Missing Load balancing strategy on map %s"), *World.GetMapName()); + return false; + } + + FSpatialGDKEditorModule& EditorModule = FModuleManager::GetModuleChecked("SpatialGDKEditor"); + + if (!EditorModule.GetLBStrategyExtensionManager().GetDefaultLaunchConfiguration(WorldSettings->LoadBalanceStrategy->GetDefaultObject(), OutLaunchConfig, OutWorldDimension)) + { + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Could not get the number of worker to launch for load balancing strategy %s"), *WorldSettings->LoadBalanceStrategy->GetName()); + return false; + } + + return true; +} + void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment() { // Don't try and start a local deployment if spatial networking is disabled. - if (!GetDefault()->bSpatialNetworking) + if (!GetDefault()->UsesSpatialNetworking()) { UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Attempted to start a local deployment but spatial networking is disabled.")); return; @@ -568,17 +569,24 @@ void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment() return; } - // Get the latest launch config. - const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); - - FString LaunchConfig; - if (SpatialGDKSettings->bGenerateDefaultLaunchConfig) + if (bSchemaBuildError) { - if (!ValidateGeneratedLaunchConfig()) + UE_LOG(LogSpatialGDKEditorToolbar, Warning, TEXT("Schema did not previously compile correctly, you may be running a stale build.")); + + EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString("Last schema generation failed or failed to run the schema compiler. Schema will most likely be out of date, which may lead to undefined behavior. Are you sure you want to continue?")); + if (Result == EAppReturnType::No) { return; } + } + + // Get the latest launch config. + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); + FString LaunchConfig; + if (SpatialGDKEditorSettings->bGenerateDefaultLaunchConfig) + { bool bRedeployRequired = false; if (!GenerateAllDefaultWorkerJsons(bRedeployRequired)) { @@ -589,22 +597,64 @@ void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment() LocalDeploymentManager->SetRedeployRequired(); } - LaunchConfig = FPaths::Combine(FPaths::ConvertRelativePathToFull(FPaths::ProjectIntermediateDir()), TEXT("Improbable/DefaultLaunchConfig.json")); - if (const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault()) + UWorld* EditorWorld = GEditor->GetEditorWorldContext().World(); + check(EditorWorld); + + LaunchConfig = FPaths::Combine(FPaths::ConvertRelativePathToFull(FPaths::ProjectIntermediateDir()), FString::Printf(TEXT("Improbable/%s_LocalLaunchConfig.json"), *EditorWorld->GetMapName())); + + FSpatialLaunchConfigDescription LaunchConfigDescription = SpatialGDKEditorSettings->LaunchConfigDesc; + if (SpatialGDKSettings->bEnableUnrealLoadBalancer) + { + FIntPoint WorldDimensions; + FWorkerTypeLaunchSection WorkerLaunch; + + if (FillWorkerLaunchConfigFromWorldSettings(*EditorWorld, WorkerLaunch, WorldDimensions)) + { + LaunchConfigDescription.World.Dimensions = WorldDimensions; + LaunchConfigDescription.ServerWorkers.Empty(SpatialGDKSettings->ServerWorkerTypes.Num()); + + for (auto WorkerType : SpatialGDKSettings->ServerWorkerTypes) + { + LaunchConfigDescription.ServerWorkers.Add(WorkerLaunch); + LaunchConfigDescription.ServerWorkers.Last().WorkerTypeName = WorkerType; + } + } + } + + for (auto& WorkerLaunchSection : LaunchConfigDescription.ServerWorkers) { - const FSpatialLaunchConfigDescription& LaunchConfigDescription = SpatialGDKEditorSettings->LaunchConfigDesc; - GenerateDefaultLaunchConfig(LaunchConfig, &LaunchConfigDescription); + WorkerLaunchSection.bManualWorkerConnectionOnly = true; + } + + if (!ValidateGeneratedLaunchConfig(LaunchConfigDescription)) + { + return; + } + + GenerateDefaultLaunchConfig(LaunchConfig, &LaunchConfigDescription); + LaunchConfigDescription.SetLevelEditorPlaySettingsWorkerTypes(); + + // Also create default launch config for cloud deployments. + { + for (auto& WorkerLaunchSection : LaunchConfigDescription.ServerWorkers) + { + WorkerLaunchSection.bManualWorkerConnectionOnly = false; + } + + FString CloudLaunchConfig = FPaths::Combine(FPaths::ConvertRelativePathToFull(FPaths::ProjectIntermediateDir()), FString::Printf(TEXT("Improbable/%s_CloudLaunchConfig.json"), *EditorWorld->GetMapName())); + GenerateDefaultLaunchConfig(CloudLaunchConfig, &LaunchConfigDescription); } } else { - LaunchConfig = SpatialGDKSettings->GetSpatialOSLaunchConfig(); + LaunchConfig = SpatialGDKEditorSettings->GetSpatialOSLaunchConfig(); } - const FString LaunchFlags = SpatialGDKSettings->GetSpatialOSCommandLineLaunchFlags(); - const FString SnapshotName = SpatialGDKSettings->GetSpatialOSSnapshotToLoad(); + const FString LaunchFlags = SpatialGDKEditorSettings->GetSpatialOSCommandLineLaunchFlags(); + const FString SnapshotName = SpatialGDKEditorSettings->GetSpatialOSSnapshotToLoad(); + const FString RuntimeVersion = SpatialGDKEditorSettings->GetSpatialOSRuntimeVersionForLocal(); - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, LaunchConfig, LaunchFlags, SnapshotName] + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, LaunchConfig, LaunchFlags, SnapshotName, RuntimeVersion] { // If the last local deployment is still stopping then wait until it's finished. while (LocalDeploymentManager->IsDeploymentStopping()) @@ -638,7 +688,7 @@ void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment() }; OnShowTaskStartNotification(TEXT("Starting local deployment...")); - LocalDeploymentManager->TryStartLocalDeployment(LaunchConfig, LaunchFlags, SnapshotName, GetOptionalExposedRuntimeIP(), CallBack); + LocalDeploymentManager->TryStartLocalDeployment(LaunchConfig, RuntimeVersion, LaunchFlags, SnapshotName, GetOptionalExposedRuntimeIP(), CallBack); }); } @@ -692,7 +742,7 @@ bool FSpatialGDKEditorToolbarModule::StartSpatialDeploymentIsVisible() const bool FSpatialGDKEditorToolbarModule::StartSpatialDeploymentCanExecute() const { - return !LocalDeploymentManager->IsDeploymentStarting() && GetDefault()->bSpatialNetworking; + return !LocalDeploymentManager->IsDeploymentStarting() && GetDefault()->UsesSpatialNetworking(); } bool FSpatialGDKEditorToolbarModule::StopSpatialDeploymentIsVisible() const @@ -791,10 +841,17 @@ void FSpatialGDKEditorToolbarModule::ShowSimulatedPlayerDeploymentDialog() FSlateApplication::Get().AddWindow(SimulatedPlayerDeploymentWindowPtr.ToSharedRef()); } +void FSpatialGDKEditorToolbarModule::OpenLaunchConfigurationEditor() +{ + ULaunchConfigurationEditor::LaunchTransientUObjectEditor(TEXT("Launch Configuration Editor")); +} + void FSpatialGDKEditorToolbarModule::GenerateSchema(bool bFullScan) { LocalDeploymentManager->SetRedeployRequired(); + bSchemaBuildError = false; + if (SpatialGDKEditorInstance->FullScanRequired()) { OnShowTaskStartNotification("Initial Schema Generation"); @@ -806,6 +863,7 @@ void FSpatialGDKEditorToolbarModule::GenerateSchema(bool bFullScan) else { OnShowFailedNotification("Initial Schema Generation failed"); + bSchemaBuildError = true; } } else if (bFullScan) @@ -819,6 +877,7 @@ void FSpatialGDKEditorToolbarModule::GenerateSchema(bool bFullScan) else { OnShowFailedNotification("Full Schema Generation failed"); + bSchemaBuildError = true; } } else @@ -832,6 +891,7 @@ void FSpatialGDKEditorToolbarModule::GenerateSchema(bool bFullScan) else { OnShowFailedNotification("Incremental Schema Generation failed"); + bSchemaBuildError = true; } } } @@ -844,14 +904,26 @@ bool FSpatialGDKEditorToolbarModule::IsSnapshotGenerated() const bool FSpatialGDKEditorToolbarModule::IsSchemaGenerated() const { - FString DescriptorPath = FSpatialGDKServicesModule::GetSpatialOSDirectory(TEXT("build/assembly/schema/schema.descriptor")); - FString GdkFolderPath = FSpatialGDKServicesModule::GetSpatialOSDirectory(TEXT("schema/unreal/gdk")); + FString DescriptorPath = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("build/assembly/schema/schema.descriptor")); + FString GdkFolderPath = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("schema/unreal/gdk")); return FPaths::FileExists(DescriptorPath) && FPaths::DirectoryExists(GdkFolderPath) && SpatialGDKEditor::Schema::GeneratedSchemaDatabaseExists(); } FString FSpatialGDKEditorToolbarModule::GetOptionalExposedRuntimeIP() const { + const UGeneralProjectSettings* GeneralProjectSettings = GetDefault(); const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); + if (GeneralProjectSettings->bEnableSpatialLocalLauncher) + { + if (SpatialGDKEditorSettings->bExposeRuntimeIP && GeneralProjectSettings->SpatialLocalDeploymentRuntimeIP != SpatialGDKEditorSettings->ExposedRuntimeIP) + { + UE_LOG(LogSpatialGDKEditorToolbar, Warning, TEXT("Local runtime IP specified from both general settings and Spatial settings! " + "Using IP specified in general settings: %s (Spatial settings has \"%s\")"), + *GeneralProjectSettings->SpatialLocalDeploymentRuntimeIP, *SpatialGDKEditorSettings->ExposedRuntimeIP); + } + return GeneralProjectSettings->SpatialLocalDeploymentRuntimeIP; + } + if (SpatialGDKEditorSettings->bExposeRuntimeIP) { return SpatialGDKEditorSettings->ExposedRuntimeIP; diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp index 4a52d985a7..cb1a3b0605 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp @@ -14,6 +14,7 @@ void FSpatialGDKEditorToolbarCommands::RegisterCommands() UI_COMMAND(StopSpatialDeployment, "Stop", "Stops SpatialOS.", EUserInterfaceActionType::Button, FInputGesture()); UI_COMMAND(LaunchInspectorWebPageAction, "Inspector", "Launches default web browser to SpatialOS Inspector.", EUserInterfaceActionType::Button, FInputGesture()); UI_COMMAND(OpenSimulatedPlayerConfigurationWindowAction, "Deploy", "Opens a configuration menu for cloud deployments.", EUserInterfaceActionType::Button, FInputGesture()); + UI_COMMAND(OpenLaunchConfigurationEditorAction, "Create Launch Configuration", "Opens an editor to create SpatialOS Launch configurations", EUserInterfaceActionType::Button, FInputGesture()); UI_COMMAND(StartSpatialService, "Start Service", "Starts the Spatial service daemon.", EUserInterfaceActionType::Button, FInputGesture()); UI_COMMAND(StopSpatialService, "Stop Service", "Stops the Spatial service daemon.", EUserInterfaceActionType::Button, FInputGesture()); } diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKSimulatedPlayerDeployment.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKSimulatedPlayerDeployment.cpp index 7d7a03ed9c..41b67946a9 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKSimulatedPlayerDeployment.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKSimulatedPlayerDeployment.cpp @@ -9,11 +9,14 @@ #include "Framework/Application/SlateApplication.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Framework/Notifications/NotificationManager.h" +#include "HAL/PlatformFilemanager.h" +#include "Misc/MessageDialog.h" #include "Runtime/Launch/Resources/Version.h" #include "SpatialCommandUtils.h" #include "SpatialGDKSettings.h" #include "SpatialGDKEditorSettings.h" #include "SpatialGDKEditorToolbar.h" +#include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesModule.h" #include "Templates/SharedPointer.h" #include "Textures/SlateIcon.h" @@ -146,6 +149,49 @@ void SSpatialGDKSimulatedPlayerDeployment::Construct(const FArguments& InArgs) .OnTextChanged(this, &SSpatialGDKSimulatedPlayerDeployment::OnDeploymentAssemblyCommited, ETextCommit::Default) ] ] + // RuntimeVersion + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(FString(TEXT("Use GDK Pinned Version")))) + .ToolTipText(FText::FromString(FString(TEXT("Whether to use the SpatialOS Runtime version associated to the current GDK version")))) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SCheckBox) + .IsChecked(this, &SSpatialGDKSimulatedPlayerDeployment::IsUsingGDKPinnedRuntimeVersion) + .OnCheckStateChanged(this, &SSpatialGDKSimulatedPlayerDeployment::OnCheckedUsePinnedVersion) + ] + ] + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(FString(TEXT("Runtime Version")))) + .ToolTipText(FText::FromString(FString(TEXT("User supplied version of the SpatialOS runtime to use")))) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SEditableTextBox) + .Text(this, &SSpatialGDKSimulatedPlayerDeployment::GetSpatialOSRuntimeVersionToUseText) + .OnTextCommitted(this, &SSpatialGDKSimulatedPlayerDeployment::OnRuntimeCustomVersionCommited) + .OnTextChanged(this, &SSpatialGDKSimulatedPlayerDeployment::OnRuntimeCustomVersionCommited, ETextCommit::Default) + .IsEnabled(this, &SSpatialGDKSimulatedPlayerDeployment::IsUsingCustomRuntimeVersion) + ] + ] // Pirmary Deployment Name + SVerticalBox::Slot() .AutoHeight() @@ -216,9 +262,9 @@ void SSpatialGDKSimulatedPlayerDeployment::Construct(const FArguments& InArgs) .BrowseButtonImage(FEditorStyle::GetBrush("PropertyWindow.Button_Ellipsis")) .BrowseButtonStyle(FEditorStyle::Get(), "HoverHintOnly") .BrowseButtonToolTip(FText::FromString(FString(TEXT("Path to the launch configuration file.")))) - .BrowseDirectory(FSpatialGDKServicesModule::GetSpatialOSDirectory()) + .BrowseDirectory(SpatialGDKServicesConstants::SpatialOSDirectory) .BrowseTitle(FText::FromString(FString(TEXT("File picker...")))) - .FilePath_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::GetPrimaryLanchConfigPath) + .FilePath_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::GetPrimaryLaunchConfigPath) .FileTypeFilter(TEXT("Launch configuration files (*.json)|*.json")) .OnPathPicked(this, &SSpatialGDKSimulatedPlayerDeployment::OnPrimaryLaunchConfigPathPicked) ] @@ -419,6 +465,18 @@ void SSpatialGDKSimulatedPlayerDeployment::OnPrimaryDeploymentNameCommited(const SpatialGDKSettings->SetPrimaryDeploymentName(InText.ToString()); } +void SSpatialGDKSimulatedPlayerDeployment::OnCheckedUsePinnedVersion(ECheckBoxState NewCheckedState) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetUseGDKPinnedRuntimeVersion(NewCheckedState == ECheckBoxState::Checked); +} + +void SSpatialGDKSimulatedPlayerDeployment::OnRuntimeCustomVersionCommited(const FText& InText, ETextCommit::Type InCommitType) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetCustomCloudSpatialOSRuntimeVersion(InText.ToString()); +} + void SSpatialGDKSimulatedPlayerDeployment::OnSnapshotPathPicked(const FString& PickedPath) { USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); @@ -508,6 +566,20 @@ FReply SSpatialGDKSimulatedPlayerDeployment::OnLaunchClicked() return FReply::Handled(); } + if (SpatialGDKSettings->IsSimulatedPlayersEnabled()) + { + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + FString BuiltWorkerFolder = GetDefault()->GetBuiltWorkerFolder(); + FString BuiltSimPlayersName = TEXT("UnrealSimulatedPlayer@Linux.zip"); + FString BuiltSimPlayerPath = FPaths::Combine(BuiltWorkerFolder, BuiltSimPlayersName); + + if (!PlatformFile.FileExists(*BuiltSimPlayerPath)) + { + FString MissingSimPlayerBuildText = FString::Printf(TEXT("Warning: Detected that %s is missing. To launch a successful SimPlayer deployment ensure that SimPlayers is built and uploaded."), *BuiltSimPlayersName); + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(MissingSimPlayerBuildText)); + } + } + if (ToolbarPtr) { ToolbarPtr->OnShowTaskStartNotification(TEXT("Starting cloud deployment...")); @@ -518,21 +590,22 @@ FReply SSpatialGDKSimulatedPlayerDeployment::OnLaunchClicked() if (TSharedPtr SpatialGDKEditorSharedPtr = SpatialGDKEditorPtr.Pin()) { SpatialGDKEditorSharedPtr->LaunchCloudDeployment( - FSimpleDelegate::CreateLambda([]() - { - if (FSpatialGDKEditorToolbarModule* ToolbarPtr = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) + FSimpleDelegate::CreateLambda([]() { - ToolbarPtr->OnShowSuccessNotification("Successfully launched cloud deployment."); - } - }), + if (FSpatialGDKEditorToolbarModule* ToolbarPtr = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) + { + ToolbarPtr->OnShowSuccessNotification("Successfully launched cloud deployment."); + } + }), - FSimpleDelegate::CreateLambda([]() - { - if (FSpatialGDKEditorToolbarModule* ToolbarPtr = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) + FSimpleDelegate::CreateLambda([]() { - ToolbarPtr->OnShowFailedNotification("Failed to launch cloud deployment. See output logs for details."); - } - })); + if (FSpatialGDKEditorToolbarModule* ToolbarPtr = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) + { + ToolbarPtr->OnShowFailedNotification("Failed to launch cloud deployment. See output logs for details."); + } + }) + ); return; } @@ -631,3 +704,22 @@ bool SSpatialGDKSimulatedPlayerDeployment::IsDeploymentConfigurationValid() cons { return true; } + +ECheckBoxState SSpatialGDKSimulatedPlayerDeployment::IsUsingGDKPinnedRuntimeVersion() const +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + return SpatialGDKSettings->GetUseGDKPinnedRuntimeVersion() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; +} + +bool SSpatialGDKSimulatedPlayerDeployment::IsUsingCustomRuntimeVersion() const +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + return !SpatialGDKSettings->GetUseGDKPinnedRuntimeVersion(); +} + +FText SSpatialGDKSimulatedPlayerDeployment::GetSpatialOSRuntimeVersionToUseText() const +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + const FString& RuntimeVersion = SpatialGDKSettings->bUseGDKPinnedRuntimeVersion ? SpatialGDKServicesConstants::SpatialOSRuntimePinnedVersion : SpatialGDKSettings->CloudRuntimeVersion; + return FText::FromString(RuntimeVersion); +} diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h index e7e4519930..44e7461fae 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h @@ -1,4 +1,5 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved + #pragma once #include "Async/Future.h" @@ -82,12 +83,14 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable void OnPropertyChanged(UObject* ObjectBeingModified, FPropertyChangedEvent& PropertyChangedEvent); void ShowSimulatedPlayerDeploymentDialog(); + void OpenLaunchConfigurationEditor(); private: bool CanExecuteSchemaGenerator() const; bool CanExecuteSnapshotGenerator() const; TSharedRef CreateGenerateSchemaMenuContent(); + TSharedRef CreateLaunchDeploymentMenuContent(); void ShowTaskStartNotification(const FString& NotificationText); @@ -95,7 +98,7 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable void ShowFailedNotification(const FString& NotificationText); - bool ValidateGeneratedLaunchConfig() const; + bool FillWorkerLaunchConfigFromWorldSettings(UWorld& World, FWorkerTypeLaunchSection& OutLaunchConfig, FIntPoint& OutWorldDimension); void GenerateSchema(bool bFullScan); @@ -110,6 +113,8 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable FDelegateHandle OnPropertyChangedDelegateHandle; bool bStopSpatialOnExit; + bool bSchemaBuildError; + TWeakPtr TaskNotificationPtr; // Sounds used for execution of tasks. diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h index a3c76854cb..7291a55343 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h @@ -29,6 +29,7 @@ class FSpatialGDKEditorToolbarCommands : public TCommands LaunchInspectorWebPageAction; TSharedPtr OpenSimulatedPlayerConfigurationWindowAction; + TSharedPtr OpenLaunchConfigurationEditorAction; TSharedPtr StartSpatialService; TSharedPtr StopSpatialService; diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKSimulatedPlayerDeployment.h b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKSimulatedPlayerDeployment.h index 290587e39e..4ee349272b 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKSimulatedPlayerDeployment.h +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKSimulatedPlayerDeployment.h @@ -36,6 +36,7 @@ class SSpatialGDKSimulatedPlayerDeployment : public SCompoundWidget void Construct(const FArguments& InArgs); private: + /** The parent window of this widget */ TWeakPtr ParentWindowPtr; @@ -50,6 +51,12 @@ class SSpatialGDKSimulatedPlayerDeployment : public SCompoundWidget /** Delegate to commit primary deployment name */ void OnPrimaryDeploymentNameCommited(const FText& InText, ETextCommit::Type InCommitType); + /** Delegate called when the user clicks the GDK Pinned Version checkbox */ + void OnCheckedUsePinnedVersion(ECheckBoxState NewCheckedState); + + /** Delegate to commit runtime version */ + void OnRuntimeCustomVersionCommited(const FText& InText, ETextCommit::Type InCommitType); + /** Delegate called when the user has picked a path for the snapshot file */ void OnSnapshotPathPicked(const FString& PickedPath); @@ -90,6 +97,9 @@ class SSpatialGDKSimulatedPlayerDeployment : public SCompoundWidget void OnCheckedSimulatedPlayers(ECheckBoxState NewCheckedState); ECheckBoxState IsSimulatedPlayersEnabled() const; + ECheckBoxState IsUsingGDKPinnedRuntimeVersion() const; + bool IsUsingCustomRuntimeVersion() const; + FText GetSpatialOSRuntimeVersionToUseText() const; /** Delegate to determine the 'Launch Deployment' button enabled state */ bool IsDeploymentConfigurationValid() const; diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/SpatialGDKEditorToolbar.Build.cs b/SpatialGDK/Source/SpatialGDKEditorToolbar/SpatialGDKEditorToolbar.Build.cs index b243f29def..cdc2d90d08 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/SpatialGDKEditorToolbar.Build.cs +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/SpatialGDKEditorToolbar.Build.cs @@ -6,8 +6,15 @@ public class SpatialGDKEditorToolbar : ModuleRules { public SpatialGDKEditorToolbar(ReadOnlyTargetRules Target) : base(Target) { + bLegacyPublicIncludePaths = false; PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; - bFasterWithoutUnity = true; +#pragma warning disable 0618 + bFasterWithoutUnity = true; // Deprecated in 4.24, replace with bUseUnity = false; once we drop support for 4.23 + if (Target.Version.MinorVersion == 24) // Due to a bug in 4.24, bFasterWithoutUnity is inversed, fixed in master, so should hopefully roll into the next release, remove this once it does + { + bFasterWithoutUnity = false; + } +#pragma warning restore 0618 PrivateIncludePaths.Add("SpatialGDKEditorToolbar/Private"); @@ -21,6 +28,7 @@ public SpatialGDKEditorToolbar(ReadOnlyTargetRules Target) : base(Target) "Engine", "EngineSettings", "InputCore", + "IOSRuntimeSettings", "LevelEditor", "Projects", "Slate", diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/Interop/Connection/EditorWorkerController.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/Interop/Connection/EditorWorkerController.cpp index 9ff9845281..ddb8cc4786 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Private/Interop/Connection/EditorWorkerController.cpp +++ b/SpatialGDK/Source/SpatialGDKServices/Private/Interop/Connection/EditorWorkerController.cpp @@ -1,4 +1,5 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved + #include "Interop/Connection/EditorWorkerController.h" #include "SpatialCommandUtils.h" @@ -66,18 +67,8 @@ struct EditorWorkerController FProcHandle ReplaceWorker(const FString& OldWorker, const FString& NewWorker) { - const FString CmdExecutable = TEXT("spatial.exe"); - - const FString CmdArgs = FString::Printf( - TEXT("local worker replace " - "--local_service_grpc_port %s " - "--existing_worker_id %s " - "--replacing_worker_id %s"), *ServicePort, *OldWorker, *NewWorker); uint32 ProcessID = 0; - FProcHandle ProcHandle = SpatialCommandUtils::CreateSpatialProcess(CmdArgs, false, true, true, &ProcessID, 2 /*PriorityModifier*/, - nullptr, nullptr, nullptr, false); - - return ProcHandle; + return SpatialCommandUtils::LocalWorkerReplace(*ServicePort, *OldWorker, *NewWorker, false, &ProcessID); } void BlockUntilWorkerReady(int32 WorkerIdx) diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp index edb90d3b82..b0243c32ff 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp +++ b/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp @@ -17,6 +17,7 @@ #include "Sockets.h" #include "SocketSubsystem.h" #include "SpatialCommandUtils.h" +#include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesModule.h" #include "UObject/CoreNet.h" @@ -24,7 +25,7 @@ DEFINE_LOG_CATEGORY(LogSpatialDeploymentManager); #define LOCTEXT_NAMESPACE "FLocalDeploymentManager" -static const FString SpatialServiceVersion(TEXT("20200120.115350.8d6b779c82")); +static const FString SpatialServiceVersion(TEXT("20200311.145308.ef0fc31004")); FLocalDeploymentManager::FLocalDeploymentManager() : bLocalDeploymentRunning(false) @@ -39,33 +40,33 @@ FLocalDeploymentManager::FLocalDeploymentManager() void FLocalDeploymentManager::PreInit(bool bChinaEnabled) { -#if PLATFORM_WINDOWS bIsInChina = bChinaEnabled; // Don't kick off background processes when running commandlets - if (IsRunningCommandlet() == false) + const bool bCommandletRunning = IsRunningCommandlet(); + + // Check for the existence of Spatial and Spot. If they don't exist then don't start any background processes. + const bool bSpatialServicesAvailable = FSpatialGDKServicesModule::SpatialPreRunChecks(bIsInChina); + + if (bCommandletRunning || !bSpatialServicesAvailable) { - // Check for the existence of Spatial and Spot. If they don't exist then don't start any background processes. Disable spatial networking if either is true. - if (!FSpatialGDKServicesModule::SpatialPreRunChecks(bIsInChina)) + if (!bSpatialServicesAvailable) { - UE_LOG(LogSpatialDeploymentManager, Warning, TEXT("Pre run checks for LocalDeploymentManager failed. Local deployments cannot be started. Spatial networking will be disabled.")); - GetMutableDefault()->bSpatialNetworking = false; - return; + UE_LOG(LogSpatialDeploymentManager, Warning, TEXT("Pre run checks for LocalDeploymentManager failed. Local deployments cannot be started.")); } + bLocalDeploymentManagerEnabled = false; + return; + } - // Ensure the worker.jsons are up to date. - WorkerBuildConfigAsync(); + // Ensure the worker.jsons are up to date. + WorkerBuildConfigAsync(); - // Watch the worker config directory for changes. - StartUpWorkerConfigDirectoryWatcher(); - } -#endif // PLATFORM_WINDOWS + // Watch the worker config directory for changes. + StartUpWorkerConfigDirectoryWatcher(); } void FLocalDeploymentManager::Init(FString RuntimeIPToExpose) { -#if PLATFORM_WINDOWS - // Don't kick off background processes when running commandlets - if (!IsRunningCommandlet()) + if (bLocalDeploymentManagerEnabled) { // If a service was running, restart to guarantee that the service is running in this project with the correct settings. UE_LOG(LogSpatialDeploymentManager, Log, TEXT("(Re)starting Spatial service in this project.")); @@ -75,7 +76,8 @@ void FLocalDeploymentManager::Init(FString RuntimeIPToExpose) // Stop existing spatial service to guarantee that any new existing spatial service would be running in the current project. TryStopSpatialService(); // Start spatial service in the current project if spatial networking is enabled - if (GetDefault()->bSpatialNetworking) + + if (GetDefault()->UsesSpatialNetworking()) { TryStartSpatialService(RuntimeIPToExpose); } @@ -88,7 +90,6 @@ void FLocalDeploymentManager::Init(FString RuntimeIPToExpose) RefreshServiceStatus(); }); } -#endif // PLATFORM_WINDOWS } void FLocalDeploymentManager::StartUpWorkerConfigDirectoryWatcher() @@ -97,8 +98,7 @@ void FLocalDeploymentManager::StartUpWorkerConfigDirectoryWatcher() if (IDirectoryWatcher* DirectoryWatcher = DirectoryWatcherModule.Get()) { // Watch the worker config directory for changes. - const FString SpatialDirectory = FSpatialGDKServicesModule::GetSpatialOSDirectory(); - FString WorkerConfigDirectory = FPaths::Combine(SpatialDirectory, TEXT("workers")); + FString WorkerConfigDirectory = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("workers")); if (FPaths::DirectoryExists(WorkerConfigDirectory)) { @@ -122,12 +122,11 @@ void FLocalDeploymentManager::WorkerBuildConfigAsync() { AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] { - FString BuildConfigArgs = FString::Printf(TEXT("worker build build-config")); FString WorkerBuildConfigResult; int32 ExitCode; - SpatialCommandUtils::ExecuteSpatialCommandAndReadOutput(BuildConfigArgs, FSpatialGDKServicesModule::GetSpatialOSDirectory(), WorkerBuildConfigResult, ExitCode, bIsInChina); + bool bSuccess = SpatialCommandUtils::BuildWorkerConfig(bIsInChina, SpatialGDKServicesConstants::SpatialOSDirectory, WorkerBuildConfigResult, ExitCode); - if (ExitCode == ExitCodeSuccess) + if (bSuccess) { UE_LOG(LogSpatialDeploymentManager, Display, TEXT("Building worker configurations succeeded!")); } @@ -140,6 +139,11 @@ void FLocalDeploymentManager::WorkerBuildConfigAsync() void FLocalDeploymentManager::RefreshServiceStatus() { + if(!bLocalDeploymentManagerEnabled) + { + return; + } + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] { IsServiceRunningAndInCorrectDirectory(); @@ -270,19 +274,32 @@ bool FLocalDeploymentManager::LocalDeploymentPreRunChecks() } } + if (!bSpatialServiceInProjectDirectory) + { + if (FMessageDialog::Open(EAppMsgType::YesNo, LOCTEXT("StopSpatialServiceFromDifferentProject", "An instance of the SpatialOS Runtime is running with another project. Would you like to stop it and start the Runtime for this project?")) == EAppReturnType::Yes) + { + bSuccess = TryStopSpatialService(); + } + else + { + bSuccess = false; + } + } + return bSuccess; } -bool FLocalDeploymentManager::FinishLocalDeployment(FString LaunchConfig, FString LaunchArgs, FString SnapshotName, FString RuntimeIPToExpose) +bool FLocalDeploymentManager::FinishLocalDeployment(FString LaunchConfig, FString RuntimeVersion, FString LaunchArgs, FString SnapshotName, FString RuntimeIPToExpose) { - FString SpotCreateArgs = FString::Printf(TEXT("alpha deployment create --launch-config=\"%s\" --name=localdeployment --project-name=%s --json --starting-snapshot-id=\"%s\" %s"), *LaunchConfig, *FSpatialGDKServicesModule::GetProjectName(), *SnapshotName, *LaunchArgs); + FString SpotCreateArgs = FString::Printf(TEXT("alpha deployment create --launch-config=\"%s\" --name=localdeployment --project-name=%s --json --starting-snapshot-id=\"%s\" --runtime-version=%s %s"), *LaunchConfig, *FSpatialGDKServicesModule::GetProjectName(), *SnapshotName, *RuntimeVersion, *LaunchArgs); FDateTime SpotCreateStart = FDateTime::Now(); FString SpotCreateResult; FString StdErr; int32 ExitCode; - FPlatformProcess::ExecProcess(*FSpatialGDKServicesModule::GetSpotExe(), *SpotCreateArgs, &ExitCode, &SpotCreateResult, &StdErr); + FPlatformProcess::ExecProcess(*SpatialGDKServicesConstants::SpotExe, *SpotCreateArgs, &ExitCode, &SpotCreateResult, &StdErr); + bStartingDeployment = false; if (ExitCode != ExitCodeSuccess) { @@ -318,14 +335,17 @@ bool FLocalDeploymentManager::FinishLocalDeployment(FString LaunchConfig, FStrin FDateTime SpotCreateEnd = FDateTime::Now(); FTimespan Span = SpotCreateEnd - SpotCreateStart; - OnDeploymentStart.Broadcast(); + AsyncTask(ENamedThreads::GameThread, [this] + { + OnDeploymentStart.Broadcast(); + }); UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Successfully created local deployment in %f seconds."), Span.GetTotalSeconds()); bSuccess = true; } else { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Local deployment creation failed. Deployment status: %s"), *DeploymentStatus); + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Local deployment creation failed. Deployment status: %s. Please check the 'Spatial Output' window for more details."), *DeploymentStatus); } } else @@ -336,8 +356,18 @@ bool FLocalDeploymentManager::FinishLocalDeployment(FString LaunchConfig, FStrin return true; } -void FLocalDeploymentManager::TryStartLocalDeployment(FString LaunchConfig, FString LaunchArgs, FString SnapshotName, FString RuntimeIPToExpose, const LocalDeploymentCallback& CallBack) +void FLocalDeploymentManager::TryStartLocalDeployment(FString LaunchConfig, FString RuntimeVersion, FString LaunchArgs, FString SnapshotName, FString RuntimeIPToExpose, const LocalDeploymentCallback& CallBack) { + if (!bLocalDeploymentManagerEnabled) + { + UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Local deployment manager is disabled because spatial services are unavailable.")); + if (CallBack) + { + CallBack(false); + } + return; + } + bRedeployRequired = false; if (bStoppingDeployment) @@ -352,14 +382,20 @@ void FLocalDeploymentManager::TryStartLocalDeployment(FString LaunchConfig, FStr if (bLocalDeploymentRunning) { UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Tried to start a local deployment but one is already running.")); - CallBack(false); + if (CallBack) + { + CallBack(false); + } return; } if (!LocalDeploymentPreRunChecks()) { UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Tried to start a local deployment but a required port is already bound by another process.")); - CallBack(false); + if (CallBack) + { + CallBack(false); + } return; } @@ -388,12 +424,12 @@ void FLocalDeploymentManager::TryStartLocalDeployment(FString LaunchConfig, FStr #else AttemptSpatialAuthResult = Async(EAsyncExecution::Thread, [this]() { return SpatialCommandUtils::AttemptSpatialAuth(bIsInChina); }, #endif - [this, LaunchConfig, LaunchArgs, SnapshotName, RuntimeIPToExpose, CallBack]() + [this, LaunchConfig, RuntimeVersion, LaunchArgs, SnapshotName, RuntimeIPToExpose, CallBack]() { bool bSuccess = AttemptSpatialAuthResult.IsReady() && AttemptSpatialAuthResult.Get() == true; if (bSuccess) { - FinishLocalDeployment(LaunchConfig, LaunchArgs, SnapshotName, RuntimeIPToExpose); + bSuccess = FinishLocalDeployment(LaunchConfig, RuntimeVersion, LaunchArgs, SnapshotName, RuntimeIPToExpose); } else { @@ -401,7 +437,10 @@ void FLocalDeploymentManager::TryStartLocalDeployment(FString LaunchConfig, FStr } bStartingDeployment = false; - CallBack(bSuccess); + if (CallBack) + { + CallBack(bSuccess); + } }); return; @@ -422,7 +461,7 @@ bool FLocalDeploymentManager::TryStopLocalDeployment() FString SpotDeleteResult; FString StdErr; int32 ExitCode; - FPlatformProcess::ExecProcess(*FSpatialGDKServicesModule::GetSpotExe(), *SpotDeleteArgs, &ExitCode, &SpotDeleteResult, &StdErr); + FPlatformProcess::ExecProcess(*SpatialGDKServicesConstants::SpotExe, *SpotDeleteArgs, &ExitCode, &SpotDeleteResult, &StdErr); bStoppingDeployment = false; if (ExitCode != ExitCodeSuccess) @@ -471,6 +510,12 @@ bool FLocalDeploymentManager::TryStopLocalDeployment() bool FLocalDeploymentManager::TryStartSpatialService(FString RuntimeIPToExpose) { + if (!bLocalDeploymentManagerEnabled) + { + UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Local deployment manager is disabled because spatial services are unavailable.")); + return false; + } + if (bSpatialServiceRunning) { UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Tried to start spatial service but it is already running.")); @@ -484,24 +529,14 @@ bool FLocalDeploymentManager::TryStartSpatialService(FString RuntimeIPToExpose) bStartingSpatialService = true; - FString SpatialServiceStartArgs = FString::Printf(TEXT("service start --version=%s"), *SpatialServiceVersion); - - // Pass exposed runtime IP if one has been specified - if (!RuntimeIPToExpose.IsEmpty()) - { - SpatialServiceStartArgs.Append(FString::Printf(TEXT(" --runtime_ip=%s"), *RuntimeIPToExpose)); - UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Trying to start spatial service with exposed runtime ip: %s"), *RuntimeIPToExpose); - } - FString ServiceStartResult; int32 ExitCode; - - - SpatialCommandUtils::ExecuteSpatialCommandAndReadOutput(SpatialServiceStartArgs, FSpatialGDKServicesModule::GetSpatialOSDirectory(), ServiceStartResult, ExitCode, bIsInChina); + bool bSuccess = SpatialCommandUtils::StartSpatialService(*SpatialServiceVersion, *RuntimeIPToExpose, bIsInChina, + SpatialGDKServicesConstants::SpatialOSDirectory, ServiceStartResult, ExitCode); bStartingSpatialService = false; - if (ExitCode == ExitCodeSuccess && ServiceStartResult.Contains(TEXT("RUNNING"))) + if (bSuccess && ServiceStartResult.Contains(TEXT("RUNNING"))) { UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Spatial service started!")); ExposedRuntimeIP = RuntimeIPToExpose; @@ -520,6 +555,11 @@ bool FLocalDeploymentManager::TryStartSpatialService(FString RuntimeIPToExpose) bool FLocalDeploymentManager::TryStopSpatialService() { + if (!bLocalDeploymentManagerEnabled) + { + return false; + } + if (bStoppingSpatialService) { UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Tried to stop spatial service but it is already being stopped.")); @@ -528,14 +568,13 @@ bool FLocalDeploymentManager::TryStopSpatialService() bStoppingSpatialService = true; - FString SpatialServiceStartArgs = FString::Printf(TEXT("service stop")); FString ServiceStopResult; int32 ExitCode; + bool bSuccess = SpatialCommandUtils::StopSpatialService(bIsInChina, SpatialGDKServicesConstants::SpatialOSDirectory, ServiceStopResult, ExitCode); - SpatialCommandUtils::ExecuteSpatialCommandAndReadOutput(SpatialServiceStartArgs, FSpatialGDKServicesModule::GetSpatialOSDirectory(), ServiceStopResult, ExitCode, bIsInChina); bStoppingSpatialService = false; - if (ExitCode == ExitCodeSuccess) + if (bSuccess) { UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Spatial service stopped!")); ExposedRuntimeIP = TEXT(""); @@ -544,10 +583,6 @@ bool FLocalDeploymentManager::TryStopSpatialService() bLocalDeploymentRunning = false; return true; } - else - { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Spatial service failed to stop! %s"), *ServiceStopResult); - } return false; } @@ -565,7 +600,7 @@ bool FLocalDeploymentManager::GetLocalDeploymentStatus() FString SpotListResult; FString StdErr; int32 ExitCode; - FPlatformProcess::ExecProcess(*FSpatialGDKServicesModule::GetSpotExe(), *SpotListArgs, &ExitCode, &SpotListResult, &StdErr); + FPlatformProcess::ExecProcess(*SpatialGDKServicesConstants::SpotExe, *SpotListArgs, &ExitCode, &SpotListResult, &StdErr); if (ExitCode != ExitCodeSuccess) { @@ -621,12 +656,17 @@ bool FLocalDeploymentManager::GetLocalDeploymentStatus() bool FLocalDeploymentManager::IsServiceRunningAndInCorrectDirectory() { + if (!bLocalDeploymentManagerEnabled) + { + return false; + } + FString SpotProjectInfoArgs = TEXT("alpha service project-info --json"); FString SpotProjectInfoResult; FString StdErr; int32 ExitCode; - FPlatformProcess::ExecProcess(*FSpatialGDKServicesModule::GetSpotExe(), *SpotProjectInfoArgs, &ExitCode, &SpotProjectInfoResult, &StdErr); + FPlatformProcess::ExecProcess(*SpatialGDKServicesConstants::SpotExe, *SpotProjectInfoArgs, &ExitCode, &SpotProjectInfoResult, &StdErr); if (ExitCode != ExitCodeSuccess) { @@ -663,7 +703,7 @@ bool FLocalDeploymentManager::IsServiceRunningAndInCorrectDirectory() // Get the project file path and ensure it matches the one for the currently running project. if (bParsingSuccess && SpotJsonContent->Get()->TryGetStringField(TEXT("projectFilePath"), SpatialServiceProjectPath)) { - FString CurrentProjectSpatialPath = FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("spatialos.json")); + FString CurrentProjectSpatialPath = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("spatialos.json")); FPaths::NormalizeDirectoryName(SpatialServiceProjectPath); FPaths::RemoveDuplicateSlashes(SpatialServiceProjectPath); diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/SSpatialOutputLog.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/SSpatialOutputLog.cpp index 901fbd156f..e78cb15853 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Private/SSpatialOutputLog.cpp +++ b/SpatialGDK/Source/SpatialGDKServices/Private/SSpatialOutputLog.cpp @@ -9,6 +9,7 @@ #include "Misc/FileHelper.h" #include "Modules/ModuleManager.h" #include "SlateOptMacros.h" +#include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesModule.h" #include "Internationalization/Regex.h" @@ -16,7 +17,7 @@ DEFINE_LOG_CATEGORY(LogSpatialOutputLog); -static const FString LocalDeploymentLogsDir(FSpatialGDKServicesModule::GetSpatialOSDirectory(TEXT("logs/localdeployment"))); +static const FString LocalDeploymentLogsDir(FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("logs/localdeployment"))); static const FString LaunchLogFilename(TEXT("launch.log")); static const float PollTimeInterval(0.05f); @@ -103,6 +104,7 @@ void SSpatialOutputLog::StartUpLogDirectoryWatcher(const FString& LogDirectory) if (!FPaths::DirectoryExists(LogDirectory)) { UE_LOG(LogSpatialOutputLog, Log, TEXT("Spatial local deployment log directory '%s' does not exist. Will create it."), *LogDirectory); + if (!FPlatformFileManager::Get().GetPlatformFile().CreateDirectoryTree(*LogDirectory)) { UE_LOG(LogSpatialOutputLog, Error, TEXT("Could not create the spatial local deployment log directory. The Spatial Output window will not function.")); @@ -253,21 +255,53 @@ void SSpatialOutputLog::StartPollTimer(const FString& LogFilePath) }); } +void SSpatialOutputLog::FormatAndPrintRawErrorLine(const FString& LogLine) +{ + const FRegexPattern ErrorPattern = FRegexPattern(TEXT("level=(.*) msg=(.*) code=(.*) code_string=(.*) error=(.*) stack=(.*)")); + FRegexMatcher ErrorMatcher(ErrorPattern, LogLine); + + if (!ErrorMatcher.FindNext()) + { + UE_LOG(LogSpatialOutputLog, Error, TEXT("Failed to parse log line: %s"), *LogLine); + return; + } + + FString ErrorLevelText = ErrorMatcher.GetCaptureGroup(1); + FString Message = ErrorMatcher.GetCaptureGroup(2); + FString ErrorCode = ErrorMatcher.GetCaptureGroup(3); + FString ErrorCodeString = ErrorMatcher.GetCaptureGroup(4); + FString ErrorMessage = ErrorMatcher.GetCaptureGroup(5); + FString Stack = ErrorMatcher.GetCaptureGroup(6); + + // The stack message comes with double escaped characters. + Stack = Stack.ReplaceEscapedCharWithChar(); + + // Format the log message to be easy to read. + FString LogMessage = FString::Printf(TEXT("%s \n Code: %s \n Code String: %s \n Error: %s \n Stack: %s"), *Message, *ErrorCode, *ErrorCodeString, *ErrorMessage, *Stack); + + // Serialization must be done on the game thread. + AsyncTask(ENamedThreads::GameThread, [this, LogMessage] + { + Serialize(*LogMessage, ELogVerbosity::Error, FName(TEXT("SpatialService"))); + }); +} + void SSpatialOutputLog::FormatAndPrintRawLogLine(const FString& LogLine) { // Log lines have the format time=LOG_TIME level=LOG_LEVEL logger=LOG_CATEGORY msg=LOG_MESSAGE - const FRegexPattern LogPattern = FRegexPattern(TEXT("level=(.*) logger=(.*\\.)?(.*) msg=(.*)")); + const FRegexPattern LogPattern = FRegexPattern(TEXT("level=(.*) msg=(.*) loggerName=(.*\\.)?(.*)")); FRegexMatcher LogMatcher(LogPattern, LogLine); if (!LogMatcher.FindNext()) { - UE_LOG(LogSpatialOutputLog, Error, TEXT("Failed to parse log line: %s"), *LogLine); + // If this log line did not match the log line regex then it is an error line which is parsed differently. + FormatAndPrintRawErrorLine(LogLine); return; } FString LogLevelText = LogMatcher.GetCaptureGroup(1); - FString LogCategory = LogMatcher.GetCaptureGroup(3); - FString LogMessage = LogMatcher.GetCaptureGroup(4); + FString LogMessage = LogMatcher.GetCaptureGroup(2); + FString LogCategory = LogMatcher.GetCaptureGroup(4); // For worker logs 'WorkerLogMessageHandler' we use the worker name as the category. The worker name can be found in the msg. // msg=[WORKER_NAME:WORKER_TYPE] ... e.g. msg=[UnrealWorkerF5C56488482FEDC37B10E382770067E3:UnrealWorker] diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/SpatialCommandUtils.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/SpatialCommandUtils.cpp index 3d19bb350b..ab769d5274 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Private/SpatialCommandUtils.cpp +++ b/SpatialGDK/Source/SpatialGDKServices/Private/SpatialCommandUtils.cpp @@ -1,6 +1,7 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "SpatialCommandUtils.h" +#include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesModule.h" DEFINE_LOG_CATEGORY(LogSpatialCommandUtils); @@ -10,46 +11,132 @@ namespace FString ChinaEnvironmentArgument = TEXT(" --environment=cn-production"); } // anonymous namespace -void SpatialCommandUtils::ExecuteSpatialCommandAndReadOutput(FString Arguments, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode, bool bIsRunningInChina) +bool SpatialCommandUtils::SpatialVersion(bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode) { + FString Command = TEXT("version"); + + if (bIsRunningInChina) + { + Command += ChinaEnvironmentArgument; + } + + FSpatialGDKServicesModule::ExecuteAndReadOutput(*SpatialGDKServicesConstants::SpatialExe, Command, DirectoryToRun, OutResult, OutExitCode); + + bool bSuccess = OutExitCode == 0; + if (!bSuccess) + { + UE_LOG(LogSpatialCommandUtils, Warning, TEXT("Spatial version failed. Error Code: %d, Error Message: %s"), OutExitCode, *OutResult); + } + + return bSuccess; +} + +bool SpatialCommandUtils::AttemptSpatialAuth(bool bIsRunningInChina) +{ + FString Command = TEXT("auth login"); + if (bIsRunningInChina) { - Arguments += ChinaEnvironmentArgument; + Command += ChinaEnvironmentArgument; + } + + int32 OutExitCode; + FString OutStdOut; + FString OutStdErr; + + FPlatformProcess::ExecProcess(*SpatialGDKServicesConstants::SpatialExe, *Command, &OutExitCode, &OutStdOut, &OutStdErr); + + bool bSuccess = OutExitCode == 0; + if (!bSuccess) + { + UE_LOG(LogSpatialCommandUtils, Warning, TEXT("Spatial auth login failed. Error Code: %d, StdOut Message: %s, StdErr Message: %s"), OutExitCode, *OutStdOut, *OutStdErr); } - FSpatialGDKServicesModule::ExecuteAndReadOutput(*FSpatialGDKServicesModule::GetSpatialExe(), Arguments, DirectoryToRun, OutResult, OutExitCode); + return bSuccess; } -void SpatialCommandUtils::ExecuteSpatialCommand(FString Arguments, int32* OutExitCode, FString* OutStdOut, FString* OutStdEr, bool bIsRunningInChina) +bool SpatialCommandUtils::StartSpatialService(const FString& Version, const FString& RuntimeIP, bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode) { + FString Command = TEXT("service start"); + if (bIsRunningInChina) { - Arguments += ChinaEnvironmentArgument; + Command += ChinaEnvironmentArgument; + } + + if (!Version.IsEmpty()) + { + Command.Append(FString::Printf(TEXT(" --version=%s"), *Version)); + } + + if (!RuntimeIP.IsEmpty()) + { + Command.Append(FString::Printf(TEXT(" --runtime_ip=%s"), *RuntimeIP)); + UE_LOG(LogSpatialCommandUtils, Verbose, TEXT("Trying to start spatial service with exposed runtime ip: %s"), *RuntimeIP); + } + + FSpatialGDKServicesModule::ExecuteAndReadOutput(*SpatialGDKServicesConstants::SpatialExe, Command, DirectoryToRun, OutResult, OutExitCode); + + bool bSuccess = OutExitCode == 0; + if (!bSuccess) + { + UE_LOG(LogSpatialCommandUtils, Warning, TEXT("Spatial start service failed. Error Code: %d, Error Message: %s"), OutExitCode, *OutResult); } - FPlatformProcess::ExecProcess(*FSpatialGDKServicesModule::GetSpatialExe(), *Arguments, OutExitCode, OutStdOut, OutStdEr); + + return bSuccess; } -FProcHandle SpatialCommandUtils::CreateSpatialProcess(FString Arguments, bool bLaunchDetached, bool bLaunchHidden, bool bLaunchReallyHidden, uint32* OutProcessID, int32 PriorityModifier, const TCHAR* OptionalWorkingDirectory, void* PipeWriteChild, void * PipeReadChild, bool bIsRunningInChina) +bool SpatialCommandUtils::StopSpatialService(bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode) { + FString Command = TEXT("service stop"); + if (bIsRunningInChina) { - Arguments += ChinaEnvironmentArgument; + Command += ChinaEnvironmentArgument; + } + + FSpatialGDKServicesModule::ExecuteAndReadOutput(*SpatialGDKServicesConstants::SpatialExe, Command, DirectoryToRun, OutResult, OutExitCode); + + bool bSuccess = OutExitCode == 0; + if (!bSuccess) + { + UE_LOG(LogSpatialCommandUtils, Warning, TEXT("Spatial stop service failed. Error Code: %d, Error Message: %s"), OutExitCode, *OutResult); } - return FPlatformProcess::CreateProc(*FSpatialGDKServicesModule::GetSpatialExe(), *Arguments, bLaunchDetached, bLaunchHidden, bLaunchReallyHidden, OutProcessID, PriorityModifier, OptionalWorkingDirectory, PipeWriteChild, PipeReadChild); + + return bSuccess; } -bool SpatialCommandUtils::AttemptSpatialAuth(bool bIsRunningInChina) +bool SpatialCommandUtils::BuildWorkerConfig(bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode) { - FString SpatialInfoResult; - FString StdErr; - int32 ExitCode; - ExecuteSpatialCommand(TEXT("auth login"), &ExitCode, &SpatialInfoResult, &StdErr, bIsRunningInChina); + FString Command = TEXT("worker build build-config"); + + if (bIsRunningInChina) + { + Command += ChinaEnvironmentArgument; + } + + FSpatialGDKServicesModule::ExecuteAndReadOutput(*SpatialGDKServicesConstants::SpatialExe, Command, DirectoryToRun, OutResult, OutExitCode); - bool bSuccess = ExitCode == 0; + bool bSuccess = OutExitCode == 0; if (!bSuccess) { - UE_LOG(LogSpatialCommandUtils, Warning, TEXT("Spatial auth login failed. Error Code: %d, Error Message: %s"), ExitCode, *SpatialInfoResult); + UE_LOG(LogSpatialCommandUtils, Warning, TEXT("Spatial build worker config failed. Error Code: %d, Error Message: %s"), OutExitCode, *OutResult); } return bSuccess; } + +FProcHandle SpatialCommandUtils::LocalWorkerReplace(const FString& ServicePort, const FString& OldWorker, const FString& NewWorker, bool bIsRunningInChina, uint32* OutProcessID) +{ + check(!ServicePort.IsEmpty()); + check(!OldWorker.IsEmpty()); + check(!NewWorker.IsEmpty()); + + FString Command = TEXT("worker build build-config"); + Command.Append(FString::Printf(TEXT(" --local_service_grpc_port %s"), *ServicePort)); + Command.Append(FString::Printf(TEXT(" --existing_worker_id %s"), *OldWorker)); + Command.Append(FString::Printf(TEXT(" --replacing_worker_id %s"), *NewWorker)); + + return FPlatformProcess::CreateProc(*SpatialGDKServicesConstants::SpatialExe, *Command, false, true, true, OutProcessID, 2 /*PriorityModifier*/, + nullptr, nullptr, nullptr); +} diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/SpatialGDKServicesModule.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/SpatialGDKServicesModule.cpp index cfb2943840..d782defe8b 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Private/SpatialGDKServicesModule.cpp +++ b/SpatialGDK/Source/SpatialGDKServices/Private/SpatialGDKServicesModule.cpp @@ -12,6 +12,7 @@ #include "SSpatialOutputLog.h" #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" +#include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesPrivate.h" #include "Widgets/Docking/SDockTab.h" @@ -21,8 +22,6 @@ DEFINE_LOG_CATEGORY(LogSpatialGDKServices); IMPLEMENT_MODULE(FSpatialGDKServicesModule, SpatialGDKServices); -const FString SpatialExe = TEXT("spatial.exe"); -const FString SpotExe = FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(TEXT("SpatialGDK/Binaries/ThirdParty/Improbable/Programs/spot.exe")); static const FName SpatialOutputLogTabName = FName(TEXT("SpatialOutputLog")); TSharedRef SpawnSpatialOutputLog(const FSpawnTabArgs& Args) @@ -60,11 +59,6 @@ FLocalDeploymentManager* FSpatialGDKServicesModule::GetLocalDeploymentManager() return &LocalDeploymentManager; } -FString FSpatialGDKServicesModule::GetSpatialOSDirectory(const FString& AppendPath) -{ - return FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectDir(), TEXT("/../spatial/"), AppendPath)); -} - FString FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(const FString& AppendPath) { FString PluginDir = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectPluginsDir(), TEXT("UnrealGDK"))); @@ -79,35 +73,25 @@ FString FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(const FString& A return FPaths::ConvertRelativePathToFull(FPaths::Combine(PluginDir, AppendPath)); } -const FString& FSpatialGDKServicesModule::GetSpotExe() -{ - return SpotExe; -} - -const FString& FSpatialGDKServicesModule::GetSpatialExe() -{ - return SpatialExe; -} - bool FSpatialGDKServicesModule::SpatialPreRunChecks(bool bIsInChina) { FString SpatialExistenceCheckResult; int32 ExitCode; - SpatialCommandUtils::ExecuteSpatialCommandAndReadOutput(TEXT("version"), GetSpatialOSDirectory(), SpatialExistenceCheckResult, ExitCode, bIsInChina); + bool bSuccess = SpatialCommandUtils::SpatialVersion(bIsInChina, SpatialGDKServicesConstants::SpatialOSDirectory, SpatialExistenceCheckResult, ExitCode); - if (ExitCode != 0) + if (!bSuccess) { - UE_LOG(LogSpatialDeploymentManager, Warning, TEXT("Spatial.exe does not exist on this machine! Please make sure Spatial is installed before trying to start a local deployment. %s"), *SpatialExistenceCheckResult); + UE_LOG(LogSpatialDeploymentManager, Warning, TEXT("%s does not exist on this machine! Please make sure Spatial is installed before trying to start a local deployment. %s"), *SpatialGDKServicesConstants::SpatialExe, *SpatialExistenceCheckResult); return false; } FString SpotExistenceCheckResult; FString StdErr; - FPlatformProcess::ExecProcess(*GetSpotExe(), TEXT("version"), &ExitCode, &SpotExistenceCheckResult, &StdErr); + FPlatformProcess::ExecProcess(*SpatialGDKServicesConstants::SpotExe, TEXT("version"), &ExitCode, &SpotExistenceCheckResult, &StdErr); if (ExitCode != 0) { - UE_LOG(LogSpatialDeploymentManager, Warning, TEXT("Spot.exe does not exist on this machine! Please make sure to run Setup.bat in the UnrealGDK Plugin before trying to start a local deployment.")); + UE_LOG(LogSpatialDeploymentManager, Warning, TEXT("%s does not exist on this machine! Please make sure to run Setup.bat in the UnrealGDK Plugin before trying to start a local deployment."), *SpatialGDKServicesConstants::SpotExe); return false; } @@ -156,27 +140,32 @@ void FSpatialGDKServicesModule::ExecuteAndReadOutput(const FString& Executable, FString FSpatialGDKServicesModule::ParseProjectName() { FString ProjectNameParsed; - const FString SpatialDirectory = FSpatialGDKServicesModule::GetSpatialOSDirectory(); FString SpatialFileName = TEXT("spatialos.json"); FString SpatialFileResult; - FFileHelper::LoadFileToString(SpatialFileResult, *FPaths::Combine(SpatialDirectory, SpatialFileName)); - TSharedPtr JsonParsedSpatialFile; - if (ParseJson(SpatialFileResult, JsonParsedSpatialFile)) + if (FFileHelper::LoadFileToString(SpatialFileResult, *FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, SpatialFileName))) { - if (JsonParsedSpatialFile->TryGetStringField(TEXT("name"), ProjectNameParsed)) + TSharedPtr JsonParsedSpatialFile; + if (ParseJson(SpatialFileResult, JsonParsedSpatialFile)) { - return ProjectNameParsed; + if (JsonParsedSpatialFile->TryGetStringField(TEXT("name"), ProjectNameParsed)) + { + return ProjectNameParsed; + } + else + { + UE_LOG(LogSpatialGDKServices, Error, TEXT("'name' does not exist in spatialos.json. Can't read project name.")); + } } else { - UE_LOG(LogSpatialGDKServices, Error, TEXT("'name' does not exist in spatialos.json. Can't read project name.")); + UE_LOG(LogSpatialGDKServices, Error, TEXT("Json parsing of spatialos.json failed. Can't get project name.")); } } else { - UE_LOG(LogSpatialGDKServices, Error, TEXT("Json parsing of spatialos.json failed. Can't get project name.")); + UE_LOG(LogSpatialGDKServices, Error, TEXT("Loading spatialos.json failed. Can't get project name.")); } ProjectNameParsed.Empty(); diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/Interop/Connection/EditorWorkerController.h b/SpatialGDK/Source/SpatialGDKServices/Public/Interop/Connection/EditorWorkerController.h index a21b84cc77..7a80870e79 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Public/Interop/Connection/EditorWorkerController.h +++ b/SpatialGDK/Source/SpatialGDKServices/Public/Interop/Connection/EditorWorkerController.h @@ -1,4 +1,5 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved + #pragma once #if WITH_EDITOR diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/LocalDeploymentManager.h b/SpatialGDK/Source/SpatialGDKServices/Public/LocalDeploymentManager.h index b81af5aeef..3ecd1a9381 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Public/LocalDeploymentManager.h +++ b/SpatialGDK/Source/SpatialGDKServices/Public/LocalDeploymentManager.h @@ -1,4 +1,5 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved + #pragma once #include "Async/Future.h" @@ -17,7 +18,6 @@ class FLocalDeploymentManager public: FLocalDeploymentManager(); - // Needs to be ran after SetInChina is called. void SPATIALGDKSERVICES_API PreInit(bool bChinaEnabled); void SPATIALGDKSERVICES_API Init(FString RuntimeIPToExpose); @@ -28,9 +28,9 @@ class FLocalDeploymentManager bool KillProcessBlockingPort(int32 Port); bool LocalDeploymentPreRunChecks(); - using LocalDeploymentCallback = TFunction; + using LocalDeploymentCallback = TFunction; - void SPATIALGDKSERVICES_API TryStartLocalDeployment(FString LaunchConfig, FString LaunchArgs, FString SnapshotName, FString RuntimeIPToExpose, const LocalDeploymentCallback& CallBack); + void SPATIALGDKSERVICES_API TryStartLocalDeployment(FString LaunchConfig, FString RuntimeVersion, FString LaunchArgs, FString SnapshotName, FString RuntimeIPToExpose, const LocalDeploymentCallback& CallBack); bool SPATIALGDKSERVICES_API TryStopLocalDeployment(); bool SPATIALGDKSERVICES_API TryStartSpatialService(FString RuntimeIPToExpose); @@ -68,7 +68,7 @@ class FLocalDeploymentManager void StartUpWorkerConfigDirectoryWatcher(); void OnWorkerConfigDirectoryChanged(const TArray& FileChanges); - bool FinishLocalDeployment(FString LaunchConfig, FString LaunchArgs, FString SnapshotName, FString RuntimeIPToExpose); + bool FinishLocalDeployment(FString LaunchConfig, FString RuntimeVersion, FString LaunchArgs, FString SnapshotName, FString RuntimeIPToExpose); TFuture AttemptSpatialAuthResult; @@ -79,6 +79,8 @@ class FLocalDeploymentManager // This is the frequency at which check the 'spatial service status' to ensure we have the correct state as the user can change spatial service outside of the editor. static const int32 RefreshFrequency = 3; + bool bLocalDeploymentManagerEnabled = true; + bool bLocalDeploymentRunning; bool bSpatialServiceRunning; bool bSpatialServiceInProjectDirectory; diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/SSpatialOutputLog.h b/SpatialGDK/Source/SpatialGDKServices/Public/SSpatialOutputLog.h index 18e6b091c3..b5320adc2c 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Public/SSpatialOutputLog.h +++ b/SpatialGDK/Source/SpatialGDKServices/Public/SSpatialOutputLog.h @@ -42,6 +42,7 @@ class SSpatialOutputLog : public SOutputLog void CloseLogReader(); void FormatAndPrintRawLogLine(const FString& LogLine); + void FormatAndPrintRawErrorLine(const FString& LogLine); void StartUpLogDirectoryWatcher(const FString& LogDirectory); void ShutdownLogDirectoryWatcher(const FString& LogDirectory); diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialCommandUtils.h b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialCommandUtils.h index 906c912dc7..9cc61bc676 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialCommandUtils.h +++ b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialCommandUtils.h @@ -10,11 +10,11 @@ class SpatialCommandUtils { public: - SPATIALGDKSERVICES_API static void ExecuteSpatialCommandAndReadOutput(FString Arguments, const FString& DirectoryToRun, FString& OutResult, int32& ExitCode, bool bIsRunningInChina); - - SPATIALGDKSERVICES_API static void ExecuteSpatialCommand(FString Arguments, int32* OutReturnCode, FString* OutStdOut, FString* OutStdEr, bool bIsRunningInChina); - - SPATIALGDKSERVICES_API static FProcHandle CreateSpatialProcess(FString Arguments, bool bLaunchDetached, bool bLaunchHidden, bool bLaunchReallyHidden, uint32* OutProcessID, int32 PriorityModifier, const TCHAR* OptionalWorkingDirectory, void* PipeWriteChild, void * PipeReadChild, bool bIsRunningInChina); - + SPATIALGDKSERVICES_API static bool SpatialVersion(bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode); SPATIALGDKSERVICES_API static bool AttemptSpatialAuth(bool bIsRunningInChina); + SPATIALGDKSERVICES_API static bool StartSpatialService(const FString& Version, const FString& RuntimeIP, bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode); + SPATIALGDKSERVICES_API static bool StopSpatialService(bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode); + SPATIALGDKSERVICES_API static bool BuildWorkerConfig(bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode); + SPATIALGDKSERVICES_API static FProcHandle LocalWorkerReplace(const FString& ServicePort, const FString& OldWorker, const FString& NewWorker, bool bIsRunningInChina, uint32* OutProcessID); + }; diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesConstants.h b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesConstants.h new file mode 100644 index 0000000000..51a193057d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesConstants.h @@ -0,0 +1,33 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialGDKServicesModule.h" + +namespace SpatialGDKServicesConstants +{ +#if PLATFORM_WINDOWS + // Assumes that spatial is installed and in the PATH + const FString SpatialPath = TEXT(""); + const FString Extension = TEXT("exe"); +#elif PLATFORM_MAC + // UNR-2518: This is currently hardcoded and we expect users to have spatial either installed or symlinked to this path. + // If they haven't, it is necessary to symlink it to /usr/local/bin. At some point we should expose this via + // the Unreal UI, however right now the SpatialGDKServices module is unable to see these. + const FString SpatialPath = TEXT("/usr/local/bin"); + const FString Extension = TEXT(""); +#endif + + static inline const FString CreateExePath(FString Path, FString ExecutableName) + { + FString ExecutableFile = FPaths::SetExtension(ExecutableName, Extension); + return FPaths::Combine(Path, ExecutableFile); + } + + const FString GDKProgramPath = FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(TEXT("SpatialGDK/Binaries/ThirdParty/Improbable/Programs")); + const FString SpatialExe = CreateExePath(SpatialPath, TEXT("spatial")); + const FString SpotExe = CreateExePath(GDKProgramPath, TEXT("spot")); + const FString SchemaCompilerExe = CreateExePath(GDKProgramPath, TEXT("schema_compiler")); + const FString SpatialOSDirectory = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectDir(), TEXT("/../spatial/"))); + const FString SpatialOSRuntimePinnedVersion("14.5.1"); +} diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesModule.h b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesModule.h index be46115107..dfe958d351 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesModule.h +++ b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesModule.h @@ -1,4 +1,5 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved + #pragma once #include "LocalDeploymentManager.h" @@ -20,10 +21,8 @@ class SPATIALGDKSERVICES_API FSpatialGDKServicesModule : public IModuleInterface FLocalDeploymentManager* GetLocalDeploymentManager(); - static FString GetSpatialOSDirectory(const FString& AppendPath = TEXT("")); static FString GetSpatialGDKPluginDirectory(const FString& AppendPath = TEXT("")); - static const FString& GetSpotExe(); - static const FString& GetSpatialExe(); + static bool SpatialPreRunChecks(bool bIsInChina); FORCEINLINE static FString GetProjectName() diff --git a/SpatialGDK/Source/SpatialGDKServices/SpatialGDKServices.Build.cs b/SpatialGDK/Source/SpatialGDKServices/SpatialGDKServices.Build.cs index 2d53ad1c99..3c3670859d 100644 --- a/SpatialGDK/Source/SpatialGDKServices/SpatialGDKServices.Build.cs +++ b/SpatialGDK/Source/SpatialGDKServices/SpatialGDKServices.Build.cs @@ -6,8 +6,15 @@ public class SpatialGDKServices : ModuleRules { public SpatialGDKServices(ReadOnlyTargetRules Target) : base(Target) { + bLegacyPublicIncludePaths = false; PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; - bFasterWithoutUnity = true; +#pragma warning disable 0618 + bFasterWithoutUnity = true; // Deprecated in 4.24, replace with bUseUnity = false; once we drop support for 4.23 + if (Target.Version.MinorVersion == 24) // Due to a bug in 4.24, bFasterWithoutUnity is inversed, fixed in master, so should hopefully roll into the next release, remove this once it does + { + bFasterWithoutUnity = false; + } +#pragma warning restore 0618 PrivateDependencyModuleNames.AddRange( new string[] { diff --git a/SpatialGDK/Source/SpatialGDKTests/Examples/SpatialGDKExampleTest.cpp b/SpatialGDK/Source/SpatialGDKTests/Examples/SpatialGDKExampleTest.cpp new file mode 100644 index 0000000000..6229bad510 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/Examples/SpatialGDKExampleTest.cpp @@ -0,0 +1,178 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/TestDefinitions.h" + +#include "HAL/IPlatformFileProfilerWrapper.h" +#include "HAL/PlatformFilemanager.h" +#include "Misc/ScopeTryLock.h" +#include "Misc/Paths.h" + +#define EXAMPLE_SIMPLE_TEST(TestName) \ + GDK_TEST(SpatialGDKExamples, SimpleExamples, TestName) + +#define EXAMPLE_COMPLEX_TEST(TestName) \ + GDK_COMPLEX_TEST(SpatialGDKExamples, ComplexExamples, TestName) + +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKExamples, Log, All); +DEFINE_LOG_CATEGORY(LogSpatialGDKExamples); + +// 1. Latent command example +namespace +{ +const double MAX_WAIT_TIME_FOR_BACKGROUND_COMPUTATION = 2.0; +const double MIN_WAIT_TIME_FOR_BACKGROUND_COMPUTATION = 1.0; +const double COMPUTATION_DURATION = 1.0; + +struct ComputationResult +{ + FCriticalSection Mutex; + int Value = 0; +}; + +} // anonymous namespace + +DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FStartBackgroundThreadComputation, TSharedPtr, InResult); +bool FStartBackgroundThreadComputation::Update() +{ + TSharedPtr LocalResult = InResult; + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [LocalResult] + { + FScopeLock BackgroundComputationLock(&LocalResult->Mutex); + FPlatformProcess::Sleep(COMPUTATION_DURATION); + LocalResult->Value = 42; + }); + + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER(FWaitForComputationAndCheckResult, FAutomationTestBase*, Test, TSharedPtr, InResult); +bool FWaitForComputationAndCheckResult::Update() +{ + const double TimePassed = FPlatformTime::Seconds() - StartTime; + + if (TimePassed >= MIN_WAIT_TIME_FOR_BACKGROUND_COMPUTATION) + { + FScopeTryLock BackgroundComputationLock(&InResult->Mutex); + + if (BackgroundComputationLock.IsLocked()) + { + Test->TestTrue("Computation result is equal to expected value", InResult->Value == 42); + return true; + } + + if (TimePassed >= MAX_WAIT_TIME_FOR_BACKGROUND_COMPUTATION) + { + Test->TestTrue("Computation finished in time", false); + return true; + } + } + + return false; +} + +EXAMPLE_SIMPLE_TEST(GIVEN_initial_value_WHEN_performing_background_compuation_THEN_the_result_is_correct) +{ + TSharedPtr ResultPtr = MakeShared(); + + ADD_LATENT_AUTOMATION_COMMAND(FStartBackgroundThreadComputation(ResultPtr)); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForComputationAndCheckResult(this, ResultPtr)); + + return true; +} + +// 2. Simple test example +EXAMPLE_SIMPLE_TEST(GIVEN_one_and_two_WHEN_summed_THEN_the_sum_is_three) +{ + int X = 1; + int Y = 2; + int Sum = X + Y; + + TestTrue("The sum is correct", Sum == 3); + + return true; +} + +// 3. Complex test example +EXAMPLE_COMPLEX_TEST(ComplexTest); +bool ComplexTest::RunTest(const FString& Parameters) +{ + TArray OutArray; + Parameters.ParseIntoArrayWS(OutArray); + + if (OutArray.Num() != 3) + { + UE_LOG(LogSpatialGDKExamples, Error, TEXT("Invalid Test Input")); + TestTrue("The input is valid", false); + return true; + } + + TArray ArgsAndResult; + for (const auto& Value : OutArray) + { + if (Value.IsNumeric()) + { + ArgsAndResult.Push(FCString::Atoi(*Value)); + } + } + + int Sum = ArgsAndResult[0] + ArgsAndResult[1]; + TestTrue("The sum is correct", Sum == ArgsAndResult[2]); + + return true; +} + +void ComplexTest::GetTests(TArray& OutBeautifiedNames, TArray& OutTestCommands) const +{ + OutBeautifiedNames.Add(TEXT("GIVEN_two_and_two_WHEN_summed_THEN_the_sum_is_four")); + OutTestCommands.Add(TEXT("2 2 4")); + + OutBeautifiedNames.Add(TEXT("GIVEN_three_and_five_WHEN_summed_THEN_the_sum_is_eight")); + OutTestCommands.Add(TEXT("3 5 8")); +} + +// 4. Example of test fixture +namespace +{ +const FString ExampleTestFolder = FPaths::Combine(FPaths::ProjectContentDir(), TEXT("ExampleTests/")); + +class ExampleTestFixture +{ +public: + ExampleTestFixture() + { + CreateTestFolders(); + } + ~ExampleTestFixture() + { + DeleteTestFolders(); + } + +private: + + void CreateTestFolders() + { + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + PlatformFile.CreateDirectoryTree(*ExampleTestFolder); + } + + void DeleteTestFolders() + { + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + PlatformFile.DeleteDirectoryRecursively(*ExampleTestFolder); + } +}; +} // anonymous namespace + +EXAMPLE_SIMPLE_TEST(GIVEN_empty_folder_WHEN_creating_a_file_THEN_the_file_has_been_created) +{ + ExampleTestFixture Fixture; + + FString FilePath = FPaths::Combine(ExampleTestFolder, TEXT("Example.txt")); + + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + PlatformFile.OpenWrite(*FilePath); + + TestTrue("Example.txt exists", PlatformFile.FileExists(*FilePath)); + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/Examples/SpatialGDKTestGuidelines.h b/SpatialGDK/Source/SpatialGDKTests/Examples/SpatialGDKTestGuidelines.h new file mode 100644 index 0000000000..2bac817b25 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/Examples/SpatialGDKTestGuidelines.h @@ -0,0 +1,86 @@ +/* +1. How to run tests: + - Tests can be run via Session Frontend in UE4 Editor + (https://docs.unrealengine.com/en-US/Programming/Automation/index.html has more information) + + - Tests can be run via command line: + {PathToUnreal}\Engine\Binaries\Win64\UE4Editor-Cmd.exe ^ + "{PathToUProjectFile}.uproject" ^ + -unattended -nopause -NullRHI -log -log=RunTests.log ^ + -ExecCmds="Automation RunTests {TestFilter}; Quit" + + where {TestFilter} is a match on the test names + (e.g SpatialGDK matches all GDK tests, FRPCContainer matches all tests in RPCContainerTest) + +2. Folder structure + - Testing code that is not a part of exposed API + - These tests should go into the Private\Tests directory within the relevant module. + When an Automation Test matches one-to-one with a particular class, the test file should be named [ClassFilename]Test.cpp, + e.g. a test that applies only to FText would be written in TextTest.cpp. + + - Testing code that is a part of exposed API or Integration tests + - These tests are located in a separate SpatialGDKTests module. + So for every component that needs to be tested - it has to be exposed via corresponding macro. + E.g. `class FRPCContainer` -> `class SPATIALGDK_API FRPCContainer`, to make FRPCContainer testable. + The macro is different in each module SPATIALGDK_API, SPATIALGDKSERVICES_API etc. + + - The folder structure inside SpatialGDKTests should resemble the folder structure for components being tested. + E.g. If file tested is + `UnrealGDK\SpatialGDK\Source\SpatialGDK\Public\Utils\RPCContainer.cpp`, + then the corresponding test should be located in + `UnrealGDK\SpatialGDK\Source\SpatialGDKTests\SpatialGDK\Utils\RPCContainer\RPCContainerTest.cpp`. + There should be a folder with the same name as component tested. It should include the test and all the supporting files. + + - In case of integration tests, that involve multiple components - they should be located in + `UnrealGDK\SpatialGDK\Source\SpatialGDKTests\IntegrationTests\` folder. + + - Tests should be stripped out in Shipping builds. + If tests are not in SpatialGDKTests module - their code should be surrounded with `#if !UE_BUILD_SHIPPING` macro + SpatialGDKTests module should be excluded in shipping builds. + +3. Test definitions (check TestDefinitions.h for more info) + - We have defined 3 types of Macro to be used when writing tests: + (https://docs.unrealengine.com/en-US/Programming/Automation/TechnicalGuide/index.html has more information) + GDK_TEST - a simple test, that should be used if it's one of a kind (i.e. it's body can't be reused, otherwise use GDK_COMPLEX_TEST), + and if doesn't rely on background threads doing the computation (otherwise use LATENT_COMMANDs). + GDK_COMPLEX_TEST - same as simple test, but allows having multiple test cases run through the same test function body. + DEFINE_LATENT_COMMAND... - used to run tests that are expected to run across multiple ticks. + Latent command names should start with `F` (e.g. DEFINE_LATENT_AUTOMATION_COMMAND(FStartDeployment)"). + + - There are 5 types of mock objects we can use in tests: + Dummy objects + are passed around but never actually used. Usually they are just used to fill parameter lists. + + Fake objects + actually have working implementations, but usually take some shortcut which makes them not suitable for production (an InMemoryTestDatabase is a good example). + + Stubs + provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test. + + Spies + are stubs that also record some information based on how they were called. + One form of this might be an email service that records how many messages it was sent. + + Mocks + are pre-programmed with expectations which form a specification of the calls they are expected to receive. + They can throw an exception if they receive a call they don't expect and are checked during verification to ensure they got all the calls they were expecting. + +4. Test naming convention + - `GIVEN_WHEN_THEN` should be used. + E.g. `GIVEN_one_and_two_WHEN_summed_THEN_the_sum_is_three` + +5. Test coverage + - Unit tests should be isolated: Tests should be runnable on any machine, in any order, without affecting each other. + If possible, tests should have no dependencies on environmental factors or global/external state. + - Unit tests should verify a single use-case or code path: + they are simpler and more understandable, and that is good for maintainability and debugging. + - Unit tests should use a minimal (1, when possible) number of TestTrue/TestFalse assertions, + that covers only what is needed for the use-case/code path you are testing. + +6. Test fixtures (to perform the setup and cleanup before and after test correspondingly) + - There are no Test Fixtures out of the box in Unreal Automation Testing Framework + The solution for now is to instantiate an object at the beginning of test, + that sets up the environment in the constructor and cleans it up in the destructor + (however, that cannot be used with Latent Commands, + since the destructor will likely to be called earlier than necessary). +*/ diff --git a/SpatialGDK/Source/SpatialGDKTests/Private/SpatialGDKTestsModule.cpp b/SpatialGDK/Source/SpatialGDKTests/Private/SpatialGDKTestsModule.cpp index 891276c00c..4a3531cf90 100644 --- a/SpatialGDK/Source/SpatialGDKTests/Private/SpatialGDKTestsModule.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/Private/SpatialGDKTestsModule.cpp @@ -10,8 +10,11 @@ DEFINE_LOG_CATEGORY(LogSpatialGDKTests); IMPLEMENT_MODULE(FSpatialGDKTestsModule, SpatialGDKTests); +void InitializeSpatialFlagEarlyValues(); + void FSpatialGDKTestsModule::StartupModule() { + InitializeSpatialFlagEarlyValues(); } void FSpatialGDKTestsModule::ShutdownModule() diff --git a/SpatialGDK/Source/SpatialGDKTests/Public/SpatialGDKTestsModule.h b/SpatialGDK/Source/SpatialGDKTests/Public/SpatialGDKTestsModule.h index 0d4b1bf2b9..562e21dc5c 100644 --- a/SpatialGDK/Source/SpatialGDKTests/Public/SpatialGDKTestsModule.h +++ b/SpatialGDK/Source/SpatialGDKTests/Public/SpatialGDKTestsModule.h @@ -1,4 +1,5 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved + #pragma once #include "LocalDeploymentManager.h" diff --git a/SpatialGDK/Source/SpatialGDKTests/Public/TestDefinitions.h b/SpatialGDK/Source/SpatialGDKTests/Public/TestDefinitions.h deleted file mode 100644 index 87d0e3f095..0000000000 --- a/SpatialGDK/Source/SpatialGDKTests/Public/TestDefinitions.h +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#pragma once - -#include "Misc/AutomationTest.h" - -#define GDK_TEST(ModuleName, ComponentName, TestName) \ - IMPLEMENT_SIMPLE_AUTOMATION_TEST(TestName, "SpatialGDK."#ModuleName"."#ComponentName"."#TestName, EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::EngineFilter) \ - bool TestName::RunTest(const FString& Parameters) - -/* -Dummy objects - are passed around but never actually used. Usually they are just used to fill parameter lists. - -Fake objects - actually have working implementations, but usually take some shortcut which makes them not suitable for production (an InMemoryTestDatabase is a good example). - -Stubs - provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test. - -Spies - are stubs that also record some information based on how they were called. - One form of this might be an email service that records how many messages it was sent. - -Mocks - are pre-programmed with expectations which form a specification of the calls they are expected to receive. - They can throw an exception if they receive a call they don't expect and are checked during verification to ensure they got all the calls they were expecting. -*/ diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/EngineClasses/SpatialVirtualWorkerTranslationManager/SpatialVirtualWorkerTranslationManagerTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/EngineClasses/SpatialVirtualWorkerTranslationManager/SpatialVirtualWorkerTranslationManagerTest.cpp new file mode 100644 index 0000000000..5d59fecf79 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/EngineClasses/SpatialVirtualWorkerTranslationManager/SpatialVirtualWorkerTranslationManagerTest.cpp @@ -0,0 +1,148 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/TestDefinitions.h" + +#include "EngineClasses/SpatialVirtualWorkerTranslator.h" +#include "Interop/Connection/SpatialWorkerConnection.h" +#include "Interop/SpatialReceiver.h" +#include "SpatialConstants.h" +#include "SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialOSWorkerInterface/SpatialOSWorkerConnectionSpy.h" +#include "SpatialGDKTests/SpatialGDK/Interop/SpatialOSDispatcherInterface/SpatialOSDispatcherSpy.h" +#include "Utils/SchemaUtils.h" +#include "UObject/UObjectGlobals.h" + +#include "CoreMinimal.h" + +#include + +#define VIRTUALWORKERTRANSLATIONMANAGER_TEST(TestName) \ + GDK_TEST(Core, SpatialVirtualWorkerTranslationManager, TestName) +namespace +{ + +// Given a TranslationManager, Dispatcher, and Connection, give the TranslationManager authority +// so that it registers a QueryDelegate with the Dispatcher Mock, then query for that Delegate +// and return it so that tests can focus on the Delegate's correctness. +EntityQueryDelegate* SetupQueryDelegateTests(SpatialVirtualWorkerTranslationManager* Manager, SpatialOSDispatcherSpy* Dispatcher, SpatialOSWorkerConnectionSpy* Connection) +{ + // Build an authority change op which gives the worker authority over the translation. + Worker_AuthorityChangeOp QueryOp; + QueryOp.entity_id = SpatialConstants::INITIAL_VIRTUAL_WORKER_TRANSLATOR_ENTITY_ID; + QueryOp.component_id = SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID; + QueryOp.authority = WORKER_AUTHORITY_AUTHORITATIVE; + + // Let the Manager know it should have authority. This should trigger an EntityQuery and register a response delegate. + Manager->AuthorityChanged(QueryOp); + Worker_RequestId EntityQueryRequestId = Connection->GetLastRequestId(); + EntityQueryDelegate* Delegate = Dispatcher->GetEntityQueryDelegate(EntityQueryRequestId); + Connection->ClearLastEntityQuery(); + + return Delegate; +} + +} // anonymous namespace + +VIRTUALWORKERTRANSLATIONMANAGER_TEST(Given_an_authority_change_THEN_query_for_worker_entities_when_appropriate) +{ + TUniquePtr Connection = MakeUnique(); + TUniquePtr Dispatcher = MakeUnique(); + TUniquePtr Manager = MakeUnique(Dispatcher.Get(), Connection.Get(), nullptr); + + // Build an authority change op which gives the worker authority over the translation. + Worker_AuthorityChangeOp QueryOp; + QueryOp.entity_id = SpatialConstants::INITIAL_VIRTUAL_WORKER_TRANSLATOR_ENTITY_ID; + QueryOp.component_id = SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID; + QueryOp.authority = WORKER_AUTHORITY_AUTHORITATIVE; + + Manager->AuthorityChanged(QueryOp); + TestTrue("On gaining authority, the TranslationManager queried for server worker entities.", Connection->GetLastEntityQuery() != nullptr); + + EntityQueryDelegate* Delegate = Dispatcher->GetEntityQueryDelegate(0); + TestTrue("An EntityQueryDelegate was added to the dispatcher when the query was made", Delegate != nullptr); + + Connection->ClearLastEntityQuery(); + Manager->AuthorityChanged(QueryOp); + TestTrue("TranslationManager doesn't make a second query if one is in flight.", Connection->GetLastEntityQuery() == nullptr); + + return true; +} + +VIRTUALWORKERTRANSLATIONMANAGER_TEST(Given_a_failed_query_response_THEN_query_again) +{ + TUniquePtr Connection = MakeUnique(); + TUniquePtr Dispatcher = MakeUnique(); + TUniquePtr Translator = MakeUnique(nullptr, SpatialConstants::TRANSLATOR_UNSET_PHYSICAL_NAME); + TUniquePtr Manager = MakeUnique(Dispatcher.Get(), Connection.Get(), Translator.Get()); + + EntityQueryDelegate* Delegate = SetupQueryDelegateTests(Manager.Get(), Dispatcher.Get(), Connection.Get()); + + Worker_EntityQueryResponseOp ResponseOp; + ResponseOp.status_code = WORKER_STATUS_CODE_TIMEOUT; + ResponseOp.result_count = 0; + ResponseOp.message = "Failed call"; + + TSet VirtualWorkerIds; + VirtualWorkerIds.Add(1); + Manager->AddVirtualWorkerIds(VirtualWorkerIds); + + Delegate->ExecuteIfBound(ResponseOp); + TestTrue("After a failed query response, the TranslationManager queried again for server worker entities.", Connection->GetLastEntityQuery() != nullptr); + + return true; +} + +VIRTUALWORKERTRANSLATIONMANAGER_TEST(Given_a_successful_query_without_enough_workers_THEN_query_again) +{ + TUniquePtr Connection = MakeUnique(); + TUniquePtr Dispatcher = MakeUnique(); + TUniquePtr Translator = MakeUnique(nullptr, SpatialConstants::TRANSLATOR_UNSET_PHYSICAL_NAME); + TUniquePtr Manager = MakeUnique(Dispatcher.Get(), Connection.Get(), Translator.Get()); + + EntityQueryDelegate* Delegate = SetupQueryDelegateTests(Manager.Get(), Dispatcher.Get(), Connection.Get()); + + Worker_EntityQueryResponseOp ResponseOp; + ResponseOp.status_code = WORKER_STATUS_CODE_SUCCESS; + ResponseOp.result_count = 0; + ResponseOp.message = "Successfully returned 0 entities"; + + // Make sure the TranslationManager is expecting more workers than are returned. + TSet VirtualWorkerIds; + VirtualWorkerIds.Add(1); + Manager->AddVirtualWorkerIds(VirtualWorkerIds); + + Delegate->ExecuteIfBound(ResponseOp); + TestTrue("When not enough workers available, the TranslationManager queried again for server worker entities.", Connection->GetLastEntityQuery() != nullptr); + + return true; +} + +VIRTUALWORKERTRANSLATIONMANAGER_TEST(Given_a_successful_query_with_invalid_workers_THEN_query_again) +{ + TUniquePtr Connection = MakeUnique(); + TUniquePtr Dispatcher = MakeUnique(); + TUniquePtr Translator = MakeUnique(nullptr, SpatialConstants::TRANSLATOR_UNSET_PHYSICAL_NAME); + TUniquePtr Manager = MakeUnique(Dispatcher.Get(), Connection.Get(), Translator.Get()); + + EntityQueryDelegate* Delegate = SetupQueryDelegateTests(Manager.Get(), Dispatcher.Get(), Connection.Get()); + + // This is an invalid entity to be returned, because it doesn't have a "Worker" component on it. + Worker_Entity worker; + worker.entity_id = 1001; + worker.component_count = 0; + + Worker_EntityQueryResponseOp ResponseOp; + ResponseOp.status_code = WORKER_STATUS_CODE_SUCCESS; + ResponseOp.result_count = 0; + ResponseOp.message = "Successfully returned 0 entities"; + ResponseOp.results = &worker; + + // Make sure the TranslationManager is only expecting a single worker. + TSet VirtualWorkerIds; + VirtualWorkerIds.Add(1); + Manager->AddVirtualWorkerIds(VirtualWorkerIds); + + Delegate->ExecuteIfBound(ResponseOp); + TestTrue("When enough workers available but they are invalid, the TranslationManager queried again for server worker entities.", Connection->GetLastEntityQuery() != nullptr); + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/EngineClasses/SpatialVirtualWorkerTranslator/SpatialVirtualWorkerTranslatorTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/EngineClasses/SpatialVirtualWorkerTranslator/SpatialVirtualWorkerTranslatorTest.cpp index f03a0a7836..53a10e985a 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/EngineClasses/SpatialVirtualWorkerTranslator/SpatialVirtualWorkerTranslatorTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/EngineClasses/SpatialVirtualWorkerTranslator/SpatialVirtualWorkerTranslatorTest.cpp @@ -1,120 +1,254 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved -#include "TestDefinitions.h" +#include "SpatialGDKTests/SpatialGDK/LoadBalancing/AbstractLBStrategy/LBStrategyStub.h" +#include "Tests/TestingSchemaHelpers.h" + +#include "Tests/TestDefinitions.h" #include "EngineClasses/SpatialVirtualWorkerTranslator.h" +#include "SpatialCommonTypes.h" +#include "SpatialConstants.h" #include "Utils/SchemaUtils.h" -#include "UObject/UObjectGlobals.h" -#include "CoreMinimal.h" +#include "Templates/UniquePtr.h" +#include "UObject/UObjectGlobals.h" #include +#include #define VIRTUALWORKERTRANSLATOR_TEST(TestName) \ GDK_TEST(Core, SpatialVirtualWorkerTranslator, TestName) -VIRTUALWORKERTRANSLATOR_TEST(Given_init_is_not_called_THEN_return_not_ready) +VIRTUALWORKERTRANSLATOR_TEST(GIVEN_init_is_not_called_THEN_return_not_ready) +{ + ULBStrategyStub* LBStrategyStub = NewObject(); + TUniquePtr Translator = MakeUnique(LBStrategyStub, SpatialConstants::TRANSLATOR_UNSET_PHYSICAL_NAME); + + TestFalse("Translator without local virtual worker ID is not ready.", Translator->IsReady()); + TestEqual("LBStrategy stub reports an invalid virtual worker ID.", LBStrategyStub->GetVirtualWorkerId(), SpatialConstants::INVALID_VIRTUAL_WORKER_ID); + + return true; +} + +VIRTUALWORKERTRANSLATOR_TEST(GIVEN_worker_name_specified_in_constructor_THEN_return_correct_local_worker_name) { - TUniquePtr translator = MakeUnique(); + TUniquePtr Translator = MakeUnique(nullptr, "my_worker_name"); - TestFalse("Uninitialized Translator is not ready.", translator->IsReady()); + TestEqual("Local physical worker name returned correctly", Translator->GetLocalPhysicalWorkerName(), "my_worker_name"); return true; } -VIRTUALWORKERTRANSLATOR_TEST(Given_init_and_set_desired_worker_count_called_THEN_return_ready) +VIRTUALWORKERTRANSLATOR_TEST(GIVEN_no_mapping_WHEN_nothing_has_changed_THEN_return_no_mappings_and_unintialized_state) { - TUniquePtr translator = MakeUnique(); - translator->Init(nullptr); - translator->SetDesiredVirtualWorkerCount(1); // unimportant random value. + ULBStrategyStub* LBStrategyStub = NewObject(); + TUniquePtr Translator = MakeUnique(LBStrategyStub, SpatialConstants::TRANSLATOR_UNSET_PHYSICAL_NAME); - TestTrue("Initialized Translator is ready.", translator->IsReady()); + TestNull("Worker 1 doesn't exist", Translator->GetPhysicalWorkerForVirtualWorker(1)); + TestEqual("Local virtual worker ID is not known.", Translator->GetLocalVirtualWorkerId(), SpatialConstants::INVALID_VIRTUAL_WORKER_ID); + TestFalse("Translator without local virtual worker ID is not ready.", Translator->IsReady()); + TestEqual("LBStrategy stub reports an invalid virtual worker ID.", LBStrategyStub->GetVirtualWorkerId(), SpatialConstants::INVALID_VIRTUAL_WORKER_ID); return true; } -VIRTUALWORKERTRANSLATOR_TEST(Given_no_mapping_WHEN_nothing_has_changed_THEN_return_no_mappings) +VIRTUALWORKERTRANSLATOR_TEST(GIVEN_no_mapping_WHEN_receiving_empty_mapping_THEN_ignore_it) { - // The class is initialized with no data. - TUniquePtr translator = MakeUnique(); + ULBStrategyStub* LBStrategyStub = NewObject(); + TUniquePtr Translator = MakeUnique(LBStrategyStub, SpatialConstants::TRANSLATOR_UNSET_PHYSICAL_NAME); + + // Create an empty mapping. + Schema_Object* DataObject = TestingSchemaHelpers::CreateTranslationComponentDataFields(); - TestTrue("Worker 1 doesn't exist", translator->GetPhysicalWorkerForVirtualWorker(1) == nullptr); + // Now apply the mapping to the translator and test the result. Because the mapping is empty, + // it should ignore the mapping and continue to report an empty mapping. + Translator->ApplyVirtualWorkerManagerData(DataObject); + + TestEqual("Local virtual worker ID is not known.", Translator->GetLocalVirtualWorkerId(), SpatialConstants::INVALID_VIRTUAL_WORKER_ID); + TestFalse("Translator without local virtual worker ID is not ready.", Translator->IsReady()); + TestEqual("LBStrategy stub reports an invalid virtual worker ID.", LBStrategyStub->GetVirtualWorkerId(), SpatialConstants::INVALID_VIRTUAL_WORKER_ID); return true; } -VIRTUALWORKERTRANSLATOR_TEST(Given_no_mapping_WHEN_it_is_updated_THEN_return_the_updated_mapping) +VIRTUALWORKERTRANSLATOR_TEST(GIVEN_no_mapping_WHEN_receiving_incomplete_mapping_THEN_ignore_it) { - // The class is initialized with no data. - TUniquePtr translator = MakeUnique(); + ULBStrategyStub* LBStrategyStub = NewObject(); + TUniquePtr Translator = MakeUnique(LBStrategyStub, SpatialConstants::TRANSLATOR_UNSET_PHYSICAL_NAME); // Create a base mapping. - Worker_ComponentData Data = {}; - Data.component_id = SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID; - Data.schema_type = Schema_CreateComponentData(); - Schema_Object* DataObject = Schema_GetComponentDataFields(Data.schema_type); + Schema_Object* DataObject = TestingSchemaHelpers::CreateTranslationComponentDataFields(); // The mapping only has the following entries: - // VirtualToPhysicalWorkerMapping.Add(2, "VW_E"); - // VirtualToPhysicalWorkerMapping.Add(3, "VW_F"); - Schema_Object* FirstEntryObject = Schema_AddObject(DataObject, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_MAPPING_ID); - Schema_AddUint32(FirstEntryObject, SpatialConstants::MAPPING_VIRTUAL_WORKER_ID, 2); - SpatialGDK::AddStringToSchema(FirstEntryObject, SpatialConstants::MAPPING_PHYSICAL_WORKER_NAME, "VW_E"); + TestingSchemaHelpers::AddTranslationComponentDataMapping(DataObject, 1, "ValidWorkerOne"); + TestingSchemaHelpers::AddTranslationComponentDataMapping(DataObject, 2, "ValidWorkerTwo"); + + // Now apply the mapping to the translator and test the result. Because the mapping doesn't have an entry for this translator, + // it should reject the mapping and continue to report an empty mapping. + Translator->ApplyVirtualWorkerManagerData(DataObject); - Schema_Object* SecondEntryObject = Schema_AddObject(DataObject, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_MAPPING_ID); - Schema_AddUint32(SecondEntryObject, SpatialConstants::MAPPING_VIRTUAL_WORKER_ID, 3); - SpatialGDK::AddStringToSchema(SecondEntryObject, SpatialConstants::MAPPING_PHYSICAL_WORKER_NAME, "VW_F"); + TestNull("There is no mapping for virtual worker 1", Translator->GetPhysicalWorkerForVirtualWorker(1)); + TestNull("There is no mapping for virtual worker 2", Translator->GetPhysicalWorkerForVirtualWorker(2)); + + TestEqual("Local virtual worker ID is not known.", Translator->GetLocalVirtualWorkerId(), SpatialConstants::INVALID_VIRTUAL_WORKER_ID); + TestFalse("Translator without local virtual worker ID is not ready.", Translator->IsReady()); + TestEqual("LBStrategy stub reports an invalid virtual worker ID.", LBStrategyStub->GetVirtualWorkerId(), SpatialConstants::INVALID_VIRTUAL_WORKER_ID); + + return true; +} + +VIRTUALWORKERTRANSLATOR_TEST(GIVEN_no_mapping_WHEN_a_valid_mapping_is_received_THEN_return_the_updated_mapping_and_become_ready) +{ + ULBStrategyStub* LBStrategyStub = NewObject(); + TUniquePtr Translator = MakeUnique(LBStrategyStub, "ValidWorkerOne"); + + // Create a base mapping. + Schema_Object* DataObject = TestingSchemaHelpers::CreateTranslationComponentDataFields(); + + // The mapping only has the following entries: + TestingSchemaHelpers::AddTranslationComponentDataMapping(DataObject, 1, "ValidWorkerOne"); + TestingSchemaHelpers::AddTranslationComponentDataMapping(DataObject, 2, "ValidWorkerTwo"); // Now apply the mapping to the translator and test the result. - Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); - translator->ApplyVirtualWorkerManagerData(ComponentObject); - - TestTrue("There is a mapping for virtual worker 2", translator->GetPhysicalWorkerForVirtualWorker(2) != nullptr); - TestTrue("VW_B overwritten with VW_E", translator->GetPhysicalWorkerForVirtualWorker(2)->Equals("VW_E")); + Translator->ApplyVirtualWorkerManagerData(DataObject); + + const PhysicalWorkerName* VirtualWorker1PhysicalName = Translator->GetPhysicalWorkerForVirtualWorker(1); + TestNotNull("There is a mapping for virtual worker 1", VirtualWorker1PhysicalName); + TestEqual("Virtual worker 1 is ValidWorkerOne", *VirtualWorker1PhysicalName, "ValidWorkerOne"); + + const PhysicalWorkerName* VirtualWorker2PhysicalName = Translator->GetPhysicalWorkerForVirtualWorker(2); + TestNotNull("There is a mapping for virtual worker 2", VirtualWorker2PhysicalName); + TestEqual("VirtualWorker 2 is ValidWorkerTwo", *VirtualWorker2PhysicalName, "ValidWorkerTwo"); - TestTrue("There is a mapping for virtual worker 3", translator->GetPhysicalWorkerForVirtualWorker(3) != nullptr); - TestTrue("VW_B overwritten with VW_F", translator->GetPhysicalWorkerForVirtualWorker(3)->Equals("VW_F")); + TestNull("There is no mapping for virtual worker 3", Translator->GetPhysicalWorkerForVirtualWorker(3)); - TestTrue("There is no mapping for virtual worker 1", translator->GetPhysicalWorkerForVirtualWorker(1) == nullptr); + TestEqual("Local virtual worker ID is known.", Translator->GetLocalVirtualWorkerId(), 1); + TestTrue("Translator with local virtual worker ID is ready.", Translator->IsReady()); + TestEqual("LBStrategy stub reports the correct virtual worker ID.", LBStrategyStub->GetVirtualWorkerId(), 1); return true; } -VIRTUALWORKERTRANSLATOR_TEST(Given_no_mapping_WHEN_query_response_received_THEN_return_the_updated_mapping) +VIRTUALWORKERTRANSLATOR_TEST(GIVEN_have_a_valid_mapping_WHEN_an_invalid_mapping_is_received_THEN_ignore_it) { - // The class is initialized with no data. - TUniquePtr translator = MakeUnique(); - - + ULBStrategyStub* LBStrategyStub = NewObject(); + TUniquePtr Translator = MakeUnique(LBStrategyStub, "ValidWorkerOne"); // Create a base mapping. - Worker_ComponentData Data = {}; - Data.component_id = SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID; - Data.schema_type = Schema_CreateComponentData(); - Schema_Object* DataObject = Schema_GetComponentDataFields(Data.schema_type); + Schema_Object* ValidDataObject = TestingSchemaHelpers::CreateTranslationComponentDataFields(); // The mapping only has the following entries: - // VirtualToPhysicalWorkerMapping.Add(2, "VW_E"); - // VirtualToPhysicalWorkerMapping.Add(3, "VW_F"); - Schema_Object* FirstEntryObject = Schema_AddObject(DataObject, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_MAPPING_ID); - Schema_AddUint32(FirstEntryObject, SpatialConstants::MAPPING_VIRTUAL_WORKER_ID, 2); - SpatialGDK::AddStringToSchema(FirstEntryObject, SpatialConstants::MAPPING_PHYSICAL_WORKER_NAME, "VW_E"); + TestingSchemaHelpers::AddTranslationComponentDataMapping(ValidDataObject, 1, "ValidWorkerOne"); + TestingSchemaHelpers::AddTranslationComponentDataMapping(ValidDataObject, 2, "ValidWorkerTwo"); - Schema_Object* SecondEntryObject = Schema_AddObject(DataObject, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_MAPPING_ID); - Schema_AddUint32(SecondEntryObject, SpatialConstants::MAPPING_VIRTUAL_WORKER_ID, 3); - SpatialGDK::AddStringToSchema(SecondEntryObject, SpatialConstants::MAPPING_PHYSICAL_WORKER_NAME, "VW_F"); + // Apply valid mapping to the translator. + Translator->ApplyVirtualWorkerManagerData(ValidDataObject); - // Now apply the mapping to the translator and test the result. - Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); - translator->ApplyVirtualWorkerManagerData(ComponentObject); + // Create an empty mapping. + Worker_ComponentData EmptyData = {}; + EmptyData.component_id = SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID; + EmptyData.schema_type = Schema_CreateComponentData(); + Schema_Object* EmptyDataObject = Schema_GetComponentDataFields(EmptyData.schema_type); + + // Now apply the mapping to the translator and test the result. Because the mapping is empty, + // it should ignore the mapping and continue to report a valid mapping. + Translator->ApplyVirtualWorkerManagerData(EmptyDataObject); + + // Translator should return the values from the initial valid mapping + const PhysicalWorkerName* VirtualWorker1PhysicalName = Translator->GetPhysicalWorkerForVirtualWorker(1); + TestNotNull("There is a mapping for virtual worker 1", VirtualWorker1PhysicalName); + TestEqual("Virtual worker 1 is ValidWorkerOne", *VirtualWorker1PhysicalName, "ValidWorkerOne"); + + const PhysicalWorkerName* VirtualWorker2PhysicalName = Translator->GetPhysicalWorkerForVirtualWorker(2); + TestNotNull("There is a mapping for virtual worker 2", VirtualWorker2PhysicalName); + TestEqual("VirtualWorker 2 is ValidWorkerTwo", *VirtualWorker2PhysicalName, "ValidWorkerTwo"); + + TestNull("There is no mapping for virtual worker 3", Translator->GetPhysicalWorkerForVirtualWorker(3)); + + TestEqual("Local virtual worker ID is known.", Translator->GetLocalVirtualWorkerId(), 1); + TestTrue("Translator with local virtual worker ID is ready.", Translator->IsReady()); + TestEqual("LBStrategy stub reports the correct virtual worker ID.", LBStrategyStub->GetVirtualWorkerId(), 1); + + return true; +} + +VIRTUALWORKERTRANSLATOR_TEST(GIVEN_have_a_valid_mapping_WHEN_another_valid_mapping_is_received_THEN_update_accordingly) +{ + ULBStrategyStub* LBStrategyStub = NewObject(); + TUniquePtr Translator = MakeUnique(LBStrategyStub, "ValidWorkerOne"); + + // Create a valid initial mapping. + Schema_Object* FirstValidDataObject = TestingSchemaHelpers::CreateTranslationComponentDataFields(); + + // The mapping only has the following entries: + TestingSchemaHelpers::AddTranslationComponentDataMapping(FirstValidDataObject, 1, "ValidWorkerOne"); + TestingSchemaHelpers::AddTranslationComponentDataMapping(FirstValidDataObject, 2, "ValidWorkerTwo"); + + // Apply valid mapping to the translator. + Translator->ApplyVirtualWorkerManagerData(FirstValidDataObject); + + // Create a second mapping. + Schema_Object* SecondValidDataObject = TestingSchemaHelpers::CreateTranslationComponentDataFields(); + + // The mapping only has the following entries: + TestingSchemaHelpers::AddTranslationComponentDataMapping(SecondValidDataObject, 1, "ValidWorkerOne"); + TestingSchemaHelpers::AddTranslationComponentDataMapping(SecondValidDataObject, 2, "ValidWorkerThree"); + + // Apply valid mapping to the translator. + Translator->ApplyVirtualWorkerManagerData(SecondValidDataObject); + + // Translator should return the values from the new mapping + const PhysicalWorkerName* VirtualWorker1PhysicalName = Translator->GetPhysicalWorkerForVirtualWorker(1); + TestNotNull("There is a mapping for virtual worker 1", VirtualWorker1PhysicalName); + TestEqual("Virtual worker 1 is ValidWorkerOne", *VirtualWorker1PhysicalName, "ValidWorkerOne"); + + const PhysicalWorkerName* VirtualWorker2PhysicalName = Translator->GetPhysicalWorkerForVirtualWorker(2); + TestNotNull("There is an updated mapping for virtual worker 2", VirtualWorker2PhysicalName); + TestEqual("VirtualWorker 2 is ValidWorkerThree", *VirtualWorker2PhysicalName, "ValidWorkerThree"); + + TestEqual("Local virtual worker ID is still known.", Translator->GetLocalVirtualWorkerId(), 1); + TestTrue("Translator with local virtual worker ID is still ready.", Translator->IsReady()); + TestEqual("LBStrategy stub reports the correct virtual worker ID.", LBStrategyStub->GetVirtualWorkerId(), 1); + + return true; +} + + +VIRTUALWORKERTRANSLATOR_TEST(GIVEN_have_a_valid_mapping_WHEN_try_to_change_local_virtual_worker_id_THEN_ignore_it) +{ + ULBStrategyStub* LBStrategyStub = NewObject(); + TUniquePtr Translator = MakeUnique(LBStrategyStub, "ValidWorkerOne"); + + // Create a valid initial mapping. + Schema_Object* FirstValidDataObject = TestingSchemaHelpers::CreateTranslationComponentDataFields(); + + // The mapping only has the following entries: + // VirtualToPhysicalWorkerMapping.Add(1, "ValidWorkerOne"); + TestingSchemaHelpers::AddTranslationComponentDataMapping(FirstValidDataObject, 1, "ValidWorkerOne"); + + // Apply valid mapping to the translator. + Translator->ApplyVirtualWorkerManagerData(FirstValidDataObject); + + // Create a second initial mapping. + Schema_Object* SecondValidDataObject = TestingSchemaHelpers::CreateTranslationComponentDataFields(); + + // The mapping only has the following entries: + TestingSchemaHelpers::AddTranslationComponentDataMapping(SecondValidDataObject, 2, "ValidWorkerOne"); + + // Apply valid mapping to the translator. + AddExpectedError(TEXT("Received mapping containing a new and updated virtual worker ID, this shouldn't happen."), EAutomationExpectedErrorFlags::Contains, 1); + Translator->ApplyVirtualWorkerManagerData(SecondValidDataObject); - TestTrue("There is a mapping for virtual worker 2", translator->GetPhysicalWorkerForVirtualWorker(2) != nullptr); - TestTrue("VW_B overwritten with VW_E", translator->GetPhysicalWorkerForVirtualWorker(2)->Equals("VW_E")); + // Translator should return the values from the original mapping + const PhysicalWorkerName* VirtualWorker1PhysicalName = Translator->GetPhysicalWorkerForVirtualWorker(1); + TestNotNull("There is a mapping for virtual worker 1", VirtualWorker1PhysicalName); + TestEqual("Virtual worker 1 is ValidWorkerOne", *VirtualWorker1PhysicalName, "ValidWorkerOne"); - TestTrue("There is a mapping for virtual worker 3", translator->GetPhysicalWorkerForVirtualWorker(3) != nullptr); - TestTrue("VW_B overwritten with VW_F", translator->GetPhysicalWorkerForVirtualWorker(3)->Equals("VW_F")); + TestNull("There is no mapping for virtual worker 2", Translator->GetPhysicalWorkerForVirtualWorker(2)); - TestTrue("There is no mapping for virtual worker 1", translator->GetPhysicalWorkerForVirtualWorker(1) == nullptr); + TestEqual("Local virtual worker ID is still known.", Translator->GetLocalVirtualWorkerId(), 1); + TestTrue("Translator with local virtual worker ID is still ready.", Translator->IsReady()); + TestEqual("LBStrategy stub reports the correct virtual worker ID.", LBStrategyStub->GetVirtualWorkerId(), 1); return true; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialOSWorkerInterface/SpatialOSWorkerConnectionSpy.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialOSWorkerInterface/SpatialOSWorkerConnectionSpy.cpp new file mode 100644 index 0000000000..35b592149c --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialOSWorkerInterface/SpatialOSWorkerConnectionSpy.cpp @@ -0,0 +1,85 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialOSWorkerConnectionSpy.h" + +#include "Interop/Connection/OutgoingMessages.h" +#include "SpatialCommonTypes.h" +#include "Utils/SpatialLatencyTracer.h" + +#include +#include + +SpatialOSWorkerConnectionSpy::SpatialOSWorkerConnectionSpy() + : NextRequestId(0) + , LastEntityQuery(nullptr) +{} + +TArray SpatialOSWorkerConnectionSpy::GetOpList() +{ + return TArray(); +} + +Worker_RequestId SpatialOSWorkerConnectionSpy::SendReserveEntityIdsRequest(uint32_t NumOfEntities) +{ + return NextRequestId++; +} + +Worker_RequestId SpatialOSWorkerConnectionSpy::SendCreateEntityRequest(TArray&& Components, const Worker_EntityId* EntityId) +{ + return NextRequestId++; +} + +Worker_RequestId SpatialOSWorkerConnectionSpy::SendDeleteEntityRequest(Worker_EntityId EntityId) +{ + return NextRequestId++; +} + +void SpatialOSWorkerConnectionSpy::SendAddComponent(Worker_EntityId EntityId, FWorkerComponentData* ComponentData) +{} + +void SpatialOSWorkerConnectionSpy::SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) +{} + +void SpatialOSWorkerConnectionSpy::SendComponentUpdate(Worker_EntityId EntityId, const FWorkerComponentUpdate* ComponentUpdate) +{} + +Worker_RequestId SpatialOSWorkerConnectionSpy::SendCommandRequest(Worker_EntityId EntityId, const Worker_CommandRequest* Request, uint32_t CommandId) +{ + return NextRequestId++; +} + +void SpatialOSWorkerConnectionSpy::SendCommandResponse(Worker_RequestId RequestId, const Worker_CommandResponse* Response) +{} + +void SpatialOSWorkerConnectionSpy::SendCommandFailure(Worker_RequestId RequestId, const FString& Message) +{} + +void SpatialOSWorkerConnectionSpy::SendLogMessage(uint8_t Level, const FName& LoggerName, const TCHAR* Message) +{} + +void SpatialOSWorkerConnectionSpy::SendComponentInterest(Worker_EntityId EntityId, TArray&& ComponentInterest) +{} + +Worker_RequestId SpatialOSWorkerConnectionSpy::SendEntityQueryRequest(const Worker_EntityQuery* EntityQuery) +{ + LastEntityQuery = EntityQuery; + return NextRequestId++; +} + +void SpatialOSWorkerConnectionSpy::SendMetrics(const SpatialGDK::SpatialMetrics& Metrics) +{} + +const Worker_EntityQuery* SpatialOSWorkerConnectionSpy::GetLastEntityQuery() +{ + return LastEntityQuery; +} + +void SpatialOSWorkerConnectionSpy::ClearLastEntityQuery() +{ + LastEntityQuery = nullptr; +} + +Worker_RequestId SpatialOSWorkerConnectionSpy::GetLastRequestId() +{ + return NextRequestId - 1; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialOSWorkerInterface/SpatialOSWorkerConnectionSpy.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialOSWorkerInterface/SpatialOSWorkerConnectionSpy.h new file mode 100644 index 0000000000..eeb16bb3c7 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialOSWorkerInterface/SpatialOSWorkerConnectionSpy.h @@ -0,0 +1,50 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Interop/Connection/SpatialOSWorkerInterface.h" + +#include "Interop/Connection/OutgoingMessages.h" +#include "SpatialCommonTypes.h" +#include "Utils/SpatialLatencyTracer.h" + +#include +#include + +// The SpatialOSWorkerConnectionSpy is intended to be a very minimal implementation of a WorkerConnection which just records then swallows +// any data sent through it. It is intended to be extended with methods to query for what data has been sent through it. +// +// Only a few methods have meaningful implementations. You are intended to extend the implementations whenever needed +// for testing code which uses the WorkerConnection. + +class SpatialOSWorkerConnectionSpy : public SpatialOSWorkerInterface +{ +public: + SpatialOSWorkerConnectionSpy(); + + virtual TArray GetOpList() override; + virtual Worker_RequestId SendReserveEntityIdsRequest(uint32_t NumOfEntities) override; + virtual Worker_RequestId SendCreateEntityRequest(TArray&& Components, const Worker_EntityId* EntityId) override; + virtual Worker_RequestId SendDeleteEntityRequest(Worker_EntityId EntityId) override; + virtual void SendAddComponent(Worker_EntityId EntityId, FWorkerComponentData* ComponentData) override; + virtual void SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) override; + virtual void SendComponentUpdate(Worker_EntityId EntityId, const FWorkerComponentUpdate* ComponentUpdate) override; + virtual Worker_RequestId SendCommandRequest(Worker_EntityId EntityId, const Worker_CommandRequest* Request, uint32_t CommandId) override; + virtual void SendCommandResponse(Worker_RequestId RequestId, const Worker_CommandResponse* Response) override; + virtual void SendCommandFailure(Worker_RequestId RequestId, const FString& Message) override; + virtual void SendLogMessage(uint8_t Level, const FName& LoggerName, const TCHAR* Message) override; + virtual void SendComponentInterest(Worker_EntityId EntityId, TArray&& ComponentInterest) override; + virtual Worker_RequestId SendEntityQueryRequest(const Worker_EntityQuery* EntityQuery) override; + virtual void SendMetrics(const SpatialGDK::SpatialMetrics& Metrics) override; + + // The following methods are used to query for state in tests. + const Worker_EntityQuery* GetLastEntityQuery(); + void ClearLastEntityQuery(); + + Worker_RequestId GetLastRequestId(); + +private: + Worker_RequestId NextRequestId; + + const Worker_EntityQuery* LastEntityQuery; +}; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialWorkerConnectionTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialWorkerConnectionTest.cpp new file mode 100644 index 0000000000..68ba9d2385 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialWorkerConnectionTest.cpp @@ -0,0 +1,366 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/TestDefinitions.h" + +#include "Interop/Connection/SpatialConnectionManager.h" +#include "Interop/Connection/SpatialWorkerConnection.h" +#include "Interop/SpatialOutputDevice.h" +#include "SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.h" + +#include "CoreMinimal.h" + +#define WORKERCONNECTION_TEST(TestName) \ + GDK_TEST(Core, SpatialWorkerConnection, TestName) + +using namespace SpatialGDK; + +namespace +{ +bool bClientConnectionProcessed = false; +bool bServerConnectionProcessed = false; +const double MAX_WAIT_TIME = 10.0; + +void ConnectionProcessed(bool bConnectAsClient) +{ + if (bConnectAsClient) + { + bClientConnectionProcessed = true; + } + else + { + bServerConnectionProcessed = true; + } +} + +void StartSetupConnectionConfigFromURL(USpatialConnectionManager* ConnectionManager, const FURL& URL, bool& bOutUseReceptionist) +{ + bOutUseReceptionist = (URL.Host != SpatialConstants::LOCATOR_HOST) && !URL.HasOption(TEXT("locator")); + if (bOutUseReceptionist) + { + ConnectionManager->ReceptionistConfig.SetReceptionistHost(URL.Host); + } + else + { + FLocatorConfig& LocatorConfig = ConnectionManager->LocatorConfig; + LocatorConfig.PlayerIdentityToken = URL.GetOption(*SpatialConstants::URL_PLAYER_IDENTITY_OPTION, TEXT("")); + LocatorConfig.LoginToken = URL.GetOption(*SpatialConstants::URL_LOGIN_OPTION, TEXT("")); + } +} + +void FinishSetupConnectionConfig(USpatialConnectionManager* ConnectionManager, const FString& WorkerType, const FURL& URL, bool bUseReceptionist) +{ + // Finish setup for the config objects regardless of loading from command line or URL + if (bUseReceptionist) + { + // Use Receptionist + ConnectionManager->SetConnectionType(ESpatialConnectionType::Receptionist); + + FReceptionistConfig& ReceptionistConfig = ConnectionManager->ReceptionistConfig; + ReceptionistConfig.WorkerType = WorkerType; + + const TCHAR* UseExternalIpForBridge = TEXT("useExternalIpForBridge"); + if (URL.HasOption(UseExternalIpForBridge)) + { + FString UseExternalIpOption = URL.GetOption(UseExternalIpForBridge, TEXT("")); + ReceptionistConfig.UseExternalIp = !UseExternalIpOption.Equals(TEXT("false"), ESearchCase::IgnoreCase); + } + } + else + { + // Use Locator + ConnectionManager->SetConnectionType(ESpatialConnectionType::Locator); + FLocatorConfig& LocatorConfig = ConnectionManager->LocatorConfig; + FParse::Value(FCommandLine::Get(), TEXT("locatorHost"), LocatorConfig.LocatorHost); + LocatorConfig.WorkerType = WorkerType; + } +} +} // anonymous namespace + +DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FWaitForSeconds, double, Seconds); +bool FWaitForSeconds::Update() +{ + const double NewTime = FPlatformTime::Seconds(); + + if (NewTime - StartTime >= Seconds) + { + return true; + } + else + { + return false; + } +} + +DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER(FSetupWorkerConnection, USpatialConnectionManager*, ConnectionManager, bool, bConnectAsClient); +bool FSetupWorkerConnection::Update() +{ + const FURL TestURL = {}; + FString WorkerType = "AutomationWorker"; + + ConnectionManager->OnConnectedCallback.BindLambda([bConnectAsClient = this->bConnectAsClient]() + { + ConnectionProcessed(bConnectAsClient); + }); + ConnectionManager->OnFailedToConnectCallback.BindLambda([bConnectAsClient = this->bConnectAsClient](uint8_t ErrorCode, const FString& ErrorMessage) + { + ConnectionProcessed(bConnectAsClient); + }); + bool bUseReceptionist = false; + StartSetupConnectionConfigFromURL(ConnectionManager, TestURL, bUseReceptionist); + FinishSetupConnectionConfig(ConnectionManager, WorkerType, TestURL, bUseReceptionist); + int32 PlayInEditorID = 0; +#if WITH_EDITOR + ConnectionManager->Connect(bConnectAsClient, PlayInEditorID); +#else + ConnectionManager->Connect(bConnectAsClient, 0); +#endif + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND(FWaitForClientAndServerWorkerConnection); +bool FWaitForClientAndServerWorkerConnection::Update() +{ + return bClientConnectionProcessed && bServerConnectionProcessed; +} + +DEFINE_LATENT_AUTOMATION_COMMAND(FResetConnectionProcessed); +bool FResetConnectionProcessed::Update() +{ + bClientConnectionProcessed = false; + bServerConnectionProcessed = false; + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCheckConnectionStatus, FAutomationTestBase*, Test, USpatialConnectionManager*, ConnectionManager, bool, bExpectedIsConnected); +bool FCheckConnectionStatus::Update() +{ + Test->TestTrue(TEXT("Worker connection status is valid"), ConnectionManager->IsConnected() == bExpectedIsConnected); + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FSendReserveEntityIdsRequest, USpatialConnectionManager*, ConnectionManager); +bool FSendReserveEntityIdsRequest::Update() +{ + uint32_t NumOfEntities = 1; + USpatialWorkerConnection* Connection = ConnectionManager->GetWorkerConnection(); + Connection->SendReserveEntityIdsRequest(NumOfEntities); + + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FSendCreateEntityRequest, USpatialConnectionManager*, ConnectionManager); +bool FSendCreateEntityRequest::Update() +{ + TArray Components; + const Worker_EntityId* EntityId = nullptr; + USpatialWorkerConnection* Connection = ConnectionManager->GetWorkerConnection(); + Connection->SendCreateEntityRequest(MoveTemp(Components), EntityId); + + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FSendDeleteEntityRequest, USpatialConnectionManager*, ConnectionManager); +bool FSendDeleteEntityRequest::Update() +{ + const Worker_EntityId EntityId = 0; + USpatialWorkerConnection* Connection = ConnectionManager->GetWorkerConnection(); + Connection->SendDeleteEntityRequest(EntityId); + + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FFindWorkerResponseOfType, FAutomationTestBase*, Test, USpatialConnectionManager*, ConnectionManager, uint8_t, ExpectedOpType); +bool FFindWorkerResponseOfType::Update() +{ + bool bFoundOpOfExpectedType = false; + USpatialWorkerConnection* Connection = ConnectionManager->GetWorkerConnection(); + for (const auto& OpList : Connection->GetOpList()) + { + for (uint32_t i = 0; i < OpList->op_count; i++) + { + if (OpList->ops[i].op_type == ExpectedOpType) + { + bFoundOpOfExpectedType = true; + break; + } + } + } + + bool bReachedTimeout = false; + const double NewTime = FPlatformTime::Seconds(); + if (NewTime - StartTime >= MAX_WAIT_TIME) + { + bReachedTimeout = true; + } + + if (bFoundOpOfExpectedType || bReachedTimeout) + { + Test->TestTrue(TEXT("Received Worker Repsonse of expected type"), bFoundOpOfExpectedType); + return true; + } + else + { + return false; + } +} + +DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FCleanupConnectionManager, USpatialConnectionManager*, ConnectionManager); +bool FCleanupConnectionManager::Update() +{ + ConnectionManager->RemoveFromRoot(); + + return true; +} + +WORKERCONNECTION_TEST(GIVEN_running_local_deployment_WHEN_connecting_client_and_server_worker_THEN_connected_successfully) +{ + // GIVEN + ADD_LATENT_AUTOMATION_COMMAND(FStartDeployment()); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForDeployment(this, EDeploymentState::IsRunning)); + + // WHEN + USpatialConnectionManager* ClientConnectionManager = NewObject(); + USpatialConnectionManager* ServerConnectionManager = NewObject(); + ClientConnectionManager->AddToRoot(); + ServerConnectionManager->AddToRoot(); + ADD_LATENT_AUTOMATION_COMMAND(FSetupWorkerConnection(ClientConnectionManager, true)); + ADD_LATENT_AUTOMATION_COMMAND(FSetupWorkerConnection(ServerConnectionManager, false)); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForClientAndServerWorkerConnection()); + + // THEN + bool bIsConnected = true; + ADD_LATENT_AUTOMATION_COMMAND(FCheckConnectionStatus(this, ClientConnectionManager, bIsConnected)); + ADD_LATENT_AUTOMATION_COMMAND(FCheckConnectionStatus(this, ServerConnectionManager, bIsConnected)); + + // CLEANUP + ADD_LATENT_AUTOMATION_COMMAND(FStopDeployment()); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForDeployment(this, EDeploymentState::IsNotRunning)); + ADD_LATENT_AUTOMATION_COMMAND(FResetConnectionProcessed()); + ADD_LATENT_AUTOMATION_COMMAND(FCleanupConnectionManager(ClientConnectionManager)); + ADD_LATENT_AUTOMATION_COMMAND(FCleanupConnectionManager(ServerConnectionManager)); + + return true; +} + +WORKERCONNECTION_TEST(GIVEN_no_local_deployment_WHEN_connecting_client_and_server_worker_THEN_connection_failed) +{ + // GIVEN + ADD_LATENT_AUTOMATION_COMMAND(FStopDeployment()); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForDeployment(this, EDeploymentState::IsNotRunning)); + + // WHEN + USpatialConnectionManager* ClientConnectionManager = NewObject(); + USpatialConnectionManager* ServerConnectionManager = NewObject(); + ClientConnectionManager->AddToRoot(); + ServerConnectionManager->AddToRoot(); + ADD_LATENT_AUTOMATION_COMMAND(FSetupWorkerConnection(ClientConnectionManager, true)); + ADD_LATENT_AUTOMATION_COMMAND(FSetupWorkerConnection(ServerConnectionManager, false)); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForClientAndServerWorkerConnection()); + + // THEN + bool bIsConnected = false; + ADD_LATENT_AUTOMATION_COMMAND(FCheckConnectionStatus(this, ClientConnectionManager, bIsConnected)); + ADD_LATENT_AUTOMATION_COMMAND(FCheckConnectionStatus(this, ServerConnectionManager, bIsConnected)); + + // CLEANUP + ADD_LATENT_AUTOMATION_COMMAND(FResetConnectionProcessed()); + ADD_LATENT_AUTOMATION_COMMAND(FCleanupConnectionManager(ClientConnectionManager)); + ADD_LATENT_AUTOMATION_COMMAND(FCleanupConnectionManager(ServerConnectionManager)); + + GEngine->ForceGarbageCollection(true); + + return true; +} + +WORKERCONNECTION_TEST(GIVEN_valid_worker_connection_WHEN_reserve_entity_ids_request_sent_THEN_reserve_entity_ids_response_received) +{ + // GIVEN + ADD_LATENT_AUTOMATION_COMMAND(FStartDeployment()); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForDeployment(this, EDeploymentState::IsRunning)); + + // WHEN + USpatialConnectionManager* ClientConnectionManager = NewObject(); + USpatialConnectionManager* ServerConnectionManager = NewObject(); + ClientConnectionManager->AddToRoot(); + ServerConnectionManager->AddToRoot(); + ADD_LATENT_AUTOMATION_COMMAND(FSetupWorkerConnection(ClientConnectionManager, true)); + ADD_LATENT_AUTOMATION_COMMAND(FSetupWorkerConnection(ServerConnectionManager, false)); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForClientAndServerWorkerConnection()); + + // THEN + ADD_LATENT_AUTOMATION_COMMAND(FSendReserveEntityIdsRequest(ClientConnectionManager)); + ADD_LATENT_AUTOMATION_COMMAND(FSendReserveEntityIdsRequest(ServerConnectionManager)); + ADD_LATENT_AUTOMATION_COMMAND(FFindWorkerResponseOfType(this, ClientConnectionManager, WORKER_OP_TYPE_RESERVE_ENTITY_IDS_RESPONSE)); + ADD_LATENT_AUTOMATION_COMMAND(FFindWorkerResponseOfType(this, ServerConnectionManager, WORKER_OP_TYPE_RESERVE_ENTITY_IDS_RESPONSE)); + + // CLEANUP + ADD_LATENT_AUTOMATION_COMMAND(FStopDeployment()); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForDeployment(this, EDeploymentState::IsNotRunning)); + ADD_LATENT_AUTOMATION_COMMAND(FResetConnectionProcessed()); + ADD_LATENT_AUTOMATION_COMMAND(FCleanupConnectionManager(ClientConnectionManager)); + ADD_LATENT_AUTOMATION_COMMAND(FCleanupConnectionManager(ServerConnectionManager)); + + return true; +} + +WORKERCONNECTION_TEST(GIVEN_valid_worker_connection_WHEN_create_entity_request_sent_THEN_create_entity_response_received) +{ + // GIVEN + ADD_LATENT_AUTOMATION_COMMAND(FStartDeployment()); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForDeployment(this, EDeploymentState::IsRunning)); + + // WHEN + USpatialConnectionManager* ClientConnectionManager = NewObject(); + USpatialConnectionManager* ServerConnectionManager = NewObject(); + ClientConnectionManager->AddToRoot(); + ServerConnectionManager->AddToRoot(); + ADD_LATENT_AUTOMATION_COMMAND(FSetupWorkerConnection(ClientConnectionManager, true)); + ADD_LATENT_AUTOMATION_COMMAND(FSetupWorkerConnection(ServerConnectionManager, false)); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForClientAndServerWorkerConnection()); + + // THEN + ADD_LATENT_AUTOMATION_COMMAND(FSendCreateEntityRequest(ClientConnectionManager)); + ADD_LATENT_AUTOMATION_COMMAND(FSendCreateEntityRequest(ServerConnectionManager)); + ADD_LATENT_AUTOMATION_COMMAND(FFindWorkerResponseOfType(this, ServerConnectionManager, WORKER_OP_TYPE_CREATE_ENTITY_RESPONSE)); + ADD_LATENT_AUTOMATION_COMMAND(FFindWorkerResponseOfType(this, ClientConnectionManager, WORKER_OP_TYPE_CREATE_ENTITY_RESPONSE)); + + // CLEANUP + ADD_LATENT_AUTOMATION_COMMAND(FStopDeployment()); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForDeployment(this, EDeploymentState::IsNotRunning)); + ADD_LATENT_AUTOMATION_COMMAND(FResetConnectionProcessed()); + ADD_LATENT_AUTOMATION_COMMAND(FCleanupConnectionManager(ClientConnectionManager)); + ADD_LATENT_AUTOMATION_COMMAND(FCleanupConnectionManager(ServerConnectionManager)); + + return true; +} + +WORKERCONNECTION_TEST(GIVEN_valid_worker_connection_WHEN_delete_entity_request_sent_THEN_delete_entity_response_received) +{ + // GIVEN + ADD_LATENT_AUTOMATION_COMMAND(FStartDeployment()); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForDeployment(this, EDeploymentState::IsRunning)); + + // WHEN + USpatialConnectionManager* ClientConnectionManager = NewObject(); + USpatialConnectionManager* ServerConnectionManager = NewObject(); + ClientConnectionManager->AddToRoot(); + ServerConnectionManager->AddToRoot(); + ADD_LATENT_AUTOMATION_COMMAND(FSetupWorkerConnection(ClientConnectionManager, true)); + ADD_LATENT_AUTOMATION_COMMAND(FSetupWorkerConnection(ServerConnectionManager, false)); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForClientAndServerWorkerConnection()); + + // THEN + ADD_LATENT_AUTOMATION_COMMAND(FSendDeleteEntityRequest(ClientConnectionManager)); + ADD_LATENT_AUTOMATION_COMMAND(FSendDeleteEntityRequest(ServerConnectionManager)); + ADD_LATENT_AUTOMATION_COMMAND(FFindWorkerResponseOfType(this, ServerConnectionManager, WORKER_OP_TYPE_DELETE_ENTITY_RESPONSE)); + ADD_LATENT_AUTOMATION_COMMAND(FFindWorkerResponseOfType(this, ClientConnectionManager, WORKER_OP_TYPE_DELETE_ENTITY_RESPONSE)); + + // CLEANUP + ADD_LATENT_AUTOMATION_COMMAND(FStopDeployment()); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForDeployment(this, EDeploymentState::IsNotRunning)); + ADD_LATENT_AUTOMATION_COMMAND(FResetConnectionProcessed()); + ADD_LATENT_AUTOMATION_COMMAND(FCleanupConnectionManager(ClientConnectionManager)); + ADD_LATENT_AUTOMATION_COMMAND(FCleanupConnectionManager(ServerConnectionManager)); + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialOSDispatcherInterface/SpatialOSDispatcherSpy.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialOSDispatcherInterface/SpatialOSDispatcherSpy.cpp new file mode 100644 index 0000000000..416c22b61c --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialOSDispatcherInterface/SpatialOSDispatcherSpy.cpp @@ -0,0 +1,79 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialOSDispatcherSpy.h" + +SpatialOSDispatcherSpy::SpatialOSDispatcherSpy() +{} + +// Dispatcher Calls +void SpatialOSDispatcherSpy::OnCriticalSection(bool InCriticalSection) +{} + +void SpatialOSDispatcherSpy::OnAddEntity(const Worker_AddEntityOp& Op) +{} + +void SpatialOSDispatcherSpy::OnAddComponent(const Worker_AddComponentOp& Op) +{} + +void SpatialOSDispatcherSpy::OnRemoveEntity(const Worker_RemoveEntityOp& Op) +{} + +void SpatialOSDispatcherSpy::OnRemoveComponent(const Worker_RemoveComponentOp& Op) +{} + +void SpatialOSDispatcherSpy::FlushRemoveComponentOps() +{} + +void SpatialOSDispatcherSpy::DropQueuedRemoveComponentOpsForEntity(Worker_EntityId EntityId) +{} + +void SpatialOSDispatcherSpy::OnAuthorityChange(const Worker_AuthorityChangeOp& Op) +{} + +void SpatialOSDispatcherSpy::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) +{} + +// This gets bound to a delegate in SpatialRPCService and is called for each RPC extracted when calling SpatialRPCService::ExtractRPCsForEntity. +bool SpatialOSDispatcherSpy::OnExtractIncomingRPC(Worker_EntityId EntityId, ERPCType RPCType, const SpatialGDK::RPCPayload& Payload) +{ + return false; +} + +void SpatialOSDispatcherSpy::OnCommandRequest(const Worker_CommandRequestOp& Op) +{} + +void SpatialOSDispatcherSpy::OnCommandResponse(const Worker_CommandResponseOp& Op) +{} + +void SpatialOSDispatcherSpy::OnReserveEntityIdsResponse(const Worker_ReserveEntityIdsResponseOp& Op) +{} + +void SpatialOSDispatcherSpy::OnCreateEntityResponse(const Worker_CreateEntityResponseOp& Op) +{} + +void SpatialOSDispatcherSpy::AddPendingActorRequest(Worker_RequestId RequestId, USpatialActorChannel* Channel) +{} + +void SpatialOSDispatcherSpy::AddPendingReliableRPC(Worker_RequestId RequestId, TSharedRef ReliableRPC) +{} + +void SpatialOSDispatcherSpy::AddEntityQueryDelegate(Worker_RequestId RequestId, EntityQueryDelegate Delegate) +{ + EntityQueryDelegates.Add(RequestId, Delegate); +} + +void SpatialOSDispatcherSpy::AddReserveEntityIdsDelegate(Worker_RequestId RequestId, ReserveEntityIDsDelegate Delegate) +{} + +void SpatialOSDispatcherSpy::AddCreateEntityDelegate(Worker_RequestId RequestId, CreateEntityDelegate Delegate) +{} + +void SpatialOSDispatcherSpy::OnEntityQueryResponse(const Worker_EntityQueryResponseOp& Op) +{} + +EntityQueryDelegate* SpatialOSDispatcherSpy::GetEntityQueryDelegate(Worker_RequestId RequestId) +{ + return EntityQueryDelegates.Find(RequestId); +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialOSDispatcherInterface/SpatialOSDispatcherSpy.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialOSDispatcherInterface/SpatialOSDispatcherSpy.h new file mode 100644 index 0000000000..45546d62ff --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialOSDispatcherInterface/SpatialOSDispatcherSpy.h @@ -0,0 +1,55 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Interop/SpatialOSDispatcherInterface.h" + +// The SpatialOSDispatcherSpy is intended as a very minimal implementation which will acknowledge +// and record any calls. It can then be used to unit test other classes and validate that given +// particular inputs, they make calls to SpatialOS which are as expected. +// +// Currently, only a few methods below have implementations. Feel free to extend this mock as needed +// for testing purposes. + +class SpatialOSDispatcherSpy : public SpatialOSDispatcherInterface +{ +public: + SpatialOSDispatcherSpy(); + virtual ~SpatialOSDispatcherSpy() {} + + // Dispatcher Calls + virtual void OnCriticalSection(bool InCriticalSection) override; + virtual void OnAddEntity(const Worker_AddEntityOp& Op) override; + virtual void OnAddComponent(const Worker_AddComponentOp& Op) override; + virtual void OnRemoveEntity(const Worker_RemoveEntityOp& Op) override; + virtual void OnRemoveComponent(const Worker_RemoveComponentOp& Op) override; + virtual void FlushRemoveComponentOps() override; + virtual void DropQueuedRemoveComponentOpsForEntity(Worker_EntityId EntityId) override; + virtual void OnAuthorityChange(const Worker_AuthorityChangeOp& Op) override; + + virtual void OnComponentUpdate(const Worker_ComponentUpdateOp& Op) override; + + // This gets bound to a delegate in SpatialRPCService and is called for each RPC extracted when calling SpatialRPCService::ExtractRPCsForEntity. + virtual bool OnExtractIncomingRPC(Worker_EntityId EntityId, ERPCType RPCType, const SpatialGDK::RPCPayload& Payload) override; + + virtual void OnCommandRequest(const Worker_CommandRequestOp& Op) override; + virtual void OnCommandResponse(const Worker_CommandResponseOp& Op) override; + + virtual void OnReserveEntityIdsResponse(const Worker_ReserveEntityIdsResponseOp& Op) override; + virtual void OnCreateEntityResponse(const Worker_CreateEntityResponseOp& Op) override; + + virtual void AddPendingActorRequest(Worker_RequestId RequestId, USpatialActorChannel* Channel) override; + virtual void AddPendingReliableRPC(Worker_RequestId RequestId, TSharedRef ReliableRPC) override; + + virtual void AddEntityQueryDelegate(Worker_RequestId RequestId, EntityQueryDelegate Delegate) override; + virtual void AddReserveEntityIdsDelegate(Worker_RequestId RequestId, ReserveEntityIDsDelegate Delegate) override; + virtual void AddCreateEntityDelegate(Worker_RequestId RequestId, CreateEntityDelegate Delegate) override; + + virtual void OnEntityQueryResponse(const Worker_EntityQueryResponseOp& Op) override; + + // Methods to extract information about calls made. + EntityQueryDelegate* GetEntityQueryDelegate(Worker_RequestId RequestId); + +private: + TMap EntityQueryDelegates; +}; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialWorkerFlags/SpatialWorkerFlagsTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialWorkerFlags/SpatialWorkerFlagsTest.cpp new file mode 100644 index 0000000000..2dfd93acae --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialWorkerFlags/SpatialWorkerFlagsTest.cpp @@ -0,0 +1,96 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Interop/SpatialWorkerFlags.h" + +#include "Tests/TestDefinitions.h" +#include "WorkerFlagsTestSpyObject.h" + +#define SPATIALWORKERFLAGS_TEST(TestName) \ + GDK_TEST(Core, SpatialWorkerFlags, TestName) + +namespace +{ + Worker_FlagUpdateOp CreateWorkerFlagUpdateOp(const char* FlagName, const char* FlagValue) + { + Worker_FlagUpdateOp Op = {}; + Op.name = FlagName; + Op.value = FlagValue; + + return Op; + } +} // anonymous namespace + +SPATIALWORKERFLAGS_TEST(GIVEN_a_flagUpdate_op_WHEN_adding_a_worker_flag_THEN_flag_added) +{ + USpatialWorkerFlags* SpatialWorkerFlags = NewObject(); + // Add test flag + Worker_FlagUpdateOp OpAddFlag = CreateWorkerFlagUpdateOp("test", "10"); + SpatialWorkerFlags->ApplyWorkerFlagUpdate(OpAddFlag); + + FString OutFlagValue; + TestTrue("Flag added in the WorkerFlags map: ", SpatialWorkerFlags->GetWorkerFlag("test", OutFlagValue)); + + return true; +} + + +SPATIALWORKERFLAGS_TEST(GIVEN_a_flagUpdate_op_WHEN_removing_a_worker_flag_THEN_flag_removed) +{ + USpatialWorkerFlags* SpatialWorkerFlags = NewObject(); + // Add test flag + Worker_FlagUpdateOp OpAddFlag = CreateWorkerFlagUpdateOp("test", "10"); + SpatialWorkerFlags->ApplyWorkerFlagUpdate(OpAddFlag); + + FString OutFlagValue; + TestTrue("Flag added in the WorkerFlags map: ", SpatialWorkerFlags->GetWorkerFlag("test", OutFlagValue)); + + // Remove test flag + Worker_FlagUpdateOp OpRemoveFlag = CreateWorkerFlagUpdateOp("test", nullptr); + SpatialWorkerFlags->ApplyWorkerFlagUpdate(OpRemoveFlag); + + TestFalse("Flag removed from the WorkerFlags map: ", SpatialWorkerFlags->GetWorkerFlag("test", OutFlagValue)); + + return true; +} + +SPATIALWORKERFLAGS_TEST(GIVEN_a_bound_delegate_WHEN_a_worker_flag_updates_THEN_bound_function_invoked) +{ + UWorkerFlagsTestSpyObject* SpyObj = NewObject(); + FOnWorkerFlagsUpdatedBP WorkerFlagDelegate; + WorkerFlagDelegate.BindDynamic(SpyObj, &UWorkerFlagsTestSpyObject::SetFlagUpdated); + + USpatialWorkerFlags* SpatialWorkerFlags = NewObject(); + SpatialWorkerFlags->BindToOnWorkerFlagsUpdated(WorkerFlagDelegate); + + // Add test flag + Worker_FlagUpdateOp OpAddFlag = CreateWorkerFlagUpdateOp("test", "10"); + SpatialWorkerFlags->ApplyWorkerFlagUpdate(OpAddFlag); + + TestTrue("Delegate Function was called", SpyObj->GetTimesFlagUpdated() == 1); + + return true; +} + +SPATIALWORKERFLAGS_TEST(GIVEN_a_bound_delegate_WHEN_unbind_the_delegate_THEN_bound_function_is_not_invoked) +{ + UWorkerFlagsTestSpyObject* SpyObj = NewObject(); + FOnWorkerFlagsUpdatedBP WorkerFlagDelegate; + WorkerFlagDelegate.BindDynamic(SpyObj, &UWorkerFlagsTestSpyObject::SetFlagUpdated); + + USpatialWorkerFlags* SpatialWorkerFlags = NewObject(); + SpatialWorkerFlags->BindToOnWorkerFlagsUpdated(WorkerFlagDelegate); + // Add test flag + Worker_FlagUpdateOp OpAddFlag = CreateWorkerFlagUpdateOp("test", "10"); + SpatialWorkerFlags->ApplyWorkerFlagUpdate(OpAddFlag); + + TestTrue("Delegate Function was called", SpyObj->GetTimesFlagUpdated() == 1); + + SpatialWorkerFlags->UnbindFromOnWorkerFlagsUpdated(WorkerFlagDelegate); + + // Update test flag + SpatialWorkerFlags->ApplyWorkerFlagUpdate(OpAddFlag); + + TestTrue("Delegate Function was called only once", SpyObj->GetTimesFlagUpdated() == 1); + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialWorkerFlags/WorkerFlagsTestSpyObject.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialWorkerFlags/WorkerFlagsTestSpyObject.cpp new file mode 100644 index 0000000000..e2388a6061 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialWorkerFlags/WorkerFlagsTestSpyObject.cpp @@ -0,0 +1,16 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "WorkerFlagsTestSpyObject.h" + +void UWorkerFlagsTestSpyObject::SetFlagUpdated(const FString& FlagName, const FString& FlagValue) +{ + TimesUpdated++; + + return; +} + +int UWorkerFlagsTestSpyObject::GetTimesFlagUpdated() const +{ + return TimesUpdated; +} + diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialWorkerFlags/WorkerFlagsTestSpyObject.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialWorkerFlags/WorkerFlagsTestSpyObject.h new file mode 100644 index 0000000000..193e744bc6 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialWorkerFlags/WorkerFlagsTestSpyObject.h @@ -0,0 +1,23 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +#pragma once + +#include "Interop/SpatialWorkerFlags.h" + +#include "WorkerFlagsTestSpyObject.generated.h" + + +UCLASS() +class UWorkerFlagsTestSpyObject : public UObject +{ + GENERATED_BODY() +public: + + UFUNCTION() + void SetFlagUpdated(const FString& FlagName, const FString& FlagValue); + + int GetTimesFlagUpdated() const; + +private: + + int TimesUpdated = 0; +}; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalanceEnforcer/SpatialLoadBalanceEnforcerTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalanceEnforcer/SpatialLoadBalanceEnforcerTest.cpp new file mode 100644 index 0000000000..f175ce919b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalanceEnforcer/SpatialLoadBalanceEnforcerTest.cpp @@ -0,0 +1,485 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/TestDefinitions.h" + +#include "EngineClasses/SpatialLoadBalanceEnforcer.h" +#include "EngineClasses/SpatialVirtualWorkerTranslator.h" +#include "Interop/SpatialStaticComponentView.h" +#include "Schema/AuthorityIntent.h" +#include "SpatialGDKTests/SpatialGDK/LoadBalancing/AbstractLBStrategy/LBStrategyStub.h" +#include "Tests/TestingComponentViewHelpers.h" +#include "Tests/TestingSchemaHelpers.h" + +#include "CoreMinimal.h" + +#define LOADBALANCEENFORCER_TEST(TestName) \ + GDK_TEST(Core, SpatialLoadBalanceEnforcer, TestName) + +// Test Globals +namespace +{ + +const PhysicalWorkerName ValidWorkerOne = TEXT("ValidWorkerOne"); +const PhysicalWorkerName ValidWorkerTwo = TEXT("ValidWorkerTwo"); + +constexpr VirtualWorkerId VirtualWorkerOne = 1; +constexpr VirtualWorkerId VirtualWorkerTwo = 2; + +constexpr Worker_EntityId EntityIdOne = 1; +constexpr Worker_EntityId EntityIdTwo = 2; + +constexpr Worker_ComponentId TestComponentIdOne = 123; +constexpr Worker_ComponentId TestComponentIdTwo = 456; + +void AddEntityToStaticComponentView(USpatialStaticComponentView& StaticComponentView, + const Worker_EntityId EntityId, VirtualWorkerId Id, Worker_Authority AuthorityIntentAuthority) +{ + TestingComponentViewHelpers::AddEntityComponentToStaticComponentView(StaticComponentView, + EntityId, SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID, + AuthorityIntentAuthority); + + TestingComponentViewHelpers::AddEntityComponentToStaticComponentView(StaticComponentView, + EntityId, SpatialConstants::ENTITY_ACL_COMPONENT_ID, + WORKER_AUTHORITY_AUTHORITATIVE); + + TestingComponentViewHelpers::AddEntityComponentToStaticComponentView(StaticComponentView, + EntityId, SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID, + AuthorityIntentAuthority); + + TestingComponentViewHelpers::AddEntityComponentToStaticComponentView(StaticComponentView, + EntityId, SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID, + AuthorityIntentAuthority); + + if (Id != SpatialConstants::INVALID_VIRTUAL_WORKER_ID) + { + StaticComponentView.GetComponentData(EntityId)->VirtualWorkerId = Id; + } +} + +TUniquePtr CreateVirtualWorkerTranslator() +{ + ULBStrategyStub* LoadBalanceStrategy = NewObject(); + TUniquePtr VirtualWorkerTranslator = MakeUnique(LoadBalanceStrategy, ValidWorkerOne); + + Schema_Object* DataObject = TestingSchemaHelpers::CreateTranslationComponentDataFields(); + + TestingSchemaHelpers::AddTranslationComponentDataMapping(DataObject, VirtualWorkerOne, ValidWorkerOne); + TestingSchemaHelpers::AddTranslationComponentDataMapping(DataObject, VirtualWorkerTwo, ValidWorkerTwo); + + VirtualWorkerTranslator->ApplyVirtualWorkerManagerData(DataObject); + + return VirtualWorkerTranslator; +} + +} // anonymous namespace + +LOADBALANCEENFORCER_TEST(GIVEN_a_static_component_view_with_no_data_WHEN_asking_load_balance_enforcer_for_acl_assignments_THEN_return_no_acl_assignment_requests) +{ + TUniquePtr VirtualWorkerTranslator = CreateVirtualWorkerTranslator(); + + // Here we simply create a static component view but do not add any data to it. + // This means that the load balance enforcer will not be able to find the virtual worker id associated with an entity and therefore fail to produce ACL requests. + USpatialStaticComponentView* StaticComponentView = NewObject(); + + TUniquePtr LoadBalanceEnforcer = MakeUnique(ValidWorkerOne, StaticComponentView, VirtualWorkerTranslator.Get()); + + LoadBalanceEnforcer->MaybeQueueAclAssignmentRequest(EntityIdOne); + LoadBalanceEnforcer->MaybeQueueAclAssignmentRequest(EntityIdTwo); + + TArray ACLRequests = LoadBalanceEnforcer->ProcessQueuedAclAssignmentRequests(); + + bool bSuccess = ACLRequests.Num() == 0; + + // Now add components to the StaticComponentView and retry getting the ACL requests. + AddEntityToStaticComponentView(*StaticComponentView, EntityIdOne, VirtualWorkerOne, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + AddEntityToStaticComponentView(*StaticComponentView, EntityIdTwo, VirtualWorkerTwo, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + + ACLRequests.Empty(); + ACLRequests = LoadBalanceEnforcer->ProcessQueuedAclAssignmentRequests(); + + bSuccess &= ACLRequests.Num() == 0; + + TestTrue("LoadBalanceEnforcer returned expected ACL assignment results", bSuccess); + + return true; +} + +LOADBALANCEENFORCER_TEST(GIVEN_load_balance_enforcer_with_valid_mapping_WHEN_asked_for_acl_assignments_THEN_return_correct_acl_assignment_requests) +{ + TUniquePtr VirtualWorkerTranslator = CreateVirtualWorkerTranslator(); + + USpatialStaticComponentView* StaticComponentView = NewObject(); + AddEntityToStaticComponentView(*StaticComponentView, EntityIdOne, VirtualWorkerOne, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + AddEntityToStaticComponentView(*StaticComponentView, EntityIdTwo, VirtualWorkerTwo, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + + TUniquePtr LoadBalanceEnforcer = MakeUnique(ValidWorkerOne, StaticComponentView, VirtualWorkerTranslator.Get()); + + LoadBalanceEnforcer->MaybeQueueAclAssignmentRequest(EntityIdOne); + LoadBalanceEnforcer->MaybeQueueAclAssignmentRequest(EntityIdTwo); + + TArray ACLRequests = LoadBalanceEnforcer->ProcessQueuedAclAssignmentRequests(); + + bool bSuccess = true; + if (ACLRequests.Num() == 2) + { + bSuccess &= ACLRequests[0].EntityId == EntityIdOne; + bSuccess &= ACLRequests[0].OwningWorkerId == ValidWorkerOne; + bSuccess &= ACLRequests[1].EntityId == EntityIdTwo; + bSuccess &= ACLRequests[1].OwningWorkerId == ValidWorkerTwo; + } + else + { + bSuccess = false; + } + + TestTrue("LoadBalanceEnforcer returned expected ACL assignment results", bSuccess); + + return true; +} + +LOADBALANCEENFORCER_TEST(GIVEN_load_balance_enforcer_with_valid_mapping_WHEN_queueing_two_acl_requests_for_the_same_entity_THEN_return_one_acl_assignment_request_for_that_entity) +{ + TUniquePtr VirtualWorkerTranslator = CreateVirtualWorkerTranslator(); + + USpatialStaticComponentView* StaticComponentView = NewObject(); + AddEntityToStaticComponentView(*StaticComponentView, EntityIdOne, VirtualWorkerOne, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + + TUniquePtr LoadBalanceEnforcer = MakeUnique(ValidWorkerOne, StaticComponentView, VirtualWorkerTranslator.Get()); + + LoadBalanceEnforcer->MaybeQueueAclAssignmentRequest(EntityIdOne); + LoadBalanceEnforcer->MaybeQueueAclAssignmentRequest(EntityIdOne); + + TArray ACLRequests = LoadBalanceEnforcer->ProcessQueuedAclAssignmentRequests(); + + bool bSuccess = true; + if (ACLRequests.Num() == 1) + { + bSuccess &= ACLRequests[0].EntityId == EntityIdOne; + bSuccess &= ACLRequests[0].OwningWorkerId == ValidWorkerOne; + } + else + { + bSuccess = false; + } + + TestTrue("LoadBalanceEnforcer returned expected ACL assignment results", bSuccess); + + return true; +} + +LOADBALANCEENFORCER_TEST(GIVEN_authority_intent_change_op_WHEN_we_inform_load_balance_enforcer_THEN_queue_authority_request) +{ + TUniquePtr VirtualWorkerTranslator = CreateVirtualWorkerTranslator(); + + USpatialStaticComponentView* StaticComponentView = NewObject(); + AddEntityToStaticComponentView(*StaticComponentView, EntityIdOne, VirtualWorkerOne, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + + TUniquePtr LoadBalanceEnforcer = MakeUnique(ValidWorkerOne, StaticComponentView, VirtualWorkerTranslator.Get()); + + Worker_ComponentUpdateOp UpdateOp; + UpdateOp.entity_id = EntityIdOne; + UpdateOp.update.component_id = SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID; + + LoadBalanceEnforcer->OnLoadBalancingComponentUpdated(UpdateOp); + + TArray ACLRequests = LoadBalanceEnforcer->ProcessQueuedAclAssignmentRequests(); + + bool bSuccess = true; + if (ACLRequests.Num() == 1) + { + bSuccess &= ACLRequests[0].EntityId == EntityIdOne; + bSuccess &= ACLRequests[0].OwningWorkerId == ValidWorkerOne; + } + else + { + bSuccess = false; + } + + TestTrue("LoadBalanceEnforcer returned expected ACL assignment results", bSuccess); + + return true; +} + +LOADBALANCEENFORCER_TEST(GIVEN_authority_change_when_not_authoritative_over_authority_intent_component_WHEN_we_inform_load_balance_enforcer_THEN_queue_authority_request) +{ + TUniquePtr VirtualWorkerTranslator = CreateVirtualWorkerTranslator(); + + // The important part of this test is that the worker does not already have authority over the AuthorityIntent component. + // In this case, we expect the load balance enforcer to create an ACL request. + USpatialStaticComponentView* StaticComponentView = NewObject(); + AddEntityToStaticComponentView(*StaticComponentView, EntityIdOne, VirtualWorkerOne, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + + TUniquePtr LoadBalanceEnforcer = MakeUnique(ValidWorkerOne, StaticComponentView, VirtualWorkerTranslator.Get()); + + Worker_AuthorityChangeOp UpdateOp; + UpdateOp.entity_id = EntityIdOne; + UpdateOp.authority = WORKER_AUTHORITY_AUTHORITATIVE; + UpdateOp.component_id = SpatialConstants::ENTITY_ACL_COMPONENT_ID; + + LoadBalanceEnforcer->OnAclAuthorityChanged(UpdateOp); + + TArray ACLRequests = LoadBalanceEnforcer->ProcessQueuedAclAssignmentRequests(); + + bool bSuccess = true; + if (ACLRequests.Num() == 1) + { + bSuccess &= ACLRequests[0].EntityId == EntityIdOne; + bSuccess &= ACLRequests[0].OwningWorkerId == ValidWorkerOne; + } + else + { + bSuccess = false; + } + + TestTrue("LoadBalanceEnforcer returned expected ACL assignment results", bSuccess); + + return true; +} + +LOADBALANCEENFORCER_TEST(GIVEN_authority_change_when_authoritative_over_authority_intent_component_WHEN_we_inform_load_balance_enforcer_THEN_return_no_acl_assignment_requests) +{ + TUniquePtr VirtualWorkerTranslator = CreateVirtualWorkerTranslator(); + + // The important part of this test is that the worker does already have authority over the AuthorityIntent component. + // In this case, we expect the load balance enforcer not to create an ACL request. + USpatialStaticComponentView* StaticComponentView = NewObject(); + AddEntityToStaticComponentView(*StaticComponentView, EntityIdOne, VirtualWorkerOne, WORKER_AUTHORITY_AUTHORITATIVE); + + TUniquePtr LoadBalanceEnforcer = MakeUnique(ValidWorkerOne, StaticComponentView, VirtualWorkerTranslator.Get()); + + Worker_AuthorityChangeOp UpdateOp; + UpdateOp.entity_id = EntityIdOne; + UpdateOp.authority = WORKER_AUTHORITY_AUTHORITATIVE; + UpdateOp.component_id = SpatialConstants::ENTITY_ACL_COMPONENT_ID; + + LoadBalanceEnforcer->OnAclAuthorityChanged(UpdateOp); + + TArray ACLRequests = LoadBalanceEnforcer->ProcessQueuedAclAssignmentRequests(); + + bool bSuccess = ACLRequests.Num() == 0; + TestTrue("LoadBalanceEnforcer returned expected ACL assignment results", bSuccess); + + return true; +} + +LOADBALANCEENFORCER_TEST(GIVEN_acl_authority_loss_WHEN_request_is_queued_THEN_return_no_acl_assignment_requests) +{ + TUniquePtr VirtualWorkerTranslator = CreateVirtualWorkerTranslator(); + + // Set up the world in such a way that we can enforce the authority, and we are not already the authoritative worker so should try and assign authority. + USpatialStaticComponentView* StaticComponentView = NewObject(); + AddEntityToStaticComponentView(*StaticComponentView, EntityIdOne, VirtualWorkerOne, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + + TUniquePtr LoadBalanceEnforcer = MakeUnique(ValidWorkerOne, StaticComponentView, VirtualWorkerTranslator.Get()); + + Worker_AuthorityChangeOp AuthOp; + AuthOp.entity_id = EntityIdOne; + AuthOp.authority = WORKER_AUTHORITY_AUTHORITATIVE; + AuthOp.component_id = SpatialConstants::ENTITY_ACL_COMPONENT_ID; + + LoadBalanceEnforcer->OnAclAuthorityChanged(AuthOp); + + // At this point, we expect there to be a queued request. + TestTrue("Assignment request is queued", LoadBalanceEnforcer->AclAssignmentRequestIsQueued(EntityIdOne)); + + AuthOp.authority = WORKER_AUTHORITY_NOT_AUTHORITATIVE; + + LoadBalanceEnforcer->OnAclAuthorityChanged(AuthOp); + + // Now we should have dropped that request. + + TArray ACLRequests = LoadBalanceEnforcer->ProcessQueuedAclAssignmentRequests(); + + bool bSuccess = ACLRequests.Num() == 0; + TestTrue("LoadBalanceEnforcer returned expected ACL assignment results", bSuccess); + + return true; +} + +LOADBALANCEENFORCER_TEST(GIVEN_entity_removal_WHEN_request_is_queued_THEN_return_no_acl_assignment_requests) +{ + TUniquePtr VirtualWorkerTranslator = CreateVirtualWorkerTranslator(); + + // Set up the world in such a way that we can enforce the authority, and we are not already the authoritative worker so should try and assign authority. + USpatialStaticComponentView* StaticComponentView = NewObject(); + AddEntityToStaticComponentView(*StaticComponentView, EntityIdOne, VirtualWorkerOne, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + + TUniquePtr LoadBalanceEnforcer = MakeUnique(ValidWorkerOne, StaticComponentView, VirtualWorkerTranslator.Get()); + + Worker_AuthorityChangeOp AuthOp; + AuthOp.entity_id = EntityIdOne; + AuthOp.authority = WORKER_AUTHORITY_AUTHORITATIVE; + AuthOp.component_id = SpatialConstants::ENTITY_ACL_COMPONENT_ID; + + LoadBalanceEnforcer->OnAclAuthorityChanged(AuthOp); + + // At this point, we expect there to be a queued request. + TestTrue("Assignment request is queued", LoadBalanceEnforcer->AclAssignmentRequestIsQueued(EntityIdOne)); + + Worker_RemoveEntityOp EntityOp; + EntityOp.entity_id = EntityIdOne; + + LoadBalanceEnforcer->OnEntityRemoved(EntityOp); + + // Now we should have dropped that request. + + TArray ACLRequests = LoadBalanceEnforcer->ProcessQueuedAclAssignmentRequests(); + + bool bSuccess = ACLRequests.Num() == 0; + TestTrue("LoadBalanceEnforcer returned expected ACL assignment results", bSuccess); + + return true; +} + +LOADBALANCEENFORCER_TEST(GIVEN_authority_intent_component_removal_WHEN_request_is_queued_THEN_return_no_acl_assignment_requests) +{ + TUniquePtr VirtualWorkerTranslator = CreateVirtualWorkerTranslator(); + + // Set up the world in such a way that we can enforce the authority, and we are not already the authoritative worker so should try and assign authority. + USpatialStaticComponentView* StaticComponentView = NewObject(); + AddEntityToStaticComponentView(*StaticComponentView, EntityIdOne, VirtualWorkerOne, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + + TUniquePtr LoadBalanceEnforcer = MakeUnique(ValidWorkerOne, StaticComponentView, VirtualWorkerTranslator.Get()); + + Worker_AuthorityChangeOp AuthOp; + AuthOp.entity_id = EntityIdOne; + AuthOp.authority = WORKER_AUTHORITY_AUTHORITATIVE; + AuthOp.component_id = SpatialConstants::ENTITY_ACL_COMPONENT_ID; + + LoadBalanceEnforcer->OnAclAuthorityChanged(AuthOp); + + // At this point, we expect there to be a queued request. + TestTrue("Assignment request is queued", LoadBalanceEnforcer->AclAssignmentRequestIsQueued(EntityIdOne)); + + Worker_RemoveComponentOp ComponentOp; + ComponentOp.entity_id = EntityIdOne; + ComponentOp.component_id = SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID; + + LoadBalanceEnforcer->OnLoadBalancingComponentRemoved(ComponentOp); + + // Now we should have dropped that request. + + TArray ACLRequests = LoadBalanceEnforcer->ProcessQueuedAclAssignmentRequests(); + + bool bSuccess = ACLRequests.Num() == 0; + TestTrue("LoadBalanceEnforcer returned expected ACL assignment results", bSuccess); + + return true; +} + +LOADBALANCEENFORCER_TEST(GIVEN_acl_component_removal_WHEN_request_is_queued_THEN_return_no_acl_assignment_requests) +{ + TUniquePtr VirtualWorkerTranslator = CreateVirtualWorkerTranslator(); + + // Set up the world in such a way that we can enforce the authority, and we are not already the authoritative worker so should try and assign authority. + USpatialStaticComponentView* StaticComponentView = NewObject(); + AddEntityToStaticComponentView(*StaticComponentView, EntityIdOne, VirtualWorkerOne, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + + TUniquePtr LoadBalanceEnforcer = MakeUnique(ValidWorkerOne, StaticComponentView, VirtualWorkerTranslator.Get()); + + Worker_AuthorityChangeOp AuthOp; + AuthOp.entity_id = EntityIdOne; + AuthOp.authority = WORKER_AUTHORITY_AUTHORITATIVE; + AuthOp.component_id = SpatialConstants::ENTITY_ACL_COMPONENT_ID; + + LoadBalanceEnforcer->OnAclAuthorityChanged(AuthOp); + + // At this point, we expect there to be a queued request. + TestTrue("Assignment request is queued", LoadBalanceEnforcer->AclAssignmentRequestIsQueued(EntityIdOne)); + + Worker_RemoveComponentOp ComponentOp; + ComponentOp.entity_id = EntityIdOne; + ComponentOp.component_id = SpatialConstants::ENTITY_ACL_COMPONENT_ID; + + LoadBalanceEnforcer->OnLoadBalancingComponentRemoved(ComponentOp); + + // Now we should have dropped that request. + + TArray ACLRequests = LoadBalanceEnforcer->ProcessQueuedAclAssignmentRequests(); + + bool bSuccess = ACLRequests.Num() == 0; + TestTrue("LoadBalanceEnforcer returned expected ACL assignment results", bSuccess); + + return true; +} + +LOADBALANCEENFORCER_TEST(GIVEN_component_presence_change_op_WHEN_we_inform_load_balance_enforcer_THEN_queue_authority_request) +{ + TUniquePtr VirtualWorkerTranslator = CreateVirtualWorkerTranslator(); + + USpatialStaticComponentView* StaticComponentView = NewObject(); + AddEntityToStaticComponentView(*StaticComponentView, EntityIdOne, VirtualWorkerOne, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + + TUniquePtr LoadBalanceEnforcer = MakeUnique(ValidWorkerOne, StaticComponentView, VirtualWorkerTranslator.Get()); + + TArray PresentComponentIds{ TestComponentIdOne, TestComponentIdTwo }; + + // Create a ComponentPresence component update op with the required components. + Worker_ComponentUpdateOp UpdateOp; + UpdateOp.entity_id = EntityIdOne; + UpdateOp.update.component_id = SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID; + UpdateOp.update.schema_type = Schema_CreateComponentUpdate(); + Schema_Object* UpdateFields = Schema_GetComponentUpdateFields(UpdateOp.update.schema_type); + Schema_AddUint32List(UpdateFields, SpatialConstants::COMPONENT_PRESENCE_COMPONENT_LIST_ID, PresentComponentIds.GetData(), PresentComponentIds.Num()); + + // Pass the ComponentPresence update to the enforcer to queue an ACL assignment. + LoadBalanceEnforcer->OnLoadBalancingComponentUpdated(UpdateOp); + + // Pass the update op to the StaticComponentView so that they can be read when the ACL assigment is processed. + StaticComponentView->OnComponentUpdate(UpdateOp); + + TArray ACLRequests = LoadBalanceEnforcer->ProcessQueuedAclAssignmentRequests(); + + bool bSuccess = true; + if (ACLRequests.Num() == 1) + { + bSuccess &= ACLRequests[0].EntityId == EntityIdOne; + bSuccess &= ACLRequests[0].OwningWorkerId == ValidWorkerOne; + bSuccess &= ACLRequests[0].ComponentIds.Contains(TestComponentIdOne); + bSuccess &= ACLRequests[0].ComponentIds.Contains(TestComponentIdTwo); + } + else + { + bSuccess = false; + } + + TestTrue("LoadBalanceEnforcer returned expected ACL assignment results", bSuccess); + + return true; +} + +LOADBALANCEENFORCER_TEST(GIVEN_component_presence_component_removal_WHEN_request_is_queued_THEN_return_no_acl_assignment_requests) +{ + TUniquePtr VirtualWorkerTranslator = CreateVirtualWorkerTranslator(); + + // Set up the world in such a way that we can enforce the authority, and we are not already the authoritative worker so should try and assign authority. + USpatialStaticComponentView* StaticComponentView = NewObject(); + AddEntityToStaticComponentView(*StaticComponentView, EntityIdOne, VirtualWorkerOne, WORKER_AUTHORITY_NOT_AUTHORITATIVE); + + TUniquePtr LoadBalanceEnforcer = MakeUnique(ValidWorkerOne, StaticComponentView, VirtualWorkerTranslator.Get()); + + Worker_AuthorityChangeOp AuthOp; + AuthOp.entity_id = EntityIdOne; + AuthOp.authority = WORKER_AUTHORITY_AUTHORITATIVE; + AuthOp.component_id = SpatialConstants::ENTITY_ACL_COMPONENT_ID; + + LoadBalanceEnforcer->OnAclAuthorityChanged(AuthOp); + + // At this point, we expect there to be a queued request. + TestTrue("Assignment request is queued", LoadBalanceEnforcer->AclAssignmentRequestIsQueued(EntityIdOne)); + + Worker_RemoveComponentOp ComponentOp; + ComponentOp.entity_id = EntityIdOne; + ComponentOp.component_id = SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID; + + LoadBalanceEnforcer->OnLoadBalancingComponentRemoved(ComponentOp); + + // Now we should have dropped that request. + + TArray ACLRequests = LoadBalanceEnforcer->ProcessQueuedAclAssignmentRequests(); + + bool bSuccess = ACLRequests.Num() == 0; + TestTrue("LoadBalanceEnforcer returned expected ACL assignment results", bSuccess); + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/AbstractLBStrategy/LBStrategyStub.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/AbstractLBStrategy/LBStrategyStub.h new file mode 100644 index 0000000000..052d8398b3 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/AbstractLBStrategy/LBStrategyStub.h @@ -0,0 +1,23 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "LoadBalancing/AbstractLBStrategy.h" +#include "SpatialCommonTypes.h" + +#include "LBStrategyStub.generated.h" + +/** + * This class is for testing purposes only. + */ +UCLASS(HideDropdown) +class SPATIALGDKTESTS_API ULBStrategyStub : public UAbstractLBStrategy +{ + GENERATED_BODY() + +public: + VirtualWorkerId GetVirtualWorkerId() const + { + return LocalVirtualWorkerId; + } +}; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/GridBasedLBStrategyTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/GridBasedLBStrategyTest.cpp index 416cc85d6f..8b97a73d4a 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/GridBasedLBStrategyTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/GridBasedLBStrategyTest.cpp @@ -1,15 +1,18 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved +#include "LoadBalancing/GridBasedLBStrategy.h" +#include "Schema/StandardLibrary.h" +#include "SpatialConstants.h" +#include "TestGridBasedLBStrategy.h" + #include "CoreMinimal.h" +#include "Engine/Engine.h" #include "Engine/World.h" -#include "LoadBalancing/GridBasedLBStrategy.h" #include "GameFramework/DefaultPawn.h" #include "GameFramework/GameStateBase.h" -#include "SpatialConstants.h" -#include "TestDefinitions.h" -#include "TestGridBasedLBStrategy.h" #include "Tests/AutomationCommon.h" #include "Tests/AutomationEditorCommon.h" +#include "Tests/TestDefinitions.h" #define GRIDBASEDLBSTRATEGY_TEST(TestName) \ GDK_TEST(Core, UGridBasedLBStrategy, TestName) @@ -17,10 +20,10 @@ // Test Globals namespace { - UWorld* TestWorld; - TMap TestActors; - UGridBasedLBStrategy* Strat; -} + +UWorld* TestWorld; +TMap TestActors; +UGridBasedLBStrategy* Strat; // Copied from AutomationCommon::GetAnyGameWorld() UWorld* GetAnyGameWorld() @@ -54,7 +57,7 @@ DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(FCreateStrategy, uint32, Rows, u bool FCreateStrategy::Update() { Strat = UTestGridBasedLBStrategy::Create(Rows, Cols, WorldWidth, WorldHeight); - Strat->Init(nullptr); + Strat->Init(); TSet VirtualWorkerIds = Strat->GetVirtualWorkerIds(); Strat->SetLocalVirtualWorkerId(VirtualWorkerIds.Array()[LocalWorkerIdIndex]); @@ -109,7 +112,7 @@ bool FWaitForActor::Update() DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCheckShouldRelinquishAuthority, FAutomationTestBase*, Test, FName, Handle, bool, bExpected); bool FCheckShouldRelinquishAuthority::Update() { - bool bActual = Strat->ShouldRelinquishAuthority(*TestActors[Handle]); + bool bActual = !Strat->ShouldHaveAuthority(*TestActors[Handle]); Test->TestEqual(FString::Printf(TEXT("Should Relinquish Authority. Actual: %d, Expected: %d"), bActual, bExpected), bActual, bExpected); @@ -166,7 +169,7 @@ bool FCheckVirtualWorkersMatch::Update() GRIDBASEDLBSTRATEGY_TEST(GIVEN_2_rows_3_cols_WHEN_get_virtual_worker_ids_is_called_THEN_it_returns_6_ids) { Strat = UTestGridBasedLBStrategy::Create(2, 3, 10000.f, 10000.f); - Strat->Init(nullptr); + Strat->Init(); TSet VirtualWorkerIds = Strat->GetVirtualWorkerIds(); TestEqual("Number of Virtual Workers", VirtualWorkerIds.Num(), 6); @@ -177,7 +180,7 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_2_rows_3_cols_WHEN_get_virtual_worker_ids_is_call GRIDBASEDLBSTRATEGY_TEST(GIVEN_a_grid_WHEN_get_virtual_worker_ids_THEN_all_worker_ids_are_valid) { Strat = UTestGridBasedLBStrategy::Create(5, 10, 10000.f, 10000.f); - Strat->Init(nullptr); + Strat->Init(); TSet VirtualWorkerIds = Strat->GetVirtualWorkerIds(); for (uint32 VirtualWorkerId : VirtualWorkerIds) @@ -191,7 +194,7 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_a_grid_WHEN_get_virtual_worker_ids_THEN_all_worke GRIDBASEDLBSTRATEGY_TEST(GIVEN_grid_is_not_ready_WHEN_local_virtual_worker_id_is_set_THEN_is_ready) { Strat = UTestGridBasedLBStrategy::Create(1, 1, 10000.f, 10000.f); - Strat->Init(nullptr); + Strat->Init(); TestFalse("IsReady Before LocalVirtualWorkerId Set", Strat->IsReady()); @@ -202,6 +205,52 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_grid_is_not_ready_WHEN_local_virtual_worker_id_is return true; } +GRIDBASEDLBSTRATEGY_TEST(GIVEN_four_cells_WHEN_get_worker_interest_for_virtual_worker_THEN_returns_correct_constraint) +{ + Strat = UTestGridBasedLBStrategy::Create(2, 2, 10000.f, 10000.f, 1000.0f); + Strat->Init(); + + // Take the top right corner, as then all our testing numbers can be positive. + Strat->SetLocalVirtualWorkerId(4); + + SpatialGDK::QueryConstraint StratConstraint = Strat->GetWorkerInterestQueryConstraint(); + + SpatialGDK::BoxConstraint Box = StratConstraint.BoxConstraint.GetValue(); + + // y is the vertical axis in SpatialOS coordinates. + SpatialGDK::Coordinates TestCentre = SpatialGDK::Coordinates{ 25.0, 0.0, 25.0 }; + // The constraint will be a 50x50 box around the centre, expanded by 10 in every direction because of the interest border, so +20 to x and z. + double TestEdgeLength = 70; + + TestEqual("Centre of the interest grid is as expected", Box.Center, TestCentre); + TestEqual("Edge length in x is as expected", Box.EdgeLength.X, TestEdgeLength); + TestEqual("Edge length in z is as expected", Box.EdgeLength.Z, TestEdgeLength); + + // The height of the box is "some very large number which is effectively infinite", so just sanity check it here. + TestTrue("Edge length in y is greater than 0", Box.EdgeLength.Y > 0); + + return true; +} + +GRIDBASEDLBSTRATEGY_TEST(GIVEN_four_cells_WHEN_get_worker_entity_position_for_virtual_worker_THEN_returns_correct_position) +{ + Strat = UTestGridBasedLBStrategy::Create(2, 2, 10000.f, 10000.f, 1000.0f); + Strat->Init(); + + // Take the top right corner, as then all our testing numbers can be positive. + Strat->SetLocalVirtualWorkerId(4); + + FVector WorkerPosition = Strat->GetWorkerEntityPosition(); + + FVector TestPosition = FVector{ 2500.0f, 2500.0f, 0.0f }; + + TestEqual("Worker entity position is as expected", WorkerPosition, TestPosition); + + return true; +} + +} // anonymous namespace + GRIDBASEDLBSTRATEGY_TEST(GIVEN_a_single_cell_and_valid_local_id_WHEN_should_relinquish_called_THEN_returns_false) { AutomationOpenMap("/Engine/Maps/Entry"); @@ -240,7 +289,7 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_moving_actor_WHEN_actor_crosses_boundary_THEN_sho { AutomationOpenMap("/Engine/Maps/Entry"); - ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(1, 2, 10000.f, 10000.f, 0)); + ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(2, 1, 10000.f, 10000.f, 0)); ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld()); ADD_LATENT_AUTOMATION_COMMAND(FSpawnActorAtLocation("Actor1", FVector(-2.f, 0.f, 0.f))); ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor("Actor1")); @@ -275,7 +324,7 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_two_cells_WHEN_actor_in_one_cell_THEN_strategy_re AutomationOpenMap("/Engine/Maps/Entry"); ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld()); - ADD_LATENT_AUTOMATION_COMMAND(FSpawnActorAtLocation("Actor1", FVector(-2500.f, 0.f, 0.f))); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActorAtLocation("Actor1", FVector(0.f, -2500.f, 0.f))); ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor("Actor1")); ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(1, 2, 10000.f, 10000.f, 0)); ADD_LATENT_AUTOMATION_COMMAND(FCheckShouldRelinquishAuthority(this, "Actor1", false)); diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.cpp index db3acafb6f..295edaf281 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.cpp @@ -2,7 +2,7 @@ #include "TestGridBasedLBStrategy.h" -UGridBasedLBStrategy* UTestGridBasedLBStrategy::Create(uint32 InRows, uint32 InCols, float WorldWidth, float WorldHeight) +UGridBasedLBStrategy* UTestGridBasedLBStrategy::Create(uint32 InRows, uint32 InCols, float WorldWidth, float WorldHeight, float InterestBorder) { UTestGridBasedLBStrategy* Strat = NewObject(); @@ -12,5 +12,7 @@ UGridBasedLBStrategy* UTestGridBasedLBStrategy::Create(uint32 InRows, uint32 InC Strat->WorldWidth = WorldWidth; Strat->WorldHeight = WorldHeight; + Strat->InterestBorder = InterestBorder; + return Strat; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.h index 434951028c..e7edc1fd89 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.h +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.h @@ -16,6 +16,5 @@ class SPATIALGDKTESTS_API UTestGridBasedLBStrategy : public UGridBasedLBStrategy public: - static UGridBasedLBStrategy* Create(uint32 Rows, uint32 Cols, float WorldWidth, float WorldHeight); - + static UGridBasedLBStrategy* Create(uint32 Rows, uint32 Cols, float WorldWidth, float WorldHeight, float InterestBorder = 0.0f); }; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/OwnershipLockingPolicy/OwnershipLockingPolicyTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/OwnershipLockingPolicy/OwnershipLockingPolicyTest.cpp new file mode 100644 index 0000000000..030108d7a8 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/OwnershipLockingPolicy/OwnershipLockingPolicyTest.cpp @@ -0,0 +1,1063 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "LoadBalancing/OwnershipLockingPolicy.h" +#include "SpatialConstants.h" +#include "Tests/TestDefinitions.h" + +#include "Containers/Array.h" +#include "Containers/Map.h" +#include "Containers/UnrealString.h" +#include "Engine/Engine.h" +#include "Engine/EngineTypes.h" +#include "GameFramework/GameStateBase.h" +#include "GameFramework/DefaultPawn.h" +#include "Improbable/SpatialEngineDelegates.h" +#include "Tests/AutomationCommon.h" +#include "Templates/SharedPointer.h" +#include "UObject/UObjectGlobals.h" + +#define OWNERSHIPLOCKINGPOLICY_TEST(TestName) \ + GDK_TEST(Core, UOwnershipLockingPolicy, TestName) + +namespace +{ + +using LockingTokenAndDebugString = TPair; + +struct TestData +{ + UWorld* TestWorld; + TMap TestActors; + TMap> TestActorToLockingTokenAndDebugStrings; + UOwnershipLockingPolicy* LockingPolicy; + SpatialDelegates::FAcquireLockDelegate AcquireLockDelegate; + SpatialDelegates::FReleaseLockDelegate ReleaseLockDelegate; +}; + +struct TestDataDeleter +{ + void operator()(TestData* Data) const noexcept + { + Data->LockingPolicy->RemoveFromRoot(); + delete Data; + } +}; + +TSharedPtr MakeNewTestData() +{ + TSharedPtr Data(new TestData, TestDataDeleter()); + + Data->LockingPolicy = NewObject(); + Data->LockingPolicy->Init(Data->AcquireLockDelegate, Data->ReleaseLockDelegate); + Data->LockingPolicy->AddToRoot(); + + return Data; +} + +// Copied from AutomationCommon::GetAnyGameWorld(). +UWorld* GetAnyGameWorld() +{ + UWorld* World = nullptr; + const TIndirectArray& WorldContexts = GEngine->GetWorldContexts(); + for (const FWorldContext& Context : WorldContexts) + { + if ((Context.WorldType == EWorldType::PIE || Context.WorldType == EWorldType::Game) + && (Context.World() != nullptr)) + { + World = Context.World(); + break; + } + } + + return World; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FWaitForWorld, TSharedPtr, Data); +bool FWaitForWorld::Update() +{ + Data->TestWorld = GetAnyGameWorld(); + + if (Data->TestWorld && Data->TestWorld->AreActorsInitialized()) + { + AGameStateBase* GameState = Data->TestWorld->GetGameState(); + if (GameState && GameState->HasMatchStarted()) + { + return true; + } + } + + return false; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER(FSpawnActor, TSharedPtr, Data, FName, Handle); +bool FSpawnActor::Update() +{ + FActorSpawnParameters SpawnParams; + SpawnParams.bNoFail = true; + SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + SpawnParams.Name = Handle; + + AActor* Actor = Data->TestWorld->SpawnActor(SpawnParams); + Data->TestActors.Add(Handle, Actor); + + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER(FWaitForActor, TSharedPtr, Data, FName, Handle); +bool FWaitForActor::Update() +{ + AActor* Actor = Data->TestActors[Handle]; + return (IsValid(Actor) && Actor->IsActorInitialized() && Actor->HasActorBegunPlay()); +} + +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FSetActorRole, TSharedPtr, Data, FName, Handle, ENetRole, Role); +bool FSetActorRole::Update() +{ + AActor* TestActor = Data->TestActors[Handle]; + TestActor->Role = Role; + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER(FDestroyActor, TSharedPtr, Data, FName, Handle); +bool FDestroyActor::Update() +{ + AActor* Actor = Data->TestActors.FindAndRemoveChecked(Handle); + AActor* OldOwner = Actor->GetOwner(); + Actor->Destroy(); + Data->LockingPolicy->OnOwnerUpdated(Actor, OldOwner); + + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FSetOwnership, TSharedPtr, Data, FName, ActorBeingOwnedHandle, FName, ActorToOwnHandle); +bool FSetOwnership::Update() +{ + AActor* ActorBeingOwned = Data->TestActors[ActorBeingOwnedHandle]; + AActor* ActorToOwn = Data->TestActors[ActorToOwnHandle]; + AActor* OldOwner = ActorBeingOwned->GetOwner(); + ActorBeingOwned->SetOwner(ActorToOwn); + Data->LockingPolicy->OnOwnerUpdated(ActorBeingOwned, OldOwner); + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(FAcquireLock, FAutomationTestBase*, Test, TSharedPtr, Data, FName, ActorHandle, FString, DebugString, bool, bExpectedSuccess); +bool FAcquireLock::Update() +{ + if (!bExpectedSuccess) + { + Test->AddExpectedError(TEXT("Called AcquireLock when CanAcquireLock returned false."), EAutomationExpectedErrorFlags::Contains, 1); + } + + AActor* Actor = Data->TestActors[ActorHandle]; + const ActorLockToken Token = Data->LockingPolicy->AcquireLock(Actor, DebugString); + const bool bAcquireLockSucceeded = Token != SpatialConstants::INVALID_ACTOR_LOCK_TOKEN; + + // If the token returned is valid, it MUST be unique. + if (bAcquireLockSucceeded) + { + for (const TPair>& ActorLockingTokenAndDebugStrings : Data->TestActorToLockingTokenAndDebugStrings) + { + const TArray& LockingTokensAndDebugStrings = ActorLockingTokenAndDebugStrings.Value; + bool TokenAlreadyExists = LockingTokensAndDebugStrings.ContainsByPredicate([Token](const LockingTokenAndDebugString& Data) + { + return Token == Data.Key; + }); + if (TokenAlreadyExists) + { + Test->AddError(FString::Printf(TEXT("AcquireLock returned a valid ActorLockToken that had already been assigned. Token: %d"), Token)); + } + } + } + + Test->TestFalse(TEXT("Expected AcquireLock to succeed but it failed"), bExpectedSuccess && !bAcquireLockSucceeded); + Test->TestFalse(TEXT("Expected AcquireLock to fail but it succeeded"), !bExpectedSuccess && bAcquireLockSucceeded); + + TArray& ActorLockTokens = Data->TestActorToLockingTokenAndDebugStrings.FindOrAdd(Actor); + ActorLockTokens.Emplace(TPairInitializer(Token, DebugString)); + + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(FReleaseLock, FAutomationTestBase*, Test, TSharedPtr, Data, FName, ActorHandle, FString, LockDebugString, bool, bExpectedSuccess); +bool FReleaseLock::Update() +{ + const AActor* Actor = Data->TestActors[ActorHandle]; + + // Find lock token based on relevant lock debug string. + TArray* LockTokenAndDebugStrings = Data->TestActorToLockingTokenAndDebugStrings.Find(Actor); + + // If we're double releasing or releasing with a non-existent token then either we should expect to fail or the test is broken. + if (LockTokenAndDebugStrings == nullptr) + { + check(!bExpectedSuccess); + Test->AddExpectedError(TEXT("Called ReleaseLock for unidentified Actor lock token."), EAutomationExpectedErrorFlags::Contains, 1); + const bool bReleaseLockSucceeded = Data->LockingPolicy->ReleaseLock(SpatialConstants::INVALID_ACTOR_LOCK_TOKEN); + Test->TestFalse(TEXT("Expected ReleaseLockDelegate to fail but it succeeded"), !bExpectedSuccess && bReleaseLockSucceeded); + return true; + } + + int32 TokenIndex = LockTokenAndDebugStrings->IndexOfByPredicate([this](const LockingTokenAndDebugString& Data) + { + return Data.Value == LockDebugString; + }); + Test->TestTrue("Found valid lock token", TokenIndex != INDEX_NONE); + + LockingTokenAndDebugString& LockTokenAndDebugString = (*LockTokenAndDebugStrings)[TokenIndex]; + + const bool bReleaseLockSucceeded = Data->LockingPolicy->ReleaseLock(LockTokenAndDebugString.Key); + + Test->TestFalse(TEXT("Expected ReleaseLockDelegate to succeed but it failed"), bExpectedSuccess && !bReleaseLockSucceeded); + + LockTokenAndDebugStrings->RemoveAt(TokenIndex); + + // If removing last token for Actor, delete map entry. + if (LockTokenAndDebugStrings->Num() == 0) + { + Data->TestActorToLockingTokenAndDebugStrings.Remove(Actor); + } + + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FReleaseAllLocks, FAutomationTestBase*, Test, TSharedPtr, Data, int32, ExpectedFailures); +bool FReleaseAllLocks::Update() +{ + const bool bExpectedSuccess = ExpectedFailures == 0; + + if (!bExpectedSuccess) + { + Test->AddExpectedError(TEXT("Called ReleaseLock for unidentified Actor lock token."), EAutomationExpectedErrorFlags::Contains, ExpectedFailures); + } + + // Attempt to release every lock token for every Actor. + for (TPair>& ActorAndLockingTokenAndDebugStringsPair : Data->TestActorToLockingTokenAndDebugStrings) + { + for (LockingTokenAndDebugString& TokenAndDebugString : ActorAndLockingTokenAndDebugStringsPair.Value) + { + const ActorLockToken Token = TokenAndDebugString.Key; + const bool bReleaseLockSucceeded = Data->LockingPolicy->ReleaseLock(Token); + Test->TestFalse(FString::Printf(TEXT("Expected ReleaseAllLocks to fail but it succeeded. Token: %d"), Token), !bExpectedSuccess && bReleaseLockSucceeded); + Test->TestFalse(FString::Printf(TEXT("Expected ReleaseAllLocks to succeed but it failed. Token: %d"), Token), bExpectedSuccess && !bReleaseLockSucceeded); + } + } + + // Cleanup the test mapping of Actors to tokens and debug strings. + Data->TestActorToLockingTokenAndDebugStrings.Reset(); + + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(FAcquireLockViaDelegate, FAutomationTestBase*, Test, TSharedPtr, Data, FName, ActorHandle, FString, DelegateLockIdentifier, bool, bExpectedSuccess); +bool FAcquireLockViaDelegate::Update() +{ + AActor* Actor = Data->TestActors[ActorHandle]; + + check(Data->AcquireLockDelegate.IsBound()); + const bool bAcquireLockSucceeded = Data->AcquireLockDelegate.Execute(Actor, DelegateLockIdentifier); + + Test->TestFalse(TEXT("Expected AcquireLockDelegate to succeed but it failed"), bExpectedSuccess && !bAcquireLockSucceeded); + Test->TestFalse(TEXT("Expected AcquireLockDelegate to fail but it succeeded"), !bExpectedSuccess && bAcquireLockSucceeded); + + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(FReleaseLockViaDelegate, FAutomationTestBase*, Test, TSharedPtr, Data, FName, ActorHandle, FString, DelegateLockIdentifier, bool, bExpectedSuccess); +bool FReleaseLockViaDelegate::Update() +{ + AActor* Actor = Data->TestActors[ActorHandle]; + + check(Data->ReleaseLockDelegate.IsBound()); + + if (!bExpectedSuccess) + { + Test->AddExpectedError(TEXT("Executed ReleaseLockDelegate for unidentified delegate lock identifier."), EAutomationExpectedErrorFlags::Contains, 1); + } + + const bool bReleaseLockSucceeded = Data->ReleaseLockDelegate.Execute(Actor, DelegateLockIdentifier); + + Test->TestFalse(TEXT("Expected ReleaseLockDelegate to succeed but it failed"), bExpectedSuccess && !bReleaseLockSucceeded); + Test->TestFalse(TEXT("Expected ReleaseLockDelegate to fail but it succeeded"), !bExpectedSuccess && bReleaseLockSucceeded); + + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_FOUR_PARAMETER(FTestIsLocked, FAutomationTestBase*, Test, TSharedPtr, Data, FName, Handle, bool, bIsLockedExpected); +bool FTestIsLocked::Update() +{ + const AActor* Actor = Data->TestActors[Handle]; + const bool bIsLocked = Data->LockingPolicy->IsLocked(Actor); + Test->TestEqual(FString::Printf(TEXT("%s. Is locked. Actual: %d. Expected: %d"), *Handle.ToString(), bIsLocked, bIsLockedExpected), bIsLocked, bIsLockedExpected); + return true; +} + +void SpawnABCDHierarchy(FAutomationTestBase* Test, TSharedPtr Data) +{ + // A + // / \ + // B D + // / + // C + + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data, "A")); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data, "B")); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data, "C")); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data, "D")); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "A")); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "B")); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "C")); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "D")); + + ADD_LATENT_AUTOMATION_COMMAND(FSetOwnership(Data, "C", "B")); + ADD_LATENT_AUTOMATION_COMMAND(FSetOwnership(Data, "B", "A")); + ADD_LATENT_AUTOMATION_COMMAND(FSetOwnership(Data, "D", "A")); +} + +void SpawnABCDEHierarchy(FAutomationTestBase* Test, TSharedPtr Data) +{ + // A E + // / \ + // B D + // / + // C + + SpawnABCDHierarchy(Test, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data, "E")); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "E")); +} + +} // anonymous namespace + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_an_actor_has_not_been_locked_WHEN_IsLocked_is_called_THEN_returns_false_with_no_lock_tokens) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + + return true; +} + +// AcquireLock and ReleaseLock + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_Actor_is_not_locked_WHEN_ReleaseLock_is_called_THEN_it_errors_and_returns_false) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + ADD_LATENT_AUTOMATION_COMMAND(FReleaseLock(this, Data, "Actor", "First lock", false)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_WHEN_the_locked_Actor_is_not_authoritative_THEN_AcquireLock_returns_false) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FSetActorRole(Data, "Actor", ROLE_SimulatedProxy)); + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "Actor", "First lock", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_WHEN_the_locked_Actor_is_deleted_THEN_ReleaseLock_returns_false) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "Actor", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FDestroyActor(Data, "Actor")); + // We want to test that the Actor deletion has correctly been cleaned up in the locking policy. + // We cannot call IsLocked with a deleted Actor so instead we try to release the lock we held + // for the Actor and check that it fails. + ADD_LATENT_AUTOMATION_COMMAND(FReleaseAllLocks(this, Data, 1)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_twice_WHEN_the_locked_Actor_is_deleted_THEN_ReleaseLock_returns_false) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "Actor", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "Actor", "Second lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FDestroyActor(Data, "Actor")); + // We want to test that the Actor deletion has correctly been cleaned up in the locking policy. + // We cannot call IsLocked with a deleted Actor so instead we try to release the lock we held + // for the Actor and check that it fails. + ADD_LATENT_AUTOMATION_COMMAND(FReleaseAllLocks(this, Data, 2)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_WHEN_IsLocked_is_called_THEN_returns_correctly_between_calls) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "Actor", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", true)); + ADD_LATENT_AUTOMATION_COMMAND(FReleaseLock(this, Data, "Actor", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_twice_WHEN_IsLocked_is_called_THEN_returns_correctly_between_calls) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "Actor", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", true)); + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "Actor", "Second lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", true)); + ADD_LATENT_AUTOMATION_COMMAND(FReleaseLock(this, Data, "Actor", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", true)); + ADD_LATENT_AUTOMATION_COMMAND(FReleaseLock(this, Data, "Actor", "Second lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_WHEN_ReleaseLock_is_called_again_THEN_it_errors_and_returns_false) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "Actor", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", true)); + ADD_LATENT_AUTOMATION_COMMAND(FReleaseLock(this, Data, "Actor", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + ADD_LATENT_AUTOMATION_COMMAND(FReleaseLock(this, Data, "Actor", "First lock", false)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_WHEN_AcquireLock_is_called_again_THEN_it_succeeds) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "Actor", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", true)); + ADD_LATENT_AUTOMATION_COMMAND(FReleaseLock(this, Data, "Actor", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "Actor", "Second lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", true)); + + return true; +} + +// Hierarchy Actors + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_on_hierarchy_leaf_Actor_WHEN_IsLocked_is_called_on_hierarchy_Actors_THEN_returns_correctly_between_calls) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + SpawnABCDHierarchy(this, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "C", "First lock", true)); + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", true)); + + ADD_LATENT_AUTOMATION_COMMAND(FReleaseLock(this, Data, "C", "First lock", true)); + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", false)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_on_hierarchy_path_Actor_WHEN_IsLocked_is_called_on_hierarchy_Actors_THEN_returns_correctly_between_calls) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + SpawnABCDHierarchy(this, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "B", "First lock", true)); + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", true)); + + ADD_LATENT_AUTOMATION_COMMAND(FReleaseLock(this, Data, "B", "First lock", true)); + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", false)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_on_hierarchy_root_Actor_WHEN_IsLocked_is_called_on_hierarchy_Actors_THEN_returns_correctly_between_calls) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + SpawnABCDHierarchy(this, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "A", "First lock", true)); + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", true)); + + ADD_LATENT_AUTOMATION_COMMAND(FReleaseLock(this, Data, "A", "First lock", true)); + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", false)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_on_multiple_hierarchy_Actors_WHEN_IsLocked_is_called_on_hierarchy_Actors_THEN_returns_correctly_between_calls) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + SpawnABCDHierarchy(this, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "C", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "D", "Second lock", true)); + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", true)); + + ADD_LATENT_AUTOMATION_COMMAND(FReleaseLock(this, Data, "C", "First lock", true)); + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", true)); + + ADD_LATENT_AUTOMATION_COMMAND(FReleaseLock(this, Data, "D", "Second lock", true)); + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", false)); + + return true; +} + +// Actor Destruction + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_leaf_Actor_WHEN_explicitly_locked_Actor_is_destroyed_THEN_IsLocked_returns_correctly_between_calls) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + SpawnABCDHierarchy(this, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "C", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FDestroyActor(Data, "C")); + + // A + // / \ + // B D + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", false)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_leaf_Actor_WHEN_hierarchy_path_Actor_is_destroyed_THEN_IsLocked_returns_correctly_between_calls) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + SpawnABCDHierarchy(this, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "C", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FDestroyActor(Data, "B")); + + // C (explicitly locked) A + // | + // D + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", false)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_leaf_Actor_WHEN_hierarchy_root_is_destroyed_THEN_IsLocked_returns_correctly_between_calls) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + SpawnABCDHierarchy(this, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "C", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FDestroyActor(Data, "A")); + + // B D + // | + // C (explicitly locked) + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", false)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_path_Actor_WHEN_hierarchy_root_is_destroyed_THEN_IsLocked_returns_correctly_between_calls) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + SpawnABCDHierarchy(this, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "B", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FDestroyActor(Data, "A")); + + // B (explicitly locked) D + // | + // C + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", false)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_root_Actor_WHEN_hierarchy_root_is_destroyed_THEN_IsLocked_returns_correctly_between_calls) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + SpawnABCDHierarchy(this, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "A", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FDestroyActor(Data, "A")); + + // B D + // | + // C + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", false)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_root_Actor_WHEN_hierarchy_path_Actor_is_destroyed_THEN_IsLocked_returns_correctly_between_calls) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + SpawnABCDHierarchy(this, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "A", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FDestroyActor(Data, "B")); + + // A (explicitly locked) + // | + // C D + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", true)); + + return true; +} + +// Owner Changes + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_leaf_hierarchy_Actor_WHEN_hierarchy_root_switches_owner_THEN_IsLocked_returns_correctly_for_all_Actors) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + SpawnABCDEHierarchy(this, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "C", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FSetOwnership(Data, "A", "E")); + + // E + // / + // A + // / \ + // B D + // / + // C (explicitly locked) + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "E", true)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_leaf_hierarchy_Actor_WHEN_hierarchy_path_Actor_switches_owner_THEN_IsLocked_returns_correctly_for_all_Actors) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + SpawnABCDEHierarchy(this, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "B", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FSetOwnership(Data, "B", "E")); + + // A E + // | | + // D B (explicitly locked) + // | + // C + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "E", true)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_leaf_hierarchy_Actor_WHEN_explicitly_locked_Actor_switches_owner_THEN_IsLocked_returns_correctly_for_all_Actors) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + SpawnABCDEHierarchy(this, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "C", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FSetOwnership(Data, "C", "E")); + + // A E + // / \ | + // B D C (explicitly locked) + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "E", true)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_path_Actor_WHEN_explictly_locked_Actor_switches_owner_THEN_IsLocked_returns_correctly_for_all_Actors) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + SpawnABCDEHierarchy(this, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "B", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FSetOwnership(Data, "B", "E")); + + // A E + // | | + // D B (explicitly locked) + // | + // C + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "E", true)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_path_Actor_WHEN_hierarchy_root_switches_owner_THEN_IsLocked_returns_correctly_for_all_Actors) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + SpawnABCDEHierarchy(this, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "B", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FSetOwnership(Data, "A", "E")); + + // E + // | + // A + // / \ + // B (explicitly locked) D + // / + // C + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "E", true)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_path_Actor_WHEN_hierarchy_leaf_switches_owner_THEN_IsLocked_returns_correctly_for_all_Actors) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + SpawnABCDEHierarchy(this, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "B", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FSetOwnership(Data, "C", "E")); + + // A E + // / \ | + // B (explicitly locked) D C + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "E", false)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_root_WHEN_hierarchy_root_switches_owner_THEN_IsLocked_returns_correctly_for_all_Actors) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + SpawnABCDEHierarchy(this, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "A", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FSetOwnership(Data, "A", "E")); + + // E + // | + // A (explicitly locked) + // / \ + // B D + // / + // C + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "E", true)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_root_WHEN_hierarchy_path_Actor_switches_owner_THEN_IsLocked_returns_correctly_for_all_Actors) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + SpawnABCDEHierarchy(this, Data); + + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "A", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FSetOwnership(Data, "B", "E")); + + // A (explicitly locked) E + // | | + // D B + // | + // C + + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "A", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "B", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "C", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "D", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "E", false)); + + return true; +} + +// AcquireLockDelegate and ReleaseLockDelegate + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_Actor_is_not_locked_WHEN_ReleaseLock_delegate_is_executed_THEN_it_errors_and_returns_false) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + ADD_LATENT_AUTOMATION_COMMAND(FReleaseLockViaDelegate(this, Data, "Actor", "First lock", false)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_delegates_are_executed_WHEN_IsLocked_is_called_THEN_returns_correctly_between_calls) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLockViaDelegate(this, Data, "Actor", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", true)); + ADD_LATENT_AUTOMATION_COMMAND(FReleaseLockViaDelegate(this, Data, "Actor", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_delegates_are_executed_twice_WHEN_IsLocked_is_called_THEN_returns_correctly_between_calls) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLockViaDelegate(this, Data, "Actor", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", true)); + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLockViaDelegate(this, Data, "Actor", "Second lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", true)); + ADD_LATENT_AUTOMATION_COMMAND(FReleaseLockViaDelegate(this, Data, "Actor", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", true)); + ADD_LATENT_AUTOMATION_COMMAND(FReleaseLockViaDelegate(this, Data, "Actor", "Second lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + + return true; +} + +OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLockDelegate_and_ReleaseLockDelegate_are_executed_WHEN_ReleaseLockDelegate_is_executed_again_THEN_it_errors_and_returns_false) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = MakeNewTestData(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "Actor")); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + ADD_LATENT_AUTOMATION_COMMAND(FAcquireLockViaDelegate(this, Data, "Actor", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", true)); + ADD_LATENT_AUTOMATION_COMMAND(FReleaseLockViaDelegate(this, Data, "Actor", "First lock", true)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); + ADD_LATENT_AUTOMATION_COMMAND(FReleaseLockViaDelegate(this, Data, "Actor", "First lock", false)); + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Schema/UnrealObjectRef/UnrealObjectRefTests.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Schema/UnrealObjectRef/UnrealObjectRefTests.cpp new file mode 100644 index 0000000000..775da63133 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Schema/UnrealObjectRef/UnrealObjectRefTests.cpp @@ -0,0 +1,32 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "CoreMinimal.h" + +#include "Tests/TestDefinitions.h" +#include "Tests/AutomationCommon.h" +#include "Schema/UnrealObjectRef.h" +#include "UObject/SoftObjectPtr.h" + +#define UNREALOBJECTREF_TEST(TestName) \ + GDK_TEST(Core, FUnrealObjectRef, TestName) + +UNREALOBJECTREF_TEST(GIVEN_a_softpointer_WHEN_making_an_object_ref_from_it_THEN_we_can_recover_it) +{ + FString PackagePath = "/Game/TestAsset/DummyAsset"; + FString ObjectName = "DummyObject"; + FSoftObjectPath SoftPath(PackagePath + "." + ObjectName); + FSoftObjectPtr DummySoftReference(SoftPath); + + FUnrealObjectRef SoftObjectRef = FUnrealObjectRef::FromSoftObjectPath(DummySoftReference.ToSoftObjectPath()); + + TestTrue("Got a stably named reference", SoftObjectRef.Path.IsSet() && SoftObjectRef.Path.IsSet()); + + FSoftObjectPtr OutPtr; + OutPtr = FUnrealObjectRef::ToSoftObjectPath(SoftObjectRef); + + TestTrue("Can serialize a SoftObjectPointer", DummySoftReference == OutPtr); + + return true; +} + +// TODO : [UNR-2691] Add tests involving the PackageMapClient, with entity Id and actual assets to generate the path to/from (needs a NetDriver right now). diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/CheckoutRadiusConstraintUtils/NetCullDistanceInterestTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/CheckoutRadiusConstraintUtils/NetCullDistanceInterestTest.cpp new file mode 100644 index 0000000000..67ad4b405b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/CheckoutRadiusConstraintUtils/NetCullDistanceInterestTest.cpp @@ -0,0 +1,75 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/TestDefinitions.h" + +#include "HAL/IPlatformFileProfilerWrapper.h" +#include "HAL/PlatformFilemanager.h" +#include "Misc/ScopeTryLock.h" +#include "Misc/Paths.h" + +#include "Utils/Interest/NetCullDistanceInterest.h" + +#define CHECKOUT_RADIUS_CONSTRAINT_TEST(TestName) \ + GDK_TEST(Core, NetCullDistanceInterest, TestName) + +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKCheckoutRadiusTest, Log, All); +DEFINE_LOG_CATEGORY(LogSpatialGDKCheckoutRadiusTest); + +// Run tests inside the SpatialGDK namespace in order to test static functions. +namespace SpatialGDK +{ + CHECKOUT_RADIUS_CONSTRAINT_TEST(GIVEN_actor_type_to_radius_map_WHEN_radius_is_duplicated_THEN_correctly_dedupes) + { + float Radius = 5.f; + TMap Map; + UClass* Class1 = NewObject(); + UClass* Class2 = NewObject(); + Map.Add(Class1, Radius); + Map.Add(Class2, Radius); + + TMap> DedupedMap = NetCullDistanceInterest::DedupeDistancesAcrossActorTypes(Map); + + int32 ExpectedSize = 1; + TestTrue("There is only one entry in the map", DedupedMap.Num() == ExpectedSize); + + TArray Classes = DedupedMap[Radius]; + TArray ExpectedClasses; + ExpectedClasses.Add(Class1); + ExpectedClasses.Add(Class2); + + TestTrue("All UClasses are accounted for", Classes == ExpectedClasses); + + return true; + } + + CHECKOUT_RADIUS_CONSTRAINT_TEST(GIVEN_actor_type_to_radius_map_WHEN_radius_is_not_duplicated_THEN_does_not_dedupe) + { + float Radius1 = 5.f; + float Radius2 = 6.f; + TMap Map; + UClass* Class1 = NewObject(); + UClass* Class2 = NewObject(); + Map.Add(Class1, Radius1); + Map.Add(Class2, Radius2); + + TMap> DedupedMap = NetCullDistanceInterest::DedupeDistancesAcrossActorTypes(Map); + + int32 ExpectedSize = 2; + TestTrue("There are two entries in the map", DedupedMap.Num() == ExpectedSize); + + TArray Classes = DedupedMap[Radius1]; + TArray ExpectedClasses; + ExpectedClasses.Add(Class1); + + TestTrue("Class for first radius is present", Classes == ExpectedClasses); + + Classes = DedupedMap[Radius2]; + ExpectedClasses.Empty(); + ExpectedClasses.Add(Class2); + + TestTrue("Class for second radius is present", Classes == ExpectedClasses); + + return true; + } +} + diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/Misc/SpatialActivationFlagsTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/Misc/SpatialActivationFlagsTest.cpp new file mode 100644 index 0000000000..055d59bda6 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/Misc/SpatialActivationFlagsTest.cpp @@ -0,0 +1,180 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "CoreMinimal.h" + +#include "Tests/TestDefinitions.h" +#include "Tests/AutomationCommon.h" +#include "Runtime/EngineSettings/Public/EngineSettings.h" + +namespace +{ + bool bEarliestFlag; + + const FString EarliestFlagReport = TEXT("Spatial activation Flag [Earliest]:"); + const FString CurrentFlagReport = TEXT("Spatial activation Flag [Current]:"); +} + +void InitializeSpatialFlagEarlyValues() +{ + bEarliestFlag = GetDefault()->UsesSpatialNetworking(); +} + +GDK_TEST(Core, UGeneralProjectSettings, SpatialActivationReport) +{ + const UGeneralProjectSettings* ProjectSettings = GetDefault(); + + UE_LOG(LogTemp, Display, TEXT("%s %i"), *EarliestFlagReport, bEarliestFlag); + UE_LOG(LogTemp, Display, TEXT("%s %i"), *CurrentFlagReport, ProjectSettings->UsesSpatialNetworking()); + + return true; +} + +namespace +{ + struct ReportedFlags + { + bool bEarliestFlag; + bool bCurrentFlag; + }; + + ReportedFlags RunSubProcessAndExtractFlags(FAutomationTestBase& Test, const FString& CommandLineArgs) + { + ReportedFlags Flags; + + int32 ReturnCode = 1; + FString StdOut; + FString StdErr; + + FPlatformProcess::ExecProcess(TEXT("UE4Editor"), *CommandLineArgs, &ReturnCode, &StdOut, &StdErr); + + Test.TestTrue("Successful run", ReturnCode == 0); + + auto ExtractFlag = [&](const FString& Pattern, bool& bFlag) + { + int32 PatternPos = StdOut.Find(Pattern); + Test.TestTrue(*(TEXT("Found pattern : ") + Pattern), PatternPos >= 0); + bFlag = FCString::Atoi(&StdOut[PatternPos + Pattern.Len() + 1]) != 0; + }; + + ExtractFlag(EarliestFlagReport, Flags.bEarliestFlag); + ExtractFlag(CurrentFlagReport, Flags.bCurrentFlag); + + return Flags; + } +} + +struct SpatialActivationFlagTestFixture +{ + SpatialActivationFlagTestFixture(FAutomationTestBase& Test) + { + ProjectPath = FPaths::GetProjectFilePath(); + CommandLineArgs = ProjectPath; + CommandLineArgs.Append(TEXT(" -ExecCmds=\"Automation RunTests SpatialGDK.Core.UGeneralProjectSettings.SpatialActivationReport; Quit\"")); + CommandLineArgs.Append(TEXT(" -TestExit=\"Automation Test Queue Empty\"")); + CommandLineArgs.Append(TEXT(" -nopause")); + CommandLineArgs.Append(TEXT(" -nosplash")); + CommandLineArgs.Append(TEXT(" -unattended")); + CommandLineArgs.Append(TEXT(" -nullRHI")); + CommandLineArgs.Append(TEXT(" -stdout")); + + SpatialFlagProperty = Cast(UGeneralProjectSettings::StaticClass()->FindPropertyByName("bSpatialNetworking")); + Test.TestNotNull("Property existence", SpatialFlagProperty); + + ProjectSettings = GetMutableDefault(); + Test.TestNotNull("Settings existence", ProjectSettings); + + SpatialFlagPtr = SpatialFlagProperty->ContainerPtrToValuePtr(ProjectSettings); + bSavedFlagValue = SpatialFlagProperty->GetPropertyValue(SpatialFlagPtr); + } + + ~SpatialActivationFlagTestFixture() + { + ProjectSettings->SetUsesSpatialNetworking(bSavedFlagValue); + ProjectSettings->UpdateSinglePropertyInConfigFile(SpatialFlagProperty, ProjectSettings->GetDefaultConfigFilename()); + } + + void ChangeSetting(bool bEnabled) + { + ProjectSettings->SetUsesSpatialNetworking(bEnabled); + ProjectSettings->UpdateSinglePropertyInConfigFile(SpatialFlagProperty, ProjectSettings->GetDefaultConfigFilename()); + } + + FString CommandLineArgs; + TFuture CheckResult; + +private: + FString ProjectPath; + UBoolProperty* SpatialFlagProperty; + UGeneralProjectSettings* ProjectSettings; + void* SpatialFlagPtr; + bool bSavedFlagValue; +}; + +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FRunSubProcessCommand, FAutomationTestBase*, Test, TSharedPtr, Fixture, bool, ExpectedValue); +bool FRunSubProcessCommand::Update() +{ + if (!Fixture->CheckResult.IsValid()) + { + Fixture->CheckResult = Async(EAsyncExecution::Thread, TFunction([&] {return RunSubProcessAndExtractFlags(*Test, Fixture->CommandLineArgs); })); + } + + if (!Fixture->CheckResult.IsReady()) + { + return false; + } + ReportedFlags Flags = Fixture->CheckResult.Get(); + + Test->TestTrue("Settings applied", Flags.bEarliestFlag == ExpectedValue); + Test->TestTrue("Expected early value", Flags.bCurrentFlag == Flags.bEarliestFlag); + + return true; +} + + +GDK_TEST(Core, UGeneralProjectSettings, SpatialActivationSetting_False) +{ + auto TestFixture = MakeShared(*this); + TestFixture->ChangeSetting(false); + + ADD_LATENT_AUTOMATION_COMMAND(FRunSubProcessCommand(this, TestFixture, false)); + + return true; +} + +GDK_TEST(Core, UGeneralProjectSettings, SpatialActivationSetting_True) +{ + auto TestFixture = MakeShared(*this); + TestFixture->ChangeSetting(true); + + ADD_LATENT_AUTOMATION_COMMAND(FRunSubProcessCommand(this, TestFixture, true)); + + return true; +} + +GDK_TEST(Core, UGeneralProjectSettings, SpatialActivationOverride_True) +{ + auto TestFixture = MakeShared(*this); + TestFixture->ChangeSetting(false); + + FString CommandLineOverride = TestFixture->CommandLineArgs; + CommandLineOverride.Append(" -OverrideSpatialNetworking=true"); + TestFixture->CommandLineArgs = CommandLineOverride; + + ADD_LATENT_AUTOMATION_COMMAND(FRunSubProcessCommand(this, TestFixture, true)); + + return true; +} + +GDK_TEST(Core, UGeneralProjectSettings, SpatialActivationOverride_False) +{ + auto TestFixture = MakeShared(*this); + TestFixture->ChangeSetting(false); + + FString CommandLineOverride = TestFixture->CommandLineArgs; + CommandLineOverride.Append(" -OverrideSpatialNetworking=false"); + TestFixture->CommandLineArgs = CommandLineOverride; + + ADD_LATENT_AUTOMATION_COMMAND(FRunSubProcessCommand(this, TestFixture, false)); + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/ObjectDummy.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/ObjectDummy.cpp index 56677a4dff..6ef4870fc3 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/ObjectDummy.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/ObjectDummy.cpp @@ -4,5 +4,5 @@ FRPCErrorInfo UObjectDummy::ProcessRPC(const FPendingRPCParams& Params) { - return FRPCErrorInfo{ nullptr, nullptr, true, ERPCQueueType::Send, ERPCResult::Success }; + return FRPCErrorInfo{ nullptr, nullptr, ERPCResult::Success }; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/ObjectSpy.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/ObjectSpy.cpp index 86f999a6bd..087826ba27 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/ObjectSpy.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/ObjectSpy.cpp @@ -4,22 +4,22 @@ // If this assertion fails, then TypeToArray and ArrayToType // functions have to be updated correspondingly -static_assert(sizeof(ESchemaComponentType) == sizeof(int32), ""); +static_assert(sizeof(ERPCType) == sizeof(uint8), ""); -TArray SpyUtils::SchemaTypeToByteArray(ESchemaComponentType Type) +TArray SpyUtils::RPCTypeToByteArray(ERPCType Type) { - int32 ConvertedType = static_cast(Type); - return TArray(reinterpret_cast(&ConvertedType), sizeof(ConvertedType)); + uint8 ConvertedType = static_cast(Type); + return TArray(&ConvertedType, sizeof(ConvertedType)); } -ESchemaComponentType SpyUtils::ByteArrayToSchemaType(const TArray& Array) +ERPCType SpyUtils::ByteArrayToRPCType(const TArray& Array) { - return ESchemaComponentType(*reinterpret_cast(&Array[0])); + return ERPCType(Array[0]); } FRPCErrorInfo UObjectSpy::ProcessRPC(const FPendingRPCParams& Params) { - ESchemaComponentType Type = SpyUtils::ByteArrayToSchemaType(Params.Payload.PayloadData); + ERPCType Type = SpyUtils::ByteArrayToRPCType(Params.Payload.PayloadData); ProcessedRPCIndices.FindOrAdd(Type).Push(Params.Payload.Index); - return FRPCErrorInfo{ nullptr, nullptr, true, ERPCQueueType::Send, ERPCResult::Success }; + return FRPCErrorInfo{ nullptr, nullptr, ERPCResult::Success }; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/ObjectSpy.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/ObjectSpy.h index 429da79c85..1dedb94b16 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/ObjectSpy.h +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/ObjectSpy.h @@ -10,8 +10,8 @@ namespace SpyUtils { - TArray SchemaTypeToByteArray(ESchemaComponentType Type); - ESchemaComponentType ByteArrayToSchemaType(const TArray& Array); + TArray RPCTypeToByteArray(ERPCType Type); + ERPCType ByteArrayToRPCType(const TArray& Array); } // namespace SpyUtils UCLASS() @@ -21,5 +21,5 @@ class UObjectSpy : public UObject public: FRPCErrorInfo ProcessRPC(const FPendingRPCParams& Params); - TMap> ProcessedRPCIndices; + TMap> ProcessedRPCIndices; }; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/ObjectStub.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/ObjectStub.cpp index 8564e76ea5..d07f2c9729 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/ObjectStub.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/ObjectStub.cpp @@ -4,5 +4,5 @@ FRPCErrorInfo UObjectStub::ProcessRPC(const FPendingRPCParams& Params) { - return FRPCErrorInfo{ nullptr, nullptr, true, ERPCQueueType::Send, ERPCResult::UnresolvedParameters }; + return FRPCErrorInfo{ nullptr, nullptr, ERPCResult::UnresolvedParameters }; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/RPCContainerTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/RPCContainerTest.cpp index 511feacba7..bb4f78ae3c 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/RPCContainerTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/RPCContainerTest.cpp @@ -1,6 +1,6 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved -#include "TestDefinitions.h" +#include "Tests/TestDefinitions.h" #include "ObjectDummy.h" #include "ObjectSpy.h" @@ -8,6 +8,7 @@ #include "Utils/RPCContainer.h" #include "Schema/RPCPayload.h" +#include "SpatialGDKSettings.h" #include "CoreMinimal.h" @@ -18,8 +19,8 @@ using namespace SpatialGDK; namespace { - ESchemaComponentType AnySchemaComponentType = ESchemaComponentType::SCHEMA_ClientReliableRPC; - ESchemaComponentType AnyOtherSchemaComponentType = ESchemaComponentType::SCHEMA_ClientUnreliableRPC; + ERPCType AnySchemaComponentType = ERPCType::ClientReliable; + ERPCType AnyOtherSchemaComponentType = ERPCType::ClientUnreliable; FUnrealObjectRef GenerateObjectRef(UObject* TargetObject) { @@ -32,10 +33,10 @@ namespace return FreeIndex++; } - FPendingRPCParams CreateMockParameters(UObject* TargetObject, ESchemaComponentType Type) + FPendingRPCParams CreateMockParameters(UObject* TargetObject, ERPCType Type) { // Use PayloadData as a place to store RPC type - RPCPayload Payload(0, GeneratePayloadFunctionIndex(), SpyUtils::SchemaTypeToByteArray(Type)); + RPCPayload Payload(0, GeneratePayloadFunctionIndex(), SpyUtils::RPCTypeToByteArray(Type)); int ReliableRPCIndex = 0; FUnrealObjectRef ObjectRef = GenerateObjectRef(TargetObject); @@ -48,18 +49,18 @@ RPCCONTAINER_TEST(GIVEN_a_container_WHEN_nothing_has_been_added_THEN_nothing_is_ { UObjectDummy* TargetObject = NewObject(); FPendingRPCParams Params = CreateMockParameters(TargetObject, AnySchemaComponentType); - FRPCContainer RPCs; + FRPCContainer RPCs(ERPCQueueType::Send); TestFalse("Has queued RPCs", RPCs.ObjectHasRPCsQueuedOfType(Params.ObjectRef.Entity, AnySchemaComponentType)); - return true; + return true; } RPCCONTAINER_TEST(GIVEN_a_container_WHEN_one_value_has_been_added_THEN_it_is_queued) { UObjectStub* TargetObject = NewObject(); FPendingRPCParams Params = CreateMockParameters(TargetObject, AnySchemaComponentType); - FRPCContainer RPCs; + FRPCContainer RPCs(ERPCQueueType::Send); RPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(TargetObject, &UObjectStub::ProcessRPC)); RPCs.ProcessOrQueueRPC(Params.ObjectRef, Params.Type, MoveTemp(Params.Payload)); @@ -74,7 +75,7 @@ RPCCONTAINER_TEST(GIVEN_a_container_WHEN_multiple_values_of_same_type_have_been_ UObjectStub* TargetObject = NewObject(); FPendingRPCParams Params1 = CreateMockParameters(TargetObject, AnyOtherSchemaComponentType); FPendingRPCParams Params2 = CreateMockParameters(TargetObject, AnyOtherSchemaComponentType); - FRPCContainer RPCs; + FRPCContainer RPCs(ERPCQueueType::Send); RPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(TargetObject, &UObjectStub::ProcessRPC)); RPCs.ProcessOrQueueRPC(Params1.ObjectRef, Params1.Type, MoveTemp(Params1.Payload)); @@ -89,7 +90,7 @@ RPCCONTAINER_TEST(GIVEN_a_container_storing_one_value_WHEN_processed_once_THEN_n { UObjectDummy* TargetObject = NewObject(); FPendingRPCParams Params = CreateMockParameters(TargetObject, AnySchemaComponentType); - FRPCContainer RPCs; + FRPCContainer RPCs(ERPCQueueType::Send); RPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(TargetObject, &UObjectDummy::ProcessRPC)); RPCs.ProcessOrQueueRPC(Params.ObjectRef, Params.Type, MoveTemp(Params.Payload)); @@ -104,7 +105,7 @@ RPCCONTAINER_TEST(GIVEN_a_container_storing_multiple_values_of_same_type_WHEN_pr UObjectDummy* TargetObject = NewObject(); FPendingRPCParams Params1 = CreateMockParameters(TargetObject, AnyOtherSchemaComponentType); FPendingRPCParams Params2 = CreateMockParameters(TargetObject, AnyOtherSchemaComponentType); - FRPCContainer RPCs; + FRPCContainer RPCs(ERPCQueueType::Send); RPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(TargetObject, &UObjectDummy::ProcessRPC)); RPCs.ProcessOrQueueRPC(Params1.ObjectRef, Params1.Type, MoveTemp(Params1.Payload)); @@ -122,7 +123,7 @@ RPCCONTAINER_TEST(GIVEN_a_container_WHEN_multiple_values_of_different_type_have_ FPendingRPCParams ParamsUnreliable = CreateMockParameters(TargetObject, AnyOtherSchemaComponentType); FPendingRPCParams ParamsReliable = CreateMockParameters(TargetObject, AnySchemaComponentType); - FRPCContainer RPCs; + FRPCContainer RPCs(ERPCQueueType::Send); RPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(TargetObject, &UObjectStub::ProcessRPC)); RPCs.ProcessOrQueueRPC(ParamsUnreliable.ObjectRef, ParamsUnreliable.Type, MoveTemp(ParamsUnreliable.Payload)); @@ -140,7 +141,7 @@ RPCCONTAINER_TEST(GIVEN_a_container_storing_multiple_values_of_different_type_WH FPendingRPCParams ParamsUnreliable = CreateMockParameters(TargetObject, AnyOtherSchemaComponentType); FPendingRPCParams ParamsReliable = CreateMockParameters(TargetObject, AnySchemaComponentType); - FRPCContainer RPCs; + FRPCContainer RPCs(ERPCQueueType::Send); RPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(TargetObject, &UObjectDummy::ProcessRPC)); RPCs.ProcessOrQueueRPC(ParamsUnreliable.ObjectRef, ParamsUnreliable.Type, MoveTemp(ParamsUnreliable.Payload)); @@ -156,10 +157,10 @@ RPCCONTAINER_TEST(GIVEN_a_container_storing_multiple_values_of_different_type_WH { UObjectSpy* TargetObject = NewObject(); FUnrealObjectRef ObjectRef = GenerateObjectRef(TargetObject); - FRPCContainer RPCs; + FRPCContainer RPCs(ERPCQueueType::Send); RPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(TargetObject, &UObjectSpy::ProcessRPC)); - TMap> RPCIndices; + TMap> RPCIndices; for (int i = 0; i < 4; ++i) { @@ -196,17 +197,19 @@ RPCCONTAINER_TEST(GIVEN_a_container_storing_multiple_values_of_different_type_WH return true; } -RPCCONTAINER_TEST(GIVEN_a_container_with_one_value_WHEN_processing_after_2_seconds_THEN_warning_is_logged) +RPCCONTAINER_TEST(GIVEN_a_container_with_one_value_WHEN_processing_after_RPCQueueWarningDefaultTimeout_seconds_THEN_warning_is_logged) { UObjectStub* TargetObject = NewObject(); FPendingRPCParams Params = CreateMockParameters(TargetObject, AnySchemaComponentType); - FRPCContainer RPCs; + FRPCContainer RPCs(ERPCQueueType::Send); RPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(TargetObject, &UObjectStub::ProcessRPC)); RPCs.ProcessOrQueueRPC(Params.ObjectRef, Params.Type, MoveTemp(Params.Payload)); AddExpectedError(TEXT("Unresolved Parameters"), EAutomationExpectedErrorFlags::Contains, 1); - FPlatformProcess::Sleep(FRPCContainer::SECONDS_BEFORE_WARNING); + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + check(SpatialGDKSettings != nullptr); + FPlatformProcess::Sleep(SpatialGDKSettings->RPCQueueWarningDefaultTimeout); RPCs.ProcessRPCs(); return true; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedGeneratedSchemaFileContents.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedGeneratedSchemaFileContents.h deleted file mode 100644 index 71a4657b2a..0000000000 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedGeneratedSchemaFileContents.h +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -namespace ExpectedFileContent -{ - -const char ASpatialTypeActor[] = "\ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved\r\n\ -// Note that this file has been generated automatically\r\n\ -package unreal.generated.spatialtypeactor;\r\n\ -\r\n\ -import \"unreal/gdk/core_types.schema\";\r\n\ -\r\n\ -component SpatialTypeActor {\r\n\ - id = {{id}};\r\n\ - bool bhidden = 1;\r\n\ - bool breplicatemovement = 2;\r\n\ - bool btearoff = 3;\r\n\ - bool bcanbedamaged = 4;\r\n\ - bytes replicatedmovement = 5;\r\n\ - UnrealObjectRef attachmentreplication_attachparent = 6;\r\n\ - bytes attachmentreplication_locationoffset = 7;\r\n\ - bytes attachmentreplication_relativescale3d = 8;\r\n\ - bytes attachmentreplication_rotationoffset = 9;\r\n\ - string attachmentreplication_attachsocket = 10;\r\n\ - UnrealObjectRef attachmentreplication_attachcomponent = 11;\r\n\ - UnrealObjectRef owner = 12;\r\n\ - uint32 role = 13;\r\n\ - uint32 remoterole = 14;\r\n\ - UnrealObjectRef instigator = 15;\r\n\ -}\r\n"; - -char ANonSpatialTypeActor[] = "\ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved\r\n\ -// Note that this file has been generated automatically\r\n\ -package unreal.generated.nonspatialtypeactor;\r\n\ -\r\n\ -import \"unreal/gdk/core_types.schema\";\r\n\ -\r\n\ -component NonSpatialTypeActor {\r\n\ - id = {{id}};\r\n\ - bool bhidden = 1;\r\n\ - bool breplicatemovement = 2;\r\n\ - bool btearoff = 3;\r\n\ - bool bcanbedamaged = 4;\r\n\ - bytes replicatedmovement = 5;\r\n\ - UnrealObjectRef attachmentreplication_attachparent = 6;\r\n\ - bytes attachmentreplication_locationoffset = 7;\r\n\ - bytes attachmentreplication_relativescale3d = 8;\r\n\ - bytes attachmentreplication_rotationoffset = 9;\r\n\ - string attachmentreplication_attachsocket = 10;\r\n\ - UnrealObjectRef attachmentreplication_attachcomponent = 11;\r\n\ - UnrealObjectRef owner = 12;\r\n\ - uint32 role = 13;\r\n\ - uint32 remoterole = 14;\r\n\ - UnrealObjectRef instigator = 15;\r\n\ -}\r\n"; - -const char ASpatialTypeActorComponent [] = "\ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved\r\n\ -// Note that this file has been generated automatically\r\n\ -package unreal.generated;\r\n\ -\r\n\ -type SpatialTypeActorComponent {\r\n\ - bool breplicates = 1;\r\n\ - bool bisactive = 2;\r\n\ -}\r\n\ -\r\n\ -component SpatialTypeActorComponentDynamic1 {\r\n\ - id = 10000;\r\n\ - data SpatialTypeActorComponent;\r\n\ -}\r\n\ -\r\n\ -component SpatialTypeActorComponentDynamic2 {\r\n\ - id = 10001;\r\n\ - data SpatialTypeActorComponent;\r\n\ -}\r\n\ -\r\n\ -component SpatialTypeActorComponentDynamic3 {\r\n\ - id = 10002;\r\n\ - data SpatialTypeActorComponent;\r\n\ -}\r\n"; - -const char ASpatialTypeActorWithActorComponent [] = "\ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved\r\n\ -// Note that this file has been generated automatically\r\n\ -package unreal.generated.spatialtypeactorwithactorcomponent;\r\n\ -\r\n\ -import \"unreal/gdk/core_types.schema\";\r\n\ -\r\n\ -component SpatialTypeActorWithActorComponent {\r\n\ - id = {{id}};\r\n\ - bool bhidden = 1;\r\n\ - bool breplicatemovement = 2;\r\n\ - bool btearoff = 3;\r\n\ - bool bcanbedamaged = 4;\r\n\ - bytes replicatedmovement = 5;\r\n\ - UnrealObjectRef attachmentreplication_attachparent = 6;\r\n\ - bytes attachmentreplication_locationoffset = 7;\r\n\ - bytes attachmentreplication_relativescale3d = 8;\r\n\ - bytes attachmentreplication_rotationoffset = 9;\r\n\ - string attachmentreplication_attachsocket = 10;\r\n\ - UnrealObjectRef attachmentreplication_attachcomponent = 11;\r\n\ - UnrealObjectRef owner = 12;\r\n\ - uint32 role = 13;\r\n\ - uint32 remoterole = 14;\r\n\ - UnrealObjectRef instigator = 15;\r\n\ - UnrealObjectRef spatialactorcomponent = 16;\r\n\ -}\r\n"; - -const char ASpatialTypeActorWithMultipleActorComponents [] = "\ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved\r\n\ -// Note that this file has been generated automatically\r\n\ -package unreal.generated.spatialtypeactorwithmultipleactorcomponents;\r\n\ -\r\n\ -import \"unreal/gdk/core_types.schema\";\r\n\ -\r\n\ -component SpatialTypeActorWithMultipleActorComponents {\r\n\ - id = {{id}};\r\n\ - bool bhidden = 1;\r\n\ - bool breplicatemovement = 2;\r\n\ - bool btearoff = 3;\r\n\ - bool bcanbedamaged = 4;\r\n\ - bytes replicatedmovement = 5;\r\n\ - UnrealObjectRef attachmentreplication_attachparent = 6;\r\n\ - bytes attachmentreplication_locationoffset = 7;\r\n\ - bytes attachmentreplication_relativescale3d = 8;\r\n\ - bytes attachmentreplication_rotationoffset = 9;\r\n\ - string attachmentreplication_attachsocket = 10;\r\n\ - UnrealObjectRef attachmentreplication_attachcomponent = 11;\r\n\ - UnrealObjectRef owner = 12;\r\n\ - uint32 role = 13;\r\n\ - uint32 remoterole = 14;\r\n\ - UnrealObjectRef instigator = 15;\r\n\ - UnrealObjectRef firstspatialactorcomponent = 16;\r\n\ - UnrealObjectRef secondspatialactorcomponent = 17;\r\n\ -}\r\n"; - -const char ASpatialTypeActorWithMultipleObjectComponents [] = "\ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved\r\n\ -// Note that this file has been generated automatically\r\n\ -package unreal.generated.spatialtypeactorwithmultipleobjectcomponents;\r\n\ -\r\n\ -import \"unreal/gdk/core_types.schema\";\r\n\ -\r\n\ -component SpatialTypeActorWithMultipleObjectComponents {\r\n\ - id = {{id}};\r\n\ - bool bhidden = 1;\r\n\ - bool breplicatemovement = 2;\r\n\ - bool btearoff = 3;\r\n\ - bool bcanbedamaged = 4;\r\n\ - bytes replicatedmovement = 5;\r\n\ - UnrealObjectRef attachmentreplication_attachparent = 6;\r\n\ - bytes attachmentreplication_locationoffset = 7;\r\n\ - bytes attachmentreplication_relativescale3d = 8;\r\n\ - bytes attachmentreplication_rotationoffset = 9;\r\n\ - string attachmentreplication_attachsocket = 10;\r\n\ - UnrealObjectRef attachmentreplication_attachcomponent = 11;\r\n\ - UnrealObjectRef owner = 12;\r\n\ - uint32 role = 13;\r\n\ - uint32 remoterole = 14;\r\n\ - UnrealObjectRef instigator = 15;\r\n\ - UnrealObjectRef firstspatialobjectcomponent = 16;\r\n\ - UnrealObjectRef secondspatialobjectcomponent = 17;\r\n\ -}\r\n"; - -} // ExpectedFileContent - diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/NonSpatialTypeActor.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/NonSpatialTypeActor.schema new file mode 100644 index 0000000000..6b452b8ff9 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/NonSpatialTypeActor.schema @@ -0,0 +1,24 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Note that this file has been generated automatically +package unreal.generated.nonspatialtypeactor; + +import "unreal/gdk/core_types.schema"; + +component NonSpatialTypeActor { + id = {{id}}; + bool bhidden = 1; + bool breplicatemovement = 2; + bool btearoff = 3; + bool bcanbedamaged = 4; + bytes replicatedmovement = 5; + UnrealObjectRef attachmentreplication_attachparent = 6; + bytes attachmentreplication_locationoffset = 7; + bytes attachmentreplication_relativescale3d = 8; + bytes attachmentreplication_rotationoffset = 9; + string attachmentreplication_attachsocket = 10; + UnrealObjectRef attachmentreplication_attachcomponent = 11; + UnrealObjectRef owner = 12; + uint32 role = 13; + uint32 remoterole = 14; + UnrealObjectRef instigator = 15; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActor.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActor.schema new file mode 100644 index 0000000000..3af04e359d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActor.schema @@ -0,0 +1,24 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Note that this file has been generated automatically +package unreal.generated.spatialtypeactor; + +import "unreal/gdk/core_types.schema"; + +component SpatialTypeActor { + id = {{id}}; + bool bhidden = 1; + bool breplicatemovement = 2; + bool btearoff = 3; + bool bcanbedamaged = 4; + bytes replicatedmovement = 5; + UnrealObjectRef attachmentreplication_attachparent = 6; + bytes attachmentreplication_locationoffset = 7; + bytes attachmentreplication_relativescale3d = 8; + bytes attachmentreplication_rotationoffset = 9; + string attachmentreplication_attachsocket = 10; + UnrealObjectRef attachmentreplication_attachcomponent = 11; + UnrealObjectRef owner = 12; + uint32 role = 13; + uint32 remoterole = 14; + UnrealObjectRef instigator = 15; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorComponent.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorComponent.schema new file mode 100644 index 0000000000..e857c08706 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorComponent.schema @@ -0,0 +1,23 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Note that this file has been generated automatically +package unreal.generated; + +type SpatialTypeActorComponent { + bool breplicates = 1; + bool bisactive = 2; +} + +component SpatialTypeActorComponentDynamic1 { + id = 10000; + data SpatialTypeActorComponent; +} + +component SpatialTypeActorComponentDynamic2 { + id = 10001; + data SpatialTypeActorComponent; +} + +component SpatialTypeActorComponentDynamic3 { + id = 10002; + data SpatialTypeActorComponent; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithActorComponent.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithActorComponent.schema new file mode 100644 index 0000000000..29e2d85d1b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithActorComponent.schema @@ -0,0 +1,25 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Note that this file has been generated automatically +package unreal.generated.spatialtypeactorwithactorcomponent; + +import "unreal/gdk/core_types.schema"; + +component SpatialTypeActorWithActorComponent { + id = {{id}}; + bool bhidden = 1; + bool breplicatemovement = 2; + bool btearoff = 3; + bool bcanbedamaged = 4; + bytes replicatedmovement = 5; + UnrealObjectRef attachmentreplication_attachparent = 6; + bytes attachmentreplication_locationoffset = 7; + bytes attachmentreplication_relativescale3d = 8; + bytes attachmentreplication_rotationoffset = 9; + string attachmentreplication_attachsocket = 10; + UnrealObjectRef attachmentreplication_attachcomponent = 11; + UnrealObjectRef owner = 12; + uint32 role = 13; + uint32 remoterole = 14; + UnrealObjectRef instigator = 15; + UnrealObjectRef spatialactorcomponent = 16; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleActorComponents.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleActorComponents.schema new file mode 100644 index 0000000000..73839c1087 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleActorComponents.schema @@ -0,0 +1,26 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Note that this file has been generated automatically +package unreal.generated.spatialtypeactorwithmultipleactorcomponents; + +import "unreal/gdk/core_types.schema"; + +component SpatialTypeActorWithMultipleActorComponents { + id = {{id}}; + bool bhidden = 1; + bool breplicatemovement = 2; + bool btearoff = 3; + bool bcanbedamaged = 4; + bytes replicatedmovement = 5; + UnrealObjectRef attachmentreplication_attachparent = 6; + bytes attachmentreplication_locationoffset = 7; + bytes attachmentreplication_relativescale3d = 8; + bytes attachmentreplication_rotationoffset = 9; + string attachmentreplication_attachsocket = 10; + UnrealObjectRef attachmentreplication_attachcomponent = 11; + UnrealObjectRef owner = 12; + uint32 role = 13; + uint32 remoterole = 14; + UnrealObjectRef instigator = 15; + UnrealObjectRef firstspatialactorcomponent = 16; + UnrealObjectRef secondspatialactorcomponent = 17; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleObjectComponents.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleObjectComponents.schema new file mode 100644 index 0000000000..1155a5e43e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleObjectComponents.schema @@ -0,0 +1,26 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Note that this file has been generated automatically +package unreal.generated.spatialtypeactorwithmultipleobjectcomponents; + +import "unreal/gdk/core_types.schema"; + +component SpatialTypeActorWithMultipleObjectComponents { + id = {{id}}; + bool bhidden = 1; + bool breplicatemovement = 2; + bool btearoff = 3; + bool bcanbedamaged = 4; + bytes replicatedmovement = 5; + UnrealObjectRef attachmentreplication_attachparent = 6; + bytes attachmentreplication_locationoffset = 7; + bytes attachmentreplication_relativescale3d = 8; + bytes attachmentreplication_rotationoffset = 9; + string attachmentreplication_attachsocket = 10; + UnrealObjectRef attachmentreplication_attachcomponent = 11; + UnrealObjectRef owner = 12; + uint32 role = 13; + uint32 remoterole = 14; + UnrealObjectRef instigator = 15; + UnrealObjectRef firstspatialobjectcomponent = 16; + UnrealObjectRef secondspatialobjectcomponent = 17; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/rpc_endpoints.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/rpc_endpoints.schema new file mode 100644 index 0000000000..81498ba52e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/rpc_endpoints.schema @@ -0,0 +1,188 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Note that this file has been generated automatically +package unreal.generated; + +import "unreal/gdk/core_types.schema"; +import "unreal/gdk/rpc_payload.schema"; + +component UnrealClientEndpoint { + id = 9978; + option client_to_server_reliable_rpc_0 = 1; + option client_to_server_reliable_rpc_1 = 2; + option client_to_server_reliable_rpc_2 = 3; + option client_to_server_reliable_rpc_3 = 4; + option client_to_server_reliable_rpc_4 = 5; + option client_to_server_reliable_rpc_5 = 6; + option client_to_server_reliable_rpc_6 = 7; + option client_to_server_reliable_rpc_7 = 8; + option client_to_server_reliable_rpc_8 = 9; + option client_to_server_reliable_rpc_9 = 10; + option client_to_server_reliable_rpc_10 = 11; + option client_to_server_reliable_rpc_11 = 12; + option client_to_server_reliable_rpc_12 = 13; + option client_to_server_reliable_rpc_13 = 14; + option client_to_server_reliable_rpc_14 = 15; + option client_to_server_reliable_rpc_15 = 16; + option client_to_server_reliable_rpc_16 = 17; + option client_to_server_reliable_rpc_17 = 18; + option client_to_server_reliable_rpc_18 = 19; + option client_to_server_reliable_rpc_19 = 20; + option client_to_server_reliable_rpc_20 = 21; + option client_to_server_reliable_rpc_21 = 22; + option client_to_server_reliable_rpc_22 = 23; + option client_to_server_reliable_rpc_23 = 24; + option client_to_server_reliable_rpc_24 = 25; + option client_to_server_reliable_rpc_25 = 26; + option client_to_server_reliable_rpc_26 = 27; + option client_to_server_reliable_rpc_27 = 28; + option client_to_server_reliable_rpc_28 = 29; + option client_to_server_reliable_rpc_29 = 30; + option client_to_server_reliable_rpc_30 = 31; + option client_to_server_reliable_rpc_31 = 32; + uint64 last_sent_client_to_server_reliable_rpc_id = 33; + option client_to_server_unreliable_rpc_0 = 34; + option client_to_server_unreliable_rpc_1 = 35; + option client_to_server_unreliable_rpc_2 = 36; + option client_to_server_unreliable_rpc_3 = 37; + option client_to_server_unreliable_rpc_4 = 38; + option client_to_server_unreliable_rpc_5 = 39; + option client_to_server_unreliable_rpc_6 = 40; + option client_to_server_unreliable_rpc_7 = 41; + option client_to_server_unreliable_rpc_8 = 42; + option client_to_server_unreliable_rpc_9 = 43; + option client_to_server_unreliable_rpc_10 = 44; + option client_to_server_unreliable_rpc_11 = 45; + option client_to_server_unreliable_rpc_12 = 46; + option client_to_server_unreliable_rpc_13 = 47; + option client_to_server_unreliable_rpc_14 = 48; + option client_to_server_unreliable_rpc_15 = 49; + option client_to_server_unreliable_rpc_16 = 50; + option client_to_server_unreliable_rpc_17 = 51; + option client_to_server_unreliable_rpc_18 = 52; + option client_to_server_unreliable_rpc_19 = 53; + option client_to_server_unreliable_rpc_20 = 54; + option client_to_server_unreliable_rpc_21 = 55; + option client_to_server_unreliable_rpc_22 = 56; + option client_to_server_unreliable_rpc_23 = 57; + option client_to_server_unreliable_rpc_24 = 58; + option client_to_server_unreliable_rpc_25 = 59; + option client_to_server_unreliable_rpc_26 = 60; + option client_to_server_unreliable_rpc_27 = 61; + option client_to_server_unreliable_rpc_28 = 62; + option client_to_server_unreliable_rpc_29 = 63; + option client_to_server_unreliable_rpc_30 = 64; + option client_to_server_unreliable_rpc_31 = 65; + uint64 last_sent_client_to_server_unreliable_rpc_id = 66; + uint64 last_acked_server_to_client_reliable_rpc_id = 67; + uint64 last_acked_server_to_client_unreliable_rpc_id = 68; +} + +component UnrealServerEndpoint { + id = 9977; + option server_to_client_reliable_rpc_0 = 1; + option server_to_client_reliable_rpc_1 = 2; + option server_to_client_reliable_rpc_2 = 3; + option server_to_client_reliable_rpc_3 = 4; + option server_to_client_reliable_rpc_4 = 5; + option server_to_client_reliable_rpc_5 = 6; + option server_to_client_reliable_rpc_6 = 7; + option server_to_client_reliable_rpc_7 = 8; + option server_to_client_reliable_rpc_8 = 9; + option server_to_client_reliable_rpc_9 = 10; + option server_to_client_reliable_rpc_10 = 11; + option server_to_client_reliable_rpc_11 = 12; + option server_to_client_reliable_rpc_12 = 13; + option server_to_client_reliable_rpc_13 = 14; + option server_to_client_reliable_rpc_14 = 15; + option server_to_client_reliable_rpc_15 = 16; + option server_to_client_reliable_rpc_16 = 17; + option server_to_client_reliable_rpc_17 = 18; + option server_to_client_reliable_rpc_18 = 19; + option server_to_client_reliable_rpc_19 = 20; + option server_to_client_reliable_rpc_20 = 21; + option server_to_client_reliable_rpc_21 = 22; + option server_to_client_reliable_rpc_22 = 23; + option server_to_client_reliable_rpc_23 = 24; + option server_to_client_reliable_rpc_24 = 25; + option server_to_client_reliable_rpc_25 = 26; + option server_to_client_reliable_rpc_26 = 27; + option server_to_client_reliable_rpc_27 = 28; + option server_to_client_reliable_rpc_28 = 29; + option server_to_client_reliable_rpc_29 = 30; + option server_to_client_reliable_rpc_30 = 31; + option server_to_client_reliable_rpc_31 = 32; + uint64 last_sent_server_to_client_reliable_rpc_id = 33; + option server_to_client_unreliable_rpc_0 = 34; + option server_to_client_unreliable_rpc_1 = 35; + option server_to_client_unreliable_rpc_2 = 36; + option server_to_client_unreliable_rpc_3 = 37; + option server_to_client_unreliable_rpc_4 = 38; + option server_to_client_unreliable_rpc_5 = 39; + option server_to_client_unreliable_rpc_6 = 40; + option server_to_client_unreliable_rpc_7 = 41; + option server_to_client_unreliable_rpc_8 = 42; + option server_to_client_unreliable_rpc_9 = 43; + option server_to_client_unreliable_rpc_10 = 44; + option server_to_client_unreliable_rpc_11 = 45; + option server_to_client_unreliable_rpc_12 = 46; + option server_to_client_unreliable_rpc_13 = 47; + option server_to_client_unreliable_rpc_14 = 48; + option server_to_client_unreliable_rpc_15 = 49; + option server_to_client_unreliable_rpc_16 = 50; + option server_to_client_unreliable_rpc_17 = 51; + option server_to_client_unreliable_rpc_18 = 52; + option server_to_client_unreliable_rpc_19 = 53; + option server_to_client_unreliable_rpc_20 = 54; + option server_to_client_unreliable_rpc_21 = 55; + option server_to_client_unreliable_rpc_22 = 56; + option server_to_client_unreliable_rpc_23 = 57; + option server_to_client_unreliable_rpc_24 = 58; + option server_to_client_unreliable_rpc_25 = 59; + option server_to_client_unreliable_rpc_26 = 60; + option server_to_client_unreliable_rpc_27 = 61; + option server_to_client_unreliable_rpc_28 = 62; + option server_to_client_unreliable_rpc_29 = 63; + option server_to_client_unreliable_rpc_30 = 64; + option server_to_client_unreliable_rpc_31 = 65; + uint64 last_sent_server_to_client_unreliable_rpc_id = 66; + uint64 last_acked_client_to_server_reliable_rpc_id = 67; + uint64 last_acked_client_to_server_unreliable_rpc_id = 68; +} + +component UnrealMulticastRPCs { + id = 9976; + option multicast_rpc_0 = 1; + option multicast_rpc_1 = 2; + option multicast_rpc_2 = 3; + option multicast_rpc_3 = 4; + option multicast_rpc_4 = 5; + option multicast_rpc_5 = 6; + option multicast_rpc_6 = 7; + option multicast_rpc_7 = 8; + option multicast_rpc_8 = 9; + option multicast_rpc_9 = 10; + option multicast_rpc_10 = 11; + option multicast_rpc_11 = 12; + option multicast_rpc_12 = 13; + option multicast_rpc_13 = 14; + option multicast_rpc_14 = 15; + option multicast_rpc_15 = 16; + option multicast_rpc_16 = 17; + option multicast_rpc_17 = 18; + option multicast_rpc_18 = 19; + option multicast_rpc_19 = 20; + option multicast_rpc_20 = 21; + option multicast_rpc_21 = 22; + option multicast_rpc_22 = 23; + option multicast_rpc_23 = 24; + option multicast_rpc_24 = 25; + option multicast_rpc_25 = 26; + option multicast_rpc_26 = 27; + option multicast_rpc_27 = 28; + option multicast_rpc_28 = 29; + option multicast_rpc_29 = 30; + option multicast_rpc_30 = 31; + option multicast_rpc_31 = 32; + uint64 last_sent_multicast_rpc_id = 33; + uint32 initially_present_multicast_rpc_count = 34; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/NonSpatialTypeActor.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/NonSpatialTypeActor.schema new file mode 100644 index 0000000000..3e455e6bbf --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/NonSpatialTypeActor.schema @@ -0,0 +1,24 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Note that this file has been generated automatically +package unreal.generated.nonspatialtypeactor; + +import "unreal/gdk/core_types.schema"; + +component NonSpatialTypeActor { + id = {{id}}; + bool bhidden = 1; + bool breplicatemovement = 2; + bool btearoff = 3; + bool bcanbedamaged = 4; + bytes replicatedmovement = 5; + UnrealObjectRef attachmentreplication_attachparent = 6; + bytes attachmentreplication_locationoffset = 7; + bytes attachmentreplication_relativescale3d = 8; + bytes attachmentreplication_rotationoffset = 9; + string attachmentreplication_attachsocket = 10; + UnrealObjectRef attachmentreplication_attachcomponent = 11; + UnrealObjectRef owner = 12; + uint32 remoterole = 13; + uint32 role = 14; + UnrealObjectRef instigator = 15; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActor.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActor.schema new file mode 100644 index 0000000000..110deee31a --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActor.schema @@ -0,0 +1,24 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Note that this file has been generated automatically +package unreal.generated.spatialtypeactor; + +import "unreal/gdk/core_types.schema"; + +component SpatialTypeActor { + id = {{id}}; + bool bhidden = 1; + bool breplicatemovement = 2; + bool btearoff = 3; + bool bcanbedamaged = 4; + bytes replicatedmovement = 5; + UnrealObjectRef attachmentreplication_attachparent = 6; + bytes attachmentreplication_locationoffset = 7; + bytes attachmentreplication_relativescale3d = 8; + bytes attachmentreplication_rotationoffset = 9; + string attachmentreplication_attachsocket = 10; + UnrealObjectRef attachmentreplication_attachcomponent = 11; + UnrealObjectRef owner = 12; + uint32 remoterole = 13; + uint32 role = 14; + UnrealObjectRef instigator = 15; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorComponent.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorComponent.schema new file mode 100644 index 0000000000..e857c08706 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorComponent.schema @@ -0,0 +1,23 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Note that this file has been generated automatically +package unreal.generated; + +type SpatialTypeActorComponent { + bool breplicates = 1; + bool bisactive = 2; +} + +component SpatialTypeActorComponentDynamic1 { + id = 10000; + data SpatialTypeActorComponent; +} + +component SpatialTypeActorComponentDynamic2 { + id = 10001; + data SpatialTypeActorComponent; +} + +component SpatialTypeActorComponentDynamic3 { + id = 10002; + data SpatialTypeActorComponent; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorWithActorComponent.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorWithActorComponent.schema new file mode 100644 index 0000000000..3df8a46770 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorWithActorComponent.schema @@ -0,0 +1,25 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Note that this file has been generated automatically +package unreal.generated.spatialtypeactorwithactorcomponent; + +import "unreal/gdk/core_types.schema"; + +component SpatialTypeActorWithActorComponent { + id = {{id}}; + bool bhidden = 1; + bool breplicatemovement = 2; + bool btearoff = 3; + bool bcanbedamaged = 4; + bytes replicatedmovement = 5; + UnrealObjectRef attachmentreplication_attachparent = 6; + bytes attachmentreplication_locationoffset = 7; + bytes attachmentreplication_relativescale3d = 8; + bytes attachmentreplication_rotationoffset = 9; + string attachmentreplication_attachsocket = 10; + UnrealObjectRef attachmentreplication_attachcomponent = 11; + UnrealObjectRef owner = 12; + uint32 remoterole = 13; + uint32 role = 14; + UnrealObjectRef instigator = 15; + UnrealObjectRef spatialactorcomponent = 16; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorWithMultipleActorComponents.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorWithMultipleActorComponents.schema new file mode 100644 index 0000000000..249b8511bb --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorWithMultipleActorComponents.schema @@ -0,0 +1,26 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Note that this file has been generated automatically +package unreal.generated.spatialtypeactorwithmultipleactorcomponents; + +import "unreal/gdk/core_types.schema"; + +component SpatialTypeActorWithMultipleActorComponents { + id = {{id}}; + bool bhidden = 1; + bool breplicatemovement = 2; + bool btearoff = 3; + bool bcanbedamaged = 4; + bytes replicatedmovement = 5; + UnrealObjectRef attachmentreplication_attachparent = 6; + bytes attachmentreplication_locationoffset = 7; + bytes attachmentreplication_relativescale3d = 8; + bytes attachmentreplication_rotationoffset = 9; + string attachmentreplication_attachsocket = 10; + UnrealObjectRef attachmentreplication_attachcomponent = 11; + UnrealObjectRef owner = 12; + uint32 remoterole = 13; + uint32 role = 14; + UnrealObjectRef instigator = 15; + UnrealObjectRef firstspatialactorcomponent = 16; + UnrealObjectRef secondspatialactorcomponent = 17; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorWithMultipleObjectComponents.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorWithMultipleObjectComponents.schema new file mode 100644 index 0000000000..dfef85c8c5 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorWithMultipleObjectComponents.schema @@ -0,0 +1,26 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Note that this file has been generated automatically +package unreal.generated.spatialtypeactorwithmultipleobjectcomponents; + +import "unreal/gdk/core_types.schema"; + +component SpatialTypeActorWithMultipleObjectComponents { + id = {{id}}; + bool bhidden = 1; + bool breplicatemovement = 2; + bool btearoff = 3; + bool bcanbedamaged = 4; + bytes replicatedmovement = 5; + UnrealObjectRef attachmentreplication_attachparent = 6; + bytes attachmentreplication_locationoffset = 7; + bytes attachmentreplication_relativescale3d = 8; + bytes attachmentreplication_rotationoffset = 9; + string attachmentreplication_attachsocket = 10; + UnrealObjectRef attachmentreplication_attachcomponent = 11; + UnrealObjectRef owner = 12; + uint32 remoterole = 13; + uint32 role = 14; + UnrealObjectRef instigator = 15; + UnrealObjectRef firstspatialobjectcomponent = 16; + UnrealObjectRef secondspatialobjectcomponent = 17; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/rpc_endpoints.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/rpc_endpoints.schema new file mode 100644 index 0000000000..81498ba52e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/rpc_endpoints.schema @@ -0,0 +1,188 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Note that this file has been generated automatically +package unreal.generated; + +import "unreal/gdk/core_types.schema"; +import "unreal/gdk/rpc_payload.schema"; + +component UnrealClientEndpoint { + id = 9978; + option client_to_server_reliable_rpc_0 = 1; + option client_to_server_reliable_rpc_1 = 2; + option client_to_server_reliable_rpc_2 = 3; + option client_to_server_reliable_rpc_3 = 4; + option client_to_server_reliable_rpc_4 = 5; + option client_to_server_reliable_rpc_5 = 6; + option client_to_server_reliable_rpc_6 = 7; + option client_to_server_reliable_rpc_7 = 8; + option client_to_server_reliable_rpc_8 = 9; + option client_to_server_reliable_rpc_9 = 10; + option client_to_server_reliable_rpc_10 = 11; + option client_to_server_reliable_rpc_11 = 12; + option client_to_server_reliable_rpc_12 = 13; + option client_to_server_reliable_rpc_13 = 14; + option client_to_server_reliable_rpc_14 = 15; + option client_to_server_reliable_rpc_15 = 16; + option client_to_server_reliable_rpc_16 = 17; + option client_to_server_reliable_rpc_17 = 18; + option client_to_server_reliable_rpc_18 = 19; + option client_to_server_reliable_rpc_19 = 20; + option client_to_server_reliable_rpc_20 = 21; + option client_to_server_reliable_rpc_21 = 22; + option client_to_server_reliable_rpc_22 = 23; + option client_to_server_reliable_rpc_23 = 24; + option client_to_server_reliable_rpc_24 = 25; + option client_to_server_reliable_rpc_25 = 26; + option client_to_server_reliable_rpc_26 = 27; + option client_to_server_reliable_rpc_27 = 28; + option client_to_server_reliable_rpc_28 = 29; + option client_to_server_reliable_rpc_29 = 30; + option client_to_server_reliable_rpc_30 = 31; + option client_to_server_reliable_rpc_31 = 32; + uint64 last_sent_client_to_server_reliable_rpc_id = 33; + option client_to_server_unreliable_rpc_0 = 34; + option client_to_server_unreliable_rpc_1 = 35; + option client_to_server_unreliable_rpc_2 = 36; + option client_to_server_unreliable_rpc_3 = 37; + option client_to_server_unreliable_rpc_4 = 38; + option client_to_server_unreliable_rpc_5 = 39; + option client_to_server_unreliable_rpc_6 = 40; + option client_to_server_unreliable_rpc_7 = 41; + option client_to_server_unreliable_rpc_8 = 42; + option client_to_server_unreliable_rpc_9 = 43; + option client_to_server_unreliable_rpc_10 = 44; + option client_to_server_unreliable_rpc_11 = 45; + option client_to_server_unreliable_rpc_12 = 46; + option client_to_server_unreliable_rpc_13 = 47; + option client_to_server_unreliable_rpc_14 = 48; + option client_to_server_unreliable_rpc_15 = 49; + option client_to_server_unreliable_rpc_16 = 50; + option client_to_server_unreliable_rpc_17 = 51; + option client_to_server_unreliable_rpc_18 = 52; + option client_to_server_unreliable_rpc_19 = 53; + option client_to_server_unreliable_rpc_20 = 54; + option client_to_server_unreliable_rpc_21 = 55; + option client_to_server_unreliable_rpc_22 = 56; + option client_to_server_unreliable_rpc_23 = 57; + option client_to_server_unreliable_rpc_24 = 58; + option client_to_server_unreliable_rpc_25 = 59; + option client_to_server_unreliable_rpc_26 = 60; + option client_to_server_unreliable_rpc_27 = 61; + option client_to_server_unreliable_rpc_28 = 62; + option client_to_server_unreliable_rpc_29 = 63; + option client_to_server_unreliable_rpc_30 = 64; + option client_to_server_unreliable_rpc_31 = 65; + uint64 last_sent_client_to_server_unreliable_rpc_id = 66; + uint64 last_acked_server_to_client_reliable_rpc_id = 67; + uint64 last_acked_server_to_client_unreliable_rpc_id = 68; +} + +component UnrealServerEndpoint { + id = 9977; + option server_to_client_reliable_rpc_0 = 1; + option server_to_client_reliable_rpc_1 = 2; + option server_to_client_reliable_rpc_2 = 3; + option server_to_client_reliable_rpc_3 = 4; + option server_to_client_reliable_rpc_4 = 5; + option server_to_client_reliable_rpc_5 = 6; + option server_to_client_reliable_rpc_6 = 7; + option server_to_client_reliable_rpc_7 = 8; + option server_to_client_reliable_rpc_8 = 9; + option server_to_client_reliable_rpc_9 = 10; + option server_to_client_reliable_rpc_10 = 11; + option server_to_client_reliable_rpc_11 = 12; + option server_to_client_reliable_rpc_12 = 13; + option server_to_client_reliable_rpc_13 = 14; + option server_to_client_reliable_rpc_14 = 15; + option server_to_client_reliable_rpc_15 = 16; + option server_to_client_reliable_rpc_16 = 17; + option server_to_client_reliable_rpc_17 = 18; + option server_to_client_reliable_rpc_18 = 19; + option server_to_client_reliable_rpc_19 = 20; + option server_to_client_reliable_rpc_20 = 21; + option server_to_client_reliable_rpc_21 = 22; + option server_to_client_reliable_rpc_22 = 23; + option server_to_client_reliable_rpc_23 = 24; + option server_to_client_reliable_rpc_24 = 25; + option server_to_client_reliable_rpc_25 = 26; + option server_to_client_reliable_rpc_26 = 27; + option server_to_client_reliable_rpc_27 = 28; + option server_to_client_reliable_rpc_28 = 29; + option server_to_client_reliable_rpc_29 = 30; + option server_to_client_reliable_rpc_30 = 31; + option server_to_client_reliable_rpc_31 = 32; + uint64 last_sent_server_to_client_reliable_rpc_id = 33; + option server_to_client_unreliable_rpc_0 = 34; + option server_to_client_unreliable_rpc_1 = 35; + option server_to_client_unreliable_rpc_2 = 36; + option server_to_client_unreliable_rpc_3 = 37; + option server_to_client_unreliable_rpc_4 = 38; + option server_to_client_unreliable_rpc_5 = 39; + option server_to_client_unreliable_rpc_6 = 40; + option server_to_client_unreliable_rpc_7 = 41; + option server_to_client_unreliable_rpc_8 = 42; + option server_to_client_unreliable_rpc_9 = 43; + option server_to_client_unreliable_rpc_10 = 44; + option server_to_client_unreliable_rpc_11 = 45; + option server_to_client_unreliable_rpc_12 = 46; + option server_to_client_unreliable_rpc_13 = 47; + option server_to_client_unreliable_rpc_14 = 48; + option server_to_client_unreliable_rpc_15 = 49; + option server_to_client_unreliable_rpc_16 = 50; + option server_to_client_unreliable_rpc_17 = 51; + option server_to_client_unreliable_rpc_18 = 52; + option server_to_client_unreliable_rpc_19 = 53; + option server_to_client_unreliable_rpc_20 = 54; + option server_to_client_unreliable_rpc_21 = 55; + option server_to_client_unreliable_rpc_22 = 56; + option server_to_client_unreliable_rpc_23 = 57; + option server_to_client_unreliable_rpc_24 = 58; + option server_to_client_unreliable_rpc_25 = 59; + option server_to_client_unreliable_rpc_26 = 60; + option server_to_client_unreliable_rpc_27 = 61; + option server_to_client_unreliable_rpc_28 = 62; + option server_to_client_unreliable_rpc_29 = 63; + option server_to_client_unreliable_rpc_30 = 64; + option server_to_client_unreliable_rpc_31 = 65; + uint64 last_sent_server_to_client_unreliable_rpc_id = 66; + uint64 last_acked_client_to_server_reliable_rpc_id = 67; + uint64 last_acked_client_to_server_unreliable_rpc_id = 68; +} + +component UnrealMulticastRPCs { + id = 9976; + option multicast_rpc_0 = 1; + option multicast_rpc_1 = 2; + option multicast_rpc_2 = 3; + option multicast_rpc_3 = 4; + option multicast_rpc_4 = 5; + option multicast_rpc_5 = 6; + option multicast_rpc_6 = 7; + option multicast_rpc_7 = 8; + option multicast_rpc_8 = 9; + option multicast_rpc_9 = 10; + option multicast_rpc_10 = 11; + option multicast_rpc_11 = 12; + option multicast_rpc_12 = 13; + option multicast_rpc_13 = 14; + option multicast_rpc_14 = 15; + option multicast_rpc_15 = 16; + option multicast_rpc_16 = 17; + option multicast_rpc_17 = 18; + option multicast_rpc_18 = 19; + option multicast_rpc_19 = 20; + option multicast_rpc_20 = 21; + option multicast_rpc_21 = 22; + option multicast_rpc_22 = 23; + option multicast_rpc_23 = 24; + option multicast_rpc_24 = 25; + option multicast_rpc_25 = 26; + option multicast_rpc_26 = 27; + option multicast_rpc_27 = 28; + option multicast_rpc_28 = 29; + option multicast_rpc_29 = 30; + option multicast_rpc_30 = 31; + option multicast_rpc_31 = 32; + uint64 last_sent_multicast_rpc_id = 33; + uint32 initially_present_multicast_rpc_count = 34; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SpatialGDKEditorSchemaGeneratorTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SpatialGDKEditorSchemaGeneratorTest.cpp index a58ee9382d..f63f9e51c0 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SpatialGDKEditorSchemaGeneratorTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SpatialGDKEditorSchemaGeneratorTest.cpp @@ -1,16 +1,19 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved -#include "TestDefinitions.h" +#include "Tests/TestDefinitions.h" -#include "ExpectedGeneratedSchemaFileContents.h" #include "SchemaGenObjectStub.h" #include "SpatialGDKEditorSchemaGenerator.h" +#include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesModule.h" +#include "SpatialGDKSettings.h" #include "Utils/SchemaDatabase.h" #include "CoreMinimal.h" +#include "GeneralProjectSettings.h" #include "HAL/PlatformFilemanager.h" #include "Misc/FileHelper.h" +#include "Misc/PackageName.h" #define LOCTEXT_NAMESPACE "SpatialGDKEDitorSchemaGeneratorTest" @@ -19,7 +22,7 @@ namespace { -const FString SchemaOutputFolder = FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("Tests/")); +const FString SchemaOutputFolder = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("Tests/")); const FString SchemaDatabaseFileName = TEXT("Spatial/Tests/SchemaDatabase"); const FString DatabaseOutputFile = TEXT("/Game/Spatial/Tests/SchemaDatabase"); @@ -33,7 +36,7 @@ TArray LoadSchemaFileForClassToStringArray(const FString& InSchemaOutpu } TArray FileContent; - FFileHelper::LoadFileToStringArray(FileContent, *FPaths::SetExtension(FPaths::Combine(FPaths::Combine(InSchemaOutputFolder, SchemaFileFolder), CurrentClass->GetName()), TEXT(".schema"))); + FFileHelper::LoadFileToStringArray(FileContent, *FPaths::SetExtension(FPaths::Combine(InSchemaOutputFolder, SchemaFileFolder, CurrentClass->GetName()), TEXT(".schema"))); return FileContent; } @@ -191,7 +194,7 @@ FString LoadSchemaFileForClass(const FString& InSchemaOutputFolder, const UClass } FString FileContent; - FFileHelper::LoadFileToString(FileContent, *FPaths::SetExtension(FPaths::Combine(FPaths::Combine(InSchemaOutputFolder, SchemaFileFolder), CurrentClass->GetName()), TEXT(".schema"))); + FFileHelper::LoadFileToString(FileContent, *FPaths::SetExtension(FPaths::Combine(InSchemaOutputFolder, SchemaFileFolder, CurrentClass->GetName()), TEXT(".schema"))); return FileContent; } @@ -238,50 +241,41 @@ const TSet& AllTestClassesSet() return TestClassesSet; }; -TMap ExpectedContents = -{ - TPair - { - "SpatialTypeActor", - ExpectedFileContent::ASpatialTypeActor - }, - TPair - { - "NonSpatialTypeActor", - ExpectedFileContent::ANonSpatialTypeActor - }, - TPair - { - "SpatialTypeActorComponent", - ExpectedFileContent::ASpatialTypeActorComponent - }, - TPair - { - "SpatialTypeActorWithActorComponent", - ExpectedFileContent::ASpatialTypeActorWithActorComponent - }, - TPair - { - "SpatialTypeActorWithMultipleActorComponents", - ExpectedFileContent::ASpatialTypeActorWithMultipleActorComponents - }, - TPair - { - "SpatialTypeActorWithMultipleObjectComponents", - ExpectedFileContent::ASpatialTypeActorWithMultipleObjectComponents - } +#if ENGINE_MINOR_VERSION <= 23 +FString ExpectedContentsDirectory = TEXT("SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema"); +#else +// Remove this once we fix 4.22 and 4.23: UNR-2988 +FString ExpectedContentsDirectory = TEXT("SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24"); +#endif +TMap ExpectedContentsFilenames = { + { "SpatialTypeActor", "SpatialTypeActor.schema" }, + { "NonSpatialTypeActor", "NonSpatialTypeActor.schema" }, + { "SpatialTypeActorComponent", "SpatialTypeActorComponent.schema" }, + { "SpatialTypeActorWithActorComponent", "SpatialTypeActorWithActorComponent.schema" }, + { "SpatialTypeActorWithMultipleActorComponents", "SpatialTypeActorWithMultipleActorComponents.schema" }, + { "SpatialTypeActorWithMultipleObjectComponents", "SpatialTypeActorWithMultipleObjectComponents.schema" } }; +uint32 ExpectedRPCEndpointsRingBufferSize = 32; +FString ExpectedRPCEndpointsSchemaFilename = TEXT("rpc_endpoints.schema"); class SchemaValidator { public: + bool ValidateGeneratedSchemaAgainstExpectedSchema(const FString& GeneratedSchemaContent, const FString& ExpectedSchemaFilename) + { + FString ExpectedContentFullPath = FPaths::Combine(FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(ExpectedContentsDirectory), ExpectedSchemaFilename); + + FString ExpectedContent; + FFileHelper::LoadFileToString(ExpectedContent, *ExpectedContentFullPath); + ExpectedContent.ReplaceInline(TEXT("{{id}}"), *FString::FromInt(GetNextFreeId())); + return (GeneratedSchemaContent.Compare(ExpectedContent) == 0); + } + bool ValidateGeneratedSchemaForClass(const FString& FileContent, const UClass* CurrentClass) { - if (FString* ExpectedContentPtr = ExpectedContents.Find(CurrentClass->GetName())) + if (FString* ExpectedContentFilenamePtr = ExpectedContentsFilenames.Find(CurrentClass->GetName())) { - FString ExpectedContent = *ExpectedContentPtr; - ExpectedContent.ReplaceInline(TEXT("{{id}}"), *FString::FromInt(GetNextFreeId())); - return (FileContent.Compare(ExpectedContent) == 0); + return ValidateGeneratedSchemaAgainstExpectedSchema(FileContent, *ExpectedContentFilenamePtr); } else { @@ -306,7 +300,7 @@ class SchemaTestFixture SpatialGDKEditor::Schema::ResetSchemaGeneratorState(); EnableSpatialNetworking(); } - ~SchemaTestFixture() + virtual ~SchemaTestFixture() { DeleteTestFolders(); ResetSpatialNetworking(); @@ -324,20 +318,49 @@ class SchemaTestFixture void EnableSpatialNetworking() { UGeneralProjectSettings* GeneralProjectSettings = GetMutableDefault(); - bCachedSpatialNetworking = GeneralProjectSettings->bSpatialNetworking; - GeneralProjectSettings->bSpatialNetworking = true; + bCachedSpatialNetworking = GeneralProjectSettings->UsesSpatialNetworking(); + GeneralProjectSettings->SetUsesSpatialNetworking(true); } void ResetSpatialNetworking() { UGeneralProjectSettings* GeneralProjectSettings = GetMutableDefault(); - GetMutableDefault()->bSpatialNetworking = bCachedSpatialNetworking; + GetMutableDefault()->SetUsesSpatialNetworking(bCachedSpatialNetworking); bCachedSpatialNetworking = true; } bool bCachedSpatialNetworking = true; }; +class SchemaRPCEndpointTestFixture : public SchemaTestFixture +{ +public: + SchemaRPCEndpointTestFixture() + { + SetMaxRPCRingBufferSize(); + } + ~SchemaRPCEndpointTestFixture() + { + ResetMaxRPCRingBufferSize(); + } + +private: + void SetMaxRPCRingBufferSize() + { + USpatialGDKSettings* SpatialGDKSettings = GetMutableDefault(); + CachedMaxRPCRingBufferSize = SpatialGDKSettings->MaxRPCRingBufferSize; + SpatialGDKSettings->MaxRPCRingBufferSize = ExpectedRPCEndpointsRingBufferSize; + } + + void ResetMaxRPCRingBufferSize() + { + USpatialGDKSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->MaxRPCRingBufferSize = CachedMaxRPCRingBufferSize; + } + + uint32 CachedMaxRPCRingBufferSize; +}; + } // anonymous namespace SCHEMA_GENERATOR_TEST(GIVEN_spatial_type_class_WHEN_checked_if_supported_THEN_is_supported) @@ -520,7 +543,7 @@ SCHEMA_GENERATOR_TEST(GIVEN_multiple_Actor_classes_WHEN_generated_schema_for_the for (const auto& CurrentClass : Classes) { FString FileContent = LoadSchemaFileForClass(SchemaOutputFolder, CurrentClass); - if(!Validator.ValidateGeneratedSchemaForClass(FileContent, CurrentClass)) + if (!Validator.ValidateGeneratedSchemaForClass(FileContent, CurrentClass)) { bGeneratedSchemaMatchesExpected = false; break; @@ -813,21 +836,26 @@ SCHEMA_GENERATOR_TEST(GIVEN_source_and_destination_of_well_known_schema_files_WH SchemaTestFixture Fixture; // GIVEN - FString GDKSchemaCopyDir = FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("/Tests/schema/unreal/gdk")); - FString CoreSDKSchemaCopyDir = FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("/Tests/build/dependencies/schema/standard_library")); + FString GDKSchemaCopyDir = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("/Tests/schema/unreal/gdk")); + FString CoreSDKSchemaCopyDir = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("/Tests/build/dependencies/schema/standard_library")); TArray GDKSchemaFilePaths = { "authority_intent.schema", + "component_presence.schema", "core_types.schema", "debug_metrics.schema", "global_state_manager.schema", "heartbeat.schema", + "net_owning_client_worker.schema", "not_streamed.schema", "relevant.schema", "rpc_components.schema", + "rpc_payload.schema", + "server_worker.schema", "singleton.schema", "spawndata.schema", "spawner.schema", + "spatial_debugging.schema", "tombstone.schema", "unreal_metadata.schema", "virtual_worker_translation.schema" @@ -852,7 +880,7 @@ SCHEMA_GENERATOR_TEST(GIVEN_source_and_destination_of_well_known_schema_files_WH { bExpectedFilesCopied = false; } - for(const auto& FilePath : GDKSchemaFilePaths) + for (const auto& FilePath : GDKSchemaFilePaths) { if (!PlatformFile.FileExists(*FPaths::Combine(GDKSchemaCopyDir, FilePath))) { @@ -867,7 +895,7 @@ SCHEMA_GENERATOR_TEST(GIVEN_source_and_destination_of_well_known_schema_files_WH { bExpectedFilesCopied = false; } - for(const auto& FilePath : CoreSDKFilePaths) + for (const auto& FilePath : CoreSDKFilePaths) { if (!PlatformFile.FileExists(*FPaths::Combine(CoreSDKSchemaCopyDir, FilePath))) { @@ -962,3 +990,18 @@ SCHEMA_GENERATOR_TEST(GIVEN_3_level_names_WHEN_generating_schema_for_sublevels_T return true; } + +SCHEMA_GENERATOR_TEST(GIVEN_no_schema_exists_WHEN_generating_schema_for_rpc_endpoints_THEN_generated_schema_matches_expected_contents) +{ + SchemaRPCEndpointTestFixture Fixture; + SchemaValidator Validator; + + SpatialGDKEditor::Schema::GenerateSchemaForRPCEndpoints(SchemaOutputFolder); + + FString FileContent; + FFileHelper::LoadFileToString(FileContent, *FPaths::Combine(SchemaOutputFolder, ExpectedRPCEndpointsSchemaFilename)); + + TestTrue("Generated RPC endpoints schema matches the expected schema", Validator.ValidateGeneratedSchemaAgainstExpectedSchema(FileContent, ExpectedRPCEndpointsSchemaFilename)); + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerTest.cpp index a98cd8343f..24c9adc615 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerTest.cpp @@ -1,163 +1,14 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved -#include "TestDefinitions.h" +#include "Tests/TestDefinitions.h" -#include "LocalDeploymentManager.h" -#include "SpatialCommandUtils.h" -#include "SpatialGDKDefaultLaunchConfigGenerator.h" -#include "SpatialGDKDefaultWorkerJsonGenerator.h" -#include "SpatialGDKEditorSettings.h" +#include "LocalDeploymentManagerUtilities.h" #include "CoreMinimal.h" #define LOCALDEPLOYMENT_TEST(TestName) \ GDK_TEST(Services, LocalDeployment, TestName) -namespace -{ - // TODO: UNR-1969 - Prepare LocalDeployment in CI pipeline - const double MAX_WAIT_TIME_FOR_LOCAL_DEPLOYMENT_OPERATION = 20.0; - - // TODO: UNR-1964 - Move EDeploymentState enum to LocalDeploymentManager - enum class EDeploymentState { IsRunning, IsNotRunning }; - - const FName AutomationWorkerType = TEXT("AutomationWorker"); - const FString AutomationLaunchConfig = TEXT("Improbable/AutomationLaunchConfig.json"); - - FLocalDeploymentManager* GetLocalDeploymentManager() - { - FSpatialGDKServicesModule& GDKServices = FModuleManager::GetModuleChecked("SpatialGDKServices"); - FLocalDeploymentManager* LocalDeploymentManager = GDKServices.GetLocalDeploymentManager(); - return LocalDeploymentManager; - } - - bool GenerateWorkerAssemblies() - { - FString BuildConfigArgs = TEXT("worker build build-config"); - FString WorkerBuildConfigResult; - int32 ExitCode; - SpatialCommandUtils::ExecuteSpatialCommandAndReadOutput(BuildConfigArgs, FSpatialGDKServicesModule::GetSpatialOSDirectory(), WorkerBuildConfigResult, ExitCode, false); - - const int32 ExitCodeSuccess = 0; - return (ExitCode == ExitCodeSuccess); - } - - bool GenerateWorkerJson() - { - const FString WorkerJsonDir = FSpatialGDKServicesModule::GetSpatialOSDirectory(TEXT("workers/unreal")); - - FString JsonPath = FPaths::Combine(WorkerJsonDir, TEXT("spatialos.UnrealAutomation.worker.json")); - if (!FPaths::FileExists(JsonPath)) - { - bool bRedeployRequired = false; - return GenerateDefaultWorkerJson(JsonPath, AutomationWorkerType.ToString(), bRedeployRequired); - } - - return true; - } -} - -DEFINE_LATENT_AUTOMATION_COMMAND(FStartDeployment); -bool FStartDeployment::Update() -{ - if (const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault()) - { - FLocalDeploymentManager* LocalDeploymentManager = GetLocalDeploymentManager(); - const FString LaunchConfig = FPaths::Combine(FPaths::ConvertRelativePathToFull(FPaths::ProjectIntermediateDir()), AutomationLaunchConfig); - const FString LaunchFlags = SpatialGDKSettings->GetSpatialOSCommandLineLaunchFlags(); - const FString SnapshotName = SpatialGDKSettings->GetSpatialOSSnapshotToLoad(); - - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [LocalDeploymentManager, LaunchConfig, LaunchFlags, SnapshotName] - { - if (!GenerateWorkerJson()) - { - return; - } - - if (!GenerateWorkerAssemblies()) - { - return; - } - - FSpatialLaunchConfigDescription LaunchConfigDescription(AutomationWorkerType); - - if (!GenerateDefaultLaunchConfig(LaunchConfig, &LaunchConfigDescription)) - { - return; - } - - if (LocalDeploymentManager->IsLocalDeploymentRunning()) - { - return; - } - - LocalDeploymentManager->TryStartLocalDeployment(LaunchConfig, LaunchFlags, SnapshotName, TEXT(""), FLocalDeploymentManager::LocalDeploymentCallback()); - }); - } - - return true; -} - -DEFINE_LATENT_AUTOMATION_COMMAND(FStopDeployment); -bool FStopDeployment::Update() -{ - FLocalDeploymentManager* LocalDeploymentManager = GetLocalDeploymentManager(); - - if (!LocalDeploymentManager->IsLocalDeploymentRunning() && !LocalDeploymentManager->IsDeploymentStopping()) - { - return true; - } - - if (!LocalDeploymentManager->IsDeploymentStopping()) - { - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [LocalDeploymentManager] - { - LocalDeploymentManager->TryStopLocalDeployment(); - }); - } - - return true; -} - -DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER(FWaitForDeployment, FAutomationTestBase*, Test, EDeploymentState, ExpectedDeploymentState); -bool FWaitForDeployment::Update() -{ - const double NewTime = FPlatformTime::Seconds(); - if (NewTime - StartTime >= MAX_WAIT_TIME_FOR_LOCAL_DEPLOYMENT_OPERATION) - { - return true; - } - - FLocalDeploymentManager* LocalDeploymentManager = GetLocalDeploymentManager(); - if (LocalDeploymentManager->IsDeploymentStopping()) - { - return false; - } - else - { - return (ExpectedDeploymentState == EDeploymentState::IsRunning) ? LocalDeploymentManager->IsLocalDeploymentRunning() : !LocalDeploymentManager->IsLocalDeploymentRunning(); - } -} - -DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER(FCheckDeploymentState, FAutomationTestBase*, Test, EDeploymentState, ExpectedDeploymentState); -bool FCheckDeploymentState::Update() -{ - FLocalDeploymentManager* LocalDeploymentManager = GetLocalDeploymentManager(); - - if (ExpectedDeploymentState == EDeploymentState::IsRunning) - { - Test->TestTrue(TEXT("Deployment is running"), LocalDeploymentManager->IsLocalDeploymentRunning() && !LocalDeploymentManager->IsDeploymentStopping()); - } - else - { - Test->TestFalse(TEXT("Deployment is not running"), LocalDeploymentManager->IsLocalDeploymentRunning() || LocalDeploymentManager->IsDeploymentStopping()); - } - - return true; -} - -/* -// UNR-1975 after fixing the flakiness of these tests, and investigating how they can be run in CI (UNR-1969), re-enable them LOCALDEPLOYMENT_TEST(GIVEN_no_deployment_running_WHEN_deployment_started_THEN_deployment_running) { // GIVEN @@ -192,4 +43,4 @@ LOCALDEPLOYMENT_TEST(GIVEN_deployment_running_WHEN_deployment_stopped_THEN_deplo // THEN ADD_LATENT_AUTOMATION_COMMAND(FCheckDeploymentState(this, EDeploymentState::IsNotRunning)); return true; -}*/ +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.cpp new file mode 100644 index 0000000000..d36e846d02 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.cpp @@ -0,0 +1,161 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "LocalDeploymentManagerUtilities.h" + +#include "LocalDeploymentManager.h" +#include "SpatialGDKDefaultLaunchConfigGenerator.h" +#include "SpatialGDKDefaultWorkerJsonGenerator.h" +#include "SpatialGDKEditorSettings.h" +#include "SpatialGDKServicesConstants.h" + +#include "CoreMinimal.h" + +namespace +{ + // TODO: UNR-1969 - Prepare LocalDeployment in CI pipeline + const double MAX_WAIT_TIME_FOR_LOCAL_DEPLOYMENT_OPERATION = 30.0; + + const FName AutomationWorkerType = TEXT("AutomationWorker"); + const FString AutomationLaunchConfig = FString(TEXT("Improbable/")) + *AutomationWorkerType.ToString() + FString(TEXT(".json")); + + FLocalDeploymentManager* GetLocalDeploymentManager() + { + FSpatialGDKServicesModule& GDKServices = FModuleManager::GetModuleChecked("SpatialGDKServices"); + FLocalDeploymentManager* LocalDeploymentManager = GDKServices.GetLocalDeploymentManager(); + return LocalDeploymentManager; + } + + bool GenerateWorkerAssemblies() + { + FString BuildConfigArgs = TEXT("worker build build-config"); + FString WorkerBuildConfigResult; + int32 ExitCode; + FSpatialGDKServicesModule::ExecuteAndReadOutput(SpatialGDKServicesConstants::SpatialExe, BuildConfigArgs, SpatialGDKServicesConstants::SpatialOSDirectory, WorkerBuildConfigResult, ExitCode); + + const int32 ExitCodeSuccess = 0; + return (ExitCode == ExitCodeSuccess); + } + + bool GenerateWorkerJson() + { + const FString WorkerJsonDir = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("workers/unreal")); + + FString Filename = FString(TEXT("spatialos.")) + *AutomationWorkerType.ToString() + FString(TEXT(".worker.json")); + FString JsonPath = FPaths::Combine(WorkerJsonDir, Filename); + if (!FPaths::FileExists(JsonPath)) + { + bool bRedeployRequired = false; + return GenerateDefaultWorkerJson(JsonPath, AutomationWorkerType.ToString(), bRedeployRequired); + } + + return true; + } +} + +bool FStartDeployment::Update() +{ + if (const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault()) + { + FLocalDeploymentManager* LocalDeploymentManager = GetLocalDeploymentManager(); + const FString LaunchConfig = FPaths::Combine(FPaths::ConvertRelativePathToFull(FPaths::ProjectIntermediateDir()), AutomationLaunchConfig); + const FString LaunchFlags = SpatialGDKSettings->GetSpatialOSCommandLineLaunchFlags(); + const FString SnapshotName = SpatialGDKSettings->GetSpatialOSSnapshotToLoad(); + const FString RuntimeVersion = SpatialGDKSettings->GetSpatialOSRuntimeVersionForLocal(); + + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [LocalDeploymentManager, LaunchConfig, LaunchFlags, SnapshotName, RuntimeVersion] + { + if (!GenerateWorkerJson()) + { + return; + } + + if (!GenerateWorkerAssemblies()) + { + return; + } + + FSpatialLaunchConfigDescription LaunchConfigDescription(AutomationWorkerType); + LaunchConfigDescription.SetLevelEditorPlaySettingsWorkerTypes(); + + if (!GenerateDefaultLaunchConfig(LaunchConfig, &LaunchConfigDescription)) + { + return; + } + + if (LocalDeploymentManager->IsLocalDeploymentRunning()) + { + return; + } + + LocalDeploymentManager->TryStartLocalDeployment(LaunchConfig, RuntimeVersion, LaunchFlags, SnapshotName, TEXT(""), nullptr); + }); + } + + return true; +} + +bool FStopDeployment::Update() +{ + FLocalDeploymentManager* LocalDeploymentManager = GetLocalDeploymentManager(); + + if (!LocalDeploymentManager->IsLocalDeploymentRunning() && !LocalDeploymentManager->IsDeploymentStopping()) + { + return true; + } + + if (!LocalDeploymentManager->IsDeploymentStopping()) + { + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [LocalDeploymentManager] + { + LocalDeploymentManager->TryStopLocalDeployment(); + }); + } + + return true; +} + +bool FWaitForDeployment::Update() +{ + FLocalDeploymentManager* const LocalDeploymentManager = GetLocalDeploymentManager(); + + const double NewTime = FPlatformTime::Seconds(); + + if (NewTime - StartTime >= MAX_WAIT_TIME_FOR_LOCAL_DEPLOYMENT_OPERATION) + { + // The given time for the deployment to start/stop has expired - test its current state. + if (ExpectedDeploymentState == EDeploymentState::IsRunning) + { + Test->TestTrue(TEXT("Deployment is running"), LocalDeploymentManager->IsLocalDeploymentRunning() && !LocalDeploymentManager->IsDeploymentStopping()); + } + else + { + Test->TestFalse(TEXT("Deployment is not running"), LocalDeploymentManager->IsLocalDeploymentRunning() || LocalDeploymentManager->IsDeploymentStopping()); + } + return true; + } + + if (LocalDeploymentManager->IsDeploymentStopping()) + { + return false; + } + else + { + return (ExpectedDeploymentState == EDeploymentState::IsRunning) ? LocalDeploymentManager->IsLocalDeploymentRunning() : !LocalDeploymentManager->IsLocalDeploymentRunning(); + } +} + +bool FCheckDeploymentState::Update() +{ + FLocalDeploymentManager* LocalDeploymentManager = GetLocalDeploymentManager(); + + if (ExpectedDeploymentState == EDeploymentState::IsRunning) + { + Test->TestTrue(TEXT("Deployment is running"), LocalDeploymentManager->IsLocalDeploymentRunning() && !LocalDeploymentManager->IsDeploymentStopping()); + } + else + { + Test->TestFalse(TEXT("Deployment is not running"), LocalDeploymentManager->IsLocalDeploymentRunning() || LocalDeploymentManager->IsDeploymentStopping()); + } + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.h new file mode 100644 index 0000000000..bdcd5e103a --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.h @@ -0,0 +1,13 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/TestDefinitions.h" + +#include "CoreMinimal.h" + +// TODO: UNR-1964 - Move EDeploymentState enum to LocalDeploymentManager +enum class EDeploymentState { IsRunning, IsNotRunning }; + +DEFINE_LATENT_AUTOMATION_COMMAND(FStartDeployment); +DEFINE_LATENT_AUTOMATION_COMMAND(FStopDeployment); +DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER(FWaitForDeployment, FAutomationTestBase*, Test, EDeploymentState, ExpectedDeploymentState); +DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER(FCheckDeploymentState, FAutomationTestBase*, Test, EDeploymentState, ExpectedDeploymentState); diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKTests.Build.cs b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKTests.Build.cs index 7a5a10d088..a03efb57cd 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKTests.Build.cs +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKTests.Build.cs @@ -6,8 +6,15 @@ public class SpatialGDKTests : ModuleRules { public SpatialGDKTests(ReadOnlyTargetRules Target) : base(Target) { + bLegacyPublicIncludePaths = false; PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; - bFasterWithoutUnity = true; +#pragma warning disable 0618 + bFasterWithoutUnity = true; // Deprecated in 4.24, replace with bUseUnity = false; once we drop support for 4.23 + if (Target.Version.MinorVersion == 24) // Due to a bug in 4.24, bFasterWithoutUnity is inversed, fixed in master, so should hopefully roll into the next release, remove this once it does + { + bFasterWithoutUnity = false; + } +#pragma warning restore 0618 PrivateDependencyModuleNames.AddRange( new string[] { diff --git a/SpatialGDK/SpatialGDK.uplugin b/SpatialGDK/SpatialGDK.uplugin index 70c6398442..b5c9b1c0bb 100644 --- a/SpatialGDK/SpatialGDK.uplugin +++ b/SpatialGDK/SpatialGDK.uplugin @@ -1,7 +1,7 @@ { "FileVersion": 3, - "Version": 5, - "VersionName": "0.8.1", + "Version": 6, + "VersionName": "0.9.0", "FriendlyName": "SpatialOS GDK for Unreal", "Description": "The SpatialOS Game Development Kit (GDK) for Unreal Engine allows you to host your game and combine multiple dedicated server instances across one seamless game world whilst using the Unreal Engine networking API.", "Category": "SpatialOS", @@ -11,7 +11,7 @@ "MarketplaceURL": "", "SupportURL": "https://forums.improbable.io/", "EnabledByDefault": true, - "CanContainContent": false, + "CanContainContent": true, "IsBetaVersion": false, "Installed": true, "Modules": [ @@ -19,7 +19,7 @@ "Name": "SpatialGDK", "Type": "Runtime", "LoadingPhase": "PreDefault", - "WhitelistPlatforms": [ "Win64", "Linux", "Mac", "XboxOne", "PS4", "IOS" ] + "WhitelistPlatforms": [ "Win64", "Linux", "Mac", "XboxOne", "PS4", "IOS", "Android" ] }, { "Name": "SpatialGDKEditor", @@ -31,7 +31,7 @@ "Name": "SpatialGDKEditorToolbar", "Type": "Editor", "LoadingPhase": "Default", - "WhitelistPlatforms": [ "Win64" ] + "WhitelistPlatforms": [ "Win64", "Mac" ] }, { "Name": "SpatialGDKEditorCommandlet", @@ -43,12 +43,12 @@ "Name": "SpatialGDKServices", "Type": "Editor", "LoadingPhase": "PreDefault", - "WhitelistPlatforms": [ "Win64" ] + "WhitelistPlatforms": [ "Win64", "Mac" ] }, { "Name": "SpatialGDKTests", "Type": "Editor", - "LoadingPhase": "Default", + "LoadingPhase": "PreLoadingScreen", "WhitelistPlatforms": [ "Win64" ] } ], @@ -58,7 +58,7 @@ "Enabled": true }, { - "Name": "GameplayAbilities", + "Name": "ReplicationGraph", "Enabled": true } ] diff --git a/ci/README.md b/ci/README.md new file mode 100644 index 0000000000..a5c5521ccf --- /dev/null +++ b/ci/README.md @@ -0,0 +1 @@ +See the ["CI Setup" wiki page](https://improbableio.atlassian.net/wiki/spaces/GBU/pages/563282518/CI+Setup) for info on our CI. diff --git a/ci/build-and-send-slack-notification.ps1 b/ci/build-and-send-slack-notification.ps1 new file mode 100644 index 0000000000..964008cb39 --- /dev/null +++ b/ci/build-and-send-slack-notification.ps1 @@ -0,0 +1,82 @@ +# Send a Slack notification with a link to the build. + +# Download previously uploaded slack attachments that were generated for each testing step +New-Item -ItemType Directory -Path "$PSScriptRoot/slack_attachments" +& buildkite-agent artifact download "*slack_attachment_*.json" "$PSScriptRoot/slack_attachments" + +$attachments = @() +$all_steps_passed = $True +Get-ChildItem -Recurse "$PSScriptRoot/slack_attachments" -Filter "*.json" | Foreach-Object { + $attachment = Get-Content -Path $_.FullName | Out-String | ConvertFrom-Json + if ($attachment.color -eq "danger") { + $all_steps_passed = $False + } + $attachments += $attachment +} + +# Build text for slack message +if ($env:NIGHTLY_BUILD -eq "true") { + $build_description = ":night_with_stars: Nightly build of *GDK for Unreal*" +} +else { + $build_description = "*GDK for Unreal* build by $env:BUILDKITE_BUILD_CREATOR" +} +if ($all_steps_passed) { + $build_result = "passed testing" +} +else { + $build_result = "failed testing" +} +$slack_text = $build_description + " " + $build_result + "." + +# Read Slack webhook secret from the vault and extract the Slack webhook URL from it. +$slack_webhook_secret = "$(imp-ci secrets read --environment=production --buildkite-org=improbable --secret-type=slack-webhook --secret-name=unreal-gdk-slack-web-hook)" +$slack_webhook_url = $slack_webhook_secret | ConvertFrom-Json | ForEach-Object { $_.url } + +$json_message = [ordered]@{ + text = "$slack_text" + attachments = @( + @{ + fallback = "Find the build at $build_url" + color = $(if ($all_steps_passed) { "good" } else { "danger" }) + fields = @( + @{ + title = "Build message" + value = "$env:BUILDKITE_MESSAGE".Substring(0, [System.Math]::Min(64, "$env:BUILDKITE_MESSAGE".Length)) + short = "true" + } + @{ + title = "GDK branch" + value = "$env:BUILDKITE_BRANCH" + short = "true" + } + ) + actions = @( + @{ + type = "button" + text = ":github: GDK commit" + url = "https://github.com/spatialos/UnrealGDK/commit/$env:BUILDKITE_COMMIT" + style = "primary" + } + @{ + type = "button" + text = ":buildkite: BK build" + url = "$env:BUILDKITE_BUILD_URL" + style = "primary" + } + ) + } + ) +} + +# Add attachments from other build steps +Foreach ($attachment in $attachments) { + $json_message.attachments += $attachment +} + +# ConverTo-Json requires a finite depth value to prevent potential non-termination due to ciruclar references (default is 2) +$json_request = $json_message | ConvertTo-Json -Depth 16 + +if (-Not $all_steps_passed) { # Only report failing builds for now + Invoke-WebRequest -UseBasicParsing -URI "$slack_webhook_url" -ContentType "application/json" -Method POST -Body "$json_request" +} diff --git a/ci/build-gdk.ps1 b/ci/build-gdk.ps1 deleted file mode 100644 index 3a759e76d0..0000000000 --- a/ci/build-gdk.ps1 +++ /dev/null @@ -1,29 +0,0 @@ -# Expects gdk_home, which is not the GDK location in the engine -param( - [string] $unreal_path = "$((Get-Item `"$($PSScriptRoot)`").parent.parent.FullName)\UnrealEngine", ## This should ultimately resolve to "C:\b\\UnrealEngine". - [string] $target_platform = "Win64", - [string] $build_output_dir -) - -pushd "$($gdk_home)" - Start-Event "build-unreal-gdk-$($target_platform)" "build-gdk" - pushd "SpatialGDK" - $gdk_build_proc = Start-Process -PassThru -NoNewWindow -FilePath "$unreal_path\Engine\Build\BatchFiles\RunUAT.bat" -ArgumentList @(` - "BuildPlugin", ` - "-Plugin=`"$($gdk_home)/SpatialGDK/SpatialGDK.uplugin`"", ` - "-TargetPlatforms=$($target_platform)", ` - # The build output directory here is intentionally meant to be outside both the GDK and the Engine, - # as apparently there were issues where BuildPlugin would fail when targeting a folder within UnrealEngine - # (and the gdk is symlinked inside the UnrealEngine) for output. - # Unreal would find two instances of the plugin during the build process (as it copies the .uplugin to the target folder at the start of the build process) - "-Package=`"$build_output_dir`"" ` - ) - $gdk_build_handle = $gdk_build_proc.Handle - Wait-Process -Id (Get-Process -InputObject $gdk_build_proc).id - if ($gdk_build_proc.ExitCode -ne 0) { - Write-Log "Failed to build Unreal GDK. Error: $($gdk_build_proc.ExitCode)" - Throw "Failed to build the Unreal GDK for $($target_platform)" - } - Finish-Event "build-unreal-gdk-$($target_platform)" "build-gdk" - popd -popd diff --git a/ci/build-project.ps1 b/ci/build-project.ps1 new file mode 100644 index 0000000000..62d8d7e831 --- /dev/null +++ b/ci/build-project.ps1 @@ -0,0 +1,49 @@ +param( + [string] $unreal_path, + [string] $test_repo_branch, + [string] $test_repo_url, + [string] $test_repo_uproject_path, + [string] $test_repo_path, + [string] $msbuild_exe, + [string] $gdk_home, + [string] $build_platform, + [string] $build_state, + [string] $build_target +) + +# Clone the testing project +Write-Output "Downloading the testing project from $($test_repo_url)" +git clone -b "$test_repo_branch" "$test_repo_url" "$test_repo_path" --depth 1 +if (-Not $?) { + Throw "Failed to clone testing project from $test_repo_url." +} + +# The Plugin does not get recognised as an Engine plugin, because we are using a pre-built version of the engine +# copying the plugin into the project's folder bypasses the issue +New-Item -ItemType Junction -Name "UnrealGDK" -Path "$test_repo_path\Game\Plugins" -Target "$gdk_home" + +# Disable tutorials, otherwise the closing of the window will crash the editor due to some graphic context reason +# Has to be this ugly settings modification, because overriding it from the commandline will not pass on this information +# to spawned Unreal editors (which we do as part of the tests) +Add-Content -Path "$unreal_path\Engine\Config\BaseEditorSettings.ini" -Value "`r`n[/Script/IntroTutorials.TutorialStateSettings]`r`nTutorialsProgress=(Tutorial=/Engine/Tutorial/Basics/LevelEditorAttract.LevelEditorAttract_C,CurrentStage=0,bUserDismissed=True)`r`n" + +Write-Output "Generating project files" +& "$unreal_path\Engine\Binaries\DotNET\UnrealBuildTool.exe" ` + "-projectfiles" ` + "-project=`"$test_repo_uproject_path`"" ` + "-game" ` + "-engine" ` + "-progress" +if ($lastExitCode -ne 0) { + throw "Failed to generate files for the testing project." +} + +Write-Output "Building project" +$build_configuration = $build_state + $(If ("$build_target" -eq "") { "" } Else { " $build_target" }) +& "$msbuild_exe" ` + "/nologo" ` + "$($test_repo_uproject_path.Replace(".uproject", ".sln"))" ` + "/p:Configuration=`"$build_configuration`";Platform=`"$build_platform`"" +if ($lastExitCode -ne 0) { + throw "Failed to build testing project." +} diff --git a/ci/build-project.sh b/ci/build-project.sh new file mode 100755 index 0000000000..6e3f7376d4 --- /dev/null +++ b/ci/build-project.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +set -e -u -o pipefail +if [[ -n "${DEBUG-}" ]]; then + set -x +fi + +pushd "$(dirname "$0")" + UNREAL_PATH="${1?Please enter the path to the Unreal Engine.}" + TEST_REPO_BRANCH="${2?Please enter the branch that you want to test.}" + TEST_REPO_URL="${3?Please enter the URL for the git repo you want to test.}" + TEST_REPO_UPROJECT_PATH="${4?Please enter the path to the uproject inside the test repo.}" + TEST_REPO_PATH="${5?Please enter the path where the test repo should be cloned to.}" + GDK_HOME="${6?Please enter the path to the GDK for Unreal repo.}" + BUILD_PLATFORM="${7?Please enter the build platform for your Unreal build.}" + BUILD_STATE="${8?Please enter the build state for your Unreal build.}" + BUILD_TARGET="${9?Please enter the build target for your Unreal build.}" + + # Clone the testing project + echo "Cloning the testing project from ${TEST_REPO_URL}" + git clone \ + --branch "${TEST_REPO_BRANCH}" \ + "${TEST_REPO_URL}" \ + "${TEST_REPO_PATH}" \ + --single-branch \ + --depth 1 + + # The Plugin does not get recognised as an Engine plugin, because we are using a pre-built version of the engine + # copying the plugin into the project's folder bypasses the issue + mkdir -p "${TEST_REPO_PATH}/Game/Plugins" + cp -R "${GDK_HOME}" "${TEST_REPO_PATH}/Game/Plugins/UnrealGDK" + + # Disable tutorials, otherwise the closing of the window will crash the editor due to some graphic context reason + echo "\r\n[/Script/IntroTutorials.TutorialStateSettings]\r\nTutorialsProgress=(Tutorial=/Engine/Tutorial/Basics/LevelEditorAttract.LevelEditorAttract_C,CurrentStage=0,bUserDismissed=True)\r\n" >> "${UNREAL_PATH}/Engine/Config/BaseEditorSettings.ini" + pushd "${UNREAL_PATH}" + echo "--- Generating project files" + "Engine/Build/BatchFiles/Mac/Build.sh" \ + -projectfiles \ + -project="${TEST_REPO_UPROJECT_PATH}" \ + -game \ + -engine \ + -progress + + echo "--- Building project" + "Engine/Build/BatchFiles/Mac/XcodeBuild.sh" \ + "${BUILD_TARGET}" \ + "${BUILD_PLATFORM}" \ + "${BUILD_STATE}" \ + "${TEST_REPO_UPROJECT_PATH}" + popd +popd diff --git a/ci/check-version-file.sh b/ci/check-version-file.sh old mode 100644 new mode 100755 index ce41a27aea..0a53e30d69 --- a/ci/check-version-file.sh +++ b/ci/check-version-file.sh @@ -7,45 +7,45 @@ set -euo pipefail # Ensure that at least one engine version is listed if [$(cat ci/unreal-engine.version) = ""]; then - error_msg="No version has been listed in the unreal-engine.version file." + ERROR_MSG="No version has been listed in the unreal-engine.version file." - echo $error_msg | buildkite-agent annotate --context "check-version-file" --style error + echo "${ERROR_MSG}" | buildkite-agent annotate --context "check-version-file" --style error - printf '%s\n' "$error_msg" >&2 + echo "${ERROR_MSG}" >&2 exit 1 fi; # Enforce pinned engine versions on the following branches protected_branches=(release preview) -is_protected=0 -for item in ${protected_branches[@]} +IS_PROTECTED=0 +for ITEM in ${protected_branches[@]} do # If this commit is part of a PR merging into one of the protected branches, make sure we are on a pinned engine version. # IMPORTANT: For this to work, make sure that a new build is triggered in buildkite when a PR is opened. (This is a pipeline setting in the buildkite web UI) # If not, buildkite may re-use a build of this branch before the PR was created, in which case the merge target was not known, and this check will have passed. - if [ "$BUILDKITE_PULL_REQUEST_BASE_BRANCH" == "$item" ]; then - is_protected=1 + if [[ "${BUILDKITE_PULL_REQUEST_BASE_BRANCH}" == "${ITEM}" ]]; then + IS_PROTECTED=1 fi # Also check when we're on a protected branch itself, in case a non-pinned engine version somehow got into the branch. - if [ "$BUILDKITE_BRANCH" == "$item" ]; then - is_protected=1 + if [[ "${BUILDKITE_BRANCH}" == "${ITEM}" ]]; then + IS_PROTECTED=1 fi done -if [ $is_protected -eq 1 ]; then +if [[ $IS_PROTECTED -eq 1 ]]; then # Ensure that every listed engine version is pinned IFS=$'\n' - for engine_version in $(cat < ci/unreal-engine.version); do - echo "Found engine version $engine_version" + for ENGINE_VERSION in $(cat < ci/unreal-engine.version); do + echo "Found engine version ${ENGINE_VERSION}" - if [[ $engine_version == HEAD* ]]; then # version starts with "HEAD" - error_msg="The merge target branch does not allow floating (HEAD) engine versions. Use pinned versions. (Of the form UnrealEngine-{commit hash})" + if [[ "${ENGINE_VERSION}" =~ "HEAD"* ]]; then # version starts with "HEAD" + ERROR_MSG="The merge target branch does not allow floating (HEAD) engine versions. Use pinned versions. (Of the form UnrealEngine-{commit hash})" - echo $error_msg | buildkite-agent annotate --context "check-version-file" --style error + echo $ERROR_MSG | buildkite-agent annotate --context "check-version-file" --style error - printf '%s\n' "$error_msg" >&2 + echo "${ERROR_MSG}" >&2 exit 1 fi done diff --git a/ci/cleanup.ps1 b/ci/cleanup.ps1 index b72b8e3794..4d4a432c35 100644 --- a/ci/cleanup.ps1 +++ b/ci/cleanup.ps1 @@ -1,15 +1,30 @@ param ( - [string] $unreal_path = "$((Get-Item `"$($PSScriptRoot)`").parent.parent.FullName)\UnrealEngine" ## This should ultimately resolve to "C:\b\\UnrealEngine". + [string] $unreal_path = "$((Get-Item `"$($PSScriptRoot)`").parent.parent.FullName)\UnrealEngine", ## This should ultimately resolve to "C:\b\\UnrealEngine". + [string] $project_name = "NetworkTestProject" ) -$gdk_in_engine = "$unreal_path\Engine\Plugins\UnrealGDK" + +$project_absolute_path = "$((Get-Item `"$($PSScriptRoot)`").parent.parent.FullName)\$project_name" ## This should ultimately resolve to "C:\b\\NetworkTestProject". + +# Workaround for UNR-2156 and UNR-2076, where spatiald / runtime processes sometimes never close, or where runtimes are orphaned +# Clean up any spatiald and java (i.e. runtime) processes that may not have been shut down +& spatial "service" "stop" +Stop-Process -Name "java" -Force -ErrorAction SilentlyContinue # Clean up the symlinks -if (Test-Path "$gdk_in_engine") { - (Get-Item "$gdk_in_engine").Delete() -} -if (Test-Path "$unreal_path\Samples\StarterContent\Plugins\UnrealGDK") { - (Get-Item "$unreal_path\Samples\StarterContent\Plugins\UnrealGDK").Delete() # TODO needs to stay in sync with setup-tests -} if (Test-Path "$unreal_path") { (Get-Item "$unreal_path").Delete() } + +$gdk_in_test_repo = "$project_absolute_path\Game\Plugins\UnrealGDK" +if (Test-Path "$gdk_in_test_repo") { + (Get-Item "$gdk_in_test_repo").Delete() +} + +# Clean up testing project +if (Test-Path $project_absolute_path) { + Write-Output "Removing existing project" + Remove-Item $project_absolute_path -Recurse -Force + if (-Not $?) { + Throw "Failed to remove existing project at $($project_absolute_path)." + } +} diff --git a/ci/cleanup.sh b/ci/cleanup.sh new file mode 100755 index 0000000000..78824df8f3 --- /dev/null +++ b/ci/cleanup.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +pushd "$(dirname "$0")" + + UNREAL_PATH="${1:-"$(pwd)/../../UnrealEngine"}" + BUILD_PROJECT="${2:-NetworkTestProject}" + + PROJECT_ABSOLUTE_PATH="$(pwd)/../../${BUILD_PROJECT}" + GDK_IN_TEST_REPO="${PROJECT_ABSOLUTE_PATH}/Game/Plugins/UnrealGDK" + + # Workaround for UNR-2156 and UNR-2076, where spatiald / runtime processes sometimes never close, or where runtimes are orphaned + # Clean up any spatiald and java (i.e. runtime) processes that may not have been shut down + spatial service stop + pkill -9 -f java + + rm -rf ${UNREAL_PATH} + rm -rf ${GDK_IN_TEST_REPO} + rm -rf ${PROJECT_ABSOLUTE_PATH} +popd diff --git a/ci/common.ps1 b/ci/common.ps1 index 5021020e64..c41d731158 100644 --- a/ci/common.ps1 +++ b/ci/common.ps1 @@ -1,13 +1,16 @@ +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + function Write-Log() { - param( - [string] $msg, - [Parameter(Mandatory=$false)] [bool] $expand = $false - ) - if ($expand) { - Write-Output "+++ $($msg)" - } else { - Write-Output "--- $($msg)" - } + param( + [string] $msg, + [Parameter(Mandatory = $False)] [bool] $expand = $False + ) + if ($expand) { + Write-Output "+++ $($msg)" + } + else { + Write-Output "--- $($msg)" + } } function Start-Event() { @@ -18,9 +21,9 @@ function Start-Event() { # Start this tracing span. Start-Process -NoNewWindow "imp-ci" -ArgumentList @(` - "events", "new", ` - "--name", "$($event_name)", ` - "--child-of", "$($event_parent)" + "events", "new", ` + "--name", "$($event_name)", ` + "--child-of", "$($event_parent)" ) | Out-Null Write-Log "$($event_name)" @@ -34,9 +37,9 @@ function Finish-Event() { # Emit the end marker for this tracing span. Start-Process -NoNewWindow "imp-ci" -ArgumentList @(` - "events", "new", ` - "--name", "$($event_name)", ` - "--child-of", "$($event_parent)" + "events", "new", ` + "--name", "$($event_name)", ` + "--child-of", "$($event_parent)" ) | Out-Null } diff --git a/ci/gdk_build.template.steps.yaml b/ci/gdk_build.template.steps.yaml new file mode 100644 index 0000000000..d0483327f6 --- /dev/null +++ b/ci/gdk_build.template.steps.yaml @@ -0,0 +1,68 @@ +--- +# This is designed to trap and retry failures because agent lost +# connection. Agent exits with -1 in this case. +agent_transients: &agent_transients + exit_status: -1 + limit: 3 +# BK system error +bk_system_error: &bk_system_error + exit_status: 255 + limit: 3 +# job was interrupted by a signal (e.g. ctrl+c etc) +bk_interrupted_by_signal: &bk_interrupted_by_signal + exit_status: 15 + limit: 3 + + +windows: &windows + agents: + - "agent_count=1" + - "capable_of_building=gdk-for-unreal" + - "environment=production" + - "machine_type=${BK_MACHINE_TYPE}" + - "platform=windows" + - "permission_set=builder" + - "scaler_version=2" + - "queue=${CI_WINDOWS_BUILDER_QUEUE:-v4-20-03-26-102432-bk9951-8afe0ffb}" + - "boot_disk_size_gb=500" + retry: + automatic: + - <<: *agent_transients + - <<: *bk_system_error + - <<: *bk_interrupted_by_signal + timeout_in_minutes: 120 + plugins: + - ca-johnson/taskkill#v4.1: ~ + +macos: &macos + agents: + - "capable_of_building=gdk-for-unreal" + - "environment=production" + - "permission_set=builder" + - "platform=macos" + - "queue=${DARWIN_BUILDER_QUEUE:-v4-9c6ee0ef-d}" + timeout_in_minutes: 120 + retry: + automatic: + - <<: *agent_transients + - <<: *bk_system_error + - <<: *bk_interrupted_by_signal + +env: + FASTBUILD_CACHE_PATH: "\\\\gdk-for-unreal-cache.${CI_ENVIRONMENT}-intinf-eu1.i8e.io\\samba\\fastbuild" + FASTBUILD_CACHE_MODE: rw + # FASTBUILD_BROKERAGE_PATH: "\\\\fastbuild-brokerage.${CI_ENVIRONMENT}-intinf-eu1.i8e.io\\samba" TODO: UNR-3208 - Temporarily disabled until distribution issues resolved. + +steps: + - <<: *BUILDKITE_AGENT_PLACEHOLDER + label: "build-${ENGINE_COMMIT_HASH}-${BUILD_PLATFORM}-${BUILD_TARGET}-${BUILD_STATE}-${TEST_CONFIG}" + command: "${BUILD_COMMAND}" + artifact_paths: + - "../UnrealEngine/Engine/Programs/AutomationTool/Saved/Logs/*" + env: + BUILD_ALL_CONFIGURATIONS: "${BUILD_ALL_CONFIGURATIONS}" + ENGINE_COMMIT_HASH: "${ENGINE_COMMIT_HASH}" + BUILD_PLATFORM: "${BUILD_PLATFORM}" + BUILD_TARGET: "${BUILD_TARGET}" + BUILD_STATE: "${BUILD_STATE}" + TEST_CONFIG: "${TEST_CONFIG}" diff --git a/ci/gdk_build_template.steps.yaml b/ci/gdk_build_template.steps.yaml deleted file mode 100644 index 7a6b84d02b..0000000000 --- a/ci/gdk_build_template.steps.yaml +++ /dev/null @@ -1,46 +0,0 @@ -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 - -common: &common - agents: - - "agent_count=1" - - "capable_of_building=gdk-for-unreal" - - "environment=production" - - "machine_type=quad-high-cpu" # this name matches to SpatialOS node-size names - - "platform=windows" - - "permission_set=builder" - - "scaler_version=2" - - "queue=${CI_WINDOWS_BUILDER_QUEUE:-v3-1572523200-2f33678e336fc673-------z}" - retry: - automatic: - - <<: *agent_transients - timeout_in_minutes: 60 - plugins: - - ca-johnson/taskkill#v4.1: ~ - -env: - FASTBUILD_CACHE_PATH: "\\\\gdk-for-unreal-cache.${CI_ENVIRONMENT}-intinf-eu1.i8e.io\\samba\\fastbuild" - FASTBUILD_CACHE_MODE: rw - FASTBUILD_BROKERAGE_PATH: "\\\\fastbuild-brokerage.${CI_ENVIRONMENT}-intinf-eu1.i8e.io\\samba" - -steps: - - label: "build-and-test-GDK-:windows:-ENGINE_COMMIT_HASH_PLACEHOLDER" - command: "powershell ./ci/setup-build-test-gdk.ps1 -target_platform Win64" - <<: *common # This folds the YAML named anchor into this step. Overrides, if any, should follow, not precede. - artifact_paths: - - "../UnrealEngine/Engine/Programs/AutomationTool/Saved/Logs/*" - - "../UnrealEngine/Samples/StarterContent/Saved/Logs/*" - - "ci/TestResults/*" - env: - ENGINE_COMMIT_HASH: "ENGINE_COMMIT_HASH_PLACEHOLDER" - - - label: "build-GDK-:linux:-ENGINE_COMMIT_HASH_PLACEHOLDER" - command: "powershell ./ci/setup-build-test-gdk.ps1 -target_platform Linux" - <<: *common # This folds the YAML named anchor into this step. Overrides, if any, should follow, not precede. - artifact_paths: - - "../UnrealEngine/Engine/Programs/AutomationTool/Saved/Logs/*" - env: - ENGINE_COMMIT_HASH: "ENGINE_COMMIT_HASH_PLACEHOLDER" diff --git a/ci/generate-and-upload-build-steps.sh b/ci/generate-and-upload-build-steps.sh new file mode 100755 index 0000000000..a191a5530e --- /dev/null +++ b/ci/generate-and-upload-build-steps.sh @@ -0,0 +1,99 @@ +#!/bin/bash +set -euo pipefail + +upload_build_configuration_step() { + export ENGINE_COMMIT_HASH="${1}" + export BUILD_PLATFORM="${2}" + export BUILD_TARGET="${3}" + export BUILD_STATE="${4}" + export TEST_CONFIG="${5:-default}" + + if [[ ${BUILD_PLATFORM} == "Mac" ]]; then + export BUILD_COMMAND="./ci/setup-build-test-gdk.sh" + REPLACE_STRING="s|BUILDKITE_AGENT_PLACEHOLDER|macos|g;" + else + export BUILD_COMMAND="powershell ./ci/setup-build-test-gdk.ps1" + REPLACE_STRING="s|BUILDKITE_AGENT_PLACEHOLDER|windows|g;" + fi + + sed "$REPLACE_STRING" "ci/gdk_build.template.steps.yaml" | buildkite-agent pipeline upload +} + +generate_build_configuration_steps () { + # See https://docs.unrealengine.com/en-US/Programming/Development/BuildConfigurations/index.html for possible configurations + ENGINE_COMMIT_HASH="${1}" + + if [[ -z "${NIGHTLY_BUILD+x}" ]]; then + export BK_MACHINE_TYPE="quad-high-cpu" + else + export BK_MACHINE_TYPE="single-high-cpu" # nightly builds run on smaller nodes + fi + + if [[ -z "${MAC_BUILD:-}" ]]; then + # if the BUILD_ALL_CONFIGURATIONS environment variable doesn't exist, then... + if [[ -z "${BUILD_ALL_CONFIGURATIONS+x}" ]]; then + echo "Building for subset of supported configurations. Generating the appropriate steps..." + + SLOW_NETWORKING_TESTS_LOCAL="${SLOW_NETWORKING_TESTS:-false}" + # if the SLOW_NETWORKING_TESTS variable is not set or empty, look at whether this is a nightly build + if [[ -z "${SLOW_NETWORKING_TESTS+x}" ]]; then + if [[ "${NIGHTLY_BUILD:-false,,}" == "true" ]]; then + SLOW_NETWORKING_TESTS_LOCAL="true" + fi + fi + + if [[ "${SLOW_NETWORKING_TESTS_LOCAL,,}" == "true" ]]; then + # Start a build with native tests as a separate step + upload_build_configuration_step "${ENGINE_COMMIT_HASH}" "Win64" "Editor" "Development" "Native" + fi + + # Win64 Development Editor build configuration + upload_build_configuration_step "${ENGINE_COMMIT_HASH}" "Win64" "Editor" "Development" + + # Linux Development NoEditor build configuration + upload_build_configuration_step "${ENGINE_COMMIT_HASH}" "Linux" "" "Development" + else + echo "Building for all supported configurations. Generating the appropriate steps..." + + export BK_MACHINE_TYPE="single-high-cpu" # run the weekly with smaller nodes, since this is not time-critical + + # Editor builds (Test and Shipping build states do not exist for the Editor build target) + for BUILD_STATE in "DebugGame" "Development"; do + upload_build_configuration_step "${ENGINE_COMMIT_HASH}" "Win64" "Editor" "${BUILD_STATE}" + done + + # Generate all possible builds for non-Editor build targets + for BUILD_PLATFORM in "Win64" "Linux"; do + for BUILD_TARGET in "" "Client" "Server"; do + for BUILD_STATE in "DebugGame" "Development" "Shipping"; do + upload_build_configuration_step "${ENGINE_COMMIT_HASH}" "${BUILD_PLATFORM}" "${BUILD_TARGET}" "${BUILD_STATE}" + done + done + done + fi + else + if [[ -z "${BUILD_ALL_CONFIGURATIONS+x}" ]]; then + # MacOS Development Editor build configuration + upload_build_configuration_step "${ENGINE_COMMIT_HASH}" "Mac" "Editor" "Development" + else + # Editor builds (Test and Shipping build states do not exist for the Editor build target) + for BUILD_STATE in "DebugGame" "Development"; do + upload_build_configuration_step "${ENGINE_COMMIT_HASH}" "Mac" "Editor" "${BUILD_STATE}" + done + fi + + fi +} + +# This script generates steps for each engine version listed in unreal-engine.version, +# based on the gdk_build.template.steps.yaml template +if [[ -z "${ENGINE_VERSION+x}" ]]; then + echo "Generating build steps for each engine version listed in unreal-engine.version" + IFS=$'\n' + for COMMIT_HASH in $(cat < ci/unreal-engine.version); do + generate_build_configuration_steps "${COMMIT_HASH}" + done +else + echo "Generating steps for the specified engine version: ${ENGINE_VERSION}" + generate_build_configuration_steps "${ENGINE_VERSION}" +fi; diff --git a/ci/generate_and_upload_build_steps.sh b/ci/generate_and_upload_build_steps.sh deleted file mode 100644 index 983d8c5b9b..0000000000 --- a/ci/generate_and_upload_build_steps.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# This script generates steps for each engine version listed in unreal-engine.version, and adds those to generated_base.steps.yaml -# The steps are based on the template in generated_steps.steps.yaml - -if [ -z "${ENGINE_VERSION}" ]; then - echo "Generating build steps for each engine version listed in unreal-engine.version" - IFS=$'\n' - for commit_hash in $(cat < ci/unreal-engine.version); do - sed "s/ENGINE_COMMIT_HASH_PLACEHOLDER/$commit_hash/g" ci/gdk_build_template.steps.yaml | buildkite-agent pipeline upload - done -else - echo "Generating steps for the specified engine version: $ENGINE_VERSION" - sed "s/ENGINE_COMMIT_HASH_PLACEHOLDER/$ENGINE_VERSION/g" ci/gdk_build_template.steps.yaml | buildkite-agent pipeline upload -fi; diff --git a/ci/get-engine.ps1 b/ci/get-engine.ps1 index 802dd7b80d..a728e82e12 100644 --- a/ci/get-engine.ps1 +++ b/ci/get-engine.ps1 @@ -1,31 +1,34 @@ +# This script is used directly as part of the UnrealGDKExampleProject CI, so providing default values may be strictly necessary + param( # Note: this directory is outside the build directory and will not get automatically cleaned up from agents unless agents are restarted. [string] $engine_cache_directory = "$($pwd.drive.root)UnrealEngine-Cache", # Unreal path is a symlink to a specific Engine version located in Engine cache directory. This should ultimately resolve to "C:\b\\UnrealEngine". [string] $unreal_path = "$((Get-Item `"$($PSScriptRoot)`").parent.parent.FullName)\UnrealEngine", - + [string] $gcs_publish_bucket = "io-internal-infra-unreal-artifacts-production/UnrealEngine" ) -pushd "$($gdk_home)" +Push-Location "$($gdk_home)" # Fetch the version of Unreal Engine we need - pushd "ci" + Push-Location "ci" # Allow overriding the engine version if required if (Test-Path env:ENGINE_COMMIT_HASH) { $version_description = (Get-Item -Path env:ENGINE_COMMIT_HASH).Value - Write-Log "Using engine version defined by ENGINE_COMMIT_HASH: $($version_description)" - } else { + Write-Output "Using engine version defined by ENGINE_COMMIT_HASH: $($version_description)" + } + else { # Read Engine version from the file and trim any trailing white spaces and new lines. $version_description = Get-Content -Path "unreal-engine.version" -First 1 - Write-Log "Using engine version found in unreal-engine.version file: $($version_description)" + Write-Output "Using engine version found in unreal-engine.version file: $($version_description)" } # Check if we are using a 'floating' engine version, meaning that we want to get the latest built version of the engine on some branch # This is specified by putting "HEAD name/of-a-branch" in the unreal-engine.version file # If so, retrieve the version of the latest build from GCS, and use that going forward. - $head_version_prefix = "HEAD " + $head_version_prefix = "HEAD " if ($version_description.StartsWith($head_version_prefix)) { $version_branch = $version_description.Remove(0, $head_version_prefix.Length) # Remove the prefix to just get the branch name $version_branch = $version_branch.Replace("/", "_") # Replace / with _ since / is treated as the folder seperator in GCS @@ -33,50 +36,51 @@ pushd "$($gdk_home)" # Download the head pointer file for the given branch, which contains the latest built version of the engine from that branch $head_pointer_gcs_path = "gs://$($gcs_publish_bucket)/HEAD/$($version_branch).version" $unreal_version = $(gsutil cp $head_pointer_gcs_path -) # the '-' at the end instructs gsutil to download the file and output the contents to stdout - } else { + } + else { $unreal_version = $version_description } - popd - + Pop-Location + ## Create an UnrealEngine-Cache directory if it doesn't already exist. New-Item -ItemType Directory -Path $engine_cache_directory -Force - pushd $engine_cache_directory + Push-Location $engine_cache_directory Start-Event "download-unreal-engine" "get-unreal-engine" + $engine_gcs_path = "gs://$($gcs_publish_bucket)/$($unreal_version).zip" + Write-Output "Downloading Unreal Engine artifacts version $unreal_version from $($engine_gcs_path)" - $engine_gcs_path = "gs://$($gcs_publish_bucket)/$($unreal_version).zip" - Write-Log "Downloading Unreal Engine artifacts version $unreal_version from $($engine_gcs_path)" - - $gsu_proc = Start-Process -Wait -PassThru -NoNewWindow "gsutil" -ArgumentList @(` - "cp", ` - "-n", ` # noclobber - "$($engine_gcs_path)", ` - "$($unreal_version).zip" ` - ) + $gsu_proc = Start-Process -Wait -PassThru -NoNewWindow "gsutil" -ArgumentList @(` + "cp", ` + "-n", ` # noclobber + "$($engine_gcs_path)", ` + "$($unreal_version).zip" ` + ) Finish-Event "download-unreal-engine" "get-unreal-engine" + if ($gsu_proc.ExitCode -ne 0) { - Write-Log "Failed to download Engine artifacts. Error: $($gsu_proc.ExitCode)" - Throw "Failed to download Engine artifacts" + Write-Log "Failed to download Engine artifact. Error: $($gsu_proc.ExitCode)" + Throw "Failed to download Engine artifact. If you're trying to download an Engine artifact more than 60 days old it may have been deleted. You can build it again at https://buildkite.com/improbable/unrealengine-premerge" } Start-Event "unzip-unreal-engine" "get-unreal-engine" - Write-Log "Unzipping Unreal Engine" - $zip_proc = Start-Process -Wait -PassThru -NoNewWindow "7z" -ArgumentList @(` - "x", ` - "$($unreal_version).zip", ` - "-o$($unreal_version)", ` - "-aos" ` # skip existing files - ) + $zip_proc = Start-Process -Wait -PassThru -NoNewWindow "7z" -ArgumentList @(` + "x", ` + "$($unreal_version).zip", ` + "-o$($unreal_version)", ` + "-aos" ` # skip existing files + ) Finish-Event "unzip-unreal-engine" "get-unreal-engine" + if ($zip_proc.ExitCode -ne 0) { Write-Log "Failed to unzip Unreal Engine. Error: $($zip_proc.ExitCode)" Throw "Failed to unzip Unreal Engine." } - popd + Pop-Location ## Create an UnrealEngine symlink to the correct directory Remove-Item $unreal_path -ErrorAction ignore -Recurse -Force - cmd /c mklink /J $unreal_path "$engine_cache_directory\$($unreal_version)" + New-Item -ItemType Junction -Path "$unreal_path" -Target "$engine_cache_directory\$($unreal_version)" $clang_path = "$unreal_path\ClangToolchain" Write-Log "Setting LINUX_MULTIARCH_ROOT environment variable to $($clang_path)" @@ -88,7 +92,7 @@ pushd "$($gdk_home)" # Trapping error codes on this is tricky, because it doesn't always return 0 on success, and frankly, we just don't know what it _will_ return. # Note: this fails to install .NET framework, but it's probably fine, as it's set up on Unreal build agents already (check gdk-for-unreal.build-capability/roles/gdk_for_unreal_choco/tasks/Windows.yml) Start-Process -Wait -PassThru -NoNewWindow -FilePath "$($unreal_path)/Engine/Extras/Redist/en-us/UE4PrereqSetup_x64.exe" -ArgumentList @(` - "/quiet" ` + "/quiet" ` ) Finish-Event "installing-unreal-engine-prerequisites" "get-unreal-engine" -popd +Pop-Location diff --git a/ci/get-engine.sh b/ci/get-engine.sh new file mode 100755 index 0000000000..d82c4b18bc --- /dev/null +++ b/ci/get-engine.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +set -e -u -o pipefail +if [[ -n "${DEBUG-}" ]]; then + set -x +fi + +pushd "$(dirname "$0")" + # Unreal path is the path to the Engine directory. No symlinking for mac, because they seem to cause issues during the build. + #This should ultimately resolve to "/Users/buildkite-agent/builds//improbable/UnrealEngine". + UNREAL_PATH="${1:-"$(pwd)/../../UnrealEngine"}" + # The GCS bucket that stores the built out Unreal Engine we want to retrieve + GCS_PUBLISH_BUCKET="${2:-io-internal-infra-unreal-artifacts-production/UnrealEngine}" + GDK_HOME="$(pwd)/.." + + pushd "${GDK_HOME}" + # Fetch the version of Unreal Engine we need + pushd "ci" + # Allow overriding the engine version if required + if [[ -n "${ENGINE_COMMIT_HASH:-}" ]]; then + VERSION_DESCRIPTION="${ENGINE_COMMIT_HASH}" + echo "Using engine version defined by ENGINE_COMMIT_HASH: ${VERSION_DESCRIPTION}" + else + # Read Engine version from the file and trim any trailing white spaces and new lines. + VERSION_DESCRIPTION=$(head -n 1 unreal-engine.version) + echo "Using engine version found in unreal-engine.version file: ${VERSION_DESCRIPTION}" + fi + + # Check if we are using a 'floating' engine version, meaning that we want to get the latest built version of the engine on some branch + # This is specified by putting "HEAD name/of-a-branch" in the unreal-engine.version file + # If so, retrieve the version of the latest build from GCS, and use that going forward. + HEAD_VERSION_PREFIX="HEAD " + if [[ "${VERSION_DESCRIPTION}" == ${HEAD_VERSION_PREFIX}* ]]; then + VERSION_BRANCH=${VERSION_DESCRIPTION#"${HEAD_VERSION_PREFIX}"} # Remove the prefix to just get the branch name + VERSION_BRANCH=$(echo ${VERSION_BRANCH} | tr "/" "_") # Replace / with _ since / is treated as the folder seperator in GCS + + # Download the head pointer file for the given branch, which contains the latest built version of the engine from that branch + HEAD_POINTER_GCS_PATH="gs://${GCS_PUBLISH_BUCKET}/HEAD/mac-${VERSION_BRANCH}.version" + UNREAL_VERSION=$(gsutil cp "${HEAD_POINTER_GCS_PATH}" -) # the '-' at the end instructs gsutil to download the file and output the contents to stdout + else + UNREAL_VERSION="Mac-UnrealEngine-${VERSION_DESCRIPTION}" + fi + popd + + echo "--- download-unreal-engine" + ENGINE_GCS_PATH="gs://${GCS_PUBLISH_BUCKET}/${UNREAL_VERSION}.zip" + echo "Downloading Unreal Engine artifacts version ${UNREAL_VERSION} from ${ENGINE_GCS_PATH}" + gsutil cp -n "${ENGINE_GCS_PATH}" "${UNREAL_VERSION}".zip + 7z x "${UNREAL_VERSION}".zip -o${UNREAL_PATH} -aos + popd +popd diff --git a/ci/report-tests.ps1 b/ci/report-tests.ps1 index 2b650ba9c7..3c3833d8bd 100644 --- a/ci/report-tests.ps1 +++ b/ci/report-tests.ps1 @@ -1,7 +1,11 @@ param( - [string] $test_result_dir + [string] $test_result_dir, + [string] $target_platform ) +# Artifact path used by Buildkite (drop the initial C:\) +$formatted_test_result_dir = (Split-Path -Path "$test_result_dir" -NoQualifier).Substring(1) + if (Test-Path "$test_result_dir\index.html" -PathType Leaf) { # The Unreal Engine produces a mostly undocumented index.html/index.json as the result of running a test suite, for now seems mostly # for internal use - but it's an okay visualisation for test results, so we fix it up here to display as a build artifact in CI @@ -25,16 +29,116 @@ if (Test-Path "$test_result_dir\index.html" -PathType Leaf) { for ($i = 0; $i -lt $replacement_strings.length; $i = $i + 2) { $first = $replacement_strings[$i] - $second = $replacement_strings[$i+1] + $second = $replacement_strings[$i + 1] ((Get-Content -Path "$test_result_dir\index.html" -Raw) -Replace $first, $second) | Set-Content -Path "$test_result_dir\index.html" } # %5C is the escape code for a backslash \, needed to successfully reach the artifact from the serving site - ((Get-Content -Path "$test_result_dir\index.html" -Raw) -Replace "index.json", "ci%5CTestResults%5Cindex.json") | Set-Content -Path "$test_result_dir\index.html" + ((Get-Content -Path "$test_result_dir\index.html" -Raw) -Replace "index.json", "$($formatted_test_result_dir.Replace("\","%5C"))%5Cindex.json") | Set-Content -Path "$test_result_dir\index.html" - echo "Test results in a nicer format can be found here.`n" | Out-File "$gdk_home/annotation.md" + Write-Output "Test results in a nicer format can be found here.`n" | Out-File "$gdk_home/annotation.md" Get-Content "$gdk_home/annotation.md" | buildkite-agent annotate ` --context "unreal-gdk-test-artifact-location" ` --style info } + +# Upload artifacts to Buildkite, capture output to extract artifact ID in the Slack message generation +# Command format is the results of Powershell weirdness, likely related to the following: +# https://stackoverflow.com/questions/2095088/error-when-calling-3rd-party-executable-from-powershell-when-using-an-ide +$upload_output = & cmd /c 'buildkite-agent 2>&1' artifact upload "$test_result_dir\*" | Out-String + +# Artifacts are assigned an ID upon upload, so grab IDs from upload process output to build the artifact URLs +Try { + $test_results_id = (Select-String -Pattern "[^ ]* $($formatted_test_result_dir.Replace("\","\\"))\\index.html" -InputObject $upload_output -CaseSensitive).Matches[0].Value.Split(" ")[0] + $test_log_id = (Select-String -Pattern "[^ ]* $($formatted_test_result_dir.Replace("\","\\"))\\tests.log" -InputObject $upload_output -CaseSensitive).Matches[0].Value.Split(" ")[0] +} +Catch { + Write-Error "Failed to parse artifact ID from the buildkite uploading output: $upload_output" + Throw $_ +} +$test_results_url = "https://buildkite.com/organizations/$env:BUILDKITE_ORGANIZATION_SLUG/pipelines/$env:BUILDKITE_PIPELINE_SLUG/builds/$env:BUILDKITE_BUILD_ID/jobs/$env:BUILDKITE_JOB_ID/artifacts/$test_results_id" +$test_log_url = "https://buildkite.com/organizations/$env:BUILDKITE_ORGANIZATION_SLUG/pipelines/$env:BUILDKITE_PIPELINE_SLUG/builds/$env:BUILDKITE_BUILD_ID/jobs/$env:BUILDKITE_JOB_ID/artifacts/$test_log_id" + +# Read the test results +$results_path = Join-Path -Path $test_result_dir -ChildPath "index.json" +$results_json = Get-Content $results_path -Raw +$test_results_obj = ConvertFrom-Json $results_json +$tests_passed = $test_results_obj.failed -eq 0 + +# Build Slack attachment +$total_tests_succeeded = $test_results_obj.succeeded + $test_results_obj.succeededWithWarnings +$total_tests_run = $total_tests_succeeded + $test_results_obj.failed +$slack_attachment = [ordered]@{ + fallback = "Find the test results at $test_results_url" + color = $(if ($tests_passed) { "good" } else { "danger" }) + fields = @( + @{ + value = "*$env:ENGINE_COMMIT_HASH* $(Split-Path $test_result_dir -Leaf)" + short = $True + } + @{ + value = "Passed $total_tests_succeeded / $total_tests_run tests." + short = $True + } + ) + actions = @( + @{ + type = "button" + text = ":bar_chart: Test results" + url = "$test_results_url" + style = "primary" + } + @{ + type = "button" + text = ":page_with_curl: Test log" + url = "$test_log_url" + style = "primary" + } + ) +} + +$slack_attachment | ConvertTo-Json | Set-Content -Path "$test_result_dir\slack_attachment_$env:BUILDKITE_STEP_ID.json" + +buildkite-agent artifact upload "$test_result_dir\slack_attachment_$env:BUILDKITE_STEP_ID.json" + +# Count the number of SpatialGDK tests in order to report this +$num_gdk_tests = 0 +Foreach ($test in $test_results_obj.tests) { + if ($test.fulltestPath.Contains("SpatialGDK.")) { + $num_gdk_tests += 1 + } +} + +# Count the number of Project (functional) tests in order to report this +$num_project_tests = 0 +Foreach ($test in $test_results_obj.tests) { + if ($test.fulltestPath.Contains("Project.")) { + $num_project_tests += 1 + } +} + +# Define and upload test summary JSON artifact for longer-term test metric tracking (see upload-test-metrics.sh) +$test_summary = [pscustomobject]@{ + time = Get-Date -UFormat %s + build_url = "$env:BUILDKITE_BUILD_URL" + platform = "$target_platform" + unreal_engine_commit = "$env:ENGINE_COMMIT_HASH" + passed_all_tests = $tests_passed + tests_duration_seconds = $test_results_obj.totalDuration + num_tests = $total_tests_run + num_gdk_tests = $num_gdk_tests + num_project_tests = $num_project_tests + test_result_directory_name = Split-Path $test_result_dir -Leaf +} +$test_summary | ConvertTo-Json -Compress | Set-Content -Path "$test_result_dir\test_summary_$env:BUILDKITE_STEP_ID.json" + +buildkite-agent artifact upload "$test_result_dir\test_summary_$env:BUILDKITE_STEP_ID.json" + +# Fail this build if any tests failed +if (-Not $tests_passed) { + $fail_msg = "$($test_results_obj.failed) tests failed. Logs for these tests are contained in the tests.log artifact." + Write-Log $fail_msg + Throw $fail_msg +} +Write-Log "All tests passed!" diff --git a/ci/run-tests.ps1 b/ci/run-tests.ps1 index fbd91b28ff..09bd0e41d5 100644 --- a/ci/run-tests.ps1 +++ b/ci/run-tests.ps1 @@ -1,8 +1,13 @@ param( [string] $unreal_editor_path, [string] $uproject_path, - [string] $output_dir, - [string] $log_file_name + [string] $test_repo_path, + [string] $log_file_path, + [string] $test_repo_map, + [string] $report_output_path, + [string] $tests_path = "SpatialGDK", + [string] $additional_gdk_options = "", + [bool] $run_with_spatial = $False ) # This resolves a path to be absolute, without actually reading the filesystem. @@ -14,48 +19,71 @@ function Force-ResolvePath { return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path) } +if ($run_with_spatial) { + # Generate schema and snapshots + Write-Output "Generating snapshot and schema for testing project" + Start-Process "$unreal_editor_path" -Wait -PassThru -NoNewWindow -ArgumentList @(` + "$uproject_path", ` + "-NoShaderCompile", # Prevent shader compilation + "-nopause", # Close the unreal log window automatically on exit + "-nosplash", # No splash screen + "-unattended", # Disable anything requiring user feedback + "-nullRHI", # Hard to find documentation for, but seems to indicate that we want something akin to a headless (i.e. no UI / windowing) editor + "-run=GenerateSchemaAndSnapshots", # Run the commandlet + "-MapPaths=`"$test_repo_map`"" # Which maps to run the commandlet for + ) + + # Create the default snapshot + Copy-Item -Force ` + -Path "$test_repo_path\spatial\snapshots\$test_repo_map.snapshot" ` + -Destination "$test_repo_path\spatial\snapshots\default.snapshot" +} + +# Create the TestResults directory if it does not exist, for storing results +New-Item -Path "$PSScriptRoot" -Name "$report_output_path" -ItemType "directory" -ErrorAction SilentlyContinue +$output_dir = "$PSScriptRoot\$report_output_path" + # We want absolute paths since paths given to the unreal editor are interpreted as relative to the UE4Editor binary # Absolute paths are more reliable $ue_path_absolute = Force-ResolvePath $unreal_editor_path $uproject_path_absolute = Force-ResolvePath $uproject_path $output_dir_absolute = Force-ResolvePath $output_dir +$additional_gdk_options_arr = $additional_gdk_options.Split(";") +$additional_gdk_options = "" +Foreach ($additional_gdk_option in $additional_gdk_options_arr) { + if ($additional_gdk_options -ne "") { + $additional_gdk_options += "," + } + $additional_gdk_options += "[/Script/SpatialGDK.SpatialGDKSettings]:$additional_gdk_option" +} + $cmd_args_list = @( ` - "`"$uproject_path_absolute`"", ` # We need some project to run tests in, but for unit tests the exact project shouldn't matter - "-ExecCmds=`"Automation RunTests SpatialGDK; Quit`"", ` # Run all tests in the SpatialGDK group. See https://docs.unrealengine.com/en-US/Programming/Automation/index.html for docs on the automation system - "-TestExit=`"Automation Test Queue Empty`"", ` # When to close the editor - "-ReportOutputPath=`"$($output_dir_absolute)`"", ` # Output folder for test results. If it doesn't exist, gets created. If it does, all contents get deleted before new results get placed there. - "Log=`"$($log_file_name)`"", ` # Sets the name of the log file produced during this run. This file is saved in /Saved/Logs/. The lack of "-" is correct, -Log is a flag and doesn't set the file name - "-nopause", ` # Close the unreal log window automatically on exit - "-nosplash", ` # No splash screen - "-unattended", ` # Disable anything requiring user feedback - "-nullRHI" # Hard to find documentation for, but seems to indicate that we want something akin to a headless (i.e. no UI / windowing) editor + "`"$uproject_path_absolute`"", # We need some project to run tests in, but for unit tests the exact project shouldn't matter + "`"$test_repo_map`"", # The map to run tests in + "-ExecCmds=`"Automation RunTests $tests_path; Quit`"", # Run all tests. See https://docs.unrealengine.com/en-US/Programming/Automation/index.html for docs on the automation system + "-TestExit=`"Automation Test Queue Empty`"", # When to close the editor + "-ReportOutputPath=`"$($output_dir_absolute)`"", # Output folder for test results. If it doesn't exist, gets created. If it does, all contents get deleted before new results get placed there. + "-ABSLOG=`"$($log_file_path)`"", # Sets the path for the log file produced during this run. + "-nopause", # Close the unreal log window automatically on exit + "-nosplash", # No splash screen + "-unattended", # Disable anything requiring user feedback + "-nullRHI", # Hard to find documentation for, but seems to indicate that we want something akin to a headless (i.e. no UI / windowing) editor + "-stdout", # Print to output + "-ini:SpatialGDKSettings:$additional_gdk_options" # Pass changes to configuration files from above + "-OverrideSpatialNetworking=$run_with_spatial" # A parameter to switch beetween different networking implementations ) -Write-Log "Running $($ue_path_absolute) $($cmd_args_list)" - -$run_tests_proc = Start-Process -PassThru -NoNewWindow $ue_path_absolute -ArgumentList $cmd_args_list -Wait-Process -Id (Get-Process -InputObject $run_tests_proc).id +Write-Output "Running $($ue_path_absolute) $($cmd_args_list)" -# Workaround for UNR-2156, where spatiald / runtime processes sometimes never close -# Clean up any spatiald and java (i.e. runtime) processes that may not have been shut down -Stop-Process -Name "spatiald" -ErrorAction SilentlyContinue # if no process exists, just keep going -Stop-Process -Name "java" -ErrorAction SilentlyContinue # if no process exists, just keep going - -Write-Log "Exited with code: $($run_tests_proc.ExitCode)" # I can't find any indication of what the exit codes actually mean, so let's not rely on them - -## Read the test results, and pass/fail this build step -$results_path = Join-Path -Path $output_dir_absolute -ChildPath "index.json" -$results_json = Get-Content $results_path -Raw - -$results_obj = ConvertFrom-Json $results_json - -Write-Log "Test results are displayed in a nicer form in the artifacts (index.html / index.json)" - -if ($results_obj.failed -ne 0) { - $fail_msg = "$($results_obj.failed) tests failed." - Write-Log $fail_msg - Throw $fail_msg +$run_tests_proc = Start-Process $ue_path_absolute -PassThru -NoNewWindow -ArgumentList $cmd_args_list +try { + # Give the Unreal Editor 30 minutes to run the tests, otherwise kill it + # This is so we can get some logs out of it, before we are cancelled by buildkite + Wait-Process -Timeout 1800 -InputObject $run_tests_proc +} +catch { + Stop-Process -Force -InputObject $run_tests_proc # kill the dangling process + buildkite-agent artifact upload "$log_file_path" # If the tests timed out, upload the log and throw an error + throw $_ } - -Write-Log "All tests passed!" diff --git a/ci/run-tests.sh b/ci/run-tests.sh new file mode 100755 index 0000000000..05fa204796 --- /dev/null +++ b/ci/run-tests.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +set -e -u -o pipefail +if [[ -n "${DEBUG-}" ]]; then + set -x +fi + +pushd "$(dirname "$0")" + UNREAL_PATH="${1?Please enter the path to the Unreal Engine.}" + TEST_PROJECT_NAME="${2?Please enter the name of the test project.}" + UPROJECT_PATH="${3?Please enter the absolute path to the uproject path.}" + RESULTS_NAME="${4?Please enter the name of the results folder.}" + TEST_REPO_MAP="${5?Please specify which map to test.}" + TESTS_PATH="${6:-SpatialGDK}" + RUN_WITH_SPATIAL="${7:-}" + + GDK_HOME="$(pwd)/.." + BUILD_HOME="$(pwd)/../.." + TEST_REPO_PATH="${BUILD_HOME}/${TEST_PROJECT_NAME}" + REPORT_OUTPUT_PATH="${GDK_HOME}/ci/${RESULTS_NAME}" + LOG_FILE_PATH="${REPORT_OUTPUT_PATH}/tests.log" + + pushd "${UNREAL_PATH}" + UNREAL_EDITOR_PATH="Engine/Binaries/Mac/UE4Editor.app/Contents/MacOS/UE4Editor" + if [[ -n "${RUN_WITH_SPATIAL}" ]]; then + echo "Generating snapshot and schema for testing project" + "${UNREAL_EDITOR_PATH}" \ + "${UPROJECT_PATH}" \ + -NoShaderCompile \ + -nopause \ + -nosplash \ + -unattended \ + -nullRHI \ + -run=GenerateSchemaAndSnapshots \ + -MapPaths="${TEST_REPO_MAP}" + + cp "${TEST_REPO_PATH}/spatial/snapshots/${TEST_REPO_MAP}.snapshot" "${TEST_REPO_PATH}/spatial/snapshots/default.snapshot" + fi + + mkdir "${REPORT_OUTPUT_PATH}" + "${UNREAL_EDITOR_PATH}" \ + "${UPROJECT_PATH}" \ + "${TEST_REPO_MAP}" \ + -execCmds="Automation RunTests ${TESTS_PATH}; Quit" \ + -TestExit="Automation Test Queue Empty" \ + -ReportOutputPath="${REPORT_OUTPUT_PATH}" \ + -ABSLOG="${LOG_FILE_PATH}" \ + -nopause \ + -nosplash \ + -unattended \ + -nullRHI \ + -OverrideSpatialNetworking="${RUN_WITH_SPATIAL}" + popd + + # TODO: UNR-3167 - report tests +popd diff --git a/ci/setup-build-test-gdk.ps1 b/ci/setup-build-test-gdk.ps1 index 9e879279ad..88e2d43bf2 100644 --- a/ci/setup-build-test-gdk.ps1 +++ b/ci/setup-build-test-gdk.ps1 @@ -1,57 +1,165 @@ param( - [string] $gdk_home = (Get-Item "$($PSScriptRoot)").parent.FullName, ## The root of the UnrealGDK repo - [string] $gcs_publish_bucket = "io-internal-infra-unreal-artifacts-production/UnrealEngine", - [string] $msbuild_exe = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\MSBuild.exe", - [string] $target_platform = "Win64", - [string] $build_home = (Get-Item "$($PSScriptRoot)").parent.parent.FullName, ## The root of the entire build. Should ultimately resolve to "C:\b\\". - [string] $unreal_path = "$build_home\UnrealEngine", - [string] $testing_project_name = "StarterContent" ## For now, has to be inside the Engine's Samples folder + [string] $gdk_home = (Get-Item "$($PSScriptRoot)").parent.FullName, ## The root of the UnrealGDK repo + [string] $gcs_publish_bucket = "io-internal-infra-unreal-artifacts-production/UnrealEngine", + [string] $msbuild_exe = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\MSBuild.exe", + [string] $build_home = (Get-Item "$($PSScriptRoot)").parent.parent.FullName, ## The root of the entire build. Should ultimately resolve to "C:\b\\". + [string] $unreal_engine_symlink_dir = "$build_home\UnrealEngine" ) +class TestSuite { + [ValidateNotNullOrEmpty()][string]$test_repo_url + [ValidateNotNullOrEmpty()][string]$test_repo_branch + [ValidateNotNullOrEmpty()][string]$test_repo_relative_uproject_path + [ValidateNotNullOrEmpty()][string]$test_repo_map + [ValidateNotNullOrEmpty()][string]$test_project_name + [ValidateNotNullOrEmpty()][string]$test_results_dir + [ValidateNotNullOrEmpty()][string]$tests_path + [ValidateNotNull()] [string]$additional_gdk_options + [bool] $run_with_spatial + + TestSuite([string] $test_repo_url, [string] $test_repo_branch, [string] $test_repo_relative_uproject_path, [string] $test_repo_map, [string] $test_project_name, [string] $test_results_dir, [string] $tests_path, [string] $additional_gdk_options, [bool] $run_with_spatial) { + $this.test_repo_url = $test_repo_url + $this.test_repo_branch = $test_repo_branch + $this.test_repo_relative_uproject_path = $test_repo_relative_uproject_path + $this.test_repo_map = $test_repo_map + $this.test_project_name = $test_project_name + $this.test_results_dir = $test_results_dir + $this.tests_path = $tests_path + $this.additional_gdk_options = $additional_gdk_options + $this.run_with_spatial = $run_with_spatial + } +} + +[string] $test_repo_url = "git@github.com:improbable/UnrealGDKEngineNetTest.git" +[string] $test_repo_relative_uproject_path = "Game\EngineNetTest.uproject" +[string] $test_repo_map = "NetworkingMap" +[string] $test_project_name = "NetworkTestProject" +[string] $test_repo_branch = "0.9.0" +[string] $user_gdk_settings = "" + +# Allow overriding testing branch via environment variable +if (Test-Path env:TEST_REPO_BRANCH) { + $test_repo_branch = $env:TEST_REPO_BRANCH +} + +if (Test-Path env:GDK_SETTINGS) { + $user_gdk_settings = ";" + $env:GDK_SETTINGS +} + +$tests = @() + +# If building all configurations, use the test gyms, since the network testing project only compiles for the Editor configs +# There are basically two situations here: either we are trying to run tests, in which case we use EngineNetTest +# Or, we try different build targets, in which case we use UnrealGDKTestGyms +if (Test-Path env:BUILD_ALL_CONFIGURATIONS) { + $test_repo_url = "git@github.com:spatialos/UnrealGDKTestGyms.git" + $test_repo_relative_uproject_path = "Game\GDKTestGyms.uproject" + $test_repo_map = "EmptyGym" + $test_project_name = "GDKTestGyms" + + $tests += [TestSuite]::new("$test_repo_url", "$test_repo_branch", "$test_repo_relative_uproject_path", "$test_repo_map", "$test_project_name", "TestResults", "SpatialGDK", "bEnableUnrealLoadBalancer=false$user_gdk_settings", $True) +} +else{ + if ((Test-Path env:TEST_CONFIG) -And ($env:TEST_CONFIG -eq "Native")) { + $tests += [TestSuite]::new("$test_repo_url", "$test_repo_branch", "$test_repo_relative_uproject_path", "$test_repo_map", "$test_project_name", "VanillaTestResults", "/Game/SpatialNetworkingMap", "$user_gdk_settings", $False) + } + else { + $tests += [TestSuite]::new("$test_repo_url", "$test_repo_branch", "$test_repo_relative_uproject_path", "$test_repo_map", "$test_project_name", "TestResults", "SpatialGDK+/Game/SpatialNetworkingMap", "$user_gdk_settings", $True) + # enable load-balancing once the tests pass reliably and the testing repo is updated + # $tests += [TestSuite]::new("$test_repo_url", "$test_repo_branch", "$test_repo_relative_uproject_path", "$test_repo_map", "$test_project_name", "LoadbalancerTestResults", "/Game/Spatial_ZoningMap_1S_2C", "bEnableUnrealLoadBalancer=true;LoadBalancingWorkerType=(WorkerTypeName=`"UnrealWorker`")$user_gdk_settings", $True) + } + + if ($env:SLOW_NETWORKING_TESTS) { + $tests[0].tests_path += "+/Game/NetworkingMap" + $tests[0].test_results_dir = "Slow" + $tests[0].test_results_dir + } +} + . "$PSScriptRoot\common.ps1" -Start-Event "cleanup-symlinks" "command" -&$PSScriptRoot"\cleanup.ps1" -unreal_path "$unreal_path" -Finish-Event "cleanup-symlinks" "command" +# Guard against other runs not cleaning up after themselves +Foreach ($test in $tests) { + $test_project_name = $test.test_project_name + & $PSScriptRoot"\cleanup.ps1" ` + -project_name "$test_project_name" +} # Download Unreal Engine Start-Event "get-unreal-engine" "command" -&$PSScriptRoot"\get-engine.ps1" -unreal_path "$unreal_path" +& $PSScriptRoot"\get-engine.ps1" -unreal_path "$unreal_engine_symlink_dir" Finish-Event "get-unreal-engine" "command" -Start-Event "symlink-gdk" "command" -$gdk_in_engine = "$unreal_path\Engine\Plugins\UnrealGDK" -New-Item -ItemType Junction -Name "UnrealGDK" -Path "$unreal_path\Engine\Plugins" -Target "$gdk_home" -Finish-Event "symlink-gdk" "command" - # Run the required setup steps Start-Event "setup-gdk" "command" -&$PSScriptRoot"\setup-gdk.ps1" -unreal_path $unreal_path -gdk_path "$gdk_in_engine" +& $PSScriptRoot"\setup-gdk.ps1" -gdk_path "$gdk_in_engine" -msbuild_path "$msbuild_exe" Finish-Event "setup-gdk" "command" -# Build the GDK plugin -Start-Event "build-gdk" "command" -&$PSScriptRoot"\build-gdk.ps1" -target_platform $($target_platform) -build_output_dir "$build_home\SpatialGDKBuild" -unreal_path $unreal_path -Finish-Event "build-gdk" "command" - -# Only run tests on Windows, as we do not have a linux agent - should not matter -if ($target_platform -eq "Win64") { - Start-Event "setup-tests" "command" - &$PSScriptRoot"\setup-tests.ps1" -build_output_dir "$build_home\SpatialGDKBuild" -project_path "$unreal_path\Samples\$testing_project_name" -unreal_path $unreal_path - Finish-Event "setup-tests" "command" - - Start-Event "test-gdk" "command" - Try{ - &$PSScriptRoot"\run-tests.ps1" -unreal_editor_path "$unreal_path\Engine\Binaries\$target_platform\UE4Editor.exe" -uproject_path "$unreal_path\Samples\$testing_project_name\$testing_project_name.uproject" -output_dir "$PSScriptRoot\TestResults" -log_file_name "tests.log" - } - Catch { - Throw $_ - } - Finally { - Finish-Event "test-gdk" "command" - - Start-Event "report-tests" "command" - &$PSScriptRoot"\report-tests.ps1" -test_result_dir "$PSScriptRoot\TestResults" - Finish-Event "report-tests" "command" - } +class CachedProject { + [ValidateNotNullOrEmpty()][string]$test_repo_url + [ValidateNotNullOrEmpty()][string]$test_repo_branch + + CachedProject([string] $test_repo_url, [string] $test_repo_branch) { + $this.test_repo_url = $test_repo_url + $this.test_repo_branch = $test_repo_branch + } +} + +$projects_cached = @() + +Foreach ($test in $tests) { + $test_repo_url = $test.test_repo_url + $test_repo_branch = $test.test_repo_branch + $test_repo_relative_uproject_path = $test.test_repo_relative_uproject_path + $test_repo_map = $test.test_repo_map + $test_project_name = $test.test_project_name + $test_results_dir = $test.test_results_dir + $tests_path = $test.tests_path + $additional_gdk_options = $test.additional_gdk_options + $run_with_spatial = $test.run_with_spatial + + $project_is_cached = $False + Foreach ($cached_project in $projects_cached) { + if (($test_repo_url -eq $cached_project.test_repo_url) -and ($test_repo_branch -eq $cached_project.test_repo_branch)) { + $project_is_cached = $True + } + } + + if (-Not $project_is_cached) { + # Build the testing project + Start-Event "build-project" "command" + & $PSScriptRoot"\build-project.ps1" ` + -unreal_path "$unreal_engine_symlink_dir" ` + -test_repo_branch "$test_repo_branch" ` + -test_repo_url "$test_repo_url" ` + -test_repo_uproject_path "$build_home\$test_project_name\$test_repo_relative_uproject_path" ` + -test_repo_path "$build_home\$test_project_name" ` + -msbuild_exe "$msbuild_exe" ` + -gdk_home "$gdk_home" ` + -build_platform "$env:BUILD_PLATFORM" ` + -build_state "$env:BUILD_STATE" ` + -build_target "$env:BUILD_TARGET" + + $projects_cached += [CachedProject]::new($test_repo_url, $test_repo_branch) + Finish-Event "build-project" "command" + } + + # Only run tests on Windows, as we do not have a linux agent - should not matter + if ($env:BUILD_PLATFORM -eq "Win64" -And $env:BUILD_TARGET -eq "Editor" -And $env:BUILD_STATE -eq "Development") { + Start-Event "test-gdk" "command" + & $PSScriptRoot"\run-tests.ps1" ` + -unreal_editor_path "$unreal_engine_symlink_dir\Engine\Binaries\Win64\UE4Editor.exe" ` + -uproject_path "$build_home\$test_project_name\$test_repo_relative_uproject_path" ` + -test_repo_path "$build_home\$test_project_name" ` + -log_file_path "$PSScriptRoot\$test_project_name\$test_results_dir\tests.log" ` + -report_output_path "$test_project_name\$test_results_dir" ` + -test_repo_map "$test_repo_map" ` + -tests_path "$tests_path" ` + -additional_gdk_options "$additional_gdk_options" ` + -run_with_spatial $run_with_spatial + Finish-Event "test-gdk" "command" + + Start-Event "report-tests" "command" + & $PSScriptRoot"\report-tests.ps1" -test_result_dir "$PSScriptRoot\$test_project_name\$test_results_dir" -target_platform "$env:BUILD_PLATFORM" + Finish-Event "report-tests" "command" + } } diff --git a/ci/setup-build-test-gdk.sh b/ci/setup-build-test-gdk.sh new file mode 100755 index 0000000000..ab4da0e997 --- /dev/null +++ b/ci/setup-build-test-gdk.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +set -e -u -o pipefail +if [[ -n "${DEBUG-}" ]]; then + set -x +fi + +source /opt/improbable/environment + +pushd "$(dirname "$0")" + GDK_HOME="${1:-"$(pwd)/.."}" + GCS_PUBLISH_BUCKET="${2:-io-internal-infra-unreal-artifacts-production/UnrealEngine}" + BUILD_HOME="${3:-"$(pwd)/../.."}" + + UNREAL_PATH="${BUILD_HOME}/UnrealEngine" + TEST_UPROJECT_NAME="EngineNetTest" + TEST_REPO_URL="git@github.com:improbable/UnrealGDKEngineNetTest.git" + TEST_REPO_MAP="NetworkingMap" + TEST_PROJECT_NAME="NetworkTestProject" + CHOSEN_TEST_REPO_BRANCH="${TEST_REPO_BRANCH:-master}" + SLOW_NETWORKING_TESTS=false + + # Download Unreal Engine + echo "--- get-unreal-engine" + "${GDK_HOME}/ci/get-engine.sh" \ + "${UNREAL_PATH}" \ + "${GCS_PUBLISH_BUCKET}" + + # Run the required setup steps + echo "--- setup-gdk" + "${GDK_HOME}/Setup.sh" --mobile + + # Build the testing project + UPROJECT_PATH="${BUILD_HOME}/${TEST_PROJECT_NAME}/Game/${TEST_UPROJECT_NAME}.uproject" + + echo "--- build-project" + "${GDK_HOME}"/ci/build-project.sh \ + "${UNREAL_PATH}" \ + "${CHOSEN_TEST_REPO_BRANCH}" \ + "${TEST_REPO_URL}" \ + "${UPROJECT_PATH}" \ + "${BUILD_HOME}/${TEST_PROJECT_NAME}" \ + "${GDK_HOME}" \ + "${BUILD_PLATFORM}" \ + "${BUILD_STATE}" \ + "${TEST_UPROJECT_NAME}${BUILD_TARGET}" + + # TODO UNR-3164 - re-enable tests after we made sure they work for Mac + # echo "--- run-fast-tests" + # "${GDK_HOME}/ci/run-tests.sh" \ + # "${UNREAL_PATH}" \ + # "${TEST_PROJECT_NAME}" \ + # "${UPROJECT_PATH}" \ + # "FastTestResults" \ + # "NetworkingMap" \ + # "SpatialGDK+/Game/SpatialNetworkingMap" \ + # "True" + + # if [[ -n "${SLOW_NETWORKING_TESTS}" ]]; then + # echo "--- run-slow-networking-tests" + # "${GDK_HOME}/ci/run-tests.sh" \ + # "${UNREAL_PATH}" \ + # "${TEST_PROJECT_NAME}" \ + # "${UPROJECT_PATH}" \ + # "VanillaTestResults" \ + # "NetworkingMap" \ + # "+/Game/NetworkingMap" \ + # "" + # fi +popd diff --git a/ci/setup-gdk.ps1 b/ci/setup-gdk.ps1 index 7d29fbd87c..0d2a787e5b 100644 --- a/ci/setup-gdk.ps1 +++ b/ci/setup-gdk.ps1 @@ -1,13 +1,19 @@ # Expects gdk_home, which is not the GDK location in the engine +# This script is used directly as part of the UnrealGDKExampleProject CI, so providing default values may be strictly necessary param ( [string] $gdk_path = "$gdk_home", - [string] $msbuild_path = "$((Get-Item 'Env:programfiles(x86)').Value)\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\MSBuild.exe" ## Location of MSBuild.exe on the build agent, as it only has the build tools, not the full visual studio + [string] $msbuild_path = "$((Get-Item 'Env:programfiles(x86)').Value)\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\MSBuild.exe", ## Location of MSBuild.exe on the build agent, as it only has the build tools, not the full visual studio + [switch] $includeTraceLibs ) -pushd $gdk_path +Push-Location $gdk_path if (-Not (Test-Path env:NO_PAUSE)) { # seems like this is set somewhere previously in CI, but just to make sure $env:NO_PAUSE = 1 } $env:MSBUILD_EXE = "`"$msbuild_path`"" - cmd /c Setup.bat -popd + if($includeTraceLibs) { + cmd /c SetupIncTraceLibs.bat --mobile + } else { + cmd /c Setup.bat --mobile + } +Pop-Location diff --git a/ci/setup-tests.ps1 b/ci/setup-tests.ps1 deleted file mode 100644 index 43e1dcd350..0000000000 --- a/ci/setup-tests.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# Expects gdk_home -param( - [string] $build_output_dir, - [string] $project_path, - [string] $unreal_path = "$((Get-Item `"$($PSScriptRoot)`").parent.parent.FullName)\UnrealEngine" ## This should ultimately resolve to "C:\b\\UnrealEngine". -) - -# Copy the built files back into the SpatialGDK folder, to have a complete plugin -# The trailing \ on the destination path is important! -Copy-Item -Path "$build_output_dir\*" -Destination "$gdk_home\SpatialGDK\" -Recurse -Container -ErrorAction SilentlyContinue - -# The Plugin does not get recognised as an Engine plugin, because we are using a pre-built version of the engine -# copying the plugin into the project's folder bypasses the issue -New-Item -Path "$project_path" -Name "Plugins" -ItemType "directory" -ErrorAction SilentlyContinue -New-Item -ItemType Junction -Name "UnrealGDK" -Path "$project_path\Plugins" -Target "$gdk_home" - -# Create the TestResults directory if it does not exist, for storing results -New-Item -Path "$PSScriptRoot" -Name "TestResults" -ItemType "directory" -ErrorAction SilentlyContinue - -# Disable tutorials, otherwise the closing of the window will crash the editor due to some graphic context reason -Add-Content -Path "$unreal_path\Engine\Config\BaseEditorSettings.ini" -Value "`r`n[/Script/IntroTutorials.TutorialStateSettings]`r`nTutorialsProgress=(Tutorial=/Engine/Tutorial/Basics/LevelEditorAttract.LevelEditorAttract_C,CurrentStage=0,bUserDismissed=True)`r`n" -# TODO: To be fixed with a dedicated testing project instead of StarterContent - maybe UNR-2047 -Add-Content -Path "$project_path\Config\DefaultGame.ini" -Value "`r`n[/Script/EngineSettings.GeneralProjectSettings]`r`nbSpatialNetworking=True`r`n" diff --git a/ci/unreal-engine.version b/ci/unreal-engine.version index afdcfe0dd2..4592fb509b 100644 --- a/ci/unreal-engine.version +++ b/ci/unreal-engine.version @@ -1,2 +1,2 @@ -UnrealEngine-21c4d81d3bc5db3eab66070d9b6f6c905c60299f -UnrealEngine-f6a7b93f227ddc5e9fa9aa84d0dd16ac341f36ee +UnrealEngine-6333a45a65e6503baedd45fb684abced5bb413c3 +UnrealEngine-7e12afba8575bf84f7a39df239837af040c31c81 \ No newline at end of file diff --git a/ci/upload-test-metrics.sh b/ci/upload-test-metrics.sh new file mode 100755 index 0000000000..a34c81aade --- /dev/null +++ b/ci/upload-test-metrics.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -euo pipefail + +# Fetch the test summary artifacts uploaded earlier +mkdir "./test_summaries" +buildkite-agent artifact download "*test_summary*.json" "./test_summaries" + +# Define target upload location +PROJECT="holocentroid-aimful-6523579" +DATASET="UnrealGDK" +TABLE="ci_metrics" + +# Make sure that the gcp secret is always removed +GCP_SECRET="$(mktemp)" +function cleanup { + rm -rf "${GCP_SECRET}" +} +trap cleanup EXIT + +# Fetch Google credentials so that we can upload the metrics to the GCS bucket. +imp-ci secrets read --environment=production --buildkite-org=improbable \ + --secret-type=gce-key-pair --secret-name=qa-unreal-gce-service-account \ + --write-to=${GCP_SECRET} +gcloud auth activate-service-account --key-file "${GCP_SECRET}" + +# Upload metrics +for json_file in ./test_summaries/*.json; do + cat "${json_file}" | bq --project_id "${PROJECT}" --dataset_id "${DATASET}" insert "${TABLE}" +done