diff --git a/.buildkite/premerge.definition.yaml b/.buildkite/premerge.definition.yaml index efe1a2c4b7..4cc85ded0e 100755 --- a/.buildkite/premerge.definition.yaml +++ b/.buildkite/premerge.definition.yaml @@ -7,5 +7,5 @@ github: teams: - name: Everyone permission: BUILD_AND_READ -- name: gbu/develop/unreal +- name: gen/team/unreal permission: MANAGE_BUILD_AND_READ \ No newline at end of file diff --git a/.buildkite/premerge.steps.yaml b/.buildkite/premerge.steps.yaml index 6bb2a0a29d..1da9bc554d 100755 --- a/.buildkite/premerge.steps.yaml +++ b/.buildkite/premerge.steps.yaml @@ -34,7 +34,7 @@ windows: &windows - "platform=windows" - "permission_set=builder" - "scaler_version=2" - - "queue=${CI_WINDOWS_BUILDER_QUEUE:-v4-20-03-26-102432-bk9951-8afe0ffb}" + - "queue=${CI_WINDOWS_BUILDER_QUEUE:-v4-20-11-18-224740-bk17641-0c4125be-d}" retry: automatic: - <<: *agent_transients @@ -42,7 +42,7 @@ windows: &windows - <<: *bk_interrupted_by_signal timeout_in_minutes: 60 plugins: - - ca-johnson/taskkill#v4.1: ~ + - improbable-eng/taskkill#v4.4.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. diff --git a/.buildkite/release.definition.yaml b/.buildkite/release.definition.yaml index 59478f7bc2..7b08fd1ac1 100644 --- a/.buildkite/release.definition.yaml +++ b/.buildkite/release.definition.yaml @@ -7,5 +7,5 @@ github: teams: - name: Everyone permission: READ_ONLY -- name: gbu/develop/unreal +- name: gen/team/unreal permission: MANAGE_BUILD_AND_READ diff --git a/.buildkite/release.steps.yaml b/.buildkite/release.steps.yaml index cb6d89d391..b4b00dcaf2 100644 --- a/.buildkite/release.steps.yaml +++ b/.buildkite/release.steps.yaml @@ -35,7 +35,17 @@ steps: key: "engine-source-branches" required: true hint: "The Unreal Engine branch (or branches) that you want to create release candidates from. Put each branch on a separate line with the primary Engine version at the top." - default: "4.24-SpatialOSUnrealGDK\n4.23-SpatialOSUnrealGDK" + default: "4.26-SpatialOSUnrealGDK\n4.25-SpatialOSUnrealGDK\n4.24-SpatialOSUnrealGDK" + + - select: "Actual release or dry-run?" + key: "dry-run" + required: true + default: "true" + options: + - label: "Dry-run release" + value: "true" + - label: "Full actual release" + value: "false" # Stage 1: Prepare the release candidates. Prepare steps create a PR and upload metadata but do not release anything. - label: "Prepare the release" @@ -47,9 +57,9 @@ steps: concurrency: 1 concurrency_group: "unrealgdk-release" - - wait - -# Stage 2: Builds all UnrealEngine release candidates, compresses and uploads Engine artifacts to Google Cloud Storage for use by test pipelines. + - wait: ~ + + # Stage 2: Builds all UnrealEngine release candidates, compresses and uploads Engine artifacts to Google Cloud Storage for use by test pipelines. - label: "Build & upload all UnrealEngine release candidates" command: ci/generate-unrealengine-premerge-trigger.sh | tee /dev/fd/2 | buildkite-agent pipeline upload <<: *common # This folds the YAML named anchor into this step. Overrides, if any, should follow, not precede. @@ -61,8 +71,7 @@ steps: #TODO: This step is actually not strictly necessary. It will be removed as part of: UNR-3662 skip: true - # Stage 3: Run all tests against the release candidates. - # Block steps require a human to click a button. This + # Stage 3: Run all tests against the release candidates. - block: "Run all tests" prompt: "This action triggers all tests. Tests depend on the presence of unrealengine-premerge artifacts in Google Cloud Storage. Only click OK if the above unrealengine-premerge build(s) have passed." @@ -74,11 +83,23 @@ steps: permit_on_passed: true concurrency: 1 concurrency_group: "unrealgdk-release" - - - wait - # Stage 4: Promote the release candiates to their respective release branches. + - wait: ~ + + # Stage 4: Prepare the release candidates for the full release, updating references to the post-release state. + - block: "Prepare release candidate branches for full release" + prompt: "This will prepare all the release candidate branches with updated release-like references in preparation for the full release, which should be soon afterwards." + - label: "Prepare release canidadate branches for full release" + command: ci/prepare-full-release.sh + <<: *common # This folds the YAML named anchor into this step. Overrides, if any, should follow, not precede. + retry: + manual: + permit_on_passed: true + concurrency: 1 + concurrency_group: "unrealgdk-release" + + # Stage 5: Promote the release candiates to their respective release branches. # Block steps require a human to click a button, this is a safety precaution. - block: "Unblock the release" prompt: "This action will merge all release candidates into their respective release branches." @@ -89,16 +110,13 @@ steps: concurrency_group: "unrealgdk-release" <<: *common # This folds the YAML named anchor into this step. Overrides, if any, should follow, not precede. - # Stage 5: Mirror the release code from Github to Gitee + # Stage 6: Mirror the release code from Github to Gitee - block: "Unblock mirror code to gitee" prompt: "This action will mirror all of the release code to gitee." - trigger: platform-copybara label: "Run platform-copybara" - depends_on: Release - build: - message: "Triggered from UnrealGDK Release" - commit: "HEAD" - branch: "master" - concurrency: 1 - concurrency_group: "unrealgdk-release" + build: + message: "Triggered from UnrealGDK Release" + commit: "HEAD" + branch: "master" diff --git a/.gitattributes b/.gitattributes index e7f35426d5..6eabb37f78 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,8 +1,9 @@ * text=auto *.go text eol=lf -# Bat files should not be normalized +# Bat files and version files should not be normalized *.bat -text +*.version -text # Don't count vendored go code as part of language stats. go/src/improbable.io/vendor/* linguist-vendored diff --git a/CHANGELOG.md b/CHANGELOG.md index d4f9508cbb..eb248bf41a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,126 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [`x.y.z`] - Unreleased +## [`0.12.0`] - 2021-02-01 + +### Breaking changes: +- We no longer support Server Travel. This feature is being re-designed and will be reintroduced in a future version. +- The condition for sending Spatial position updates has been changed, the two variables `PositionUpdateFrequency` and `PositionDistanceThreshold` have now been removed from the GDK settings. To update your project: + 1. Set the value of `PositionUpdateLowerThresholdCentimeters` to the value of `PositionDistanceThreshold` and the value of `PositionUpdateLowerThresholdSeconds` to 60*(1/`PositionUpdateFrequency`). This makes sure that Actors send Spatial position updates as often as they did before this change. + 2. Set the value of `PositionUpdateThresholdMaxCentimeters` and `PositionUpdateThresholdMaxSeconds` to larger values than the lower thresholds. + NOTE: If your project does not use custom values for the `PositionUpdateFrequency` or `PositionDistanceThreshold` then, by default, the updates will be sent with the same frequency as before and no action is required. +- Removed the `OnAuthorityLossImminent` Actor event. +- 'WorkerLogLevel' in Runtime Settings is split into two new settings - 'LocalWorkerLogLevel' and 'CloudWorkerLogLevel'. Update these values which will be set to 'Warning' by default. +- The Unreal GDK has been updated to run against SpatialOS 15.0. Older version of SpatialOS will no longer work with the Unreal GDK. +- In SpatialWorkerFlags.h some renaming took place around using delegates on flag changes. +These functions and structs can be referenced in both code and blueprints and it may require updating both: + 1. Delegate struct `FOnWorkerFlagsUpdatedBP` has been renamed `FOnAnyWorkerFlagUpdatedBP`, + 2. Delegate struct `FOnWorkerFlagsUpdated` has been renamed `FOnAnyWorkerFlagUpdated`, + 3. Function `BindToOnWorkerFlagsUpdated` has been renamed to `RegisterAnyFlagUpdatedCallback` + 4. Function `UnbindFromOnWorkerFlagsUpdated` has been renamed to `UnregisterAnyFlagUpdatedCallback` +- Worker configurations must now be stored in the launch config for local deployments. These can be added in the SpatialGDKEditorSettings as before. +- Spot and SpatialD (Spatial Service) dependencies have been removed. +- Compatibility Mode runtime is no longer supported. +- Running without Ring Buffered RPCs is no longer supported, and the option has been removed from SpatialGDKSettings. +- The schema database format has been updated and versioning introduced. Please regenerate your schema after updating. +- The CookAndGenerateSchemaCommandlet no longer automatically deletes previously generated schema. Deletion of previously generated schema is now controlled by the `-DeleteExistingGeneratedSchema` flag. + +### Features: +- The DeploymentLauncher tool can be used to start multiple simulated player deployments at once. +- Schema bundles are generated by default (in addition to the schema descriptor). Specifying a Schema bundle location via `-AdditionalSchemaCompilerArguments="--bundle_out=..."` will result in the bundle being written to both the default location (`build/assembly/schema/schema.sb`) _and_ the location specified by the additional arguments. +- Spatial Position Updates are sent based on a different logic. Previously, a position update was sent to Spatial if enough time had passed since the Actor's last update(computed using the SpatialUpdateFrequency) AND if the Actor had moved more than `PositionDistanceThreshold` centimeters. This change allows for Spatial position updates to be sent if any of the following is true: + 1. The time elapsed since the last sent Spatial position update is greater than or equal to `PositionUpdateLowerThresholdSeconds` AND the distance travelled since the last update is greater than or equal to `PositionUpdateLowerThresholdCentimeters`. + 1. The time elapsed since the last sent Spatial position update is greater than or equal to `PositionUpdateThresholdMaxSeconds` AND the Actor has moved a non-zero amount. + 1. The distance travelled since the last Spatial position update was sent is greater than or equal to `PositionUpdateThresholdMaxCentimeters`. +- New setting "Auto-stop local SpatialOS deployment" allows you to specify Never (doesn't automatically stop), OnEndPIE (when a PIE session is ended) and OnExitEditor (only when the editor is shutdown). The default is OnExitEditor. +- Added `OnActorReady` bindable callback triggered when SpatialOS entity data is first received after creating an entity corresponding to an Actor. This event indicates you can safely refer to the entity without risk of inconsistent state after worker crashes or snapshot restarts. +- Added support for the main build target having `TargetType.Client` (`.Target.cs`). This target is automatically built with arguments `-client -noserver` passed to UAT when building from the editor. If you use the GDK build script or executable manually, you need to pass `-client -noserver` when building this target (for example, `BuildWorker.bat GDKShooter Win64 Development GDKShooter.uproject -client -noserver`). +- Added ability to specify `USpatialMultiWorkerSettings` class from command line. Specify a `SoftClassPath` via `-OverrideMultiWorkerSettingsClass=MultiWorkerSettingsClassName`. +- You can override the load balancing strategy in-editor so that it is different from the cloud. Set `Editor Multi Worker Settings Class` in the `World Settings` to specify the in-editor load balancing strategy. If it is not specified, the existing `Multi Worker Settings Class` defines both the local and cloud load balancing strategy. +- Added `BroadcastNetworkFailure` with type `OutdatedClient` on client schema hash mismatch with server. Add your own callback to `GEngine->NetworkFailureEvent` to add custom behaviour for outdated clients attempting to join. +- You can see the Spatial Debugger in-editor mode similar to the one you see in play mode. Select `Spatial Editor Debugger` from the drop-down menu on the `Start Deployment` button on the toolbar to toggle the visibility of the worker boundaries on and off in-editor. +- Enabled the `bUseSpatialView` property by default and added the `--OverrideUseSpatialView` flag. +- Added facilities to manipulate worker interest and Actor authority in SpatialFunctionalTest. +- Allow specifying the locator port via `?locatorPort=` URL option when performing client travel. +- You can now enable/disable the multi-worker load balancing strategy with an in-editor toggle so that no Uasset files are changed. Select `Enable Multi-Worker` from the drop-down menu on the `Start Deployment` button on the toolbar to use the multi-worker strategy or de-select to use a single worker strategy in the editor. The `Enable Multi-Worker` toggle in World Settings and the command line option `-OverrideMultiWorker` have been removed as they are now redundant. +- Enabled packaging the command line arguments when building a mobile client by default. +- Added settings for the positioning and opacity of the Spatial Debugger worker region visualisation. +- You can now configure what the Spatial Debugger visualises in an in-game menu. Use F9 (by default) to open and close it. The key can be changed through a setting on the Spatial Debugger object. +- Added a setting for the Spatial Debugger to visualise all replicated actors in the local player's hierarchy, instead of just the player's controller, player state and pawn. +- You can now see worker information displayed on the worker's boundaries. The worker name and virtual worker ID displayed corresponds to the worker that you are currently looking at and will be visible when you are near a border. +- You can now filter logs for Local and Cloud deployments separately with Editor settings. The 'WorkerLogLevel' GDK setting was removed and has been replaced by 'LocalWorkerLogLevel' and 'CloudWorkerLogLevel'. +- You can now disable logging to SpatialOS for local and/or cloud deployments from the GUI (Project Settings -> Runtime Settings -> Logging). The command line argument -NoLogToSpatial can still be used for that as well. +- Servers now log a warning message when detecting a client has timed out. +- Handover is now optional depending on whether the load balancing strategy implementations require it . See `RequiresHandoverData` +- Improved the failed hierarchy migration logs. The logs are now more specific, the frequency of repeated logs is suppressed and cross-server migration diagnostic logs have been added. +- You can now select an actor for SpatialOS debugging in-game. Use F9 (by default) to open the Spatial Debugger in-game config menu and then press the `Start Select Actor(s)` button. Hover over an actor with the mouse to highlight and right-click (by default) to select. You can select multiple actors. To deselect an actor right-click on it a second time. If there are multiple actors under the cursor use the mouse wheel (by default) to highlight the desired actor then right-click to confirm your selection. +- SpatialWorldSettings is now the default world settings in supported engine versions. +- Worker SDK version compatibility is checked at compile time. +- SpatialWorkerFlags has reworked how to add callbacks for flag updates: + 1. `BindToOnWorkerFlagsUpdated` is changed to `RegisterAnyFlagUpdatedCallback` to better differentiate it from the newly added functions for register callbacks. + 2. `RegisterFlagUpdatedCallback` is added to register callbacks for individual flag updates + 3. `RegisterAndInvokeAnyFlagUpdatedCallback` & `RegisterAndInvokeFlagUpdatedCallback` variants are added that will invoke the callback if the flag was previously set. +- Overhauled local deployment workflows. Significantly faster local deployment start times (0.1s~ vs 7s~). + - Switched to using binary (standalone) versions of the Runtime and Inspector which allow us to start deployments much faster. + - Runtime and Inspector binary fetching is now handled by the GDK. To fetch these binaries you must start the game project at least once. + - The Runtime and Inspector will be restarted between each Editor session. + - Support for standalone launch configurations introduced, these are now generated by default for your local deployments, worker configurations for local deployments are now stored inside the launch config. + - Generated local launch configs can still be found in `\Game\Intermediate\Improbable\_LocalLaunchConfig.json`. + - Cloud deployments still use the older launch config and worker config format, we still generate these automatically for your maps. + - Generated cloud launch configs can be found in `\Game\Intermediate\Improbable\_CloudLaunchConfig.json`. + - The Launch Configuration Generator tool can be used to generate either local or cloud launch configs via a checkbox in the GUI. + - Compatibility Mode Runtime is no longer supported. + - Local deployment logs timestamp format has changed to `yyyy.mm.dd-hh.mm.ss`. Example log path: `\spatial\logs\localdeployment\2020.12.02-14.13.39` + - Launch.log no longer exists in the localdeployment logs, the equivalent logs can be found in the runtime.log + - Local deployments are now restarted by default when using PIE. We recommend this setting for all your projects. + - Snapshots taken of running local deployments are saved in `\spatial\snapshots\\snapshot_.snapshot` + - Inspector URL is now http://localhost:33333/inspector-v2 + - Inspector version can be overridden in the SpatialGDKEditorSettings under `Inspector Version Override` +- The SpatialNetDriver can disconnect a client worker when given the system entity ID for that client and does so when `GameMode::PreLogin` returns with a non-empty error message. +- Unreal Engine version 4.26.0 is supported! Refer to https://documentation.improbable.io/gdk-for-unreal/docs/keep-your-gdk-up-to-date for versioning information and how to upgrade. +- Running with an out-of-date schema database reports a version warning when attempting to launch in editor. +- Reworked schema generation (incremental and full) pop-ups to be clearer. + +### Bug fixes: +- Fixed a bug that stopped the travel URL being used for initial Spatial connection if the command line arguments could not be used. +- Added the `Handover` tag to `APlayerController::LastSpectatorSyncLocation` and `APlayerController::LastSpectatorSyncRotation` in order to fix a character spawning issue for players starting in the `Spectating` state when using zoning. +- No longer AddOwnerInterestToServer unless the owner is replicating, otherwise this warning fires erroneously: "Interest for Actor is out of date because owner does not have an entity id." +- Properly handle pairs of Add/Remove component in critical section. The issue manifested in the form of remnant actors which the worker should have lost interest in. +- Made the DeploymentLauncher stop multiple deployments in parallel to improve performance when working with large numbers of deployments. +- Fixed an issue where possessing a new pawn and immediately setting the owner of the old pawn to the controller could cause server RPCs from that pawn to be dropped. +- Added support for the `bHidden` relevancy flag. Clients will not checkout Actors that have `bHidden` set to true (unless they are always relevant or the root component has collisions enabled). +- Fixed an issue with deployments failing due to the incorrect number of workers when the launch config was specified, rather than automatically generated. +- Fixed the `too many dynamic subobjects` error on Clients appearing when a Startup Actor, with one dynamic subobject was leaving and re-entering interest multiple times. Added the `RemovedDynamicSubobjectObjectRefs` map in `USpatialPackageMapClient` that keeps the dynamic subobjects removed from Startup Actor's client's interest to avoid duplication of the dynamic subojects when the Startup Actor re-enters the Client's interest. +- Fixed an issue that prevented the Interest component from being initialized properly when provided with `Worker_ComponentData`. +- Cleaned up startup logs of a few noisy messages. +- Fixed a crash that sometimes occurred upon trying to resolve a pointer to an object that has been unloaded. +- Fixed a crash when spawn requests are forwarded but the `APlayerStart` actor is not resolvable on the target worker. +- By default, only an Actor's replicated owner hierarchy is used when determining which worker should have authority over an actor. Non-replicated Actors are now ignored. +- Fixed a crash that would sometimes occur when connection to SpatialOS fails. +- Fixed a crash that occurred when an actor subobject became invalid after applying initial component data. +- Non-replicated Actors net roles are not touched during startup. +- Fixed a bug which dropped component updates on authority delegation. +- The DeploymentLauncher checks the validity of the simulated players deployment name. +- Worker configuration watcher only rebuilds worker configs when `*.worker.json` files are changed. +- Added support for FPredictionKey's conditional replication logic. GameplayCues now activate on all clients, instead of only the client that initiated them. +- Fixed a bug where deployment would fail in the presence of trailing spaces in the `Flags` and `LegacyFlags` fields of the `SpatialGDKEditorSettings`. +- Fixed a crash that would occur when performing multiple Client Travels at once. +- Fixed an issue where bReplicates would not be handed over correctly when dynamically set. +- Add ServerOnlyAlwaysRelevant component and component set schema definitions +- Fixed a snapshot reloading issue where worker would create extra actors, as if they were loading on a fresh deployment. +- Server workers use TCP (instead of KCP) by default. +- Fixed a rare crash where a RepNotify callback can modify a GDK data structure being iterated upon. +- Fixed race condition in Spatial Test framework that would cause tests to time out with one or more workers not ready to begin the test. +- Fixed client connection not being cleaned up when moving out of interest of a server. +- Fixed an assertion being triggered on async loaded entities due to queuing some component addition. +- Fixed a bug where consecutive invocations of CookAndGenerateSchemaCommandlet for different levels could fail when running the schema compiler. +- Fixed a crash that occured when opening the session frontend with VS 16.8.0 using the bundled dbghelp.dll. +- Spatial Debugger no longer consumes input. +- Fixed an issue in the SpatialTestCharacterMigration test where trigger boxes sometimes wouldn't trigger at low framerates. +- Spatial bundles no longer requested at startup if `UGeneralProjectSettings::bSpatialNetworking` is disabled. +- Fixed an issue where heartbeats could be ran on a controller after its destruction +- Fixed an issue that led to the launch config being left in non-classic style with certain engine and project path configurations + ## [`0.11.0`] - 2020-09-03 ### Breaking changes: diff --git a/RequireSetup b/RequireSetup index 5f63e5bd9d..77d9c83a1b 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. -61 +74 diff --git a/Setup.bat b/Setup.bat index 869d3dbfd7..4ae9b2bb44 100644 --- a/Setup.bat +++ b/Setup.bat @@ -9,7 +9,10 @@ pushd "%~dp0" call :MarkStartOfBlock "%~0" call :MarkStartOfBlock "Setup the git hooks" - if not exist .git\hooks goto SkipGitHooks + if not exist .git\hooks ( + echo ".git\hooks not found: skipping git hook setup" + goto SkipGitHooks + ) rem Remove the old post-checkout hook. if exist .git\hooks\post-checkout del .git\hooks\post-checkout @@ -17,13 +20,15 @@ call :MarkStartOfBlock "Setup the git hooks" rem Remove the old post-merge hook. if exist .git\hooks\post-merge del .git\hooks\post-merge - rem Add git hook to run Setup.bat when RequireSetup file has been updated. - echo #!/usr/bin/env bash>.git\hooks\post-merge - echo changed_files="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)">>.git\hooks\post-merge - echo check_run() {>>.git\hooks\post-merge - echo echo "$changed_files" ^| grep --quiet "$1" ^&^& exec $2>>.git\hooks\post-merge - echo }>>.git\hooks\post-merge - echo check_run RequireSetup "cmd.exe /c Setup.bat %*">>.git\hooks\post-merge + rem Remove the old pre-commit hook. + if exist .git\hooks\pre-commit del .git\hooks\pre-commit + + rem Copy git hooks to .git directory. + xcopy /s /i /q "%~dp0\SpatialGDK\Extras\git" "%~dp0\.git\hooks" + + rem We pass Setup.bat args, such as --mobile, to the post-merge hook to run Setup.bat with the same args in future. + set POST_MERGE_HOOK="%~dp0\.git\hooks\post-merge" + powershell -Command "(Get-Content -Path %POST_MERGE_HOOK%) -replace \"SETUP_ARGS\", \"%*\" | Set-Content -Path %POST_MERGE_HOOK%" :SkipGitHooks call :MarkEndOfBlock "Setup the git hooks" @@ -48,7 +53,6 @@ call :MarkEndOfBlock "Check dependencies" call :MarkStartOfBlock "Setup variables" set /p PINNED_CORE_SDK_VERSION=<.\SpatialGDK\Extras\core-sdk.version - set /p PINNED_SPOT_VERSION=<.\SpatialGDK\Extras\spot.version set BUILD_DIR=%~dp0SpatialGDK\Build set CORE_SDK_DIR=%BUILD_DIR%\core_sdk set WORKER_SDK_DIR=%~dp0SpatialGDK\Source\SpatialGDK\Public\WorkerSDK @@ -109,36 +113,34 @@ call :MarkStartOfBlock "Create folders" call :MarkEndOfBlock "Create folders" call :MarkStartOfBlock "Retrieve dependencies" - spatial package retrieve tools schema_compiler-x86_64-win32 %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\tools\schema_compiler-x86_64-win32.zip" - 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-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" + call :ExecuteAndCheck spatial package retrieve tools schema_compiler-x86_64-win32 %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\tools\schema_compiler-x86_64-win32.zip" + call :ExecuteAndCheck spatial package retrieve schema standard_library %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\schema\standard_library.zip" + call :ExecuteAndCheck spatial package retrieve worker_sdk c_headers %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c_headers.zip" + call :ExecuteAndCheck spatial package retrieve worker_sdk c-dynamic-x86_64-vc141_md-win32 %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-vc141_md-win32.zip" + call :ExecuteAndCheck spatial package retrieve worker_sdk c-dynamic-x86_64-clang1000-linux %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-clang1000-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_ndk21-android %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-arm64v8a-clang_clang_ndk21-android.zip" - spatial package retrieve worker_sdk c-dynamic-armv7a-clang_ndk21-android %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-armv7a-clang_clang_ndk21-android.zip" - spatial package retrieve worker_sdk c-dynamic-x86_64-clang_ndk21-android %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-clang_clang_ndk21-android.zip" + call :ExecuteAndCheck 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" + call :ExecuteAndCheck spatial package retrieve worker_sdk c-dynamic-arm64v8a-clang_ndk21d-android %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-arm64v8a-clang_clang_ndk21-android.zip" + call :ExecuteAndCheck spatial package retrieve worker_sdk c-dynamic-armv7a-clang_ndk21d-android %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-armv7a-clang_clang_ndk21-android.zip" + call :ExecuteAndCheck spatial package retrieve worker_sdk c-dynamic-x86_64-clang_ndk21d-android %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-clang_clang_ndk21-android.zip" ) - spatial package retrieve worker_sdk csharp %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\csharp.zip" - spatial package retrieve spot spot-win64 %PINNED_SPOT_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%BINARIES_DIR%\Programs\spot.exe" + call :ExecuteAndCheck spatial package retrieve worker_sdk csharp_cinterop %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\csharp_cinterop.zip" call :MarkEndOfBlock "Retrieve dependencies" REM There is a race condition between retrieve and unzip, add version call to stall briefly -call spatial version +call spatial version +if ERRORLEVEL 1 pause && exit /b %ERRORLEVEL% call :MarkStartOfBlock "Unpack dependencies" - powershell -Command "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c_headers.zip\" -DestinationPath \"%BINARIES_DIR%\Headers\" -Force; "^ - "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86-vc141_md-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Win32\" -Force; "^ - "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%\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;" + powershell -Command "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c_headers.zip\" -DestinationPath \"%BINARIES_DIR%\Headers\" -Force; "^ + "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-vc141_md-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Win64\" -Force; "^ + "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-clang1000-linux.zip\" -DestinationPath \"%BINARIES_DIR%\Linux\" -Force; "^ + "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\csharp_cinterop.zip\" -DestinationPath \"%BINARIES_DIR%\Programs\worker_sdk\csharp_cinterop\" -Force; "^ + "Expand-Archive -Path \"%CORE_SDK_DIR%\tools\schema_compiler-x86_64-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Programs\" -Force; "^ + "Expand-Archive -Path \"%CORE_SDK_DIR%\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;"^ + 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_clang_ndk21-android.zip\" -DestinationPath \"%BINARIES_DIR%\Android\arm64-v8a\" -Force; "^ "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-armv7a-clang_clang_ndk21-android.zip\" -DestinationPath \"%BINARIES_DIR%\Android\armeabi-v7a\" -Force; "^ "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-clang_clang_ndk21-android.zip\" -DestinationPath \"%BINARIES_DIR%\Android\x86_64\" -Force; "^ @@ -159,7 +161,7 @@ if exist "%SPATIAL_DIR%" ( ) call :MarkStartOfBlock "Build C# utilities" - %MSBUILD_EXE% /nologo /verbosity:minimal .\SpatialGDK\Build\Programs\Improbable.Unreal.Scripts\Improbable.Unreal.Scripts.sln /property:Configuration=Release /restore + call :ExecuteAndCheck %MSBUILD_EXE% /nologo /verbosity:minimal .\SpatialGDK\Build\Programs\Improbable.Unreal.Scripts\Improbable.Unreal.Scripts.sln /property:Configuration=Release /restore call :MarkEndOfBlock "Build C# utilities" call :MarkEndOfBlock "%~0" @@ -181,3 +183,12 @@ exit /b 0 :MarkEndOfBlock echo Finished: %~1 exit /b 0 + +:ExecuteAndCheck +%* +if ERRORLEVEL 1 ( + echo ERROR: Command '%*' did not complete successfully. Aborting... + pause + exit 1 +) +exit /b 0 diff --git a/Setup.sh b/Setup.sh index f893ad66a4..2cdef332f1 100755 --- a/Setup.sh +++ b/Setup.sh @@ -10,8 +10,7 @@ fi pushd "$(dirname "$0")" -PINNED_CORE_SDK_VERSION=$(cat ./SpatialGDK/Extras/core-sdk.version) -PINNED_SPOT_VERSION=$(cat ./SpatialGDK/Extras/spot.version) +PINNED_CORE_SDK_VERSION=$(head -n 1 ./SpatialGDK/Extras/core-sdk.version | tr -d '\r') BUILD_DIR="$(pwd)/SpatialGDK/Build" CORE_SDK_DIR="${BUILD_DIR}/core_sdk" WORKER_SDK_DIR="$(pwd)/SpatialGDK/Source/SpatialGDK/Public/WorkerSDK" @@ -22,19 +21,6 @@ SPATIAL_DIR="$(pwd)/../../../spatial" DOWNLOAD_MOBILE= USE_CHINA_SERVICES_REGION= -while test $# -gt 0 -do - case "$1" in - --china) - DOMAIN_ENVIRONMENT_VAR="--environment cn-production" - USE_CHINA_SERVICES_REGION=true - ;; - --mobile) DOWNLOAD_MOBILE=true - ;; - esac - shift -done - echo "Setup the git hooks" if [[ -e .git/hooks ]]; then # Remove the old post-checkout hook. @@ -47,10 +33,34 @@ if [[ -e .git/hooks ]]; then rm -f .git/hooks/post-merge fi + # Remove the old pre-commit hook. + if [[ -e .git/hooks/pre-commit ]]; then + rm -f .git/hooks/pre-commit + fi + # Add git hook to run Setup.sh when RequireSetup file has been updated. - cp "$(pwd)/SpatialGDK/Extras/git/post-merge" "$(pwd)/.git/hooks" + cp -R "$(pwd)/SpatialGDK/Extras/git/." "$(pwd)/.git/hooks" + + # We pass Setup.sh args, such as --mobile, to the post-merge hook to run Setup.sh with the same args in future. + sed -i "" -e "s/SETUP_ARGS/$*/g" .git/hooks/post-merge + + # This needs to be runnable. + chmod +x .git/hooks/pre-commit fi +while test $# -gt 0 +do + case "$1" in + --china) + DOMAIN_ENVIRONMENT_VAR="--environment cn-production" + USE_CHINA_SERVICES_REGION=true + ;; + --mobile) DOWNLOAD_MOBILE=true + ;; + esac + shift +done + # Create or remove an empty file in the plugin directory indicating whether to use China services region. if [[ -n "${USE_CHINA_SERVICES_REGION}" ]]; then touch UseChinaServicesRegion @@ -90,14 +100,11 @@ spatial package retrieve worker_sdk c-dynamic-x86_64-clang-macos "${ if [[ -n "${DOWNLOAD_MOBILE}" ]]; then spatial package retrieve worker_sdk c-static-fullylinked-arm-clang-ios "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-static-fullylinked-arm-clang-ios.zip - spatial package retrieve worker_sdk c-dynamic-arm64v8a-clang_ndk21-android "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-arm64v8a-clang_ndk21-android.zip - spatial package retrieve worker_sdk c-dynamic-armv7a-clang_ndk21-android "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-armv7a-clang_ndk21-android.zip - spatial package retrieve worker_sdk c-dynamic-x86_64-clang_ndk21-android "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang_ndk21-android.zip + spatial package retrieve worker_sdk c-dynamic-arm64v8a-clang_ndk21d-android "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-arm64v8a-clang_ndk21d-android.zip + spatial package retrieve worker_sdk c-dynamic-armv7a-clang_ndk21d-android "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-armv7a-clang_ndk21d-android.zip + spatial package retrieve worker_sdk c-dynamic-x86_64-clang_ndk21d-android "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang_ndk21d-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 +spatial package retrieve worker_sdk csharp_cinterop "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/csharp_cinterop.zip echo "Unpack dependencies" unzip -oq "${CORE_SDK_DIR}"/tools/schema_compiler-x86_64-macos.zip -d "${BINARIES_DIR}"/Programs/ @@ -108,12 +115,12 @@ unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang-macos.zip if [[ -n "${DOWNLOAD_MOBILE}" ]]; then unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-static-fullylinked-arm-clang-ios.zip -d "${BINARIES_DIR}"/IOS/ - unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-arm64v8a-clang_ndk21-android.zip -d "${BINARIES_DIR}"/Android/arm64-v8a/ - unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-armv7a-clang_ndk21-android.zip -d "${BINARIES_DIR}"/Android/armeabi-v7a/ - unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang_ndk21-android.zip -d "${BINARIES_DIR}"/Android/x86_64/ + unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-arm64v8a-clang_ndk21d-android.zip -d "${BINARIES_DIR}"/Android/arm64-v8a/ + unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-armv7a-clang_ndk21d-android.zip -d "${BINARIES_DIR}"/Android/armeabi-v7a/ + unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang_ndk21d-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/ +unzip -oq "${CORE_SDK_DIR}"/worker_sdk/csharp_cinterop.zip -d "${BINARIES_DIR}"/Programs/worker_sdk/csharp_cinterop/ cp -R "${BINARIES_DIR}"/Headers/include/ "${WORKER_SDK_DIR}" if [[ -d "${SPATIAL_DIR}" ]]; then diff --git a/SetupIncTraceLibs.bat b/SetupIncTraceLibs.bat index b7d1eb65b4..6b0fe9eb8a 100644 --- a/SetupIncTraceLibs.bat +++ b/SetupIncTraceLibs.bat @@ -18,8 +18,8 @@ call :MarkStartOfBlock "Create folders" call :MarkEndOfBlock "Create folders" call :MarkStartOfBlock "Retrieve dependencies" - spatial package retrieve internal trace-dynamic-x86_64-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" + spatial package retrieve internal trace-dynamic-x86_64-vc141_md-win32 %PINNED_CORE_SDK_VERSION% "%CORE_SDK_DIR%\trace_lib\trace-win32.zip" + spatial package retrieve internal trace-dynamic-x86_64-clang1000-linux %PINNED_CORE_SDK_VERSION% "%CORE_SDK_DIR%\trace_lib\trace-linux.zip" call :MarkEndOfBlock "Retrieve dependencies" REM There is a race condition between retrieve and unzip, add version call to stall briefly @@ -28,7 +28,12 @@ call spatial version call :MarkStartOfBlock "Unpack dependencies" powershell -Command "Expand-Archive -Path \"%CORE_SDK_DIR%\trace_lib\trace-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Win64\" -Force;"^ "Expand-Archive -Path \"%CORE_SDK_DIR%\trace_lib\trace-linux.zip\" -DestinationPath \"%BINARIES_DIR%\Linux\" -Force;" - xcopy /s /i /q "%BINARIES_DIR%\Win64\improbable" "%WORKER_SDK_DIR%\improbable" + xcopy /s /i /q "%BINARIES_DIR%\Win64\include\improbable" "%WORKER_SDK_DIR%\improbable\legacy" + + set LEGACY_FOLDER=%WORKER_SDK_DIR%\improbable\legacy\ + set TRACE_HEADER="%LEGACY_FOLDER%trace.h" + powershell -Command "(Get-Content '%TRACE_HEADER%').replace('#include ', '#include ') | Set-Content -Force '%TRACE_HEADER%'" + call :MarkEndOfBlock "Unpack dependencies" call :MarkEndOfBlock "%~0" diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs index abcd6efe94..60bcc4b80b 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs @@ -38,6 +38,7 @@ public static void Main(string[] args) var projectFile = Path.GetFullPath(args[3]); var noBuild = args.Count(arg => arg.ToLowerInvariant() == "-nobuild") > 0; var noCompile = args.Count(arg => arg.ToLowerInvariant() == "-nocompile") > 0; + var noServer = args.Count(arg => arg.ToLowerInvariant() == "-noserver") > 0; var additionalUATArgs = string.Join(" ", args.Skip(4).Where(arg => (arg.ToLowerInvariant() != "-nobuild") && (arg.ToLowerInvariant() != "-nocompile"))); var stagingDir = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(projectFile), "../spatial", "build", "unreal")); @@ -177,16 +178,16 @@ public static void Main(string[] args) additionalUATArgs }); - var windowsNoEditorPath = Path.Combine(stagingDir, "WindowsNoEditor"); + var windowsTargetPath = Path.Combine(stagingDir, noServer ? "WindowsClient" : "WindowsNoEditor"); - ForceSpatialNetworkingUnlessPakSpecified(additionalUATArgs, windowsNoEditorPath, baseGameName); + ForceSpatialNetworkingUnlessPakSpecified(additionalUATArgs, windowsTargetPath, baseGameName); - RenameExeForLauncher(windowsNoEditorPath, baseGameName); + RenameExeForLauncher(windowsTargetPath, baseGameName); Common.RunRedirected(runUATBat, new[] { "ZipUtils", - "-add=" + Quote(windowsNoEditorPath), + "-add=" + Quote(windowsTargetPath), "-archive=" + Quote(Path.Combine(outputDir, "UnrealClient@Windows.zip")), }); } @@ -221,7 +222,7 @@ public static void Main(string[] args) additionalUATArgs }); - var linuxSimulatedPlayerPath = Path.Combine(stagingDir, "LinuxNoEditor"); + var linuxSimulatedPlayerPath = Path.Combine(stagingDir, noServer ? "LinuxClient" : "LinuxNoEditor"); ForceSpatialNetworkingUnlessPakSpecified(additionalUATArgs, linuxSimulatedPlayerPath, baseGameName); diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.csproj b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.csproj index 51aeceb530..fb9aed6c4b 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.csproj +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.csproj @@ -9,7 +9,7 @@ Properties Build Build - v4.5 + v4.6.2 512 true diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/CodeGenerator.csproj b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/CodeGenerator.csproj index 7e7423ebb8..9da3d06030 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/CodeGenerator.csproj +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/CodeGenerator.csproj @@ -9,7 +9,7 @@ Properties CodeGenerator CodeGenerator - v4.5 + v4.6.2 512 true diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/Common.csproj b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/Common.csproj index 9a77dd2d27..50aa7fe05e 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/Common.csproj +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/Common.csproj @@ -9,7 +9,7 @@ Properties Common Common - v4.5 + v4.6.2 512 diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs index 2027ef4171..bfe295e952 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs @@ -13,6 +13,7 @@ using System.Net; using System.Security.Cryptography; using System; +using System.Threading.Tasks; namespace Improbable { @@ -27,11 +28,16 @@ internal class DeploymentLauncher private const string CHINA_ENDPOINT_URL = "platform.api.spatialoschina.com"; private const int CHINA_ENDPOINT_PORT = 443; - private static readonly string ChinaRefreshToken = File.ReadAllText(Path.Combine(Environment.ExpandEnvironmentVariables("%LOCALAPPDATA%"), ".improbable/oauth2/oauth2_refresh_token_cn-production")); + private static readonly string ChinaProductionTokenPath = Path.Combine(Environment.ExpandEnvironmentVariables("%LOCALAPPDATA%"), ".improbable/oauth2/oauth2_refresh_token_cn-production"); - private static readonly PlatformRefreshTokenCredential ChinaCredentials = new PlatformRefreshTokenCredential(ChinaRefreshToken, - "https://auth.spatialoschina.com/auth/v1/authcode", - "https://auth.spatialoschina.com/auth/v1/token"); + // Populated in the Main method if the Chinese platform is to be used + private static string ChinaRefreshToken = String.Empty; + private static PlatformRefreshTokenCredential ChinaCredentials; + + private static string GetConsoleHost(bool useChinaPlatform) + { + return useChinaPlatform ? "console.spatialoschina.com" : "console.improbable.io"; + } private static string UploadSnapshot(SnapshotServiceClient client, string snapshotPath, string projectName, string deploymentName, bool useChinaPlatform) @@ -110,7 +116,8 @@ private static PlatformRefreshTokenCredential GetPlatformRefreshTokenCredential( private static int CreateDeployment(string[] args, bool useChinaPlatform) { - bool launchSimPlayerDeployment = args.Length == 15; + // Argument count can vary because of optional arguments + bool launchSimPlayerDeployment = args.Length == 16 || args.Length == 15; var projectName = args[1]; var assemblyName = args[2]; @@ -122,24 +129,46 @@ private static int CreateDeployment(string[] args, bool useChinaPlatform) var mainDeploymentCluster = args[8]; var mainDeploymentTags = args[9]; - var simDeploymentName = string.Empty; + var simDeploymentBaseName = string.Empty; var simDeploymentJson = string.Empty; var simDeploymentRegion = string.Empty; var simDeploymentCluster = string.Empty; - var simNumPlayers = 0; + var numSimPlayers = 0; + var maxPlayersPerDeployment = -1; // Will be initialized to numSimPlayers if (launchSimPlayerDeployment) { - simDeploymentName = args[10]; + simDeploymentBaseName = args[10]; simDeploymentJson = args[11]; simDeploymentRegion = args[12]; simDeploymentCluster = args[13]; - if (!Int32.TryParse(args[14], out simNumPlayers)) + if (!Int32.TryParse(args[14], out numSimPlayers)) { Console.WriteLine("Cannot parse the number of simulated players to connect."); return 1; } + else if (numSimPlayers <= 0) + { + Console.WriteLine("The number of players must be positive."); + return 1; + } + + // Start a single deployment by default + maxPlayersPerDeployment = numSimPlayers; + if (args.Length >= 16) + { + if (!Int32.TryParse(args[15], out maxPlayersPerDeployment)) + { + Console.WriteLine("Cannot parse the maximum number of simulated players per deployment."); + return 1; + } + else if (maxPlayersPerDeployment <= 0) + { + Console.WriteLine("The maximum number of simulated players per deployment must be positive."); + return 1; + } + } } try @@ -159,7 +188,7 @@ private static int CreateDeployment(string[] args, bool useChinaPlatform) if (!launchSimPlayerDeployment) { // Don't launch a simulated player deployment. Wait for main deployment to be created and then return. - Console.WriteLine("Waiting for deployment to be ready..."); + Console.WriteLine("Waiting for the main deployment to be ready..."); var result = createMainDeploymentOp.PollUntilCompleted().GetResultOrNull(); if (result == null) { @@ -171,47 +200,50 @@ private static int CreateDeployment(string[] args, bool useChinaPlatform) return 0; } - if (DeploymentExists(deploymentServiceClient, projectName, simDeploymentName)) - { - StopDeploymentByName(deploymentServiceClient, projectName, simDeploymentName); - } - - // we are using the main deployment snapshot also for the sim player deployment, because we only need to specify a snapshot + // We are using the main deployment snapshot also for the sim player deployment(s), because we only need to specify a snapshot // to be able to start the deployment. The sim players don't care about the actual snapshot. - var createSimDeploymentOp = CreateSimPlayerDeploymentAsync(deploymentServiceClient, - projectName, assemblyName, runtimeVersion, mainDeploymentName, simDeploymentName, + var simDeploymentCreationOps = CreateSimPlayerDeploymentsAsync(deploymentServiceClient, + projectName, assemblyName, runtimeVersion, mainDeploymentName, simDeploymentBaseName, simDeploymentJson, mainDeploymentSnapshotPath, simDeploymentRegion, simDeploymentCluster, - simNumPlayers, useChinaPlatform); + numSimPlayers, maxPlayersPerDeployment, useChinaPlatform); - // Wait for both deployments to be created. - Console.WriteLine("Waiting for deployments to be ready..."); + if (simDeploymentCreationOps == null || simDeploymentCreationOps.Count == 0) + { + Console.WriteLine("Failed to start any simulated player deployments."); + return 1; + } + + // Wait for the main deployment to be ready + Console.WriteLine("Waiting for the main deployment to be ready..."); var mainDeploymentResult = createMainDeploymentOp.PollUntilCompleted().GetResultOrNull(); if (mainDeploymentResult == null) { Console.WriteLine("Failed to create the main deployment"); return 1; } - Console.WriteLine("Successfully created the main deployment"); - var simPlayerDeployment = createSimDeploymentOp.PollUntilCompleted().GetResultOrNull(); - if (simPlayerDeployment == null) + + // Waiting for the simulated player deployment(s) to be ready + var numSuccessfullyStartedSimDeployments = 0; + for (var simDeploymentIndex = 0; simDeploymentIndex < simDeploymentCreationOps.Count; simDeploymentIndex++) { - Console.WriteLine("Failed to create the simulated player deployment"); - return 1; - } + var deploymentDescription = $"(deployment {simDeploymentIndex + 1}/{simDeploymentCreationOps.Count})"; + Console.WriteLine($"Waiting for the simulated player deployment to be ready... {deploymentDescription}"); + + var simPlayerDeployment = simDeploymentCreationOps[simDeploymentIndex].PollUntilCompleted().GetResultOrNull(); + if (simPlayerDeployment == null) + { + Console.WriteLine($"Failed to create the simulated player deployment {deploymentDescription}"); + continue; + } - Console.WriteLine("Successfully created the simulated player deployment"); + Console.WriteLine($"Deployment startup complete! Setting its flags..."); + UpdateSimDeploymentFlags(simPlayerDeployment, true, deploymentServiceClient); - // Update coordinator worker flag for simulated player deployment to notify target deployment is ready. - simPlayerDeployment.WorkerFlags.Add(new WorkerFlag - { - Key = TARGET_DEPLOYMENT_READY_TAG, - Value = "true", - WorkerType = CoordinatorWorkerName - }); - deploymentServiceClient.UpdateDeployment(new UpdateDeploymentRequest { Deployment = simPlayerDeployment }); + numSuccessfullyStartedSimDeployments++; + } - Console.WriteLine("Done! Simulated players will start to connect to your deployment"); + Console.WriteLine($"Successfully started {numSuccessfullyStartedSimDeployments} out of {simDeploymentCreationOps.Count} simulated player deployments."); } catch (Grpc.Core.RpcException e) { @@ -241,80 +273,110 @@ private static int CreateSimDeployments(string[] args, bool useChinaPlatform) var assemblyName = args[2]; var runtimeVersion = args[3]; var targetDeploymentName = args[4]; - var simDeploymentName = args[5]; + var simDeploymentBaseName = args[5]; var simDeploymentJson = args[6]; var simDeploymentRegion = args[7]; var simDeploymentCluster = args[8]; var simDeploymentSnapshotPath = args[9]; - - var simNumPlayers = 0; - if (!Int32.TryParse(args[10], out simNumPlayers)) + var numSimplayers = 0; + if (!Int32.TryParse(args[10], out numSimplayers)) { Console.WriteLine("Cannot parse the number of simulated players to connect."); return 1; } + else if (numSimplayers <= 0) + { + Console.WriteLine("The number of players must be positive."); + return 1; + } var autoConnect = false; - if (!Boolean.TryParse(args[11], out autoConnect)) + if (!Boolean.TryParse(args[12], out autoConnect)) { Console.WriteLine("Cannot parse the auto-connect flag."); return 1; } - try + var maxPlayersPerDeployment = numSimplayers; + if (args.Length >= 12) { - var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(useChinaPlatform)); - - if (DeploymentExists(deploymentServiceClient, projectName, simDeploymentName)) + if (!Int32.TryParse(args[11], out maxPlayersPerDeployment)) { - StopDeploymentByName(deploymentServiceClient, projectName, simDeploymentName); + Console.WriteLine("Cannot parse the maximum number of simulated players per deployments."); + return 1; } - - var createSimDeploymentOp = CreateSimPlayerDeploymentAsync(deploymentServiceClient, - projectName, assemblyName, runtimeVersion, targetDeploymentName, simDeploymentName, - simDeploymentJson, simDeploymentSnapshotPath, simDeploymentRegion, simDeploymentCluster, simNumPlayers, useChinaPlatform); - - // Wait for both deployments to be created. - Console.WriteLine("Waiting for the simulated player deployment to be ready..."); - var simPlayerDeployment = createSimDeploymentOp.PollUntilCompleted().GetResultOrNull(); - if (simPlayerDeployment == null) + else if (maxPlayersPerDeployment <= 0) { - Console.WriteLine("Failed to create the simulated player deployment"); + Console.WriteLine("The maximum number of simulated players per deployment must be positive."); return 1; } + } - Console.WriteLine("Successfully created the simulated player deployment"); + var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(useChinaPlatform)); - // Update coordinator worker flag for simulated player deployment to notify target deployment is ready. - simPlayerDeployment.WorkerFlags.Add(new WorkerFlag - { - Key = TARGET_DEPLOYMENT_READY_TAG, - Value = autoConnect.ToString().ToLower(), - WorkerType = CoordinatorWorkerName - }); - deploymentServiceClient.UpdateDeployment(new UpdateDeploymentRequest { Deployment = simPlayerDeployment }); + var simDeploymentCreationOps = CreateSimPlayerDeploymentsAsync(deploymentServiceClient, + projectName, assemblyName, runtimeVersion, targetDeploymentName, simDeploymentBaseName, + simDeploymentJson, simDeploymentSnapshotPath, simDeploymentRegion, simDeploymentCluster, + numSimplayers, maxPlayersPerDeployment, useChinaPlatform); - if (autoConnect) - { - Console.WriteLine("Done! Simulated players will start to connect to your deployment"); - } + if (simDeploymentCreationOps == null || simDeploymentCreationOps.Count == 0) + { + Console.WriteLine("Failed to start any simulated player deployments."); + return 1; } - catch (Grpc.Core.RpcException e) + + var numSuccessfullyStartedDeployments = 0; + for (var simDeploymentIndex = 0; simDeploymentIndex < simDeploymentCreationOps.Count; simDeploymentIndex++) { - 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 + var deploymentDescription = $"(deployment {simDeploymentIndex + 1}/{simDeploymentCreationOps.Count})"; + Console.WriteLine($"Waiting for the simulated player deployment to be ready... {deploymentDescription}"); + + var simPlayerDeployment = simDeploymentCreationOps[simDeploymentIndex].PollUntilCompleted().GetResultOrNull(); + if (simPlayerDeployment == null) { - throw; + Console.WriteLine($"Failed to create the simulated player deployment {deploymentDescription}"); + continue; } + + Console.WriteLine($"Deployment startup complete! Setting its flags..."); + UpdateSimDeploymentFlags(simPlayerDeployment, autoConnect, deploymentServiceClient); + + numSuccessfullyStartedDeployments++; } + Console.WriteLine($"Successfully started {numSuccessfullyStartedDeployments} out of {simDeploymentCreationOps.Count} simulated player deployments."); + return 0; } + private static void UpdateSimDeploymentFlags(Deployment deployment, bool autoConnect, DeploymentServiceClient deploymentServiceClient) + { + // Update coordinator worker flag for simulated player deployment to notify target deployment is ready. + deployment.WorkerFlags.Add(new WorkerFlag + { + Key = TARGET_DEPLOYMENT_READY_TAG, + Value = autoConnect.ToString().ToLower(), + WorkerType = CoordinatorWorkerName + }); + deploymentServiceClient.UpdateDeployment(new UpdateDeploymentRequest { Deployment = deployment }); + + if (autoConnect) + { + Console.WriteLine($"Simulated players from this deployment '{deployment.Name}' will start to connect to the target deployment"); + } + } + + // Determines the name for a simulated player deployment. The first index is assumed to be 1. + private static string GetSimDeploymentName(string baseName, int index) + { + if (index == 1) + { + return baseName; + } + + return baseName + ("_" + index); + } + private static bool DeploymentExists(DeploymentServiceClient deploymentServiceClient, string projectName, string deploymentName) { @@ -323,7 +385,6 @@ private static bool DeploymentExists(DeploymentServiceClient deploymentServiceCl return activeDeployments.FirstOrDefault(d => d.Name == deploymentName) != null; } - private static void StopDeploymentByName(DeploymentServiceClient deploymentServiceClient, string projectName, string deploymentName) { @@ -402,7 +463,7 @@ private static Operation CreateMainDeploym } Console.WriteLine( - $"Creating the main deployment {mainDeploymentName} in project {projectName} with snapshot ID {mainSnapshotId}. Link: https://console.improbable.io/projects/{projectName}/deployments/{mainDeploymentName}/overview"); + $"Creating the main deployment {mainDeploymentName} in project {projectName} with snapshot ID {mainSnapshotId}. Link: https://{GetConsoleHost(useChinaPlatform)}/projects/{projectName}/deployments/{mainDeploymentName}/overview"); var mainDeploymentCreateOp = deploymentServiceClient.CreateDeployment(new CreateDeploymentRequest { @@ -414,7 +475,7 @@ private static Operation CreateMainDeploym private static Operation CreateSimPlayerDeploymentAsync(DeploymentServiceClient deploymentServiceClient, string projectName, string assemblyName, string runtimeVersion, string mainDeploymentName, string simDeploymentName, - string simDeploymentJsonPath, string simDeploymentSnapshotPath, string regionCode, string clusterCode, int simNumPlayers, bool useChinaPlatform) + string simDeploymentJsonPath, string simDeploymentSnapshotPath, string regionCode, string clusterCode, int numSimPlayers, bool useChinaPlatform) { var snapshotServiceClient = SnapshotServiceClient.Create(GetApiEndpoint(useChinaPlatform), GetPlatformRefreshTokenCredential(useChinaPlatform)); @@ -439,10 +500,6 @@ private static Operation CreateSimPlayerDe }); // Add worker flags to sim deployment JSON. - var regionFlag = new JObject(); - regionFlag.Add("name", "simulated_players_region"); - regionFlag.Add("value", regionCode); - var devAuthTokenFlag = new JObject(); devAuthTokenFlag.Add("name", "simulated_players_dev_auth_token"); devAuthTokenFlag.Add("value", dat.TokenSecret); @@ -453,29 +510,29 @@ private static Operation CreateSimPlayerDe var numSimulatedPlayersFlag = new JObject(); numSimulatedPlayersFlag.Add("name", "total_num_simulated_players"); - numSimulatedPlayersFlag.Add("value", $"{simNumPlayers}"); + numSimulatedPlayersFlag.Add("value", $"{numSimPlayers}"); - var simWorkerConfigJson = File.ReadAllText(simDeploymentJsonPath); - dynamic simWorkerConfig = JObject.Parse(simWorkerConfigJson); + var simDeploymentConfigJson = File.ReadAllText(simDeploymentJsonPath); + dynamic simDeploymentConfig = JObject.Parse(simDeploymentConfigJson); if (simDeploymentJsonPath.EndsWith(".pb.json")) { - for (var i = 0; i < simWorkerConfig.worker_flagz.Count; ++i) + for (var i = 0; i < simDeploymentConfig.worker_flagz.Count; ++i) { - if (simWorkerConfig.worker_flagz[i].worker_type == CoordinatorWorkerName) + if (simDeploymentConfig.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); + simDeploymentConfig.worker_flagz[i].flagz.Add(devAuthTokenFlag); + simDeploymentConfig.worker_flagz[i].flagz.Add(targetDeploymentFlag); + simDeploymentConfig.worker_flagz[i].flagz.Add(numSimulatedPlayersFlag); break; } } - for (var i = 0; i < simWorkerConfig.flagz.Count; ++i) + for (var i = 0; i < simDeploymentConfig.flagz.Count; ++i) { - if (simWorkerConfig.flagz[i].name == "loadbalancer_v2_config_json") + if (simDeploymentConfig.flagz[i].name == "loadbalancer_v2_config_json") { - string layerConfigJson = simWorkerConfig.flagz[i].value; + string layerConfigJson = simDeploymentConfig.flagz[i].value; dynamic loadBalanceConfig = JObject.Parse(layerConfigJson); var lbLayerConfigurations = loadBalanceConfig.layerConfigurations; for (var j = 0; j < lbLayerConfigurations.Count; ++j) @@ -483,25 +540,25 @@ private static Operation CreateSimPlayerDe if (lbLayerConfigurations[j].layer == CoordinatorWorkerName) { var rectangleGrid = lbLayerConfigurations[j].rectangleGrid; - rectangleGrid.cols = simNumPlayers; + rectangleGrid.cols = numSimPlayers; rectangleGrid.rows = 1; break; } } - simWorkerConfig.flagz[i].value = Newtonsoft.Json.JsonConvert.SerializeObject(loadBalanceConfig); + simDeploymentConfig.flagz[i].value = Newtonsoft.Json.JsonConvert.SerializeObject(loadBalanceConfig); break; } } } else // regular non pb.json { - for (var i = 0; i < simWorkerConfig.workers.Count; ++i) + for (var i = 0; i < simDeploymentConfig.workers.Count; ++i) { - if (simWorkerConfig.workers[i].worker_type == CoordinatorWorkerName) + if (simDeploymentConfig.workers[i].worker_type == CoordinatorWorkerName) { - simWorkerConfig.workers[i].flags.Add(devAuthTokenFlag); - simWorkerConfig.workers[i].flags.Add(targetDeploymentFlag); - simWorkerConfig.workers[i].flags.Add(numSimulatedPlayersFlag); + simDeploymentConfig.workers[i].flags.Add(devAuthTokenFlag); + simDeploymentConfig.workers[i].flags.Add(targetDeploymentFlag); + simDeploymentConfig.workers[i].flags.Add(numSimulatedPlayersFlag); } } @@ -511,27 +568,25 @@ private static Operation CreateSimPlayerDe // 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; + var lbLayerConfigurations = simDeploymentConfig.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.cols = numSimPlayers; rectangleGrid.rows = 1; } } } - simWorkerConfigJson = simWorkerConfig.ToString(); - // Create simulated player deployment. - var simDeploymentConfig = new Deployment + var simDeployment = new Deployment { AssemblyId = assemblyName, LaunchConfig = new LaunchConfig { - ConfigJson = simWorkerConfigJson + ConfigJson = simDeploymentConfig.ToString() }, Name = simDeploymentName, ProjectName = projectName, @@ -541,27 +596,92 @@ private static Operation CreateSimPlayerDe if (!String.IsNullOrEmpty(clusterCode)) { - simDeploymentConfig.ClusterCode = clusterCode; + simDeployment.ClusterCode = clusterCode; } else { - simDeploymentConfig.RegionCode = regionCode; + simDeployment.RegionCode = regionCode; } - simDeploymentConfig.Tag.Add(DEPLOYMENT_LAUNCHED_BY_LAUNCHER_TAG); - simDeploymentConfig.Tag.Add(SIM_PLAYER_DEPLOYMENT_TAG); + simDeployment.Tag.Add(DEPLOYMENT_LAUNCHED_BY_LAUNCHER_TAG); + simDeployment.Tag.Add(SIM_PLAYER_DEPLOYMENT_TAG); Console.WriteLine( - $"Creating the simulated player deployment {simDeploymentName} in project {projectName} with {simNumPlayers} simulated players. Link: https://console.improbable.io/projects/{projectName}/deployments/{simDeploymentName}/overview"); + $"Creating the simulated player deployment {simDeploymentName} in project {projectName} with {numSimPlayers} simulated players. Link: https://{GetConsoleHost(useChinaPlatform)}/projects/{projectName}/deployments/{simDeploymentName}/overview"); var simDeploymentCreateOp = deploymentServiceClient.CreateDeployment(new CreateDeploymentRequest { - Deployment = simDeploymentConfig + Deployment = simDeployment }); return simDeploymentCreateOp; } + private static List> CreateSimPlayerDeploymentsAsync(DeploymentServiceClient deploymentServiceClient, + string projectName, string assemblyName, string runtimeVersion, string mainDeploymentName, string simDeploymentBaseName, + string simDeploymentJsonPath, string simDeploymentSnapshotPath, string regionCode, string clusterCode, int numSimPlayers, int maxPlayersPerDeployment, bool useChinaPlatform) + { + var operations = new List>(); + + var numSimDeployments = (int)Math.Ceiling(numSimPlayers / (double)maxPlayersPerDeployment); + + var longestName = GetSimDeploymentName(simDeploymentBaseName, numSimDeployments); + if (longestName.Length > 32) + { + Console.WriteLine($"The deployment name may not exceed 32 characters. '{longestName}' would have {longestName.Length}."); + return operations; + } + + for (var simPlayerDeploymentId = 1; simPlayerDeploymentId <= numSimDeployments; ++simPlayerDeploymentId) + { + var simDeploymentName = GetSimDeploymentName(simDeploymentBaseName, simPlayerDeploymentId); + + try + { + if (DeploymentExists(deploymentServiceClient, projectName, simDeploymentName)) + { + StopDeploymentByName(deploymentServiceClient, projectName, simDeploymentName); + } + + // Determine the amount of simulated players in this deployment + var numSimPlayersPerDeployment = numSimPlayers / numSimDeployments; + // Spread leftover simulated players over deployments if the total isn't a multiple of the deployment count + if (simPlayerDeploymentId <= numSimPlayers % numSimDeployments) + { + ++numSimPlayersPerDeployment; + } + + Console.WriteLine($"Kicking off startup of deployment {simPlayerDeploymentId} out of the target {numSimDeployments}"); + var createSimDeploymentOp = CreateSimPlayerDeploymentAsync(deploymentServiceClient, + projectName, assemblyName, runtimeVersion, mainDeploymentName, simDeploymentName, + simDeploymentJsonPath, simDeploymentSnapshotPath, regionCode, clusterCode, numSimPlayersPerDeployment, useChinaPlatform); + + operations.Add(createSimDeploymentOp); + } + catch (Grpc.Core.RpcException e) + { + if (e.Status.StatusCode == Grpc.Core.StatusCode.NotFound) + { + Console.WriteLine($"Unable to launch the deployment(s). This is likely because the project '{projectName}' or assembly '{assemblyName}' doesn't exist."); + Console.WriteLine($"Detail: '{e.Status.Detail}'"); + } + else if (e.Status.StatusCode == Grpc.Core.StatusCode.ResourceExhausted) + { + Console.WriteLine($"Unable to launch the deployment(s). Cloud cluster resources exhausted, Detail: '{e.Status.Detail}'"); + } + else + { + Console.WriteLine($"Unable to launch the deployment(s). Detail: '{e.Status.Detail}'"); + } + + Console.WriteLine($"No further deployments will be started. Initiated startup for {simPlayerDeploymentId - 1} out of the target {numSimDeployments} deployments."); + + return operations; + } + } + + return operations; + } private static int StopDeployments(string[] args, bool useChinaPlatform) { @@ -569,49 +689,63 @@ private static int StopDeployments(string[] args, bool useChinaPlatform) var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(useChinaPlatform), GetPlatformRefreshTokenCredential(useChinaPlatform)); + var deploymentIdsToStop = new List(); + if (args.Length == 3) { // Stop only the specified deployment. var deploymentId = args[2]; - StopDeploymentById(deploymentServiceClient, projectName, deploymentId); - - return 0; + deploymentIdsToStop.Add(deploymentId); } - - // Stop all active deployments launched by this launcher. - var activeDeployments = ListLaunchedActiveDeployments(deploymentServiceClient, projectName); - - foreach (var deployment in activeDeployments) + else { - var deploymentId = deployment.Id; - StopDeploymentById(deploymentServiceClient, projectName, deploymentId); + // Stop all active deployments launched by this launcher. + var activeDeployments = ListLaunchedActiveDeployments(deploymentServiceClient, projectName); + foreach (var deployment in activeDeployments) + { + deploymentIdsToStop.Add(deployment.Id); + } } - return 0; - } + var deploymentIdsToTasks = new Dictionary(); + var erroredDeploymentIds = new List(); + + Console.WriteLine($"Will stop {deploymentIdsToStop.Count()} deployment(s)"); + foreach (var deploymentId in deploymentIdsToStop) + { + deploymentIdsToTasks.Add(deploymentId, StopDeploymentByIdAsync(deploymentServiceClient, projectName, deploymentId)); + }; - private static void StopDeploymentById(DeploymentServiceClient client, string projectName, string deploymentId) - { try { - Console.WriteLine($"Stopping deployment with id {deploymentId}"); - client.StopDeployment(new StopDeploymentRequest - { - Id = deploymentId, - ProjectName = projectName - }); + Task.WaitAll(deploymentIdsToTasks.Values.ToArray()); } - catch (Grpc.Core.RpcException e) + catch { - if (e.Status.StatusCode == Grpc.Core.StatusCode.NotFound) + // Retrieve individual exceptions from AggregateException thrown by Task.WaitAll + var throwers = deploymentIdsToTasks.Where(task => task.Value.Exception != null); + foreach (KeyValuePair erroredTask in throwers) { - Console.WriteLine(""); - } - else - { - throw; + Exception inner = erroredTask.Value.Exception.InnerException; + + string erroredDeploymentId = erroredTask.Key; + erroredDeploymentIds.Add(erroredDeploymentId); + Console.WriteLine($"Error while stopping deployment {erroredDeploymentId}: {inner.Message}"); } } + + Console.WriteLine($"Deployment(s) stopped with {erroredDeploymentIds.Count()} errors"); + return 0; + } + + private static Task StopDeploymentByIdAsync(DeploymentServiceClient client, string projectName, string deploymentId) + { + Console.WriteLine($"Stopping deployment with id {deploymentId}"); + return client.StopDeploymentAsync(new StopDeploymentRequest + { + Id = deploymentId, + ProjectName = projectName + }); } private static int ListDeployments(string[] args, bool useChinaPlatform) @@ -624,7 +758,7 @@ private static int ListDeployments(string[] args, bool useChinaPlatform) foreach (var deployment in activeDeployments) { var status = deployment.Status; - var overviewPageUrl = $"https://console.improbable.io/projects/{projectName}/deployments/{deployment.Name}/overview/{deployment.Id}"; + var overviewPageUrl = $"https://{GetConsoleHost(useChinaPlatform)}/projects/{projectName}/deployments/{deployment.Name}/overview/{deployment.Id}"; if (deployment.Tag.Contains(SIM_PLAYER_DEPLOYMENT_TAG)) { @@ -671,10 +805,10 @@ private static IEnumerable ListLaunchedActiveDeployments(DeploymentS private static void ShowUsage() { Console.WriteLine("Usage:"); - 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 create [ []]"); + Console.WriteLine($" Starts a cloud deployment with optional simulated player deployments. The deployments can be started in different regions ('EU', 'US', 'AP' and 'CN'). If simulated player deployment details are provided but the maximum number of players per deployment is left unspecified, a single deployment is started for all simulated players."); + Console.WriteLine("DeploymentLauncher createsim []"); + Console.WriteLine($" Starts simulated player deployment(s). Can be started in a different region from the target deployment ('EU', 'US', 'AP' and 'CN'). A single deployment for all simulated players is started by default."); 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."); @@ -693,9 +827,24 @@ private static int Main(string[] args) bool useChinaPlatform = flags.Contains("--china"); + if (useChinaPlatform) + { + if (!File.Exists(ChinaProductionTokenPath)) + { + Console.WriteLine("The 'china' flag was passed, but you are not authenticated for the 'cn-production' environment."); + return 1; + } + + ChinaRefreshToken = File.ReadAllText(ChinaProductionTokenPath); + ChinaCredentials = new PlatformRefreshTokenCredential(ChinaRefreshToken, + "https://auth.spatialoschina.com/auth/v1/authcode", + "https://auth.spatialoschina.com/auth/v1/token"); + } + + // Argument count for the same command can vary because of optional arguments if (args.Length == 0 || - (args[0] == "create" && (args.Length != 15 && args.Length != 10)) || - (args[0] == "createsim" && args.Length != 12) || + (args[0] == "create" && (args.Length != 16 && args.Length != 15 && args.Length != 10)) || + (args[0] == "createsim" && (args.Length != 13 && args.Length != 12)) || (args[0] == "stop" && (args.Length != 2 && args.Length != 3)) || (args[0] == "list" && args.Length != 2)) { diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.csproj b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.csproj index 35a390efb4..e946da4d81 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.csproj +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.csproj @@ -9,7 +9,7 @@ Properties DeploymentLauncher DeploymentLauncher - v4.5.1 + v4.6.2 512 true diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Linter/Linter.csproj b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Linter/Linter.csproj index adcca8d49f..0296c5a762 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Linter/Linter.csproj +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Linter/Linter.csproj @@ -9,7 +9,7 @@ Properties Linter Linter - v4.5 + v4.6.2 512 true diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/CoordinatorConnection.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/CoordinatorConnection.cs index e930aeff86..6447b4204f 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/CoordinatorConnection.cs +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/CoordinatorConnection.cs @@ -2,7 +2,7 @@ using System; using System.Threading; -using Improbable.Worker; +using Improbable.Worker.CInterop; namespace Improbable.WorkerCoordinator { @@ -18,7 +18,7 @@ public static Connection ConnectAndKeepAlive(Logger logger, string receptionistH WorkerType = coordinatorWorkerType, Network = { - ConnectionType = NetworkConnectionType.ModularTcp, + ConnectionType = NetworkConnectionType.Tcp, UseExternalIp = false } }; @@ -47,20 +47,28 @@ private static void KeepConnectionAlive(Connection connection, Logger logger) { var thread = new Thread(() => { - Dispatcher dispatcher = new Dispatcher(); var isConnected = true; - dispatcher.OnDisconnect(op => - { - logger.WriteError("[disconnect] " + op.Reason, logToConnectionIfExists: false); - isConnected = false; - }); - while (isConnected) { - using (var opList = connection.GetOpList(GetOpListTimeoutInMilliseconds)) + using (OpList opList = connection.GetOpList(GetOpListTimeoutInMilliseconds)) { - dispatcher.Process(opList); + var OpCount = opList.GetOpCount(); + for (var i = 0; i < OpCount; ++i) + { + switch (opList.GetOpType(i)) + { + case OpType.Disconnect: + { + DisconnectOp op = opList.GetDisconnectOp(i); + logger.WriteError("[disconnect] " + op.Reason, logToConnectionIfExists: false); + isConnected = false; + } + break; + default: + break; + } + } } } }); diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Logger.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Logger.cs index f6a8be2569..8b9dd08540 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Logger.cs +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Logger.cs @@ -1,7 +1,7 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved using System.IO; -using Improbable.Worker; +using Improbable.Worker.CInterop; namespace Improbable.WorkerCoordinator { diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs index 3074ee8320..d290d75b1f 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs @@ -5,8 +5,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Improbable.Collections; -using Improbable.Worker; +using Improbable.Worker.CInterop; namespace Improbable.WorkerCoordinator { @@ -118,17 +117,18 @@ public override void Run() { var connection = CoordinatorConnection.ConnectAndKeepAlive(Logger, ReceptionistHost, ReceptionistPort, CoordinatorWorkerId, CoordinatorWorkerType); - // Read worker flags. - Option devAuthTokenOpt = connection.GetWorkerFlag(DevAuthTokenWorkerFlag); - Option targetDeploymentOpt = connection.GetWorkerFlag(TargetDeploymentWorkerFlag); - int deploymentTotalNumSimulatedPlayers = int.Parse(GetWorkerFlagOrDefault(connection, DeploymentTotalNumSimulatedPlayersWorkerFlag, "100")); Logger.WriteLog("Waiting for target deployment to become ready."); var deploymentReadyTask = Task.Run(() => WaitForTargetDeploymentReady(connection)); if (!deploymentReadyTask.Wait(TimeSpan.FromMinutes(15))) { throw new TimeoutException("Timed out waiting for the deployment to be ready. Waited 15 minutes."); - } + } + + // Read worker flags. + string devAuthToken = connection.GetWorkerFlag(DevAuthTokenWorkerFlag); + string targetDeployment = connection.GetWorkerFlag(TargetDeploymentWorkerFlag); + int deploymentTotalNumSimulatedPlayers = int.Parse(GetWorkerFlagOrDefault(connection, DeploymentTotalNumSimulatedPlayersWorkerFlag, "100")); Logger.WriteLog($"Target deployment is ready. Starting {NumSimulatedPlayersToStart} simulated players."); Thread.Sleep(InitialStartDelayMillis); @@ -156,24 +156,24 @@ public override void Run() } Thread.Sleep(timeToSleep); - StartSimulatedPlayer(clientName, devAuthTokenOpt, targetDeploymentOpt); + StartSimulatedPlayer(clientName, devAuthToken, targetDeployment); } // Wait for all clients to exit. WaitForPlayersToExit(); } - private void StartSimulatedPlayer(string simulatedPlayerName, Option devAuthTokenOpt, Option targetDeploymentOpt) + private void StartSimulatedPlayer(string simulatedPlayerName, string devAuthToken, string targetDeployment) { try { // Pass in the dev auth token and the target deployment - if (devAuthTokenOpt.HasValue && targetDeploymentOpt.HasValue) + if (!String.IsNullOrEmpty(devAuthToken) && !String.IsNullOrEmpty(targetDeployment)) { string[] simulatedPlayerArgs = Util.ReplacePlaceholderArgs(SimulatedPlayerArgs, new Dictionary() { { SimulatedPlayerWorkerNamePlaceholderArg, simulatedPlayerName }, - { DevAuthTokenPlaceholderArg, devAuthTokenOpt.Value }, - { TargetDeploymentPlaceholderArg, targetDeploymentOpt.Value } + { DevAuthTokenPlaceholderArg, devAuthToken }, + { TargetDeploymentPlaceholderArg, targetDeployment } }); // Prepend the simulated player id as an argument to the start client script. @@ -198,10 +198,10 @@ private void StartSimulatedPlayer(string simulatedPlayerName, Option dev private static string GetWorkerFlagOrDefault(Connection connection, string flagName, string defaultValue) { - Option flagValue = connection.GetWorkerFlag(flagName); - if (flagValue.HasValue) + string flagValue = connection.GetWorkerFlag(flagName); + if (flagValue != null) { - return flagValue.Value; + return flagValue; } return defaultValue; @@ -211,8 +211,8 @@ private void WaitForTargetDeploymentReady(Connection connection) { while (true) { - var readyFlagOpt = connection.GetWorkerFlag(TargetDeploymentReadyWorkerFlag); - if (readyFlagOpt.HasValue && readyFlagOpt.Value.ToLower() == "true") + string readyFlag = connection.GetWorkerFlag(TargetDeploymentReadyWorkerFlag); + if (String.Compare(readyFlag, "true", true) == 0) { // Ready. break; diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/WorkerCoordinator.csproj b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/WorkerCoordinator.csproj index c85d28a535..d61e23cacf 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/WorkerCoordinator.csproj +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/WorkerCoordinator.csproj @@ -3,19 +3,19 @@ Debug - AnyCPU + x64 {C41625B0-CDB7-4480-B2E4-AEB27AF3B198} Exe Properties WorkerCoordinator WorkerCoordinator - v4.5.1 + v4.6.2 512 true - AnyCPU + x64 true full false @@ -45,12 +45,12 @@ - - ..\..\..\..\Binaries\ThirdParty\Improbable\Programs\worker_sdk\csharp\Improbable.Worker.dll - - - libimprobable_worker.so - PreserveNewest + + ..\..\..\..\Binaries\ThirdParty\Improbable\Programs\worker_sdk\csharp_cinterop\Improbable.Worker.CInterop.dll + + + libimprobable_worker.so + PreserveNewest diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WriteLinuxScript/WriteLinuxScript.csproj b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WriteLinuxScript/WriteLinuxScript.csproj index bc393ce218..c5e360bf38 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WriteLinuxScript/WriteLinuxScript.csproj +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WriteLinuxScript/WriteLinuxScript.csproj @@ -9,7 +9,7 @@ Properties WriteLinuxScript WriteLinuxScript - v4.5 + v4.6.2 512 true diff --git a/SpatialGDK/Content/Maps/Empty.umap b/SpatialGDK/Content/Maps/Empty.umap new file mode 100644 index 0000000000..3285772f5a Binary files /dev/null and b/SpatialGDK/Content/Maps/Empty.umap differ diff --git a/SpatialGDK/Content/SpatialDebugger/BP_SpatialDebugger.uasset b/SpatialGDK/Content/SpatialDebugger/BP_SpatialDebugger.uasset index daf8c9b2b8..efb4b598e5 100644 Binary files a/SpatialGDK/Content/SpatialDebugger/BP_SpatialDebugger.uasset and b/SpatialGDK/Content/SpatialDebugger/BP_SpatialDebugger.uasset differ diff --git a/SpatialGDK/Content/SpatialDebugger/Fonts/MuliFont.uasset b/SpatialGDK/Content/SpatialDebugger/Fonts/MuliFont.uasset new file mode 100644 index 0000000000..e7e0c3ff05 Binary files /dev/null and b/SpatialGDK/Content/SpatialDebugger/Fonts/MuliFont.uasset differ diff --git a/SpatialGDK/Content/SpatialDebugger/Materials/GlowingWireframeMaterial.uasset b/SpatialGDK/Content/SpatialDebugger/Materials/GlowingWireframeMaterial.uasset new file mode 100644 index 0000000000..d193284f6f Binary files /dev/null and b/SpatialGDK/Content/SpatialDebugger/Materials/GlowingWireframeMaterial.uasset differ diff --git a/SpatialGDK/Content/SpatialDebugger/Materials/TranslucentWorkerRegion.uasset b/SpatialGDK/Content/SpatialDebugger/Materials/TranslucentWorkerRegion.uasset index 24c20d0f35..191f4794c0 100644 Binary files a/SpatialGDK/Content/SpatialDebugger/Materials/TranslucentWorkerRegion.uasset and b/SpatialGDK/Content/SpatialDebugger/Materials/TranslucentWorkerRegion.uasset differ diff --git a/SpatialGDK/Content/SpatialDebugger/Materials/WorkerRegionCombinedMaterial.uasset b/SpatialGDK/Content/SpatialDebugger/Materials/WorkerRegionCombinedMaterial.uasset new file mode 100644 index 0000000000..b11d14995c Binary files /dev/null and b/SpatialGDK/Content/SpatialDebugger/Materials/WorkerRegionCombinedMaterial.uasset differ diff --git a/SpatialGDK/Content/SpatialDebugger/Textures/crosshair.uasset b/SpatialGDK/Content/SpatialDebugger/Textures/crosshair.uasset new file mode 100644 index 0000000000..7d6567743f Binary files /dev/null and b/SpatialGDK/Content/SpatialDebugger/Textures/crosshair.uasset differ diff --git a/SpatialGDK/Content/SpatialDebugger/WBP_SpatialDebuggerConfigMenu.uasset b/SpatialGDK/Content/SpatialDebugger/WBP_SpatialDebuggerConfigMenu.uasset new file mode 100644 index 0000000000..f378444465 Binary files /dev/null and b/SpatialGDK/Content/SpatialDebugger/WBP_SpatialDebuggerConfigMenu.uasset differ diff --git a/SpatialGDK/Extras/clang-format/.gitignore b/SpatialGDK/Extras/clang-format/.gitignore new file mode 100644 index 0000000000..f9be8dfe09 --- /dev/null +++ b/SpatialGDK/Extras/clang-format/.gitignore @@ -0,0 +1 @@ +!* diff --git a/SpatialGDK/Extras/clang-format/README.md b/SpatialGDK/Extras/clang-format/README.md new file mode 100644 index 0000000000..0c9f71f476 --- /dev/null +++ b/SpatialGDK/Extras/clang-format/README.md @@ -0,0 +1,42 @@ +# Introduction +[Clang-format](https://clang.llvm.org/docs/ClangFormat.html) is a tool to automatically format C/C++/Objective-C code. + +The GDK has a clang-format config file defined in `SpatialGDK/Source/.clang-format` which we run on all `.h/.cpp` files in the `SpatialGDK/Source` folder. + +We have a pre-commit hook to run `clang-format` against changed files (via git diff). We also recommend you setup your IDE to make sure that all code files are always formatted on save. + +The GDK uses version 11 of `clang-format`. Different clang-format versions are usually not backwards-compatible and will reformat code, sometimes in subtle ways. You should only use the submitted binary within the GDK to format your code. + +# Installation +You do not need to do any additional installation for the pre-commit hook to work. + +## Using with Visual Studio +Visual Studio needs to reference the `clang-format` binary in the GDK plugin folder. + +Go to `Tools` > `Options` > `Text Editor` > `C/C++` > `Formatting`. + +Tick the `Enable ClangFormat support` box. + +Hit `Browse` under `Use custom clang-format.exe file`, and navigate to the `clang-format` binary in the GDK folder, as shown. + +![Visual Studio example](visual-studio.png) + +Formatting a document in visual studio can be invoked by hitting the shortcut Ctrl+K Ctrl+D (you can keep Ctrl held down and then press K and then D). + +However, there is a Visual Studio extension called Format Document on Save which will format the document whenever it’s saved. + +## Using with Rider +In Rider, set up a file watcher for all C++ code files in the relevant modules (recursively). This will always execute on save. + +Go to `File` > `Settings` > `Tools` > `File Watchers`. + +Click the `+` icon in the top-right. + +In the `Scope` field, add a new Scope: + +![Rider watcher scope](rider-scope.png) + + +and fill out the rest of the watcher like this: + +![Rider watcher](rider-watcher.png) diff --git a/SpatialGDK/Extras/clang-format/mac/clang-format b/SpatialGDK/Extras/clang-format/mac/clang-format new file mode 100755 index 0000000000..cf170123e3 Binary files /dev/null and b/SpatialGDK/Extras/clang-format/mac/clang-format differ diff --git a/SpatialGDK/Extras/clang-format/rider-scope.png b/SpatialGDK/Extras/clang-format/rider-scope.png new file mode 100644 index 0000000000..2cfc501a95 Binary files /dev/null and b/SpatialGDK/Extras/clang-format/rider-scope.png differ diff --git a/SpatialGDK/Extras/clang-format/rider-watcher.png b/SpatialGDK/Extras/clang-format/rider-watcher.png new file mode 100644 index 0000000000..1fec943cfc Binary files /dev/null and b/SpatialGDK/Extras/clang-format/rider-watcher.png differ diff --git a/SpatialGDK/Extras/clang-format/visual-studio.png b/SpatialGDK/Extras/clang-format/visual-studio.png new file mode 100644 index 0000000000..a14937dfdb Binary files /dev/null and b/SpatialGDK/Extras/clang-format/visual-studio.png differ diff --git a/SpatialGDK/Extras/clang-format/win/clang-format.exe b/SpatialGDK/Extras/clang-format/win/clang-format.exe new file mode 100644 index 0000000000..8ce3861966 Binary files /dev/null and b/SpatialGDK/Extras/clang-format/win/clang-format.exe differ diff --git a/SpatialGDK/Extras/core-sdk.version b/SpatialGDK/Extras/core-sdk.version index e4ce12fcfb..f50da6c2a6 100644 --- a/SpatialGDK/Extras/core-sdk.version +++ b/SpatialGDK/Extras/core-sdk.version @@ -1 +1,2 @@ -14.6.1 +15.0.0 +// If changing version, update SpatialGDK/Source/SpatialGDK/Public/Utils/WorkerVersionCheck.h too diff --git a/SpatialGDK/Extras/git/post-merge b/SpatialGDK/Extras/git/post-merge index 5fc82097da..a4bd7a48c2 100755 --- a/SpatialGDK/Extras/git/post-merge +++ b/SpatialGDK/Extras/git/post-merge @@ -1,4 +1,12 @@ #!/usr/bin/env bash +set -e -u -o pipefail +[[ -n "${DEBUG:-}" ]] && set -x + changed_files="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)" -echo "${changed_files}" | grep --quiet "RequireSetup" && sh Setup.sh + +[[ "$(uname -s)" != "Darwin" ]] \ + && setup_command="exec cmd.exe /c Setup.bat SETUP_ARGS" \ + || setup_command="sh Setup.sh SETUP_ARGS" + +echo "${changed_files}" | grep --quiet RequireSetup && ${setup_command} diff --git a/SpatialGDK/Extras/git/pre-commit b/SpatialGDK/Extras/git/pre-commit new file mode 100644 index 0000000000..7e0fb09383 --- /dev/null +++ b/SpatialGDK/Extras/git/pre-commit @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -e -u -o pipefail +[[ -n "${DEBUG:-}" ]] && set -x + +if [[ "$(uname -s)" != "Darwin" ]]; then + clang_command="SpatialGDK/Extras/clang-format/win/clang-format" +else + clang_command="SpatialGDK/Extras/clang-format/mac/clang-format" +fi + +# Hit 'em with that clang-format +for FILE in $(git diff --cached --name-only --diff-filter=d); do + if [[ ( "${FILE}" == *.h ) || ( "${FILE}" == *.cpp ) ]]; then + ${clang_command} --verbose -i "${FILE}" + fi +done diff --git a/SpatialGDK/Extras/internal-documentation/release-process.md b/SpatialGDK/Extras/internal-documentation/release-process.md index b2723567fb..eb73ba9801 100644 --- a/SpatialGDK/Extras/internal-documentation/release-process.md +++ b/SpatialGDK/Extras/internal-documentation/release-process.md @@ -8,6 +8,7 @@ This document outlines the process for releasing a version of the GDK for Unreal ## Release 1. Notify `#dev-unreal-internal` that you intend to commence a release. Ask if anyone `@here` knows of any blocking defects in code or documentation that should be resolved prior to commencement of the release process. +1. Notify `@techwriters` in #docs that they may commence their [CHANGELOG review process](https://improbableio.atlassian.net/l/c/4FsZzbHk). 1. If nobody objects to the release, navigate to [unrealgdk-release](https://buildkite.com/improbable/unrealgdk-release/) and select the New Build button. 1. In the Message field type "Releasing [GDK release version]". 1. The "Commit" field is prepopulated with `HEAD`, leave it as is. diff --git a/SpatialGDK/Extras/schema/authority_intent.schema b/SpatialGDK/Extras/schema/authority_intent.schema index e6df0b92ed..cacc8c111b 100644 --- a/SpatialGDK/Extras/schema/authority_intent.schema +++ b/SpatialGDK/Extras/schema/authority_intent.schema @@ -6,4 +6,4 @@ component AuthorityIntent { // Id assigned to the Unreal server worker which should be authoritative for this entity. // 0 is reserved as an invalid/unset value. uint32 virtual_worker_id = 1; -} +} \ No newline at end of file diff --git a/SpatialGDK/Extras/schema/component_presence.schema b/SpatialGDK/Extras/schema/component_presence.schema deleted file mode 100644 index 7465da5e90..0000000000 --- a/SpatialGDK/Extras/schema/component_presence.schema +++ /dev/null @@ -1,13 +0,0 @@ -// 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/debug_component.schema b/SpatialGDK/Extras/schema/debug_component.schema new file mode 100644 index 0000000000..1fe4d96048 --- /dev/null +++ b/SpatialGDK/Extras/schema/debug_component.schema @@ -0,0 +1,8 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +package unreal; + +component DebugComponent { + id = 9995; + option worker_id_delegation = 1; + string actor_tags = 2; +} \ No newline at end of file diff --git a/SpatialGDK/Extras/schema/debug_metrics.schema b/SpatialGDK/Extras/schema/debug_metrics.schema index e4256f04bf..80fef2824d 100644 --- a/SpatialGDK/Extras/schema/debug_metrics.schema +++ b/SpatialGDK/Extras/schema/debug_metrics.schema @@ -13,4 +13,4 @@ component DebugMetrics { command Void start_rpc_metrics(Void); command Void stop_rpc_metrics(Void); command Void modify_spatial_settings(ModifySettingPayload); -} +} \ No newline at end of file diff --git a/SpatialGDK/Extras/schema/heartbeat.schema b/SpatialGDK/Extras/schema/heartbeat.schema index ae33dc0940..dd3df9849a 100644 --- a/SpatialGDK/Extras/schema/heartbeat.schema +++ b/SpatialGDK/Extras/schema/heartbeat.schema @@ -8,4 +8,4 @@ component Heartbeat { id = 9991; event HeartbeatEvent heartbeat; bool client_has_quit = 1; -} +} \ No newline at end of file diff --git a/SpatialGDK/Extras/schema/known_entity_auth_component_set.schema b/SpatialGDK/Extras/schema/known_entity_auth_component_set.schema new file mode 100644 index 0000000000..0d65f9889e --- /dev/null +++ b/SpatialGDK/Extras/schema/known_entity_auth_component_set.schema @@ -0,0 +1,23 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +package unreal; + +import "improbable/standard_library.schema"; +import "unreal/gdk/spawner.schema"; +import "unreal/gdk/global_state_manager.schema"; +import "unreal/gdk/virtual_worker_translation.schema"; +import "unreal/gdk/server_worker.schema"; + +component_set KnownEntityAuthComponentSet { + id = 9905; + components = [ + improbable.Position, + improbable.Metadata, + improbable.Interest, + unreal.PlayerSpawner, + unreal.DeploymentMap, + unreal.StartupActorManager, + unreal.GSMShutdown, + unreal.VirtualWorkerTranslation, + unreal.ServerWorker + ]; +} diff --git a/SpatialGDK/Extras/schema/migration_diagnostic.schema b/SpatialGDK/Extras/schema/migration_diagnostic.schema new file mode 100644 index 0000000000..87dfbe253a --- /dev/null +++ b/SpatialGDK/Extras/schema/migration_diagnostic.schema @@ -0,0 +1,39 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +package unreal; + +type MigrationDiagnosticRequest {} + +type MigrationDiagnosticResponse +{ + // Id of the worker which is authoritative for the blocked actor. + uint32 authoritative_worker_id = 1; + + // The entity id of the blocked actor + EntityId actor_entity_id = 2; + + // Whether or not the blocked actor is replicated. + bool actor_replicated = 3; + + // Whether or not the blocked actor has authority. + bool actor_authority = 4; + + // Whether or not the blocked actor is locked. + bool actor_locked = 5; + + // Whether or not the authoritative worker thinks the blocked actor should migrate. + bool actor_evaluation = 6; + + // The id of the worker the blocked actor should migrate to + uint32 destination_worker_id = 7; + + // The entity id of the owner of the actor that cannot migrate + EntityId owner_entity_id = 8; + +} + +component MigrationDiagnostic +{ + id = 9969; + + command MigrationDiagnosticResponse diagnostic(MigrationDiagnosticRequest); +} diff --git a/SpatialGDK/Extras/schema/net_owning_client_worker.schema b/SpatialGDK/Extras/schema/net_owning_client_worker.schema index 9d15ce029f..693121d414 100644 --- a/SpatialGDK/Extras/schema/net_owning_client_worker.schema +++ b/SpatialGDK/Extras/schema/net_owning_client_worker.schema @@ -6,10 +6,11 @@ package unreal; 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; -} + // The partition_entity_id is an optional entity ID string that is set + // when the relevant Actor or subobject becomes net-owned by a client + // connection. This entity ID is the partition entity ID that the client + // worker claims in order to gain authority over entity components in the + // world. Currently, this will always be the PlayerController spawned during + // the connection. + option partition_entity_id = 1; +} \ No newline at end of file diff --git a/SpatialGDK/Extras/schema/not_streamed.schema b/SpatialGDK/Extras/schema/not_streamed.schema index ca48e49c1f..3331bc4c01 100644 --- a/SpatialGDK/Extras/schema/not_streamed.schema +++ b/SpatialGDK/Extras/schema/not_streamed.schema @@ -4,4 +4,4 @@ package unreal; // Actor that is a part of the PersistentLevel or created with SpawnActor component NotStreamed { id = 9986; -} +} \ No newline at end of file diff --git a/SpatialGDK/Extras/schema/partition_shadow.schema b/SpatialGDK/Extras/schema/partition_shadow.schema new file mode 100644 index 0000000000..c583bf814b --- /dev/null +++ b/SpatialGDK/Extras/schema/partition_shadow.schema @@ -0,0 +1,9 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +package unreal; + +// This is created on Partition entities to shadow the improbable.restricted.Partition component, which are not +// saved to snapshots, so that we can query for Partition entities when loading from a snapshot and reconcile +// the saved AuthorityDelegation and VirtualWorkerTranslation component data. +component PartitionShadow { + id = 9967; +} diff --git a/SpatialGDK/Extras/schema/query_tags.schema b/SpatialGDK/Extras/schema/query_tags.schema new file mode 100644 index 0000000000..6aa9e1f57c --- /dev/null +++ b/SpatialGDK/Extras/schema/query_tags.schema @@ -0,0 +1,20 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +package unreal; + +// System query tags for entity completeness + +component ActorAuthTag { + id = 2001; +} + +component ActorNonAuthTag { + id = 2002; +} + +component LBTag { + id = 2005; +} + +component GDKKnownEntityTag { + id = 2007; +} diff --git a/SpatialGDK/Extras/schema/relevant.schema b/SpatialGDK/Extras/schema/relevant.schema index 33a0779d33..b89c924db0 100644 --- a/SpatialGDK/Extras/schema/relevant.schema +++ b/SpatialGDK/Extras/schema/relevant.schema @@ -5,6 +5,14 @@ component AlwaysRelevant { id = 9983; } +component ServerOnlyAlwaysRelevant { + id = 9968; +} + component Dormant { id = 9981; } + +component Visible { + id = 9970; +} diff --git a/SpatialGDK/Extras/schema/rpc_components.schema b/SpatialGDK/Extras/schema/rpc_components.schema index f1d85fb574..e993892b4a 100644 --- a/SpatialGDK/Extras/schema/rpc_components.schema +++ b/SpatialGDK/Extras/schema/rpc_components.schema @@ -4,35 +4,7 @@ package unreal; import "unreal/gdk/core_types.schema"; import "unreal/gdk/rpc_payload.schema"; -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; -} - -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; -} - component UnrealServerToServerCommandEndpoint { id = 9973; command Void server_to_server_rpc_command(UnrealRPCPayload); } - -component UnrealMulticastRPCEndpointLegacy { - id = 9987; - event UnrealRPCPayload unreliable_multicast_rpc; -} - -// Component that contains a list of RPCs to be executed -// as a part of entity creation request -component RPCsOnEntityCreation { - id = 9985; - list rpcs = 1; - command Void clear_rpcs(Void); -} - diff --git a/SpatialGDK/Extras/schema/server_worker.schema b/SpatialGDK/Extras/schema/server_worker.schema index d921635897..6cce00a40c 100644 --- a/SpatialGDK/Extras/schema/server_worker.schema +++ b/SpatialGDK/Extras/schema/server_worker.schema @@ -7,7 +7,7 @@ import "unreal/gdk/spawner.schema"; type ForwardSpawnPlayerRequest { SpawnPlayerRequest spawn_player_request = 1; UnrealObjectRef player_start = 2; - string client_worker_id = 3; + EntityId client_system_entity_id = 3; } type ForwardSpawnPlayerResponse { @@ -18,5 +18,6 @@ component ServerWorker { id = 9974; string worker_name = 1; bool ready_to_begin_play = 2; + EntityId server_system_entity_id = 3; command ForwardSpawnPlayerResponse forward_spawn_player(ForwardSpawnPlayerRequest); } diff --git a/SpatialGDK/Extras/schema/spatial_debugging.schema b/SpatialGDK/Extras/schema/spatial_debugging.schema index 096339f947..ff9f6adecd 100644 --- a/SpatialGDK/Extras/schema/spatial_debugging.schema +++ b/SpatialGDK/Extras/schema/spatial_debugging.schema @@ -10,7 +10,7 @@ component SpatialDebugging { // 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; @@ -20,4 +20,4 @@ component SpatialDebugging { // Whether or not the entity is locked. bool is_locked = 5; -} +} \ No newline at end of file diff --git a/SpatialGDK/Extras/schema/spawndata.schema b/SpatialGDK/Extras/schema/spawndata.schema index 369464ad02..7f5de68b13 100644 --- a/SpatialGDK/Extras/schema/spawndata.schema +++ b/SpatialGDK/Extras/schema/spawndata.schema @@ -22,4 +22,4 @@ component SpawnData { Rotator rotation = 2; Vector3f scale = 3; Vector3f velocity = 4; -} +} \ No newline at end of file diff --git a/SpatialGDK/Extras/schema/spawner.schema b/SpatialGDK/Extras/schema/spawner.schema index 7f671d1335..493f8baef0 100644 --- a/SpatialGDK/Extras/schema/spawner.schema +++ b/SpatialGDK/Extras/schema/spawner.schema @@ -6,6 +6,7 @@ type SpawnPlayerRequest { bytes unique_id = 2; string online_platform_name = 3; bool simulated = 4; + EntityId client_system_entity_id = 5; } type SpawnPlayerResponse { } @@ -13,4 +14,4 @@ type SpawnPlayerResponse { } component PlayerSpawner { id = 9998; command SpawnPlayerResponse spawn_player(SpawnPlayerRequest); -} +} \ No newline at end of file diff --git a/SpatialGDK/Extras/schema/tombstone.schema b/SpatialGDK/Extras/schema/tombstone.schema index 17e3683357..8b83f3f178 100644 --- a/SpatialGDK/Extras/schema/tombstone.schema +++ b/SpatialGDK/Extras/schema/tombstone.schema @@ -3,4 +3,4 @@ package unreal; component Tombstone { id = 9982; -} +} \ No newline at end of file diff --git a/SpatialGDK/Extras/schema/unreal_metadata.schema b/SpatialGDK/Extras/schema/unreal_metadata.schema index beb648c876..46e5e8f11b 100644 --- a/SpatialGDK/Extras/schema/unreal_metadata.schema +++ b/SpatialGDK/Extras/schema/unreal_metadata.schema @@ -8,4 +8,4 @@ component UnrealMetadata { option stably_named_ref = 1; // Exists when entity represents a stably named Actor (RF_WasLoaded) string class_path = 2; option net_startup = 3; // Exists only when entity has a stably_named_ref -} +} \ No newline at end of file diff --git a/SpatialGDK/Extras/schema/virtual_worker_translation.schema b/SpatialGDK/Extras/schema/virtual_worker_translation.schema index 1bfb48055e..f682ae62e2 100644 --- a/SpatialGDK/Extras/schema/virtual_worker_translation.schema +++ b/SpatialGDK/Extras/schema/virtual_worker_translation.schema @@ -10,9 +10,10 @@ type VirtualWorkerMapping { uint32 virtual_worker_id = 1; string physical_worker_name = 2; EntityId server_worker_entity = 3; + EntityId partition_id = 4; } component VirtualWorkerTranslation { id = 9979; - transient list virtual_worker_mapping = 1; -} + list virtual_worker_mapping = 1; +} \ No newline at end of file diff --git a/SpatialGDK/Extras/spot.version b/SpatialGDK/Extras/spot.version deleted file mode 100644 index dbe4d3c059..0000000000 --- a/SpatialGDK/Extras/spot.version +++ /dev/null @@ -1 +0,0 @@ -20191029.144741.87a7d78768 diff --git a/SpatialGDK/Extras/templates/WorkerJsonTemplate.json b/SpatialGDK/Extras/templates/WorkerJsonTemplate.json index 25374a8baf..b12fbbc372 100644 --- a/SpatialGDK/Extras/templates/WorkerJsonTemplate.json +++ b/SpatialGDK/Extras/templates/WorkerJsonTemplate.json @@ -18,17 +18,6 @@ } ] }, - "bridge": { - "worker_attribute_set": { - "attributes": [ - "{{WorkerTypeName}}" - ] - }, - "component_delivery": { - "default": "RELIABLE_ORDERED", - "checkout_all_initially": true - } - }, "managed": { "windows": { "artifact_name": "UnrealEditor@Windows.zip", @@ -42,17 +31,17 @@ "-nopause", "-messaging", "-SaveToUserDir", - "+appName", + "-appName", "${IMPROBABLE_PROJECT_NAME}", - "+receptionistHost", + "-receptionistHost", "${IMPROBABLE_RECEPTIONIST_HOST}", - "+receptionistPort", + "-receptionistPort", "${IMPROBABLE_RECEPTIONIST_PORT}", - "+workerType", + "-workerType", "${IMPROBABLE_WORKER_NAME}", - "+workerId", + "-workerId", "${IMPROBABLE_WORKER_ID}", - "+linkProtocol", + "-linkProtocol", "Tcp", "-abslog=${IMPROBABLE_LOG_FILE}", "-NoVerifyGC" @@ -64,17 +53,17 @@ "arguments": [ "${IMPROBABLE_WORKER_ID}", "${IMPROBABLE_LOG_FILE}", - "+appName", + "-appName", "${IMPROBABLE_PROJECT_NAME}", - "+receptionistHost", + "-receptionistHost", "${IMPROBABLE_RECEPTIONIST_HOST}", - "+receptionistPort", + "-receptionistPort", "${IMPROBABLE_RECEPTIONIST_PORT}", - "+workerType", + "-workerType", "${IMPROBABLE_WORKER_NAME}", - "+workerId", + "-workerId", "${IMPROBABLE_WORKER_ID}", - "+linkProtocol", + "-linkProtocol", "Tcp", "-NoVerifyGC" ] diff --git a/SpatialGDK/Source/.clang-format b/SpatialGDK/Source/.clang-format new file mode 100644 index 0000000000..8eb77b9eec --- /dev/null +++ b/SpatialGDK/Source/.clang-format @@ -0,0 +1,132 @@ +Language: Cpp +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Right +AlignOperands: true +AlignTrailingComments: true +AllowAllConstructorInitializersOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: false +AllowShortLambdasOnASingleLine: Empty +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: true +BinPackParameters: true +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Allman +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeComma +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 140 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: false +DerivePointerAlignment: false +DisableFormat: false +FixNamespaceComments: true +ForEachMacros: + - for +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '.*\.generated\.h' + Priority: 100 + - Regex: '.*(PCH).*' + Priority: -1 + - Regex: '".*"' + Priority: 1 + - Regex: '^<.*\.(h)>' + Priority: 3 + - Regex: '^<.*>' + Priority: 4 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: false +IndentPPDirectives: None +IndentWidth: 4 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Never +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + CanonicalDelimiter: '' +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 4 +UseTab: Always diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/ActorInterestComponent.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/ActorInterestComponent.cpp index 21a7928c9e..29b1ab0bb5 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/ActorInterestComponent.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/ActorInterestComponent.cpp @@ -2,10 +2,11 @@ #include "EngineClasses/Components/ActorInterestComponent.h" -#include "Schema/Interest.h" #include "Interop/SpatialClassInfoManager.h" +#include "Schema/Interest.h" -void UActorInterestComponent::PopulateFrequencyToConstraintsMap(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::FrequencyToConstraintsMap& OutFrequencyToQueryConstraints) 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. diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/SpatialPingComponent.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/SpatialPingComponent.cpp index 6c56cd8408..c534f8f3d3 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/SpatialPingComponent.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/SpatialPingComponent.cpp @@ -6,6 +6,7 @@ #include "GameFramework/PlayerController.h" #include "Kismet/GameplayStatics.h" #include "Net/UnrealNetwork.h" +#include "Runtime/Launch/Resources/Version.h" #include "TimerManager.h" DEFINE_LOG_CATEGORY(LogSpatialPingComponent); @@ -38,7 +39,9 @@ void USpatialPingComponent::BeginPlay() OwningController = Cast(GetOwner()); if (OwningController == nullptr) { - UE_LOG(LogSpatialPingComponent, Warning, TEXT("SpatialPingComponent did not find a valid owning PlayerController and will not function correctly. Ensure this component is only attached to a PlayerController.")); + UE_LOG(LogSpatialPingComponent, Warning, + TEXT("SpatialPingComponent did not find a valid owning PlayerController and will not function correctly. Ensure this " + "component is only attached to a PlayerController.")); } if (bStartWithPingEnabled) @@ -92,14 +95,16 @@ void USpatialPingComponent::EnablePing() { if (UGameplayStatics::GetGlobalTimeDilation(World) != 1.f) { - UE_LOG(LogSpatialPingComponent, Warning, TEXT("Global time dilation is not 1. This will affect the rate at which SpatialPingComponent sends ping requests.")); + UE_LOG(LogSpatialPingComponent, Warning, + TEXT("Global time dilation is not 1. This will affect the rate at which SpatialPingComponent sends ping requests.")); } LastSentPingID = 0; TimeoutCount = 0; // Send a new ping, which will trigger a self-perpetuating sequence via timers. SendNewPing(); - // Set looping timer to 'tick' this component, it doesn't send any pings but matches the MinPingInterval for passing updates to the owning controller. + // Set looping timer to 'tick' this component, it doesn't send any pings but matches the MinPingInterval for passing updates to the + // owning controller. World->GetTimerManager().SetTimer(PingTickHandle, this, &USpatialPingComponent::TickPingComponent, MinPingInterval, true); bIsPingEnabled = true; } @@ -151,7 +156,8 @@ void USpatialPingComponent::OnPingTimeout() void USpatialPingComponent::OnRep_ReplicatedPingID() { - // Check that replicated ping ID matches the last sent AND check ping is enabled to catch cases where a value is replicated after being locally disabled. + // Check that replicated ping ID matches the last sent AND check ping is enabled to catch cases where a value is replicated after being + // locally disabled. if (ReplicatedPingID == LastSentPingID && bIsPingEnabled) { UWorld* World = GetWorld(); diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp index 9a2b4ed47f..9ffe92bf90 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp @@ -14,21 +14,21 @@ #include "Settings/LevelEditorPlaySettings.h" #endif -#include "EngineStats.h" #include "EngineClasses/SpatialNetConnection.h" #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" +#include "EngineStats.h" +#include "Interop/Connection/SpatialEventTracer.h" #include "Interop/GlobalStateManager.h" #include "Interop/SpatialReceiver.h" #include "Interop/SpatialSender.h" #include "LoadBalancing/AbstractLBStrategy.h" -#include "Schema/ClientRPCEndpointLegacy.h" #include "Schema/NetOwningClientWorker.h" -#include "Schema/ServerRPCEndpointLegacy.h" #include "SpatialConstants.h" #include "SpatialGDKSettings.h" #include "Utils/GDKPropertyMacros.h" #include "Utils/RepLayoutUtils.h" +#include "Utils/SchemaOption.h" #include "Utils/SpatialActorUtils.h" DEFINE_LOG_CATEGORY(LogSpatialActorChannel); @@ -38,8 +38,6 @@ DECLARE_CYCLE_STAT(TEXT("UpdateSpatialPosition"), STAT_SpatialActorChannelUpdate 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 @@ -62,7 +60,7 @@ void UpdateChangelistHistory(TUniquePtr& RepState) { const int32 HistoryIndex = i % MaxSendingChangeHistory; - FRepChangedHistory & HistoryItem = SendingRepState->ChangeHistory[HistoryIndex]; + FRepChangedHistory& HistoryItem = SendingRepState->ChangeHistory[HistoryIndex]; // All active history items should contain a change list check(HistoryItem.Changed.Num() > 0); @@ -91,7 +89,7 @@ bool FSpatialObjectRepState::MoveMappedObjectToUnmapped_r(const FUnrealObjectRef { FObjectReferences& ObjReferences = ObjReferencePair.Value; - if (ObjReferences.Array != NULL) + if (ObjReferences.Array.IsValid()) { if (MoveMappedObjectToUnmapped_r(ObjRef, *ObjReferences.Array)) { @@ -121,9 +119,10 @@ bool FSpatialObjectRepState::MoveMappedObjectToUnmapped(const FUnrealObjectRef& return false; } -void FSpatialObjectRepState::GatherObjectRef(TSet& OutReferenced, TSet& OutUnresolved, const FObjectReferences& CurReferences) const +void FSpatialObjectRepState::GatherObjectRef(TSet& OutReferenced, TSet& OutUnresolved, + const FObjectReferences& CurReferences) const { - if (CurReferences.Array) + if (CurReferences.Array.IsValid()) { for (auto const& Entry : *CurReferences.Array) { @@ -144,7 +143,7 @@ void FSpatialObjectRepState::UpdateRefToRepStateMap(FObjectToRepStateMap& RepSta // Inspired by FObjectReplicator::UpdateGuidToReplicatorMap UnresolvedRefs.Empty(); - TSet< FUnrealObjectRef > LocalReferencedObj; + TSet LocalReferencedObj; for (auto& Entry : ReferenceMap) { GatherObjectRef(LocalReferencedObj, UnresolvedRefs, Entry.Value); @@ -212,9 +211,9 @@ void USpatialActorChannel::Init(UNetConnection* InConnection, int32 ChannelIndex LastPositionSinceUpdate = FVector::ZeroVector; TimeWhenPositionLastUpdated = 0.0; AuthorityReceivedTimestamp = 0; + bNeedOwnerInterestUpdate = false; PendingDynamicSubobjects.Empty(); - SavedConnectionOwningWorkerId.Empty(); SavedInterestBucketComponentID = SpatialConstants::INVALID_COMPONENT_ID; FramesTillDormancyAllowed = 0; @@ -240,7 +239,7 @@ void USpatialActorChannel::RetireEntityIfAuthoritative() return; } - const bool bHasAuthority = NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialGDK::Position::ComponentId); + const bool bHasAuthority = NetDriver->HasServerAuthority(EntityId); if (Actor != nullptr) { if (bHasAuthority) @@ -263,13 +262,15 @@ void USpatialActorChannel::RetireEntityIfAuthoritative() else if (bCreatedEntity) // We have not gained authority yet { Actor->SetReplicates(false); - Receiver->RetireWhenAuthoritive(EntityId, NetDriver->ClassInfoManager->GetComponentIdForClass(*Actor->GetClass()), Actor->IsNetStartupActor(), Actor->GetTearOff()); // Ensure we don't recreate the actor + Receiver->RetireWhenAuthoritive(EntityId, NetDriver->ClassInfoManager->GetComponentIdForClass(*Actor->GetClass()), + Actor->IsNetStartupActor(), Actor->GetTearOff()); // Ensure we don't recreate the actor } } else { // This is unsupported, and shouldn't happen, don't attempt to cleanup entity to better indicate something has gone wrong - UE_LOG(LogSpatialActorChannel, Error, TEXT("RetireEntityIfAuthoritative called on actor channel with null actor - entity id (%lld)"), EntityId); + UE_LOG(LogSpatialActorChannel, Error, + TEXT("RetireEntityIfAuthoritative called on actor channel with null actor - entity id (%lld)"), EntityId); } } @@ -280,10 +281,8 @@ bool USpatialActorChannel::CleanUp(const bool bForDestroy, EChannelCloseReason C #if WITH_EDITOR const bool bDeleteDynamicEntities = GetDefault()->GetDeleteDynamicEntities(); - if (bDeleteDynamicEntities && - NetDriver->IsServer() && - NetDriver->GetActorChannelByEntityId(EntityId) != nullptr && - CloseReason != EChannelCloseReason::Dormancy) + if (bDeleteDynamicEntities && NetDriver->IsServer() && NetDriver->GetActorChannelByEntityId(EntityId) != nullptr + && CloseReason != EChannelCloseReason::Dormancy) { // If we're a server worker, and the entity hasn't already been cleaned up, delete it on shutdown. RetireEntityIfAuthoritative(); @@ -302,13 +301,12 @@ bool USpatialActorChannel::CleanUp(const bool bForDestroy, EChannelCloseReason C if (CloseReason == EChannelCloseReason::Destroyed || CloseReason == EChannelCloseReason::LevelUnloaded) { - Receiver->ClearPendingRPCs(EntityId); + NetDriver->GetRPCService()->ClearPendingRPCs(EntityId); Sender->ClearPendingRPCs(EntityId); } NetDriver->RemoveActorChannel(EntityId, *this); } - return UActorChannel::CleanUp(bForDestroy, CloseReason); } @@ -366,23 +364,17 @@ void USpatialActorChannel::UpdateShadowData() for (UActorComponent* ActorComponent : Actor->GetReplicatedComponents()) { FObjectReplicator& ComponentReplicator = FindOrCreateReplicator(ActorComponent).Get(); - ResetShadowData(*ComponentReplicator.RepLayout, ComponentReplicator.ChangelistMgr->GetRepChangelistState()->StaticBuffer, ActorComponent); - } -} - -void USpatialActorChannel::UpdateSpatialPositionWithFrequencyCheck() -{ - // Check that there has been a sufficient amount of time since the last update. - if ((NetDriver->GetElapsedTime() - TimeWhenPositionLastUpdated) >= (1.0f / GetDefault()->PositionUpdateFrequency)) - { - UpdateSpatialPosition(); + ResetShadowData(*ComponentReplicator.RepLayout, ComponentReplicator.ChangelistMgr->GetRepChangelistState()->StaticBuffer, + ActorComponent); } } FRepChangeState USpatialActorChannel::CreateInitialRepChangeState(TWeakObjectPtr Object) { checkf(Object != nullptr, TEXT("Attempted to create initial rep change state on an object which is null.")); - checkf(!Object->IsPendingKill(), TEXT("Attempted to create initial rep change state on an object which is pending kill. This will fail to create a RepLayout: "), *Object->GetName()); + checkf(!Object->IsPendingKill(), + TEXT("Attempted to create initial rep change state on an object which is pending kill. This will fail to create a RepLayout: "), + *Object->GetName()); FObjectReplicator& Replicator = FindOrCreateReplicator(Object.Get()).Get(); @@ -430,6 +422,29 @@ FHandoverChangeState USpatialActorChannel::CreateInitialHandoverChangeState(cons return HandoverChanged; } +void USpatialActorChannel::UpdateVisibleComponent(AActor* InActor) +{ + // Make sure that the InActor is not a PlayerController, GameplayDebuggerCategoryReplicator or GameMode. + if (SpatialGDK::DoesActorClassIgnoreVisibilityCheck(InActor)) + { + return; + } + + // Unreal applies the following rules (in order) in determining the relevant set of Actors for a player: + // If the Actor is hidden (bHidden == true) and the root component does not collide then the Actor is not relevant. + // We apply the same rules to add/remove the Visible component to an actor that determines if clients will checkout the actor or + // not. Make sure that the Actor is also not always relevant. + if (InActor->IsHidden() && (!InActor->GetRootComponent() || !InActor->GetRootComponent()->IsCollisionEnabled()) + && !InActor->bAlwaysRelevant) + { + NetDriver->RefreshActorVisibility(InActor, false); + } + else + { + NetDriver->RefreshActorVisibility(InActor, true); + } +} + int64 USpatialActorChannel::ReplicateActor() { SCOPE_CYCLE_COUNTER(STAT_SpatialActorChannelReplicateActor); @@ -455,7 +470,11 @@ int64 USpatialActorChannel::ReplicateActor() // Group actors by exact class, one level below parent native class. SCOPE_CYCLE_UOBJECT(ReplicateActor, Actor); +#if ENGINE_MINOR_VERSION >= 26 + const bool bReplay = ActorWorld && ActorWorld->GetDemoNetDriver() == Connection->GetDriver(); +#else const bool bReplay = ActorWorld && ActorWorld->DemoNetDriver == Connection->GetDriver(); +#endif ////////////////////////////////////////////////////////////////////////// // Begin - error and stat duplication from DataChannel::ReplicateActor() @@ -464,7 +483,8 @@ int64 USpatialActorChannel::ReplicateActor() GNumReplicateActorCalls++; } - // triggering replication of an Actor while already in the middle of replication can result in invalid data being sent and is therefore illegal + // 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())); @@ -531,9 +551,11 @@ int64 USpatialActorChannel::ReplicateActor() #endif RepFlags.bReplay = bReplay; - UE_LOG(LogNetTraffic, Log, TEXT("Replicate %s, bNetInitial: %d, bNetOwner: %d"), *Actor->GetName(), RepFlags.bNetInitial, RepFlags.bNetOwner); + UE_LOG(LogNetTraffic, Log, TEXT("Replicate %s, bNetInitial: %d, bNetOwner: %d"), *Actor->GetName(), RepFlags.bNetInitial, + RepFlags.bNetOwner); - FMemMark MemMark(FMemStack::Get()); // The calls to ReplicateProperties will allocate memory on FMemStack::Get(), and use it in ::PostSendBunch. we free it below + FMemMark MemMark(FMemStack::Get()); // The calls to ReplicateProperties will allocate memory on FMemStack::Get(), and use it in + // ::PostSendBunch. we free it below // ---------------------------------------------------------- // Replicate Actor and Component properties and RPCs @@ -554,14 +576,38 @@ int64 USpatialActorChannel::ReplicateActor() } else { - UpdateSpatialPositionWithFrequencyCheck(); + UpdateSpatialPosition(); } } + if (Actor->GetIsHiddenDirty()) + { + UpdateVisibleComponent(Actor); + Actor->SetIsHiddenDirty(false); + } + // Update the replicated property change list. FRepChangelistState* ChangelistState = ActorReplicator->ChangelistMgr->GetRepChangelistState(); - ActorReplicator->RepLayout->UpdateChangelistMgr(ActorReplicator->RepState->GetSendingRepState(), *ActorReplicator->ChangelistMgr, Actor, Connection->Driver->ReplicationFrame, RepFlags, bForceCompareProperties); +#if ENGINE_MINOR_VERSION >= 26 + const ERepLayoutResult UpdateResult = + ActorReplicator->RepLayout->UpdateChangelistMgr(ActorReplicator->RepState->GetSendingRepState(), *ActorReplicator->ChangelistMgr, + Actor, Connection->Driver->ReplicationFrame, RepFlags, bForceCompareProperties); + + if (UNLIKELY(ERepLayoutResult::FatalError == UpdateResult)) + { + // This happens when a replicated array is over the maximum size (UINT16_MAX). + // Native Unreal just closes the connection at this point, but we can't do that as + // it may lead to unexpected consequences for the deployment. Instead, we just early out. + // TODO: UNR-4667 - Investigate this behavior in more detail. + + // Connection->SetPendingCloseDueToReplicationFailure(); + return 0; + } +#else + ActorReplicator->RepLayout->UpdateChangelistMgr(ActorReplicator->RepState->GetSendingRepState(), *ActorReplicator->ChangelistMgr, Actor, + Connection->Driver->ReplicationFrame, RepFlags, bForceCompareProperties); +#endif FSendingRepState* SendingRepState = ActorReplicator->RepState->GetSendingRepState(); const int32 PossibleNewHistoryIndex = SendingRepState->HistoryEnd % MaxSendingChangeHistory; @@ -581,7 +627,8 @@ int64 USpatialActorChannel::ReplicateActor() } else { - UE_LOG(LogSpatialActorChannel, Warning, TEXT("EntityId: %lld Actor: %s Changelist with index %d has no changed items"), EntityId, *Actor->GetName(), i); + UE_LOG(LogSpatialActorChannel, Warning, TEXT("EntityId: %lld Actor: %s Changelist with index %d has no changed items"), + EntityId, *Actor->GetName(), i); } } @@ -598,6 +645,12 @@ int64 USpatialActorChannel::ReplicateActor() ReplicationBytesWritten = 0; + if (!bCreatingNewEntity && NeedOwnerInterestUpdate() && NetDriver->InterestFactory->DoOwnersHaveEntityId(Actor)) + { + Sender->UpdateInterestComponent(Actor); + SetNeedOwnerInterestUpdate(false); + } + // If any properties have changed, send a component update. if (bCreatingNewEntity || RepChanged.Num() > 0 || HandoverChangeState.Num() > 0) { @@ -677,14 +730,16 @@ int64 USpatialActorChannel::ReplicateActor() TSharedRef>* SubobjectHandoverShadowData = HandoverShadowDataMap.Find(Subobject); if (SubobjectHandoverShadowData == nullptr) { - UE_LOG(LogSpatialActorChannel, Warning, TEXT("EntityId: %lld Actor: %s HandoverShadowData not found for Subobject %s"), EntityId, *Actor->GetName(), *Subobject->GetName()); + UE_LOG(LogSpatialActorChannel, Warning, TEXT("EntityId: %lld Actor: %s HandoverShadowData not found for Subobject %s"), + EntityId, *Actor->GetName(), *Subobject->GetName()); continue; } FHandoverChangeState SubobjectHandoverChangeState = GetHandoverChangeList(SubobjectHandoverShadowData->Get(), Subobject); if (SubobjectHandoverChangeState.Num() > 0) { - Sender->SendComponentUpdates(Subobject, SubobjectInfo, this, nullptr, &SubobjectHandoverChangeState, ReplicationBytesWritten); + Sender->SendComponentUpdates(Subobject, SubobjectInfo, this, nullptr, &SubobjectHandoverChangeState, + ReplicationBytesWritten); } } @@ -697,9 +752,10 @@ int64 USpatialActorChannel::ReplicateActor() if (ObjectRef.IsValid()) { - OnSubobjectDeleted(ObjectRef, RepComp.Key()); + OnSubobjectDeleted(ObjectRef, RepComp.Key(), RepComp.Value()->GetWeakObjectPtr()); - Sender->SendRemoveComponentForClassInfo(EntityId, NetDriver->ClassInfoManager->GetClassInfoByComponentId(ObjectRef.Offset)); + Sender->SendRemoveComponentForClassInfo(EntityId, + NetDriver->ClassInfoManager->GetClassInfoByComponentId(ObjectRef.Offset)); } RepComp.Value()->CleanUp(); @@ -719,7 +775,7 @@ int64 USpatialActorChannel::ReplicateActor() bIsReplicatingActor = false; - bForceCompareProperties = false; // Only do this once per frame when set + bForceCompareProperties = false; // Only do this once per frame when set if (ReplicationBytesWritten > 0) { @@ -755,37 +811,7 @@ void USpatialActorChannel::DynamicallyAttachSubobject(UObject* Object) check(Info != nullptr); - // Check to see if we already have authority over the subobject to be added - if (NetDriver->StaticComponentView->HasAuthority(EntityId, Info->SchemaComponents[SCHEMA_Data])) - { - Sender->SendAddComponentForSubobject(this, Object, *Info, ReplicationBytesWritten); - } - else - { - // If we don't, modify the entity ACL to gain authority. - PendingDynamicSubobjects.Add(TWeakObjectPtr(Object)); - Sender->GainAuthorityThenAddComponent(this, Object, Info); - } -} - -bool USpatialActorChannel::IsListening() const -{ - if (NetDriver->IsServer()) - { - if (SpatialGDK::ClientRPCEndpointLegacy* Endpoint = NetDriver->StaticComponentView->GetComponentData(EntityId)) - { - return Endpoint->bReady; - } - } - else - { - if (SpatialGDK::ServerRPCEndpointLegacy* Endpoint = NetDriver->StaticComponentView->GetComponentData(EntityId)) - { - return Endpoint->bReady; - } - } - - return false; + Sender->SendAddComponentForSubobject(this, Object, *Info, ReplicationBytesWritten); } bool USpatialActorChannel::ReplicateSubobject(UObject* Object, const FReplicationFlags& RepFlags) @@ -827,7 +853,26 @@ bool USpatialActorChannel::ReplicateSubobject(UObject* Object, const FReplicatio FRepChangelistState* ChangelistState = Replicator.ChangelistMgr->GetRepChangelistState(); - Replicator.RepLayout->UpdateChangelistMgr(Replicator.RepState->GetSendingRepState(), *Replicator.ChangelistMgr, Object, Replicator.Connection->Driver->ReplicationFrame, RepFlags, bForceCompareProperties); +#if ENGINE_MINOR_VERSION >= 26 + const ERepLayoutResult UpdateResult = + Replicator.RepLayout->UpdateChangelistMgr(Replicator.RepState->GetSendingRepState(), *Replicator.ChangelistMgr, Object, + Replicator.Connection->Driver->ReplicationFrame, RepFlags, bForceCompareProperties); + + if (UNLIKELY(ERepLayoutResult::FatalError == UpdateResult)) + { + // This happens when a replicated array is over the maximum size (UINT16_MAX). + // Native Unreal just closes the connection at this point, but we can't do that as + // it may lead to unexpected consequences for the deployment. Instead, we just early out. + // TODO: UNR-4667 - Investigate this behavior in more detail. + + // Connection->SetPendingCloseDueToReplicationFailure(); + return false; + } +#else + Replicator.RepLayout->UpdateChangelistMgr(Replicator.RepState->GetSendingRepState(), *Replicator.ChangelistMgr, Object, + Replicator.Connection->Driver->ReplicationFrame, RepFlags, bForceCompareProperties); +#endif + FSendingRepState* SendingRepState = Replicator.RepState->GetSendingRepState(); const int32 PossibleNewHistoryIndex = SendingRepState->HistoryEnd % MaxSendingChangeHistory; @@ -847,7 +892,9 @@ bool USpatialActorChannel::ReplicateSubobject(UObject* Object, const FReplicatio } else { - UE_LOG(LogSpatialActorChannel, Warning, TEXT("EntityId: %lld Actor: %s Subobject: %s Changelist with index %d has no changed items"), EntityId, *Actor->GetName(), *Object->GetName(), i); + UE_LOG(LogSpatialActorChannel, Warning, + TEXT("EntityId: %lld Actor: %s Subobject: %s Changelist with index %d has no changed items"), EntityId, + *Actor->GetName(), *Object->GetName(), i); } } @@ -860,7 +907,9 @@ bool USpatialActorChannel::ReplicateSubobject(UObject* Object, const FReplicatio FUnrealObjectRef ObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromObject(Object); if (!ObjectRef.IsValid()) { - UE_LOG(LogSpatialActorChannel, Verbose, TEXT("Attempted to replicate an invalid ObjectRef. This may be a dynamic component that couldn't attach: %s"), *Object->GetName()); + 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; } @@ -928,7 +977,6 @@ TMap USpatialActorChannel::GetHandoverSubobjects() Object = NetDriver->PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(EntityId, SubobjectInfoPair.Key)).Get(); } - if (Object == nullptr) { continue; @@ -1006,7 +1054,8 @@ void USpatialActorChannel::SetChannelActor(AActor* InActor, ESetChannelActorFlag } else { - UE_LOG(LogSpatialActorChannel, Verbose, TEXT("Opened channel for actor %s with existing entity ID %lld."), *InActor->GetName(), EntityId); + UE_LOG(LogSpatialActorChannel, Verbose, TEXT("Opened channel for actor %s with existing entity ID %lld."), *InActor->GetName(), + EntityId); if (PackageMap->IsEntityIdPendingCreation(EntityId)) { @@ -1035,8 +1084,6 @@ void USpatialActorChannel::SetChannelActor(AActor* InActor, ESetChannelActorFlag check(!HandoverShadowDataMap.Contains(Subobject)); InitializeHandoverShadowData(HandoverShadowDataMap.Add(Subobject, MakeShared>()).Get(), Subobject); } - - SavedConnectionOwningWorkerId = SpatialGDK::GetConnectionOwningWorkerId(InActor); } bool USpatialActorChannel::TryResolveActor() @@ -1062,7 +1109,8 @@ FObjectReplicator* USpatialActorChannel::PreReceiveSpatialUpdate(UObject* Target { // SpatialReceiver tried to resolve this object in the PackageMap, but it didn't propagate to GuidCache. // This could happen if the UnrealObjectRef was already mapped to a different object that's been destroyed. - UE_LOG(LogSpatialActorChannel, Error, TEXT("PreReceiveSpatialUpdate: NetGUID is invalid! Object: %s"), *TargetObject->GetPathName()); + UE_LOG(LogSpatialActorChannel, Error, TEXT("PreReceiveSpatialUpdate: NetGUID is invalid! Object: %s"), + *TargetObject->GetPathName()); return nullptr; } @@ -1072,13 +1120,38 @@ FObjectReplicator* USpatialActorChannel::PreReceiveSpatialUpdate(UObject* Target return &Replicator; } -void USpatialActorChannel::PostReceiveSpatialUpdate(UObject* TargetObject, const TArray& RepNotifies) +void USpatialActorChannel::PostReceiveSpatialUpdate(UObject* TargetObject, const TArray& RepNotifies, + const TMap& PropertySpanIds) { FObjectReplicator& Replicator = FindOrCreateReplicator(TargetObject).Get(); TargetObject->PostNetReceive(); Replicator.RepState->GetReceivingRepState()->RepNotifies = RepNotifies; + SpatialGDK::SpatialEventTracer* EventTracer = NetDriver->Connection->GetEventTracer(); + + auto PreCallRepNotify = [EventTracer, PropertySpanIds](GDK_PROPERTY(Property) * Property) { + const FSpatialGDKSpanId* SpanId = PropertySpanIds.Find(Property); + if (SpanId != nullptr) + { + EventTracer->AddToStack(*SpanId); + } + }; + + auto PostCallRepNotify = [EventTracer, PropertySpanIds](GDK_PROPERTY(Property) * Property) { + const FSpatialGDKSpanId* SpanId = PropertySpanIds.Find(Property); + if (SpanId != nullptr) + { + EventTracer->PopFromStack(); + } + }; + + if (EventTracer != nullptr && PropertySpanIds.Num() > 0) + { + Replicator.RepLayout->PreRepNotify.BindLambda(PreCallRepNotify); + Replicator.RepLayout->PostRepNotify.BindLambda(PostCallRepNotify); + } + Replicator.CallRepNotifies(false); } @@ -1099,19 +1172,25 @@ void USpatialActorChannel::OnCreateEntityResponse(const Worker_CreateEntityRespo switch (static_cast(Op.status_code)) { case WORKER_STATUS_CODE_SUCCESS: - UE_LOG(LogSpatialActorChannel, Verbose, TEXT("Create entity request succeeded. " - "Actor %s, request id: %d, entity id: %lld, message: %s"), *Actor->GetName(), Op.request_id, Op.entity_id, UTF8_TO_TCHAR(Op.message)); + UE_LOG(LogSpatialActorChannel, Verbose, + TEXT("Create entity request succeeded. " + "Actor %s, request id: %d, entity id: %lld, message: %s"), + *Actor->GetName(), Op.request_id, Op.entity_id, UTF8_TO_TCHAR(Op.message)); break; case WORKER_STATUS_CODE_TIMEOUT: if (bEntityIsInView) { - UE_LOG(LogSpatialActorChannel, Log, TEXT("Create entity request failed but the entity was already in view. " - "Actor %s, request id: %d, entity id: %lld, message: %s"), *Actor->GetName(), Op.request_id, Op.entity_id, UTF8_TO_TCHAR(Op.message)); + UE_LOG(LogSpatialActorChannel, Log, + TEXT("Create entity request failed but the entity was already in view. " + "Actor %s, request id: %d, entity id: %lld, message: %s"), + *Actor->GetName(), Op.request_id, Op.entity_id, UTF8_TO_TCHAR(Op.message)); } else { - 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)); + 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)); // TODO: UNR-664 - Track these bytes written to use in saturation. uint32 BytesWritten = 0; @@ -1121,21 +1200,37 @@ void USpatialActorChannel::OnCreateEntityResponse(const Worker_CreateEntityRespo case WORKER_STATUS_CODE_APPLICATION_ERROR: if (bEntityIsInView) { - UE_LOG(LogSpatialActorChannel, Log, TEXT("Create entity request failed as the entity already exists and is in view. " - "Actor %s, request id: %d, entity id: %lld, message: %s"), *Actor->GetName(), Op.request_id, Op.entity_id, UTF8_TO_TCHAR(Op.message)); + UE_LOG(LogSpatialActorChannel, Log, + TEXT("Create entity request failed as the entity already exists and is in view. " + "Actor %s, request id: %d, entity id: %lld, message: %s"), + *Actor->GetName(), Op.request_id, Op.entity_id, UTF8_TO_TCHAR(Op.message)); } else { - UE_LOG(LogSpatialActorChannel, Warning, TEXT("Create entity request failed." - "Either the reservation expired, the entity already existed, or the entity was invalid. " - "Actor %s, request id: %d, entity id: %lld, message: %s"), *Actor->GetName(), Op.request_id, Op.entity_id, UTF8_TO_TCHAR(Op.message)); + UE_LOG(LogSpatialActorChannel, Warning, + TEXT("Create entity request failed." + "Either the reservation expired, the entity already existed, or the entity was invalid. " + "Actor %s, request id: %d, entity id: %lld, message: %s"), + *Actor->GetName(), Op.request_id, Op.entity_id, UTF8_TO_TCHAR(Op.message)); } break; default: - UE_LOG(LogSpatialActorChannel, Error, TEXT("Create entity request failed. This likely indicates a bug in the Unreal GDK and should be reported." - "Actor %s, request id: %d, entity id: %lld, message: %s"), *Actor->GetName(), Op.request_id, Op.entity_id, UTF8_TO_TCHAR(Op.message)); + UE_LOG(LogSpatialActorChannel, Error, + TEXT("Create entity request failed. This likely indicates a bug in the Unreal GDK and should be reported." + "Actor %s, request id: %d, entity id: %lld, message: %s"), + *Actor->GetName(), Op.request_id, Op.entity_id, UTF8_TO_TCHAR(Op.message)); break; } + + if (static_cast(Op.status_code) == WORKER_STATUS_CODE_SUCCESS && Actor->IsA()) + { + // With USLB, we want the client worker that results in the spawning of a PlayerController to claim the + // PlayerController entity as a partition entity so the client can become authoritative over necessary + // components (such as client RPC endpoints, heartbeat component, etc). + const Worker_EntityId ClientSystemEntityId = SpatialGDK::GetConnectionOwningClientSystemEntityId(Cast(Actor)); + check(ClientSystemEntityId != SpatialConstants::INVALID_ENTITY_ID); + Sender->SendClaimPartitionRequest(ClientSystemEntityId, Op.entity_id); + } } void USpatialActorChannel::UpdateSpatialPosition() @@ -1164,15 +1259,12 @@ void USpatialActorChannel::UpdateSpatialPosition() } } - // Check that the Actor has moved sufficiently far to be updated - const float SpatialPositionThresholdSquared = FMath::Square(GetDefault()->PositionDistanceThreshold); - FVector ActorSpatialPosition = SpatialGDK::GetActorSpatialPosition(Actor); - if (FVector::DistSquared(ActorSpatialPosition, LastPositionSinceUpdate) < SpatialPositionThresholdSquared) + if (!SatisfiesSpatialPositionUpdateRequirements()) { return; } - LastPositionSinceUpdate = ActorSpatialPosition; + LastPositionSinceUpdate = SpatialGDK::GetActorSpatialPosition(Actor); TimeWhenPositionLastUpdated = NetDriver->GetElapsedTime(); SendPositionUpdate(Actor, EntityId, LastPositionSinceUpdate); @@ -1188,7 +1280,7 @@ void USpatialActorChannel::UpdateSpatialPosition() void USpatialActorChannel::SendPositionUpdate(AActor* InActor, Worker_EntityId InEntityId, const FVector& NewPosition) { - if (InEntityId != SpatialConstants::INVALID_ENTITY_ID && NetDriver->StaticComponentView->HasAuthority(InEntityId, SpatialConstants::POSITION_COMPONENT_ID)) + if (InEntityId != SpatialConstants::INVALID_ENTITY_ID && NetDriver->HasServerAuthority(InEntityId)) { Sender->SendPositionUpdate(InEntityId, NewPosition); } @@ -1199,12 +1291,12 @@ void USpatialActorChannel::SendPositionUpdate(AActor* InActor, Worker_EntityId I } } -void USpatialActorChannel::RemoveRepNotifiesWithUnresolvedObjs(TArray& RepNotifies, const FRepLayout& RepLayout, const FObjectReferencesMap& RefMap, UObject* Object) +void USpatialActorChannel::RemoveRepNotifiesWithUnresolvedObjs(TArray& RepNotifies, const FRepLayout& RepLayout, + const FObjectReferencesMap& RefMap, UObject* Object) { // Prevent rep notify callbacks from being issued when unresolved obj references exist inside UStructs. // This prevents undefined behaviour when engine rep callbacks are issued where they don't expect unresolved objects in native flow. - RepNotifies.RemoveAll([&](GDK_PROPERTY(Property)* Property) - { + RepNotifies.RemoveAll([&](GDK_PROPERTY(Property) * Property) { for (auto& ObjRef : RefMap) { // ParentIndex will be -1 for handover properties. @@ -1220,10 +1312,12 @@ void USpatialActorChannel::RemoveRepNotifiesWithUnresolvedObjs(TArrayArrayDim > 1 || GDK_CASTFIELD(Property) != nullptr; + bool bIsArray = RepLayout.Parents[ObjRef.Value.ParentIndex].Property->ArrayDim > 1 + || GDK_CASTFIELD(Property) != nullptr; if (bIsSameRepNotify && !bIsArray) { - UE_LOG(LogSpatialActorChannel, Verbose, TEXT("RepNotify %s on %s ignored due to unresolved Actor"), *Property->GetName(), *Object->GetName()); + UE_LOG(LogSpatialActorChannel, Verbose, TEXT("RepNotify %s on %s ignored due to unresolved Actor"), *Property->GetName(), + *Object->GetName()); return true; } } @@ -1235,8 +1329,7 @@ void USpatialActorChannel::ServerProcessOwnershipChange() { SCOPE_CYCLE_COUNTER(STAT_ServerProcessOwnershipChange); { - if (!IsReadyForReplication() - || !IsAuthoritativeServer()) + if (!IsReadyForReplication() || !IsAuthoritativeServer()) { return; } @@ -1247,35 +1340,36 @@ void USpatialActorChannel::ServerProcessOwnershipChange() bool bUpdatedThisActor = false; // Changing an Actor's owner can affect its NetConnection so we need to reevaluate this. - FString NewClientConnectionWorkerId = SpatialGDK::GetConnectionOwningWorkerId(Actor); - if (SavedConnectionOwningWorkerId != NewClientConnectionWorkerId) + check(NetDriver->HasServerAuthority(EntityId)); + SpatialGDK::NetOwningClientWorker* CurrentNetOwningClientData = + NetDriver->StaticComponentView->GetComponentData(EntityId); + const Worker_PartitionId CurrentClientPartitionId = CurrentNetOwningClientData->ClientPartitionId.IsSet() + ? CurrentNetOwningClientData->ClientPartitionId.GetValue() + : SpatialConstants::INVALID_ENTITY_ID; + const Worker_PartitionId NewClientConnectionPartitionId = SpatialGDK::GetConnectionOwningPartitionId(Actor); + if (CurrentClientPartitionId != NewClientConnectionPartitionId) { // Update the NetOwningClientWorker component. - check(NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID)); - SpatialGDK::NetOwningClientWorker* NetOwningClientWorkerData = NetDriver->StaticComponentView->GetComponentData(EntityId); - NetOwningClientWorkerData->WorkerId = NewClientConnectionWorkerId; - FWorkerComponentUpdate Update = NetOwningClientWorkerData->CreateNetOwningClientWorkerUpdate(); + CurrentNetOwningClientData->SetPartitionId(NewClientConnectionPartitionId); + FWorkerComponentUpdate Update = CurrentNetOwningClientData->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; + // Notify the load balance enforcer of a potential short circuit if we are the delegation authoritative worker. + NetDriver->LoadBalanceEnforcer->ShortCircuitMaybeRefreshAuthorityDelegation(EntityId); bUpdatedThisActor = true; } + // Owner changed, update the actor's interest over it. + Sender->UpdateInterestComponent(Actor); + SetNeedOwnerInterestUpdate(!NetDriver->InterestFactory->DoOwnersHaveEntityId(Actor)); + // 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); + const Worker_ComponentId NewInterestBucketComponentId = NetDriver->ClassInfoManager->ComputeActorInterestComponentId(Actor); if (SavedInterestBucketComponentID != NewInterestBucketComponentId) { Sender->SendInterestBucketComponentChange(EntityId, SavedInterestBucketComponentID, NewInterestBucketComponentId); - SavedInterestBucketComponentID = NewInterestBucketComponentId; - bUpdatedThisActor = true; } @@ -1318,15 +1412,16 @@ void USpatialActorChannel::ClientProcessOwnershipChange(bool bNewNetOwned) } } -void USpatialActorChannel::OnSubobjectDeleted(const FUnrealObjectRef& ObjectRef, UObject* Object) +void USpatialActorChannel::OnSubobjectDeleted(const FUnrealObjectRef& ObjectRef, UObject* Object, + const TWeakObjectPtr& ObjectWeakPtr) { CreateSubObjects.Remove(Object); Receiver->MoveMappedObjectToUnmapped(ObjectRef); - if (FSpatialObjectRepState* SubObjectRefMap = ObjectReferenceMap.Find(Object)) + if (FSpatialObjectRepState* SubObjectRefMap = ObjectReferenceMap.Find(ObjectWeakPtr)) { Receiver->CleanupRepStateMap(*SubObjectRefMap); - ObjectReferenceMap.Remove(Object); + ObjectReferenceMap.Remove(ObjectWeakPtr); } } @@ -1341,3 +1436,44 @@ void USpatialActorChannel::ResetShadowData(FRepLayout& RepLayout, FRepStateStati RepLayout.CopyProperties(StaticBuffer, reinterpret_cast(TargetObject)); } } + +bool USpatialActorChannel::SatisfiesSpatialPositionUpdateRequirements() +{ + // Check that the Actor satisfies both lower thresholds OR either of the maximum thresholds + FVector ActorSpatialPosition = SpatialGDK::GetActorSpatialPosition(Actor); + const float DistanceTravelledSinceLastUpdateSquared = FVector::DistSquared(ActorSpatialPosition, LastPositionSinceUpdate); + + // If the Actor did not travel at all, then we consider its position to be up to date and we early out. + if (FMath::IsNearlyZero(DistanceTravelledSinceLastUpdateSquared)) + { + return false; + } + + const float TimeSinceLastPositionUpdate = NetDriver->GetElapsedTime() - TimeWhenPositionLastUpdated; + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + const float SpatialMinimumPositionThresholdSquared = FMath::Square(SpatialGDKSettings->PositionUpdateLowerThresholdCentimeters); + const float SpatialMaximumPositionThresholdSquared = FMath::Square(SpatialGDKSettings->PositionUpdateThresholdMaxCentimeters); + + if (TimeSinceLastPositionUpdate >= SpatialGDKSettings->PositionUpdateLowerThresholdSeconds + && DistanceTravelledSinceLastUpdateSquared >= SpatialMinimumPositionThresholdSquared) + { + return true; + } + + if (TimeSinceLastPositionUpdate >= SpatialGDKSettings->PositionUpdateThresholdMaxSeconds) + { + return true; + } + + if (DistanceTravelledSinceLastUpdateSquared >= SpatialMaximumPositionThresholdSquared) + { + return true; + } + + return false; +} + +void FObjectReferencesMapDeleter::operator()(FObjectReferencesMap* Ptr) const +{ + delete Ptr; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialFastArrayNetSerialize.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialFastArrayNetSerialize.cpp index a85c1f1aaf..f3b0aa2fe8 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialFastArrayNetSerialize.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialFastArrayNetSerialize.cpp @@ -8,8 +8,9 @@ namespace SpatialGDK { - -bool FSpatialNetDeltaSerializeInfo::DeltaSerializeRead(USpatialNetDriver* NetDriver, FSpatialNetBitReader& Reader, UObject* Object, int32 ArrayIndex, GDK_PROPERTY(Property)* ParentProperty, UScriptStruct* NetDeltaStruct) +bool FSpatialNetDeltaSerializeInfo::DeltaSerializeRead(USpatialNetDriver* NetDriver, FSpatialNetBitReader& Reader, UObject* Object, + int32 ArrayIndex, GDK_PROPERTY(Property) * ParentProperty, + UScriptStruct* NetDeltaStruct) { FSpatialNetDeltaSerializeInfo NetDeltaInfo; @@ -30,7 +31,9 @@ bool FSpatialNetDeltaSerializeInfo::DeltaSerializeRead(USpatialNetDriver* NetDri return CppStructOps->NetDeltaSerialize(NetDeltaInfo, Destination); } -bool FSpatialNetDeltaSerializeInfo::DeltaSerializeWrite(USpatialNetDriver* NetDriver, FSpatialNetBitWriter& Writer, UObject* Object, int32 ArrayIndex, GDK_PROPERTY(Property)* ParentProperty, UScriptStruct* NetDeltaStruct) +bool FSpatialNetDeltaSerializeInfo::DeltaSerializeWrite(USpatialNetDriver* NetDriver, FSpatialNetBitWriter& Writer, UObject* Object, + int32 ArrayIndex, GDK_PROPERTY(Property) * ParentProperty, + UScriptStruct* NetDeltaStruct) { FSpatialNetDeltaSerializeInfo NetDeltaInfo; @@ -51,7 +54,8 @@ bool FSpatialNetDeltaSerializeInfo::DeltaSerializeWrite(USpatialNetDriver* NetDr return CppStructOps->NetDeltaSerialize(NetDeltaInfo, Source); } -void SpatialFastArrayNetSerializeCB::NetSerializeStruct(UScriptStruct* Struct, FBitArchive& Ar, UPackageMap* PackageMap, void* Data, bool& bHasUnmapped) +void SpatialFastArrayNetSerializeCB::NetSerializeStruct(UScriptStruct* Struct, FBitArchive& Ar, UPackageMap* PackageMap, void* Data, + bool& bHasUnmapped) { // Check if struct has custom NetSerialize function, otherwise call standard struct replication if (Struct->StructFlags & STRUCT_NetSerializeNative) diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp index 599c644377..2f1ed397c0 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp @@ -2,6 +2,7 @@ #include "EngineClasses/SpatialGameInstance.h" +#include "Engine/Engine.h" #include "Engine/NetConnection.h" #include "GeneralProjectSettings.h" #include "Misc/Guid.h" @@ -17,6 +18,8 @@ #include "Interop/Connection/SpatialWorkerConnection.h" #include "Interop/GlobalStateManager.h" #include "Interop/SpatialStaticComponentView.h" +#include "Interop/SpatialWorkerFlags.h" +#include "SpatialConstants.h" #include "Utils/SpatialDebugger.h" #include "Utils/SpatialLatencyTracer.h" #include "Utils/SpatialMetrics.h" @@ -28,29 +31,34 @@ DEFINE_LOG_CATEGORY(LogSpatialGameInstance); USpatialGameInstance::USpatialGameInstance() : Super() , bIsSpatialNetDriverReady(false) -{} + , bPreparingForShutdown(false) +{ +} bool USpatialGameInstance::HasSpatialNetDriver() const { bool bHasSpatialNetDriver = false; + const bool bUseSpatial = GetDefault()->UsesSpatialNetworking(); + if (WorldContext != nullptr) { UWorld* World = GetWorld(); - UNetDriver * NetDriver = GEngine->FindNamedNetDriver(World, NAME_PendingNetDriver); + UNetDriver* NetDriver = GEngine->FindNamedNetDriver(World, NAME_PendingNetDriver); bool bShouldDestroyNetDriver = false; if (NetDriver == nullptr) { // If Spatial networking is enabled, override the GameNetDriver with the SpatialNetDriver - if (GetDefault()->UsesSpatialNetworking()) + if (bUseSpatial) { - if (FNetDriverDefinition* DriverDefinition = GEngine->NetDriverDefinitions.FindByPredicate([](const FNetDriverDefinition& CurDef) + if (FNetDriverDefinition* DriverDefinition = + GEngine->NetDriverDefinitions.FindByPredicate([](const FNetDriverDefinition& CurDef) { + return CurDef.DefName == NAME_GameNetDriver; + })) { - return CurDef.DefName == NAME_GameNetDriver; - })) - { - DriverDefinition->DriverClassName = DriverDefinition->DriverClassNameFallback = TEXT("/Script/SpatialGDK.SpatialNetDriver"); + DriverDefinition->DriverClassName = DriverDefinition->DriverClassNameFallback = + TEXT("/Script/SpatialGDK.SpatialNetDriver"); } } @@ -69,11 +77,12 @@ bool USpatialGameInstance::HasSpatialNetDriver() const } } - if (GetDefault()->UsesSpatialNetworking() && !bHasSpatialNetDriver) + if (bUseSpatial && !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 " - "guide and that you don't override the main net driver.")); + 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 " + "guide and that you don't override the main net driver.")); } return bHasSpatialNetDriver; @@ -109,7 +118,8 @@ void USpatialGameInstance::DestroySpatialConnectionManager() } #if WITH_EDITOR -FGameInstancePIEResult USpatialGameInstance::StartPlayInEditorGameInstance(ULocalPlayer* LocalPlayer, const FGameInstancePIEParameters& Params) +FGameInstancePIEResult USpatialGameInstance::StartPlayInEditorGameInstance(ULocalPlayer* LocalPlayer, + const FGameInstancePIEParameters& Params) { SpatialWorkerType = Params.SpatialWorkerType; bIsSimulatedPlayer = Params.bIsSimulatedPlayer; @@ -131,7 +141,8 @@ void USpatialGameInstance::StartSpatialConnection() 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)); + FString WorkerName = + FString::Printf(TEXT("%s:%s"), *SpatialWorkerType.ToString(), *FGuid::NewGuid().ToString(EGuidFormats::Digits)); SpatialLatencyTracer->SetWorkerId(WorkerName); } #endif @@ -143,7 +154,8 @@ void USpatialGameInstance::TryInjectSpatialLocatorIntoCommandLine() { SetHasPreviouslyConnectedToSpatial(); // Native Unreal creates a NetDriver and attempts to automatically connect if a Host is specified as the first commandline argument. - // Since the SpatialOS Launcher does not specify this, we need to check for a locator loginToken to allow automatic connection to provide parity with native. + // 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. // Initialize a locator configuration which will parse command line arguments. FLocatorConfig LocatorConfig; @@ -217,7 +229,7 @@ void USpatialGameInstance::Init() } } -void USpatialGameInstance::HandleOnConnected() +void USpatialGameInstance::HandleOnConnected(const USpatialNetDriver& NetDriver) { UE_LOG(LogSpatialGameInstance, Log, TEXT("Successfully connected to SpatialOS")); SpatialWorkerId = SpatialConnectionManager->GetWorkerConnection()->GetWorkerId(); @@ -230,21 +242,24 @@ void USpatialGameInstance::HandleOnConnected() #endif OnSpatialConnected.Broadcast(); + + if (NetDriver.IsServer()) + { + FOnWorkerFlagUpdatedBP WorkerFlagDelegate; + WorkerFlagDelegate.BindDynamic(this, &USpatialGameInstance::HandlePrepareShutdownWorkerFlagUpdated); + + NetDriver.SpatialWorkerFlags->RegisterFlagUpdatedCallback(SpatialConstants::SHUTDOWN_PREPARATION_WORKER_FLAG, WorkerFlagDelegate); + } } -void USpatialGameInstance::CleanupCachedLevelsAfterConnection() +void USpatialGameInstance::HandlePrepareShutdownWorkerFlagUpdated(const FString& FlagName, const FString& FlagValue) { - // Cleanup any actors which were created during level load. - UWorld* World = GetWorld(); - check(World != nullptr); - for (ULevel* Level : CachedLevelsForNetworkIntialize) + if (!bPreparingForShutdown) { - if (World->ContainsLevel(Level)) - { - CleanupLevelInitializedNetworkActors(Level); - } + bPreparingForShutdown = true; + UE_LOG(LogSpatialGameInstance, Log, TEXT("Shutdown preparation triggered.")); + OnPrepareShutdown.Broadcast(); } - CachedLevelsForNetworkIntialize.Empty(); } void USpatialGameInstance::HandleOnConnectionFailed(const FString& Reason) @@ -262,65 +277,22 @@ void USpatialGameInstance::HandleOnPlayerSpawnFailed(const FString& Reason) OnSpatialPlayerSpawnFailed.Broadcast(Reason); } -void USpatialGameInstance::OnLevelInitializedNetworkActors(ULevel* LoadedLevel, UWorld* OwningWorld) +void USpatialGameInstance::OnLevelInitializedNetworkActors(ULevel* LoadedLevel, UWorld* OwningWorld) const { - if (OwningWorld != GetWorld() - || !OwningWorld->IsServer() - || !GetDefault()->UsesSpatialNetworking() - || (OwningWorld->WorldType != EWorldType::PIE - && OwningWorld->WorldType != EWorldType::Game + UE_LOG(LogSpatialOSNetDriver, Log, TEXT("OnLevelInitializedNetworkActors: Level (%s) OwningWorld (%s) World (%s)"), + *GetNameSafe(LoadedLevel), *GetNameSafe(OwningWorld), *GetNameSafe(OwningWorld)); + + if (OwningWorld != GetWorld() || !OwningWorld->IsServer() || OwningWorld->GetNetDriver() == nullptr + || !Cast(OwningWorld->GetNetDriver())->IsReady() + || (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; } - if (bIsSpatialNetDriverReady) - { - CleanupLevelInitializedNetworkActors(LoadedLevel); - } - else - { - CachedLevelsForNetworkIntialize.Add(LoadedLevel); - } -} - -void USpatialGameInstance::CleanupLevelInitializedNetworkActors(ULevel* LoadedLevel) -{ - bIsSpatialNetDriverReady = true; - for (int32 ActorIndex = 0; ActorIndex < LoadedLevel->Actors.Num(); ActorIndex++) + for (AActor* Actor : LoadedLevel->Actors) { - AActor* Actor = LoadedLevel->Actors[ActorIndex]; - if (Actor == nullptr) - { - continue; - } - - if (USpatialStatics::IsSpatialOffloadingEnabled(GetWorld())) - { - if (!USpatialStatics::IsActorGroupOwnerForActor(Actor)) - { - if (!Actor->bNetLoadOnNonAuthServer) - { - Actor->Destroy(true); - } - else - { - UE_LOG(LogSpatialGameInstance, Verbose, TEXT("This worker %s is not the owner of startup actor %s, exchanging Roles"), *GetPathNameSafe(Actor)); - ENetRole Temp = Actor->Role; - Actor->Role = Actor->RemoteRole; - Actor->RemoteRole = Temp; - } - } - } - 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; - } - } + GlobalStateManager->HandleActorBasedOnLoadBalancer(Actor); } } diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialLoadBalanceEnforcer.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialLoadBalanceEnforcer.cpp index cd03b575d5..2c0e05f9e4 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialLoadBalanceEnforcer.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialLoadBalanceEnforcer.cpp @@ -1,247 +1,171 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "EngineClasses/SpatialLoadBalanceEnforcer.h" + +#include "EngineClasses/SpatialNetDriver.h" +#include "EngineClasses/SpatialPackageMapClient.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" +#include "SpatialConstants.h" +#include "SpatialView/EntityDelta.h" +#include "SpatialView/SubView.h" +#include "SpatialView/ViewDelta.h" DEFINE_LOG_CATEGORY(LogSpatialLoadBalanceEnforcer); -using namespace SpatialGDK; - -SpatialLoadBalanceEnforcer::SpatialLoadBalanceEnforcer(const PhysicalWorkerName& InWorkerId, const USpatialStaticComponentView* InStaticComponentView, const SpatialVirtualWorkerTranslator* InVirtualWorkerTranslator) +namespace SpatialGDK +{ +SpatialLoadBalanceEnforcer::SpatialLoadBalanceEnforcer(const PhysicalWorkerName& InWorkerId, const FSubView& InSubView, + const SpatialVirtualWorkerTranslator* InVirtualWorkerTranslator, + TUniqueFunction InUpdateSender) : WorkerId(InWorkerId) - , StaticComponentView(InStaticComponentView) + , SubView(&InSubView) , VirtualWorkerTranslator(InVirtualWorkerTranslator) + , UpdateSender(MoveTemp(InUpdateSender)) { - check(InStaticComponentView != nullptr); check(InVirtualWorkerTranslator != nullptr); } -void SpatialLoadBalanceEnforcer::OnLoadBalancingComponentAdded(const Worker_AddComponentOp& Op) +void SpatialLoadBalanceEnforcer::Advance() { - 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)) + const FSubViewDelta& SubViewDelta = SubView->GetViewDelta(); + for (const EntityDelta& Delta : SubViewDelta.EntityDeltas) { - 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); + bool bRefresh = false; + switch (Delta.Type) + { + case EntityDelta::UPDATE: + { + for (const ComponentChange& Change : Delta.ComponentUpdates) + { + bRefresh |= ApplyComponentUpdate(Delta.EntityId, Change.ComponentId, Change.Update); + } + for (const ComponentChange& Change : Delta.ComponentsRefreshed) + { + bRefresh |= ApplyComponentRefresh(Delta.EntityId, Change.ComponentId, Change.CompleteUpdate.Data); + } + break; + } + case EntityDelta::ADD: + PopulateDataStore(Delta.EntityId); + bRefresh = true; + break; + case EntityDelta::REMOVE: + DataStore.Remove(Delta.EntityId); + break; + case EntityDelta::TEMPORARILY_REMOVED: + DataStore.Remove(Delta.EntityId); + PopulateDataStore(Delta.EntityId); + bRefresh = true; + break; + default: + break; + } + + if (bRefresh) + { + RefreshAuthority(Delta.EntityId); + } } } -void SpatialLoadBalanceEnforcer::OnEntityRemoved(const Worker_RemoveEntityOp& Op) +void SpatialLoadBalanceEnforcer::ShortCircuitMaybeRefreshAuthorityDelegation(const Worker_EntityId EntityId) { - if (AclAssignmentRequestIsQueued(Op.entity_id)) + const EntityViewElement& Element = SubView->GetView()[EntityId]; + if (Element.Components.ContainsByPredicate(ComponentIdEquality{ SpatialConstants::LB_TAG_COMPONENT_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); + // Our entity will be out of date during a short circuit. Refresh the state here before refreshing the authority delegation. + DataStore.Remove(EntityId); + PopulateDataStore(EntityId); + RefreshAuthority(EntityId); } } -void SpatialLoadBalanceEnforcer::OnAclAuthorityChanged(const Worker_AuthorityChangeOp& AuthOp) +void SpatialLoadBalanceEnforcer::RefreshAuthority(const Worker_EntityId EntityId) { - // This class should only be informed of ACL authority changes. - check(AuthOp.component_id == SpatialConstants::ENTITY_ACL_COMPONENT_ID); + const Worker_ComponentUpdate Update = CreateAuthorityDelegationUpdate(EntityId); - 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); + UpdateSender(EntityComponentUpdate{ EntityId, ComponentUpdate(OwningComponentUpdatePtr(Update.schema_type), Update.component_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) +Worker_ComponentUpdate SpatialLoadBalanceEnforcer::CreateAuthorityDelegationUpdate(const Worker_EntityId EntityId) { - if (!CanEnforce(EntityId)) - { - return; - } + LBComponents& Components = DataStore[EntityId]; - const SpatialGDK::AuthorityIntent* AuthorityIntentComponent = StaticComponentView->GetComponentData(EntityId); - const PhysicalWorkerName* OwningWorkerId = VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(AuthorityIntentComponent->VirtualWorkerId); + const Worker_PartitionId AuthoritativeServerPartition = + VirtualWorkerTranslator->GetPartitionEntityForVirtualWorker(Components.Intent.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; - } + const NetOwningClientWorker& NetOwningClientWorker = Components.OwningClientWorker; + const Worker_PartitionId ClientWorkerPartitionId = NetOwningClientWorker.ClientPartitionId.IsSet() + ? NetOwningClientWorker.ClientPartitionId.GetValue() + : SpatialConstants::INVALID_PARTITION_ID; - if (AclAssignmentRequestIsQueued(EntityId)) - { - UE_LOG(LogSpatialLoadBalanceEnforcer, Verbose, TEXT("Avoiding queueing a duplicate ACL assignment request. Entity: %lld. Worker: %s."), - EntityId, *WorkerId); - return; - } + AuthorityDelegation& AuthorityDelegationComponent = Components.Delegation; + AuthorityDelegationComponent.Delegations.Add(SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID, ClientWorkerPartitionId); + AuthorityDelegationComponent.Delegations.Add(SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID, AuthoritativeServerPartition); - QueueAclAssignmentRequest(EntityId); + return AuthorityDelegationComponent.CreateAuthorityDelegationUpdate(); } -bool SpatialLoadBalanceEnforcer::AclAssignmentRequestIsQueued(const Worker_EntityId EntityId) const +void SpatialLoadBalanceEnforcer::PopulateDataStore(const Worker_EntityId EntityId) { - return AclWriteAuthAssignmentRequests.Contains(EntityId); -} - -TArray SpatialLoadBalanceEnforcer::ProcessQueuedAclAssignmentRequests() -{ - TArray PendingRequests; - - TArray CompletedRequests; - CompletedRequests.Reserve(AclWriteAuthAssignmentRequests.Num()); - - for (Worker_EntityId EntityId : AclWriteAuthAssignmentRequests) + LBComponents& Components = DataStore.Emplace(EntityId, LBComponents{}); + for (const ComponentData& Data : SubView->GetView()[EntityId].Components) { - const SpatialGDK::AuthorityIntent* AuthorityIntentComponent = StaticComponentView->GetComponentData(EntityId); - if (AuthorityIntentComponent == nullptr) + switch (Data.GetComponentId()) { - // 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; + case SpatialConstants::AUTHORITY_DELEGATION_COMPONENT_ID: + Components.Delegation = AuthorityDelegation(Data.GetUnderlying()); + break; + case SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID: + Components.Intent = AuthorityIntent(Data.GetUnderlying()); + break; + case SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID: + Components.OwningClientWorker = NetOwningClientWorker(Data.GetUnderlying()); + break; + default: + break; } - - 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) +bool SpatialLoadBalanceEnforcer::ApplyComponentUpdate(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId, + Schema_ComponentUpdate* Update) { - UE_LOG(LogSpatialLoadBalanceEnforcer, Verbose, TEXT("Queueing ACL assignment request for entity %lld on worker %s."), EntityId, *WorkerId); - AclWriteAuthAssignmentRequests.Add(EntityId); + switch (ComponentId) + { + case SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID: + DataStore[EntityId].Intent.ApplyComponentUpdate(Update); + return true; + case SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID: + DataStore[EntityId].OwningClientWorker.ApplyComponentUpdate(Update); + return true; + default: + break; + } + return false; } -bool SpatialLoadBalanceEnforcer::CanEnforce(Worker_EntityId EntityId) const +bool SpatialLoadBalanceEnforcer::ApplyComponentRefresh(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId, + Schema_ComponentData* Data) { - // 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); + switch (ComponentId) + { + case SpatialConstants::AUTHORITY_DELEGATION_COMPONENT_ID: + DataStore[EntityId].Delegation = AuthorityDelegation(Data); + break; + case SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID: + DataStore[EntityId].Intent = AuthorityIntent(Data); + return true; + case SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID: + DataStore[EntityId].OwningClientWorker = NetOwningClientWorker(Data); + return true; + default: + break; + } + return false; } -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; -} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitReader.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitReader.cpp index 0b2f520097..2882e4c0de 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitReader.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitReader.cpp @@ -9,56 +9,74 @@ DEFINE_LOG_CATEGORY(LogSpatialNetBitReader); -FSpatialNetBitReader::FSpatialNetBitReader(USpatialPackageMapClient* InPackageMap, uint8* Source, int64 CountBits, TSet& InDynamicRefs, TSet& InUnresolvedRefs) +static thread_local FSpatialNetBitReader* GCurrentReader = nullptr; + +FSpatialNetBitReader::FSpatialNetBitReader(USpatialPackageMapClient* InPackageMap, uint8* Source, int64 CountBits, + TSet& InDynamicRefs, TSet& InUnresolvedRefs) : FNetBitReader(InPackageMap, Source, CountBits) , DynamicRefs(InDynamicRefs) - , UnresolvedRefs(InUnresolvedRefs) {} + , UnresolvedRefs(InUnresolvedRefs) +{ + // Limitation of using a global TLS pointer, you can have at most a single instance of this object per thread. + // There should be no need to have more than one at a given time, but should that be the case we could move to a set per thread instead + // of a pointer. + check(GCurrentReader == nullptr); + GCurrentReader = this; +} + +FSpatialNetBitReader::~FSpatialNetBitReader() +{ + GCurrentReader = nullptr; +} -void FSpatialNetBitReader::DeserializeObjectRef(FUnrealObjectRef& ObjectRef) +void FSpatialNetBitReader::DeserializeObjectRef(FArchive& Archive, FUnrealObjectRef& ObjectRef) { int64 EntityId; - *this << EntityId; + Archive << EntityId; ObjectRef.Entity = EntityId; - *this << ObjectRef.Offset; + Archive << ObjectRef.Offset; uint8 HasPath; - SerializeBits(&HasPath, 1); + Archive.SerializeBits(&HasPath, 1); if (HasPath) { FString Path; - *this << Path; + Archive << Path; ObjectRef.Path = Path; } uint8 HasOuter; - SerializeBits(&HasOuter, 1); + Archive.SerializeBits(&HasOuter, 1); if (HasOuter) { ObjectRef.Outer = FUnrealObjectRef(); - DeserializeObjectRef(*ObjectRef.Outer); + DeserializeObjectRef(Archive, *ObjectRef.Outer); } - SerializeBits(&ObjectRef.bNoLoadOnClient, 1); - SerializeBits(&ObjectRef.bUseClassPathToLoadObject, 1); + Archive.SerializeBits(&ObjectRef.bNoLoadOnClient, 1); + Archive.SerializeBits(&ObjectRef.bUseClassPathToLoadObject, 1); } -UObject* FSpatialNetBitReader::ReadObject(bool& bUnresolved) +UObject* FSpatialNetBitReader::ReadObject(FArchive& Archive, USpatialPackageMapClient* PackageMap, bool& bUnresolved) { FUnrealObjectRef ObjectRef; - DeserializeObjectRef(ObjectRef); + DeserializeObjectRef(Archive, ObjectRef); check(ObjectRef != FUnrealObjectRef::UNRESOLVED_OBJECT_REF); - UObject* Value = FUnrealObjectRef::ToObjectPtr(ObjectRef, Cast(PackageMap), bUnresolved); + UObject* Value = FUnrealObjectRef::ToObjectPtr(ObjectRef, PackageMap, bUnresolved); - if (bUnresolved) - { - UnresolvedRefs.Add(ObjectRef); - } - else if (Value && !Value->IsFullNameStableForNetworking()) + if (GCurrentReader != nullptr) { - DynamicRefs.Add(ObjectRef); + if (bUnresolved) + { + GCurrentReader->UnresolvedRefs.Add(ObjectRef); + } + else if (Value && !Value->IsFullNameStableForNetworking()) + { + GCurrentReader->DynamicRefs.Add(ObjectRef); + } } return Value; @@ -67,7 +85,7 @@ UObject* FSpatialNetBitReader::ReadObject(bool& bUnresolved) FArchive& FSpatialNetBitReader::operator<<(UObject*& Value) { bool bUnresolved = false; - Value = ReadObject(bUnresolved); + Value = ReadObject(*this, Cast(PackageMap), bUnresolved); return *this; } diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitWriter.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitWriter.cpp index ea7a6b062d..51efbb8c8b 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitWriter.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitWriter.cpp @@ -14,37 +14,42 @@ DEFINE_LOG_CATEGORY(LogSpatialNetSerialize); FSpatialNetBitWriter::FSpatialNetBitWriter(USpatialPackageMapClient* InPackageMap) : FNetBitWriter(InPackageMap, 0) -{} +{ +} -void FSpatialNetBitWriter::SerializeObjectRef(FUnrealObjectRef& ObjectRef) +void FSpatialNetBitWriter::SerializeObjectRef(FArchive& Archive, FUnrealObjectRef& ObjectRef) { int64 EntityId = ObjectRef.Entity; - *this << EntityId; - *this << ObjectRef.Offset; + Archive << EntityId; + Archive << ObjectRef.Offset; uint8 HasPath = ObjectRef.Path.IsSet(); - SerializeBits(&HasPath, 1); + Archive.SerializeBits(&HasPath, 1); if (HasPath) { - *this << ObjectRef.Path.GetValue(); + Archive << ObjectRef.Path.GetValue(); } uint8 HasOuter = ObjectRef.Outer.IsSet(); - SerializeBits(&HasOuter, 1); + Archive.SerializeBits(&HasOuter, 1); if (HasOuter) { - SerializeObjectRef(*ObjectRef.Outer); + SerializeObjectRef(Archive, *ObjectRef.Outer); } - SerializeBits(&ObjectRef.bNoLoadOnClient, 1); - SerializeBits(&ObjectRef.bUseClassPathToLoadObject, 1); + Archive.SerializeBits(&ObjectRef.bNoLoadOnClient, 1); + Archive.SerializeBits(&ObjectRef.bUseClassPathToLoadObject, 1); } -FArchive& FSpatialNetBitWriter::operator<<(UObject*& Value) +void FSpatialNetBitWriter::WriteObject(FArchive& Archive, USpatialPackageMapClient* PackageMap, UObject* Object) { - FUnrealObjectRef ObjectRef = FUnrealObjectRef::FromObjectPtr(Value, Cast(PackageMap)); - SerializeObjectRef(ObjectRef); + FUnrealObjectRef ObjectRef = FUnrealObjectRef::FromObjectPtr(Object, PackageMap); + SerializeObjectRef(Archive, ObjectRef); +} +FArchive& FSpatialNetBitWriter::operator<<(UObject*& Value) +{ + WriteObject(*this, Cast(PackageMap), Value); return *this; } diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetConnection.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetConnection.cpp index 69a32fca12..889e09e730 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetConnection.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetConnection.cpp @@ -10,8 +10,8 @@ #include "SpatialConstants.h" #include "SpatialGDKSettings.h" -#include "GameFramework/PlayerController.h" #include "GameFramework/Pawn.h" +#include "GameFramework/PlayerController.h" #include "TimerManager.h" #include @@ -34,7 +34,7 @@ USpatialNetConnection::USpatialNetConnection(const FObjectInitializer& ObjectIni void USpatialNetConnection::BeginDestroy() { DisableHeartbeat(); - + Super::BeginDestroy(); } @@ -48,7 +48,8 @@ void USpatialNetConnection::CleanUp() Super::CleanUp(); } -void USpatialNetConnection::InitBase(UNetDriver* InDriver, class FSocket* InSocket, const FURL& InURL, EConnectionState InState, int32 InMaxPacket /*= 0*/, int32 InPacketOverhead /*= 0*/) +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); @@ -68,20 +69,20 @@ void USpatialNetConnection::InitBase(UNetDriver* InDriver, class FSocket* InSock void USpatialNetConnection::LowLevelSend(void* Data, int32 CountBits, FOutPacketTraits& Traits) { - //Intentionally does not call Super:: + // Intentionally does not call Super:: } bool USpatialNetConnection::ClientHasInitializedLevelFor(const AActor* TestActor) const { check(Driver->IsServer()); return true; - //Intentionally does not call Super:: + // Intentionally does not call Super:: } int32 USpatialNetConnection::IsNetReady(bool Saturate) { - // TODO: UNR-664 - Currently we do not report the number of bits sent when replicating, this means channel saturation cannot be checked properly. - // This will always return true until we solve this. + // TODO: UNR-664 - Currently we do not report the number of bits sent when replicating, this means channel saturation cannot be checked + // properly. This will always return true until we solve this. return true; } @@ -119,36 +120,11 @@ void USpatialNetConnection::FlushDormancy(AActor* Actor) } } -void USpatialNetConnection::ClientNotifyClientHasQuit() -{ - if (PlayerControllerEntity != SpatialConstants::INVALID_ENTITY_ID) - { - if (!Cast(Driver)->StaticComponentView->HasAuthority(PlayerControllerEntity, SpatialConstants::HEARTBEAT_COMPONENT_ID)) - { - UE_LOG(LogSpatialNetConnection, Warning, TEXT("Quit the game but no authority over Heartbeat component: NetConnection %s, PlayerController entity %lld"), *GetName(), PlayerControllerEntity); - return; - } - - FWorkerComponentUpdate Update = {}; - Update.component_id = SpatialConstants::HEARTBEAT_COMPONENT_ID; - Update.schema_type = Schema_CreateComponentUpdate(); - Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); - - Schema_AddBool(ComponentObject, SpatialConstants::HEARTBEAT_CLIENT_HAS_QUIT_ID, true); - - Cast(Driver)->Connection->SendComponentUpdate(PlayerControllerEntity, &Update); - } - else - { - UE_LOG(LogSpatialNetConnection, Warning, TEXT("Quitting before Heartbeat component has been initialized: NetConnection %s"), *GetName()); - } -} - void USpatialNetConnection::InitHeartbeat(FTimerManager* InTimerManager, Worker_EntityId InPlayerControllerEntity) { - UE_LOG(LogSpatialNetConnection, Log, TEXT("Init Heartbeat component: NetConnection %s, PlayerController entity %lld"), *GetName(), 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; @@ -169,36 +145,48 @@ void USpatialNetConnection::SetHeartbeatTimeoutTimer() Timeout = GetDefault()->HeartbeatTimeoutWithEditorSeconds; #endif - TimerManager->SetTimer(HeartbeatTimer, [WeakThis = TWeakObjectPtr(this)]() - { - if (USpatialNetConnection* Connection = WeakThis.Get()) - { - // This client timed out. Disconnect it and trigger OnDisconnected logic. - Connection->CleanUp(); - } - }, Timeout, false); + TimerManager->SetTimer( + HeartbeatTimer, + [WeakThis = TWeakObjectPtr(this)]() { + if (USpatialNetConnection* Connection = WeakThis.Get()) + { + // This client timed out. Disconnect it and trigger OnDisconnected logic. + UE_LOG(LogSpatialNetConnection, Warning, + TEXT("Client timed out - destroying connection: NetConnection %s, PlayerController entity %lld"), + *Connection->GetName(), Connection->PlayerControllerEntity); + Connection->CleanUp(); + } + }, + Timeout, false); } void USpatialNetConnection::SetHeartbeatEventTimer() { - TimerManager->SetTimer(HeartbeatTimer, [WeakThis = TWeakObjectPtr(this)]() - { - if (USpatialNetConnection* Connection = WeakThis.Get()) - { - FWorkerComponentUpdate ComponentUpdate = {}; - - ComponentUpdate.component_id = SpatialConstants::HEARTBEAT_COMPONENT_ID; - ComponentUpdate.schema_type = Schema_CreateComponentUpdate(); - Schema_Object* EventsObject = Schema_GetComponentUpdateEvents(ComponentUpdate.schema_type); - Schema_AddObject(EventsObject, SpatialConstants::HEARTBEAT_EVENT_ID); - - USpatialWorkerConnection* WorkerConnection = Cast(Connection->Driver)->Connection; - if (WorkerConnection != nullptr) + TimerManager->SetTimer( + HeartbeatTimer, + [WeakThis = TWeakObjectPtr(this)]() { + if (USpatialNetConnection* Connection = WeakThis.Get()) { - WorkerConnection->SendComponentUpdate(Connection->PlayerControllerEntity, &ComponentUpdate); + FWorkerComponentUpdate ComponentUpdate = {}; + + ComponentUpdate.component_id = SpatialConstants::HEARTBEAT_COMPONENT_ID; + ComponentUpdate.schema_type = Schema_CreateComponentUpdate(); + Schema_Object* EventsObject = Schema_GetComponentUpdateEvents(ComponentUpdate.schema_type); + Schema_AddObject(EventsObject, SpatialConstants::HEARTBEAT_EVENT_ID); + + USpatialWorkerConnection* WorkerConnection = Cast(Connection->Driver)->Connection; + if (WorkerConnection != nullptr) + { + WorkerConnection->SendComponentUpdate(Connection->PlayerControllerEntity, &ComponentUpdate); + } } - } - }, GetDefault()->HeartbeatIntervalSeconds, true, 0.0f); + }, + GetDefault()->HeartbeatIntervalSeconds, true, 0.0f); + + if (PlayerController != nullptr) + { + PlayerController->OnDestroyed.AddDynamic(this, &USpatialNetConnection::OnControllerDestroyed); + } } void USpatialNetConnection::DisableHeartbeat() @@ -215,3 +203,38 @@ void USpatialNetConnection::OnHeartbeat() { SetHeartbeatTimeoutTimer(); } + +void USpatialNetConnection::ClientNotifyClientHasQuit() +{ + if (PlayerControllerEntity != SpatialConstants::INVALID_ENTITY_ID) + { + if (!Cast(Driver)->StaticComponentView->HasAuthority(PlayerControllerEntity, + SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID)) + { + UE_LOG(LogSpatialNetConnection, Warning, + TEXT("Quit the game but no authority over Heartbeat component: NetConnection %s, PlayerController entity %lld"), + *GetName(), PlayerControllerEntity); + return; + } + + FWorkerComponentUpdate Update = {}; + Update.component_id = SpatialConstants::HEARTBEAT_COMPONENT_ID; + Update.schema_type = Schema_CreateComponentUpdate(); + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); + + Schema_AddBool(ComponentObject, SpatialConstants::HEARTBEAT_CLIENT_HAS_QUIT_ID, true); + + Cast(Driver)->Connection->SendComponentUpdate(PlayerControllerEntity, &Update); + } + else + { + UE_LOG(LogSpatialNetConnection, Verbose, TEXT("Quitting before Heartbeat component has been initialized: NetConnection %s"), + *GetName()); + } +} + +void USpatialNetConnection::OnControllerDestroyed(AActor* /*DestroyedActor*/) +{ + // Controller destroyed, prevent future heartbeat updates + DisableHeartbeat(); +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp index 7c9464d6b2..c77d9022a5 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp @@ -2,6 +2,7 @@ #include "EngineClasses/SpatialNetDriver.h" +#include "Containers/StringConv.h" #include "Engine/ActorChannel.h" #include "Engine/ChildConnection.h" #include "Engine/Engine.h" @@ -14,14 +15,16 @@ #include "Net/DataReplication.h" #include "Net/RepLayout.h" #include "SocketSubsystem.h" -#include "UObject/WeakObjectPtrTemplates.h" #include "UObject/UObjectIterator.h" +#include "UObject/WeakObjectPtrTemplates.h" #include "EngineClasses/SpatialActorChannel.h" #include "EngineClasses/SpatialGameInstance.h" #include "EngineClasses/SpatialNetConnection.h" +#include "EngineClasses/SpatialNetDriverDebugContext.h" #include "EngineClasses/SpatialPackageMapClient.h" #include "EngineClasses/SpatialPendingNetGame.h" +#include "EngineClasses/SpatialReplicationGraph.h" #include "EngineClasses/SpatialWorldSettings.h" #include "Interop/Connection/SpatialConnectionManager.h" #include "Interop/Connection/SpatialWorkerConnection.h" @@ -32,12 +35,19 @@ #include "Interop/SpatialReceiver.h" #include "Interop/SpatialSender.h" #include "Interop/SpatialWorkerFlags.h" +#include "Interop/WellKnownEntitySystem.h" #include "LoadBalancing/AbstractLBStrategy.h" +#include "LoadBalancing/DebugLBStrategy.h" #include "LoadBalancing/GridBasedLBStrategy.h" #include "LoadBalancing/LayeredLBStrategy.h" #include "LoadBalancing/OwnershipLockingPolicy.h" #include "SpatialConstants.h" #include "SpatialGDKSettings.h" +#include "SpatialView/EntityComponentTypes.h" +#include "SpatialView/EntityView.h" +#include "SpatialView/OpList/ViewDeltaLegacyOpList.h" +#include "SpatialView/SubView.h" +#include "SpatialView/ViewDelta.h" #include "Utils/ComponentFactory.h" #include "Utils/EntityPool.h" #include "Utils/ErrorCodeRemapping.h" @@ -57,12 +67,9 @@ #endif using SpatialGDK::ComponentFactory; -using SpatialGDK::FindFirstOpOfType; -using SpatialGDK::AppendAllOpsOfType; -using SpatialGDK::FindFirstOpOfTypeForComponent; using SpatialGDK::InterestFactory; -using SpatialGDK::RPCPayload; using SpatialGDK::OpList; +using SpatialGDK::RPCPayload; DEFINE_LOG_CATEGORY(LogSpatialOSNetDriver); @@ -78,6 +85,7 @@ DEFINE_STAT(STAT_SpatialActorsChanged); USpatialNetDriver::USpatialNetDriver(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , LoadBalanceStrategy(nullptr) + , DebugCtx(nullptr) , LoadBalanceEnforcer(nullptr) , bAuthoritativeDestruction(true) , bConnectAsClient(false) @@ -87,13 +95,22 @@ USpatialNetDriver::USpatialNetDriver(const FObjectInitializer& ObjectInitializer , bMapLoaded(false) , SessionId(0) , NextRPCIndex(0) - , TimeWhenPositionLastUpdated(0.f) + , StartupTimestamp(0) + , MigrationTimestamp(0) { // Due to changes in 4.23, we now use an outdated flow in ComponentReader::ApplySchemaObject // Native Unreal now iterates over all commands on clients, and no longer has access to a BaseHandleToCmdIndex // in the RepLayout, the below change forces its creation on clients, but this is a workaround // TODO: UNR-2375 bMaySendProperties = true; + +#if ENGINE_MINOR_VERSION >= 26 + // Due to changes in 4.26, which remove almost all usages of InternalAck, we now need this + // flag to tell NetDriver to not replicate actors when we call our super UNetDriver::TickFlush. + bSkipServerReplicateActors = true; +#endif + + SpatialDebuggerReady = NewObject(); } bool USpatialNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, const FURL& URL, bool bReuseAddressAndPort, FString& Error) @@ -166,8 +183,8 @@ bool USpatialNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, c if (LocalDeploymentManager->ShouldWaitForDeployment()) { UE_LOG(LogSpatialOSNetDriver, Display, TEXT("Waiting for local SpatialOS deployment to start before connecting...")); - SpatialDeploymentStartHandle = LocalDeploymentManager->OnDeploymentStart.AddLambda([WeakThis = TWeakObjectPtr(this), URL] - { + SpatialDeploymentStartHandle = + LocalDeploymentManager->OnDeploymentStart.AddLambda([WeakThis = TWeakObjectPtr(this), URL] { if (!WeakThis.IsValid()) { return; @@ -175,7 +192,8 @@ bool USpatialNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, c UE_LOG(LogSpatialOSNetDriver, Display, TEXT("Local deployment started, connecting with URL: %s"), *URL.ToString()); WeakThis.Get()->InitiateConnectionToSpatialOS(URL); - if (FSpatialGDKServicesModule* GDKServices = FModuleManager::GetModulePtr("SpatialGDKServices")) + if (FSpatialGDKServicesModule* GDKServices = + FModuleManager::GetModulePtr("SpatialGDKServices")) { GDKServices->GetLocalDeploymentManager()->OnDeploymentStart.Remove(WeakThis.Get()->SpatialDeploymentStartHandle); } @@ -195,21 +213,22 @@ bool USpatialNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, c USpatialGameInstance* USpatialNetDriver::GetGameInstance() const { - USpatialGameInstance* GameInstance = nullptr; - - // A client does not have a world at this point, so we use the WorldContext + // A client might not have a world at this point, so we use the WorldContext // to get a reference to the GameInstance if (bConnectAsClient) { - const FWorldContext& WorldContext = GEngine->GetWorldContextFromPendingNetGameNetDriverChecked(this); - GameInstance = Cast(WorldContext.OwningGameInstance); + if (const FWorldContext* WorldContext = GEngine->GetWorldContextFromPendingNetGameNetDriver(this)) + { + return Cast(WorldContext->OwningGameInstance); + } } - else + + if (GetWorld() != nullptr) { - GameInstance = Cast(GetWorld()->GetGameInstance()); + return Cast(GetWorld()->GetGameInstance()); } - return GameInstance; + return nullptr; } void USpatialNetDriver::InitiateConnectionToSpatialOS(const FURL& URL) @@ -218,7 +237,8 @@ void USpatialNetDriver::InitiateConnectionToSpatialOS(const FURL& URL) if (GameInstance == nullptr) { - UE_LOG(LogSpatialOSNetDriver, Error, TEXT("A SpatialGameInstance is required. Make sure your game's GameInstance inherits from SpatialGameInstance")); + UE_LOG(LogSpatialOSNetDriver, Error, + TEXT("A SpatialGameInstance is required. Make sure your game's GameInstance inherits from SpatialGameInstance")); return; } @@ -244,6 +264,7 @@ void USpatialNetDriver::InitiateConnectionToSpatialOS(const FURL& URL) ConnectionManager = GameInstance->GetSpatialConnectionManager(); ConnectionManager->OnConnectedCallback.BindUObject(this, &USpatialNetDriver::OnConnectionToSpatialOSSucceeded); ConnectionManager->OnFailedToConnectCallback.BindUObject(this, &USpatialNetDriver::OnConnectionToSpatialOSFailed); + ConnectionManager->SetComponentSets(ClassInfoManager->SchemaDatabase->ComponentSetIdToComponentIds); // 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. @@ -292,6 +313,16 @@ void USpatialNetDriver::OnConnectionToSpatialOSSucceeded() Connection = ConnectionManager->GetWorkerConnection(); check(Connection); + // If the current Connection comes from an outdated ClientTravel, the associated NetDriver (this) won't match + // the NetDriver from the Engine, resulting in a crash. Here, if the NetDriver is outdated, we leave the callback. + if (bConnectAsClient && GEngine->GetWorldContextFromPendingNetGameNetDriver(this) == nullptr) + { + UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Outdated NetDriver connection skipped. May be due to an outdated ClientTravel")); + ConnectionManager->OnConnectedCallback.Unbind(); + ConnectionManager->OnFailedToConnectCallback.Unbind(); + return; + } + // 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 (!bConnectAsClient) @@ -309,7 +340,7 @@ void USpatialNetDriver::OnConnectionToSpatialOSSucceeded() USpatialGameInstance* GameInstance = GetGameInstance(); check(GameInstance != nullptr); - GameInstance->HandleOnConnected(); + GameInstance->HandleOnConnected(*this); } void USpatialNetDriver::OnConnectionToSpatialOSFailed(uint8_t ConnectionStatusCode, const FString& ErrorMessage) @@ -318,7 +349,8 @@ void USpatialNetDriver::OnConnectionToSpatialOSFailed(uint8_t ConnectionStatusCo { if (GEngine != nullptr && GameInstance->GetWorld() != nullptr) { - GEngine->BroadcastNetworkFailure(GameInstance->GetWorld(), this, ENetworkFailure::FromDisconnectOpStatusCode(ConnectionStatusCode), *ErrorMessage); + GEngine->BroadcastNetworkFailure(GameInstance->GetWorld(), this, + ENetworkFailure::FromDisconnectOpStatusCode(ConnectionStatusCode), *ErrorMessage); } GameInstance->HandleOnConnectionFailed(ErrorMessage); @@ -337,11 +369,11 @@ void USpatialNetDriver::InitializeSpatialOutputDevice() { PIEIndex = GEngine->GetWorldContextFromPendingNetGameNetDriverChecked(this).PIEInstance; } -#endif //WITH_EDITOR +#endif // WITH_EDITOR FName LoggerName = FName(TEXT("Unreal")); - if (const USpatialGameInstance * GameInstance = GetGameInstance()) + if (const USpatialGameInstance* GameInstance = GetGameInstance()) { LoggerName = GameInstance->GetSpatialWorkerType(); } @@ -375,36 +407,28 @@ void USpatialNetDriver::CreateAndInitializeCoreClasses() SpatialMetrics = NewObject(); SpatialWorkerFlags = NewObject(); - const USpatialGDKSettings* SpatialSettings = GetDefault(); -#if !UE_BUILD_SHIPPING - // If metrics display is enabled, spawn an Actor to replicate the information to each client - if (IsServer()) - { - if (SpatialSettings->bEnableMetricsDisplay) - { - SpatialMetricsDisplay = GetWorld()->SpawnActor(); - } + CreateAndInitializeLoadBalancingClasses(); - if (SpatialSettings->SpatialDebugger != nullptr) - { - SpatialDebugger = GetWorld()->SpawnActor(SpatialSettings->SpatialDebugger); - } - } -#endif + const FFilterPredicate ActorFilter = [](const Worker_EntityId, const SpatialGDK::EntityViewElement& Element) { + return !Element.Components.ContainsByPredicate(SpatialGDK::ComponentIdEquality{ SpatialConstants::TOMBSTONE_COMPONENT_ID }); + }; + const TArray RefreshCallbacks = { Connection->GetCoordinator().CreateComponentExistenceRefreshCallback( + SpatialConstants::TOMBSTONE_COMPONENT_ID) }; - CreateAndInitializeLoadBalancingClasses(); + const SpatialGDK::FSubView& ActorAuthSubview = + Connection->GetCoordinator().CreateSubView(SpatialConstants::ACTOR_AUTH_TAG_COMPONENT_ID, ActorFilter, RefreshCallbacks); + const SpatialGDK::FSubView& ActorNonAuthSubview = + Connection->GetCoordinator().CreateSubView(SpatialConstants::ACTOR_NON_AUTH_TAG_COMPONENT_ID, ActorFilter, RefreshCallbacks); - if (SpatialSettings->UseRPCRingBuffer()) - { - RPCService = MakeUnique(ExtractRPCDelegate::CreateUObject(Receiver, &USpatialReceiver::OnExtractIncomingRPC), StaticComponentView, USpatialLatencyTracer::GetTracer(GetWorld())); - } + RPCService = MakeUnique( + ActorAuthSubview, ActorNonAuthSubview, USpatialLatencyTracer::GetTracer(GetWorld()), Connection->GetEventTracer(), this); Dispatcher->Init(Receiver, StaticComponentView, SpatialMetrics, SpatialWorkerFlags); - Sender->Init(this, &TimerManager, RPCService.Get()); - Receiver->Init(this, &TimerManager, RPCService.Get()); + Sender->Init(this, &TimerManager, RPCService.Get(), Connection->GetEventTracer()); + Receiver->Init(this, &TimerManager, RPCService.Get(), Connection->GetEventTracer()); GlobalStateManager->Init(this); SnapshotManager->Init(Connection, GlobalStateManager, Receiver); - PlayerSpawner->Init(this, &TimerManager); + PlayerSpawner->Init(this); PlayerSpawner->OnPlayerSpawnFailed.BindUObject(GameInstance, &USpatialGameInstance::HandleOnPlayerSpawnFailed); SpatialMetrics->Init(Connection, NetServerMaxTickRate, IsServer()); SpatialMetrics->ControllerRefProvider.BindUObject(this, &USpatialNetDriver::GetCurrentPlayerControllerRef); @@ -422,6 +446,17 @@ void USpatialNetDriver::CreateAndInitializeCoreClasses() // The interest factory depends on the package map, so is created last. InterestFactory = MakeUnique(ClassInfoManager, PackageMap); + + if (!IsServer()) + { + return; + } + + SpatialGDK::FSubView& WellKnownSubView = Connection->GetCoordinator().CreateSubView( + SpatialConstants::GDK_KNOWN_ENTITY_TAG_COMPONENT_ID, SpatialGDK::FSubView::NoFilter, SpatialGDK::FSubView::NoDispatcherCallbacks); + WellKnownEntitySystem = MakeUnique(WellKnownSubView, Receiver, Connection, + LoadBalanceStrategy->GetMinimumRequiredWorkers(), + *VirtualWorkerTranslator, *GlobalStateManager); } void USpatialNetDriver::CreateAndInitializeLoadBalancingClasses() @@ -434,35 +469,45 @@ void USpatialNetDriver::CreateAndInitializeLoadBalancingClasses() const UWorld* CurrentWorld = GetWorld(); check(CurrentWorld != nullptr); - const ASpatialWorldSettings* WorldSettings = Cast(CurrentWorld->GetWorldSettings()); - check(WorldSettings != nullptr); + const bool bMultiWorkerEnabled = USpatialStatics::IsMultiWorkerEnabled(); - const bool bMultiWorkerEnabled = USpatialStatics::IsSpatialMultiWorkerEnabled(CurrentWorld); + const TSubclassOf MultiWorkerSettingsClass = + USpatialStatics::GetSpatialMultiWorkerClass(CurrentWorld); - // If multi worker is disabled, the USpatialMultiWorkerSettings CDO will give us single worker behaviour. - const TSubclassOf MultiWorkerSettingsClass = bMultiWorkerEnabled ? - *WorldSettings->MultiWorkerSettingsClass : - USpatialMultiWorkerSettings::StaticClass(); - - const UAbstractSpatialMultiWorkerSettings* MultiWorkerSettings = NewObject(this, *MultiWorkerSettingsClass); + const UAbstractSpatialMultiWorkerSettings* MultiWorkerSettings = + MultiWorkerSettingsClass->GetDefaultObject(); if (bMultiWorkerEnabled && MultiWorkerSettings->LockingPolicy == nullptr) { - UE_LOG(LogSpatialOSNetDriver, Error, TEXT("If Load balancing is enabled, there must be a Locking Policy set. Using default policy.")); + UE_LOG(LogSpatialOSNetDriver, Error, + TEXT("If Load balancing is enabled, there must be a Locking Policy set. Using default policy.")); } - const TSubclassOf LockingPolicyClass = bMultiWorkerEnabled && *MultiWorkerSettings->LockingPolicy != nullptr ? - *MultiWorkerSettings->LockingPolicy : - UOwnershipLockingPolicy::StaticClass(); + const TSubclassOf LockingPolicyClass = bMultiWorkerEnabled && *MultiWorkerSettings->LockingPolicy != nullptr + ? *MultiWorkerSettings->LockingPolicy + : UOwnershipLockingPolicy::StaticClass(); LoadBalanceStrategy = NewObject(this); LoadBalanceStrategy->Init(); Cast(LoadBalanceStrategy)->SetLayers(MultiWorkerSettings->WorkerLayers); LoadBalanceStrategy->SetVirtualWorkerIds(1, LoadBalanceStrategy->GetMinimumRequiredWorkers()); - VirtualWorkerTranslator = MakeUnique(LoadBalanceStrategy, Connection->GetWorkerId()); + VirtualWorkerTranslator = MakeUnique(LoadBalanceStrategy, this, Connection->GetWorkerId()); + + const SpatialGDK::FSubView& LBSubView = Connection->GetCoordinator().CreateSubView( + SpatialConstants::LB_TAG_COMPONENT_ID, SpatialGDK::FSubView::NoFilter, SpatialGDK::FSubView::NoDispatcherCallbacks); - LoadBalanceEnforcer = MakeUnique(Connection->GetWorkerId(), StaticComponentView, VirtualWorkerTranslator.Get()); + TUniqueFunction AuthorityUpdateSender = + [this](SpatialGDK::EntityComponentUpdate AuthorityUpdate) { + // We pass the component update function of the view coordinator rather than the connection. This + // is so any updates are written to the local view before being sent. This does mean the connection send + // is not fully async right now, but could be if we replaced this with a "send and flush", which would + // be hard to do now due to short circuiting, but in the near future when LB runs on its own worker then + // we can make that optimisation. + Connection->GetCoordinator().SendComponentUpdate(AuthorityUpdate.EntityId, MoveTemp(AuthorityUpdate.Update), {}); + }; + LoadBalanceEnforcer = MakeUnique( + Connection->GetWorkerId(), LBSubView, VirtualWorkerTranslator.Get(), MoveTemp(AuthorityUpdateSender)); LockingPolicy = NewObject(this, LockingPolicyClass); LockingPolicy->Init(AcquireLockDelegate, ReleaseLockDelegate); @@ -477,8 +522,8 @@ void USpatialNetDriver::CreateServerSpatialOSNetConnection() ISocketSubsystem* SocketSubsystem = GetSocketSubsystem(); // This is just a fake address so that Unreal doesn't ensure-crash on disconnecting from SpatialOS - // See UNetDriver::RemoveClientConnection for more details, but basically there is a TMap which uses internet addresses as the key and an unitialised - // internet address for a connection causes the TMap.Find to fail + // See UNetDriver::RemoveClientConnection for more details, but basically there is a TMap which uses internet addresses as the key and + // an unitialised internet address for a connection causes the TMap.Find to fail TSharedRef FromAddr = SocketSubsystem->CreateInternetAddr(); bool bIsAddressValid = false; FromAddr->SetIp(*SpatialConstants::LOCAL_HOST, bIsAddressValid); @@ -494,13 +539,33 @@ void USpatialNetDriver::CreateServerSpatialOSNetConnection() Notify->NotifyAcceptedConnection(NetConnection); NetConnection->bReliableSpatialConnection = true; AddClientConnection(NetConnection); - //Since this is not a "real" client connection, we immediately pretend that it is fully logged on. + // Since this is not a "real" client connection, we immediately pretend that it is fully logged on. NetConnection->SetClientLoginState(EClientLoginState::Welcomed); - // Bind the ProcessServerTravel delegate to the spatial variant. This ensures that if ServerTravel is called and Spatial networking is enabled, we can travel properly. + // Bind the ProcessServerTravel delegate to the spatial variant. This ensures that if ServerTravel is called and Spatial networking is + // enabled, we can travel properly. GetWorld()->SpatialProcessServerTravelDelegate.BindStatic(SpatialProcessServerTravel); } +void USpatialNetDriver::CleanUpServerConnectionForPC(APlayerController* PC) +{ + // Can't do Cast(PC->Player) as Player is null for some reason. + // Perhaps a slight defect in how SpatialNetDriver handles setting up a player? + // Instead we simply iterate through all connections and find the one with the matching (correctly set) OwningActor + for (UNetConnection* ClientConnection : ClientConnections) + { + if (ClientConnection->OwningActor == PC) + { + USpatialNetConnection* SpatialConnection = Cast(ClientConnection); + check(SpatialConnection != nullptr); + SpatialConnection->CleanUp(); + return; + } + } + UE_LOG(LogSpatialOSNetDriver, Error, + TEXT("While trying to clean up a PlayerController, its client connection was not found and thus cleanup was not performed")); +} + bool USpatialNetDriver::ClientCanSendPlayerSpawnRequests() { return GlobalStateManager->GetAcceptingPlayers() && SessionId == GlobalStateManager->GetSessionId(); @@ -508,18 +573,33 @@ bool USpatialNetDriver::ClientCanSendPlayerSpawnRequests() void USpatialNetDriver::OnGSMQuerySuccess() { + StartupClientDebugString.Empty(); // 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? + if (ClassInfoManager->SchemaDatabase->SchemaBundleHash != 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); + UE_LOG(LogSpatialOSNetDriver, Error, + TEXT("Your client's schema does not match your deployment's schema. Client hash: '%u' Server hash: '%u'"), + ClassInfoManager->SchemaDatabase->SchemaBundleHash, ServerHash); + + if (USpatialGameInstance* GameInstance = GetGameInstance()) + { + if (GEngine != nullptr && GameInstance->GetWorld() != nullptr) + { + GEngine->BroadcastNetworkFailure( + GameInstance->GetWorld(), this, ENetworkFailure::OutdatedClient, + TEXT("Your version of the game does not match that of the server. Please try updating your game version.")); + return; + } + } } UWorld* CurrentWorld = GetWorld(); const FString& DeploymentMapURL = GlobalStateManager->GetDeploymentMapURL(); - if (CurrentWorld == nullptr || CurrentWorld->RemovePIEPrefix(DeploymentMapURL) != CurrentWorld->RemovePIEPrefix(CurrentWorld->URL.Map)) + 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); @@ -562,27 +642,30 @@ void USpatialNetDriver::RetryQueryGSM() RetryTimerDelay = 0.1f; #endif - UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Retrying query for GSM in %f seconds"), RetryTimerDelay); + UE_LOG(LogSpatialOSNetDriver, Verbose, 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) + TimerManager.SetTimer( + RetryTimer, + [WeakThis = TWeakObjectPtr(this)]() { + if (WeakThis.IsValid()) { - UGlobalStateManager::QueryDelegate QueryDelegate; - QueryDelegate.BindUObject(WeakThis.Get(), &USpatialNetDriver::GSMQueryDelegateFunction); - GSM->QueryGSM(QueryDelegate); + if (UGlobalStateManager* GSM = WeakThis.Get()->GlobalStateManager) + { + UGlobalStateManager::QueryDelegate QueryDelegate; + QueryDelegate.BindUObject(WeakThis.Get(), &USpatialNetDriver::GSMQueryDelegateFunction); + GSM->QueryGSM(QueryDelegate); + } } - } - }, RetryTimerDelay, false); + }, + RetryTimerDelay, false); } void USpatialNetDriver::GSMQueryDelegateFunction(const Worker_EntityQueryResponseOp& Op) { bool bNewAcceptingPlayers = false; int32 QuerySessionId = 0; - bool bQueryResponseSuccess = GlobalStateManager->GetAcceptingPlayersAndSessionIdFromQueryResponse(Op, bNewAcceptingPlayers, QuerySessionId); + bool bQueryResponseSuccess = + GlobalStateManager->GetAcceptingPlayersAndSessionIdFromQueryResponse(Op, bNewAcceptingPlayers, QuerySessionId); if (!bQueryResponseSuccess) { @@ -592,13 +675,15 @@ void USpatialNetDriver::GSMQueryDelegateFunction(const Worker_EntityQueryRespons } else if (!bNewAcceptingPlayers) { - UE_LOG(LogSpatialOSNetDriver, Log, TEXT("GlobalStateManager not accepting players. Usually caused by servers not registering themselves with the deployment yet. Did you launch the correct number of servers?")); + StartupClientDebugString = FString( + TEXT("GlobalStateManager not accepting players. This is likely caused by waiting for all the required servers to connect")); RetryQueryGSM(); return; } else if (QuerySessionId != SessionId) { - UE_LOG(LogSpatialOSNetDriver, Log, TEXT("GlobalStateManager session id mismatch - got (%d) expected (%d)."), QuerySessionId, SessionId); + StartupClientDebugString = + FString::Printf(TEXT("GlobalStateManager session id mismatch - got (%d) expected (%d)."), QuerySessionId, SessionId); RetryQueryGSM(); return; } @@ -620,18 +705,17 @@ void USpatialNetDriver::QueryGSMToLoadMap() GlobalStateManager->QueryGSM(QueryDelegate); } -void USpatialNetDriver::OnActorSpawned(AActor* Actor) +void USpatialNetDriver::OnActorSpawned(AActor* Actor) const { const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); - if (!SpatialGDKSettings->bEnableMultiWorkerDebuggingWarnings) + if (SpatialGDKSettings->bEnableCrossLayerActorSpawning) { return; } - if (!Actor->GetIsReplicated() || - Actor->GetLocalRole() != ROLE_Authority || - !Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_SpatialType) || - (IsReady() && USpatialStatics::IsActorGroupOwnerForActor(Actor))) + if (!Actor->GetIsReplicated() || Actor->GetLocalRole() != ROLE_Authority + || !Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_SpatialType) + || (IsReady() && USpatialStatics::IsActorGroupOwnerForActor(Actor))) { // We only want to delete actors which are replicated and we somehow gain local authority over, // when they should be in a different Layer. @@ -640,23 +724,21 @@ void USpatialNetDriver::OnActorSpawned(AActor* Actor) if (!IsReady()) { - UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Spawned replicated actor %s (owner: %s) before the NetDriver was ready. This is not supported. Actors should only be spawned after BeginPlay is called."), - *GetNameSafe(Actor), *GetNameSafe(Actor->GetOwner())); + UE_LOG(LogSpatialOSNetDriver, Warning, + TEXT("Spawned replicated actor %s (owner: %s) before the NetDriver was ready. This is not supported. Actors should only be " + "spawned after BeginPlay is called."), + *GetNameSafe(Actor), *GetNameSafe(Actor->GetOwner())); return; } - if (LoadBalanceStrategy != nullptr) - { - UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Worker ID %d spawned replicated actor %s (owner: %s) but should not have authority. It should be owned by %d. The actor will be destroyed in 0.01s"), - LoadBalanceStrategy->GetLocalVirtualWorkerId(), *GetNameSafe(Actor), *GetNameSafe(Actor->GetOwner()), LoadBalanceStrategy->WhoShouldHaveAuthority(*Actor)); - } - else - { - UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Worker spawned replicated actor %s (owner: %s) but should not have authority. The actor will be destroyed in 0.01s"), - *GetNameSafe(Actor), *GetNameSafe(Actor->GetOwner())); - } + UE_LOG(LogSpatialOSNetDriver, Error, + TEXT("Worker ID %d spawned replicated actor %s (owner: %s) but should not have authority. It should be owned by %d. The actor " + "will be destroyed in 0.01s"), + LoadBalanceStrategy->GetLocalVirtualWorkerId(), *GetNameSafe(Actor), *GetNameSafe(Actor->GetOwner()), + LoadBalanceStrategy->WhoShouldHaveAuthority(*Actor)); - // 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. + // 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); } @@ -678,9 +760,9 @@ void USpatialNetDriver::OnMapLoaded(UWorld* LoadedWorld) if (IsServer()) { - if (GlobalStateManager != nullptr && - !GlobalStateManager->GetCanBeginPlay() && - StaticComponentView->HasAuthority(GlobalStateManager->GlobalStateManagerEntityId, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID)) + if (GlobalStateManager != nullptr && !GlobalStateManager->GetCanBeginPlay() + && StaticComponentView->HasAuthority(GlobalStateManager->GlobalStateManagerEntityId, + SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID)) { // ServerTravel - Increment the session id, so users don't rejoin the old game. GlobalStateManager->TriggerBeginPlay(); @@ -697,7 +779,9 @@ void USpatialNetDriver::OnMapLoaded(UWorld* LoadedWorld) } else { - 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.")); + 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(); } } @@ -715,41 +799,6 @@ void USpatialNetDriver::MakePlayerSpawnRequest() } } -void USpatialNetDriver::OnLevelAddedToWorld(ULevel* LoadedLevel, UWorld* OwningWorld) -{ - UE_LOG(LogSpatialOSNetDriver, Log, TEXT("OnLevelAddedToWorld: Level (%s) OwningWorld (%s) World (%s)"), - *GetNameSafe(LoadedLevel), *GetNameSafe(OwningWorld), *GetNameSafe(World)); - - if (OwningWorld != World - || !IsServer() - || GlobalStateManager == nullptr) - { - // If the world isn't our owning world, we are a client, or we loaded the levels - // before connecting to Spatial, we return early. - return; - } - - const bool bHaveGSMAuthority = StaticComponentView->HasAuthority(SpatialConstants::INITIAL_GLOBAL_STATE_MANAGER_ENTITY_ID, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID); - - if (!LoadBalanceStrategy->IsReady()) - { - // Load balancer isn't ready, this should only occur when servers are loading composition levels on startup, before connecting to spatial. - return; - } - - 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()) - { - const bool bRoleAuthoritative = LoadBalanceStrategy->ShouldHaveAuthority(*Actor); - Actor->Role = bRoleAuthoritative ? ROLE_Authority : ROLE_SimulatedProxy; - Actor->RemoteRole = bRoleAuthoritative ? ROLE_SimulatedProxy : ROLE_Authority; - } - } -} - // NOTE: This method is a clone of the ProcessServerTravel located in GameModeBase with modifications to support Spatial. // Will be called via a delegate that has been set in the UWorld instead of the original in GameModeBase. void USpatialNetDriver::SpatialProcessServerTravel(const FString& URL, bool bAbsolute, AGameModeBase* GameMode) @@ -759,7 +808,8 @@ void USpatialNetDriver::SpatialProcessServerTravel(const FString& URL, bool bAbs UWorld* World = GameMode->GetWorld(); USpatialNetDriver* NetDriver = Cast(World->GetNetDriver()); - if (!NetDriver->StaticComponentView->HasAuthority(SpatialConstants::INITIAL_GLOBAL_STATE_MANAGER_ENTITY_ID, SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID)) + if (!NetDriver->StaticComponentView->HasAuthority(SpatialConstants::INITIAL_GLOBAL_STATE_MANAGER_ENTITY_ID, + SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_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.")); @@ -821,8 +871,7 @@ void USpatialNetDriver::SpatialProcessServerTravel(const FString& URL, bool bAbs // FinishServerTravel - Allows Unreal to finish it's normal server travel. PostWorldWipeDelegate FinishServerTravel; - FinishServerTravel.BindLambda([World, NetDriver, NewURL, NetMode, bSeamless, bAbsolute] - { + FinishServerTravel.BindLambda([World, NetDriver, NewURL, NetMode, bSeamless, bAbsolute] { UE_LOG(LogGameMode, Log, TEXT("SpatialServerTravel - Finishing Server Travel : %s"), *NewURL); check(World); World->NextURL = NewURL; @@ -850,10 +899,19 @@ void USpatialNetDriver::BeginDestroy() if (Connection != nullptr) { + // Delete all load-balancing partition entities if we're translator authoritative. + if (VirtualWorkerTranslationManager != nullptr) + { + for (const auto& Partition : VirtualWorkerTranslationManager->GetAllPartitions()) + { + Connection->SendDeleteEntityRequest(Partition.PartitionEntityId, SpatialGDK::RETRY_UNTIL_COMPLETE); + } + } + // Cleanup our corresponding worker entity if it exists. if (WorkerEntityId != SpatialConstants::INVALID_ENTITY_ID) { - Connection->SendDeleteEntityRequest(WorkerEntityId); + Connection->SendDeleteEntityRequest(WorkerEntityId, SpatialGDK::RETRY_UNTIL_COMPLETE); // Flush the connection and wait a moment to allow the message to propagate. // TODO: UNR-3697 - This needs to be handled more correctly @@ -894,7 +952,7 @@ void USpatialNetDriver::PostInitProperties() bool USpatialNetDriver::IsLevelInitializedForActor(const AActor* InActor, const UNetConnection* InConnection) const { - //In our case, the connection is not specific to a client. Thus, it's not relevant whether the level is initialized. + // In our case, the connection is not specific to a client. Thus, it's not relevant whether the level is initialized. return true; } @@ -920,18 +978,23 @@ void USpatialNetDriver::NotifyActorDestroyed(AActor* ThisActor, bool IsSeamlessT const Worker_EntityId EntityId = PackageMap->GetEntityIdFromObject(ThisActor); // 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()) + 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()); + 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)) + if (!HasServerAuthority(EntityId)) { - UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Retiring dormant entity that we don't have spatial authority over [%lld][%s]"), EntityId, *ThisActor->GetName()); + UE_LOG(LogSpatialOSNetDriver, Warning, + TEXT("Retiring dormant entity that we don't have spatial authority over [%lld][%s]"), EntityId, + *ThisActor->GetName()); } Sender->RetireEntity(EntityId, ThisActor->IsNetStartupActor()); } @@ -967,6 +1030,8 @@ void USpatialNetDriver::NotifyActorDestroyed(AActor* ThisActor, bool IsSeamlessT void USpatialNetDriver::Shutdown() { + USpatialNetDriverDebugContext::DisableDebugSpatialGDK(this); + if (!IsServer()) { // Notify the server that we're disconnecting so it can clean up our actors. @@ -989,21 +1054,21 @@ void USpatialNetDriver::Shutdown() { for (const Worker_EntityId EntityId : DormantEntities) { - if (StaticComponentView->HasAuthority(EntityId, SpatialGDK::Position::ComponentId)) + if (HasServerAuthority(EntityId)) { - Connection->SendDeleteEntityRequest(EntityId); + Connection->SendDeleteEntityRequest(EntityId, SpatialGDK::RETRY_UNTIL_COMPLETE); } } for (const Worker_EntityId EntityId : TombstonedEntities) { - if (StaticComponentView->HasAuthority(EntityId, SpatialGDK::Position::ComponentId)) + if (HasServerAuthority(EntityId)) { - Connection->SendDeleteEntityRequest(EntityId); + Connection->SendDeleteEntityRequest(EntityId, SpatialGDK::RETRY_UNTIL_COMPLETE); } } } -#endif //WITH_EDITOR +#endif // WITH_EDITOR } void USpatialNetDriver::NotifyActorFullyDormantForConnection(AActor* Actor, UNetConnection* NetConnection) @@ -1032,6 +1097,11 @@ void USpatialNetDriver::OnOwnerUpdated(AActor* Actor, AActor* OldOwner) LockingPolicy->OnOwnerUpdated(Actor, OldOwner); } + if (USpatialReplicationGraph* ReplicationGraph = Cast(GetReplicationDriver())) + { + ReplicationGraph->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) @@ -1053,14 +1123,28 @@ void USpatialNetDriver::OnOwnerUpdated(AActor* Actor, AActor* OldOwner) Channel->MarkInterestDirty(); - Channel->ServerProcessOwnershipChange(); + OwnershipChangedEntities.Add(EntityId); } -//SpatialGDK: Functions in the ifdef block below are modified versions of the UNetDriver:: implementations. +void USpatialNetDriver::ProcessOwnershipChanges() +{ + for (Worker_EntityId EntityId : OwnershipChangedEntities) + { + if (USpatialActorChannel* Channel = GetActorChannelByEntityId(EntityId)) + { + Channel->ServerProcessOwnershipChange(); + } + } + + OwnershipChangedEntities.Empty(); +} + +// SpatialGDK: Functions in the ifdef block below are modified versions of the UNetDriver:: implementations. #if WITH_SERVER_CODE // Returns true if this actor should replicate to *any* of the passed in connections -static FORCEINLINE_DEBUGGABLE bool IsActorRelevantToConnection(const AActor* Actor, UActorChannel* ActorChannel, const TArray& ConnectionViewers) +static FORCEINLINE_DEBUGGABLE bool IsActorRelevantToConnection(const AActor* Actor, UActorChannel* ActorChannel, + const TArray& ConnectionViewers) { // An actor without a channel yet will need to be replicated at least // once to have a channel and entity created for it @@ -1088,7 +1172,8 @@ static FORCEINLINE_DEBUGGABLE bool IsActorDormant(FNetworkObjectInfo* ActorInfo, } // Returns true if this actor wants to go dormant for a particular connection -static FORCEINLINE_DEBUGGABLE bool ShouldActorGoDormant(AActor* Actor, const TArray& ConnectionViewers, UActorChannel* Channel, const float Time, const bool bLowNetBandwidth) +static FORCEINLINE_DEBUGGABLE bool ShouldActorGoDormant(AActor* Actor, const TArray& ConnectionViewers, UActorChannel* Channel, + const float Time, const bool bLowNetBandwidth) { if (Actor->NetDormancy <= DORM_Awake || !Channel || Channel->bPendingDormancy || Channel->Dormant) { @@ -1100,7 +1185,9 @@ static FORCEINLINE_DEBUGGABLE bool ShouldActorGoDormant(AActor* Actor, const TAr { for (int32 viewerIdx = 0; viewerIdx < ConnectionViewers.Num(); viewerIdx++) { - if (!Actor->GetNetDormancy(ConnectionViewers[viewerIdx].ViewLocation, ConnectionViewers[viewerIdx].ViewDir, ConnectionViewers[viewerIdx].InViewer, ConnectionViewers[viewerIdx].ViewTarget, Channel, Time, bLowNetBandwidth)) + if (!Actor->GetNetDormancy(ConnectionViewers[viewerIdx].ViewLocation, ConnectionViewers[viewerIdx].ViewDir, + ConnectionViewers[viewerIdx].InViewer, ConnectionViewers[viewerIdx].ViewTarget, Channel, Time, + bLowNetBandwidth)) { return false; } @@ -1122,23 +1209,27 @@ int32 USpatialNetDriver::ServerReplicateActors_PrepConnections(const float Delta { USpatialNetConnection* SpatialConnection = Cast(ClientConnections[ConnIdx]); check(SpatialConnection); - check(SpatialConnection->State == USOCK_Pending || SpatialConnection->State == USOCK_Open || SpatialConnection->State == USOCK_Closed); + check(SpatialConnection->State == USOCK_Pending || SpatialConnection->State == USOCK_Open + || SpatialConnection->State == USOCK_Closed); checkSlow(SpatialConnection->GetUChildConnection() == NULL); // Handle not ready channels. - //@note: we cannot add Connection->IsNetReady(0) here to check for saturation, as if that's the case we still want to figure out the list of relevant actors - // to reset their NetUpdateTime so that they will get sent as soon as the connection is no longer saturated + // @note: we cannot add Connection->IsNetReady(0) here to check for saturation, as if that's the case we still + // want to figure out the list of relevant actors to reset their NetUpdateTime so that they will get sent as + // soon as the connection is no longer saturated. AActor* OwningActor = SpatialConnection->OwningActor; - //SpatialGDK: We allow a connection without an owner to process if it's meant to be the connection to the fake SpatialOS client. - if ((SpatialConnection->bReliableSpatialConnection || OwningActor != NULL) && SpatialConnection->State == USOCK_Open && (GetElapsedTime() - SpatialConnection->LastReceiveTime < 1.5f)) + // SpatialGDK: We allow a connection without an owner to process if it's meant to be the connection to the fake SpatialOS client. + if ((SpatialConnection->bReliableSpatialConnection || OwningActor != NULL) && SpatialConnection->State == USOCK_Open + && (GetElapsedTime() - SpatialConnection->LastReceiveTime < 1.5f)) { check(SpatialConnection->bReliableSpatialConnection || World == OwningActor->GetWorld()); bFoundReadyConnection = true; // the view target is what the player controller is looking at OR the owning actor itself when using beacons - SpatialConnection->ViewTarget = SpatialConnection->PlayerController ? SpatialConnection->PlayerController->GetViewTarget() : OwningActor; + SpatialConnection->ViewTarget = + SpatialConnection->PlayerController ? SpatialConnection->PlayerController->GetViewTarget() : OwningActor; } else { @@ -1147,7 +1238,10 @@ int32 USpatialNetDriver::ServerReplicateActors_PrepConnections(const float Delta if (SpatialConnection->Children.Num() > 0) { - UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Child connections present on Spatial connection %s! We don't support splitscreen yet, so this will not function correctly."), *SpatialConnection->GetName()); + UE_LOG(LogSpatialOSNetDriver, Error, + TEXT("Child connections present on Spatial connection %s! We don't support splitscreen yet, so this will not function " + "correctly."), + *SpatialConnection->GetName()); } } @@ -1165,7 +1259,7 @@ struct FCompareActorPriorityAndMigration { const bool AMigrates = MigrationHandler.GetActorsToMigrate().Contains(A.ActorInfo->Actor); const bool BMigrates = MigrationHandler.GetActorsToMigrate().Contains(B.ActorInfo->Actor); - if(AMigrates == BMigrates) + if (AMigrates == BMigrates) { return B.Priority < A.Priority; } @@ -1181,7 +1275,10 @@ struct FCompareActorPriorityAndMigration const FSpatialLoadBalancingHandler& MigrationHandler; }; -int32 USpatialNetDriver::ServerReplicateActors_PrioritizeActors(UNetConnection* InConnection, const TArray& ConnectionViewers, FSpatialLoadBalancingHandler& MigrationHandler, const TArray ConsiderList, const bool bCPUSaturated, FActorPriority*& OutPriorityList, FActorPriority**& OutPriorityActors) +int32 USpatialNetDriver::ServerReplicateActors_PrioritizeActors(UNetConnection* InConnection, const TArray& ConnectionViewers, + FSpatialLoadBalancingHandler& MigrationHandler, + const TArray ConsiderList, const bool bCPUSaturated, + FActorPriority*& OutPriorityList, FActorPriority**& OutPriorityActors) { // Since this function signature is copied from NetworkDriver.cpp, I don't want to change the signature. But we expect // that the input connection will be the SpatialOS server connection to the runtime (the first client connection), @@ -1249,11 +1346,13 @@ int32 USpatialNetDriver::ServerReplicateActors_PrioritizeActors(UNetConnection* // NOTE - We use NetTag to make sure SentTemporaries didn't already mark this actor to be skipped if (Actor->NetTag != NetTag) { - UE_LOG(LogNetTraffic, Log, TEXT("Consider %s alwaysrelevant %d frequency %f "), *Actor->GetName(), Actor->bAlwaysRelevant, Actor->NetUpdateFrequency); + UE_LOG(LogNetTraffic, Log, TEXT("Consider %s alwaysrelevant %d frequency %f "), *Actor->GetName(), Actor->bAlwaysRelevant, + Actor->NetUpdateFrequency); Actor->NetTag = NetTag; - OutPriorityList[FinalSortedCount] = FActorPriority(PriorityConnection, Channel, ActorInfo, ConnectionViewers, bLowNetBandwidth); + OutPriorityList[FinalSortedCount] = + FActorPriority(PriorityConnection, Channel, ActorInfo, ConnectionViewers, bLowNetBandwidth); OutPriorityActors[FinalSortedCount] = OutPriorityList + FinalSortedCount; FinalSortedCount++; @@ -1277,7 +1376,8 @@ int32 USpatialNetDriver::ServerReplicateActors_PrioritizeActors(UNetConnection* if (MigrationHandler.GetActorsToMigrate().Num() > 0) { - // Process actors migrating first, in order to not have them separated if they need to migrate together and replication rate limiting happens. + // Process actors migrating first, in order to not have them separated if they need to migrate together and replication rate + // limiting happens. Sort(OutPriorityActors, FinalSortedCount, FCompareActorPriorityAndMigration(MigrationHandler)); } else @@ -1287,12 +1387,17 @@ int32 USpatialNetDriver::ServerReplicateActors_PrioritizeActors(UNetConnection* } } - UE_LOG(LogNetTraffic, Log, TEXT("ServerReplicateActors_PrioritizeActors: Potential %04i ConsiderList %03i FinalSortedCount %03i"), MaxSortedActors, ConsiderList.Num(), FinalSortedCount); + UE_LOG(LogNetTraffic, Log, TEXT("ServerReplicateActors_PrioritizeActors: Potential %04i ConsiderList %03i FinalSortedCount %03i"), + MaxSortedActors, ConsiderList.Num(), FinalSortedCount); return FinalSortedCount; } -void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConnection* InConnection, const TArray& ConnectionViewers, FSpatialLoadBalancingHandler& MigrationHandler, FActorPriority** PriorityActors, const int32 FinalSortedCount, int32& OutUpdated) +void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConnection* InConnection, + const TArray& ConnectionViewers, + FSpatialLoadBalancingHandler& MigrationHandler, + FActorPriority** PriorityActors, const int32 FinalSortedCount, + int32& OutUpdated) { SCOPE_CYCLE_COUNTER(STAT_SpatialProcessPrioritizedActors); @@ -1306,7 +1411,8 @@ void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConne SET_DWORD_STAT(STAT_SpatialActorsRelevant, 0); SET_DWORD_STAT(STAT_SpatialActorsChanged, 0); - // SpatialGDK - Here Unreal would check if the InConnection was saturated (!IsNetReady) and early out. Removed this as we do not currently use channel saturation. + // SpatialGDK - Here Unreal would check if the InConnection was saturated (!IsNetReady) and early out. Removed this as we do not + // currently use channel saturation. int32 ActorUpdatesThisConnection = 0; int32 ActorUpdatesThisConnectionSent = 0; @@ -1323,7 +1429,8 @@ void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConne int32 MaxActorsToReplicate = (ActorReplicationRateLimit > 0) ? ActorReplicationRateLimit : INT32_MAX; if (MaxActorsToReplicate < NumActorsMigrating) { - UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("ActorReplicationRateLimit of %i ignored because %i actors need to migrate"), MaxActorsToReplicate, NumActorsMigrating); + UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("ActorReplicationRateLimit of %i ignored because %i actors need to migrate"), + MaxActorsToReplicate, NumActorsMigrating); MaxActorsToReplicate = NumActorsMigrating; } int32 FinalReplicatedCount = 0; @@ -1334,7 +1441,8 @@ void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConne if (PriorityActors[j]->ActorInfo == NULL && PriorityActors[j]->DestructionInfo) { // Make sure client has streaming level loaded - if (PriorityActors[j]->DestructionInfo->StreamingLevelName != NAME_None && !InConnection->ClientVisibleLevelNames.Contains(PriorityActors[j]->DestructionInfo->StreamingLevelName)) + if (PriorityActors[j]->DestructionInfo->StreamingLevelName != NAME_None + && !InConnection->ClientVisibleLevelNames.Contains(PriorityActors[j]->DestructionInfo->StreamingLevelName)) { // This deletion entry is for an actor in a streaming level the connection doesn't have loaded, so skip it continue; @@ -1342,19 +1450,24 @@ void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConne UActorChannel* Channel = (UActorChannel*)InConnection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::OpenedLocally); if (Channel) { - UE_LOG(LogNetTraffic, Log, TEXT("Server replicate actor creating destroy channel for NetGUID <%s,%s> Priority: %d"), *PriorityActors[j]->DestructionInfo->NetGUID.ToString(), *PriorityActors[j]->DestructionInfo->PathName, PriorityActors[j]->Priority); + UE_LOG(LogNetTraffic, Log, TEXT("Server replicate actor creating destroy channel for NetGUID <%s,%s> Priority: %d"), + *PriorityActors[j]->DestructionInfo->NetGUID.ToString(), *PriorityActors[j]->DestructionInfo->PathName, + PriorityActors[j]->Priority); - InConnection->GetDestroyedStartupOrDormantActorGUIDs().Remove(PriorityActors[j]->DestructionInfo->NetGUID); // Remove from connections to-be-destroyed list (close bunch of reliable, so it will make it there) + InConnection->GetDestroyedStartupOrDormantActorGUIDs().Remove( + PriorityActors[j]->DestructionInfo->NetGUID); // Remove from connections to-be-destroyed list (close bunch of reliable, + // so it will make it there) } continue; } -#if !( UE_BUILD_SHIPPING || UE_BUILD_TEST ) +#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) static IConsoleVariable* DebugObjectCvar = IConsoleManager::Get().FindConsoleVariable(TEXT("net.PackageMap.DebugObject")); static IConsoleVariable* DebugAllObjectsCvar = IConsoleManager::Get().FindConsoleVariable(TEXT("net.PackageMap.DebugAll")); - if (PriorityActors[j]->ActorInfo && - ((DebugObjectCvar && !DebugObjectCvar->GetString().IsEmpty() && PriorityActors[j]->ActorInfo->Actor->GetName().Contains(DebugObjectCvar->GetString())) || - (DebugAllObjectsCvar && DebugAllObjectsCvar->GetInt() != 0))) + if (PriorityActors[j]->ActorInfo + && ((DebugObjectCvar && !DebugObjectCvar->GetString().IsEmpty() + && PriorityActors[j]->ActorInfo->Actor->GetName().Contains(DebugObjectCvar->GetString())) + || (DebugAllObjectsCvar && DebugAllObjectsCvar->GetInt() != 0))) { UE_LOG(LogNetPackageMap, Log, TEXT("Evaluating actor for replication %s"), *PriorityActors[j]->ActorInfo->Actor->GetName()); } @@ -1381,16 +1494,21 @@ void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConne // SpatialGDK - Creation of new entities should always be handled and therefore is checked prior to actor throttling. // There is an EntityCreationRateLimit to prevent overloading Spatial with creation requests if the developer desires. - // Creation of a new entity occurs when the channel is currently nullptr or if the channel does not have bCreatedEntity set to true. - if (FinalCreationCount < MaxEntitiesToCreate && !Actor->GetTearOff() && (Channel == nullptr || Channel->bCreatingNewEntity)) + // Creation of a new entity occurs when the channel is currently nullptr or if the channel does not have bCreatedEntity set to + // true. + if (!Actor->GetTearOff() && (Channel == nullptr || Channel->bCreatingNewEntity)) { - bIsRelevant = true; - FinalCreationCount++; + if (FinalCreationCount < MaxEntitiesToCreate) + { + bIsRelevant = true; + FinalCreationCount++; + } } - // SpatialGDK - We will only replicate the highest priority actors up the the rate limit and the final tick of TearOff actors. + // SpatialGDK - We will only replicate the highest priority actors up to the rate limit and the final tick of TearOff actors. // Actors not replicated this frame will have their priority increased based on the time since the last replicated. // TearOff actors would normally replicate their final tick due to RecentlyRelevant, after which the channel is closed. - // With throttling we no longer always replicate when RecentlyRelevant is true, thus we ensure to always replicate a TearOff actor while it still has a channel. + // With throttling we no longer always replicate when RecentlyRelevant is true, thus we ensure to always replicate a TearOff + // actor while it still has a channel. else if ((FinalReplicatedCount < MaxActorsToReplicate && !Actor->GetTearOff()) || (Actor->GetTearOff() && Channel != nullptr)) { bIsRelevant = true; @@ -1406,7 +1524,8 @@ void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConne // we can't create the channel if the client is in a different world than we are // or the package map doesn't support the actor's class/archetype (or the actor itself in the case of serializable actors) // or it's an editor placed actor and the client hasn't initialized the level it's in - if (Channel == nullptr && GuidCache->SupportsObject(Actor->GetClass()) && GuidCache->SupportsObject(Actor->IsNetStartupActor() ? Actor : Actor->GetArchetype())) + if (Channel == nullptr && GuidCache->SupportsObject(Actor->GetClass()) + && GuidCache->SupportsObject(Actor->IsNetStartupActor() ? Actor : Actor->GetArchetype())) { if (!Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_SpatialType)) { @@ -1451,10 +1570,12 @@ void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConne // Calculate min delta (max rate actor will update), and max delta (slowest rate actor will update) const float MinOptimalDelta = 1.0f / Actor->NetUpdateFrequency; const float MaxOptimalDelta = FMath::Max(1.0f / Actor->MinNetUpdateFrequency, MinOptimalDelta); - const float DeltaBetweenReplications = (World->TimeSeconds - PriorityActors[j]->ActorInfo->LastNetReplicateTime); + const float DeltaBetweenReplications = + (World->TimeSeconds - PriorityActors[j]->ActorInfo->LastNetReplicateTime); // Choose an optimal time, we choose 70% of the actual rate to allow frequency to go up if needed - PriorityActors[j]->ActorInfo->OptimalNetUpdateDelta = FMath::Clamp(DeltaBetweenReplications * 0.7f, MinOptimalDelta, MaxOptimalDelta); + PriorityActors[j]->ActorInfo->OptimalNetUpdateDelta = + FMath::Clamp(DeltaBetweenReplications * 0.7f, MinOptimalDelta, MaxOptimalDelta); PriorityActors[j]->ActorInfo->LastNetReplicateTime = World->TimeSeconds; } @@ -1480,8 +1601,9 @@ void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConne SET_DWORD_STAT(STAT_SpatialActorsRelevant, ActorUpdatesThisConnection); SET_DWORD_STAT(STAT_SpatialActorsChanged, ActorUpdatesThisConnectionSent); - // SpatialGDK - Here Unreal would return the position of the last replicated actor in PriorityActors before the channel became saturated. - // In Spatial we use ActorReplicationRateLimit and EntityCreationRateLimit to limit replication so this return value is not relevant. + // SpatialGDK - Here Unreal would return the position of the last replicated actor in PriorityActors before the channel became + // saturated. In Spatial we use ActorReplicationRateLimit and EntityCreationRateLimit to limit replication so this return value is not + // relevant. } #endif // WITH_SERVER_CODE @@ -1512,7 +1634,8 @@ void USpatialNetDriver::ProcessRPC(AActor* Actor, UObject* SubObject, UFunction* 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()); + 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, Parameters); @@ -1522,7 +1645,8 @@ void USpatialNetDriver::ProcessRPC(AActor* Actor, UObject* SubObject, UFunction* // SpatialGDK: This is a modified and simplified version of UNetDriver::ServerReplicateActors. // In our implementation, connections on the server do not represent clients. They represent direct connections to SpatialOS. -// For this reason, things like ready checks, acks, throttling based on number of updated connections, interest management are irrelevant at this level. +// For this reason, things like ready checks, acks, throttling based on number of updated connections, interest management are irrelevant at +// this level. int32 USpatialNetDriver::ServerReplicateActors(float DeltaSeconds) { SCOPE_CYCLE_COUNTER(STAT_SpatialServerReplicateActors); @@ -1582,12 +1706,11 @@ int32 USpatialNetDriver::ServerReplicateActors(float DeltaSeconds) // Build the consider list (actors that are ready to replicate) ServerReplicateActors_BuildConsiderList(ConsiderList, ServerTickTime); - const bool bIsMultiWorkerEnabled = USpatialStatics::IsSpatialMultiWorkerEnabled(GetWorld()); - FSpatialLoadBalancingHandler MigrationHandler(this); FSpatialNetDriverLoadBalancingContext LoadBalancingContext(this, ConsiderList); - if (bIsMultiWorkerEnabled) + bool bHandoverEnabled = USpatialStatics::IsHandoverEnabled(this); + if (bHandoverEnabled) { MigrationHandler.EvaluateActorsToMigrate(LoadBalancingContext); LoadBalancingContext.UpdateWithAdditionalActors(); @@ -1610,10 +1733,11 @@ int32 USpatialNetDriver::ServerReplicateActors(float DeltaSeconds) if (ClientConnection->ViewTarget != nullptr) { - new(ConnectionViewers)FNetViewer(ClientConnection, 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 + // 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(); @@ -1621,7 +1745,10 @@ int32 USpatialNetDriver::ServerReplicateActors(float DeltaSeconds) 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()); + 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()); } } } @@ -1632,18 +1759,21 @@ int32 USpatialNetDriver::ServerReplicateActors(float DeltaSeconds) FActorPriority** PriorityActors = NULL; // Get a sorted list of actors for this connection - const int32 FinalSortedCount = ServerReplicateActors_PrioritizeActors(SpatialConnection, ConnectionViewers, MigrationHandler, ConsiderList, bCPUSaturated, PriorityList, PriorityActors); + const int32 FinalSortedCount = ServerReplicateActors_PrioritizeActors(SpatialConnection, ConnectionViewers, MigrationHandler, + ConsiderList, bCPUSaturated, PriorityList, PriorityActors); // Process the sorted list of actors for this connection - ServerReplicateActors_ProcessPrioritizedActors(SpatialConnection, ConnectionViewers, MigrationHandler, PriorityActors, FinalSortedCount, Updated); + ServerReplicateActors_ProcessPrioritizedActors(SpatialConnection, ConnectionViewers, MigrationHandler, PriorityActors, FinalSortedCount, + Updated); - if (bIsMultiWorkerEnabled) + if (bHandoverEnabled) { // Once an up to date version of the actors have been sent, do the actual migration. MigrationHandler.ProcessMigrations(); } - // SpatialGDK - Here Unreal would mark relevant actors that weren't processed this frame as bPendingNetUpdate. This is not used in the SpatialGDK and so has been removed. + // SpatialGDK - Here Unreal would mark relevant actors that weren't processed this frame as bPendingNetUpdate. This is not used in the + // SpatialGDK and so has been removed. RelevantActorMark.Pop(); ConnectionViewers.Reset(); @@ -1661,6 +1791,11 @@ int32 USpatialNetDriver::ServerReplicateActors(float DeltaSeconds) DebugRelevantActors = false; } + if (DebugCtx) + { + DebugCtx->TickServer(); + } + #if !UE_BUILD_SHIPPING ConsiderListSize = FinalSortedCount; #endif @@ -1680,46 +1815,59 @@ void USpatialNetDriver::TickDispatch(float DeltaTime) { const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); - TArray OpLists = Connection->GetOpList(); + Connection->Advance(DeltaTime); - // Servers will queue ops at startup until we've extracted necessary information from the op stream - if (!bIsReadyToStart) + if (Connection->HasDisconnected()) { - HandleStartupOpQueueing(MoveTemp(OpLists)); + Receiver->OnDisconnect(Connection->GetConnectionStatus(), Connection->GetDisconnectReason()); return; } + if (LoadBalanceEnforcer.IsValid()) + { + SCOPE_CYCLE_COUNTER(STAT_SpatialUpdateAuthority); + LoadBalanceEnforcer->Advance(); + // Immediately flush. The messages to spatial created by the load balance enforcer in response + // to other workers should be looped back as quick as possible. + Connection->Flush(); + } + + if (RPCService.IsValid()) + { + RPCService->AdvanceView(); + } + { SCOPE_CYCLE_COUNTER(STAT_SpatialProcessOps); - for (const OpList& Ops : OpLists) - { - Dispatcher->ProcessOps(Ops); - } + Dispatcher->ProcessOps(GetOpsFromEntityDeltas(Connection->GetEntityDeltas())); + Dispatcher->ProcessOps(Connection->GetWorkerMessages()); + Receiver->ProcessActorsFromAsyncLoading(); } - if (SpatialMetrics != nullptr && SpatialGDKSettings->bEnableMetrics) + if (RPCService.IsValid()) { - SpatialMetrics->TickMetrics(GetElapsedTime()); + RPCService->ProcessChanges(GetElapsedTime()); } - if (LoadBalanceEnforcer.IsValid()) + if (WellKnownEntitySystem.IsValid()) { - SCOPE_CYCLE_COUNTER(STAT_SpatialUpdateAuthority); - for (const auto& AclAssignmentRequest : LoadBalanceEnforcer->ProcessQueuedAclAssignmentRequests()) - { - Sender->SetAclWriteAuthority(AclAssignmentRequest); - } + WellKnownEntitySystem->Advance(); + } + + if (!bIsReadyToStart) + { + TryFinishStartup(); + } + + if (SpatialMetrics != nullptr && SpatialGDKSettings->bEnableMetrics) + { + SpatialMetrics->TickMetrics(GetElapsedTime()); } } } -void USpatialNetDriver::ProcessRemoteFunction( - AActor* Actor, - UFunction* Function, - void* Parameters, - FOutParmRec* OutParms, - FFrame* Stack, - UObject* SubObject) +void USpatialNetDriver::ProcessRemoteFunction(AActor* Actor, UFunction* Function, void* Parameters, FOutParmRec* OutParms, FFrame* Stack, + UObject* SubObject) { if (Connection == nullptr) { @@ -1730,18 +1878,22 @@ void USpatialNetDriver::ProcessRemoteFunction( USpatialNetConnection* NetConnection = GetSpatialOSNetConnection(); if (NetConnection == nullptr) { - UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Attempted to call ProcessRemoteFunction but no SpatialOSNetConnection existed. Has this worker established a connection?")); + UE_LOG(LogSpatialOSNetDriver, Error, + TEXT("Attempted to call ProcessRemoteFunction but no SpatialOSNetConnection existed. Has this worker established a " + "connection?")); return; } // This check mimics the way Unreal natively checks whether an AActor has ownership for sending server RPCs. // The function GetNetConnection() goes up the AActor ownership chain until it reaches an AActor that is possesed by an AController and - // hence a UNetConnection. Server RPCs should only be sent by AActor instances that either are possessed by a UNetConnection or are owned by - // other AActor instances possessed by a UNetConnection. For native Unreal reference see ProcessRemoteFunction() of IpNetDriver.cpp. - // However if we are on the server, and the RPC is a CrossServer or NetMulticast RPC, this can be invoked without an owner. + // hence a UNetConnection. Server RPCs should only be sent by AActor instances that either are possessed by a UNetConnection or are + // owned by other AActor instances possessed by a UNetConnection. For native Unreal reference see ProcessRemoteFunction() of + // IpNetDriver.cpp. However if we are on the server, and the RPC is a CrossServer or NetMulticast RPC, this can be invoked without an + // owner. if (!Actor->GetNetConnection() && !(Function->FunctionFlags & (FUNC_NetCrossServer | FUNC_NetMulticast) && IsServer())) { - UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("No owning connection for actor %s. Function %s will not be processed."), *Actor->GetName(), *Function->GetName()); + UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("No owning connection for actor %s. Function %s will not be processed."), + *Actor->GetName(), *Function->GetName()); return; } @@ -1750,7 +1902,9 @@ void USpatialNetDriver::ProcessRemoteFunction( if (!CallingObject->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_SpatialType)) { - UE_LOG(LogSpatialOSNetDriver, Verbose, TEXT("Trying to call RPC %s on object %s (class %s) that isn't supported by Spatial. This RPC will be dropped."), *Function->GetName(), *CallingObject->GetName(), *CallingObject->GetClass()->GetName()); + UE_LOG(LogSpatialOSNetDriver, Verbose, + TEXT("Trying to call RPC %s on object %s (class %s) that isn't supported by Spatial. This RPC will be dropped."), + *Function->GetName(), *CallingObject->GetName(), *CallingObject->GetClass()->GetName()); return; } @@ -1776,7 +1930,7 @@ void USpatialNetDriver::ProcessRemoteFunction( Out = Out->NextOutParm; } - void* Dest = It->ContainerPtrToValuePtr< void >(Parameters); + void* Dest = It->ContainerPtrToValuePtr(Parameters); const int32 CopySize = It->ElementSize * It->ArrayDim; @@ -1817,7 +1971,9 @@ void USpatialNetDriver::PollPendingLoads() } else { - UE_LOG(LogSpatialPackageMap, Warning, TEXT("Object %s which was being asynchronously loaded was not found after loading has completed."), *ObjectReference.ToString()); + UE_LOG(LogSpatialPackageMap, Warning, + TEXT("Object %s which was being asynchronously loaded was not found after loading has completed."), + *ObjectReference.ToString()); } IterPending.RemoveCurrent(); @@ -1847,36 +2003,28 @@ void USpatialNetDriver::TickFlush(float DeltaTime) if (SpatialGDKSettings->bBatchSpatialPositionUpdates && Sender != nullptr) { - if ((GetElapsedTime() - TimeWhenPositionLastUpdated) >= (1.0f / SpatialGDKSettings->PositionUpdateFrequency)) - { - TimeWhenPositionLastUpdated = GetElapsedTime(); - - Sender->ProcessPositionUpdates(); - } - } - - if (Connection != nullptr) - { - Connection->MaybeFlush(); + Sender->ProcessPositionUpdates(); } #endif // WITH_SERVER_CODE } - if (SpatialGDKSettings->UseRPCRingBuffer() && Sender != nullptr) + if (Sender != nullptr) { Sender->FlushRPCService(); } + if (IsServer()) + { + ProcessOwnershipChanges(); + } + ProcessPendingDormancy(); TimerManager.Tick(DeltaTime); - if (SpatialGDKSettings->bRunSpatialWorkerConnectionOnGameThread || SpatialGDKSettings->bUseSpatialView) + if (Connection != nullptr) { - if (Connection != nullptr) - { - Connection->ProcessOutgoingMessages(); - } + Connection->Flush(); } // Super::TickFlush() will not call ReplicateActors() because Spatial connections have InternalAck set to true. @@ -1884,7 +2032,7 @@ void USpatialNetDriver::TickFlush(float DeltaTime) Super::TickFlush(DeltaTime); } -USpatialNetConnection * USpatialNetDriver::GetSpatialOSNetConnection() const +USpatialNetConnection* USpatialNetDriver::GetSpatialOSNetConnection() const { if (ServerConnection) { @@ -1900,24 +2048,8 @@ 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) +bool USpatialNetDriver::CreateSpatialNetConnection(const FURL& InUrl, const FUniqueNetIdRepl& UniqueId, const FName& OnlinePlatformName, + const Worker_EntityId& ClientSystemEntityId, USpatialNetConnection** OutConn) { check(*OutConn == nullptr); *OutConn = NewObject(GetTransientPackage(), NetConnectionClass); @@ -1926,8 +2058,8 @@ bool USpatialNetDriver::CreateSpatialNetConnection(const FURL& InUrl, const FUni USpatialNetConnection* SpatialConnection = *OutConn; // We create a "dummy" connection that corresponds to this player. This connection won't transmit any data. - // We may not need to keep it in the future, but for now it looks like path of least resistance is to have one UPlayer (UConnection) per player. - // We use an internal counter to give each client a unique IP address for Unreal's internal bookkeeping. + // We may not need to keep it in the future, but for now it looks like path of least resistance is to have one UPlayer (UConnection) per + // player. We use an internal counter to give each client a unique IP address for Unreal's internal bookkeeping. ISocketSubsystem* SocketSubsystem = GetSocketSubsystem(); TSharedRef FromAddr = SocketSubsystem->CreateInternetAddr(); FromAddr->SetIp(UniqueClientIpAddressCounter++); @@ -1935,25 +2067,22 @@ bool USpatialNetDriver::CreateSpatialNetConnection(const FURL& InUrl, const FUni SpatialConnection->InitRemoteConnection(this, nullptr, InUrl, *FromAddr, USOCK_Open); Notify->NotifyAcceptedConnection(SpatialConnection); - // TODO: This also currently sets all dormant actors to the active list (because the dormancy needs to be processed for the new connection) - // This is unnecessary however, as we only have a single relevant connection in Spatial. Could be a performance win to not do this. + // TODO: This also currently sets all dormant actors to the active list (because the dormancy needs to be processed for the new + // connection) This is unnecessary however, as we only have a single relevant connection in Spatial. Could be a performance win to not + // do this. AddClientConnection(SpatialConnection); // Set the unique net ID for this player. This and the code below is adapted from World.cpp:4499 SpatialConnection->PlayerId = UniqueId; SpatialConnection->SetPlayerOnlinePlatformName(OnlinePlatformName); - - // Get the worker attribute. - const TCHAR* WorkerAttributeOption = InUrl.GetOption(TEXT("workerAttribute"), nullptr); - check(WorkerAttributeOption); - SpatialConnection->ConnectionOwningWorkerId = FString(WorkerAttributeOption).Mid(1); // Trim off the = at the beginning. + SpatialConnection->ConnectionClientWorkerSystemEntityId = ClientSystemEntityId; // Register workerId and its connection. - if (TOptional WorkerId = ExtractWorkerIDFromAttribute(SpatialConnection->ConnectionOwningWorkerId)) + if (ClientSystemEntityId != SpatialConstants::INVALID_ENTITY_ID) { - UE_LOG(LogSpatialOSNetDriver, Verbose, TEXT("Worker %s 's NetConnection created."), *WorkerId.GetValue()); + UE_LOG(LogSpatialOSNetDriver, Verbose, TEXT("Worker %lld 's NetConnection created."), ClientSystemEntityId); - WorkerConnections.Add(WorkerId.GetValue(), SpatialConnection); + RegisterClientConnection(ClientSystemEntityId, SpatialConnection); } // We will now ask GameMode/GameSession if it's ok for this user to join. @@ -1964,7 +2093,8 @@ bool USpatialNetDriver::CreateSpatialNetConnection(const FURL& InUrl, const FUni // skip to the first option in the URL const FString UrlString = InUrl.ToString(); const TCHAR* Tmp = *UrlString; - for (; *Tmp && *Tmp != '?'; Tmp++); + for (; *Tmp && *Tmp != '?'; Tmp++) + ; FString ErrorMsg; AGameModeBase* GameMode = GetWorld()->GetAuthGameMode(); @@ -1975,6 +2105,9 @@ bool USpatialNetDriver::CreateSpatialNetConnection(const FURL& InUrl, const FUni if (!ErrorMsg.IsEmpty()) { UE_LOG(LogSpatialOSNetDriver, Error, TEXT("PreLogin failure: %s"), *ErrorMsg); + + DisconnectPlayer(ClientSystemEntityId); + // TODO: Destroy connection. UNR-584 return false; } @@ -1987,20 +2120,14 @@ bool USpatialNetDriver::CreateSpatialNetConnection(const FURL& InUrl, const FUni return true; } -void USpatialNetDriver::CleanUpClientConnection(USpatialNetConnection* ConnectionCleanedUp) +void USpatialNetDriver::RegisterClientConnection(const Worker_EntityId InWorkerEntityId, USpatialNetConnection* ClientConnection) { - if (!ConnectionCleanedUp->ConnectionOwningWorkerId.IsEmpty()) - { - if (TOptional WorkerId = ExtractWorkerIDFromAttribute(*ConnectionCleanedUp->ConnectionOwningWorkerId)) - { - WorkerConnections.Remove(WorkerId.GetValue()); - } - } + WorkerConnections.Add(InWorkerEntityId, ClientConnection); } -TWeakObjectPtr USpatialNetDriver::FindClientConnectionFromWorkerId(const FString& WorkerId) +TWeakObjectPtr USpatialNetDriver::FindClientConnectionFromWorkerEntityId(const Worker_EntityId InWorkerEntityId) { - if (TWeakObjectPtr* ClientConnectionPtr = WorkerConnections.Find(WorkerId)) + if (TWeakObjectPtr* ClientConnectionPtr = WorkerConnections.Find(InWorkerEntityId)) { return *ClientConnectionPtr; } @@ -2008,9 +2135,27 @@ TWeakObjectPtr USpatialNetDriver::FindClientConnectionFro return {}; } +void USpatialNetDriver::CleanUpClientConnection(USpatialNetConnection* ConnectionCleanedUp) +{ + if (ConnectionCleanedUp->ConnectionClientWorkerSystemEntityId != SpatialConstants::INVALID_ENTITY_ID) + { + WorkerConnections.Remove(ConnectionCleanedUp->ConnectionClientWorkerSystemEntityId); + } +} + +bool USpatialNetDriver::HasServerAuthority(Worker_EntityId EntityId) const +{ + return StaticComponentView->HasAuthority(EntityId, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID); +} + +bool USpatialNetDriver::HasClientAuthority(Worker_EntityId EntityId) const +{ + return StaticComponentView->HasAuthority(EntityId, SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID); +} + void USpatialNetDriver::ProcessPendingDormancy() { - TSet> RemainingChannels; + decltype(PendingDormantChannels) RemainingChannels; for (auto& PendingDormantChannel : PendingDormantChannels) { if (PendingDormantChannel.IsValid()) @@ -2033,18 +2178,20 @@ void USpatialNetDriver::ProcessPendingDormancy() PendingDormantChannels = MoveTemp(RemainingChannels); } -void USpatialNetDriver::AcceptNewPlayer(const FURL& InUrl, const FUniqueNetIdRepl& UniqueId, const FName& OnlinePlatformName) +void USpatialNetDriver::AcceptNewPlayer(const FURL& InUrl, const FUniqueNetIdRepl& UniqueId, const FName& OnlinePlatformName, + const Worker_EntityId& ClientSystemEntityId) { USpatialNetConnection* SpatialConnection = nullptr; - if (!CreateSpatialNetConnection(InUrl, UniqueId, OnlinePlatformName, &SpatialConnection)) + if (!CreateSpatialNetConnection(InUrl, UniqueId, OnlinePlatformName, ClientSystemEntityId, &SpatialConnection)) { UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Failed to create SpatialNetConnection!")); return; } FString ErrorMsg; - SpatialConnection->PlayerController = GetWorld()->SpawnPlayActor(SpatialConnection, ROLE_AutonomousProxy, InUrl, SpatialConnection->PlayerId, ErrorMsg); + SpatialConnection->PlayerController = + GetWorld()->SpawnPlayActor(SpatialConnection, ROLE_AutonomousProxy, InUrl, SpatialConnection->PlayerId, ErrorMsg); if (SpatialConnection->PlayerController == nullptr) { @@ -2052,23 +2199,29 @@ void USpatialNetDriver::AcceptNewPlayer(const FURL& InUrl, const FUniqueNetIdRep UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Join failure: %s"), *ErrorMsg); SpatialConnection->FlushNet(true); } + + // Preallocate the PlayerController entity so the AuthorityDelegation client authoritative components can be set + // correctly at spawn. + USpatialActorChannel* Channel = GetOrCreateSpatialActorChannel(SpatialConnection->PlayerController); + USpatialNetConnection* NetConnection = Cast(Channel->Actor->GetNetConnection()); + check(NetConnection != nullptr); + NetConnection->PlayerControllerEntity = Channel->GetEntityId(); } // This function is called for server workers who received the PC over the wire -void USpatialNetDriver::PostSpawnPlayerController(APlayerController* PlayerController, const FString& ClientWorkerId) +void USpatialNetDriver::PostSpawnPlayerController(APlayerController* PlayerController) { check(PlayerController != nullptr); - 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=") + ClientWorkerId; // We create a connection here so that any code that searches for owning connection, etc on the server // resolves ownership correctly USpatialNetConnection* OwnershipConnection = nullptr; - if (!CreateSpatialNetConnection(FURL(nullptr, *URLString, TRAVEL_Absolute), FUniqueNetIdRepl(), FName(), &OwnershipConnection)) + if (!CreateSpatialNetConnection(FURL(nullptr, *URLString, TRAVEL_Absolute), FUniqueNetIdRepl(), FName(), + SpatialConstants::INVALID_ENTITY_ID, &OwnershipConnection)) { UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Failed to create SpatialNetConnection!")); return; @@ -2087,6 +2240,26 @@ void USpatialNetDriver::PostSpawnPlayerController(APlayerController* PlayerContr PlayerController->SetPlayer(OwnershipConnection); } +void USpatialNetDriver::DisconnectPlayer(Worker_EntityId ClientEntityId) +{ + Worker_CommandRequest Request = {}; + Request.component_id = SpatialConstants::WORKER_COMPONENT_ID; + Request.command_index = SpatialConstants::WORKER_DISCONNECT_COMMAND_ID; + Request.schema_type = Schema_CreateCommandRequest(); + Worker_RequestId RequestId = Connection->SendCommandRequest(ClientEntityId, &Request, SpatialGDK::RETRY_UNTIL_COMPLETE, {}); + + SystemEntityCommandDelegate CommandResponseDelegate; + CommandResponseDelegate.BindWeakLambda(this, [this, ClientEntityId](const Worker_CommandResponseOp& Op) { + TWeakObjectPtr ClientConnection = FindClientConnectionFromWorkerEntityId(ClientEntityId); + if (ClientConnection.IsValid()) + { + ClientConnection->CleanUp(); + } + }); + + Receiver->AddSystemEntityCommandDelegate(RequestId, CommandResponseDelegate); +} + bool USpatialNetDriver::Exec(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar) { #if !UE_BUILD_SHIPPING @@ -2098,7 +2271,8 @@ bool USpatialNetDriver::Exec(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& A return UNetDriver::Exec(InWorld, Cmd, Ar); } -// This function is literally a copy paste of UNetDriver::HandleNetDumpServerRPCCommand. Didn't want to refactor to avoid divergence from engine. +// This function is literally a copy paste of UNetDriver::HandleNetDumpServerRPCCommand. Didn't want to refactor to avoid divergence from +// engine. #if !UE_BUILD_SHIPPING bool USpatialNetDriver::HandleNetDumpCrossServerRPCCommand(const TCHAR* Cmd, FOutputDevice& Ar) { @@ -2107,11 +2281,14 @@ bool USpatialNetDriver::HandleNetDumpCrossServerRPCCommand(const TCHAR* Cmd, FOu { bool bHasNetFields = false; - ensureMsgf(!ClassIt->HasAnyFlags(RF_NeedLoad | RF_NeedPostLoad), TEXT("UNetDriver::HandleNetDumpCrossServerRPCCommand: %s has flag RF_NeedPostLoad. NetFields and ClassReps will be incorrect!"), *GetFullNameSafe(*ClassIt)); + ensureMsgf( + !ClassIt->HasAnyFlags(RF_NeedLoad | RF_NeedPostLoad), + TEXT("UNetDriver::HandleNetDumpCrossServerRPCCommand: %s has flag RF_NeedPostLoad. NetFields and ClassReps will be incorrect!"), + *GetFullNameSafe(*ClassIt)); for (int32 i = 0; i < ClassIt->NetFields.Num(); i++) { - UFunction * Function = Cast(ClassIt->NetFields[i]); + UFunction* Function = Cast(ClassIt->NetFields[i]); if (Function != NULL && Function->FunctionFlags & FUNC_NetCrossServer) { @@ -2129,17 +2306,18 @@ bool USpatialNetDriver::HandleNetDumpCrossServerRPCCommand(const TCHAR* Cmd, FOu for (int32 i = 0; i < ClassIt->NetFields.Num(); i++) { - UFunction * Function = Cast(ClassIt->NetFields[i]); + UFunction* Function = Cast(ClassIt->NetFields[i]); if (Function != NULL && Function->FunctionFlags & FUNC_NetCrossServer) { - const FClassNetCache * ClassCache = NetCache->GetClassNetCache(*ClassIt); + const FClassNetCache* ClassCache = NetCache->GetClassNetCache(*ClassIt); - const FFieldNetCache * FieldCache = ClassCache->GetFromField(Function); + const FFieldNetCache* FieldCache = ClassCache->GetFromField(Function); - TArray< GDK_PROPERTY(Property) * > Parms; + TArray Parms; - for (TFieldIterator It(Function); It && (It->PropertyFlags & (CPF_Parm | CPF_ReturnParm)) == CPF_Parm; ++It) + for (TFieldIterator It(Function); + It && (It->PropertyFlags & (CPF_Parm | CPF_ReturnParm)) == CPF_Parm; ++It) { Parms.Add(*It); } @@ -2191,7 +2369,8 @@ void USpatialPendingNetGame::InitNetDriver() { check(GIsClient); - // This is a trimmed down version of UPendingNetGame::InitNetDriver(). We don't send any Unreal connection packets, just set up the net driver. + // This is a trimmed down version of UPendingNetGame::InitNetDriver(). We don't send any Unreal connection packets, just set up the net + // driver. if (!GDisallowNetworkTravel) { // Try to create network driver. @@ -2218,7 +2397,10 @@ void USpatialPendingNetGame::InitNetDriver() } else { - ConnectionError = NSLOCTEXT("Engine", "UsedCheatCommands", "Console commands were used which are disallowed in netplay. You must restart the game to create a match.").ToString(); + ConnectionError = + NSLOCTEXT("Engine", "UsedCheatCommands", + "Console commands were used which are disallowed in netplay. You must restart the game to create a match.") + .ToString(); } } @@ -2242,7 +2424,8 @@ void USpatialNetDriver::RemoveActorChannel(Worker_EntityId EntityId, USpatialAct if (!EntityToActorChannel.Contains(EntityId)) { - UE_LOG(LogSpatialOSNetDriver, Verbose, TEXT("RemoveActorChannel: Failed to find entity/channel mapping for entity %lld."), EntityId); + UE_LOG(LogSpatialOSNetDriver, Verbose, TEXT("RemoveActorChannel: Failed to find entity/channel mapping for entity %lld."), + EntityId); return; } @@ -2270,13 +2453,18 @@ USpatialActorChannel* USpatialNetDriver::GetOrCreateSpatialActorChannel(UObject* if (USpatialActorChannel* ActorChannel = GetActorChannelByEntityId(PackageMap->GetEntityIdFromObject(TargetActor))) { // This can happen if schema database is out of date and had no entry for a static subobject. - UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("GetOrCreateSpatialActorChannel: No channel for target object but channel already present for actor. Target object: %s, actor: %s"), *TargetObject->GetPathName(), *TargetActor->GetPathName()); + UE_LOG(LogSpatialOSNetDriver, Warning, + TEXT("GetOrCreateSpatialActorChannel: No channel for target object but channel already present for actor. Target " + "object: %s, actor: %s"), + *TargetObject->GetPathName(), *TargetActor->GetPathName()); return ActorChannel; } if (TargetActor->IsPendingKillPending()) { - UE_LOG(LogSpatialOSNetDriver, Log, TEXT("A SpatialActorChannel will not be created for %s because the Actor is being destroyed."), *GetNameSafe(TargetActor)); + UE_LOG(LogSpatialOSNetDriver, Log, + TEXT("A SpatialActorChannel will not be created for %s because the Actor is being destroyed."), + *GetNameSafe(TargetActor)); return nullptr; } @@ -2309,7 +2497,7 @@ void USpatialNetDriver::RefreshActorDormancy(AActor* Actor, bool bMakeDormant) return; } - const bool bHasAuthority = StaticComponentView->HasAuthority(EntityId, SpatialConstants::DORMANT_COMPONENT_ID); + const bool bHasAuthority = HasServerAuthority(EntityId); if (bHasAuthority == false) { UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Unable to flush dormancy on actor (%s) without authority"), *Actor->GetName()); @@ -2343,6 +2531,48 @@ void USpatialNetDriver::RefreshActorDormancy(AActor* Actor, bool bMakeDormant) } } +void USpatialNetDriver::RefreshActorVisibility(AActor* Actor, bool bMakeVisible) +{ + check(IsServer()); + check(Actor); + + const Worker_EntityId EntityId = PackageMap->GetEntityIdFromObject(Actor); + if (EntityId == SpatialConstants::INVALID_ENTITY_ID) + { + UE_LOG(LogSpatialOSNetDriver, Verbose, TEXT("Unable to change visibility on an actor without entity id. Actor's name: %s"), + *Actor->GetName()); + return; + } + + const bool bHasAuthority = HasServerAuthority(EntityId); + if (!bHasAuthority) + { + UE_LOG(LogSpatialOSNetDriver, Verbose, TEXT("Unable to change visibility on an actor without authority. Actor's name: %s "), + *Actor->GetName()); + return; + } + + const bool bVisibilityComponentExists = StaticComponentView->HasComponent(EntityId, SpatialConstants::VISIBLE_COMPONENT_ID); + + // If the Actor is Visible make sure it has the Visible component + if (bMakeVisible && !bVisibilityComponentExists) + { + Worker_AddComponentOp AddComponentOp{}; + AddComponentOp.entity_id = EntityId; + AddComponentOp.data = ComponentFactory::CreateEmptyComponentData(SpatialConstants::VISIBLE_COMPONENT_ID); + Sender->SendAddComponents(AddComponentOp.entity_id, { AddComponentOp.data }); + StaticComponentView->OnAddComponent(AddComponentOp); + } + else if (!bMakeVisible && bVisibilityComponentExists) + { + Worker_RemoveComponentOp RemoveComponentOp{}; + RemoveComponentOp.entity_id = EntityId; + RemoveComponentOp.component_id = SpatialConstants::VISIBLE_COMPONENT_ID; + Sender->SendRemoveComponents(EntityId, { SpatialConstants::VISIBLE_COMPONENT_ID }); + StaticComponentView->OnRemoveComponent(RemoveComponentOp); + } +} + void USpatialNetDriver::AddPendingDormantChannel(USpatialActorChannel* Channel) { PendingDormantChannels.Emplace(Channel); @@ -2385,7 +2615,8 @@ USpatialActorChannel* USpatialNetDriver::CreateSpatialActorChannel(AActor* Actor USpatialNetConnection* NetConnection = GetSpatialOSNetConnection(); check(NetConnection != nullptr); - USpatialActorChannel* Channel = static_cast(NetConnection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::OpenedLocally)); + USpatialActorChannel* Channel = + static_cast(NetConnection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::OpenedLocally)); if (Channel == nullptr) { UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Failed to create a channel for Actor %s."), *GetNameSafe(Actor)); @@ -2407,214 +2638,72 @@ void USpatialNetDriver::WipeWorld(const PostWorldWipeDelegate& LoadSnapshotAfter void USpatialNetDriver::DelayedRetireEntity(Worker_EntityId EntityId, float Delay, bool bIsNetStartupActor) { FTimerHandle RetryTimer; - TimerManager.SetTimer(RetryTimer, [this, EntityId, bIsNetStartupActor]() - { - Sender->RetireEntity(EntityId, bIsNetStartupActor); - }, Delay, false); + TimerManager.SetTimer( + RetryTimer, + [this, EntityId, bIsNetStartupActor]() { + Sender->RetireEntity(EntityId, bIsNetStartupActor); + }, + Delay, false); } -void USpatialNetDriver::HandleStartupOpQueueing(TArray InOpLists) +void USpatialNetDriver::TryFinishStartup() { - if (InOpLists.Num() == 0) - { - return; - } + // Limit Log frequency. + const USpatialGDKSettings* Settings = GetDefault(); + bool bShouldLogStartup = HasTimedOut(Settings->StartupLogRate, StartupTimestamp); if (IsServer()) { - bIsReadyToStart = FindAndDispatchStartupOpsServer(InOpLists); - - if (bIsReadyToStart) - { - // Process levels which were loaded before the connection to Spatial was ready. - GetGameInstance()->CleanupCachedLevelsAfterConnection(); - - // We know at this point that we have all the information to set the worker's interest query. - Sender->UpdateServerWorkerEntityInterestAndPosition(); - - // We've found and dispatched all ops we need for startup, - // trigger BeginPlay() on the GSM and process the queued ops. - // Note that FindAndDispatchStartupOps() will have notified the Dispatcher - // to skip the startup ops that we've processed already. - GlobalStateManager->TriggerBeginPlay(); - } - } - else - { - bIsReadyToStart = FindAndDispatchStartupOpsClient(InOpLists); - } - - QueuedStartupOpLists.Append(MoveTemp(InOpLists)); - - if (!bIsReadyToStart) - { - return; - } - - for (const OpList& Ops : QueuedStartupOpLists) - { - Dispatcher->ProcessOps(Ops); - } - - // Sanity check that the dispatcher encountered, skipped, and removed - // all Ops we asked it to skip - check(Dispatcher->GetNumOpsToSkip() == 0); - - QueuedStartupOpLists.Empty(); -} - -bool USpatialNetDriver::FindAndDispatchStartupOpsServer(const TArray& InOpLists) -{ - TArray FoundOps; - - AppendAllOpsOfType(InOpLists, WORKER_OP_TYPE_ENTITY_QUERY_RESPONSE, FoundOps); - - // To correctly initialize the ServerWorkerEntity on each server during op queueing, we need to catch several ops here. - // Note that this will break if any other CreateEntity requests are issued during the startup flow. - { - Worker_Op* CreateEntityResponseOp = FindFirstOpOfType(InOpLists, WORKER_OP_TYPE_CREATE_ENTITY_RESPONSE); - - Worker_Op* AddComponentOp = FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_ADD_COMPONENT, SpatialConstants::SERVER_WORKER_COMPONENT_ID); - - Worker_Op* AuthorityChangedOp = FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_AUTHORITY_CHANGE, SpatialConstants::SERVER_WORKER_COMPONENT_ID); - - if (CreateEntityResponseOp != nullptr) + if (!PackageMap->IsEntityPoolReady()) { - FoundOps.Add(CreateEntityResponseOp); + UE_CLOG(bShouldLogStartup, LogSpatialOSNetDriver, Log, TEXT("Waiting for the EntityPool to be ready.")); } - - if (AddComponentOp != nullptr) + else if (!GlobalStateManager->IsReady()) { - FoundOps.Add(AddComponentOp); + UE_CLOG(bShouldLogStartup, LogSpatialOSNetDriver, Log, + TEXT("Waiting for the GSM to be ready (this includes waiting for the expected number of servers to be connected)")); } - - if (AuthorityChangedOp != nullptr) + else if (VirtualWorkerTranslator.IsValid() && !VirtualWorkerTranslator->IsReady()) { - FoundOps.Add(AuthorityChangedOp); + UE_CLOG(bShouldLogStartup, LogSpatialOSNetDriver, Log, TEXT("Waiting for the load balancing system to be ready.")); } - } - - // 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. - if (!PackageMap->IsEntityPoolReady()) - { - Worker_Op* EntityIdReservationResponseOp = FindFirstOpOfType(InOpLists, WORKER_OP_TYPE_RESERVE_ENTITY_IDS_RESPONSE); - - if (EntityIdReservationResponseOp != nullptr) + else if (!StaticComponentView->HasEntity(VirtualWorkerTranslator->GetClaimedPartitionId())) { - FoundOps.Add(EntityIdReservationResponseOp); + UE_CLOG(bShouldLogStartup, LogSpatialOSNetDriver, Log, TEXT("Waiting for the partition entity to be ready.")); } - } - - // Search for StartupActorManager ops we need and process them - if (!GlobalStateManager->IsReady()) - { - Worker_Op* AddComponentOp = FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_ADD_COMPONENT, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID); - - Worker_Op* AuthorityChangedOp = FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_AUTHORITY_CHANGE, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID); - - Worker_Op* ComponentUpdateOp = FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_COMPONENT_UPDATE, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID); - - if (AddComponentOp != nullptr) + else { - FoundOps.Add(AddComponentOp); - } + UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Ready to begin processing.")); + bIsReadyToStart = true; + Connection->SetStartupComplete(); - if (AuthorityChangedOp != nullptr) - { - FoundOps.Add(AuthorityChangedOp); - } +#if WITH_EDITORONLY_DATA + ASpatialWorldSettings* WorldSettings = Cast(GetWorld()->GetWorldSettings()); + if (WorldSettings && WorldSettings->bEnableDebugInterface) + { + USpatialNetDriverDebugContext::EnableDebugSpatialGDK(this); + } +#endif - if (ComponentUpdateOp != nullptr) - { - FoundOps.Add(ComponentUpdateOp); + // 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 + // to skip the startup ops that we've processed already. + GlobalStateManager->TriggerBeginPlay(); } } - - if (VirtualWorkerTranslator.IsValid() && !VirtualWorkerTranslator->IsReady()) + else { - Worker_Op* AddComponentOp = FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_ADD_COMPONENT, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID); - - Worker_Op* AuthorityChangedOp = FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_AUTHORITY_CHANGE, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID); - - Worker_Op* ComponentUpdateOp = FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_COMPONENT_UPDATE, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID); - - 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) + if (bMapLoaded) { - UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Processing Translation component update to bootstrap SpatialVirtualWorkerTranslator.")); - FoundOps.Add(ComponentUpdateOp); + bIsReadyToStart = true; + Connection->SetStartupComplete(); } - } - - SelectiveProcessOps(FoundOps); - - if (!PackageMap->IsEntityPoolReady()) - { - UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Waiting for the EntityPool to be ready.")); - return false; - } - else if (!GlobalStateManager->IsReady()) - { - UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Waiting for the GSM to be ready.")); - return false; - } - else if (VirtualWorkerTranslator.IsValid() && !VirtualWorkerTranslator->IsReady()) - { - GlobalStateManager->QueryTranslation(); - UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Waiting for the Load balancing system to be ready.")); - return false; - } - UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Ready to begin processing.")); - return true; -} - -bool USpatialNetDriver::FindAndDispatchStartupOpsClient(const TArray& InOpLists) -{ - if (bMapLoaded) - { - return true; - } - else - { - // Search for the entity query response for the GlobalStateManager - Worker_Op* Op = FindFirstOpOfType(InOpLists, WORKER_OP_TYPE_ENTITY_QUERY_RESPONSE); - - TArray FoundOps; - if (Op != nullptr) + else { - FoundOps.Add(Op); + UE_CLOG(bShouldLogStartup, LogSpatialOSNetDriver, Log, TEXT("Waiting for the deployment to be ready : %s"), + StartupClientDebugString.IsEmpty() ? TEXT("Waiting for connection.") : *StartupClientDebugString) } - - SelectiveProcessOps(FoundOps); - return false; - } -} - -void USpatialNetDriver::SelectiveProcessOps(TArray FoundOps) -{ - // For each Op we've found, make a Worker_OpList that just contains that Op, - // and pass it to the dispatcher for processing. This allows us to avoid copying - // the Ops around and dealing with memory that is / should be managed by the Worker SDK. - // The Op remains owned by the original OpList. Finally, notify the dispatcher to skip - // these Ops when they are encountered later when we process the queued ops. - for (Worker_Op* FoundOp : FoundOps) - { - OpList Op = { FoundOp, 1, nullptr }; - Dispatcher->ProcessOps(Op); - Dispatcher->MarkOpToSkip(FoundOp); } } @@ -2642,6 +2731,50 @@ bool USpatialNetDriver::IsReady() const return bIsReadyToStart; } +bool USpatialNetDriver::IsLogged(Worker_EntityId ActorEntityId, EActorMigrationResult ActorMigrationFailure) +{ + // Clear the log migration store at the specified interval + const USpatialGDKSettings* Settings = GetDefault(); + if (HasTimedOut(Settings->ActorMigrationLogRate, MigrationTimestamp)) + { + MigrationFailureLogStore.Empty(); + } + + // Check if the pair of actor and failure reason have already been logged + bool bIsLogged = MigrationFailureLogStore.FindPair(ActorEntityId, ActorMigrationFailure) != nullptr; + if (!bIsLogged) + { + MigrationFailureLogStore.AddUnique(ActorEntityId, ActorMigrationFailure); + } + return bIsLogged; +} + +int64 USpatialNetDriver::GetClientID() const +{ + if (IsServer()) + { + return SpatialConstants::INVALID_ENTITY_ID; + } + + if (USpatialNetConnection* NetConnection = GetSpatialOSNetConnection()) + { + return static_cast(NetConnection->PlayerControllerEntity); + } + return SpatialConstants::INVALID_ENTITY_ID; +} + +bool USpatialNetDriver::HasTimedOut(const float Interval, uint64& TimeStamp) +{ + const uint64 WatchdogTimer = Interval / FPlatformTime::GetSecondsPerCycle64(); + const uint64 CurrentTime = FPlatformTime::Cycles64(); + if (CurrentTime - TimeStamp > WatchdogTimer) + { + TimeStamp = CurrentTime; + return true; + } + return false; +} + // This should only be called once on each client, in the SpatialDebugger constructor after the class is replicated to each client. void USpatialNetDriver::SetSpatialDebugger(ASpatialDebugger* InSpatialDebugger) { @@ -2653,6 +2786,7 @@ void USpatialNetDriver::SetSpatialDebugger(ASpatialDebugger* InSpatialDebugger) } SpatialDebugger = InSpatialDebugger; + SpatialDebuggerReady->Ready(); } FUnrealObjectRef USpatialNetDriver::GetCurrentPlayerControllerRef() @@ -2669,11 +2803,3 @@ FUnrealObjectRef USpatialNetDriver::GetCurrentPlayerControllerRef() } 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->SetNumberOfVirtualWorkers(LoadBalanceStrategy->GetMinimumRequiredWorkers()); -} diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriverDebugContext.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriverDebugContext.cpp new file mode 100644 index 0000000000..5bc47aa00d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriverDebugContext.cpp @@ -0,0 +1,409 @@ +#include "EngineClasses/SpatialNetDriverDebugContext.h" + +#include "EngineClasses/SpatialNetDriver.h" +#include "EngineClasses/SpatialPackageMapClient.h" +#include "Interop/Connection/SpatialWorkerConnection.h" +#include "Interop/SpatialSender.h" +#include "Interop/SpatialStaticComponentView.h" +#include "LoadBalancing/DebugLBStrategy.h" +#include "Utils/SpatialActorUtils.h" + +namespace +{ +// Utility function, extracted from TSet::Intersect +template +bool IsSetIntersectionEmpty(const TSet& Set1, const TSet& Set2) +{ + const bool b2Smaller = (Set1.Num() > Set2.Num()); + const TSet& A = (b2Smaller ? Set2 : Set1); + const TSet& B = (b2Smaller ? Set1 : Set2); + + for (auto SetIt = A.CreateConstIterator(); SetIt; ++SetIt) + { + if (B.Contains(*SetIt)) + { + return false; + } + } + return true; +} +} // namespace + +void USpatialNetDriverDebugContext::EnableDebugSpatialGDK(USpatialNetDriver* NetDriver) +{ + check(NetDriver); + + if (NetDriver->DebugCtx == nullptr) + { + if (!ensureMsgf(NetDriver->LoadBalanceStrategy, TEXT("Enabling SpatialGDKDebug too soon"))) + { + return; + } + NetDriver->DebugCtx = NewObject(); + NetDriver->DebugCtx->Init(NetDriver); + } +} + +void USpatialNetDriverDebugContext::DisableDebugSpatialGDK(USpatialNetDriver* NetDriver) +{ + if (NetDriver->DebugCtx != nullptr) + { + NetDriver->DebugCtx->Cleanup(); + } +} + +void USpatialNetDriverDebugContext::Init(USpatialNetDriver* InNetDriver) +{ + NetDriver = InNetDriver; + DebugStrategy = NewObject(); + DebugStrategy->InitDebugStrategy(this, NetDriver->LoadBalanceStrategy); + NetDriver->LoadBalanceStrategy = DebugStrategy; + + NetDriver->Sender->UpdatePartitionEntityInterestAndPosition(); +} + +void USpatialNetDriverDebugContext::Cleanup() +{ + Reset(); + NetDriver->LoadBalanceStrategy = Cast(DebugStrategy)->GetWrappedStrategy(); + NetDriver->DebugCtx = nullptr; + NetDriver->Sender->UpdatePartitionEntityInterestAndPosition(); +} + +void USpatialNetDriverDebugContext::Reset() +{ + for (const auto& Entry : NetDriver->Connection->GetView()) + { + const SpatialGDK::EntityViewElement& ViewElement = Entry.Value; + if (ViewElement.Authority.Contains(SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) + && ViewElement.Components.ContainsByPredicate([](const SpatialGDK::ComponentData& Data) { + return Data.GetComponentId() == SpatialConstants::GDK_DEBUG_COMPONENT_ID; + })) + { + NetDriver->Sender->SendRemoveComponents(Entry.Key, { SpatialConstants::GDK_DEBUG_COMPONENT_ID }); + } + } + + SemanticInterest.Empty(); + SemanticDelegations.Empty(); + CachedInterestSet.Empty(); + ActorDebugInfo.Empty(); + + NetDriver->Sender->UpdatePartitionEntityInterestAndPosition(); +} + +USpatialNetDriverDebugContext::DebugComponentView& USpatialNetDriverDebugContext::GetDebugComponentView(AActor* Actor) +{ + check(Actor && Actor->HasAuthority()); + SpatialGDK::DebugComponent* DbgComp = nullptr; + + Worker_EntityId Entity = NetDriver->PackageMap->GetEntityIdFromObject(Actor); + if (Entity != SpatialConstants::INVALID_ENTITY_ID) + { + DbgComp = NetDriver->StaticComponentView->GetComponentData(Entity); + } + + DebugComponentView& Comp = ActorDebugInfo.FindOrAdd(Actor); + if (DbgComp && Comp.Entity == SpatialConstants::INVALID_ENTITY_ID) + { + Comp.Component = *DbgComp; + Comp.bAdded = true; + } + Comp.Entity = Entity; + + return Comp; +} + +void USpatialNetDriverDebugContext::AddActorTag(AActor* Actor, FName Tag) +{ + if (Actor->HasAuthority()) + { + DebugComponentView& Comp = GetDebugComponentView(Actor); + Comp.Component.ActorTags.Add(Tag); + if (SemanticInterest.Contains(Tag) && Comp.Entity != SpatialConstants::INVALID_ENTITY_ID) + { + AddEntityToWatch(Comp.Entity); + } + Comp.bDirty = true; + } +} + +void USpatialNetDriverDebugContext::RemoveActorTag(AActor* Actor, FName Tag) +{ + if (Actor->HasAuthority()) + { + DebugComponentView& Comp = GetDebugComponentView(Actor); + Comp.Component.ActorTags.Remove(Tag); + if (IsSetIntersectionEmpty(SemanticInterest, Comp.Component.ActorTags) && Comp.Entity != SpatialConstants::INVALID_ENTITY_ID) + { + RemoveEntityToWatch(Comp.Entity); + } + Comp.bDirty = true; + } +} + +void USpatialNetDriverDebugContext::OnDebugComponentUpdateReceived(Worker_EntityId Entity) +{ + SpatialGDK::DebugComponent* DbgComp = NetDriver->StaticComponentView->GetComponentData(Entity); + check(DbgComp); + if (!NetDriver->StaticComponentView->HasAuthority(Entity, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID)) + { + if (IsSetIntersectionEmpty(SemanticInterest, DbgComp->ActorTags)) + { + RemoveEntityToWatch(Entity); + } + else + { + AddEntityToWatch(Entity); + } + } +} + +void USpatialNetDriverDebugContext::OnDebugComponentAuthLost(Worker_EntityId EntityId) +{ + for (auto Iterator = ActorDebugInfo.CreateIterator(); Iterator; ++Iterator) + { + if (Iterator->Value.Entity == EntityId) + { + Iterator.RemoveCurrent(); + break; + } + } +} + +void USpatialNetDriverDebugContext::AddEntityToWatch(Worker_EntityId Entity) +{ + bool bAlreadyWatchingEntity = false; + CachedInterestSet.Add(Entity, &bAlreadyWatchingEntity); + bNeedToUpdateInterest |= !bAlreadyWatchingEntity; +} + +void USpatialNetDriverDebugContext::RemoveEntityToWatch(Worker_EntityId Entity) +{ + if (CachedInterestSet.Remove(Entity) > 0) + { + bNeedToUpdateInterest = true; + } +} + +void USpatialNetDriverDebugContext::AddInterestOnTag(FName Tag) +{ + bool bAlreadyInSet = false; + SemanticInterest.Add(Tag, &bAlreadyInSet); + + if (!bAlreadyInSet) + { + TArray EntityIds; + NetDriver->StaticComponentView->GetEntityIds(EntityIds); + + for (auto Entity : EntityIds) + { + if (SpatialGDK::DebugComponent* DbgComp = NetDriver->StaticComponentView->GetComponentData(Entity)) + { + if (DbgComp->ActorTags.Contains(Tag)) + { + AddEntityToWatch(Entity); + } + } + } + + for (const auto& Item : ActorDebugInfo) + { + if (Item.Value.Component.ActorTags.Contains(Tag)) + { + Worker_EntityId Entity = NetDriver->PackageMap->GetEntityIdFromObject(Item.Key); + if (Entity != SpatialConstants::INVALID_ENTITY_ID) + { + AddEntityToWatch(Entity); + } + } + } + } +} + +void USpatialNetDriverDebugContext::RemoveInterestOnTag(FName Tag) +{ + if (SemanticInterest.Remove(Tag) > 0) + { + CachedInterestSet.Empty(); + bNeedToUpdateInterest = true; + + TArray EntityIds; + NetDriver->StaticComponentView->GetEntityIds(EntityIds); + + for (auto Entity : EntityIds) + { + if (SpatialGDK::DebugComponent* DbgComp = NetDriver->StaticComponentView->GetComponentData(Entity)) + { + if (!IsSetIntersectionEmpty(DbgComp->ActorTags, SemanticInterest)) + { + AddEntityToWatch(Entity); + } + } + } + + for (const auto& Item : ActorDebugInfo) + { + if (!IsSetIntersectionEmpty(Item.Value.Component.ActorTags, SemanticInterest)) + { + Worker_EntityId Entity = NetDriver->PackageMap->GetEntityIdFromObject(Item.Key); + if (Entity != SpatialConstants::INVALID_ENTITY_ID) + { + AddEntityToWatch(Entity); + } + } + } + } +} + +void USpatialNetDriverDebugContext::KeepActorOnLocalWorker(AActor* Actor) +{ + if (Actor->HasAuthority()) + { + DebugComponentView& Comp = GetDebugComponentView(Actor); + Comp.Component.DelegatedWorkerId = DebugStrategy->GetLocalVirtualWorkerId(); + Comp.bDirty = true; + } +} + +void USpatialNetDriverDebugContext::DelegateTagToWorker(FName Tag, uint32 WorkerId) +{ + SemanticDelegations.Add(Tag, WorkerId); +} + +void USpatialNetDriverDebugContext::RemoveTagDelegation(FName Tag) +{ + SemanticDelegations.Remove(Tag); +} + +TOptional USpatialNetDriverDebugContext::GetActorHierarchyExplicitDelegation(const AActor* Actor) +{ + const AActor* NetOwner = SpatialGDK::GetReplicatedHierarchyRoot(Actor); + return GetActorHierarchyExplicitDelegation_Traverse(NetOwner); +} + +TOptional USpatialNetDriverDebugContext::GetActorHierarchyExplicitDelegation_Traverse(const AActor* Actor) +{ + TOptional CandidateDelegation = GetActorExplicitDelegation(Actor); + for (const AActor* Child : Actor->Children) + { + TOptional ChildDelegation = GetActorHierarchyExplicitDelegation_Traverse(Child); + if (ChildDelegation) + { + ensureMsgf( + !CandidateDelegation.IsSet() || !ChildDelegation.IsSet() || CandidateDelegation.GetValue() == ChildDelegation.GetValue(), + TEXT("Inconsistent delegation. Actor %s is delegated to %i but a child is delegated to %i"), *Actor->GetName(), + CandidateDelegation.GetValue(), ChildDelegation.GetValue()); + + CandidateDelegation = ChildDelegation; + } + } + + return CandidateDelegation; +} + +TOptional USpatialNetDriverDebugContext::GetActorExplicitDelegation(const AActor* Actor) +{ + SpatialGDK::DebugComponent* DbgComp = nullptr; + if (DebugComponentView* DebugInfo = ActorDebugInfo.Find(Actor)) + { + DbgComp = &DebugInfo->Component; + } + else + { + Worker_EntityId Entity = NetDriver->PackageMap->GetEntityIdFromObject(Actor); + if (Entity != SpatialConstants::INVALID_ENTITY_ID) + { + DbgComp = NetDriver->StaticComponentView->GetComponentData(Entity); + } + } + + if (!DbgComp) + { + return {}; + } + + if (DbgComp->DelegatedWorkerId) + { + return *DbgComp->DelegatedWorkerId; + } + + TOptional CandidateDelegation; + for (auto Tag : DbgComp->ActorTags) + { + if (VirtualWorkerId* Worker = SemanticDelegations.Find(Tag)) + { + if (ensureMsgf(!CandidateDelegation.IsSet() || CandidateDelegation.GetValue() == *Worker, + TEXT("Inconsistent delegation. Actor %s delegated to both %i and %i"), *Actor->GetName(), + CandidateDelegation.GetValue(), *Worker)) + { + CandidateDelegation = *Worker; + } + } + } + + return CandidateDelegation; +} + +void USpatialNetDriverDebugContext::TickServer() +{ + for (auto& Entry : ActorDebugInfo) + { + AActor* Actor = Entry.Key; + DebugComponentView& View = Entry.Value; + if (!View.bAdded) + { + Worker_EntityId Entity = NetDriver->PackageMap->GetEntityIdFromObject(Actor); + if (Entity != SpatialConstants::INVALID_ENTITY_ID) + { + if (!IsSetIntersectionEmpty(View.Component.ActorTags, SemanticInterest)) + { + AddEntityToWatch(Entity); + } + + // There is a requirement of readiness before we can use SendAddComponent + if (IsActorReady(Actor)) + { + Worker_ComponentData CompData = View.Component.CreateDebugComponent(); + NetDriver->Sender->SendAddComponents(Entity, { CompData }); + View.Entity = Entity; + View.bAdded = true; + } + } + } + else if (View.bDirty) + { + FWorkerComponentUpdate CompUpdate = View.Component.CreateDebugComponentUpdate(); + NetDriver->Connection->SendComponentUpdate(View.Entity, &CompUpdate); + View.bDirty = false; + } + } + + if (NeedEntityInterestUpdate()) + { + NetDriver->Sender->UpdatePartitionEntityInterestAndPosition(); + } +} + +bool USpatialNetDriverDebugContext::IsActorReady(AActor* Actor) +{ + Worker_EntityId Entity = NetDriver->PackageMap->GetEntityIdFromObject(Actor); + if (Entity != SpatialConstants::INVALID_ENTITY_ID) + { + return NetDriver->HasServerAuthority(Entity); + } + return false; +} + +SpatialGDK::QueryConstraint USpatialNetDriverDebugContext::ComputeAdditionalEntityQueryConstraint() const +{ + SpatialGDK::QueryConstraint EntitiesConstraint; + for (Worker_EntityId Entity : CachedInterestSet) + { + SpatialGDK::QueryConstraint EntityQuery; + EntityQuery.EntityIdConstraint = Entity; + EntitiesConstraint.OrConstraint.Add(EntityQuery); + } + + return EntitiesConstraint; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp index 0a4d3069db..4acc954a99 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp @@ -3,8 +3,9 @@ #include "EngineClasses/SpatialPackageMapClient.h" #include "EngineClasses/SpatialActorChannel.h" -#include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialNetBitReader.h" +#include "EngineClasses/SpatialNetConnection.h" +#include "EngineClasses/SpatialNetDriver.h" #include "Interop/Connection/SpatialWorkerConnection.h" #include "Interop/SpatialReceiver.h" #include "Interop/SpatialSender.h" @@ -12,10 +13,11 @@ #include "SpatialConstants.h" #include "Utils/SchemaOption.h" -#include "EngineUtils.h" #include "Engine/Engine.h" +#include "EngineUtils.h" #include "GameFramework/Actor.h" #include "Kismet/GameplayStatics.h" +#include "Runtime/Launch/Resources/Version.h" #include "UObject/UObjectGlobals.h" DEFINE_LOG_CATEGORY(LogSpatialPackageMap); @@ -34,31 +36,32 @@ void USpatialPackageMapClient::Init(USpatialNetDriver* NetDriver, FTimerManager* void GetSubobjects(UObject* ParentObject, TArray& InSubobjects) { InSubobjects.Empty(); - ForEachObjectWithOuter(ParentObject, [&InSubobjects](UObject* Object) - { - // Objects can only be allocated NetGUIDs if this is true. - if (Object->IsSupportedForNetworking() && !Object->IsPendingKill() && !Object->IsEditorOnly()) - { - // Walk up the outer chain and ensure that no object is PendingKill. This is required because although - // EInternalObjectFlags::PendingKill prevents objects that are PendingKill themselves from getting added - // to the list, it'll still add children of PendingKill objects. This then causes an assertion within - // FNetGUIDCache::RegisterNetGUID_Server where it again iterates up the object's owner chain, assigning - // ids and ensuring that no object is set to PendingKill in the process. - UObject* Outer = Object->GetOuter(); - while (Outer != nullptr) + ForEachObjectWithOuter( + ParentObject, + [&InSubobjects](UObject* Object) { + // Objects can only be allocated NetGUIDs if this is true. + if (Object->IsSupportedForNetworking() && !Object->IsPendingKill() && !Object->IsEditorOnly()) { - if (Outer->IsPendingKill()) + // Walk up the outer chain and ensure that no object is PendingKill. This is required because although + // EInternalObjectFlags::PendingKill prevents objects that are PendingKill themselves from getting added + // to the list, it'll still add children of PendingKill objects. This then causes an assertion within + // FNetGUIDCache::RegisterNetGUID_Server where it again iterates up the object's owner chain, assigning + // ids and ensuring that no object is set to PendingKill in the process. + UObject* Outer = Object->GetOuter(); + while (Outer != nullptr) { - return; + if (Outer->IsPendingKill()) + { + return; + } + Outer = Outer->GetOuter(); } - Outer = Outer->GetOuter(); + InSubobjects.Add(Object); } - InSubobjects.Add(Object); - } - }, true, RF_NoFlags, EInternalObjectFlags::PendingKill); + }, + true, RF_NoFlags, EInternalObjectFlags::PendingKill); - InSubobjects.StableSort([](UObject& A, UObject& B) - { + InSubobjects.StableSort([](UObject& A, UObject& B) { return A.GetName() < B.GetName(); }); } @@ -150,7 +153,8 @@ bool USpatialPackageMapClient::ResolveEntityActor(AActor* Actor, Worker_EntityId if (GetEntityIdFromObject(Actor) != EntityId) { - UE_LOG(LogSpatialPackageMap, Error, TEXT("ResolveEntityActor failed for Actor: %s with NetGUID: %s and passed entity ID: %lld"), *Actor->GetName(), *NetGUID.ToString(), EntityId); + UE_LOG(LogSpatialPackageMap, Error, TEXT("ResolveEntityActor failed for Actor: %s with NetGUID: %s and passed entity ID: %lld"), + *Actor->GetName(), *NetGUID.ToString(), EntityId); return false; } @@ -200,13 +204,13 @@ FNetworkGUID USpatialPackageMapClient::ResolveStablyNamedObject(UObject* Object) return SpatialGuidCache->AssignNewStablyNamedObjectNetGUID(Object); } -FUnrealObjectRef USpatialPackageMapClient::GetUnrealObjectRefFromNetGUID(const FNetworkGUID & NetGUID) const +FUnrealObjectRef USpatialPackageMapClient::GetUnrealObjectRefFromNetGUID(const FNetworkGUID& NetGUID) const { FSpatialNetGUIDCache* SpatialGuidCache = static_cast(GuidCache.Get()); return SpatialGuidCache->GetUnrealObjectRefFromNetGUID(NetGUID); } -FNetworkGUID USpatialPackageMapClient::GetNetGUIDFromUnrealObjectRef(const FUnrealObjectRef & ObjectRef) const +FNetworkGUID USpatialPackageMapClient::GetNetGUIDFromUnrealObjectRef(const FUnrealObjectRef& ObjectRef) const { FSpatialNetGUIDCache* SpatialGuidCache = static_cast(GuidCache.Get()); return SpatialGuidCache->GetNetGUIDFromUnrealObjectRef(ObjectRef); @@ -230,7 +234,33 @@ TWeakObjectPtr USpatialPackageMapClient::GetObjectFromUnrealObjectRef(c return nullptr; } -TWeakObjectPtr USpatialPackageMapClient::GetObjectFromEntityId(const Worker_EntityId& EntityId) +FNetworkGUID* USpatialPackageMapClient::GetRemovedDynamicSubobjectNetGUID(const FUnrealObjectRef& ObjectRef) +{ + if (FNetworkGUID* NetGUID = RemovedDynamicSubobjectObjectRefs.Find(ObjectRef)) + { + return NetGUID; + } + return nullptr; +} + +void USpatialPackageMapClient::AddRemovedDynamicSubobjectObjectRef(const FUnrealObjectRef& ObjectRef, const FNetworkGUID& NetGUID) +{ + RemovedDynamicSubobjectObjectRefs.Emplace(ObjectRef, NetGUID); +} + +void USpatialPackageMapClient::ClearRemovedDynamicSubobjectObjectRefs(const Worker_EntityId& InEntityId) +{ + for (auto DynamicSubobjectIterator = RemovedDynamicSubobjectObjectRefs.CreateIterator(); DynamicSubobjectIterator; + ++DynamicSubobjectIterator) + { + if (DynamicSubobjectIterator->Key.Entity == InEntityId) + { + DynamicSubobjectIterator.RemoveCurrent(); + } + } +} + +TWeakObjectPtr USpatialPackageMapClient::GetObjectFromEntityId(const Worker_EntityId EntityId) { return GetObjectFromUnrealObjectRef(FUnrealObjectRef(EntityId, 0)); } @@ -294,7 +324,8 @@ AActor* USpatialPackageMapClient::GetUniqueActorInstanceByClass(UClass* UniqueOb return FoundActors[0]; } - UE_LOG(LogSpatialPackageMap, Warning, TEXT("Found %d Actors for class: %s. There should only be one."), FoundActors.Num(), *UniqueObjectClass->GetName()); + UE_LOG(LogSpatialPackageMap, Warning, TEXT("Found %d Actors for class: %s. There should only be one."), FoundActors.Num(), + *UniqueObjectClass->GetName()); return nullptr; } @@ -314,20 +345,21 @@ FEntityPoolReadyEvent& USpatialPackageMapClient::GetEntityPoolReadyDelegate() return EntityPool->GetEntityPoolReadyDelegate(); } -bool USpatialPackageMapClient::SerializeObject(FArchive& Ar, UClass* InClass, UObject*& Obj, FNetworkGUID *OutNetGUID) +bool USpatialPackageMapClient::SerializeObject(FArchive& Ar, UClass* InClass, UObject*& Obj, FNetworkGUID* OutNetGUID) { // Super::SerializeObject is not called here on purpose if (Ar.IsSaving()) { - Ar << Obj; + FSpatialNetBitWriter::WriteObject(Ar, this, Obj); return true; } + else + { + bool bUnresolved = false; + Obj = FSpatialNetBitReader::ReadObject(Ar, this, bUnresolved); - FSpatialNetBitReader& Reader = static_cast(Ar); - bool bUnresolved = false; - Obj = Reader.ReadObject(bUnresolved); - - return !bUnresolved; + return !bUnresolved; + } } const FClassInfo* USpatialPackageMapClient::TryResolveNewDynamicSubobjectAndGetClassInfo(UObject* Object) @@ -340,13 +372,16 @@ const FClassInfo* USpatialPackageMapClient::TryResolveNewDynamicSubobjectAndGetC FUnrealObjectRef Ref = GetUnrealObjectRefFromObject(Object); if (Ref.IsValid()) { - UE_LOG(LogSpatialPackageMap, Error, TEXT("Trying to resolve a dynamic subobject twice! Object %s, Actor %s, EntityId %d."), *GetNameSafe(Object), *GetNameSafe(Actor), EntityId); + UE_LOG(LogSpatialPackageMap, Error, TEXT("Trying to resolve a dynamic subobject twice! Object %s, Actor %s, EntityId %d."), + *GetNameSafe(Object), *GetNameSafe(Actor), EntityId); return nullptr; } - const FClassInfo* Info = Cast(GuidCache->Driver)->ClassInfoManager->GetClassInfoForNewSubobject(Object, EntityId, this); + const FClassInfo* Info = + Cast(GuidCache->Driver)->ClassInfoManager->GetClassInfoForNewSubobject(Object, EntityId, this); - // If we don't get the info, an error is logged in the above function, that we have exceeded the maximum number of dynamic subobjects on the entity + // If we don't get the info, an error is logged in the above function, that we have exceeded the maximum number of dynamic + // subobjects on the entity if (Info != nullptr) { ResolveSubobject(Object, FUnrealObjectRef(EntityId, Info->SchemaComponents[SCHEMA_Data])); @@ -355,11 +390,17 @@ const FClassInfo* USpatialPackageMapClient::TryResolveNewDynamicSubobjectAndGetC return Info; } - UE_LOG(LogSpatialPackageMap, Error, TEXT("While trying to resolve a new dynamic subobject %s, the parent actor %s was not resolved."), *GetNameSafe(Object), *GetNameSafe(Actor)); + UE_LOG(LogSpatialPackageMap, Error, TEXT("While trying to resolve a new dynamic subobject %s, the parent actor %s was not resolved."), + *GetNameSafe(Object), *GetNameSafe(Actor)); return nullptr; } +Worker_EntityId USpatialPackageMapClient::AllocateNewEntityId() const +{ + return EntityPool->GetNextEntityId(); +} + FSpatialNetGUIDCache::FSpatialNetGUIDCache(USpatialNetDriver* InDriver) : FNetGUIDCache(InDriver) { @@ -399,8 +440,8 @@ FNetworkGUID FSpatialNetGUIDCache::AssignNewEntityActorNetGUID(AActor* Actor, Wo RegisterObjectRef(NetGUID, EntityObjectRef); } - UE_LOG(LogSpatialPackageMap, Verbose, TEXT("Registered new object ref for actor: %s. NetGUID: %s, entity ID: %lld"), - *Actor->GetName(), *NetGUID.ToString(), EntityId); + UE_LOG(LogSpatialPackageMap, Verbose, TEXT("Registered new object ref for actor: %s. NetGUID: %s, entity ID: %lld"), *Actor->GetName(), + *NetGUID.ToString(), EntityId); const FClassInfo& Info = SpatialNetDriver->ClassInfoManager->GetOrCreateClassInfoByClass(Actor->GetClass()); const SubobjectToOffsetMap& SubobjectToOffset = SpatialGDK::CreateOffsetMapFromActor(Actor, Info); @@ -419,7 +460,8 @@ FNetworkGUID FSpatialNetGUIDCache::AssignNewEntityActorNetGUID(AActor* Actor, Wo { if (Subobject->GetFName().ToString().Equals(TEXT("PersistentLevel")) && !Subobject->IsA()) { - UE_LOG(LogSpatialPackageMap, Fatal, TEXT("Found object called PersistentLevel which isn't a Level! This is not allowed when using the GDK")); + UE_LOG(LogSpatialPackageMap, Fatal, + TEXT("Found object called PersistentLevel which isn't a Level! This is not allowed when using the GDK")); } // Using StablyNamedRef for the outer since referencing ObjectRef in the map @@ -437,8 +479,9 @@ FNetworkGUID FSpatialNetGUIDCache::AssignNewEntityActorNetGUID(AActor* Actor, Wo RegisterObjectRef(SubobjectNetGUID, EntityIdSubobjectRef); - 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()); + 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()); } return NetGUID; @@ -470,10 +513,10 @@ FNetworkGUID FSpatialNetGUIDCache::AssignNewStablyNamedObjectNetGUID(UObject* Ob OuterGUID = AssignNewStablyNamedObjectNetGUID(OuterObject); } - if (Object->GetFName().ToString().Equals(TEXT("PersistentLevel")) && !Object->IsA()) { - UE_LOG(LogSpatialPackageMap, Fatal, TEXT("Found object called PersistentLevel which isn't a Level! This is not allowed when using the GDK")); + UE_LOG(LogSpatialPackageMap, Fatal, + TEXT("Found object called PersistentLevel which isn't a Level! This is not allowed when using the GDK")); } bool bNoLoadOnClient = false; @@ -484,7 +527,9 @@ FNetworkGUID FSpatialNetGUIDCache::AssignNewStablyNamedObjectNetGUID(UObject* Ob // resolve the references. bNoLoadOnClient = !CanClientLoadObject(Object, NetGUID); } - FUnrealObjectRef StablyNamedObjRef(0, 0, Object->GetFName().ToString(), (OuterGUID.IsValid() && !OuterGUID.IsDefault()) ? GetUnrealObjectRefFromNetGUID(OuterGUID) : FUnrealObjectRef(), bNoLoadOnClient); + FUnrealObjectRef StablyNamedObjRef( + 0, 0, Object->GetFName().ToString(), + (OuterGUID.IsValid() && !OuterGUID.IsDefault()) ? GetUnrealObjectRefFromNetGUID(OuterGUID) : FUnrealObjectRef(), bNoLoadOnClient); RegisterObjectRef(NetGUID, StablyNamedObjRef); return NetGUID; @@ -495,7 +540,8 @@ void FSpatialNetGUIDCache::RemoveEntityNetGUID(Worker_EntityId EntityId) // Remove actor subobjects. USpatialNetDriver* SpatialNetDriver = Cast(Driver); - SpatialGDK::UnrealMetadata* UnrealMetadata = SpatialNetDriver->StaticComponentView->GetComponentData(EntityId); + SpatialGDK::UnrealMetadata* UnrealMetadata = + SpatialNetDriver->StaticComponentView->GetComponentData(EntityId); // If UnrealMetadata is nullptr (can happen if the editor is closing down) just return. if (UnrealMetadata == nullptr) @@ -530,7 +576,8 @@ void FSpatialNetGUIDCache::RemoveEntityNetGUID(Worker_EntityId EntityId) if (StablyNamedRefOption.IsSet()) { - UnrealObjectRefToNetGUID.Remove(FUnrealObjectRef(0, 0, SubobjectInfoPair.Value->SubobjectName.ToString(), StablyNamedRefOption.GetValue())); + UnrealObjectRefToNetGUID.Remove( + FUnrealObjectRef(0, 0, SubobjectInfoPair.Value->SubobjectName.ToString(), StablyNamedRefOption.GetValue())); } } } @@ -574,7 +621,8 @@ void FSpatialNetGUIDCache::RemoveSubobjectNetGUID(const FUnrealObjectRef& Subobj } USpatialNetDriver* SpatialNetDriver = Cast(Driver); - SpatialGDK::UnrealMetadata* UnrealMetadata = SpatialNetDriver->StaticComponentView->GetComponentData(SubobjectRef.Entity); + SpatialGDK::UnrealMetadata* UnrealMetadata = + SpatialNetDriver->StaticComponentView->GetComponentData(SubobjectRef.Entity); // If UnrealMetadata is nullptr (can happen if the editor is closing down) just return. if (UnrealMetadata == nullptr) @@ -591,7 +639,8 @@ void FSpatialNetGUIDCache::RemoveSubobjectNetGUID(const FUnrealObjectRef& Subobj if (UnrealMetadata->NativeClass.IsStale()) { - UE_LOG(LogSpatialPackageMap, Warning, TEXT("Attempting to remove stale subobject from package map - %s"), *UnrealMetadata->ClassPath); + UE_LOG(LogSpatialPackageMap, Warning, TEXT("Attempting to remove stale subobject from package map - %s"), + *UnrealMetadata->ClassPath); } else { @@ -604,7 +653,8 @@ void FSpatialNetGUIDCache::RemoveSubobjectNetGUID(const FUnrealObjectRef& Subobj if (StablyNamedRefOption.IsSet()) { - UnrealObjectRefToNetGUID.Remove(FUnrealObjectRef(0, 0, SubobjectInfoPtr->Get().SubobjectName.ToString(), StablyNamedRefOption.GetValue())); + UnrealObjectRefToNetGUID.Remove( + FUnrealObjectRef(0, 0, SubobjectInfoPtr->Get().SubobjectName.ToString(), StablyNamedRefOption.GetValue())); } } } @@ -653,7 +703,11 @@ void FSpatialNetGUIDCache::NetworkRemapObjectRefPaths(FUnrealObjectRef& ObjectRe if (Iterator->Path.IsSet()) { FString TempPath(*Iterator->Path); +#if ENGINE_MINOR_VERSION >= 26 + GEngine->NetworkRemapPath(Cast(Driver)->GetSpatialOSNetConnection(), TempPath, bReading); +#else GEngine->NetworkRemapPath(Driver, TempPath, bReading); +#endif Iterator->Path = TempPath; } if (!Iterator->Outer.IsSet()) @@ -685,17 +739,22 @@ FNetworkGUID FSpatialNetGUIDCache::GetNetGUIDFromEntityId(Worker_EntityId Entity return (NetGUID == nullptr) ? FNetworkGUID(0) : *NetGUID; } -FNetworkGUID FSpatialNetGUIDCache::RegisterNetGUIDFromPathForStaticObject(const FString& PathName, const FNetworkGUID& OuterGUID, bool bNoLoadOnClient) +FNetworkGUID FSpatialNetGUIDCache::RegisterNetGUIDFromPathForStaticObject(const FString& PathName, const FNetworkGUID& OuterGUID, + bool bNoLoadOnClient) { // Put the PIE prefix back (if applicable) so that the correct object can be found. FString TempPath = PathName; - GEngine->NetworkRemapPath(Driver, TempPath, true); +#if ENGINE_MINOR_VERSION >= 26 + GEngine->NetworkRemapPath(Cast(Driver)->GetSpatialOSNetConnection(), TempPath, true /*bIsReading*/); +#else + GEngine->NetworkRemapPath(Driver, TempPath, true /*bIsReading*/); +#endif // This function should only be called for stably named object references, not dynamic ones. FNetGuidCacheObject CacheObject; CacheObject.PathName = FName(*TempPath); CacheObject.OuterGUID = OuterGUID; - CacheObject.bNoLoad = bNoLoadOnClient; // server decides whether the client should load objects (e.g. don't load levels) + CacheObject.bNoLoad = bNoLoadOnClient; // server decides whether the client should load objects (e.g. don't load levels) CacheObject.bIgnoreWhenMissing = bNoLoadOnClient; FNetworkGUID NetGUID = GenerateNewNetGUID(1); RegisterNetGUID_Internal(NetGUID, CacheObject); @@ -705,8 +764,8 @@ FNetworkGUID FSpatialNetGUIDCache::RegisterNetGUIDFromPathForStaticObject(const FNetworkGUID FSpatialNetGUIDCache::GenerateNewNetGUID(const int32 IsStatic) { // Here we have to borrow from FNetGuidCache::AssignNewNetGUID_Server to avoid a source change. -#define COMPOSE_NET_GUID(Index, IsStatic) (((Index) << 1) | (IsStatic) ) -#define ALLOC_NEW_NET_GUID(IsStatic) (COMPOSE_NET_GUID(++UniqueNetIDs[IsStatic], IsStatic)) +#define COMPOSE_NET_GUID(Index, IsStatic) (((Index) << 1) | (IsStatic)) +#define ALLOC_NEW_NET_GUID(IsStatic) (COMPOSE_NET_GUID(++UniqueNetIDs[IsStatic], IsStatic)) // Generate new NetGUID and assign it FNetworkGUID NetGUID = FNetworkGUID(ALLOC_NEW_NET_GUID(IsStatic)); @@ -732,9 +791,7 @@ FNetworkGUID FSpatialNetGUIDCache::GetOrAssignNetGUID_SpatialGDK(UObject* Object RegisterNetGUID_Internal(NetGUID, CacheObject); UE_LOG(LogSpatialPackageMap, Verbose, TEXT("%s: NetGUID for object %s was not found in the cache. Generated new NetGUID %s."), - *Cast(Driver)->Connection->GetWorkerId(), - *Object->GetPathName(), - *NetGUID.ToString()); + *Cast(Driver)->Connection->GetWorkerId(), *Object->GetPathName(), *NetGUID.ToString()); } check((NetGUID.IsValid() && !NetGUID.IsDefault()) || Object == nullptr); @@ -747,10 +804,13 @@ void FSpatialNetGUIDCache::RegisterObjectRef(FNetworkGUID NetGUID, const FUnreal FUnrealObjectRef RemappedObjectRef = ObjectRef; NetworkRemapObjectRefPaths(RemappedObjectRef, false /*bIsReading*/); - checkfSlow(!NetGUIDToUnrealObjectRef.Contains(NetGUID) || (NetGUIDToUnrealObjectRef.Contains(NetGUID) && NetGUIDToUnrealObjectRef.FindChecked(NetGUID) == RemappedObjectRef), - TEXT("NetGUID to UnrealObjectRef mismatch - NetGUID: %s ObjRef in map: %s ObjRef expected: %s"), *NetGUID.ToString(), - *NetGUIDToUnrealObjectRef.FindChecked(NetGUID).ToString(), *RemappedObjectRef.ToString()); - checkfSlow(!UnrealObjectRefToNetGUID.Contains(RemappedObjectRef) || (UnrealObjectRefToNetGUID.Contains(RemappedObjectRef) && UnrealObjectRefToNetGUID.FindChecked(RemappedObjectRef) == NetGUID), + checkfSlow(!NetGUIDToUnrealObjectRef.Contains(NetGUID) + || (NetGUIDToUnrealObjectRef.Contains(NetGUID) && NetGUIDToUnrealObjectRef.FindChecked(NetGUID) == RemappedObjectRef), + TEXT("NetGUID to UnrealObjectRef mismatch - NetGUID: %s ObjRef in map: %s ObjRef expected: %s"), *NetGUID.ToString(), + *NetGUIDToUnrealObjectRef.FindChecked(NetGUID).ToString(), *RemappedObjectRef.ToString()); + checkfSlow( + !UnrealObjectRefToNetGUID.Contains(RemappedObjectRef) + || (UnrealObjectRefToNetGUID.Contains(RemappedObjectRef) && UnrealObjectRefToNetGUID.FindChecked(RemappedObjectRef) == NetGUID), TEXT("UnrealObjectRef to NetGUID mismatch - UnrealObjectRef: %s NetGUID in map: %s NetGUID expected: %s"), *NetGUID.ToString(), *UnrealObjectRefToNetGUID.FindChecked(RemappedObjectRef).ToString(), *RemappedObjectRef.ToString()); NetGUIDToUnrealObjectRef.Emplace(NetGUID, RemappedObjectRef); diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialReplicationGraph.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialReplicationGraph.cpp index 931d5de054..2d64625d88 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialReplicationGraph.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialReplicationGraph.cpp @@ -4,6 +4,21 @@ #include "EngineClasses/SpatialActorChannel.h" #include "EngineClasses/SpatialNetDriver.h" +#include "Interop/SpatialReplicationGraphLoadBalancingHandler.h" +#include "Utils/SpatialActorUtils.h" + +void USpatialReplicationGraph::InitForNetDriver(UNetDriver* InNetDriver) +{ + UReplicationGraph::InitForNetDriver(InNetDriver); + + if (USpatialStatics::IsMultiWorkerEnabled()) + { + if (USpatialNetDriver* SpatialNetDriver = Cast(InNetDriver)) + { + LoadBalancingHandler = MakeUnique(SpatialNetDriver); + } + } +} UActorChannel* USpatialReplicationGraph::GetOrCreateSpatialActorChannel(UObject* TargetObject) { @@ -19,3 +34,56 @@ UActorChannel* USpatialReplicationGraph::GetOrCreateSpatialActorChannel(UObject* return nullptr; } + +void USpatialReplicationGraph::OnOwnerUpdated(AActor* Actor, AActor* OldOwner) +{ + AActor* NewOwner = Actor->GetOwner(); + + if (OldOwner == NewOwner) + { + return; + } + + if (OldOwner != nullptr) + { + GlobalActorReplicationInfoMap.RemoveDependentActor(OldOwner, Actor); + } + + if (NewOwner != nullptr) + { + GlobalActorReplicationInfoMap.Get(NewOwner); + GlobalActorReplicationInfoMap.Get(Actor); + GlobalActorReplicationInfoMap.AddDependentActor(NewOwner, Actor); + } +} + +void USpatialReplicationGraph::PreReplicateActors(UNetReplicationGraphConnection* ConnectionManager) +{ + if (LoadBalancingHandler.IsValid()) + { + USpatialNetDriver* SpatialNetDriver = Cast(NetDriver); + FSpatialReplicationGraphLoadBalancingContext LoadBalancingCtx(SpatialNetDriver, this, ConnectionManager->ActorInfoMap, + PrioritizedReplicationList); + LoadBalancingHandler->EvaluateActorsToMigrate(LoadBalancingCtx); + + for (AActor* Actor : LoadBalancingCtx.AdditionalActorsToReplicate) + { + // Only add net owners to the list as they will visit their dependents when replicated. + AActor* NetOwner = SpatialGDK::GetReplicatedHierarchyRoot(Actor); + if (NetOwner == Actor) + { + FConnectionReplicationActorInfo& ConnectionData = ConnectionManager->ActorInfoMap.FindOrAdd(Actor); + FGlobalActorReplicationInfo& GlobalData = GlobalActorReplicationInfoMap.Get(Actor); + PrioritizedReplicationList.Items.Emplace(FPrioritizedRepList::FItem(0, Actor, &GlobalData, &ConnectionData)); + } + } + } +} + +void USpatialReplicationGraph::PostReplicateActors(UNetReplicationGraphConnection* ConnectionManager) +{ + if (LoadBalancingHandler.IsValid()) + { + LoadBalancingHandler->ProcessMigrations(); + } +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp index 7d42bb5fba..89dc315430 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp @@ -1,55 +1,103 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "EngineClasses/SpatialVirtualWorkerTranslationManager.h" + +#include "EngineClasses/SpatialPackageMapClient.h" #include "EngineClasses/SpatialVirtualWorkerTranslator.h" #include "Interop/Connection/SpatialWorkerConnection.h" #include "Interop/SpatialOSDispatcherInterface.h" #include "SpatialConstants.h" +#include "Utils/EntityFactory.h" #include "Utils/SchemaUtils.h" DEFINE_LOG_CATEGORY(LogSpatialVirtualWorkerTranslationManager); -SpatialVirtualWorkerTranslationManager::SpatialVirtualWorkerTranslationManager( - SpatialOSDispatcherInterface* InReceiver, - SpatialOSWorkerInterface* InConnection, - SpatialVirtualWorkerTranslator* InTranslator) - : Receiver(InReceiver) +SpatialVirtualWorkerTranslationManager::SpatialVirtualWorkerTranslationManager(SpatialOSDispatcherInterface* InReceiver, + SpatialOSWorkerInterface* InConnection, + SpatialVirtualWorkerTranslator* InTranslator) + : Translator(InTranslator) + , Receiver(InReceiver) , Connection(InConnection) - , Translator(InTranslator) + , Partitions({}) , bWorkerEntityQueryInFlight(false) -{} +{ +} -void SpatialVirtualWorkerTranslationManager::SetNumberOfVirtualWorkers(const uint32 NumVirtualWorkers) +void SpatialVirtualWorkerTranslationManager::SetNumberOfVirtualWorkers(const uint32 InNumVirtualWorkers) { - UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT("TranslationManager is configured to look for %d workers"), NumVirtualWorkers); + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT("TranslationManager is configured to look for %d workers"), + InNumVirtualWorkers); + + NumVirtualWorkers = InNumVirtualWorkers; + + // Currently, this should only be called once on startup. In the future we may allow for more flexibility. + VirtualWorkersToAssign.Reserve(NumVirtualWorkers); - // Currently, this should only be called once on startup. In the future we may allow for more - // flexibility. for (uint32 i = 1; i <= NumVirtualWorkers; i++) { - UnassignedVirtualWorkers.Enqueue(i); + VirtualWorkersToAssign.Emplace(i); } } -void SpatialVirtualWorkerTranslationManager::AuthorityChanged(const Worker_AuthorityChangeOp& AuthOp) +void SpatialVirtualWorkerTranslationManager::AuthorityChanged(const Worker_ComponentSetAuthorityChangeOp& AuthOp) { - check(AuthOp.component_id == SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID); + check(AuthOp.component_set_id == SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_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.")); + 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.")); + const int32 ExistingTranslatorMappingCount = Translator->GetMappingCount(); + if (ExistingTranslatorMappingCount == 0) + { + // Fresh deployment, we need to create partition entities before we start assigning virtual workers. + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, + TEXT("Gained authority over the VirtualWorker translation, spawning partition entities.")); + SpawnPartitionEntitiesForVirtualWorkerIds(); + } + else if (ExistingTranslatorMappingCount == NumVirtualWorkers) + { + // Partitions already exist, reclaim them with latest server worker entities + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, + TEXT("Gained authority over the VirtualWorker translation, reclaiming partition entities.")); + ReclaimPartitionEntities(); + } + else + { + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Error, + TEXT("Gained authority with invalid translator mapping count. Are you attempting to load a snapshot with a different load " + "balancing strategy? Expected (%d) Present (%d)"), + NumVirtualWorkers, ExistingTranslatorMappingCount); + } +} - // 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. +void SpatialVirtualWorkerTranslationManager::SpawnPartitionEntitiesForVirtualWorkerIds() +{ + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT("Spawning partition entities for %d virtual workers"), + VirtualWorkersToAssign.Num()); + for (const VirtualWorkerId VirtualWorkerId : VirtualWorkersToAssign) + { + const Worker_EntityId PartitionEntityId = Translator->NetDriver->PackageMap->AllocateNewEntityId(); + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT("- Virtual Worker: %d. Entity: %lld. "), VirtualWorkerId, + PartitionEntityId); + SpawnPartitionEntity(PartitionEntityId, VirtualWorkerId); + } +} - // Query for all connection entities, so we can detect if some worker has died and needs to be updated in - // the mapping. +void SpatialVirtualWorkerTranslationManager::ReclaimPartitionEntities() +{ + Partitions.Empty(); + for (const VirtualWorkerId VirtualWorkerId : VirtualWorkersToAssign) + { + const Worker_PartitionId PartitionId = Translator->GetPartitionEntityForVirtualWorker(VirtualWorkerId); + check(PartitionId != SpatialConstants::INVALID_ENTITY_ID); + Partitions.Emplace(PartitionInfo{ PartitionId, VirtualWorkerId, SpatialConstants::INVALID_ENTITY_ID }); + } QueryForServerWorkerEntities(); } @@ -60,15 +108,18 @@ void SpatialVirtualWorkerTranslationManager::WriteMappingToSchema(Schema_Object* { 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); + SpatialGDK::AddStringToSchema(EntryObject, SpatialConstants::MAPPING_PHYSICAL_WORKER_NAME_ID, Entry.Value.WorkerName); + Schema_AddEntityId(EntryObject, SpatialConstants::MAPPING_SERVER_WORKER_ENTITY_ID, Entry.Value.ServerWorkerEntityId); + Schema_AddEntityId(EntryObject, SpatialConstants::MAPPING_PARTITION_ID, Entry.Value.PartitionEntityId); } } // 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) +bool SpatialVirtualWorkerTranslationManager::AllServerWorkersAreReady(const Worker_EntityQueryResponseOp& Op, uint32& ServerWorkersNotReady) { + ServerWorkersNotReady = 0; + // The query response is an array of entities. Each of these represents a worker. for (uint32_t i = 0; i < Op.result_count; ++i) { @@ -76,7 +127,7 @@ void SpatialVirtualWorkerTranslationManager::ConstructVirtualWorkerMappingFromQu 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, + // Server worker 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) { @@ -85,20 +136,49 @@ void SpatialVirtualWorkerTranslationManager::ConstructVirtualWorkerMappingFromQu // 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); + const bool bWorkerIsReadyToBeginPlay = + SpatialGDK::GetBoolFromSchema(ComponentObject, SpatialConstants::SERVER_WORKER_READY_TO_BEGIN_PLAY_ID); if (!bWorkerIsReadyToBeginPlay) { - continue; + ServerWorkersNotReady++; } + } + } + } - // 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); - } + return ServerWorkersNotReady == 0; +} + +// 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::AssignPartitionsToEachServerWorkerFromQueryResponse(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]; + + // Server worker 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); + + PartitionInfo& Partition = Partitions[i]; + + // 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. + PhysicalWorkerName WorkerName = SpatialGDK::GetStringFromSchema(ComponentObject, SpatialConstants::SERVER_WORKER_NAME_ID); + + Worker_EntityId SystemEntityId = Schema_GetEntityId(ComponentObject, SpatialConstants::SERVER_WORKER_SYSTEM_ENTITY_ID); + + // We store this so we can lookup when ClaimPartition commands fail. + Partition.SimulatingWorkerSystemEntityId = SystemEntityId; + + AssignPartitionToWorker(WorkerName, Entity.entity_id, SystemEntityId, Partition); } } } @@ -125,13 +205,70 @@ void SpatialVirtualWorkerTranslationManager::SendVirtualWorkerMappingUpdate() co Connection->SendComponentUpdate(SpatialConstants::INITIAL_VIRTUAL_WORKER_TRANSLATOR_ENTITY_ID, &Update); } +void SpatialVirtualWorkerTranslationManager::SpawnPartitionEntity(Worker_EntityId PartitionEntityId, VirtualWorkerId VirtualWorkerId) +{ + TArray Components = SpatialGDK::EntityFactory::CreatePartitionEntityComponents( + PartitionEntityId, Translator->NetDriver->InterestFactory.Get(), Translator->LoadBalanceStrategy.Get(), VirtualWorkerId, + Translator->NetDriver->DebugCtx != nullptr); + + const Worker_RequestId RequestId = + Connection->SendCreateEntityRequest(MoveTemp(Components), &PartitionEntityId, SpatialGDK::RETRY_UNTIL_COMPLETE); + + CreateEntityDelegate OnCreateWorkerEntityResponse; + OnCreateWorkerEntityResponse.BindLambda([this, VirtualWorkerId](const Worker_CreateEntityResponseOp& Op) { + if (Op.status_code == WORKER_STATUS_CODE_SUCCESS) + { + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, + TEXT("Successfully created partition entity. " + "Entity: %lld. Virtual Worker: %d"), + Op.entity_id, VirtualWorkerId); + OnPartitionEntityCreation(Op.entity_id, VirtualWorkerId); + return; + } + + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Error, + TEXT("Partition entity creation failed: \"%s\". " + "Entity: %lld. Virtual Worker: %d"), + UTF8_TO_TCHAR(Op.message), Op.entity_id, VirtualWorkerId); + }); + + Receiver->AddCreateEntityDelegate(RequestId, MoveTemp(OnCreateWorkerEntityResponse)); +} + +void SpatialVirtualWorkerTranslationManager::OnPartitionEntityCreation(Worker_EntityId PartitionEntityId, VirtualWorkerId VirtualWorker) +{ + Partitions.Emplace(PartitionInfo{ PartitionEntityId, VirtualWorker, SpatialConstants::INVALID_ENTITY_ID }); + + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, + TEXT("Adding translation manager mapping. Virtual worker %d -> Partition entity %lld"), VirtualWorker, PartitionEntityId); + + if (Partitions.Num() == VirtualWorkersToAssign.Num()) + { + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, + TEXT("Found all %d required partitions, querying for server worker entities"), VirtualWorkersToAssign.Num()); + QueryForServerWorkerEntities(); + } + else + { + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Verbose, + TEXT("Didn't find all %d required partitions, only found %d, currently have:"), VirtualWorkersToAssign.Num(), + Partitions.Num()); + for (const PartitionInfo& Partition : Partitions) + { + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Verbose, TEXT(" - virtual worker %d -> partition entity %lld"), + Partition.VirtualWorker, Partition.PartitionEntityId); + } + } +} + void SpatialVirtualWorkerTranslationManager::QueryForServerWorkerEntities() { - UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT("Sending query for WorkerEntities")); + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT("Sending query for server worker entities")); if (bWorkerEntityQueryInFlight) { - UE_LOG(LogSpatialVirtualWorkerTranslationManager, Warning, TEXT("Trying to query for worker entities while a previous query is still in flight!")); + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Warning, + TEXT("Trying to query for worker entities while a previous query is still in flight!")); return; } @@ -146,11 +283,10 @@ void SpatialVirtualWorkerTranslationManager::QueryForServerWorkerEntities() 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); + const Worker_RequestId RequestID = Connection->SendEntityQueryRequest(&WorkerEntityQuery, SpatialGDK::RETRY_UNTIL_COMPLETE); bWorkerEntityQueryInFlight = true; // Register a method to handle the query response. @@ -169,39 +305,45 @@ void SpatialVirtualWorkerTranslationManager::ServerWorkerEntityQueryDelegate(con 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); + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Error, TEXT("Server worker entity query failed: %s, retrying."), + UTF8_TO_TCHAR(Op.message)); + return; } - // If the translation mapping is complete, publish it. Otherwise retry the server worker entity query. - if (UnassignedVirtualWorkers.IsEmpty()) + if (Op.result_count != NumVirtualWorkers) { - SendVirtualWorkerMappingUpdate(); - } - else - { - UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT("Waiting for all virtual workers to be assigned before publishing translation update.")); + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, + TEXT("Waiting for all virtual workers to be assigned before publishing translation update. " + "We currently have %i workers connected out of the %i required"), + Op.result_count, NumVirtualWorkers); QueryForServerWorkerEntities(); + return; } -} -void SpatialVirtualWorkerTranslationManager::AssignWorker(const PhysicalWorkerName& Name, const Worker_EntityId& ServerWorkerEntityId) -{ - if (PhysicalToVirtualWorkerMapping.Contains(Name)) + uint32 ServerWorkersNotReady; + if (!AllServerWorkersAreReady(Op, ServerWorkersNotReady)) { + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Warning, + TEXT("Query found correct number of server workers but %d were not ready."), ServerWorkersNotReady); + QueryForServerWorkerEntities(); return; } - // Get a VirtualWorkerId from the list of unassigned work. - VirtualWorkerId Id; - UnassignedVirtualWorkers.Dequeue(Id); + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT("Found all required server worker entities ready to play.")); + AssignPartitionsToEachServerWorkerFromQueryResponse(Op); - VirtualToPhysicalWorkerMapping.Add(Id, MakeTuple(Name, ServerWorkerEntityId)); - PhysicalToVirtualWorkerMapping.Add(Name, Id); + SendVirtualWorkerMappingUpdate(); +} + +void SpatialVirtualWorkerTranslationManager::AssignPartitionToWorker(const PhysicalWorkerName& WorkerName, + const Worker_EntityId& ServerWorkerEntityId, + const Worker_EntityId& SystemEntityId, const PartitionInfo& Partition) +{ + VirtualToPhysicalWorkerMapping.Add(Partition.VirtualWorker, SpatialVirtualWorkerTranslator::WorkerInformation{ + WorkerName, ServerWorkerEntityId, Partition.PartitionEntityId }); + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, + TEXT("Assigned VirtualWorker %d with partition ID %lld to simulate on worker %s"), Partition.VirtualWorker, + Partition.PartitionEntityId, *WorkerName); - UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT("Assigned VirtualWorker %d to simulate on Worker %s"), Id, *Name); + Translator->NetDriver->Sender->SendClaimPartitionRequest(SystemEntityId, Partition.PartitionEntityId); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslator.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslator.cpp index 7debc8eb46..f680556dce 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslator.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslator.cpp @@ -1,77 +1,69 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "EngineClasses/SpatialVirtualWorkerTranslator.h" + +#include "EngineClasses/SpatialNetDriver.h" +#include "Interop/Connection/SpatialWorkerConnection.h" #include "LoadBalancing/AbstractLBStrategy.h" #include "SpatialConstants.h" #include "Utils/SchemaUtils.h" DEFINE_LOG_CATEGORY(LogSpatialVirtualWorkerTranslator); -SpatialVirtualWorkerTranslator::SpatialVirtualWorkerTranslator(UAbstractLBStrategy* InLoadBalanceStrategy, - PhysicalWorkerName InPhysicalWorkerName) - : LoadBalanceStrategy(InLoadBalanceStrategy) +SpatialVirtualWorkerTranslator::SpatialVirtualWorkerTranslator(UAbstractLBStrategy* InLoadBalanceStrategy, USpatialNetDriver* InNetDriver, + PhysicalWorkerName InLocalPhysicalWorkerName) + : NetDriver(InNetDriver) + , LoadBalanceStrategy(InLoadBalanceStrategy) , bIsReady(false) - , LocalPhysicalWorkerName(InPhysicalWorkerName) + , LocalPhysicalWorkerName(InLocalPhysicalWorkerName) , LocalVirtualWorkerId(SpatialConstants::INVALID_VIRTUAL_WORKER_ID) -{} + , LocalPartitionId(SpatialConstants::INVALID_ENTITY_ID) +{ +} const PhysicalWorkerName* SpatialVirtualWorkerTranslator::GetPhysicalWorkerForVirtualWorker(VirtualWorkerId Id) const { - if (const TPair* PhysicalWorkerInfo = VirtualToPhysicalWorkerMapping.Find(Id)) + if (const SpatialVirtualWorkerTranslator::WorkerInformation* PhysicalWorkerInfo = VirtualToPhysicalWorkerMapping.Find(Id)) { - return &PhysicalWorkerInfo->Key; + return &PhysicalWorkerInfo->WorkerName; } return nullptr; } -Worker_EntityId SpatialVirtualWorkerTranslator::GetServerWorkerEntityForVirtualWorker(VirtualWorkerId Id) const +Worker_PartitionId SpatialVirtualWorkerTranslator::GetPartitionEntityForVirtualWorker(VirtualWorkerId Id) const { - if (const TPair* PhysicalWorkerInfo = VirtualToPhysicalWorkerMapping.Find(Id)) + if (const SpatialVirtualWorkerTranslator::WorkerInformation* PhysicalWorkerInfo = VirtualToPhysicalWorkerMapping.Find(Id)) { - return PhysicalWorkerInfo->Value; + return PhysicalWorkerInfo->PartitionEntityId; } return SpatialConstants::INVALID_ENTITY_ID; } -void SpatialVirtualWorkerTranslator::ApplyVirtualWorkerManagerData(Schema_Object* ComponentObject) +Worker_EntityId SpatialVirtualWorkerTranslator::GetServerWorkerEntityForVirtualWorker(VirtualWorkerId Id) const { - 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) + if (const SpatialVirtualWorkerTranslator::WorkerInformation* PhysicalWorkerInfo = VirtualToPhysicalWorkerMapping.Find(Id)) { - 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); + return PhysicalWorkerInfo->ServerWorkerEntityId; } + + return SpatialConstants::INVALID_ENTITY_ID; } -// 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 +void SpatialVirtualWorkerTranslator::ApplyVirtualWorkerManagerData(Schema_Object* ComponentObject) { - int32 TranslationCount = (int32)Schema_GetObjectCount(Object, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_MAPPING_ID); + UE_LOG(LogSpatialVirtualWorkerTranslator, Log, TEXT("(%s) ApplyVirtualWorkerManagerData"), *LocalPhysicalWorkerName); - for (int32 i = 0; i < TranslationCount; i++) + // 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) { - // 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) - { - 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; - } + UE_LOG(LogSpatialVirtualWorkerTranslator, Verbose, + TEXT("Translator assignment: Virtual Worker %d to %s with server worker entity: %lld"), Entry.Key, *(Entry.Value.WorkerName), + Entry.Value.ServerWorkerEntityId); } - - return false; } // The translation schema is a list of Mappings, where each entry has a virtual and physical worker ID. @@ -79,37 +71,40 @@ bool SpatialVirtualWorkerTranslator::IsValidMapping(Schema_Object* Object) const // a worker first becomes authoritative for the mapping. void SpatialVirtualWorkerTranslator::ApplyMappingFromSchema(Schema_Object* Object) { - if (!IsValidMapping(Object)) - { - 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. - VirtualToPhysicalWorkerMapping.Empty(); - int32 TranslationCount = (int32)Schema_GetObjectCount(Object, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_MAPPING_ID); - VirtualToPhysicalWorkerMapping.Reserve(TranslationCount); + const uint32 TranslationCount = Schema_GetObjectCount(Object, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_MAPPING_ID); + VirtualToPhysicalWorkerMapping.Empty(TranslationCount); + + UE_LOG(LogSpatialVirtualWorkerTranslator, Verbose, TEXT("(%d) Apply valid mapping from schema"), LocalVirtualWorkerId); - for (int32 i = 0; i < TranslationCount; i++) + for (uint32 i = 0; i < TranslationCount; i++) { // 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); - 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); + const VirtualWorkerId VirtualWorkerId = Schema_GetUint32(MappingObject, SpatialConstants::MAPPING_VIRTUAL_WORKER_ID); + const PhysicalWorkerName WorkerName = + SpatialGDK::GetStringFromSchema(MappingObject, SpatialConstants::MAPPING_PHYSICAL_WORKER_NAME_ID); + const Worker_EntityId ServerWorkerEntityId = Schema_GetEntityId(MappingObject, SpatialConstants::MAPPING_SERVER_WORKER_ENTITY_ID); + const Worker_PartitionId PartitionEntityId = Schema_GetEntityId(MappingObject, SpatialConstants::MAPPING_PARTITION_ID); + + UE_LOG(LogSpatialVirtualWorkerTranslator, Log, + TEXT("Translator assignment: Virtual Worker %d to %s with server worker entity: %lld"), VirtualWorkerId, *WorkerName, + ServerWorkerEntityId); // Insert each into the provided map. - UpdateMapping(VirtualWorkerId, PhysicalWorkerName, ServerWorkerEntityId); + UpdateMapping(VirtualWorkerId, WorkerName, PartitionEntityId, ServerWorkerEntityId); } } -void SpatialVirtualWorkerTranslator::UpdateMapping(VirtualWorkerId Id, PhysicalWorkerName Name, Worker_EntityId ServerWorkerEntityId) +void SpatialVirtualWorkerTranslator::UpdateMapping(VirtualWorkerId Id, PhysicalWorkerName WorkerName, Worker_PartitionId PartitionEntityId, + Worker_EntityId ServerWorkerEntityId) { - VirtualToPhysicalWorkerMapping.Add(Id, MakeTuple(Name, ServerWorkerEntityId)); + VirtualToPhysicalWorkerMapping.Add(Id, WorkerInformation{ WorkerName, ServerWorkerEntityId, PartitionEntityId }); - if (LocalVirtualWorkerId == SpatialConstants::INVALID_VIRTUAL_WORKER_ID && Name == LocalPhysicalWorkerName) + if (LocalVirtualWorkerId == SpatialConstants::INVALID_VIRTUAL_WORKER_ID && WorkerName == LocalPhysicalWorkerName) { LocalVirtualWorkerId = Id; + LocalPartitionId = PartitionEntityId; bIsReady = true; // Tell the strategy about the local virtual worker id. This is an "if" and not a "check" to allow unit tests which don't diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialWorldSettings.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialWorldSettings.cpp new file mode 100644 index 0000000000..976edda78e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialWorldSettings.cpp @@ -0,0 +1,94 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +#include "EngineClasses/SpatialWorldSettings.h" + +#include "EngineUtils.h" +#include "Utils/SpatialDebugger.h" +#include "Utils/SpatialStatics.h" + +ASpatialWorldSettings::ASpatialWorldSettings(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , MultiWorkerSettingsClass(nullptr) + , EditorMultiWorkerSettingsOverride(nullptr) +{ +} + +TSubclassOf ASpatialWorldSettings::GetMultiWorkerSettingsClass(bool bForceNonEditorSettings /*= false*/) +{ + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + + if (SpatialGDKSettings->OverrideMultiWorkerSettingsClass.IsSet()) + { + // If command line override for Multi Worker Settings is set then use the specified Multi Worker Settings class. + FString OverrideMultiWorkerSettingsClass = SpatialGDKSettings->OverrideMultiWorkerSettingsClass.GetValue(); + FSoftClassPath MultiWorkerSettingsSoftClassPath(OverrideMultiWorkerSettingsClass); + MultiWorkerSettingsClass = MultiWorkerSettingsSoftClassPath.TryLoadClass(); + checkf(MultiWorkerSettingsClass != nullptr, TEXT("%s is not a valid class"), *OverrideMultiWorkerSettingsClass); + return GetValidWorkerSettings(); + } + else if (bForceNonEditorSettings && MultiWorkerSettingsClass != nullptr) + { + // If bForceNonEditorSettings is set and the multi worker setting class is set use the multi worker settings. + return MultiWorkerSettingsClass; + } + else if (bForceNonEditorSettings) + { + // If bForceNonEditorSettings is set and no multi worker settings class is set always return a valid class (use single worker + // behaviour). + return USpatialMultiWorkerSettings::StaticClass(); + } + else if (!USpatialStatics::IsMultiWorkerEnabled()) + { + // If multi worker is disabled in editor, use the single worker behaviour. + return USpatialMultiWorkerSettings::StaticClass(); + } +#if WITH_EDITOR + else if (EditorMultiWorkerSettingsOverride != nullptr) + { + // If the editor override Multi Worker Settings is set and we are in the Editor use the Editor Multi Worker Settings. + return EditorMultiWorkerSettingsOverride; + } +#endif // WITH_EDITOR + return GetValidWorkerSettings(); +} + +TSubclassOf ASpatialWorldSettings::GetValidWorkerSettings() const +{ + if (MultiWorkerSettingsClass != nullptr) + { + // If the MultiWorkerSettingsClass is set, return it. + return MultiWorkerSettingsClass; + } + else + { + // Otherwise, return a valid class, return single worker settings class. + return USpatialMultiWorkerSettings::StaticClass(); + } +} + +#if WITH_EDITOR +void ASpatialWorldSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (PropertyChangedEvent.Property != nullptr) + { + const FName PropertyName(PropertyChangedEvent.Property->GetFName()); + if (PropertyName == GET_MEMBER_NAME_CHECKED(ASpatialWorldSettings, MultiWorkerSettingsClass) + || PropertyName == GET_MEMBER_NAME_CHECKED(ASpatialWorldSettings, EditorMultiWorkerSettingsOverride)) + { + EditorRefreshSpatialDebugger(); + } + } +} + +void ASpatialWorldSettings::EditorRefreshSpatialDebugger() +{ + // Refresh the worker boundaries in the editor + UWorld* World = GEditor->GetEditorWorldContext().World(); + for (TActorIterator It(World); It; ++It) + { + ASpatialDebugger* FoundActor = *It; + FoundActor->EditorRefreshWorkerRegions(); + } +} +#endif // WITH_EDITOR diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/ConnectionConfig.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/ConnectionConfig.cpp new file mode 100644 index 0000000000..ddf8756743 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/ConnectionConfig.cpp @@ -0,0 +1,3 @@ +#include "Interop/Connection/ConnectionConfig.h" + +DEFINE_LOG_CATEGORY(LogConnectionConfig); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/LegacySpatialWorkerConnection.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/LegacySpatialWorkerConnection.cpp deleted file mode 100644 index 5319266c60..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/LegacySpatialWorkerConnection.cpp +++ /dev/null @@ -1,432 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#include "Interop/Connection/LegacySpatialWorkerConnection.h" -#include "SpatialView/OpList/WorkerConnectionOpList.h" - -#include "Async/Async.h" -#include "SpatialGDKSettings.h" - -DEFINE_LOG_CATEGORY(LogSpatialWorkerConnection); - -using namespace SpatialGDK; - -void ULegacySpatialWorkerConnection::SetConnection(Worker_Connection* WorkerConnectionIn) -{ - WorkerConnection = WorkerConnectionIn; - - CacheWorkerAttributes(); - - const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); - if (!SpatialGDKSettings->bRunSpatialWorkerConnectionOnGameThread) - { - if (OpsProcessingThread == nullptr) - { - bool bCanWake = SpatialGDKSettings->bWorkerFlushAfterOutgoingNetworkOp; - float WaitTimeS = 1.0f / (GetDefault()->OpsUpdateRate); - int32 WaitTimeMs = static_cast(FTimespan::FromSeconds(WaitTimeS).GetTotalMilliseconds()); - if (WaitTimeMs <= 0) - { - UE_LOG(LogSpatialWorkerConnection, Warning, TEXT("Clamping wait time for worker ops thread to the minimum rate of 1ms.")); - WaitTimeMs = 1; - } - ThreadWaitCondition.Emplace(bCanWake, WaitTimeMs); - - InitializeOpsProcessingThread(); - } - } -} - -void ULegacySpatialWorkerConnection::FinishDestroy() -{ - UE_LOG(LogSpatialWorkerConnection, Log, TEXT("Destroying SpatialWorkerconnection.")); - - DestroyConnection(); - - Super::FinishDestroy(); -} - -void ULegacySpatialWorkerConnection::DestroyConnection() -{ - Stop(); // Stop OpsProcessingThread - if (OpsProcessingThread != nullptr) - { - OpsProcessingThread->WaitForCompletion(); - OpsProcessingThread = nullptr; - } - - ThreadWaitCondition.Reset(); // Set TOptional value to null - - if (WorkerConnection) - { - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [WorkerConnection = WorkerConnection] - { - Worker_Connection_Destroy(WorkerConnection); - }); - - WorkerConnection = nullptr; - } - - NextRequestId = 0; - KeepRunning.AtomicSet(true); -} - -TArray ULegacySpatialWorkerConnection::GetOpList() -{ - const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); - if (SpatialGDKSettings->bRunSpatialWorkerConnectionOnGameThread) - { - QueueLatestOpList(); - } - - TArray OpLists; - while (!OpListQueue.IsEmpty()) - { - OpList OutOpList; - OpListQueue.Dequeue(OutOpList); - OpLists.Add(MoveTemp(OutOpList)); - } - - return OpLists; -} - -Worker_RequestId ULegacySpatialWorkerConnection::SendReserveEntityIdsRequest(uint32_t NumOfEntities) -{ - QueueOutgoingMessage(NumOfEntities); - return NextRequestId++; -} - -Worker_RequestId ULegacySpatialWorkerConnection::SendCreateEntityRequest(TArray Components, const Worker_EntityId* EntityId) -{ - QueueOutgoingMessage(MoveTemp(Components), EntityId); - return NextRequestId++; -} - -Worker_RequestId ULegacySpatialWorkerConnection::SendDeleteEntityRequest(Worker_EntityId EntityId) -{ - QueueOutgoingMessage(EntityId); - return NextRequestId++; -} - -void ULegacySpatialWorkerConnection::SendAddComponent(Worker_EntityId EntityId, FWorkerComponentData* ComponentData) -{ - QueueOutgoingMessage(EntityId, *ComponentData); -} - -void ULegacySpatialWorkerConnection::SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) -{ - QueueOutgoingMessage(EntityId, ComponentId); -} - -void ULegacySpatialWorkerConnection::SendComponentUpdate(Worker_EntityId EntityId, FWorkerComponentUpdate* ComponentUpdate) -{ - QueueOutgoingMessage(EntityId, *ComponentUpdate); -} - -Worker_RequestId ULegacySpatialWorkerConnection::SendCommandRequest(Worker_EntityId EntityId, Worker_CommandRequest* Request, uint32_t CommandId) -{ - QueueOutgoingMessage(EntityId, *Request, CommandId); - return NextRequestId++; -} - -void ULegacySpatialWorkerConnection::SendCommandResponse(Worker_RequestId RequestId, Worker_CommandResponse* Response) -{ - QueueOutgoingMessage(RequestId, *Response); -} - -void ULegacySpatialWorkerConnection::SendCommandFailure(Worker_RequestId RequestId, const FString& Message) -{ - QueueOutgoingMessage(RequestId, Message); -} - -void ULegacySpatialWorkerConnection::SendLogMessage(const uint8_t Level, const FName& LoggerName, const TCHAR* Message) -{ - QueueOutgoingMessage(Level, LoggerName, Message); -} - -void ULegacySpatialWorkerConnection::SendComponentInterest(Worker_EntityId EntityId, TArray&& ComponentInterest) -{ - QueueOutgoingMessage(EntityId, MoveTemp(ComponentInterest)); -} - -Worker_RequestId ULegacySpatialWorkerConnection::SendEntityQueryRequest(const Worker_EntityQuery* EntityQuery) -{ - QueueOutgoingMessage(*EntityQuery); - return NextRequestId++; -} - -void ULegacySpatialWorkerConnection::SendMetrics(SpatialMetrics Metrics) -{ - QueueOutgoingMessage(MoveTemp(Metrics)); -} - -PhysicalWorkerName ULegacySpatialWorkerConnection::GetWorkerId() const -{ - return PhysicalWorkerName(UTF8_TO_TCHAR(Worker_Connection_GetWorkerId(WorkerConnection))); -} - -const TArray& ULegacySpatialWorkerConnection::GetWorkerAttributes() const -{ - return CachedWorkerAttributes; -} - -void ULegacySpatialWorkerConnection::CacheWorkerAttributes() -{ - const Worker_WorkerAttributes* Attributes = Worker_Connection_GetWorkerAttributes(WorkerConnection); - - CachedWorkerAttributes.Empty(); - - if (Attributes->attributes == nullptr) - { - return; - } - - for (uint32 Index = 0; Index < Attributes->attribute_count; ++Index) - { - CachedWorkerAttributes.Add(UTF8_TO_TCHAR(Attributes->attributes[Index])); - } -} - -uint32 ULegacySpatialWorkerConnection::Run() -{ - const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); - check(!SpatialGDKSettings->bRunSpatialWorkerConnectionOnGameThread); - - while (KeepRunning) - { - ThreadWaitCondition->Wait(); - QueueLatestOpList(); - ProcessOutgoingMessages(); - } - - return 0; -} - -void ULegacySpatialWorkerConnection::Stop() -{ - KeepRunning.AtomicSet(false); -} - -void ULegacySpatialWorkerConnection::InitializeOpsProcessingThread() -{ - check(IsInGameThread()); - - OpsProcessingThread = FRunnableThread::Create(this, TEXT("SpatialWorkerConnectionWorker"), 0); - check(OpsProcessingThread); -} - -void ULegacySpatialWorkerConnection::QueueLatestOpList() -{ - OpList Ops = GetOpListFromConnection(WorkerConnection); - - if (Ops.Count > 0) - { - OpListQueue.Enqueue(MoveTemp(Ops)); - } -} - -void ULegacySpatialWorkerConnection::ProcessOutgoingMessages() -{ - bool bSentData = false; - while (!OutgoingMessagesQueue.IsEmpty()) - { - bSentData = true; - - TUniquePtr OutgoingMessage; - OutgoingMessagesQueue.Dequeue(OutgoingMessage); - - OnDequeueMessage.Broadcast(OutgoingMessage.Get()); - - static const Worker_UpdateParameters DisableLoopback{ /*loopback*/ WORKER_COMPONENT_UPDATE_LOOPBACK_NONE }; - - switch (OutgoingMessage->Type) - { - case EOutgoingMessageType::ReserveEntityIdsRequest: - { - FReserveEntityIdsRequest* Message = static_cast(OutgoingMessage.Get()); - - Worker_Connection_SendReserveEntityIdsRequest(WorkerConnection, - Message->NumOfEntities, - nullptr); - break; - } - case EOutgoingMessageType::CreateEntityRequest: - { - 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, - ComponentCount, - ComponentData, - Message->EntityId.IsSet() ? &(Message->EntityId.GetValue()) : nullptr, - nullptr); - break; - } - case EOutgoingMessageType::DeleteEntityRequest: - { - FDeleteEntityRequest* Message = static_cast(OutgoingMessage.Get()); - - Worker_Connection_SendDeleteEntityRequest(WorkerConnection, - Message->EntityId, - nullptr); - break; - } - case EOutgoingMessageType::AddComponent: - { - FAddComponent* Message = static_cast(OutgoingMessage.Get()); - - Worker_Connection_SendAddComponent(WorkerConnection, - Message->EntityId, - &Message->Data, - &DisableLoopback); - break; - } - case EOutgoingMessageType::RemoveComponent: - { - FRemoveComponent* Message = static_cast(OutgoingMessage.Get()); - - Worker_Connection_SendRemoveComponent(WorkerConnection, - Message->EntityId, - Message->ComponentId, - &DisableLoopback); - break; - } - case EOutgoingMessageType::ComponentUpdate: - { - FComponentUpdate* Message = static_cast(OutgoingMessage.Get()); - - Worker_Connection_SendComponentUpdate(WorkerConnection, - Message->EntityId, - &Message->Update, - &DisableLoopback); - - break; - } - case EOutgoingMessageType::CommandRequest: - { - FCommandRequest* Message = static_cast(OutgoingMessage.Get()); - - static const Worker_CommandParameters DefaultCommandParams{}; - Worker_Connection_SendCommandRequest(WorkerConnection, - Message->EntityId, - &Message->Request, - nullptr, - &DefaultCommandParams); - break; - } - case EOutgoingMessageType::CommandResponse: - { - FCommandResponse* Message = static_cast(OutgoingMessage.Get()); - - Worker_Connection_SendCommandResponse(WorkerConnection, - Message->RequestId, - &Message->Response); - break; - } - case EOutgoingMessageType::CommandFailure: - { - FCommandFailure* Message = static_cast(OutgoingMessage.Get()); - - Worker_Connection_SendCommandFailure(WorkerConnection, - Message->RequestId, - TCHAR_TO_UTF8(*Message->Message)); - break; - } - case EOutgoingMessageType::LogMessage: - { - FLogMessage* Message = static_cast(OutgoingMessage.Get()); - - FTCHARToUTF8 LoggerName(*Message->LoggerName.ToString()); - FTCHARToUTF8 LogString(*Message->Message); - - Worker_LogMessage LogMessage{}; - LogMessage.level = Message->Level; - LogMessage.logger_name = LoggerName.Get(); - LogMessage.message = LogString.Get(); - Worker_Connection_SendLogMessage(WorkerConnection, &LogMessage); - break; - } - case EOutgoingMessageType::ComponentInterest: - { - FComponentInterest* Message = static_cast(OutgoingMessage.Get()); - - Worker_Connection_SendComponentInterest(WorkerConnection, - Message->EntityId, - Message->Interests.GetData(), - Message->Interests.Num()); - break; - } - case EOutgoingMessageType::EntityQueryRequest: - { - FEntityQueryRequest* Message = static_cast(OutgoingMessage.Get()); - - Worker_Connection_SendEntityQueryRequest(WorkerConnection, - &Message->EntityQuery, - nullptr); - break; - } - case EOutgoingMessageType::Metrics: - { - FMetrics* Message = static_cast(OutgoingMessage.Get()); - - Message->Metrics.SendToConnection(WorkerConnection); - break; - } - default: - { - checkNoEntry(); - break; - } - } - } - - // Flush worker API calls - if (bSentData) - { - Worker_Connection_Alpha_Flush(WorkerConnection); - } -} - -void ULegacySpatialWorkerConnection::MaybeFlush() -{ - const USpatialGDKSettings* Settings = GetDefault(); - if (Settings->bWorkerFlushAfterOutgoingNetworkOp) - { - Flush(); - } -} - -void ULegacySpatialWorkerConnection::Flush() -{ - const USpatialGDKSettings* Settings = GetDefault(); - if (Settings->bRunSpatialWorkerConnectionOnGameThread) - { - ProcessOutgoingMessages(); - } - else if (ensure(ThreadWaitCondition.IsSet())) - { - ThreadWaitCondition->Wake(); // No-op if wake is not enabled. - } -} - -template -void ULegacySpatialWorkerConnection::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. - auto Message = MakeUnique(Forward(Args)...); - OnEnqueueMessage.Broadcast(Message.Get()); - OutgoingMessagesQueue.Enqueue(MoveTemp(Message)); -} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/OutgoingMessages.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/OutgoingMessages.cpp index fb0b35eb6c..2023fcfaff 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/OutgoingMessages.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/OutgoingMessages.cpp @@ -4,14 +4,14 @@ namespace SpatialGDK { - void FEntityQueryRequest::TraverseConstraint(Worker_Constraint* Constraint) { switch (Constraint->constraint_type) { case WORKER_CONSTRAINT_TYPE_AND: { - TUniquePtr NewConstraints = MakeUnique(Constraint->constraint.and_constraint.constraint_count); + TUniquePtr NewConstraints = + MakeUnique(Constraint->constraint.and_constraint.constraint_count); for (unsigned int i = 0; i < Constraint->constraint.and_constraint.constraint_count; i++) { @@ -25,7 +25,8 @@ void FEntityQueryRequest::TraverseConstraint(Worker_Constraint* Constraint) } case WORKER_CONSTRAINT_TYPE_OR: { - TUniquePtr NewConstraints = MakeUnique(Constraint->constraint.or_constraint.constraint_count); + TUniquePtr NewConstraints = + MakeUnique(Constraint->constraint.or_constraint.constraint_count); for (unsigned int i = 0; i < Constraint->constraint.or_constraint.constraint_count; i++) { diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp index fdcb1a94c9..926f00ed27 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp @@ -2,10 +2,9 @@ #include "Interop/Connection/SpatialConnectionManager.h" -#include "SpatialGDKSettings.h" -#include "Interop/Connection/LegacySpatialWorkerConnection.h" #include "Interop/Connection/SpatialWorkerConnection.h" -#include "Interop/Connection/SpatialViewWorkerConnection.h" +#include "SpatialConstants.h" +#include "SpatialGDKSettings.h" #include "Utils/ErrorCodeRemapping.h" #include "Async/Async.h" @@ -57,43 +56,43 @@ struct ConfigureConnection Params.network.connection_type = Config.LinkProtocol; Params.network.use_external_ip = Config.UseExternalIp; - Params.network.modular_tcp.multiplex_level = Config.TcpMultiplexLevel; + Params.network.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; + Params.network.tcp.downstream_tcp.flush_delay_millis = 0; + Params.network.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.kcp.upstream_compression = nullptr; + Params.network.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; + Params.network.kcp.upstream_kcp.flush_interval_millis = Config.UdpUpstreamIntervalMS; + Params.network.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; + Params.network.tcp.downstream_heartbeat = &HeartbeatParams; + Params.network.tcp.upstream_heartbeat = &HeartbeatParams; + Params.network.kcp.downstream_heartbeat = &HeartbeatParams; + Params.network.kcp.upstream_heartbeat = &HeartbeatParams; #endif // Use insecure connections default. - Params.network.modular_kcp.security_type = WORKER_NETWORK_SECURITY_TYPE_INSECURE; - Params.network.modular_tcp.security_type = WORKER_NETWORK_SECURITY_TYPE_INSECURE; + Params.network.kcp.security_type = WORKER_NETWORK_SECURITY_TYPE_INSECURE; + Params.network.tcp.security_type = WORKER_NETWORK_SECURITY_TYPE_INSECURE; // Override the security type to be secure only if the user has requested it and we are not using an editor build. - if ((!bConnectAsClient && GetDefault()->bUseSecureServerConnection) || (bConnectAsClient && GetDefault()->bUseSecureClientConnection)) + if ((!bConnectAsClient && GetDefault()->bUseSecureServerConnection) + || (bConnectAsClient && GetDefault()->bUseSecureClientConnection)) { #if WITH_EDITOR - UE_LOG(LogSpatialWorkerConnection, Warning, TEXT("Secure connection requested but this is not supported in Editor builds. Connection will be insecure.")); + UE_LOG(LogSpatialWorkerConnection, Warning, + TEXT("Secure connection requested but this is not supported in Editor builds. Connection will be insecure.")); #else - Params.network.modular_kcp.security_type = WORKER_NETWORK_SECURITY_TYPE_TLS; - Params.network.modular_tcp.security_type = WORKER_NETWORK_SECURITY_TYPE_TLS; + Params.network.kcp.security_type = WORKER_NETWORK_SECURITY_TYPE_TLS; + Params.network.tcp.security_type = WORKER_NETWORK_SECURITY_TYPE_TLS; #endif } - - Params.enable_dynamic_components = true; } FString FormatWorkerSDKLogFilePrefix() const @@ -133,8 +132,7 @@ void USpatialConnectionManager::DestroyConnection() { if (WorkerLocator) { - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [WorkerLocator = WorkerLocator] - { + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [WorkerLocator = WorkerLocator] { Worker_Locator_Destroy(WorkerLocator); }); @@ -155,17 +153,16 @@ void USpatialConnectionManager::Connect(bool bInitAsClient, uint32 PlayInEditorI if (bIsConnected) { check(bInitAsClient == bConnectAsClient); - AsyncTask(ENamedThreads::GameThread, [WeakThis = TWeakObjectPtr(this)] + AsyncTask(ENamedThreads::GameThread, [WeakThis = TWeakObjectPtr(this)] { + if (WeakThis.IsValid()) { - if (WeakThis.IsValid()) - { - WeakThis->OnConnectionSuccess(); - } - else - { - UE_LOG(LogSpatialConnectionManager, Error, TEXT("SpatialConnectionManager is not valid but was already connected.")); - } - }); + WeakThis->OnConnectionSuccess(); + } + else + { + UE_LOG(LogSpatialConnectionManager, Error, TEXT("SpatialConnectionManager is not valid but was already connected.")); + } + }); return; } @@ -202,17 +199,19 @@ void USpatialConnectionManager::Connect(bool bInitAsClient, uint32 PlayInEditorI } } -void USpatialConnectionManager::OnLoginTokens(void* UserData, const Worker_Alpha_LoginTokensResponse* LoginTokens) +void USpatialConnectionManager::OnLoginTokens(void* UserData, const Worker_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)); + 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?")); + 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; } @@ -221,7 +220,7 @@ void USpatialConnectionManager::OnLoginTokens(void* UserData, const Worker_Alpha ConnectionManager->ProcessLoginTokensResponse(LoginTokens); } -void USpatialConnectionManager::ProcessLoginTokensResponse(const Worker_Alpha_LoginTokensResponse* LoginTokens) +void USpatialConnectionManager::ProcessLoginTokensResponse(const Worker_LoginTokensResponse* LoginTokens) { // If LoginTokenResCallback is callable and returns true, return early. if (LoginTokenResCallback && LoginTokenResCallback(LoginTokens)) @@ -230,7 +229,8 @@ void USpatialConnectionManager::ProcessLoginTokensResponse(const Worker_Alpha_Lo } 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 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); @@ -253,7 +253,10 @@ void USpatialConnectionManager::ProcessLoginTokensResponse(const Worker_Alpha_Lo if (!bFoundDeployment) { - OnConnectionFailure(WORKER_CONNECTION_STATUS_CODE_NETWORK_ERROR, FString::Printf(TEXT("Deployment not found! Make sure that the deployment with name '%s' is running and has the 'dev_login' deployment tag."), *DeploymentToConnect)); + OnConnectionFailure(WORKER_CONNECTION_STATUS_CODE_NETWORK_ERROR, + FString::Printf(TEXT("Deployment not found! Make sure that the deployment with name '%s' is running and " + "has the 'dev_login' deployment tag."), + *DeploymentToConnect)); return; } } @@ -262,26 +265,39 @@ void USpatialConnectionManager::ProcessLoginTokensResponse(const Worker_Alpha_Lo ConnectToLocator(&DevAuthConfig); } +void USpatialConnectionManager::SetComponentSets(const TMap& InComponentSetMap) +{ + for (const auto& ComponentSet : InComponentSetMap) + { + for (const auto& Component : ComponentSet.Value.ComponentIDs) + { + ComponentSetData.ComponentSets.FindOrAdd(ComponentSet.Key).Add(Component); + } + } +} + void USpatialConnectionManager::RequestDeploymentLoginTokens() { - Worker_Alpha_LoginTokensRequest LTParams{}; + Worker_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)) + if (Worker_LoginTokensResponseFuture* LTFuture = + Worker_CreateDevelopmentLoginTokensAsync(TCHAR_TO_UTF8(*DevAuthConfig.LocatorHost), DevAuthConfig.LocatorPort, <Params)) { - Worker_Alpha_LoginTokensResponseFuture_Get(LTFuture, nullptr, this, &USpatialConnectionManager::OnLoginTokens); + Worker_LoginTokensResponseFuture_Get(LTFuture, nullptr, this, &USpatialConnectionManager::OnLoginTokens); } } -void USpatialConnectionManager::OnPlayerIdentityToken(void* UserData, const Worker_Alpha_PlayerIdentityTokenResponse* PIToken) +void USpatialConnectionManager::OnPlayerIdentityToken(void* UserData, const Worker_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)); + UE_LOG(LogSpatialWorkerConnection, Error, TEXT("Failed to get PlayerIdentityToken, StatusCode: %d, Error: %s"), + PIToken->status.code, UTF8_TO_TCHAR(PIToken->status.detail)); return; } @@ -299,16 +315,17 @@ void USpatialConnectionManager::StartDevelopmentAuth(const FString& DevAuthToken FTCHARToUTF8 DisplayName(*DevAuthConfig.DisplayName); FTCHARToUTF8 MetaData(*DevAuthConfig.MetaData); - Worker_Alpha_PlayerIdentityTokenRequest PITParams{}; + Worker_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)) + if (Worker_PlayerIdentityTokenResponseFuture* PITFuture = Worker_CreateDevelopmentPlayerIdentityTokenAsync( + TCHAR_TO_UTF8(*DevAuthConfig.LocatorHost), DevAuthConfig.LocatorPort, &PITParams)) { - Worker_Alpha_PlayerIdentityTokenResponseFuture_Get(PITFuture, nullptr, this, &USpatialConnectionManager::OnPlayerIdentityToken); + Worker_PlayerIdentityTokenResponseFuture_Get(PITFuture, nullptr, this, &USpatialConnectionManager::OnPlayerIdentityToken); } } @@ -318,11 +335,14 @@ void USpatialConnectionManager::ConnectToReceptionist(uint32 PlayInEditorID) ConfigureConnection ConnectionConfig(ReceptionistConfig, bConnectAsClient); - Worker_ConnectionFuture* ConnectionFuture = Worker_ConnectAsync( - TCHAR_TO_UTF8(*ReceptionistConfig.GetReceptionistHost()), ReceptionistConfig.GetReceptionistPort(), - TCHAR_TO_UTF8(*ReceptionistConfig.WorkerId), &ConnectionConfig.Params); + TSharedPtr EventTracer = CreateEventTracer(ReceptionistConfig.WorkerId); + ConnectionConfig.Params.event_tracer = EventTracer != nullptr ? EventTracer->GetWorkerEventTracer() : nullptr; - FinishConnecting(ConnectionFuture); + Worker_ConnectionFuture* ConnectionFuture = + Worker_ConnectAsync(TCHAR_TO_UTF8(*ReceptionistConfig.GetReceptionistHost()), ReceptionistConfig.GetReceptionistPort(), + TCHAR_TO_UTF8(*ReceptionistConfig.WorkerId), &ConnectionConfig.Params); + + FinishConnecting(ConnectionFuture, MoveTemp(EventTracer)); } void USpatialConnectionManager::ConnectToLocator(FLocatorConfig* InLocatorConfig) @@ -337,36 +357,36 @@ void USpatialConnectionManager::ConnectToLocator(FLocatorConfig* InLocatorConfig ConfigureConnection ConnectionConfig(*InLocatorConfig, bConnectAsClient); + TSharedPtr EventTracer = CreateEventTracer(InLocatorConfig->WorkerId); + ConnectionConfig.Params.event_tracer = EventTracer != nullptr ? EventTracer->GetWorkerEventTracer() : nullptr; + 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); + WorkerLocator = Worker_Locator_Create(TCHAR_TO_UTF8(*InLocatorConfig->LocatorHost), InLocatorConfig->LocatorPort, &LocatorParams); Worker_ConnectionFuture* ConnectionFuture = Worker_Locator_ConnectAsync(WorkerLocator, &ConnectionConfig.Params); - FinishConnecting(ConnectionFuture); + FinishConnecting(ConnectionFuture, MoveTemp(EventTracer)); } -void USpatialConnectionManager::FinishConnecting(Worker_ConnectionFuture* ConnectionFuture) +void USpatialConnectionManager::FinishConnecting(Worker_ConnectionFuture* ConnectionFuture, + TSharedPtr NewEventTracer) { TWeakObjectPtr WeakSpatialConnectionManager(this); - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [ConnectionFuture, WeakSpatialConnectionManager] - { + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [ConnectionFuture, WeakSpatialConnectionManager, + EventTracing = MoveTemp(NewEventTracer)]() mutable { Worker_Connection* NewCAPIWorkerConnection = Worker_ConnectionFuture_Get(ConnectionFuture, nullptr); Worker_ConnectionFuture_Destroy(ConnectionFuture); - AsyncTask(ENamedThreads::GameThread, [WeakSpatialConnectionManager, NewCAPIWorkerConnection] - { + AsyncTask(ENamedThreads::GameThread, [WeakSpatialConnectionManager, NewCAPIWorkerConnection, + EventTracing = MoveTemp(EventTracing)]() mutable { if (!WeakSpatialConnectionManager.IsValid()) { // The game instance was destroyed before the connection finished, so just clean up the connection. @@ -376,26 +396,21 @@ void USpatialConnectionManager::FinishConnecting(Worker_ConnectionFuture* Connec USpatialConnectionManager* SpatialConnectionManager = WeakSpatialConnectionManager.Get(); - if (Worker_Connection_IsConnected(NewCAPIWorkerConnection)) + const uint8_t ConnectionStatusCode = Worker_Connection_GetConnectionStatusCode(NewCAPIWorkerConnection); + + if (ConnectionStatusCode == WORKER_CONNECTION_STATUS_CODE_SUCCESS) { const USpatialGDKSettings* Settings = GetDefault(); - if (Settings->bUseSpatialView) - { - SpatialConnectionManager->WorkerConnection = NewObject(); - } - else - { - SpatialConnectionManager->WorkerConnection = NewObject(); - } - SpatialConnectionManager->WorkerConnection->SetConnection(NewCAPIWorkerConnection); + SpatialConnectionManager->WorkerConnection = NewObject(); + + SpatialConnectionManager->WorkerConnection->SetConnection(NewCAPIWorkerConnection, MoveTemp(EventTracing), + SpatialConnectionManager->ComponentSetData); SpatialConnectionManager->OnConnectionSuccess(); } else { - const uint8_t ConnectionStatusCode = Worker_Connection_GetConnectionStatusCode(NewCAPIWorkerConnection); const FString ErrorMessage(UTF8_TO_TCHAR(Worker_Connection_GetConnectionStatusDetailString(NewCAPIWorkerConnection))); - - // TODO: Try to reconnect - UNR-576 + Worker_Connection_Destroy(NewCAPIWorkerConnection); SpatialConnectionManager->OnConnectionFailure(ConnectionStatusCode, ErrorMessage); } }); @@ -410,7 +425,7 @@ ESpatialConnectionType USpatialConnectionManager::GetConnectionType() const void USpatialConnectionManager::SetConnectionType(ESpatialConnectionType InConnectionType) { // The locator config may not have been initialized - check(!(InConnectionType == ESpatialConnectionType::Locator && LocatorConfig.LocatorHost.IsEmpty())) + check(!(InConnectionType == ESpatialConnectionType::Locator && LocatorConfig.LocatorHost.IsEmpty())); ConnectionType = InConnectionType; } @@ -437,8 +452,11 @@ bool USpatialConnectionManager::TrySetupConnectionConfigFromCommandLine(const FS { UE_LOG(LogSpatialWorkerConnection, Log, TEXT("Setting up receptionist config from command line arguments")); bSuccessfullyLoaded = ReceptionistConfig.TryLoadCommandLineArgs(); - SetConnectionType(ESpatialConnectionType::Receptionist); - ReceptionistConfig.WorkerType = SpatialWorkerType; + if (bSuccessfullyLoaded) + { + ReceptionistConfig.WorkerType = SpatialWorkerType; + SetConnectionType(ESpatialConnectionType::Receptionist); + } } } @@ -461,6 +479,12 @@ void USpatialConnectionManager::SetupConnectionConfigFromURL(const FURL& URL, co FParse::Value(FCommandLine::Get(), TEXT("locatorHost"), LocatorHostOverride); } + int32 LocatorPortOverride = SpatialConstants::LOCATOR_PORT; + if (const TCHAR* LocatorPortOption = URL.GetOption(TEXT("locatorPort="), nullptr)) + { + LocatorPortOverride = FCString::Atoi(LocatorPortOption); + } + if (URL.HasOption(TEXT("devauth"))) { // Use devauth login flow. @@ -469,6 +493,7 @@ void USpatialConnectionManager::SetupConnectionConfigFromURL(const FURL& URL, co { DevAuthConfig.LocatorHost = LocatorHostOverride; } + DevAuthConfig.LocatorPort = LocatorPortOverride; 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); @@ -484,6 +509,7 @@ void USpatialConnectionManager::SetupConnectionConfigFromURL(const FURL& URL, co { LocatorConfig.LocatorHost = LocatorHostOverride; } + LocatorConfig.LocatorPort = LocatorPortOverride; LocatorConfig.PlayerIdentityToken = URL.GetOption(*SpatialConstants::URL_PLAYER_IDENTITY_OPTION, TEXT("")); LocatorConfig.LoginToken = URL.GetOption(*SpatialConstants::URL_LOGIN_OPTION, TEXT("")); LocatorConfig.WorkerType = SpatialWorkerType; @@ -511,3 +537,14 @@ void USpatialConnectionManager::OnConnectionFailure(uint8_t ConnectionStatusCode OnFailedToConnectCallback.ExecuteIfBound(ConnectionStatusCode, ErrorMessage); } + +TSharedPtr USpatialConnectionManager::CreateEventTracer(const FString& WorkerId) +{ + const USpatialGDKSettings* Settings = GetDefault(); + if (Settings == nullptr || !Settings->bEventTracingEnabled) + { + return nullptr; + } + + return MakeShared(WorkerId); +}; diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialEventTracer.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialEventTracer.cpp new file mode 100644 index 0000000000..569bd78332 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialEventTracer.cpp @@ -0,0 +1,306 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Interop/Connection/SpatialEventTracer.h" + +#include +#include + +#include "HAL/PlatformFile.h" +#include "HAL/PlatformFilemanager.h" +#include "Interop/Connection/SpatialTraceEventBuilder.h" +#include "SpatialGDKSettings.h" + +DEFINE_LOG_CATEGORY(LogSpatialEventTracer); + +namespace SpatialGDK +{ +void SpatialEventTracer::TraceCallback(void* UserData, const Trace_Item* Item) +{ + SpatialEventTracer* EventTracer = static_cast(UserData); + + Io_Stream* Stream = EventTracer->Stream.Get(); + if (!ensure(Stream != nullptr)) + { + return; + } + + uint32_t ItemSize = Trace_GetSerializedItemSize(Item); + if (EventTracer->BytesWrittenToStream + ItemSize <= EventTracer->MaxFileSize) + { + EventTracer->BytesWrittenToStream += ItemSize; + int Code = Trace_SerializeItemToStream(Stream, Item, ItemSize); + if (Code != 1) + { + UE_LOG(LogSpatialEventTracer, Error, TEXT("Failed to serialize to with error code %d (%s"), Code, Trace_GetLastError()); + } + } + else + { + EventTracer->BytesWrittenToStream = EventTracer->MaxFileSize; + } +} + +SpatialScopedActiveSpanId::SpatialScopedActiveSpanId(SpatialEventTracer* InEventTracer, const FSpatialGDKSpanId& InCurrentSpanId) + : CurrentSpanId(InCurrentSpanId) + , EventTracer(nullptr) +{ + if (InEventTracer == nullptr) + { + return; + } + + EventTracer = InEventTracer->GetWorkerEventTracer(); + Trace_EventTracer_SetActiveSpanId(EventTracer, CurrentSpanId.GetConstId()); +} + +SpatialScopedActiveSpanId::~SpatialScopedActiveSpanId() +{ + if (EventTracer != nullptr) + { + Trace_EventTracer_ClearActiveSpanId(EventTracer); + } +} + +SpatialEventTracer::SpatialEventTracer(const FString& WorkerId) +{ + const USpatialGDKSettings* Settings = GetDefault(); + MaxFileSize = Settings->MaxEventTracingFileSizeBytes; + + Trace_EventTracer_Parameters parameters = {}; + parameters.user_data = this; + parameters.callback = &SpatialEventTracer::TraceCallback; + EventTracer = Trace_EventTracer_Create(¶meters); + + Trace_SamplingParameters SamplingParameters = {}; + SamplingParameters.sampling_mode = Trace_SamplingMode::TRACE_SAMPLING_MODE_PROBABILISTIC; + + TArray SpanSamplingProbabilities; + TArray AnsiStrings; // Worker requires platform ansi const char* + + for (const auto& Pair : Settings->EventSamplingModeOverrides) + { + int32 Index = AnsiStrings.Add((const char*)TCHAR_TO_ANSI(*Pair.Key.ToString())); + SpanSamplingProbabilities.Add({ AnsiStrings[Index].c_str(), Pair.Value }); + } + + SamplingParameters.probabilistic_parameters.default_probability = Settings->SamplingProbability; + SamplingParameters.probabilistic_parameters.probability_count = SpanSamplingProbabilities.Num(); + SamplingParameters.probabilistic_parameters.probabilities = SpanSamplingProbabilities.GetData(); + + Trace_EventTracer_SetSampler(EventTracer, &SamplingParameters); + + UE_LOG(LogSpatialEventTracer, Log, TEXT("Spatial event tracing enabled.")); + + // Open a local file + FString EventTracePath = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("EventTracing")); + FString AbsLogPath; + if (FParse::Value(FCommandLine::Get(), TEXT("eventLogPath="), AbsLogPath, false)) + { + EventTracePath = FPaths::GetPath(AbsLogPath); + } + + FolderPath = EventTracePath; + const FString FullFileName = FString::Printf(TEXT("EventTrace_%s_%s.trace"), *WorkerId, *FDateTime::Now().ToString()); + const FString FilePath = FPaths::Combine(FolderPath, FullFileName); + + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + if (PlatformFile.CreateDirectoryTree(*FolderPath)) + { + UE_LOG(LogSpatialEventTracer, Log, TEXT("Capturing trace to %s."), *FilePath); + Stream.Reset(Io_CreateFileStream(TCHAR_TO_ANSI(*FilePath), Io_OpenMode::IO_OPEN_MODE_WRITE)); + } +} + +SpatialEventTracer::~SpatialEventTracer() +{ + UE_LOG(LogSpatialEventTracer, Log, TEXT("Spatial event tracing disabled.")); + Trace_EventTracer_Destroy(EventTracer); +} + +FUserSpanId SpatialEventTracer::GDKSpanIdToUserSpanId(const FSpatialGDKSpanId& SpanId) +{ + FUserSpanId UserSpanId; + UserSpanId.Data.SetNum(TRACE_SPAN_ID_SIZE_BYTES); + FMemory::Memcpy(UserSpanId.Data.GetData(), SpanId.GetConstId(), TRACE_SPAN_ID_SIZE_BYTES); + return UserSpanId; +} + +FSpatialGDKSpanId SpatialEventTracer::UserSpanIdToGDKSpanId(const FUserSpanId& UserSpanId) +{ + if (!UserSpanId.IsValid()) + { + return {}; + } + + FSpatialGDKSpanId TraceSpanId; + FMemory::Memcpy(TraceSpanId.GetId(), UserSpanId.Data.GetData(), TRACE_SPAN_ID_SIZE_BYTES); + return TraceSpanId; +} + +FSpatialGDKSpanId SpatialEventTracer::TraceEvent(const FSpatialTraceEvent& SpatialTraceEvent, const Trace_SpanIdType* Causes /* = nullptr*/, + int32 NumCauses /* = 0*/) +{ + if (Causes == nullptr && NumCauses > 0) + { + return {}; + } + + // Worker requires ansi const char* + std::string MessageSrc = (const char*)TCHAR_TO_ANSI(*SpatialTraceEvent.Message); // Worker requires platform ansi const char* + std::string TypeSrc = (const char*)TCHAR_TO_ANSI(*SpatialTraceEvent.Type.ToString()); // Worker requires platform ansi const char* + + // We could add the data to this event if a custom sampling callback was used. + // This would allow for sampling dependent on trace event data. + Trace_Event Event = { nullptr, 0, MessageSrc.c_str(), TypeSrc.c_str(), nullptr }; + + Trace_SamplingResult SpanSamplingResult = Trace_EventTracer_ShouldSampleSpan(EventTracer, Causes, NumCauses, &Event); + if (SpanSamplingResult.decision == Trace_SamplingDecision::TRACE_SHOULD_NOT_SAMPLE) + { + return {}; + } + + FSpatialGDKSpanId TraceSpanId; + Trace_EventTracer_AddSpan(EventTracer, Causes, NumCauses, &Event, TraceSpanId.GetId()); + Event.span_id = TraceSpanId.GetConstId(); + + Trace_SamplingResult EventSamplingResult = Trace_EventTracer_ShouldSampleEvent(EventTracer, &Event); + switch (EventSamplingResult.decision) + { + case Trace_SamplingDecision::TRACE_SHOULD_NOT_SAMPLE: + { + return TraceSpanId; + } + case Trace_SamplingDecision::TRACE_SHOULD_SAMPLE_WITHOUT_DATA: + { + Trace_EventTracer_AddEvent(EventTracer, &Event); + return TraceSpanId; + } + case Trace_SamplingDecision::TRACE_SHOULD_SAMPLE: + { + Trace_EventData* EventData = Trace_EventData_Create(); + + for (const auto& Pair : SpatialTraceEvent.Data) + { + std::string KeySrc = (const char*)TCHAR_TO_ANSI(*Pair.Key); // Worker requires platform ansi const char* + const char* Key = KeySrc.c_str(); + std::string ValueSrc = (const char*)TCHAR_TO_ANSI(*Pair.Value); // Worker requires platform ansi const char* + const char* Value = ValueSrc.c_str(); + Trace_EventData_AddStringFields(EventData, 1, &Key, &Value); + } + + // Frame counter + { + const char* FrameCountStr = "FrameNum"; + char TmpBuffer[64]; + FCStringAnsi::Sprintf(TmpBuffer, "%" PRIu64, GFrameCounter); + const char* TmpBufferPtr = TmpBuffer; + Trace_EventData_AddStringFields(EventData, 1, &FrameCountStr, &TmpBufferPtr); + } + + Event.data = EventData; + Trace_EventTracer_AddEvent(EventTracer, &Event); + Trace_EventData_Destroy(EventData); + return TraceSpanId; + } + default: + { + UE_LOG(LogSpatialEventTracer, Log, TEXT("Could not handle invalid sampling decision %d."), + static_cast(EventSamplingResult.decision)); + return {}; + } + } +} + +void SpatialEventTracer::StreamDeleter::operator()(Io_Stream* StreamToDestroy) const +{ + Io_Stream_Destroy(StreamToDestroy); +} + +void SpatialEventTracer::AddComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, const FSpatialGDKSpanId& SpanId) +{ + EntityComponentSpanIds.FindOrAdd({ EntityId, ComponentId }, SpanId); +} + +void SpatialEventTracer::RemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) +{ + EntityComponentSpanIds.Remove({ EntityId, ComponentId }); +} + +void SpatialEventTracer::UpdateComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, const FSpatialGDKSpanId& SpanId) +{ + FSpatialGDKSpanId& StoredSpanId = EntityComponentSpanIds.FindOrAdd({ EntityId, ComponentId }); + FSpatialGDKSpanId CauseSpanIds[2] = { SpanId, StoredSpanId }; + StoredSpanId = TraceEvent(FSpatialTraceEventBuilder::CreateMergeComponentUpdate(EntityId, ComponentId), + reinterpret_cast(&CauseSpanIds), 2); +} + +FSpatialGDKSpanId SpatialEventTracer::GetSpanId(const EntityComponentId& Id) const +{ + const FSpatialGDKSpanId* SpanId = EntityComponentSpanIds.Find(Id); + if (SpanId == nullptr) + { + return {}; + } + + return *SpanId; +} + +void SpatialEventTracer::AddToStack(const FSpatialGDKSpanId& SpanId) +{ + SpanIdStack.Add(SpanId); +} + +FSpatialGDKSpanId SpatialEventTracer::PopFromStack() +{ + if (SpanIdStack.Num() == 0) + { + return {}; + } + return SpanIdStack.Pop(); +} + +FSpatialGDKSpanId SpatialEventTracer::GetFromStack() const +{ + const int32 Size = SpanIdStack.Num(); + if (Size == 0) + { + return {}; + } + return SpanIdStack[Size - 1]; +} + +bool SpatialEventTracer::IsStackEmpty() const +{ + return SpanIdStack.Num() == 0; +} + +void SpatialEventTracer::AddLatentPropertyUpdateSpanId(const TWeakObjectPtr& Object, const FSpatialGDKSpanId& SpanId) +{ + FSpatialGDKSpanId* ExistingSpanId = ObjectSpanIdStacks.Find(Object); + if (ExistingSpanId == nullptr) + { + ObjectSpanIdStacks.Add(Object, SpanId); + } + else + { + FSpatialGDKSpanId CauseSpanIds[2] = { SpanId, *ExistingSpanId }; + *ExistingSpanId = TraceEvent(FSpatialTraceEventBuilder::CreateObjectPropertyComponentUpdate(Object.Get()), + reinterpret_cast(&CauseSpanIds), 2); + } +} + +FSpatialGDKSpanId SpatialEventTracer::PopLatentPropertyUpdateSpanId(const TWeakObjectPtr& Object) +{ + FSpatialGDKSpanId* ExistingSpanId = ObjectSpanIdStacks.Find(Object); + if (ExistingSpanId == nullptr) + { + return {}; + } + + FSpatialGDKSpanId TempSpanId = *ExistingSpanId; + ObjectSpanIdStacks.Remove(Object); + + return TempSpanId; +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialEventTracerUserInterface.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialEventTracerUserInterface.cpp new file mode 100644 index 0000000000..efc1f837a3 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialEventTracerUserInterface.cpp @@ -0,0 +1,141 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Interop/Connection/SpatialEventTracerUserInterface.h" + +#include "Engine/Engine.h" +#include "EngineClasses/SpatialNetDriver.h" +#include "EngineClasses/SpatialPackageMapClient.h" +#include "Interop/Connection/SpatialEventTracer.h" +#include "Interop/Connection/SpatialWorkerConnection.h" +#include "Interop/SpatialClassInfoManager.h" + +DEFINE_LOG_CATEGORY(LogSpatialEventTracerUserInterface); + +FUserSpanId USpatialEventTracerUserInterface::TraceEvent(UObject* WorldContextObject, const FSpatialTraceEvent& SpatialTraceEvent) +{ + SpatialGDK::SpatialEventTracer* EventTracer = GetEventTracer(WorldContextObject); + if (EventTracer == nullptr) + { + return {}; + } + + FSpatialGDKSpanId SpanId = EventTracer->TraceEvent(SpatialTraceEvent, nullptr /*CauseSpanId*/, 0 /*NumCauses*/); + return SpatialGDK::SpatialEventTracer::GDKSpanIdToUserSpanId(SpanId); +} + +FUserSpanId USpatialEventTracerUserInterface::TraceEventWithCauses(UObject* WorldContextObject, const FSpatialTraceEvent& SpatialTraceEvent, + const TArray& Causes) +{ + SpatialGDK::SpatialEventTracer* EventTracer = GetEventTracer(WorldContextObject); + if (EventTracer == nullptr) + { + return {}; + } + + TArray CauseSpanIds; + for (const FUserSpanId& UserSpanIdCause : Causes) + { + if (!UserSpanIdCause.IsValid()) + { + UE_LOG(LogSpatialEventTracerUserInterface, Warning, + TEXT("USpatialEventTracerUserInterface::CreateSpanIdWithCauses - Invalid input cause")); + continue; + } + + FSpatialGDKSpanId CauseSpanId = SpatialGDK::SpatialEventTracer::UserSpanIdToGDKSpanId(UserSpanIdCause); + CauseSpanIds.Add(CauseSpanId); + } + + FSpatialGDKSpanId SpanId = EventTracer->TraceEvent(SpatialTraceEvent, CauseSpanIds.GetData()->GetId(), CauseSpanIds.Num()); + return SpatialGDK::SpatialEventTracer::GDKSpanIdToUserSpanId(SpanId); +} + +void USpatialEventTracerUserInterface::TraceRPC(UObject* WorldContextObject, FEventTracerRPCDelegate Delegate, + const FUserSpanId& UserSpanId) +{ + SpatialGDK::SpatialEventTracer* EventTracer = GetEventTracer(WorldContextObject); + if (EventTracer == nullptr) + { + Delegate.Execute(); + return; + } + + if (!UserSpanId.IsValid()) + { + Delegate.Execute(); + return; + } + + FSpatialGDKSpanId SpanId = SpatialGDK::SpatialEventTracer::UserSpanIdToGDKSpanId(UserSpanId); + EventTracer->AddToStack(SpanId); + Delegate.Execute(); + EventTracer->PopFromStack(); +} + +bool USpatialEventTracerUserInterface::GetActiveSpanId(UObject* WorldContextObject, FUserSpanId& OutUserSpanId) +{ + const SpatialGDK::SpatialEventTracer* EventTracer = GetEventTracer(WorldContextObject); + if (EventTracer == nullptr) + { + return false; + } + + if (EventTracer->IsStackEmpty()) + { + return false; + } + + FSpatialGDKSpanId SpanId = EventTracer->GetFromStack(); + OutUserSpanId = SpatialGDK::SpatialEventTracer::GDKSpanIdToUserSpanId(SpanId); + return true; +} + +void USpatialEventTracerUserInterface::TraceProperty(UObject* WorldContextObject, UObject* Object, const FUserSpanId& UserSpanId) +{ + SpatialGDK::SpatialEventTracer* EventTracer = GetEventTracer(WorldContextObject); + if (EventTracer == nullptr) + { + return; + } + + USpatialNetDriver* NetDriver = GetSpatialNetDriver(WorldContextObject); + if (NetDriver == nullptr) + { + return; + } + + if (!UserSpanId.IsValid()) + { + return; + } + + FSpatialGDKSpanId SpanId = SpatialGDK::SpatialEventTracer::UserSpanIdToGDKSpanId(UserSpanId); + EventTracer->AddLatentPropertyUpdateSpanId(Object, SpanId); +} + +SpatialGDK::SpatialEventTracer* USpatialEventTracerUserInterface::GetEventTracer(UObject* WorldContextObject) +{ + const USpatialNetDriver* NetDriver = GetSpatialNetDriver(WorldContextObject); + if (NetDriver == nullptr || NetDriver->Connection == nullptr) + { + UE_LOG(LogSpatialEventTracerUserInterface, Error, + TEXT("USpatialEventTracerUserInterface::GetEventTracer - NetDriver or Connection is null")); + return nullptr; + } + + return NetDriver->Connection->GetEventTracer(); +} + +USpatialNetDriver* USpatialEventTracerUserInterface::GetSpatialNetDriver(UObject* WorldContextObject) +{ + const UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull); + if (World == nullptr) + { + UE_LOG(LogSpatialEventTracerUserInterface, Error, + TEXT("USpatialEventTracerUserInterface::GetSpatialNetDriver - World is null, will use GWorld instead")); + World = GWorld; + } + + USpatialNetDriver* NetDriver = Cast(World->GetNetDriver()); + return NetDriver; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialGDKSpanId.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialGDKSpanId.cpp new file mode 100644 index 0000000000..9718d4542c --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialGDKSpanId.cpp @@ -0,0 +1,47 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Interop/Connection/SpatialGDKSpanId.h" + +static_assert(sizeof(FSpatialGDKSpanId) == TRACE_SPAN_ID_SIZE_BYTES, "Size must match TRACE_SPAN_ID_SIZE_BYTES"); + +// ----- FSpatialGDKSpanId ----- + +FSpatialGDKSpanId::FSpatialGDKSpanId() +{ + WriteId(Trace_SpanId_Null()); +} + +FSpatialGDKSpanId::FSpatialGDKSpanId(const Trace_SpanIdType* TraceSpanId) +{ + WriteId(TraceSpanId != nullptr ? TraceSpanId : Trace_SpanId_Null()); +} + +void FSpatialGDKSpanId::WriteId(const Trace_SpanIdType* TraceSpanId) +{ + FMemory::Memcpy(Id, TraceSpanId, TRACE_SPAN_ID_SIZE_BYTES); +} + +Trace_SpanIdType* FSpatialGDKSpanId::GetId() +{ + return Id; +} + +const Trace_SpanIdType* FSpatialGDKSpanId::GetConstId() const +{ + return Id; +} + +FString FSpatialGDKSpanId::ToString() const +{ + return ToString(Id); +} + +FString FSpatialGDKSpanId::ToString(const Trace_SpanIdType* TraceSpanId) +{ + FString HexStr; + for (int i = 0; i < TRACE_SPAN_ID_SIZE_BYTES; i++) + { + HexStr += FString::Printf(TEXT("%02x"), TraceSpanId[i]); + } + return HexStr; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialTraceEventBuilder.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialTraceEventBuilder.cpp new file mode 100644 index 0000000000..a96070ee40 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialTraceEventBuilder.cpp @@ -0,0 +1,354 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Interop/Connection/SpatialTraceEventBuilder.h" + +#include "EngineClasses/SpatialNetDriver.h" +#include "EngineClasses/SpatialPackageMapClient.h" +#include "Utils/SpatialActorUtils.h" + +namespace SpatialGDK +{ +FSpatialTraceEventBuilder::FSpatialTraceEventBuilder(FName InType) + : SpatialTraceEvent(MoveTemp(InType), "") +{ +} + +FSpatialTraceEventBuilder::FSpatialTraceEventBuilder(FName InType, FString InMessage) + : SpatialTraceEvent(MoveTemp(InType), MoveTemp(InMessage)) +{ +} + +FSpatialTraceEventBuilder FSpatialTraceEventBuilder::AddObject(FString Key, const UObject* Object) +{ + if (Object != nullptr) + { + if (const AActor* Actor = Cast(Object)) + { + AddKeyValue(Key + TEXT("ActorPosition"), Actor->GetTransform().GetTranslation().ToString()); + } + if (UWorld* World = Object->GetWorld()) + { + if (USpatialNetDriver* NetDriver = Cast(World->GetNetDriver())) + { + AddKeyValue(Key + TEXT("NetGuid"), NetDriver->PackageMap->GetNetGUIDFromObject(Object).ToString()); + } + } + AddKeyValue(MoveTemp(Key), Object->GetName()); + } + return *this; +} + +FSpatialTraceEventBuilder FSpatialTraceEventBuilder::AddFunction(FString Key, const UFunction* Function) +{ + if (Function != nullptr) + { + AddKeyValue(MoveTemp(Key), Function->GetName()); + } + return *this; +} + +FSpatialTraceEventBuilder FSpatialTraceEventBuilder::AddEntityId(FString Key, const Worker_EntityId EntityId) +{ + AddKeyValue(MoveTemp(Key), FString::FromInt(EntityId)); + return *this; +} + +FSpatialTraceEventBuilder FSpatialTraceEventBuilder::AddComponentId(FString Key, const Worker_ComponentId ComponentId) +{ + AddKeyValue(MoveTemp(Key), FString::FromInt(ComponentId)); + return *this; +} + +FSpatialTraceEventBuilder FSpatialTraceEventBuilder::AddFieldId(FString Key, const uint32 FieldId) +{ + AddKeyValue(MoveTemp(Key), FString::FromInt(FieldId)); + return *this; +} + +FSpatialTraceEventBuilder FSpatialTraceEventBuilder::AddNewWorkerId(FString Key, const uint32 NewWorkerId) +{ + AddKeyValue(MoveTemp(Key), FString::FromInt(NewWorkerId)); + return *this; +} + +FSpatialTraceEventBuilder FSpatialTraceEventBuilder::AddCommand(FString Key, const FString& Command) +{ + AddKeyValue(MoveTemp(Key), Command); + return *this; +} + +FSpatialTraceEventBuilder FSpatialTraceEventBuilder::AddRequestId(FString Key, const int64 RequestId) +{ + AddKeyValue(MoveTemp(Key), FString::FromInt(RequestId)); + return *this; +} + +FSpatialTraceEventBuilder FSpatialTraceEventBuilder::AddAuthority(FString Key, const Worker_Authority Authority) +{ + AddKeyValue(MoveTemp(Key), AuthorityToString(Authority)); + return *this; +} + +FSpatialTraceEventBuilder FSpatialTraceEventBuilder::AddKeyValue(FString Key, FString Value) +{ + SpatialTraceEvent.AddData(MoveTemp(Key), MoveTemp(Value)); + return *this; +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::GetEvent() && +{ + return MoveTemp(SpatialTraceEvent); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateProcessRPC(const UObject* Object, UFunction* Function, + const EventTraceUniqueId& LinearTraceId) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "process_rpc") + .AddObject(TEXT("Object"), Object) + .AddFunction(TEXT("Function"), Function) + .AddKeyValue(TEXT("LinearTraceId"), LinearTraceId.ToString()) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreatePushRPC(const UObject* Object, UFunction* Function) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "push_rpc") + .AddObject(TEXT("Object"), Object) + .AddFunction(TEXT("Function"), Function) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateSendRPC(const EventTraceUniqueId& LinearTraceId) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "send_rpc") + .AddKeyValue(TEXT("LinearTraceId"), LinearTraceId.ToString()) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateQueueRPC() +{ + return FSpatialTraceEventBuilder("queue_rpc").GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateRetryRPC() +{ + return FSpatialTraceEventBuilder("retry_rpc").GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreatePropertyChanged(const UObject* Object, const Worker_EntityId EntityId, + const FString& PropertyName, EventTraceUniqueId LinearTraceId) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "property_changed") + .AddObject(TEXT("Object"), Object) + .AddEntityId(TEXT("EntityId"), EntityId) + .AddKeyValue(TEXT("PropertyName"), PropertyName) + .AddKeyValue(TEXT("LinearTraceId"), LinearTraceId.ToString()) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateSendPropertyUpdate(const UObject* Object, const Worker_EntityId EntityId, + const Worker_ComponentId ComponentId) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "send_property_update") + .AddObject(TEXT("Object"), Object) + .AddEntityId(TEXT("EntityId"), EntityId) + .AddEntityId(TEXT("ComponentId"), ComponentId) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateReceivePropertyUpdate(const UObject* Object, const Worker_EntityId EntityId, + const Worker_ComponentId ComponentId, const FString& PropertyName, + EventTraceUniqueId LinearTraceId) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "receive_property_update") + .AddObject(TEXT("Object"), Object) + .AddEntityId(TEXT("EntityId"), EntityId) + .AddComponentId(TEXT("ComponentId"), ComponentId) + .AddKeyValue("PropertyName", PropertyName) + .AddKeyValue(TEXT("LinearTraceId"), LinearTraceId.ToString()) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateMergeSendRPCs(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "merge_send_rpcs") + .AddEntityId(TEXT("EntityId"), EntityId) + .AddComponentId(TEXT("ComponentId"), ComponentId) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateMergeComponentUpdate(const Worker_EntityId EntityId, + const Worker_ComponentId ComponentId) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "merge_component_update") + .AddEntityId(TEXT("EntityId"), EntityId) + .AddComponentId(TEXT("ComponentId"), ComponentId) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateObjectPropertyComponentUpdate(const UObject* Object) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "merge_property_update").AddObject(TEXT("Object"), Object).GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateSendCommandRequest(const FString& Command, const int64 RequestId) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "send_command_request") + .AddCommand(TEXT("Command"), Command) + .AddRequestId(TEXT("RequestId"), RequestId) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateReceiveCommandRequest(const FString& Command, const int64 RequestId) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "receive_command_request") + .AddCommand(TEXT("Command"), Command) + .AddRequestId(TEXT("RequestId"), RequestId) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateReceiveCommandRequest(const FString& Command, const UObject* Actor, + const UObject* TargetObject, const UFunction* Function, + const int32 TraceId, const int64 RequestId) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "receive_command_request") + .AddCommand(TEXT("Command"), Command) + .AddObject(TEXT("Object"), Actor) + .AddObject(TEXT("TargetObject"), TargetObject) + .AddFunction(TEXT("Function"), Function) + .AddKeyValue(TEXT("TraceId"), FString::FromInt(TraceId)) + .AddRequestId(TEXT("RequestId"), RequestId) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateSendCommandResponse(const int64 RequestId, const bool bSuccess) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "send_command_response") + .AddRequestId(TEXT("RequestId"), RequestId) + .AddKeyValue(TEXT("Success"), BoolToString(bSuccess)) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateReceiveCommandResponse(const FString& Command, const int64 RequestId) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "receive_command_response") + .AddCommand(TEXT("Command"), Command) + .AddRequestId(TEXT("RequestId"), RequestId) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateReceiveCommandResponse(const UObject* Actor, const int64 RequestId, const bool bSuccess) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "receive_command_response") + .AddObject(TEXT("Object"), Actor) + .AddRequestId(TEXT("RequestId"), RequestId) + .AddKeyValue(TEXT("Success"), BoolToString(bSuccess)) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateReceiveCommandResponse(const UObject* Actor, const UObject* TargetObject, + const UFunction* Function, int64 RequestId, const bool bSuccess) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "receive_command_response") + .AddObject(TEXT("Object"), Actor) + .AddObject(TEXT("TargetObject"), TargetObject) + .AddFunction(TEXT("Function"), Function) + .AddRequestId(TEXT("RequestId"), RequestId) + .AddKeyValue(TEXT("Success"), BoolToString(bSuccess)) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateSendRemoveEntity(const UObject* Object, const Worker_EntityId EntityId) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "send_remove_entity") + .AddObject(TEXT("Object"), Object) + .AddEntityId(TEXT("EntityId"), EntityId) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateReceiveRemoveEntity(const Worker_EntityId EntityId) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "receive_remove_entity").AddEntityId(TEXT("EntityId"), EntityId).GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateSendCreateEntity(const UObject* Object, const Worker_EntityId EntityId) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "send_create_entity") + .AddObject(TEXT("Object"), Object) + .AddEntityId(TEXT("EntityId"), EntityId) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateReceiveCreateEntity(const Worker_EntityId EntityId) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "receive_create_entity").AddEntityId(TEXT("EntityId"), EntityId).GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateReceiveCreateEntitySuccess(const UObject* Object, const Worker_EntityId EntityId) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "receive_create_entity_success") + .AddObject(TEXT("Object"), Object) + .AddEntityId(TEXT("EntityId"), EntityId) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateSendRetireEntity(const UObject* Object, const Worker_EntityId EntityId) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "send_retire_entity") + .AddObject(TEXT("Object"), Object) + .AddEntityId(TEXT("EntityId"), EntityId) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateAuthorityIntentUpdate(VirtualWorkerId WorkerId, const UObject* Object) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "authority_intent_update") + .AddObject(TEXT("Object"), Object) + .AddNewWorkerId(TEXT("NewWorkerId"), WorkerId) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateAuthorityChange(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId, + const Worker_Authority Authority) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "authority_change") + .AddEntityId(TEXT("EntityId"), EntityId) + .AddComponentId(TEXT("ComponentId"), ComponentId) + .AddAuthority(TEXT("Authority"), Authority) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateComponentUpdate(const UObject* Object, const UObject* TargetObject, + const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "component_update") + .AddObject(TEXT("Object"), Object) + .AddObject(TEXT("TargetObject"), TargetObject) + .AddEntityId(TEXT("EntityId"), EntityId) + .AddComponentId(TEXT("ComponentId"), ComponentId) + .GetEvent(); +} + +FSpatialTraceEvent FSpatialTraceEventBuilder::CreateGenericMessage(FString Message) +{ + return FSpatialTraceEventBuilder(GDK_EVENT_NAMESPACE "generic_message", MoveTemp(Message)).GetEvent(); +} + +FString FSpatialTraceEventBuilder::AuthorityToString(Worker_Authority Authority) +{ + switch (Authority) + { + case Worker_Authority::WORKER_AUTHORITY_NOT_AUTHORITATIVE: + return TEXT("NotAuthoritative"); + case Worker_Authority::WORKER_AUTHORITY_AUTHORITATIVE: + return TEXT("Authoritative"); + default: + return TEXT("Unknown"); + } +} + +FString FSpatialTraceEventBuilder::BoolToString(bool bInput) +{ + return bInput ? TEXT("True") : TEXT("False"); +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialTraceUniqueId.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialTraceUniqueId.cpp new file mode 100644 index 0000000000..c8f8f4131f --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialTraceUniqueId.cpp @@ -0,0 +1,22 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Interop/Connection/SpatialTraceUniqueId.h" + +using namespace SpatialGDK; + +FString EventTraceUniqueId::ToString() const +{ + return FString::Printf(TEXT("%0X"), Hash); +} + +EventTraceUniqueId EventTraceUniqueId::GenerateForRPC(Worker_EntityId Entity, uint8 Type, uint64 Id) +{ + uint32 ComputedHash = HashCombine(HashCombine(GetTypeHash(static_cast(Entity)), GetTypeHash(Type)), GetTypeHash(Id)); + return EventTraceUniqueId(ComputedHash); +} + +EventTraceUniqueId EventTraceUniqueId::GenerateForProperty(Worker_EntityId Entity, const GDK_PROPERTY(Property) * Property) +{ + uint32 ComputedHash = HashCombine(GetTypeHash(static_cast(Entity)), GetTypeHash(Property->GetName())); + return EventTraceUniqueId(ComputedHash); +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialViewWorkerConnection.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialViewWorkerConnection.cpp deleted file mode 100644 index 0fe39addce..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialViewWorkerConnection.cpp +++ /dev/null @@ -1,167 +0,0 @@ -#include "Interop/Connection/SpatialViewWorkerConnection.h" - -#include "SpatialGDKSettings.h" -#include "SpatialView/CommandRequest.h" -#include "SpatialView/ComponentData.h" -#include "SpatialView/ConnectionHandler/SpatialOSConnectionHandler.h" -#include "SpatialView/ViewCoordinator.h" - -namespace -{ - -SpatialGDK::ComponentData ToComponentData(FWorkerComponentData* Data) -{ - return SpatialGDK::ComponentData(SpatialGDK::OwningComponentDataPtr(Data->schema_type), Data->component_id); -} - -SpatialGDK::ComponentUpdate ToComponentUpdate(FWorkerComponentUpdate* Update) -{ - return SpatialGDK::ComponentUpdate(SpatialGDK::OwningComponentUpdatePtr(Update->schema_type), Update->component_id); -} - -} // anonymous namespace - -void USpatialViewWorkerConnection::SetConnection(Worker_Connection* WorkerConnectionIn) -{ - TUniquePtr Handler = MakeUnique(WorkerConnectionIn); - Coordinator = MakeUnique(MoveTemp(Handler)); -} - -void USpatialViewWorkerConnection::FinishDestroy() -{ - Coordinator.Reset(); - Super::FinishDestroy(); -} - -void USpatialViewWorkerConnection::DestroyConnection() -{ - Coordinator.Reset(); -} - -TArray USpatialViewWorkerConnection::GetOpList() -{ - check(Coordinator.IsValid()); - TArray OpLists; - OpLists.Add(Coordinator->Advance()); - return OpLists; -} - -Worker_RequestId USpatialViewWorkerConnection::SendReserveEntityIdsRequest(uint32_t NumOfEntities) -{ - check(Coordinator.IsValid()); - return Coordinator->SendReserveEntityIdsRequest(NumOfEntities); -} - -Worker_RequestId USpatialViewWorkerConnection::SendCreateEntityRequest(TArray Components, const Worker_EntityId* EntityId) -{ - check(Coordinator.IsValid()); - const TOptional Id = EntityId ? *EntityId : TOptional(); - TArray Data; - Data.Reserve(Components.Num()); - for (auto& Component : Components) - { - Data.Emplace(SpatialGDK::OwningComponentDataPtr(Component.schema_type), Component.component_id); - } - return Coordinator->SendCreateEntityRequest(MoveTemp(Data), Id); -} - -Worker_RequestId USpatialViewWorkerConnection::SendDeleteEntityRequest(Worker_EntityId EntityId) -{ - check(Coordinator.IsValid()); - return Coordinator->SendDeleteEntityRequest(EntityId); -} - -void USpatialViewWorkerConnection::SendAddComponent(Worker_EntityId EntityId, FWorkerComponentData* ComponentData) -{ - check(Coordinator.IsValid()); - return Coordinator->SendAddComponent(EntityId, ToComponentData(ComponentData)); -} - -void USpatialViewWorkerConnection::SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) -{ - check(Coordinator.IsValid()); - return Coordinator->SendRemoveComponent(EntityId, ComponentId); -} - -void USpatialViewWorkerConnection::SendComponentUpdate(Worker_EntityId EntityId, FWorkerComponentUpdate* ComponentUpdate) -{ - check(Coordinator.IsValid()); - return Coordinator->SendComponentUpdate(EntityId, ToComponentUpdate(ComponentUpdate)); -} - -Worker_RequestId USpatialViewWorkerConnection::SendCommandRequest(Worker_EntityId EntityId, - Worker_CommandRequest* Request, uint32_t CommandId) -{ - check(Coordinator.IsValid()); - return Coordinator->SendEntityCommandRequest(EntityId, SpatialGDK::CommandRequest( - SpatialGDK::OwningCommandRequestPtr(Request->schema_type) , Request->component_id, Request->command_index)); -} - -void USpatialViewWorkerConnection::SendCommandResponse(Worker_RequestId RequestId, Worker_CommandResponse* Response) -{ - check(Coordinator.IsValid()); - Coordinator->SendEntityCommandResponse(RequestId, SpatialGDK::CommandResponse( - SpatialGDK::OwningCommandResponsePtr(Response->schema_type) , Response->component_id, Response->command_index)); -} - -void USpatialViewWorkerConnection::SendCommandFailure(Worker_RequestId RequestId, const FString& Message) -{ - check(Coordinator.IsValid()); - Coordinator->SendEntityCommandFailure(RequestId, Message); -} - -void USpatialViewWorkerConnection::SendLogMessage(uint8_t Level, const FName& LoggerName, const TCHAR* Message) -{ - check(Coordinator.IsValid()); - Coordinator->SendLogMessage(static_cast(Level), LoggerName, Message); -} - -void USpatialViewWorkerConnection::SendComponentInterest(Worker_EntityId EntityId, - TArray&& ComponentInterest) -{ - // Deprecated. - checkNoEntry(); -} - -Worker_RequestId USpatialViewWorkerConnection::SendEntityQueryRequest(const Worker_EntityQuery* EntityQuery) -{ - check(Coordinator.IsValid()); - return Coordinator->SendEntityQueryRequest(SpatialGDK::EntityQuery(*EntityQuery)); -} - -void USpatialViewWorkerConnection::SendMetrics(SpatialGDK::SpatialMetrics Metrics) -{ - check(Coordinator.IsValid()); - Coordinator->SendMetrics(MoveTemp(Metrics)); -} - -PhysicalWorkerName USpatialViewWorkerConnection::GetWorkerId() const -{ - check(Coordinator.IsValid()); - return Coordinator->GetWorkerId(); -} - -const TArray& USpatialViewWorkerConnection::GetWorkerAttributes() const -{ - check(Coordinator.IsValid()); - return Coordinator->GetWorkerAttributes(); -} - -void USpatialViewWorkerConnection::ProcessOutgoingMessages() -{ - Coordinator->FlushMessagesToSend(); -} - -void USpatialViewWorkerConnection::MaybeFlush() -{ - const USpatialGDKSettings* Settings = GetDefault(); - if (Settings->bWorkerFlushAfterOutgoingNetworkOp) - { - Coordinator->FlushMessagesToSend(); - } -} - -void USpatialViewWorkerConnection::Flush() -{ - Coordinator->FlushMessagesToSend(); -} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp new file mode 100644 index 0000000000..7bbdb4e2ba --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp @@ -0,0 +1,342 @@ +#include "Interop/Connection/SpatialWorkerConnection.h" + +#include "Interop/Connection/SpatialEventTracer.h" +#include "SpatialGDKSettings.h" +#include "SpatialView/CommandRequest.h" +#include "SpatialView/CommandRetryHandler.h" +#include "SpatialView/ComponentData.h" +#include "SpatialView/ConnectionHandler/InitialOpListConnectionHandler.h" +#include "SpatialView/ConnectionHandler/SpatialOSConnectionHandler.h" + +DEFINE_LOG_CATEGORY(LogSpatialWorkerConnection); + +namespace +{ +SpatialGDK::ComponentData ToComponentData(FWorkerComponentData* Data) +{ + return SpatialGDK::ComponentData(SpatialGDK::OwningComponentDataPtr(Data->schema_type), Data->component_id); +} + +SpatialGDK::ComponentUpdate ToComponentUpdate(FWorkerComponentUpdate* Update) +{ + return SpatialGDK::ComponentUpdate(SpatialGDK::OwningComponentUpdatePtr(Update->schema_type), Update->component_id); +} + +} // anonymous namespace + +void USpatialWorkerConnection::SetConnection(Worker_Connection* WorkerConnectionIn, + TSharedPtr SharedEventTracer, + SpatialGDK::FComponentSetData ComponentSetData) +{ + EventTracer = SharedEventTracer.Get(); + StartupComplete = false; + TUniquePtr Handler = + MakeUnique(WorkerConnectionIn, SharedEventTracer); + TUniquePtr InitialOpListHandler = MakeUnique( + MoveTemp(Handler), [this](SpatialGDK::OpList& Ops, SpatialGDK::ExtractedOpListData& ExtractedOps) { + if (StartupComplete) + { + return true; + } + ExtractStartupOps(Ops, ExtractedOps); + return false; + }); + Coordinator = MakeUnique(MoveTemp(InitialOpListHandler), SharedEventTracer, MoveTemp(ComponentSetData)); +} + +void USpatialWorkerConnection::FinishDestroy() +{ + Coordinator.Reset(); + Super::FinishDestroy(); +} + +const TArray& USpatialWorkerConnection::GetEntityDeltas() +{ + check(Coordinator.IsValid()); + return Coordinator->GetViewDelta().GetEntityDeltas(); +} + +const TArray& USpatialWorkerConnection::GetWorkerMessages() +{ + check(Coordinator.IsValid()); + return Coordinator->GetViewDelta().GetWorkerMessages(); +} + +void USpatialWorkerConnection::DestroyConnection() +{ + Coordinator.Reset(); +} + +Worker_RequestId USpatialWorkerConnection::SendReserveEntityIdsRequest(uint32_t NumOfEntities, const SpatialGDK::FRetryData& RetryData) +{ + check(Coordinator.IsValid()); + return Coordinator->SendReserveEntityIdsRequest(NumOfEntities, RetryData); +} + +Worker_RequestId USpatialWorkerConnection::SendCreateEntityRequest(TArray Components, const Worker_EntityId* EntityId, + const SpatialGDK::FRetryData& RetryData, const FSpatialGDKSpanId& SpanId) +{ + check(Coordinator.IsValid()); + const TOptional Id = EntityId != nullptr ? *EntityId : TOptional(); + TArray Data; + Data.Reserve(Components.Num()); + for (auto& Component : Components) + { + Data.Emplace(SpatialGDK::OwningComponentDataPtr(Component.schema_type), Component.component_id); + } + + return Coordinator->SendCreateEntityRequest(MoveTemp(Data), Id, RetryData, SpanId); +} + +Worker_RequestId USpatialWorkerConnection::SendDeleteEntityRequest(Worker_EntityId EntityId, const SpatialGDK::FRetryData& RetryData, + const FSpatialGDKSpanId& SpanId) +{ + check(Coordinator.IsValid()); + return Coordinator->SendDeleteEntityRequest(EntityId, RetryData, SpanId); +} + +void USpatialWorkerConnection::SendAddComponent(Worker_EntityId EntityId, FWorkerComponentData* ComponentData, + const FSpatialGDKSpanId& SpanId) +{ + check(Coordinator.IsValid()); + Coordinator->SendAddComponent(EntityId, ToComponentData(ComponentData), SpanId); +} + +void USpatialWorkerConnection::SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, + const FSpatialGDKSpanId& SpanId) +{ + check(Coordinator.IsValid()); + Coordinator->SendRemoveComponent(EntityId, ComponentId, SpanId); +} + +void USpatialWorkerConnection::SendComponentUpdate(Worker_EntityId EntityId, FWorkerComponentUpdate* ComponentUpdate, + const FSpatialGDKSpanId& SpanId) +{ + check(Coordinator.IsValid()); + Coordinator->SendComponentUpdate(EntityId, ToComponentUpdate(ComponentUpdate), SpanId); +} + +Worker_RequestId USpatialWorkerConnection::SendCommandRequest(Worker_EntityId EntityId, Worker_CommandRequest* Request, + const SpatialGDK::FRetryData& RetryData, const FSpatialGDKSpanId& SpanId) +{ + check(Coordinator.IsValid()); + return Coordinator->SendEntityCommandRequest(EntityId, + SpatialGDK::CommandRequest(SpatialGDK::OwningCommandRequestPtr(Request->schema_type), + Request->component_id, Request->command_index), + RetryData, SpanId); +} + +void USpatialWorkerConnection::SendCommandResponse(Worker_RequestId RequestId, Worker_CommandResponse* Response, + const FSpatialGDKSpanId& SpanId) +{ + check(Coordinator.IsValid()); + Coordinator->SendEntityCommandResponse(RequestId, + SpatialGDK::CommandResponse(SpatialGDK::OwningCommandResponsePtr(Response->schema_type), + Response->component_id, Response->command_index), + SpanId); +} + +void USpatialWorkerConnection::SendCommandFailure(Worker_RequestId RequestId, const FString& Message, const FSpatialGDKSpanId& SpanId) +{ + check(Coordinator.IsValid()); + Coordinator->SendEntityCommandFailure(RequestId, Message, SpanId); +} + +void USpatialWorkerConnection::SendLogMessage(uint8_t Level, const FName& LoggerName, const TCHAR* Message) +{ + check(Coordinator.IsValid()); + Coordinator->SendLogMessage(static_cast(Level), LoggerName, Message); +} + +Worker_RequestId USpatialWorkerConnection::SendEntityQueryRequest(const Worker_EntityQuery* EntityQuery, + const SpatialGDK::FRetryData& RetryData) +{ + check(Coordinator.IsValid()); + return Coordinator->SendEntityQueryRequest(SpatialGDK::EntityQuery(*EntityQuery), RetryData); +} + +void USpatialWorkerConnection::SendMetrics(SpatialGDK::SpatialMetrics Metrics) +{ + check(Coordinator.IsValid()); + Coordinator->SendMetrics(MoveTemp(Metrics)); +} + +void USpatialWorkerConnection::Advance(float DeltaTimeS) +{ + check(Coordinator.IsValid()); + Coordinator->Advance(DeltaTimeS); +} + +bool USpatialWorkerConnection::HasDisconnected() const +{ + check(Coordinator.IsValid()); + return Coordinator->GetViewDelta().HasConnectionStatusChanged(); +} + +Worker_ConnectionStatusCode USpatialWorkerConnection::GetConnectionStatus() const +{ + check(Coordinator.IsValid()); + return Coordinator->GetViewDelta().GetConnectionStatusChange(); +} + +FString USpatialWorkerConnection::GetDisconnectReason() const +{ + check(Coordinator.IsValid()); + return Coordinator->GetViewDelta().GetConnectionStatusChangeMessage(); +} + +const SpatialGDK::EntityView& USpatialWorkerConnection::GetView() const +{ + check(Coordinator.IsValid()); + return Coordinator->GetView(); +} + +SpatialGDK::ViewCoordinator& USpatialWorkerConnection::GetCoordinator() const +{ + return *Coordinator; +} + +PhysicalWorkerName USpatialWorkerConnection::GetWorkerId() const +{ + check(Coordinator.IsValid()); + return Coordinator->GetWorkerId(); +} + +Worker_EntityId USpatialWorkerConnection::GetWorkerSystemEntityId() const +{ + check(Coordinator.IsValid()); + return Coordinator->GetWorkerSystemEntityId(); +} + +SpatialGDK::CallbackId USpatialWorkerConnection::RegisterComponentAddedCallback(Worker_ComponentId ComponentId, + SpatialGDK::FComponentValueCallback Callback) +{ + check(Coordinator.IsValid()); + return Coordinator->RegisterComponentAddedCallback(ComponentId, MoveTemp(Callback)); +} + +SpatialGDK::CallbackId USpatialWorkerConnection::RegisterComponentRemovedCallback(Worker_ComponentId ComponentId, + SpatialGDK::FComponentValueCallback Callback) +{ + check(Coordinator.IsValid()); + return Coordinator->RegisterComponentRemovedCallback(ComponentId, MoveTemp(Callback)); +} + +SpatialGDK::CallbackId USpatialWorkerConnection::RegisterComponentValueCallback(Worker_ComponentId ComponentId, + SpatialGDK::FComponentValueCallback Callback) +{ + check(Coordinator.IsValid()); + return Coordinator->RegisterComponentValueCallback(ComponentId, MoveTemp(Callback)); +} + +SpatialGDK::CallbackId USpatialWorkerConnection::RegisterAuthorityGainedCallback(Worker_ComponentId ComponentId, + SpatialGDK::FEntityCallback Callback) +{ + check(Coordinator.IsValid()); + return Coordinator->RegisterAuthorityGainedCallback(ComponentId, MoveTemp(Callback)); +} + +SpatialGDK::CallbackId USpatialWorkerConnection::RegisterAuthorityLostCallback(Worker_ComponentId ComponentId, + SpatialGDK::FEntityCallback Callback) +{ + check(Coordinator.IsValid()); + return Coordinator->RegisterAuthorityLostCallback(ComponentId, MoveTemp(Callback)); +} + +SpatialGDK::CallbackId USpatialWorkerConnection::RegisterAuthorityLostTempCallback(Worker_ComponentId ComponentId, + SpatialGDK::FEntityCallback Callback) +{ + check(Coordinator.IsValid()); + return Coordinator->RegisterAuthorityLostTempCallback(ComponentId, MoveTemp(Callback)); +} + +void USpatialWorkerConnection::RemoveCallback(SpatialGDK::CallbackId Id) +{ + check(Coordinator.IsValid()); + Coordinator->RemoveCallback(Id); +} + +void USpatialWorkerConnection::Flush() +{ + Coordinator->FlushMessagesToSend(); +} + +void USpatialWorkerConnection::SetStartupComplete() +{ + StartupComplete = true; +} + +bool USpatialWorkerConnection::IsStartupComponent(Worker_ComponentId Id) +{ + return Id == SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID || Id == SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID + || Id == SpatialConstants::SERVER_WORKER_COMPONENT_ID || Id == SpatialConstants::GDK_KNOWN_ENTITY_TAG_COMPONENT_ID; +} + +void USpatialWorkerConnection::ExtractStartupOps(SpatialGDK::OpList& OpList, SpatialGDK::ExtractedOpListData& ExtractedOpList) +{ + for (uint32 i = 0; i < OpList.Count; ++i) + { + Worker_Op& Op = OpList.Ops[i]; + switch (static_cast(Op.op_type)) + { + case WORKER_OP_TYPE_ADD_ENTITY: + ExtractedOpList.AddOp(Op); + break; + case WORKER_OP_TYPE_REMOVE_ENTITY: + ExtractedOpList.AddOp(Op); + break; + case WORKER_OP_TYPE_RESERVE_ENTITY_IDS_RESPONSE: + ExtractedOpList.AddOp(Op); + break; + case WORKER_OP_TYPE_CREATE_ENTITY_RESPONSE: + ExtractedOpList.AddOp(Op); + break; + case WORKER_OP_TYPE_DELETE_ENTITY_RESPONSE: + ExtractedOpList.AddOp(Op); + break; + case WORKER_OP_TYPE_ENTITY_QUERY_RESPONSE: + ExtractedOpList.AddOp(Op); + break; + case WORKER_OP_TYPE_ADD_COMPONENT: + if (IsStartupComponent(Op.op.add_component.data.component_id)) + { + ExtractedOpList.AddOp(Op); + } + break; + case WORKER_OP_TYPE_REMOVE_COMPONENT: + if (IsStartupComponent(Op.op.remove_component.component_id)) + { + ExtractedOpList.AddOp(Op); + } + break; + case WORKER_OP_TYPE_COMPONENT_SET_AUTHORITY_CHANGE: + if (Op.op.component_set_authority_change.component_set_id == SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID) + { + ExtractedOpList.AddOp(Op); + } + break; + case WORKER_OP_TYPE_COMPONENT_UPDATE: + if (IsStartupComponent(Op.op.component_update.update.component_id)) + { + ExtractedOpList.AddOp(Op); + } + break; + case WORKER_OP_TYPE_COMMAND_REQUEST: + break; + case WORKER_OP_TYPE_COMMAND_RESPONSE: + ExtractedOpList.AddOp(Op); + break; + case WORKER_OP_TYPE_DISCONNECT: + ExtractedOpList.AddOp(Op); + break; + case WORKER_OP_TYPE_FLAG_UPDATE: + break; + case WORKER_OP_TYPE_METRICS: + break; + case WORKER_OP_TYPE_CRITICAL_SECTION: + break; + default: + break; + } + } +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp index b9f6da832a..a38f8cc379 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp @@ -3,8 +3,8 @@ #include "Interop/GlobalStateManager.h" #if WITH_EDITOR -#include "Settings/LevelEditorPlaySettings.h" #include "Editor.h" +#include "Settings/LevelEditorPlaySettings.h" #endif #include "Engine/Classes/AI/AISystemBase.h" @@ -14,17 +14,16 @@ #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" #include "EngineUtils.h" -#include "GameFramework/GameModeBase.h" #include "Interop/Connection/SpatialWorkerConnection.h" #include "Interop/SpatialReceiver.h" #include "Interop/SpatialSender.h" -#include "LoadBalancing/AbstractLBStrategy.h" #include "Kismet/GameplayStatics.h" +#include "LoadBalancing/AbstractLBStrategy.h" #include "Schema/ServerWorker.h" -#include "Schema/UnrealMetadata.h" #include "SpatialConstants.h" #include "UObject/UObjectGlobals.h" -#include "Utils/EntityPool.h" +#include "Utils/SpatialDebugger.h" +#include "Utils/SpatialMetricsDisplay.h" #include "Utils/SpatialStatics.h" DEFINE_LOG_CATEGORY(LogGlobalStateManager); @@ -62,9 +61,9 @@ void UGlobalStateManager::Init(USpatialNetDriver* InNetDriver) bTranslationQueryInFlight = false; } -void UGlobalStateManager::ApplyDeploymentMapData(const Worker_ComponentData& Data) +void UGlobalStateManager::ApplyDeploymentMapData(Schema_ComponentData* Data) { - Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data); SetDeploymentMapURL(GetStringFromSchema(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_MAP_URL_ID)); @@ -75,9 +74,9 @@ void UGlobalStateManager::ApplyDeploymentMapData(const Worker_ComponentData& Dat SchemaHash = Schema_GetUint32(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_SCHEMA_HASH); } -void UGlobalStateManager::ApplyStartupActorManagerData(const Worker_ComponentData& Data) +void UGlobalStateManager::ApplyStartupActorManagerData(Schema_ComponentData* Data) { - Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data); bCanBeginPlay = GetBoolFromSchema(ComponentObject, SpatialConstants::STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_ID); @@ -92,9 +91,11 @@ void UGlobalStateManager::TrySendWorkerReadyToBeginPlay() // 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 bWorkerEntityReady = NetDriver->WorkerEntityId != SpatialConstants::INVALID_ENTITY_ID && - StaticComponentView->HasAuthority(NetDriver->WorkerEntityId, SpatialConstants::SERVER_WORKER_COMPONENT_ID); + const bool bHasReceivedStartupActorData = + StaticComponentView->HasComponent(GlobalStateManagerEntityId, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID); + const bool bWorkerEntityReady = + NetDriver->WorkerEntityId != SpatialConstants::INVALID_ENTITY_ID + && StaticComponentView->HasAuthority(NetDriver->WorkerEntityId, SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID); if (bHasSentReadyForVirtualWorkerAssignment || !bHasReceivedStartupActorData || !bWorkerEntityReady) { @@ -111,9 +112,9 @@ void UGlobalStateManager::TrySendWorkerReadyToBeginPlay() NetDriver->Connection->SendComponentUpdate(NetDriver->WorkerEntityId, &Update); } -void UGlobalStateManager::ApplyDeploymentMapUpdate(const Worker_ComponentUpdate& Update) +void UGlobalStateManager::ApplyDeploymentMapUpdate(Schema_ComponentUpdate* Update) { - Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update); if (Schema_GetObjectCount(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_MAP_URL_ID) == 1) { @@ -146,17 +147,17 @@ void UGlobalStateManager::OnPrePIEEnded(bool bValue) void UGlobalStateManager::SendShutdownMultiProcessRequest() { /** When running with Use Single Process unticked, send a shutdown command to the servers to allow SpatialOS to shutdown. - * Standard UnrealEngine behavior is to call TerminateProc on external processes and there is no method to send any messaging - * to those external process. - * The GDK requires shutdown code to be ran for workers to disconnect cleanly so instead of abruptly shutting down the server worker, - * just send a command to the worker to begin it's shutdown phase. - */ + * Standard UnrealEngine behavior is to call TerminateProc on external processes and there is no method to send any messaging + * to those external process. + * The GDK requires shutdown code to be ran for workers to disconnect cleanly so instead of abruptly shutting down the server worker, + * just send a command to the worker to begin it's shutdown phase. + */ Worker_CommandRequest CommandRequest = {}; CommandRequest.component_id = SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID; CommandRequest.command_index = SpatialConstants::SHUTDOWN_MULTI_PROCESS_REQUEST_ID; CommandRequest.schema_type = Schema_CreateCommandRequest(); - NetDriver->Connection->SendCommandRequest(GlobalStateManagerEntityId, &CommandRequest, SpatialConstants::SHUTDOWN_MULTI_PROCESS_REQUEST_ID); + NetDriver->Connection->SendCommandRequest(GlobalStateManagerEntityId, &CommandRequest, RETRY_UNTIL_COMPLETE, {}); } void UGlobalStateManager::ReceiveShutdownMultiProcessRequest() @@ -165,7 +166,8 @@ void UGlobalStateManager::ReceiveShutdownMultiProcessRequest() { UE_LOG(LogGlobalStateManager, Log, TEXT("Received shutdown multi-process request.")); - // Since the server works are shutting down, set reset the accepting_players flag to false to prevent race conditions where the client connects quicker than the server. + // Since the server works are shutting down, set reset the accepting_players flag to false to prevent race conditions where the + // client connects quicker than the server. SetAcceptingPlayers(false); DeploymentSessionId = 0; SendSessionIdUpdate(); @@ -178,14 +180,17 @@ void UGlobalStateManager::ReceiveShutdownMultiProcessRequest() } } -void UGlobalStateManager::OnShutdownComponentUpdate(const Worker_ComponentUpdate& Update) +#if WITH_EDITOR +void UGlobalStateManager::OnShutdownComponentUpdate(Schema_ComponentUpdate* Update) { - Schema_Object* EventsObject = Schema_GetComponentUpdateEvents(Update.schema_type); + Schema_Object* EventsObject = Schema_GetComponentUpdateEvents(Update); + // TODO(UNR-4395): Probably should be a bool in state - probably a non-persistent entity if (Schema_GetObjectCount(EventsObject, SpatialConstants::SHUTDOWN_ADDITIONAL_SERVERS_EVENT_ID) > 0) { ReceiveShutdownAdditionalServersEvent(); } } +#endif // WITH_EDITOR void UGlobalStateManager::ReceiveShutdownAdditionalServersEvent() { @@ -199,9 +204,10 @@ void UGlobalStateManager::ReceiveShutdownAdditionalServersEvent() void UGlobalStateManager::SendShutdownAdditionalServersEvent() { - if (!NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID)) + if (!StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID)) { - UE_LOG(LogGlobalStateManager, Warning, TEXT("Tried to send shutdown_additional_servers event on the GSM but this worker does not have authority.")); + UE_LOG(LogGlobalStateManager, Warning, + TEXT("Tried to send shutdown_additional_servers event on the GSM but this worker does not have authority.")); return; } @@ -216,23 +222,28 @@ void UGlobalStateManager::SendShutdownAdditionalServersEvent() } #endif // WITH_EDITOR -void UGlobalStateManager::ApplyStartupActorManagerUpdate(const Worker_ComponentUpdate& Update) +void UGlobalStateManager::ApplyStartupActorManagerUpdate(Schema_ComponentUpdate* Update) { - Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); - + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update); + + // The update can only happen after having read the initial GSM state. + // It is gated on the leader getting its VirtualWorkerId, gated in the Translation manager getting all the workers it need + // gated on all workers sending ReadyToBeginPlay, which happens in ApplyStartupActorManagerData. + // We are in the same situation as the leader when it is running AuthorityChanged on STARTUP_ACTOR_MANAGER_COMPONENT_ID. + // So we apply the same logic on setting bCanSpawnWithAuthority before reading the new value of bCanBeginPlay. + bCanSpawnWithAuthority = !bCanBeginPlay; bCanBeginPlay = GetBoolFromSchema(ComponentObject, SpatialConstants::STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_ID); - bCanSpawnWithAuthority = true; } void UGlobalStateManager::SetDeploymentState() { - check(NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID)); + check(StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID)); UWorld* CurrentWorld = NetDriver->GetWorld(); // Send the component update that we can now accept players. UE_LOG(LogGlobalStateManager, Log, TEXT("Setting deployment URL to '%s'"), *CurrentWorld->URL.Map); - UE_LOG(LogGlobalStateManager, Log, TEXT("Setting schema hash to '%u'"), NetDriver->ClassInfoManager->SchemaDatabase->SchemaDescriptorHash); + UE_LOG(LogGlobalStateManager, Log, TEXT("Setting schema hash to '%u'"), NetDriver->ClassInfoManager->SchemaDatabase->SchemaBundleHash); FWorkerComponentUpdate Update = {}; Update.component_id = SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID; @@ -243,7 +254,8 @@ void UGlobalStateManager::SetDeploymentState() AddStringToSchema(UpdateObject, SpatialConstants::DEPLOYMENT_MAP_MAP_URL_ID, CurrentWorld->RemovePIEPrefix(CurrentWorld->URL.Map)); // Set the schema hash for connecting workers to check against - Schema_AddUint32(UpdateObject, SpatialConstants::DEPLOYMENT_MAP_SCHEMA_HASH, NetDriver->ClassInfoManager->SchemaDatabase->SchemaDescriptorHash); + Schema_AddUint32(UpdateObject, SpatialConstants::DEPLOYMENT_MAP_SCHEMA_HASH, + NetDriver->ClassInfoManager->SchemaDatabase->SchemaBundleHash); // Component updates are short circuited so we set the updated state here and then send the component update. NetDriver->Connection->SendComponentUpdate(GlobalStateManagerEntityId, &Update); @@ -255,7 +267,8 @@ void UGlobalStateManager::SetAcceptingPlayers(bool bInAcceptingPlayers) // - 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 bHasDeploymentMapAuthority = + StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID); const bool bHasBegunPlay = NetDriver->GetWorld()->HasBegunPlay(); const bool bIsDuplicatingCurrentState = bAcceptingPlayers == bInAcceptingPlayers; if (!bHasDeploymentMapAuthority || !bHasBegunPlay || bIsDuplicatingCurrentState) @@ -278,64 +291,43 @@ void UGlobalStateManager::SetAcceptingPlayers(bool bInAcceptingPlayers) NetDriver->Connection->SendComponentUpdate(GlobalStateManagerEntityId, &Update); } -void UGlobalStateManager::AuthorityChanged(const Worker_AuthorityChangeOp& AuthOp) +void UGlobalStateManager::AuthorityChanged(const Worker_ComponentSetAuthorityChangeOp& AuthOp) { - UE_LOG(LogGlobalStateManager, Verbose, TEXT("Authority over the GSM component %d has changed. This worker %s authority."), AuthOp.component_id, - AuthOp.authority == WORKER_AUTHORITY_AUTHORITATIVE ? TEXT("now has") : TEXT ("does not have")); + UE_LOG(LogGlobalStateManager, Verbose, TEXT("Authority over the GSM component %d has changed. This worker %s authority."), + AuthOp.component_set_id, AuthOp.authority == WORKER_AUTHORITY_AUTHORITATIVE ? TEXT("now has") : TEXT("does not have")); if (AuthOp.authority != WORKER_AUTHORITY_AUTHORITATIVE) { return; } - switch (AuthOp.component_id) + if (StaticComponentView->HasComponent(AuthOp.entity_id, SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID)) { - case SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID: - { - GlobalStateManagerEntityId = AuthOp.entity_id; - SetDeploymentState(); - SetAcceptingPlayers(true); - break; - } - case SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID: - { - // 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: - { - break; - } + GlobalStateManagerEntityId = AuthOp.entity_id; + SetDeploymentState(); } -} -bool UGlobalStateManager::HandlesComponent(const Worker_ComponentId ComponentId) const -{ - switch (ComponentId) + if (StaticComponentView->HasComponent(AuthOp.entity_id, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID)) { - case SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID: - case SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID: - case SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID: - return true; - default: - return false; + // 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; } } void UGlobalStateManager::ResetGSM() { - UE_LOG(LogGlobalStateManager, Display, TEXT("GlobalStateManager not accepting players and resetting BeginPlay lifecycle properties. Session restarting.")); + UE_LOG(LogGlobalStateManager, Display, + TEXT("GlobalStateManager not accepting players and resetting BeginPlay lifecycle properties. Session restarting.")); SetAcceptingPlayers(false); @@ -348,7 +340,9 @@ void UGlobalStateManager::BeginDestroy() Super::BeginDestroy(); #if WITH_EDITOR - if (NetDriver != nullptr && NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID)) + if (NetDriver != nullptr + && NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, + SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID)) { // If we are deleting dynamically spawned entities, we need to if (GetDefault()->GetDeleteDynamicEntities()) @@ -365,42 +359,101 @@ void UGlobalStateManager::BeginDestroy() #endif } -void UGlobalStateManager::SetAllActorRolesBasedOnLBStrategy() +void UGlobalStateManager::HandleActorBasedOnLoadBalancer(AActor* Actor) const { - for (TActorIterator It(NetDriver->World); It; ++It) + if (Actor == nullptr || Actor->IsPendingKill()) { - AActor* Actor = *It; - if (Actor != nullptr && !Actor->IsPendingKill()) - { - if (Actor->GetIsReplicated()) - { - const bool bAuthoritative = NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*Actor); - Actor->Role = bAuthoritative ? ROLE_Authority : ROLE_SimulatedProxy; - Actor->RemoteRole = bAuthoritative ? ROLE_SimulatedProxy : ROLE_Authority; - } - } + return; + } + + if (USpatialStatics::IsSpatialOffloadingEnabled(GetWorld()) && !USpatialStatics::IsActorGroupOwnerForActor(Actor) + && !Actor->bNetLoadOnNonAuthServer) + { + Actor->Destroy(true); + return; + } + + if (!Actor->GetIsReplicated()) + { + return; + } + + // Replicated level Actors should only be initially authority if: + // - these are workers starting as part of a fresh deployment (tracked by the bCanSpawnWithAuthority bool), + // - these actors are marked as NotPersistent and we're loading from a saved snapshot (which means bCanSpawnWithAuthority is false) + // - the load balancing strategy says this server should be authoritative (as opposed to some other server). + const bool bAuthoritative = (bCanSpawnWithAuthority || Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_NotPersistent)) + && NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*Actor); + + Actor->Role = bAuthoritative ? ROLE_Authority : ROLE_SimulatedProxy; + Actor->RemoteRole = bAuthoritative ? ROLE_SimulatedProxy : ROLE_Authority; + + UE_LOG(LogGlobalStateManager, Verbose, TEXT("GSM updated actor authority: %s %s."), *Actor->GetPathName(), + bAuthoritative ? TEXT("authoritative") : TEXT("not authoritative")); +} + +Worker_EntityId UGlobalStateManager::GetLocalServerWorkerEntityId() const +{ + if (ensure(NetDriver != nullptr)) + { + return NetDriver->WorkerEntityId; + } + + return SpatialConstants::INVALID_ENTITY_ID; +} + +void UGlobalStateManager::ClaimSnapshotPartition() const +{ + if (ensure(Sender != nullptr)) + { + Sender->SendClaimPartitionRequest(NetDriver->Connection->GetWorkerSystemEntityId(), + SpatialConstants::INITIAL_SNAPSHOT_PARTITION_ENTITY_ID); } } void UGlobalStateManager::TriggerBeginPlay() { - const bool bHasStartupActorAuthority = NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID); + const bool bHasStartupActorAuthority = + StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_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 !UE_BUILD_SHIPPING + const USpatialGDKSettings* SpatialSettings = GetDefault(); + if (NetDriver->IsServer()) + { + // If metrics display is enabled, spawn an Actor to replicate the information to each client. + if (SpatialSettings->bEnableMetricsDisplay) + { + NetDriver->SpatialMetricsDisplay = NetDriver->World->SpawnActor(); + } + if (SpatialSettings->SpatialDebugger != nullptr) + { + NetDriver->SpatialDebugger = NetDriver->World->SpawnActor(SpatialSettings->SpatialDebugger); + } + } +#endif // If we're loading from a snapshot, we shouldn't try and call BeginPlay with authority. - if (bCanSpawnWithAuthority) + for (TActorIterator ActorIterator(NetDriver->World); ActorIterator; ++ActorIterator) { - SetAllActorRolesBasedOnLBStrategy(); + HandleActorBasedOnLoadBalancer(*ActorIterator); } NetDriver->World->GetWorldSettings()->SetGSMReadyForPlay(); NetDriver->World->GetWorldSettings()->NotifyBeginPlay(); + + // Hmm - this seems necessary because unless we call this after NotifyBeginPlay has been triggered, it won't actually + // do anything, because internally it checks that BeginPlay has actually been called. I'm not sure why we called + // SetAcceptingPlayers above though unless it was only to catch the non-auth server instances. In which case the auth + // server is failing to call SetAcceptingPlayers again at some later point. + // + // I've now removed it from the other places it used to be called, because I believe they were both neither no longer + // valid. Above because the world tick won't have begun, and during the deployment man auth gained, for the same reason. + // Leaving this comment block in for review reasons but will remove before merging. + SetAcceptingPlayers(true); } bool UGlobalStateManager::GetCanBeginPlay() const @@ -410,12 +463,13 @@ bool UGlobalStateManager::GetCanBeginPlay() const bool UGlobalStateManager::IsReady() const { - return GetCanBeginPlay() || NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID); + return GetCanBeginPlay() + || StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID); } void UGlobalStateManager::SendCanBeginPlayUpdate(const bool bInCanBeginPlay) { - check(NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID)); + check(StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID)); bCanBeginPlay = bInCanBeginPlay; @@ -430,8 +484,8 @@ void UGlobalStateManager::SendCanBeginPlayUpdate(const bool bInCanBeginPlay) } // Queries for the GlobalStateManager in the deployment. -// 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. +// 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) { // Build a constraint for the GSM. @@ -444,14 +498,12 @@ void UGlobalStateManager::QueryGSM(const QueryDelegate& Callback) Worker_EntityQuery GSMQuery{}; GSMQuery.constraint = GSMConstraint; - GSMQuery.result_type = WORKER_RESULT_TYPE_SNAPSHOT; Worker_RequestId RequestID; - RequestID = NetDriver->Connection->SendEntityQueryRequest(&GSMQuery); + RequestID = NetDriver->Connection->SendEntityQueryRequest(&GSMQuery, RETRY_UNTIL_COMPLETE); EntityQueryDelegate GSMQueryDelegate; - GSMQueryDelegate.BindLambda([this, Callback](const Worker_EntityQueryResponseOp& Op) - { + GSMQueryDelegate.BindLambda([this, Callback](const Worker_EntityQueryResponseOp& Op) { if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) { UE_LOG(LogGlobalStateManager, Warning, TEXT("Could not find GSM via entity query: %s"), UTF8_TO_TCHAR(Op.message)); @@ -479,24 +531,22 @@ void UGlobalStateManager::QueryTranslation() } // Build a constraint for the Virtual Worker Translation. - Worker_ComponentConstraint TranslationComponentConstraint{}; + Worker_ComponentConstraint TranslationComponentConstraint; TranslationComponentConstraint.component_id = SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID; - Worker_Constraint TranslationConstraint{}; + Worker_Constraint TranslationConstraint; TranslationConstraint.constraint_type = WORKER_CONSTRAINT_TYPE_COMPONENT; TranslationConstraint.constraint.component_constraint = TranslationComponentConstraint; Worker_EntityQuery TranslationQuery{}; TranslationQuery.constraint = TranslationConstraint; - TranslationQuery.result_type = WORKER_RESULT_TYPE_SNAPSHOT; - Worker_RequestId RequestID = NetDriver->Connection->SendEntityQueryRequest(&TranslationQuery); + Worker_RequestId RequestID = NetDriver->Connection->SendEntityQueryRequest(&TranslationQuery, RETRY_UNTIL_COMPLETE); bTranslationQueryInFlight = true; TWeakObjectPtr WeakGlobalStateManager(this); EntityQueryDelegate TranslationQueryDelegate; - TranslationQueryDelegate.BindLambda([WeakGlobalStateManager](const Worker_EntityQueryResponseOp& Op) - { + TranslationQueryDelegate.BindLambda([WeakGlobalStateManager](const Worker_EntityQueryResponseOp& Op) { if (!WeakGlobalStateManager.IsValid()) { // The GSM was destroyed before receiving the response. @@ -537,12 +587,13 @@ void UGlobalStateManager::ApplyDeploymentMapDataFromQueryResponse(const Worker_E Worker_ComponentData Data = Op.results[0].components[i]; if (Data.component_id == SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID) { - ApplyDeploymentMapData(Data); + ApplyDeploymentMapData(Data.schema_type); } } } -bool UGlobalStateManager::GetAcceptingPlayersAndSessionIdFromQueryResponse(const Worker_EntityQueryResponseOp& Op, bool& OutAcceptingPlayers, int32& OutSessionId) +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")); @@ -576,14 +627,15 @@ bool UGlobalStateManager::GetAcceptingPlayersAndSessionIdFromQueryResponse(const } } - UE_LOG(LogGlobalStateManager, Warning, TEXT("Entity query response for the GSM did not contain both AcceptingPlayers and SessionId states.")); + UE_LOG(LogGlobalStateManager, Warning, + TEXT("Entity query response for the GSM did not contain both AcceptingPlayers and SessionId states.")); return false; } void UGlobalStateManager::SetDeploymentMapURL(const FString& MapURL) { - UE_LOG(LogGlobalStateManager, Log, TEXT("Setting DeploymentMapURL: %s"), *MapURL); + UE_LOG(LogGlobalStateManager, Verbose, TEXT("Setting DeploymentMapURL: %s"), *MapURL); DeploymentMapURL = MapURL; } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/ClientServerRPCService.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/ClientServerRPCService.cpp new file mode 100644 index 0000000000..bbf2982c11 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/ClientServerRPCService.cpp @@ -0,0 +1,405 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Interop/RPCs/ClientServerRPCService.h" + +#include "EngineClasses/SpatialPackageMapClient.h" +#include "Interop/SpatialStaticComponentView.h" +#include "Schema/ClientEndpoint.h" +#include "Schema/ServerEndpoint.h" +#include "SpatialConstants.h" + +DEFINE_LOG_CATEGORY(LogClientServerRPCService); + +namespace SpatialGDK +{ +ClientServerRPCService::ClientServerRPCService(const ExtractRPCDelegate InExtractRPCCallback, const FSubView& InSubView, + USpatialNetDriver* InNetDriver, FRPCStore& InRPCStore) + : ExtractRPCCallback(InExtractRPCCallback) + , SubView(&InSubView) + , NetDriver(InNetDriver) + , RPCStore(&InRPCStore) +{ +} + +void ClientServerRPCService::AdvanceView() +{ + const FSubViewDelta& SubViewDelta = SubView->GetViewDelta(); + for (const EntityDelta& Delta : SubViewDelta.EntityDeltas) + { + switch (Delta.Type) + { + case EntityDelta::UPDATE: + { + for (const ComponentChange& Change : Delta.ComponentUpdates) + { + if (IsClientOrServerEndpoint(Change.ComponentId)) + { + ApplyComponentUpdate(Delta.EntityId, Change.ComponentId, Change.Update); + } + } + break; + } + case EntityDelta::ADD: + PopulateDataStore(Delta.EntityId); + SetEntityData(Delta.EntityId); + break; + case EntityDelta::REMOVE: + ClientServerDataStore.Remove(Delta.EntityId); + break; + case EntityDelta::TEMPORARILY_REMOVED: + ClientServerDataStore.Remove(Delta.EntityId); + PopulateDataStore(Delta.EntityId); + SetEntityData(Delta.EntityId); + break; + default: + break; + } + } +} + +void ClientServerRPCService::ProcessChanges() +{ + const FSubViewDelta& SubViewDelta = SubView->GetViewDelta(); + for (const EntityDelta& Delta : SubViewDelta.EntityDeltas) + { + switch (Delta.Type) + { + case EntityDelta::UPDATE: + { + for (const ComponentChange& Change : Delta.ComponentUpdates) + { + ComponentUpdate(Delta.EntityId, Change.ComponentId, Change.Update); + } + break; + } + case EntityDelta::ADD: + EntityAdded(Delta.EntityId); + break; + case EntityDelta::TEMPORARILY_REMOVED: + EntityAdded(Delta.EntityId); + break; + default: + break; + } + } +} + +bool ClientServerRPCService::ContainsOverflowedRPC(const EntityRPCType& EntityRPC) const +{ + return OverflowedRPCs.Contains(EntityRPC); +} + +TMap>& ClientServerRPCService::GetOverflowedRPCs() +{ + return OverflowedRPCs; +} + +void ClientServerRPCService::AddOverflowedRPC(const EntityRPCType EntityType, PendingRPCPayload&& Payload) +{ + OverflowedRPCs.FindOrAdd(EntityType).Add(MoveTemp(Payload)); +} + +void ClientServerRPCService::IncrementAckedRPCID(const Worker_EntityId EntityId, const ERPCType Type) +{ + const EntityRPCType EntityTypePair = EntityRPCType(EntityId, Type); + uint64* LastAckedRPCId = LastAckedRPCIds.Find(EntityTypePair); + if (LastAckedRPCId == nullptr) + { + UE_LOG(LogClientServerRPCService, Warning, + TEXT("ClientServerRPCService::IncrementAckedRPCID: Could not find last acked RPC id. Entity: %lld, RPC type: %s"), EntityId, + *SpatialConstants::RPCTypeToString(Type)); + return; + } + + ++(*LastAckedRPCId); + + const EntityComponentId EntityComponentPair = { EntityId, RPCRingBufferUtils::GetAckComponentId(Type) }; + Schema_Object* EndpointObject = Schema_GetComponentUpdateFields(RPCStore->GetOrCreateComponentUpdate(EntityComponentPair)); + + RPCRingBufferUtils::WriteAckToSchema(EndpointObject, Type, *LastAckedRPCId); +} + +uint64 ClientServerRPCService::GetAckFromView(const Worker_EntityId EntityId, const ERPCType Type) +{ + switch (Type) + { + case ERPCType::ClientReliable: + return ClientServerDataStore[EntityId].Client.ReliableRPCAck; + case ERPCType::ClientUnreliable: + return ClientServerDataStore[EntityId].Client.UnreliableRPCAck; + case ERPCType::ServerReliable: + return ClientServerDataStore[EntityId].Server.ReliableRPCAck; + case ERPCType::ServerUnreliable: + return ClientServerDataStore[EntityId].Server.UnreliableRPCAck; + default: + checkNoEntry(); + return 0; + } +} + +void ClientServerRPCService::SetEntityData(Worker_EntityId EntityId) +{ + for (const Worker_ComponentId ComponentId : SubView->GetView()[EntityId].Authority) + { + OnEndpointAuthorityGained(EntityId, ComponentId); + } +} + +void ClientServerRPCService::EntityAdded(const Worker_EntityId EntityId) +{ + for (const Worker_ComponentId ComponentId : SubView->GetView()[EntityId].Authority) + { + ExtractRPCsForEntity(EntityId, ComponentId == SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID + ? SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID + : SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID); + } +} + +void ClientServerRPCService::ComponentUpdate(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId, + Schema_ComponentUpdate* Update) +{ + if (!IsClientOrServerEndpoint(ComponentId)) + { + return; + } + HandleRPC(EntityId, ComponentId); +} + +void ClientServerRPCService::PopulateDataStore(const Worker_EntityId EntityId) +{ + const EntityViewElement& Entity = SubView->GetView()[EntityId]; + const ClientEndpoint Client = ClientEndpoint( + Entity.Components.FindByPredicate(ComponentIdEquality{ SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID })->GetUnderlying()); + const ServerEndpoint Server = ServerEndpoint( + Entity.Components.FindByPredicate(ComponentIdEquality{ SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID })->GetUnderlying()); + ClientServerDataStore.Emplace(EntityId, ClientServerEndpoints{ Client, Server }); +} + +void ClientServerRPCService::ApplyComponentUpdate(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId, + Schema_ComponentUpdate* Update) +{ + switch (ComponentId) + { + case SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID: + ClientServerDataStore[EntityId].Client.ApplyComponentUpdate(Update); + break; + case SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID: + ClientServerDataStore[EntityId].Server.ApplyComponentUpdate(Update); + break; + default: + break; + } +} + +void ClientServerRPCService::OnEndpointAuthorityGained(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +{ + switch (ComponentId) + { + case SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID: + { + const ClientEndpoint& Endpoint = ClientServerDataStore[EntityId].Client; + LastSeenRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientReliable), Endpoint.ReliableRPCAck); + LastSeenRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientUnreliable), Endpoint.UnreliableRPCAck); + LastAckedRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientReliable), Endpoint.ReliableRPCAck); + LastAckedRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientUnreliable), Endpoint.UnreliableRPCAck); + RPCStore->LastSentRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerReliable), Endpoint.ReliableRPCBuffer.LastSentRPCId); + RPCStore->LastSentRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerUnreliable), Endpoint.UnreliableRPCBuffer.LastSentRPCId); + break; + } + case SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID: + { + const ServerEndpoint& Endpoint = ClientServerDataStore[EntityId].Server; + LastSeenRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerReliable), Endpoint.ReliableRPCAck); + LastSeenRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerUnreliable), Endpoint.UnreliableRPCAck); + LastAckedRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerReliable), Endpoint.ReliableRPCAck); + LastAckedRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerUnreliable), Endpoint.UnreliableRPCAck); + RPCStore->LastSentRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientReliable), Endpoint.ReliableRPCBuffer.LastSentRPCId); + RPCStore->LastSentRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientUnreliable), Endpoint.UnreliableRPCBuffer.LastSentRPCId); + break; + } + default: + checkNoEntry(); + break; + } +} + +void ClientServerRPCService::OnEndpointAuthorityLost(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +{ + switch (ComponentId) + { + case SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID: + { + LastSeenRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ClientReliable)); + LastSeenRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ClientUnreliable)); + LastAckedRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ClientReliable)); + LastAckedRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ClientUnreliable)); + RPCStore->LastSentRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerReliable)); + RPCStore->LastSentRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerUnreliable)); + ClearOverflowedRPCs(EntityId); + break; + } + case SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID: + { + LastSeenRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerReliable)); + LastSeenRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerUnreliable)); + LastAckedRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerReliable)); + LastAckedRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerUnreliable)); + RPCStore->LastSentRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ClientReliable)); + RPCStore->LastSentRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ClientUnreliable)); + ClearOverflowedRPCs(EntityId); + break; + } + default: + checkNoEntry(); + break; + } +} + +void ClientServerRPCService::ClearOverflowedRPCs(const Worker_EntityId EntityId) +{ + for (uint8 RPCType = static_cast(ERPCType::ClientReliable); RPCType <= static_cast(ERPCType::NetMulticast); RPCType++) + { + OverflowedRPCs.Remove(EntityRPCType(EntityId, static_cast(RPCType))); + } +} + +void ClientServerRPCService::HandleRPC(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +{ + // 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 = ComponentId == SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID; + if (bIsServerRpc && SubView->HasAuthority(EntityId, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID)) + { + const TWeakObjectPtr ActorReceivingRPC = NetDriver->PackageMap->GetObjectFromEntityId(EntityId); + if (!ActorReceivingRPC.IsValid()) + { + UE_LOG(LogClientServerRPCService, Log, + TEXT("Entity receiving ring buffer RPC does not exist in PackageMap, possibly due to corresponding actor getting " + "destroyed. Entity: %lld, Component: %d"), + EntityId, ComponentId); + return; + } + + const bool bActorRoleIsSimulatedProxy = Cast(ActorReceivingRPC.Get())->Role == ROLE_SimulatedProxy; + if (bActorRoleIsSimulatedProxy) + { + UE_LOG(LogClientServerRPCService, Verbose, + TEXT("Will not process server RPC, Actor role changed to SimulatedProxy. This happens on migration. Entity: %lld"), + EntityId); + return; + } + } + ExtractRPCsForEntity(EntityId, ComponentId); +} + +void ClientServerRPCService::ExtractRPCsForEntity(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +{ + switch (ComponentId) + { + case SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID: + ExtractRPCsForType(EntityId, ERPCType::ServerReliable); + ExtractRPCsForType(EntityId, ERPCType::ServerUnreliable); + break; + case SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID: + ExtractRPCsForType(EntityId, ERPCType::ClientReliable); + ExtractRPCsForType(EntityId, ERPCType::ClientUnreliable); + break; + default: + checkNoEntry(); + break; + } +} + +void ClientServerRPCService::ExtractRPCsForType(const Worker_EntityId EntityId, const ERPCType Type) +{ + const EntityRPCType EntityTypePair = EntityRPCType(EntityId, Type); + + if (!LastSeenRPCIds.Contains(EntityTypePair)) + { + UE_LOG(LogClientServerRPCService, Warning, + TEXT("Tried to extract RPCs but no entry in Last Seen Map! This can happen after server travel. Entity: %lld, type: %s"), + EntityId, *SpatialConstants::RPCTypeToString(Type)); + return; + } + const uint64 LastSeenRPCId = LastSeenRPCIds[EntityTypePair]; + + const RPCRingBuffer& Buffer = GetBufferFromView(EntityId, Type); + + uint64 LastProcessedRPCId = LastSeenRPCId; + if (Buffer.LastSentRPCId >= LastSeenRPCId) + { + uint64 FirstRPCIdToRead = LastSeenRPCId + 1; + + const uint32 BufferSize = RPCRingBufferUtils::GetRingBufferSize(Type); + if (Buffer.LastSentRPCId > LastSeenRPCId + BufferSize) + { + UE_LOG(LogClientServerRPCService, Warning, + TEXT("ClientServerRPCService::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()) + { + ExtractRPCCallback.Execute(FUnrealObjectRef(EntityId, Element.GetValue().Offset), Element.GetValue(), RPCId); + LastProcessedRPCId = RPCId; + } + else + { + UE_LOG( + LogClientServerRPCService, Warning, + TEXT("ClientServerRPCService::ExtractRPCsForType: Ring buffer element empty. Entity: %lld, RPC type: %s, empty element " + "RPC id: %d"), + EntityId, *SpatialConstants::RPCTypeToString(Type), RPCId); + } + } + } + else + { + UE_LOG( + LogClientServerRPCService, Warning, + TEXT("ClientServerRPCService::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) + { + LastSeenRPCIds[EntityTypePair] = LastProcessedRPCId; + } +} + +const RPCRingBuffer& ClientServerRPCService::GetBufferFromView(const Worker_EntityId EntityId, const ERPCType Type) +{ + switch (Type) + { + // Server sends Client RPCs, so ClientReliable & ClientUnreliable buffers live on ServerEndpoint. + case ERPCType::ClientReliable: + return ClientServerDataStore[EntityId].Server.ReliableRPCBuffer; + case ERPCType::ClientUnreliable: + return ClientServerDataStore[EntityId].Server.UnreliableRPCBuffer; + + // Client sends Server RPCs, so ServerReliable & ServerUnreliable buffers live on ClientEndpoint. + case ERPCType::ServerReliable: + return ClientServerDataStore[EntityId].Client.ReliableRPCBuffer; + case ERPCType::ServerUnreliable: + return ClientServerDataStore[EntityId].Client.UnreliableRPCBuffer; + default: + checkNoEntry(); + static const RPCRingBuffer DummyBuffer(ERPCType::Invalid); + return DummyBuffer; + } +} + +bool ClientServerRPCService::IsClientOrServerEndpoint(const Worker_ComponentId ComponentId) +{ + return ComponentId == SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID || ComponentId == SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID; +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/MulticastRPCService.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/MulticastRPCService.cpp new file mode 100644 index 0000000000..a677836969 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/MulticastRPCService.cpp @@ -0,0 +1,259 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Interop/RPCs/MulticastRPCService.h" + +#include "EngineClasses/SpatialPackageMapClient.h" +#include "Interop/RPCs/SpatialRPCService.h" +#include "Schema/MulticastRPCs.h" +#include "SpatialConstants.h" +#include "Utils/RepLayoutUtils.h" + +DEFINE_LOG_CATEGORY(LogMulticastRPCService); + +namespace SpatialGDK +{ +MulticastRPCService::MulticastRPCService(const ExtractRPCDelegate InExtractRPCCallback, const FSubView& InSubView, FRPCStore& InRPCStore) + : ExtractRPCCallback(InExtractRPCCallback) + , SubView(&InSubView) + , RPCStore(&InRPCStore) +{ +} + +void MulticastRPCService::AdvanceView() +{ + const FSubViewDelta& SubViewDelta = SubView->GetViewDelta(); + for (const EntityDelta& Delta : SubViewDelta.EntityDeltas) + { + switch (Delta.Type) + { + case EntityDelta::UPDATE: + { + for (const AuthorityChange& Change : Delta.AuthorityLostTemporarily) + { + // We process auth lost temporarily twice. Once before updates and once after, so as not + // to process updates that we received while we think we are still authoritiative. + AuthorityLost(Delta.EntityId, Change.ComponentId); + } + for (const AuthorityChange& Change : Delta.AuthorityLost) + { + AuthorityLost(Delta.EntityId, Change.ComponentId); + } + for (const ComponentChange& Change : Delta.ComponentUpdates) + { + if (Change.ComponentId == SpatialConstants::MULTICAST_RPCS_COMPONENT_ID) + { + ApplyComponentUpdate(Delta.EntityId, Change.Update); + } + else if (Change.ComponentId > SpatialConstants::MULTICAST_RPCS_COMPONENT_ID) + { + break; + } + } + for (const AuthorityChange& Change : Delta.AuthorityGained) + { + AuthorityGained(Delta.EntityId, Change.ComponentId); + } + for (const AuthorityChange& Change : Delta.AuthorityLostTemporarily) + { + // Updates that we could have received while we weren't authoritative have now been processed. + // Regain authority. + AuthorityGained(Delta.EntityId, Change.ComponentId); + } + } + case EntityDelta::ADD: + PopulateDataStore(Delta.EntityId); + break; + case EntityDelta::REMOVE: + OnRemoveMulticastRPCComponentForEntity(Delta.EntityId); + MulticastDataStore.Remove(Delta.EntityId); + break; + case EntityDelta::TEMPORARILY_REMOVED: + OnRemoveMulticastRPCComponentForEntity(Delta.EntityId); + MulticastDataStore.Remove(Delta.EntityId); + PopulateDataStore(Delta.EntityId); + break; + default: + break; + } + } +} + +void MulticastRPCService::ProcessChanges() +{ + const FSubViewDelta& SubViewDelta = SubView->GetViewDelta(); + for (const EntityDelta& Delta : SubViewDelta.EntityDeltas) + { + switch (Delta.Type) + { + case EntityDelta::UPDATE: + { + for (const ComponentChange& Change : Delta.ComponentUpdates) + { + ComponentUpdate(Delta.EntityId, Change.ComponentId, Change.Update); + } + break; + } + case EntityDelta::ADD: + EntityAdded(Delta.EntityId); + break; + case EntityDelta::TEMPORARILY_REMOVED: + EntityAdded(Delta.EntityId); + break; + default: + break; + } + } +} + +void MulticastRPCService::EntityAdded(const Worker_EntityId EntityId) +{ + OnCheckoutMulticastRPCComponentOnEntity(EntityId); + for (const Worker_ComponentId ComponentId : SubView->GetView()[EntityId].Authority) + { + AuthorityGained(EntityId, ComponentId); + } +} + +void MulticastRPCService::ComponentUpdate(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId, + Schema_ComponentUpdate* Update) +{ + if (ComponentId != SpatialConstants::MULTICAST_RPCS_COMPONENT_ID) + { + return; + } + ExtractRPCs(EntityId); +} + +void MulticastRPCService::AuthorityGained(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +{ + if (ComponentId != SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) + { + return; + } + OnEndpointAuthorityGained(EntityId, ComponentId); +} + +void MulticastRPCService::AuthorityLost(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +{ + if (ComponentId != SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) + { + return; + } + OnEndpointAuthorityLost(EntityId, ComponentId); +} + +void MulticastRPCService::PopulateDataStore(const Worker_EntityId EntityId) +{ + MulticastDataStore.Emplace( + EntityId, MulticastRPCs(SubView->GetView()[EntityId] + .Components.FindByPredicate(ComponentIdEquality{ SpatialConstants::MULTICAST_RPCS_COMPONENT_ID }) + ->GetUnderlying())); +} + +void MulticastRPCService::ApplyComponentUpdate(const Worker_EntityId EntityId, Schema_ComponentUpdate* Update) +{ + MulticastDataStore[EntityId].ApplyComponentUpdate(Update); +} + +void MulticastRPCService::OnCheckoutMulticastRPCComponentOnEntity(const Worker_EntityId EntityId) +{ + const MulticastRPCs& Component = MulticastDataStore[EntityId]; + + // When checking out entity, ignore multicast RPCs that are already on the component. + LastSeenMulticastRPCIds.Add(EntityId, Component.MulticastRPCBuffer.LastSentRPCId); +} + +void MulticastRPCService::OnRemoveMulticastRPCComponentForEntity(const Worker_EntityId EntityId) +{ + LastSeenMulticastRPCIds.Remove(EntityId); +} + +void MulticastRPCService::OnEndpointAuthorityGained(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +{ + const MulticastRPCs& Component = MulticastDataStore[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. + RPCStore->LastSentRPCIds.Add(EntityRPCType(EntityId, ERPCType::NetMulticast), Component.InitiallyPresentMulticastRPCsCount); + + const RPCRingBufferDescriptor Descriptor = RPCRingBufferUtils::GetRingBufferDescriptor(ERPCType::NetMulticast); + Schema_Object* SchemaObject = Schema_GetComponentUpdateFields( + RPCStore->GetOrCreateComponentUpdate(EntityComponentId{ EntityId, SpatialConstants::MULTICAST_RPCS_COMPONENT_ID })); + Schema_AddUint64(SchemaObject, Descriptor.LastSentRPCFieldId, Component.InitiallyPresentMulticastRPCsCount); + } + else + { + RPCStore->LastSentRPCIds.Add(EntityRPCType(EntityId, ERPCType::NetMulticast), Component.MulticastRPCBuffer.LastSentRPCId); + } +} + +void MulticastRPCService::OnEndpointAuthorityLost(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +{ + // Set last seen to last sent, so we don't process own RPCs after crossing the boundary. + LastSeenMulticastRPCIds.Add(EntityId, RPCStore->LastSentRPCIds[EntityRPCType(EntityId, ERPCType::NetMulticast)]); + RPCStore->LastSentRPCIds.Remove(EntityRPCType(EntityId, ERPCType::NetMulticast)); +} + +void MulticastRPCService::ExtractRPCs(const Worker_EntityId EntityId) +{ + if (!LastSeenMulticastRPCIds.Contains(EntityId)) + { + UE_LOG(LogMulticastRPCService, Warning, + TEXT("Tried to extract RPCs but no entry in Last Seen Map! This can happen after server travel. Entity: %lld, type: " + "Multicast"), + EntityId); + return; + } + const uint64 LastSeenRPCId = LastSeenMulticastRPCIds[EntityId]; + + const RPCRingBuffer& Buffer = MulticastDataStore[EntityId].MulticastRPCBuffer; + + uint64 LastProcessedRPCId = LastSeenRPCId; + if (Buffer.LastSentRPCId >= LastSeenRPCId) + { + uint64 FirstRPCIdToRead = LastSeenRPCId + 1; + + const uint32 BufferSize = RPCRingBufferUtils::GetRingBufferSize(ERPCType::NetMulticast); + if (Buffer.LastSentRPCId > LastSeenRPCId + BufferSize) + { + UE_LOG( + LogMulticastRPCService, Warning, + TEXT("MulticastRPCService::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(ERPCType::NetMulticast), 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()) + { + ExtractRPCCallback.Execute(FUnrealObjectRef(EntityId, Element.GetValue().Offset), Element.GetValue(), RPCId); + LastProcessedRPCId = RPCId; + } + else + { + UE_LOG(LogMulticastRPCService, Warning, + TEXT("MulticastRPCService::ExtractRPCsForType: Ring buffer element empty. Entity: %lld, RPC type: %s, empty element " + "RPC id: %d"), + EntityId, *SpatialConstants::RPCTypeToString(ERPCType::NetMulticast), RPCId); + } + } + } + else + { + UE_LOG(LogMulticastRPCService, Warning, + TEXT("MulticastRPCService::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(ERPCType::NetMulticast), Buffer.LastSentRPCId, LastSeenRPCId); + } + + if (LastProcessedRPCId > LastSeenRPCId) + { + LastSeenMulticastRPCIds[EntityId] = LastProcessedRPCId; + } +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/RPCStore.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/RPCStore.cpp new file mode 100644 index 0000000000..11105ecee8 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/RPCStore.cpp @@ -0,0 +1,36 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Interop/RPCs/RPCStore.h" + +namespace SpatialGDK +{ +Schema_ComponentUpdate* FRPCStore::GetOrCreateComponentUpdate(const EntityComponentId EntityComponentIdPair, + const FSpatialGDKSpanId& SpanId /*= {}*/) +{ + PendingUpdate* ComponentUpdatePtr = PendingComponentUpdatesToSend.Find(EntityComponentIdPair); + if (ComponentUpdatePtr == nullptr) + { + ComponentUpdatePtr = &PendingComponentUpdatesToSend.Emplace(EntityComponentIdPair, Schema_CreateComponentUpdate()); + } + return ComponentUpdatePtr->Update; +} + +void FRPCStore::AddSpanIdForComponentUpdate(EntityComponentId EntityComponentIdPair, const FSpatialGDKSpanId& SpanId) +{ + PendingUpdate* ComponentUpdatePtr = PendingComponentUpdatesToSend.Find(EntityComponentIdPair); + if (ComponentUpdatePtr != nullptr) + { + ComponentUpdatePtr->SpanIds.Add(SpanId); + } +} + +Schema_ComponentData* FRPCStore::GetOrCreateComponentData(const 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/RPCs/SpatialRPCService.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/SpatialRPCService.cpp new file mode 100644 index 0000000000..6eb0baa756 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/RPCs/SpatialRPCService.cpp @@ -0,0 +1,549 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Interop/RPCs/SpatialRPCService.h" + +#include "EngineClasses/SpatialNetBitReader.h" +#include "EngineClasses/SpatialPackageMapClient.h" +#include "Interop/Connection/SpatialTraceEventBuilder.h" +#include "Interop/SpatialStaticComponentView.h" +#include "SpatialConstants.h" +#include "Utils/RepLayoutUtils.h" +#include "Utils/SpatialLatencyTracer.h" + +DEFINE_LOG_CATEGORY(LogSpatialRPCService); + +namespace SpatialGDK +{ +SpatialRPCService::SpatialRPCService(const FSubView& InActorAuthSubView, const FSubView& InActorNonAuthSubView, + USpatialLatencyTracer* InSpatialLatencyTracer, SpatialEventTracer* InEventTracer, + USpatialNetDriver* InNetDriver) + : NetDriver(InNetDriver) + , SpatialLatencyTracer(InSpatialLatencyTracer) + , EventTracer(InEventTracer) + , RPCStore(FRPCStore()) + , ClientServerRPCs(ExtractRPCDelegate::CreateRaw(this, &SpatialRPCService::ProcessOrQueueIncomingRPC), InActorAuthSubView, InNetDriver, + RPCStore) + , MulticastRPCs(ExtractRPCDelegate::CreateRaw(this, &SpatialRPCService::ProcessOrQueueIncomingRPC), InActorNonAuthSubView, RPCStore) + , AuthSubView(&InActorAuthSubView) + , LastProcessingTime(-GetDefault()->QueuedIncomingRPCRetryTime) +{ + IncomingRPCs.BindProcessingFunction(FProcessRPCDelegate::CreateRaw(this, &SpatialRPCService::ApplyRPC)); +} + +void SpatialRPCService::AdvanceView() +{ + ClientServerRPCs.AdvanceView(); + MulticastRPCs.AdvanceView(); +} + +void SpatialRPCService::ProcessChanges(const float NetDriverTime) +{ + ClientServerRPCs.ProcessChanges(); + MulticastRPCs.ProcessChanges(); + + if (NetDriverTime - LastProcessingTime > GetDefault()->QueuedIncomingRPCRetryTime) + { + LastProcessingTime = NetDriverTime; + ProcessIncomingRPCs(); + } +} + +void SpatialRPCService::ProcessIncomingRPCs() +{ + IncomingRPCs.ProcessRPCs(); +} + +EPushRPCResult SpatialRPCService::PushRPC(const Worker_EntityId EntityId, const ERPCType Type, RPCPayload Payload, + const bool bCreatedEntity, UObject* Target, UFunction* Function) +{ + const EntityRPCType EntityType = EntityRPCType(EntityId, Type); + + EPushRPCResult Result = EPushRPCResult::Success; + PendingRPCPayload PendingPayload = { Payload }; + + if (EventTracer != nullptr) + { + PendingPayload.SpanId = EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreatePushRPC(Target, Function), + EventTracer->GetFromStack().GetConstId(), 1); + } + +#if TRACE_LIB_ACTIVE + TraceKey Trace = Payload.Trace; +#endif + + if (RPCRingBufferUtils::ShouldQueueOverflowed(Type) && ClientServerRPCs.ContainsOverflowedRPC(EntityType)) + { + if (EventTracer != nullptr) + { + PendingPayload.SpanId = + EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateQueueRPC(), PendingPayload.SpanId.GetConstId(), 1); + } + + // Already has queued RPCs of this type, queue until those are pushed. + ClientServerRPCs.AddOverflowedRPC(EntityType, MoveTemp(PendingPayload)); + Result = EPushRPCResult::QueueOverflowed; + } + else + { + Result = PushRPCInternal(EntityId, Type, PendingPayload, bCreatedEntity); + if (Result == EPushRPCResult::QueueOverflowed) + { + ClientServerRPCs.AddOverflowedRPC(EntityType, MoveTemp(PendingPayload)); + } + } + +#if TRACE_LIB_ACTIVE + ProcessResultToLatencyTrace(Result, Trace); +#endif + + return Result; +} + +void SpatialRPCService::PushOverflowedRPCs() +{ + for (auto It = ClientServerRPCs.GetOverflowedRPCs().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 (PendingRPCPayload& Payload : OverflowedRPCArray) + { + const EPushRPCResult Result = PushRPCInternal(EntityId, Type, Payload, false); + + switch (Result) + { + case EPushRPCResult::Success: + NumProcessed++; + break; + case EPushRPCResult::QueueOverflowed: + if (NumProcessed > 0) + { + UE_LOG(LogSpatialRPCService, Log, + TEXT("SpatialRPCService::PushOverflowedRPCs: Sent some but not all overflowed RPCs. RPCs sent %d, RPCs still " + "overflowed: %d, Entity: %lld, RPC type: %s"), + NumProcessed, OverflowedRPCArray.Num() - NumProcessed, EntityId, *SpatialConstants::RPCTypeToString(Type)); + } + if (EventTracer != nullptr) + { + Payload.SpanId = EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateQueueRPC(), Payload.SpanId.GetConstId(), 1); + } + 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; + default: + checkNoEntry(); + } +#if TRACE_LIB_ACTIVE + ProcessResultToLatencyTrace(Result, Payload.Payload.Trace); +#endif + + // This includes the valid case of RPCs still overflowing (EPushRPCResult::QueueOverflowed), as well as the error cases. + if (Result != EPushRPCResult::Success) + { + break; + } + } + + if (NumProcessed == OverflowedRPCArray.Num() || bShouldDrop) + { + It.RemoveCurrent(); + } + else + { + OverflowedRPCArray.RemoveAt(0, NumProcessed); + } + } +} + +TArray SpatialRPCService::GetRPCsAndAcksToSend() +{ + TArray UpdatesToSend; + + for (auto& It : RPCStore.PendingComponentUpdatesToSend) + { + UpdateToSend& UpdateToSend = UpdatesToSend.AddZeroed_GetRef(); + UpdateToSend.EntityId = It.Key.EntityId; + UpdateToSend.Update.component_id = It.Key.ComponentId; + UpdateToSend.Update.schema_type = It.Value.Update; + + if (EventTracer != nullptr) + { + UpdateToSend.SpanId = EventTracer->TraceEvent( + FSpatialTraceEventBuilder::CreateMergeSendRPCs(UpdateToSend.EntityId, UpdateToSend.Update.component_id), + It.Value.SpanIds.GetData()->GetConstId(), It.Value.SpanIds.Num()); + } + +#if TRACE_LIB_ACTIVE + TraceKey Trace = InvalidTraceKey; + PendingTraces.RemoveAndCopyValue(It.Key, Trace); + UpdateToSend.Update.Trace = Trace; +#endif + } + + RPCStore.PendingComponentUpdatesToSend.Empty(); + + return UpdatesToSend; +} + +TArray SpatialRPCService::GetRPCComponentsOnEntityCreation(const Worker_EntityId EntityId) +{ + static TArray EndpointComponentIds = { SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID, + SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID, + SpatialConstants::MULTICAST_RPCS_COMPONENT_ID }; + + TArray Components; + + for (Worker_ComponentId EndpointComponentId : EndpointComponentIds) + { + const EntityComponentId EntityComponent = { EntityId, EndpointComponentId }; + + FWorkerComponentData& Component = Components.Emplace_GetRef(FWorkerComponentData{}); + Component.component_id = EndpointComponentId; + if (Schema_ComponentData** ComponentData = RPCStore.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), + RPCStore.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; +#if TRACE_LIB_ACTIVE + TraceKey Trace = InvalidTraceKey; + PendingTraces.RemoveAndCopyValue(EntityComponent, Trace); + Component.Trace = Trace; +#endif + RPCStore.PendingRPCsOnEntityCreation.Remove(EntityComponent); + } + else + { + Component.schema_type = Schema_CreateComponentData(); + } + } + + return Components; +} + +void SpatialRPCService::ProcessOrQueueIncomingRPC(const FUnrealObjectRef& InTargetObjectRef, RPCPayload InPayload, + TOptional RPCIdForLinearEventTrace) +{ + const TWeakObjectPtr TargetObjectWeakPtr = NetDriver->PackageMap->GetObjectFromUnrealObjectRef(InTargetObjectRef); + if (!TargetObjectWeakPtr.IsValid()) + { + UE_LOG(LogSpatialRPCService, Verbose, TEXT("The object has been deleted, dropping the RPC")); + return; + } + + UObject* TargetObject = TargetObjectWeakPtr.Get(); + const FClassInfo& ClassInfo = NetDriver->ClassInfoManager->GetOrCreateClassInfoByObject(TargetObject); + + if (InPayload.Index >= static_cast(ClassInfo.RPCs.Num())) + { + // This should only happen if there's a class layout disagreement between workers, which would indicate incompatible binaries. + UE_LOG(LogSpatialRPCService, Error, TEXT("Invalid RPC index (%d) received on %s, dropping the RPC"), InPayload.Index, + *TargetObject->GetPathName()); + return; + } + UFunction* Function = ClassInfo.RPCs[InPayload.Index]; + if (Function == nullptr) + { + UE_LOG(LogSpatialRPCService, Error, TEXT("Missing function info received on %s, dropping the RPC"), *TargetObject->GetPathName()); + return; + } + + const FRPCInfo& RPCInfo = NetDriver->ClassInfoManager->GetRPCInfo(TargetObject, Function); + const ERPCType Type = RPCInfo.Type; + + IncomingRPCs.ProcessOrQueueRPC(InTargetObjectRef, Type, MoveTemp(InPayload), RPCIdForLinearEventTrace); +} + +void SpatialRPCService::ClearPendingRPCs(Worker_EntityId EntityId) +{ + IncomingRPCs.DropForEntity(EntityId); +} + +EPushRPCResult SpatialRPCService::PushRPCInternal(const Worker_EntityId EntityId, const ERPCType Type, PendingRPCPayload Payload, + const bool bCreatedEntity) +{ + const Worker_ComponentId RingBufferComponentId = RPCRingBufferUtils::GetRingBufferComponentId(Type); + const Worker_ComponentSetId RingBufferAuthComponentSetId = RPCRingBufferUtils::GetRingBufferAuthComponentSetId(Type); + + const EntityComponentId EntityComponent = { EntityId, RingBufferComponentId }; + const EntityRPCType EntityType = EntityRPCType(EntityId, Type); + + Schema_Object* EndpointObject; + uint64 LastAckedRPCId; + if (AuthSubView->HasComponent(EntityId, RingBufferComponentId)) + { + if (!AuthSubView->HasAuthority(EntityId, RingBufferAuthComponentSetId)) + { + if (bCreatedEntity) + { + return EPushRPCResult::EntityBeingCreated; + } + return EPushRPCResult::NoRingBufferAuthority; + } + + EndpointObject = Schema_GetComponentUpdateFields(RPCStore.GetOrCreateComponentUpdate(EntityComponent, Payload.SpanId)); + + if (Type == ERPCType::NetMulticast) + { + // Assume all multicast RPCs are auto-acked. + LastAckedRPCId = RPCStore.LastSentRPCIds.FindRef(EntityType); + } + else + { + // We shouldn't have authority over the component that has the acks. + if (AuthSubView->HasAuthority(EntityId, RPCRingBufferUtils::GetAckAuthComponentSetId(Type))) + { + return EPushRPCResult::HasAckAuthority; + } + + LastAckedRPCId = ClientServerRPCs.GetAckFromView(EntityId, Type); + } + } + else + { + if (bCreatedEntity) + { + return EPushRPCResult::EntityBeingCreated; + } + // If the entity isn't in the view, we assume this RPC was called before + // CreateEntityRequest, so we put it into a component data object. + EndpointObject = Schema_GetComponentDataFields(RPCStore.GetOrCreateComponentData(EntityComponent)); + + LastAckedRPCId = 0; + } + + const uint64 NewRPCId = RPCStore.LastSentRPCIds.FindRef(EntityType) + 1; + + // Check capacity. + if (LastAckedRPCId + RPCRingBufferUtils::GetRingBufferSize(Type) >= NewRPCId) + { + if (EventTracer != nullptr) + { + EventTraceUniqueId LinearTraceId = EventTraceUniqueId::GenerateForRPC(EntityId, static_cast(Type), NewRPCId); + FSpatialGDKSpanId SpanId = + EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateSendRPC(LinearTraceId), Payload.SpanId.GetConstId(), 1); + RPCStore.AddSpanIdForComponentUpdate(EntityComponent, SpanId); + } + + RPCRingBufferUtils::WriteRPCToSchema(EndpointObject, Type, NewRPCId, Payload.Payload); + +#if TRACE_LIB_ACTIVE + if (SpatialLatencyTracer != nullptr && Payload.Payload.Trace != InvalidTraceKey) + { + if (PendingTraces.Find(EntityComponent) == nullptr) + { + PendingTraces.Add(EntityComponent, Payload.Payload.Trace); + } + else + { + SpatialLatencyTracer->WriteAndEndTrace(Payload.Payload.Trace, + TEXT("Multiple rpc updates in single update, ending further stack tracing"), true); + } + } +#endif + + RPCStore.LastSentRPCIds.Add(EntityType, NewRPCId); + } + else + { + // Overflowed + if (RPCRingBufferUtils::ShouldQueueOverflowed(Type)) + { + return EPushRPCResult::QueueOverflowed; + } + else + { + return EPushRPCResult::DropOverflowed; + } + } + + return EPushRPCResult::Success; +} + +FRPCErrorInfo SpatialRPCService::ApplyRPC(const FPendingRPCParams& Params) +{ + TWeakObjectPtr TargetObjectWeakPtr = NetDriver->PackageMap->GetObjectFromUnrealObjectRef(Params.ObjectRef); + if (!TargetObjectWeakPtr.IsValid()) + { + return FRPCErrorInfo{ nullptr, nullptr, ERPCResult::UnresolvedTargetObject, ERPCQueueProcessResult::StopProcessing }; + } + + UObject* TargetObject = TargetObjectWeakPtr.Get(); + const FClassInfo& ClassInfo = NetDriver->ClassInfoManager->GetOrCreateClassInfoByObject(TargetObjectWeakPtr.Get()); + UFunction* Function = ClassInfo.RPCs[Params.Payload.Index]; + if (Function == nullptr) + { + return FRPCErrorInfo{ TargetObject, nullptr, ERPCResult::MissingFunctionInfo, ERPCQueueProcessResult::ContinueProcessing }; + } + + return ApplyRPCInternal(TargetObject, Function, Params); +} + +FRPCErrorInfo SpatialRPCService::ApplyRPCInternal(UObject* TargetObject, UFunction* Function, const FPendingRPCParams& PendingRPCParams) +{ + FRPCErrorInfo ErrorInfo = { TargetObject, Function, ERPCResult::UnresolvedParameters }; + + uint8* Parms = (uint8*)FMemory_Alloca(Function->ParmsSize); + FMemory::Memzero(Parms, Function->ParmsSize); + + TSet UnresolvedRefs; + { + TSet MappedRefs; + RPCPayload PayloadCopy = PendingRPCParams.Payload; + FSpatialNetBitReader PayloadReader(NetDriver->PackageMap, PayloadCopy.PayloadData.GetData(), PayloadCopy.CountDataBits(), + MappedRefs, UnresolvedRefs); + + TSharedPtr RepLayout = NetDriver->GetFunctionRepLayout(Function); + RepLayout_ReceivePropertiesForRPC(*RepLayout, PayloadReader, Parms); + } + + const USpatialGDKSettings* SpatialSettings = GetDefault(); + + const float TimeQueued = (FDateTime::Now() - PendingRPCParams.Timestamp).GetTotalSeconds(); + const int32 UnresolvedRefCount = UnresolvedRefs.Num(); + + if (UnresolvedRefCount == 0 || SpatialSettings->QueuedIncomingRPCWaitTime < TimeQueued) + { + if (UnresolvedRefCount > 0 && !SpatialSettings->ShouldRPCTypeAllowUnresolvedParameters(PendingRPCParams.Type) + && (Function->SpatialFunctionFlags & SPATIALFUNC_AllowUnresolvedParameters) == 0) + { + const FString UnresolvedEntityIds = FString::JoinBy(UnresolvedRefs, TEXT(", "), [](const FUnrealObjectRef& Ref) { + return Ref.ToString(); + }); + + UE_LOG(LogSpatialRPCService, Warning, + TEXT("Executed RPC %s::%s with unresolved references (%s) after %.3f seconds of queueing. Owner name: %s"), + *GetNameSafe(TargetObject), *GetNameSafe(Function), *UnresolvedEntityIds, TimeQueued, + *GetNameSafe(TargetObject->GetOuter())); + } + + // Get the RPC target Actor. + AActor* Actor = TargetObject->IsA() ? Cast(TargetObject) : TargetObject->GetTypedOuter(); + ERPCType RPCType = PendingRPCParams.Type; + + if (Actor->Role == ROLE_SimulatedProxy && (RPCType == ERPCType::ServerReliable || RPCType == ERPCType::ServerUnreliable)) + { + ErrorInfo.ErrorCode = ERPCResult::NoAuthority; + ErrorInfo.QueueProcessResult = ERPCQueueProcessResult::DropEntireQueue; + } + else + { + bool bUseEventTracer = + EventTracer != nullptr && RPCType != ERPCType::CrossServer && PendingRPCParams.RPCIdForLinearEventTrace.IsSet(); + if (bUseEventTracer) + { + Worker_ComponentId ComponentId = RPCRingBufferUtils::GetRingBufferComponentId(RPCType); + EntityComponentId Id = EntityComponentId(PendingRPCParams.ObjectRef.Entity, ComponentId); + FSpatialGDKSpanId CauseSpanId = EventTracer->GetSpanId(Id); + + EventTraceUniqueId LinearTraceId = EventTraceUniqueId::GenerateForRPC( + PendingRPCParams.ObjectRef.Entity, static_cast(RPCType), PendingRPCParams.RPCIdForLinearEventTrace.GetValue()); + FSpatialGDKSpanId SpanId = EventTracer->TraceEvent( + FSpatialTraceEventBuilder::CreateProcessRPC(TargetObject, Function, LinearTraceId), CauseSpanId.GetConstId(), 1); + EventTracer->AddToStack(SpanId); + } + + TargetObject->ProcessEvent(Function, Parms); + + if (bUseEventTracer) + { + EventTracer->PopFromStack(); + } + + if (RPCType != ERPCType::CrossServer && RPCType != ERPCType::NetMulticast) + { + ClientServerRPCs.IncrementAckedRPCID(PendingRPCParams.ObjectRef.Entity, RPCType); + } + + ErrorInfo.ErrorCode = ERPCResult::Success; + } + } + + // Destroy the parameters. + // warning: highly dependent on UObject::ProcessEvent freeing of parms! + for (TFieldIterator It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It) + { + It->DestroyValue_InContainer(Parms); + } + + return ErrorInfo; +} + +#if TRACE_LIB_ACTIVE +void SpatialRPCService::ProcessResultToLatencyTrace(const EPushRPCResult Result, const TraceKey Trace) +{ + if (SpatialLatencyTracer != nullptr && Trace != InvalidTraceKey) + { + bool bEndTrace = false; + FString TraceMsg; + switch (Result) + { + case SpatialGDK::EPushRPCResult::Success: + // No further action + break; + case SpatialGDK::EPushRPCResult::QueueOverflowed: + TraceMsg = TEXT("Overflowed"); + break; + case SpatialGDK::EPushRPCResult::DropOverflowed: + TraceMsg = TEXT("OverflowedAndDropped"); + bEndTrace = true; + break; + case SpatialGDK::EPushRPCResult::HasAckAuthority: + TraceMsg = TEXT("NoAckAuth"); + bEndTrace = true; + break; + case SpatialGDK::EPushRPCResult::NoRingBufferAuthority: + TraceMsg = TEXT("NoRingBufferAuth"); + bEndTrace = true; + break; + default: + TraceMsg = TEXT("UnrecognisedResult"); + break; + } + + if (bEndTrace) + { + // This RPC has been dropped, end the trace + SpatialLatencyTracer->WriteAndEndTrace(Trace, TraceMsg, false); + } + else if (!TraceMsg.IsEmpty()) + { + // This RPC will be sent later + SpatialLatencyTracer->WriteToLatencyTrace(Trace, TraceMsg); + } + } +} +#endif // TRACE_LIB_ACTIVE +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp index c54caa28b6..d9f0f539c7 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp @@ -15,12 +15,15 @@ #include "Kismet/KismetSystemLibrary.h" #endif +#include "EngineClasses/SpatialNetConnection.h" #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" #include "EngineClasses/SpatialWorldSettings.h" #include "LoadBalancing/AbstractLBStrategy.h" +#include "LoadBalancing/SpatialMultiWorkerSettings.h" #include "Utils/GDKPropertyMacros.h" #include "Utils/RepLayoutUtils.h" +#include "Utils/SpatialStatics.h" DEFINE_LOG_CATEGORY(LogSpatialClassInfoManager); @@ -29,12 +32,23 @@ bool USpatialClassInfoManager::TryInit(USpatialNetDriver* InNetDriver) check(InNetDriver != nullptr); NetDriver = InNetDriver; - FSoftObjectPath SchemaDatabasePath = FSoftObjectPath(FPaths::SetExtension(SpatialConstants::SCHEMA_DATABASE_ASSET_PATH, TEXT(".SchemaDatabase"))); + FSoftObjectPath SchemaDatabasePath = + FSoftObjectPath(FPaths::SetExtension(SpatialConstants::SCHEMA_DATABASE_ASSET_PATH, TEXT(".SchemaDatabase"))); SchemaDatabase = Cast(SchemaDatabasePath.TryLoad()); if (SchemaDatabase == nullptr) { - UE_LOG(LogSpatialClassInfoManager, Error, TEXT("SchemaDatabase not found! Please generate schema or turn off SpatialOS networking.")); + UE_LOG(LogSpatialClassInfoManager, Error, + TEXT("SchemaDatabase not found! Please generate schema or turn off SpatialOS networking.")); + QuitGame(); + return false; + } + + if (SchemaDatabase->SchemaDatabaseVersion < ESchemaDatabaseVersion::LatestVersion) + { + UE_LOG(LogSpatialClassInfoManager, Error, + TEXT("SchemaDatabase version old! Loaded: %d Expected: %d Please regenerate schema or turn off SpatialOS networking."), + SchemaDatabase->SchemaDatabaseVersion, ESchemaDatabaseVersion::LatestVersion); QuitGame(); return false; } @@ -46,11 +60,14 @@ bool USpatialClassInfoManager::ValidateOrExit_IsSupportedClass(const FString& Pa { if (!IsSupportedClass(PathName)) { - UE_LOG(LogSpatialClassInfoManager, Error, TEXT("Could not find class %s in schema database. Double-check whether replication is enabled for this class, the class is marked as SpatialType, and schema has been generated."), *PathName); + UE_LOG(LogSpatialClassInfoManager, Error, + TEXT("Could not find class %s in schema database. Double-check whether replication is enabled for this class, the class is " + "marked as SpatialType, and schema has been generated."), + *PathName); #if !UE_BUILD_SHIPPING UE_LOG(LogSpatialClassInfoManager, Error, TEXT("Disconnecting due to no generated schema for %s."), *PathName); QuitGame(); -#endif //!UE_BUILD_SHIPPING +#endif //! UE_BUILD_SHIPPING return false; } @@ -109,12 +126,17 @@ void USpatialClassInfoManager::CreateClassInfoForClass(UClass* Class) { // Remove PIE prefix on class if it exists to properly look up the class. FString ClassPath = Class->GetPathName(); - GEngine->NetworkRemapPath(NetDriver, ClassPath, false); +#if ENGINE_MINOR_VERSION >= 26 + GEngine->NetworkRemapPath(NetDriver->GetSpatialOSNetConnection(), ClassPath, false /*bIsReading*/); +#else + GEngine->NetworkRemapPath(NetDriver, ClassPath, false /*bIsReading*/); +#endif TSharedRef Info = ClassInfoMap.Add(Class, MakeShared()); Info->Class = Class; - // Note: we have to add Class to ClassInfoMap before quitting, as it is expected to be in there by GetOrCreateClassInfoByClass. Therefore the quitting logic cannot be moved higher up. + // Note: we have to add Class to ClassInfoMap before quitting, as it is expected to be in there by GetOrCreateClassInfoByClass. + // Therefore the quitting logic cannot be moved higher up. if (!ValidateOrExit_IsSupportedClass(ClassPath)) { return; @@ -181,8 +203,7 @@ void USpatialClassInfoManager::CreateClassInfoForClass(UClass* Class) void USpatialClassInfoManager::FinishConstructingActorClassInfo(const FString& ClassPath, TSharedRef& Info) { - ForAllSchemaComponentTypes([&](ESchemaComponentType Type) - { + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { Worker_ComponentId ComponentId = SchemaDatabase->ActorClassPathToSchema[ClassPath].SchemaComponents[Type]; if (!ShouldTrackHandoverProperties() && Type == SCHEMA_Handover) @@ -207,7 +228,10 @@ void USpatialClassInfoManager::FinishConstructingActorClassInfo(const FString& C UClass* SubobjectClass = ResolveClass(SubobjectSchemaData.ClassPath); if (SubobjectClass == nullptr) { - UE_LOG(LogSpatialClassInfoManager, Error, TEXT("Failed to resolve the class for subobject %s (class path: %s) on actor class %s! This subobject will not be able to replicate in Spatial!"), *SubobjectSchemaData.Name.ToString(), *SubobjectSchemaData.ClassPath, *ClassPath); + UE_LOG(LogSpatialClassInfoManager, Error, + TEXT("Failed to resolve the class for subobject %s (class path: %s) on actor class %s! This subobject will not be able " + "to replicate in Spatial!"), + *SubobjectSchemaData.Name.ToString(), *SubobjectSchemaData.ClassPath, *ClassPath); continue; } @@ -217,8 +241,7 @@ void USpatialClassInfoManager::FinishConstructingActorClassInfo(const FString& C TSharedRef ActorSubobjectInfo = MakeShared(SubobjectInfo); ActorSubobjectInfo->SubobjectName = SubobjectSchemaData.Name; - ForAllSchemaComponentTypes([&](ESchemaComponentType Type) - { + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { if (!ShouldTrackHandoverProperties() && Type == SCHEMA_Handover) { return; @@ -248,8 +271,7 @@ void USpatialClassInfoManager::FinishConstructingSubobjectClassInfo(const FStrin int32 Offset = DynamicSubobjectData.SchemaComponents[SCHEMA_Data]; check(Offset != SpatialConstants::INVALID_COMPONENT_ID); - ForAllSchemaComponentTypes([&](ESchemaComponentType Type) - { + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { Worker_ComponentId ComponentId = DynamicSubobjectData.SchemaComponents[Type]; if (ComponentId != SpatialConstants::INVALID_COMPONENT_ID) @@ -269,17 +291,14 @@ bool USpatialClassInfoManager::ShouldTrackHandoverProperties() const { // There's currently a bug that lets handover data get sent to clients in the initial // burst of data for an entity, which leads to log spam in the SpatialReceiver. By tracking handover - // properties on clients, we can prevent that spam. + // properties on clients, we can prevent that spam. Cannot be removed yet because of Kraken, + // UNR-4358 will remove this in a squid-only world. if (!NetDriver->IsServer()) { return true; } - const USpatialGDKSettings* Settings = GetDefault(); - - const UAbstractLBStrategy* Strategy = NetDriver->LoadBalanceStrategy; - check(Strategy != nullptr); - return Strategy->RequiresHandoverData() || Settings->bEnableHandover; + return USpatialStatics::IsHandoverEnabled(NetDriver); } void USpatialClassInfoManager::TryCreateClassInfoForComponentId(Worker_ComponentId ComponentId) @@ -346,12 +365,15 @@ UClass* USpatialClassInfoManager::GetClassByComponentId(Worker_ComponentId Compo } else { - UE_LOG(LogSpatialClassInfoManager, Warning, TEXT("Class corresponding to component %d has been unloaded! Will try to reload based on the component id."), ComponentId); + UE_LOG(LogSpatialClassInfoManager, Log, + TEXT("Class corresponding to component %d has been unloaded! Will try to reload based on the component id."), ComponentId); - // 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. + // 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 TryCreateClassInfoForComponentId). + // 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()) @@ -369,7 +391,6 @@ UClass* USpatialClassInfoManager::GetClassByComponentId(Worker_ComponentId Compo uint32 USpatialClassInfoManager::GetComponentIdForClass(const UClass& Class) const { - const FString ClassPath = Class.GetPathName(); if (const FActorSchemaData* ActorSchemaData = SchemaDatabase->ActorClassPathToSchema.Find(Class.GetPathName())) { return ActorSchemaData->SchemaComponents[SCHEMA_Data]; @@ -377,7 +398,8 @@ uint32 USpatialClassInfoManager::GetComponentIdForClass(const UClass& Class) con return SpatialConstants::INVALID_COMPONENT_ID; } -TArray USpatialClassInfoManager::GetComponentIdsForClassHierarchy(const UClass& BaseClass, const bool bIncludeDerivedTypes /* = true */) const +TArray USpatialClassInfoManager::GetComponentIdsForClassHierarchy(const UClass& BaseClass, + const bool bIncludeDerivedTypes /* = true */) const { TArray OutComponentIds; @@ -405,13 +427,11 @@ TArray USpatialClassInfoManager::GetComponentIdsForClassHier { OutComponentIds.Add(ComponentId); } - } return OutComponentIds; } - bool USpatialClassInfoManager::GetOffsetByComponentId(Worker_ComponentId ComponentId, uint32& OutOffset) { if (!ComponentToOffsetMap.Contains(ComponentId)) @@ -488,25 +508,8 @@ const TMap& USpatialClassInfoManager::GetNetCullDista 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* USpatialClassInfoManager::GetClassInfoForNewSubobject(const UObject* Object, Worker_EntityId EntityId, + USpatialPackageMapClient* PackageMapClient) { const FClassInfo* Info = nullptr; @@ -516,7 +519,8 @@ const FClassInfo* USpatialClassInfoManager::GetClassInfoForNewSubobject(const UO // which has not been used on this entity. for (const auto& DynamicSubobjectInfo : SubobjectInfo.DynamicSubobjectInfo) { - if (!PackageMapClient->GetObjectFromUnrealObjectRef(FUnrealObjectRef(EntityId, DynamicSubobjectInfo->SchemaComponents[SCHEMA_Data])).IsValid()) + if (!PackageMapClient->GetObjectFromUnrealObjectRef(FUnrealObjectRef(EntityId, DynamicSubobjectInfo->SchemaComponents[SCHEMA_Data])) + .IsValid()) { Info = &DynamicSubobjectInfo.Get(); break; @@ -527,8 +531,10 @@ const FClassInfo* USpatialClassInfoManager::GetClassInfoForNewSubobject(const UO if (Info == nullptr) { const AActor* Actor = Cast(PackageMapClient->GetObjectFromEntityId(EntityId)); - UE_LOG(LogSpatialPackageMap, Error, TEXT("Too many dynamic subobjects of type %s attached to Actor %s! Please increase" - " the max number of dynamically attached subobjects per class in the SpatialOS runtime settings."), *Object->GetClass()->GetName(), *GetNameSafe(Actor)); + UE_LOG(LogSpatialPackageMap, Error, + TEXT("Too many dynamic subobjects of type %s attached to Actor %s! Please increase" + " the max number of dynamically attached subobjects per class in the SpatialOS runtime settings."), + *Object->GetClass()->GetName(), *GetNameSafe(Actor)); } return Info; @@ -550,7 +556,8 @@ bool USpatialClassInfoManager::IsNetCullDistanceComponent(Worker_ComponentId Com bool USpatialClassInfoManager::IsGeneratedQBIMarkerComponent(Worker_ComponentId ComponentId) const { - return IsSublevelComponent(ComponentId) || IsNetCullDistanceComponent(ComponentId); + return IsSublevelComponent(ComponentId) || IsNetCullDistanceComponent(ComponentId) + || SpatialConstants::IsEntityCompletenessComponent(ComponentId); } void USpatialClassInfoManager::QuitGame() @@ -577,10 +584,19 @@ Worker_ComponentId USpatialClassInfoManager::ComputeActorInterestComponentId(con if (ActorForRelevancy->bAlwaysRelevant) { - return SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID; + if (ActorForRelevancy->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_ServerOnly)) + { + return SpatialConstants::SERVER_ONLY_ALWAYS_RELEVANT_COMPONENT_ID; + } + else + { + return SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID; + } } - if (GetDefault()->bEnableNetCullDistanceInterest) + // Don't add NCD component to player controller and server only actors as we don't want client's to gain interest in them + if (GetDefault()->bEnableNetCullDistanceInterest && !Actor->IsA() + && !Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_ServerOnly)) { Worker_ComponentId NCDComponentId = GetComponentIdForNetCullDistance(ActorForRelevancy->NetCullDistanceSquared); if (NCDComponentId != SpatialConstants::INVALID_COMPONENT_ID) @@ -591,14 +607,18 @@ Worker_ComponentId USpatialClassInfoManager::ComputeActorInterestComponentId(con 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()); + 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?"), + 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()); } } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialDispatcher.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialDispatcher.cpp index 8bd1f4049b..67d8fcef32 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialDispatcher.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialDispatcher.cpp @@ -13,7 +13,8 @@ DEFINE_LOG_CATEGORY(LogSpatialView); -void SpatialDispatcher::Init(USpatialReceiver* InReceiver, USpatialStaticComponentView* InStaticComponentView, USpatialMetrics* InSpatialMetrics, USpatialWorkerFlags* InSpatialWorkerFlags) +void SpatialDispatcher::Init(USpatialReceiver* InReceiver, USpatialStaticComponentView* InStaticComponentView, + USpatialMetrics* InSpatialMetrics, USpatialWorkerFlags* InSpatialWorkerFlags) { check(InReceiver != nullptr); Receiver = InReceiver; @@ -26,89 +27,84 @@ void SpatialDispatcher::Init(USpatialReceiver* InReceiver, USpatialStaticCompone SpatialWorkerFlags = InSpatialWorkerFlags; } -void SpatialDispatcher::ProcessOps(const SpatialGDK::OpList& Ops) +void SpatialDispatcher::ProcessOps(const TArray& Ops) { check(Receiver.IsValid()); check(StaticComponentView.IsValid()); - for (size_t i = 0; i < Ops.Count; ++i) + for (const Worker_Op& Op : Ops) { - Worker_Op* Op = &Ops.Ops[i]; - - if (OpsToSkip.Num() != 0 && - OpsToSkip.Contains(Op)) - { - OpsToSkip.Remove(Op); - continue; - } - if (IsExternalSchemaOp(Op)) { ProcessExternalSchemaOp(Op); continue; } - switch (Op->op_type) + switch (Op.op_type) { // Critical Section case WORKER_OP_TYPE_CRITICAL_SECTION: - Receiver->OnCriticalSection(Op->op.critical_section.in_critical_section != 0); + Receiver->OnCriticalSection(Op.op.critical_section.in_critical_section != 0); break; // Entity Lifetime case WORKER_OP_TYPE_ADD_ENTITY: - Receiver->OnAddEntity(Op->op.add_entity); + Receiver->OnAddEntity(Op.op.add_entity); break; case WORKER_OP_TYPE_REMOVE_ENTITY: - Receiver->OnRemoveEntity(Op->op.remove_entity); - StaticComponentView->OnRemoveEntity(Op->op.remove_entity.entity_id); - Receiver->DropQueuedRemoveComponentOpsForEntity(Op->op.remove_entity.entity_id); + Receiver->OnRemoveEntity(Op.op.remove_entity); + StaticComponentView->OnRemoveEntity(Op.op.remove_entity.entity_id); + Receiver->DropQueuedRemoveComponentOpsForEntity(Op.op.remove_entity.entity_id); break; // Components case WORKER_OP_TYPE_ADD_COMPONENT: - StaticComponentView->OnAddComponent(Op->op.add_component); - Receiver->OnAddComponent(Op->op.add_component); + StaticComponentView->OnAddComponent(Op.op.add_component); + Receiver->OnAddComponent(Op.op.add_component); break; case WORKER_OP_TYPE_REMOVE_COMPONENT: - Receiver->OnRemoveComponent(Op->op.remove_component); + Receiver->OnRemoveComponent(Op.op.remove_component); break; case WORKER_OP_TYPE_COMPONENT_UPDATE: - StaticComponentView->OnComponentUpdate(Op->op.component_update); - Receiver->OnComponentUpdate(Op->op.component_update); + StaticComponentView->OnComponentUpdate(Op.op.component_update); + Receiver->OnComponentUpdate(Op.op.component_update); break; // Commands case WORKER_OP_TYPE_COMMAND_REQUEST: - Receiver->OnCommandRequest(Op->op.command_request); + Receiver->OnCommandRequest(Op); break; case WORKER_OP_TYPE_COMMAND_RESPONSE: - Receiver->OnCommandResponse(Op->op.command_response); + Receiver->OnCommandResponse(Op); break; // Authority Change - case WORKER_OP_TYPE_AUTHORITY_CHANGE: - Receiver->OnAuthorityChange(Op->op.authority_change); + case WORKER_OP_TYPE_COMPONENT_SET_AUTHORITY_CHANGE: + Receiver->OnAuthorityChange(Op.op.component_set_authority_change); break; // World Command Responses case WORKER_OP_TYPE_RESERVE_ENTITY_IDS_RESPONSE: - Receiver->OnReserveEntityIdsResponse(Op->op.reserve_entity_ids_response); + Receiver->OnReserveEntityIdsResponse(Op.op.reserve_entity_ids_response); break; case WORKER_OP_TYPE_CREATE_ENTITY_RESPONSE: - Receiver->OnCreateEntityResponse(Op->op.create_entity_response); + Receiver->OnCreateEntityResponse(Op); break; case WORKER_OP_TYPE_DELETE_ENTITY_RESPONSE: break; case WORKER_OP_TYPE_ENTITY_QUERY_RESPONSE: - Receiver->OnEntityQueryResponse(Op->op.entity_query_response); + Receiver->OnEntityQueryResponse(Op.op.entity_query_response); break; case WORKER_OP_TYPE_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)); + if (Op.op.flag_update.value == nullptr) + { + SpatialWorkerFlags->RemoveWorkerFlag(UTF8_TO_TCHAR(Op.op.flag_update.name)); + } + else + { + SpatialWorkerFlags->SetWorkerFlag(UTF8_TO_TCHAR(Op.op.flag_update.name), UTF8_TO_TCHAR(Op.op.flag_update.value)); + } break; case WORKER_OP_TYPE_METRICS: #if !UE_BUILD_SHIPPING @@ -117,9 +113,7 @@ void SpatialDispatcher::ProcessOps(const SpatialGDK::OpList& Ops) #endif break; case WORKER_OP_TYPE_DISCONNECT: - Receiver->OnDisconnect(Op->op.disconnect); break; - default: break; } @@ -129,29 +123,29 @@ void SpatialDispatcher::ProcessOps(const SpatialGDK::OpList& Ops) Receiver->FlushRetryRPCs(); } -bool SpatialDispatcher::IsExternalSchemaOp(Worker_Op* Op) const +bool SpatialDispatcher::IsExternalSchemaOp(const Worker_Op& Op) const { Worker_ComponentId ComponentId = SpatialGDK::GetComponentId(Op); return SpatialConstants::MIN_EXTERNAL_SCHEMA_ID <= ComponentId && ComponentId <= SpatialConstants::MAX_EXTERNAL_SCHEMA_ID; } -void SpatialDispatcher::ProcessExternalSchemaOp(Worker_Op* Op) +void SpatialDispatcher::ProcessExternalSchemaOp(const Worker_Op& Op) { Worker_ComponentId ComponentId = SpatialGDK::GetComponentId(Op); check(ComponentId != SpatialConstants::INVALID_COMPONENT_ID); check(StaticComponentView.IsValid()); - switch (Op->op_type) + switch (Op.op_type) { - case WORKER_OP_TYPE_AUTHORITY_CHANGE: - StaticComponentView->OnAuthorityChange(Op->op.authority_change); + case WORKER_OP_TYPE_COMPONENT_SET_AUTHORITY_CHANGE: + StaticComponentView->OnAuthorityChange(Op.op.component_set_authority_change); // Intentional fall-through case WORKER_OP_TYPE_ADD_COMPONENT: case WORKER_OP_TYPE_REMOVE_COMPONENT: case WORKER_OP_TYPE_COMPONENT_UPDATE: case WORKER_OP_TYPE_COMMAND_REQUEST: case WORKER_OP_TYPE_COMMAND_RESPONSE: - RunCallbacks(ComponentId, Op); + RunCallbacks(ComponentId, &Op); break; default: // This should never happen providing the GetComponentId function has @@ -161,55 +155,56 @@ void SpatialDispatcher::ProcessExternalSchemaOp(Worker_Op* Op) } } -SpatialDispatcher::FCallbackId SpatialDispatcher::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) - { + return AddGenericOpCallback(ComponentId, WORKER_OP_TYPE_ADD_COMPONENT, [Callback](const Worker_Op* Op) { Callback(Op->op.add_component); }); } -SpatialDispatcher::FCallbackId SpatialDispatcher::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) - { + return AddGenericOpCallback(ComponentId, WORKER_OP_TYPE_REMOVE_COMPONENT, [Callback](const Worker_Op* Op) { Callback(Op->op.remove_component); }); } -SpatialDispatcher::FCallbackId SpatialDispatcher::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); + return AddGenericOpCallback(ComponentId, WORKER_OP_TYPE_COMPONENT_SET_AUTHORITY_CHANGE, [Callback](const Worker_Op* Op) { + Callback(Op->op.component_set_authority_change); }); } -SpatialDispatcher::FCallbackId SpatialDispatcher::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) - { + return AddGenericOpCallback(ComponentId, WORKER_OP_TYPE_COMPONENT_UPDATE, [Callback](const Worker_Op* Op) { Callback(Op->op.component_update); }); } -SpatialDispatcher::FCallbackId SpatialDispatcher::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) - { + return AddGenericOpCallback(ComponentId, WORKER_OP_TYPE_COMMAND_REQUEST, [Callback](const Worker_Op* Op) { Callback(Op->op.command_request); }); } -SpatialDispatcher::FCallbackId SpatialDispatcher::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) - { + return AddGenericOpCallback(ComponentId, WORKER_OP_TYPE_COMMAND_RESPONSE, [Callback](const Worker_Op* Op) { Callback(Op->op.command_response); }); } -SpatialDispatcher::FCallbackId SpatialDispatcher::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++; @@ -238,8 +233,7 @@ bool SpatialDispatcher::RemoveOpCallback(FCallbackId CallbackId) return false; } - int32 CallbackIndex = ComponentCallbacks->IndexOfByPredicate([CallbackId](const UserOpCallbackData& Data) - { + int32 CallbackIndex = ComponentCallbacks->IndexOfByPredicate([CallbackId](const UserOpCallbackData& Data) { return Data.Id == CallbackId; }); if (CallbackIndex == INDEX_NONE) @@ -282,13 +276,3 @@ void SpatialDispatcher::RunCallbacks(Worker_ComponentId ComponentId, const Worke CallbackData.Callback(Op); } } - -void SpatialDispatcher::MarkOpToSkip(const Worker_Op* Op) -{ - OpsToSkip.Add(Op); -} - -int SpatialDispatcher::GetNumOpsToSkip() const -{ - return OpsToSkip.Num(); -} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialInterestConstraints.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialInterestConstraints.cpp index 3668c0902e..fee2afc2c1 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialInterestConstraints.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialInterestConstraints.cpp @@ -42,35 +42,43 @@ void UAndConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoM void USphereConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const { - OutConstraint.SphereConstraint = SpatialGDK::SphereConstraint{ SpatialGDK::Coordinates::FromFVector(Center), static_cast(Radius) / 100.0 }; + OutConstraint.SphereConstraint = + SpatialGDK::SphereConstraint{ SpatialGDK::Coordinates::FromFVector(Center), static_cast(Radius) / 100.0 }; } -void UCylinderConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const +void UCylinderConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, + SpatialGDK::QueryConstraint& OutConstraint) const { - OutConstraint.CylinderConstraint = SpatialGDK::CylinderConstraint{ SpatialGDK::Coordinates::FromFVector(Center), static_cast(Radius) / 100.0 }; + OutConstraint.CylinderConstraint = + SpatialGDK::CylinderConstraint{ SpatialGDK::Coordinates::FromFVector(Center), static_cast(Radius) / 100.0 }; } void UBoxConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const { - OutConstraint.BoxConstraint = SpatialGDK::BoxConstraint{ SpatialGDK::Coordinates::FromFVector(Center), SpatialGDK::Coordinates::FromFVector(EdgeLengths) }; + OutConstraint.BoxConstraint = + SpatialGDK::BoxConstraint{ SpatialGDK::Coordinates::FromFVector(Center), SpatialGDK::Coordinates::FromFVector(EdgeLengths) }; } -void URelativeSphereConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const +void URelativeSphereConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, + SpatialGDK::QueryConstraint& OutConstraint) const { OutConstraint.RelativeSphereConstraint = SpatialGDK::RelativeSphereConstraint{ static_cast(Radius) / 100.0 }; } -void URelativeCylinderConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const +void URelativeCylinderConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, + SpatialGDK::QueryConstraint& OutConstraint) const { OutConstraint.RelativeCylinderConstraint = SpatialGDK::RelativeCylinderConstraint{ static_cast(Radius) / 100.0 }; } -void URelativeBoxConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const +void URelativeBoxConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, + SpatialGDK::QueryConstraint& OutConstraint) const { OutConstraint.RelativeBoxConstraint = SpatialGDK::RelativeBoxConstraint{ SpatialGDK::Coordinates::FromFVector(EdgeLengths) }; } -void UCheckoutRadiusConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const +void UCheckoutRadiusConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, + SpatialGDK::QueryConstraint& OutConstraint) const { if (!ActorClass.Get()) { @@ -96,7 +104,8 @@ void UCheckoutRadiusConstraint::CreateConstraint(const USpatialClassInfoManager& } } -void UActorClassConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const +void UActorClassConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, + SpatialGDK::QueryConstraint& OutConstraint) const { if (!ActorClass.Get()) { @@ -112,14 +121,16 @@ void UActorClassConstraint::CreateConstraint(const USpatialClassInfoManager& Cla } } -void UComponentClassConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const +void UComponentClassConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, + SpatialGDK::QueryConstraint& OutConstraint) const { if (!ComponentClass.Get()) { return; } - TArray ComponentIds = ClassInfoManager.GetComponentIdsForClassHierarchy(*ComponentClass.Get(), bIncludeDerivedClasses); + TArray ComponentIds = + ClassInfoManager.GetComponentIdsForClassHierarchy(*ComponentClass.Get(), bIncludeDerivedClasses); for (Worker_ComponentId ComponentId : ComponentIds) { SpatialGDK::QueryConstraint ComponentTypeConstraint; diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialNetDriverLoadBalancingHandler.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialNetDriverLoadBalancingHandler.cpp index 4f9a9dc466..03d365ed41 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialNetDriverLoadBalancingHandler.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialNetDriverLoadBalancingHandler.cpp @@ -5,11 +5,11 @@ #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" -FSpatialNetDriverLoadBalancingContext::FSpatialNetDriverLoadBalancingContext(USpatialNetDriver* InNetDriver, TArray& InOutNetworkObjects) +FSpatialNetDriverLoadBalancingContext::FSpatialNetDriverLoadBalancingContext(USpatialNetDriver* InNetDriver, + TArray& InOutNetworkObjects) : NetDriver(InNetDriver) , NetworkObjects(InOutNetworkObjects) { - } void FSpatialNetDriverLoadBalancingContext::UpdateWithAdditionalActors() @@ -29,52 +29,44 @@ void FSpatialNetDriverLoadBalancingContext::UpdateWithAdditionalActors() } } -bool FSpatialNetDriverLoadBalancingContext::IsActorReadyForMigration(AActor* Actor) +EActorMigrationResult FSpatialNetDriverLoadBalancingContext::IsActorReadyForMigration(AActor* Actor) { - // Auth check. if (!Actor->HasAuthority()) { - return false; + return EActorMigrationResult::NotAuthoritative; + } + + if (!Actor->IsActorReady()) + { + return EActorMigrationResult::NotReady; } // These checks are extracted from UNetDriver::ServerReplicateActors_BuildNetworkObjects if (Actor->IsPendingKillPending()) { - return false; + return EActorMigrationResult::PendingKill; } // Verify the actor is actually initialized (it might have been intentionally spawn deferred until a later frame) if (!Actor->IsActorInitialized()) { - return false; + return EActorMigrationResult::NotInitialized; } // Don't send actors that may still be streaming in or out ULevel* Level = Actor->GetLevel(); if (Level->HasVisibilityChangeRequestPending() || Level->bIsAssociatingLevel) { - return false; + return EActorMigrationResult::Streaming; } if (Actor->NetDormancy == DORM_Initial && Actor->IsNetStartupActor()) { - return false; - } - - // Additional check that the actor is seen by the spatial runtime. - Worker_EntityId EntityId = NetDriver->PackageMap->GetEntityIdFromObject(Actor); - if (EntityId == SpatialConstants::INVALID_ENTITY_ID) - { - return false; - } - - if (!NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::POSITION_COMPONENT_ID)) - { - return false; + return EActorMigrationResult::NetDormant; } - return true; + return EActorMigrationResult::Success; } FSpatialNetDriverLoadBalancingContext::FNetworkObjectsArrayAdaptor FSpatialNetDriverLoadBalancingContext::GetActorsBeingReplicated() @@ -92,7 +84,7 @@ void FSpatialNetDriverLoadBalancingContext::RemoveAdditionalActor(AActor* Actor) void FSpatialNetDriverLoadBalancingContext::AddActorToReplicate(AActor* Actor) { - if(FNetworkObjectInfo* Info = NetDriver->FindNetworkObjectInfo(Actor)) + if (FNetworkObjectInfo* Info = NetDriver->FindNetworkObjectInfo(Actor)) { AdditionalActorsToReplicate.Add(Info); } @@ -102,4 +94,3 @@ TArray& FSpatialNetDriverLoadBalancingContext::GetDependentActors(AActo { return Actor->Children; } - diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialNetDriverLoadBalancingHandler.h b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialNetDriverLoadBalancingHandler.h index 1c83730c2d..11dc5517a2 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialNetDriverLoadBalancingHandler.h +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialNetDriverLoadBalancingHandler.h @@ -3,8 +3,7 @@ #pragma once #include "Engine/NetworkObjectList.h" - -class USpatialNetDriver; +#include "EngineClasses/SpatialNetDriver.h" struct FSpatialNetDriverLoadBalancingContext { @@ -16,43 +15,44 @@ struct FSpatialNetDriverLoadBalancingContext struct Iterator { Iterator(TArray::RangedForIteratorType Iterator) - :IteratorImpl(Iterator) - {} - + : IteratorImpl(Iterator) + { + } + AActor* operator*() const { return (*IteratorImpl)->Actor; } - void operator ++() { ++IteratorImpl; } - bool operator != (Iterator const& iRHS) const { return IteratorImpl != iRHS.IteratorImpl; } - + void operator++() { ++IteratorImpl; } + bool operator!=(Iterator const& iRHS) const { return IteratorImpl != iRHS.IteratorImpl; } + TArray::RangedForIteratorType IteratorImpl; }; - + FNetworkObjectsArrayAdaptor(TArray& InNetworkObjects) : NetworkObjects(InNetworkObjects) - {} - + { + } + Iterator begin() { return Iterator(NetworkObjects.begin()); } Iterator end() { return Iterator(NetworkObjects.end()); } - + TArray& NetworkObjects; }; - + FNetworkObjectsArrayAdaptor GetActorsBeingReplicated(); - + void RemoveAdditionalActor(AActor* Actor); - + void AddActorToReplicate(AActor* Actor); - + TArray& GetDependentActors(AActor* Actor); void UpdateWithAdditionalActors(); - bool IsActorReadyForMigration(AActor*); + EActorMigrationResult IsActorReadyForMigration(AActor* Actor); protected: - USpatialNetDriver* NetDriver; TSet AdditionalActorsToReplicate; - + TArray& NetworkObjects; }; diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialOutputDevice.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialOutputDevice.cpp index e4201ecf28..cc90dcd404 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialOutputDevice.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialOutputDevice.cpp @@ -6,7 +6,8 @@ #include "Interop/Connection/SpatialWorkerConnection.h" FSpatialOutputDevice::FSpatialOutputDevice(USpatialWorkerConnection* InConnection, FName InLoggerName, int32 InPIEIndex) - : FilterLevel(ELogVerbosity::Type(GetDefault()->WorkerLogLevel.GetValue())) + : LocalFilterLevel(ELogVerbosity::Type(GetDefault()->LocalWorkerLogLevel.GetValue())) + , CloudFilterLevel(ELogVerbosity::Type(GetDefault()->CloudWorkerLogLevel.GetValue())) , Connection(InConnection) , LoggerName(InLoggerName) , PIEIndex(InPIEIndex) @@ -25,19 +26,21 @@ FSpatialOutputDevice::~FSpatialOutputDevice() void FSpatialOutputDevice::Serialize(const TCHAR* InData, ELogVerbosity::Type Verbosity, const class FName& Category) { // Log category LogSpatial ignores the verbosity check. - if (Verbosity > FilterLevel && Category != FName("LogSpatial")) - { - return; - } if (bLogToSpatial && Connection != nullptr) { #if WITH_EDITOR - if (GPlayInEditorID != PIEIndex) + if ((Verbosity > LocalFilterLevel && Category != FName("LogSpatial")) || GPlayInEditorID != PIEIndex + || LocalFilterLevel == ELogVerbosity::NoLogging) + { + return; + } +#else // !WITH_EDITOR + if ((Verbosity > CloudFilterLevel && Category != FName("LogSpatial")) || CloudFilterLevel == ELogVerbosity::NoLogging) { return; } -#endif //WITH_EDITOR +#endif // WITH_EDITOR Connection->SendLogMessage(ConvertLogLevelToSpatial(Verbosity), LoggerName, InData); } } @@ -52,9 +55,14 @@ void FSpatialOutputDevice::RemoveRedirectCategory(const FName& Category) CategoriesToRedirect.Remove(Category); } -void FSpatialOutputDevice::SetVerbosityFilterLevel(ELogVerbosity::Type Verbosity) +void FSpatialOutputDevice::SetVerbosityLocalFilterLevel(ELogVerbosity::Type Verbosity) +{ + LocalFilterLevel = Verbosity; +} + +void FSpatialOutputDevice::SetVerbosityCloudFilterLevel(ELogVerbosity::Type Verbosity) { - FilterLevel = Verbosity; + CloudFilterLevel = Verbosity; } Worker_LogLevel FSpatialOutputDevice::ConvertLogLevelToSpatial(ELogVerbosity::Type Verbosity) @@ -62,12 +70,16 @@ Worker_LogLevel FSpatialOutputDevice::ConvertLogLevelToSpatial(ELogVerbosity::Ty switch (Verbosity) { case ELogVerbosity::Fatal: - return WORKER_LOG_LEVEL_FATAL; + return WORKER_LOG_LEVEL_ERROR; case ELogVerbosity::Error: return WORKER_LOG_LEVEL_ERROR; case ELogVerbosity::Warning: return WORKER_LOG_LEVEL_WARN; - default: + case ELogVerbosity::Display: + return WORKER_LOG_LEVEL_INFO; + case ELogVerbosity::Log: return WORKER_LOG_LEVEL_INFO; + default: + return WORKER_LOG_LEVEL_DEBUG; } } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp index 8e75f2d278..454af9a21e 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp @@ -10,34 +10,26 @@ #include "Schema/UnrealObjectRef.h" #include "SpatialCommonTypes.h" #include "SpatialConstants.h" -#include "SpatialGDKSettings.h" +#include "UObject/SoftObjectPath.h" #include "Utils/SchemaUtils.h" +#include "Containers/StringConv.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; -void USpatialPlayerSpawner::Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimerManager) +void USpatialPlayerSpawner::Init(USpatialNetDriver* InNetDriver) { NetDriver = InNetDriver; - TimerManager = InTimerManager; - - NumberOfAttempts = 0; } void USpatialPlayerSpawner::SendPlayerSpawnRequest() @@ -49,13 +41,11 @@ void USpatialPlayerSpawner::SendPlayerSpawnRequest() Worker_EntityQuery SpatialSpawnerQuery{}; SpatialSpawnerQuery.constraint = SpatialSpawnerConstraint; - SpatialSpawnerQuery.result_type = WORKER_RESULT_TYPE_SNAPSHOT; - const Worker_RequestId RequestID = NetDriver->Connection->SendEntityQueryRequest(&SpatialSpawnerQuery); + const Worker_RequestId RequestID = NetDriver->Connection->SendEntityQueryRequest(&SpatialSpawnerQuery, RETRY_UNTIL_COMPLETE); EntityQueryDelegate SpatialSpawnerQueryDelegate; - SpatialSpawnerQueryDelegate.BindLambda([this, RequestID](const Worker_EntityQueryResponseOp& Op) - { + SpatialSpawnerQueryDelegate.BindLambda([this, RequestID](const Worker_EntityQueryResponseOp& Op) { FString Reason; if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) @@ -70,9 +60,9 @@ void USpatialPlayerSpawner::SendPlayerSpawnRequest() { checkf(Op.result_count == 1, TEXT("There should never be more than one SpatialSpawner entity.")); - SpatialGDK::SpawnPlayerRequest SpawnRequest = ObtainPlayerParams(); + 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); + NetDriver->Connection->SendCommandRequest(Op.results[0].entity_id, &SpawnPlayerCommandRequest, RETRY_MAX_TIMES, {}); } if (!Reason.IsEmpty()) @@ -84,8 +74,6 @@ void USpatialPlayerSpawner::SendPlayerSpawnRequest() UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Sending player spawn request")); NetDriver->Receiver->AddEntityQueryDelegate(RequestID, SpatialSpawnerQueryDelegate); - - ++NumberOfAttempts; } SpatialGDK::SpawnPlayerRequest USpatialPlayerSpawner::ObtainPlayerParams() const @@ -109,7 +97,8 @@ SpatialGDK::SpawnPlayerRequest USpatialPlayerSpawner::ObtainPlayerParams() const LoginURL.AddOption(*FString::Printf(TEXT("Name=%s"), *OverrideName)); } - LoginURL.AddOption(*FString::Printf(TEXT("workerAttribute=%s"), *FString::Format(TEXT("workerId:{0}"), { NetDriver->Connection->GetWorkerId() }))); + LoginURL.AddOption( + *FString::Printf(TEXT("workerAttribute=%s"), *FString::Format(TEXT("workerId:{0}"), { NetDriver->Connection->GetWorkerId() }))); if (bIsSimulatedPlayer) { @@ -140,7 +129,9 @@ SpatialGDK::SpawnPlayerRequest USpatialPlayerSpawner::ObtainPlayerParams() const const FName OnlinePlatformName = WorldContext->OwningGameInstance->GetOnlinePlatformName(); - return { LoginURL, UniqueId, OnlinePlatformName, bIsSimulatedPlayer }; + const Worker_EntityId ClientSystemEntityId = NetDriver->Connection->GetWorkerSystemEntityId(); + + return { LoginURL, UniqueId, OnlinePlatformName, bIsSimulatedPlayer, ClientSystemEntityId }; } void USpatialPlayerSpawner::ReceivePlayerSpawnResponseOnClient(const Worker_CommandResponseOp& Op) @@ -149,24 +140,10 @@ void USpatialPlayerSpawner::ReceivePlayerSpawnResponseOnClient(const Worker_Comm { UE_LOG(LogSpatialPlayerSpawner, Display, TEXT("PlayerSpawn returned from server sucessfully")); } - else if (NumberOfAttempts < SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS) - { - UE_LOG(LogSpatialPlayerSpawner, Warning, TEXT("Player spawn request failed: \"%s\""), - UTF8_TO_TCHAR(Op.message)); - - FTimerHandle RetryTimer; - TimerManager->SetTimer(RetryTimer, [WeakThis = TWeakObjectPtr(this)]() - { - if (USpatialPlayerSpawner* Spawner = WeakThis.Get()) - { - Spawner->SendPlayerSpawnRequest(); - } - }, SpatialConstants::GetCommandRetryWaitTimeSeconds(NumberOfAttempts), false); - } else { FString Reason = FString::Printf(TEXT("Player spawn request failed too many times. (%u attempts)"), - SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS); + SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS); UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("%s"), *Reason); OnPlayerSpawnFailed.ExecuteIfBound(Reason); } @@ -176,35 +153,34 @@ void USpatialPlayerSpawner::ReceivePlayerSpawnRequestOnServer(const Worker_Comma { UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Received PlayerSpawn request on server")); - const FUTF8ToTCHAR FStringConversion(reinterpret_cast(Op.caller_worker_id), strlen(Op.caller_worker_id)); - FString ClientWorkerId(FStringConversion.Length(), FStringConversion.Get()); - // Accept the player if we have not already accepted a player from this worker. bool bAlreadyHasPlayer; - WorkersWithPlayersSpawned.Emplace(ClientWorkerId, &bAlreadyHasPlayer); + WorkersWithPlayersSpawned.Emplace(Op.caller_worker_entity_id, &bAlreadyHasPlayer); if (bAlreadyHasPlayer) { - UE_LOG(LogSpatialPlayerSpawner, Verbose, TEXT("Ignoring duplicate PlayerSpawn request. Client worker ID: %s"), *ClientWorkerId); + UE_LOG(LogSpatialPlayerSpawner, Verbose, TEXT("Ignoring duplicate PlayerSpawn request. Client worker ID: %lld"), + Op.caller_worker_entity_id); return; } Schema_Object* RequestPayload = Schema_GetCommandRequestObject(Op.request.schema_type); - FindPlayerStartAndProcessPlayerSpawn(RequestPayload, ClientWorkerId); + FindPlayerStartAndProcessPlayerSpawn(RequestPayload, Op.caller_worker_entity_id); Worker_CommandResponse Response = PlayerSpawner::CreatePlayerSpawnResponse(); NetDriver->Connection->SendCommandResponse(Op.request_id, &Response); } -void USpatialPlayerSpawner::FindPlayerStartAndProcessPlayerSpawn(Schema_Object* SpawnPlayerRequest, const PhysicalWorkerName& ClientWorkerId) +void USpatialPlayerSpawner::FindPlayerStartAndProcessPlayerSpawn(Schema_Object* SpawnPlayerRequest, const Worker_EntityId& ClientWorkerId) { - // If the load balancing strategy dictates that this worker should have authority over the chosen PlayerStart THEN the spawn is handled locally, - // Else if the the PlayerStart is handled by another worker THEN forward the request to that worker to prevent an initial player migration, - // Else if a PlayerStart can't be found THEN we could be on the wrong worker type, so forward to the GameMode authoritative server. + // If the load balancing strategy dictates that this worker should have authority over the chosen PlayerStart THEN the spawn is handled + // locally, Else if the the PlayerStart is handled by another worker THEN forward the request to that worker to prevent an initial + // player migration, Else if a PlayerStart can't be found THEN we could be on the wrong worker type, so forward to the GameMode + // authoritative server. // // This implementation depends on: // 1) the load-balancing strategy having the same rules for PlayerStart Actors and Characters / Controllers / Player States or, // 2) the authoritative virtual worker ID for a PlayerStart Actor not changing during the lifetime of a deployment. - check (NetDriver->LoadBalanceStrategy != nullptr) + check(NetDriver->LoadBalanceStrategy != nullptr); // We need to specifically extract the URL from the PlayerSpawn request for finding a PlayerStart. const FURL Url = PlayerSpawner::ExtractUrlFromPlayerSpawnParams(SpawnPlayerRequest); @@ -215,7 +191,8 @@ void USpatialPlayerSpawner::FindPlayerStartAndProcessPlayerSpawn(Schema_Object* // If the PlayerStart is authoritative locally, spawn the player locally. if (PlayerStartActor != nullptr && NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*PlayerStartActor)) { - UE_LOG(LogSpatialPlayerSpawner, Verbose, TEXT("Handling SpawnPlayerRequest request locally. Client worker ID: %s."), *ClientWorkerId); + UE_LOG(LogSpatialPlayerSpawner, Verbose, TEXT("Handling SpawnPlayerRequest request locally. Client worker ID: %lld."), + ClientWorkerId); PassSpawnRequestToNetDriver(SpawnPlayerRequest, PlayerStartActor); return; } @@ -232,7 +209,8 @@ void USpatialPlayerSpawner::FindPlayerStartAndProcessPlayerSpawn(Schema_Object* VirtualWorkerToForwardTo = NetDriver->LoadBalanceStrategy->WhoShouldHaveAuthority(*UGameplayStatics::GetGameMode(GetWorld())); if (VirtualWorkerToForwardTo == SpatialConstants::INVALID_VIRTUAL_WORKER_ID) { - UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("The server authoritative over the GameMode could not locate any PlayerStart, this is unsupported.")); + UE_LOG(LogSpatialPlayerSpawner, Error, + TEXT("The server authoritative over the GameMode could not locate any PlayerStart, this is unsupported.")); } } else if (!NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*PlayerStartActor)) @@ -240,8 +218,9 @@ void USpatialPlayerSpawner::FindPlayerStartAndProcessPlayerSpawn(Schema_Object* VirtualWorkerToForwardTo = NetDriver->LoadBalanceStrategy->WhoShouldHaveAuthority(*PlayerStartActor); if (VirtualWorkerToForwardTo == SpatialConstants::INVALID_VIRTUAL_WORKER_ID) { - UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Load-balance strategy returned invalid virtual worker ID for selected PlayerStart Actor: %s"), - *GetNameSafe(PlayerStartActor)); + UE_LOG(LogSpatialPlayerSpawner, Error, + TEXT("Load-balance strategy returned invalid virtual worker ID for selected PlayerStart Actor: %s"), + *GetNameSafe(PlayerStartActor)); } } @@ -264,58 +243,70 @@ void USpatialPlayerSpawner::PassSpawnRequestToNetDriver(const Schema_Object* Pla // 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); + NetDriver->AcceptNewPlayer(SpawnRequest.LoginURL, SpawnRequest.UniqueId, SpawnRequest.OnlinePlatformName, + SpawnRequest.ClientSystemEntityId); GameMode->SetPrioritizedPlayerStart(nullptr); } -void USpatialPlayerSpawner::ForwardSpawnRequestToStrategizedServer(const Schema_Object* OriginalPlayerSpawnRequest, AActor* PlayerStart, const PhysicalWorkerName& ClientWorkerId, const VirtualWorkerId SpawningVirtualWorker) +void USpatialPlayerSpawner::ForwardSpawnRequestToStrategizedServer(const Schema_Object* OriginalPlayerSpawnRequest, AActor* PlayerStart, + const Worker_EntityId& ClientWorkerId, + const VirtualWorkerId SpawningVirtualWorker) { - UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Forwarding player spawn request to strategized worker. Client ID: %s. PlayerStart: %s. Strategeized virtual worker %d"), - *ClientWorkerId, *GetNameSafe(PlayerStart), SpawningVirtualWorker); + UE_LOG(LogSpatialPlayerSpawner, Log, + TEXT("Forwarding player spawn request to strategized worker. Client ID: %lld. PlayerStart: %s. Strategeized virtual worker %d"), + ClientWorkerId, *GetNameSafe(PlayerStart), SpawningVirtualWorker); // Find the server worker entity corresponding to the PlayerStart strategized virtual worker. - const Worker_EntityId ServerWorkerEntity = NetDriver->VirtualWorkerTranslator->GetServerWorkerEntityForVirtualWorker(SpawningVirtualWorker); + const Worker_EntityId ServerWorkerEntity = + NetDriver->VirtualWorkerTranslator->GetServerWorkerEntityForVirtualWorker(SpawningVirtualWorker); if (ServerWorkerEntity == SpatialConstants::INVALID_ENTITY_ID) { - UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Player spawning failed. Virtual worker translator returned invalid server worker entity ID. Virtual worker: %d. " - "Defaulting to normal player spawning flow."), SpawningVirtualWorker); + UE_LOG(LogSpatialPlayerSpawner, Error, + TEXT("Player spawning failed. Virtual worker translator returned invalid server worker entity ID. Virtual worker: %d. " + "Defaulting to normal player spawning flow."), + SpawningVirtualWorker); PassSpawnRequestToNetDriver(OriginalPlayerSpawnRequest, nullptr); return; } // To pass the PlayerStart Actor to another worker we use a FUnrealObjectRef. - // The PlayerStartObjectRef can be null if we are trying to just forward the spawn request to the correct worker layer, rather than some specific PlayerStart authoritative worker. + // The PlayerStartObjectRef can be null if we are trying to just forward the spawn request to the correct worker layer, rather than some + // specific PlayerStart authoritative worker. FUnrealObjectRef PlayerStartObjectRef = FUnrealObjectRef::NULL_OBJECT_REF; if (PlayerStart != nullptr) { - const FNetworkGUID PlayerStartGuid = NetDriver->PackageMap->ResolveStablyNamedObject(PlayerStart); - PlayerStartObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromNetGUID(PlayerStartGuid); + PlayerStartObjectRef = FUnrealObjectRef::FromObjectPtr(PlayerStart, NetDriver->PackageMap); } // 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)); + ServerWorker::CreateForwardPlayerSpawnSchemaRequest(ForwardSpawnPlayerSchemaRequest, PlayerStartObjectRef, OriginalPlayerSpawnRequest, + ClientWorkerId); + Worker_CommandRequest ForwardSpawnPlayerRequest = + ServerWorker::CreateForwardPlayerSpawnRequest(Schema_CopyCommandRequest(ForwardSpawnPlayerSchemaRequest)); - const Worker_RequestId RequestId = NetDriver->Connection->SendCommandRequest(ServerWorkerEntity, &ForwardSpawnPlayerRequest, SpatialConstants::SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND_ID); + const Worker_RequestId RequestId = + NetDriver->Connection->SendCommandRequest(ServerWorkerEntity, &ForwardSpawnPlayerRequest, RETRY_MAX_TIMES, {}); - OutgoingForwardPlayerSpawnRequests.Add(RequestId, TUniquePtr(ForwardSpawnPlayerSchemaRequest)); + OutgoingForwardPlayerSpawnRequests.Add(RequestId, + TUniquePtr(ForwardSpawnPlayerSchemaRequest)); } 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); + Worker_EntityId ClientWorkerId = Schema_GetEntityId(Payload, SpatialConstants::FORWARD_SPAWN_PLAYER_CLIENT_SYSTEM_ENTITY_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); + UE_LOG(LogSpatialPlayerSpawner, Verbose, TEXT("Ignoring duplicate forward player spawn request. Client worker ID: %lld"), + ClientWorkerId); return; } @@ -330,18 +321,22 @@ void USpatialPlayerSpawner::ReceiveForwardedPlayerSpawnRequest(const Worker_Comm if (bRequestHandledSuccessfully) { - UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Received ForwardPlayerSpawn request. Client worker ID: %s. PlayerStart: %s"), *ClientWorkerId, *PlayerStart->GetName()); + UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Received ForwardPlayerSpawn request. Client worker ID: %lld. 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"), *ClientWorkerId); + UE_LOG(LogSpatialPlayerSpawner, Error, + TEXT("PlayerStart Actor UnrealObjectRef was invalid on forwarded player spawn request worker: %lld"), ClientWorkerId); } } else { - UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("PlayerStart Actor was null object ref in forward spawn request. This is intentional when handing request to the correct " - "load balancing layer. Attempting to find a player start again.")); + UE_LOG( + LogSpatialPlayerSpawner, Log, + TEXT("PlayerStart Actor was null object ref in forward spawn request. This is intentional when handing request to the correct " + "load balancing layer. Attempting to find a player start again.")); FindPlayerStartAndProcessPlayerSpawn(PlayerSpawnData, ClientWorkerId); } @@ -353,7 +348,8 @@ void USpatialPlayerSpawner::ReceiveForwardPlayerSpawnResponse(const Worker_Comma { 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); + const bool bForwardingSucceeding = GetBoolFromSchema(Schema_GetCommandResponseObject(Op.response.schema_type), + SpatialConstants::FORWARD_SPAWN_PLAYER_RESPONSE_SUCCESS_ID); if (bForwardingSucceeding) { // If forwarding the player spawn request succeeded, clean up our outgoing request map. @@ -370,18 +366,10 @@ void USpatialPlayerSpawner::ReceiveForwardPlayerSpawnResponse(const Worker_Comma } 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) +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)) @@ -393,20 +381,25 @@ void USpatialPlayerSpawner::RetryForwardSpawnPlayerRequest(const Worker_EntityId Schema_Object* OldRequestPayload = Schema_GetCommandRequestObject(OldRequest.Get()); // 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 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.")); + 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); + const Worker_EntityId ClientWorkerId = + Schema_GetEntityId(OldRequestPayload, SpatialConstants::FORWARD_SPAWN_PLAYER_CLIENT_SYSTEM_ENTITY_ID); FindPlayerStartAndProcessPlayerSpawn(SpawnPlayerData, ClientWorkerId); return; } // Resend the ForwardSpawnPlayer request. - Worker_CommandRequest ForwardSpawnPlayerRequest = ServerWorker::CreateForwardPlayerSpawnRequest(Schema_CopyCommandRequest(OldRequest.Get())); - const Worker_RequestId NewRequestId = NetDriver->Connection->SendCommandRequest(EntityId, &ForwardSpawnPlayerRequest, SpatialConstants::SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND_ID); + Worker_CommandRequest ForwardSpawnPlayerRequest = + ServerWorker::CreateForwardPlayerSpawnRequest(Schema_CopyCommandRequest(OldRequest.Get())); + const Worker_RequestId NewRequestId = + NetDriver->Connection->SendCommandRequest(EntityId, &ForwardSpawnPlayerRequest, RETRY_UNTIL_COMPLETE, {}); // Move the request data from the old request ID map entry across to the new ID entry. OutgoingForwardPlayerSpawnRequests.Add(NewRequestId, TUniquePtr(OldRequest.Get())); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialRPCService.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialRPCService.cpp deleted file mode 100644 index 5844d22fc7..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialRPCService.cpp +++ /dev/null @@ -1,642 +0,0 @@ -// 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" -#include "Utils/SpatialLatencyTracer.h" - -DEFINE_LOG_CATEGORY(LogSpatialRPCService); - -namespace SpatialGDK -{ - -SpatialRPCService::SpatialRPCService(ExtractRPCDelegate ExtractRPCCallback, const USpatialStaticComponentView* View, USpatialLatencyTracer* SpatialLatencyTracer) - : ExtractRPCCallback(ExtractRPCCallback) - , View(View) - , SpatialLatencyTracer(SpatialLatencyTracer) -{ -} - -EPushRPCResult SpatialRPCService::PushRPC(Worker_EntityId EntityId, ERPCType Type, RPCPayload Payload, bool bCreatedEntity) -{ - EntityRPCType EntityType = EntityRPCType(EntityId, Type); - - EPushRPCResult Result = EPushRPCResult::Success; - - if (RPCRingBufferUtils::ShouldQueueOverflowed(Type) && OverflowedRPCs.Contains(EntityType)) - { - // Already has queued RPCs of this type, queue until those are pushed. - AddOverflowedRPC(EntityType, MoveTemp(Payload)); - Result = EPushRPCResult::QueueOverflowed; - } - else - { - Result = PushRPCInternal(EntityId, Type, MoveTemp(Payload), bCreatedEntity); - - if (Result == EPushRPCResult::QueueOverflowed) - { - AddOverflowedRPC(EntityType, MoveTemp(Payload)); - } - } - -#if TRACE_LIB_ACTIVE - ProcessResultToLatencyTrace(Result, Payload.Trace); -#endif - - return Result; -} - -EPushRPCResult SpatialRPCService::PushRPCInternal(Worker_EntityId EntityId, ERPCType Type, RPCPayload&& Payload, bool bCreatedEntity) -{ - 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)) - { - if (bCreatedEntity) - { - return EPushRPCResult::EntityBeingCreated; - } - 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 (bCreatedEntity) - { - return EPushRPCResult::EntityBeingCreated; - } - // If the entity isn't in the view, we assume this RPC was called before - // CreateEntityRequest, so we put it into a component data object. - EndpointObject = Schema_GetComponentDataFields(GetOrCreateComponentData(EntityComponent)); - - LastAckedRPCId = 0; - } - - uint64 NewRPCId = LastSentRPCIds.FindRef(EntityType) + 1; - - // Check capacity. - if (LastAckedRPCId + RPCRingBufferUtils::GetRingBufferSize(Type) >= NewRPCId) - { - RPCRingBufferUtils::WriteRPCToSchema(EndpointObject, Type, NewRPCId, Payload); - -#if TRACE_LIB_ACTIVE - if (SpatialLatencyTracer != nullptr && Payload.Trace != InvalidTraceKey) - { - if (PendingTraces.Find(EntityComponent) == nullptr) - { - PendingTraces.Add(EntityComponent, Payload.Trace); - } - else - { - SpatialLatencyTracer->WriteAndEndTrace(Payload.Trace, TEXT("Multiple rpc updates in single update, ending further stack tracing"), true); - } - } -#endif - - LastSentRPCIds.Add(EntityType, NewRPCId); - } - else - { - // 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) - { - const EPushRPCResult Result = PushRPCInternal(EntityId, Type, MoveTemp(Payload), false); - - switch (Result) - { - case EPushRPCResult::Success: - NumProcessed++; - break; - case EPushRPCResult::QueueOverflowed: - UE_LOG(LogSpatialRPCService, Log, - TEXT("SpatialRPCService::PushOverflowedRPCs: Sent some but not all overflowed RPCs. RPCs sent %d, RPCs still overflowed: %d, Entity: %lld, RPC type: %s"), - NumProcessed, OverflowedRPCArray.Num() - NumProcessed, EntityId, *SpatialConstants::RPCTypeToString(Type)); - 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; - default: - checkNoEntry(); - } - -#if TRACE_LIB_ACTIVE - ProcessResultToLatencyTrace(Result, Payload.Trace); -#endif - - // This includes the valid case of RPCs still overflowing (EPushRPCResult::QueueOverflowed), as well as the error cases. - if (Result != EPushRPCResult::Success) - { - 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; -#if TRACE_LIB_ACTIVE - TraceKey Trace = InvalidTraceKey; - PendingTraces.RemoveAndCopyValue(It.Key, Trace); - UpdateToSend.Update.Trace = Trace; -#endif - } - - 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 }; - - FWorkerComponentData& Component = Components.Emplace_GetRef(FWorkerComponentData{}); - 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; -#if TRACE_LIB_ACTIVE - TraceKey Trace = InvalidTraceKey; - PendingTraces.RemoveAndCopyValue(EntityComponent, Trace); - Component.Trace = Trace; -#endif - 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); - LastSeenRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientReliable), Endpoint->ReliableRPCAck); - LastSeenRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientUnreliable), Endpoint->UnreliableRPCAck); - LastAckedRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientReliable), Endpoint->ReliableRPCAck); - LastAckedRPCIds.Add(EntityRPCType(EntityId, ERPCType::ClientUnreliable), Endpoint->UnreliableRPCAck); - 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); - LastSeenRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerReliable), Endpoint->ReliableRPCAck); - LastSeenRPCIds.Add(EntityRPCType(EntityId, ERPCType::ServerUnreliable), Endpoint->UnreliableRPCAck); - 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: - { - LastSeenRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ClientReliable)); - LastSeenRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ClientUnreliable)); - 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: - { - LastSeenRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerReliable)); - LastSeenRPCIds.Remove(EntityRPCType(EntityId, ERPCType::ServerUnreliable)); - 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) - { - if (!LastSeenMulticastRPCIds.Contains(EntityId)) - { - UE_LOG(LogSpatialRPCService, Warning, - TEXT("Tried to extract RPCs but no entry in Last Seen Map! This can happen after server travel. Entity: %lld, type: " - "Multicast"), - EntityId); - return; - } - LastSeenRPCId = LastSeenMulticastRPCIds[EntityId]; - } - else - { - if (!LastSeenRPCIds.Contains(EntityTypePair)) - { - UE_LOG(LogSpatialRPCService, Warning, - TEXT("Tried to extract RPCs but no entry in Last Seen Map! This can happen after server travel. Entity: %lld, type: %s"), - EntityId, *SpatialConstants::RPCTypeToString(Type)); - return; - } - LastSeenRPCId = LastSeenRPCIds[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()) - { - const 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 - { - LastSeenRPCIds[EntityTypePair] = LastProcessedRPCId; - } - } -} - -void SpatialRPCService::IncrementAckedRPCID(Worker_EntityId EntityId, ERPCType Type) -{ - if (Type == ERPCType::NetMulticast) - { - return; - } - - EntityRPCType EntityTypePair = EntityRPCType(EntityId, Type); - uint64* LastAckedRPCId = LastAckedRPCIds.Find(EntityTypePair); - if (LastAckedRPCId == nullptr) - { - UE_LOG(LogSpatialRPCService, Warning, TEXT("SpatialRPCService::IncrementAckedRPCID: Could not find last acked RPC id. Entity: %lld, RPC type: %s"), EntityId, *SpatialConstants::RPCTypeToString(Type)); - return; - } - - ++(*LastAckedRPCId); - - const EntityComponentId EntityComponentPair = { EntityId, RPCRingBufferUtils::GetAckComponentId(Type) }; - Schema_Object* EndpointObject = Schema_GetComponentUpdateFields(GetOrCreateComponentUpdate(EntityComponentPair)); - - RPCRingBufferUtils::WriteAckToSchema(EndpointObject, Type, *LastAckedRPCId); -} - -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; -} - -#if TRACE_LIB_ACTIVE -void SpatialRPCService::ProcessResultToLatencyTrace(const EPushRPCResult Result, const TraceKey Trace) -{ - if (SpatialLatencyTracer != nullptr && Trace != InvalidTraceKey) - { - bool bEndTrace = false; - FString TraceMsg; - switch (Result) - { - case SpatialGDK::EPushRPCResult::Success: - // No further action - break; - case SpatialGDK::EPushRPCResult::QueueOverflowed: - TraceMsg = TEXT("Overflowed"); - break; - case SpatialGDK::EPushRPCResult::DropOverflowed: - TraceMsg = TEXT("OverflowedAndDropped"); - bEndTrace = true; - break; - case SpatialGDK::EPushRPCResult::HasAckAuthority: - TraceMsg = TEXT("NoAckAuth"); - bEndTrace = true; - break; - case SpatialGDK::EPushRPCResult::NoRingBufferAuthority: - TraceMsg = TEXT("NoRingBufferAuth"); - bEndTrace = true; - break; - default: - TraceMsg = TEXT("UnrecognisedResult"); - break; - } - - if (bEndTrace) - { - // This RPC has been dropped, end the trace - SpatialLatencyTracer->WriteAndEndTrace(Trace, TraceMsg, false); - } - else if (!TraceMsg.IsEmpty()) - { - // This RPC will be sent later - SpatialLatencyTracer->WriteToLatencyTrace(Trace, TraceMsg); - } - } -} -#endif // TRACE_LIB_ACTIVE - -} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp index 6b7a615b8a..8c159297f8 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp @@ -11,17 +11,21 @@ #include "EngineClasses/SpatialActorChannel.h" #include "EngineClasses/SpatialFastArrayNetSerialize.h" +#include "EngineClasses/SpatialLoadBalanceEnforcer.h" #include "EngineClasses/SpatialNetConnection.h" +#include "EngineClasses/SpatialNetDriverDebugContext.h" #include "EngineClasses/SpatialPackageMapClient.h" #include "EngineClasses/SpatialVirtualWorkerTranslator.h" -#include "EngineClasses/SpatialLoadBalanceEnforcer.h" +#include "Interop/Connection/SpatialEventTracer.h" +#include "Interop/Connection/SpatialTraceEventBuilder.h" #include "Interop/Connection/SpatialWorkerConnection.h" #include "Interop/GlobalStateManager.h" #include "Interop/SpatialPlayerSpawner.h" #include "Interop/SpatialSender.h" -#include "Schema/AuthorityIntent.h" #include "Schema/DynamicComponent.h" +#include "Schema/MigrationDiagnostic.h" #include "Schema/RPCPayload.h" +#include "Schema/Restricted.h" #include "Schema/SpawnData.h" #include "Schema/Tombstone.h" #include "Schema/UnrealMetadata.h" @@ -44,20 +48,34 @@ DECLARE_CYCLE_STAT(TEXT("Receiver ComponentUpdate"), STAT_ReceiverComponentUpdat 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 SystemEntityCommandResponse"), STAT_ReceiverSystemEntityCommandResponse, 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, SpatialGDK::SpatialRPCService* InRPCService) +namespace // Anonymous namespace +{ +struct ChannelObjectsToBeResolved +{ + USpatialActorChannel* Channel; + TWeakObjectPtr Object; + FSpatialObjectRepState* RepState; +}; +#if DO_CHECK +FUsageLock ObjectRefToRepStateUsageLock; // A debug helper to trigger an ensure if something weird happens (re-entrancy) +#endif +} // namespace + +void USpatialReceiver::Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimerManager, SpatialRPCService* InRPCService, + SpatialEventTracer* InEventTracer) { NetDriver = InNetDriver; StaticComponentView = InNetDriver->StaticComponentView; @@ -65,12 +83,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(); + EventTracer = InEventTracer; } void USpatialReceiver::OnCriticalSection(bool InCriticalSection) @@ -106,7 +121,10 @@ void USpatialReceiver::LeaveCriticalSection() { OnEntityAddedDelegate.Broadcast(PendingAddEntity); } - PendingAddComponents.RemoveAll([PendingAddEntity](const PendingAddComponentWrapper& Component) {return Component.EntityId == PendingAddEntity;}); + + PendingAddComponents.RemoveAll([PendingAddEntity](const PendingAddComponentWrapper& Component) { + return Component.EntityId == PendingAddEntity && Component.ComponentId != SpatialConstants::GDK_DEBUG_COMPONENT_ID; + }); } // The reason the AuthorityChange processing is split according to authority is to avoid cases @@ -114,7 +132,7 @@ void USpatialReceiver::LeaveCriticalSection() // We process Lose Auth -> Add Components -> Gain Auth. A common thing that happens is that on handover we get // ComponentData -> Gain Auth, and with this split you receive data as if you were a client to get the most up-to-date state, // and then gain authority. Similarly, you first lose authority, and then receive data, in the opposite situation. - for (Worker_AuthorityChangeOp& PendingAuthorityChange : PendingAuthorityChanges) + for (Worker_ComponentSetAuthorityChangeOp& PendingAuthorityChange : PendingAuthorityChanges) { if (PendingAuthorityChange.authority != WORKER_AUTHORITY_AUTHORITATIVE) { @@ -128,11 +146,23 @@ void USpatialReceiver::LeaveCriticalSection() { continue; } + + if (PendingAddComponent.ComponentId == SpatialConstants::GDK_DEBUG_COMPONENT_ID) + { + if (NetDriver->DebugCtx != nullptr) + { + NetDriver->DebugCtx->OnDebugComponentUpdateReceived(PendingAddComponent.EntityId); + } + continue; + } + USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(PendingAddComponent.EntityId); if (Channel == nullptr) { - UE_LOG(LogSpatialReceiver, Error, TEXT("Got an add component for an entity that doesn't have an associated actor channel." - " Entity id: %lld, component id: %d."), PendingAddComponent.EntityId, PendingAddComponent.ComponentId); + UE_LOG(LogSpatialReceiver, Error, + TEXT("Got an add component for an entity that doesn't have an associated actor channel." + " Entity id: %lld, component id: %d."), + PendingAddComponent.EntityId, PendingAddComponent.ComponentId); continue; } if (Channel->bCreatedEntity) @@ -142,13 +172,14 @@ void USpatialReceiver::LeaveCriticalSection() continue; } - UE_LOG(LogSpatialReceiver, Verbose, + UE_LOG( + LogSpatialReceiver, Verbose, TEXT("Add component inside of a critical section, outside of an add entity, being handled: entity id %lld, component id %d."), PendingAddComponent.EntityId, PendingAddComponent.ComponentId); HandleIndividualAddComponent(PendingAddComponent.EntityId, PendingAddComponent.ComponentId, MoveTemp(PendingAddComponent.Data)); } - for (Worker_AuthorityChangeOp& PendingAuthorityChange : PendingAuthorityChanges) + for (Worker_ComponentSetAuthorityChangeOp& PendingAuthorityChange : PendingAuthorityChanges) { if (PendingAuthorityChange.authority == WORKER_AUTHORITY_AUTHORITATIVE) { @@ -171,14 +202,9 @@ void USpatialReceiver::OnAddEntity(const Worker_AddEntityOp& Op) 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); + 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; - } + const bool bWaitingForAsyncLoad = IsEntityWaitingForAsyncLoad(Op.entity_id); if (HasEntityBeenRequestedForDelete(Op.entity_id)) { @@ -188,10 +214,10 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op) // 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; + return RemoveComponentOp.entity_id == Op.entity_id && RemoveComponentOp.component_id == Op.data.component_id; }); + // Handle the first batch of components which do not need queuing when doing async loading. switch (Op.data.component_id) { case SpatialConstants::METADATA_COMPONENT_ID: @@ -203,17 +229,23 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op) case SpatialConstants::NOT_STREAMED_COMPONENT_ID: case SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID: case SpatialConstants::HEARTBEAT_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_LEGACY: - case SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY: + case SpatialConstants::SERVER_ONLY_ALWAYS_RELEVANT_COMPONENT_ID: + case SpatialConstants::VISIBLE_COMPONENT_ID: 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: case SpatialConstants::SERVER_WORKER_COMPONENT_ID: + case SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID: + case SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID: + case SpatialConstants::AUTHORITY_DELEGATION_COMPONENT_ID: + case SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID: + case SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID: + case SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID: + case SpatialConstants::MULTICAST_RPCS_COMPONENT_ID: + case SpatialConstants::MIGRATION_DIAGNOSTIC_COMPONENT_ID: // We either don't care about processing these components or we only need to store // the data (which is handled by the SpatialStaticComponentView). return; @@ -222,17 +254,34 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op) // 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.AddUnique(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); + + // PendingAddActor should only be populated with actors we actually want to check out. + // So a local and ready actor should not be considered as an actor pending addition. + // Nitty-gritty implementation side effect is also that we do not want to stomp + // the local state of a not ready-actor, because it is locally authoritative and will remain + // that way until it is marked as ready. Putting it in PendingAddActor has the side effect + // that the received component data will get dropped (likely outdated data), and is + // something we do not wish to happen for ready actor (likely new data received through + // a component refresh on authority delegation). + if (!bWaitingForAsyncLoad) + { + AActor* EntityActor = Cast(PackageMap->GetObjectFromEntityId(Op.entity_id)); + if (EntityActor == nullptr || !EntityActor->IsActorReady()) + { + PendingAddActors.AddUnique(Op.entity_id); + } } return; + } + + if (bWaitingForAsyncLoad) + { + QueueAddComponentOpForAsyncLoad(Op); + return; + } + + switch (Op.data.component_id) + { case SpatialConstants::WORKER_COMPONENT_ID: if (NetDriver->IsServer() && !WorkerConnectionEntities.Contains(Op.entity_id)) { @@ -242,19 +291,6 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op) 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::DEPLOYMENT_MAP_COMPONENT_ID: - GlobalStateManager->ApplyDeploymentMapData(Op.data); - return; - case SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID: - GlobalStateManager->ApplyStartupActorManagerData(Op.data); - return; case SpatialConstants::TOMBSTONE_COMPONENT_ID: RemoveActor(Op.entity_id); return; @@ -269,11 +305,26 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op) NetDriver->RegisterDormantEntityId(Op.entity_id); } return; - case SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID: - if (NetDriver->VirtualWorkerTranslator.IsValid()) + case SpatialConstants::GDK_DEBUG_COMPONENT_ID: + if (NetDriver->DebugCtx != nullptr) { - Schema_Object* ComponentObject = Schema_GetComponentDataFields(Op.data.schema_type); - NetDriver->VirtualWorkerTranslator->ApplyVirtualWorkerManagerData(ComponentObject); + if (bInCriticalSection) + { + PendingAddComponents.AddUnique( + PendingAddComponentWrapper(Op.entity_id, Op.data.component_id, MakeUnique(Op.data))); + } + else + { + NetDriver->DebugCtx->OnDebugComponentUpdateReceived(Op.entity_id); + } + } + return; + case SpatialConstants::PARTITION_COMPONENT_ID: + if (APlayerController* Controller = Cast(PackageMap->GetObjectFromEntityId(Op.entity_id).Get())) + { + const Partition* PartitionComp = StaticComponentView->GetComponentData(Op.entity_id); + NetDriver->RegisterClientConnection(PartitionComp->WorkerConnectionId, + Cast(Controller->GetNetConnection())); } return; } @@ -290,11 +341,12 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op) if (bInCriticalSection) { - PendingAddComponents.AddUnique(PendingAddComponentWrapper(Op.entity_id, Op.data.component_id, MakeUnique(Op.data))); + PendingAddComponents.AddUnique( + PendingAddComponentWrapper(Op.entity_id, Op.data.component_id, MakeUnique(Op.data))); } else { - HandleIndividualAddComponent(Op.entity_id, Op.data.component_id, MakeUnique(Op.data)); + HandleIndividualAddComponent(Op.entity_id, Op.data.component_id, MakeUnique(Op.data)); } } @@ -304,25 +356,23 @@ void USpatialReceiver::OnRemoveEntity(const Worker_RemoveEntityOp& Op) // Stop tracking if the entity was deleted as a result of deleting the actor during creation. // This assumes that authority will be gained before interest is gained and lost. - const int32 RetiredActorIndex = EntitiesToRetireOnAuthorityGain.IndexOfByPredicate([Op](const DeferredRetire& Retire) { return Op.entity_id == Retire.EntityId; }); + const int32 RetiredActorIndex = EntitiesToRetireOnAuthorityGain.IndexOfByPredicate([Op](const DeferredRetire& Retire) { + return Op.entity_id == Retire.EntityId; + }); if (RetiredActorIndex != INDEX_NONE) { EntitiesToRetireOnAuthorityGain.RemoveAtSwap(RetiredActorIndex); } - if (LoadBalanceEnforcer != nullptr) - { - LoadBalanceEnforcer->OnEntityRemoved(Op); - } - 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. + // 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); + TWeakObjectPtr ClientConnectionPtr = NetDriver->FindClientConnectionFromWorkerEntityId(Op.entity_id); if (USpatialNetConnection* ClientConnection = ClientConnectionPtr.Get()) { if (APlayerController* Controller = ClientConnection->GetPlayerController(/*InWorld*/ nullptr)) @@ -330,7 +380,8 @@ void USpatialReceiver::OnRemoveEntity(const Worker_RemoveEntityOp& Op) 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)); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Worker %s disconnected after its system identity was removed."), + *(*WorkerName)); CloseClientConnection(ClientConnection, PCEntity); } } @@ -345,10 +396,9 @@ void USpatialReceiver::OnRemoveComponent(const Worker_RemoveComponentOp& Op) // We should exit early if we're receiving a duplicate RemoveComponent op. This can happen with dynamic // components enabled. We detect if the op is a duplicate via the queue of ops to be processed (duplicate // op receive in the same op list). - if (QueuedRemoveComponentOps.ContainsByPredicate([&Op](const Worker_RemoveComponentOp& QueuedOp) - { - return QueuedOp.entity_id == Op.entity_id && QueuedOp.component_id == Op.component_id; - })) + if (QueuedRemoveComponentOps.ContainsByPredicate([&Op](const Worker_RemoveComponentOp& QueuedOp) { + return QueuedOp.entity_id == Op.entity_id && QueuedOp.component_id == Op.component_id; + })) { return; } @@ -362,19 +412,21 @@ void USpatialReceiver::OnRemoveComponent(const Worker_RemoveComponentOp& Op) } else { + if (bInCriticalSection) + { + PendingAddActors.Remove(Op.entity_id); + } 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)) + if (bInCriticalSection) { - LoadBalanceEnforcer->OnLoadBalancingComponentRemoved(Op); + // Cancel out the Pending adds to avoid getting errors if an actor is not created for these components when leaving the critical + // section. Paired Add/Remove could happen, and processing the queued ops would happen too late to prevent it. + PendingAddComponents.RemoveAll([&Op](const PendingAddComponentWrapper& PendingAdd) { + return PendingAdd.EntityId == Op.entity_id && PendingAdd.ComponentId == Op.component_id; + }); } // We are queuing here because if an Actor is removed from your view, remove component ops will be @@ -458,7 +510,8 @@ void USpatialReceiver::ProcessRemoveComponent(const Worker_RemoveComponentOp& Op { if (USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(Op.entity_id)) { - Channel->OnSubobjectDeleted(ObjectRef, Object); + TWeakObjectPtr WeakPtr(Object); + Channel->OnSubobjectDeleted(ObjectRef, Object, WeakPtr); Actor->OnSubobjectDestroyFromReplication(Object); @@ -479,11 +532,11 @@ void USpatialReceiver::UpdateShadowData(Worker_EntityId EntityId) ActorChannel->UpdateShadowData(); } -void USpatialReceiver::OnAuthorityChange(const Worker_AuthorityChangeOp& Op) +void USpatialReceiver::OnAuthorityChange(const Worker_ComponentSetAuthorityChangeOp& Op) { if (HasEntityBeenRequestedForDelete(Op.entity_id)) { - if (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE && Op.component_id == SpatialConstants::POSITION_COMPONENT_ID) + if (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE && Op.component_set_id == SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) { HandleEntityDeletedAuthority(Op.entity_id); } @@ -494,17 +547,6 @@ void USpatialReceiver::OnAuthorityChange(const Worker_AuthorityChangeOp& Op) // This way systems that depend on having non-stale state can function correctly. StaticComponentView->OnAuthorityChange(Op); - if (Op.component_id == SpatialConstants::SERVER_WORKER_COMPONENT_ID && Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) - { - GlobalStateManager->TrySendWorkerReadyToBeginPlay(); - return; - } - - if (Op.component_id == SpatialConstants::ENTITY_ACL_COMPONENT_ID && LoadBalanceEnforcer != nullptr) - { - LoadBalanceEnforcer->OnAclAuthorityChanged(Op); - } - SCOPE_CYCLE_COUNTER(STAT_ReceiverAuthChange); if (IsEntityWaitingForAsyncLoad(Op.entity_id)) { @@ -512,18 +554,6 @@ void USpatialReceiver::OnAuthorityChange(const Worker_AuthorityChangeOp& Op) return; } - // Process authority gained event immediately, so if we're in a critical section, the RPCService will - // be correctly configured to process RPCs sent during Actor creation - if (GetDefault()->UseRPCRingBuffer() && RPCService != nullptr && Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) - { - if (Op.component_id == SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID || - Op.component_id == SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID || - Op.component_id == SpatialConstants::MULTICAST_RPCS_COMPONENT_ID) - { - RPCService->OnEndpointAuthorityGained(Op.entity_id, Op.component_id); - } - } - if (bInCriticalSection) { // The actor receiving flow requires authority to be handled after all components have been received, so buffer those if we @@ -535,14 +565,15 @@ void USpatialReceiver::OnAuthorityChange(const Worker_AuthorityChangeOp& Op) HandleActorAuthority(Op); } -void USpatialReceiver::HandlePlayerLifecycleAuthority(const Worker_AuthorityChangeOp& Op, APlayerController* PlayerController) +void USpatialReceiver::HandlePlayerLifecycleAuthority(const Worker_ComponentSetAuthorityChangeOp& Op, APlayerController* PlayerController) { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("HandlePlayerLifecycleAuthority for PlayerController %d."), *AActor::GetDebugName(PlayerController)); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("HandlePlayerLifecycleAuthority for PlayerController %s."), + *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) || - (!NetDriver->IsServer() && Op.component_id == SpatialConstants::HEARTBEAT_COMPONENT_ID)) + if ((NetDriver->IsServer() && Op.component_set_id == SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) + || (!NetDriver->IsServer() && Op.component_set_id == SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID)) { if (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) { @@ -569,25 +600,10 @@ void USpatialReceiver::HandlePlayerLifecycleAuthority(const Worker_AuthorityChan } } -void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) +void USpatialReceiver::HandleActorAuthority(const Worker_ComponentSetAuthorityChangeOp& 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 - && Op.authority == WORKER_AUTHORITY_AUTHORITATIVE - && Op.component_id == SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID) + if (NetDriver->SpatialDebugger != nullptr && Op.authority == WORKER_AUTHORITY_AUTHORITATIVE + && Op.component_set_id == SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) { NetDriver->SpatialDebugger->ActorAuthorityChanged(Op); } @@ -598,35 +614,23 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) return; } + // TODO - Using bActorHadAuthority should be replaced with better tracking system to Actor entity creation [UNR-3960] + const bool bActorHadAuthority = Actor->HasAuthority(); + USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(Op.entity_id); if (Channel != nullptr) { - if (Op.component_id == SpatialConstants::POSITION_COMPONENT_ID) + if (Op.component_set_id == SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) { Channel->SetServerAuthority(Op.authority == WORKER_AUTHORITY_AUTHORITATIVE); } - else if (Op.component_id == SpatialConstants::GetClientAuthorityComponent(GetDefault()->UseRPCRingBuffer())) + else if (Op.component_set_id == SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID) { 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()); - if (RPCsOnEntityCreation* QueuedRPCs = StaticComponentView->GetComponentData(Op.entity_id)) - { - if (QueuedRPCs->HasRPCPayloadData()) - { - ProcessQueuedActorRPCsOnEntityCreation(Op.entity_id, *QueuedRPCs); - } - - Sender->SendRequestToClearRPCsOnEntityCreation(Op.entity_id); - } - } - if (APlayerController* PlayerController = Cast(Actor)) { HandlePlayerLifecycleAuthority(Op, PlayerController); @@ -637,7 +641,7 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) // TODO UNR-955 - Remove this once batch reservation of EntityIds are in. if (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) { - Sender->ProcessUpdatesQueuedUntilAuthority(Op.entity_id, Op.component_id); + Sender->ProcessUpdatesQueuedUntilAuthority(Op.entity_id, Op.component_set_id); } // If we became authoritative over the position component. set our role to be ROLE_Authority @@ -647,7 +651,7 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) // is player controlled when gaining authority over the pawn and need to wait for the player // state. Likewise, it's possible that the player state doesn't have a pointer to its pawn // yet, so we need to wait for the pawn to arrive. - if (Op.component_id == SpatialConstants::POSITION_COMPONENT_ID) + if (Op.component_set_id == SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) { if (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) { @@ -658,6 +662,12 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) Actor->Role = ROLE_Authority; Actor->RemoteRole = ROLE_SimulatedProxy; + // bReplicates is not replicated, but this actor is replicated. + if (!Actor->GetIsReplicated()) + { + Actor->SetReplicates(true); + } + if (Actor->IsA()) { Actor->RemoteRole = ROLE_AutonomousProxy; @@ -687,18 +697,28 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) UpdateShadowData(Op.entity_id); } + // TODO - Using bActorHadAuthority should be replaced with better tracking system to Actor entity creation [UNR-3960] + // When receiving AuthorityGained from SpatialOS, the Actor role will be ROLE_Authority iff this + // worker is receiving entity data for the 1st time after spawning the entity. In all other cases, + // the Actor role will have been explicitly set to ROLE_SimulatedProxy previously during the + // entity creation flow. + if (bActorHadAuthority) + { + Actor->SetActorReady(true); + } + + // We still want to call OnAuthorityGained if the Actor migrated to this worker or was loaded from a snapshot. Actor->OnAuthorityGained(); } else { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Received authority over actor %s, with entity id %lld, which has no channel. This means it attempted to delete it earlier, when it had no authority. Retrying to delete now."), *Actor->GetName(), Op.entity_id); + UE_LOG(LogSpatialReceiver, Verbose, + TEXT("Received authority over actor %s, with entity id %lld, which has no channel. This means it attempted to " + "delete it earlier, when it had no authority. Retrying to delete now."), + *Actor->GetName(), Op.entity_id); Sender->RetireEntity(Op.entity_id, Actor->IsNetStartupActor()); } } - else if (Op.authority == WORKER_AUTHORITY_AUTHORITY_LOSS_IMMINENT) - { - Actor->OnAuthorityLossImminent(); - } else if (Op.authority == WORKER_AUTHORITY_NOT_AUTHORITATIVE) { if (Channel != nullptr) @@ -707,8 +727,8 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) } // 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. + // set AuthorityDelegation to another server-side worker partition. This conditional exists to dodge + // calling OnAuthorityLost twice. if (Actor->Role != ROLE_SimulatedProxy) { Actor->Role = ROLE_SimulatedProxy; @@ -718,40 +738,12 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) } } } - - // Subobject Delegation - TPair EntityComponentPair = MakeTuple(static_cast(Op.entity_id), Op.component_id); - if (TSharedRef* PendingSubobjectAttachmentPtr = PendingEntitySubobjectDelegations.Find(EntityComponentPair)) - { - FPendingSubobjectAttachment& PendingSubobjectAttachment = PendingSubobjectAttachmentPtr->Get(); - - PendingSubobjectAttachment.PendingAuthorityDelegations.Remove(Op.component_id); - - if (PendingSubobjectAttachment.PendingAuthorityDelegations.Num() == 0) - { - if (UObject* Object = PendingSubobjectAttachment.Subobject.Get()) - { - if (IsValid(Channel)) - { - // TODO: UNR-664 - We should track the bytes sent here and factor them into channel saturation. - uint32 BytesWritten = 0; - Sender->SendAddComponentForSubobject(Channel, Object, *PendingSubobjectAttachment.Info, BytesWritten); - } - } - } - - PendingEntitySubobjectDelegations.Remove(EntityComponentPair); - } } - else if (Op.component_id == SpatialConstants::GetClientAuthorityComponent(GetDefault()->UseRPCRingBuffer())) + else if (Op.component_set_id == SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID) { if (Channel != nullptr) { - // Soft handover isn't supported currently. - if (Op.authority != WORKER_AUTHORITY_AUTHORITY_LOSS_IMMINENT) - { - Channel->ClientProcessOwnershipChange(Op.authority == WORKER_AUTHORITY_AUTHORITATIVE); - } + Channel->ClientProcessOwnershipChange(Op.authority == WORKER_AUTHORITY_AUTHORITATIVE); } // If we are a Pawn or PlayerController, our local role should be ROLE_AutonomousProxy. Otherwise ROLE_SimulatedProxy @@ -761,43 +753,10 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) } } - 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 (GetDefault()->UseRPCRingBuffer() && RPCService != nullptr) - { - if (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) - { - if (Op.component_id != SpatialConstants::MULTICAST_RPCS_COMPONENT_ID) - { - // If we have just received authority over the client endpoint, then we are a client. In that case, - // we want to scrape the server endpoint for any server -> client RPCs that are waiting to be called. - const Worker_ComponentId ComponentToExtractFrom = Op.component_id == SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID ? SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID : SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID; - RPCService->ExtractRPCsForEntity(Op.entity_id, ComponentToExtractFrom); - } - } - else if (Op.authority == WORKER_AUTHORITY_NOT_AUTHORITATIVE) - { - RPCService->OnEndpointAuthorityLost(Op.entity_id, Op.component_id); - } - } - else - { - 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 (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) + if (NetDriver->DebugCtx && Op.authority == WORKER_AUTHORITY_NOT_AUTHORITATIVE + && Op.component_set_id == SpatialConstants::SERVER_AUTH_COMPONENT_SET_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) - { - Sender->SendServerEndpointReadyUpdate(Op.entity_id); - } + NetDriver->DebugCtx->OnDebugComponentAuthLost(Op.entity_id); } } @@ -854,19 +813,26 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) AActor* EntityActor = Cast(PackageMap->GetObjectFromEntityId(EntityId)); if (EntityActor != nullptr) { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("%s: Entity %lld for Actor %s has been checked out on the worker which spawned it."), - *NetDriver->Connection->GetWorkerId(), EntityId, *EntityActor->GetName()); + if (!EntityActor->IsActorReady()) + { + UE_LOG(LogSpatialReceiver, Verbose, TEXT("%s: Entity %lld for Actor %s has been checked out on the worker which spawned it."), + *NetDriver->Connection->GetWorkerId(), EntityId, *EntityActor->GetName()); + } + return; } - 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); + 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) { - 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); + 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; } @@ -877,7 +843,8 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) // (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); + UE_LOG(LogSpatialReceiver, Verbose, + TEXT("The received actor with entity ID %lld was already torn off. The actor will not be spawned."), EntityId); return; } @@ -894,7 +861,8 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) // 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); + 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); @@ -909,18 +877,28 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) { // If entity is a PlayerController, create channel on the PlayerController's connection. Connection = PlayerController->NetConnection; + + // If this already has a partition component, assign the client mapping + if (const Partition* PartitionComp = StaticComponentView->GetComponentData(EntityId)) + { + NetDriver->RegisterClientConnection(PartitionComp->WorkerConnectionId, + Cast(PlayerController->GetNetConnection())); + } } } if (Connection == nullptr) { - UE_LOG(LogSpatialReceiver, Error, TEXT("Unable to find SpatialOSNetConnection! Has this worker been disconnected from SpatialOS due to a timeout?")); + UE_LOG(LogSpatialReceiver, Error, + TEXT("Unable to find SpatialOSNetConnection! Has this worker been disconnected from SpatialOS due to a timeout?")); return; } 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()); + 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; } @@ -929,12 +907,15 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(EntityId); if (Channel == nullptr) { - Channel = Cast(Connection->CreateChannelByName(NAME_Actor, NetDriver->IsServer() ? EChannelCreateFlags::OpenedLocally : EChannelCreateFlags::None)); + Channel = Cast(Connection->CreateChannelByName( + NAME_Actor, NetDriver->IsServer() ? EChannelCreateFlags::OpenedLocally : EChannelCreateFlags::None)); } 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()); + 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; } @@ -956,9 +937,10 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) continue; } - if (PendingAddComponent.EntityId == EntityId) + if (PendingAddComponent.EntityId == EntityId && PendingAddComponent.ComponentId != SpatialConstants::GDK_DEBUG_COMPONENT_ID) { - ApplyComponentDataOnActorCreation(EntityId, PendingAddComponent.Data->Data.GetWorkerComponentData(), *Channel, ActorClassInfo, ObjectsToResolvePendingOpsFor); + ApplyComponentDataOnActorCreation(EntityId, PendingAddComponent.Data->Data.GetWorkerComponentData(), *Channel, ActorClassInfo, + ObjectsToResolvePendingOpsFor); } } @@ -988,6 +970,9 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) } } + // Any Actor created here will have been received over the wire as an entity so we can mark it ready. + EntityActor->SetActorReady(false); + // Taken from PostNetInit if (NetDriver->GetWorld()->HasBegunPlay() && !EntityActor->HasActorBegunPlay()) { @@ -997,7 +982,9 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) // flow) take care of setting roles correctly. if (EntityActor->HasAuthority()) { - UE_LOG(LogSpatialReceiver, Error, TEXT("Trying to unexpectedly spawn received network Actor with authority. Actor %s. Entity: %lld"), *EntityActor->GetName(), EntityId); + UE_LOG(LogSpatialReceiver, Error, + TEXT("Trying to unexpectedly spawn received network Actor with authority. Actor %s. Entity: %lld"), + *EntityActor->GetName(), EntityId); EntityActor->Role = ROLE_SimulatedProxy; EntityActor->RemoteRole = ROLE_Authority; } @@ -1027,7 +1014,8 @@ void USpatialReceiver::RemoveActor(Worker_EntityId EntityId) AActor* Actor = Cast(WeakActor.Get()); - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Worker %s Remove Actor: %s %lld"), *NetDriver->Connection->GetWorkerId(), Actor && !Actor->IsPendingKill() ? *Actor->GetName() : TEXT("nullptr"), EntityId); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Worker %s Remove Actor: %s %lld"), *NetDriver->Connection->GetWorkerId(), + Actor && !Actor->IsPendingKill() ? *Actor->GetName() : TEXT("nullptr"), EntityId); // Cleanup pending add components if any exist. if (USpatialActorChannel* ActorChannel = NetDriver->GetActorChannelByEntityId(EntityId)) @@ -1051,7 +1039,10 @@ void USpatialReceiver::RemoveActor(Worker_EntityId EntityId) { if (USpatialActorChannel* ActorChannel = NetDriver->GetActorChannelByEntityId(EntityId)) { - UE_LOG(LogSpatialReceiver, Warning, TEXT("RemoveActor: actor for entity %lld was already deleted (likely on the authoritative worker) but still has an open actor channel."), EntityId); + UE_LOG(LogSpatialReceiver, Warning, + TEXT("RemoveActor: actor for entity %lld was already deleted (likely on the authoritative worker) but still has an open " + "actor channel."), + EntityId); ActorChannel->ConditionalCleanUp(false, EChannelCloseReason::Destroyed); } return; @@ -1083,7 +1074,8 @@ void USpatialReceiver::RemoveActor(Worker_EntityId EntityId) 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 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); @@ -1093,9 +1085,26 @@ void USpatialReceiver::RemoveActor(Worker_EntityId EntityId) // Actor is a startup actor that is a part of the level. If it's not Tombstone'd, then it // has just fallen out of our view and we should only remove the entity. - if (Actor->IsFullNameStableForNetworking() && - StaticComponentView->HasComponent(EntityId, SpatialConstants::TOMBSTONE_COMPONENT_ID) == false) + if (Actor->IsFullNameStableForNetworking() + && StaticComponentView->HasComponent(EntityId, SpatialConstants::TOMBSTONE_COMPONENT_ID) == false) { + PackageMap->ClearRemovedDynamicSubobjectObjectRefs(EntityId); + if (USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(EntityId)) + { + for (UObject* DynamicSubobject : Channel->CreateSubObjects) + { + FNetworkGUID SubobjectNetGUID = PackageMap->GetNetGUIDFromObject(DynamicSubobject); + if (SubobjectNetGUID.IsValid()) + { + FUnrealObjectRef SubobjectRef = PackageMap->GetUnrealObjectRefFromNetGUID(SubobjectNetGUID); + + if (SubobjectRef.IsValid() && IsDynamicSubObject(Actor, SubobjectRef.Offset)) + { + PackageMap->AddRemovedDynamicSubobjectObjectRef(SubobjectRef, SubobjectNetGUID); + } + } + } + } // We can't call CleanupDeletedEntity here as we need the NetDriver to maintain the EntityId // to Actor Channel mapping for the DestroyActor to function correctly PackageMap->RemoveEntityActor(EntityId); @@ -1108,7 +1117,8 @@ void USpatialReceiver::RemoveActor(Worker_EntityId EntityId) PC->Player = nullptr; } - // Workaround for camera loss on handover: prevent UnPossess() (non-authoritative destruction of pawn, while being authoritative over the controller) + // Workaround for camera loss on handover: prevent UnPossess() (non-authoritative destruction of pawn, while being authoritative over + // the controller) // TODO: Check how AI controllers are affected by this (UNR-430) // TODO: This should be solved properly by working sets (UNR-411) if (APawn* Pawn = Cast(Actor)) @@ -1152,10 +1162,17 @@ void USpatialReceiver::DestroyActor(AActor* Actor, Worker_EntityId EntityId) } else { - 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); + 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); } } + if (APlayerController* PC = Cast(Actor)) + { + NetDriver->CleanUpServerConnectionForPC(PC); + } + // It is safe to call AActor::Destroy even if the destruction has already started. if (Actor != nullptr && !Actor->Destroy(true)) { @@ -1166,7 +1183,8 @@ void USpatialReceiver::DestroyActor(AActor* Actor, Worker_EntityId EntityId) check(PackageMap->GetObjectFromEntityId(EntityId) == nullptr); } -AActor* USpatialReceiver::TryGetOrCreateActor(UnrealMetadata* UnrealMetadataComp, SpawnData* SpawnDataComp, NetOwningClientWorker* NetOwningClientWorkerComp) +AActor* USpatialReceiver::TryGetOrCreateActor(UnrealMetadata* UnrealMetadataComp, SpawnData* SpawnDataComp, + NetOwningClientWorker* NetOwningClientWorkerComp) { if (UnrealMetadataComp->StablyNamedRef.IsSet()) { @@ -1196,7 +1214,8 @@ AActor* USpatialReceiver::TryGetOrCreateActor(UnrealMetadata* UnrealMetadataComp } // This function is only called for client and server workers who did not spawn the Actor -AActor* USpatialReceiver::CreateActor(UnrealMetadata* UnrealMetadataComp, SpawnData* SpawnDataComp, NetOwningClientWorker* NetOwningClientWorkerComp) +AActor* USpatialReceiver::CreateActor(UnrealMetadata* UnrealMetadataComp, SpawnData* SpawnDataComp, + NetOwningClientWorker* NetOwningClientWorkerComp) { UClass* ActorClass = UnrealMetadataComp->GetNativeEntityClass(); @@ -1223,8 +1242,8 @@ AActor* USpatialReceiver::CreateActor(UnrealMetadata* UnrealMetadataComp, SpawnD if (NetDriver->IsServer() && bCreatingPlayerController) { // 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); + check(NetOwningClientWorkerComp->ClientPartitionId.IsSet()); + NetDriver->PostSpawnPlayerController(Cast(NewActor)); } // Imitate the behavior in UPackageMapClient::SerializeNewActor. @@ -1260,7 +1279,9 @@ FTransform USpatialReceiver::GetRelativeSpawnTransform(UClass* ActorClass, FTran return NewTransform; } -void USpatialReceiver::ApplyComponentDataOnActorCreation(Worker_EntityId EntityId, const Worker_ComponentData& Data, USpatialActorChannel& Channel, const FClassInfo& ActorClassInfo, TArray& OutObjectsToResolve) +void USpatialReceiver::ApplyComponentDataOnActorCreation(Worker_EntityId EntityId, const Worker_ComponentData& Data, + USpatialActorChannel& Channel, const FClassInfo& ActorClassInfo, + TArray& OutObjectsToResolve) { AActor* Actor = Channel.GetActor(); @@ -1268,7 +1289,10 @@ void USpatialReceiver::ApplyComponentDataOnActorCreation(Worker_EntityId EntityI bool bFoundOffset = ClassInfoManager->GetOffsetByComponentId(Data.component_id, Offset); if (!bFoundOffset) { - UE_LOG(LogSpatialReceiver, Warning, TEXT("Worker: %s EntityId: %lld, ComponentId: %d - Could not find offset for component id when applying component data to Actor %s!"), *NetDriver->Connection->GetWorkerId(), EntityId, Data.component_id, *Actor->GetPathName()); + UE_LOG(LogSpatialReceiver, Warning, + TEXT("Worker: %s EntityId: %lld, ComponentId: %d - Could not find offset for component id when applying component data to " + "Actor %s!"), + *NetDriver->Connection->GetWorkerId(), EntityId, Data.component_id, *Actor->GetPathName()); return; } @@ -1276,14 +1300,29 @@ void USpatialReceiver::ApplyComponentDataOnActorCreation(Worker_EntityId EntityI TWeakObjectPtr TargetObject = PackageMap->GetObjectFromUnrealObjectRef(TargetObjectRef); if (!TargetObject.IsValid()) { - bool bIsDynamicSubobject = !ActorClassInfo.SubobjectInfo.Contains(Offset); - if (!bIsDynamicSubobject) + if (!IsDynamicSubObject(Actor, Offset)) { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Tried to apply component data on actor creation for a static subobject that's been deleted, will skip. Entity: %lld, Component: %d, Actor: %s"), EntityId, Data.component_id, *Actor->GetPathName()); + UE_LOG(LogSpatialReceiver, Verbose, + TEXT("Tried to apply component data on actor creation for a static subobject that's been deleted, will skip. Entity: " + "%lld, Component: %d, Actor: %s"), + EntityId, Data.component_id, *Actor->GetPathName()); return; } - // If we can't find this subobject, it's a dynamically attached object. Create it now. + // If we can't find this subobject, it's a dynamically attached object. Check if we created previously. + if (FNetworkGUID* SubobjectNetGUID = PackageMap->GetRemovedDynamicSubobjectNetGUID(TargetObjectRef)) + { + if (UObject* DynamicSubobject = PackageMap->GetObjectFromNetGUID(*SubobjectNetGUID, false)) + { + PackageMap->ResolveSubobject(DynamicSubobject, TargetObjectRef); + ApplyComponentData(Channel, *DynamicSubobject, Data); + + OutObjectsToResolve.Add(ObjectPtrRefPair(DynamicSubobject, TargetObjectRef)); + return; + } + } + + // If the dynamically attached object was not created before. Create it now. TargetObject = NewObject(Actor, ClassInfoManager->GetClassByComponentId(Data.component_id)); Actor->OnSubobjectCreatedFromReplication(TargetObject.Get()); @@ -1293,19 +1332,32 @@ void USpatialReceiver::ApplyComponentDataOnActorCreation(Worker_EntityId EntityI Channel.CreateSubObjects.Add(TargetObject.Get()); } + FString TargetObjectPath = TargetObject->GetPathName(); ApplyComponentData(Channel, *TargetObject, Data); - OutObjectsToResolve.Add(ObjectPtrRefPair(TargetObject.Get(), TargetObjectRef)); + if (TargetObject.IsValid()) + { + OutObjectsToResolve.Add(ObjectPtrRefPair(TargetObject.Get(), TargetObjectRef)); + } + else + { + // TODO: remove / downgrade this to a log after verifying we handle this properly - UNR-4379 + UE_LOG(LogSpatialReceiver, Warning, TEXT("Actor subobject got invalidated after applying component data! Subobject: %s"), + *TargetObjectPath); + } } -void USpatialReceiver::HandleIndividualAddComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, TUniquePtr Data) +void USpatialReceiver::HandleIndividualAddComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, + TUniquePtr Data) { uint32 Offset = 0; bool bFoundOffset = ClassInfoManager->GetOffsetByComponentId(ComponentId, Offset); if (!bFoundOffset) { - UE_LOG(LogSpatialReceiver, Warning, TEXT("Could not find offset for component id when receiving dynamic AddComponent." - " (EntityId %lld, ComponentId %d)"), EntityId, ComponentId); + UE_LOG(LogSpatialReceiver, Warning, + TEXT("Could not find offset for component id when receiving dynamic AddComponent." + " (EntityId %lld, ComponentId %d)"), + EntityId, ComponentId); return; } @@ -1323,26 +1375,28 @@ void USpatialReceiver::HandleIndividualAddComponent(Worker_EntityId EntityId, Wo AActor* Actor = Cast(PackageMap->GetObjectFromEntityId(EntityId).Get()); if (Actor == nullptr) { - UE_LOG(LogSpatialReceiver, Warning, TEXT("Received an add component op for subobject of type %s on entity %lld but couldn't find Actor!"), *Info.Class->GetName(), EntityId); + UE_LOG(LogSpatialReceiver, Warning, + TEXT("Received an add component op for subobject of type %s on entity %lld but couldn't find Actor!"), + *Info.Class->GetName(), EntityId); return; } // Check if this is a static subobject that's been destroyed by the receiver. - const FClassInfo& ActorClassInfo = ClassInfoManager->GetOrCreateClassInfoByClass(Actor->GetClass()); - bool bIsDynamicSubobject = !ActorClassInfo.SubobjectInfo.Contains(Offset); - if (!bIsDynamicSubobject) + if (!IsDynamicSubObject(Actor, Offset)) { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Tried to apply component data on add component for a static subobject that's been deleted, will skip. Entity: %lld, Component: %d, Actor: %s"), EntityId, ComponentId, *Actor->GetPathName()); + UE_LOG(LogSpatialReceiver, Verbose, + TEXT("Tried to apply component data on add component for a static subobject that's been deleted, will skip. Entity: %lld, " + "Component: %d, Actor: %s"), + EntityId, ComponentId, *Actor->GetPathName()); return; } // Otherwise this is a dynamically attached component. We need to make sure we have all related components before creation. PendingDynamicSubobjectComponents.Add(MakeTuple(static_cast(EntityId), ComponentId), - PendingAddComponentWrapper(EntityId, ComponentId, MoveTemp(Data))); + PendingAddComponentWrapper(EntityId, ComponentId, MoveTemp(Data))); bool bReadyToCreate = true; - ForAllSchemaComponentTypes([&](ESchemaComponentType Type) - { + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { Worker_ComponentId SchemaComponentId = Info.SchemaComponents[Type]; if (SchemaComponentId == SpatialConstants::INVALID_COMPONENT_ID) @@ -1367,7 +1421,9 @@ void USpatialReceiver::AttachDynamicSubobject(AActor* Actor, Worker_EntityId Ent USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(EntityId); if (Channel == nullptr) { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Tried to dynamically attach subobject of type %s to entity %lld but couldn't find Channel!"), *Info.Class->GetName(), EntityId); + UE_LOG(LogSpatialReceiver, Verbose, + TEXT("Tried to dynamically attach subobject of type %s to entity %lld but couldn't find Channel!"), *Info.Class->GetName(), + EntityId); return; } @@ -1380,8 +1436,7 @@ void USpatialReceiver::AttachDynamicSubobject(AActor* Actor, Worker_EntityId Ent Channel->CreateSubObjects.Add(Subobject); - ForAllSchemaComponentTypes([&](ESchemaComponentType Type) - { + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { Worker_ComponentId ComponentId = Info.SchemaComponents[Type]; if (ComponentId == SpatialConstants::INVALID_COMPONENT_ID) @@ -1389,7 +1444,8 @@ void USpatialReceiver::AttachDynamicSubobject(AActor* Actor, Worker_EntityId Ent return; } - TPair EntityComponentPair = MakeTuple(static_cast(EntityId), ComponentId); + TPair EntityComponentPair = + MakeTuple(static_cast(EntityId), ComponentId); PendingAddComponentWrapper& AddComponent = PendingDynamicSubobjectComponents[EntityComponentPair]; ApplyComponentData(*Channel, *Subobject, AddComponent.Data->Data.GetWorkerComponentData()); @@ -1408,10 +1464,7 @@ struct USpatialReceiver::RepStateUpdateHelper { } - ~RepStateUpdateHelper() - { - check(bUpdatePerfomed); - } + ~RepStateUpdateHelper() { check(bUpdatePerfomed); } FObjectReferencesMap& GetRefMap() { @@ -1430,12 +1483,14 @@ struct USpatialReceiver::RepStateUpdateHelper { if (ObjectRepState == nullptr && TempRefMap.Num() > 0) { - ObjectRepState = &Channel.ObjectReferenceMap.Add(ObjectPtr, FSpatialObjectRepState(FChannelObjectPair(&Channel, ObjectPtr))); + ObjectRepState = + &Channel.ObjectReferenceMap.Add(ObjectPtr, FSpatialObjectRepState(FChannelObjectPair(&Channel, ObjectPtr))); ObjectRepState->ReferenceMap = MoveTemp(TempRefMap); } if (ObjectRepState) { + GDK_ENSURE_NO_MODIFICATIONS(ObjectRefToRepStateUsageLock); ObjectRepState->UpdateRefToRepStateMap(Receiver.ObjectRefToRepStateMap); if (ObjectRepState->ReferencedObj.Num() == 0) @@ -1449,7 +1504,7 @@ struct USpatialReceiver::RepStateUpdateHelper #endif } - //TSet UnresolvedRefs; + // TSet UnresolvedRefs; private: FObjectReferencesMap TempRefMap; TWeakObjectPtr ObjectPtr; @@ -1465,7 +1520,6 @@ void USpatialReceiver::ApplyComponentData(USpatialActorChannel& Channel, UObject checkf(Class, TEXT("Component %d isn't hand-written and not present in ComponentToClassMap."), Data.component_id); ESchemaComponentType ComponentType = ClassInfoManager->GetCategoryByComponentId(Data.component_id); - if (ComponentType == SCHEMA_Data || ComponentType == SCHEMA_OwnerOnly) { if (ComponentType == SCHEMA_Data && TargetObject.IsA()) @@ -1479,7 +1533,7 @@ void USpatialReceiver::ApplyComponentData(USpatialActorChannel& Channel, UObject } RepStateUpdateHelper RepStateHelper(Channel, TargetObject); - ComponentReader Reader(NetDriver, RepStateHelper.GetRefMap()); + ComponentReader Reader(NetDriver, RepStateHelper.GetRefMap(), EventTracer); bool bOutReferencesChanged = false; Reader.ApplyComponentData(Data, TargetObject, Channel, /* bIsHandover */ false, bOutReferencesChanged); @@ -1489,7 +1543,7 @@ void USpatialReceiver::ApplyComponentData(USpatialActorChannel& Channel, UObject { RepStateUpdateHelper RepStateHelper(Channel, TargetObject); - ComponentReader Reader(NetDriver, RepStateHelper.GetRefMap()); + ComponentReader Reader(NetDriver, RepStateHelper.GetRefMap(), EventTracer); bool bOutReferencesChanged = false; Reader.ApplyComponentData(Data, TargetObject, Channel, /* bIsHandover */ true, bOutReferencesChanged); @@ -1497,7 +1551,8 @@ void USpatialReceiver::ApplyComponentData(USpatialActorChannel& Channel, UObject } 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); } } @@ -1512,72 +1567,58 @@ void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) switch (Op.update.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::INTEREST_COMPONENT_ID: + case SpatialConstants::AUTHORITY_DELEGATION_COMPONENT_ID: + case SpatialConstants::PARTITION_COMPONENT_ID: case SpatialConstants::SPAWN_DATA_COMPONENT_ID: case SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID: case SpatialConstants::UNREAL_METADATA_COMPONENT_ID: case SpatialConstants::NOT_STREAMED_COMPONENT_ID: - case SpatialConstants::RPCS_ON_ENTITY_CREATION_ID: case SpatialConstants::DEBUG_METRICS_COMPONENT_ID: case SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID: + case SpatialConstants::SERVER_ONLY_ALWAYS_RELEVANT_COMPONENT_ID: + case SpatialConstants::VISIBLE_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::CLIENT_ENDPOINT_COMPONENT_ID: + case SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID: + case SpatialConstants::MULTICAST_RPCS_COMPONENT_ID: case SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID: -#if WITH_EDITOR - GlobalStateManager->OnShutdownComponentUpdate(Op.update); -#endif // WITH_EDITOR - return; - case SpatialConstants::HEARTBEAT_COMPONENT_ID: - OnHeartbeatComponentUpdate(Op); - return; + case SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID: + case SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID: + case SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID: case SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID: - NetDriver->GlobalStateManager->ApplyDeploymentMapUpdate(Op.update); - return; case SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID: - NetDriver->GlobalStateManager->ApplyStartupActorManagerUpdate(Op.update); - return; - 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); + case SpatialConstants::MIGRATION_DIAGNOSTIC_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::AUTHORITY_INTENT_COMPONENT_ID: - case SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID: - case SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID: - if (LoadBalanceEnforcer != nullptr) - { - LoadBalanceEnforcer->OnLoadBalancingComponentUpdated(Op); - } + case SpatialConstants::HEARTBEAT_COMPONENT_ID: + OnHeartbeatComponentUpdate(Op); return; - case SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID: - if (NetDriver->VirtualWorkerTranslator.IsValid()) + case SpatialConstants::GDK_DEBUG_COMPONENT_ID: + if (NetDriver->DebugCtx != nullptr) { - Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Op.update.schema_type); - NetDriver->VirtualWorkerTranslator->ApplyVirtualWorkerManagerData(ComponentObject); + NetDriver->DebugCtx->OnDebugComponentUpdateReceived(Op.entity_id); } 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) { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Entity: %d Component: %d - Skipping because this is a reserved spatial system component"), Op.entity_id, Op.update.component_id); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Entity: %d Component: %d - Skipping because this is a reserved spatial system component"), + Op.entity_id, Op.update.component_id); return; } // If this entity has a Tombstone component, abort all component processing if (const Tombstone* TombstoneComponent = StaticComponentView->GetComponentData(Op.entity_id)) { - UE_LOG(LogSpatialReceiver, Warning, TEXT("Received component update for Entity: %lld Component: %d after tombstone marked dead. Aborting update."), Op.entity_id, Op.update.component_id); + UE_LOG(LogSpatialReceiver, Warning, + TEXT("Received component update for Entity: %lld Component: %d after tombstone marked dead. Aborting update."), + Op.entity_id, Op.update.component_id); return; } @@ -1603,13 +1644,19 @@ void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) } else { - UE_LOG(LogSpatialReceiver, Warning, TEXT("Worker: %s Dormant actor (entity: %lld) has been deleted on this worker but we have received a component update (id: %d) from the server."), *NetDriver->Connection->GetWorkerId(), Op.entity_id, Op.update.component_id); + UE_LOG(LogSpatialReceiver, Warning, + TEXT("Worker: %s Dormant actor (entity: %lld) has been deleted on this worker but we have received a component " + "update (id: %d) from the server."), + *NetDriver->Connection->GetWorkerId(), Op.entity_id, Op.update.component_id); return; } } else { - UE_LOG(LogSpatialReceiver, Log, TEXT("Worker: %s Entity: %lld Component: %d - No actor channel for update. This most likely occured due to the component updates that are sent when authority is lost during entity deletion."), *NetDriver->Connection->GetWorkerId(), Op.entity_id, Op.update.component_id); + UE_LOG(LogSpatialReceiver, Log, + TEXT("Worker: %s Entity: %lld Component: %d - No actor channel for update. This most likely occured due to the " + "component updates that are sent when authority is lost during entity deletion."), + *NetDriver->Connection->GetWorkerId(), Op.entity_id, Op.update.component_id); return; } } @@ -1618,7 +1665,9 @@ void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) bool bFoundOffset = ClassInfoManager->GetOffsetByComponentId(Op.update.component_id, Offset); if (!bFoundOffset) { - UE_LOG(LogSpatialReceiver, Warning, TEXT("Worker: %s EntityId %d ComponentId %d - Could not find offset for component id when receiving a component update."), *NetDriver->Connection->GetWorkerId(), Op.entity_id, Op.update.component_id); + UE_LOG(LogSpatialReceiver, Warning, + TEXT("Worker: %s EntityId %d ComponentId %d - Could not find offset for component id when receiving a component update."), + *NetDriver->Connection->GetWorkerId(), Op.entity_id, Op.update.component_id); return; } @@ -1635,10 +1684,19 @@ void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) if (TargetObject == nullptr) { - UE_LOG(LogSpatialReceiver, Warning, TEXT("Entity: %d Component: %d - Couldn't find target object for update"), Op.entity_id, Op.update.component_id); + UE_LOG(LogSpatialReceiver, Warning, TEXT("Entity: %d Component: %d - Couldn't find target object for update"), Op.entity_id, + Op.update.component_id); return; } + if (EventTracer != nullptr) + { + FSpatialGDKSpanId CauseSpanId = EventTracer->GetSpanId(EntityComponentId(Op.entity_id, Op.update.component_id)); + EventTracer->TraceEvent( + FSpatialTraceEventBuilder::CreateComponentUpdate(Channel->Actor, TargetObject, Op.entity_id, Op.update.component_id), + CauseSpanId.GetConstId(), 1); + } + ESchemaComponentType Category = ClassInfoManager->GetCategoryByComponentId(Op.update.component_id); if (Category == ESchemaComponentType::SCHEMA_Data || Category == ESchemaComponentType::SCHEMA_OwnerOnly) @@ -1651,7 +1709,8 @@ void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) 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); + 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; } @@ -1659,123 +1718,99 @@ void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) } else { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Entity: %d Component: %d - Skipping because it's an empty component update from an RPC component. (most likely as a result of gaining authority)"), Op.entity_id, Op.update.component_id); + UE_LOG(LogSpatialReceiver, Verbose, + TEXT("Entity: %d Component: %d - Skipping because it's an empty component update from an RPC component. (most likely as a " + "result of gaining authority)"), + Op.entity_id, Op.update.component_id); } } -void USpatialReceiver::HandleRPCLegacy(const Worker_ComponentUpdateOp& Op) +void USpatialReceiver::OnCommandRequest(const Worker_Op& Op) { - SCOPE_CYCLE_COUNTER(STAT_ReceiverHandleRPCLegacy); - Worker_EntityId EntityId = Op.entity_id; + SCOPE_CYCLE_COUNTER(STAT_ReceiverCommandRequest); - // 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_LEGACY - ? SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY : SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY; + const Worker_CommandRequestOp& CommandRequestOp = Op.op.command_request; + const Worker_CommandRequest& Request = CommandRequestOp.request; + const Worker_EntityId EntityId = CommandRequestOp.entity_id; + const Worker_ComponentId ComponentId = Request.component_id; + const Worker_RequestId RequestId = CommandRequestOp.request_id; + const Schema_FieldId CommandIndex = Request.command_index; - // Multicast RPCs should be executed by whoever receives them. - if (Op.update.component_id != SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID_LEGACY) + if (IsEntityWaitingForAsyncLoad(CommandRequestOp.entity_id)) { - if (!StaticComponentView->HasAuthority(Op.entity_id, RPCEndpointComponentId)) - { - return; - } + UE_LOG(LogSpatialReceiver, Warning, + TEXT("USpatialReceiver::OnCommandRequest: Actor class async loading, cannot handle command. Entity %lld, Class %s"), + EntityId, *EntitiesWaitingForAsyncLoad[EntityId].ClassPath); + Sender->SendCommandFailure(RequestId, TEXT("Target actor async loading."), FSpatialGDKSpanId(Op.span_id)); + return; } - ProcessRPCEventField(EntityId, Op, RPCEndpointComponentId); -} - -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 = SpatialConstants::UNREAL_RPC_ENDPOINT_EVENT_ID; - uint32 EventCount = Schema_GetObjectCount(EventsObject, EventId); - - for (uint32 i = 0; i < EventCount; i++) + if (ComponentId == SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID + && CommandIndex == SpatialConstants::PLAYER_SPAWNER_SPAWN_PLAYER_COMMAND_ID) { - Schema_Object* EventData = Schema_IndexObject(EventsObject, EventId, i); - - RPCPayload Payload(EventData); - - FUnrealObjectRef ObjectRef(EntityId, Payload.Offset); + NetDriver->PlayerSpawner->ReceivePlayerSpawnRequestOnServer(CommandRequestOp); - if (UObject* TargetObject = PackageMap->GetObjectFromUnrealObjectRef(ObjectRef).Get()) + if (EventTracer != nullptr) { - ProcessOrQueueIncomingRPC(ObjectRef, MoveTemp(Payload)); + EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateReceiveCommandRequest(TEXT("SPAWN_PLAYER_COMMAND"), RequestId), + Op.span_id, 1); } - } -} -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; } - - // 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)) + else if (ComponentId == SpatialConstants::SERVER_WORKER_COMPONENT_ID + && CommandIndex == SpatialConstants::SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND_ID) { - const TWeakObjectPtr ActorReceivingRPC = PackageMap->GetObjectFromEntityId(Op.entity_id); - if (!ActorReceivingRPC.IsValid()) - { - 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; - } + NetDriver->PlayerSpawner->ReceiveForwardedPlayerSpawnRequest(CommandRequestOp); - const bool bActorRoleIsSimulatedProxy = Cast(ActorReceivingRPC.Get())->Role == ROLE_SimulatedProxy; - if (bActorRoleIsSimulatedProxy) + if (EventTracer != nullptr) { - 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; + EventTracer->TraceEvent( + FSpatialTraceEventBuilder::CreateReceiveCommandRequest(TEXT("SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND"), RequestId), + Op.span_id, 1); } - } - 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 (IsEntityWaitingForAsyncLoad(Op.entity_id)) - { - 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; - } - - 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) + else if (ComponentId == SpatialConstants::MIGRATION_DIAGNOSTIC_COMPONENT_ID + && CommandIndex == SpatialConstants::MIGRATION_DIAGNOSTIC_COMMAND_ID) { - Sender->ClearRPCsOnEntityCreation(Op.entity_id); - Sender->SendEmptyCommandResponse(Op.request.component_id, CommandIndex, Op.request_id); + check(NetDriver != nullptr); + check(NetDriver->Connection != nullptr); + + AActor* BlockingActor = Cast(PackageMap->GetObjectFromEntityId(EntityId)); + if (IsValid(BlockingActor)) + { + Worker_CommandResponse Response = MigrationDiagnostic::CreateMigrationDiagnosticResponse(NetDriver, EntityId, BlockingActor); + + Sender->SendCommandResponse(RequestId, Response, FSpatialGDKSpanId(Op.span_id)); + } + else + { + UE_LOG(LogSpatialReceiver, Warning, + TEXT("Migration diaganostic log failed because cannot retreive actor for entity (%llu) on authoritative worker %s"), + EntityId, *NetDriver->Connection->GetWorkerId()); + } + return; } #if WITH_EDITOR - else if (Op.request.component_id == SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID && CommandIndex == SpatialConstants::SHUTDOWN_MULTI_PROCESS_REQUEST_ID) + else if (ComponentId == SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID + && CommandIndex == SpatialConstants::SHUTDOWN_MULTI_PROCESS_REQUEST_ID) { NetDriver->GlobalStateManager->ReceiveShutdownMultiProcessRequest(); + + if (EventTracer != nullptr) + { + EventTracer->TraceEvent( + FSpatialTraceEventBuilder::CreateReceiveCommandRequest(TEXT("SHUTDOWN_MULTI_PROCESS_REQUEST"), RequestId), Op.span_id, 1); + } + return; } #endif // WITH_EDITOR #if !UE_BUILD_SHIPPING - else if (Op.request.component_id == SpatialConstants::DEBUG_METRICS_COMPONENT_ID) + else if (ComponentId == SpatialConstants::DEBUG_METRICS_COMPONENT_ID) { switch (CommandIndex) { @@ -1787,88 +1822,213 @@ void USpatialReceiver::OnCommandRequest(const Worker_CommandRequestOp& Op) break; case SpatialConstants::DEBUG_METRICS_MODIFY_SETTINGS_ID: { - Schema_Object* Payload = Schema_GetCommandRequestObject(Op.request.schema_type); + Schema_Object* Payload = Schema_GetCommandRequestObject(CommandRequestOp.request.schema_type); NetDriver->SpatialMetrics->OnModifySettingCommand(Payload); break; } default: - UE_LOG(LogSpatialReceiver, Error, TEXT("Unknown command index for DebugMetrics component: %d, entity: %lld"), CommandIndex, Op.entity_id); + UE_LOG(LogSpatialReceiver, Error, TEXT("Unknown command index for DebugMetrics component: %d, entity: %lld"), CommandIndex, + EntityId); break; } - Sender->SendEmptyCommandResponse(Op.request.component_id, CommandIndex, Op.request_id); + Sender->SendEmptyCommandResponse(ComponentId, CommandIndex, RequestId, FSpatialGDKSpanId(Op.span_id)); return; } #endif // !UE_BUILD_SHIPPING - Schema_Object* RequestObject = Schema_GetCommandRequestObject(Op.request.schema_type); + Schema_Object* RequestObject = Schema_GetCommandRequestObject(Request.schema_type); RPCPayload Payload(RequestObject); - FUnrealObjectRef ObjectRef = FUnrealObjectRef(Op.entity_id, Payload.Offset); + const FUnrealObjectRef ObjectRef = FUnrealObjectRef(EntityId, Payload.Offset); UObject* TargetObject = PackageMap->GetObjectFromUnrealObjectRef(ObjectRef).Get(); if (TargetObject == nullptr) { - UE_LOG(LogSpatialReceiver, Warning, TEXT("No target object found for EntityId %d"), Op.entity_id); - Sender->SendEmptyCommandResponse(Op.request.component_id, CommandIndex, Op.request_id); + UE_LOG(LogSpatialReceiver, Warning, TEXT("No target object found for EntityId %d"), EntityId); + Sender->SendEmptyCommandResponse(ComponentId, CommandIndex, RequestId, FSpatialGDKSpanId(Op.span_id)); return; } const FClassInfo& Info = ClassInfoManager->GetOrCreateClassInfoByObject(TargetObject); UFunction* Function = Info.RPCs[Payload.Index]; - const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Received command request (entity: %lld, component: %d, function: %s)"), - Op.entity_id, Op.request.component_id, *Function->GetName()); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Received command request (entity: %lld, component: %d, function: %s)"), EntityId, ComponentId, + *Function->GetName()); + + RPCService->ProcessOrQueueIncomingRPC(ObjectRef, MoveTemp(Payload), /* RPCIdForLinearEventTrace */ TOptional{}); + Sender->SendEmptyCommandResponse(ComponentId, CommandIndex, RequestId, FSpatialGDKSpanId(Op.span_id)); - ProcessOrQueueIncomingRPC(ObjectRef, MoveTemp(Payload)); - Sender->SendEmptyCommandResponse(Op.request.component_id, CommandIndex, Op.request_id); + AActor* TargetActor = Cast(PackageMap->GetObjectFromEntityId(EntityId)); +#if TRACE_LIB_ACTIVE + TraceKey TraceId = Payload.Trace; +#else + TraceKey TraceId = InvalidTraceKey; +#endif + if (EventTracer != nullptr) + { + UObject* TraceTargetObject = TargetActor != TargetObject ? TargetObject : nullptr; + EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateReceiveCommandRequest("RPC_COMMAND_REQUEST", TargetActor, + TraceTargetObject, Function, TraceId, RequestId), + Op.span_id, 1); + } } -void USpatialReceiver::OnCommandResponse(const Worker_CommandResponseOp& Op) +void USpatialReceiver::OnCommandResponse(const Worker_Op& Op) { + const Worker_CommandResponseOp& CommandResponseOp = Op.op.command_response; + const Worker_CommandResponse& Repsonse = CommandResponseOp.response; + const Worker_ComponentId ComponentId = Repsonse.component_id; + const Worker_RequestId RequestId = CommandResponseOp.request_id; + SCOPE_CYCLE_COUNTER(STAT_ReceiverCommandResponse); - if (Op.response.component_id == SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID) + if (ComponentId == SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID) { - NetDriver->PlayerSpawner->ReceivePlayerSpawnResponseOnClient(Op); + NetDriver->PlayerSpawner->ReceivePlayerSpawnResponseOnClient(CommandResponseOp); + + if (EventTracer != nullptr) + { + EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateReceiveCommandResponse(TEXT("SPAWN_PLAYER_COMMAND"), RequestId), + Op.span_id, 1); + } + return; } - else if (Op.response.component_id == SpatialConstants::SERVER_WORKER_COMPONENT_ID) + else if (ComponentId == SpatialConstants::SERVER_WORKER_COMPONENT_ID) { - NetDriver->PlayerSpawner->ReceiveForwardPlayerSpawnResponse(Op); + NetDriver->PlayerSpawner->ReceiveForwardPlayerSpawnResponse(CommandResponseOp); + + if (EventTracer != nullptr) + { + EventTracer->TraceEvent( + FSpatialTraceEventBuilder::CreateReceiveCommandResponse(TEXT("SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND"), RequestId), + Op.span_id, 1); + } + return; } + else if (Op.op.command_response.response.component_id == SpatialConstants::WORKER_COMPONENT_ID) + { + OnSystemEntityCommandResponse(Op.op.command_response); + return; + } + else if (ComponentId == SpatialConstants::MIGRATION_DIAGNOSTIC_COMPONENT_ID) + { + check(NetDriver != nullptr); + check(NetDriver->Connection != nullptr); + if (CommandResponseOp.status_code != WORKER_STATUS_CODE_SUCCESS) + { + UE_LOG(LogSpatialReceiver, Warning, TEXT("Migration diaganostic log failed, status code %i."), CommandResponseOp.status_code); + return; + } + + Schema_Object* ResponseObject = Schema_GetCommandResponseObject(CommandResponseOp.response.schema_type); + Worker_EntityId EntityId = Schema_GetInt64(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_ENTITY_ID); + AActor* BlockingActor = Cast(PackageMap->GetObjectFromEntityId(EntityId)); + if (IsValid(BlockingActor)) + { + FString MigrationDiagnosticLog = MigrationDiagnostic::CreateMigrationDiagnosticLog(NetDriver, ResponseObject, BlockingActor); + if (!MigrationDiagnosticLog.IsEmpty()) + { + UE_LOG(LogSpatialReceiver, Warning, TEXT("%s"), *MigrationDiagnosticLog); + } + } + else + { + UE_LOG(LogSpatialReceiver, Warning, TEXT("Migration diaganostic log failed because blocking actor (%llu) is not valid."), + EntityId); + } + + return; + } ReceiveCommandResponse(Op); } +void USpatialReceiver::ReceiveWorkerDisconnectResponse(const Worker_CommandResponseOp& Op) +{ + if (SystemEntityCommandDelegate* RequestDelegate = SystemEntityCommandDelegates.Find(Op.request_id)) + { + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Executing ReceiveWorkerDisconnectResponse with delegate, request id: %d, message: %s"), + Op.request_id, UTF8_TO_TCHAR(Op.message)); + RequestDelegate->ExecuteIfBound(Op); + } + else + { + UE_LOG(LogSpatialReceiver, Warning, + TEXT("Received ReceiveWorkerDisconnectResponse but with no delegate set, request id: %d, message: %s"), Op.request_id, + UTF8_TO_TCHAR(Op.message)); + } +} + +void USpatialReceiver::ReceiveClaimPartitionResponse(const Worker_CommandResponseOp& Op) +{ + const Worker_PartitionId PartitionId = PendingPartitionAssignments.FindAndRemoveChecked(Op.request_id); + + if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) + { + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Error, + TEXT("ClaimPartition command failed for a reason other than timeout. " + "This is fatal. Partition entity: %lld. Reason: %s"), + PartitionId, UTF8_TO_TCHAR(Op.message)); + return; + } + + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, + TEXT("ClaimPartition command succeeded. " + "Worker sytem entity: %lld. Partition entity: %lld"), + Op.entity_id, PartitionId); +} + void USpatialReceiver::FlushRetryRPCs() { Sender->FlushRetryRPCs(); } -void USpatialReceiver::ReceiveCommandResponse(const Worker_CommandResponseOp& Op) +void USpatialReceiver::ReceiveCommandResponse(const Worker_Op& Op) { - TSharedRef* ReliableRPCPtr = PendingReliableRPCs.Find(Op.request_id); + const Worker_CommandResponseOp& CommandResponseOp = Op.op.command_response; + const Worker_CommandResponse& Repsonse = CommandResponseOp.response; + const Worker_EntityId EntityId = CommandResponseOp.entity_id; + const Worker_ComponentId ComponentId = Repsonse.component_id; + const Worker_RequestId RequestId = CommandResponseOp.request_id; + const uint8_t StatusCode = CommandResponseOp.status_code; + + AActor* TargetActor = Cast(PackageMap->GetObjectFromEntityId(EntityId)); + TSharedRef* ReliableRPCPtr = PendingReliableRPCs.Find(RequestId); if (ReliableRPCPtr == nullptr) { - // We received a response for some other command, ignore. + if (EventTracer != nullptr) + { + // We received a response for some other command, ignore. + EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateReceiveCommandResponse(TargetActor, RequestId, false), Op.span_id, 1); + } + return; } TSharedRef ReliableRPC = *ReliableRPCPtr; - PendingReliableRPCs.Remove(Op.request_id); - if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) + PendingReliableRPCs.Remove(RequestId); + + UObject* TargetObject = ReliableRPC->TargetObject.Get() != TargetActor ? ReliableRPC->TargetObject.Get() : nullptr; + if (EventTracer != nullptr) + { + EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateReceiveCommandResponse(TargetActor, TargetObject, ReliableRPC->Function, + RequestId, WORKER_STATUS_CODE_SUCCESS), + Op.span_id, 1); + } + + if (StatusCode != WORKER_STATUS_CODE_SUCCESS) { bool bCanRetry = false; // Only attempt to retry if the error code indicates it makes sense too - if ((Op.status_code == WORKER_STATUS_CODE_TIMEOUT || Op.status_code == WORKER_STATUS_CODE_NOT_FOUND) + if ((StatusCode == WORKER_STATUS_CODE_TIMEOUT || StatusCode == WORKER_STATUS_CODE_NOT_FOUND) && (ReliableRPC->Attempts < SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS)) { bCanRetry = true; } // Don't apply the retry limit on auth lost, as it should eventually succeed - else if (Op.status_code == WORKER_STATUS_CODE_AUTHORITY_LOST) + else if (StatusCode == WORKER_STATUS_CODE_AUTHORITY_LOST) { bCanRetry = true; } @@ -1877,38 +2037,42 @@ void USpatialReceiver::ReceiveCommandResponse(const Worker_CommandResponseOp& Op { float WaitTime = SpatialConstants::GetCommandRetryWaitTimeSeconds(ReliableRPC->Attempts); UE_LOG(LogSpatialReceiver, Log, TEXT("%s: retrying in %f seconds. Error code: %d Message: %s"), - *ReliableRPC->Function->GetName(), WaitTime, (int)Op.status_code, UTF8_TO_TCHAR(Op.message)); + *ReliableRPC->Function->GetName(), WaitTime, (int)StatusCode, UTF8_TO_TCHAR(CommandResponseOp.message)); if (!ReliableRPC->TargetObject.IsValid()) { UE_LOG(LogSpatialReceiver, Warning, TEXT("%s: target object was destroyed before we could deliver the RPC."), - *ReliableRPC->Function->GetName()); + *ReliableRPC->Function->GetName()); return; } // Queue retry FTimerHandle RetryTimer; - TimerManager->SetTimer(RetryTimer, [WeakSender = TWeakObjectPtr(Sender), ReliableRPC]() - { - if (USpatialSender* SpatialSender = WeakSender.Get()) - { - SpatialSender->EnqueueRetryRPC(ReliableRPC); - } - }, WaitTime, false); + TimerManager->SetTimer( + RetryTimer, + [WeakSender = TWeakObjectPtr(Sender), ReliableRPC]() { + if (USpatialSender* SpatialSender = WeakSender.Get()) + { + SpatialSender->EnqueueRetryRPC(ReliableRPC); + } + }, + WaitTime, false); } else { UE_LOG(LogSpatialReceiver, Error, TEXT("%s: failed too many times, giving up (%u attempts). Error code: %d Message: %s"), - *ReliableRPC->Function->GetName(), SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS, (int)Op.status_code, UTF8_TO_TCHAR(Op.message)); + *ReliableRPC->Function->GetName(), SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS, (int)StatusCode, + UTF8_TO_TCHAR(CommandResponseOp.message)); } } } -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) { RepStateUpdateHelper RepStateHelper(Channel, TargetObject); - ComponentReader Reader(NetDriver, RepStateHelper.GetRefMap()); + ComponentReader Reader(NetDriver, RepStateHelper.GetRefMap(), EventTracer); bool bOutReferencesChanged = false; Reader.ApplyComponentUpdate(ComponentUpdate, TargetObject, Channel, bIsHandover, bOutReferencesChanged); RepStateHelper.Update(*this, Channel, TargetObject, bOutReferencesChanged); @@ -1927,164 +2091,114 @@ void USpatialReceiver::ApplyComponentUpdate(const Worker_ComponentUpdate& Compon } } -FRPCErrorInfo USpatialReceiver::ApplyRPCInternal(UObject* TargetObject, UFunction* Function, const FPendingRPCParams& PendingRPCParams) -{ - FRPCErrorInfo ErrorInfo = { TargetObject, Function, ERPCResult::UnresolvedParameters }; - - uint8* Parms = (uint8*)FMemory_Alloca(Function->ParmsSize); - FMemory::Memzero(Parms, Function->ParmsSize); - - TSet UnresolvedRefs; - TSet MappedRefs; - RPCPayload PayloadCopy = PendingRPCParams.Payload; - FSpatialNetBitReader PayloadReader(PackageMap, PayloadCopy.PayloadData.GetData(), PayloadCopy.CountDataBits(), MappedRefs, UnresolvedRefs); - - TSharedPtr RepLayout = NetDriver->GetFunctionRepLayout(Function); - RepLayout_ReceivePropertiesForRPC(*RepLayout, PayloadReader, Parms); - - const USpatialGDKSettings* SpatialSettings = GetDefault(); - - const float TimeQueued = (FDateTime::Now() - PendingRPCParams.Timestamp).GetTotalSeconds(); - const int32 UnresolvedRefCount = UnresolvedRefs.Num(); - - if (UnresolvedRefCount == 0 || SpatialSettings->QueuedIncomingRPCWaitTime < TimeQueued) - { - if (UnresolvedRefCount > 0 && - !SpatialSettings->ShouldRPCTypeAllowUnresolvedParameters(PendingRPCParams.Type) && - (Function->SpatialFunctionFlags & SPATIALFUNC_AllowUnresolvedParameters) == 0) - { - const FString UnresolvedEntityIds = FString::JoinBy(UnresolvedRefs, TEXT(", "), [](const FUnrealObjectRef& Ref) - { - return Ref.ToString(); - }); - - UE_LOG(LogSpatialReceiver, Warning, TEXT("Executed RPC %s::%s with unresolved references (%s) after %.3f seconds of queueing. Owner name: %s"), *GetNameSafe(TargetObject), *GetNameSafe(Function), *UnresolvedEntityIds, TimeQueued, *GetNameSafe(TargetObject->GetOuter())); - } - - // Get the RPC target Actor. - AActor* Actor = TargetObject->IsA() ? Cast(TargetObject) : TargetObject->GetTypedOuter(); - ERPCType RPCType = PendingRPCParams.Type; - - if (Actor->Role == ROLE_SimulatedProxy && - (RPCType == ERPCType::ServerReliable || - RPCType == ERPCType::ServerUnreliable)) - { - ErrorInfo.ErrorCode = ERPCResult::NoAuthority; - ErrorInfo.QueueProcessResult = ERPCQueueProcessResult::DropEntireQueue; - } - else - { - TargetObject->ProcessEvent(Function, Parms); - - if (GetDefault()->UseRPCRingBuffer() && - RPCService != nullptr && - RPCType != ERPCType::CrossServer && - RPCType != ERPCType::NetMulticast) - { - RPCService->IncrementAckedRPCID(PendingRPCParams.ObjectRef.Entity, RPCType); - } - - ErrorInfo.ErrorCode = ERPCResult::Success; - } - } - - // Destroy the parameters. - // warning: highly dependent on UObject::ProcessEvent freeing of parms! - for (TFieldIterator It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It) - { - It->DestroyValue_InContainer(Parms); - } - - return ErrorInfo; -} - -FRPCErrorInfo USpatialReceiver::ApplyRPC(const FPendingRPCParams& Params) -{ - SCOPE_CYCLE_COUNTER(STAT_ReceiverApplyRPC); - - TWeakObjectPtr TargetObjectWeakPtr = PackageMap->GetObjectFromUnrealObjectRef(Params.ObjectRef); - if (!TargetObjectWeakPtr.IsValid()) - { - return FRPCErrorInfo{ nullptr, nullptr, ERPCResult::UnresolvedTargetObject, ERPCQueueProcessResult::StopProcessing }; - } - - UObject* TargetObject = TargetObjectWeakPtr.Get(); - const FClassInfo& ClassInfo = ClassInfoManager->GetOrCreateClassInfoByObject(TargetObjectWeakPtr.Get()); - UFunction* Function = ClassInfo.RPCs[Params.Payload.Index]; - if (Function == nullptr) - { - return FRPCErrorInfo{ TargetObject, nullptr, ERPCResult::MissingFunctionInfo, ERPCQueueProcessResult::ContinueProcessing }; - } - - return ApplyRPCInternal(TargetObject, Function, Params); -} - 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)); + UE_LOG(LogSpatialReceiver, Warning, TEXT("ReserveEntityIds request failed: request id: %d, message: %s"), Op.request_id, + UTF8_TO_TCHAR(Op.message)); } else { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("ReserveEntityIds request succeeded: request id: %d, message: %s"), Op.request_id, UTF8_TO_TCHAR(Op.message)); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("ReserveEntityIds request succeeded: request id: %d, message: %s"), Op.request_id, + UTF8_TO_TCHAR(Op.message)); } if (ReserveEntityIDsDelegate* RequestDelegate = ReserveEntityIDsDelegates.Find(Op.request_id)) { - UE_LOG(LogSpatialReceiver, Log, TEXT("Executing ReserveEntityIdsResponse with delegate, request id: %d, first entity id: %lld, message: %s"), Op.request_id, Op.first_entity_id, UTF8_TO_TCHAR(Op.message)); + UE_LOG(LogSpatialReceiver, Log, + TEXT("Executing ReserveEntityIdsResponse with delegate, request id: %d, first entity id: %lld, message: %s"), Op.request_id, + Op.first_entity_id, UTF8_TO_TCHAR(Op.message)); RequestDelegate->ExecuteIfBound(Op); ReserveEntityIDsDelegates.Remove(Op.request_id); } else { - UE_LOG(LogSpatialReceiver, Warning, TEXT("Received ReserveEntityIdsResponse but with no delegate set, request id: %d, first entity id: %lld, message: %s"), Op.request_id, Op.first_entity_id, UTF8_TO_TCHAR(Op.message)); + UE_LOG(LogSpatialReceiver, Warning, + TEXT("Received ReserveEntityIdsResponse but with no delegate set, request id: %d, first entity id: %lld, message: %s"), + Op.request_id, Op.first_entity_id, UTF8_TO_TCHAR(Op.message)); } } -void USpatialReceiver::OnCreateEntityResponse(const Worker_CreateEntityResponseOp& Op) +void USpatialReceiver::OnCreateEntityResponse(const Worker_Op& Op) { + const Worker_CreateEntityResponseOp& CreateEntityResponseOp = Op.op.create_entity_response; + const Worker_EntityId EntityId = CreateEntityResponseOp.entity_id; + const Worker_RequestId RequestId = CreateEntityResponseOp.request_id; + const uint8_t StatusCode = CreateEntityResponseOp.status_code; + SCOPE_CYCLE_COUNTER(STAT_ReceiverCreateEntityResponse); - switch (static_cast(Op.status_code)) + switch (static_cast(StatusCode)) { case WORKER_STATUS_CODE_SUCCESS: - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Create entity request succeeded. " - "Request id: %d, entity id: %lld, message: %s"), Op.request_id, Op.entity_id, UTF8_TO_TCHAR(Op.message)); + UE_LOG(LogSpatialReceiver, Verbose, + TEXT("Create entity request succeeded. " + "Request id: %d, entity id: %lld, message: %s"), + RequestId, EntityId, UTF8_TO_TCHAR(CreateEntityResponseOp.message)); break; case WORKER_STATUS_CODE_TIMEOUT: - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Create entity request timed out. " - "Request id: %d, entity id: %lld, message: %s"), Op.request_id, Op.entity_id, UTF8_TO_TCHAR(Op.message)); + UE_LOG(LogSpatialReceiver, Verbose, + TEXT("Create entity request timed out. " + "Request id: %d, entity id: %lld, message: %s"), + RequestId, EntityId, UTF8_TO_TCHAR(CreateEntityResponseOp.message)); break; case WORKER_STATUS_CODE_APPLICATION_ERROR: - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Create entity request failed. " - "Either the reservation expired, the entity already existed, or the entity was invalid. " - "Request id: %d, entity id: %lld, message: %s"), Op.request_id, Op.entity_id, UTF8_TO_TCHAR(Op.message)); + UE_LOG(LogSpatialReceiver, Verbose, + TEXT("Create entity request failed. " + "Either the reservation expired, the entity already existed, or the entity was invalid. " + "Request id: %d, entity id: %lld, message: %s"), + RequestId, EntityId, UTF8_TO_TCHAR(CreateEntityResponseOp.message)); break; default: - UE_LOG(LogSpatialReceiver, Error, TEXT("Create entity request failed. This likely indicates a bug in the Unreal GDK and should be reported. " - "Request id: %d, entity id: %lld, message: %s"), Op.request_id, Op.entity_id, UTF8_TO_TCHAR(Op.message)); + UE_LOG(LogSpatialReceiver, Error, + TEXT("Create entity request failed. This likely indicates a bug in the Unreal GDK and should be reported. " + "Request id: %d, entity id: %lld, message: %s"), + RequestId, EntityId, UTF8_TO_TCHAR(CreateEntityResponseOp.message)); break; } - if (CreateEntityDelegate* Delegate = CreateEntityDelegates.Find(Op.request_id)) + if (CreateEntityDelegate* Delegate = CreateEntityDelegates.Find(RequestId)) { - Delegate->ExecuteIfBound(Op); - CreateEntityDelegates.Remove(Op.request_id); + Delegate->ExecuteIfBound(CreateEntityResponseOp); + CreateEntityDelegates.Remove(RequestId); } - TWeakObjectPtr Channel = PopPendingActorRequest(Op.request_id); + TWeakObjectPtr Channel = PopPendingActorRequest(RequestId); - // It's possible for the ActorChannel to have been closed by the time we receive a response. Actor validity is checked within the channel. + // It's possible for the ActorChannel to have been closed by the time we receive a response. Actor validity is checked within the + // channel. if (Channel.IsValid()) { - Channel->OnCreateEntityResponse(Op); + Channel->OnCreateEntityResponse(CreateEntityResponseOp); + + if (EventTracer != nullptr) + { + EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateReceiveCreateEntitySuccess(Channel->Actor, EntityId), Op.span_id, 1); + } } else if (Channel.IsStale()) { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Received CreateEntityResponse for actor which no longer has an actor channel: " - "request id: %d, entity id: %lld. This should only happen in the case where we attempt to delete the entity before we have authority. " - "The entity will therefore be deleted once authority is gained."), Op.request_id, Op.entity_id); + UE_LOG(LogSpatialReceiver, Verbose, + TEXT("Received CreateEntityResponse for actor which no longer has an actor channel: " + "request id: %d, entity id: %lld. This should only happen in the case where we attempt to delete the entity before we " + "have authority. " + "The entity will therefore be deleted once authority is gained."), + RequestId, EntityId); + + FString Message = + FString::Printf(TEXT("Stale Actor Channel - tried to delete entity before gaining authority. Actor - %s EntityId - %d"), + *Channel->Actor->GetName(), EntityId); + + if (EventTracer != nullptr) + { + EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateGenericMessage(Message), Op.span_id, 1); + } + } + else if (EventTracer != nullptr) + { + EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateGenericMessage(TEXT("Create entity response unknown error")), Op.span_id, + 1); } } @@ -2093,17 +2207,49 @@ void USpatialReceiver::OnEntityQueryResponse(const Worker_EntityQueryResponseOp& 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)); + UE_LOG(LogSpatialReceiver, Error, TEXT("EntityQuery failed: request id: %d, message: %s"), Op.request_id, + UTF8_TO_TCHAR(Op.message)); } if (EntityQueryDelegate* RequestDelegate = EntityQueryDelegates.Find(Op.request_id)) { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Executing EntityQueryResponse with delegate, request id: %d, number of entities: %d, message: %s"), Op.request_id, Op.result_count, UTF8_TO_TCHAR(Op.message)); + UE_LOG(LogSpatialReceiver, Verbose, + TEXT("Executing EntityQueryResponse with delegate, request id: %d, number of entities: %d, message: %s"), Op.request_id, + Op.result_count, UTF8_TO_TCHAR(Op.message)); RequestDelegate->ExecuteIfBound(Op); } else { - UE_LOG(LogSpatialReceiver, Warning, TEXT("Received EntityQueryResponse but with no delegate set, request id: %d, number of entities: %d, message: %s"), Op.request_id, Op.result_count, UTF8_TO_TCHAR(Op.message)); + UE_LOG(LogSpatialReceiver, Warning, + TEXT("Received EntityQueryResponse but with no delegate set, request id: %d, number of entities: %d, message: %s"), + Op.request_id, Op.result_count, UTF8_TO_TCHAR(Op.message)); + } +} + +void USpatialReceiver::OnSystemEntityCommandResponse(const Worker_CommandResponseOp& Op) +{ + SCOPE_CYCLE_COUNTER(STAT_ReceiverSystemEntityCommandResponse); + if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) + { + UE_LOG(LogSpatialReceiver, Error, TEXT("SystemEntityCommand failed: request id: %d, message: %s"), Op.request_id, + UTF8_TO_TCHAR(Op.message)); + } + + switch (Op.response.command_index) + { + case SpatialConstants::WORKER_DISCONNECT_COMMAND_ID: + { + ReceiveWorkerDisconnectResponse(Op); + return; + } + case SpatialConstants::WORKER_CLAIM_PARTITION_COMMAND_ID: + { + ReceiveClaimPartitionResponse(Op); + return; + } + default: + checkNoEntry(); + return; } } @@ -2132,6 +2278,11 @@ void USpatialReceiver::AddCreateEntityDelegate(Worker_RequestId RequestId, Creat CreateEntityDelegates.Add(RequestId, MoveTemp(Delegate)); } +void USpatialReceiver::AddSystemEntityCommandDelegate(Worker_RequestId RequestId, SystemEntityCommandDelegate Delegate) +{ + SystemEntityCommandDelegates.Add(RequestId, MoveTemp(Delegate)); +} + TWeakObjectPtr USpatialReceiver::PopPendingActorRequest(Worker_RequestId RequestId) { TWeakObjectPtr* ChannelPtr = PendingActorRequests.Find(RequestId); @@ -2144,20 +2295,11 @@ TWeakObjectPtr USpatialReceiver::PopPendingActorRequest(Wo return Channel; } -void USpatialReceiver::ProcessQueuedActorRPCsOnEntityCreation(Worker_EntityId EntityId, RPCsOnEntityCreation& QueuedRPCs) -{ - for (auto& RPC : QueuedRPCs.RPCs) - { - const FUnrealObjectRef ObjectRef(EntityId, RPC.Offset); - ProcessOrQueueIncomingRPC(ObjectRef, MoveTemp(RPC)); - } -} - -void USpatialReceiver::OnDisconnect(Worker_DisconnectOp& Op) +void USpatialReceiver::OnDisconnect(uint8 StatusCode, const FString& Reason) { if (GEngine != nullptr) { - GEngine->BroadcastNetworkFailure(NetDriver->GetWorld(), NetDriver, ENetworkFailure::FromDisconnectOpStatusCode(Op.connection_status_code), UTF8_TO_TCHAR(Op.reason)); + GEngine->BroadcastNetworkFailure(NetDriver->GetWorld(), NetDriver, ENetworkFailure::FromDisconnectOpStatusCode(StatusCode), Reason); } } @@ -2186,52 +2328,10 @@ bool USpatialReceiver::IsPendingOpsOnChannel(USpatialActorChannel& Channel) return false; } -void USpatialReceiver::ClearPendingRPCs(Worker_EntityId EntityId) -{ - IncomingRPCs.DropForEntity(EntityId); -} - -void USpatialReceiver::ProcessOrQueueIncomingRPC(const FUnrealObjectRef& InTargetObjectRef, SpatialGDK::RPCPayload InPayload) -{ - TWeakObjectPtr TargetObjectWeakPtr = PackageMap->GetObjectFromUnrealObjectRef(InTargetObjectRef); - if (!TargetObjectWeakPtr.IsValid()) - { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("The object has been deleted, dropping the RPC")); - return; - } - - UObject* TargetObject = TargetObjectWeakPtr.Get(); - const FClassInfo& ClassInfo = ClassInfoManager->GetOrCreateClassInfoByObject(TargetObject); - - if (InPayload.Index >= static_cast(ClassInfo.RPCs.Num())) - { - // This should only happen if there's a class layout disagreement between workers, which would indicate incompatible binaries. - UE_LOG(LogSpatialReceiver, Error, TEXT("Invalid RPC index (%d) received on %s, dropping the RPC"), InPayload.Index, *TargetObject->GetPathName()); - return; - } - UFunction* Function = ClassInfo.RPCs[InPayload.Index]; - if (Function == nullptr) - { - UE_LOG(LogSpatialReceiver, Error, TEXT("Missing function info received on %s, dropping the RPC"), *TargetObject->GetPathName()); - return; - } - - const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); - ERPCType Type = RPCInfo.Type; - - IncomingRPCs.ProcessOrQueueRPC(InTargetObjectRef, Type, MoveTemp(InPayload)); -} - -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()); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Resolving pending object refs and RPCs which depend on object: %s %s."), *Object->GetName(), + *ObjectRef.ToString()); ResolveIncomingOperations(Object, ObjectRef); @@ -2247,7 +2347,7 @@ void USpatialReceiver::ResolvePendingOperations(UObject* Object, const FUnrealOb } // TODO: UNR-1650 We're trying to resolve all queues, which introduces more overhead. - IncomingRPCs.ProcessRPCs(); + RPCService->ProcessIncomingRPCs(); } void USpatialReceiver::ResolveIncomingOperations(UObject* Object, const FUnrealObjectRef& ObjectRef) @@ -2261,10 +2361,18 @@ void USpatialReceiver::ResolveIncomingOperations(UObject* Object, const FUnrealO return; } - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Resolving incoming operations depending on object ref %s, resolved object: %s"), *ObjectRef.ToString(), *Object->GetName()); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Resolving incoming operations depending on object ref %s, resolved object: %s"), + *ObjectRef.ToString(), *Object->GetName()); + + // Rep-notify can modify ObjectRefToRepStateMap in some situations which can cause the TSet to access invalid + // memory if a) the set is removed or b) the TMap containing the TSet is reallocated. So to fix this we just gather + // the channel-object-repstate tuples to inspect here, and only afterwards do we write the resolved objects and call Rep-notifies. + TArray ObjectsToInspect; for (auto ChannelObjectIter = TargetObjectSet->CreateIterator(); ChannelObjectIter; ++ChannelObjectIter) { + GDK_ENSURE_NO_MODIFICATIONS(ObjectRefToRepStateUsageLock); + USpatialActorChannel* DependentChannel = ChannelObjectIter->Key.Get(); if (!DependentChannel) { @@ -2295,7 +2403,9 @@ 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()); + 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()); DependentChannel->ObjectReferenceMap.Remove(ChannelObjectIter->Value); continue; } @@ -2304,12 +2414,29 @@ 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()); + 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()); DependentChannel->ObjectReferenceMap.Remove(ChannelObjectIter->Value); continue; } } + ObjectsToInspect.Add(ChannelObjectsToBeResolved{ DependentChannel, ReplicatingObject, RepState }); + } + + for (const auto& ObjectToInspect : ObjectsToInspect) + { + USpatialActorChannel* DependentChannel = ObjectToInspect.Channel; + // Since the RepNotifies could theoretically destroy objects and close channels + // we will check here again to see if the object is still valid and the channel is open + if (!ObjectToInspect.Object.IsValid() || DependentChannel->Actor == nullptr) + { + continue; + } + UObject* ReplicatingObject = ObjectToInspect.Object.Get(); + FSpatialObjectRepState* RepState = ObjectToInspect.RepState; bool bSomeObjectsWereMapped = false; TArray RepNotifies; @@ -2320,21 +2447,26 @@ void USpatialReceiver::ResolveIncomingOperations(UObject* Object, const FUnrealO DependentChannel->ResetShadowData(RepLayout, ShadowData, ReplicatingObject); } - ResolveObjectReferences(RepLayout, ReplicatingObject, *RepState, RepState->ReferenceMap, ShadowData.GetData(), (uint8*)ReplicatingObject, ReplicatingObject->GetClass()->GetPropertiesSize(), RepNotifies, bSomeObjectsWereMapped); + ResolveObjectReferences(RepLayout, ReplicatingObject, *RepState, RepState->ReferenceMap, ShadowData.GetData(), + (uint8*)ReplicatingObject, ReplicatingObject->GetClass()->GetPropertiesSize(), RepNotifies, + bSomeObjectsWereMapped); if (bSomeObjectsWereMapped) { DependentChannel->RemoveRepNotifiesWithUnresolvedObjs(RepNotifies, RepLayout, RepState->ReferenceMap, ReplicatingObject); UE_LOG(LogSpatialReceiver, Verbose, TEXT("Resolved for target object %s"), *ReplicatingObject->GetName()); - DependentChannel->PostReceiveSpatialUpdate(ReplicatingObject, RepNotifies); + DependentChannel->PostReceiveSpatialUpdate(ReplicatingObject, RepNotifies, {}); } RepState->UnresolvedRefs.Remove(ObjectRef); } } -void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* ReplicatedObject, FSpatialObjectRepState& RepState, FObjectReferencesMap& ObjectReferencesMap, uint8* RESTRICT StoredData, uint8* RESTRICT Data, int32 MaxAbsOffset, TArray& RepNotifies, bool& bOutSomeObjectsWereMapped) +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) { @@ -2342,7 +2474,8 @@ void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* R if (AbsOffset >= MaxAbsOffset) { - UE_LOG(LogSpatialReceiver, Error, TEXT("ResolveObjectReferences: Removed unresolved reference: AbsOffset >= MaxAbsOffset: %d"), AbsOffset); + UE_LOG(LogSpatialReceiver, Error, TEXT("ResolveObjectReferences: Removed unresolved reference: AbsOffset >= MaxAbsOffset: %d"), + AbsOffset); It.RemoveCurrent(); continue; } @@ -2372,7 +2505,9 @@ void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* R int32 NewMaxOffset = Array->Num() * ArrayProperty->Inner->ElementSize; - ResolveObjectReferences(RepLayout, ReplicatedObject, RepState, *ObjectReferences.Array, bIsHandover ? nullptr : (uint8*)StoredArray->GetData(), (uint8*)Array->GetData(), NewMaxOffset, RepNotifies, bOutSomeObjectsWereMapped); + ResolveObjectReferences(RepLayout, ReplicatedObject, RepState, *ObjectReferences.Array, + bIsHandover ? nullptr : (uint8*)StoredArray->GetData(), (uint8*)Array->GetData(), NewMaxOffset, + RepNotifies, bOutSomeObjectsWereMapped); continue; } @@ -2390,7 +2525,9 @@ void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* R { check(Object != nullptr); - 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()); + 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()); if (ObjectReferences.bSingleProp) { @@ -2429,12 +2566,14 @@ void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* R { TSet NewMappedRefs; TSet NewUnresolvedRefs; - FSpatialNetBitReader ValueDataReader(PackageMap, ObjectReferences.Buffer.GetData(), ObjectReferences.NumBufferBits, NewMappedRefs, NewUnresolvedRefs); + FSpatialNetBitReader ValueDataReader(PackageMap, ObjectReferences.Buffer.GetData(), ObjectReferences.NumBufferBits, + NewMappedRefs, NewUnresolvedRefs); check(Property->IsA()); UScriptStruct* NetDeltaStruct = GetFastArraySerializerProperty(GDK_CASTFIELD(Property)); - FSpatialNetDeltaSerializeInfo::DeltaSerializeRead(NetDriver, ValueDataReader, ReplicatedObject, Parent->ArrayIndex, Parent->Property, NetDeltaStruct); + FSpatialNetDeltaSerializeInfo::DeltaSerializeRead(NetDriver, ValueDataReader, ReplicatedObject, Parent->ArrayIndex, + Parent->Property, NetDeltaStruct); ObjectReferences.MappedRefs.Append(NewMappedRefs); } @@ -2442,11 +2581,13 @@ void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* R { TSet NewMappedRefs; TSet NewUnresolvedRefs; - FSpatialNetBitReader BitReader(PackageMap, ObjectReferences.Buffer.GetData(), ObjectReferences.NumBufferBits, NewMappedRefs, NewUnresolvedRefs); + FSpatialNetBitReader BitReader(PackageMap, ObjectReferences.Buffer.GetData(), ObjectReferences.NumBufferBits, NewMappedRefs, + NewUnresolvedRefs); check(Property->IsA()); bool bHasUnresolved = false; - ReadStructProperty(BitReader, GDK_CASTFIELD(Property), NetDriver, Data + AbsOffset, bHasUnresolved); + ReadStructProperty(BitReader, GDK_CASTFIELD(Property), NetDriver, Data + AbsOffset, + bHasUnresolved); ObjectReferences.MappedRefs.Append(NewMappedRefs); } @@ -2480,7 +2621,9 @@ void USpatialReceiver::OnHeartbeatComponentUpdate(const Worker_ComponentUpdateOp if (!ConnectionPtr->IsValid()) { - UE_LOG(LogSpatialReceiver, Warning, TEXT("Received heartbeat component update after NetConnection has been cleaned up. PlayerController entity: %lld"), Op.entity_id); + UE_LOG(LogSpatialReceiver, Warning, + TEXT("Received heartbeat component update after NetConnection has been cleaned up. PlayerController entity: %lld"), + Op.entity_id); AuthorityPlayerControllerConnectionMap.Remove(Op.entity_id); return; } @@ -2493,15 +2636,16 @@ void USpatialReceiver::OnHeartbeatComponentUpdate(const Worker_ComponentUpdateOp { if (EventCount > 1) { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Received multiple heartbeat events in a single component update, entity %lld."), Op.entity_id); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Received multiple heartbeat events in a single component update, entity %lld."), + Op.entity_id); } NetConnection->OnHeartbeat(); } Schema_Object* FieldsObject = Schema_GetComponentUpdateFields(Op.update.schema_type); - if (Schema_GetBoolCount(FieldsObject, SpatialConstants::HEARTBEAT_CLIENT_HAS_QUIT_ID) > 0 && - GetBoolFromSchema(FieldsObject, SpatialConstants::HEARTBEAT_CLIENT_HAS_QUIT_ID)) + if (Schema_GetBoolCount(FieldsObject, SpatialConstants::HEARTBEAT_CLIENT_HAS_QUIT_ID) > 0 + && GetBoolFromSchema(FieldsObject, SpatialConstants::HEARTBEAT_CLIENT_HAS_QUIT_ID)) { // Client has disconnected, let's clean up their connection. CloseClientConnection(NetConnection, Op.entity_id); @@ -2514,18 +2658,6 @@ void USpatialReceiver::CloseClientConnection(USpatialNetConnection* ClientConnec AuthorityPlayerControllerConnectionMap.Remove(PlayerControllerEntityId); } -void USpatialReceiver::PeriodicallyProcessIncomingRPCs() -{ - FTimerHandle IncomingRPCsPeriodicProcessTimer; - TimerManager->SetTimer(IncomingRPCsPeriodicProcessTimer, [WeakThis = TWeakObjectPtr(this)]() - { - if (USpatialReceiver* SpatialReceiver = WeakThis.Get()) - { - SpatialReceiver->IncomingRPCs.ProcessRPCs(); - } - }, GetDefault()->QueuedIncomingRPCRetryTime, true); -} - bool USpatialReceiver::NeedToLoadClass(const FString& ClassPath) { UObject* ClassObject = FindObject(nullptr, *ClassPath, false); @@ -2541,8 +2673,9 @@ bool USpatialReceiver::NeedToLoadClass(const FString& ClassPath) // Without it, we could be using an object loaded in memory, but not completely ready to be used. // Looking through PackageMapClient's code, which handles asset async loading in Native unreal, checking // UPackage::IsFullyLoaded, or UObject::HasAnyInternalFlag(EInternalObjectFlag::AsyncLoading) should tell us if it is the case. - // In practice, these tests are not enough to prevent using objects too early (symptom is RF_NeedPostLoad being set, and crash when using them later). - // GetAsyncLoadPercentage will actually look through the async loading thread's UAsyncPackage maps to see if there are any entries. + // In practice, these tests are not enough to prevent using objects too early (symptom is RF_NeedPostLoad being set, and crash when + // using them later). GetAsyncLoadPercentage will actually look through the async loading thread's UAsyncPackage maps to see if there + // are any entries. // TODO : UNR-3374 This looks like an expensive check, but it does the job. We should investigate further // what is the issue with the other flags and why they do not give us reliable information. @@ -2572,7 +2705,8 @@ void USpatialReceiver::StartAsyncLoadingClass(const FString& ClassPath, Worker_E { // 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); + UE_LOG(LogSpatialReceiver, Error, + TEXT("USpatialReceiver::ReceiveActor: Checked out entity but it's already waiting for async load! Entity: %lld"), EntityId); } EntityWaitingForAsyncLoad AsyncLoadEntity; @@ -2583,7 +2717,8 @@ void USpatialReceiver::StartAsyncLoadingClass(const FString& ClassPath, Worker_E 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")); + 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)); @@ -2592,37 +2727,53 @@ void USpatialReceiver::StartAsyncLoadingClass(const FString& ClassPath, Worker_E 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; - } - if (Result != EAsyncLoadingResult::Succeeded) { - UE_LOG(LogSpatialReceiver, Error, TEXT("USpatialReceiver::OnAsyncPackageLoaded: Package was not loaded successfully. Package: %s"), *PackageName.ToString()); + UE_LOG(LogSpatialReceiver, Error, TEXT("USpatialReceiver::OnAsyncPackageLoaded: Package was not loaded successfully. Package: %s"), + *PackageName.ToString()); + AsyncLoadingPackages.Remove(PackageName); return; } - for (Worker_EntityId Entity : Entities) + LoadedPackages.Add(PackageName); +} + +void USpatialReceiver::ProcessActorsFromAsyncLoading() +{ + static_assert(TContainerTraits::MoveWillEmptyContainer, "Moving the set won't empty it"); + TSet PackagesToProcess = MoveTemp(LoadedPackages); + + for (const auto& PackageName : PackagesToProcess) { - if (IsEntityWaitingForAsyncLoad(Entity)) + TArray Entities; + if (!AsyncLoadingPackages.RemoveAndCopyValue(PackageName, Entities)) { - UE_LOG(LogSpatialReceiver, Log, TEXT("Finished async loading package %s for entity %lld."), *PackageName.ToString(), Entity); + UE_LOG(LogSpatialReceiver, Error, + TEXT("USpatialReceiver::OnAsyncPackageLoaded: Package loaded but no entry in AsyncLoadingPackages. Package: %s"), + *PackageName.ToString()); + return; + } - // Save critical section if we're in one and restore upon leaving this scope. - CriticalSectionSaveState CriticalSectionState(*this); + for (Worker_EntityId Entity : Entities) + { + if (IsEntityWaitingForAsyncLoad(Entity)) + { + UE_LOG(LogSpatialReceiver, Log, TEXT("Finished async loading package %s for entity %lld."), *PackageName.ToString(), + Entity); - EntityWaitingForAsyncLoad AsyncLoadEntity = EntitiesWaitingForAsyncLoad.FindAndRemoveChecked(Entity); - PendingAddActors.Add(Entity); - PendingAddComponents = MoveTemp(AsyncLoadEntity.InitialPendingAddComponents); - LeaveCriticalSection(); + // Save critical section if we're in one and restore upon leaving this scope. + CriticalSectionSaveState CriticalSectionState(*this); - OpList Ops = MoveTemp(AsyncLoadEntity.PendingOps).CreateOpList(); - for (uint32 i = 0; i < Ops.Count; ++i) - { - HandleQueuedOpForAsyncLoad(Ops.Ops[i]); + EntityWaitingForAsyncLoad AsyncLoadEntity = EntitiesWaitingForAsyncLoad.FindAndRemoveChecked(Entity); + PendingAddActors.Add(Entity); + PendingAddComponents = MoveTemp(AsyncLoadEntity.InitialPendingAddComponents); + LeaveCriticalSection(); + + OpList Ops = MoveTemp(AsyncLoadEntity.PendingOps).CreateOpList(); + for (uint32 i = 0; i < Ops.Count; ++i) + { + HandleQueuedOpForAsyncLoad(Ops.Ops[i]); + } } } } @@ -2645,7 +2796,8 @@ void USpatialReceiver::MoveMappedObjectToUnmapped(const FUnrealObjectRef& Ref) } } -void USpatialReceiver::RetireWhenAuthoritive(Worker_EntityId EntityId, Worker_ComponentId ActorClassId, bool bIsNetStartup, bool bNeedsTearOff) +void USpatialReceiver::RetireWhenAuthoritive(Worker_EntityId EntityId, Worker_ComponentId ActorClassId, bool bIsNetStartup, + bool bNeedsTearOff) { DeferredRetire DeferredObj = { EntityId, ActorClassId, bIsNetStartup, bNeedsTearOff }; EntitiesToRetireOnAuthorityGain.Add(DeferredObj); @@ -2670,11 +2822,14 @@ void USpatialReceiver::QueueRemoveComponentOpForAsyncLoad(const Worker_RemoveCom AsyncLoadEntity.PendingOps.RemoveComponent(Op.entity_id, Op.component_id); } -void USpatialReceiver::QueueAuthorityOpForAsyncLoad(const Worker_AuthorityChangeOp& Op) +void USpatialReceiver::QueueAuthorityOpForAsyncLoad(const Worker_ComponentSetAuthorityChangeOp& Op) { EntityWaitingForAsyncLoad& AsyncLoadEntity = EntitiesWaitingForAsyncLoad.FindChecked(Op.entity_id); - AsyncLoadEntity.PendingOps.SetAuthority(Op.entity_id, Op.component_id, static_cast(Op.authority)); + // todo UNR-4198 - This needs to be changed as it abuses the authority ops in a way that happens to work here. + // It's fine in this case to not give a valid set of component data in the authority op as we don't try to parse the component + // data when reading it. You couldn't create a view delta from this op list but it works here. + AsyncLoadEntity.PendingOps.SetAuthority(Op.entity_id, Op.component_set_id, static_cast(Op.authority), {}); } void USpatialReceiver::QueueComponentUpdateOpForAsyncLoad(const Worker_ComponentUpdateOp& Op) @@ -2707,17 +2862,21 @@ TArray USpatialReceiver::ExtractAddComponents(Worker EntityComponentOpListBuilder USpatialReceiver::ExtractAuthorityOps(Worker_EntityId Entity) { EntityComponentOpListBuilder ExtractedOps; - TArray RemainingOps; + TArray RemainingOps; - for (const Worker_AuthorityChangeOp& Op : PendingAuthorityChanges) + for (const Worker_ComponentSetAuthorityChangeOp& PendingAuthorityChange : PendingAuthorityChanges) { - if (Op.entity_id == Entity) + if (PendingAuthorityChange.entity_id == Entity) { - ExtractedOps.SetAuthority(Entity, Op.component_id, static_cast(Op.authority)); + // todo UNR-4198 - This needs to be changed as it abuses the authority ops in a way that happens to work here. + // It's fine in this case to not give a valid set of component data in the authority op as we don't try to parse the component + // data when reading it. You couldn't create a view delta from this op list but it works here. + ExtractedOps.SetAuthority(Entity, PendingAuthorityChange.component_set_id, + static_cast(PendingAuthorityChange.authority), {}); } else { - RemainingOps.Add(Op); + RemainingOps.Add(PendingAuthorityChange); } } PendingAuthorityChanges = MoveTemp(RemainingOps); @@ -2734,8 +2893,8 @@ void USpatialReceiver::HandleQueuedOpForAsyncLoad(const Worker_Op& Op) case WORKER_OP_TYPE_REMOVE_COMPONENT: ProcessRemoveComponent(Op.op.remove_component); break; - case WORKER_OP_TYPE_AUTHORITY_CHANGE: - HandleActorAuthority(Op.op.authority_change); + case WORKER_OP_TYPE_COMPONENT_SET_AUTHORITY_CHANGE: + HandleActorAuthority(Op.op.component_set_authority_change); break; case WORKER_OP_TYPE_COMPONENT_UPDATE: OnComponentUpdate(Op.op.component_update); @@ -2774,27 +2933,32 @@ USpatialReceiver::CriticalSectionSaveState::~CriticalSectionSaveState() namespace { - FString GetObjectNameFromRepState(const FSpatialObjectRepState& RepState) +FString GetObjectNameFromRepState(const FSpatialObjectRepState& RepState) +{ + if (UObject* Obj = RepState.GetChannelObjectPair().Value.Get()) { - if (UObject* Obj = RepState.GetChannelObjectPair().Value.Get()) - { - return Obj->GetName(); - } - return TEXT(""); + return Obj->GetName(); } + return TEXT(""); } +} // namespace 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))) + 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)); + 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) { + GDK_ENSURE_NO_MODIFICATIONS(ObjectRefToRepStateUsageLock); ObjectRefToRepStateMap.Remove(Ref); } } @@ -2803,7 +2967,9 @@ void USpatialReceiver::CleanupRepStateMap(FSpatialObjectRepState& RepState) bool USpatialReceiver::HasEntityBeenRequestedForDelete(Worker_EntityId EntityId) { - return EntitiesToRetireOnAuthorityGain.ContainsByPredicate([EntityId](const DeferredRetire& Retire) { return EntityId == Retire.EntityId; }); + return EntitiesToRetireOnAuthorityGain.ContainsByPredicate([EntityId](const DeferredRetire& Retire) { + return EntityId == Retire.EntityId; + }); } void USpatialReceiver::HandleDeferredEntityDeletion(const DeferredRetire& Retire) @@ -2821,9 +2987,17 @@ void USpatialReceiver::HandleDeferredEntityDeletion(const DeferredRetire& Retire void USpatialReceiver::HandleEntityDeletedAuthority(Worker_EntityId EntityId) { - int32 Index = EntitiesToRetireOnAuthorityGain.IndexOfByPredicate([EntityId](const DeferredRetire& Retire) { return Retire.EntityId == EntityId; }); + int32 Index = EntitiesToRetireOnAuthorityGain.IndexOfByPredicate([EntityId](const DeferredRetire& Retire) { + return Retire.EntityId == EntityId; + }); if (Index != INDEX_NONE) { HandleDeferredEntityDeletion(EntitiesToRetireOnAuthorityGain[Index]); } } + +bool USpatialReceiver::IsDynamicSubObject(AActor* Actor, uint32 SubObjectOffset) +{ + const FClassInfo& ActorClassInfo = ClassInfoManager->GetOrCreateClassInfoByClass(Actor->GetClass()); + return !ActorClassInfo.SubobjectInfo.Contains(SubObjectOffset); +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReplicationGraphLoadBalancingHandler.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReplicationGraphLoadBalancingHandler.cpp new file mode 100644 index 0000000000..7cee4eee29 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReplicationGraphLoadBalancingHandler.cpp @@ -0,0 +1,94 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialReplicationGraphLoadBalancingHandler.h" + +#include "EngineClasses/SpatialReplicationGraph.h" + +FSpatialReplicationGraphLoadBalancingContext::FSpatialReplicationGraphLoadBalancingContext(USpatialNetDriver* InNetDriver, + USpatialReplicationGraph* InReplicationGraph, + FPerConnectionActorInfoMap& InInfoMap, + FPrioritizedRepList& InRepList) + : NetDriver(InNetDriver) + , ReplicationGraph(InReplicationGraph) + , InfoMap(InInfoMap) + , ActorsToReplicate(InRepList) +{ +} + +FSpatialReplicationGraphLoadBalancingContext::FRepListArrayAdaptor FSpatialReplicationGraphLoadBalancingContext::GetActorsBeingReplicated() +{ + return FRepListArrayAdaptor(ActorsToReplicate); +} + +void FSpatialReplicationGraphLoadBalancingContext::RemoveAdditionalActor(AActor* Actor) +{ + AdditionalActorsToReplicate.Remove(Actor); +} + +void FSpatialReplicationGraphLoadBalancingContext::AddActorToReplicate(AActor* Actor) +{ + ReplicationGraph->ForceNetUpdate(Actor); + AdditionalActorsToReplicate.Add(Actor); +} + +#if ENGINE_MINOR_VERSION >= 26 +const FGlobalActorReplicationInfo::FDependantListType& FSpatialReplicationGraphLoadBalancingContext::GetDependentActors(AActor* Actor) +{ + static FGlobalActorReplicationInfo::FDependantListType EmptyList; + + if (FGlobalActorReplicationInfo* GlobalActorInfo = ReplicationGraph->GetGlobalActorReplicationInfoMap().Find(Actor)) + { + return GlobalActorInfo->GetDependentActorList(); + } + return EmptyList; +} +#else +FActorRepListRefView FSpatialReplicationGraphLoadBalancingContext::GetDependentActors(AActor* Actor) +{ + static FActorRepListRefView EmptyList = [] { + FActorRepListRefView List; + List.Reset(0); + return List; + }(); + + if (FGlobalActorReplicationInfo* GlobalActorInfo = ReplicationGraph->GetGlobalActorReplicationInfoMap().Find(Actor)) + { + const FActorRepListRefView& DependentActorList = GlobalActorInfo->GetDependentActorList(); + if (DependentActorList.IsValid()) + { + return DependentActorList; + } + } + return EmptyList; +} +#endif + +EActorMigrationResult FSpatialReplicationGraphLoadBalancingContext::IsActorReadyForMigration(AActor* Actor) +{ + if (!Actor->HasAuthority()) + { + return EActorMigrationResult::NotAuthoritative; + } + + if (!Actor->IsActorReady()) + { + return EActorMigrationResult::NotReady; + } + + // The following checks are extracted from UReplicationGraph::ReplicateActorListsForConnections_Default + // More accurately, from the loop with the section named NET_ReplicateActors_PrioritizeForConnection + // The part called "Distance Scaling" is ignored, since it is SpatialOS's job. + + if (!Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_SpatialType)) + { + return EActorMigrationResult::NoSpatialClassFlags; + } + + FConnectionReplicationActorInfo& ConnectionData = InfoMap.FindOrAdd(Actor); + if (ConnectionData.bDormantOnConnection) + { + return EActorMigrationResult::DormantOnConnection; + } + + return EActorMigrationResult::Success; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReplicationGraphLoadBalancingHandler.h b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReplicationGraphLoadBalancingHandler.h new file mode 100644 index 0000000000..30c4825929 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReplicationGraphLoadBalancingHandler.h @@ -0,0 +1,63 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Utils/SpatialLoadBalancingHandler.h" + +#include "ReplicationGraphTypes.h" + +class USpatialReplicationGraph; + +// Specialization of the load balancing handler for the ReplicationGraph. +struct FSpatialReplicationGraphLoadBalancingContext +{ + FSpatialReplicationGraphLoadBalancingContext(USpatialNetDriver* InNetDriver, USpatialReplicationGraph* InReplicationGraph, + FPerConnectionActorInfoMap& InfoMap, FPrioritizedRepList& InRepList); + + struct FRepListArrayAdaptor + { + struct Iterator + { + Iterator(TArray::RangedForIteratorType Iterator) + : IteratorImpl(Iterator) + { + } + + AActor* operator*() const { return (*IteratorImpl).Actor; } + void operator++() { ++IteratorImpl; } + bool operator!=(Iterator const& iRHS) const { return IteratorImpl != iRHS.IteratorImpl; } + + TArray::RangedForIteratorType IteratorImpl; + }; + + FRepListArrayAdaptor(FPrioritizedRepList& InRepList) + : RepList(InRepList) + { + } + + Iterator begin() { return Iterator(RepList.Items.begin()); } + Iterator end() { return Iterator(RepList.Items.end()); } + + FPrioritizedRepList& RepList; + }; + + FRepListArrayAdaptor GetActorsBeingReplicated(); + + void RemoveAdditionalActor(AActor* Actor); + + void AddActorToReplicate(AActor* Actor); + +#if ENGINE_MINOR_VERSION >= 26 + const FGlobalActorReplicationInfo::FDependantListType& GetDependentActors(AActor* Actor); +#else + FActorRepListRefView GetDependentActors(AActor* Actor); +#endif + + EActorMigrationResult IsActorReadyForMigration(AActor* Actor); + + USpatialNetDriver* NetDriver; + USpatialReplicationGraph* ReplicationGraph; + FPerConnectionActorInfoMap& InfoMap; + FPrioritizedRepList& ActorsToReplicate; + TSet AdditionalActorsToReplicate; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp index 47a41c94bd..87f8b6ef93 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp @@ -2,26 +2,27 @@ #include "Interop/SpatialSender.h" +#include "Engine/Engine.h" #include "GameFramework/PlayerController.h" #include "GameFramework/PlayerState.h" +#include "Net/NetworkProfiler.h" +#include "Runtime/Launch/Resources/Version.h" -#include "Engine/Engine.h" #include "EngineClasses/SpatialActorChannel.h" +#include "EngineClasses/SpatialLoadBalanceEnforcer.h" #include "EngineClasses/SpatialNetConnection.h" #include "EngineClasses/SpatialNetDriver.h" +#include "EngineClasses/SpatialNetDriverDebugContext.h" #include "EngineClasses/SpatialPackageMapClient.h" -#include "EngineClasses/SpatialLoadBalanceEnforcer.h" +#include "Interop/Connection/SpatialEventTracer.h" +#include "Interop/Connection/SpatialTraceEventBuilder.h" #include "Interop/Connection/SpatialWorkerConnection.h" #include "Interop/GlobalStateManager.h" #include "Interop/SpatialReceiver.h" #include "LoadBalancing/AbstractLBStrategy.h" -#include "Net/NetworkProfiler.h" #include "Schema/AuthorityIntent.h" -#include "Schema/ClientRPCEndpointLegacy.h" -#include "Schema/ComponentPresence.h" #include "Schema/Interest.h" #include "Schema/RPCPayload.h" -#include "Schema/ServerRPCEndpointLegacy.h" #include "Schema/ServerWorker.h" #include "Schema/StandardLibrary.h" #include "Schema/Tombstone.h" @@ -47,7 +48,55 @@ DECLARE_CYCLE_STAT(TEXT("Sender UpdateInterestComponent"), STAT_SpatialSenderUpd 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) +namespace +{ +struct FChangeListPropertyIterator +{ + const FRepChangeState* Changes; + FChangelistIterator ChangeListIterator; + FRepHandleIterator HandleIterator; + bool bValid; + FChangeListPropertyIterator(const FRepChangeState* Changes) + : Changes(Changes) + , ChangeListIterator(Changes->RepChanged, 0) + , HandleIterator(static_cast(Changes->RepLayout.GetOwner()), ChangeListIterator, Changes->RepLayout.Cmds, + Changes->RepLayout.BaseHandleToCmdIndex, /* InMaxArrayIndex */ 0, /* InMinCmdIndex */ 1, 0, + /* InMaxCmdIndex */ Changes->RepLayout.Cmds.Num() - 1) + , bValid(HandleIterator.NextHandle()) + { + } + + GDK_PROPERTY(Property) * operator*() const + { + if (bValid) + { + const FRepLayoutCmd& Cmd = Changes->RepLayout.Cmds[HandleIterator.CmdIndex]; + return Cmd.Property; + } + return nullptr; + } + + operator bool() const { return bValid; } + + FChangeListPropertyIterator& operator++() + { + // Move forward + if (bValid && Changes->RepLayout.Cmds[HandleIterator.CmdIndex].Type == ERepLayoutCmdType::DynamicArray) + { + bValid = !HandleIterator.JumpOverArray(); + } + if (bValid) + { + bValid = HandleIterator.NextHandle(); + } + return *this; + } +}; +} // namespace + +FReliableRPCForRetry::FReliableRPCForRetry(UObject* InTargetObject, UFunction* InFunction, Worker_ComponentId InComponentId, + Schema_FieldId InRPCIndex, const TArray& InPayload, int InRetryIndex, + const FSpatialGDKSpanId& InSpanId) : TargetObject(InTargetObject) , Function(InFunction) , ComponentId(InComponentId) @@ -55,10 +104,12 @@ FReliableRPCForRetry::FReliableRPCForRetry(UObject* InTargetObject, UFunction* I , Payload(InPayload) , Attempts(1) , RetryIndex(InRetryIndex) + , SpanId(InSpanId) { } -void USpatialSender::Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimerManager, SpatialGDK::SpatialRPCService* InRPCService) +void USpatialSender::Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimerManager, SpatialGDK::SpatialRPCService* InRPCService, + SpatialGDK::SpatialEventTracer* InEventTracer) { NetDriver = InNetDriver; StaticComponentView = InNetDriver->StaticComponentView; @@ -68,6 +119,7 @@ void USpatialSender::Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimer ClassInfoManager = InNetDriver->ClassInfoManager; TimerManager = InTimerManager; RPCService = InRPCService; + EventTracer = InEventTracer; OutgoingRPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(this, &USpatialSender::SendRPC)); @@ -81,15 +133,21 @@ void USpatialSender::Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimer Worker_RequestId USpatialSender::CreateEntity(USpatialActorChannel* Channel, uint32& OutBytesWritten) { EntityFactory DataFactory(NetDriver, PackageMap, ClassInfoManager, RPCService); - TArray ComponentDatas = DataFactory.CreateEntityComponents(Channel, OutgoingOnCreateEntityRPCs, OutBytesWritten); + TArray ComponentDatas = DataFactory.CreateEntityComponents(Channel, OutBytesWritten); // If the Actor was loaded rather than dynamically spawned, associate it with its owning sublevel. ComponentDatas.Add(CreateLevelComponentData(Channel->Actor)); - ComponentDatas.Add(ComponentPresence(EntityFactory::GetComponentPresenceList(ComponentDatas)).CreateComponentPresenceData()); - Worker_EntityId EntityId = Channel->GetEntityId(); - Worker_RequestId CreateEntityRequestId = Connection->SendCreateEntityRequest(MoveTemp(ComponentDatas), &EntityId); + + FSpatialGDKSpanId SpanId; + if (EventTracer != nullptr) + { + SpanId = EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateSendCreateEntity(Channel->Actor, EntityId)); + } + + Worker_RequestId CreateEntityRequestId = + Connection->SendCreateEntityRequest(MoveTemp(ComponentDatas), &EntityId, RETRY_UNTIL_COMPLETE, SpanId); return CreateEntityRequestId; } @@ -106,8 +164,9 @@ Worker_ComponentData USpatialSender::CreateLevelComponentData(AActor* Actor) } else { - UE_LOG(LogSpatialSender, Error, TEXT("Could not find Streaming Level Component for Level %s, processing Actor %s. Have you generated schema?"), - *ActorWorld->GetOuter()->GetPathName(), *Actor->GetPathName()); + UE_LOG(LogSpatialSender, Error, + TEXT("Could not find Streaming Level Component for Level %s, processing Actor %s. Have you generated schema?"), + *ActorWorld->GetOuter()->GetPathName(), *Actor->GetPathName()); } } @@ -117,23 +176,27 @@ Worker_ComponentData USpatialSender::CreateLevelComponentData(AActor* Actor) void USpatialSender::PeriodicallyProcessOutgoingRPCs() { FTimerHandle Timer; - TimerManager->SetTimer(Timer, [WeakThis = TWeakObjectPtr(this)]() - { - if (USpatialSender* SpatialSender = WeakThis.Get()) - { - SpatialSender->OutgoingRPCs.ProcessRPCs(); - } - }, GetDefault()->QueuedOutgoingRPCRetryTime, true); + TimerManager->SetTimer( + Timer, + [WeakThis = TWeakObjectPtr(this)]() { + if (USpatialSender* SpatialSender = WeakThis.Get()) + { + SpatialSender->OutgoingRPCs.ProcessRPCs(); + } + }, + GetDefault()->QueuedOutgoingRPCRetryTime, true); } -void USpatialSender::SendAddComponentForSubobject(USpatialActorChannel* Channel, UObject* Subobject, const FClassInfo& SubobjectInfo, uint32& OutBytesWritten) +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, USpatialLatencyTracer::GetTracer(Subobject)); - TArray SubobjectDatas = DataFactory.CreateComponentDatas(Subobject, SubobjectInfo, SubobjectRepChanges, SubobjectHandoverChanges, OutBytesWritten); + TArray SubobjectDatas = + DataFactory.CreateComponentDatas(Subobject, SubobjectInfo, SubobjectRepChanges, SubobjectHandoverChanges, OutBytesWritten); SendAddComponents(Channel->GetEntityId(), SubobjectDatas); Channel->PendingDynamicSubobjects.Remove(TWeakObjectPtr(Subobject)); @@ -146,69 +209,12 @@ void USpatialSender::SendAddComponents(Worker_EntityId EntityId, TArrayHasAuthority(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) -{ - Worker_EntityId EntityId = Channel->GetEntityId(); - - TSharedRef PendingSubobjectAttachment = MakeShared(); - PendingSubobjectAttachment->Subobject = Object; - 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]; - if (ComponentId != SpatialConstants::INVALID_COMPONENT_ID) - { - // For each valid ComponentId, we need to wait for its authority delegation before - // adding the subobject. - PendingSubobjectAttachment->PendingAuthorityDelegations.Add(ComponentId); - Receiver->PendingEntitySubobjectDelegations.Add( - MakeTuple(static_cast(EntityId), ComponentId), - PendingSubobjectAttachment); - - NewComponentIds.Add(ComponentId); - } - }); - - // 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 WorkerRequirementSet AuthoritativeWorkerRequirementSet = { SpatialConstants::UnrealServerAttributeSet }; - - 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::SendRemoveComponentForClassInfo(Worker_EntityId EntityId, const FClassInfo& Info) { TArray ComponentsToRemove; @@ -228,12 +234,6 @@ void USpatialSender::SendRemoveComponentForClassInfo(Worker_EntityId EntityId, c 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); @@ -248,75 +248,80 @@ void USpatialSender::CreateServerWorkerEntity() // Creates an entity authoritative on this server worker, ensuring it will be able to receive updates for the GSM. void USpatialSender::RetryServerWorkerEntityCreation(Worker_EntityId EntityId, int AttemptCounter) { - const WorkerRequirementSet WorkerIdPermission{ { FString::Format(TEXT("workerId:{0}"), { Connection->GetWorkerId() }) } }; - - WriteAclMap ComponentWriteAcl; - ComponentWriteAcl.Add(SpatialConstants::POSITION_COMPONENT_ID, WorkerIdPermission); - 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); + check(NetDriver != nullptr); 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(ServerWorker(Connection->GetWorkerId(), false).CreateServerWorkerData()); + Components.Add(ServerWorker(Connection->GetWorkerId(), false, Connection->GetWorkerSystemEntityId()).CreateServerWorkerData()); + + AuthorityDelegationMap DelegationMap; + DelegationMap.Add(SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID, EntityId); + Components.Add(AuthorityDelegation(DelegationMap).CreateAuthorityDelegationData()); + 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. + + // The load balance strategy won't 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()); - const Worker_RequestId RequestId = Connection->SendCreateEntityRequest(MoveTemp(Components), &EntityId); + // GDK known entities completeness tags. + Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::GDK_KNOWN_ENTITY_TAG_COMPONENT_ID)); + + const Worker_RequestId RequestId = Connection->SendCreateEntityRequest(MoveTemp(Components), &EntityId, RETRY_UNTIL_COMPLETE); CreateEntityDelegate OnCreateWorkerEntityResponse; - OnCreateWorkerEntityResponse.BindLambda([WeakSender = TWeakObjectPtr(this), EntityId, AttemptCounter](const Worker_CreateEntityResponseOp& Op) - { - if (!WeakSender.IsValid()) - { - return; - } - USpatialSender* Sender = WeakSender.Get(); + OnCreateWorkerEntityResponse.BindLambda( + [WeakSender = TWeakObjectPtr(this), EntityId, AttemptCounter](const Worker_CreateEntityResponseOp& Op) { + if (!WeakSender.IsValid()) + { + return; + } + USpatialSender* Sender = WeakSender.Get(); - if (Op.status_code == WORKER_STATUS_CODE_SUCCESS) - { - Sender->NetDriver->WorkerEntityId = Op.entity_id; - return; - } + if (Op.status_code == WORKER_STATUS_CODE_SUCCESS) + { + Sender->NetDriver->WorkerEntityId = Op.entity_id; - // Given the nature of commands, it's possible we have multiple create commands in flight at once. If a command fails where - // we've already set the worker entity ID locally, this means we already successfully create the entity, so nothing needs doing. - if (Op.status_code != WORKER_STATUS_CODE_SUCCESS && Sender->NetDriver->WorkerEntityId != SpatialConstants::INVALID_ENTITY_ID) - { - return; - } + // We claim each server worker entity as a partition for server worker interest. This is necessary for getting + // interest in the VirtualWorkerTranslator component. + Sender->SendClaimPartitionRequest(Sender->NetDriver->Connection->GetWorkerSystemEntityId(), Op.entity_id); - if (Op.status_code != WORKER_STATUS_CODE_TIMEOUT) - { - UE_LOG(LogSpatialSender, Error, TEXT("Worker entity creation request failed: \"%s\""), - UTF8_TO_TCHAR(Op.message)); - return; - } + return; + } - if (AttemptCounter == SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS) - { - UE_LOG(LogSpatialSender, Error, TEXT("Worker entity creation request timed out too many times. (%u attempts)"), - SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS); - return; - } + // Given the nature of commands, it's possible we have multiple create commands in flight at once. If a command fails where + // we've already set the worker entity ID locally, this means we already successfully create the entity, so nothing needs doing. + if (Op.status_code != WORKER_STATUS_CODE_SUCCESS && Sender->NetDriver->WorkerEntityId != SpatialConstants::INVALID_ENTITY_ID) + { + return; + } - UE_LOG(LogSpatialSender, Warning, TEXT("Worker entity creation request timed out and will retry.")); - FTimerHandle RetryTimer; - Sender->TimerManager->SetTimer(RetryTimer, [WeakSender, EntityId, AttemptCounter]() - { - if (USpatialSender* SpatialSender = WeakSender.Get()) + if (Op.status_code != WORKER_STATUS_CODE_TIMEOUT) { - SpatialSender->RetryServerWorkerEntityCreation(EntityId, AttemptCounter + 1); + UE_LOG(LogSpatialSender, Error, TEXT("Worker entity creation request failed: \"%s\""), UTF8_TO_TCHAR(Op.message)); + return; } - }, SpatialConstants::GetCommandRetryWaitTimeSeconds(AttemptCounter), false); - }); + + if (AttemptCounter == SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS) + { + UE_LOG(LogSpatialSender, Error, TEXT("Worker entity creation request timed out too many times. (%u attempts)"), + SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS); + return; + } + + UE_LOG(LogSpatialSender, Warning, TEXT("Worker entity creation request timed out and will retry.")); + FTimerHandle RetryTimer; + Sender->TimerManager->SetTimer( + RetryTimer, + [WeakSender, EntityId, AttemptCounter]() { + if (USpatialSender* SpatialSender = WeakSender.Get()) + { + SpatialSender->RetryServerWorkerEntityCreation(EntityId, AttemptCounter + 1); + } + }, + SpatialConstants::GetCommandRetryWaitTimeSeconds(AttemptCounter), false); + }); Receiver->AddCreateEntityDelegate(RequestId, MoveTemp(OnCreateWorkerEntityResponse)); } @@ -330,11 +335,26 @@ bool USpatialSender::ValidateOrExit_IsSupportedClass(const FString& PathName) { // Level blueprint classes could have a PIE prefix, this will remove it. FString RemappedPathName = PathName; - GEngine->NetworkRemapPath(NetDriver, RemappedPathName, false); +#if ENGINE_MINOR_VERSION >= 26 + GEngine->NetworkRemapPath(NetDriver->GetSpatialOSNetConnection(), RemappedPathName, false /*bIsReading*/); +#else + GEngine->NetworkRemapPath(NetDriver, RemappedPathName, false /*bIsReading*/); +#endif return ClassInfoManager->ValidateOrExit_IsSupportedClass(RemappedPathName); } +void USpatialSender::SendClaimPartitionRequest(Worker_EntityId SystemWorkerEntityId, Worker_PartitionId PartitionId) const +{ + UE_LOG(LogSpatialSender, Log, + TEXT("SendClaimPartitionRequest. Worker: %s, SystemWorkerEntityId: %lld. " + "PartitionId: %lld"), + *Connection->GetWorkerId(), SystemWorkerEntityId, PartitionId); + Worker_CommandRequest CommandRequest = Worker::CreateClaimPartitionRequest(PartitionId); + const Worker_RequestId RequestId = Connection->SendCommandRequest(SystemWorkerEntityId, &CommandRequest, RETRY_UNTIL_COMPLETE, {}); + Receiver->PendingPartitionAssignments.Add(RequestId, PartitionId); +} + void USpatialSender::DeleteEntityComponentData(TArray& EntityComponents) { for (FWorkerComponentData& Component : EntityComponents) @@ -351,12 +371,8 @@ TArray USpatialSender::CopyEntityComponentData(const TArra Copy.Reserve(EntityComponents.Num()); for (const FWorkerComponentData& Component : EntityComponents) { - Copy.Emplace(Worker_ComponentData{ - Component.reserved, - Component.component_id, - Schema_CopyComponentData(Component.schema_type), - nullptr - }); + Copy.Emplace( + Worker_ComponentData{ Component.reserved, Component.component_id, Schema_CopyComponentData(Component.schema_type), nullptr }); } return Copy; @@ -364,27 +380,34 @@ TArray USpatialSender::CopyEntityComponentData(const TArra void USpatialSender::CreateEntityWithRetries(Worker_EntityId EntityId, FString EntityName, TArray EntityComponents) { - const Worker_RequestId RequestId = Connection->SendCreateEntityRequest(CopyEntityComponentData(EntityComponents), &EntityId); + const Worker_RequestId RequestId = + Connection->SendCreateEntityRequest(CopyEntityComponentData(EntityComponents), &EntityId, RETRY_UNTIL_COMPLETE); CreateEntityDelegate Delegate; - Delegate.BindLambda([this, EntityId, Name = MoveTemp(EntityName), Components = MoveTemp(EntityComponents)](const Worker_CreateEntityResponseOp& Op) mutable - { + 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); + 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); + 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); + 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; } @@ -393,28 +416,32 @@ void USpatialSender::CreateEntityWithRetries(Worker_EntityId EntityId, FString E Receiver->AddCreateEntityDelegate(RequestId, MoveTemp(Delegate)); } -void USpatialSender::UpdateServerWorkerEntityInterestAndPosition() +void USpatialSender::UpdatePartitionEntityInterestAndPosition() { check(Connection != nullptr); check(NetDriver != nullptr); - if (NetDriver->WorkerEntityId == SpatialConstants::INVALID_ENTITY_ID) - { - // No worker entity to update. - return; - } + check(NetDriver->VirtualWorkerTranslator != nullptr + && NetDriver->VirtualWorkerTranslator->GetClaimedPartitionId() != SpatialConstants::INVALID_ENTITY_ID); + check(NetDriver->LoadBalanceStrategy != nullptr && NetDriver->LoadBalanceStrategy->IsReady()); + + Worker_PartitionId PartitionId = NetDriver->VirtualWorkerTranslator->GetClaimedPartitionId(); + VirtualWorkerId VirtualId = NetDriver->VirtualWorkerTranslator->GetLocalVirtualWorkerId(); // 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); + FWorkerComponentUpdate InterestUpdate = + NetDriver->InterestFactory + ->CreatePartitionInterest(NetDriver->LoadBalanceStrategy, VirtualId, NetDriver->DebugCtx != nullptr /*bDebug*/) + .CreateInterestUpdate(); - 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()); - } + Connection->SendComponentUpdate(PartitionId, &InterestUpdate); + + // Also update the position of the partition entity to the center of the load balancing region. + SendPositionUpdate(PartitionId, NetDriver->LoadBalanceStrategy->GetWorkerEntityPosition()); } -void USpatialSender::SendComponentUpdates(UObject* Object, const FClassInfo& Info, USpatialActorChannel* Channel, const FRepChangeState* RepChanges, const FHandoverChangeState* HandoverChanges, uint32& OutBytesWritten) +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(); @@ -424,24 +451,63 @@ void USpatialSender::SendComponentUpdates(UObject* Object, const FClassInfo& Inf USpatialLatencyTracer* Tracer = USpatialLatencyTracer::GetTracer(Object); ComponentFactory UpdateFactory(Channel->GetInterestDirty(), NetDriver, Tracer); - TArray ComponentUpdates = UpdateFactory.CreateComponentUpdates(Object, Info, EntityId, RepChanges, HandoverChanges, OutBytesWritten); + TArray ComponentUpdates = + UpdateFactory.CreateComponentUpdates(Object, Info, EntityId, RepChanges, HandoverChanges, OutBytesWritten); + + TArray PropertySpans; + if (EventTracer != nullptr && RepChanges != nullptr + && RepChanges->RepChanged.Num() > 0) // Only need to add these if they are actively being traced + { + FSpatialGDKSpanId CauseSpanId; + if (EventTracer != nullptr) + { + CauseSpanId = EventTracer->PopLatentPropertyUpdateSpanId(Object); + } + + for (FChangeListPropertyIterator Itr(RepChanges); Itr; ++Itr) + { + GDK_PROPERTY(Property)* Property = *Itr; + + EventTraceUniqueId LinearTraceId = EventTraceUniqueId::GenerateForProperty(EntityId, Property); + FSpatialGDKSpanId PropertySpan = EventTracer->TraceEvent( + FSpatialTraceEventBuilder::CreatePropertyChanged(Object, EntityId, Property->GetName(), LinearTraceId), + CauseSpanId.GetConstId(), 1); + + PropertySpans.Push(PropertySpan); + } + } - for(int i = 0; i < ComponentUpdates.Num(); i++) + // It's not clear if this is ever valid for authority to not be true anymore (since component sets), but still possible if we attempt + // to process updates whilst an entity creation is in progress, or after the entity has been deleted or removed from view. So in the + // meantime we've kept the checking and queuing of updates, along with an error message. + const bool bHasAuthority = NetDriver->HasServerAuthority(EntityId); + if (!bHasAuthority) + { + UE_LOG(LogSpatialSender, Warning, + TEXT("Trying to send component update but don't have authority! Update will be queued and sent when authority gained. " + "entity: %lld"), + EntityId); + + // 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); + UpdatesQueuedUntilAuthority.Append(ComponentUpdates); + return; + } + + for (int i = 0; i < ComponentUpdates.Num(); i++) { FWorkerComponentUpdate& Update = ComponentUpdates[i]; - if (!NetDriver->StaticComponentView->HasAuthority(EntityId, Update.component_id)) + + FSpatialGDKSpanId SpanId; + if (EventTracer) { - 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); - - // 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); - UpdatesQueuedUntilAuthority.Add(Update); - continue; + SpanId = EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateSendPropertyUpdate(Object, EntityId, Update.component_id), + (const Trace_SpanIdType*)PropertySpans.GetData(), PropertySpans.Num()); } - Connection->SendComponentUpdate(EntityId, &Update); + Connection->SendComponentUpdate(EntityId, &Update, SpanId); } } @@ -475,30 +541,33 @@ void USpatialSender::FlushRPCService() TArray RPCs = RPCService->GetRPCsAndAcksToSend(); for (SpatialRPCService::UpdateToSend& Update : RPCs) { - Connection->SendComponentUpdate(Update.EntityId, &Update.Update); + Connection->SendComponentUpdate(Update.EntityId, &Update.Update, Update.SpanId); } - if (RPCs.Num()) + if (RPCs.Num() && GetDefault()->bWorkerFlushAfterOutgoingNetworkOp) { - Connection->MaybeFlush(); + Connection->Flush(); } } } -RPCPayload USpatialSender::CreateRPCPayloadFromParams(UObject* TargetObject, const FUnrealObjectRef& TargetObjectRef, UFunction* Function, 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); #if TRACE_LIB_ACTIVE - return RPCPayload(TargetObjectRef.Offset, RPCInfo.Index, TArray(PayloadWriter.GetData(), PayloadWriter.GetNumBytes()), USpatialLatencyTracer::GetTracer(TargetObject)->RetrievePendingTrace(TargetObject, Function)); + 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::SendInterestBucketComponentChange(const Worker_EntityId EntityId, const Worker_ComponentId OldComponent, const Worker_ComponentId NewComponent) +void USpatialSender::SendInterestBucketComponentChange(const Worker_EntityId EntityId, const Worker_ComponentId OldComponent, + const Worker_ComponentId NewComponent) { if (OldComponent != SpatialConstants::INVALID_COMPONENT_ID) { @@ -541,9 +610,10 @@ void USpatialSender::SendActorTornOffUpdate(Worker_EntityId EntityId, Worker_Com void USpatialSender::SendPositionUpdate(Worker_EntityId EntityId, const FVector& Location) { #if !UE_BUILD_SHIPPING - if (!NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::POSITION_COMPONENT_ID)) + if (!NetDriver->HasServerAuthority(EntityId)) { - UE_LOG(LogSpatialSender, Verbose, TEXT("Trying to send Position component update but don't have authority! Update will not be sent. Entity: %lld"), EntityId); + UE_LOG(LogSpatialSender, Verbose, + TEXT("Trying to send Position component update but don't have authority! Update will not be sent. Entity: %lld"), EntityId); return; } #endif @@ -552,73 +622,40 @@ void USpatialSender::SendPositionUpdate(Worker_EntityId EntityId, const FVector& Connection->SendComponentUpdate(EntityId, &Update); } -void USpatialSender::SendAuthorityIntentUpdate(const AActor& Actor, VirtualWorkerId NewAuthoritativeVirtualWorkerId) +void USpatialSender::SendAuthorityIntentUpdate(const AActor& Actor, VirtualWorkerId NewAuthoritativeVirtualWorkerId) const { 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; - } + checkf(AuthorityIntentComponent->VirtualWorkerId != NewAuthoritativeVirtualWorkerId, + TEXT("Attempted to update AuthorityIntent twice to the same value. Actor: %s. Entity ID: %lld. Virtual worker: '%d'"), + *GetNameSafe(&Actor), EntityId, NewAuthoritativeVirtualWorkerId); 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); - } + UE_LOG(LogSpatialSender, Log, + TEXT("(%s) Sending AuthorityIntent update for entity id %d. Virtual worker '%d' should become authoritative over %s"), + *NetDriver->Connection->GetWorkerId(), EntityId, NewAuthoritativeVirtualWorkerId, *GetNameSafe(&Actor)); 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); + FSpatialGDKSpanId SpanId; + if (EventTracer != nullptr) + { + SpanId = EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateAuthorityIntentUpdate(NewAuthoritativeVirtualWorkerId, &Actor)); + } - const WorkerAttributeSet OwningServerWorkerAttributeSet = { WriteWorkerId }; + Connection->SendComponentUpdate(EntityId, &Update, SpanId); - EntityAcl* NewAcl = StaticComponentView->GetComponentData(Request.EntityId); - NewAcl->ReadAcl = Request.ReadAcl; + // Notify the enforcer directly on the worker that sends the component update, as the update will short circuit. + // This should always happen with USLB. + NetDriver->LoadBalanceEnforcer->ShortCircuitMaybeRefreshAuthorityDelegation(EntityId); - for (const Worker_ComponentId& ComponentId : Request.ComponentIds) + if (NetDriver->SpatialDebugger != nullptr) { - 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::UnrealServerAttributeSet } ); - continue; - } - - NewAcl->ComponentWriteAcl.Add(ComponentId, { OwningServerWorkerAttributeSet }); + NetDriver->SpatialDebugger->ActorAuthorityIntentChanged(EntityId, NewAuthoritativeVirtualWorkerId); } - - 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) @@ -647,7 +684,6 @@ FRPCErrorInfo USpatialSender::SendRPC(const FPendingRPCParams& Params) } const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); - bool bUseRPCRingBuffer = GetDefault()->UseRPCRingBuffer(); if (RPCInfo.Type == ERPCType::CrossServer) { @@ -655,40 +691,19 @@ FRPCErrorInfo USpatialSender::SendRPC(const FPendingRPCParams& Params) return FRPCErrorInfo{ TargetObject, Function, ERPCResult::Success }; } - if (bUseRPCRingBuffer && RPCService != nullptr) + checkf(RPCService != nullptr, TEXT("RPCService is assumed to be valid.")); + if (SendRingBufferedRPC(TargetObject, Function, Params.Payload, Channel, Params.ObjectRef)) { - if (SendRingBufferedRPC(TargetObject, Function, Params.Payload, Channel, Params.ObjectRef)) - { - return FRPCErrorInfo{ TargetObject, Function, ERPCResult::Success }; - } - else - { - return FRPCErrorInfo{ TargetObject, Function, ERPCResult::RPCServiceFailure }; - } + return FRPCErrorInfo{ TargetObject, Function, ERPCResult::Success }; } - - if (Channel->bCreatingNewEntity && Function->HasAnyFunctionFlags(FUNC_NetClient)) + else { - SendOnEntityCreationRPC(TargetObject, Function, Params.Payload, Channel, Params.ObjectRef); - return FRPCErrorInfo{ TargetObject, Function, ERPCResult::Success }; + return FRPCErrorInfo{ TargetObject, Function, ERPCResult::RPCServiceFailure }; } - - return SendLegacyRPC(TargetObject, Function, Params.Payload, Channel, Params.ObjectRef); } -void USpatialSender::SendOnEntityCreationRPC(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef) -{ - check(NetDriver->IsServer()); - - const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); - - OutgoingOnCreateEntityRPCs.FindOrAdd(Channel->Actor).RPCs.Add(Payload); -#if !UE_BUILD_SHIPPING - TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type); -#endif // !UE_BUILD_SHIPPING -} - -void USpatialSender::SendCrossServerRPC(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef) +void USpatialSender::SendCrossServerRPC(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, + USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef) { const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); @@ -697,71 +712,38 @@ void USpatialSender::SendCrossServerRPC(UObject* TargetObject, UFunction* Functi Worker_EntityId EntityId = SpatialConstants::INVALID_ENTITY_ID; Worker_CommandRequest CommandRequest = CreateRPCCommandRequest(TargetObject, Payload, ComponentId, RPCInfo.Index, EntityId); + FSpatialGDKSpanId SpanId; + if (EventTracer != nullptr) + { + SpanId = EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreatePushRPC(TargetObject, Function)); + } + check(EntityId != SpatialConstants::INVALID_ENTITY_ID); - Worker_RequestId RequestId = Connection->SendCommandRequest(EntityId, &CommandRequest, SpatialConstants::UNREAL_RPC_ENDPOINT_COMMAND_ID); + Worker_RequestId RequestId = Connection->SendCommandRequest(EntityId, &CommandRequest, NO_RETRIES, SpanId); if (Function->HasAnyFunctionFlags(FUNC_NetReliable)) { UE_LOG(LogSpatialSender, Verbose, TEXT("Sending reliable command request (entity: %lld, component: %d, function: %s, attempt: 1)"), - EntityId, CommandRequest.component_id, *Function->GetName()); - Receiver->AddPendingReliableRPC(RequestId, MakeShared(TargetObject, Function, ComponentId, RPCInfo.Index, Payload.PayloadData, 0)); + EntityId, CommandRequest.component_id, *Function->GetName()); + Receiver->AddPendingReliableRPC(RequestId, MakeShared(TargetObject, Function, ComponentId, RPCInfo.Index, + Payload.PayloadData, 0, SpanId)); } else { - UE_LOG(LogSpatialSender, Verbose, TEXT("Sending unreliable command request (entity: %lld, component: %d, function: %s)"), - EntityId, CommandRequest.component_id, *Function->GetName()); + UE_LOG(LogSpatialSender, Verbose, TEXT("Sending unreliable command request (entity: %lld, component: %d, function: %s)"), EntityId, + CommandRequest.component_id, *Function->GetName()); } #if !UE_BUILD_SHIPPING TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type); #endif // !UE_BUILD_SHIPPING } -FRPCErrorInfo USpatialSender::SendLegacyRPC(UObject* TargetObject, UFunction* Function, const RPCPayload& Payload, USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef) +bool USpatialSender::SendRingBufferedRPC(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, + USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef) { const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); - - // Check if the Channel is listening - if ((RPCInfo.Type != ERPCType::NetMulticast) && !Channel->IsListening()) - { - // If the Entity endpoint is not yet ready to receive RPCs - - // treat the corresponding object as unresolved and queue RPC - // However, it doesn't matter in case of Multicast - return FRPCErrorInfo{ TargetObject, Function, ERPCResult::SpatialActorChannelNotListening }; - } - - // Check for Authority - Worker_EntityId EntityId = TargetObjectRef.Entity; - check(EntityId != SpatialConstants::INVALID_ENTITY_ID); - - Worker_ComponentId ComponentId = SpatialConstants::RPCTypeToWorkerComponentIdLegacy(RPCInfo.Type); - if (!NetDriver->StaticComponentView->HasAuthority(EntityId, ComponentId)) - { - ERPCQueueProcessResult QueueProcessResult = ERPCQueueProcessResult::DropEntireQueue; - if (AActor* TargetActor = Cast(TargetObject)) - { - if (TargetActor->HasAuthority()) - { - QueueProcessResult = ERPCQueueProcessResult::StopProcessing; - } - } - return FRPCErrorInfo{ TargetObject, Function, ERPCResult::NoAuthority, QueueProcessResult }; - } - - FWorkerComponentUpdate ComponentUpdate = CreateRPCEventUpdate(TargetObject, Payload, ComponentId, RPCInfo.Index); - - Connection->SendComponentUpdate(EntityId, &ComponentUpdate); - Connection->MaybeFlush(); -#if !UE_BUILD_SHIPPING - TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type); -#endif // !UE_BUILD_SHIPPING - - return FRPCErrorInfo{ TargetObject, Function, ERPCResult::Success }; -} - -bool USpatialSender::SendRingBufferedRPC(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef) -{ - const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); - const EPushRPCResult Result = RPCService->PushRPC(TargetObjectRef.Entity, RPCInfo.Type, Payload, Channel->bCreatedEntity); + const EPushRPCResult Result = + RPCService->PushRPC(TargetObjectRef.Entity, RPCInfo.Type, Payload, Channel->bCreatedEntity, TargetObject, Function); if (Result == EPushRPCResult::Success) { @@ -778,28 +760,35 @@ bool USpatialSender::SendRingBufferedRPC(UObject* TargetObject, UFunction* Funct switch (Result) { case EPushRPCResult::QueueOverflowed: - UE_LOG(LogSpatialSender, Log, TEXT("USpatialSender::SendRingBufferedRPC: Ring buffer queue overflowed, queuing RPC locally. Actor: %s, entity: %lld, function: %s"), - *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); + UE_LOG(LogSpatialSender, Log, + TEXT("USpatialSender::SendRingBufferedRPC: Ring buffer queue overflowed, queuing RPC locally. Actor: %s, entity: %lld, " + "function: %s"), + *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); return true; case EPushRPCResult::DropOverflowed: - UE_LOG(LogSpatialSender, Log, TEXT("USpatialSender::SendRingBufferedRPC: Ring buffer queue overflowed, dropping RPC. Actor: %s, entity: %lld, function: %s"), + UE_LOG( + LogSpatialSender, Log, + TEXT("USpatialSender::SendRingBufferedRPC: Ring buffer queue overflowed, dropping RPC. Actor: %s, entity: %lld, function: %s"), *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); return true; case EPushRPCResult::HasAckAuthority: UE_LOG(LogSpatialSender, Warning, - TEXT("USpatialSender::SendRingBufferedRPC: Worker has authority over ack component for RPC it is sending. RPC will not be sent. Actor: %s, entity: %lld, function: %s"), - *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); + TEXT("USpatialSender::SendRingBufferedRPC: Worker has authority over ack component for RPC it is sending. RPC will not be " + "sent. Actor: %s, entity: %lld, function: %s"), + *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); return true; case EPushRPCResult::NoRingBufferAuthority: // TODO: Change engine logic that calls Client RPCs from non-auth servers and change this to error. UNR-2517 UE_LOG(LogSpatialSender, Log, - TEXT("USpatialSender::SendRingBufferedRPC: Failed to send RPC because the worker does not have authority over ring buffer component. Actor: %s, entity: %lld, function: %s"), - *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); + TEXT("USpatialSender::SendRingBufferedRPC: Failed to send RPC because the worker does not have authority over ring buffer " + "component. Actor: %s, entity: %lld, function: %s"), + *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); return true; case EPushRPCResult::EntityBeingCreated: UE_LOG(LogSpatialSender, Log, - TEXT("USpatialSender::SendRingBufferedRPC: RPC was called between entity creation and initial authority gain, so it will be queued. Actor: %s, entity: %lld, function: %s"), - *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); + TEXT("USpatialSender::SendRingBufferedRPC: RPC was called between entity creation and initial authority gain, so it will be " + "queued. Actor: %s, entity: %lld, function: %s"), + *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); return false; default: return true; @@ -824,7 +813,9 @@ 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; }); + RetryRPCs.Sort([](const TSharedRef& A, const TSharedRef& B) { + return A->RetryIndex < B->RetryIndex; + }); for (auto& RetryRPC : RetryRPCs) { RetryReliableRPC(RetryRPC); @@ -844,17 +835,24 @@ void USpatialSender::RetryReliableRPC(TSharedRef RetryRPC) FUnrealObjectRef TargetObjectRef = PackageMap->GetUnrealObjectRefFromObject(TargetObject); if (TargetObjectRef == FUnrealObjectRef::UNRESOLVED_OBJECT_REF) { - UE_LOG(LogSpatialSender, Warning, TEXT("Actor %s got unresolved (?) before RPC %s could be retried. This RPC will not be sent."), *TargetObject->GetName(), *RetryRPC->Function->GetName()); + UE_LOG(LogSpatialSender, Warning, TEXT("Actor %s got unresolved (?) before RPC %s could be retried. This RPC will not be sent."), + *TargetObject->GetName(), *RetryRPC->Function->GetName()); return; } + FSpatialGDKSpanId NewSpanId; + if (EventTracer != nullptr) + { + NewSpanId = EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateRetryRPC(), RetryRPC->SpanId.GetConstId(), 1); + } + Worker_CommandRequest CommandRequest = CreateRetryRPCCommandRequest(*RetryRPC, TargetObjectRef.Offset); - Worker_RequestId RequestId = Connection->SendCommandRequest(TargetObjectRef.Entity, &CommandRequest, SpatialConstants::UNREAL_RPC_ENDPOINT_COMMAND_ID); + Worker_RequestId RequestId = Connection->SendCommandRequest(TargetObjectRef.Entity, &CommandRequest, NO_RETRIES, NewSpanId); // The number of attempts is used to determine the delay in case the command times out and we need to resend it. RetryRPC->Attempts++; UE_LOG(LogSpatialSender, Verbose, TEXT("Sending reliable command request (entity: %lld, component: %d, function: %s, attempt: %d)"), - TargetObjectRef.Entity, RetryRPC->ComponentId, *RetryRPC->Function->GetName(), RetryRPC->Attempts); + TargetObjectRef.Entity, RetryRPC->ComponentId, *RetryRPC->Function->GetName(), RetryRPC->Attempts); Receiver->AddPendingReliableRPC(RequestId, RetryRPC); } @@ -878,42 +876,14 @@ void USpatialSender::ProcessPositionUpdates() void USpatialSender::SendCreateEntityRequest(USpatialActorChannel* Channel, uint32& OutBytesWritten) { - UE_LOG(LogSpatialSender, Log, TEXT("Sending create entity request for %s with EntityId %lld, HasAuthority: %d"), *Channel->Actor->GetName(), Channel->GetEntityId(), Channel->Actor->HasAuthority()); + 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); Receiver->AddPendingActorRequest(RequestId, Channel); } -void USpatialSender::SendRequestToClearRPCsOnEntityCreation(Worker_EntityId EntityId) -{ - Worker_CommandRequest CommandRequest = RPCsOnEntityCreation::CreateClearFieldsCommandRequest(); - NetDriver->Connection->SendCommandRequest(EntityId, &CommandRequest, SpatialConstants::CLEAR_RPCS_ON_ENTITY_CREATION); -} - -void USpatialSender::ClearRPCsOnEntityCreation(Worker_EntityId EntityId) -{ - check(NetDriver->IsServer()); - FWorkerComponentUpdate Update = RPCsOnEntityCreation::CreateClearFieldsUpdate(); - NetDriver->Connection->SendComponentUpdate(EntityId, &Update); -} - -void USpatialSender::SendClientEndpointReadyUpdate(Worker_EntityId EntityId) -{ - ClientRPCEndpointLegacy Endpoint; - Endpoint.bReady = true; - FWorkerComponentUpdate Update = Endpoint.CreateRPCEndpointUpdate(); - NetDriver->Connection->SendComponentUpdate(EntityId, &Update); -} - -void USpatialSender::SendServerEndpointReadyUpdate(Worker_EntityId EntityId) -{ - ServerRPCEndpointLegacy Endpoint; - Endpoint.bReady = true; - FWorkerComponentUpdate Update = Endpoint.CreateRPCEndpointUpdate(); - NetDriver->Connection->SendComponentUpdate(EntityId, &Update); -} - void USpatialSender::ProcessOrQueueOutgoingRPC(const FUnrealObjectRef& InTargetObjectRef, SpatialGDK::RPCPayload&& InPayload) { TWeakObjectPtr TargetObjectWeakPtr = PackageMap->GetObjectFromUnrealObjectRef(InTargetObjectRef); @@ -928,7 +898,7 @@ void USpatialSender::ProcessOrQueueOutgoingRPC(const FUnrealObjectRef& InTargetO UFunction* Function = ClassInfo.RPCs[InPayload.Index]; const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); - OutgoingRPCs.ProcessOrQueueRPC(InTargetObjectRef, RPCInfo.Type, MoveTemp(InPayload)); + OutgoingRPCs.ProcessOrQueueRPC(InTargetObjectRef, RPCInfo.Type, MoveTemp(InPayload), 0); // Try to send all pending RPCs unconditionally OutgoingRPCs.ProcessRPCs(); @@ -944,7 +914,9 @@ FSpatialNetBitWriter USpatialSender::PackRPCDataToSpatialNetBitWriter(UFunction* return PayloadWriter; } -Worker_CommandRequest USpatialSender::CreateRPCCommandRequest(UObject* TargetObject, const RPCPayload& Payload, Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, Worker_EntityId& OutEntityId) +Worker_CommandRequest USpatialSender::CreateRPCCommandRequest(UObject* TargetObject, const RPCPayload& Payload, + Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, + Worker_EntityId& OutEntityId) { Worker_CommandRequest CommandRequest = {}; CommandRequest.component_id = ComponentId; @@ -957,7 +929,8 @@ Worker_CommandRequest USpatialSender::CreateRPCCommandRequest(UObject* TargetObj OutEntityId = TargetObjectRef.Entity; - RPCPayload::WriteToSchemaObject(RequestObject, TargetObjectRef.Offset, CommandIndex, Payload.PayloadData.GetData(), Payload.PayloadData.Num()); + RPCPayload::WriteToSchemaObject(RequestObject, TargetObjectRef.Offset, CommandIndex, Payload.PayloadData.GetData(), + Payload.PayloadData.Num()); return CommandRequest; } @@ -975,62 +948,46 @@ Worker_CommandRequest USpatialSender::CreateRetryRPCCommandRequest(const FReliab return CommandRequest; } -FWorkerComponentUpdate USpatialSender::CreateRPCEventUpdate(UObject* TargetObject, const RPCPayload& Payload, Worker_ComponentId ComponentId, Schema_FieldId EventIndex) +void USpatialSender::SendCommandResponse(Worker_RequestId RequestId, Worker_CommandResponse& Response, const FSpatialGDKSpanId& CauseSpanId) { - FWorkerComponentUpdate ComponentUpdate = {}; - - ComponentUpdate.component_id = ComponentId; - ComponentUpdate.schema_type = Schema_CreateComponentUpdate(); - Schema_Object* EventsObject = Schema_GetComponentUpdateEvents(ComponentUpdate.schema_type); - Schema_Object* EventData = Schema_AddObject(EventsObject, SpatialConstants::UNREAL_RPC_ENDPOINT_EVENT_ID); - - FUnrealObjectRef TargetObjectRef(PackageMap->GetUnrealObjectRefFromNetGUID(PackageMap->GetNetGUIDFromObject(TargetObject))); - ensure(TargetObjectRef != FUnrealObjectRef::UNRESOLVED_OBJECT_REF); - - Payload.WriteToSchemaObject(EventData); - -#if TRACE_LIB_ACTIVE - ComponentUpdate.Trace = Payload.Trace; -#endif - - return ComponentUpdate; -} + FSpatialGDKSpanId SpanId; + if (EventTracer != nullptr) + { + SpanId = + EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateSendCommandResponse(RequestId, true), CauseSpanId.GetConstId(), 1); + } -void USpatialSender::SendCommandResponse(Worker_RequestId RequestId, Worker_CommandResponse& Response) -{ - Connection->SendCommandResponse(RequestId, &Response); + Connection->SendCommandResponse(RequestId, &Response, SpanId); } -void USpatialSender::SendEmptyCommandResponse(Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, Worker_RequestId RequestId) +void USpatialSender::SendEmptyCommandResponse(Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, Worker_RequestId RequestId, + const FSpatialGDKSpanId& CauseSpanId) { Worker_CommandResponse Response = {}; Response.component_id = ComponentId; Response.command_index = CommandIndex; Response.schema_type = Schema_CreateCommandResponse(); - Connection->SendCommandResponse(RequestId, &Response); -} + FSpatialGDKSpanId SpanId; + if (EventTracer != nullptr) + { + SpanId = + EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateSendCommandResponse(RequestId, true), CauseSpanId.GetConstId(), 1); + } -void USpatialSender::SendCommandFailure(Worker_RequestId RequestId, const FString& Message) -{ - Connection->SendCommandFailure(RequestId, Message); + Connection->SendCommandResponse(RequestId, &Response, SpanId); } -// 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) +void USpatialSender::SendCommandFailure(Worker_RequestId RequestId, const FString& Message, const FSpatialGDKSpanId& CauseSpanId) { - check(StaticComponentView->HasAuthority(EntityId, SpatialConstants::ENTITY_ACL_COMPONENT_ID)); - - WorkerAttributeSet OwningClientAttribute = { OwnerWorkerAttribute }; - WorkerRequirementSet OwningClientOnly = { OwningClientAttribute }; - - 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(); + FSpatialGDKSpanId SpanId; + if (EventTracer != nullptr) + { + SpanId = + EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateSendCommandResponse(RequestId, false), CauseSpanId.GetConstId(), 1); + } - Connection->SendComponentUpdate(EntityId, &Update); + Connection->SendCommandFailure(RequestId, Message, SpanId); } void USpatialSender::UpdateInterestComponent(AActor* Actor) @@ -1044,7 +1001,8 @@ void USpatialSender::UpdateInterestComponent(AActor* Actor) return; } - FWorkerComponentUpdate Update = NetDriver->InterestFactory->CreateInterestUpdate(Actor, ClassInfoManager->GetOrCreateClassInfoByObject(Actor), EntityId); + FWorkerComponentUpdate Update = + NetDriver->InterestFactory->CreateInterestUpdate(Actor, ClassInfoManager->GetOrCreateClassInfoByObject(Actor), EntityId); Connection->SendComponentUpdate(EntityId, &Update); } @@ -1070,8 +1028,15 @@ void USpatialSender::RetireEntity(const Worker_EntityId EntityId, bool bIsNetSta // Actor no longer guaranteed to be in package map, but still useful for additional logging info AActor* Actor = Cast(PackageMap->GetObjectFromEntityId(EntityId)); - UE_LOG(LogSpatialSender, Log, TEXT("Sending delete entity request for %s with EntityId %lld, HasAuthority: %d"), *GetPathNameSafe(Actor), EntityId, Actor != nullptr ? Actor->HasAuthority() : false); - Connection->SendDeleteEntityRequest(EntityId); + UE_LOG(LogSpatialSender, Log, TEXT("Sending delete entity request for %s with EntityId %lld, HasAuthority: %d"), + *GetPathNameSafe(Actor), EntityId, Actor != nullptr ? Actor->HasAuthority() : false); + + if (EventTracer != nullptr) + { + FSpatialGDKSpanId SpanId = EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateSendRetireEntity(Actor, EntityId)); + } + + Connection->SendDeleteEntityRequest(EntityId, RETRY_UNTIL_COMPLETE); } } @@ -1086,12 +1051,12 @@ void USpatialSender::CreateTombstoneEntity(AActor* 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); + UE_LOG(LogSpatialSender, Log, + TEXT("Creating tombstone entity for actor. " + "Actor: %s. Entity ID: %d."), + *Actor->GetName(), EntityId); #if WITH_EDITOR NetDriver->TrackTombstone(EntityId); @@ -1100,7 +1065,7 @@ void USpatialSender::CreateTombstoneEntity(AActor* Actor) void USpatialSender::AddTombstoneToEntity(const Worker_EntityId EntityId) { - check(NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::TOMBSTONE_COMPONENT_ID)); + check(NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID)); Worker_AddComponentOp AddComponentOp{}; AddComponentOp.entity_id = EntityId; diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSnapshotManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSnapshotManager.cpp index 97541dfdf6..cb58e4deaa 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSnapshotManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSnapshotManager.cpp @@ -16,9 +16,11 @@ SpatialSnapshotManager::SpatialSnapshotManager() : Connection(nullptr) , GlobalStateManager(nullptr) , Receiver(nullptr) -{} +{ +} -void SpatialSnapshotManager::Init(USpatialWorkerConnection* InConnection, UGlobalStateManager* InGlobalStateManager, USpatialReceiver* InReceiver) +void SpatialSnapshotManager::Init(USpatialWorkerConnection* InConnection, UGlobalStateManager* InGlobalStateManager, + USpatialReceiver* InReceiver) { check(InConnection != nullptr); Connection = InConnection; @@ -36,7 +38,8 @@ void SpatialSnapshotManager::Init(USpatialWorkerConnection* InConnection, UGloba // Should only be triggered by the worker which is authoritative over the GSM. void SpatialSnapshotManager::WorldWipe(const PostWorldWipeDelegate& PostWorldWipeDelegate) { - UE_LOG(LogSnapshotManager, Log, TEXT("World wipe for deployment has been triggered. All entities with the UnrealMetaData component 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 UnrealMetadataConstraint; UnrealMetadataConstraint.constraint_type = WORKER_CONSTRAINT_TYPE_COMPONENT; @@ -44,15 +47,12 @@ void SpatialSnapshotManager::WorldWipe(const PostWorldWipeDelegate& PostWorldWip Worker_EntityQuery WorldQuery{}; WorldQuery.constraint = UnrealMetadataConstraint; - WorldQuery.result_type = WORKER_RESULT_TYPE_SNAPSHOT; - Worker_RequestId RequestID; check(Connection.IsValid()); - RequestID = Connection->SendEntityQueryRequest(&WorldQuery); + const Worker_RequestId RequestID = Connection->SendEntityQueryRequest(&WorldQuery, RETRY_UNTIL_COMPLETE); EntityQueryDelegate WorldQueryDelegate; - WorldQueryDelegate.BindLambda([Connection = this->Connection, PostWorldWipeDelegate](const Worker_EntityQueryResponseOp& Op) - { + WorldQueryDelegate.BindLambda([Connection = this->Connection, PostWorldWipeDelegate](const Worker_EntityQueryResponseOp& Op) { if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) { UE_LOG(LogSnapshotManager, Error, TEXT("SnapshotManager WorldWipe - World entity query failed: %s"), UTF8_TO_TCHAR(Op.message)); @@ -83,11 +83,12 @@ void SpatialSnapshotManager::DeleteEntities(const Worker_EntityQueryResponseOp& { UE_LOG(LogSnapshotManager, Verbose, TEXT("Sending delete request for: %i"), Op.results[i].entity_id); check(Connection.IsValid()); - Connection->SendDeleteEntityRequest(Op.results[i].entity_id); + Connection->SendDeleteEntityRequest(Op.results[i].entity_id, RETRY_UNTIL_COMPLETE); } } -// GetSnapshotPath will take a snapshot (with or without the .snapshot extension) name and convert it to a relative path in the Game/Content folder. +// GetSnapshotPath will take a snapshot (with or without the .snapshot extension) name and convert it to a relative path in the Game/Content +// folder. FString GetSnapshotPath(const FString& SnapshotName) { FString SnapshotsDirectory = FPaths::ProjectContentDir() + TEXT("Spatial/Snapshots/"); @@ -165,8 +166,8 @@ void SpatialSnapshotManager::LoadSnapshot(const FString& SnapshotName) // Set up reserve IDs delegate ReserveEntityIDsDelegate SpawnEntitiesDelegate; - SpawnEntitiesDelegate.BindLambda([Connection = this->Connection, GlobalStateManager = this->GlobalStateManager, EntitiesToSpawn](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 @@ -191,7 +192,7 @@ void SpatialSnapshotManager::LoadSnapshot(const FString& SnapshotName) } UE_LOG(LogSnapshotManager, Log, TEXT("Sending entity create request for: %i"), ReservedEntityID); - Connection->SendCreateEntityRequest(MoveTemp(EntityToSpawn), &ReservedEntityID); + Connection->SendCreateEntityRequest(MoveTemp(EntityToSpawn), &ReservedEntityID, RETRY_UNTIL_COMPLETE); } GlobalStateManager->SetDeploymentState(); @@ -200,7 +201,7 @@ void SpatialSnapshotManager::LoadSnapshot(const FString& SnapshotName) // Reserve the Entity IDs check(Connection.IsValid()); - Worker_RequestId ReserveRequestID = Connection->SendReserveEntityIdsRequest(EntitiesToSpawn.Num()); + const Worker_RequestId ReserveRequestID = Connection->SendReserveEntityIdsRequest(EntitiesToSpawn.Num(), RETRY_UNTIL_COMPLETE); // TODO: UNR-654 // References to entities that are stored within the snapshot need remapping once we know the new entity IDs. diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp index 2481a7d3a2..ee1caa3971 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp @@ -4,18 +4,18 @@ #include "Schema/AuthorityIntent.h" #include "Schema/ClientEndpoint.h" -#include "Schema/ClientRPCEndpointLegacy.h" #include "Schema/Component.h" -#include "Schema/ComponentPresence.h" +#include "Schema/DebugComponent.h" #include "Schema/Heartbeat.h" #include "Schema/Interest.h" #include "Schema/MulticastRPCs.h" #include "Schema/NetOwningClientWorker.h" #include "Schema/RPCPayload.h" +#include "Schema/Restricted.h" #include "Schema/ServerEndpoint.h" -#include "Schema/ServerRPCEndpointLegacy.h" #include "Schema/SpatialDebugging.h" #include "Schema/SpawnData.h" +#include "Schema/StandardLibrary.h" #include "Schema/UnrealMetadata.h" Worker_Authority USpatialStaticComponentView::GetAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const @@ -36,6 +36,11 @@ bool USpatialStaticComponentView::HasAuthority(Worker_EntityId EntityId, Worker_ return GetAuthority(EntityId, ComponentId) == WORKER_AUTHORITY_AUTHORITATIVE; } +bool USpatialStaticComponentView::HasEntity(Worker_EntityId EntityId) const +{ + return EntityComponentMap.Find(EntityId) != nullptr; +} + bool USpatialStaticComponentView::HasComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const { if (auto* EntityComponentStorage = EntityComponentMap.Find(EntityId)) @@ -48,19 +53,9 @@ bool USpatialStaticComponentView::HasComponent(Worker_EntityId EntityId, Worker_ void USpatialStaticComponentView::OnAddComponent(const Worker_AddComponentOp& Op) { - // With dynamic components enabled, it's possible to get duplicate AddComponent ops which need handling idempotently. - // For the sake of efficiency we just exit early here. - if (HasComponent(Op.entity_id, Op.data.component_id)) - { - return; - } - TUniquePtr Data; switch (Op.data.component_id) { - case SpatialConstants::ENTITY_ACL_COMPONENT_ID: - Data = MakeUnique(Op.data); - break; case SpatialConstants::METADATA_COMPONENT_ID: Data = MakeUnique(Op.data); break; @@ -85,15 +80,6 @@ void USpatialStaticComponentView::OnAddComponent(const Worker_AddComponentOp& Op case SpatialConstants::HEARTBEAT_COMPONENT_ID: Data = MakeUnique(Op.data); break; - case SpatialConstants::RPCS_ON_ENTITY_CREATION_ID: - Data = MakeUnique(Op.data); - break; - case SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY: - Data = MakeUnique(Op.data); - break; - case SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY: - Data = MakeUnique(Op.data); - break; case SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID: Data = MakeUnique(Op.data); break; @@ -109,12 +95,18 @@ void USpatialStaticComponentView::OnAddComponent(const Worker_AddComponentOp& Op 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; + case SpatialConstants::AUTHORITY_DELEGATION_COMPONENT_ID: + Data = MakeUnique(Op.data); + break; + case SpatialConstants::GDK_DEBUG_COMPONENT_ID: + Data = MakeUnique(Op.data); + break; + case SpatialConstants::PARTITION_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; @@ -142,18 +134,9 @@ void USpatialStaticComponentView::OnComponentUpdate(const Worker_ComponentUpdate switch (Op.update.component_id) { - case SpatialConstants::ENTITY_ACL_COMPONENT_ID: - Component = GetComponentData(Op.entity_id); - break; case SpatialConstants::POSITION_COMPONENT_ID: Component = GetComponentData(Op.entity_id); break; - case SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY: - Component = GetComponentData(Op.entity_id); - break; - 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; @@ -169,12 +152,18 @@ void USpatialStaticComponentView::OnComponentUpdate(const Worker_ComponentUpdate 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; + case SpatialConstants::AUTHORITY_DELEGATION_COMPONENT_ID: + Component = GetComponentData(Op.entity_id); + break; + case SpatialConstants::GDK_DEBUG_COMPONENT_ID: + Component = GetComponentData(Op.entity_id); + break; + case SpatialConstants::PARTITION_COMPONENT_ID: + Component = GetComponentData(Op.entity_id); + break; default: return; } @@ -185,7 +174,7 @@ void USpatialStaticComponentView::OnComponentUpdate(const Worker_ComponentUpdate } } -void USpatialStaticComponentView::OnAuthorityChange(const Worker_AuthorityChangeOp& Op) +void USpatialStaticComponentView::OnAuthorityChange(const Worker_ComponentSetAuthorityChangeOp& Op) { - EntityComponentAuthorityMap.FindOrAdd(Op.entity_id).FindOrAdd(Op.component_id) = (Worker_Authority)Op.authority; + EntityComponentAuthorityMap.FindOrAdd(Op.entity_id).FindOrAdd(Op.component_set_id) = (Worker_Authority)Op.authority; } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialWorkerFlags.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialWorkerFlags.cpp index c3929129ed..a28e3b5efc 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialWorkerFlags.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialWorkerFlags.cpp @@ -13,29 +13,60 @@ bool USpatialWorkerFlags::GetWorkerFlag(const FString& InFlagName, FString& OutF return false; } -void USpatialWorkerFlags::ApplyWorkerFlagUpdate(const Worker_FlagUpdateOp& Op) +void USpatialWorkerFlags::SetWorkerFlag(const FString FlagName, FString FlagValue) { - FString NewName = FString(UTF8_TO_TCHAR(Op.name)); - - if (Op.value != nullptr) + FString& StoredFlagValue = WorkerFlags.FindOrAdd(FlagName); + StoredFlagValue = MoveTemp(FlagValue); + if (FOnWorkerFlagUpdated* OnWorkerFlagUpdatedPtr = WorkerFlagCallbacks.Find(FlagName)) { - FString NewValue = FString(UTF8_TO_TCHAR(Op.value)); - FString& ValueFlag = WorkerFlags.FindOrAdd(NewName); - ValueFlag = NewValue; - OnWorkerFlagsUpdated.Broadcast(NewName, NewValue); + OnWorkerFlagUpdatedPtr->Broadcast(FlagName, StoredFlagValue); } - else + OnAnyWorkerFlagUpdated.Broadcast(FlagName, StoredFlagValue); +} + +void USpatialWorkerFlags::RemoveWorkerFlag(const FString FlagName) +{ + WorkerFlags.Remove(FlagName); +} + +void USpatialWorkerFlags::RegisterAnyFlagUpdatedCallback(const FOnAnyWorkerFlagUpdatedBP& InDelegate) +{ + OnAnyWorkerFlagUpdated.Add(InDelegate); +} + +void USpatialWorkerFlags::UnregisterAnyFlagUpdatedCallback(const FOnAnyWorkerFlagUpdatedBP& InDelegate) +{ + OnAnyWorkerFlagUpdated.Remove(InDelegate); +} + +void USpatialWorkerFlags::RegisterAndInvokeAnyFlagUpdatedCallback(const FOnAnyWorkerFlagUpdatedBP& InDelegate) +{ + RegisterAnyFlagUpdatedCallback(InDelegate); + for (const auto& FlagValuePair : WorkerFlags) { - WorkerFlags.Remove(NewName); + InDelegate.Execute(FlagValuePair.Key, FlagValuePair.Value); } } -void USpatialWorkerFlags::BindToOnWorkerFlagsUpdated(const FOnWorkerFlagsUpdatedBP& InDelegate) +void USpatialWorkerFlags::RegisterFlagUpdatedCallback(const FString& InFlagName, const FOnWorkerFlagUpdatedBP& InDelegate) +{ + FOnWorkerFlagUpdated& OnWorkerFlagUpdated = WorkerFlagCallbacks.FindOrAdd(InFlagName); + OnWorkerFlagUpdated.Add(InDelegate); +} + +void USpatialWorkerFlags::UnregisterFlagUpdatedCallback(const FString& InFlagName, const FOnWorkerFlagUpdatedBP& InDelegate) { - OnWorkerFlagsUpdated.Add(InDelegate); + if (FOnWorkerFlagUpdated* OnWorkerFlagUpdatedPtr = WorkerFlagCallbacks.Find(InFlagName)) + { + OnWorkerFlagUpdatedPtr->Remove(InDelegate); + } } -void USpatialWorkerFlags::UnbindFromOnWorkerFlagsUpdated(const FOnWorkerFlagsUpdatedBP& InDelegate) +void USpatialWorkerFlags::RegisterAndInvokeFlagUpdatedCallback(const FString& InFlagName, const FOnWorkerFlagUpdatedBP& InDelegate) { - OnWorkerFlagsUpdated.Remove(InDelegate); + RegisterFlagUpdatedCallback(InFlagName, InDelegate); + if (const FString* ValuePtr = WorkerFlags.Find(InFlagName)) + { + InDelegate.Execute(InFlagName, *ValuePtr); + } } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/WellKnownEntitySystem.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/WellKnownEntitySystem.cpp new file mode 100644 index 0000000000..1eaafdb202 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/WellKnownEntitySystem.cpp @@ -0,0 +1,184 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Interop/WellKnownEntitySystem.h" + +#include "Interop/SpatialReceiver.h" + +DEFINE_LOG_CATEGORY(LogWellKnownEntitySystem); + +namespace SpatialGDK +{ +WellKnownEntitySystem::WellKnownEntitySystem(const FSubView& SubView, USpatialReceiver* InReceiver, USpatialWorkerConnection* InConnection, + const int InNumberOfWorkers, SpatialVirtualWorkerTranslator& InVirtualWorkerTranslator, + UGlobalStateManager& InGlobalStateManager) + : SubView(&SubView) + , Receiver(InReceiver) + , VirtualWorkerTranslator(&InVirtualWorkerTranslator) + , GlobalStateManager(&InGlobalStateManager) + , Connection(InConnection) + , NumberOfWorkers(InNumberOfWorkers) +{ +} + +void WellKnownEntitySystem::Advance() +{ + const FSubViewDelta& SubViewDelta = SubView->GetViewDelta(); + for (const EntityDelta& Delta : SubViewDelta.EntityDeltas) + { + switch (Delta.Type) + { + case EntityDelta::UPDATE: + { + for (const ComponentChange& Change : Delta.ComponentUpdates) + { + ProcessComponentUpdate(Change.ComponentId, Change.Update); + } + for (const ComponentChange& Change : Delta.ComponentsAdded) + { + ProcessComponentAdd(Change.ComponentId, Change.Data); + } + for (const AuthorityChange& Change : Delta.AuthorityGained) + { + ProcessAuthorityGain(Delta.EntityId, Change.ComponentId); + } + break; + } + case EntityDelta::ADD: + ProcessEntityAdd(Delta.EntityId); + break; + default: + break; + } + } +} + +void WellKnownEntitySystem::ProcessComponentUpdate(const Worker_ComponentId ComponentId, Schema_ComponentUpdate* Update) +{ + switch (ComponentId) + { + case SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID: + VirtualWorkerTranslator->ApplyVirtualWorkerManagerData(Schema_GetComponentUpdateFields(Update)); + break; + case SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID: + GlobalStateManager->ApplyDeploymentMapUpdate(Update); + break; + case SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID: + GlobalStateManager->ApplyStartupActorManagerUpdate(Update); + break; + case SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID: +#if WITH_EDITOR + GlobalStateManager->OnShutdownComponentUpdate(Update); +#endif // WITH_EDITOR + break; + default: + break; + } +} + +void WellKnownEntitySystem::ProcessComponentAdd(const Worker_ComponentId ComponentId, Schema_ComponentData* Data) +{ + switch (ComponentId) + { + case SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID: + VirtualWorkerTranslator->ApplyVirtualWorkerManagerData(Schema_GetComponentDataFields(Data)); + break; + case SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID: + GlobalStateManager->ApplyDeploymentMapData(Data); + break; + case SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID: + GlobalStateManager->ApplyStartupActorManagerData(Data); + break; + case SpatialConstants::SERVER_WORKER_COMPONENT_ID: + MaybeClaimSnapshotPartition(); + break; + default: + break; + } +} + +void WellKnownEntitySystem::ProcessAuthorityGain(const Worker_EntityId EntityId, const Worker_ComponentSetId ComponentSetId) +{ + GlobalStateManager->AuthorityChanged({ EntityId, ComponentSetId, WORKER_AUTHORITY_AUTHORITATIVE }); + + if (SubView->GetView()[EntityId].Components.ContainsByPredicate( + SpatialGDK::ComponentIdEquality{ SpatialConstants::SERVER_WORKER_COMPONENT_ID })) + { + GlobalStateManager->TrySendWorkerReadyToBeginPlay(); + } + + if (SubView->GetView()[EntityId].Components.ContainsByPredicate( + SpatialGDK::ComponentIdEquality{ SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID })) + { + InitializeVirtualWorkerTranslationManager(); + VirtualWorkerTranslationManager->AuthorityChanged({ EntityId, ComponentSetId, WORKER_AUTHORITY_AUTHORITATIVE }); + } +} + +void WellKnownEntitySystem::ProcessEntityAdd(const Worker_EntityId EntityId) +{ + const EntityViewElement& Element = SubView->GetView()[EntityId]; + for (const ComponentData& ComponentData : Element.Components) + { + ProcessComponentAdd(ComponentData.GetComponentId(), ComponentData.GetUnderlying()); + } + for (const Worker_ComponentSetId ComponentId : Element.Authority) + { + ProcessAuthorityGain(EntityId, ComponentId); + } +} + +// 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 WellKnownEntitySystem::InitializeVirtualWorkerTranslationManager() +{ + VirtualWorkerTranslationManager = MakeUnique(Receiver, Connection, VirtualWorkerTranslator); + VirtualWorkerTranslationManager->SetNumberOfVirtualWorkers(NumberOfWorkers); +} + +void WellKnownEntitySystem::MaybeClaimSnapshotPartition() +{ + // Perform a naive leader election where we wait for the correct number of server workers to be present in the deployment, and then + // whichever server has the lowest server worker entity ID becomes the leader and claims the snapshot partition. + const Worker_EntityId LocalServerWorkerEntityId = GlobalStateManager->GetLocalServerWorkerEntityId(); + + if (LocalServerWorkerEntityId == SpatialConstants::INVALID_ENTITY_ID) + { + UE_LOG(LogWellKnownEntitySystem, Warning, TEXT("MaybeClaimSnapshotPartition aborted due to lack of local server worker entity")); + return; + } + + Worker_EntityId LowestEntityId = LocalServerWorkerEntityId; + + int ServerCount = 0; + for (const auto& Iter : SubView->GetView()) + { + const Worker_EntityId EntityId = Iter.Key; + const SpatialGDK::EntityViewElement& Element = Iter.Value; + if (Element.Components.ContainsByPredicate([](const SpatialGDK::ComponentData& CompData) { + return CompData.GetComponentId() == SpatialConstants::SERVER_WORKER_COMPONENT_ID; + })) + { + ServerCount++; + + if (EntityId < LowestEntityId) + { + LowestEntityId = EntityId; + } + } + } + + if (LocalServerWorkerEntityId == LowestEntityId && ServerCount >= NumberOfWorkers) + { + UE_LOG(LogWellKnownEntitySystem, Log, TEXT("MaybeClaimSnapshotPartition claiming snapshot partition")); + GlobalStateManager->ClaimSnapshotPartition(); + } + + if (ServerCount > NumberOfWorkers) + { + UE_LOG(LogWellKnownEntitySystem, Warning, + TEXT("MaybeClaimSnapshotPartition found too many server worker entities, expected %d got %d."), NumberOfWorkers, + ServerCount); + } +} + +} // Namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/DebugLBStrategy.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/DebugLBStrategy.cpp new file mode 100644 index 0000000000..fbf4e6b3d9 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/DebugLBStrategy.cpp @@ -0,0 +1,100 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "LoadBalancing/DebugLBStrategy.h" + +#include "EngineClasses/SpatialNetDriverDebugContext.h" +#include "EngineClasses/SpatialWorldSettings.h" + +DEFINE_LOG_CATEGORY(LogDebugLBStrategy); + +UDebugLBStrategy::UDebugLBStrategy() = default; + +void UDebugLBStrategy::InitDebugStrategy(USpatialNetDriverDebugContext* InDebugCtx, UAbstractLBStrategy* InWrappedStrategy) +{ + DebugCtx = InDebugCtx; + WrappedStrategy = InWrappedStrategy; + LocalVirtualWorkerId = InWrappedStrategy->GetLocalVirtualWorkerId(); +} + +void UDebugLBStrategy::SetLocalVirtualWorkerId(VirtualWorkerId InLocalVirtualWorkerId) +{ + check(WrappedStrategy); + WrappedStrategy->SetLocalVirtualWorkerId(InLocalVirtualWorkerId); + LocalVirtualWorkerId = WrappedStrategy->GetLocalVirtualWorkerId(); +} + +TSet UDebugLBStrategy::GetVirtualWorkerIds() const +{ + check(WrappedStrategy); + return WrappedStrategy->GetVirtualWorkerIds(); +} + +bool UDebugLBStrategy::ShouldHaveAuthority(const AActor& Actor) const +{ + check(WrappedStrategy); + + TOptional WorkerId = DebugCtx->GetActorHierarchyExplicitDelegation(&Actor); + if (WorkerId) + { + return WorkerId.GetValue() == GetLocalVirtualWorkerId(); + } + + return WrappedStrategy->ShouldHaveAuthority(Actor); +} + +VirtualWorkerId UDebugLBStrategy::WhoShouldHaveAuthority(const AActor& Actor) const +{ + check(WrappedStrategy); + + TOptional WorkerId = DebugCtx->GetActorHierarchyExplicitDelegation(&Actor); + if (WorkerId) + { + return WorkerId.GetValue(); + } + + return WrappedStrategy->WhoShouldHaveAuthority(Actor); +} + +SpatialGDK::QueryConstraint UDebugLBStrategy::GetWorkerInterestQueryConstraint(const VirtualWorkerId VirtualWorker) const +{ + check(WrappedStrategy); + + SpatialGDK::QueryConstraint DefaultConstraint = WrappedStrategy->GetWorkerInterestQueryConstraint(VirtualWorker); + SpatialGDK::QueryConstraint AdditionalConstraint = DebugCtx->ComputeAdditionalEntityQueryConstraint(); + DebugCtx->ClearNeedEntityInterestUpdate(); + + if (AdditionalConstraint.IsValid()) + { + SpatialGDK::QueryConstraint WorkerConstraint; + WorkerConstraint.OrConstraint.Add(DefaultConstraint); + WorkerConstraint.OrConstraint.Add(AdditionalConstraint); + + return WorkerConstraint; + } + + return DefaultConstraint; +} + +FVector UDebugLBStrategy::GetWorkerEntityPosition() const +{ + check(WrappedStrategy); + return WrappedStrategy->GetWorkerEntityPosition(); +} + +uint32 UDebugLBStrategy::GetMinimumRequiredWorkers() const +{ + check(WrappedStrategy); + return WrappedStrategy->GetMinimumRequiredWorkers(); +} + +void UDebugLBStrategy::SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtualWorkerId, const VirtualWorkerId& LastVirtualWorkerId) +{ + check(WrappedStrategy); + WrappedStrategy->SetVirtualWorkerIds(FirstVirtualWorkerId, LastVirtualWorkerId); +} + +UAbstractLBStrategy* UDebugLBStrategy::GetLBStrategyForVisualRendering() const +{ + check(WrappedStrategy); + return WrappedStrategy->GetLBStrategyForVisualRendering(); +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp index 0fb4b146ae..a1d6e163fb 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp @@ -3,7 +3,9 @@ #include "LoadBalancing/GridBasedLBStrategy.h" #include "EngineClasses/SpatialNetDriver.h" +#include "EngineClasses/SpatialWorldSettings.h" #include "Utils/SpatialActorUtils.h" +#include "Utils/SpatialStatics.h" #include "Templates/Tuple.h" @@ -85,7 +87,8 @@ 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)); + UE_LOG(LogGridBasedLBStrategy, Warning, TEXT("GridBasedLBStrategy not ready to relinquish authority for Actor %s."), + *AActor::GetDebugName(&Actor)); return false; } @@ -102,7 +105,8 @@ VirtualWorkerId UGridBasedLBStrategy::WhoShouldHaveAuthority(const AActor& Actor { if (!IsReady()) { - UE_LOG(LogGridBasedLBStrategy, Warning, TEXT("GridBasedLBStrategy not ready to decide on authority for Actor %s."), *AActor::GetDebugName(&Actor)); + UE_LOG(LogGridBasedLBStrategy, Warning, TEXT("GridBasedLBStrategy not ready to decide on authority for Actor %s."), + *AActor::GetDebugName(&Actor)); return SpatialConstants::INVALID_VIRTUAL_WORKER_ID; } @@ -113,32 +117,38 @@ VirtualWorkerId UGridBasedLBStrategy::WhoShouldHaveAuthority(const AActor& Actor { if (IsInside(WorkerCells[i], Actor2DLocation)) { - UE_LOG(LogGridBasedLBStrategy, Log, TEXT("Actor: %s, grid %d, worker %d for position %s"), *AActor::GetDebugName(&Actor), i, VirtualWorkerIds[i], *Actor2DLocation.ToString()); + UE_LOG(LogGridBasedLBStrategy, Log, TEXT("Actor: %s, grid %d, worker %d for position %s"), *AActor::GetDebugName(&Actor), i, + VirtualWorkerIds[i], *Actor2DLocation.ToString()); return VirtualWorkerIds[i]; } } - UE_LOG(LogGridBasedLBStrategy, Error, TEXT("GridBasedLBStrategy couldn't determine virtual worker for Actor %s at position %s"), *AActor::GetDebugName(&Actor), *Actor2DLocation.ToString()); + UE_LOG(LogGridBasedLBStrategy, Error, TEXT("GridBasedLBStrategy couldn't determine virtual worker for Actor %s at position %s"), + *AActor::GetDebugName(&Actor), *Actor2DLocation.ToString()); return SpatialConstants::INVALID_VIRTUAL_WORKER_ID; } -SpatialGDK::QueryConstraint UGridBasedLBStrategy::GetWorkerInterestQueryConstraint() const +SpatialGDK::QueryConstraint UGridBasedLBStrategy::GetWorkerInterestQueryConstraint(const VirtualWorkerId VirtualWorker) const { - // For a grid-based strategy, the interest area is the cell that the worker is authoritative over plus some border region. - check(IsReady()); - check(bIsStrategyUsedOnLocalWorker); + const int32 WorkerCell = VirtualWorkerIds.IndexOfByKey(VirtualWorker); + checkf(WorkerCell != INDEX_NONE, + TEXT("Tried to get worker interest query from a GridBasedLBStrategy with an unknown virtual worker ID. " + "Virtual worker: %d"), + VirtualWorker); - const FBox2D Interest2D = WorkerCells[LocalCellId].ExpandBy(InterestBorder); + // For a grid-based strategy, the interest area is the cell that the worker is authoritative over plus some border region. + const FBox2D Interest2D = WorkerCells[WorkerCell].ExpandBy(InterestBorder); const FVector2D Center2D = Interest2D.GetCenter(); - const FVector Center3D{ Center2D.X, Center2D.Y, 0.0f}; + 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}; + const FVector EdgeLengths3D{ EdgeLengths2D.X, EdgeLengths2D.Y, FLT_MAX }; SpatialGDK::QueryConstraint Constraint; - Constraint.BoxConstraint = SpatialGDK::BoxConstraint{ SpatialGDK::Coordinates::FromFVector(Center3D), SpatialGDK::EdgeLength::FromFVector(EdgeLengths3D) }; + Constraint.BoxConstraint = + SpatialGDK::BoxConstraint{ SpatialGDK::Coordinates::FromFVector(Center3D), SpatialGDK::EdgeLength::FromFVector(EdgeLengths3D) }; return Constraint; } @@ -158,7 +168,8 @@ uint32 UGridBasedLBStrategy::GetMinimumRequiredWorkers() const void UGridBasedLBStrategy::SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtualWorkerId, const VirtualWorkerId& LastVirtualWorkerId) { UE_LOG(LogGridBasedLBStrategy, Log, TEXT("Setting VirtualWorkerIds %d to %d"), FirstVirtualWorkerId, LastVirtualWorkerId); - for (VirtualWorkerId CurrentVirtualWorkerId = FirstVirtualWorkerId; CurrentVirtualWorkerId <= LastVirtualWorkerId; CurrentVirtualWorkerId++) + for (VirtualWorkerId CurrentVirtualWorkerId = FirstVirtualWorkerId; CurrentVirtualWorkerId <= LastVirtualWorkerId; + CurrentVirtualWorkerId++) { VirtualWorkerIds.Add(CurrentVirtualWorkerId); } @@ -166,8 +177,7 @@ void UGridBasedLBStrategy::SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtu 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; + 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 @@ -181,3 +191,40 @@ UGridBasedLBStrategy::LBStrategyRegions UGridBasedLBStrategy::GetLBStrategyRegio } return VirtualWorkerToCell; } + +#if WITH_EDITOR +void UGridBasedLBStrategy::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (PropertyChangedEvent.Property != nullptr) + { + const FName PropertyName(PropertyChangedEvent.Property->GetFName()); + if (PropertyName == GET_MEMBER_NAME_CHECKED(UGridBasedLBStrategy, Rows) + || PropertyName == GET_MEMBER_NAME_CHECKED(UGridBasedLBStrategy, Cols) + || PropertyName == GET_MEMBER_NAME_CHECKED(UGridBasedLBStrategy, WorldWidth) + || PropertyName == GET_MEMBER_NAME_CHECKED(UGridBasedLBStrategy, WorldHeight)) + { + const UWorld* World = GEditor->GetEditorWorldContext().World(); + check(World != nullptr); + + const UAbstractSpatialMultiWorkerSettings* MultiWorkerSettings = + USpatialStatics::GetSpatialMultiWorkerClass(World)->GetDefaultObject(); + + for (const FLayerInfo WorkerLayer : MultiWorkerSettings->WorkerLayers) + { + if (WorkerLayer.Name == SpatialConstants::DefaultLayer) + { + const TSubclassOf VisibleLoadBalanceStrategy = WorkerLayer.LoadBalanceStrategy; + + if (VisibleLoadBalanceStrategy != nullptr && VisibleLoadBalanceStrategy == GetClass()) + { + ASpatialWorldSettings::EditorRefreshSpatialDebugger(); + break; + } + } + } + } + } +} +#endif // WITH_EDITOR diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/LayeredLBStrategy.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/LayeredLBStrategy.cpp index 72822e4aec..166df8239a 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/LayeredLBStrategy.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/LayeredLBStrategy.cpp @@ -25,8 +25,7 @@ void ULayeredLBStrategy::SetLayers(const TArray& WorkerLayers) for (const FLayerInfo& LayerInfo : WorkerLayers) { checkf(*LayerInfo.LoadBalanceStrategy != nullptr, - TEXT("WorkerLayer %s does not specify a load balancing strategy (or it cannot be resolved)"), - *LayerInfo.Name.ToString()); + TEXT("WorkerLayer %s does not specify a load balancing strategy (or it cannot be resolved)"), *LayerInfo.Name.ToString()); UE_LOG(LogLayeredLBStrategy, Log, TEXT("Creating LBStrategy for Layer %s."), *LayerInfo.Name.ToString()); @@ -45,8 +44,8 @@ void ULayeredLBStrategy::SetLocalVirtualWorkerId(VirtualWorkerId InLocalVirtualW if (LocalVirtualWorkerId != SpatialConstants::INVALID_VIRTUAL_WORKER_ID) { UE_LOG(LogLayeredLBStrategy, Error, - TEXT("The Local Virtual Worker Id cannot be set twice. Current value: %d Requested new value: %d"), - LocalVirtualWorkerId, InLocalVirtualWorkerId); + TEXT("The Local Virtual Worker Id cannot be set twice. Current value: %d Requested new value: %d"), LocalVirtualWorkerId, + InLocalVirtualWorkerId); return; } @@ -66,7 +65,8 @@ bool ULayeredLBStrategy::ShouldHaveAuthority(const AActor& Actor) const { if (!IsReady()) { - UE_LOG(LogLayeredLBStrategy, Warning, TEXT("LayeredLBStrategy not ready to relinquish authority for Actor %s."), *AActor::GetDebugName(&Actor)); + UE_LOG(LogLayeredLBStrategy, Warning, TEXT("LayeredLBStrategy not ready to relinquish authority for Actor %s."), + *AActor::GetDebugName(&Actor)); return false; } @@ -79,7 +79,8 @@ bool ULayeredLBStrategy::ShouldHaveAuthority(const AActor& Actor) const const FName& LayerName = GetLayerNameForActor(*RootOwner); if (!LayerNameToLBStrategy.Contains(LayerName)) { - UE_LOG(LogLayeredLBStrategy, Error, TEXT("LayeredLBStrategy doesn't have a LBStrategy for Actor %s which is in Layer %s."), *AActor::GetDebugName(RootOwner), *LayerName.ToString()); + UE_LOG(LogLayeredLBStrategy, Error, TEXT("LayeredLBStrategy doesn't have a LBStrategy for Actor %s which is in Layer %s."), + *AActor::GetDebugName(RootOwner), *LayerName.ToString()); return false; } @@ -96,7 +97,8 @@ VirtualWorkerId ULayeredLBStrategy::WhoShouldHaveAuthority(const AActor& Actor) { if (!IsReady()) { - UE_LOG(LogLayeredLBStrategy, Warning, TEXT("LayeredLBStrategy not ready to decide on authority for Actor %s."), *AActor::GetDebugName(&Actor)); + UE_LOG(LogLayeredLBStrategy, Warning, TEXT("LayeredLBStrategy not ready to decide on authority for Actor %s."), + *AActor::GetDebugName(&Actor)); return SpatialConstants::INVALID_VIRTUAL_WORKER_ID; } @@ -109,32 +111,45 @@ VirtualWorkerId ULayeredLBStrategy::WhoShouldHaveAuthority(const AActor& Actor) const FName& LayerName = GetLayerNameForActor(*RootOwner); if (!LayerNameToLBStrategy.Contains(LayerName)) { - UE_LOG(LogLayeredLBStrategy, Error, TEXT("LayeredLBStrategy doesn't have a LBStrategy for Actor %s which is in Layer %s."), *AActor::GetDebugName(RootOwner), *LayerName.ToString()); + UE_LOG(LogLayeredLBStrategy, Error, TEXT("LayeredLBStrategy doesn't have a LBStrategy for Actor %s which is in Layer %s."), + *AActor::GetDebugName(RootOwner), *LayerName.ToString()); return SpatialConstants::INVALID_VIRTUAL_WORKER_ID; } const VirtualWorkerId ReturnedWorkerId = LayerNameToLBStrategy[LayerName]->WhoShouldHaveAuthority(*RootOwner); - UE_LOG(LogLayeredLBStrategy, Log, TEXT("LayeredLBStrategy returning virtual worker id %d for Actor %s."), ReturnedWorkerId, *AActor::GetDebugName(RootOwner)); + UE_LOG(LogLayeredLBStrategy, Log, TEXT("LayeredLBStrategy returning virtual worker id %d for Actor %s."), ReturnedWorkerId, + *AActor::GetDebugName(RootOwner)); return ReturnedWorkerId; } -SpatialGDK::QueryConstraint ULayeredLBStrategy::GetWorkerInterestQueryConstraint() const +SpatialGDK::QueryConstraint ULayeredLBStrategy::GetWorkerInterestQueryConstraint(const VirtualWorkerId VirtualWorker) const { - check(IsReady()); - if (!VirtualWorkerIdToLayerName.Contains(LocalVirtualWorkerId)) + if (!VirtualWorkerIdToLayerName.Contains(VirtualWorker)) { - UE_LOG(LogLayeredLBStrategy, Error, TEXT("LayeredLBStrategy doesn't have a LBStrategy for worker %d."), LocalVirtualWorkerId); + UE_LOG(LogLayeredLBStrategy, Error, TEXT("LayeredLBStrategy doesn't have a LBStrategy for worker %d."), VirtualWorker); SpatialGDK::QueryConstraint Constraint; Constraint.ComponentConstraint = 0; return Constraint; } else { - const FName& LayerName = VirtualWorkerIdToLayerName[LocalVirtualWorkerId]; + const FName& LayerName = VirtualWorkerIdToLayerName[VirtualWorker]; check(LayerNameToLBStrategy.Contains(LayerName)); - return LayerNameToLBStrategy[LayerName]->GetWorkerInterestQueryConstraint(); + return LayerNameToLBStrategy[LayerName]->GetWorkerInterestQueryConstraint(VirtualWorker); + } +} + +bool ULayeredLBStrategy::RequiresHandoverData() const +{ + for (const auto& Elem : LayerNameToLBStrategy) + { + if (Elem.Value->RequiresHandoverData()) + { + return true; + } } + return false; } FVector ULayeredLBStrategy::GetWorkerEntityPosition() const @@ -162,7 +177,8 @@ uint32 ULayeredLBStrategy::GetMinimumRequiredWorkers() const MinimumRequiredWorkers += Elem.Value->GetMinimumRequiredWorkers(); } - UE_LOG(LogLayeredLBStrategy, Verbose, TEXT("LayeredLBStrategy needs %d workers to support all layer strategies."), MinimumRequiredWorkers); + UE_LOG(LogLayeredLBStrategy, Verbose, TEXT("LayeredLBStrategy needs %d workers to support all layer strategies."), + MinimumRequiredWorkers); return MinimumRequiredWorkers; } @@ -181,10 +197,12 @@ void ULayeredLBStrategy::SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtual VirtualWorkerId LastVirtualWorkerIdToAssign = NextWorkerIdToAssign + MinimumRequiredWorkers - 1; if (LastVirtualWorkerIdToAssign > LastVirtualWorkerId) { - UE_LOG(LogLayeredLBStrategy, Error, TEXT("LayeredLBStrategy was not given enough VirtualWorkerIds to meet the demands of the layer strategies.")); + UE_LOG(LogLayeredLBStrategy, Error, + TEXT("LayeredLBStrategy was not given enough VirtualWorkerIds to meet the demands of the layer strategies.")); return; } - UE_LOG(LogLayeredLBStrategy, Log, TEXT("LayeredLBStrategy assigning VirtualWorkerIds %d to %d to Layer %s"), NextWorkerIdToAssign, LastVirtualWorkerIdToAssign, *Elem.Key.ToString()); + UE_LOG(LogLayeredLBStrategy, Log, TEXT("LayeredLBStrategy assigning VirtualWorkerIds %d to %d to Layer %s"), NextWorkerIdToAssign, + LastVirtualWorkerIdToAssign, *Elem.Key.ToString()); LBStrategy->SetVirtualWorkerIds(NextWorkerIdToAssign, LastVirtualWorkerIdToAssign); for (VirtualWorkerId id = NextWorkerIdToAssign; id <= LastVirtualWorkerIdToAssign; id++) @@ -196,7 +214,8 @@ void ULayeredLBStrategy::SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtual } // Keep a copy of the VirtualWorkerIds. This is temporary and will be removed in the next PR. - for (VirtualWorkerId CurrentVirtualWorkerId = FirstVirtualWorkerId; CurrentVirtualWorkerId <= LastVirtualWorkerId; CurrentVirtualWorkerId++) + for (VirtualWorkerId CurrentVirtualWorkerId = FirstVirtualWorkerId; CurrentVirtualWorkerId <= LastVirtualWorkerId; + CurrentVirtualWorkerId++) { VirtualWorkerIds.Add(CurrentVirtualWorkerId); } @@ -214,12 +233,44 @@ bool ULayeredLBStrategy::CouldHaveAuthority(const TSubclassOf Class) con UAbstractLBStrategy* ULayeredLBStrategy::GetLBStrategyForVisualRendering() const { // The default strategy is guaranteed to exist as long as the strategy is ready. - check(IsReady()); checkf(LayerNameToLBStrategy.Contains(SpatialConstants::DefaultLayer), - TEXT("Load balancing strategy does not contain default layer which is needed to render worker debug visualization. " - "Default layer presence should be enforced by MultiWorkerSettings edit validation. Class: %s"), *GetNameSafe(this)); + TEXT("Load balancing strategy does not contain default layer which is needed to render worker debug visualization. " + "Default layer presence should be enforced by MultiWorkerSettings edit validation. Class: %s"), + *GetNameSafe(this)); + return GetLBStrategyForLayer(SpatialConstants::DefaultLayer); +} + +UAbstractLBStrategy* ULayeredLBStrategy::GetLBStrategyForLayer(FName Layer) const +{ + // Editor has the option to display the load balanced zones and could query the strategy anytime. +#ifndef WITH_EDITOR + check(IsReady()); +#endif + + if (UAbstractLBStrategy* const* Entry = LayerNameToLBStrategy.Find(Layer)) + { + return *Entry; + } + return nullptr; +} + +FName ULayeredLBStrategy::GetLocalLayerName() const +{ + if (!IsReady()) + { + UE_LOG(LogLayeredLBStrategy, Error, TEXT("Tried to get worker layer name before the load balancing strategy was ready.")); + return NAME_None; + } + + const FName* LocalLayerName = VirtualWorkerIdToLayerName.Find(LocalVirtualWorkerId); + if (LocalLayerName == nullptr) + { + UE_LOG(LogLayeredLBStrategy, Error, TEXT("Load balancing strategy didn't contain mapping between virtual worker ID to layer name."), + LocalVirtualWorkerId); + return NAME_None; + } - return LayerNameToLBStrategy[SpatialConstants::DefaultLayer]; + return *LocalLayerName; } FName ULayeredLBStrategy::GetLayerNameForClass(const TSubclassOf Class) const @@ -236,7 +287,7 @@ FName ULayeredLBStrategy::GetLayerNameForClass(const TSubclassOf Class) { if (const FName* Layer = ClassPathToLayer.Find(ClassPtr)) { - FName LayerHolder = *Layer; + const FName LayerHolder = *Layer; if (FoundClass != Class) { ClassPathToLayer.Add(TSoftClassPtr(Class), LayerHolder); diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/OwnershipLockingPolicy.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/OwnershipLockingPolicy.cpp index a6fd752b4b..9e2f582320 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/OwnershipLockingPolicy.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/OwnershipLockingPolicy.cpp @@ -2,7 +2,6 @@ #include "LoadBalancing/OwnershipLockingPolicy.h" -#include "Schema/AuthorityIntent.h" #include "Schema/Component.h" #include "Utils/SpatialActorUtils.h" @@ -26,7 +25,8 @@ ActorLockToken UOwnershipLockingPolicy::AcquireLock(AActor* Actor, FString Debug { if (!CanAcquireLock(Actor)) { - UE_LOG(LogOwnershipLockingPolicy, Error, TEXT("Called AcquireLock but CanAcquireLock returned false. Actor: %s."), *GetNameSafe(Actor)); + UE_LOG(LogOwnershipLockingPolicy, Error, TEXT("Called AcquireLock but CanAcquireLock returned false. Actor: %s."), + *GetNameSafe(Actor)); return SpatialConstants::INVALID_ACTOR_LOCK_TOKEN; } @@ -43,14 +43,14 @@ ActorLockToken UOwnershipLockingPolicy::AcquireLock(AActor* Actor, FString Debug Actor->OnDestroyed.AddDynamic(this, &UOwnershipLockingPolicy::OnExplicitlyLockedActorDeleted); } - AActor* OwnershipHierarchyRoot = SpatialGDK::GetTopmostOwner(Actor); + AActor* OwnershipHierarchyRoot = SpatialGDK::GetTopmostReplicatedOwner(Actor); AddOwnershipHierarchyRootInformation(OwnershipHierarchyRoot, Actor); ActorToLockingState.Add(Actor, MigrationLockElement{ 1, OwnershipHierarchyRoot }); } UE_LOG(LogOwnershipLockingPolicy, Verbose, TEXT("Acquiring migration lock. Actor: %s. Lock name: %s. Token %lld: Locks held: %d."), - *GetNameSafe(Actor), *DebugString, NextToken, ActorToLockingState.Find(Actor)->LockCount); + *GetNameSafe(Actor), *DebugString, NextToken, ActorToLockingState.Find(Actor)->LockCount); TokenToNameAndActor.Emplace(NextToken, LockNameAndActor{ MoveTemp(DebugString), Actor }); return NextToken++; } @@ -66,7 +66,8 @@ bool UOwnershipLockingPolicy::ReleaseLock(const ActorLockToken Token) AActor* Actor = NameAndActor->Actor; const FString& Name = NameAndActor->LockName; - UE_LOG(LogOwnershipLockingPolicy, Verbose, TEXT("Releasing Actor migration lock. Actor: %s. Token: %lld. Lock name: %s"), *Actor->GetName(), Token, *Name); + UE_LOG(LogOwnershipLockingPolicy, Verbose, TEXT("Releasing Actor migration lock. Actor: %s. Token: %lld. Lock name: %s"), + *Actor->GetName(), Token, *Name); check(ActorToLockingState.Contains(Actor)); @@ -107,7 +108,7 @@ bool UOwnershipLockingPolicy::IsLocked(const AActor* Actor) const } // Is the hierarchy root of this Actor explicitly locked or on a locked hierarchy ownership path. - if (AActor* HierarchyRoot = SpatialGDK::GetTopmostOwner(Actor)) + if (AActor* HierarchyRoot = SpatialGDK::GetTopmostReplicatedOwner(Actor)) { return IsExplicitlyLocked(HierarchyRoot) || IsLockedHierarchyRoot(HierarchyRoot); } @@ -139,14 +140,23 @@ bool UOwnershipLockingPolicy::IsLockedHierarchyRoot(const AActor* Actor) const bool UOwnershipLockingPolicy::AcquireLockFromDelegate(AActor* ActorToLock, const FString& DelegateLockIdentifier) { + if (DelegateLockingIdentifierToActorLockToken.Contains(DelegateLockIdentifier)) + { + UE_LOG(LogOwnershipLockingPolicy, Error, + TEXT("AcquireLockFromDelegate: A lock with identifier \"%s\" already exists for actor \"%s\"."), *DelegateLockIdentifier, + *GetNameSafe(ActorToLock)); + return false; + } + const 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")); + UE_LOG(LogOwnershipLockingPolicy, Error, + TEXT("AcquireLock called from engine delegate returned an invalid token. Lock identifier: %s, Actor: %s"), + *DelegateLockIdentifier, *GetNameSafe(ActorToLock)); return false; } - check(!DelegateLockingIdentifierToActorLockToken.Contains(DelegateLockIdentifier)); DelegateLockingIdentifierToActorLockToken.Add(DelegateLockIdentifier, LockToken); return true; } @@ -155,7 +165,9 @@ bool UOwnershipLockingPolicy::ReleaseLockFromDelegate(AActor* ActorToRelease, co { if (!DelegateLockingIdentifierToActorLockToken.Contains(DelegateLockIdentifier)) { - UE_LOG(LogOwnershipLockingPolicy, Error, TEXT("Executed ReleaseLockDelegate for unidentified delegate lock identifier. Token: %s."), *DelegateLockIdentifier); + UE_LOG(LogOwnershipLockingPolicy, Error, + TEXT("ReleaseLockFromDelegate: Lock identifier \"%s\" has no lock associated with it for actor \"%s\"."), + *DelegateLockIdentifier, *GetNameSafe(ActorToRelease)); return false; } const ActorLockToken LockToken = DelegateLockingIdentifierToActorLockToken.FindAndRemoveChecked(DelegateLockIdentifier); @@ -183,13 +195,13 @@ void UOwnershipLockingPolicy::OnOwnerUpdated(const AActor* Actor, const AActor* // recalculate ownership hierarchies of all explicitly locked Actors in that hierarchy. else if (OldOwner != nullptr) { - const AActor* OldHierarchyRoot = OldOwner->GetOwner() != nullptr ? SpatialGDK::GetTopmostOwner(OldOwner) : OldOwner; + const AActor* OldHierarchyRoot = OldOwner->GetOwner() != nullptr ? SpatialGDK::GetTopmostReplicatedOwner(OldOwner) : OldOwner; if (IsLockedHierarchyRoot(OldHierarchyRoot)) { RecalculateAllExplicitlyLockedActorsInThisHierarchy(OldHierarchyRoot); } } - } +} void UOwnershipLockingPolicy::OnExplicitlyLockedActorDeleted(AActor* DestroyedActor) { @@ -221,7 +233,8 @@ void UOwnershipLockingPolicy::OnHierarchyRootActorDeleted(AActor* DeletedHierarc void UOwnershipLockingPolicy::RecalculateAllExplicitlyLockedActorsInThisHierarchy(const AActor* HierarchyRoot) { - TArray ExplicitlyLockedActorsWithThisActorInOwnershipPath = LockedOwnershipRootActorToExplicitlyLockedActors.FindChecked(HierarchyRoot); + TArray ExplicitlyLockedActorsWithThisActorInOwnershipPath = + LockedOwnershipRootActorToExplicitlyLockedActors.FindChecked(HierarchyRoot); for (const AActor* ExplicitlyLockedActor : ExplicitlyLockedActorsWithThisActorInOwnershipPath) { RecalculateLockedActorOwnershipHierarchyInformation(ExplicitlyLockedActor); @@ -235,7 +248,7 @@ void UOwnershipLockingPolicy::RecalculateLockedActorOwnershipHierarchyInformatio RemoveOwnershipHierarchyRootInformation(OldHierarchyRoot, ExplicitlyLockedActor); // For the new ownership path, update ownership path Actor mapping to explicitly locked Actors to include this Actor. - AActor* NewOwnershipHierarchyRoot = SpatialGDK::GetTopmostOwner(ExplicitlyLockedActor); + AActor* NewOwnershipHierarchyRoot = SpatialGDK::GetTopmostReplicatedOwner(ExplicitlyLockedActor); ActorToLockingState.FindChecked(ExplicitlyLockedActor).HierarchyRoot = NewOwnershipHierarchyRoot; AddOwnershipHierarchyRootInformation(NewOwnershipHierarchyRoot, ExplicitlyLockedActor); } @@ -248,7 +261,8 @@ void UOwnershipLockingPolicy::RemoveOwnershipHierarchyRootInformation(AActor* Hi } // Find Actors in this root Actor's hierarchy which are explicitly locked. - TArray& ExplicitlyLockedActorsWithThisActorOnPath = LockedOwnershipRootActorToExplicitlyLockedActors.FindChecked(HierarchyRoot); + 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, @@ -273,7 +287,8 @@ void UOwnershipLockingPolicy::AddOwnershipHierarchyRootInformation(AActor* Hiera // 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); + TArray& ExplicitlyLockedActorsWithThisActorOnPath = + LockedOwnershipRootActorToExplicitlyLockedActors.FindOrAdd(HierarchyRoot); ExplicitlyLockedActorsWithThisActorOnPath.AddUnique(ExplicitlyLockedActor); if (!HierarchyRoot->OnDestroyed.IsAlreadyBound(this, &UOwnershipLockingPolicy::OnHierarchyRootActorDeleted)) diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/SpatialMultiWorkerSettings.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/SpatialMultiWorkerSettings.cpp index c2bcca19b0..4a67726715 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/SpatialMultiWorkerSettings.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/SpatialMultiWorkerSettings.cpp @@ -2,11 +2,13 @@ #include "LoadBalancing/SpatialMultiWorkerSettings.h" +#include "EngineClasses/SpatialWorldSettings.h" #include "LoadBalancing/AbstractLBStrategy.h" #include "LoadBalancing/GridBasedLBStrategy.h" #include "LoadBalancing/LayeredLBStrategy.h" #include "LoadBalancing/OwnershipLockingPolicy.h" #include "Utils/LayerInfo.h" +#include "Utils/SpatialStatics.h" #include "Misc/MessageDialog.h" @@ -27,13 +29,28 @@ void UAbstractSpatialMultiWorkerSettings::PostEditChangeProperty(struct FPropert ValidateNoActorClassesDuplicatedAmongLayers(); ValidateAllLayersHaveUniqueNonemptyNames(); ValidateAllLayersHaveLoadBalancingStrategy(); + + EditorRefreshSpatialDebugger(); } else if (Name == GET_MEMBER_NAME_CHECKED(UAbstractSpatialMultiWorkerSettings, LockingPolicy)) { ValidateLockingPolicyIsSet(); } +} +void UAbstractSpatialMultiWorkerSettings::EditorRefreshSpatialDebugger() const +{ + const UWorld* World = GEditor->GetEditorWorldContext().World(); + check(World != nullptr); + + const TSubclassOf VisibleMultiWorkerSettingsClass = + USpatialStatics::GetSpatialMultiWorkerClass(World); + + if (VisibleMultiWorkerSettingsClass != nullptr && VisibleMultiWorkerSettingsClass == GetClass()) + { + ASpatialWorldSettings::EditorRefreshSpatialDebugger(); + } }; -#endif +#endif // WITH_EDITOR uint32 UAbstractSpatialMultiWorkerSettings::GetMinimumRequiredWorkerCount() const { @@ -57,12 +74,13 @@ void UAbstractSpatialMultiWorkerSettings::ValidateFirstLayerIsDefaultLayer() void UAbstractSpatialMultiWorkerSettings::ValidateNonEmptyWorkerLayers() { - if (WorkerLayers.Num() == 0 ) + if (WorkerLayers.Num() == 0) { WorkerLayers.Emplace(UAbstractSpatialMultiWorkerSettings::GetDefaultLayerInfo()); - FMessageDialog::Open(EAppMsgType::Ok, - FText::Format(LOCTEXT("EmptyWorkerLayer_Prompt", "You need at least one layer in your settings. " - "Adding back the default layer. File: {0}"), FText::FromString(GetNameSafe(this)))); + FMessageDialog::Open(EAppMsgType::Ok, FText::Format(LOCTEXT("EmptyWorkerLayer_Prompt", + "You need at least one layer in your settings. " + "Adding back the default layer. File: {0}"), + FText::FromString(GetNameSafe(this)))); } } @@ -77,9 +95,12 @@ void UAbstractSpatialMultiWorkerSettings::ValidateSomeLayerHasActorClass() if (!bHasTopLevelActorClass) { WorkerLayers[0].ActorClasses.Add(AActor::StaticClass()); - FMessageDialog::Open(EAppMsgType::Ok,FText::Format(LOCTEXT("MissingActorLayer_Prompt", - "Some worker layer must contain the root Actor class. Adding AActor to the first worker layer entry. " - "File: {0}"), FText::FromString(GetNameSafe(this)))); + FMessageDialog::Open( + EAppMsgType::Ok, + FText::Format(LOCTEXT("MissingActorLayer_Prompt", + "Some worker layer must contain the root Actor class. Adding AActor to the first worker layer entry. " + "File: {0}"), + FText::FromString(GetNameSafe(this)))); } } @@ -112,9 +133,12 @@ void UAbstractSpatialMultiWorkerSettings::ValidateNoActorClassesDuplicatedAmongL { DuplicatedActorsList.Append(FString::Printf(TEXT("%s, "), *DuplicatedClass.GetAssetName())); } - FMessageDialog::Open(EAppMsgType::Ok,FText::Format(LOCTEXT("MultipleActorLayers_Prompt", - "Defining the same Actor type across multiple layers is invalid. Removed all occurences after the first. " - "File: {0}. Duplicate Actor types: {1}"), FText::FromString(GetNameSafe(this)), FText::FromString(DuplicatedActorsList))); + FMessageDialog::Open( + EAppMsgType::Ok, + FText::Format(LOCTEXT("MultipleActorLayers_Prompt", + "Defining the same Actor type across multiple layers is invalid. Removed all occurences after the first. " + "File: {0}. Duplicate Actor types: {1}"), + FText::FromString(GetNameSafe(this)), FText::FromString(DuplicatedActorsList))); } } @@ -145,9 +169,10 @@ void UAbstractSpatialMultiWorkerSettings::ValidateAllLayersHaveUniqueNonemptyNam if (bSomeLayerNameWasChanged) { - FMessageDialog::Open(EAppMsgType::Ok, - FText::Format(LOCTEXT("BadLayerName_Prompt", "Found a worker layer with a duplicate name. " - "This has been fixed, please check your layers. File: {0}"), FText::FromString(GetNameSafe(this)))); + FMessageDialog::Open(EAppMsgType::Ok, FText::Format(LOCTEXT("BadLayerName_Prompt", + "Found a worker layer with a duplicate name. " + "This has been fixed, please check your layers. File: {0}"), + FText::FromString(GetNameSafe(this)))); } } @@ -166,9 +191,11 @@ void UAbstractSpatialMultiWorkerSettings::ValidateAllLayersHaveLoadBalancingStra if (bSomeLayerWasMissingStrategy) { - FMessageDialog::Open(EAppMsgType::Ok, FText::Format(LOCTEXT("UnsetLoadBalancingStrategy_Prompt", - "Found a worker layer with an unset load balancing strategy. Defaulting to a 1x1 grid. " - "File: {0}"), FText::FromString(GetNameSafe(this)))); + FMessageDialog::Open(EAppMsgType::Ok, + FText::Format(LOCTEXT("UnsetLoadBalancingStrategy_Prompt", + "Found a worker layer with an unset load balancing strategy. Defaulting to a 1x1 grid. " + "File: {0}"), + FText::FromString(GetNameSafe(this)))); } } @@ -177,9 +204,10 @@ void UAbstractSpatialMultiWorkerSettings::ValidateLockingPolicyIsSet() if (*LockingPolicy == nullptr) { LockingPolicy = UOwnershipLockingPolicy::StaticClass(); - FMessageDialog::Open(EAppMsgType::Ok, - FText::Format(LOCTEXT("UnsetLockingPolicy_Prompt", "Locking policy must be set. " - "Resetting to default policy. File: {0}"), FText::FromString(GetNameSafe(this)))); + FMessageDialog::Open(EAppMsgType::Ok, FText::Format(LOCTEXT("UnsetLockingPolicy_Prompt", + "Locking policy must be set. " + "Resetting to default policy. File: {0}"), + FText::FromString(GetNameSafe(this)))); } } #endif diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/WorkerRegion.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/WorkerRegion.cpp index 513bbce254..0413abf397 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/WorkerRegion.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/WorkerRegion.cpp @@ -2,19 +2,21 @@ #include "LoadBalancing/WorkerRegion.h" +#include "Engine/Canvas.h" +#include "Engine/StaticMesh.h" #include "Materials/MaterialInstanceDynamic.h" +#include "Runtime/Engine/Classes/Engine/CanvasRenderTarget2D.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"); -} +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 FName WORKER_TEXT_MATERIAL_TP2D_PARAM = TEXT("TP2D"); +const FString CUBE_MESH_PATH = TEXT("/Engine/BasicShapes/Cube.Cube"); +} // namespace AWorkerRegion::AWorkerRegion(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) @@ -25,15 +27,51 @@ AWorkerRegion::AWorkerRegion(const FObjectInitializer& ObjectInitializer) SetRootComponent(Mesh); } -void AWorkerRegion::Init(UMaterial* Material, const FColor& Color, const FBox2D& Extents, const float VerticalScale) +void AWorkerRegion::Init(UMaterial* BackgroundMaterial, UMaterial* InCombinedMaterial, UFont* InWorkerInfoFont, const FColor& Color, + const float Opacity, const FBox2D& Extents, const float Height, const float VerticalScale, + const FString& InWorkerInfo) { - SetHeight(DEFAULT_WORKER_REGION_HEIGHT); + // Background translucent coloured worker material + BackgroundMaterialInstance = UMaterialInstanceDynamic::Create(BackgroundMaterial, nullptr); + SetHeight(Height); + + // Setup the basic boundary material, this will always be shown in the editor + Mesh->SetMaterial(0, BackgroundMaterialInstance); + + // For runtime, initialise the canvas for creating the combined boundary material which will be rendered when the + // DrawToCanvasRenderTarget callback is triggered + CombinedMaterial = InCombinedMaterial; + WorkerInfoFont = InWorkerInfoFont; + WorkerInfo = InWorkerInfo; + CanvasRenderTarget = UCanvasRenderTarget2D::CreateCanvasRenderTarget2D(this, UCanvasRenderTarget2D::StaticClass(), 1024, 1024); + CanvasRenderTarget->OnCanvasRenderTargetUpdate.AddDynamic(this, &AWorkerRegion::DrawToCanvasRenderTarget); + // Setup the boundary material to combine background and text - needs to be created before SetOpacity + CombinedMaterialInstance = UMaterialInstanceDynamic::Create(CombinedMaterial, nullptr); - MaterialInstance = UMaterialInstanceDynamic::Create(Material, nullptr); - Mesh->SetMaterial(0, MaterialInstance); - SetOpacity(DEFAULT_WORKER_REGION_OPACITY); + SetOpacity(Opacity); SetColor(Color); SetPositionAndScale(Extents, VerticalScale); + + // At runtime, calls DrawToCanvasRenderTarget to render the dynamic boundary material, does not get triggered when we are in the editor + CanvasRenderTarget->UpdateResource(); +} + +// Render the dynamic boundary material with a translucent coloured background and worker information, note this callback is only triggered +// at runtime and not in the editor +void AWorkerRegion::DrawToCanvasRenderTarget(UCanvas* Canvas, int32 Width, int32 Height) +{ + // Set the boundary material that combines background and text + Mesh->SetMaterial(0, CombinedMaterialInstance); + + // Draw the worker background to the canvas + Canvas->K2_DrawMaterial(BackgroundMaterialInstance, FVector2D(0, 0), FVector2D(Width, Height), FVector2D(0, 0)); + + // Draw the worker information to the canvas + Canvas->SetDrawColor(FColor::White); + Canvas->DrawText(WorkerInfoFont, WorkerInfo, 100, 500, 1.0, 1.0); + + // Write the canvas data to the dynamic boundary material + CombinedMaterialInstance->SetTextureParameterValue(WORKER_TEXT_MATERIAL_TP2D_PARAM, CanvasRenderTarget); } void AWorkerRegion::SetHeight(const float Height) @@ -44,7 +82,8 @@ void AWorkerRegion::SetHeight(const float Height) void AWorkerRegion::SetOpacity(const float Opacity) { - MaterialInstance->SetScalarParameterValue(WORKER_REGION_MATERIAL_OPACITY_PARAM, Opacity); + BackgroundMaterialInstance->SetScalarParameterValue(WORKER_REGION_MATERIAL_OPACITY_PARAM, Opacity); + CombinedMaterialInstance->SetScalarParameterValue(WORKER_REGION_MATERIAL_OPACITY_PARAM, Opacity); } void AWorkerRegion::SetPositionAndScale(const FBox2D& Extents, const float VerticalScale) @@ -61,11 +100,11 @@ void AWorkerRegion::SetPositionAndScale(const FBox2D& Extents, const float Verti const float ScaleX = (MaxX - MinX) / 100; const float ScaleY = (MaxY - MinY) / 100; - SetActorLocation(FVector(CenterX, CenterY, CurrentLocation.Z)); + 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); + BackgroundMaterialInstance->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 index 29ab6fc3da..f49f60ae7e 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Schema/ClientEndpoint.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Schema/ClientEndpoint.cpp @@ -4,17 +4,26 @@ namespace SpatialGDK { - ClientEndpoint::ClientEndpoint(const Worker_ComponentData& Data) + : ClientEndpoint(Data.schema_type) +{ +} + +ClientEndpoint::ClientEndpoint(Schema_ComponentData* Data) : ReliableRPCBuffer(ERPCType::ServerReliable) , UnreliableRPCBuffer(ERPCType::ServerUnreliable) { - ReadFromSchema(Schema_GetComponentDataFields(Data.schema_type)); + ReadFromSchema(Schema_GetComponentDataFields(Data)); } void ClientEndpoint::ApplyComponentUpdate(const Worker_ComponentUpdate& Update) { - ReadFromSchema(Schema_GetComponentUpdateFields(Update.schema_type)); + ApplyComponentUpdate(Update.schema_type); +} + +void ClientEndpoint::ApplyComponentUpdate(Schema_ComponentUpdate* Update) +{ + ReadFromSchema(Schema_GetComponentUpdateFields(Update)); } void ClientEndpoint::ReadFromSchema(Schema_Object* SchemaObject) diff --git a/SpatialGDK/Source/SpatialGDK/Private/Schema/MulticastRPCs.cpp b/SpatialGDK/Source/SpatialGDK/Private/Schema/MulticastRPCs.cpp index b8d424fc93..bf5cfdb6b6 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Schema/MulticastRPCs.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Schema/MulticastRPCs.cpp @@ -4,16 +4,25 @@ namespace SpatialGDK { - MulticastRPCs::MulticastRPCs(const Worker_ComponentData& Data) + : MulticastRPCs(Data.schema_type) +{ +} + +MulticastRPCs::MulticastRPCs(Schema_ComponentData* Data) : MulticastRPCBuffer(ERPCType::NetMulticast) { - ReadFromSchema(Schema_GetComponentDataFields(Data.schema_type)); + ReadFromSchema(Schema_GetComponentDataFields(Data)); } void MulticastRPCs::ApplyComponentUpdate(const Worker_ComponentUpdate& Update) { - ReadFromSchema(Schema_GetComponentUpdateFields(Update.schema_type)); + ApplyComponentUpdate(Update.schema_type); +} + +void MulticastRPCs::ApplyComponentUpdate(Schema_ComponentUpdate* Update) +{ + ReadFromSchema(Schema_GetComponentUpdateFields(Update)); } void MulticastRPCs::ReadFromSchema(Schema_Object* SchemaObject) diff --git a/SpatialGDK/Source/SpatialGDK/Private/Schema/ServerEndpoint.cpp b/SpatialGDK/Source/SpatialGDK/Private/Schema/ServerEndpoint.cpp index fe0ceddd19..baa2113085 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Schema/ServerEndpoint.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Schema/ServerEndpoint.cpp @@ -4,17 +4,26 @@ namespace SpatialGDK { - ServerEndpoint::ServerEndpoint(const Worker_ComponentData& Data) + : ServerEndpoint(Data.schema_type) +{ +} + +ServerEndpoint::ServerEndpoint(Schema_ComponentData* Data) : ReliableRPCBuffer(ERPCType::ClientReliable) , UnreliableRPCBuffer(ERPCType::ClientUnreliable) { - ReadFromSchema(Schema_GetComponentDataFields(Data.schema_type)); + ReadFromSchema(Schema_GetComponentDataFields(Data)); } void ServerEndpoint::ApplyComponentUpdate(const Worker_ComponentUpdate& Update) { - ReadFromSchema(Schema_GetComponentUpdateFields(Update.schema_type)); + ApplyComponentUpdate(Update.schema_type); +} + +void ServerEndpoint::ApplyComponentUpdate(Schema_ComponentUpdate* Update) +{ + ReadFromSchema(Schema_GetComponentUpdateFields(Update)); } void ServerEndpoint::ReadFromSchema(Schema_Object* SchemaObject) diff --git a/SpatialGDK/Source/SpatialGDK/Private/Schema/UnrealObjectRef.cpp b/SpatialGDK/Source/SpatialGDK/Private/Schema/UnrealObjectRef.cpp index fb2375fe72..a9a6fb07f8 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Schema/UnrealObjectRef.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Schema/UnrealObjectRef.cpp @@ -54,12 +54,15 @@ UObject* FUnrealObjectRef::ToObjectPtr(const FUnrealObjectRef& ObjectRef, USpati } // 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. + // 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. FString FullPath; SpatialGDK::GetFullPathFromUnrealObjectReference(ObjectRef, FullPath); - UE_LOG(LogUnrealObjectRef, Warning, TEXT("Object ref did not map to valid object. Streaming level not loaded or actor deleted. Will be set to nullptr: %s %s"), - *ObjectRef.ToString(), FullPath.IsEmpty() ? TEXT("[NO PATH]") : *FullPath); + UE_LOG(LogUnrealObjectRef, Verbose, + TEXT("Object ref did not map to valid object. Streaming level not loaded or actor deleted. Will be set to nullptr: " + "%s %s"), + *ObjectRef.ToString(), FullPath.IsEmpty() ? TEXT("[NO PATH]") : *FullPath); + bOutUnresolved = true; } return Value; @@ -137,11 +140,13 @@ FUnrealObjectRef FUnrealObjectRef::FromObjectPtr(UObject* ObjectValue, USpatialP } } - // Check if the object is a newly referenced dynamic subobject, in which case we can create the object ref if we have the entity id of the parent actor. + // Check if the object is a newly referenced dynamic subobject, in which case we can create the object ref if we have the + // entity id of the parent actor. if (!ObjectValue->IsA()) { PackageMap->TryResolveNewDynamicSubobjectAndGetClassInfo(ObjectValue); - ObjectRef = PackageMap->GetUnrealObjectRefFromObject(ObjectValue); // This should now be valid, as we resolve the object in the line before + ObjectRef = PackageMap->GetUnrealObjectRefFromObject( + ObjectValue); // This should now be valid, as we resolve the object in the line before if (ObjectRef.IsValid()) { return ObjectRef; @@ -149,7 +154,8 @@ FUnrealObjectRef FUnrealObjectRef::FromObjectPtr(UObject* ObjectValue, USpatialP } // Unresolved object. - UE_LOG(LogUnrealObjectRef, Warning, TEXT("FUnrealObjectRef::FromObjectPtr: ObjectValue is unresolved! %s"), *ObjectValue->GetName()); + UE_LOG(LogUnrealObjectRef, Warning, TEXT("FUnrealObjectRef::FromObjectPtr: ObjectValue is unresolved! %s"), + *ObjectValue->GetName()); ObjectRef = FUnrealObjectRef::NULL_OBJECT_REF; } } @@ -212,10 +218,8 @@ bool FUnrealObjectRef::ShouldLoadObjectFromClassPath(UObject* Object) bool FUnrealObjectRef::IsUniqueActorClass(UClass* Class) { - return Class->IsChildOf() - || Class->IsChildOf() - || Class->IsChildOf() - || Class->IsChildOf(); + return Class->IsChildOf() || Class->IsChildOf() || Class->IsChildOf() + || Class->IsChildOf(); } FUnrealObjectRef FUnrealObjectRef::GetRefFromObjectClassPath(UObject* Object, USpatialPackageMapClient* PackageMap) diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKConsoleCommands.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKConsoleCommands.cpp index 089e8e2158..2ebe3ec9c7 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKConsoleCommands.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKConsoleCommands.cpp @@ -2,36 +2,35 @@ #include "SpatialGDKConsoleCommands.h" -#include "SpatialConstants.h" #include "Engine/Engine.h" +#include "SpatialConstants.h" DEFINE_LOG_CATEGORY(LogSpatialGDKConsoleCommands) namespace SpatialGDKConsoleCommands { - void ConsoleCommand_ConnectToLocator(const TArray& Args, UWorld* World) +void ConsoleCommand_ConnectToLocator(const TArray& Args, UWorld* World) +{ + if (Args.Num() != 2) { - 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); + UE_LOG(LogSpatialGDKConsoleCommands, Log, + TEXT("ConsoleCommand_ConnectToLocator takes 2 arguments (login, playerToken). Only %d given."), Args.Num()); + return; } - FAutoConsoleCommandWithWorldAndArgs ConnectToLocatorCommand = FAutoConsoleCommandWithWorldAndArgs( - TEXT("ConnectToLocator"), - TEXT("Usage: ConnectToLocator "), - FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&ConsoleCommand_ConnectToLocator) - ); + 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)); +} // namespace SpatialGDKConsoleCommands diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKModule.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKModule.cpp index 7cabaaeaf0..a0ab67f677 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKModule.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKModule.cpp @@ -8,12 +8,8 @@ DEFINE_LOG_CATEGORY(LogSpatialGDKModule); IMPLEMENT_MODULE(FSpatialGDKModule, SpatialGDK) -void FSpatialGDKModule::StartupModule() -{ -} +void FSpatialGDKModule::StartupModule() {} -void FSpatialGDKModule::ShutdownModule() -{ -} +void FSpatialGDKModule::ShutdownModule() {} #undef LOCTEXT_NAMESPACE diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp index 4d434bfeca..bc4e814106 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp @@ -3,11 +3,12 @@ #include "SpatialGDKSettings.h" #include "Improbable/SpatialEngineConstants.h" -#include "Misc/MessageDialog.h" #include "Misc/CommandLine.h" +#include "Misc/MessageDialog.h" #include "SpatialConstants.h" #include "Utils/GDKPropertyMacros.h" +#include "Utils/SpatialStatics.h" #if WITH_EDITOR #include "HAL/PlatformFilemanager.h" @@ -24,44 +25,60 @@ DEFINE_LOG_CATEGORY(LogSpatialGDKSettings); namespace { - void CheckCmdLineOverrideBool(const TCHAR* CommandLine, const TCHAR* Parameter, const TCHAR* PrettyName, bool& bOutValue) - { +constexpr int32 DefaultEventTracingFileSize = 256 * 1024 * 1024; // 256mb + +void CheckCmdLineOverrideBool(const TCHAR* CommandLine, const TCHAR* Parameter, const TCHAR* PrettyName, bool& bOutValue) +{ #if ALLOW_SPATIAL_CMDLINE_PARSING // Command-line only enabled for non-shipping or with target rule bEnableSpatialCmdlineInShipping enabled - if(FParse::Param(CommandLine, Parameter)) - { - bOutValue = true; - } - else + if (FParse::Param(CommandLine, Parameter)) + { + bOutValue = true; + } + else + { + TCHAR TempStr[16]; + if (FParse::Value(CommandLine, Parameter, TempStr, 16) && TempStr[0] == '=') { - TCHAR TempStr[16]; - if (FParse::Value(CommandLine, Parameter, TempStr, 16) && TempStr[0] == '=') - { - bOutValue = FCString::ToBool(TempStr + 1); // + 1 to skip = - } + bOutValue = FCString::ToBool(TempStr + 1); // + 1 to skip = } -#endif // ALLOW_SPATIAL_CMDLINE_PARSING - UE_LOG(LogSpatialGDKSettings, Log, TEXT("%s is %s."), PrettyName, bOutValue ? TEXT("enabled") : TEXT("disabled")); } +#endif // ALLOW_SPATIAL_CMDLINE_PARSING + UE_LOG(LogSpatialGDKSettings, Log, TEXT("%s is %s."), PrettyName, bOutValue ? TEXT("enabled") : TEXT("disabled")); +} - void CheckCmdLineOverrideOptionalBool(const TCHAR* CommandLine, const TCHAR* Parameter, const TCHAR* PrettyName, TOptional& bOutValue) - { +void CheckCmdLineOverrideOptionalBool(const TCHAR* CommandLine, const TCHAR* Parameter, const TCHAR* PrettyName, TOptional& bOutValue) +{ #if ALLOW_SPATIAL_CMDLINE_PARSING // Command-line only enabled for non-shipping or with target rule bEnableSpatialCmdlineInShipping enabled - if (FParse::Param(CommandLine, Parameter)) - { - bOutValue = true; - } - else + if (FParse::Param(CommandLine, Parameter)) + { + bOutValue = true; + } + else + { + TCHAR TempStr[16]; + if (FParse::Value(CommandLine, Parameter, TempStr, 16) && TempStr[0] == '=') { - TCHAR TempStr[16]; - if (FParse::Value(CommandLine, Parameter, TempStr, 16) && TempStr[0] == '=') - { - bOutValue = FCString::ToBool(TempStr + 1); // + 1 to skip = - } + bOutValue = FCString::ToBool(TempStr + 1); // + 1 to skip = } + } #endif // ALLOW_SPATIAL_CMDLINE_PARSING - UE_LOG(LogSpatialGDKSettings, Log, TEXT("%s is %s."), PrettyName, bOutValue.IsSet() ? bOutValue ? TEXT("enabled") : TEXT("disabled") : TEXT("not set")); + UE_LOG(LogSpatialGDKSettings, Log, TEXT("%s is %s."), PrettyName, + bOutValue.IsSet() ? bOutValue ? TEXT("enabled") : TEXT("disabled") : TEXT("not set")); +} + +void CheckCmdLineOverrideOptionalString(const TCHAR* CommandLine, const TCHAR* Parameter, const TCHAR* PrettyName, + TOptional& StrOutValue) +{ +#if ALLOW_SPATIAL_CMDLINE_PARSING + FString TempStr; + if (FParse::Value(CommandLine, Parameter, TempStr) && TempStr[0] == '=') + { + StrOutValue = TempStr.Right(TempStr.Len() - 1); // + 1 to skip = } +#endif // ALLOW_SPATIAL_CMDLINE_PARSING + UE_LOG(LogSpatialGDKSettings, Log, TEXT("%s is %s."), PrettyName, StrOutValue.IsSet() ? *(StrOutValue.GetValue()) : TEXT("not set")); } +} // namespace USpatialGDKSettings::USpatialGDKSettings(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) @@ -75,13 +92,14 @@ USpatialGDKSettings::USpatialGDKSettings(const FObjectInitializer& ObjectInitial , EntityCreationRateLimit(0) , bUseIsActorRelevantForConnection(false) , OpsUpdateRate(1000.0f) - , bEnableHandover(false) , MaxNetCullDistanceSquared(0.0f) // Default disabled , QueuedIncomingRPCWaitTime(1.0f) , QueuedIncomingRPCRetryTime(1.0f) , QueuedOutgoingRPCRetryTime(1.0f) - , PositionUpdateFrequency(1.0f) - , PositionDistanceThreshold(100.0f) // 1m (100cm) + , PositionUpdateLowerThresholdSeconds(1.0f) // 1 second + , PositionUpdateLowerThresholdCentimeters(100.0f) // 1m (100cm) + , PositionUpdateThresholdMaxSeconds(60.0f) // 1 minute (60 seconds) + , PositionUpdateThresholdMaxCentimeters(5000.0f) // 50m (5000cm) , bEnableMetrics(true) , bEnableMetricsDisplay(false) , MetricsReportRate(2.0f) @@ -89,16 +107,17 @@ USpatialGDKSettings::USpatialGDKSettings(const FObjectInitializer& ObjectInitial , bBatchSpatialPositionUpdates(false) , MaxDynamicallyAttachedSubobjectsPerClass(3) , ServicesRegion(EServicesRegion::Default) - , WorkerLogLevel(ESettingsWorkerLogVerbosity::Warning) - , bRunSpatialWorkerConnectionOnGameThread(false) - , bUseRPCRingBuffers(true) + , WorkerLogLevel(ESettingsWorkerLogVerbosity::Warning) // Deprecated - UNR-4348 + , LocalWorkerLogLevel(WorkerLogLevel) + , CloudWorkerLogLevel(WorkerLogLevel) + , bEnableMultiWorker(true) , DefaultRPCRingBufferSize(32) , MaxRPCRingBufferSize(32) // TODO - UNR 2514 - These defaults are not necessarily optimal - readdress when we have better data , bTcpNoDelay(false) , UdpServerDownstreamUpdateIntervalMS(1) , UdpClientDownstreamUpdateIntervalMS(1) - , bWorkerFlushAfterOutgoingNetworkOp(false) + , bWorkerFlushAfterOutgoingNetworkOp(true) // TODO - end , bAsyncLoadNewClassesOnEntityCheckout(false) , RPCQueueWarningDefaultTimeout(2.0f) @@ -108,8 +127,12 @@ USpatialGDKSettings::USpatialGDKSettings(const FObjectInitializer& ObjectInitial , bUseSecureClientConnection(false) , bUseSecureServerConnection(false) , bEnableClientQueriesOnServer(false) - , bUseSpatialView(false) - , bEnableMultiWorkerDebuggingWarnings(false) + , bEnableCrossLayerActorSpawning(true) + , StartupLogRate(5.0f) + , ActorMigrationLogRate(5.0f) + , bEventTracingEnabled(false) + , SamplingProbability(1.0f) + , MaxEventTracingFileSizeBytes(DefaultEventTracingFileSize) { DefaultReceptionistHost = SpatialConstants::LOCAL_HOST; } @@ -120,17 +143,24 @@ 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("OverrideHandover"), TEXT("Handover"), bEnableHandover); - CheckCmdLineOverrideOptionalBool(CommandLine, TEXT("OverrideMultiWorker"), TEXT("Multi-Worker"), bOverrideMultiWorker); - CheckCmdLineOverrideBool(CommandLine, TEXT("EnableMultiWorkerDebuggingWarnings"), TEXT("Multi-Worker Debugging Warnings"), bEnableMultiWorkerDebuggingWarnings); - CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideRPCRingBuffers"), TEXT("RPC ring buffers"), bUseRPCRingBuffers); - CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideSpatialWorkerConnectionOnGameThread"), TEXT("Spatial worker connection on game thread"), bRunSpatialWorkerConnectionOnGameThread); - 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); - CheckCmdLineOverrideBool(CommandLine, TEXT("OverridePreventClientCloudDeploymentAutoConnect"), TEXT("Prevent client cloud deployment auto connect"), bPreventClientCloudDeploymentAutoConnect); - CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideWorkerFlushAfterOutgoingNetworkOp"), TEXT("Flush worker ops after sending an outgoing network op."), bWorkerFlushAfterOutgoingNetworkOp); + CheckCmdLineOverrideBool(CommandLine, TEXT("EnableCrossLayerActorSpawning"), TEXT("Multiserver cross-layer Actor spawning"), + bEnableCrossLayerActorSpawning); + 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); + CheckCmdLineOverrideBool(CommandLine, TEXT("OverridePreventClientCloudDeploymentAutoConnect"), + TEXT("Prevent client cloud deployment auto connect"), bPreventClientCloudDeploymentAutoConnect); + CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideWorkerFlushAfterOutgoingNetworkOp"), + TEXT("Flush worker ops after sending an outgoing network op."), bWorkerFlushAfterOutgoingNetworkOp); + CheckCmdLineOverrideOptionalString(CommandLine, TEXT("OverrideMultiWorkerSettingsClass"), TEXT("Override MultiWorker Settings Class"), + OverrideMultiWorkerSettingsClass); + UE_LOG(LogSpatialGDKSettings, Log, TEXT("Spatial Networking is %s."), + USpatialStatics::IsSpatialNetworkingEnabled() ? TEXT("enabled") : TEXT("disabled")); } #if WITH_EDITOR @@ -145,9 +175,11 @@ void USpatialGDKSettings::PostEditChangeProperty(struct FPropertyChangedEvent& P if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, MaxDynamicallyAttachedSubobjectsPerClass)) { - FMessageDialog::Open(EAppMsgType::Ok, - LOCTEXT("RegenerateSchemaDynamicSubobjects_Prompt", "You MUST regenerate schema using the full scan option after changing the number of max dynamic subobjects. " - "Failing to do will result in unintended behavior or crashes!")); + FMessageDialog::Open( + EAppMsgType::Ok, + LOCTEXT("RegenerateSchemaDynamicSubobjects_Prompt", + "You MUST regenerate schema using the full scan option after changing the number of max dynamic subobjects. " + "Failing to do will result in unintended behavior or crashes!")); } else if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, ServicesRegion)) { @@ -155,7 +187,7 @@ void USpatialGDKSettings::PostEditChangeProperty(struct FPropertyChangedEvent& P } } -bool USpatialGDKSettings::CanEditChange(const GDK_PROPERTY(Property)* InProperty) const +bool USpatialGDKSettings::CanEditChange(const GDK_PROPERTY(Property) * InProperty) const { if (!InProperty) { @@ -164,20 +196,14 @@ bool USpatialGDKSettings::CanEditChange(const GDK_PROPERTY(Property)* InProperty const FName Name = InProperty->GetFName(); - 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; } void USpatialGDKSettings::UpdateServicesRegionFile() { // Create or remove an empty file in the plugin directory indicating whether to use China services region. - const FString UseChinaServicesRegionFilepath = FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(SpatialGDKServicesConstants::UseChinaServicesRegionFilename); + const FString UseChinaServicesRegionFilepath = + FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(SpatialGDKServicesConstants::UseChinaServicesRegionFilename); if (IsRunningInChina()) { if (!FPaths::FileExists(UseChinaServicesRegionFilepath)) @@ -206,13 +232,6 @@ uint32 USpatialGDKSettings::GetRPCRingBufferSize(ERPCType RPCType) const 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; -} - float USpatialGDKSettings::GetSecondsBeforeWarning(const ERPCResult Result) const { if (const float* CustomSecondsBeforeWarning = RPCQueueWarningTimeouts.Find(Result)) @@ -247,4 +266,11 @@ bool USpatialGDKSettings::GetPreventClientCloudDeploymentAutoConnect() const return (IsRunningGame() || IsRunningClientOnly()) && bPreventClientCloudDeploymentAutoConnect; }; +#if WITH_EDITOR +void USpatialGDKSettings::SetMultiWorkerEditorEnabled(bool bIsEnabled) +{ + bEnableMultiWorker = bIsEnabled; +} +#endif // WITH_EDITOR + #undef LOCTEXT_NAMESPACE diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/AuthorityRecord.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/AuthorityRecord.cpp index 495bf431da..3986d75739 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/AuthorityRecord.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/AuthorityRecord.cpp @@ -2,15 +2,14 @@ namespace SpatialGDK { - void AuthorityRecord::SetAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId, Worker_Authority Authority) { - const EntityComponentId Id = {EntityId, ComponentId}; + 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 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)) { @@ -28,9 +27,6 @@ void AuthorityRecord::SetAuthority(Worker_EntityId EntityId, Worker_ComponentId AuthorityGained.Push(Id); } break; - case WORKER_AUTHORITY_AUTHORITY_LOSS_IMMINENT: - // Deliberately ignore loss imminent. - break; } } diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/CommandRequest.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/CommandRequest.cpp index 2600fdaa2c..1cbe04ddb9 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/CommandRequest.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/CommandRequest.cpp @@ -2,7 +2,6 @@ namespace SpatialGDK { - CommandRequest::CommandRequest(Worker_ComponentId ComponentId, Worker_CommandIndex CommandIndex) : ComponentId(ComponentId) , CommandIndex(CommandIndex) @@ -17,7 +16,8 @@ CommandRequest::CommandRequest(OwningCommandRequestPtr Data, Worker_ComponentId { } -CommandRequest CommandRequest::CreateCopy(const Schema_CommandRequest* Data, Worker_ComponentId ComponentId, Worker_CommandIndex CommandIndex) +CommandRequest CommandRequest::CreateCopy(const Schema_CommandRequest* Data, Worker_ComponentId ComponentId, + Worker_CommandIndex CommandIndex) { return CommandRequest(OwningCommandRequestPtr(Schema_CopyCommandRequest(Data)), ComponentId, CommandIndex); } @@ -56,4 +56,4 @@ Worker_CommandIndex CommandRequest::GetCommandIndex() const return CommandIndex; } -} // SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/CommandResponse.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/CommandResponse.cpp index b91933b38b..3f97649fca 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/CommandResponse.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/CommandResponse.cpp @@ -2,7 +2,6 @@ namespace SpatialGDK { - CommandResponse::CommandResponse(Worker_ComponentId ComponentId, Worker_CommandIndex CommandIndex) : ComponentId(ComponentId) , CommandIndex(CommandIndex) @@ -17,7 +16,8 @@ CommandResponse::CommandResponse(OwningCommandResponsePtr Data, Worker_Component { } -CommandResponse CommandResponse::CreateCopy(const Schema_CommandResponse* Data, Worker_ComponentId ComponentId, Worker_CommandIndex CommandIndex) +CommandResponse CommandResponse::CreateCopy(const Schema_CommandResponse* Data, Worker_ComponentId ComponentId, + Worker_CommandIndex CommandIndex) { return CommandResponse(OwningCommandResponsePtr(Schema_CopyCommandResponse(Data)), ComponentId, CommandIndex); } @@ -56,4 +56,4 @@ Worker_CommandIndex CommandResponse::GetCommandIndex() const return CommandIndex; } -} // SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ComponentData.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ComponentData.cpp index 08c9e652ff..8728743970 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ComponentData.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ComponentData.cpp @@ -5,7 +5,6 @@ namespace SpatialGDK { - ComponentData::ComponentData(Worker_ComponentId Id) : ComponentId(Id) , Data(Schema_CreateComponentData()) @@ -58,7 +57,7 @@ Schema_ComponentData* ComponentData::GetUnderlying() const Worker_ComponentData ComponentData::GetWorkerComponentData() const { check(Data.IsValid()); - return {nullptr, ComponentId, Data.Get(), nullptr}; + return { nullptr, ComponentId, Data.Get(), nullptr }; } Worker_ComponentId ComponentData::GetComponentId() const diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ComponentUpdate.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ComponentUpdate.cpp index 62770cd998..e8e7425f38 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ComponentUpdate.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ComponentUpdate.cpp @@ -4,7 +4,6 @@ namespace SpatialGDK { - ComponentUpdate::ComponentUpdate(Worker_ComponentId Id) : ComponentId(Id) , Update(Schema_CreateComponentUpdate()) @@ -64,7 +63,7 @@ Schema_ComponentUpdate* ComponentUpdate::GetUnderlying() const Worker_ComponentUpdate ComponentUpdate::GetWorkerComponentUpdate() const { check(Update.IsValid()); - return {nullptr, ComponentId, Update.Get(), nullptr}; + return { nullptr, ComponentId, Update.Get(), nullptr }; } Worker_ComponentId ComponentUpdate::GetComponentId() const diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ConnectionHandler/InitialOpListConnectionHandler.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ConnectionHandler/InitialOpListConnectionHandler.cpp new file mode 100644 index 0000000000..97bdc9f4bd --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ConnectionHandler/InitialOpListConnectionHandler.cpp @@ -0,0 +1,103 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialView/ConnectionHandler/InitialOpListConnectionHandler.h" + +namespace SpatialGDK +{ +InitialOpListConnectionHandler::InitialOpListConnectionHandler(TUniquePtr InnerHandler, + TFunction OpExtractor) + : State(EXTRACTING_OPS) + , InnerHandler(MoveTemp(InnerHandler)) + , OpExtractor(MoveTemp(OpExtractor)) +{ +} + +void InitialOpListConnectionHandler::Advance() +{ + InnerHandler->Advance(); +} + +uint32 InitialOpListConnectionHandler::GetOpListCount() +{ + switch (State) + { + case EXTRACTING_OPS: + return 1; + case FLUSHING_QUEUED_OP_LISTS: + return QueuedOpLists.Num(); + case PASS_THROUGH: + return InnerHandler->GetOpListCount(); + default: + checkNoEntry(); + return 0; + } +} + +OpList InitialOpListConnectionHandler::GetNextOpList() +{ + switch (State) + { + case EXTRACTING_OPS: + return QueueAndExtractOps(); + case FLUSHING_QUEUED_OP_LISTS: + { + OpList Temp = MoveTemp(QueuedOpLists[0]); + QueuedOpLists.RemoveAt(0); + if (QueuedOpLists.Num() == 0) + { + State = PASS_THROUGH; + } + return Temp; + } + case PASS_THROUGH: + return InnerHandler->GetNextOpList(); + default: + checkNoEntry(); + return {}; + } +} + +void InitialOpListConnectionHandler::SendMessages(TUniquePtr Messages) +{ + InnerHandler->SendMessages(MoveTemp(Messages)); +} + +const FString& InitialOpListConnectionHandler::GetWorkerId() const +{ + return InnerHandler->GetWorkerId(); +} + +Worker_EntityId InitialOpListConnectionHandler::GetWorkerSystemEntityId() const +{ + return InnerHandler->GetWorkerSystemEntityId(); +} + +OpList InitialOpListConnectionHandler::QueueAndExtractOps() +{ + TUniquePtr ExtractedOpList = MakeUnique(); + + // Extract from an empty op list to ensure forward progress. + OpList EmptyOpList = {}; + if (OpExtractor(EmptyOpList, *ExtractedOpList)) + { + State = FLUSHING_QUEUED_OP_LISTS; + return OpList{ ExtractedOpList->ExtractedOps.GetData(), static_cast(ExtractedOpList->ExtractedOps.Num()), + MoveTemp(ExtractedOpList) }; + } + + // Extract and queue ops from the inner connection handler. + const uint32 OpListsAvailable = InnerHandler->GetOpListCount(); + for (uint32 i = 0; i < OpListsAvailable; ++i) + { + QueuedOpLists.Push(InnerHandler->GetNextOpList()); + if (OpExtractor(QueuedOpLists.Last(), *ExtractedOpList)) + { + State = FLUSHING_QUEUED_OP_LISTS; + break; + } + } + + return OpList{ ExtractedOpList->ExtractedOps.GetData(), static_cast(ExtractedOpList->ExtractedOps.Num()), + MoveTemp(ExtractedOpList) }; +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ConnectionHandler/SpatialOSConnectionHandler.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ConnectionHandler/SpatialOSConnectionHandler.cpp index 46ed09e631..1a0eaf1105 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ConnectionHandler/SpatialOSConnectionHandler.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ConnectionHandler/SpatialOSConnectionHandler.cpp @@ -2,23 +2,26 @@ #include "SpatialView/ConnectionHandler/SpatialOSConnectionHandler.h" +#include "Async/Async.h" +#include "Interop/Connection/SpatialEventTracer.h" +#include "SpatialView/OpList/WorkerConnectionOpList.h" + +#include "Async/Async.h" + +#include +#include + namespace SpatialGDK { - -SpatialOSConnectionHandler::SpatialOSConnectionHandler(Worker_Connection* Connection) - : Connection(Connection) - , WorkerId(UTF8_TO_TCHAR(Worker_Connection_GetWorkerId (Connection))) +SpatialOSConnectionHandler::SpatialOSConnectionHandler(Worker_Connection* Connection, TSharedPtr EventTracer) + : EventTracer(MoveTemp(EventTracer)) + , Connection(Connection) + , WorkerId(UTF8_TO_TCHAR(Worker_Connection_GetWorkerId(Connection))) + , WorkerSystemEntityId(Worker_Connection_GetWorkerEntityId(Connection)) { - const Worker_WorkerAttributes* Attributes = Worker_Connection_GetWorkerAttributes(Connection); - for (uint32 i = 0; i < Attributes->attribute_count; ++i) - { - WorkerAttributes.Push(FString(UTF8_TO_TCHAR(Attributes->attributes[i]))); - } } -void SpatialOSConnectionHandler::Advance() -{ -} +void SpatialOSConnectionHandler::Advance() {} uint32 SpatialOSConnectionHandler::GetOpListCount() { @@ -66,34 +69,28 @@ OpList SpatialOSConnectionHandler::GetNextOpList() void SpatialOSConnectionHandler::SendMessages(TUniquePtr Messages) { - const Worker_UpdateParameters UpdateParams = {0 /*loopback*/}; - const Worker_CommandParameters CommandParams = {0 /*allow_short_circuit*/}; + const Worker_UpdateParameters UpdateParams = { 0 /*loopback*/ }; + const Worker_CommandParameters CommandParams = { 0 /*allow_short_circuit*/ }; for (auto& Message : Messages->ComponentMessages) { + SpatialScopedActiveSpanId SpanWrapper(EventTracer.Get(), Message.SpanId); switch (Message.GetType()) { case OutgoingComponentMessage::ADD: { - Worker_ComponentData Data = { - nullptr, Message.ComponentId, - MoveTemp(Message).ReleaseComponentAdded().Release(), nullptr - }; + Worker_ComponentData Data = { nullptr, Message.ComponentId, MoveTemp(Message).ReleaseComponentAdded().Release(), nullptr }; Worker_Connection_SendAddComponent(Connection.Get(), Message.EntityId, &Data, &UpdateParams); break; } case OutgoingComponentMessage::UPDATE: { - Worker_ComponentUpdate Update = { - nullptr, Message.ComponentId, - MoveTemp(Message).ReleaseComponentUpdate().Release(), nullptr - }; + Worker_ComponentUpdate Update = { nullptr, Message.ComponentId, MoveTemp(Message).ReleaseComponentUpdate().Release(), nullptr }; Worker_Connection_SendComponentUpdate(Connection.Get(), Message.EntityId, &Update, &UpdateParams); break; } case OutgoingComponentMessage::REMOVE: { - Worker_Connection_SendRemoveComponent(Connection.Get(), - Message.EntityId, Message.ComponentId, &UpdateParams); + Worker_Connection_SendRemoveComponent(Connection.Get(), Message.EntityId, Message.ComponentId, &UpdateParams); break; } default: @@ -105,8 +102,7 @@ void SpatialOSConnectionHandler::SendMessages(TUniquePtr Message for (auto& Request : Messages->ReserveEntityIdsRequests) { const uint32* Timeout = Request.TimeoutMillis.IsSet() ? &Request.TimeoutMillis.GetValue() : nullptr; - const Worker_RequestId Id = Worker_Connection_SendReserveEntityIdsRequest(Connection.Get(), - Request.NumberOfEntityIds, Timeout); + const Worker_RequestId Id = Worker_Connection_SendReserveEntityIdsRequest(Connection.Get(), Request.NumberOfEntityIds, Timeout); InternalToUserRequestId.Emplace(Id, Request.RequestId); } @@ -116,23 +112,23 @@ void SpatialOSConnectionHandler::SendMessages(TUniquePtr Message Components.Reserve(Request.EntityComponents.Num()); for (ComponentData& Component : Request.EntityComponents) { - Components.Push(Worker_ComponentData{ - nullptr, Component.GetComponentId(), MoveTemp(Component).Release(), - nullptr - }); + Components.Push(Worker_ComponentData{ nullptr, Component.GetComponentId(), MoveTemp(Component).Release(), nullptr }); } + + SpatialScopedActiveSpanId SpanWrapper(EventTracer.Get(), Request.SpanId); + Worker_EntityId* EntityId = Request.EntityId.IsSet() ? &Request.EntityId.GetValue() : nullptr; const uint32* Timeout = Request.TimeoutMillis.IsSet() ? &Request.TimeoutMillis.GetValue() : nullptr; - const Worker_RequestId Id = Worker_Connection_SendCreateEntityRequest(Connection.Get(), Components.Num(), - Components.GetData(), EntityId, Timeout); + const Worker_RequestId Id = + Worker_Connection_SendCreateEntityRequest(Connection.Get(), Components.Num(), Components.GetData(), EntityId, Timeout); InternalToUserRequestId.Emplace(Id, Request.RequestId); } for (auto& Request : Messages->DeleteEntityRequests) { + SpatialScopedActiveSpanId SpanWrapper(EventTracer.Get(), Request.SpanId); const uint32* Timeout = Request.TimeoutMillis.IsSet() ? &Request.TimeoutMillis.GetValue() : nullptr; - const Worker_RequestId Id = Worker_Connection_SendDeleteEntityRequest(Connection.Get(), Request.EntityId, - Timeout); + const Worker_RequestId Id = Worker_Connection_SendDeleteEntityRequest(Connection.Get(), Request.EntityId, Timeout); InternalToUserRequestId.Emplace(Id, Request.RequestId); } @@ -146,27 +142,25 @@ void SpatialOSConnectionHandler::SendMessages(TUniquePtr Message for (auto& Request : Messages->EntityCommandRequests) { + SpatialScopedActiveSpanId SpanWrapper(EventTracer.Get(), Request.SpanId); const uint32* Timeout = Request.TimeoutMillis.IsSet() ? &Request.TimeoutMillis.GetValue() : nullptr; - Worker_CommandRequest r = { - nullptr, Request.Request.GetComponentId(), Request.Request.GetCommandIndex(), - MoveTemp(Request.Request).Release(), nullptr - }; - const Worker_RequestId Id = Worker_Connection_SendCommandRequest(Connection.Get(), Request.EntityId, &r, - Timeout, &CommandParams); + Worker_CommandRequest r = { nullptr, Request.Request.GetComponentId(), Request.Request.GetCommandIndex(), + MoveTemp(Request.Request).Release(), nullptr }; + const Worker_RequestId Id = Worker_Connection_SendCommandRequest(Connection.Get(), Request.EntityId, &r, Timeout, &CommandParams); InternalToUserRequestId.Emplace(Id, Request.RequestId); } for (auto& Response : Messages->EntityCommandResponses) { - Worker_CommandResponse r = { - nullptr, Response.Response.GetComponentId(), - Response.Response.GetCommandIndex(), MoveTemp(Response.Response).Release(), nullptr - }; + SpatialScopedActiveSpanId SpanWrapper(EventTracer.Get(), Response.SpanId); + Worker_CommandResponse r = { nullptr, Response.Response.GetComponentId(), Response.Response.GetCommandIndex(), + MoveTemp(Response.Response).Release(), nullptr }; Worker_Connection_SendCommandResponse(Connection.Get(), Response.RequestId, &r); } for (auto& Failure : Messages->EntityCommandFailures) { + SpatialScopedActiveSpanId SpanWrapper(EventTracer.Get(), Failure.SpanId); Worker_Connection_SendCommandFailure(Connection.Get(), Failure.RequestId, TCHAR_TO_UTF8(*Failure.Message)); } @@ -174,7 +168,7 @@ void SpatialOSConnectionHandler::SendMessages(TUniquePtr Message { FTCHARToUTF8 LoggerName(*Log.LoggerName.ToString()); FTCHARToUTF8 LogString(*Log.Message); - Worker_LogMessage L = {static_cast(Log.Level), LoggerName.Get(), LogString.Get()}; + Worker_LogMessage L = { static_cast(Log.Level), LoggerName.Get(), LogString.Get() }; Worker_Connection_SendLogMessage(Connection.Get(), &L); } @@ -182,6 +176,8 @@ void SpatialOSConnectionHandler::SendMessages(TUniquePtr Message { Metrics.SendToConnection(Connection.Get()); } + + Worker_Connection_Alpha_Flush(Connection.Get()); } const FString& SpatialOSConnectionHandler::GetWorkerId() const @@ -189,17 +185,24 @@ const FString& SpatialOSConnectionHandler::GetWorkerId() const return WorkerId; } -const TArray& SpatialOSConnectionHandler::GetWorkerAttributes() const +Worker_EntityId SpatialOSConnectionHandler::GetWorkerSystemEntityId() const { - return WorkerAttributes; + return WorkerSystemEntityId; } -void SpatialOSConnectionHandler::ConnectionDeleter::operator()(Worker_Connection* ConnectionToDelete) const noexcept +void SpatialOSConnectionHandler::WorkerConnectionDeleter::operator()(Worker_Connection* ConnectionToDestroy) const noexcept { - if (ConnectionToDelete != nullptr) - { - Worker_Connection_Destroy(ConnectionToDelete); - } + Worker_Connection_Destroy(ConnectionToDestroy); +} + +SpatialOSConnectionHandler::~SpatialOSConnectionHandler() +{ + // TODO: UNR-4211 - this is a mitigation for the slow connection destruction code in pie. + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, + [Connection = MoveTemp(Connection), EventTracer = MoveTemp(EventTracer)]() mutable { + Connection.Reset(nullptr); + EventTracer.Reset(); + }); } } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/CriticalSectionFilter.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/CriticalSectionFilter.cpp new file mode 100644 index 0000000000..06a1949502 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/CriticalSectionFilter.cpp @@ -0,0 +1,67 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialView/CriticalSectionFilter.h" + +#include "SpatialView/OpList/SplitOpList.h" + +namespace SpatialGDK +{ +void FCriticalSectionFilter::AddOpList(OpList Ops) +{ + // Work out which ops are in an open critical section by scanning backwards to the first critical sections op. + // Critical sections can't overlap so if the first op we find (the last one in the list) tells us the status of all received ops, + // + // If the first critical section op found is closed, we know that there are no open critical sections. + // In this case we expose all received ops as ready. + // + // If the first critical section op we find is open, we know that all subsequent ops are in an open critical section. + // In this case we split the op list around the open critical section start, set the first part to ready and queue the second. + for (uint32 i = Ops.Count; i > 0; --i) + { + Worker_Op& Op = Ops.Ops[i - 1]; + if (Op.op_type != WORKER_OP_TYPE_CRITICAL_SECTION) + { + continue; + } + + // There can only be one critical section open at a time. + // So any previous open critical section must now be closed. + for (OpList& OpenCriticalSection : OpenCriticalSectionOps) + { + ReadyOps.Add(MoveTemp(OpenCriticalSection)); + } + OpenCriticalSectionOps.Empty(); + + // If critical section op is opening the section then enqueue any ops before this point and store the open critical section. + if (Op.op.critical_section.in_critical_section) + { + SplitOpListPair SplitOpLists(MoveTemp(Ops), i); + ReadyOps.Add(MoveTemp(SplitOpLists.Head)); + OpenCriticalSectionOps.Add(MoveTemp(SplitOpLists.Tail)); + } + // If critical section op is closing the section then enqueue all ops. + else + { + ReadyOps.Add(MoveTemp(Ops)); + } + return; + } + + // If no critical section is present then either add this to existing open section ops if there are any or enqueue if not. + if (OpenCriticalSectionOps.Num()) + { + OpenCriticalSectionOps.Push(MoveTemp(Ops)); + } + else + { + ReadyOps.Push(MoveTemp(Ops)); + } +} + +TArray FCriticalSectionFilter::GetReadyOpLists() +{ + TArray Temp = MoveTemp(ReadyOps); + ReadyOps.Empty(); + return Temp; +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/Dispatcher.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/Dispatcher.cpp new file mode 100644 index 0000000000..a05dd8bd4a --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/Dispatcher.cpp @@ -0,0 +1,266 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialView/Dispatcher.h" + +#include "Algo/BinarySearch.h" +#include "SpatialView/EntityComponentTypes.h" + +namespace SpatialGDK +{ +FDispatcher::FDispatcher() + : NextCallbackId(1) +{ +} + +void FDispatcher::InvokeCallbacks(const TArray& Deltas) +{ + for (const EntityDelta& Delta : Deltas) + { + HandleComponentPresenceChanges(Delta.EntityId, Delta.ComponentsAdded, &FComponentCallbacks::ComponentAddedCallbacks); + HandleComponentPresenceChanges(Delta.EntityId, Delta.ComponentsRemoved, &FComponentCallbacks::ComponentRemovedCallbacks); + HandleComponentValueChanges(Delta.EntityId, Delta.ComponentUpdates); + HandleComponentValueChanges(Delta.EntityId, Delta.ComponentsRefreshed); + + HandleAuthorityChange(Delta.EntityId, Delta.AuthorityGained, &FAuthorityCallbacks::AuthorityGainedCallbacks); + HandleAuthorityChange(Delta.EntityId, Delta.AuthorityLost, &FAuthorityCallbacks::AuthorityLostCallbacks); + HandleAuthorityChange(Delta.EntityId, Delta.AuthorityLostTemporarily, &FAuthorityCallbacks::AuthorityLostTemporarilyCallbacks); + } +} + +CallbackId FDispatcher::RegisterAndInvokeComponentAddedCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback, + const EntityView& View) +{ + InvokeWithExistingValues(ComponentId, Callback, View); + return RegisterComponentAddedCallback(ComponentId, MoveTemp(Callback)); +} + +CallbackId FDispatcher::RegisterAndInvokeComponentRemovedCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback, + const EntityView& View) +{ + for (const auto& Entity : View) + { + if (!Entity.Value.Components.ContainsByPredicate(ComponentIdEquality{ ComponentId })) + { + Callback({ Entity.Key, ComponentChange(ComponentId) }); + } + } + return RegisterComponentRemovedCallback(ComponentId, MoveTemp(Callback)); +} + +CallbackId FDispatcher::RegisterAndInvokeComponentValueCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback, + const EntityView& View) +{ + InvokeWithExistingValues(ComponentId, Callback, View); + return RegisterComponentValueCallback(ComponentId, MoveTemp(Callback)); +} + +CallbackId FDispatcher::RegisterAndInvokeAuthorityGainedCallback(Worker_ComponentId ComponentId, FEntityCallback Callback, + const EntityView& View) +{ + for (const auto& Entity : View) + { + if (Entity.Value.Authority.Contains(ComponentId)) + { + Callback(Entity.Key); + } + } + return RegisterAuthorityGainedCallback(ComponentId, MoveTemp(Callback)); +} + +CallbackId FDispatcher::RegisterAndInvokeAuthorityLostCallback(Worker_ComponentId ComponentId, FEntityCallback Callback, + const EntityView& View) +{ + for (const auto& Entity : View) + { + if (!Entity.Value.Authority.Contains(ComponentId)) + { + Callback(Entity.Key); + } + } + return RegisterAuthorityLostCallback(ComponentId, MoveTemp(Callback)); +} + +CallbackId FDispatcher::RegisterComponentAddedCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback) +{ + const int32 Index = Algo::LowerBound(ComponentCallbacks, ComponentId, FComponentCallbacks::ComponentIdComparator()); + if (Index == ComponentCallbacks.Num() || ComponentCallbacks[Index].Id != ComponentId) + { + ComponentCallbacks.EmplaceAt(Index, ComponentId); + } + ComponentCallbacks[Index].ComponentAddedCallbacks.Register(NextCallbackId, MoveTemp(Callback)); + return NextCallbackId++; +} + +CallbackId FDispatcher::RegisterComponentRemovedCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback) +{ + const int32 Index = Algo::LowerBound(ComponentCallbacks, ComponentId, FComponentCallbacks::ComponentIdComparator()); + if (Index == ComponentCallbacks.Num() || ComponentCallbacks[Index].Id != ComponentId) + { + ComponentCallbacks.EmplaceAt(Index, ComponentId); + } + ComponentCallbacks[Index].ComponentRemovedCallbacks.Register(NextCallbackId, MoveTemp(Callback)); + return NextCallbackId++; +} + +CallbackId FDispatcher::RegisterComponentValueCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback) +{ + const int32 Index = Algo::LowerBound(ComponentCallbacks, ComponentId, FComponentCallbacks::ComponentIdComparator()); + if (Index == ComponentCallbacks.Num() || ComponentCallbacks[Index].Id != ComponentId) + { + ComponentCallbacks.EmplaceAt(Index, ComponentId); + } + ComponentCallbacks[Index].ComponentValueCallbacks.Register(NextCallbackId, MoveTemp(Callback)); + return NextCallbackId++; +} + +CallbackId FDispatcher::RegisterAuthorityGainedCallback(Worker_ComponentId ComponentId, FEntityCallback Callback) +{ + const int32 Index = Algo::LowerBound(AuthorityCallbacks, ComponentId, FAuthorityCallbacks::ComponentIdComparator()); + if (Index == AuthorityCallbacks.Num() || AuthorityCallbacks[Index].Id != ComponentId) + { + AuthorityCallbacks.EmplaceAt(Index, ComponentId); + } + AuthorityCallbacks[Index].AuthorityGainedCallbacks.Register(NextCallbackId, MoveTemp(Callback)); + return NextCallbackId++; +} + +CallbackId FDispatcher::RegisterAuthorityLostCallback(Worker_ComponentId ComponentId, FEntityCallback Callback) +{ + const int32 Index = Algo::LowerBound(AuthorityCallbacks, ComponentId, FAuthorityCallbacks::ComponentIdComparator()); + if (Index == AuthorityCallbacks.Num() || AuthorityCallbacks[Index].Id != ComponentId) + { + AuthorityCallbacks.EmplaceAt(Index, ComponentId); + } + AuthorityCallbacks[Index].AuthorityLostCallbacks.Register(NextCallbackId, MoveTemp(Callback)); + return NextCallbackId++; +} + +CallbackId FDispatcher::RegisterAuthorityLostTempCallback(Worker_ComponentId ComponentId, FEntityCallback Callback) +{ + const int32 Index = Algo::LowerBound(AuthorityCallbacks, ComponentId, FAuthorityCallbacks::ComponentIdComparator()); + if (Index == AuthorityCallbacks.Num() || AuthorityCallbacks[Index].Id != ComponentId) + { + AuthorityCallbacks.EmplaceAt(Index, ComponentId); + } + AuthorityCallbacks[Index].AuthorityLostTemporarilyCallbacks.Register(NextCallbackId, MoveTemp(Callback)); + return NextCallbackId++; +} + +void FDispatcher::RemoveCallback(CallbackId Id) +{ + for (FComponentCallbacks& Callback : ComponentCallbacks) + { + Callback.ComponentAddedCallbacks.Remove(Id); + Callback.ComponentRemovedCallbacks.Remove(Id); + } + + for (FAuthorityCallbacks& Callback : AuthorityCallbacks) + { + Callback.AuthorityGainedCallbacks.Remove(Id); + Callback.AuthorityLostCallbacks.Remove(Id); + Callback.AuthorityLostTemporarilyCallbacks.Remove(Id); + } +} + +void FDispatcher::InvokeWithExistingValues(Worker_ComponentId ComponentId, const FComponentValueCallback& Callback, const EntityView& View) +{ + for (const auto& Entity : View) + { + const ComponentData* It = Entity.Value.Components.FindByPredicate(ComponentIdEquality{ ComponentId }); + if (It != nullptr) + { + const ComponentChange Change(ComponentId, It->GetUnderlying()); + Callback({ Entity.Key, Change }); + } + } +} + +void FDispatcher::HandleComponentPresenceChanges(Worker_EntityId EntityId, const ComponentSpan& ComponentChanges, + TCallbacks FComponentCallbacks::*Callbacks) +{ + auto* CallbackIt = ComponentCallbacks.GetData(); + auto* ChangeIt = ComponentChanges.GetData(); + + const auto* CallbackEnd = ComponentCallbacks.GetData() + ComponentCallbacks.Num(); + const auto* ChangeEnd = ComponentChanges.GetData() + ComponentChanges.Num(); + + // Find the intersection between callbacks and changes and invoke all such callbacks. + while (CallbackIt != CallbackEnd && ChangeIt != ChangeEnd) + { + if (CallbackIt->Id < ChangeIt->ComponentId) + { + ++CallbackIt; + } + else if (ChangeIt->ComponentId < CallbackIt->Id) + { + ++ChangeIt; + } + else + { + const FEntityComponentChange EntityComponentChange = { EntityId, *ChangeIt }; + (CallbackIt->*Callbacks).Invoke(EntityComponentChange); + CallbackIt->ComponentValueCallbacks.Invoke(EntityComponentChange); + ++ChangeIt; + ++CallbackIt; + } + } +} + +void FDispatcher::HandleComponentValueChanges(Worker_EntityId EntityId, const ComponentSpan& ComponentChanges) +{ + auto* CallbackIt = ComponentCallbacks.GetData(); + auto* ChangeIt = ComponentChanges.GetData(); + + const auto* CallbackEnd = ComponentCallbacks.GetData() + ComponentCallbacks.Num(); + const auto* ChangeEnd = ComponentChanges.GetData() + ComponentChanges.Num(); + + // Find the intersection between callbacks and changes and invoke all such callbacks. + while (CallbackIt != CallbackEnd && ChangeIt != ChangeEnd) + { + if (CallbackIt->Id < ChangeIt->ComponentId) + { + ++CallbackIt; + } + else if (ChangeIt->ComponentId < CallbackIt->Id) + { + ++ChangeIt; + } + else + { + const FEntityComponentChange EntityComponentChange = { EntityId, *ChangeIt }; + CallbackIt->ComponentValueCallbacks.Invoke(EntityComponentChange); + ++ChangeIt; + ++CallbackIt; + } + } +} + +void FDispatcher::HandleAuthorityChange(Worker_EntityId EntityId, const ComponentSpan& AuthorityChanges, + TCallbacks FAuthorityCallbacks::*Callbacks) +{ + auto* CallbackIt = AuthorityCallbacks.GetData(); + auto* ChangeIt = AuthorityChanges.GetData(); + + const auto* CallbackEnd = AuthorityCallbacks.GetData() + AuthorityCallbacks.Num(); + const auto* ChangeEnd = AuthorityChanges.GetData() + AuthorityChanges.Num(); + + // Find the intersection between callbacks and changes and invoke all such callbacks. + while (CallbackIt != CallbackEnd && ChangeIt != ChangeEnd) + { + if (CallbackIt->Id < ChangeIt->ComponentId) + { + ++CallbackIt; + } + else if (ChangeIt->ComponentId < CallbackIt->Id) + { + ++ChangeIt; + } + else + { + (CallbackIt->*Callbacks).Invoke(EntityId); + ++ChangeIt; + ++CallbackIt; + } + } +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityComponentRecord.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityComponentRecord.cpp index 4063d64d96..71071e42b0 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityComponentRecord.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityComponentRecord.cpp @@ -4,7 +4,6 @@ namespace SpatialGDK { - void EntityComponentRecord::AddComponent(Worker_EntityId EntityId, ComponentData Data) { const EntityComponentId Id = { EntityId, Data.GetComponentId() }; diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityComponentUpdateRecord.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityComponentUpdateRecord.cpp index adee8f37ec..6ca62d686b 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityComponentUpdateRecord.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityComponentUpdateRecord.cpp @@ -4,11 +4,10 @@ namespace SpatialGDK { - void EntityComponentUpdateRecord::AddComponentDataAsUpdate(Worker_EntityId EntityId, ComponentData CompleteUpdate) { - const EntityComponentId Id = {EntityId, CompleteUpdate.GetComponentId()}; - EntityComponentUpdate* FoundUpdate = Updates.FindByPredicate(EntityComponentIdEquality{Id}); + const EntityComponentId Id = { EntityId, CompleteUpdate.GetComponentId() }; + EntityComponentUpdate* FoundUpdate = Updates.FindByPredicate(EntityComponentIdEquality{ Id }); if (FoundUpdate) { @@ -23,8 +22,8 @@ void EntityComponentUpdateRecord::AddComponentDataAsUpdate(Worker_EntityId Entit void EntityComponentUpdateRecord::AddComponentUpdate(Worker_EntityId EntityId, ComponentUpdate Update) { - const EntityComponentId Id = {EntityId, Update.GetComponentId()}; - EntityComponentCompleteUpdate* FoundCompleteUpdate = CompleteUpdates.FindByPredicate(EntityComponentIdEquality{Id}); + const EntityComponentId Id = { EntityId, Update.GetComponentId() }; + EntityComponentCompleteUpdate* FoundCompleteUpdate = CompleteUpdates.FindByPredicate(EntityComponentIdEquality{ Id }); if (FoundCompleteUpdate != nullptr) { @@ -39,9 +38,9 @@ void EntityComponentUpdateRecord::AddComponentUpdate(Worker_EntityId EntityId, C void EntityComponentUpdateRecord::RemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) { - const EntityComponentId Id = {EntityId, ComponentId}; + const EntityComponentId Id = { EntityId, ComponentId }; - const EntityComponentUpdate* FoundUpdate = Updates.FindByPredicate(EntityComponentIdEquality{Id}); + const EntityComponentUpdate* FoundUpdate = Updates.FindByPredicate(EntityComponentIdEquality{ Id }); if (FoundUpdate) { Updates.RemoveAtSwap(FoundUpdate - Updates.GetData()); @@ -49,7 +48,7 @@ void EntityComponentUpdateRecord::RemoveComponent(Worker_EntityId EntityId, Work // If the entity-component is recorded as updated, it can't also be completely-updated so we don't need to search for it. else { - const EntityComponentCompleteUpdate* FoundCompleteUpdate = CompleteUpdates.FindByPredicate(EntityComponentIdEquality{Id}); + const EntityComponentCompleteUpdate* FoundCompleteUpdate = CompleteUpdates.FindByPredicate(EntityComponentIdEquality{ Id }); if (FoundCompleteUpdate) { CompleteUpdates.RemoveAtSwap(FoundCompleteUpdate - CompleteUpdates.GetData()); @@ -75,8 +74,8 @@ const TArray& EntityComponentUpdateRecord::GetCom void EntityComponentUpdateRecord::InsertOrMergeUpdate(Worker_EntityId EntityId, ComponentUpdate Update) { - const EntityComponentId Id = {EntityId, Update.GetComponentId()}; - EntityComponentUpdate* FoundUpdate = Updates.FindByPredicate(EntityComponentIdEquality{Id}); + const EntityComponentId Id = { EntityId, Update.GetComponentId() }; + EntityComponentUpdate* FoundUpdate = Updates.FindByPredicate(EntityComponentIdEquality{ Id }); if (FoundUpdate != nullptr) { @@ -90,8 +89,8 @@ void EntityComponentUpdateRecord::InsertOrMergeUpdate(Worker_EntityId EntityId, void EntityComponentUpdateRecord::InsertOrSetCompleteUpdate(Worker_EntityId EntityId, ComponentData CompleteUpdate) { - const EntityComponentId Id = {EntityId, CompleteUpdate.GetComponentId()}; - EntityComponentCompleteUpdate* FoundCompleteUpdate = CompleteUpdates.FindByPredicate(EntityComponentIdEquality{Id}); + const EntityComponentId Id = { EntityId, CompleteUpdate.GetComponentId() }; + EntityComponentCompleteUpdate* FoundCompleteUpdate = CompleteUpdates.FindByPredicate(EntityComponentIdEquality{ Id }); if (FoundCompleteUpdate != nullptr) { diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityPresenceRecord.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityPresenceRecord.cpp index 430facba49..7f7913214c 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityPresenceRecord.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityPresenceRecord.cpp @@ -2,7 +2,6 @@ namespace SpatialGDK { - void EntityPresenceRecord::AddEntity(Worker_EntityId EntityId) { if (EntitiesRemoved.RemoveSingleSwap(EntityId) == 0) @@ -35,4 +34,4 @@ const TArray& EntityPresenceRecord::GetEntitiesRemoved() const return EntitiesRemoved; } -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityQuery.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityQuery.cpp index be416eb534..a5285b59b4 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityQuery.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityQuery.cpp @@ -1,29 +1,24 @@ -#include "SpatialView/EntityQuery.h" +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialView/EntityQuery.h" namespace SpatialGDK { - EntityQuery::EntityQuery(const Worker_EntityQuery& Query) - : ResultType(static_cast(Query.result_type)) { Constraints.Reserve(GetNestedConstraintCount(Query.constraint)); Constraints.Add(Query.constraint); StoreChildConstraints(Query.constraint, 0); - if (Query.result_type == WORKER_RESULT_TYPE_SNAPSHOT && Query.snapshot_result_type_component_ids) + if (Query.snapshot_result_type_component_ids) { SnapshotComponentIds.Reserve(Query.snapshot_result_type_component_id_count); - SnapshotComponentIds.Append(Query.snapshot_result_type_component_ids, Query.snapshot_result_type_component_id_count); + SnapshotComponentIds.Append(Query.snapshot_result_type_component_ids, Query.snapshot_result_type_component_id_count); } } Worker_EntityQuery EntityQuery::GetWorkerQuery() const { - return Worker_EntityQuery { - Constraints[0], - ResultType, - static_cast(SnapshotComponentIds.Num()), - ResultType == WORKER_RESULT_TYPE_SNAPSHOT ? SnapshotComponentIds.GetData() : nullptr - }; + return Worker_EntityQuery{ Constraints[0], static_cast(SnapshotComponentIds.Num()), SnapshotComponentIds.GetData() }; } int32 EntityQuery::GetNestedConstraintCount(const Worker_Constraint& Constraint) @@ -115,4 +110,4 @@ void EntityQuery::StoreChildConstraints(const Worker_Constraint& Constraint, int } } -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/OpList/EntityComponentOpList.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/OpList/EntityComponentOpList.cpp index 3d93fc28f3..2b39678506 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/OpList/EntityComponentOpList.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/OpList/EntityComponentOpList.cpp @@ -1,15 +1,37 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "SpatialView/OpList/EntityComponentOpList.h" +#include "SpatialView/EntityView.h" +#include "SpatialView/OpList/StringStorage.h" + namespace SpatialGDK { - EntityComponentOpListBuilder::EntityComponentOpListBuilder() : OpListData(MakeUnique()) { } +EntityComponentOpListBuilder& EntityComponentOpListBuilder::AddEntity(Worker_EntityId EntityId) +{ + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_ADD_ENTITY; + Op.op.add_entity.entity_id = EntityId; + + OpListData->Ops.Add(Op); + return *this; +} + +EntityComponentOpListBuilder& EntityComponentOpListBuilder::RemoveEntity(Worker_EntityId EntityId) +{ + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_REMOVE_ENTITY; + Op.op.remove_entity.entity_id = EntityId; + + OpListData->Ops.Add(Op); + return *this; +} + EntityComponentOpListBuilder& EntityComponentOpListBuilder::AddComponent(Worker_EntityId EntityId, ComponentData Data) { Worker_Op Op = {}; @@ -45,21 +67,139 @@ EntityComponentOpListBuilder& EntityComponentOpListBuilder::RemoveComponent(Work return *this; } -EntityComponentOpListBuilder& EntityComponentOpListBuilder::SetAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId, Worker_Authority Authority) +EntityComponentOpListBuilder& EntityComponentOpListBuilder::SetAuthority(Worker_EntityId EntityId, Worker_ComponentSetId ComponentSetId, + Worker_Authority Authority, TArray Components) { Worker_Op Op = {}; - Op.op_type = WORKER_OP_TYPE_AUTHORITY_CHANGE; - Op.op.authority_change.entity_id = EntityId; - Op.op.authority_change.component_id = ComponentId; - Op.op.authority_change.authority = Authority; + Op.op_type = WORKER_OP_TYPE_COMPONENT_SET_AUTHORITY_CHANGE; + Op.op.component_set_authority_change.entity_id = EntityId; + Op.op.component_set_authority_change.component_set_id = ComponentSetId; + Op.op.component_set_authority_change.authority = Authority; + Op.op.component_set_authority_change.canonical_component_set_data_count = Components.Num(); + Op.op.component_set_authority_change.canonical_component_set_data = StoreComponentDataArray(MoveTemp(Components)); OpListData->Ops.Add(Op); return *this; } +EntityComponentOpListBuilder& EntityComponentOpListBuilder::SetDisconnect(Worker_ConnectionStatusCode StatusCode, + StringStorage DisconnectReason) +{ + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_DISCONNECT; + Op.op.disconnect.connection_status_code = StatusCode; + Op.op.disconnect.reason = StoreString(MoveTemp(DisconnectReason)); + OpListData->Ops.Add(Op); + return *this; +} + +EntityComponentOpListBuilder& EntityComponentOpListBuilder::AddCreateEntityCommandResponse(Worker_EntityId EntityID, + Worker_RequestId RequestId, + Worker_StatusCode StatusCode, + StringStorage Message) +{ + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_CREATE_ENTITY_RESPONSE; + Op.op.create_entity_response.entity_id = EntityID; + Op.op.create_entity_response.request_id = RequestId; + Op.op.create_entity_response.status_code = StatusCode; + Op.op.create_entity_response.message = StoreString(MoveTemp(Message)); + OpListData->Ops.Add(Op); + return *this; +} + +EntityComponentOpListBuilder& EntityComponentOpListBuilder::AddReserveEntityIdsCommandResponse( + Worker_EntityId EntityID, uint32 NumberOfEntities, Worker_RequestId RequestId, Worker_StatusCode StatusCode, StringStorage Message) +{ + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_RESERVE_ENTITY_IDS_RESPONSE; + Op.op.reserve_entity_ids_response.first_entity_id = EntityID; + Op.op.reserve_entity_ids_response.number_of_entity_ids = NumberOfEntities; + Op.op.reserve_entity_ids_response.request_id = RequestId; + Op.op.reserve_entity_ids_response.status_code = StatusCode; + Op.op.reserve_entity_ids_response.message = StoreString(MoveTemp(Message)); + OpListData->Ops.Add(Op); + return *this; +} + +EntityComponentOpListBuilder& EntityComponentOpListBuilder::AddDeleteEntityCommandResponse(Worker_EntityId EntityID, + Worker_RequestId RequestId, + Worker_StatusCode StatusCode, + StringStorage Message) +{ + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_DELETE_ENTITY_RESPONSE; + Op.op.delete_entity_response.entity_id = EntityID; + Op.op.delete_entity_response.request_id = RequestId; + Op.op.delete_entity_response.status_code = StatusCode; + Op.op.delete_entity_response.message = StoreString(MoveTemp(Message)); + OpListData->Ops.Add(Op); + return *this; +} + +EntityComponentOpListBuilder& EntityComponentOpListBuilder::AddEntityQueryCommandResponse(Worker_RequestId RequestId, + TArray Results, + Worker_StatusCode StatusCode, + StringStorage Message) +{ + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_ENTITY_QUERY_RESPONSE; + Op.op.entity_query_response.result_count = Results.Num(); + Op.op.entity_query_response.results = StoreQueriedEntities(MoveTemp(Results)); + Op.op.entity_query_response.request_id = RequestId; + Op.op.entity_query_response.status_code = StatusCode; + Op.op.entity_query_response.message = StoreString(MoveTemp(Message)); + OpListData->Ops.Add(Op); + return *this; +} + +EntityComponentOpListBuilder& EntityComponentOpListBuilder::AddEntityCommandResponse(Worker_EntityId EntityID, Worker_RequestId RequestId, + Worker_StatusCode StatusCode, StringStorage Message) +{ + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_COMMAND_RESPONSE; + Op.op.command_response.entity_id = EntityID; + Op.op.command_response.request_id = RequestId; + Op.op.command_response.status_code = StatusCode; + Op.op.command_response.message = StoreString(MoveTemp(Message)); + OpListData->Ops.Add(Op); + return *this; +} + OpList EntityComponentOpListBuilder::CreateOpList() && { - return {OpListData->Ops.GetData(), static_cast(OpListData->Ops.Num()), MoveTemp(OpListData)}; + return { OpListData->Ops.GetData(), static_cast(OpListData->Ops.Num()), MoveTemp(OpListData) }; } +const char* EntityComponentOpListBuilder::StoreString(StringStorage Message) const +{ + OpListData->MessageStorage.Add(MoveTemp(Message)); + return OpListData->MessageStorage.Last().Get(); +} + +const Worker_Entity* EntityComponentOpListBuilder::StoreQueriedEntities(TArray Entities) const +{ + TArray WorkerEntities = OpListData->QueriedEntities.Add_GetRef(TArray()); + for (auto& Entity : Entities) + { + Worker_Entity CurrentEntity; + CurrentEntity.entity_id = Entity.EntityId; + CurrentEntity.component_count = Entity.Components.Num(); + CurrentEntity.components = StoreComponentDataArray(MoveTemp(Entity.Components)); + WorkerEntities.Add(MoveTemp(CurrentEntity)); + } + + return WorkerEntities.GetData(); +} + +const Worker_ComponentData* EntityComponentOpListBuilder::StoreComponentDataArray(TArray Components) const +{ + TArray& ComponentArray = OpListData->ComponentArrayStorage.Add_GetRef(TArray()); + for (ComponentData& Component : Components) + { + ComponentArray.Add(Component.GetWorkerComponentData()); + OpListData->DataStorage.Add(MoveTemp(Component)); + } + return ComponentArray.GetData(); +} } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/OpList/ViewDeltaLegacyOpList.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/OpList/ViewDeltaLegacyOpList.cpp index 75eb234093..f277924c68 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/OpList/ViewDeltaLegacyOpList.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/OpList/ViewDeltaLegacyOpList.cpp @@ -1,45 +1,14 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "SpatialView/OpList/ViewDeltaLegacyOpList.h" - -#include "Algo/StableSort.h" #include "Containers/StringConv.h" -namespace -{ - -Worker_EntityId GetEntityIdFromOp(const Worker_Op& Op) -{ - switch (static_cast(Op.op_type)) - { - case WORKER_OP_TYPE_ADD_ENTITY: - return Op.op.add_entity.entity_id; - case WORKER_OP_TYPE_REMOVE_ENTITY: - return Op.op.remove_entity.entity_id; - case WORKER_OP_TYPE_ADD_COMPONENT: - return Op.op.add_component.entity_id; - case WORKER_OP_TYPE_REMOVE_COMPONENT: - return Op.op.remove_component.entity_id; - case WORKER_OP_TYPE_AUTHORITY_CHANGE: - return Op.op.authority_change.entity_id; - case WORKER_OP_TYPE_COMPONENT_UPDATE: - return Op.op.component_update.entity_id; - default: - checkNoEntry(); - } - return 0; -} - -} // anonymous namespace - namespace SpatialGDK { - -OpList GetOpListFromViewDelta(ViewDelta Delta) +TArray GetOpsFromEntityDeltas(const TArray& Deltas) { - // The order of ops should be: - // Disconnect (we do not need to add further ops if disconnected). + // The order of ops per entity should be: // Add entities // Add components // Authority lost (from lost and lost temporarily) @@ -47,183 +16,116 @@ OpList GetOpListFromViewDelta(ViewDelta Delta) // Remove components // Authority gained (from gained and lost temporarily) // Entities Removed (can be reordered with authority gained) - // - // We can then order this by entity ID and surround the ops for each entity in a critical section. - // - // Worker messages can be placed anywhere. - - auto OpData = MakeUnique(); - - if (Delta.HasDisconnected()) - { - Worker_Op Op = {}; - Op.op_type = WORKER_OP_TYPE_DISCONNECT; - Op.op.disconnect.connection_status_code = Delta.GetConnectionStatus(); - - // Convert an FString to a char* that we can store. - const TCHAR* Reason = *Delta.GetDisconnectReason(); - int32 SourceLength = TCString::Strlen(Reason); - // Includes the null terminator. - int32 BufferSize = FTCHARToUTF8_Convert::ConvertedLength(Reason, SourceLength) + 1; - OpData->DisconnectReason = MakeUnique(BufferSize); - FTCHARToUTF8_Convert::Convert(OpData->DisconnectReason.Get(), BufferSize, Reason, SourceLength + 1); - - Op.op.disconnect.reason = OpData->DisconnectReason.Get(); - OpData->Ops.Push(Op); - - return {OpData->Ops.GetData(), static_cast(OpData->Ops.Num()), MoveTemp(OpData)}; - } TArray Ops; - for (const Worker_EntityId& Id : Delta.GetEntitiesAdded()) - { - Worker_Op Op = {}; - Op.op_type = WORKER_OP_TYPE_ADD_ENTITY; - Op.op.add_entity.entity_id = Id; - Ops.Push(Op); - } - - for (const EntityComponentData& Data : Delta.GetComponentsAdded()) - { - Worker_Op Op = {}; - Op.op_type = WORKER_OP_TYPE_ADD_COMPONENT; - Op.op.add_component.entity_id = Data.EntityId; - Op.op.add_component.data = Worker_ComponentData{ - nullptr, Data.Data.GetComponentId(), - Data.Data.GetUnderlying(), nullptr - }; - Ops.Push(Op); - } - - for (const EntityComponentId& Id : Delta.GetAuthorityLost()) + for (const EntityDelta& Entity : Deltas) { - 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; - Ops.Push(Op); - } + Worker_Op StartCriticalSection = {}; + StartCriticalSection.op_type = WORKER_OP_TYPE_CRITICAL_SECTION; + StartCriticalSection.op.critical_section.in_critical_section = 1; + Ops.Add(StartCriticalSection); - for (const EntityComponentId& Id : Delta.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; - Ops.Push(Op); - } - - for (const EntityComponentCompleteUpdate& Update : Delta.GetCompleteUpdates()) - { - // We deliberately ignore the events update here to avoid breaking code that expects each update to contain data. - Worker_Op AddOp = {}; - AddOp.op_type = WORKER_OP_TYPE_ADD_COMPONENT; - AddOp.op.add_component.entity_id = Update.EntityId; - AddOp.op.add_component.data = Worker_ComponentData{ - nullptr, Update.CompleteUpdate.GetComponentId(), - Update.CompleteUpdate.GetUnderlying(), nullptr - }; - Ops.Push(AddOp); - } + if (Entity.Type == EntityDelta::ADD) + { + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_ADD_ENTITY; + Op.op.add_entity.entity_id = Entity.EntityId; + Ops.Push(Op); + } - for (const EntityComponentUpdate& Update : Delta.GetUpdates()) - { - Worker_Op Op = {}; - Op.op_type = WORKER_OP_TYPE_COMPONENT_UPDATE; - Op.op.component_update.entity_id = Update.EntityId; - Op.op.component_update.update = Worker_ComponentUpdate{ - nullptr, Update.Update.GetComponentId(), - Update.Update.GetUnderlying(), nullptr - }; - Ops.Push(Op); - } + for (const ComponentChange& Change : Entity.ComponentsAdded) + { + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_ADD_COMPONENT; + Op.op.add_component.entity_id = Entity.EntityId; + Op.op.add_component.data = Worker_ComponentData{ nullptr, Change.ComponentId, Change.Data, nullptr }; + Ops.Push(Op); + } - for (const EntityComponentId& Id : Delta.GetComponentsRemoved()) - { - Worker_Op Op = {}; - Op.op_type = WORKER_OP_TYPE_REMOVE_COMPONENT; - Op.op.remove_component.entity_id = Id.EntityId; - Op.op.remove_component.component_id = Id.ComponentId; - Ops.Push(Op); - } + for (const AuthorityChange& Change : Entity.AuthorityLost) + { + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_COMPONENT_SET_AUTHORITY_CHANGE; + Op.op.component_set_authority_change.entity_id = Entity.EntityId; + Op.op.component_set_authority_change.component_set_id = Change.ComponentId; + Op.op.component_set_authority_change.authority = WORKER_AUTHORITY_NOT_AUTHORITATIVE; + Ops.Push(Op); + } - for (const EntityComponentId& Id : Delta.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; - Ops.Push(Op); - } + for (const AuthorityChange& Change : Entity.AuthorityLostTemporarily) + { + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_COMPONENT_SET_AUTHORITY_CHANGE; + Op.op.component_set_authority_change.entity_id = Entity.EntityId; + Op.op.component_set_authority_change.component_set_id = Change.ComponentId; + Op.op.component_set_authority_change.authority = WORKER_AUTHORITY_NOT_AUTHORITATIVE; + Ops.Push(Op); + } - for (const EntityComponentId& Id : Delta.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; - Ops.Push(Op); - } + for (const ComponentChange& Change : Entity.ComponentsRefreshed) + { + // We deliberately ignore the events update here to avoid breaking code that expects each update to contain data. + Worker_Op AddOp = {}; + AddOp.op_type = WORKER_OP_TYPE_ADD_COMPONENT; + AddOp.op.add_component.entity_id = Entity.EntityId; + AddOp.op.add_component.data = Worker_ComponentData{ nullptr, Change.ComponentId, Change.CompleteUpdate.Data, nullptr }; + Ops.Push(AddOp); + } - for (const Worker_EntityId& Id : Delta.GetEntitiesRemoved()) - { - Worker_Op Op = {}; - Op.op_type = WORKER_OP_TYPE_REMOVE_ENTITY; - Op.op.remove_entity.entity_id = Id; - Ops.Push(Op); - } + for (const ComponentChange& Change : Entity.ComponentUpdates) + { + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_COMPONENT_UPDATE; + Op.op.component_update.entity_id = Entity.EntityId; + Op.op.component_update.update = Worker_ComponentUpdate{ nullptr, Change.ComponentId, Change.Update, nullptr }; + Ops.Push(Op); + } - // Sort the entity ops by entity ID and surround each set of entity ops with a critical section. - Algo::StableSort(Ops, [](const Worker_Op& Lhs, const Worker_Op& Rhs) - { - return GetEntityIdFromOp(Lhs) < GetEntityIdFromOp(Rhs); - }); + for (const ComponentChange& Change : Entity.ComponentsRemoved) + { + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_REMOVE_COMPONENT; + Op.op.remove_component.entity_id = Entity.EntityId; + Op.op.remove_component.component_id = Change.ComponentId; + Ops.Push(Op); + } - OpData->Ops.Reserve(Ops.Num()); + for (const AuthorityChange& Change : Entity.AuthorityLostTemporarily) + { + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_COMPONENT_SET_AUTHORITY_CHANGE; + Op.op.component_set_authority_change.entity_id = Entity.EntityId; + Op.op.component_set_authority_change.component_set_id = Change.ComponentId; + Op.op.component_set_authority_change.authority = WORKER_AUTHORITY_AUTHORITATIVE; + Ops.Push(Op); + } - Worker_EntityId PreviousEntityId = 0; - for (const Worker_Op& Op : Ops) - { - const Worker_EntityId CurrentEntityId = GetEntityIdFromOp(Op); + for (const AuthorityChange& Change : Entity.AuthorityGained) + { + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_COMPONENT_SET_AUTHORITY_CHANGE; + Op.op.component_set_authority_change.entity_id = Entity.EntityId; + Op.op.component_set_authority_change.component_set_id = Change.ComponentId; + Op.op.component_set_authority_change.authority = WORKER_AUTHORITY_AUTHORITATIVE; + Ops.Push(Op); + } - if (CurrentEntityId > PreviousEntityId) + if (Entity.Type == EntityDelta::REMOVE) { - if (PreviousEntityId != 0) - { - Worker_Op EndCriticalSection = {}; - EndCriticalSection.op_type = WORKER_OP_TYPE_CRITICAL_SECTION; - EndCriticalSection.op.critical_section.in_critical_section = 0; - OpData->Ops.Add(EndCriticalSection); - } - - Worker_Op StartCriticalSection = {}; - StartCriticalSection.op_type = WORKER_OP_TYPE_CRITICAL_SECTION; - StartCriticalSection.op.critical_section.in_critical_section = 1; - OpData->Ops.Add(StartCriticalSection); - - PreviousEntityId = CurrentEntityId; + Worker_Op Op = {}; + Op.op_type = WORKER_OP_TYPE_REMOVE_ENTITY; + Op.op.remove_entity.entity_id = Entity.EntityId; + Ops.Push(Op); } - OpData->Ops.Add(Op); - } - if (PreviousEntityId > 0) - { Worker_Op EndCriticalSection = {}; EndCriticalSection.op_type = WORKER_OP_TYPE_CRITICAL_SECTION; EndCriticalSection.op.critical_section.in_critical_section = 0; - OpData->Ops.Add(EndCriticalSection); + Ops.Add(EndCriticalSection); } - // Worker messages do not have ordering constraints so can just go at the end. - OpData->Ops.Append(Delta.GetWorkerMessages()); - - OpData->Delta = MoveTemp(Delta); - return {OpData->Ops.GetData(), static_cast(OpData->Ops.Num()), MoveTemp(OpData)}; + return Ops; } } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ReceivedOpEventHandler.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ReceivedOpEventHandler.cpp new file mode 100644 index 0000000000..0d9d17de2b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ReceivedOpEventHandler.cpp @@ -0,0 +1,56 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialView/ReceivedOpEventHandler.h" + +#include "Interop/Connection/SpatialTraceEventBuilder.h" + +namespace SpatialGDK +{ +FReceivedOpEventHandler::FReceivedOpEventHandler(TSharedPtr EventTracer) + : EventTracer(MoveTemp(EventTracer)) +{ +} + +void FReceivedOpEventHandler::ProcessOpLists(const OpList& Ops) +{ + if (EventTracer == nullptr) + { + return; + } + + for (uint32 i = 0; i < Ops.Count; ++i) + { + Worker_Op& Op = Ops.Ops[i]; + + switch (static_cast(Op.op_type)) + { + case WORKER_OP_TYPE_ADD_ENTITY: + EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateReceiveCreateEntity(Op.op.add_entity.entity_id), Op.span_id, 1); + break; + case WORKER_OP_TYPE_REMOVE_ENTITY: + EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateReceiveRemoveEntity(Op.op.remove_entity.entity_id), Op.span_id, 1); + break; + case WORKER_OP_TYPE_ADD_COMPONENT: + EventTracer->AddComponent(Op.op.add_component.entity_id, Op.op.add_component.data.component_id, FSpatialGDKSpanId(Op.span_id)); + break; + case WORKER_OP_TYPE_REMOVE_COMPONENT: + EventTracer->RemoveComponent(Op.op.remove_component.entity_id, Op.op.remove_component.component_id); + break; + case WORKER_OP_TYPE_COMPONENT_SET_AUTHORITY_CHANGE: + EventTracer->TraceEvent( + FSpatialTraceEventBuilder::CreateAuthorityChange( + Op.op.component_set_authority_change.entity_id, Op.op.component_set_authority_change.component_set_id, + static_cast(Op.op.component_set_authority_change.authority)), + Op.span_id, 1); + break; + case WORKER_OP_TYPE_COMPONENT_UPDATE: + EventTracer->UpdateComponent(Op.op.component_update.entity_id, Op.op.component_update.update.component_id, + FSpatialGDKSpanId(Op.span_id)); + break; + default: + break; + } + } +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/SubView.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/SubView.cpp new file mode 100644 index 0000000000..09d69c6405 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/SubView.cpp @@ -0,0 +1,217 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialView/SubView.h" + +#include "SpatialView/EntityComponentTypes.h" +#include "Utils/ComponentFactory.h" + +namespace SpatialGDK +{ +const FFilterPredicate FSubView::NoFilter = [](const Worker_EntityId&, const EntityViewElement&) { + return true; +}; +const TArray FSubView::NoDispatcherCallbacks = TArray{}; +const FComponentChangeRefreshPredicate FSubView::NoComponentChangeRefreshPredicate = [](const FEntityComponentChange&) { + return true; +}; +const FAuthorityChangeRefreshPredicate FSubView::NoAuthorityChangeRefreshPredicate = [](const Worker_EntityId) { + return true; +}; + +FSubView::FSubView(const Worker_ComponentId InTagComponentId, FFilterPredicate InFilter, const EntityView* InView, FDispatcher& Dispatcher, + const TArray& DispatcherRefreshCallbacks) + : TagComponentId(InTagComponentId) + , Filter(MoveTemp(InFilter)) + , View(InView) +{ + RegisterTagCallbacks(Dispatcher); + RegisterRefreshCallbacks(DispatcherRefreshCallbacks); +} + +void FSubView::Advance(const ViewDelta& Delta) +{ + // Note: Complete entities will be a longer list than the others for the majority of iterations under + // probable normal usage. This sort could then become expensive, and a potential optimisation would be + // to maintain the ordering of complete entities when merging in the newly complete entities and enforcing + // that complete entities is always sorted. This would also need to be enforced in the temporarily incomplete case. + // If this sort shows up in a profile it would be worth trying. + Algo::Sort(CompleteEntities); + Algo::Sort(NewlyCompleteEntities); + Algo::Sort(NewlyIncompleteEntities); + Algo::Sort(TemporarilyIncompleteEntities); + + Delta.Project(SubViewDelta, CompleteEntities, NewlyCompleteEntities, NewlyIncompleteEntities, TemporarilyIncompleteEntities); + + CompleteEntities.Append(NewlyCompleteEntities); + NewlyCompleteEntities.Empty(); + NewlyIncompleteEntities.Empty(); + TemporarilyIncompleteEntities.Empty(); +} + +const FSubViewDelta& FSubView::GetViewDelta() const +{ + return SubViewDelta; +} + +void FSubView::RefreshEntity(const Worker_EntityId EntityId) +{ + if (TaggedEntities.Contains(EntityId)) + { + CheckEntityAgainstFilter(EntityId); + } +} + +const EntityView& FSubView::GetView() const +{ + return *View; +} + +bool FSubView::HasComponent(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) const +{ + const EntityViewElement* Entity = View->Find(EntityId); + if (Entity == nullptr) + { + return false; + } + return Entity->Components.ContainsByPredicate(ComponentIdEquality{ ComponentId }); +} + +bool FSubView::HasAuthority(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) const +{ + const EntityViewElement* Entity = View->Find(EntityId); + if (Entity == nullptr) + { + return false; + } + return Entity->Authority.Contains(ComponentId); +} + +FDispatcherRefreshCallback FSubView::CreateComponentExistenceRefreshCallback(FDispatcher& Dispatcher, const Worker_ComponentId ComponentId, + const FComponentChangeRefreshPredicate& RefreshPredicate) +{ + return [ComponentId, &Dispatcher, RefreshPredicate](const FRefreshCallback& Callback) { + Dispatcher.RegisterComponentAddedCallback(ComponentId, [RefreshPredicate, Callback](const FEntityComponentChange& Change) { + if (RefreshPredicate(Change)) + { + Callback(Change.EntityId); + } + }); + Dispatcher.RegisterComponentRemovedCallback(ComponentId, [RefreshPredicate, Callback](const FEntityComponentChange& Change) { + if (RefreshPredicate(Change)) + { + Callback(Change.EntityId); + } + }); + }; +} + +FDispatcherRefreshCallback FSubView::CreateComponentChangedRefreshCallback(FDispatcher& Dispatcher, const Worker_ComponentId ComponentId, + const FComponentChangeRefreshPredicate& RefreshPredicate) +{ + return [ComponentId, &Dispatcher, RefreshPredicate](const FRefreshCallback& Callback) { + Dispatcher.RegisterComponentValueCallback(ComponentId, [RefreshPredicate, Callback](const FEntityComponentChange& Change) { + if (RefreshPredicate(Change)) + { + Callback(Change.EntityId); + } + }); + }; +} + +FDispatcherRefreshCallback FSubView::CreateAuthorityChangeRefreshCallback(FDispatcher& Dispatcher, const Worker_ComponentId ComponentId, + const FAuthorityChangeRefreshPredicate& RefreshPredicate) +{ + return [ComponentId, &Dispatcher, RefreshPredicate](const FRefreshCallback& Callback) { + Dispatcher.RegisterAuthorityGainedCallback(ComponentId, [RefreshPredicate, Callback](const Worker_EntityId Id) { + if (RefreshPredicate(Id)) + { + Callback(Id); + } + }); + Dispatcher.RegisterAuthorityLostCallback(ComponentId, [RefreshPredicate, Callback](const Worker_EntityId Id) { + if (RefreshPredicate(Id)) + { + Callback(Id); + } + }); + }; +} + +void FSubView::RegisterTagCallbacks(FDispatcher& Dispatcher) +{ + Dispatcher.RegisterAndInvokeComponentAddedCallback( + TagComponentId, + [this](const FEntityComponentChange& Change) { + OnTaggedEntityAdded(Change.EntityId); + }, + *View); + + Dispatcher.RegisterComponentRemovedCallback(TagComponentId, [this](const FEntityComponentChange& Change) { + OnTaggedEntityRemoved(Change.EntityId); + }); +} + +void FSubView::RegisterRefreshCallbacks(const TArray& DispatcherRefreshCallbacks) +{ + const FRefreshCallback RefreshEntityCallback = [this](const Worker_EntityId EntityId) { + RefreshEntity(EntityId); + }; + for (FDispatcherRefreshCallback Callback : DispatcherRefreshCallbacks) + { + Callback(RefreshEntityCallback); + } +} + +void FSubView::OnTaggedEntityAdded(const Worker_EntityId EntityId) +{ + TaggedEntities.Add(EntityId); + CheckEntityAgainstFilter(EntityId); +} + +void FSubView::OnTaggedEntityRemoved(const Worker_EntityId EntityId) +{ + TaggedEntities.RemoveSingleSwap(EntityId); + EntityIncomplete(EntityId); +} + +void FSubView::CheckEntityAgainstFilter(const Worker_EntityId EntityId) +{ + if (Filter(EntityId, (*View)[EntityId])) + { + EntityComplete(EntityId); + return; + } + EntityIncomplete(EntityId); +} + +void FSubView::EntityComplete(const Worker_EntityId EntityId) +{ + // We were just about to remove this entity, but it has become complete again before the delta was read. + // Mark it as temporarily incomplete, but otherwise treat it as if it hadn't gone incomplete. + if (NewlyIncompleteEntities.RemoveSingleSwap(EntityId)) + { + CompleteEntities.Add(EntityId); + TemporarilyIncompleteEntities.Add(EntityId); + return; + } + // This is new to us. Mark it as newly complete. + if (!NewlyCompleteEntities.Contains(EntityId) && !CompleteEntities.Contains(EntityId)) + { + NewlyCompleteEntities.Add(EntityId); + } +} + +void FSubView::EntityIncomplete(const Worker_EntityId EntityId) +{ + // If we were about to add this, don't. It's as if we never saw it. + if (NewlyCompleteEntities.RemoveSingleSwap(EntityId)) + { + return; + } + // Otherwise, if it is currently complete, we need to remove it, and mark it as about to remove. + if (CompleteEntities.RemoveSingleSwap(EntityId)) + { + NewlyIncompleteEntities.Add(EntityId); + } +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewCoordinator.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewCoordinator.cpp index e95d313c8d..48f183c052 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewCoordinator.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewCoordinator.cpp @@ -1,13 +1,17 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "SpatialView/ViewCoordinator.h" + #include "SpatialView/OpList/ViewDeltaLegacyOpList.h" namespace SpatialGDK { - -ViewCoordinator::ViewCoordinator(TUniquePtr ConnectionHandler) - : ConnectionHandler(MoveTemp(ConnectionHandler)), NextRequestId(1) +ViewCoordinator::ViewCoordinator(TUniquePtr ConnectionHandler, TSharedPtr EventTracer, + FComponentSetData ComponentSetData) + : View(MoveTemp(ComponentSetData)) + , ConnectionHandler(MoveTemp(ConnectionHandler)) + , NextRequestId(1) + , ReceivedOpEventHandler(MoveTemp(EventTracer)) { } @@ -16,15 +20,48 @@ ViewCoordinator::~ViewCoordinator() FlushMessagesToSend(); } -OpList ViewCoordinator::Advance() +void ViewCoordinator::Advance(float DeltaTimeS) { + // Get new op lists. ConnectionHandler->Advance(); const uint32 OpListCount = ConnectionHandler->GetOpListCount(); + + // Hold back open critical sections. for (uint32 i = 0; i < OpListCount; ++i) { - View.EnqueueOpList(ConnectionHandler->GetNextOpList()); + OpList Ops = ConnectionHandler->GetNextOpList(); + ReserveEntityIdRetryHandler.ProcessOps(DeltaTimeS, Ops, View); + CreateEntityRetryHandler.ProcessOps(DeltaTimeS, Ops, View); + DeleteEntityRetryHandler.ProcessOps(DeltaTimeS, Ops, View); + EntityQueryRetryHandler.ProcessOps(DeltaTimeS, Ops, View); + EntityCommandRetryHandler.ProcessOps(DeltaTimeS, Ops, View); + CriticalSectionFilter.AddOpList(MoveTemp(Ops)); + } + + // Process ops. + TArray OpLists = CriticalSectionFilter.GetReadyOpLists(); + for (const OpList& Ops : OpLists) + { + ReceivedOpEventHandler.ProcessOpLists(Ops); + } + View.AdvanceViewDelta(MoveTemp(OpLists)); + + // Process the view delta. + Dispatcher.InvokeCallbacks(View.GetViewDelta().GetEntityDeltas()); + for (const TUniquePtr& SubviewToAdvance : SubViews) + { + SubviewToAdvance->Advance(View.GetViewDelta()); } - return GetOpListFromViewDelta(View.GenerateViewDelta()); +} + +const ViewDelta& ViewCoordinator::GetViewDelta() const +{ + return View.GetViewDelta(); +} + +const EntityView& ViewCoordinator::GetView() const +{ + return View.GetView(); } void ViewCoordinator::FlushMessagesToSend() @@ -32,61 +69,77 @@ void ViewCoordinator::FlushMessagesToSend() ConnectionHandler->SendMessages(View.FlushLocalChanges()); } -void ViewCoordinator::SendAddComponent(Worker_EntityId EntityId, ComponentData Data) +FSubView& ViewCoordinator::CreateSubView(Worker_ComponentId Tag, const FFilterPredicate& Filter, + const TArray& DispatcherRefreshCallbacks) { - View.SendAddComponent(EntityId, MoveTemp(Data)); + const int Index = SubViews.Emplace(MakeUnique(Tag, Filter, &View.GetView(), Dispatcher, DispatcherRefreshCallbacks)); + return *SubViews[Index]; } -void ViewCoordinator::SendComponentUpdate(Worker_EntityId EntityId, ComponentUpdate Update) +void ViewCoordinator::RefreshEntityCompleteness(Worker_EntityId EntityId) { - View.SendComponentUpdate(EntityId, MoveTemp(Update)); + for (const TUniquePtr& SubviewToRefresh : SubViews) + { + SubviewToRefresh->RefreshEntity(EntityId); + } } -void ViewCoordinator::SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) +void ViewCoordinator::SendAddComponent(Worker_EntityId EntityId, ComponentData Data, const FSpatialGDKSpanId& SpanId) { - View.SendRemoveComponent(EntityId, ComponentId); + View.SendAddComponent(EntityId, MoveTemp(Data), SpanId); +} + +void ViewCoordinator::SendComponentUpdate(Worker_EntityId EntityId, ComponentUpdate Update, const FSpatialGDKSpanId& SpanId) +{ + View.SendComponentUpdate(EntityId, MoveTemp(Update), SpanId); +} + +void ViewCoordinator::SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, const FSpatialGDKSpanId& SpanId) +{ + View.SendRemoveComponent(EntityId, ComponentId, SpanId); } Worker_RequestId ViewCoordinator::SendReserveEntityIdsRequest(uint32 NumberOfEntityIds, TOptional TimeoutMillis) { - View.SendReserveEntityIdsRequest({NextRequestId, NumberOfEntityIds, TimeoutMillis}); + View.SendReserveEntityIdsRequest({ NextRequestId, NumberOfEntityIds, TimeoutMillis }); return NextRequestId++; } -Worker_RequestId ViewCoordinator::SendCreateEntityRequest(TArray EntityComponents, - TOptional EntityId, TOptional TimeoutMillis) +Worker_RequestId ViewCoordinator::SendCreateEntityRequest(TArray EntityComponents, TOptional EntityId, + TOptional TimeoutMillis, const FSpatialGDKSpanId& SpanId) { - View.SendCreateEntityRequest({NextRequestId, MoveTemp(EntityComponents), EntityId, TimeoutMillis}); + View.SendCreateEntityRequest({ NextRequestId, MoveTemp(EntityComponents), EntityId, TimeoutMillis, SpanId }); return NextRequestId++; } -Worker_RequestId ViewCoordinator::SendDeleteEntityRequest(Worker_EntityId EntityId, TOptional TimeoutMillis) +Worker_RequestId ViewCoordinator::SendDeleteEntityRequest(Worker_EntityId EntityId, TOptional TimeoutMillis, + const FSpatialGDKSpanId& SpanId) { - View.SendDeleteEntityRequest({NextRequestId, EntityId, TimeoutMillis}); + View.SendDeleteEntityRequest({ NextRequestId, EntityId, TimeoutMillis, SpanId }); return NextRequestId++; } Worker_RequestId ViewCoordinator::SendEntityQueryRequest(EntityQuery Query, TOptional TimeoutMillis) { - View.SendEntityQueryRequest({NextRequestId, MoveTemp(Query), TimeoutMillis}); + View.SendEntityQueryRequest({ NextRequestId, MoveTemp(Query), TimeoutMillis }); return NextRequestId++; } Worker_RequestId ViewCoordinator::SendEntityCommandRequest(Worker_EntityId EntityId, CommandRequest Request, - TOptional TimeoutMillis) + TOptional TimeoutMillis, const FSpatialGDKSpanId& SpanId) { - View.SendEntityCommandRequest({EntityId, NextRequestId, MoveTemp(Request), TimeoutMillis}); + View.SendEntityCommandRequest({ EntityId, NextRequestId, MoveTemp(Request), TimeoutMillis, SpanId }); return NextRequestId++; } -void ViewCoordinator::SendEntityCommandResponse(Worker_RequestId RequestId, CommandResponse Response) +void ViewCoordinator::SendEntityCommandResponse(Worker_RequestId RequestId, CommandResponse Response, const FSpatialGDKSpanId& SpanId) { - View.SendEntityCommandResponse({RequestId, MoveTemp(Response)}); + View.SendEntityCommandResponse({ RequestId, MoveTemp(Response), SpanId }); } -void ViewCoordinator::SendEntityCommandFailure(Worker_RequestId RequestId, FString Message) +void ViewCoordinator::SendEntityCommandFailure(Worker_RequestId RequestId, FString Message, const FSpatialGDKSpanId& SpanId) { - View.SendEntityCommandFailure({RequestId, MoveTemp(Message)}); + View.SendEntityCommandFailure({ RequestId, MoveTemp(Message), SpanId }); } void ViewCoordinator::SendMetrics(SpatialMetrics Metrics) @@ -96,7 +149,92 @@ void ViewCoordinator::SendMetrics(SpatialMetrics Metrics) void ViewCoordinator::SendLogMessage(Worker_LogLevel Level, const FName& LoggerName, FString Message) { - View.SendLogMessage({Level, LoggerName, MoveTemp(Message)}); + View.SendLogMessage({ Level, LoggerName, MoveTemp(Message) }); +} + +Worker_RequestId ViewCoordinator::SendReserveEntityIdsRequest(uint32 NumberOfEntityIds, FRetryData RetryData) +{ + ReserveEntityIdRetryHandler.SendRequest(NextRequestId, NumberOfEntityIds, RetryData, View); + return NextRequestId++; +} + +Worker_RequestId ViewCoordinator::SendCreateEntityRequest(TArray EntityComponents, TOptional EntityId, + FRetryData RetryData, const FSpatialGDKSpanId& SpanId) +{ + CreateEntityRetryHandler.SendRequest(NextRequestId, { MoveTemp(EntityComponents), EntityId, SpanId }, RetryData, View); + return NextRequestId++; +} + +Worker_RequestId ViewCoordinator::SendDeleteEntityRequest(Worker_EntityId EntityId, FRetryData RetryData, const FSpatialGDKSpanId& SpanId) +{ + DeleteEntityRetryHandler.SendRequest(NextRequestId, { EntityId, SpanId }, RetryData, View); + return NextRequestId++; +} + +Worker_RequestId ViewCoordinator::SendEntityQueryRequest(EntityQuery Query, FRetryData RetryData) +{ + EntityQueryRetryHandler.SendRequest(NextRequestId, MoveTemp(Query), RetryData, View); + return NextRequestId++; +} + +Worker_RequestId ViewCoordinator::SendEntityCommandRequest(Worker_EntityId EntityId, CommandRequest Request, FRetryData RetryData, + const FSpatialGDKSpanId& SpanId) +{ + EntityCommandRetryHandler.SendRequest(NextRequestId, { EntityId, MoveTemp(Request), SpanId }, RetryData, View); + return NextRequestId++; +} + +CallbackId ViewCoordinator::RegisterComponentAddedCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback) +{ + return Dispatcher.RegisterComponentAddedCallback(ComponentId, MoveTemp(Callback)); +} + +CallbackId ViewCoordinator::RegisterComponentRemovedCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback) +{ + return Dispatcher.RegisterComponentRemovedCallback(ComponentId, MoveTemp(Callback)); +} + +CallbackId ViewCoordinator::RegisterComponentValueCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback) +{ + return Dispatcher.RegisterComponentValueCallback(ComponentId, MoveTemp(Callback)); +} + +CallbackId ViewCoordinator::RegisterAuthorityGainedCallback(Worker_ComponentId ComponentId, FEntityCallback Callback) +{ + return Dispatcher.RegisterAuthorityGainedCallback(ComponentId, MoveTemp(Callback)); +} + +CallbackId ViewCoordinator::RegisterAuthorityLostCallback(Worker_ComponentId ComponentId, FEntityCallback Callback) +{ + return Dispatcher.RegisterAuthorityLostCallback(ComponentId, MoveTemp(Callback)); +} + +CallbackId ViewCoordinator::RegisterAuthorityLostTempCallback(Worker_ComponentId ComponentId, FEntityCallback Callback) +{ + return Dispatcher.RegisterAuthorityLostTempCallback(ComponentId, MoveTemp(Callback)); +} + +void ViewCoordinator::RemoveCallback(CallbackId Id) +{ + Dispatcher.RemoveCallback(Id); +} + +FDispatcherRefreshCallback ViewCoordinator::CreateComponentExistenceRefreshCallback( + Worker_ComponentId ComponentId, const FComponentChangeRefreshPredicate& RefreshPredicate) +{ + return FSubView::CreateComponentExistenceRefreshCallback(Dispatcher, ComponentId, RefreshPredicate); +} + +FDispatcherRefreshCallback ViewCoordinator::CreateComponentChangedRefreshCallback(Worker_ComponentId ComponentId, + const FComponentChangeRefreshPredicate& RefreshPredicate) +{ + return FSubView::CreateComponentChangedRefreshCallback(Dispatcher, ComponentId, RefreshPredicate); +} + +FDispatcherRefreshCallback ViewCoordinator::CreateAuthorityChangeRefreshCallback(Worker_ComponentId ComponentId, + const FAuthorityChangeRefreshPredicate& RefreshPredicate) +{ + return FSubView::CreateAuthorityChangeRefreshCallback(Dispatcher, ComponentId, RefreshPredicate); } const FString& ViewCoordinator::GetWorkerId() const @@ -104,9 +242,9 @@ const FString& ViewCoordinator::GetWorkerId() const return ConnectionHandler->GetWorkerId(); } -const TArray& ViewCoordinator::GetWorkerAttributes() const +Worker_EntityId ViewCoordinator::GetWorkerSystemEntityId() const { - return ConnectionHandler->GetWorkerAttributes(); + return ConnectionHandler->GetWorkerSystemEntityId(); } -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewDelta.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewDelta.cpp index 4b5d428069..cf8b785a77 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewDelta.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewDelta.cpp @@ -2,173 +2,663 @@ #include "SpatialView/ViewDelta.h" +#include "Interop/Connection/SpatialTraceEventBuilder.h" +#include "SpatialView/EntityComponentTypes.h" + +#include "Algo/StableSort.h" +#include + namespace SpatialGDK { +void ViewDelta::SetFromOpList(TArray OpLists, EntityView& View, const FComponentSetData& ComponentSetData) +{ + Clear(); + for (OpList& Ops : OpLists) + { + ProcessOpList(Ops, View, ComponentSetData); + } + OpListStorage = MoveTemp(OpLists); + + PopulateEntityDeltas(View); +} -void ViewDelta::AddOpList(OpList Ops, TSet& ComponentsPresent) +void ViewDelta::Project(FSubViewDelta& SubDelta, const TArray& CompleteEntities, + const TArray& NewlyCompleteEntities, const TArray& NewlyIncompleteEntities, + const TArray& TemporarilyIncompleteEntities) const { - for (uint32 i = 0; i < Ops.Count; ++i) + SubDelta.EntityDeltas.Empty(); + + // No projection is applied to worker messages, as they are not entity specific. + SubDelta.WorkerMessages = &WorkerMessages; + + // All arrays here are sorted by entity ID. + auto DeltaIt = EntityDeltas.CreateConstIterator(); + auto CompleteIt = CompleteEntities.CreateConstIterator(); + auto NewlyCompleteIt = NewlyCompleteEntities.CreateConstIterator(); + auto NewlyIncompleteIt = NewlyIncompleteEntities.CreateConstIterator(); + auto TemporarilyIncompleteIt = TemporarilyIncompleteEntities.CreateConstIterator(); + + for (;;) { - ProcessOp(Ops.Ops[i], ComponentsPresent); + const Worker_EntityId DeltaId = DeltaIt ? DeltaIt->EntityId : SENTINEL_ENTITY_ID; + const Worker_EntityId CompleteId = CompleteIt ? *CompleteIt : SENTINEL_ENTITY_ID; + const Worker_EntityId NewlyCompleteId = NewlyCompleteIt ? *NewlyCompleteIt : SENTINEL_ENTITY_ID; + const Worker_EntityId NewlyIncompleteId = NewlyIncompleteIt ? *NewlyIncompleteIt : SENTINEL_ENTITY_ID; + const Worker_EntityId TemporarilyIncompleteId = TemporarilyIncompleteIt ? *TemporarilyIncompleteIt : SENTINEL_ENTITY_ID; + const uint64 MinEntityId = FMath::Min3(FMath::Min(static_cast(DeltaId), static_cast(CompleteId)), + FMath::Min(static_cast(NewlyCompleteId), static_cast(NewlyIncompleteId)), + static_cast(TemporarilyIncompleteId)); + const Worker_EntityId CurrentEntityId = static_cast(MinEntityId); + // If no list has elements left to read then stop. + if (CurrentEntityId == SENTINEL_ENTITY_ID) + { + break; + } + + // Find the intersection between complete entities and the entity IDs in the view delta, add them to this + // delta. + if (CompleteId == CurrentEntityId && DeltaId == CurrentEntityId) + { + EntityDelta CompleteDelta = *DeltaIt; + if (TemporarilyIncompleteId == CurrentEntityId) + { + // This is a delta for a complete entity which was also temporarily removed. Change its type to + // reflect that. + CompleteDelta.Type = EntityDelta::TEMPORARILY_REMOVED; + ++TemporarilyIncompleteIt; + } + SubDelta.EntityDeltas.Emplace(CompleteDelta); + } + // Temporarily incomplete entities which aren't present in the projecting view delta are represented as marker + // temporarily removed entities with no state. + else if (TemporarilyIncompleteId == CurrentEntityId) + { + SubDelta.EntityDeltas.Emplace(EntityDelta{ CurrentEntityId, EntityDelta::TEMPORARILY_REMOVED }); + ++TemporarilyIncompleteIt; + } + // Newly complete entities are represented as marker add entities with no state. + else if (NewlyCompleteId == CurrentEntityId) + { + SubDelta.EntityDeltas.Emplace(EntityDelta{ CurrentEntityId, EntityDelta::ADD }); + ++NewlyCompleteIt; + } + // Newly incomplete entities are represented as marker remove entities with no state. + else if (NewlyIncompleteId == CurrentEntityId) + { + SubDelta.EntityDeltas.Emplace(EntityDelta{ CurrentEntityId, EntityDelta::REMOVE }); + ++NewlyIncompleteIt; + } + + // Logic for incrementing complete and delta iterators. If either iterator is done, null the other, + // as there can no longer be any intersection. + if (CompleteId == CurrentEntityId) + { + ++CompleteIt; + if (!CompleteIt) + { + DeltaIt.SetToEnd(); + } + } + if (DeltaId == CurrentEntityId) + { + ++DeltaIt; + if (!DeltaIt) + { + CompleteIt.SetToEnd(); + } + } } - OpLists.Add(MoveTemp(Ops)); } -bool ViewDelta::HasDisconnected() const +void ViewDelta::Clear() +{ + EntityChanges.Empty(); + ComponentChanges.Empty(); + AuthorityChanges.Empty(); + + ConnectionStatusCode = 0; + + EntityDeltas.Empty(); + WorkerMessages.Empty(); + AuthorityGainedForDelta.Empty(); + AuthorityLostForDelta.Empty(); + AuthorityLostTempForDelta.Empty(); + ComponentsAddedForDelta.Empty(); + ComponentsRemovedForDelta.Empty(); + ComponentUpdatesForDelta.Empty(); + ComponentsRefreshedForDelta.Empty(); + OpListStorage.Empty(); +} + +const TArray& ViewDelta::GetEntityDeltas() const { - return ConnectionStatus != 0; + return EntityDeltas; } -uint8 ViewDelta::GetConnectionStatus() const +const TArray& ViewDelta::GetWorkerMessages() const { - check(HasDisconnected()); - return ConnectionStatus; + return WorkerMessages; } -FString ViewDelta::GetDisconnectReason() const +bool ViewDelta::HasConnectionStatusChanged() const { - check(HasDisconnected()); - return DisconnectReason; + return ConnectionStatusCode != 0; } -const TArray& ViewDelta::GetEntitiesAdded() const +Worker_ConnectionStatusCode ViewDelta::GetConnectionStatusChange() const { - return EntityPresenceChanges.GetEntitiesAdded(); + check(HasConnectionStatusChanged()); + return static_cast(ConnectionStatusCode); } -const TArray& ViewDelta::GetEntitiesRemoved() const +FString ViewDelta::GetConnectionStatusChangeMessage() const { - return EntityPresenceChanges.GetEntitiesRemoved(); + check(HasConnectionStatusChanged()); + return ConnectionStatusMessage; } -const TArray& ViewDelta::GetAuthorityGained() const +ViewDelta::ReceivedComponentChange::ReceivedComponentChange(const Worker_AddComponentOp& Op) + : EntityId(Op.entity_id) + , ComponentId(Op.data.component_id) + , Type(ADD) + , ComponentAdded(Op.data.schema_type) { - return AuthorityChanges.GetAuthorityGained(); } -const TArray& ViewDelta::GetAuthorityLost() const +ViewDelta::ReceivedComponentChange::ReceivedComponentChange(const Worker_ComponentUpdateOp& Op) + : EntityId(Op.entity_id) + , ComponentId(Op.update.component_id) + , Type(UPDATE) + , ComponentUpdate(Op.update.schema_type) { - return AuthorityChanges.GetAuthorityLost(); } -const TArray& ViewDelta::GetAuthorityLostTemporarily() const +ViewDelta::ReceivedComponentChange::ReceivedComponentChange(const Worker_RemoveComponentOp& Op) + : EntityId(Op.entity_id) + , ComponentId(Op.component_id) + , Type(REMOVE) { - return AuthorityChanges.GetAuthorityLostTemporarily(); } -const TArray& ViewDelta::GetComponentsAdded() const +bool ViewDelta::DifferentEntity::operator()(const ReceivedEntityChange& E) const { - return EntityComponentChanges.GetComponentsAdded(); + return E.EntityId != EntityId; } -const TArray& ViewDelta::GetComponentsRemoved() const +bool ViewDelta::DifferentEntity::operator()(const ReceivedComponentChange& Op) const { - return EntityComponentChanges.GetComponentsRemoved(); + return Op.EntityId != EntityId; } -const TArray& ViewDelta::GetUpdates() const +bool ViewDelta::DifferentEntity::operator()(const Worker_ComponentSetAuthorityChangeOp& Op) const { - return EntityComponentChanges.GetUpdates(); + return Op.entity_id != EntityId; } -const TArray& ViewDelta::GetCompleteUpdates() const +bool ViewDelta::DifferentEntityComponent::operator()(const ReceivedComponentChange& Op) const { - return EntityComponentChanges.GetCompleteUpdates(); + return Op.ComponentId != ComponentId || Op.EntityId != EntityId; } -const TArray& ViewDelta::GetWorkerMessages() const +bool ViewDelta::DifferentEntityComponent::operator()(const Worker_ComponentSetAuthorityChangeOp& Op) const { - return WorkerMessages; + return Op.component_set_id != ComponentId || Op.entity_id != EntityId; } -void ViewDelta::Clear() +bool ViewDelta::EntityComponentComparison::operator()(const ReceivedComponentChange& Lhs, const ReceivedComponentChange& Rhs) const { - WorkerMessages.Empty(); - OpLists.Empty(); - AuthorityChanges.Clear(); - EntityComponentChanges.Clear(); - ConnectionStatus = 0; -} - -void ViewDelta::ProcessOp(const Worker_Op& Op, TSet& ComponentsPresent) -{ - switch (static_cast(Op.op_type)) - { - case WORKER_OP_TYPE_DISCONNECT: - ConnectionStatus = Op.op.disconnect.connection_status_code; - DisconnectReason = FString(Op.op.disconnect.reason); - break; - case WORKER_OP_TYPE_FLAG_UPDATE: - case WORKER_OP_TYPE_LOG_MESSAGE: - case WORKER_OP_TYPE_METRICS: - WorkerMessages.Add(Op); - break; - case WORKER_OP_TYPE_CRITICAL_SECTION: - // Ignore. - break; - case WORKER_OP_TYPE_ADD_ENTITY: - EntityPresenceChanges.AddEntity(Op.op.add_entity.entity_id); - break; - case WORKER_OP_TYPE_REMOVE_ENTITY: - EntityPresenceChanges.RemoveEntity(Op.op.remove_entity.entity_id); - break; - case WORKER_OP_TYPE_RESERVE_ENTITY_IDS_RESPONSE: - case WORKER_OP_TYPE_CREATE_ENTITY_RESPONSE: - case WORKER_OP_TYPE_DELETE_ENTITY_RESPONSE: - case WORKER_OP_TYPE_ENTITY_QUERY_RESPONSE: - WorkerMessages.Add(Op); - break; - case WORKER_OP_TYPE_ADD_COMPONENT: - HandleAddComponent(Op.op.add_component, ComponentsPresent); - break; - case WORKER_OP_TYPE_REMOVE_COMPONENT: - HandleRemoveComponent(Op.op.remove_component, ComponentsPresent); - break; - case WORKER_OP_TYPE_AUTHORITY_CHANGE: - HandleAuthorityChange(Op.op.authority_change); - break; - case WORKER_OP_TYPE_COMPONENT_UPDATE: - HandleComponentUpdate(Op.op.component_update); - break; - case WORKER_OP_TYPE_COMMAND_REQUEST: - case WORKER_OP_TYPE_COMMAND_RESPONSE: - WorkerMessages.Add(Op); - break; - } -} - -void ViewDelta::HandleAuthorityChange(const Worker_AuthorityChangeOp& Op) -{ - AuthorityChanges.SetAuthority(Op.entity_id, Op.component_id, static_cast(Op.authority)); -} - -void ViewDelta::HandleAddComponent(const Worker_AddComponentOp& Op, TSet& ComponentsPresent) -{ - const EntityComponentId Id = { Op.entity_id, Op.data.component_id }; - if (ComponentsPresent.Contains(Id)) - { - EntityComponentChanges.AddComponentAsUpdate(Id.EntityId, ComponentData::CreateCopy(Op.data.schema_type, Id.ComponentId)); + if (Lhs.EntityId != Rhs.EntityId) + { + return Lhs.EntityId < Rhs.EntityId; } - else + return Lhs.ComponentId < Rhs.ComponentId; +} + +bool ViewDelta::EntityComponentComparison::operator()(const Worker_ComponentSetAuthorityChangeOp& Lhs, + const Worker_ComponentSetAuthorityChangeOp& Rhs) const +{ + if (Lhs.entity_id != Rhs.entity_id) { - ComponentsPresent.Add(Id); - EntityComponentChanges.AddComponent(Id.EntityId, ComponentData::CreateCopy(Op.data.schema_type, Id.ComponentId)); + return Lhs.entity_id < Rhs.entity_id; } + return Lhs.component_set_id < Rhs.component_set_id; +} + +bool ViewDelta::EntityComparison::operator()(const ReceivedEntityChange& Lhs, const ReceivedEntityChange& Rhs) const +{ + return Lhs.EntityId < Rhs.EntityId; } -void ViewDelta::HandleComponentUpdate(const Worker_ComponentUpdateOp& Op) +ComponentChange ViewDelta::CalculateAdd(ReceivedComponentChange* Start, ReceivedComponentChange* End, TArray& Components) { - EntityComponentChanges.AddUpdate(Op.entity_id, ComponentUpdate::CreateCopy(Op.update.schema_type, Op.update.component_id)); + // There must be at least one component add; anything before it can be ignored. + ReceivedComponentChange* It = std::find_if(Start, End, [](const ReceivedComponentChange& Op) { + return Op.Type == ReceivedComponentChange::ADD; + }); + + Schema_ComponentData* Data = It->ComponentAdded; + ++It; + + while (It != End) + { + switch (It->Type) + { + case ReceivedComponentChange::ADD: + Data = It->ComponentAdded; + break; + case ReceivedComponentChange::UPDATE: + Schema_ApplyComponentUpdateToData(It->ComponentUpdate, Data); + break; + case ReceivedComponentChange::REMOVE: + break; + } + ++It; + } + Components.Emplace(ComponentData::CreateCopy(Data, Start->ComponentId)); + // We don't want to reference the component in the view as is isn't stable. + return ComponentChange(Start->ComponentId, Data); } -void ViewDelta::HandleRemoveComponent(const Worker_RemoveComponentOp& Op, TSet& ComponentsPresent) +ComponentChange ViewDelta::CalculateCompleteUpdate(ReceivedComponentChange* Start, ReceivedComponentChange* End, Schema_ComponentData* Data, + Schema_ComponentUpdate* Events, ComponentData& Component) { - const EntityComponentId Id = { Op.entity_id, Op.component_id }; - // If the component has been added, remove it. Otherwise drop the op. - if (ComponentsPresent.Remove(Id)) + for (auto It = Start; It != End; ++It) + { + switch (It->Type) + { + case ReceivedComponentChange::ADD: + Data = It->ComponentAdded; + break; + case ReceivedComponentChange::UPDATE: + if (Data) + { + Schema_ApplyComponentUpdateToData(It->ComponentUpdate, Data); + } + if (Events) + { + Schema_MergeComponentUpdateIntoUpdate(It->ComponentUpdate, Events); + } + else + { + Events = It->ComponentUpdate; + } + break; + case ReceivedComponentChange::REMOVE: + break; + } + } + + Component = ComponentData::CreateCopy(Data, Start->ComponentId); + Schema_Object* EventsObj = Events ? Schema_GetComponentUpdateEvents(Events) : nullptr; + // Use the data from the op list as pointers from the view aren't stable. + return ComponentChange(Start->ComponentId, Data, EventsObj); +} + +ComponentChange ViewDelta::CalculateUpdate(ReceivedComponentChange* Start, ReceivedComponentChange* End, ComponentData& Component) +{ + // For an update we don't know if we are calculating a complete-update or a regular update. + // So the first message processed might be an add or an update. + auto It = std::find_if(Start, End, [](const ReceivedComponentChange& Op) { + return Op.Type != ReceivedComponentChange::REMOVE; + }); + + // If the first message is an add then calculate a complete-update. + if (It->Type == ReceivedComponentChange::ADD) { - EntityComponentChanges.RemoveComponent(Id.EntityId, Id.ComponentId); + return CalculateCompleteUpdate(It + 1, End, It->ComponentAdded, nullptr, Component); } + + Schema_ComponentUpdate* Update = It->ComponentUpdate; + ++It; + while (It != End) + { + switch (It->Type) + { + case ReceivedComponentChange::ADD: + return CalculateCompleteUpdate(It + 1, End, It->ComponentAdded, Update, Component); + case ReceivedComponentChange::UPDATE: + Schema_MergeComponentUpdateIntoUpdate(It->ComponentUpdate, Update); + break; + case ReceivedComponentChange::REMOVE: + return CalculateCompleteUpdate(It + 1, End, nullptr, nullptr, Component); + } + ++It; + } + + Schema_ApplyComponentUpdateToData(Update, Component.GetUnderlying()); + Component = Component.DeepCopy(); + return ComponentChange(Start->ComponentId, Update); +} + +void ViewDelta::ProcessOpList(const OpList& Ops, const EntityView& View, const FComponentSetData& ComponentSetData) +{ + for (uint32 i = 0; i < Ops.Count; ++i) + { + const Worker_Op& Op = Ops.Ops[i]; + switch (static_cast(Op.op_type)) + { + case WORKER_OP_TYPE_DISCONNECT: + ConnectionStatusCode = Op.op.disconnect.connection_status_code; + ConnectionStatusMessage = Op.op.disconnect.reason; + break; + case WORKER_OP_TYPE_CRITICAL_SECTION: + // Ignore critical sections. + break; + case WORKER_OP_TYPE_ADD_ENTITY: + EntityChanges.Push(ReceivedEntityChange{ Op.op.add_entity.entity_id, true }); + break; + case WORKER_OP_TYPE_REMOVE_ENTITY: + EntityChanges.Push(ReceivedEntityChange{ Op.op.remove_entity.entity_id, false }); + break; + case WORKER_OP_TYPE_METRICS: + case WORKER_OP_TYPE_FLAG_UPDATE: + case WORKER_OP_TYPE_RESERVE_ENTITY_IDS_RESPONSE: + case WORKER_OP_TYPE_CREATE_ENTITY_RESPONSE: + case WORKER_OP_TYPE_DELETE_ENTITY_RESPONSE: + case WORKER_OP_TYPE_ENTITY_QUERY_RESPONSE: + case WORKER_OP_TYPE_COMMAND_REQUEST: + case WORKER_OP_TYPE_COMMAND_RESPONSE: + WorkerMessages.Push(Op); + break; + case WORKER_OP_TYPE_ADD_COMPONENT: + ComponentChanges.Emplace(Op.op.add_component); + break; + case WORKER_OP_TYPE_REMOVE_COMPONENT: + ComponentChanges.Emplace(Op.op.remove_component); + break; + case WORKER_OP_TYPE_COMPONENT_SET_AUTHORITY_CHANGE: + GenerateComponentChangesFromSetData(Op.op.component_set_authority_change, View, ComponentSetData); + AuthorityChanges.Emplace(Op.op.component_set_authority_change); + break; + case WORKER_OP_TYPE_COMPONENT_UPDATE: + ComponentChanges.Emplace(Op.op.component_update); + break; + default: + break; + } + } +} + +void ViewDelta::GenerateComponentChangesFromSetData(const Worker_ComponentSetAuthorityChangeOp& Op, const EntityView& View, + const FComponentSetData& ComponentSetData) +{ + // Generate component changes to: + // * Remove all components on the entity, that are in the component set. + // * Add all components the with data in the op. + // If one component is both removed and added then this is interpreted as component refresh in the view delta. + // Otherwise the component will be added or removed as appropriate. + + const TSet& Set = ComponentSetData.ComponentSets[Op.component_set_id]; + + // If a component on the entity is in the set then generate a remove operation. + if (const EntityViewElement* Entity = View.Find(Op.entity_id)) + { + for (const ComponentData& Component : Entity->Components) + { + const Worker_ComponentId ComponentId = Component.GetComponentId(); + if (Set.Contains(ComponentId)) + { + Worker_RemoveComponentOp RemoveOp = { Op.entity_id, ComponentId }; + ComponentChanges.Emplace(RemoveOp); + } + } + } + + // If the component has data in the authority op then generate an add operation. + for (uint32 i = 0; i < Op.canonical_component_set_data_count; ++i) + { + Worker_AddComponentOp AddOp = { Op.entity_id, Op.canonical_component_set_data[i] }; + ComponentChanges.Emplace(AddOp); + } +} + +void ViewDelta::PopulateEntityDeltas(EntityView& View) +{ + // Make sure there is enough space in the view delta storage. + // This allows us to rely on stable pointers as we add new elements. + ComponentsAddedForDelta.Reserve(ComponentChanges.Num()); + ComponentsRemovedForDelta.Reserve(ComponentChanges.Num()); + ComponentUpdatesForDelta.Reserve(ComponentChanges.Num()); + ComponentsRefreshedForDelta.Reserve(ComponentChanges.Num()); + AuthorityGainedForDelta.Reserve(AuthorityChanges.Num()); + AuthorityLostForDelta.Reserve(AuthorityChanges.Num()); + AuthorityLostTempForDelta.Reserve(AuthorityChanges.Num()); + + Algo::StableSort(ComponentChanges, EntityComponentComparison{}); + Algo::StableSort(AuthorityChanges, EntityComponentComparison{}); + Algo::StableSort(EntityChanges, EntityComparison{}); + + // Add sentinel elements to the ends of the arrays. + // Prevents the need for bounds checks on the iterators. + ComponentChanges.Emplace(Worker_RemoveComponentOp{ SENTINEL_ENTITY_ID, 0 }); + AuthorityChanges.Emplace(Worker_ComponentSetAuthorityChangeOp{ SENTINEL_ENTITY_ID, 0, 0 }); + EntityChanges.Emplace(ReceivedEntityChange{ SENTINEL_ENTITY_ID, false }); + + auto ComponentIt = ComponentChanges.GetData(); + auto AuthorityIt = AuthorityChanges.GetData(); + auto EntityIt = EntityChanges.GetData(); + + ReceivedComponentChange* ComponentChangesEnd = ComponentIt + ComponentChanges.Num(); + Worker_ComponentSetAuthorityChangeOp* AuthorityChangesEnd = AuthorityIt + AuthorityChanges.Num(); + ReceivedEntityChange* EntityChangesEnd = EntityIt + EntityChanges.Num(); + + // At the beginning of each loop each iterator should point to the first element for an entity. + // Each loop we want to work with a single entity ID. + // We check the entities each iterator is pointing to and pick the smallest one. + // If that is the sentinel ID then stop. + for (;;) + { + // Get the next entity ID. We want to pick the smallest entity referenced by the iterators. + // Convert to uint64 to ensure the sentinel value is larger than all valid IDs. + const uint64 MinEntityId = FMath::Min3(static_cast(ComponentIt->EntityId), static_cast(AuthorityIt->entity_id), + static_cast(EntityIt->EntityId)); + + // If no list has elements left to read then stop. + if (static_cast(MinEntityId) == SENTINEL_ENTITY_ID) + { + break; + } + + const Worker_EntityId CurrentEntityId = static_cast(MinEntityId); + + EntityDelta Delta = {}; + Delta.EntityId = CurrentEntityId; + + EntityViewElement* ViewElement = View.Find(CurrentEntityId); + const bool bAlreadyExisted = ViewElement != nullptr; + + if (ViewElement == nullptr) + { + ViewElement = &View.Add(CurrentEntityId); + } + + if (ComponentIt->EntityId == CurrentEntityId) + { + ComponentIt = ProcessEntityComponentChanges(ComponentIt, ComponentChangesEnd, ViewElement->Components, Delta); + } + + if (AuthorityIt->entity_id == CurrentEntityId) + { + AuthorityIt = ProcessEntityAuthorityChanges(AuthorityIt, AuthorityChangesEnd, ViewElement->Authority, Delta); + } + + if (EntityIt->EntityId == CurrentEntityId) + { + EntityIt = ProcessEntityExistenceChange(EntityIt, EntityChangesEnd, Delta, bAlreadyExisted, View); + // Did the entity flicker into view for less than a tick. + if (Delta.Type == EntityDelta::UPDATE && !bAlreadyExisted) + { + View.Remove(CurrentEntityId); + continue; + } + } + + EntityDeltas.Push(Delta); + } +} + +ViewDelta::ReceivedComponentChange* ViewDelta::ProcessEntityComponentChanges(ReceivedComponentChange* It, ReceivedComponentChange* End, + TArray& Components, EntityDelta& Delta) +{ + int32 AddCount = 0; + int32 UpdateCount = 0; + int32 RemoveCount = 0; + int32 RefreshCount = 0; + + const Worker_EntityId EntityId = It->EntityId; + // At the end of each loop `It` should point to the first element for an entity-component. + // Stop and return when the component is for a different entity. + // There will always be at least one iteration of the loop. + for (;;) + { + ReceivedComponentChange* NextComponentIt = std::find_if(It, End, DifferentEntityComponent{ EntityId, It->ComponentId }); + + ComponentData* Component = Components.FindByPredicate(ComponentIdEquality{ It->ComponentId }); + const bool bComponentExists = Component != nullptr; + + // The element one before NextComponentIt must be the last element for this component. + switch ((NextComponentIt - 1)->Type) + { + case ReceivedComponentChange::ADD: + if (bComponentExists) + { + ComponentsRefreshedForDelta.Emplace(CalculateCompleteUpdate(It, NextComponentIt, nullptr, nullptr, *Component)); + ++RefreshCount; + } + else + { + ComponentsAddedForDelta.Emplace(CalculateAdd(It, NextComponentIt, Components)); + ++AddCount; + } + break; + case ReceivedComponentChange::UPDATE: + if (bComponentExists) + { + ComponentChange Update = CalculateUpdate(It, NextComponentIt, *Component); + if (Update.Type == ComponentChange::COMPLETE_UPDATE) + { + ComponentsRefreshedForDelta.Emplace(Update); + ++RefreshCount; + } + else + { + ComponentUpdatesForDelta.Emplace(Update); + ++UpdateCount; + } + } + else + { + ComponentsAddedForDelta.Emplace(CalculateAdd(It, NextComponentIt, Components)); + ++AddCount; + } + break; + case ReceivedComponentChange::REMOVE: + if (bComponentExists) + { + ComponentsRemovedForDelta.Emplace(It->ComponentId); + Components.RemoveAtSwap(Component - Components.GetData()); + ++RemoveCount; + } + break; + } + + if (NextComponentIt->EntityId != EntityId) + { + Delta.ComponentsAdded = { ComponentsAddedForDelta.GetData() + ComponentsAddedForDelta.Num() - AddCount, AddCount }; + Delta.ComponentsRemoved = { ComponentsRemovedForDelta.GetData() + ComponentsRemovedForDelta.Num() - RemoveCount, RemoveCount }; + Delta.ComponentUpdates = { ComponentUpdatesForDelta.GetData() + ComponentUpdatesForDelta.Num() - UpdateCount, UpdateCount }; + Delta.ComponentsRefreshed = { ComponentsRefreshedForDelta.GetData() + ComponentsRefreshedForDelta.Num() - RefreshCount, + RefreshCount }; + return NextComponentIt; + } + + It = NextComponentIt; + } +} + +Worker_ComponentSetAuthorityChangeOp* ViewDelta::ProcessEntityAuthorityChanges(Worker_ComponentSetAuthorityChangeOp* It, + Worker_ComponentSetAuthorityChangeOp* End, + TArray& EntityAuthority, + EntityDelta& Delta) +{ + int32 GainCount = 0; + int32 LossCount = 0; + int32 LossTempCount = 0; + + const Worker_EntityId EntityId = It->entity_id; + // After each loop the iterator points to the first op relating to the next entity-component. + // Stop and return when that component is for a different entity. + // There will always be at least one iteration of the loop. + for (;;) + { + // Find the last element for this entity-component. + const Worker_ComponentSetId ComponentId = It->component_set_id; // TODO: fix this moving from component to component set + It = std::find_if(It, End, DifferentEntityComponent{ EntityId, ComponentId }) - 1; + const int32 AuthorityIndex = EntityAuthority.Find(ComponentId); + const bool bHasAuthority = AuthorityIndex != INDEX_NONE; + + if (It->authority == WORKER_AUTHORITY_AUTHORITATIVE) + { + if (bHasAuthority) + { + AuthorityLostTempForDelta.Emplace(ComponentId, AuthorityChange::AUTHORITY_LOST_TEMPORARILY); + ++LossTempCount; + } + else + { + EntityAuthority.Push(ComponentId); + AuthorityGainedForDelta.Emplace(ComponentId, AuthorityChange::AUTHORITY_GAINED); + ++GainCount; + } + } + else if (bHasAuthority) + { + AuthorityLostForDelta.Emplace(ComponentId, AuthorityChange::AUTHORITY_LOST); + EntityAuthority.RemoveAtSwap(AuthorityIndex); + ++LossCount; + } + + // Move to the next entity-component. + ++It; + + if (It->entity_id != EntityId) + { + Delta.AuthorityGained = { AuthorityGainedForDelta.GetData() + AuthorityGainedForDelta.Num() - GainCount, GainCount }; + Delta.AuthorityLost = { AuthorityLostForDelta.GetData() + AuthorityLostForDelta.Num() - LossCount, LossCount }; + Delta.AuthorityLostTemporarily = { AuthorityLostTempForDelta.GetData() + AuthorityLostTempForDelta.Num() - LossTempCount, + LossTempCount }; + return It; + } + } +} + +ViewDelta::ReceivedEntityChange* ViewDelta::ProcessEntityExistenceChange(ReceivedEntityChange* It, ReceivedEntityChange* End, + EntityDelta& Delta, bool bAlreadyInView, EntityView& View) +{ + // Find the last element relating to the same entity. + const Worker_EntityId EntityId = It->EntityId; + It = std::find_if(It, End, DifferentEntity{ EntityId }) - 1; + + const bool bEntityAdded = It->bAdded; + + // If the entity's presence has not changed then it's an update. + if (bEntityAdded == bAlreadyInView) + { + Delta.Type = EntityDelta::UPDATE; + return It + 1; + } + + if (bEntityAdded) + { + Delta.Type = EntityDelta::ADD; + } + else + { + Delta.Type = EntityDelta::REMOVE; + View.Remove(EntityId); + } + + return It + 1; } -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/WorkerView.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/WorkerView.cpp index 91a6d16a02..305e6d9539 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/WorkerView.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/WorkerView.cpp @@ -1,72 +1,32 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "SpatialView/WorkerView.h" + +#include "SpatialView/EntityComponentTypes.h" #include "SpatialView/MessagesToSend.h" #include "SpatialView/OpList/SplitOpList.h" namespace SpatialGDK { +WorkerView::WorkerView(FComponentSetData ComponentSetData) + : ComponentSetData(MoveTemp(ComponentSetData)) + , LocalChanges(MakeUnique()) +{ +} -WorkerView::WorkerView() -: LocalChanges(MakeUnique()) +void WorkerView::AdvanceViewDelta(TArray OpLists) { + Delta.SetFromOpList(MoveTemp(OpLists), View, ComponentSetData); } -ViewDelta WorkerView::GenerateViewDelta() +const ViewDelta& WorkerView::GetViewDelta() const { - ViewDelta Delta; - for (auto& Ops : QueuedOps) - { - Delta.AddOpList(MoveTemp(Ops), AddedComponents); - } - QueuedOps.Empty(); return Delta; } -void WorkerView::EnqueueOpList(OpList Ops) +const EntityView& WorkerView::GetView() const { - //Ensure that we only process closed critical sections. - // Scan backwards looking for critical sections ops. - for (uint32 i = Ops.Count; i > 0; --i) - { - Worker_Op& Op = Ops.Ops[i - 1]; - if (Op.op_type != WORKER_OP_TYPE_CRITICAL_SECTION) - { - continue; - } - - // There can only be one critical section open at a time. - // So any previous open critical section must now be closed. - for (OpList& OpenCriticalSection : OpenCriticalSectionOps) - { - QueuedOps.Add(MoveTemp(OpenCriticalSection)); - } - OpenCriticalSectionOps.Empty(); - - // If critical section op is opening the section then enqueue any ops before this point and store the open critical section. - if (Op.op.critical_section.in_critical_section) - { - SplitOpListPair SplitOpLists(MoveTemp(Ops), i); - QueuedOps.Add(MoveTemp(SplitOpLists.Head)); - OpenCriticalSectionOps.Add(MoveTemp(SplitOpLists.Tail)); - } - // If critical section op is closing the section then enqueue all ops. - else - { - QueuedOps.Add(MoveTemp(Ops)); - } - return; - } - - // If no critical section is present then either add this to existing open section ops if there are any or enqueue if not. - if (OpenCriticalSectionOps.Num()) - { - OpenCriticalSectionOps.Push(MoveTemp(Ops)); - } - else - { - QueuedOps.Push(MoveTemp(Ops)); - } + return View; } TUniquePtr WorkerView::FlushLocalChanges() @@ -76,21 +36,40 @@ TUniquePtr WorkerView::FlushLocalChanges() return OutgoingMessages; } -void WorkerView::SendAddComponent(Worker_EntityId EntityId, ComponentData Data) +void WorkerView::SendAddComponent(Worker_EntityId EntityId, ComponentData Data, const FSpatialGDKSpanId& SpanId) { - AddedComponents.Add(EntityComponentId{ EntityId, Data.GetComponentId() }); - LocalChanges->ComponentMessages.Emplace(EntityId, MoveTemp(Data)); + EntityViewElement* Element = View.Find(EntityId); + if (ensure(Element != nullptr)) + { + Element->Components.Emplace(Data.DeepCopy()); + LocalChanges->ComponentMessages.Emplace(EntityId, MoveTemp(Data), SpanId); + } } -void WorkerView::SendComponentUpdate(Worker_EntityId EntityId, ComponentUpdate Update) +void WorkerView::SendComponentUpdate(Worker_EntityId EntityId, ComponentUpdate Update, const FSpatialGDKSpanId& SpanId) { - LocalChanges->ComponentMessages.Emplace(EntityId, MoveTemp(Update)); + EntityViewElement* Element = View.Find(EntityId); + if (ensure(Element != nullptr)) + { + ComponentData* Component = Element->Components.FindByPredicate(ComponentIdEquality{ Update.GetComponentId() }); + if (Component != nullptr) + { + Component->ApplyUpdate(Update); + } + LocalChanges->ComponentMessages.Emplace(EntityId, MoveTemp(Update), SpanId); + } } -void WorkerView::SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) +void WorkerView::SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, const FSpatialGDKSpanId& SpanId) { - AddedComponents.Remove(EntityComponentId{ EntityId, ComponentId }); - LocalChanges->ComponentMessages.Emplace(EntityId, ComponentId); + EntityViewElement* Element = View.Find(EntityId); + if (ensure(Element != nullptr)) + { + ComponentData* Component = Element->Components.FindByPredicate(ComponentIdEquality{ ComponentId }); + check(Component != nullptr); + Element->Components.RemoveAtSwap(Component - Element->Components.GetData()); + LocalChanges->ComponentMessages.Emplace(EntityId, ComponentId, SpanId); + } } void WorkerView::SendReserveEntityIdsRequest(ReserveEntityIdsRequest Request) @@ -138,4 +117,4 @@ void WorkerView::SendLogMessage(LogMessage Log) LocalChanges->Logs.Add(MoveTemp(Log)); } -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/LoadBalanceEnforcerTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/LoadBalanceEnforcerTest.cpp new file mode 100644 index 0000000000..38456e3d26 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/LoadBalanceEnforcerTest.cpp @@ -0,0 +1,290 @@ +// 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 "Tests/TestingSchemaHelpers.h" + +#include "CoreMinimal.h" + +#include "SpatialConstants.h" +#include "SpatialView/ComponentData.h" +#include "SpatialView/Dispatcher.h" +#include "SpatialView/EntityComponentTypes.h" +#include "SpatialView/EntityView.h" +#include "SpatialView/SubView.h" +#include "SpatialView/ViewDelta.h" +#include "Tests/SpatialView/SpatialViewUtils.h" +#include "Utils/SchemaOption.h" + +#define LOADBALANCEENFORCER_TEST(TestName) GDK_TEST(Core, SpatialLoadBalanceEnforcer, TestName) + +// Test Globals +namespace +{ +const PhysicalWorkerName ThisWorker = TEXT("ThisWorker"); +const PhysicalWorkerName OtherWorker = TEXT("OtherWorker"); +const PhysicalWorkerName ClientWorker = TEXT("ClientWorker"); +const PhysicalWorkerName OtherClientWorker = TEXT("OtherClientWorker"); + +const Worker_PartitionId ThisWorkerId = 101; +const Worker_PartitionId OtherWorkerId = 102; +const Worker_PartitionId ClientWorkerId = 103; +const Worker_PartitionId OtherClientWorkerId = 104; + +constexpr VirtualWorkerId ThisVirtualWorker = 1; +constexpr VirtualWorkerId OtherVirtualWorker = 2; + +constexpr Worker_EntityId EntityIdOne = 1; + +constexpr Worker_ComponentId TestComponentIdOne = 123; +constexpr Worker_ComponentId TestComponentIdTwo = 456; + +const TArray NonAuthDelegationLBComponents = { SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID }; +const TArray TestComponentIds = { TestComponentIdOne, TestComponentIdTwo }; +const TArray ClientComponentIds = { SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID }; + +TUniquePtr CreateVirtualWorkerTranslator() +{ + TUniquePtr VirtualWorkerTranslator = + MakeUnique(nullptr, nullptr, ThisWorker); + + Schema_Object* DataObject = TestingSchemaHelpers::CreateTranslationComponentDataFields(); + + TestingSchemaHelpers::AddTranslationComponentDataMapping(DataObject, ThisVirtualWorker, ThisWorker, ThisWorkerId); + TestingSchemaHelpers::AddTranslationComponentDataMapping(DataObject, OtherVirtualWorker, OtherWorker, OtherWorkerId); + + VirtualWorkerTranslator->ApplyVirtualWorkerManagerData(DataObject); + + return VirtualWorkerTranslator; +} + +SpatialGDK::ComponentData MakeComponentDataFromData(const Worker_ComponentData Data) +{ + return SpatialGDK::ComponentData(SpatialGDK::OwningComponentDataPtr(Data.schema_type), Data.component_id); +} + +SpatialGDK::ComponentUpdate MakeComponentUpdateFromUpdate(const Worker_ComponentUpdate Update) +{ + return SpatialGDK::ComponentUpdate(SpatialGDK::OwningComponentUpdatePtr(Update.schema_type), Update.component_id); +} + +// Adds an entity to the view with correct LB components. Also assigns the current authority to the passed worker, +// and the auth intent to the passed virtual worker. +// Optionally pass a client name to designate the entity as net owned by that client. +void AddLBEntityToView(SpatialGDK::EntityView& View, const Worker_EntityId EntityId, const Worker_PartitionId AuthPartitionId, + const VirtualWorkerId IntentWorkerId, + const Worker_PartitionId ClientAuthPartitionId = SpatialConstants::INVALID_ENTITY_ID) +{ + AddEntityToView(View, EntityId); + + AuthorityDelegationMap DelegationMap; + DelegationMap.Add(SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID, AuthPartitionId); + + if (ClientAuthPartitionId != SpatialConstants::INVALID_ENTITY_ID) + { + DelegationMap.Add(SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID, ClientAuthPartitionId); + } + + AddComponentToView(View, EntityId, + MakeComponentDataFromData(SpatialGDK::AuthorityDelegation(DelegationMap).CreateAuthorityDelegationData())); + AddComponentToView(View, EntityId, MakeComponentDataFromData(SpatialGDK::AuthorityIntent::CreateAuthorityIntentData(IntentWorkerId))); + AddComponentToView( + View, EntityId, + MakeComponentDataFromData(SpatialGDK::NetOwningClientWorker::CreateNetOwningClientWorkerData(ClientAuthPartitionId))); + AddComponentToView(View, EntityId, SpatialGDK::ComponentData(SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID)); + AddComponentToView(View, EntityId, SpatialGDK::ComponentData(SpatialConstants::HEARTBEAT_COMPONENT_ID)); + AddComponentToView(View, EntityId, SpatialGDK::ComponentData(SpatialConstants::LB_TAG_COMPONENT_ID)); + + AddAuthorityToView(View, EntityId, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID); +} + +AuthorityDelegationMap GetAuthDelegationMapFromUpdate(const SpatialGDK::EntityComponentUpdate& Update) +{ + AuthorityDelegationMap AuthDelegation; + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.Update.GetUnderlying()); + // This is never emptied, so does not need an additional check for cleared fields + uint32 KVPairCount = Schema_GetObjectCount(ComponentObject, 1); + for (uint32 i = 0; i < KVPairCount; i++) + { + Schema_Object* KVPairObject = Schema_IndexObject(ComponentObject, 1, i); + uint32 Key = Schema_GetUint32(KVPairObject, SCHEMA_MAP_KEY_FIELD_ID); + Worker_PartitionId Value = Schema_GetInt64(KVPairObject, SCHEMA_MAP_VALUE_FIELD_ID); + + AuthDelegation.Add(Key, Value); + } + + return AuthDelegation; +} + +bool AuthorityMapDelegatesComponents(const AuthorityDelegationMap& DelegationMap, const Worker_PartitionId DelegatedPartitionId, + const TArray& DelegatedComponents) +{ + for (Worker_ComponentId ComponentId : DelegatedComponents) + { + const auto Entry = DelegationMap.Find(ComponentId); + if (Entry == nullptr) + { + return false; + } + if (*Entry != DelegatedPartitionId) + { + return false; + } + } + return true; +} + +} // anonymous namespace + +LOADBALANCEENFORCER_TEST(GIVEN_a_view_with_no_data_WHEN_advance_load_balance_enforcer_THEN_return_no_auth_delegation_assignment_requests) +{ + SpatialGDK::FDispatcher Dispatcher; + SpatialGDK::EntityView View; + const TUniquePtr VirtualWorkerTranslator = CreateVirtualWorkerTranslator(); + SpatialGDK::FSubView SubView(SpatialConstants::LB_TAG_COMPONENT_ID, SpatialGDK::FSubView::NoFilter, &View, Dispatcher, + SpatialGDK::FSubView::NoDispatcherCallbacks); + + bool bInvoked = false; + + SpatialGDK::SpatialLoadBalanceEnforcer LoadBalanceEnforcer = SpatialGDK::SpatialLoadBalanceEnforcer( + ThisWorker, SubView, VirtualWorkerTranslator.Get(), [&bInvoked](SpatialGDK::EntityComponentUpdate) { + bInvoked = true; + }); + + // The view has no entities in it. We expect the enforcer not to produce any authority delegation requests. + LoadBalanceEnforcer.Advance(); + TestFalse("LoadBalanceEnforcer did not try to send an authority delegation update", bInvoked); + + return true; +} + +LOADBALANCEENFORCER_TEST( + GIVEN_load_balance_enforcer_with_valid_mapping_WHEN_asked_for_auth_delegation_assignments_THEN_return_correct_auth_delegation_assignment_requests) +{ + SpatialGDK::FDispatcher Dispatcher; + SpatialGDK::EntityView View; + const TUniquePtr VirtualWorkerTranslator = CreateVirtualWorkerTranslator(); + + AddLBEntityToView(View, EntityIdOne, ThisWorkerId, OtherVirtualWorker); + + SpatialGDK::FSubView SubView(SpatialConstants::LB_TAG_COMPONENT_ID, SpatialGDK::FSubView::NoFilter, &View, Dispatcher, + SpatialGDK::FSubView::NoDispatcherCallbacks); + + TArray Updates; + + SpatialGDK::SpatialLoadBalanceEnforcer LoadBalanceEnforcer = SpatialGDK::SpatialLoadBalanceEnforcer( + ThisWorker, SubView, VirtualWorkerTranslator.Get(), [&Updates](SpatialGDK::EntityComponentUpdate Update) { + Updates.Add(MoveTemp(Update)); + }); + + LoadBalanceEnforcer.ShortCircuitMaybeRefreshAuthorityDelegation(EntityIdOne); + + bool bSuccess = true; + if (Updates.Num() == 1) + { + bSuccess &= Updates[0].EntityId == EntityIdOne; + bSuccess &= Updates[0].Update.GetComponentId() == SpatialConstants::AUTHORITY_DELEGATION_COMPONENT_ID; + AuthorityDelegationMap AuthDelegationMap = GetAuthDelegationMapFromUpdate(Updates[0]); + bSuccess &= AuthorityMapDelegatesComponents(AuthDelegationMap, OtherWorkerId, NonAuthDelegationLBComponents); + } + else + { + bSuccess = false; + } + + TestTrue("LoadBalanceEnforcer returned expected authority delegation assignment results", bSuccess); + + return true; +} + +LOADBALANCEENFORCER_TEST(GIVEN_authority_intent_change_op_WHEN_we_inform_load_balance_enforcer_THEN_send_auth_delegation_update) +{ + SpatialGDK::FDispatcher Dispatcher; + SpatialGDK::EntityView View; + SpatialGDK::ViewDelta Delta; + const TUniquePtr VirtualWorkerTranslator = CreateVirtualWorkerTranslator(); + + AddLBEntityToView(View, EntityIdOne, OtherWorkerId, ThisVirtualWorker); + + SpatialGDK::FSubView SubView(SpatialConstants::LB_TAG_COMPONENT_ID, SpatialGDK::FSubView::NoFilter, &View, Dispatcher, + SpatialGDK::FSubView::NoDispatcherCallbacks); + + TArray Updates; + + SpatialGDK::SpatialLoadBalanceEnforcer LoadBalanceEnforcer = SpatialGDK::SpatialLoadBalanceEnforcer( + ThisWorker, SubView, VirtualWorkerTranslator.Get(), [&Updates](SpatialGDK::EntityComponentUpdate Update) { + Updates.Add(MoveTemp(Update)); + }); + + PopulateViewDeltaWithComponentUpdated( + Delta, View, EntityIdOne, + MakeComponentUpdateFromUpdate(SpatialGDK::AuthorityIntent::CreateAuthorityIntentUpdate(ThisVirtualWorker))); + SubView.Advance(Delta); + LoadBalanceEnforcer.Advance(); + + bool bSuccess = true; + if (Updates.Num() == 1) + { + bSuccess &= Updates[0].EntityId == EntityIdOne; + bSuccess &= Updates[0].Update.GetComponentId() == SpatialConstants::AUTHORITY_DELEGATION_COMPONENT_ID; + AuthorityDelegationMap AuthDelegationMap = GetAuthDelegationMapFromUpdate(Updates[0]); + bSuccess &= AuthorityMapDelegatesComponents(AuthDelegationMap, ThisWorkerId, NonAuthDelegationLBComponents); + } + else + { + bSuccess = false; + } + + TestTrue("LoadBalanceEnforcer returned expected authority delegation assignment results", bSuccess); + + return true; +} + +LOADBALANCEENFORCER_TEST( + GIVEN_net_owning_client_change_op_WHEN_we_advance_load_balance_enforcer_THEN_auth_delegation_update_contains_new_client_delegation) +{ + SpatialGDK::FDispatcher Dispatcher; + SpatialGDK::EntityView View; + SpatialGDK::ViewDelta Delta; + const TUniquePtr VirtualWorkerTranslator = CreateVirtualWorkerTranslator(); + + AddLBEntityToView(View, EntityIdOne, ThisWorkerId, ThisVirtualWorker, ClientWorkerId); + + SpatialGDK::FSubView SubView(SpatialConstants::LB_TAG_COMPONENT_ID, SpatialGDK::FSubView::NoFilter, &View, Dispatcher, + SpatialGDK::FSubView::NoDispatcherCallbacks); + + TArray Updates; + + SpatialGDK::SpatialLoadBalanceEnforcer LoadBalanceEnforcer = SpatialGDK::SpatialLoadBalanceEnforcer( + ThisWorker, SubView, VirtualWorkerTranslator.Get(), [&Updates](SpatialGDK::EntityComponentUpdate Update) { + Updates.Add(MoveTemp(Update)); + }); + + // The net owning component uses the full workerdId:{worker} form for its data format, which is a real gotcha. + PopulateViewDeltaWithComponentUpdated( + Delta, View, EntityIdOne, + MakeComponentUpdateFromUpdate(SpatialGDK::NetOwningClientWorker::CreateNetOwningClientWorkerUpdate(OtherClientWorkerId))); + SubView.Advance(Delta); + LoadBalanceEnforcer.Advance(); + + bool bSuccess = true; + if (Updates.Num() == 1) + { + bSuccess &= Updates[0].EntityId == EntityIdOne; + bSuccess &= Updates[0].Update.GetComponentId() == SpatialConstants::AUTHORITY_DELEGATION_COMPONENT_ID; + AuthorityDelegationMap AuthDelegationMap = GetAuthDelegationMapFromUpdate(Updates[0]); + bSuccess &= AuthorityMapDelegatesComponents(AuthDelegationMap, OtherClientWorkerId, ClientComponentIds); + } + else + { + bSuccess = false; + } + + TestTrue("LoadBalanceEnforcer returned expected authority delegation assignment results", bSuccess); + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/RPCServiceTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/RPCServiceTest.cpp index 9a814ccb45..b66e8a273f 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Tests/RPCServiceTest.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/RPCServiceTest.cpp @@ -2,31 +2,30 @@ #include "CoreMinimal.h" -//Engine -#include "Engine/Engine.h" -#include "GameFramework/GameStateBase.h" +// Engine #include "Misc/AutomationTest.h" -#include "Tests/AutomationCommon.h" -#include "Tests/TestActor.h" #include "Tests/TestDefinitions.h" -#include "Tests/TestingComponentViewHelpers.h" -//GDK +// GDK +#include "Interop/RPCs/SpatialRPCService.h" #include "Interop/SpatialReceiver.h" -#include "Interop/SpatialRPCService.h" -#include "Interop/SpatialStaticComponentView.h" #include "Schema/RPCPayload.h" #include "SpatialConstants.h" #include "SpatialGDKSettings.h" +#include "SpatialView/ComponentData.h" +#include "SpatialView/Dispatcher.h" +#include "SpatialView/EntityComponentTypes.h" +#include "SpatialView/EntityView.h" +#include "SpatialView/SubView.h" +#include "SpatialView/ViewDelta.h" +#include "Tests/SpatialView/SpatialViewUtils.h" #include "Utils/RPCRingBuffer.h" -#define RPC_SERVICE_TEST(TestName) \ - GDK_TEST(Core, SpatialRPCService, TestName) +#define RPC_SERVICE_TEST(TestName) GDK_TEST(Core, SpatialRPCService, TestName) // Test Globals namespace { - enum ERPCEndpointType : uint8_t { SERVER_AUTH, @@ -46,7 +45,8 @@ struct EntityPayload EntityPayload(Worker_EntityId InEntityID, const SpatialGDK::RPCPayload& InPayload) : EntityId(InEntityID) , Payload(InPayload) - {} + { + } Worker_EntityId EntityId; SpatialGDK::RPCPayload Payload; @@ -57,131 +57,140 @@ 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; -}); +// Initialise view and subviews. These will be overwritten before using. +SpatialGDK::EntityView TestView; +SpatialGDK::FDispatcher TestDispatcher; +SpatialGDK::FSubView AuthSubView = SpatialGDK::FSubView(SpatialConstants::ACTOR_AUTH_TAG_COMPONENT_ID, SpatialGDK::FSubView::NoFilter, + &TestView, TestDispatcher, SpatialGDK::FSubView::NoDispatcherCallbacks); +SpatialGDK::FSubView NonAuthSubView = + SpatialGDK::FSubView(SpatialConstants::ACTOR_NON_AUTH_TAG_COMPONENT_ID, SpatialGDK::FSubView::NoFilter, &TestView, TestDispatcher, + SpatialGDK::FSubView::NoDispatcherCallbacks); +FTimerManager Timer; + +SpatialGDK::ComponentData MakeComponentDataFromData(Schema_ComponentData* Data, const Worker_ComponentId ComponentId) +{ + return SpatialGDK::ComponentData(SpatialGDK::OwningComponentDataPtr(Data), ComponentId); +} -Worker_Authority GetClientAuthorityFromRPCEndpointType(ERPCEndpointType RPCEndpointType) +void AddClientAuthorityFromRPCEndpointType(SpatialGDK::EntityView& View, const Worker_EntityId EntityId, + const ERPCEndpointType RPCEndpointType) { - switch (RPCEndpointType) + if (RPCEndpointType == CLIENT_AUTH || RPCEndpointType == SERVER_AND_CLIENT_AUTH) { - case CLIENT_AUTH: - case SERVER_AND_CLIENT_AUTH: - return WORKER_AUTHORITY_AUTHORITATIVE; - break; - case SERVER_AUTH: - default: - return WORKER_AUTHORITY_NOT_AUTHORITATIVE; - break; + AddAuthorityToView(View, EntityId, SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID); } } -Worker_Authority GetServerAuthorityFromRPCEndpointType(ERPCEndpointType RPCEndpointType) +void AddServerAuthorityFromRPCEndpointType(SpatialGDK::EntityView& View, const Worker_EntityId EntityId, + const ERPCEndpointType RPCEndpointType) { - switch (RPCEndpointType) + if (RPCEndpointType == SERVER_AUTH || RPCEndpointType == SERVER_AND_CLIENT_AUTH) { - case SERVER_AUTH: - case SERVER_AND_CLIENT_AUTH: - return WORKER_AUTHORITY_AUTHORITATIVE; - break; - case CLIENT_AUTH: - default: - return WORKER_AUTHORITY_NOT_AUTHORITATIVE; - break; + AddAuthorityToView(View, EntityId, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID); } } -Worker_Authority GetMulticastAuthorityFromRPCEndpointType(ERPCEndpointType RPCEndpointType) +void AddMulticastAuthorityFromRPCEndpointType(SpatialGDK::EntityView& View, const Worker_EntityId EntityId, + const ERPCEndpointType RPCEndpointType) { - return GetServerAuthorityFromRPCEndpointType(RPCEndpointType); + if (RPCEndpointType == SERVER_AUTH || RPCEndpointType == SERVER_AND_CLIENT_AUTH) + { + AddAuthorityToView(View, EntityId, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID); + } } -void AddEntityToStaticComponentView(USpatialStaticComponentView& StaticComponentView, Worker_EntityId EntityId, ERPCEndpointType RPCEndpointType) +void AddRPCEntityToView(SpatialGDK::EntityView& View, const Worker_EntityId EntityId, const ERPCEndpointType RPCEndpointType, + SpatialGDK::ComponentData ClientData = SpatialGDK::ComponentData{ SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID }) { - 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)); + AddEntityToView(View, EntityId); + AddComponentToView(View, EntityId, SpatialGDK::ComponentData{ SpatialConstants::ACTOR_NON_AUTH_TAG_COMPONENT_ID }); + if (RPCEndpointType != NO_AUTH) + { + AddComponentToView(View, EntityId, SpatialGDK::ComponentData{ SpatialConstants::ACTOR_AUTH_TAG_COMPONENT_ID }); + } + AddComponentToView(View, EntityId, MoveTemp(ClientData)); + AddComponentToView(View, EntityId, SpatialGDK::ComponentData{ SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID }); + AddComponentToView(View, EntityId, SpatialGDK::ComponentData{ SpatialConstants::MULTICAST_RPCS_COMPONENT_ID }); + AddClientAuthorityFromRPCEndpointType(View, EntityId, RPCEndpointType); + AddServerAuthorityFromRPCEndpointType(View, EntityId, RPCEndpointType); + AddMulticastAuthorityFromRPCEndpointType(View, EntityId, RPCEndpointType); }; -USpatialStaticComponentView* CreateStaticComponentView(const TArray& EntityIdArray, ERPCEndpointType RPCEndpointType) +void PopulateView(SpatialGDK::EntityView& View, const TArray& EntityIdArray, const ERPCEndpointType RPCEndpointType) { - USpatialStaticComponentView* StaticComponentView = NewObject(); for (Worker_EntityId EntityId : EntityIdArray) { - AddEntityToStaticComponentView(*StaticComponentView, EntityId, RPCEndpointType); + AddRPCEntityToView(View, EntityId, RPCEndpointType); } - return StaticComponentView; } -SpatialGDK::SpatialRPCService CreateRPCService(const TArray& EntityIdArray, - ERPCEndpointType RPCEndpointType, - ExtractRPCDelegate RPCDelegate = DefaultRPCDelegate, - USpatialStaticComponentView* StaticComponentView = nullptr) +// Creates an RPC service with initial authority dependent on the RPCEndpointType for the entities specified in +// EntityIdArray. +SpatialGDK::SpatialRPCService CreateRPCService(const TArray& EntityIdArray, const ERPCEndpointType RPCEndpointType, + SpatialGDK::EntityView& View) { - if (StaticComponentView == nullptr) + // Remove all callbacks. + TestDispatcher = SpatialGDK::FDispatcher(); + // If the passed view is empty, we populate it with new entities with the given entity IDs. + if (View.Num() == 0) { - StaticComponentView = CreateStaticComponentView(EntityIdArray, RPCEndpointType); + PopulateView(View, EntityIdArray, RPCEndpointType); } - SpatialGDK::SpatialRPCService RPCService = SpatialGDK::SpatialRPCService(RPCDelegate, StaticComponentView, nullptr); + AuthSubView = SpatialGDK::FSubView(SpatialConstants::ACTOR_AUTH_TAG_COMPONENT_ID, SpatialGDK::FSubView::NoFilter, &View, TestDispatcher, + SpatialGDK::FSubView::NoDispatcherCallbacks); + NonAuthSubView = SpatialGDK::FSubView(SpatialConstants::ACTOR_NON_AUTH_TAG_COMPONENT_ID, SpatialGDK::FSubView::NoFilter, &View, + TestDispatcher, SpatialGDK::FSubView::NoDispatcherCallbacks); + SpatialGDK::SpatialRPCService RPCService = SpatialGDK::SpatialRPCService(AuthSubView, NonAuthSubView, nullptr, nullptr, nullptr); - for (Worker_EntityId EntityId : EntityIdArray) - { - if (GetClientAuthorityFromRPCEndpointType(RPCEndpointType) == WORKER_AUTHORITY_AUTHORITATIVE) - { - RPCService.OnEndpointAuthorityGained(EntityId, SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID); - } + const SpatialGDK::ViewDelta Delta; + AuthSubView.Advance(Delta); + NonAuthSubView.Advance(Delta); - if (GetServerAuthorityFromRPCEndpointType(RPCEndpointType) == WORKER_AUTHORITY_AUTHORITATIVE) - { - RPCService.OnEndpointAuthorityGained(EntityId, SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID); - } - } + RPCService.AdvanceView(); + RPCService.ProcessChanges(0); 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; + 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) +bool CompareSchemaObjectToSendAndPayload(Schema_Object* SchemaObject, const SpatialGDK::RPCPayload& Payload, const ERPCType RPCType, + const uint64 RPCId) { - SpatialGDK::RPCRingBufferDescriptor Descriptor = SpatialGDK::RPCRingBufferUtils::GetRingBufferDescriptor(RPCType); + const 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) +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; + return CompareSchemaObjectToSendAndPayload(Schema_GetComponentUpdateFields(Update.Update.schema_type), EntityPayloadItem.Payload, + RPCType, RPCId) + && Update.EntityId == EntityPayloadItem.EntityId; } -bool CompareComponentDataAndEntityPayload(const FWorkerComponentData& ComponentData, const EntityPayload& EntityPayloadItem, ERPCType RPCType, uint64 RPCId) +bool CompareComponentDataAndEntityPayload(const FWorkerComponentData& ComponentData, const EntityPayload& EntityPayloadItem, + const ERPCType RPCType, const uint64 RPCId) { - return CompareSchemaObjectToSendAndPayload(Schema_GetComponentDataFields(ComponentData.schema_type), EntityPayloadItem.Payload, RPCType, RPCId); + return CompareSchemaObjectToSendAndPayload(Schema_GetComponentDataFields(ComponentData.schema_type), EntityPayloadItem.Payload, RPCType, + RPCId); } -FWorkerComponentData GetComponentDataOnEntityCreationFromRPCService(SpatialGDK::SpatialRPCService& RPCService, Worker_EntityId EntityID, ERPCType RPCType) +FWorkerComponentData GetComponentDataOnEntityCreationFromRPCService(SpatialGDK::SpatialRPCService& RPCService, + const Worker_EntityId EntityID, const ERPCType RPCType) { Worker_ComponentId ExpectedUpdateComponentId = SpatialGDK::RPCRingBufferUtils::GetRingBufferComponentId(RPCType); TArray ComponentDataArray = RPCService.GetRPCComponentsOnEntityCreation(EntityID); - const FWorkerComponentData* ComponentData = ComponentDataArray.FindByPredicate([ExpectedUpdateComponentId](const FWorkerComponentData& CompData) { - return CompData.component_id == ExpectedUpdateComponentId; - }); + const FWorkerComponentData* ComponentData = + ComponentDataArray.FindByPredicate([ExpectedUpdateComponentId](const FWorkerComponentData& CompData) { + return CompData.component_id == ExpectedUpdateComponentId; + }); if (ComponentData == nullptr) { @@ -194,174 +203,200 @@ FWorkerComponentData GetComponentDataOnEntityCreationFromRPCService(SpatialGDK:: 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, false); - TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::Success)); + SpatialGDK::EntityView View; + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH, View); + const SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload, false); + TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::Success); return true; } RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_client_unreliable_rpcs_to_the_service_THEN_rpc_push_result_success) { - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH); - SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientUnreliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::Success)); + SpatialGDK::EntityView View; + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH, View); + const SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientUnreliable, SimplePayload, false); + TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::Success); return true; } -RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_server_reliable_rpcs_to_the_service_THEN_rpc_push_result_no_buffer_authority) +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, false); - TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority)); + SpatialGDK::EntityView View; + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH, View); + const SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerReliable, SimplePayload, false); + TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority); return true; } -RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_server_unreliable_rpcs_to_the_service_THEN_rpc_push_result_no_buffer_authority) +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, false); - TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority)); + SpatialGDK::EntityView View; + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH, View); + const SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerUnreliable, SimplePayload, false); + TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority); return true; } -RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_client_reliable_rpcs_to_the_service_THEN_rpc_push_result_no_buffer_authority) +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, false); - TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority)); + SpatialGDK::EntityView View; + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH, View); + const SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload, false); + TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority); return true; } -RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_client_unreliable_rpcs_to_the_service_THEN_rpc_push_result_no_buffer_authority) +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, false); - TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority)); + SpatialGDK::EntityView View; + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH, View); + const SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientUnreliable, SimplePayload, false); + TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority); return true; } RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_server_reliable_rpcs_to_the_service_THEN_rpc_push_result_success) { - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH); - SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerReliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::Success)); + SpatialGDK::EntityView View; + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH, View); + const SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerReliable, SimplePayload, false); + TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::Success); return true; } RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_server_unreliable_rpcs_to_the_service_THEN_rpc_push_result_success) { - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH); - SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerUnreliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::Success)); + SpatialGDK::EntityView View; + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH, View); + const SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerUnreliable, SimplePayload, false); + TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::Success); return true; } RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_multicast_rpcs_to_the_service_THEN_rpc_push_result_no_buffer_authority) { - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH); - SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload, false); - TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority)); + SpatialGDK::EntityView View; + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH, View); + const SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload, false); + 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, false); - TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::Success)); + SpatialGDK::EntityView View; + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH, View); + const SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload, false); + 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, false); - TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::HasAckAuthority)); + SpatialGDK::EntityView View; + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AND_CLIENT_AUTH, View); + const SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload, false); + TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::HasAckAuthority); return true; } -RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_overflow_client_reliable_rpcs_to_the_service_THEN_rpc_push_result_queue_overflowed) +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); + SpatialGDK::EntityView View; + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH, View); // Send RPCs to the point where we will overflow - uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ClientReliable); + const uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ClientReliable); for (uint32 i = 0; i < RPCsToSend; ++i) { RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload, false); } - SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::QueueOverflowed)); + const SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload, false); + TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::QueueOverflowed); return true; } -RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_overflow_client_unreliable_rpcs_to_the_service_THEN_rpc_push_result_drop_overflow) +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); + SpatialGDK::EntityView View; + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH, View); // Send RPCs to the point where we will overflow - uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ClientUnreliable); + const uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ClientUnreliable); for (uint32 i = 0; i < RPCsToSend; ++i) { RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientUnreliable, SimplePayload, false); } - SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientUnreliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::DropOverflowed)); + const SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientUnreliable, SimplePayload, false); + TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::DropOverflowed); return true; } -RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_overflow_client_reliable_rpcs_to_the_service_THEN_rpc_push_result_queue_overflowed) +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); + SpatialGDK::EntityView View; + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH, View); // Send RPCs to the point where we will overflow - uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ServerReliable); + const uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ServerReliable); for (uint32 i = 0; i < RPCsToSend; ++i) { RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerReliable, SimplePayload, false); } - SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerReliable, SimplePayload, false); - TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::QueueOverflowed)); + const SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerReliable, SimplePayload, false); + TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::QueueOverflowed); return true; } -RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_overflow_client_unreliable_rpcs_to_the_service_THEN_rpc_push_result_drop_overflow) +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); + SpatialGDK::EntityView View; + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, CLIENT_AUTH, View); // Send RPCs to the point where we will overflow - uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ServerUnreliable); + const uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ServerUnreliable); for (uint32 i = 0; i < RPCsToSend; ++i) { RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerUnreliable, SimplePayload, false); } - SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerUnreliable, SimplePayload, false); + const SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerUnreliable, SimplePayload, false); TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::DropOverflowed)); return true; } RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_overflow_multicast_rpcs_to_the_service_THEN_rpc_push_result_success) { - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH); + SpatialGDK::EntityView View; + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({ RPCTestEntityId_1 }, SERVER_AUTH, View); // Send RPCs to the point where we will overflow - uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::NetMulticast); + const uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::NetMulticast); for (uint32 i = 0; i < RPCsToSend; ++i) { RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload, false); } - SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload, false); - TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::Success)); + const SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload, false); + TestTrue("Push RPC returned expected results", Result == SpatialGDK::EPushRPCResult::Success); return true; } -RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_server_unreliable_rpcs_to_the_service_THEN_payloads_are_writen_correctly_to_component_updates) +RPC_SERVICE_TEST( + GIVEN_authority_over_client_endpoint_WHEN_push_server_unreliable_rpcs_to_the_service_THEN_payloads_are_written_correctly_to_component_updates) { + SpatialGDK::EntityView View; TArray EntityPayloads; EntityPayloads.Add(EntityPayload(RPCTestEntityId_1, SimplePayload)); EntityPayloads.Add(EntityPayload(RPCTestEntityId_2, SimplePayload)); @@ -370,7 +405,7 @@ RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_server_unreliabl EntityIdArray.Add(RPCTestEntityId_1); EntityIdArray.Add(RPCTestEntityId_2); - SpatialGDK::SpatialRPCService RPCService = CreateRPCService(EntityIdArray, CLIENT_AUTH); + SpatialGDK::SpatialRPCService RPCService = CreateRPCService(EntityIdArray, CLIENT_AUTH, View); for (const EntityPayload& EntityPayloadItem : EntityPayloads) { RPCService.PushRPC(EntityPayloadItem.EntityId, ERPCType::ServerUnreliable, EntityPayloadItem.Payload, false); @@ -401,192 +436,34 @@ RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_server_unreliabl RPC_SERVICE_TEST(GIVEN_no_authority_over_rpc_endpoint_WHEN_push_client_reliable_rpcs_to_the_service_THEN_component_data_matches_payload) { + SpatialGDK::EntityView View; // Create RPCService with empty component view - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({}, NO_AUTH); + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({}, NO_AUTH, View); RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload, false); - FWorkerComponentData ComponentData = GetComponentDataOnEntityCreationFromRPCService(RPCService, RPCTestEntityId_1, ERPCType::ClientReliable); - bool bTestPassed = CompareComponentDataAndEntityPayload(ComponentData, EntityPayload(RPCTestEntityId_1, SimplePayload), ERPCType::ClientReliable, 1); + const FWorkerComponentData ComponentData = + GetComponentDataOnEntityCreationFromRPCService(RPCService, RPCTestEntityId_1, ERPCType::ClientReliable); + const bool bTestPassed = + CompareComponentDataAndEntityPayload(ComponentData, EntityPayload(RPCTestEntityId_1, SimplePayload), ERPCType::ClientReliable, 1); TestTrue("Entity creation test returned expected results", bTestPassed); return true; } RPC_SERVICE_TEST(GIVEN_no_authority_over_rpc_endpoint_WHEN_push_multicast_rpcs_to_the_service_THEN_initially_present_set) { + SpatialGDK::EntityView View; // Create RPCService with empty component view - SpatialGDK::SpatialRPCService RPCService = CreateRPCService({}, NO_AUTH); + SpatialGDK::SpatialRPCService RPCService = CreateRPCService({}, NO_AUTH, View); RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload, false); RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload, false); - FWorkerComponentData ComponentData = GetComponentDataOnEntityCreationFromRPCService(RPCService, RPCTestEntityId_1, ERPCType::NetMulticast); + const FWorkerComponentData ComponentData = + GetComponentDataOnEntityCreationFromRPCService(RPCService, RPCTestEntityId_1, ERPCType::NetMulticast); const Schema_Object* SchemaObject = Schema_GetComponentDataFields(ComponentData.schema_type); - uint32 InitiallyPresent = Schema_GetUint32(SchemaObject, SpatialGDK::RPCRingBufferUtils::GetInitiallyPresentMulticastRPCsCountFieldId()); + const 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; -} - -DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FWaitForWorld, TSharedPtr, Data); -bool FWaitForWorld::Update() -{ - 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; - } - } - - if (World != nullptr && World->AreActorsInitialized()) - { - AGameStateBase* GameState = World->GetGameState(); - if (GameState != nullptr && GameState->HasMatchStarted()) - { - Data->TestWorld = World; - return true; - } - } - - return false; -} - -DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FSpawnActor, TSharedPtr, Data); -bool FSpawnActor::Update() -{ - FActorSpawnParameters SpawnParams; - SpawnParams.bNoFail = true; - SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; - - AActor* Actor = Data->TestWorld->SpawnActor(SpawnParams); - Data->Actor = Actor; - - return true; -} - -DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FWaitForActor, TSharedPtr, Data); -bool FWaitForActor::Update() -{ - AActor* Actor = Data->Actor; - return (IsValid(Actor) && Actor->IsActorInitialized() && Actor->HasActorBegunPlay()); -} - -DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER(FDropRPCQueueTest, FAutomationTestBase*, Test, TSharedPtr, Data); -bool FDropRPCQueueTest::Update() -{ - if (!ensure(Test != nullptr)) - { - return true; - } - - if (!ensure(Data != nullptr) || - !ensure(Data->Actor != nullptr)) - { - Test->TestTrue("Correct RPC queue command was returned by the receiver after attempting to process an RPC without authority over the actor", false); - } - - AActor* Actor = Data->Actor; - USpatialNetDriver* SpatialNetDriver = Cast(Data->TestWorld->NetDriver); - Worker_EntityId EntityId = SpatialNetDriver->PackageMap->GetEntityIdFromObject(Actor); - - Schema_ComponentData* ClientComponentData = Schema_CreateComponentData(); - Schema_Object* ClientSchemaObject = Schema_GetComponentDataFields(ClientComponentData); - - // Write RPC to ring buffer. - const FRPCInfo& RPCInfo = SpatialNetDriver->ClassInfoManager->GetRPCInfo(Actor, ATestActor::StaticClass()->FindFunctionByName("TestServerRPC")); - const SpatialGDK::RPCPayload RPCPayload = SpatialGDK::RPCPayload(0, RPCInfo.Index, TArray({}, 0)); - SpatialGDK::RPCRingBufferUtils::WriteRPCToSchema(ClientSchemaObject, ERPCType::ClientReliable, 1, RPCPayload); - - const ERPCEndpointType EndpointType = SERVER_AUTH; - TestingComponentViewHelpers::AddEntityComponentToStaticComponentView(*SpatialNetDriver->StaticComponentView, - EntityId, SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID, - ClientComponentData, - GetClientAuthorityFromRPCEndpointType(EndpointType)); - TestingComponentViewHelpers::AddEntityComponentToStaticComponentView(*SpatialNetDriver->StaticComponentView, - EntityId, SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID, - GetServerAuthorityFromRPCEndpointType(EndpointType)); - - USpatialReceiver* Receiver = SpatialNetDriver->Receiver; - SpatialGDK::SpatialRPCService* RPCService = SpatialNetDriver->GetRPCService(); - RPCService->OnEndpointAuthorityGained(EntityId, SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID); - - bool bTestSuccess = false; - FProcessRPCDelegate RPCDelegate = FProcessRPCDelegate::CreateLambda([&Receiver, &bTestSuccess](const FPendingRPCParams& Params) - { - FRPCErrorInfo RPCErrorInfo = Receiver->ApplyRPC(Params); - bTestSuccess = RPCErrorInfo.ErrorCode == ERPCResult::NoAuthority; - bTestSuccess &= RPCErrorInfo.QueueProcessResult == ERPCQueueProcessResult::DropEntireQueue; - - return RPCErrorInfo; - }); - - // Bind new process function. - FRPCContainer& RPCContainer = Receiver->GetRPCContainer(); - RPCContainer.BindProcessingFunction(RPCDelegate); - - // Change actor authority and process. - Actor->Role = ROLE_SimulatedProxy; - RPCService->ExtractRPCsForEntity(EntityId, SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID); - - RPCContainer.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(Receiver, &USpatialReceiver::ApplyRPC)); - - Test->TestTrue("Correct RPC queue command was returned by the receiver after attempting to process an RPC without authority over the actor", bTestSuccess); - - return true; -} - -#if (WITH_DEV_AUTOMATION_TESTS || WITH_PERF_AUTOMATION_TESTS) // Automation* functions only available with these flags - -RPC_SERVICE_TEST(GIVEN_receiving_an_rpc_whose_target_we_do_not_have_authority_over_WHEN_we_process_the_rpc_THEN_return_DropEntireQueue_queue_command) -{ - AutomationOpenMap("/Engine/Maps/Entry"); - - TSharedPtr Data = MakeShared(); - - ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); - ADD_LATENT_AUTOMATION_COMMAND(FSpawnActor(Data)); - ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data)); - ADD_LATENT_AUTOMATION_COMMAND(FDropRPCQueueTest(this, Data)); - - return true; -} - -#endif // (WITH_DEV_AUTOMATION_TESTS || WITH_PERF_AUTOMATION_TESTS) diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/AuthorityRecordTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/AuthorityRecordTest.cpp index ce62590273..47e0facc4e 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/AuthorityRecordTest.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/AuthorityRecordTest.cpp @@ -4,27 +4,26 @@ #include "SpatialView/AuthorityRecord.h" -#define AUTHORITYRECORD_TEST(TestName) \ - GDK_TEST(Core, AuthorityRecord, TestName) +#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; +class AuthorityChangeRecordFixture +{ +public: + const Worker_EntityId kTestEntityId = 1337; + const Worker_ComponentId kTestComponentId = 1338; - const EntityComponentId kEntityComponentId{ kTestEntityId, kTestComponentId }; + const EntityComponentId kEntityComponentId{ kTestEntityId, kTestComponentId }; - AuthorityRecord Record; + AuthorityRecord Record; - TArray ExpectedAuthorityGained; - TArray ExpectedAuthorityLost; - TArray ExpectedAuthorityLostTemporarily; - }; + TArray ExpectedAuthorityGained; + TArray ExpectedAuthorityLost; + TArray ExpectedAuthorityLostTemporarily; +}; } // anonymous namespace AUTHORITYRECORD_TEST(GIVEN_EmptyAuthorityRecord_WHEN_set_to_authoritative_THEN_AuthorityRecord_has_AuthorityGainedRecord) @@ -39,7 +38,8 @@ AUTHORITYRECORD_TEST(GIVEN_EmptyAuthorityRecord_WHEN_set_to_authoritative_THEN_A 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); + TestTrue(TEXT("Comparing AuthorityLostTemporarily"), + Fixture.Record.GetAuthorityLostTemporarily() == Fixture.ExpectedAuthorityLostTemporarily); return true; } @@ -56,7 +56,8 @@ AUTHORITYRECORD_TEST(GIVEN_AuthorityRecord_with_AuthoritativeRecord_WHEN_set_to_ // 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); + TestTrue(TEXT("Comparing AuthorityLostTemporarily"), + Fixture.Record.GetAuthorityLostTemporarily() == Fixture.ExpectedAuthorityLostTemporarily); return true; } @@ -73,7 +74,8 @@ AUTHORITYRECORD_TEST(GIVEN_empty_AuthorityRecord_WHEN_set_to_NonAuthoritative_TH 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); + TestTrue(TEXT("Comparing AuthorityLostTemporarily"), + Fixture.Record.GetAuthorityLostTemporarily() == Fixture.ExpectedAuthorityLostTemporarily); return true; } @@ -91,12 +93,14 @@ AUTHORITYRECORD_TEST(GIVEN_AuthorityRecord_with_NonAuthoritativeRecord_WHEN_set_ 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); + 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) +AUTHORITYRECORD_TEST( + GIVEN_AuthorityRecord_with_NonAuthoritativeRecord_WHEN_set_to_Authoritative_and_NonAuthoritative_THEN_has_AuthorityLostRecord) { // GIVEN AuthorityChangeRecordFixture Fixture; @@ -110,12 +114,14 @@ AUTHORITYRECORD_TEST(GIVEN_AuthorityRecord_with_NonAuthoritativeRecord_WHEN_set_ 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); + 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) +AUTHORITYRECORD_TEST( + GIVEN_AuthorityRecord_with_AuthoritativeRecord_NonAuthoritativeRecord_and_AuthorityLostTemporarilyRecorde_WHEN_Cleared_THEN_has_no_records) { // GIVEN AuthorityChangeRecordFixture Fixture; @@ -128,7 +134,8 @@ AUTHORITYRECORD_TEST(GIVEN_AuthorityRecord_with_AuthoritativeRecord_NonAuthorita 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); + TestTrue(TEXT("Comparing AuthorityLostTemporarily"), + Fixture.Record.GetAuthorityLostTemporarily() == Fixture.ExpectedAuthorityLostTemporarily); // WHEN Fixture.Record.Clear(); @@ -139,7 +146,8 @@ AUTHORITYRECORD_TEST(GIVEN_AuthorityRecord_with_AuthoritativeRecord_NonAuthorita 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); + TestTrue(TEXT("Comparing AuthorityLostTemporarily"), + Fixture.Record.GetAuthorityLostTemporarily() == Fixture.ExpectedAuthorityLostTemporarily); return true; } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/CallbacksTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/CallbacksTest.cpp new file mode 100644 index 0000000000..79e4cb1b23 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/CallbacksTest.cpp @@ -0,0 +1,205 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/TestDefinitions.h" + +#include "SpatialView/Callbacks.h" + +#define CALLBACKS_TEST(TestName) GDK_TEST(Core, Callbacks, TestName) + +using FIntCallback = SpatialGDK::TCallbacks::CallbackType; + +CALLBACKS_TEST(GIVEN_Callbacks_With_Callback_WHEN_IsEmpty_Called_THEN_Returns_False) +{ + // GIVEN + SpatialGDK::TCallbacks Callbacks; + TestTrue("Callbacks is initially empty", Callbacks.IsEmpty()); + + // WHEN + const FIntCallback Callback; + Callbacks.Register(1, Callback); + const bool IsEmpty = Callbacks.IsEmpty(); + + // THEN + TestFalse("Callbacks is no longer empty", IsEmpty); + + return true; +} + +CALLBACKS_TEST(GIVEN_Callbacks_With_Callback_WHEN_Invoke_Called_THEN_Invokes_Callback) +{ + // GIVEN + SpatialGDK::TCallbacks Callbacks; + bool Invoked = false; + const FIntCallback Callback = [&Invoked](int) { + Invoked = true; + }; + Callbacks.Register(1, Callback); + + // WHEN + Callbacks.Invoke(0); + + // THEN + TestTrue("Callback was invoked", Invoked); + + return true; +} + +CALLBACKS_TEST(GIVEN_Callbacks_With_Callback_WHEN_Invoke_Called_THEN_Invokes_Callback_With_Value) +{ + // GIVEN + int CorrectValue = 1; + SpatialGDK::TCallbacks Callbacks; + int InvokeCountWithCorrectValue = 0; + const FIntCallback Callback = [&InvokeCountWithCorrectValue, CorrectValue](int Value) { + if (Value == CorrectValue) + { + InvokeCountWithCorrectValue++; + } + }; + Callbacks.Register(1, Callback); + + // WHEN + Callbacks.Invoke(1); + Callbacks.Invoke(0); + Callbacks.Invoke(1); + + // THEN + TestEqual("Callback was invoked with the correct value", InvokeCountWithCorrectValue, 2); + + return true; +} + +CALLBACKS_TEST(GIVEN_Callbacks_With_Two_Callback_WHEN_Invoke_Called_THEN_Invokes_Both_Callbacks) +{ + // GIVEN + SpatialGDK::TCallbacks Callbacks; + int InvokeCount = 0; + const FIntCallback Callback = [&InvokeCount](int) { + InvokeCount++; + }; + Callbacks.Register(1, Callback); + Callbacks.Register(2, Callback); + + // WHEN + Callbacks.Invoke(1); + + // THEN + TestEqual("Callback was invoked twice", InvokeCount, 2); + + return true; +} + +CALLBACKS_TEST(GIVEN_Callbacks_With_Callback_WHEN_Callback_Removed_THEN_No_Longer_Calls_Back) +{ + // GIVEN + SpatialGDK::CallbackId Id = 1; + SpatialGDK::TCallbacks Callbacks; + int InvokeCount = 0; + const FIntCallback Callback = [&InvokeCount](int) { + InvokeCount++; + }; + Callbacks.Register(Id, Callback); + + // WHEN + Callbacks.Invoke(1); + Callbacks.Remove(Id); + Callbacks.Invoke(1); + + // THEN + TestEqual("Callback was invoked once", InvokeCount, 1); + + return true; +} + +CALLBACKS_TEST(GIVEN_Callbacks_With_Callback_WHEN_Callback_Adds_Other_Callback_THEN_Only_Calls_First_Callback) +{ + // GIVEN + SpatialGDK::TCallbacks Callbacks; + int InvokeCount = 0; + const FIntCallback SecondCallback = [&InvokeCount](int) { + InvokeCount++; + }; + const FIntCallback FirstCallback = [&InvokeCount, &Callbacks, SecondCallback](int) { + InvokeCount++; + Callbacks.Register(2, SecondCallback); + }; + Callbacks.Register(1, FirstCallback); + + // WHEN + Callbacks.Invoke(1); + + // THEN + TestEqual("Callback was invoked once", InvokeCount, 1); + + // sanity check: Both callbacks invoked on second invocation + InvokeCount = 0; + Callbacks.Invoke(1); + TestEqual("Both callbacks invoked", InvokeCount, 2); + + return true; +} + +CALLBACKS_TEST(GIVEN_Callbacks_With_Two_Callback_WHEN_Callback_Removes_Other_Callback_THEN_Calls_Both_Callbacks) +{ + // GIVEN + SpatialGDK::CallbackId Id = 2; + SpatialGDK::TCallbacks Callbacks; + int InvokeCount = 0; + const FIntCallback SecondCallback = [&InvokeCount](int) { + InvokeCount++; + }; + const FIntCallback FirstCallback = [&InvokeCount, &Callbacks, Id](int) { + InvokeCount++; + Callbacks.Remove(Id); + }; + Callbacks.Register(1, FirstCallback); + Callbacks.Register(Id, SecondCallback); + + // WHEN + Callbacks.Invoke(1); + + // THEN + TestEqual("Both callbacks were invoked", InvokeCount, 2); + + // sanity check: Only one callback invoked on second invocation + InvokeCount = 0; + Callbacks.Invoke(1); + TestEqual("Only one callback invoked", InvokeCount, 1); + + return true; +} + +CALLBACKS_TEST(GIVEN_Callbacks_WHEN_Callbacks_Add_And_Remove_Other_Callback_THEN_Other_Callback_Not_Invoked) +{ + // GIVEN + SpatialGDK::CallbackId Id = 1; + SpatialGDK::CallbackId AddId = 2; + SpatialGDK::CallbackId RemoveId = 3; + SpatialGDK::TCallbacks Callbacks; + bool Invoked = false; + const FIntCallback Callback = [&Invoked](int) { + Invoked = true; + }; + const FIntCallback AddCallback = [&Callbacks, Id, Callback](int) { + Callbacks.Register(Id, Callback); + }; + const FIntCallback RemoveCallback = [&Callbacks, Id](int) { + Callbacks.Remove(Id); + }; + Callbacks.Register(RemoveId, RemoveCallback); + Callbacks.Register(AddId, AddCallback); + + // WHEN + Callbacks.Invoke(1); + + // THEN + TestFalse("Callback was not invoked", Invoked); + + // sanity check: Not invoked on a second call after the other callbacks have been removed + Callbacks.Remove(AddId); + Callbacks.Remove(RemoveId); + Callbacks.Invoke(1); + TestFalse("Callback was not invoked", Invoked); + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/CommandRetryHandlerTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/CommandRetryHandlerTest.cpp new file mode 100644 index 0000000000..96922b935b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/CommandRetryHandlerTest.cpp @@ -0,0 +1,281 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialView/Callbacks.h" +#include "SpatialView/CommandRetryHandlerImpl.h" +#include "SpatialView/ComponentData.h" +#include "Tests/SpatialView/ExpectedMessagesToSend.h" +#include "Tests/SpatialView/SpatialViewUtils.h" +#include "Tests/TestDefinitions.h" + +#define COMMANDRETRYHANDLER_TEST(TestName) GDK_TEST(Core, CommandRetryHandler, TestName) + +namespace SpatialGDK +{ +namespace +{ +const Worker_EntityId TestEntityId = 1; +const Worker_RequestId TestRequestId = 2; +const Worker_RequestId RetryRequestId = -TestRequestId; +const Worker_ComponentId TestComponentId = 3; +const double TestComponentValue = 20; +const Worker_CommandIndex TestCommandIndex = 4; +const uint32 TestNumOfEntities = 10; +const float TimeAdvanced = 5.f; +OpList EmptyOpList = {}; +constexpr FRetryData TWO_RETRIES = { 2, 0, 0.1f, 5.0f, 0 }; +const Worker_ComponentSetId TestComponentSetId = 5; +const FComponentSetData ComponentSetData = { { { TestComponentSetId, { TestComponentId } } } }; +} // anonymous namespace + +EntityQuery CreateTestEntityQuery() +{ + Worker_EntityQuery WorkerEntityQuery; + WorkerEntityQuery.constraint.constraint_type = WORKER_CONSTRAINT_TYPE_ENTITY_ID; + WorkerEntityQuery.constraint.constraint.entity_id_constraint = Worker_EntityIdConstraint{ TestEntityId }; + WorkerEntityQuery.snapshot_result_type_component_id_count = 1; + TArray WorkerComponentIds = { TestComponentId }; + WorkerEntityQuery.snapshot_result_type_component_ids = WorkerComponentIds.GetData(); + return EntityQuery(WorkerEntityQuery); +} + +COMMANDRETRYHANDLER_TEST(GIVEN_success_WHEN_process_ops_THEN_no_retry) +{ + WorkerView View(ComponentSetData); + TCommandRetryHandler Handler; + + EntityComponentOpListBuilder Builder; + Builder.AddCreateEntityCommandResponse(TestEntityId, TestRequestId, WORKER_STATUS_CODE_SUCCESS, StringStorage("Success")); + OpList FirstOpList = MoveTemp(Builder).CreateOpList(); + TArray EntityComponents; + EntityComponents.Add(CreateTestComponentData(TestComponentId, TestComponentValue)); + Handler.SendRequest(1, { MoveTemp(EntityComponents), TestEntityId }, RETRY_UNTIL_COMPLETE, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, FirstOpList, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, EmptyOpList, View); + + const TUniquePtr ActualMessagesPtr = View.FlushLocalChanges(); + TestTrue("MessagesToSend are equal", ExpectedMessagesToSend().Compare(*ActualMessagesPtr.Get())); + return true; +} + +COMMANDRETRYHANDLER_TEST(GIVEN_time_out_WHEN_create_entity_THEN_retry) +{ + WorkerView View(ComponentSetData); + TCommandRetryHandler Handler; + TArray EntityComponents; + EntityComponents.Add(CreateTestComponentData(TestComponentId, TestComponentValue)); + + EntityComponentOpListBuilder Builder; + Builder.AddCreateEntityCommandResponse(TestEntityId, TestRequestId, WORKER_STATUS_CODE_TIMEOUT, StringStorage("Time out")); + OpList FirstOpList = MoveTemp(Builder).CreateOpList(); + + Handler.SendRequest(TestRequestId, { MoveTemp(EntityComponents), TestEntityId }, RETRY_UNTIL_COMPLETE, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, FirstOpList, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, EmptyOpList, View); + + ExpectedMessagesToSend TestMessages; + TArray TestComponents; + TestComponents.Add(CreateTestComponentData(TestComponentId, TestComponentValue)); + TestMessages.AddCreateEntityRequest(TestRequestId, TestEntityId, MoveTemp(TestComponents)); + const TUniquePtr ActualMessagesPtr = View.FlushLocalChanges(); + TestTrue("MessagesToSend are equal", TestMessages.Compare(*ActualMessagesPtr.Get())); + return true; +} + +COMMANDRETRYHANDLER_TEST(GIVEN_time_out_WHEN_reserve_entity_ids_THEN_retry) +{ + WorkerView View(ComponentSetData); + TCommandRetryHandler Handler; + + EntityComponentOpListBuilder Builder; + Builder.AddReserveEntityIdsCommandResponse(TestEntityId, TestNumOfEntities, TestRequestId, WORKER_STATUS_CODE_TIMEOUT, + StringStorage("Time out")); + OpList FirstOpList = MoveTemp(Builder).CreateOpList(); + Handler.SendRequest(TestRequestId, TestNumOfEntities, RETRY_UNTIL_COMPLETE, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, FirstOpList, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, EmptyOpList, View); + + ExpectedMessagesToSend TestMessages; + TestMessages.AddReserveEntityIdsRequest(TestRequestId, TestNumOfEntities); + const TUniquePtr ActualMessagesPtr = View.FlushLocalChanges(); + TestTrue("MessagesToSend are equal", TestMessages.Compare(*ActualMessagesPtr.Get())); + return true; +} + +COMMANDRETRYHANDLER_TEST(GIVEN_application_error_WHEN_reserve_entity_ids_THEN_no_retry) +{ + WorkerView View(ComponentSetData); + TCommandRetryHandler Handler; + + EntityComponentOpListBuilder Builder; + Builder.AddReserveEntityIdsCommandResponse(TestEntityId, TestNumOfEntities, TestRequestId, WORKER_STATUS_CODE_APPLICATION_ERROR, + StringStorage("Application Error")); + OpList FirstOpList = MoveTemp(Builder).CreateOpList(); + Handler.SendRequest(TestRequestId, TestNumOfEntities, RETRY_UNTIL_COMPLETE, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, FirstOpList, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, EmptyOpList, View); + + const TUniquePtr ActualMessagesPtr = View.FlushLocalChanges(); + TestTrue("MessagesToSend are equal", ExpectedMessagesToSend().Compare(*ActualMessagesPtr.Get())); + return true; +} + +COMMANDRETRYHANDLER_TEST(GIVEN_time_out_WHEN_delete_entity_THEN_retry) +{ + WorkerView View(ComponentSetData); + TCommandRetryHandler Handler; + + EntityComponentOpListBuilder Builder; + Builder.AddDeleteEntityCommandResponse(TestEntityId, TestRequestId, WORKER_STATUS_CODE_TIMEOUT, StringStorage("Time out")); + OpList FirstOpList = MoveTemp(Builder).CreateOpList(); + Handler.SendRequest(TestRequestId, { TestEntityId }, RETRY_UNTIL_COMPLETE, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, FirstOpList, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, EmptyOpList, View); + + ExpectedMessagesToSend TestMessages; + TestMessages.AddDeleteEntityCommandRequest(TestRequestId, TestEntityId); + const TUniquePtr ActualMessagesPtr = View.FlushLocalChanges(); + TestTrue("MessagesToSend are equal", TestMessages.Compare(*ActualMessagesPtr.Get())); + return true; +} + +COMMANDRETRYHANDLER_TEST(GIVEN_time_out_WHEN_query_entity_THEN_retry) +{ + WorkerView View(ComponentSetData); + TCommandRetryHandler Handler; + + EntityComponentOpListBuilder Builder; + TArray Entities; + OpListEntity Entity; + Entity.EntityId = TestEntityId; + Entity.Components.Add(CreateTestComponentData(TestComponentId, TestComponentValue)); + Entities.Add(MoveTemp(Entity)); + Builder.AddEntityQueryCommandResponse(TestRequestId, MoveTemp(Entities), WORKER_STATUS_CODE_TIMEOUT, StringStorage("Time out")); + OpList FirstOpList = MoveTemp(Builder).CreateOpList(); + Handler.SendRequest(TestRequestId, CreateTestEntityQuery(), RETRY_UNTIL_COMPLETE, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, FirstOpList, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, EmptyOpList, View); + + ExpectedMessagesToSend TestMessages; + TestMessages.AddEntityQueryRequest(TestRequestId, CreateTestEntityQuery()); + const TUniquePtr ActualMessagesPtr = View.FlushLocalChanges(); + TestTrue("MessagesToSend are equal", TestMessages.Compare(*ActualMessagesPtr.Get())); + return true; +} + +COMMANDRETRYHANDLER_TEST(GIVEN_time_out_WHEN_entity_command_request_THEN_retry) +{ + WorkerView View(ComponentSetData); + TCommandRetryHandler Handler; + + EntityComponentOpListBuilder Builder; + Builder.AddEntityCommandResponse(TestEntityId, TestRequestId, WORKER_STATUS_CODE_TIMEOUT, StringStorage("Time out")); + OpList FirstOpList = MoveTemp(Builder).CreateOpList(); + Handler.SendRequest(TestRequestId, { TestEntityId, CommandRequest(TestComponentId, TestCommandIndex) }, RETRY_UNTIL_COMPLETE, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, FirstOpList, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, EmptyOpList, View); + + ExpectedMessagesToSend TestMessages; + TestMessages.AddEntityCommandRequest(TestRequestId, TestEntityId, TestComponentId, TestCommandIndex); + const TUniquePtr ActualMessagesPtr = View.FlushLocalChanges(); + TestTrue("MessagesToSend are equal", TestMessages.Compare(*ActualMessagesPtr.Get())); + return true; +} + +COMMANDRETRYHANDLER_TEST(GIVEN_multiple_time_outs_WHEN_entity_command_request_THEN_no_retry) +{ + WorkerView View(ComponentSetData); + TCommandRetryHandler Handler; + + // send request and receive first failure + EntityComponentOpListBuilder Builder; + Builder.AddEntityCommandResponse(TestEntityId, TestRequestId, WORKER_STATUS_CODE_TIMEOUT, StringStorage("Time out")); + OpList FirstOpList = MoveTemp(Builder).CreateOpList(); + Handler.SendRequest(TestRequestId, { TestEntityId, CommandRequest(TestComponentId, TestCommandIndex) }, TWO_RETRIES, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, FirstOpList, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, EmptyOpList, View); + + ExpectedMessagesToSend TestMessages; + TestMessages.AddEntityCommandRequest(TestRequestId, TestEntityId, TestComponentId, TestCommandIndex); + TUniquePtr ActualMessagesPtr = View.FlushLocalChanges(); + TestTrue("MessagesToSend are equal", TestMessages.Compare(*ActualMessagesPtr.Get())); + + // Second failure, try again + Builder = EntityComponentOpListBuilder(); + Builder.AddEntityCommandResponse(TestEntityId, TestRequestId, WORKER_STATUS_CODE_TIMEOUT, StringStorage("Time out")); + OpList SecondOpList = MoveTemp(Builder).CreateOpList(); + Handler.ProcessOps(TimeAdvanced, SecondOpList, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, EmptyOpList, View); + + TestMessages = ExpectedMessagesToSend(); + TestMessages.AddEntityCommandRequest(TestRequestId, TestEntityId, TestComponentId, TestCommandIndex); + ActualMessagesPtr = View.FlushLocalChanges(); + TestTrue("MessagesToSend are equal", TestMessages.Compare(*ActualMessagesPtr.Get())); + + // Third failure, no retry + Builder = EntityComponentOpListBuilder(); + Builder.AddEntityCommandResponse(TestEntityId, TestRequestId, WORKER_STATUS_CODE_TIMEOUT, StringStorage("Time out")); + OpList ThirdOpList = MoveTemp(Builder).CreateOpList(); + Handler.ProcessOps(TimeAdvanced, ThirdOpList, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, EmptyOpList, View); + + ActualMessagesPtr = View.FlushLocalChanges(); + TestTrue("MessagesToSend are equal", ExpectedMessagesToSend().Compare(*ActualMessagesPtr.Get())); + return true; +} + +COMMANDRETRYHANDLER_TEST(GIVEN_authority_lost_WHEN_entity_command_request_THEN_retry) +{ + WorkerView View(ComponentSetData); + TCommandRetryHandler Handler; + + EntityComponentOpListBuilder Builder; + Builder.AddEntityCommandResponse(TestEntityId, TestRequestId, WORKER_STATUS_CODE_AUTHORITY_LOST, StringStorage("Authority Lost")); + OpList FirstOpList = MoveTemp(Builder).CreateOpList(); + Handler.SendRequest(TestRequestId, { TestEntityId, CommandRequest(TestComponentId, TestCommandIndex) }, RETRY_UNTIL_COMPLETE, View); + View.FlushLocalChanges(); + + Handler.ProcessOps(TimeAdvanced, FirstOpList, View); + + ExpectedMessagesToSend TestMessages; + TestMessages.AddEntityCommandRequest(TestRequestId, TestEntityId, TestComponentId, TestCommandIndex); + const TUniquePtr ActualMessagesPtr = View.FlushLocalChanges(); + TestTrue("MessagesToSend are equal", TestMessages.Compare(*ActualMessagesPtr.Get())); + return true; +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/DispatcherTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/DispatcherTest.cpp new file mode 100644 index 0000000000..d5a91592e8 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/DispatcherTest.cpp @@ -0,0 +1,337 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialView/Callbacks.h" +#include "SpatialView/ComponentData.h" +#include "SpatialView/ComponentSetData.h" +#include "SpatialView/Dispatcher.h" +#include "SpatialView/EntityDelta.h" +#include "SpatialView/EntityView.h" +#include "SpatialView/OpList/EntityComponentOpList.h" +#include "SpatialView/ViewDelta.h" +#include "Tests/SpatialView/ComponentTestUtils.h" +#include "Tests/SpatialView/SpatialViewUtils.h" +#include "Tests/TestDefinitions.h" + +#define DISPATCHER_TEST(TestName) GDK_TEST(Core, Dispatcher, TestName) + +namespace +{ +constexpr Worker_ComponentId COMPONENT_ID = 1000; +constexpr Worker_ComponentId OTHER_COMPONENT_ID = 1001; +constexpr Worker_EntityId ENTITY_ID = 1; +constexpr Worker_EntityId OTHER_ENTITY_ID = 2; +constexpr double COMPONENT_VALUE = 3; +constexpr double OTHER_COMPONENT_VALUE = 4; +constexpr Worker_ComponentSetId COMPONENT_SET_ID = 1000; +constexpr Worker_ComponentSetId OTHER_COMPONENT_SET_ID = 1001; + +const SpatialGDK::FComponentSetData COMPONENT_SET_DATA = { { { COMPONENT_SET_ID, { COMPONENT_ID } }, + { OTHER_COMPONENT_SET_ID, { OTHER_COMPONENT_ID } } } }; +} // anonymous namespace + +namespace SpatialGDK +{ +DISPATCHER_TEST(GIVEN_Dispatcher_WHEN_Callback_Added_Then_Invoked_THEN_Callback_Invoked_With_Correct_Values) +{ + bool Invoked = false; + FDispatcher Dispatcher; + EntityView View; + ViewDelta Delta; + + const FComponentValueCallback Callback = [&Invoked](const FEntityComponentChange& Change) { + if (Change.EntityId == ENTITY_ID && Change.Change.ComponentId == COMPONENT_ID + && GetValueFromTestComponentData(Change.Change.Data) == COMPONENT_VALUE) + { + Invoked = true; + } + }; + Dispatcher.RegisterComponentAddedCallback(COMPONENT_ID, Callback); + + AddEntityToView(View, ENTITY_ID); + PopulateViewDeltaWithComponentAdded(Delta, View, ENTITY_ID, CreateTestComponentData(COMPONENT_ID, COMPONENT_VALUE)); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + + TestTrue("Callback was invoked", Invoked); + + // Now a few more times, but with incorrect values, just in case + Invoked = false; + + PopulateViewDeltaWithComponentAdded(Delta, View, ENTITY_ID, CreateTestComponentData(COMPONENT_ID, OTHER_COMPONENT_VALUE)); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + TestFalse("Callback was not invoked", Invoked); + + PopulateViewDeltaWithComponentAdded(Delta, View, ENTITY_ID, CreateTestComponentData(OTHER_COMPONENT_ID, COMPONENT_VALUE)); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + TestFalse("Callback was not invoked", Invoked); + + AddEntityToView(View, OTHER_ENTITY_ID); + PopulateViewDeltaWithComponentAdded(Delta, View, OTHER_ENTITY_ID, CreateTestComponentData(COMPONENT_ID, COMPONENT_VALUE)); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + TestFalse("Callback was not invoked", Invoked); + + return true; +} + +DISPATCHER_TEST(GIVEN_Dispatcher_With_Callback_WHEN_Callback_Removed_THEN_Callback_Not_Invoked) +{ + bool Invoked = false; + FDispatcher Dispatcher; + EntityView View; + ViewDelta Delta; + + const FComponentValueCallback Callback = [&Invoked](const FEntityComponentChange&) { + Invoked = true; + }; + + const CallbackId Id = Dispatcher.RegisterComponentAddedCallback(COMPONENT_ID, Callback); + AddEntityToView(View, ENTITY_ID); + PopulateViewDeltaWithComponentAdded(Delta, View, ENTITY_ID, CreateTestComponentData(COMPONENT_ID, COMPONENT_VALUE)); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + + TestTrue("Callback was invoked", Invoked); + + Invoked = false; + Dispatcher.RemoveCallback(Id); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + + TestFalse("Callback was not invoked again", Invoked); + + return true; +} + +DISPATCHER_TEST(GIVEN_Dispatcher_WHEN_Callback_Added_And_Invoked_THEN_Callback_Invoked_With_Correct_Values) +{ + bool Invoked = false; + FDispatcher Dispatcher; + EntityView View; + ViewDelta Delta; + + const FComponentValueCallback Callback = [&Invoked](const FEntityComponentChange& Change) { + if (Change.EntityId == ENTITY_ID && Change.Change.ComponentId == COMPONENT_ID + && GetValueFromTestComponentData(Change.Change.Data) == COMPONENT_VALUE) + { + Invoked = true; + } + }; + + AddEntityToView(View, ENTITY_ID); + AddComponentToView(View, ENTITY_ID, CreateTestComponentData(COMPONENT_ID, COMPONENT_VALUE)); + + Dispatcher.RegisterAndInvokeComponentAddedCallback(COMPONENT_ID, Callback, View); + + TestTrue("Callback was invoked", Invoked); + + // Double check the callback is actually called on invocation as well. + View[ENTITY_ID].Components.Empty(); + Invoked = false; + PopulateViewDeltaWithComponentAdded(Delta, View, ENTITY_ID, CreateTestComponentData(COMPONENT_ID, COMPONENT_VALUE)); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + + TestTrue("Callback was invoked", Invoked); + + return true; +} + +DISPATCHER_TEST(GIVEN_Dispatcher_WHEN_Component_Changed_Callback_Added_Then_Invoked_THEN_Callback_Invoked) +{ + bool Invoked = false; + FDispatcher Dispatcher; + EntityView View; + ViewDelta Delta; + + const FComponentValueCallback Callback = [&Invoked](const FEntityComponentChange&) { + Invoked = true; + }; + Dispatcher.RegisterComponentValueCallback(COMPONENT_ID, Callback); + + AddEntityToView(View, ENTITY_ID); + PopulateViewDeltaWithComponentAdded(Delta, View, ENTITY_ID, CreateTestComponentData(COMPONENT_ID, COMPONENT_VALUE)); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + + TestTrue("Callback was invoked", Invoked); + + PopulateViewDeltaWithComponentUpdated(Delta, View, ENTITY_ID, CreateTestComponentUpdate(COMPONENT_ID, OTHER_COMPONENT_VALUE)); + Invoked = false; + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + + TestTrue("Callback was invoked again", Invoked); + + return true; +} + +DISPATCHER_TEST(GIVEN_Dispatcher_WHEN_Component_Removed_Callback_Added_Then_Invoked_THEN_Callback_Invoked) +{ + bool Invoked = false; + FDispatcher Dispatcher; + EntityView View; + ViewDelta Delta; + + const FComponentValueCallback Callback = [&Invoked](const FEntityComponentChange&) { + Invoked = true; + }; + Dispatcher.RegisterComponentRemovedCallback(COMPONENT_ID, Callback); + + AddEntityToView(View, ENTITY_ID); + AddComponentToView(View, ENTITY_ID, ComponentData{ COMPONENT_ID }); + + PopulateViewDeltaWithComponentRemoved(Delta, View, ENTITY_ID, COMPONENT_ID); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + + TestTrue("Callback was invoked", Invoked); + + return true; +} + +DISPATCHER_TEST(GIVEN_Dispatcher_WHEN_Authority_Gained_Callback_Added_Then_Invoked_THEN_Callback_Invoked) +{ + bool Invoked = false; + FDispatcher Dispatcher; + EntityView View; + ViewDelta Delta; + + const FEntityCallback Callback = [&Invoked](const Worker_EntityId&) { + Invoked = true; + }; + Dispatcher.RegisterAuthorityGainedCallback(COMPONENT_ID, Callback); + + AddEntityToView(View, ENTITY_ID); + AddComponentToView(View, ENTITY_ID, ComponentData(COMPONENT_ID)); + + PopulateViewDeltaWithAuthorityChange(Delta, View, ENTITY_ID, COMPONENT_SET_ID, WORKER_AUTHORITY_AUTHORITATIVE, COMPONENT_SET_DATA); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + + TestTrue("Callback was invoked", Invoked); + + return true; +} + +DISPATCHER_TEST(GIVEN_Dispatcher_WHEN_Authority_Lost_Callback_Added_Then_Invoked_THEN_Callback_Invoked) +{ + bool Invoked = false; + FDispatcher Dispatcher; + EntityView View; + ViewDelta Delta; + + const FEntityCallback Callback = [&Invoked](const Worker_EntityId&) { + Invoked = true; + }; + Dispatcher.RegisterAuthorityLostCallback(COMPONENT_SET_ID, Callback); + + AddEntityToView(View, ENTITY_ID); + AddComponentToView(View, ENTITY_ID, ComponentData{ COMPONENT_ID }); + AddAuthorityToView(View, ENTITY_ID, COMPONENT_SET_ID); + + PopulateViewDeltaWithAuthorityChange(Delta, View, ENTITY_ID, COMPONENT_ID, WORKER_AUTHORITY_NOT_AUTHORITATIVE, COMPONENT_SET_DATA); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + + TestTrue("Callback was invoked", Invoked); + + return true; +} + +DISPATCHER_TEST(GIVEN_Dispatcher_WHEN_Authority_Lost_Temp_Callback_Added_Then_Invoked_THEN_Callback_Invoked) +{ + bool Invoked = false; + FDispatcher Dispatcher; + EntityView View; + ViewDelta Delta; + + const FEntityCallback Callback = [&Invoked](const Worker_EntityId&) { + Invoked = true; + }; + Dispatcher.RegisterAuthorityLostTempCallback(COMPONENT_ID, Callback); + + AddEntityToView(View, ENTITY_ID); + AddComponentToView(View, ENTITY_ID, ComponentData(COMPONENT_ID)); + AddAuthorityToView(View, ENTITY_ID, COMPONENT_SET_ID); + + PopulateViewDeltaWithAuthorityLostTemp(Delta, View, ENTITY_ID, COMPONENT_ID, COMPONENT_SET_DATA); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + + TestTrue("Callback was invoked", Invoked); + + return true; +} + +DISPATCHER_TEST(GIVEN_Dispatcher_WHEN_Many_Callbacks_Added_Then_Invoked_THEN_All_Callbacks_Correctly_Invoked) +{ + int InvokeCount = 0; + int NumberOfCallbacks = 100; + FDispatcher Dispatcher; + EntityView View; + ViewDelta Delta; + + const FComponentValueCallback Callback = [&InvokeCount](const FEntityComponentChange&) { + ++InvokeCount; + }; + for (int i = 0; i < NumberOfCallbacks; ++i) + { + Dispatcher.RegisterComponentAddedCallback(COMPONENT_ID, Callback); + } + + AddEntityToView(View, ENTITY_ID); + PopulateViewDeltaWithComponentAdded(Delta, View, ENTITY_ID, CreateTestComponentData(COMPONENT_ID, COMPONENT_VALUE)); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + + TestEqual("Callback was invoked the expected number of times", InvokeCount, NumberOfCallbacks); + + return true; +} + +DISPATCHER_TEST(GIVEN_Dispatcher_With_Component_Removed_Callback_WHEN_Entity_Removed_THEN_Callback_Invoked) +{ + bool Invoked = false; + FDispatcher Dispatcher; + EntityView View; + ViewDelta Delta; + + const FComponentValueCallback Callback = [&Invoked](const FEntityComponentChange&) { + Invoked = true; + }; + Dispatcher.RegisterComponentRemovedCallback(COMPONENT_ID, Callback); + + AddEntityToView(View, ENTITY_ID); + AddComponentToView(View, ENTITY_ID, ComponentData(COMPONENT_ID)); + + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.RemoveComponent(ENTITY_ID, COMPONENT_ID); + OpListBuilder.RemoveEntity(ENTITY_ID); + + SetFromOpList(Delta, View, MoveTemp(OpListBuilder), COMPONENT_SET_DATA); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + + TestTrue("Callback was invoked", Invoked); + + return true; +} + +DISPATCHER_TEST( + GIVEN_Dispatcher_With_Component_Removed_Callback_WHEN_Entity_Removed_And_Added_With_Different_Components_THEN_Callback_Invoked) +{ + bool Invoked = false; + FDispatcher Dispatcher; + EntityView View; + ViewDelta Delta; + + const FComponentValueCallback Callback = [&Invoked](const FEntityComponentChange&) { + Invoked = true; + }; + Dispatcher.RegisterComponentRemovedCallback(COMPONENT_ID, Callback); + + AddEntityToView(View, ENTITY_ID); + AddComponentToView(View, ENTITY_ID, ComponentData(COMPONENT_ID)); + + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.RemoveComponent(ENTITY_ID, COMPONENT_ID); + OpListBuilder.RemoveEntity(ENTITY_ID); + OpListBuilder.AddEntity(ENTITY_ID); + OpListBuilder.AddComponent(ENTITY_ID, ComponentData(OTHER_COMPONENT_ID)); + + SetFromOpList(Delta, View, MoveTemp(OpListBuilder), COMPONENT_SET_DATA); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + + TestTrue("Callback was invoked", Invoked); + + return true; +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentRecordTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentRecordTest.cpp index ce200264ca..8845264850 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentRecordTest.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentRecordTest.cpp @@ -4,21 +4,19 @@ #include "SpatialView/EntityComponentRecord.h" -#include "EntityComponentTestUtils.h" +#include "Tests/SpatialView/ComponentTestUtils.h" -#define ENTITYCOMPONENTRECORD_TEST(TestName) \ - GDK_TEST(Core, EntityComponentRecord, TestName) +#define ENTITYCOMPONENTRECORD_TEST(TestName) GDK_TEST(Core, EntityComponentRecord, TestName) namespace SpatialGDK { - namespace { - const Worker_EntityId TEST_ENTITY_ID = 1337; - const Worker_ComponentId TEST_COMPONENT_ID = 1338; - const double TEST_VALUE = 7331; - const double TEST_UPDATE_VALUE = 7332; -} // anonymous namespace +const Worker_EntityId TEST_ENTITY_ID = 1337; +const Worker_ComponentId TEST_COMPONENT_ID = 1338; +const double TEST_VALUE = 7331; +const double TEST_UPDATE_VALUE = 7332; +} // anonymous namespace ENTITYCOMPONENTRECORD_TEST(GIVEN_empty_component_record_WHEN_component_added_THEN_has_component_data) { @@ -142,7 +140,8 @@ ENTITYCOMPONENTRECORD_TEST(GIVEN_empty_component_record_WHEN_updated_added_THEN_ return true; } -ENTITYCOMPONENTRECORD_TEST(GIVEN_component_record_with_component_WHEN_complete_update_added_THEN_component_record_has_updated_component_data) +ENTITYCOMPONENTRECORD_TEST( + GIVEN_component_record_with_component_WHEN_complete_update_added_THEN_component_record_has_updated_component_data) { // GIVEN ComponentData TestData = CreateTestComponentData(TEST_COMPONENT_ID, TEST_VALUE); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentUpdateRecordTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentUpdateRecordTest.cpp index 2c2f0ef96e..77b92a3ccb 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentUpdateRecordTest.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentUpdateRecordTest.cpp @@ -4,28 +4,26 @@ #include "SpatialView/EntityComponentUpdateRecord.h" -#include "EntityComponentTestUtils.h" +#include "Tests/SpatialView/ComponentTestUtils.h" -#define ENTITYCOMPONENTUPDATERECORD_TEST(TestName) \ - GDK_TEST(Core, EntityComponentUpdateRecord, TestName) +#define ENTITYCOMPONENTUPDATERECORD_TEST(TestName) GDK_TEST(Core, EntityComponentUpdateRecord, TestName) namespace SpatialGDK { - namespace { - const Worker_EntityId TEST_ENTITY_ID = 1337; +const Worker_EntityId TEST_ENTITY_ID = 1337; - const Worker_ComponentId TEST_COMPONENT_ID = 1338; - const Worker_ComponentId COMPONENT_ID_TO_REMOVE = 1347; - const Worker_ComponentId COMPONENT_ID_TO_KEEP = 1348; +const Worker_ComponentId TEST_COMPONENT_ID = 1338; +const Worker_ComponentId COMPONENT_ID_TO_REMOVE = 1347; +const Worker_ComponentId COMPONENT_ID_TO_KEEP = 1348; - const int EVENT_VALUE = 7332; +const int EVENT_VALUE = 7332; - const double TEST_VALUE = 7331; - const double TEST_UPDATE_VALUE = 7332; - const double UPDATE_VALUE = 7333; -} // anonymous namespace +const double TEST_VALUE = 7331; +const double TEST_UPDATE_VALUE = 7332; +const double UPDATE_VALUE = 7333; +} // anonymous namespace ENTITYCOMPONENTUPDATERECORD_TEST(GIVEN_empty_update_record_WHEN_update_added_THEN_update_record_has_the_update) { @@ -130,7 +128,8 @@ ENTITYCOMPONENTUPDATERECORD_TEST(GIVEN_update_record_with_a_complete_update_WHEN const TArray ExpectedUpdates{}; TArray ExpectedCompleteUpdates; - ExpectedCompleteUpdates.Push(EntityComponentCompleteUpdate{ TEST_ENTITY_ID, MoveTemp(ExpectedCompleteUpdate), MoveTemp(ExpectedEvent) }); + ExpectedCompleteUpdates.Push( + EntityComponentCompleteUpdate{ TEST_ENTITY_ID, MoveTemp(ExpectedCompleteUpdate), MoveTemp(ExpectedEvent) }); EntityComponentUpdateRecord Storage; Storage.AddComponentDataAsUpdate(TEST_ENTITY_ID, MoveTemp(CompleteUpdate)); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/ExpectedMessagesToSend.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/ExpectedMessagesToSend.cpp new file mode 100644 index 0000000000..2a5b532fca --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/ExpectedMessagesToSend.cpp @@ -0,0 +1,93 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/SpatialView/ExpectedMessagesToSend.h" +#include "Tests/SpatialView/CommandTestUtils.h" + +namespace SpatialGDK +{ +ExpectedMessagesToSend& ExpectedMessagesToSend::AddCreateEntityRequest(Worker_RequestId RequestId, Worker_EntityId EntityId, + TArray ComponentData) +{ + CreateEntityRequest TestCreateEntityRequest; + TestCreateEntityRequest.RequestId = RequestId; + TestCreateEntityRequest.EntityId = EntityId; + TestCreateEntityRequest.EntityComponents = MoveTemp(ComponentData); + TestCreateEntityRequest.TimeoutMillis = 0; + CreateEntityRequests.Push(MoveTemp(TestCreateEntityRequest)); + return *this; +} + +ExpectedMessagesToSend& ExpectedMessagesToSend::AddEntityCommandRequest(Worker_RequestId RequestId, Worker_EntityId EntityId, + Worker_ComponentId ComponentId, Worker_CommandIndex CommandIndex) +{ + EntityCommandRequests.Push({ EntityId, RequestId, CommandRequest(ComponentId, CommandIndex), 0 }); + return *this; +} + +ExpectedMessagesToSend& ExpectedMessagesToSend::AddDeleteEntityCommandRequest(Worker_RequestId RequestId, Worker_EntityId EntityId) +{ + DeleteEntityRequests.Push({ RequestId, EntityId, 0 }); + return *this; +} + +ExpectedMessagesToSend& ExpectedMessagesToSend::AddReserveEntityIdsRequest(Worker_RequestId RequestId, uint32 NumOfEntities) +{ + ReserveEntityIdsRequests.Push({ RequestId, NumOfEntities, 0 }); + return *this; +} + +ExpectedMessagesToSend& ExpectedMessagesToSend::AddEntityQueryRequest(Worker_RequestId RequestId, EntityQuery Query) +{ + EntityQueryRequests.Push({ RequestId, MoveTemp(Query), 0 }); + return *this; +} + +ExpectedMessagesToSend& ExpectedMessagesToSend::AddEntityCommandResponse(Worker_RequestId RequestId, Worker_ComponentId ComponentId, + Worker_CommandIndex CommandIndex) +{ + EntityCommandResponse Response{ RequestId, CommandResponse(ComponentId, CommandIndex) }; + EntityCommandResponses.Push(MoveTemp(Response)); + return *this; +} + +ExpectedMessagesToSend& ExpectedMessagesToSend::AddEntityCommandFailure(Worker_RequestId RequestId, FString Message) +{ + EntityCommandFailures.Push({ RequestId, Message }); + return *this; +} + +bool ExpectedMessagesToSend::Compare(const MessagesToSend& MessagesToSend) const +{ + if (!AreEquivalent(ReserveEntityIdsRequests, MessagesToSend.ReserveEntityIdsRequests, CompareReseverEntityIdsRequests)) + { + return false; + } + + if (!AreEquivalent(CreateEntityRequests, MessagesToSend.CreateEntityRequests, CompareCreateEntityRequests)) + { + return false; + } + + if (!AreEquivalent(DeleteEntityRequests, MessagesToSend.DeleteEntityRequests, CompareDeleteEntityRequests)) + { + return false; + } + + if (!AreEquivalent(EntityQueryRequests, MessagesToSend.EntityQueryRequests, CompareEntityQueryRequests)) + { + return false; + } + + if (!AreEquivalent(EntityCommandRequests, MessagesToSend.EntityCommandRequests, CompareEntityCommandRequests)) + { + return false; + } + + if (!AreEquivalent(EntityCommandResponses, MessagesToSend.EntityCommandResponses, CompareEntityCommandResponses)) + { + return false; + } + + return AreEquivalent(EntityCommandFailures, MessagesToSend.EntityCommandFailures, CompareEntityCommandFailuers); +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/ExpectedViewDelta.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/ExpectedViewDelta.cpp new file mode 100644 index 0000000000..a21646678b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/ExpectedViewDelta.cpp @@ -0,0 +1,203 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/SpatialView/ExpectedViewDelta.h" + +#include "Algo/Compare.h" +#include "SpatialView/ViewDelta.h" +#include "Tests/SpatialView/ComponentTestUtils.h" +#include "Tests/SpatialView/ExpectedEntityDelta.h" + +namespace SpatialGDK +{ +ExpectedViewDelta& ExpectedViewDelta::AddEntityDelta(const Worker_EntityId EntityId, const EntityChangeType ChangeType) +{ + EntityDeltas.Add( + EntityId, { EntityId, ChangeType == UPDATE ? ExpectedEntityDelta::UPDATE + : ChangeType == ADD ? ExpectedEntityDelta::ADD + : ChangeType == REMOVE ? ExpectedEntityDelta::REMOVE + : ExpectedEntityDelta::TEMPORARILY_REMOVED }); + return *this; +} + +ExpectedViewDelta& ExpectedViewDelta::AddComponentAdded(const Worker_EntityId EntityId, ComponentData Data) +{ + EntityDeltas[EntityId].ComponentsAdded.Push(ComponentChange(Data.GetComponentId(), Data.GetUnderlying())); + EntityDeltas[EntityId].DataStorage.Push(MoveTemp(Data)); + return *this; +} + +ExpectedViewDelta& ExpectedViewDelta::AddComponentRemoved(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +{ + EntityDeltas[EntityId].ComponentsRemoved.Push(ComponentChange(ComponentId)); + return *this; +} + +ExpectedViewDelta& ExpectedViewDelta::AddComponentUpdate(const Worker_EntityId EntityId, ComponentUpdate Update) +{ + EntityDeltas[EntityId].ComponentUpdates.Push(ComponentChange(Update.GetComponentId(), Update.GetUnderlying())); + EntityDeltas[EntityId].UpdateStorage.Push(MoveTemp(Update)); + return *this; +} + +ExpectedViewDelta& ExpectedViewDelta::AddComponentRefreshed(const Worker_EntityId EntityId, ComponentUpdate Update, ComponentData Data) +{ + EntityDeltas[EntityId].ComponentsRefreshed.Push(ComponentChange(Update.GetComponentId(), Data.GetUnderlying(), Update.GetEvents())); + EntityDeltas[EntityId].DataStorage.Push(MoveTemp(Data)); + EntityDeltas[EntityId].UpdateStorage.Push(MoveTemp(Update)); + return *this; +} + +ExpectedViewDelta& ExpectedViewDelta::AddAuthorityGained(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +{ + EntityDeltas[EntityId].AuthorityGained.Add(AuthorityChange(ComponentId, AuthorityChange::AUTHORITY_GAINED)); + return *this; +} + +ExpectedViewDelta& ExpectedViewDelta::AddAuthorityLost(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +{ + EntityDeltas[EntityId].AuthorityLost.Add(AuthorityChange(ComponentId, AuthorityChange::AUTHORITY_LOST)); + return *this; +} + +ExpectedViewDelta& ExpectedViewDelta::AddAuthorityLostTemporarily(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +{ + EntityDeltas[EntityId].AuthorityLostTemporarily.Add(AuthorityChange(ComponentId, AuthorityChange::AUTHORITY_LOST_TEMPORARILY)); + return *this; +} + +ExpectedViewDelta& ExpectedViewDelta::AddDisconnect(const uint8 StatusCode, FString StatusMessage) +{ + ConnectionStatusCode = StatusCode; + ConnectionStatusMessage = MoveTemp(StatusMessage); + return *this; +} + +void ExpectedViewDelta::SortEntityDeltas() +{ + for (auto& Pair : EntityDeltas) + { + Pair.Value.AuthorityGained.Sort(CompareAuthorityChangeById); + Pair.Value.AuthorityLost.Sort(CompareAuthorityChangeById); + Pair.Value.AuthorityLostTemporarily.Sort(CompareAuthorityChangeById); + Pair.Value.ComponentsAdded.Sort(CompareComponentChangeById); + Pair.Value.ComponentsRemoved.Sort(CompareComponentChangeById); + Pair.Value.ComponentsRefreshed.Sort(CompareComponentChangeById); + Pair.Value.ComponentUpdates.Sort(CompareComponentChangeById); + } + + EntityDeltas.KeySort(CompareWorkerEntityId); +} + +bool ExpectedViewDelta::Compare(const ViewDelta& Other) +{ + // We need to check if a disconnect op has been processed during the last tick. + // First call HasConnectionStatusChanged() before comparing the values stored. + if (Other.HasConnectionStatusChanged()) + { + if (ConnectionStatusCode != Other.GetConnectionStatusChange()) + { + return false; + } + + if (ConnectionStatusMessage != Other.GetConnectionStatusChangeMessage()) + { + return false; + } + } + + return CompareDeltas(Other.GetEntityDeltas()); +} + +bool ExpectedViewDelta::Compare(const FSubViewDelta& Other) +{ + return CompareDeltas(Other.EntityDeltas); +} + +bool ExpectedViewDelta::CompareDeltas(const TArray& Other) +{ + if (EntityDeltas.Num() != Other.Num()) + { + return false; + } + + SortEntityDeltas(); + TArray DeltaKeys; + EntityDeltas.GetKeys(DeltaKeys); + for (int32 i = 0; i < DeltaKeys.Num(); ++i) + { + const ExpectedEntityDelta& LhsEntityDelta = EntityDeltas[DeltaKeys[i]]; + const EntityDelta& RhsEntityDelta = Other[i]; + if (LhsEntityDelta.EntityId != RhsEntityDelta.EntityId) + { + return false; + } + + switch (LhsEntityDelta.Type) + { + case ExpectedEntityDelta::UPDATE: + if (!(RhsEntityDelta.Type == EntityDelta::UPDATE)) + { + return false; + } + break; + case ExpectedEntityDelta::ADD: + if (!(RhsEntityDelta.Type == EntityDelta::ADD)) + { + return false; + } + break; + case ExpectedEntityDelta::REMOVE: + if (!(RhsEntityDelta.Type == EntityDelta::REMOVE)) + { + return false; + } + break; + case ExpectedEntityDelta::TEMPORARILY_REMOVED: + if (!(RhsEntityDelta.Type == EntityDelta::TEMPORARILY_REMOVED)) + { + return false; + } + break; + default: + checkNoEntry(); + } + + if (!CompareData(LhsEntityDelta.AuthorityGained, RhsEntityDelta.AuthorityGained, CompareAuthorityChanges)) + { + return false; + } + + if (!CompareData(LhsEntityDelta.AuthorityLost, RhsEntityDelta.AuthorityLost, CompareAuthorityChanges)) + { + return false; + } + + if (!CompareData(LhsEntityDelta.AuthorityLostTemporarily, RhsEntityDelta.AuthorityLostTemporarily, CompareAuthorityChanges)) + { + return false; + } + + if (!CompareData(LhsEntityDelta.ComponentsAdded, RhsEntityDelta.ComponentsAdded, CompareComponentChanges)) + { + return false; + } + + if (!CompareData(LhsEntityDelta.ComponentsRemoved, RhsEntityDelta.ComponentsRemoved, CompareComponentChanges)) + { + return false; + } + + if (!CompareData(LhsEntityDelta.ComponentsRefreshed, RhsEntityDelta.ComponentsRefreshed, CompareComponentChanges)) + { + return false; + } + + if (!CompareData(LhsEntityDelta.ComponentUpdates, RhsEntityDelta.ComponentUpdates, CompareComponentChanges)) + { + return false; + } + } + + return true; +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/SubViewTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/SubViewTest.cpp new file mode 100644 index 0000000000..9a678d065c --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/SubViewTest.cpp @@ -0,0 +1,154 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialView/ViewCoordinator.h" +#include "Tests/SpatialView/SpatialViewUtils.h" +#include "Tests/TestDefinitions.h" +#include "Utils/ComponentFactory.h" + +#define SUBVIEW_TEST(TestName) GDK_TEST(Core, SubView, TestName) + +namespace SpatialGDK +{ +SUBVIEW_TEST(GIVEN_SubView_Without_Filter_WHEN_Tagged_Entity_Added_THEN_Delta_Contains_Entity) +{ + const Worker_EntityId TaggedEntityId = 2; + const Worker_ComponentId TagComponentId = 1; + + FDispatcher Dispatcher; + EntityView View; + ViewDelta Delta; + + FSubView SubView(TagComponentId, FSubView::NoFilter, &View, Dispatcher, FSubView::NoDispatcherCallbacks); + + AddEntityToView(View, TaggedEntityId); + PopulateViewDeltaWithComponentAdded(Delta, View, TaggedEntityId, ComponentData{ TagComponentId }); + + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + + SubView.Advance(Delta); + FSubViewDelta SubDelta = SubView.GetViewDelta(); + + // The tagged entity should pass through to the sub view delta. + TestEqual("There is one entity delta", SubDelta.EntityDeltas.Num(), 1); + if (SubDelta.EntityDeltas.Num() != 1) + { + // early out so we don't crash - test has already failed + return true; + } + TestEqual("The entity delta is for the correct entity ID", SubDelta.EntityDeltas[0].EntityId, TaggedEntityId); + + return true; +} + +SUBVIEW_TEST( + GIVEN_SubView_With_Filter_WHEN_Tagged_Entities_Added_THEN_Delta_Only_Contains_Filtered_Entities_ALSO_Dispatcher_Callback_Refreshes_Correctly) +{ + const Worker_EntityId TaggedEntityId = 2; + const Worker_EntityId OtherTaggedEntityId = 3; + const Worker_ComponentId TagComponentId = 1; + const Worker_ComponentId ValueComponentId = 2; + const double CorrectValue = 1; + const double IncorrectValue = 2; + + FDispatcher Dispatcher; + EntityView View; + ViewDelta Delta; + + FFilterPredicate Filter = [ValueComponentId, CorrectValue](const Worker_EntityId&, const EntityViewElement& Element) { + const ComponentData* It = Element.Components.FindByPredicate(ComponentIdEquality{ ValueComponentId }); + if (GetValueFromTestComponentData(It->GetUnderlying()) == CorrectValue) + { + return true; + } + return false; + }; + auto RefreshCallbacks = TArray{ FSubView::CreateComponentChangedRefreshCallback( + Dispatcher, ValueComponentId, FSubView::NoComponentChangeRefreshPredicate) }; + + FSubView SubView(TagComponentId, Filter, &View, Dispatcher, RefreshCallbacks); + + AddEntityToView(View, TaggedEntityId); + AddComponentToView(View, TaggedEntityId, CreateTestComponentData(ValueComponentId, CorrectValue)); + AddEntityToView(View, OtherTaggedEntityId); + AddComponentToView(View, OtherTaggedEntityId, CreateTestComponentData(ValueComponentId, IncorrectValue)); + + PopulateViewDeltaWithComponentAdded(Delta, View, TaggedEntityId, ComponentData{ TagComponentId }); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + SubView.Advance(Delta); + FSubViewDelta SubDelta = SubView.GetViewDelta(); + + // The tagged entity should pass through to the sub view delta. + TestEqual("There is one entity delta", SubDelta.EntityDeltas.Num(), 1); + if (SubDelta.EntityDeltas.Num() != 1) + { + // early out so we don't crash - test has already failed + return true; + } + TestEqual("The entity delta is for the correct entity ID", SubDelta.EntityDeltas[0].EntityId, TaggedEntityId); + + PopulateViewDeltaWithComponentAdded(Delta, View, OtherTaggedEntityId, ComponentData{ TagComponentId }); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + SubView.Advance(Delta); + SubDelta = SubView.GetViewDelta(); + + TestEqual("There are no entity deltas", SubDelta.EntityDeltas.Num(), 0); + + PopulateViewDeltaWithComponentUpdated(Delta, View, OtherTaggedEntityId, CreateTestComponentUpdate(ValueComponentId, CorrectValue)); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + SubView.Advance(Delta); + SubDelta = SubView.GetViewDelta(); + + TestEqual("There is one entity delta", SubDelta.EntityDeltas.Num(), 1); + if (SubDelta.EntityDeltas.Num() != 1) + { + // early out so we don't crash - test has already failed + return true; + } + TestEqual("The entity delta is for the correct entity ID", SubDelta.EntityDeltas[0].EntityId, OtherTaggedEntityId); + + return true; +} + +SUBVIEW_TEST(GIVEN_Tagged_Incomplete_Entity_Which_Should_Be_Complete_WHEN_Refresh_Entity_THEN_Entity_Is_Complete) +{ + const Worker_EntityId TaggedEntityId = 2; + const Worker_ComponentId TagComponentId = 1; + + FDispatcher Dispatcher; + EntityView View; + ViewDelta Delta; + + bool IsFilterComplete = false; + + FFilterPredicate Filter = [&IsFilterComplete](const Worker_EntityId&, const EntityViewElement&) { + return IsFilterComplete; + }; + + FSubView SubView(TagComponentId, Filter, &View, Dispatcher, FSubView::NoDispatcherCallbacks); + + AddEntityToView(View, TaggedEntityId); + + PopulateViewDeltaWithComponentAdded(Delta, View, TaggedEntityId, ComponentData{ TagComponentId }); + Dispatcher.InvokeCallbacks(Delta.GetEntityDeltas()); + SubView.Advance(Delta); + FSubViewDelta SubDelta = SubView.GetViewDelta(); + + TestEqual("There are no entity deltas", SubDelta.EntityDeltas.Num(), 0); + + IsFilterComplete = true; + SubView.RefreshEntity(TaggedEntityId); + Delta.Clear(); + SubView.Advance(Delta); + SubDelta = SubView.GetViewDelta(); + + TestEqual("There is one entity delta", SubDelta.EntityDeltas.Num(), 1); + if (SubDelta.EntityDeltas.Num() != 1) + { + // early out so we don't crash - test has already failed + return true; + } + TestEqual("The entity delta is for the correct entity ID", SubDelta.EntityDeltas[0].EntityId, TaggedEntityId); + + return true; +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/ViewCoordinatorTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/ViewCoordinatorTest.cpp new file mode 100644 index 0000000000..592a3287bd --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/ViewCoordinatorTest.cpp @@ -0,0 +1,230 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialView/OpList/EntityComponentOpList.h" +#include "SpatialView/OpList/ExtractedOpList.h" +#include "SpatialView/ViewCoordinator.h" +#include "Tests/SpatialView/ComponentTestUtils.h" +#include "Tests/TestDefinitions.h" + +#define VIEWCOORDINATOR_TEST(TestName) GDK_TEST(Core, ViewCoordinator, TestName) + +namespace SpatialGDK +{ +// A stub for controlling the series of oplists fed into the view coordinator. The list of oplists given to +// SetListsOfOplists will be processed one list of oplists at a time on each call to Advance. +class ConnectionHandlerStub : public AbstractConnectionHandler +{ +public: + void SetListsOfOpLists(TArray> List) { ListsOfOpLists = MoveTemp(List); } + + virtual void Advance() override + { + QueuedOpLists = MoveTemp(ListsOfOpLists[0]); + ListsOfOpLists.RemoveAt(0); + } + + virtual uint32 GetOpListCount() override { return QueuedOpLists.Num(); } + + virtual OpList GetNextOpList() override + { + OpList Temp = MoveTemp(QueuedOpLists[0]); + QueuedOpLists.RemoveAt(0); + return Temp; + } + + virtual void SendMessages(TUniquePtr Messages) override {} + + virtual const FString& GetWorkerId() const override { return WorkerId; } + + virtual Worker_EntityId GetWorkerSystemEntityId() const override { return WorkerSystemEntityId; } + +private: + TArray> ListsOfOpLists; + TArray QueuedOpLists; + Worker_EntityId WorkerSystemEntityId = 1; + FString WorkerId = TEXT("test_worker"); + TArray Attributes = { TEXT("test") }; +}; + +VIEWCOORDINATOR_TEST(GIVEN_view_coordinator_WHEN_create_unfiltered_sub_view_THEN_returns_sub_view_which_passes_through_only_tagged_entity) +{ + const Worker_EntityId EntityId = 1; + const Worker_EntityId TaggedEntityId = 2; + const Worker_ComponentId ComponentId = 1; + const Worker_ComponentId TagComponentId = 2; + + TArray> ListsOfOpLists; + TArray OpLists; + EntityComponentOpListBuilder Builder; + Builder.AddEntity(TaggedEntityId); + Builder.AddComponent(TaggedEntityId, ComponentData(TagComponentId)); + Builder.AddEntity(EntityId); + Builder.AddComponent(EntityId, ComponentData(ComponentId)); + OpLists.Add(MoveTemp(Builder).CreateOpList()); + ListsOfOpLists.Add(MoveTemp(OpLists)); + + auto Handler = MakeUnique(); + Handler->SetListsOfOpLists(MoveTemp(ListsOfOpLists)); + ViewCoordinator Coordinator(MoveTemp(Handler), nullptr, FComponentSetData()); + auto& SubView = Coordinator.CreateSubView(TagComponentId, FSubView::NoFilter, FSubView::NoDispatcherCallbacks); + + Coordinator.Advance(0.0f); + FSubViewDelta Delta = SubView.GetViewDelta(); + + // Only the tagged entity should pass through to the sub view delta. + TestEqual("There is one entity delta", Delta.EntityDeltas.Num(), 1); + if (Delta.EntityDeltas.Num() != 1) + { + // test already failed + return true; + } + TestEqual("The entity delta is for the correct entity ID", Delta.EntityDeltas[0].EntityId, TaggedEntityId); + + return true; +} + +VIEWCOORDINATOR_TEST(GIVEN_view_coordinator_WHEN_create_filtered_sub_view_THEN_returns_sub_view_which_filters_tagged_entities) +{ + const Worker_EntityId TaggedEntityId = 2; + const Worker_EntityId OtherTaggedEntityId = 3; + const Worker_ComponentId TagComponentId = 2; + const Worker_ComponentId ValueComponentId = 3; + const double CorrectValue = 1; + const double IncorrectValue = 2; + FComponentSetData ComponentSetData; + ComponentSetData.ComponentSets.Add(TagComponentId, { TagComponentId }); + ComponentSetData.ComponentSets.Add(ValueComponentId, { ValueComponentId }); + + TArray> ListsOfOpLists; + TArray OpLists; + EntityComponentOpListBuilder Builder; + Builder.AddEntity(TaggedEntityId); + Builder.AddComponent(TaggedEntityId, ComponentData(TagComponentId)); + Builder.AddComponent(TaggedEntityId, CreateTestComponentData(ValueComponentId, CorrectValue)); + Builder.AddEntity(OtherTaggedEntityId); + Builder.AddComponent(OtherTaggedEntityId, ComponentData(TagComponentId)); + Builder.AddComponent(OtherTaggedEntityId, CreateTestComponentData(ValueComponentId, IncorrectValue)); + OpLists.Add(MoveTemp(Builder).CreateOpList()); + ListsOfOpLists.Add(MoveTemp(OpLists)); + + Builder = EntityComponentOpListBuilder(); + OpLists.Empty(); + Builder.UpdateComponent(OtherTaggedEntityId, CreateTestComponentUpdate(ValueComponentId, CorrectValue)); + OpLists.Add(MoveTemp(Builder).CreateOpList()); + ListsOfOpLists.Add(MoveTemp(OpLists)); + + auto Handler = MakeUnique(); + Handler->SetListsOfOpLists(MoveTemp(ListsOfOpLists)); + ViewCoordinator Coordinator(MoveTemp(Handler), nullptr, ComponentSetData); + + auto& SubView = Coordinator.CreateSubView( + TagComponentId, + [CorrectValue, ValueComponentId](const Worker_EntityId&, const EntityViewElement& Element) { + const ComponentData* It = Element.Components.FindByPredicate(ComponentIdEquality{ ValueComponentId }); + if (GetValueFromTestComponentData(It->GetUnderlying()) == CorrectValue) + { + return true; + } + return false; + }, + TArray{ Coordinator.CreateComponentChangedRefreshCallback(ValueComponentId) }); + + Coordinator.Advance(0.0f); + FSubViewDelta Delta = SubView.GetViewDelta(); + + // Only the tagged entity with the correct value should pass through to the sub view delta. + TestEqual("There is one entity delta", Delta.EntityDeltas.Num(), 1); + if (Delta.EntityDeltas.Num() != 1) + { + return true; + } + TestEqual("The entity delta is for the correct entity ID", Delta.EntityDeltas[0].EntityId, TaggedEntityId); + + Coordinator.Advance(0.0f); + Delta = SubView.GetViewDelta(); + + // The value on the other entity should have updated, so we should see an add for the second entity. + TestEqual("There is one entity delta", Delta.EntityDeltas.Num(), 1); + if (Delta.EntityDeltas.Num() != 1) + { + return true; + } + TestEqual("The entity delta is for the correct entity ID", Delta.EntityDeltas[0].EntityId, OtherTaggedEntityId); + + return true; +} + +VIEWCOORDINATOR_TEST(GIVEN_view_coordinator_with_multiple_tracked_subviews_WHEN_refresh_THEN_all_subviews_refreshed) +{ + const Worker_EntityId TaggedEntityId = 2; + const Worker_ComponentId TagComponentId = 2; + FComponentSetData ComponentSetData; + ComponentSetData.ComponentSets.Add(TagComponentId, { TagComponentId }); + + bool EntityComplete = false; + const int NumberOfSubViews = 100; + + TArray> ListsOfOpLists; + + TArray OpLists; + EntityComponentOpListBuilder Builder; + Builder.AddEntity(TaggedEntityId); + Builder.AddComponent(TaggedEntityId, ComponentData(TagComponentId)); + OpLists.Add(MoveTemp(Builder).CreateOpList()); + ListsOfOpLists.Add(MoveTemp(OpLists)); + + TArray SecondOpLists; + ListsOfOpLists.Add(MoveTemp(SecondOpLists)); + + auto Handler = MakeUnique(); + Handler->SetListsOfOpLists(MoveTemp(ListsOfOpLists)); + ViewCoordinator Coordinator(MoveTemp(Handler), nullptr, ComponentSetData); + + TArray SubViews; + + for (int i = 0; i < NumberOfSubViews; ++i) + { + SubViews.Emplace(&Coordinator.CreateSubView( + TagComponentId, + [&EntityComplete](const Worker_EntityId&, const EntityViewElement&) { + return EntityComplete; + }, + FSubView::NoDispatcherCallbacks)); + } + + Coordinator.Advance(0.0f); + FSubViewDelta Delta; + + // All the subviews should have no complete entities, so their deltas should be empty. + for (int i = 0; i < NumberOfSubViews; ++i) + { + for (FSubView* SubView : SubViews) + { + Delta = SubView->GetViewDelta(); + TestEqual("There are no entity deltas", Delta.EntityDeltas.Num(), 0); + } + } + + EntityComplete = true; + Coordinator.RefreshEntityCompleteness(TaggedEntityId); + Coordinator.Advance(0.0f); + + // All the subviews' filters will have changed their truth value due to the change in local state. + for (int i = 0; i < NumberOfSubViews; ++i) + { + for (FSubView* SubView : SubViews) + { + Delta = SubView->GetViewDelta(); + TestEqual("There is one entity delta", Delta.EntityDeltas.Num(), 1); + if (Delta.EntityDeltas.Num() != 1) + { + // test has already failed + return true; + } + TestEqual("The entity delta is for the correct entity ID", Delta.EntityDeltas[0].EntityId, TaggedEntityId); + } + } + + return true; +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/ViewDeltaTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/ViewDeltaTest.cpp index ab2bffba9d..c18a45ba21 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/ViewDeltaTest.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/ViewDeltaTest.cpp @@ -1,8 +1,661 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved +#include "SpatialView/ComponentSetData.h" +#include "Tests/SpatialView/SpatialViewUtils.h" #include "Tests/TestDefinitions.h" +#include "SpatialView/OpList/EntityComponentOpList.h" #include "SpatialView/ViewDelta.h" +#include "Tests/SpatialView/ExpectedViewDelta.h" -#define VIEWDELTA_TEST(TestName) \ - GDK_TEST(Core, ViewDelta, TestName) +#define VIEWDELTA_TEST(TestName) GDK_TEST(Core, ViewDelta, TestName) + +namespace +{ +const Worker_EntityId TestEntityId = 1; +const Worker_EntityId OtherTestEntityId = 2; +const Worker_EntityId AnotherTestEntityId = 3; +const Worker_EntityId YetAnotherTestEntityId = 4; +const Worker_ComponentId TestComponentId = 1; +const Worker_ComponentId OtherTestComponentId = 2; +const Worker_ComponentSetId TestComponentSetId = 3; +const double TestComponentValue = 20; +const double OtherTestComponentValue = 30; +const double TestEventValue = 25; + +const SpatialGDK::FComponentSetData ComponentSetData = { { { TestComponentSetId, { TestComponentId, OtherTestComponentId } } } }; +} // anonymous namespace + +namespace SpatialGDK +{ +struct ViewDeltaTestFixture +{ + ViewDeltaTestFixture() { ComponentSetData.ComponentSets.Add(TestComponentSetId).Add(TestComponentId); } + + Worker_EntityId TestEntityId = 1; + Worker_EntityId OtherTestEntityId = 2; + Worker_EntityId AnotherTestEntityId = 3; + Worker_EntityId YetAnotherTestEntityId = 4; + Worker_ComponentId TestComponentId = 1; + Worker_ComponentSetId TestComponentSetId = 1; + double TestComponentValue = 20; + double OtherTestComponentValue = 30; + double TestEventValue = 25; + FComponentSetData ComponentSetData; +}; + +VIEWDELTA_TEST(GIVEN_empty_view_WHEN_add_entity_THEN_get_entity_in_view_and_delta) +{ + ViewDelta InputDelta; + EntityView InputView; + + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.AddEntity(TestEntityId); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + AddEntityToView(ExpectedView, TestEntityId); + + ExpectedViewDelta ExpectedDelta; + ExpectedDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::ADD); + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_entity_in_view_WHEN_remove_entity_THEN_empty_view) +{ + ViewDelta InputDelta; + EntityView InputView; + AddEntityToView(InputView, TestEntityId); + + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.RemoveEntity(TestEntityId); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + ExpectedViewDelta ExpectedDelta; + ExpectedDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::REMOVE); + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_entity_in_view_WHEN_add_component_THEN_entity_and_component_in_view) +{ + ViewDelta InputDelta; + EntityView InputView; + AddEntityToView(InputView, TestEntityId); + + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.AddComponent(TestEntityId, CreateTestComponentData(TestComponentId, TestComponentValue)); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + AddEntityToView(ExpectedView, TestEntityId); + AddComponentToView(ExpectedView, TestEntityId, CreateTestComponentData(TestComponentId, TestComponentValue)); + + ExpectedViewDelta ExpectedDelta; + ExpectedDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::UPDATE); + ExpectedDelta.AddComponentAdded(TestEntityId, CreateTestComponentData(TestComponentId, TestComponentValue)); + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_entity_and_component_in_view_WHEN_update_component_THEN_component_udated_in_view) +{ + ViewDelta InputDelta; + EntityView InputView; + AddEntityToView(InputView, TestEntityId); + AddComponentToView(InputView, TestEntityId, CreateTestComponentData(TestComponentId, TestComponentValue)); + + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.UpdateComponent(TestEntityId, CreateTestComponentUpdate(TestComponentId, OtherTestComponentValue)); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + AddEntityToView(ExpectedView, TestEntityId); + AddComponentToView(ExpectedView, TestEntityId, CreateTestComponentData(TestComponentId, OtherTestComponentValue)); + + ExpectedViewDelta ExpectedDelta; + ExpectedDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::UPDATE); + ExpectedDelta.AddComponentUpdate(TestEntityId, CreateTestComponentUpdate(TestComponentId, OtherTestComponentValue)); + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_entity_and_component_in_view_WHEN_remove_component_THEN_component_not_in_view) +{ + ViewDelta InputDelta; + EntityView InputView; + AddEntityToView(InputView, TestEntityId); + AddComponentToView(InputView, TestEntityId, CreateTestComponentData(TestComponentId, TestComponentValue)); + + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.RemoveComponent(TestEntityId, TestComponentId); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + AddEntityToView(ExpectedView, TestEntityId); + + ExpectedViewDelta ExpectedDelta; + ExpectedDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::UPDATE); + ExpectedDelta.AddComponentRemoved(TestEntityId, TestComponentId); + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_entity_and_component_in_view_WHEN_authority_gained_THEN_authority_in_view) +{ + ViewDelta InputDelta; + EntityView InputView; + ComponentData TestComponentData = CreateTestComponentData(TestComponentId, TestComponentValue); + + AddEntityToView(InputView, TestEntityId); + AddComponentToView(InputView, TestEntityId, TestComponentData.DeepCopy()); + + EntityComponentOpListBuilder OpListBuilder; + TArray ComponentsInSet; + ComponentsInSet.Add(TestComponentData.DeepCopy()); + OpListBuilder.SetAuthority(TestEntityId, TestComponentSetId, WORKER_AUTHORITY_AUTHORITATIVE, MoveTemp(ComponentsInSet)); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + AddEntityToView(ExpectedView, TestEntityId); + AddComponentToView(ExpectedView, TestEntityId, TestComponentData.DeepCopy()); + AddAuthorityToView(ExpectedView, TestEntityId, TestComponentSetId); + + ExpectedViewDelta ExpectedDelta; + ExpectedDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::UPDATE); + ExpectedDelta.AddAuthorityGained(TestEntityId, TestComponentSetId); + ExpectedDelta.AddComponentRefreshed(TestEntityId, ComponentUpdate(TestComponentId), TestComponentData.DeepCopy()); + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_entity_and_auth_component_in_view_WHEN_authority_lost_THEN_unauth_component_in_view) +{ + ViewDelta InputDelta; + EntityView InputView; + ComponentData TestComponentData = CreateTestComponentData(TestComponentId, TestComponentValue); + + AddEntityToView(InputView, TestEntityId); + AddComponentToView(InputView, TestEntityId, TestComponentData.DeepCopy()); + AddAuthorityToView(InputView, TestEntityId, TestComponentSetId); + + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.SetAuthority(TestEntityId, TestComponentSetId, WORKER_AUTHORITY_NOT_AUTHORITATIVE, + CopyComponentSetOnEntity(TestEntityId, TestComponentSetId, InputView, ComponentSetData)); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + AddEntityToView(ExpectedView, TestEntityId); + AddComponentToView(ExpectedView, TestEntityId, TestComponentData.DeepCopy()); + + ExpectedViewDelta ExpectedDelta; + ExpectedDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::UPDATE); + ExpectedDelta.AddComponentRefreshed(TestEntityId, ComponentUpdate(TestComponentId), TestComponentData.DeepCopy()); + ExpectedDelta.AddAuthorityLost(TestEntityId, TestComponentSetId); + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_entity_and_components_in_view_WHEN_authority_gained_with_no_component_data_THEN_components_removed_from_view) +{ + ViewDelta InputDelta; + EntityView InputView; + ComponentData TestComponentData = CreateTestComponentData(TestComponentId, TestComponentValue); + ComponentData OtherTestComponentData = CreateTestComponentData(OtherTestComponentId, TestComponentValue); + + AddEntityToView(InputView, TestEntityId); + AddComponentToView(InputView, TestEntityId, TestComponentData.DeepCopy()); + AddComponentToView(InputView, TestEntityId, OtherTestComponentData.DeepCopy()); + + EntityComponentOpListBuilder OpListBuilder; + // Set authority with no component data - implying the components should be removed. + OpListBuilder.SetAuthority(TestEntityId, TestComponentSetId, WORKER_AUTHORITY_AUTHORITATIVE, {}); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + AddEntityToView(ExpectedView, TestEntityId); + AddAuthorityToView(ExpectedView, TestEntityId, TestComponentSetId); + + ExpectedViewDelta ExpectedDelta; + ExpectedDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::UPDATE); + ExpectedDelta.AddComponentRemoved(TestEntityId, TestComponentId); + ExpectedDelta.AddComponentRemoved(TestEntityId, OtherTestComponentId); + ExpectedDelta.AddAuthorityGained(TestEntityId, TestComponentSetId); + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_entity_with_no_components_WHEN_authority_gained_with_non_empty_component_data_THEN_component_added_to_view) +{ + ViewDelta InputDelta; + EntityView InputView; + ComponentData TestComponentData = CreateTestComponentData(TestComponentId, TestComponentValue); + ComponentData OtherTestComponentData = CreateTestComponentData(OtherTestComponentId, TestComponentValue); + + AddEntityToView(InputView, TestEntityId); + + EntityComponentOpListBuilder OpListBuilder; + TArray CanonicalSetData; + CanonicalSetData.Add(TestComponentData.DeepCopy()); + CanonicalSetData.Add(OtherTestComponentData.DeepCopy()); + OpListBuilder.SetAuthority(TestEntityId, TestComponentSetId, WORKER_AUTHORITY_AUTHORITATIVE, MoveTemp(CanonicalSetData)); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + AddEntityToView(ExpectedView, TestEntityId); + AddAuthorityToView(ExpectedView, TestEntityId, TestComponentSetId); + AddComponentToView(ExpectedView, TestEntityId, TestComponentData.DeepCopy()); + AddComponentToView(ExpectedView, TestEntityId, OtherTestComponentData.DeepCopy()); + + ExpectedViewDelta ExpectedDelta; + ExpectedDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::UPDATE); + ExpectedDelta.AddComponentAdded(TestEntityId, TestComponentData.DeepCopy()); + ExpectedDelta.AddComponentAdded(TestEntityId, OtherTestComponentData.DeepCopy()); + ExpectedDelta.AddAuthorityGained(TestEntityId, TestComponentSetId); + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + + return true; +} + +// There are two components in the component set, call them X and Y. We start with X on the entity but not Y. +// We add an authority delegation that has only Y in the canonical data. +// We expect to see Y added and X removed. +VIEWDELTA_TEST(GIVEN_one_component_from_set_WHEN_set_delegation_with_only_other_component_THEN_removes_old_and_adds_new) +{ + ViewDelta InputDelta; + EntityView InputView; + ComponentData TestComponentData = CreateTestComponentData(TestComponentId, TestComponentValue); + ComponentData OtherTestComponentData = CreateTestComponentData(OtherTestComponentId, TestComponentValue); + + AddEntityToView(InputView, TestEntityId); + AddComponentToView(InputView, TestEntityId, TestComponentData.DeepCopy()); + + EntityComponentOpListBuilder OpListBuilder; + TArray CanonicalSetData; + CanonicalSetData.Add(OtherTestComponentData.DeepCopy()); + OpListBuilder.SetAuthority(TestEntityId, TestComponentSetId, WORKER_AUTHORITY_AUTHORITATIVE, MoveTemp(CanonicalSetData)); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + AddEntityToView(ExpectedView, TestEntityId); + AddAuthorityToView(ExpectedView, TestEntityId, TestComponentSetId); + AddComponentToView(ExpectedView, TestEntityId, OtherTestComponentData.DeepCopy()); + + ExpectedViewDelta ExpectedDelta; + ExpectedDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::UPDATE); + ExpectedDelta.AddComponentRemoved(TestEntityId, TestComponentId); + ExpectedDelta.AddComponentAdded(TestEntityId, OtherTestComponentData.DeepCopy()); + ExpectedDelta.AddAuthorityGained(TestEntityId, TestComponentSetId); + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_connected_view_WHEN_disconnect_op_THEN_disconnected_view) +{ + ViewDelta InputDelta; + EntityView InputView; + + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.SetDisconnect(WORKER_CONNECTION_STATUS_CODE_REJECTED, StringStorage("Test disconnection reason")); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + + ExpectedViewDelta ExpectedDelta; + ExpectedDelta.AddDisconnect(WORKER_CONNECTION_STATUS_CODE_REJECTED, TEXT("Test disconnection reason")); + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_entity_and_auth_component_in_view_WHEN_authority_lost_and_gained_THEN_authority_lost_temporarily) +{ + ViewDelta InputDelta; + EntityView InputView; + ComponentData TestComponentData = CreateTestComponentData(TestComponentId, TestComponentValue); + + AddEntityToView(InputView, TestEntityId); + AddComponentToView(InputView, TestEntityId, TestComponentData.DeepCopy()); + AddAuthorityToView(InputView, TestEntityId, TestComponentSetId); + + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.SetAuthority(TestEntityId, TestComponentSetId, WORKER_AUTHORITY_NOT_AUTHORITATIVE, + CopyComponentSetOnEntity(TestEntityId, TestComponentSetId, InputView, ComponentSetData)); + OpListBuilder.SetAuthority(TestEntityId, TestComponentSetId, WORKER_AUTHORITY_AUTHORITATIVE, + CopyComponentSetOnEntity(TestEntityId, TestComponentSetId, InputView, ComponentSetData)); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + AddEntityToView(ExpectedView, TestEntityId); + AddComponentToView(ExpectedView, TestEntityId, TestComponentData.DeepCopy()); + AddAuthorityToView(ExpectedView, TestEntityId, TestComponentSetId); + + ExpectedViewDelta ExpectedDelta; + ExpectedDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::UPDATE); + ExpectedDelta.AddAuthorityLostTemporarily(TestEntityId, TestComponentSetId); + ExpectedDelta.AddComponentRefreshed(TestEntityId, ComponentUpdate(TestComponentId), TestComponentData.DeepCopy()); + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_empty_view_WHEN_add_remove_THEN_get_empty_view_and_delta) +{ + ViewDelta InputDelta; + EntityView InputView; + + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.AddEntity(TestEntityId); + OpListBuilder.RemoveEntity(TestEntityId); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + ExpectedViewDelta ExpectedDelta; + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_entity_and_component_in_view_WHEN_update_and_add_component_THEN_component_refresh) +{ + ViewDelta InputDelta; + EntityView InputView; + AddEntityToView(InputView, TestEntityId); + AddComponentToView(InputView, TestEntityId, CreateTestComponentData(TestComponentId, TestComponentValue)); + + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.UpdateComponent(TestEntityId, CreateTestComponentEvent(TestComponentId, TestEventValue)); + OpListBuilder.AddComponent(TestEntityId, CreateTestComponentData(TestComponentId, OtherTestComponentValue)); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + AddEntityToView(ExpectedView, TestEntityId); + AddComponentToView(ExpectedView, TestEntityId, CreateTestComponentData(TestComponentId, OtherTestComponentValue)); + + ExpectedViewDelta ExpectedDelta; + ExpectedDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::UPDATE); + ExpectedDelta.AddComponentRefreshed(TestEntityId, CreateTestComponentEvent(TestComponentId, TestEventValue), + CreateTestComponentData(TestComponentId, OtherTestComponentValue)); + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_entity_and_component_in_view_WHEN_remove_and_add_component_THEN_component_refresh) +{ + ViewDelta InputDelta; + EntityView InputView; + AddEntityToView(InputView, TestEntityId); + AddComponentToView(InputView, TestEntityId, CreateTestComponentData(TestComponentId, TestComponentValue)); + + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.RemoveComponent(TestEntityId, TestComponentId); + OpListBuilder.AddComponent(TestEntityId, CreateTestComponentData(TestComponentId, OtherTestComponentValue)); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + AddEntityToView(ExpectedView, TestEntityId); + AddComponentToView(ExpectedView, TestEntityId, CreateTestComponentData(TestComponentId, OtherTestComponentValue)); + + ExpectedViewDelta ExpectedDelta; + ExpectedDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::UPDATE); + ExpectedDelta.AddComponentRefreshed(TestEntityId, ComponentUpdate(TestComponentId), + CreateTestComponentData(TestComponentId, OtherTestComponentValue)); + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_entity_view_WHEN_entity_remove_and_add_THEN_no_entity_flag) +{ + ViewDelta InputDelta; + EntityView InputView; + AddEntityToView(InputView, TestEntityId); + + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.RemoveEntity(TestEntityId); + OpListBuilder.AddEntity(TestEntityId); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + AddEntityToView(ExpectedView, TestEntityId); + + ExpectedViewDelta ExpectedDelta; + ExpectedDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::UPDATE); + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_empty_view_WHEN_add_remove_add_THEN_entity_in_view_and_delta) +{ + ViewDelta InputDelta; + EntityView InputView; + + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.AddEntity(TestEntityId); + OpListBuilder.RemoveEntity(TestEntityId); + OpListBuilder.AddEntity(TestEntityId); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + AddEntityToView(ExpectedView, TestEntityId); + + ExpectedViewDelta ExpectedDelta; + ExpectedDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::EntityChangeType::ADD); + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_empty_view_WHEN_add_entity_add_component_THEN_entity_and_component_in_view_and_delta) +{ + ViewDelta InputDelta; + EntityView InputView; + + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.AddEntity(TestEntityId); + OpListBuilder.AddComponent(TestEntityId, CreateTestComponentData(TestComponentId, TestComponentValue)); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + AddEntityToView(ExpectedView, TestEntityId); + AddComponentToView(ExpectedView, TestEntityId, CreateTestComponentData(TestComponentId, TestComponentValue)); + + ExpectedViewDelta ExpectedDelta; + ExpectedDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::EntityChangeType::ADD); + ExpectedDelta.AddComponentAdded(TestEntityId, CreateTestComponentData(TestComponentId, TestComponentValue)); + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + return true; +} + +VIEWDELTA_TEST(GIVEN_entity_and_component_in_view_WHEN_remove_entity_THEN_empty_view_remove_ops_in_delta) +{ + ViewDelta InputDelta; + EntityView InputView; + AddEntityToView(InputView, TestEntityId); + AddComponentToView(InputView, TestEntityId, CreateTestComponentData(TestComponentId, TestComponentValue)); + + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.RemoveComponent(TestEntityId, TestComponentId); + OpListBuilder.RemoveEntity(TestEntityId); + SetFromOpList(InputDelta, InputView, MoveTemp(OpListBuilder), ComponentSetData); + + EntityView ExpectedView; + + ExpectedViewDelta ExpectedDelta; + ExpectedDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::EntityChangeType::REMOVE); + ExpectedDelta.AddComponentRemoved(TestEntityId, TestComponentId); + + TestTrue("View Deltas are equal", ExpectedDelta.Compare(InputDelta)); + TestTrue("Views are equal", CompareViews(InputView, ExpectedView)); + + return true; +} + +// Projection Tests +VIEWDELTA_TEST(GIVEN_view_delta_with_update_for_entity_complete_WHEN_project_THEN_contains_update) +{ + ViewDelta Delta; + FSubViewDelta SubViewDelta; + EntityView View; + AddEntityToView(View, TestEntityId); + AddComponentToView(View, TestEntityId, CreateTestComponentData(TestComponentId, TestComponentValue)); + + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.UpdateComponent(TestEntityId, CreateTestComponentUpdate(TestComponentId, OtherTestComponentValue)); + SetFromOpList(Delta, View, MoveTemp(OpListBuilder), ComponentSetData); + + Delta.Project(SubViewDelta, TArray{ TestEntityId }, TArray{}, TArray{}, + TArray{}); + + ExpectedViewDelta ExpectedSubViewDelta; + ExpectedSubViewDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::UPDATE); + ExpectedSubViewDelta.AddComponentUpdate(TestEntityId, CreateTestComponentUpdate(TestComponentId, OtherTestComponentValue)); + + TestTrue("View Deltas are equal", ExpectedSubViewDelta.Compare(SubViewDelta)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_empty_view_delta_with_newly_complete_entity_WHEN_project_THEN_contains_marker_add) +{ + ViewDelta Delta; + FSubViewDelta SubViewDelta; + EntityView View; + AddEntityToView(View, TestEntityId); + AddComponentToView(View, TestEntityId, CreateTestComponentData(TestComponentId, TestComponentValue)); + + Delta.Project(SubViewDelta, TArray{}, TArray{ TestEntityId }, TArray{}, + TArray{}); + + ExpectedViewDelta ExpectedSubViewDelta; + ExpectedSubViewDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::ADD); + + TestTrue("View Deltas are equal", ExpectedSubViewDelta.Compare(SubViewDelta)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_empty_view_delta_with_newly_incomplete_entity_WHEN_project_THEN_contains_marker_remove) +{ + ViewDelta Delta; + FSubViewDelta SubViewDelta; + EntityView View; + AddEntityToView(View, TestEntityId); + AddComponentToView(View, TestEntityId, CreateTestComponentData(TestComponentId, TestComponentValue)); + + Delta.Project(SubViewDelta, TArray{}, TArray{}, TArray{ TestEntityId }, + TArray{}); + + ExpectedViewDelta ExpectedSubViewDelta; + ExpectedSubViewDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::REMOVE); + + TestTrue("View Deltas are equal", ExpectedSubViewDelta.Compare(SubViewDelta)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_empty_view_delta_with_temporarily_incomplete_entity_WHEN_project_THEN_contains_marker_temporary_remove) +{ + ViewDelta Delta; + FSubViewDelta SubViewDelta; + EntityView View; + AddEntityToView(View, TestEntityId); + AddComponentToView(View, TestEntityId, CreateTestComponentData(TestComponentId, TestComponentValue)); + + Delta.Project(SubViewDelta, TArray{}, TArray{}, TArray{}, + TArray{ TestEntityId }); + + ExpectedViewDelta ExpectedSubViewDelta; + ExpectedSubViewDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::TEMPORARILY_REMOVED); + + TestTrue("View Deltas are equal", ExpectedSubViewDelta.Compare(SubViewDelta)); + + return true; +} + +VIEWDELTA_TEST(GIVEN_arbitrary_delta_and_completeness_WHEN_project_THEN_subview_delta_correct) +{ + ViewDelta Delta; + FSubViewDelta SubViewDelta; + EntityView View; + AddEntityToView(View, TestEntityId); + AddComponentToView(View, TestEntityId, CreateTestComponentData(TestComponentId, TestComponentValue)); + AddEntityToView(View, OtherTestEntityId); + AddEntityToView(View, AnotherTestEntityId); + AddEntityToView(View, YetAnotherTestEntityId); + AddComponentToView(View, YetAnotherTestEntityId, CreateTestComponentData(TestComponentId, OtherTestComponentValue)); + + TArray OpLists; + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.UpdateComponent(TestEntityId, CreateTestComponentUpdate(TestComponentId, OtherTestComponentValue)); + OpLists.Push(MoveTemp(OpListBuilder).CreateOpList()); + OpListBuilder = EntityComponentOpListBuilder{}; + OpListBuilder.UpdateComponent(YetAnotherTestEntityId, CreateTestComponentUpdate(TestComponentId, TestComponentValue)); + OpLists.Push(MoveTemp(OpListBuilder).CreateOpList()); + Delta.SetFromOpList(MoveTemp(OpLists), View, ComponentSetData); + + Delta.Project(SubViewDelta, TArray{ TestEntityId, YetAnotherTestEntityId }, + TArray{ OtherTestEntityId }, TArray{ AnotherTestEntityId }, TArray{}); + + ExpectedViewDelta ExpectedSubViewDelta; + ExpectedSubViewDelta.AddEntityDelta(TestEntityId, ExpectedViewDelta::UPDATE); + ExpectedSubViewDelta.AddComponentUpdate(TestEntityId, CreateTestComponentUpdate(TestComponentId, OtherTestComponentValue)); + ExpectedSubViewDelta.AddEntityDelta(OtherTestEntityId, ExpectedViewDelta::ADD); + ExpectedSubViewDelta.AddEntityDelta(AnotherTestEntityId, ExpectedViewDelta::REMOVE); + ExpectedSubViewDelta.AddEntityDelta(YetAnotherTestEntityId, ExpectedViewDelta::UPDATE); + ExpectedSubViewDelta.AddComponentUpdate(YetAnotherTestEntityId, CreateTestComponentUpdate(TestComponentId, TestComponentValue)); + + TestTrue("View Deltas are equal", ExpectedSubViewDelta.Compare(SubViewDelta)); + + return true; +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/WorkerViewTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/WorkerViewTest.cpp deleted file mode 100644 index 1cf4de0e62..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/WorkerViewTest.cpp +++ /dev/null @@ -1,55 +0,0 @@ -// 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(MoveTemp(Request)); - - // WHEN - const 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(MoveTemp(Request)); - View.SendCreateEntityRequest(MoveTemp(Request)); - - auto Messages = View.FlushLocalChanges(); - - // THEN - TestTrue("WorkerView has multiple CreateEntityRequest", Messages->CreateEntityRequests.Num() > 1); - - return true; -} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/TestingComponentViewHelpers.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/TestingComponentViewHelpers.cpp deleted file mode 100644 index 043cf4003b..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Private/Tests/TestingComponentViewHelpers.cpp +++ /dev/null @@ -1,32 +0,0 @@ -// 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 index a3c0938909..78429a6333 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Tests/TestingSchemaHelpers.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/TestingSchemaHelpers.cpp @@ -16,9 +16,11 @@ Schema_Object* TestingSchemaHelpers::CreateTranslationComponentDataFields() return Schema_GetComponentDataFields(Data.schema_type); } -void TestingSchemaHelpers::AddTranslationComponentDataMapping(Schema_Object* ComponentDataFields, VirtualWorkerId VWId, const PhysicalWorkerName& WorkerName) +void TestingSchemaHelpers::AddTranslationComponentDataMapping(Schema_Object* ComponentDataFields, VirtualWorkerId VWId, + const PhysicalWorkerName& WorkerName, Worker_PartitionId PartitionId) { 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); + SpatialGDK::AddStringToSchema(SchemaObject, SpatialConstants::MAPPING_PHYSICAL_WORKER_NAME_ID, WorkerName); + Schema_AddEntityId(SchemaObject, SpatialConstants::MAPPING_PARTITION_ID, PartitionId); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp index 1c05297dfc..fbc6cf8e9c 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp @@ -26,28 +26,30 @@ DECLARE_CYCLE_STAT(TEXT("Factory ProcessFastArrayUpdate"), STAT_FactoryProcessFa namespace { - template - TraceKey* GetTraceKeyFromComponentObject(T& Obj) - { +template +TraceKey* GetTraceKeyFromComponentObject(T& Obj) +{ #if TRACE_LIB_ACTIVE - return &Obj.Trace; + return &Obj.Trace; #else - return nullptr; + return nullptr; #endif - } } +} // namespace namespace SpatialGDK { - ComponentFactory::ComponentFactory(bool bInterestDirty, USpatialNetDriver* InNetDriver, USpatialLatencyTracer* InLatencyTracer) : NetDriver(InNetDriver) , PackageMap(InNetDriver->PackageMap) , ClassInfoManager(InNetDriver->ClassInfoManager) , bInterestHasChanged(bInterestDirty) , LatencyTracer(InLatencyTracer) -{ } +{ +} -uint32 ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FRepChangeState& Changes, ESchemaComponentType PropertyGroup, bool bIsInitialData, TraceKey* OutLatencyTraceId, TArray* ClearedIds /*= nullptr*/) +uint32 ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FRepChangeState& Changes, + ESchemaComponentType PropertyGroup, bool bIsInitialData, TraceKey* OutLatencyTraceId, + TArray* ClearedIds /*= nullptr*/) { SCOPE_CYCLE_COUNTER(STAT_FactoryProcessPropertyUpdates); @@ -57,7 +59,8 @@ uint32 ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObjec if (Changes.RepChanged.Num() > 0) { FChangelistIterator ChangelistIterator(Changes.RepChanged, 0); - FRepHandleIterator HandleIterator(static_cast(Changes.RepLayout.GetOwner()), ChangelistIterator, Changes.RepLayout.Cmds, Changes.RepLayout.BaseHandleToCmdIndex, 0, 1, 0, Changes.RepLayout.Cmds.Num() - 1); + FRepHandleIterator HandleIterator(static_cast(Changes.RepLayout.GetOwner()), ChangelistIterator, Changes.RepLayout.Cmds, + Changes.RepLayout.BaseHandleToCmdIndex, 0, 1, 0, Changes.RepLayout.Cmds.Num() - 1); while (HandleIterator.NextHandle()) { const FRepLayoutCmd& Cmd = Changes.RepLayout.Cmds[HandleIterator.CmdIndex]; @@ -78,7 +81,9 @@ uint32 ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObjec // 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()); + UE_LOG(LogComponentFactory, Warning, + TEXT("%s property trace being dropped because too many active on this actor (%s)"), *Cmd.Property->GetName(), + *Object->GetName()); LatencyTracer->WriteAndEndTrace(*OutLatencyTraceId, TEXT("Multiple actor component traces not supported"), true); } *OutLatencyTraceId = PropertyKey; @@ -106,7 +111,9 @@ uint32 ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObjec FSpatialNetBitWriter ValueDataWriter(PackageMap); - if (FSpatialNetDeltaSerializeInfo::DeltaSerializeWrite(NetDriver, ValueDataWriter, Object, Parent.ArrayIndex, Parent.Property, NetDeltaStruct) || bIsInitialData) + if (FSpatialNetDeltaSerializeInfo::DeltaSerializeWrite(NetDriver, ValueDataWriter, Object, Parent.ArrayIndex, + Parent.Property, NetDeltaStruct) + || bIsInitialData) { AddBytesToSchema(ComponentObject, HandleIterator.Handle, ValueDataWriter); } @@ -123,13 +130,19 @@ uint32 ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObjec #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 + - 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)); + NETWORK_PROFILER( + GNetworkProfiler.TrackReplicateProperty(Cmd.Property, (ProfilerBytesEnd - ProfilerBytesStart) * CHAR_BIT, nullptr)); #endif } @@ -148,7 +161,9 @@ uint32 ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObjec return BytesEnd - BytesStart; } -uint32 ComponentFactory::FillHandoverSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes, bool bIsInitialData, TraceKey* OutLatencyTraceId, TArray* ClearedIds /* = nullptr */) +uint32 ComponentFactory::FillHandoverSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FClassInfo& Info, + const FHandoverChangeState& Changes, bool bIsInitialData, TraceKey* OutLatencyTraceId, + TArray* ClearedIds /* = nullptr */) { const uint32 BytesStart = Schema_GetWriteBufferLength(ComponentObject); @@ -165,7 +180,8 @@ uint32 ComponentFactory::FillHandoverSchemaObject(Schema_Object* ComponentObject // 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()); + UE_LOG(LogComponentFactory, Warning, TEXT("%s handover trace being dropped because too many active on this actor (%s)"), + *PropertyInfo.Property->GetName(), *Object->GetName()); LatencyTracer->WriteAndEndTrace(*OutLatencyTraceId, TEXT("Multiple actor component traces not supported"), true); } *OutLatencyTraceId = LatencyTracer->RetrievePendingTrace(Object, PropertyInfo.Property); @@ -179,7 +195,8 @@ uint32 ComponentFactory::FillHandoverSchemaObject(Schema_Object* ComponentObject return BytesEnd - BytesStart; } -void ComponentFactory::AddProperty(Schema_Object* Object, Schema_FieldId FieldId, GDK_PROPERTY(Property)* Property, const uint8* Data, TArray* ClearedIds) +void ComponentFactory::AddProperty(Schema_Object* Object, Schema_FieldId FieldId, GDK_PROPERTY(Property) * Property, const uint8* Data, + TArray* ClearedIds) { if (GDK_PROPERTY(StructProperty)* StructProperty = GDK_CASTFIELD(Property)) { @@ -312,25 +329,32 @@ void ComponentFactory::AddProperty(Schema_Object* Object, Schema_FieldId FieldId AddProperty(Object, FieldId, EnumProperty->GetUnderlyingProperty(), Data, ClearedIds); } } - else if (Property->IsA() || Property->IsA() || Property->IsA()) + else if (Property->IsA() || Property->IsA() + || Property->IsA()) { // These properties can be set to replicate, but won't serialize across the network. } else if (Property->IsA()) { - UE_LOG(LogComponentFactory, Error, TEXT("Class %s with name %s in field %d: Replicated TMaps are not supported."), *Property->GetClass()->GetName(), *Property->GetName(), FieldId); + UE_LOG(LogComponentFactory, Error, TEXT("Class %s with name %s in field %d: Replicated TMaps are not supported."), + *Property->GetClass()->GetName(), *Property->GetName(), FieldId); } else if (Property->IsA()) { - UE_LOG(LogComponentFactory, Error, TEXT("Class %s with name %s in field %d: Replicated TSets are not supported."), *Property->GetClass()->GetName(), *Property->GetName(), FieldId); + UE_LOG(LogComponentFactory, Error, TEXT("Class %s with name %s in field %d: Replicated TSets are not supported."), + *Property->GetClass()->GetName(), *Property->GetName(), FieldId); } else { - UE_LOG(LogComponentFactory, Error, TEXT("Class %s with name %s in field %d: Attempted to add unknown property type."), *Property->GetClass()->GetName(), *Property->GetName(), FieldId); + UE_LOG(LogComponentFactory, Error, TEXT("Class %s with name %s in field %d: Attempted to add unknown property type."), + *Property->GetClass()->GetName(), *Property->GetName(), FieldId); } } -TArray ComponentFactory::CreateComponentDatas(UObject* Object, const FClassInfo& Info, const FRepChangeState& RepChangeState, const FHandoverChangeState& HandoverChangeState, uint32& OutBytesWritten) +TArray ComponentFactory::CreateComponentDatas(UObject* Object, const FClassInfo& Info, + const FRepChangeState& RepChangeState, + const FHandoverChangeState& HandoverChangeState, + uint32& OutBytesWritten) { TArray ComponentDatas; @@ -341,18 +365,21 @@ TArray ComponentFactory::CreateComponentDatas(UObject* Obj if (Info.SchemaComponents[SCHEMA_OwnerOnly] != SpatialConstants::INVALID_COMPONENT_ID) { - ComponentDatas.Add(CreateComponentData(Info.SchemaComponents[SCHEMA_OwnerOnly], Object, RepChangeState, SCHEMA_OwnerOnly, OutBytesWritten)); + 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, OutBytesWritten)); + ComponentDatas.Add( + CreateHandoverComponentData(Info.SchemaComponents[SCHEMA_Handover], Object, Info, HandoverChangeState, OutBytesWritten)); } return ComponentDatas; } -FWorkerComponentData ComponentFactory::CreateComponentData(Worker_ComponentId ComponentId, UObject* Object, const FRepChangeState& Changes, ESchemaComponentType PropertyGroup, uint32& OutBytesWritten) +FWorkerComponentData ComponentFactory::CreateComponentData(Worker_ComponentId ComponentId, UObject* Object, const FRepChangeState& Changes, + ESchemaComponentType PropertyGroup, uint32& OutBytesWritten) { FWorkerComponentData ComponentData = {}; ComponentData.component_id = ComponentId; @@ -361,7 +388,8 @@ FWorkerComponentData ComponentFactory::CreateComponentData(Worker_ComponentId Co // We're currently ignoring ClearedId fields, which is problematic if the initial replicated state // is different to what the default state is (the client will have the incorrect data). UNR:959 - OutBytesWritten += FillSchemaObject(ComponentObject, Object, Changes, PropertyGroup, true, GetTraceKeyFromComponentObject(ComponentData)); + OutBytesWritten += + FillSchemaObject(ComponentObject, Object, Changes, PropertyGroup, true, GetTraceKeyFromComponentObject(ComponentData)); return ComponentData; } @@ -375,17 +403,22 @@ FWorkerComponentData ComponentFactory::CreateEmptyComponentData(Worker_Component return ComponentData; } -FWorkerComponentData ComponentFactory::CreateHandoverComponentData(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes, uint32& OutBytesWritten) +FWorkerComponentData ComponentFactory::CreateHandoverComponentData(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, + const FHandoverChangeState& Changes, uint32& OutBytesWritten) { FWorkerComponentData ComponentData = CreateEmptyComponentData(ComponentId); Schema_Object* ComponentObject = Schema_GetComponentDataFields(ComponentData.schema_type); - OutBytesWritten += FillHandoverSchemaObject(ComponentObject, Object, Info, Changes, true, GetTraceKeyFromComponentObject(ComponentData)); + OutBytesWritten += + FillHandoverSchemaObject(ComponentObject, Object, Info, Changes, true, GetTraceKeyFromComponentObject(ComponentData)); return ComponentData; } -TArray ComponentFactory::CreateComponentUpdates(UObject* Object, const FClassInfo& Info, Worker_EntityId EntityId, const FRepChangeState* RepChangeState, const FHandoverChangeState* HandoverChangeState, uint32& OutBytesWritten) +TArray ComponentFactory::CreateComponentUpdates(UObject* Object, const FClassInfo& Info, Worker_EntityId EntityId, + const FRepChangeState* RepChangeState, + const FHandoverChangeState* HandoverChangeState, + uint32& OutBytesWritten) { TArray ComponentUpdates; @@ -394,7 +427,8 @@ TArray ComponentFactory::CreateComponentUpdates(UObject* if (Info.SchemaComponents[SCHEMA_Data] != SpatialConstants::INVALID_COMPONENT_ID) { uint32 BytesWritten = 0; - FWorkerComponentUpdate MultiClientUpdate = CreateComponentUpdate(Info.SchemaComponents[SCHEMA_Data], Object, *RepChangeState, SCHEMA_Data, BytesWritten); + FWorkerComponentUpdate MultiClientUpdate = + CreateComponentUpdate(Info.SchemaComponents[SCHEMA_Data], Object, *RepChangeState, SCHEMA_Data, BytesWritten); if (BytesWritten > 0) { ComponentUpdates.Add(MultiClientUpdate); @@ -405,7 +439,8 @@ TArray ComponentFactory::CreateComponentUpdates(UObject* if (Info.SchemaComponents[SCHEMA_OwnerOnly] != SpatialConstants::INVALID_COMPONENT_ID) { uint32 BytesWritten = 0; - FWorkerComponentUpdate SingleClientUpdate = CreateComponentUpdate(Info.SchemaComponents[SCHEMA_OwnerOnly], Object, *RepChangeState, SCHEMA_OwnerOnly, BytesWritten); + FWorkerComponentUpdate SingleClientUpdate = + CreateComponentUpdate(Info.SchemaComponents[SCHEMA_OwnerOnly], Object, *RepChangeState, SCHEMA_OwnerOnly, BytesWritten); if (BytesWritten > 0) { ComponentUpdates.Add(SingleClientUpdate); @@ -419,7 +454,8 @@ TArray ComponentFactory::CreateComponentUpdates(UObject* if (Info.SchemaComponents[SCHEMA_Handover] != SpatialConstants::INVALID_COMPONENT_ID) { uint32 BytesWritten = 0; - FWorkerComponentUpdate HandoverUpdate = CreateHandoverComponentUpdate(Info.SchemaComponents[SCHEMA_Handover], Object, Info, *HandoverChangeState, BytesWritten); + FWorkerComponentUpdate HandoverUpdate = + CreateHandoverComponentUpdate(Info.SchemaComponents[SCHEMA_Handover], Object, Info, *HandoverChangeState, BytesWritten); if (BytesWritten > 0) { ComponentUpdates.Add(HandoverUpdate); @@ -432,12 +468,21 @@ TArray ComponentFactory::CreateComponentUpdates(UObject* if (Object->IsA() && bInterestHasChanged) { ComponentUpdates.Add(NetDriver->InterestFactory->CreateInterestUpdate((AActor*)Object, Info, EntityId)); + + // There should not be a need to update the channel's up to date flag here. + checkSlow(([this, Object]() { + USpatialActorChannel* Channel = NetDriver->GetOrCreateSpatialActorChannel(Cast(Object)); + + return Channel && Channel->NeedOwnerInterestUpdate() == !NetDriver->InterestFactory->DoOwnersHaveEntityId(Cast(Object)); + }())); } return ComponentUpdates; } -FWorkerComponentUpdate ComponentFactory::CreateComponentUpdate(Worker_ComponentId ComponentId, UObject* Object, const FRepChangeState& Changes, ESchemaComponentType PropertyGroup, uint32& OutBytesWritten) +FWorkerComponentUpdate ComponentFactory::CreateComponentUpdate(Worker_ComponentId ComponentId, UObject* Object, + const FRepChangeState& Changes, ESchemaComponentType PropertyGroup, + uint32& OutBytesWritten) { FWorkerComponentUpdate ComponentUpdate = {}; @@ -447,7 +492,8 @@ FWorkerComponentUpdate ComponentFactory::CreateComponentUpdate(Worker_ComponentI TArray ClearedIds; - uint32 BytesWritten = FillSchemaObject(ComponentObject, Object, Changes, PropertyGroup, false, GetTraceKeyFromComponentObject(ComponentUpdate), &ClearedIds); + uint32 BytesWritten = FillSchemaObject(ComponentObject, Object, Changes, PropertyGroup, false, + GetTraceKeyFromComponentObject(ComponentUpdate), &ClearedIds); for (Schema_FieldId Id : ClearedIds) { @@ -465,7 +511,9 @@ FWorkerComponentUpdate ComponentFactory::CreateComponentUpdate(Worker_ComponentI return ComponentUpdate; } -FWorkerComponentUpdate ComponentFactory::CreateHandoverComponentUpdate(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes, uint32& OutBytesWritten) +FWorkerComponentUpdate ComponentFactory::CreateHandoverComponentUpdate(Worker_ComponentId ComponentId, UObject* Object, + const FClassInfo& Info, const FHandoverChangeState& Changes, + uint32& OutBytesWritten) { FWorkerComponentUpdate ComponentUpdate = {}; @@ -475,7 +523,8 @@ FWorkerComponentUpdate ComponentFactory::CreateHandoverComponentUpdate(Worker_Co TArray ClearedIds; - uint32 BytesWritten = FillHandoverSchemaObject(ComponentObject, Object, Info, Changes, false, GetTraceKeyFromComponentObject(ComponentUpdate), &ClearedIds); + uint32 BytesWritten = FillHandoverSchemaObject(ComponentObject, Object, Info, Changes, false, + GetTraceKeyFromComponentObject(ComponentUpdate), &ClearedIds); for (Schema_FieldId Id : ClearedIds) { diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp index c0d06c9cd7..1582cd426e 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp @@ -9,6 +9,8 @@ #include "EngineClasses/SpatialFastArrayNetSerialize.h" #include "EngineClasses/SpatialNetBitReader.h" +#include "Interop/Connection/SpatialEventTracer.h" +#include "Interop/Connection/SpatialTraceEventBuilder.h" #include "Interop/SpatialConditionMapFilter.h" #include "SpatialConstants.h" #include "Utils/GDKPropertyMacros.h" @@ -25,75 +27,80 @@ DECLARE_CYCLE_STAT(TEXT("Reader ApplyArray"), STAT_ReaderApplyArray, STATGROUP_S namespace { - bool FORCEINLINE ObjectRefSetsAreSame(const TSet< FUnrealObjectRef >& A, const TSet< FUnrealObjectRef >& B) +bool FORCEINLINE ObjectRefSetsAreSame(const TSet& A, const TSet& B) +{ + if (A.Num() != B.Num()) { - if (A.Num() != B.Num()) + return false; + } + + for (const FUnrealObjectRef& CompareRef : A) + { + if (!B.Contains(CompareRef)) { 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; } - - bool ReferencesChanged(FObjectReferencesMap& InObjectReferencesMap, int32 Offset, bool bHasReferences, const TSet& NewDynamicRefs, const TSet NewUnresolvedRefs) + if (CurEntry && bHasReferences) { - 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; + 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); +bool ReferencesChanged(FObjectReferencesMap& InObjectReferencesMap, int32 Offset, bool bHasReferences, const FUnrealObjectRef& ObjectRef, + bool bUnresolved) +{ + FObjectReferences* CurEntry = InObjectReferencesMap.Find(Offset); - if (bHasReferences ^ (CurEntry != nullptr)) + if (bHasReferences ^ (CurEntry != nullptr)) + { + return true; + } + if (CurEntry && bHasReferences) + { + if (!bUnresolved) { - return true; + return CurEntry->MappedRefs.Num() != 1 || CurEntry->UnresolvedRefs.Num() != 0 || *CurEntry->MappedRefs.begin() != ObjectRef; } - if (CurEntry && bHasReferences) + else { - 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 CurEntry->MappedRefs.Num() != 0 || CurEntry->UnresolvedRefs.Num() != 1 || *CurEntry->UnresolvedRefs.begin() != ObjectRef; } - return false; } + return false; } +} // namespace namespace SpatialGDK { - -ComponentReader::ComponentReader(USpatialNetDriver* InNetDriver, FObjectReferencesMap& InObjectReferencesMap/*, TSet& InUnresolvedRefs*/) +ComponentReader::ComponentReader(USpatialNetDriver* InNetDriver, + FObjectReferencesMap& InObjectReferencesMap, /*, TSet& InUnresolvedRefs*/ + SpatialEventTracer* InEventTracer) : PackageMap(InNetDriver->PackageMap) , NetDriver(InNetDriver) , ClassInfoManager(InNetDriver->ClassInfoManager) + , EventTracer(InEventTracer) , RootObjectReferencesMap(InObjectReferencesMap) { } -void ComponentReader::ApplyComponentData(const Worker_ComponentData& ComponentData, UObject& Object, USpatialActorChannel& Channel, bool bIsHandover, bool& bOutReferencesChanged) +void ComponentReader::ApplyComponentData(const Worker_ComponentData& ComponentData, UObject& Object, USpatialActorChannel& Channel, + bool bIsHandover, bool& bOutReferencesChanged) { if (Object.IsPendingKill()) { @@ -116,7 +123,8 @@ void ComponentReader::ApplyComponentData(const Worker_ComponentData& ComponentDa } } -void ComponentReader::ApplyComponentUpdate(const Worker_ComponentUpdate& ComponentUpdate, UObject& Object, USpatialActorChannel& Channel, bool bIsHandover, bool& bOutReferencesChanged) +void ComponentReader::ApplyComponentUpdate(const Worker_ComponentUpdate& ComponentUpdate, UObject& Object, USpatialActorChannel& Channel, + bool bIsHandover, bool& bOutReferencesChanged) { if (Object.IsPendingKill()) { @@ -142,7 +150,8 @@ void ComponentReader::ApplyComponentUpdate(const Worker_ComponentUpdate& Compone { if (bIsHandover) { - ApplyHandoverSchemaObject(ComponentObject, Object, Channel, false, UpdatedIds, ComponentUpdate.component_id, bOutReferencesChanged); + ApplyHandoverSchemaObject(ComponentObject, Object, Channel, false, UpdatedIds, ComponentUpdate.component_id, + bOutReferencesChanged); } else { @@ -151,7 +160,9 @@ void ComponentReader::ApplyComponentUpdate(const Worker_ComponentUpdate& Compone } } -void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject& Object, USpatialActorChannel& Channel, bool bIsInitialData, const TArray& UpdatedIds, Worker_ComponentId ComponentId, bool& bOutReferencesChanged) +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); if (Replicator == nullptr) @@ -168,21 +179,33 @@ void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject& bool bIsAuthServer = Channel.IsAuthoritativeServer(); bool bAutonomousProxy = Channel.IsClientAutonomousProxy(); bool bIsClient = NetDriver->GetNetMode() == NM_Client; + bool bEventTracerEnabled = EventTracer != nullptr; FSpatialConditionMapFilter ConditionMap(&Channel, bIsClient); TArray RepNotifies; + TMap PropertySpanIds; { // Scoped to exclude OnRep callbacks which are already tracked per OnRep function SCOPE_CYCLE_COUNTER(STAT_ReaderApplyPropertyUpdates); + Worker_EntityId EntityId = Channel.GetEntityId(); + FSpatialGDKSpanId CauseSpanId; + if (bEventTracerEnabled) + { + CauseSpanId = EventTracer->GetSpanId(EntityComponentId(EntityId, ComponentId)); + } + 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); + 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; } @@ -193,10 +216,8 @@ void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject& if (NetDriver->IsServer() || ConditionMap.IsRelevant(Parent.Condition)) { - // This is mostly copied from ReceivePropertyHelper in RepLayout.cpp - auto GetSwappedCmd = [&Cmd, &Cmds, &Parents, bIsAuthServer, &Replicator, &Channel, &Parent]() -> const FRepLayoutCmd& - { + auto GetSwappedCmd = [&Cmd, &Cmds, &Parents, bIsAuthServer, &Replicator, &Channel, &Parent]() -> const FRepLayoutCmd& { #if ENGINE_MINOR_VERSION >= 25 // Only swap Role/RemoteRole for actors if (EnumHasAnyFlags(Replicator->RepLayout->GetFlags(), ERepLayoutFlags::IsActor) && !Channel.GetSkipRoleSwap()) @@ -241,7 +262,8 @@ void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject& GDK_PROPERTY(ArrayProperty)* ArrayProperty = GDK_CASTFIELD(Cmd.Property); if (ArrayProperty == nullptr) { - UE_LOG(LogSpatialComponentReader, Error, TEXT("Failed to apply Schema Object %s. One of it's properties is null"), *Object.GetName()); + UE_LOG(LogSpatialComponentReader, Error, TEXT("Failed to apply Schema Object %s. One of it's properties is null"), + *Object.GetName()); continue; } @@ -258,7 +280,8 @@ void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject& if (ValueData.Num() > 0) { - FSpatialNetDeltaSerializeInfo::DeltaSerializeRead(NetDriver, ValueDataReader, &Object, Parent.ArrayIndex, Parent.Property, NetDeltaStruct); + FSpatialNetDeltaSerializeInfo::DeltaSerializeRead(NetDriver, ValueDataReader, &Object, Parent.ArrayIndex, + Parent.Property, NetDeltaStruct); } FObjectReferences* CurEntry = RootObjectReferencesMap.Find(SwappedCmd.Offset); @@ -268,7 +291,10 @@ void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject& { if (bHasReferences) { - RootObjectReferencesMap.Add(SwappedCmd.Offset, FObjectReferences(ValueData, CountBits, MoveTemp(NewMappedRefs), MoveTemp(NewUnresolvedRefs), ShadowOffset, Cmd.ParentIndex, ArrayProperty, /* bFastArrayProp */ true)); + RootObjectReferencesMap.Add( + SwappedCmd.Offset, + FObjectReferences(ValueData, CountBits, MoveTemp(NewMappedRefs), MoveTemp(NewUnresolvedRefs), + ShadowOffset, Cmd.ParentIndex, ArrayProperty, /* bFastArrayProp */ true)); } else { @@ -279,12 +305,14 @@ void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject& } else { - ApplyArray(ComponentObject, FieldId, RootObjectReferencesMap, ArrayProperty, Data, SwappedCmd.Offset, ShadowOffset, Cmd.ParentIndex, bOutReferencesChanged); + ApplyArray(ComponentObject, FieldId, RootObjectReferencesMap, ArrayProperty, Data, SwappedCmd.Offset, ShadowOffset, + Cmd.ParentIndex, bOutReferencesChanged); } } else { - ApplyProperty(ComponentObject, FieldId, RootObjectReferencesMap, 0, Cmd.Property, Data, SwappedCmd.Offset, ShadowOffset, Cmd.ParentIndex, bOutReferencesChanged); + ApplyProperty(ComponentObject, FieldId, RootObjectReferencesMap, 0, Cmd.Property, Data, SwappedCmd.Offset, ShadowOffset, + Cmd.ParentIndex, bOutReferencesChanged); } if (Cmd.Property->GetFName() == NAME_RemoteRole) @@ -298,10 +326,25 @@ void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject& } } + FSpatialGDKSpanId SpanId; + if (bEventTracerEnabled) + { + EventTraceUniqueId LinearTraceId = EventTraceUniqueId::GenerateForProperty(EntityId, Cmd.Property); + SpanId = EventTracer->TraceEvent(FSpatialTraceEventBuilder::CreateReceivePropertyUpdate( + &Object, EntityId, ComponentId, Cmd.Property->GetName(), LinearTraceId), + CauseSpanId.GetConstId(), 1); + } + // Parent.Property is the "root" replicated property, e.g. if a struct property was flattened if (Parent.Property->HasAnyPropertyFlags(CPF_RepNotify)) { - bool bIsIdentical = Cmd.Property->Identical(RepState->GetReceivingRepState()->StaticBuffer.GetData() + SwappedCmd.ShadowOffset, Data); + bool bIsIdentical = + Cmd.Property->Identical(RepState->GetReceivingRepState()->StaticBuffer.GetData() + SwappedCmd.ShadowOffset, Data); + + if (bEventTracerEnabled) + { + PropertySpanIds.Add(Parent.Property, SpanId); + } // Only call RepNotify for REPNOTIFY_Always if we are not applying initial data. if (bIsInitialData) @@ -325,10 +368,12 @@ void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject& Channel.RemoveRepNotifiesWithUnresolvedObjs(RepNotifies, *Replicator->RepLayout, RootObjectReferencesMap, &Object); - Channel.PostReceiveSpatialUpdate(&Object, RepNotifies); + Channel.PostReceiveSpatialUpdate(&Object, RepNotifies, PropertySpanIds); } -void ComponentReader::ApplyHandoverSchemaObject(Schema_Object* ComponentObject, UObject& Object, USpatialActorChannel& Channel, bool bIsInitialData, const TArray& UpdatedIds, Worker_ComponentId ComponentId, bool& bOutReferencesChanged) +void ComponentReader::ApplyHandoverSchemaObject(Schema_Object* ComponentObject, UObject& Object, USpatialActorChannel& Channel, + bool bIsInitialData, const TArray& UpdatedIds, + Worker_ComponentId ComponentId, bool& bOutReferencesChanged) { SCOPE_CYCLE_COUNTER(STAT_ReaderApplyHandoverPropertyUpdates); @@ -346,7 +391,10 @@ void ComponentReader::ApplyHandoverSchemaObject(Schema_Object* ComponentObject, // 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]; @@ -355,18 +403,22 @@ void ComponentReader::ApplyHandoverSchemaObject(Schema_Object* ComponentObject, if (GDK_PROPERTY(ArrayProperty)* ArrayProperty = GDK_CASTFIELD(PropertyInfo.Property)) { - ApplyArray(ComponentObject, FieldId, RootObjectReferencesMap, ArrayProperty, Data, PropertyInfo.Offset, -1, -1, bOutReferencesChanged); + ApplyArray(ComponentObject, FieldId, RootObjectReferencesMap, ArrayProperty, Data, PropertyInfo.Offset, -1, -1, + bOutReferencesChanged); } else { - ApplyProperty(ComponentObject, FieldId, RootObjectReferencesMap, 0, PropertyInfo.Property, Data, PropertyInfo.Offset, -1, -1, bOutReferencesChanged); + 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, GDK_PROPERTY(Property)* Property, uint8* Data, int32 Offset, int32 ShadowOffset, int32 ParentIndex, bool& bOutReferencesChanged) +void ComponentReader::ApplyProperty(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, + uint32 Index, GDK_PROPERTY(Property) * Property, uint8* Data, int32 Offset, int32 ShadowOffset, + int32 ParentIndex, bool& bOutReferencesChanged) { SCOPE_CYCLE_COUNTER(STAT_ReaderApplyProperty); @@ -387,7 +439,8 @@ void ComponentReader::ApplyProperty(Schema_Object* Object, Schema_FieldId FieldI { if (bHasReferences) { - InObjectReferencesMap.Add(Offset, FObjectReferences(ValueData, CountBits, MoveTemp(NewDynamicRefs), MoveTemp(NewUnresolvedRefs), ShadowOffset, ParentIndex, Property)); + InObjectReferencesMap.Add(Offset, FObjectReferences(ValueData, CountBits, MoveTemp(NewDynamicRefs), + MoveTemp(NewUnresolvedRefs), ShadowOffset, ParentIndex, Property)); } else { @@ -470,12 +523,13 @@ void ComponentReader::ApplyProperty(Schema_Object* Object, Schema_FieldId FieldI } bOutReferencesChanged = true; } - if(!bUnresolved) + if (!bUnresolved) { 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()); + checkf(ObjectValue->IsA(ObjectProperty->PropertyClass), TEXT("Object ref %s maps to object %s with the wrong class."), + *ObjectRef.ToString(), *ObjectValue->GetFullName()); } } } @@ -500,7 +554,8 @@ void ComponentReader::ApplyProperty(Schema_Object* Object, Schema_FieldId FieldI } else { - ApplyProperty(Object, FieldId, InObjectReferencesMap, Index, EnumProperty->GetUnderlyingProperty(), Data, Offset, ShadowOffset, ParentIndex, bOutReferencesChanged); + ApplyProperty(Object, FieldId, InObjectReferencesMap, Index, EnumProperty->GetUnderlyingProperty(), Data, Offset, ShadowOffset, + ParentIndex, bOutReferencesChanged); } } else @@ -509,7 +564,9 @@ void ComponentReader::ApplyProperty(Schema_Object* Object, Schema_FieldId FieldI } } -void ComponentReader::ApplyArray(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, GDK_PROPERTY(ArrayProperty)* Property, uint8* Data, int32 Offset, int32 ShadowOffset, int32 ParentIndex, bool& bOutReferencesChanged) +void ComponentReader::ApplyArray(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, + GDK_PROPERTY(ArrayProperty) * Property, uint8* Data, int32 Offset, int32 ShadowOffset, int32 ParentIndex, + bool& bOutReferencesChanged) { SCOPE_CYCLE_COUNTER(STAT_ReaderApplyArray); @@ -537,7 +594,8 @@ 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, bOutReferencesChanged); + ApplyProperty(Object, FieldId, *ArrayObjectReferences, i, Property->Inner, ArrayHelper.GetRawPtr(i), ElementOffset, ElementOffset, + ParentIndex, bOutReferencesChanged); } if (ArrayObjectReferences->Num() > 0) @@ -561,7 +619,7 @@ void ComponentReader::ApplyArray(Schema_Object* Object, Schema_FieldId FieldId, } } -uint32 ComponentReader::GetPropertyCount(const Schema_Object* Object, Schema_FieldId FieldId, GDK_PROPERTY(Property)* Property) +uint32 ComponentReader::GetPropertyCount(const Schema_Object* Object, Schema_FieldId FieldId, GDK_PROPERTY(Property) * Property) { if (GDK_PROPERTY(StructProperty)* StructProperty = GDK_CASTFIELD(Property)) { diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityFactory.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityFactory.cpp index fc2542338c..6b13367117 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityFactory.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityFactory.cpp @@ -3,20 +3,19 @@ #include "Utils/EntityFactory.h" #include "EngineClasses/SpatialActorChannel.h" +#include "EngineClasses/SpatialNetConnection.h" #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" #include "EngineClasses/SpatialVirtualWorkerTranslator.h" -#include "Interop/SpatialRPCService.h" +#include "Interop/RPCs/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/SpatialDebugging.h" #include "Schema/SpawnData.h" +#include "Schema/StandardLibrary.h" #include "Schema/Tombstone.h" #include "SpatialCommonTypes.h" #include "SpatialConstants.h" @@ -30,159 +29,61 @@ #include "Engine/LevelScriptActor.h" #include "GameFramework/GameModeBase.h" #include "GameFramework/GameStateBase.h" +#include "Runtime/Launch/Resources/Version.h" DEFINE_LOG_CATEGORY(LogEntityFactory); namespace SpatialGDK { - -EntityFactory::EntityFactory(USpatialNetDriver* InNetDriver, USpatialPackageMapClient* InPackageMap, USpatialClassInfoManager* InClassInfoManager, SpatialRPCService* InRPCService) +EntityFactory::EntityFactory(USpatialNetDriver* InNetDriver, USpatialPackageMapClient* InPackageMap, + USpatialClassInfoManager* InClassInfoManager, SpatialRPCService* InRPCService) : NetDriver(InNetDriver) , PackageMap(InPackageMap) , ClassInfoManager(InClassInfoManager) , RPCService(InRPCService) -{ } +{ +} -TArray EntityFactory::CreateEntityComponents(USpatialActorChannel* Channel, FRPCsOnEntityCreationMap& OutgoingOnCreateEntityRPCs, uint32& OutBytesWritten) +TArray EntityFactory::CreateEntityComponents(USpatialActorChannel* Channel, uint32& OutBytesWritten) { AActor* Actor = Channel->Actor; UClass* Class = Actor->GetClass(); Worker_EntityId EntityId = Channel->GetEntityId(); - FString ClientWorkerAttribute = GetConnectionOwningWorkerId(Actor); - - WorkerRequirementSet AnyServerRequirementSet = { SpatialConstants::UnrealServerAttributeSet }; - WorkerRequirementSet AnyServerOrClientRequirementSet = { SpatialConstants::UnrealServerAttributeSet, SpatialConstants::UnrealClientAttributeSet }; - - WorkerAttributeSet OwningClientAttributeSet = { ClientWorkerAttribute }; - - WorkerRequirementSet AnyServerOrOwningClientRequirementSet = { SpatialConstants::UnrealServerAttributeSet, OwningClientAttributeSet }; - WorkerRequirementSet OwningClientOnlyRequirementSet = { OwningClientAttributeSet }; + AuthorityDelegationMap DelegationMap{}; + const Worker_PartitionId AuthoritativeServerPartitionId = NetDriver->VirtualWorkerTranslator->GetClaimedPartitionId(); + const Worker_PartitionId AuthoritativeClientPartitionId = GetConnectionOwningPartitionId(Actor); + DelegationMap.Add(SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID, AuthoritativeServerPartitionId); + DelegationMap.Add(SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID, AuthoritativeClientPartitionId); const FClassInfo& Info = ClassInfoManager->GetOrCreateClassInfoByClass(Class); // Add Load Balancer Attribute. If this is a single worker deployment, this will be just be the single worker. - WorkerAttributeSet WorkerAttributeOrSpecificWorker = SpatialConstants::UnrealServerAttributeSet; const VirtualWorkerId IntendedVirtualWorkerId = NetDriver->LoadBalanceStrategy->GetLocalVirtualWorkerId(); - if (IntendedVirtualWorkerId != SpatialConstants::INVALID_VIRTUAL_WORKER_ID) - { - const PhysicalWorkerName* IntendedAuthoritativePhysicalWorkerName = NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(IntendedVirtualWorkerId); - WorkerAttributeOrSpecificWorker = { FString::Format(TEXT("workerId:{0}"), { *IntendedAuthoritativePhysicalWorkerName }) }; - } - else - { - UE_LOG(LogEntityFactory, Error, TEXT("Load balancing strategy provided invalid local virtual worker ID during Actor spawn. Actor: %s. Strategy: %s"), *Actor->GetName(), *NetDriver->LoadBalanceStrategy->GetName()); - } - - 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::UNREAL_METADATA_COMPONENT_ID, AuthoritativeWorkerRequirementSet); - ComponentWriteAcl.Add(SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID, AuthoritativeWorkerRequirementSet); - ComponentWriteAcl.Add(SpatialConstants::ENTITY_ACL_COMPONENT_ID, AnyServerRequirementSet); - ComponentWriteAcl.Add(SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID, AuthoritativeWorkerRequirementSet); - - const USpatialGDKSettings* SpatialSettings = GetDefault(); - 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 (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); - } + checkf(IntendedVirtualWorkerId != SpatialConstants::INVALID_VIRTUAL_WORKER_ID, + TEXT("Load balancing strategy provided invalid local virtual worker ID during Actor spawn. " + "Actor: %s. Strategy: %s"), + *Actor->GetName(), *NetDriver->LoadBalanceStrategy->GetName()); 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 + // Static subobjects aren't guaranteed to exist on actor instances, check they are present before adding to delegation component 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. + // 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) @@ -198,7 +99,11 @@ TArray EntityFactory::CreateEntityComponents(USpatialActor // No path in SpatialOS should contain a PIE prefix. FString TempPath = Actor->GetFName().ToString(); +#if ENGINE_MINOR_VERSION >= 26 + GEngine->NetworkRemapPath(NetDriver->GetSpatialOSNetConnection(), TempPath, false /*bIsReading*/); +#else GEngine->NetworkRemapPath(NetDriver, TempPath, false /*bIsReading*/); +#endif StablyNamedObjectRef = FUnrealObjectRef(0, 0, TempPath, OuterObjectRef, true); bNetStartup = Actor->bNetStartup; @@ -209,8 +114,14 @@ TArray EntityFactory::CreateEntityComponents(USpatialActor 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()); + ComponentDatas.Add(NetOwningClientWorker(AuthoritativeClientPartitionId).CreateNetOwningClientWorkerData()); ComponentDatas.Add(AuthorityIntent::CreateAuthorityIntentData(IntendedVirtualWorkerId)); + ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::MIGRATION_DIAGNOSTIC_COMPONENT_ID)); + + if (ShouldActorHaveVisibleComponent(Actor)) + { + ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::VISIBLE_COMPONENT_ID)); + } if (!Class->HasAnySpatialClassFlags(SPATIALCLASS_NotPersistent)) { @@ -222,15 +133,17 @@ TArray EntityFactory::CreateEntityComponents(USpatialActor { check(NetDriver->VirtualWorkerTranslator != nullptr); - const PhysicalWorkerName* PhysicalWorkerName = NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(IntendedVirtualWorkerId); + const PhysicalWorkerName* PhysicalWorkerName = + NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(IntendedVirtualWorkerId); FColor InvalidServerTintColor = NetDriver->SpatialDebugger->InvalidServerTintColor; - FColor IntentColor = PhysicalWorkerName != nullptr ? SpatialGDK::GetColorForWorkerName(*PhysicalWorkerName) : InvalidServerTintColor; + FColor IntentColor = + PhysicalWorkerName != nullptr ? SpatialGDK::GetColorForWorkerName(*PhysicalWorkerName) : InvalidServerTintColor; const bool bIsLocked = NetDriver->LockingPolicy->IsLocked(Actor); - SpatialDebugging DebuggingInfo(SpatialConstants::INVALID_VIRTUAL_WORKER_ID, InvalidServerTintColor, IntendedVirtualWorkerId, IntentColor, bIsLocked); + SpatialDebugging DebuggingInfo(SpatialConstants::INVALID_VIRTUAL_WORKER_ID, InvalidServerTintColor, IntendedVirtualWorkerId, + IntentColor, bIsLocked); ComponentDatas.Add(DebuggingInfo.CreateSpatialDebuggingData()); - ComponentWriteAcl.Add(SpatialConstants::SPATIAL_DEBUGGING_COMPONENT_ID, AuthoritativeWorkerRequirementSet); } #endif @@ -259,33 +172,19 @@ TArray EntityFactory::CreateEntityComponents(USpatialActor FRepChangeState InitialRepChanges = Channel->CreateInitialRepChangeState(Actor); FHandoverChangeState InitialHandoverChanges = Channel->CreateInitialHandoverChangeState(Info); - TArray DynamicComponentDatas = DataFactory.CreateComponentDatas(Actor, Info, InitialRepChanges, InitialHandoverChanges, OutBytesWritten); + TArray DynamicComponentDatas = + DataFactory.CreateComponentDatas(Actor, Info, InitialRepChanges, InitialHandoverChanges, OutBytesWritten); ComponentDatas.Append(DynamicComponentDatas); ComponentDatas.Add(NetDriver->InterestFactory->CreateInterestData(Actor, Info, EntityId)); + Channel->SetNeedOwnerInterestUpdate(!NetDriver->InterestFactory->DoOwnersHaveEntityId(Actor)); + 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); - } - } + checkf(RPCService != nullptr, TEXT("Attempting to create an entity with a null RPCService.")); + ComponentDatas.Append(RPCService->GetRPCComponentsOnEntityCreation(EntityId)); // Only add subobjects which are replicating for (auto RepSubobject = Channel->ReplicationMap.CreateIterator(); RepSubobject; ++RepSubobject) @@ -315,15 +214,8 @@ TArray EntityFactory::CreateEntityComponents(USpatialActor 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); + TArray ActorSubobjectDatas = + DataFactory.CreateComponentDatas(Subobject, SubobjectInfo, SubobjectRepChanges, SubobjectHandoverChanges, OutBytesWritten); ComponentDatas.Append(ActorSubobjectDatas); } @@ -336,8 +228,9 @@ TArray EntityFactory::CreateEntityComponents(USpatialActor { 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)); + // Static subobjects aren't guaranteed to exist on actor instances, check they are present before adding to delegation component + TWeakObjectPtr WeakSubobject = + PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(Channel->GetEntityId(), SubobjectInfoPair.Key)); if (!WeakSubobject.IsValid()) { continue; @@ -358,30 +251,20 @@ TArray EntityFactory::CreateEntityComponents(USpatialActor FHandoverChangeState SubobjectHandoverChanges = Channel->CreateInitialHandoverChangeState(SubobjectInfo); - FWorkerComponentData SubobjectHandoverData = DataFactory.CreateHandoverComponentData(SubobjectInfo.SchemaComponents[SCHEMA_Handover], Subobject, SubobjectInfo, SubobjectHandoverChanges, OutBytesWritten); + 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()); + ComponentDatas.Add(AuthorityDelegation(DelegationMap).CreateAuthorityDelegationData()); - return ComponentDatas; -} + // Add Actor completeness tags. + ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::ACTOR_AUTH_TAG_COMPONENT_ID)); + ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::ACTOR_NON_AUTH_TAG_COMPONENT_ID)); + ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::LB_TAG_COMPONENT_ID)); -// 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; + return ComponentDatas; } TArray EntityFactory::CreateTombstoneEntityComponents(AActor* Actor) @@ -390,20 +273,6 @@ TArray EntityFactory::CreateTombstoneEntityComponents(AAct const UClass* Class = Actor->GetClass(); - // Construct an ACL for a read-only entity. - WorkerRequirementSet AnyServerRequirementSet = { SpatialConstants::UnrealServerAttributeSet }; - WorkerRequirementSet AnyServerOrClientRequirementSet = { SpatialConstants::UnrealServerAttributeSet, SpatialConstants::UnrealClientAttributeSet }; - - 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) @@ -414,7 +283,11 @@ TArray EntityFactory::CreateTombstoneEntityComponents(AAct // No path in SpatialOS should contain a PIE prefix. FString TempPath = Actor->GetFName().ToString(); +#if ENGINE_MINOR_VERSION >= 26 + GEngine->NetworkRemapPath(NetDriver->GetSpatialOSNetConnection(), TempPath, false /*bIsReading*/); +#else GEngine->NetworkRemapPath(NetDriver, TempPath, false /*bIsReading*/); +#endif const TSchemaOption StablyNamedObjectRef = FUnrealObjectRef(0, 0, TempPath, OuterObjectRef, true); TArray Components; @@ -422,7 +295,7 @@ TArray EntityFactory::CreateTombstoneEntityComponents(AAct Components.Add(Metadata(Class->GetName()).CreateMetadataData()); Components.Add(UnrealMetadata(StablyNamedObjectRef, Class->GetPathName(), true).CreateUnrealMetadataData()); Components.Add(Tombstone().CreateData()); - Components.Add(EntityAcl(ReadAcl, WriteAclMap()).CreateEntityAclData()); + Components.Add(AuthorityDelegation().CreateAuthorityDelegationData()); Worker_ComponentId ActorInterestComponentId = ClassInfoManager->ComputeActorInterestComponentId(Actor); if (ActorInterestComponentId != SpatialConstants::INVALID_COMPONENT_ID) @@ -430,12 +303,42 @@ TArray EntityFactory::CreateTombstoneEntityComponents(AAct Components.Add(ComponentFactory::CreateEmptyComponentData(ActorInterestComponentId)); } + if (ShouldActorHaveVisibleComponent(Actor)) + { + Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::VISIBLE_COMPONENT_ID)); + } + if (!Class->HasAnySpatialClassFlags(SPATIALCLASS_NotPersistent)) { Components.Add(Persistence().CreatePersistenceData()); } + // Add Actor completeness tags. + Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::ACTOR_AUTH_TAG_COMPONENT_ID)); + Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::ACTOR_NON_AUTH_TAG_COMPONENT_ID)); + Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::LB_TAG_COMPONENT_ID)); + + return Components; +} + +TArray EntityFactory::CreatePartitionEntityComponents(const Worker_EntityId EntityId, + const InterestFactory* InterestFactory, + const UAbstractLBStrategy* LbStrategy, + VirtualWorkerId VirtualWorker, bool bDebugContextValid) +{ + AuthorityDelegationMap DelegationMap; + DelegationMap.Add(SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID, EntityId); + + TArray Components; + Components.Add(Position().CreatePositionData()); + Components.Add(Metadata(FString::Format(TEXT("PartitionEntity:{0}"), { VirtualWorker })).CreateMetadataData()); + Components.Add(InterestFactory->CreatePartitionInterest(LbStrategy, VirtualWorker, bDebugContextValid).CreateInterestData()); + Components.Add(AuthorityDelegation(DelegationMap).CreateAuthorityDelegationData()); + Components.Add(Persistence().CreatePersistenceData()); + Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::PARTITION_SHADOW_COMPONENT_ID)); + Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::GDK_KNOWN_ENTITY_TAG_COMPONENT_ID)); + return Components; } -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityPool.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityPool.cpp index d18c10a807..637af1205a 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityPool.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityPool.cpp @@ -29,15 +29,15 @@ void UEntityPool::ReserveEntityIDs(int32 EntitiesToReserve) // Set up reserve IDs delegate ReserveEntityIDsDelegate CacheEntityIDsDelegate; - CacheEntityIDsDelegate.BindLambda([EntitiesToReserve, this](const Worker_ReserveEntityIdsResponseOp& Op) - { + CacheEntityIDsDelegate.BindLambda([EntitiesToReserve, this](const Worker_ReserveEntityIdsResponseOp& Op) { bIsAwaitingResponse = false; if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) { // UNR-630 - Temporary hack to avoid failure to reserve entities due to timeout on large maps if (Op.status_code == WORKER_STATUS_CODE_TIMEOUT) { - UE_LOG(LogSpatialEntityPool, Warning, TEXT("Failed to reserve entity IDs Reason: %s. Retrying..."), UTF8_TO_TCHAR(Op.message)); + UE_LOG(LogSpatialEntityPool, Warning, TEXT("Failed to reserve entity IDs Reason: %s. Retrying..."), + UTF8_TO_TCHAR(Op.message)); ReserveEntityIDs(EntitiesToReserve); } else @@ -52,8 +52,7 @@ void UEntityPool::ReserveEntityIDs(int32 EntitiesToReserve) check(EntitiesToReserve == Op.number_of_entity_ids); // Clean up any expired Entity ranges - ReservedEntityIDRanges = ReservedEntityIDRanges.FilterByPredicate([](const EntityRange& Element) - { + ReservedEntityIDRanges = ReservedEntityIDRanges.FilterByPredicate([](const EntityRange& Element) { return !Element.bExpired; }); @@ -62,19 +61,22 @@ void UEntityPool::ReserveEntityIDs(int32 EntitiesToReserve) NewEntityRange.LastEntityId = Op.first_entity_id + (Op.number_of_entity_ids - 1); NewEntityRange.EntityRangeId = NextEntityRangeId++; - UE_LOG(LogSpatialEntityPool, Verbose, TEXT("Reserved %d entities, caching in pool, Entity IDs: (%d, %d) Range ID: %d"), Op.number_of_entity_ids, Op.first_entity_id, NewEntityRange.LastEntityId, NewEntityRange.EntityRangeId); + UE_LOG(LogSpatialEntityPool, Verbose, TEXT("Reserved %d entities, caching in pool, Entity IDs: (%d, %d) Range ID: %d"), + Op.number_of_entity_ids, Op.first_entity_id, NewEntityRange.LastEntityId, NewEntityRange.EntityRangeId); ReservedEntityIDRanges.Add(NewEntityRange); FTimerHandle ExpirationTimer; TWeakObjectPtr WeakThis(this); - TimerManager->SetTimer(ExpirationTimer, [WeakThis, ExpiringEntityRangeId = NewEntityRange.EntityRangeId]() - { - if (UEntityPool* Pool = WeakThis.Get()) - { - Pool->OnEntityRangeExpired(ExpiringEntityRangeId); - } - }, SpatialConstants::ENTITY_RANGE_EXPIRATION_INTERVAL_SECONDS, false); + TimerManager->SetTimer( + ExpirationTimer, + [WeakThis, ExpiringEntityRangeId = NewEntityRange.EntityRangeId]() { + if (UEntityPool* Pool = WeakThis.Get()) + { + Pool->OnEntityRangeExpired(ExpiringEntityRangeId); + } + }, + SpatialConstants::ENTITY_RANGE_EXPIRATION_INTERVAL_SECONDS, false); if (!bIsReady) { @@ -84,7 +86,7 @@ void UEntityPool::ReserveEntityIDs(int32 EntitiesToReserve) }); // Reserve the Entity IDs - Worker_RequestId ReserveRequestID = NetDriver->Connection->SendReserveEntityIdsRequest(EntitiesToReserve); + Worker_RequestId ReserveRequestID = NetDriver->Connection->SendReserveEntityIdsRequest(EntitiesToReserve, RETRY_UNTIL_COMPLETE); bIsAwaitingResponse = true; // Add the spawn delegate @@ -95,8 +97,7 @@ void UEntityPool::OnEntityRangeExpired(uint32 ExpiringEntityRangeId) { UE_LOG(LogSpatialEntityPool, Verbose, TEXT("Entity range expired! Range ID: %d"), ExpiringEntityRangeId); - int32 FoundEntityRangeIndex = ReservedEntityIDRanges.IndexOfByPredicate([ExpiringEntityRangeId](const EntityRange& Element) - { + int32 FoundEntityRangeIndex = ReservedEntityIDRanges.IndexOfByPredicate([ExpiringEntityRangeId](const EntityRange& Element) { return Element.EntityRangeId == ExpiringEntityRangeId; }); @@ -110,7 +111,8 @@ void UEntityPool::OnEntityRangeExpired(uint32 ExpiringEntityRangeId) if (FoundEntityRangeIndex < ReservedEntityIDRanges.Num() - 1) { // This is not the most recent entity range, just clean up without requesting additional IDs. - UE_LOG(LogSpatialEntityPool, Verbose, TEXT("Newer range detected, cleaning up Entity range ID: %d without new request"), ExpiringEntityRangeId); + UE_LOG(LogSpatialEntityPool, Verbose, TEXT("Newer range detected, cleaning up Entity range ID: %d without new request"), + ExpiringEntityRangeId); ReservedEntityIDRanges.RemoveAt(FoundEntityRangeIndex); } else @@ -131,7 +133,8 @@ Worker_EntityId UEntityPool::GetNextEntityId() if (ReservedEntityIDRanges.Num() == 0) { // TODO: Improve error message - UE_LOG(LogSpatialEntityPool, Warning, TEXT("Tried to pop an entity ID from the pool when there were no entity IDs. Try altering your Entity Pool configuration")); + UE_LOG(LogSpatialEntityPool, Warning, + TEXT("Tried to pop an entity ID from the pool when there were no entity IDs. Try altering your Entity Pool configuration")); return SpatialConstants::INVALID_ENTITY_ID; } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/ISpatialAwaitable.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/ISpatialAwaitable.cpp new file mode 100644 index 0000000000..48d0a6162d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/ISpatialAwaitable.cpp @@ -0,0 +1,5 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Utils/ISpatialAwaitable.h" + +DEFINE_LOG_CATEGORY(LogAwaitable); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/InspectionColors.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/InspectionColors.cpp index 515e8b9806..99a95e5863 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/InspectionColors.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/InspectionColors.cpp @@ -11,104 +11,111 @@ 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; - } +const int32 MIN_HUE = 10; +const int32 MAX_HUE = 350; +const int32 MIN_SATURATION = 60; +const int32 MAX_SATURATION = 100; +const int32 MIN_LIGHTNESS = 20; +const int32 MAX_LIGHTNESS = 70; - FColor HSLtoRGB(double Hue, double Saturation, double Lightness) - { - // const[h, s, l] = hsl; - // Must be fractions of 1 +int64 GenerateValueFromThresholds(int64 Hash, int32 Min, int32 Max) +{ + return Hash % FMath::Abs(Max - Min) + Min; +} - 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; +FColor HSLtoRGB(double Hue, double Saturation, double Lightness) +{ + // const[h, s, l] = hsl; + // Must be fractions of 1 - double r = 0; - double g = 0; - double b = 0; + 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; - 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; + double r = 0; + double g = 0; + double b = 0; - return FColor{ static_cast(r), static_cast(g), static_cast(b) }; + 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; - 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); + return FColor{ static_cast(r), static_cast(g), static_cast(b) }; +} + +int64 DJBReverseHash(const FString& Input) +{ + const int32 StringLength = Input.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(Input[i]); } + return Hash; } +} // namespace FColor GetColorForWorkerName(const PhysicalWorkerName& WorkerName) { - int64 Hash = DJBReverseHash(WorkerName); + const int64 LightnessHash = FMath::Abs(DJBReverseHash("lightness: " + WorkerName)); + const double Lightness = GenerateValueFromThresholds(LightnessHash, MIN_LIGHTNESS, MAX_LIGHTNESS); + + const int64 SaturationHash = FMath::Abs(DJBReverseHash("saturation: " + WorkerName)); + const double Saturation = GenerateValueFromThresholds(SaturationHash, MIN_SATURATION, MAX_SATURATION); - 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); + const int64 HueHash = FMath::Abs(DJBReverseHash("hue: " + WorkerName)); + const double Hue = GenerateValueFromThresholds(HueHash, MIN_HUE, MAX_HUE); return SpatialGDK::HSLtoRGB(Hue, Saturation, Lightness); } -} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/Interest/NetCullDistanceInterest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/Interest/NetCullDistanceInterest.cpp index 52209ce8a9..a0e39085d2 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/Interest/NetCullDistanceInterest.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/Interest/NetCullDistanceInterest.cpp @@ -14,7 +14,6 @@ const float FullFrequencyHz = 0.f; namespace SpatialGDK { - // And this the empty optional type it will be translated to. const TSchemaOption FullFrequencyOptional = TSchemaOption(); @@ -54,14 +53,14 @@ FrequencyConstraints NetCullDistanceInterest::CreateLegacyNetCullDistanceConstra 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); + 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); + TArray CheckoutRadiusConstraints = + NetCullDistanceInterest::BuildNonDefaultActorCheckoutConstraints(DistanceToActorTypeComponents, InClassInfoManager); // Add all the different actor queries to the overall checkout constraint. for (auto& ActorCheckoutConstraint : CheckoutRadiusConstraints) @@ -144,7 +143,8 @@ FrequencyConstraints NetCullDistanceInterest::CreateNetCullDistanceConstraintWit // De dupe across frequencies. for (auto& FrequencyConstraintsPair : FrequencyToConstraints) { - TSchemaOption SpatialFrequency = FrequencyConstraintsPair.Key == FullFrequencyHz ? FullFrequencyOptional : TSchemaOption(FrequencyConstraintsPair.Key); + TSchemaOption SpatialFrequency = + FrequencyConstraintsPair.Key == FullFrequencyHz ? FullFrequencyOptional : TSchemaOption(FrequencyConstraintsPair.Key); if (FrequencyConstraintsPair.Value.Num() == 1) { CheckoutConstraints.Add({ SpatialFrequency, FrequencyConstraintsPair.Value[0] }); @@ -158,7 +158,8 @@ FrequencyConstraints NetCullDistanceInterest::CreateNetCullDistanceConstraintWit return CheckoutConstraints; } -void NetCullDistanceInterest::AddToFrequencyConstraintMap(const float Frequency, const QueryConstraint& Constraint, FrequencyToConstraintsMap& OutFrequencyToConstraints) +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. @@ -178,7 +179,7 @@ QueryConstraint NetCullDistanceInterest::GetDefaultCheckoutRadiusConstraint() 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); DefaultDistanceSquared = MaxDistanceSquared; } @@ -227,7 +228,7 @@ TMap NetCullDistanceInterest::GetActorTypeToRadius() 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); + *It->GetName(), ActorNetCullDistanceSquared, MaxDistanceSquared); ActorNetCullDistanceSquared = MaxDistanceSquared; } @@ -250,7 +251,8 @@ TMap NetCullDistanceInterest::GetActorTypeToRadius() { 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. + // 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; @@ -289,7 +291,8 @@ TMap> NetCullDistanceInterest::DedupeDistancesAcrossActor return RadiusToActorTypes; } -TArray NetCullDistanceInterest::BuildNonDefaultActorCheckoutConstraints(TMap> DistanceToActorTypes, USpatialClassInfoManager* ClassInfoManager) +TArray NetCullDistanceInterest::BuildNonDefaultActorCheckoutConstraints(TMap> DistanceToActorTypes, + USpatialClassInfoManager* ClassInfoManager) { TArray CheckoutConstraints; for (const auto& DistanceActorsPair : DistanceToActorTypes) @@ -314,13 +317,15 @@ TArray NetCullDistanceInterest::BuildNonDefaultActorCheckoutCon 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. + // 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) +void NetCullDistanceInterest::AddTypeHierarchyToConstraint(const UClass& BaseType, QueryConstraint& OutConstraint, + USpatialClassInfoManager* ClassInfoManager) { check(ClassInfoManager); TArray ComponentIds = ClassInfoManager->GetComponentIdsForClassHierarchy(BaseType); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp index 7db147c463..28283304c0 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp @@ -13,8 +13,8 @@ #include "Utils/GDKPropertyMacros.h" #include "Utils/Interest/NetCullDistanceInterest.h" -#include "Engine/World.h" #include "Engine/Classes/GameFramework/Actor.h" +#include "Engine/World.h" #include "GameFramework/PlayerController.h" #include "UObject/UObjectIterator.h" @@ -25,7 +25,6 @@ DECLARE_CYCLE_STAT(TEXT("AddUserDefinedQueries"), STAT_InterestFactoryAddUserDef namespace SpatialGDK { - InterestFactory::InterestFactory(USpatialClassInfoManager* InClassInfoManager, USpatialPackageMapClient* InPackageMap) : ClassInfoManager(InClassInfoManager) , PackageMap(InPackageMap) @@ -44,13 +43,13 @@ void InterestFactory::CreateAndCacheInterestState() SchemaResultType InterestFactory::CreateClientNonAuthInterestResultType() { - SchemaResultType ClientNonAuthResultType; + SchemaResultType ClientNonAuthResultType{}; // Add the required unreal components - ClientNonAuthResultType.Append(SpatialConstants::REQUIRED_COMPONENTS_FOR_NON_AUTH_CLIENT_INTEREST); + ClientNonAuthResultType.ComponentIds.Append(SpatialConstants::REQUIRED_COMPONENTS_FOR_NON_AUTH_CLIENT_INTEREST); // Add all data components- clients don't need to see handover or owner only components on other entities. - ClientNonAuthResultType.Append(ClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_Data)); + ClientNonAuthResultType.ComponentSetsIds.Push(SpatialConstants::DATA_COMPONENT_SET_ID); return ClientNonAuthResultType; } @@ -60,35 +59,37 @@ SchemaResultType InterestFactory::CreateClientAuthInterestResultType() SchemaResultType ClientAuthResultType; // Add the required known components - ClientAuthResultType.Append(SpatialConstants::REQUIRED_COMPONENTS_FOR_AUTH_CLIENT_INTEREST); - ClientAuthResultType.Append(SpatialConstants::REQUIRED_COMPONENTS_FOR_NON_AUTH_CLIENT_INTEREST); + ClientAuthResultType.ComponentIds.Append(SpatialConstants::REQUIRED_COMPONENTS_FOR_AUTH_CLIENT_INTEREST); + ClientAuthResultType.ComponentIds.Append(SpatialConstants::REQUIRED_COMPONENTS_FOR_NON_AUTH_CLIENT_INTEREST); // Add all the generated unreal components - ClientAuthResultType.Append(ClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_Data)); - ClientAuthResultType.Append(ClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_OwnerOnly)); + ClientAuthResultType.ComponentSetsIds.Push(SpatialConstants::DATA_COMPONENT_SET_ID); + ClientAuthResultType.ComponentSetsIds.Push(SpatialConstants::OWNER_ONLY_COMPONENT_SET_ID); return ClientAuthResultType; } SchemaResultType InterestFactory::CreateServerNonAuthInterestResultType() { - SchemaResultType ServerNonAuthResultType; + SchemaResultType ServerNonAuthResultType{}; // Add the required unreal components - ServerNonAuthResultType.Append(SpatialConstants::REQUIRED_COMPONENTS_FOR_NON_AUTH_SERVER_INTEREST); + ServerNonAuthResultType.ComponentIds.Append(SpatialConstants::REQUIRED_COMPONENTS_FOR_NON_AUTH_SERVER_INTEREST); // Add all data, owner only, and handover components - ServerNonAuthResultType.Append(ClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_Data)); - ServerNonAuthResultType.Append(ClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_OwnerOnly)); - ServerNonAuthResultType.Append(ClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_Handover)); + ServerNonAuthResultType.ComponentSetsIds.Push(SpatialConstants::DATA_COMPONENT_SET_ID); + ServerNonAuthResultType.ComponentSetsIds.Push(SpatialConstants::OWNER_ONLY_COMPONENT_SET_ID); + ServerNonAuthResultType.ComponentSetsIds.Push(SpatialConstants::HANDOVER_COMPONENT_SET_ID); return ServerNonAuthResultType; } SchemaResultType InterestFactory::CreateServerAuthInterestResultType() { + SchemaResultType ServerAuthResultType{}; // Just the components that we won't have already checked out through authority - return SpatialConstants::REQUIRED_COMPONENTS_FOR_AUTH_SERVER_INTEREST; + ServerAuthResultType.ComponentIds.Append(SpatialConstants::REQUIRED_COMPONENTS_FOR_AUTH_SERVER_INTEREST); + return ServerAuthResultType; } Worker_ComponentData InterestFactory::CreateInterestData(AActor* InActor, const FClassInfo& InInfo, const Worker_EntityId InEntityId) const @@ -96,63 +97,84 @@ Worker_ComponentData InterestFactory::CreateInterestData(AActor* InActor, const return CreateInterest(InActor, InInfo, InEntityId).CreateInterestData(); } -Worker_ComponentUpdate InterestFactory::CreateInterestUpdate(AActor* InActor, const FClassInfo& InInfo, const Worker_EntityId InEntityId) const +Worker_ComponentUpdate InterestFactory::CreateInterestUpdate(AActor* InActor, const FClassInfo& InInfo, + const Worker_EntityId InEntityId) const { return CreateInterest(InActor, InInfo, InEntityId).CreateInterestUpdate(); } -Interest InterestFactory::CreateServerWorkerInterest(const UAbstractLBStrategy* LBStrategy) +Interest InterestFactory::CreateServerWorkerInterest(const UAbstractLBStrategy* LBStrategy) const { - const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); - // 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 - ServerQuery.ResultComponentIds = ServerNonAuthInterestResultType; + Query ServerQuery{}; - // Ensure server worker receives always relevant entities - QueryConstraint AlwaysRelevantConstraint = CreateAlwaysRelevantConstraint(); - - Constraint = AlwaysRelevantConstraint; + // Workers have interest in all system worker entities. + ServerQuery = Query(); + ServerQuery.ResultComponentIds = { SpatialConstants::WORKER_COMPONENT_ID }; + ServerQuery.Constraint.ComponentConstraint = SpatialConstants::WORKER_COMPONENT_ID; + AddComponentQueryPairToInterestComponent(ServerInterest, SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID, ServerQuery); - // Also add the server worker interest defined by the load balancing strategy if there is more than one worker. - if (LBStrategy->GetMinimumRequiredWorkers() > 1) - { - check(LBStrategy != nullptr); + // And an interest in all server worker entities. + ServerQuery = Query(); + ServerQuery.ResultComponentIds = { SpatialConstants::SERVER_WORKER_COMPONENT_ID, SpatialConstants::GDK_KNOWN_ENTITY_TAG_COMPONENT_ID }; + ServerQuery.Constraint.ComponentConstraint = SpatialConstants::SERVER_WORKER_COMPONENT_ID; + AddComponentQueryPairToInterestComponent(ServerInterest, SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID, ServerQuery); - // 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; - } - } + // Ensure server worker receives core GDK snapshot entities. + ServerQuery = Query(); + ServerQuery.ResultComponentIds = ServerNonAuthInterestResultType.ComponentIds; + ServerQuery.ResultComponentSetIds = ServerNonAuthInterestResultType.ComponentSetsIds; + ServerQuery.Constraint = CreateGDKSnapshotEntitiesConstraint(); + AddComponentQueryPairToInterestComponent(ServerInterest, SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID, ServerQuery); - ServerQuery.Constraint = Constraint; - AddComponentQueryPairToInterestComponent(ServerInterest, SpatialConstants::POSITION_COMPONENT_ID, ServerQuery); + return ServerInterest; +} - // 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 InterestFactory::CreatePartitionInterest(const UAbstractLBStrategy* LBStrategy, VirtualWorkerId VirtualWorker, bool bDebug) const +{ + // Add load balancing query + Interest PartitionInterest{}; + Query PartitionQuery{}; + + AddLoadBalancingInterestQuery(LBStrategy, VirtualWorker, PartitionInterest); + + // Ensure server worker receives AlwaysRelevant entities. + PartitionQuery = Query(); + PartitionQuery.ResultComponentIds = ServerNonAuthInterestResultType.ComponentIds; + PartitionQuery.ResultComponentSetIds = ServerNonAuthInterestResultType.ComponentSetsIds; + PartitionQuery.Constraint = CreateServerAlwaysRelevantConstraint(); + AddComponentQueryPairToInterestComponent(PartitionInterest, SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID, PartitionQuery); + + // Add a self query for completeness + PartitionQuery = Query(); + PartitionQuery.ResultComponentIds = { SpatialConstants::GDK_KNOWN_ENTITY_TAG_COMPONENT_ID }; + PartitionQuery.Constraint.bSelfConstraint = true; + AddComponentQueryPairToInterestComponent(PartitionInterest, SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID, PartitionQuery); + + // Query to know about all the actors tagged with a debug component + if (bDebug) + { + PartitionQuery = Query(); + PartitionQuery.ResultComponentIds = { SpatialConstants::GDK_DEBUG_COMPONENT_ID }; + PartitionQuery.Constraint.ComponentConstraint = SpatialConstants::GDK_DEBUG_COMPONENT_ID; + AddComponentQueryPairToInterestComponent(PartitionInterest, SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID, + PartitionQuery); + } - ServerQuery = Query(); - ServerQuery.ResultComponentIds = SchemaResultType{ SpatialConstants::WORKER_COMPONENT_ID }; - ServerQuery.Constraint.ComponentConstraint = SpatialConstants::WORKER_COMPONENT_ID; - AddComponentQueryPairToInterestComponent(ServerInterest, SpatialConstants::POSITION_COMPONENT_ID, ServerQuery); + return PartitionInterest; +} - return ServerInterest; +void InterestFactory::AddLoadBalancingInterestQuery(const UAbstractLBStrategy* LBStrategy, VirtualWorkerId VirtualWorker, + Interest& OutInterest) const +{ + // Add load balancing query + Query PartitionQuery{}; + PartitionQuery.ResultComponentIds = ServerNonAuthInterestResultType.ComponentIds; + PartitionQuery.ResultComponentSetIds = ServerNonAuthInterestResultType.ComponentSetsIds; + PartitionQuery.Constraint = LBStrategy->GetWorkerInterestQueryConstraint(VirtualWorker); + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID, PartitionQuery); } Interest InterestFactory::CreateInterest(AActor* InActor, const FClassInfo& InInfo, const Worker_EntityId InEntityId) const @@ -169,17 +191,19 @@ Interest InterestFactory::CreateInterest(AActor* InActor, const FClassInfo& InIn } // Clients need to see owner only and server RPC components on entities they have authority over - AddClientSelfInterest(ResultInterest, InEntityId); + AddClientSelfInterest(ResultInterest); // Every actor needs a self query for the server to the client RPC endpoint - AddServerSelfInterest(ResultInterest, InEntityId); + AddServerSelfInterest(ResultInterest); + + AddOwnerInterestOnServer(ResultInterest, InActor, InEntityId); return ResultInterest; } void InterestFactory::AddPlayerControllerActorInterest(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo) const { - QueryConstraint LevelConstraint = CreateLevelConstraints(InActor); + const QueryConstraint LevelConstraint = CreateLevelConstraints(InActor); AddAlwaysRelevantAndInterestedQuery(OutInterest, InActor, InInfo, LevelConstraint); @@ -192,39 +216,87 @@ void InterestFactory::AddPlayerControllerActorInterest(Interest& OutInterest, co } } -void InterestFactory::AddClientSelfInterest(Interest& OutInterest, const Worker_EntityId& EntityId) const +void InterestFactory::AddClientSelfInterest(Interest& OutInterest) const { Query NewQuery; // Just an entity ID constraint is fine, as clients should not become authoritative over entities outside their loaded levels - NewQuery.Constraint.EntityIdConstraint = EntityId; - NewQuery.ResultComponentIds = ClientAuthInterestResultType; + NewQuery.Constraint.bSelfConstraint = true; + NewQuery.ResultComponentIds = ClientAuthInterestResultType.ComponentIds; + NewQuery.ResultComponentSetIds = ClientAuthInterestResultType.ComponentSetsIds; - AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::GetClientAuthorityComponent(GetDefault()->UseRPCRingBuffer()), NewQuery); + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID, NewQuery); } -void InterestFactory::AddServerSelfInterest(Interest& OutInterest, const Worker_EntityId& EntityId) const +void InterestFactory::AddServerSelfInterest(Interest& OutInterest) const { // Add a query for components all servers need to read client data Query ClientQuery; - ClientQuery.Constraint.EntityIdConstraint = EntityId; - // Temp fix for invalid initial auth server checkout constraints - UNR-3683 - // Using full snapshot ensures all components are available on checkout. Remove when root issue is resolved. - ClientQuery.FullSnapshotResult = true; - AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::POSITION_COMPONENT_ID, ClientQuery); + ClientQuery.Constraint.bSelfConstraint = true; + ClientQuery.ResultComponentIds = ServerAuthInterestResultType.ComponentIds; + ClientQuery.ResultComponentSetIds = ServerAuthInterestResultType.ComponentSetsIds; + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID, ClientQuery); - // Add a query for the load balancing worker (whoever is delegated the ACL) to read the authority intent + // Add a query for the load balancing worker (whoever is delegated the auth delegation component) to read the authority intent Query LoadBalanceQuery; - LoadBalanceQuery.Constraint.EntityIdConstraint = EntityId; - LoadBalanceQuery.ResultComponentIds = SchemaResultType{ SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID, SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID, SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID }; - AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::ENTITY_ACL_COMPONENT_ID, LoadBalanceQuery); + LoadBalanceQuery.Constraint.bSelfConstraint = true; + LoadBalanceQuery.ResultComponentIds = { SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID, + SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID, + SpatialConstants::LB_TAG_COMPONENT_ID }; + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID, LoadBalanceQuery); +} + +bool InterestFactory::DoOwnersHaveEntityId(const AActor* Actor) const +{ + AActor* Owner = Actor->GetOwner(); + + while (Owner != nullptr && !Owner->IsPendingKillPending() && Owner->GetIsReplicated()) + { + if (PackageMap->GetEntityIdFromObject(Owner) == SpatialConstants::INVALID_ENTITY_ID) + { + return false; + } + Owner = Owner->GetOwner(); + } + + return true; +} + +void InterestFactory::AddOwnerInterestOnServer(Interest& OutInterest, const AActor* InActor, const Worker_EntityId& EntityId) const +{ + AActor* Owner = InActor->GetOwner(); + Query OwnerChainQuery; + + while (Owner != nullptr && !Owner->IsPendingKillPending() && Owner->GetIsReplicated()) + { + QueryConstraint OwnerQuery; + OwnerQuery.EntityIdConstraint = PackageMap->GetEntityIdFromObject(Owner); + if (OwnerQuery.EntityIdConstraint == SpatialConstants::INVALID_ENTITY_ID) + { + UE_LOG(LogInterestFactory, Warning, + TEXT("Interest for Actor %s (%llu) is out of date because owner %s does not have an entity id." + "USpatialActorChannel::NeedOwnerInterestUpdate should be set in order to eventually update it"), + *InActor->GetName(), EntityId, *Owner->GetName()); + return; + } + OwnerChainQuery.Constraint.OrConstraint.Add(OwnerQuery); + Owner = Owner->GetOwner(); + } + + if (OwnerChainQuery.Constraint.OrConstraint.Num() != 0) + { + OwnerChainQuery.ResultComponentIds = ServerNonAuthInterestResultType.ComponentIds; + OwnerChainQuery.ResultComponentSetIds = ServerNonAuthInterestResultType.ComponentSetsIds; + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID, OwnerChainQuery); + } } -void InterestFactory::AddAlwaysRelevantAndInterestedQuery(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo, const QueryConstraint& LevelConstraint) const +void InterestFactory::AddAlwaysRelevantAndInterestedQuery(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo, + const QueryConstraint& LevelConstraint) const { const USpatialGDKSettings* Settings = GetDefault(); QueryConstraint AlwaysInterestedConstraint = CreateAlwaysInterestedConstraint(InActor, InInfo); - QueryConstraint AlwaysRelevantConstraint = CreateAlwaysRelevantConstraint(); + QueryConstraint AlwaysRelevantConstraint = CreateClientAlwaysRelevantConstraint(); QueryConstraint SystemDefinedConstraints; @@ -245,9 +317,10 @@ void InterestFactory::AddAlwaysRelevantAndInterestedQuery(Interest& OutInterest, Query ClientSystemQuery; ClientSystemQuery.Constraint = SystemAndLevelConstraint; - ClientSystemQuery.ResultComponentIds = ClientNonAuthInterestResultType; + ClientSystemQuery.ResultComponentIds = ClientNonAuthInterestResultType.ComponentIds; + ClientSystemQuery.ResultComponentSetIds = ClientNonAuthInterestResultType.ComponentSetsIds; - AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::GetClientAuthorityComponent(Settings->UseRPCRingBuffer()), ClientSystemQuery); + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID, ClientSystemQuery); // Add always interested constraint to the server as well to make sure the server sees the same as the client. // The always relevant constraint is added as part of the server worker query, so leave that out here. @@ -258,9 +331,10 @@ void InterestFactory::AddAlwaysRelevantAndInterestedQuery(Interest& OutInterest, QueryConstraint ServerSystemConstraint; ServerSystemConstraint.OrConstraint.Add(AlwaysInterestedConstraint); ServerSystemQuery.Constraint = ServerSystemConstraint; - ServerSystemQuery.ResultComponentIds = ServerNonAuthInterestResultType; + ServerSystemQuery.ResultComponentIds = ServerNonAuthInterestResultType.ComponentIds; + ServerSystemQuery.ResultComponentSetIds = ServerNonAuthInterestResultType.ComponentSetsIds; - AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::POSITION_COMPONENT_ID, ServerSystemQuery); + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID, ServerSystemQuery); } } @@ -297,12 +371,18 @@ void InterestFactory::AddUserDefinedQueries(Interest& OutInterest, const AActor* UserQuery.Constraint.AndConstraint.Add(UserConstraint); UserQuery.Constraint.AndConstraint.Add(LevelConstraint); + // Make sure that the Entity is not marked as bHidden + QueryConstraint VisibilityConstraint; + VisibilityConstraint = CreateActorVisibilityConstraint(); + UserQuery.Constraint.AndConstraint.Add(VisibilityConstraint); + // 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. - UserQuery.ResultComponentIds = ClientNonAuthInterestResultType; + UserQuery.ResultComponentIds = ClientNonAuthInterestResultType.ComponentIds; + UserQuery.ResultComponentSetIds = ClientNonAuthInterestResultType.ComponentSetsIds; - AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::GetClientAuthorityComponent(Settings->UseRPCRingBuffer()), UserQuery); + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID, 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, @@ -312,9 +392,10 @@ void InterestFactory::AddUserDefinedQueries(Interest& OutInterest, const AActor* Query ServerUserQuery; ServerUserQuery.Constraint = UserConstraint; ServerUserQuery.Frequency = FrequencyToConstraints.Key; - ServerUserQuery.ResultComponentIds = ServerNonAuthInterestResultType; + ServerUserQuery.ResultComponentIds = ServerNonAuthInterestResultType.ComponentIds; + ServerUserQuery.ResultComponentSetIds = ServerNonAuthInterestResultType.ComponentSetsIds; - AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::POSITION_COMPONENT_ID, ServerUserQuery); + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID, ServerUserQuery); } } } @@ -323,7 +404,8 @@ FrequencyToConstraintsMap InterestFactory::GetUserDefinedFrequencyToConstraintsM { // 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, + // - 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; @@ -342,7 +424,8 @@ FrequencyToConstraintsMap InterestFactory::GetUserDefinedFrequencyToConstraintsM return FrequencyToConstraints; } -void InterestFactory::GetActorUserDefinedQueryConstraints(const AActor* InActor, FrequencyToConstraintsMap& OutFrequencyToConstraints, bool bRecurseChildren) const +void InterestFactory::GetActorUserDefinedQueryConstraints(const AActor* InActor, FrequencyToConstraintsMap& OutFrequencyToConstraints, + bool bRecurseChildren) const { check(ClassInfoManager); @@ -395,10 +478,16 @@ void InterestFactory::AddNetCullDistanceQueries(Interest& OutInterest, const Que NewQuery.Constraint.AndConstraint.Add(LevelConstraint); } + // Make sure that the Entity is not marked as bHidden + QueryConstraint VisibilityConstraint; + VisibilityConstraint = CreateActorVisibilityConstraint(); + NewQuery.Constraint.AndConstraint.Add(VisibilityConstraint); + NewQuery.Frequency = CheckoutRadiusConstraintFrequencyPair.Frequency; - NewQuery.ResultComponentIds = ClientNonAuthInterestResultType; + NewQuery.ResultComponentIds = ClientNonAuthInterestResultType.ComponentIds; + NewQuery.ResultComponentSetIds = ClientNonAuthInterestResultType.ComponentSetsIds; - AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::GetClientAuthorityComponent(Settings->UseRPCRingBuffer()), NewQuery); + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID, 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) @@ -406,18 +495,20 @@ void InterestFactory::AddNetCullDistanceQueries(Interest& OutInterest, const Que Query ServerQuery; ServerQuery.Constraint = CheckoutRadiusConstraintFrequencyPair.Constraint; ServerQuery.Frequency = CheckoutRadiusConstraintFrequencyPair.Frequency; - ServerQuery.ResultComponentIds = ServerNonAuthInterestResultType; + ServerQuery.ResultComponentIds = ServerNonAuthInterestResultType.ComponentIds; + ServerQuery.ResultComponentSetIds = ServerNonAuthInterestResultType.ComponentSetsIds; - AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::POSITION_COMPONENT_ID, ServerQuery); + AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID, ServerQuery); } } } -void InterestFactory::AddComponentQueryPairToInterestComponent(Interest& OutInterest, const Worker_ComponentId ComponentId, const Query& QueryToAdd) const +void InterestFactory::AddComponentQueryPairToInterestComponent(Interest& OutInterest, const Worker_ComponentId ComponentId, + const Query& QueryToAdd) const { if (!OutInterest.ComponentInterestMap.Contains(ComponentId)) { - ComponentInterest NewComponentInterest; + ComponentSetInterest NewComponentInterest; OutInterest.ComponentInterestMap.Add(ComponentId, NewComponentInterest); } OutInterest.ComponentInterestMap[ComponentId].Queries.Add(QueryToAdd); @@ -459,7 +550,8 @@ QueryConstraint InterestFactory::CreateAlwaysInterestedConstraint(const AActor* FScriptArrayHelper ArrayHelper(ArrayProperty, Data); for (int i = 0; i < ArrayHelper.Num(); i++) { - AddObjectToConstraint(GDK_CASTFIELD(ArrayProperty->Inner), ArrayHelper.GetRawPtr(i), AlwaysInterestedConstraint); + AddObjectToConstraint(GDK_CASTFIELD(ArrayProperty->Inner), ArrayHelper.GetRawPtr(i), + AlwaysInterestedConstraint); } } else @@ -471,24 +563,43 @@ QueryConstraint InterestFactory::CreateAlwaysInterestedConstraint(const AActor* return AlwaysInterestedConstraint; } -QueryConstraint InterestFactory::CreateAlwaysRelevantConstraint() const +QueryConstraint CreateOrConstraint(const TArray& ComponentIds) { - QueryConstraint AlwaysRelevantConstraint; - - Worker_ComponentId ComponentIds[] = { - SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID, - SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID, - SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID - }; + QueryConstraint ComponentOrConstraint; for (Worker_ComponentId ComponentId : ComponentIds) { QueryConstraint Constraint; Constraint.ComponentConstraint = ComponentId; - AlwaysRelevantConstraint.OrConstraint.Add(Constraint); + ComponentOrConstraint.OrConstraint.Add(Constraint); } - return AlwaysRelevantConstraint; + return ComponentOrConstraint; +} + +QueryConstraint InterestFactory::CreateGDKSnapshotEntitiesConstraint() const +{ + return CreateOrConstraint({ SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID, + SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID, SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID }); +} + +QueryConstraint InterestFactory::CreateClientAlwaysRelevantConstraint() const +{ + return CreateOrConstraint({ SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID }); +} + +QueryConstraint InterestFactory::CreateServerAlwaysRelevantConstraint() const +{ + return CreateOrConstraint( + { SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID, SpatialConstants::SERVER_ONLY_ALWAYS_RELEVANT_COMPONENT_ID }); +} + +QueryConstraint InterestFactory::CreateActorVisibilityConstraint() const +{ + QueryConstraint ActorVisibilityConstraint; + ActorVisibilityConstraint.ComponentConstraint = SpatialConstants::VISIBLE_COMPONENT_ID; + + return ActorVisibilityConstraint; } QueryConstraint InterestFactory::CreateLevelConstraints(const AActor* InActor) const @@ -518,15 +629,17 @@ QueryConstraint InterestFactory::CreateLevelConstraints(const AActor* InActor) c } 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?"), *InActor->GetName(), *LevelPath.ToString()); + 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?"), + *InActor->GetName(), *LevelPath.ToString()); } } return LevelConstraint; } -void InterestFactory::AddObjectToConstraint(GDK_PROPERTY(ObjectPropertyBase)* Property, uint8* Data, QueryConstraint& OutConstraint) const +void InterestFactory::AddObjectToConstraint(GDK_PROPERTY(ObjectPropertyBase) * Property, uint8* Data, QueryConstraint& OutConstraint) const { UObject* ObjectOfInterest = Property->GetObjectPropertyValue(Data); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/OpUtils.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/OpUtils.cpp index 8b28f94659..41556573f0 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/OpUtils.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/OpUtils.cpp @@ -5,74 +5,22 @@ namespace SpatialGDK { - -Worker_Op* FindFirstOpOfType(const TArray& InOpLists, const Worker_OpType OpType) -{ - for (const OpList& Ops : InOpLists) - { - for (size_t i = 0; i < Ops.Count; ++i) - { - Worker_Op* Op = &Ops.Ops[i]; - - if (Op->op_type == OpType) - { - return Op; - } - } - } - return nullptr; -} - -void AppendAllOpsOfType(const TArray& InOpLists, const Worker_OpType InOpType, TArray& FoundOps) -{ - for (const OpList& Ops : InOpLists) - { - for (size_t i = 0; i < Ops.Count; ++i) - { - Worker_Op* Op = &Ops.Ops[i]; - - if (Op->op_type == InOpType) - { - FoundOps.Add(Op); - } - } - } -} - -Worker_Op* FindFirstOpOfTypeForComponent(const TArray& InOpLists, const Worker_OpType OpType, const Worker_ComponentId ComponentId) -{ - for (const OpList& Ops : InOpLists) - { - for (size_t i = 0; i < Ops.Count; ++i) - { - Worker_Op* Op = &Ops.Ops[i]; - - if ((Op->op_type == OpType) && - GetComponentId(Op) == ComponentId) - { - return Op; - } - } - } - return nullptr; -} - -Worker_ComponentId GetComponentId(const Worker_Op* Op) +Worker_ComponentId GetComponentId(const Worker_Op& Op) { - switch (Op->op_type) + switch (Op.op_type) { case WORKER_OP_TYPE_ADD_COMPONENT: - return Op->op.add_component.data.component_id; + return Op.op.add_component.data.component_id; case WORKER_OP_TYPE_REMOVE_COMPONENT: - return Op->op.remove_component.component_id; + return Op.op.remove_component.component_id; case WORKER_OP_TYPE_COMPONENT_UPDATE: - return Op->op.component_update.update.component_id; - case WORKER_OP_TYPE_AUTHORITY_CHANGE: - return Op->op.authority_change.component_id; + return Op.op.component_update.update.component_id; + case WORKER_OP_TYPE_COMPONENT_SET_AUTHORITY_CHANGE: + return Op.op.component_set_authority_change.component_set_id; case WORKER_OP_TYPE_COMMAND_REQUEST: - return Op->op.command_request.request.component_id; + return Op.op.command_request.request.component_id; case WORKER_OP_TYPE_COMMAND_RESPONSE: - return Op->op.command_response.response.component_id; + return Op.op.command_response.response.component_id; default: return SpatialConstants::INVALID_COMPONENT_ID; } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp index cc85ce8adb..2804027223 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp @@ -11,93 +11,96 @@ using namespace SpatialGDK; namespace { - FString ERPCResultToString(ERPCResult Result) +FString ERPCResultToString(ERPCResult Result) +{ + switch (Result) { - switch (Result) - { - case ERPCResult::Success: - return TEXT(""); + case ERPCResult::Success: + return TEXT(""); - case ERPCResult::UnresolvedTargetObject: - return TEXT("Unresolved Target Object"); + case ERPCResult::UnresolvedTargetObject: + return TEXT("Unresolved Target Object"); - case ERPCResult::MissingFunctionInfo: - return TEXT("Missing UFunction info"); + case ERPCResult::MissingFunctionInfo: + return TEXT("Missing UFunction info"); - case ERPCResult::UnresolvedParameters: - return TEXT("Unresolved Parameters"); + case ERPCResult::UnresolvedParameters: + return TEXT("Unresolved Parameters"); - case ERPCResult::NoActorChannel: - return TEXT("No Actor Channel"); + case ERPCResult::NoActorChannel: + return TEXT("No Actor Channel"); - case ERPCResult::SpatialActorChannelNotListening: - return TEXT("Spatial Actor Channel Not Listening"); + case ERPCResult::SpatialActorChannelNotListening: + return TEXT("Spatial Actor Channel Not Listening"); - case ERPCResult::NoNetConnection: - return TEXT("No Net Connection"); + case ERPCResult::NoNetConnection: + return TEXT("No Net Connection"); - case ERPCResult::NoAuthority: - return TEXT("No Authority"); + case ERPCResult::NoAuthority: + return TEXT("No Authority"); - case ERPCResult::InvalidRPCType: - return TEXT("Invalid RPC Type"); + case ERPCResult::InvalidRPCType: + return TEXT("Invalid RPC Type"); - case ERPCResult::NoOwningController: - return TEXT("No Owning Controller"); + case ERPCResult::NoOwningController: + return TEXT("No Owning Controller"); - case ERPCResult::NoControllerChannel: - return TEXT("No Controller Channel"); + case ERPCResult::NoControllerChannel: + return TEXT("No Controller Channel"); - case ERPCResult::ControllerChannelNotListening: - return TEXT("Controller Channel Not Listening"); + case ERPCResult::ControllerChannelNotListening: + return TEXT("Controller Channel Not Listening"); - case ERPCResult::RPCServiceFailure: - return TEXT("SpatialRPCService couldn't handle the RPC"); + case ERPCResult::RPCServiceFailure: + return TEXT("SpatialRPCService couldn't handle the RPC"); - default: - return TEXT("Unknown"); - } + default: + return TEXT("Unknown"); } +} - void LogRPCError(const FRPCErrorInfo& ErrorInfo, ERPCQueueType QueueType, 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 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"), + QueueType == ERPCQueueType::Send ? TEXT("sending") : QueueType == ERPCQueueType::Receive ? TEXT("execution") : TEXT("UNKNOWN"), + ErrorInfo.QueueProcessResult == ERPCQueueProcessResult::ContinueProcessing ? TEXT("queued") : TEXT("dropped"), *TimeDiff.ToString(), + *ERPCResultToString(ErrorInfo.ErrorCode)); + + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + check(SpatialGDKSettings != nullptr); + + if (TimeDiff.GetTotalSeconds() > SpatialGDKSettings->GetSecondsBeforeWarning(ErrorInfo.ErrorCode)) { - const FTimespan TimeDiff = FDateTime::Now() - Params.Timestamp; - - // The format is expected to be: - // 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"), - QueueType == ERPCQueueType::Send ? TEXT("sending") : QueueType == ERPCQueueType::Receive ? TEXT("execution") : TEXT("UNKNOWN"), - ErrorInfo.QueueProcessResult == ERPCQueueProcessResult::ContinueProcessing ? TEXT("queued") : TEXT("dropped"), - *TimeDiff.ToString(), - *ERPCResultToString(ErrorInfo.ErrorCode)); - - const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); - check(SpatialGDKSettings != nullptr); - - if (TimeDiff.GetTotalSeconds() > SpatialGDKSettings->GetSecondsBeforeWarning(ErrorInfo.ErrorCode)) - { - UE_LOG(LogRPCContainer, Warning, TEXT("%s"), *OutputLog); - } - else - { - UE_LOG(LogRPCContainer, Verbose, TEXT("%s"), *OutputLog); - } + UE_LOG(LogRPCContainer, Warning, TEXT("%s"), *OutputLog); + } + else + { + UE_LOG(LogRPCContainer, Verbose, TEXT("%s"), *OutputLog); } } +} // namespace -FPendingRPCParams::FPendingRPCParams(const FUnrealObjectRef& InTargetObjectRef, ERPCType InType, RPCPayload&& InPayload) +FPendingRPCParams::FPendingRPCParams(const FUnrealObjectRef& InTargetObjectRef, ERPCType InType, RPCPayload&& InPayload, + TOptional RPCIdForLinearEventTrace) : ObjectRef(InTargetObjectRef) , Payload(MoveTemp(InPayload)) , Timestamp(FDateTime::Now()) , Type(InType) + , RPCIdForLinearEventTrace(RPCIdForLinearEventTrace) { } -void FRPCContainer::ProcessOrQueueRPC(const FUnrealObjectRef& TargetObjectRef, ERPCType Type, RPCPayload&& Payload) +void FRPCContainer::ProcessOrQueueRPC(const FUnrealObjectRef& TargetObjectRef, ERPCType Type, RPCPayload&& Payload, + TOptional RPCIdForLinearEventTrace) { - FPendingRPCParams Params {TargetObjectRef, Type, MoveTemp(Payload)}; + FPendingRPCParams Params{ TargetObjectRef, Type, MoveTemp(Payload), RPCIdForLinearEventTrace }; if (!ObjectHasRPCsQueuedOfType(Params.ObjectRef.Entity, Params.Type)) { @@ -180,9 +183,9 @@ void FRPCContainer::DropForEntity(const Worker_EntityId& EntityId) bool FRPCContainer::ObjectHasRPCsQueuedOfType(const Worker_EntityId& EntityId, ERPCType Type) const { - if(const FRPCMap* MapOfQueues = QueuedRPCs.Find(Type)) + if (const FRPCMap* MapOfQueues = QueuedRPCs.Find(Type)) { - if(const FArrayOfParams* RPCList = MapOfQueues->Find(EntityId)) + if (const FArrayOfParams* RPCList = MapOfQueues->Find(EntityId)) { return (RPCList->Num() > 0); } @@ -190,7 +193,7 @@ bool FRPCContainer::ObjectHasRPCsQueuedOfType(const Worker_EntityId& EntityId, E return false; } - + FRPCContainer::FRPCContainer(ERPCQueueType InQueueType) : QueueType(InQueueType) { diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCRingBuffer.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCRingBuffer.cpp index 4b3356b629..df225d376f 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCRingBuffer.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCRingBuffer.cpp @@ -6,7 +6,6 @@ namespace SpatialGDK { - RPCRingBuffer::RPCRingBuffer(ERPCType InType) : Type(InType) { @@ -15,7 +14,6 @@ RPCRingBuffer::RPCRingBuffer(ERPCType InType) namespace RPCRingBufferUtils { - Worker_ComponentId GetRingBufferComponentId(ERPCType Type) { switch (Type) @@ -34,6 +32,23 @@ Worker_ComponentId GetRingBufferComponentId(ERPCType Type) } } +Worker_ComponentId GetRingBufferAuthComponentSetId(ERPCType Type) +{ + switch (Type) + { + case ERPCType::ClientReliable: + case ERPCType::ClientUnreliable: + case ERPCType::NetMulticast: + return SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID; + case ERPCType::ServerReliable: + case ERPCType::ServerUnreliable: + return SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID; + default: + checkNoEntry(); + return SpatialConstants::INVALID_COMPONENT_ID; + } +} + RPCRingBufferDescriptor GetRingBufferDescriptor(ERPCType Type) { RPCRingBufferDescriptor Descriptor; @@ -47,7 +62,8 @@ RPCRingBufferDescriptor GetRingBufferDescriptor(ERPCType Type) // 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). + // The numbers below are based on this structure, and have to match the component generated in SchemaGenerator + // (GenerateRPCEndpointsSchema). switch (Type) { case ERPCType::ClientReliable: @@ -90,6 +106,22 @@ Worker_ComponentId GetAckComponentId(ERPCType Type) } } +Worker_ComponentId GetAckAuthComponentSetId(ERPCType Type) +{ + switch (Type) + { + case ERPCType::ClientReliable: + case ERPCType::ClientUnreliable: + return SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID; + case ERPCType::ServerReliable: + case ERPCType::ServerUnreliable: + return SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID; + default: + checkNoEntry(); + return SpatialConstants::INVALID_COMPONENT_ID; + } +} + Schema_FieldId GetAckFieldId(ERPCType Type) { uint32 MaxRingBufferSize = GetDefault()->MaxRPCRingBufferSize; @@ -98,7 +130,8 @@ Schema_FieldId GetAckFieldId(ERPCType 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. + // 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: diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SchemaUtils.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SchemaUtils.cpp index e4fa62d743..6cec97693d 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SchemaUtils.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SchemaUtils.cpp @@ -6,7 +6,6 @@ namespace SpatialGDK { - void GetFullPathFromUnrealObjectReference(const FUnrealObjectRef& ObjectRef, FString& OutPath) { if (!ObjectRef.Path.IsSet()) diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialBasicAwaiter.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialBasicAwaiter.cpp new file mode 100644 index 0000000000..ff5c65ff08 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialBasicAwaiter.cpp @@ -0,0 +1,106 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Utils/SpatialBasicAwaiter.h" + +#include "Engine/World.h" +#include "TimerManager.h" + +void USpatialBasicAwaiter::BeginDestroy() +{ + InvokeQueuedDelegates(FString::Printf(TEXT("Awaiter '%s' was destroyed!"), *GetName())); + Super::BeginDestroy(); +} + +FDelegateHandle USpatialBasicAwaiter::Await(const FOnReady& OnReadyDelegate, const float Timeout) +{ + // We already became ready in the past, so the delegate can be executed immediately + if (bIsReady) + { + OnReadyDelegate.ExecuteIfBound(FString{}); + return FDelegateHandle{}; + } + // Still waiting to become ready, so store the delegate to be executed when we eventually do become ready + else + { + FDelegateHandle OutHandle = OnReadyEvent.Add(OnReadyDelegate); + + // If requested to, add a timer to remove the delegate in case becoming ready takes too long + if (Timeout > 0.f) + { + if (UWorld* World = GetWorld()) + { + FTimerManager& TimerManager = World->GetTimerManager(); + FTimerHandle Handle; + + TimerManager.SetTimer(Handle, + FTimerDelegate::CreateWeakLambda( + this, + [this, OutHandle, OnReadyDelegate]() { + if (OnReadyDelegate.IsBound()) + { + OnReadyDelegate.Execute(FString{ TEXT("Timed out while waiting to become Ready!") }); + OnReadyEvent.Remove(OutHandle); + } + }), + Timeout, false); + + TimeoutHandles.Add(Handle); + } + else + { + UE_LOG(LogAwaitable, Error, + TEXT("UBasicAwaiter::Await could not find a valid UWorld reference! (Could not set up timeout timer)")); + } + } + + return OutHandle; + } +} + +bool USpatialBasicAwaiter::StopAwaiting(FDelegateHandle& Handle) +{ + return OnReadyEvent.Remove(Handle); +} + +ISpatialAwaitable::FSpatialAwaitableOnResetEvent& USpatialBasicAwaiter::OnReset() +{ + return OnResetEvent; +} + +void USpatialBasicAwaiter::Ready() +{ + // Early exit if already ready + if (bIsReady) + { + return; + } + + bIsReady = true; + InvokeQueuedDelegates(); +} + +void USpatialBasicAwaiter::Reset() +{ + if (!bIsReady) + { + return; + } + + bIsReady = false; + OnResetEvent.Broadcast(); + OnResetEvent.Clear(); +} + +void USpatialBasicAwaiter::InvokeQueuedDelegates(const FString& ErrorStatus /* = FString{} */) +{ + OnReadyEvent.Broadcast(ErrorStatus); + OnReadyEvent.Clear(); + + if (UWorld* World = GetWorld()) + { + for (FTimerHandle& TimeoutHandle : TimeoutHandles) + { + World->GetTimerManager().ClearTimer(TimeoutHandle); + } + } +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebugger.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebugger.cpp index 8196c292c2..c3ff29e97e 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebugger.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebugger.cpp @@ -3,24 +3,28 @@ #include "Utils/SpatialDebugger.h" #include "EngineClasses/SpatialNetDriver.h" +#include "EngineClasses/SpatialWorldSettings.h" +#include "Interop/Connection/SpatialWorkerConnection.h" #include "Interop/SpatialReceiver.h" #include "Interop/SpatialSender.h" #include "Interop/SpatialStaticComponentView.h" #include "LoadBalancing/GridBasedLBStrategy.h" #include "LoadBalancing/LayeredLBStrategy.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 "Framework/Application/SlateApplication.h" #include "GameFramework/Pawn.h" #include "GameFramework/PlayerController.h" #include "GameFramework/PlayerState.h" +#include "GameFramework/WorldSettings.h" #include "GenericPlatform/GenericPlatformMath.h" #include "Kismet/GameplayStatics.h" +#include "Modules/ModuleManager.h" #include "Net/UnrealNetwork.h" using namespace SpatialGDK; @@ -29,8 +33,16 @@ DEFINE_LOG_CATEGORY(LogSpatialDebugger); namespace { - const FString DEFAULT_WORKER_REGION_MATERIAL = TEXT("/SpatialGDK/SpatialDebugger/Materials/TranslucentWorkerRegion.TranslucentWorkerRegion"); -} +// Background material for worker region +const FString DEFAULT_WORKER_REGION_MATERIAL = + TEXT("/SpatialGDK/SpatialDebugger/Materials/TranslucentWorkerRegion.TranslucentWorkerRegion"); +const FString DEFAULT_WIREFRAME_MATERIAL = TEXT("/SpatialGDK/SpatialDebugger/Materials/GlowingWireframeMaterial.GlowingWireframeMaterial"); +// Improbable primary font - Muli regular +const FString DEFAULT_WORKER_TEXT_FONT = TEXT("/SpatialGDK/SpatialDebugger/Fonts/MuliFont.MuliFont"); +// Material to combine both the background and the worker information in one material +const FString DEFAULT_WORKER_COMBINED_MATERIAL = + TEXT("/SpatialGDK/SpatialDebugger/Materials/WorkerRegionCombinedMaterial.WorkerRegionCombinedMaterial"); +} // anonymous namespace ASpatialDebugger::ASpatialDebugger(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) @@ -45,8 +57,12 @@ ASpatialDebugger::ASpatialDebugger(const FObjectInitializer& ObjectInitializer) NetUpdateFrequency = 1.f; + HoverIndex = 0; + NetDriver = Cast(GetNetDriver()); + OnConfigUIClosed.BindDynamic(this, &ASpatialDebugger::DefaultOnConfigUIClosed); + // 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. @@ -138,29 +154,31 @@ void ASpatialDebugger::BeginPlay() { SpatialToggleDebugger(); } + WireFrameMaterial = LoadObject(nullptr, *DEFAULT_WIREFRAME_MATERIAL); + if (WireFrameMaterial == nullptr) + { + UE_LOG(LogSpatialDebugger, Warning, TEXT("SpatialDebugger enabled but unable to get WireFrame Material.")); + } } } void ASpatialDebugger::OnAuthorityGained() { - if (NetDriver->LoadBalanceStrategy) + if (UAbstractLBStrategy* LoadBalanceStrategy = Cast(NetDriver->LoadBalanceStrategy)) { - const ULayeredLBStrategy* LayeredLBStrategy = Cast(NetDriver->LoadBalanceStrategy); - if (LayeredLBStrategy == nullptr) - { - UE_LOG(LogSpatialDebugger, Warning, TEXT("SpatialDebugger enabled but unable to get LayeredLBStrategy.")); - return; - } - - if (const UGridBasedLBStrategy* GridBasedLBStrategy = Cast(LayeredLBStrategy->GetLBStrategyForVisualRendering())) + if (const UGridBasedLBStrategy* GridBasedLBStrategy = + Cast(LoadBalanceStrategy->GetLBStrategyForVisualRendering())) { 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; + const TPair& LBStrategyRegion = LBStrategyRegions[i]; + WorkerRegionInfo.VirtualWorkerID = LBStrategyRegion.Key; + const PhysicalWorkerName* WorkerName = + NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(LBStrategyRegion.Key); + WorkerRegionInfo.WorkerName = (WorkerName == nullptr) ? "" : *WorkerName; WorkerRegionInfo.Color = (WorkerName == nullptr) ? InvalidServerTintColor : SpatialGDK::GetColorForWorkerName(*WorkerName); WorkerRegionInfo.Extents = LBStrategyRegion.Value; WorkerRegions[i] = WorkerRegionInfo; @@ -175,18 +193,45 @@ void ASpatialDebugger::CreateWorkerRegions() if (WorkerRegionMaterial == nullptr) { UE_LOG(LogSpatialDebugger, Error, TEXT("Worker regions were not rendered. Could not find default material: %s"), - *DEFAULT_WORKER_REGION_MATERIAL); + *DEFAULT_WORKER_REGION_MATERIAL); return; } + UMaterial* WorkerCombinedMaterial = LoadObject(nullptr, *DEFAULT_WORKER_COMBINED_MATERIAL); + if (WorkerCombinedMaterial == nullptr) + { + UE_LOG(LogSpatialDebugger, Error, TEXT("Worker regions were not rendered. Could not find default material: %s"), + *DEFAULT_WORKER_COMBINED_MATERIAL); + } + + UFont* WorkerInfoFont = LoadObject(nullptr, *DEFAULT_WORKER_TEXT_FONT); + if (WorkerInfoFont == nullptr) + { + UE_LOG(LogSpatialDebugger, Error, TEXT("Worker information was not rendered. Could not find default font: %s"), + *DEFAULT_WORKER_TEXT_FONT); + } + // Create new actors for all new worker regions FActorSpawnParameters SpawnParams; SpawnParams.bNoFail = true; + UWorld* World = GetWorld(); +#if WITH_EDITOR + if (World == nullptr) + { + // We are in the editor at design time + World = GEditor->GetEditorWorldContext().World(); + } + SpawnParams.bHideFromSceneOutliner = true; +#endif + check(World != nullptr); SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; for (const FWorkerRegionInfo& WorkerRegionData : WorkerRegions) { - AWorkerRegion* WorkerRegion = GetWorld()->SpawnActor(SpawnParams); - WorkerRegion->Init(WorkerRegionMaterial, WorkerRegionData.Color, WorkerRegionData.Extents, WorkerRegionVerticalScale); + AWorkerRegion* WorkerRegion = World->SpawnActor(SpawnParams); + FString WorkerInfo = FString::Printf(TEXT("You are looking at virtual worker number %d\n%s"), WorkerRegionData.VirtualWorkerID, + *WorkerRegionData.WorkerName); + WorkerRegion->Init(WorkerRegionMaterial, WorkerCombinedMaterial, WorkerInfoFont, WorkerRegionData.Color, WorkerRegionOpacity, + WorkerRegionData.Extents, WorkerRegionHeight, WorkerRegionVerticalScale, WorkerInfo); WorkerRegion->SetActorEnableCollision(false); } } @@ -228,9 +273,10 @@ void ASpatialDebugger::Destroyed() if (DrawDebugDelegateHandle.IsValid()) { UDebugDrawService::Unregister(DrawDebugDelegateHandle); - DestroyWorkerRegions(); } + DestroyWorkerRegions(); + Super::Destroyed(); } @@ -244,8 +290,10 @@ void ASpatialDebugger::LoadIcons() 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_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); } @@ -269,10 +317,166 @@ void ASpatialDebugger::OnEntityAdded(const Worker_EntityId EntityId) if (Actor->IsA()) { LocalPlayerController = Cast(Actor); + + if (GetNetMode() == NM_Client) + { + LocalPlayerController->InputComponent->BindKey(ConfigUIToggleKey, IE_Pressed, this, &ASpatialDebugger::OnToggleConfigUI) + .bConsumeInput = false; + LocalPlayerController->InputComponent->BindKey(SelectActorKey, IE_Pressed, this, &ASpatialDebugger::OnSelectActor) + .bConsumeInput = false; + LocalPlayerController->InputComponent->BindKey(HighlightActorKey, IE_Pressed, this, &ASpatialDebugger::OnHighlightActor) + .bConsumeInput = false; + } + } + } +} + +void ASpatialDebugger::OnToggleConfigUI() +{ + if (ConfigUIWidget == nullptr) + { + if (ConfigUIClass != nullptr) + { + ConfigUIWidget = CreateWidget(LocalPlayerController.Get(), ConfigUIClass); + if (ConfigUIWidget == nullptr) + { + UE_LOG(LogSpatialDebugger, Error, + TEXT("SpatialDebugger config UI will not load. Couldn't create config UI widget for class: %s"), + *GetNameSafe(ConfigUIClass)); + return; + } + else + { + ConfigUIWidget->SetSpatialDebugger(this); + } + } + else + { + UE_LOG(LogSpatialDebugger, Error, + TEXT("SpatialDebugger config UI will not load. ConfigUIClass is not set on the spatial debugger.")); + return; + } + + ConfigUIWidget->AddToViewport(); + + FInputModeGameAndUI InputModeSettings; + InputModeSettings.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock); + InputModeSettings.SetWidgetToFocus(ConfigUIWidget->TakeWidget()); + + LocalPlayerController->SetInputMode(InputModeSettings); + LocalPlayerController->bShowMouseCursor = true; + + ConfigUIWidget->OnShow(); + } + else + { + ConfigUIWidget->RemoveFromParent(); + ConfigUIWidget = nullptr; + OnConfigUIClosed.ExecuteIfBound(); + } +} + +void ASpatialDebugger::ToggleSelectActor() +{ + // This should only be toggled when the config UI window is open + if (!ConfigUIWidget->IsVisible()) + { + return; + } + + bSelectActor = !bSelectActor; + if (bSelectActor) + { + if (CrosshairTexture != nullptr) + { + // Hide the mouse cursor as we will draw our own custom crosshair + LocalPlayerController->bShowMouseCursor = false; + // Sets back the focus to the game viewport - need to hide mouse cursor instantly + FSlateApplication::Get().SetAllUserFocusToGameViewport(); } + + // Set the object types to query in the raycast based + for (TEnumAsByte ActorTypeToQuery : SelectCollisionTypesToQuery) + { + CollisionObjectParams.AddObjectTypesToQuery(ActorTypeToQuery); + } + } + else + { + // Change mouse cursor back to normal + LocalPlayerController->bShowMouseCursor = true; + + RevertHoverMaterials(); + + // Clear selected actors + SelectedActors.Empty(); + HoverIndex = 0; + HitActors.Empty(); } } +void ASpatialDebugger::OnSelectActor() +{ + if (HitActors.Num() > 0) + { + TWeakObjectPtr SelectedActor = GetHitActor(); + + if (SelectedActor.IsValid()) + { + if (SelectedActors.Contains(SelectedActor)) + { + // Already selected so deselect + SelectedActors.Remove(SelectedActor); + } + else + { + // Add selected actor to enable drawing tags + SelectedActors.Add(SelectedActor); + } + } + } +} + +void ASpatialDebugger::OnHighlightActor() +{ + HoverIndex++; +} + +void ASpatialDebugger::DefaultOnConfigUIClosed() +{ + if (LocalPlayerController.IsValid()) + { + FInputModeGameOnly InputModeSettings; + LocalPlayerController->SetInputMode(InputModeSettings); + LocalPlayerController->bShowMouseCursor = false; + } +} + +void ASpatialDebugger::SetShowWorkerRegions(const bool bNewShow) +{ + if (bNewShow != bShowWorkerRegions) + { + if (IsEnabled()) + { + if (bNewShow) + { + CreateWorkerRegions(); + } + else + { + DestroyWorkerRegions(); + } + } + + bShowWorkerRegions = bNewShow; + } +} + +bool ASpatialDebugger::IsSelectActorEnabled() const +{ + return bSelectActor; +} + void ASpatialDebugger::OnEntityRemoved(const Worker_EntityId EntityId) { check(NetDriver != nullptr && !NetDriver->IsServer()); @@ -280,9 +484,9 @@ void ASpatialDebugger::OnEntityRemoved(const Worker_EntityId EntityId) EntityActorMapping.Remove(EntityId); } -void ASpatialDebugger::ActorAuthorityChanged(const Worker_AuthorityChangeOp& AuthOp) const +void ASpatialDebugger::ActorAuthorityChanged(const Worker_ComponentSetAuthorityChangeOp& AuthOp) const { - check(AuthOp.authority == WORKER_AUTHORITY_AUTHORITATIVE && AuthOp.component_id == SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID); + check(AuthOp.authority == WORKER_AUTHORITY_AUTHORITATIVE && AuthOp.component_set_id == SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID); if (NetDriver->VirtualWorkerTranslator == nullptr) { @@ -290,20 +494,30 @@ void ASpatialDebugger::ActorAuthorityChanged(const Worker_AuthorityChangeOp& Aut return; } - VirtualWorkerId LocalVirtualWorkerId = NetDriver->VirtualWorkerTranslator->GetLocalVirtualWorkerId(); - FColor LocalVirtualWorkerColor = SpatialGDK::GetColorForWorkerName(NetDriver->VirtualWorkerTranslator->GetLocalPhysicalWorkerName()); + const VirtualWorkerId LocalVirtualWorkerId = NetDriver->VirtualWorkerTranslator->GetLocalVirtualWorkerId(); + const 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); + SpatialDebugging NewDebuggingInfo(LocalVirtualWorkerId, LocalVirtualWorkerColor, SpatialConstants::INVALID_VIRTUAL_WORKER_ID, + InvalidServerTintColor, false); NetDriver->Sender->SendAddComponents(AuthOp.entity_id, { NewDebuggingInfo.CreateSpatialDebuggingData() }); return; } - + DebuggingInfo->AuthoritativeVirtualWorkerId = LocalVirtualWorkerId; DebuggingInfo->AuthoritativeColor = LocalVirtualWorkerColor; + + // Ensure the intent colour is up to date, as the physical worker name may have changed in the event of a snapshot reload + const PhysicalWorkerName* AuthIntentPhysicalWorkerName = + NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(DebuggingInfo->IntentVirtualWorkerId); + DebuggingInfo->IntentColor = (AuthIntentPhysicalWorkerName != nullptr) + ? SpatialGDK::GetColorForWorkerName(*AuthIntentPhysicalWorkerName) + : InvalidServerTintColor; + FWorkerComponentUpdate DebuggingUpdate = DebuggingInfo->CreateSpatialDebuggingUpdate(); NetDriver->Connection->SendComponentUpdate(AuthOp.entity_id, &DebuggingUpdate); } @@ -314,7 +528,8 @@ void ASpatialDebugger::ActorAuthorityIntentChanged(Worker_EntityId EntityId, Vir check(DebuggingInfo != nullptr); DebuggingInfo->IntentVirtualWorkerId = NewIntentVirtualWorkerId; - const PhysicalWorkerName* NewAuthoritativePhysicalWorkerName = NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(NewIntentVirtualWorkerId); + const PhysicalWorkerName* NewAuthoritativePhysicalWorkerName = + NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(NewIntentVirtualWorkerId); check(NewAuthoritativePhysicalWorkerName != nullptr); DebuggingInfo->IntentColor = SpatialGDK::GetColorForWorkerName(*NewAuthoritativePhysicalWorkerName); @@ -322,13 +537,11 @@ void ASpatialDebugger::ActorAuthorityIntentChanged(Worker_EntityId EntityId, Vir NetDriver->Connection->SendComponentUpdate(EntityId, &DebuggingUpdate); } -void ASpatialDebugger::DrawTag(UCanvas* Canvas, const FVector2D& ScreenLocation, const Worker_EntityId EntityId, const FString& ActorName) +void ASpatialDebugger::DrawTag(UCanvas* Canvas, const FVector2D& ScreenLocation, const Worker_EntityId EntityId, const FString& ActorName, + const bool bCentre) { 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)) { @@ -337,13 +550,58 @@ void ASpatialDebugger::DrawTag(UCanvas* Canvas, const FVector2D& ScreenLocation, const SpatialDebugging* DebuggingInfo = NetDriver->StaticComponentView->GetComponentData(EntityId); - static const float BaseHorizontalOffset(16.0f); - if (!FApp::CanEverRender()) // DrawIcon can attempt to use the underlying texture resource even when using nullrhi { return; } + static const float BaseHorizontalOffset = 16.0f; + static const float NumberScale = 0.75f; + static const float TextScale = 0.5f; + const float AuthIdWidth = NumberScale * GetNumberOfDigitsIn(DebuggingInfo->AuthoritativeVirtualWorkerId); + const float AuthIntentIdWidth = NumberScale * GetNumberOfDigitsIn(DebuggingInfo->IntentVirtualWorkerId); + const float EntityIdWidth = NumberScale * GetNumberOfDigitsIn(EntityId); + + int32 HorizontalOffset = 0; + if (bCentre) + { + // If tag should be centered, calculate the total width of the icons and text to be rendered + float TagWidth = 0; + if (bShowLock) + { + // If showing the lock, add the lock icon width + TagWidth += BaseHorizontalOffset; + } + if (bShowAuth) + { + // If showing the authority, add the authority icon width and the width of the authoritative virtual worker ID + TagWidth += BaseHorizontalOffset; + TagWidth += (BaseHorizontalOffset * AuthIdWidth); + } + if (bShowAuthIntent) + { + // If showing the authority intent, add the authority intent icon width and the width of the authoritative intent virtual worker + // ID + TagWidth += BaseHorizontalOffset; + TagWidth += (BaseHorizontalOffset * AuthIntentIdWidth); + } + if (bShowEntityId) + { + // If showing the entity ID, add the width of the entity ID + TagWidth += (BaseHorizontalOffset * EntityIdWidth); + } + if (bShowActorName) + { + // If showing the actor name, add the width of the actor name + const float ActorNameWidth = TextScale * ActorName.Len(); + TagWidth += (BaseHorizontalOffset * ActorNameWidth); + } + + // Calculate the offset based on the total width of the tag + HorizontalOffset = TagWidth / -2; + } + + // Draw icons and text based on the offset if (bShowLock) { SCOPE_CYCLE_COUNTER(STAT_DrawIcons); @@ -363,11 +621,11 @@ void ASpatialDebugger::DrawTag(UCanvas* Canvas, const FVector2D& ScreenLocation, 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->DrawScaledIcon(Icons[ICON_BOX], ScreenLocation.X + HorizontalOffset, ScreenLocation.Y, FVector(AuthIdWidth, 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); + Canvas->DrawText(RenderFont, FString::FromInt(DebuggingInfo->AuthoritativeVirtualWorkerId), ScreenLocation.X + HorizontalOffset + 1, + ScreenLocation.Y, 1.1f, 1.1f, FontRenderInfo); + HorizontalOffset += (BaseHorizontalOffset * AuthIdWidth); } if (bShowAuthIntent) @@ -376,13 +634,14 @@ void ASpatialDebugger::DrawTag(UCanvas* Canvas, const FVector2D& ScreenLocation, 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; + HorizontalOffset += BaseHorizontalOffset; 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->DrawScaledIcon(Icons[ICON_BOX], ScreenLocation.X + HorizontalOffset, ScreenLocation.Y, + FVector(AuthIntentIdWidth, 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); + Canvas->DrawText(RenderFont, FString::FromInt(DebuggingInfo->IntentVirtualWorkerId), ScreenLocation.X + HorizontalOffset + 1, + ScreenLocation.Y, 1.1f, 1.1f, FontRenderInfo); + HorizontalOffset += (BaseHorizontalOffset * AuthIntentIdWidth); } FString Label; @@ -426,55 +685,280 @@ void ASpatialDebugger::DrawDebug(UCanvas* Canvas, APlayerController* /* Controll #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()) + 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 (bSelectActor) + { + SelectActorsToTag(Canvas); + return; + } - if (LocalPawn.IsValid()) + if (ActorTagDrawMode >= EActorTagDrawMode::LocalPlayer) { - PlayerLocation = LocalPawn->GetActorLocation(); + DrawDebugLocalPlayer(Canvas); } - for (TPair>& EntityActorPair : EntityActorMapping) + if (ActorTagDrawMode == EActorTagDrawMode::All) { - const TWeakObjectPtr Actor = EntityActorPair.Value; - const Worker_EntityId EntityId = EntityActorPair.Key; + FVector PlayerLocation = GetLocalPawnLocation(); - if (Actor != nullptr) + for (TPair>& EntityActorPair : EntityActorMapping) { - FVector ActorLocation = Actor->GetActorLocation(); + const TWeakObjectPtr Actor = EntityActorPair.Value; + const Worker_EntityId EntityId = EntityActorPair.Key; + + if (Actor != nullptr) + { + FVector2D ScreenLocation = ProjectActorToScreen(Actor, PlayerLocation); + if (ScreenLocation.IsZero()) + { + continue; + } + + DrawTag(Canvas, ScreenLocation, EntityId, Actor->GetName(), true /*bCentre*/); + } + } + } +} + +void ASpatialDebugger::SelectActorsToTag(UCanvas* Canvas) +{ + if (LocalPlayerController.IsValid()) + { + FVector2D NewMousePosition; - if (ActorLocation.IsZero()) + if (LocalPlayerController->GetMousePosition(NewMousePosition.X, NewMousePosition.Y)) + { + if (CrosshairTexture != nullptr) { - continue; + // Display a crosshair icon for the mouse cursor + // Offset by half of the texture's dimensions so that the center of the texture aligns with the center of the Canvas. + FVector2D CrossHairDrawPosition(NewMousePosition.X - (CrosshairTexture->GetSurfaceWidth() * 0.5f), + NewMousePosition.Y - (CrosshairTexture->GetSurfaceHeight() * 0.5f)); + + // Draw the crosshair at the mouse position. + FCanvasTileItem TileItem(CrossHairDrawPosition, CrosshairTexture->Resource, FLinearColor::White); + TileItem.BlendMode = SE_BLEND_Translucent; + Canvas->DrawItem(TileItem); } - if (FVector::Dist(PlayerLocation, ActorLocation) > MaxRange) + TWeakObjectPtr NewHoverActor = GetActorAtPosition(NewMousePosition); + HighlightActorUnderCursor(NewHoverActor); + } + + // Draw tags above selected actors + for (TWeakObjectPtr SelectedActor : SelectedActors) + { + if (SelectedActor.IsValid()) { - continue; + if (const Worker_EntityId_Key* HitEntityId = EntityActorMapping.FindKey(SelectedActor)) + { + FVector PlayerLocation = GetLocalPawnLocation(); + + FVector2D ScreenLocation = ProjectActorToScreen(SelectedActor, PlayerLocation); + if (!ScreenLocation.IsZero()) + { + DrawTag(Canvas, ScreenLocation, *HitEntityId, SelectedActor->GetName(), true /*bCentre*/); + } + } } + } + } +} + +void ASpatialDebugger::HighlightActorUnderCursor(TWeakObjectPtr& NewHoverActor) +{ + // Check if highlighting feature is enabled and the glowing wire frame material is set + if (!bShowHighlight || WireFrameMaterial == nullptr) + { + return; + } - FVector2D ScreenLocation = FVector2D::ZeroVector; - if (LocalPlayerController.IsValid()) + if (!NewHoverActor.IsValid()) + { + // No actor under the cursor so revert hover materials on previous actor + RevertHoverMaterials(); + } + else if (NewHoverActor != HoverActor) + { + // New actor under the cursor + + // Revert hover materials on previous actor + RevertHoverMaterials(); + + // Set hover materials on new actor + TArray ActorComponents; + NewHoverActor->GetComponents(UMeshComponent::StaticClass(), ActorComponents, true); + for (UActorComponent* NewActorComponent : ActorComponents) + { + // Store previous components + TWeakObjectPtr MeshComponent(Cast(NewActorComponent)); + TWeakObjectPtr MeshMaterial = MeshComponent->GetMaterial(0); + if (MeshComponent.IsValid() && MeshMaterial.IsValid()) { - SCOPE_CYCLE_COUNTER(STAT_Projection); - UGameplayStatics::ProjectWorldToScreen(LocalPlayerController.Get(), ActorLocation + WorldSpaceActorTagOffset, ScreenLocation, false); + ActorMeshComponents.Add(MeshComponent); + // Store previous materials + ActorMeshMaterials.Add(MeshMaterial); + // Set wireframe material on new actor + MeshComponent->SetMaterial(0, WireFrameMaterial); } + } + HoverActor = NewHoverActor; + } +} - if (ScreenLocation.IsZero()) +void ASpatialDebugger::RevertHoverMaterials() +{ + if (!bShowHighlight) + { + return; + } + + if (HoverActor.IsValid()) + { + // Revert materials on previous actor + for (int i = 0; i < ActorMeshComponents.Num(); i++) + { + TWeakObjectPtr ActorMeshComponent = ActorMeshComponents[i]; + TWeakObjectPtr ActorMeshMaterial = ActorMeshMaterials[i]; + if (ActorMeshComponent.IsValid() && ActorMeshMaterial.IsValid()) { - continue; + ActorMeshComponent->SetMaterial(0, ActorMeshMaterial.Get()); } + } - DrawTag(Canvas, ScreenLocation, EntityId, Actor->GetName()); + // Clear previous materials + ActorMeshMaterials.Empty(); + ActorMeshComponents.Empty(); + + HoverActor = nullptr; + } +} + +TWeakObjectPtr ASpatialDebugger::GetActorAtPosition(const FVector2D& NewMousePosition) +{ + if (!LocalPlayerController.IsValid()) + { + return nullptr; + } + else if (NewMousePosition != MousePosition) + { + // Mouse has moved so raycast to find actors currently under the mouse cursor + MousePosition = NewMousePosition; + + FVector WorldLocation; + FVector WorldRotation; + LocalPlayerController->DeprojectScreenPositionToWorld(NewMousePosition.X, NewMousePosition.Y, WorldLocation, + WorldRotation); // Mouse cursor position + FVector StartTrace = WorldLocation; + FVector EndTrace = StartTrace + WorldRotation * MaxRange; + + HitActors.Empty(); + + TArray HitResults; + bool bHit = GetWorld()->LineTraceMultiByObjectType(HitResults, StartTrace, EndTrace, CollisionObjectParams); + if (bHit) + { + // When the raycast hits an actor then it is highlighted, whilst the actor remains under the crosshair. If there are multiple + // hit results, the user can select the next by using the mouse scroll wheel + for (const FHitResult& HitResult : HitResults) + { + const TWeakObjectPtr HitActor = HitResult.GetActor(); + + if (!HitActor.IsValid() || HitActors.Contains(HitActor)) + { + // The hit results may include the same actor multiple times so just ignore duplicates + continue; + } + + // Only add actors to the list of hit actors if they have a valid entity id and screen position. As later when we scroll + // through the actors, we only want to highlight ones that we can show a tag for. + if (const Worker_EntityId_Key* HitEntityId = EntityActorMapping.FindKey(HitResult.GetActor())) + { + FVector PlayerLocation = GetLocalPawnLocation(); + + FVector2D ScreenLocation = ProjectActorToScreen(HitActor, PlayerLocation); + if (!ScreenLocation.IsZero()) + { + HitActors.Add(HitActor); + } + } + } } } + + return GetHitActor(); +} + +// Return actor selected from list dependent on the hover index, which is selected independently with the mouse wheel (by default) +TWeakObjectPtr ASpatialDebugger::GetHitActor() +{ + if (HitActors.Num() == 0) + { + return nullptr; + } + + // Validate hover index + if (HoverIndex >= HitActors.Num()) + { + // Reset hover index + HoverIndex = 0; + } + + return HitActors[HoverIndex]; +} + +FVector2D ASpatialDebugger::ProjectActorToScreen(const TWeakObjectPtr Actor, const FVector& PlayerLocation) +{ + FVector2D ScreenLocation = FVector2D::ZeroVector; + + FVector ActorLocation = Actor->GetActorLocation(); + + if (ActorLocation.IsZero()) + { + return ScreenLocation; + } + + if (FVector::Dist(PlayerLocation, ActorLocation) > MaxRange) + { + return ScreenLocation; + } + + if (LocalPlayerController.IsValid()) + { + SCOPE_CYCLE_COUNTER(STAT_Projection); + UGameplayStatics::ProjectWorldToScreen(LocalPlayerController.Get(), ActorLocation + WorldSpaceActorTagOffset, ScreenLocation, + false); + return ScreenLocation; + } + return ScreenLocation; +} + +FVector ASpatialDebugger::GetLocalPawnLocation() +{ + FVector PlayerLocation = FVector::ZeroVector; + if (LocalPawn.IsValid()) + { + PlayerLocation = LocalPawn->GetActorLocation(); + } + return PlayerLocation; +} + +void GetReplicatedActorsInHierarchy(const AActor* Actor, TArray& HierarchyActors) +{ + if (Actor->GetIsReplicated() && !HierarchyActors.Contains(Actor)) + { + HierarchyActors.Add(Actor); + } + for (const AActor* Child : Actor->Children) + { + GetReplicatedActorsInHierarchy(Child, HierarchyActors); + } } void ASpatialDebugger::DrawDebugLocalPlayer(UCanvas* Canvas) @@ -484,23 +968,20 @@ void ASpatialDebugger::DrawDebugLocalPlayer(UCanvas* Canvas) return; } - const TArray> LocalPlayerActors = + TArray ActorsToDisplay = { LocalPlayerState.Get(), LocalPlayerController.Get(), LocalPawn.Get() }; + + if (bShowPlayerHierarchy) { - LocalPawn, - LocalPlayerController, - LocalPlayerState - }; + GetReplicatedActorsInHierarchy(LocalPlayerController.Get(), ActorsToDisplay); + } FVector2D ScreenLocation(PlayerPanelStartX, PlayerPanelStartY); - for (int32 i = 0; i < LocalPlayerActors.Num(); ++i) + for (int32 i = 0; i < ActorsToDisplay.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; - } + const Worker_EntityId EntityId = NetDriver->PackageMap->GetEntityIdFromObject(ActorsToDisplay[i]); + DrawTag(Canvas, ScreenLocation, EntityId, ActorsToDisplay[i]->GetName(), false /*bCentre*/); + ScreenLocation.Y += PLAYER_TAG_VERTICAL_OFFSET; } } @@ -516,10 +997,106 @@ void ASpatialDebugger::SpatialToggleDebugger() } else { - DrawDebugDelegateHandle = UDebugDrawService::Register(TEXT("Game"), FDebugDrawDelegate::CreateUObject(this, &ASpatialDebugger::DrawDebug)); + DrawDebugDelegateHandle = + UDebugDrawService::Register(TEXT("Game"), FDebugDrawDelegate::CreateUObject(this, &ASpatialDebugger::DrawDebug)); if (bShowWorkerRegions) { CreateWorkerRegions(); } } } + +bool ASpatialDebugger::IsEnabled() +{ + return DrawDebugDelegateHandle.IsValid(); +} + +#if WITH_EDITOR +void ASpatialDebugger::EditorRefreshDisplay() +{ + if (GEditor != nullptr && GEditor->GetActiveViewport() != nullptr) + { + // Redraw editor window to show changes + GEditor->GetActiveViewport()->Invalidate(); + } +} + +void ASpatialDebugger::EditorSpatialToggleDebugger(bool bEnabled) +{ + bShowWorkerRegions = bEnabled; + EditorRefreshWorkerRegions(); +} + +void ASpatialDebugger::EditorRefreshWorkerRegions() +{ + DestroyWorkerRegions(); + + if (bShowWorkerRegions && EditorAllowWorkerBoundaries()) + { + EditorInitialiseWorkerRegions(); + CreateWorkerRegions(); + } + + EditorRefreshDisplay(); +} + +bool ASpatialDebugger::EditorAllowWorkerBoundaries() const +{ + // Check if spatial networking is enabled. + return GetDefault()->UsesSpatialNetworking(); +} + +void ASpatialDebugger::EditorInitialiseWorkerRegions() +{ + WorkerRegions.Empty(); + + const UWorld* World = GEditor->GetEditorWorldContext().World(); + check(World != nullptr); + + const UAbstractSpatialMultiWorkerSettings* MultiWorkerSettings = + USpatialStatics::GetSpatialMultiWorkerClass(World)->GetDefaultObject(); + + ULayeredLBStrategy* LoadBalanceStrategy = NewObject(); + LoadBalanceStrategy->Init(); + LoadBalanceStrategy->SetLayers(MultiWorkerSettings->WorkerLayers); + + if (const UGridBasedLBStrategy* GridBasedLBStrategy = + Cast(LoadBalanceStrategy->GetLBStrategyForVisualRendering())) + { + LoadBalanceStrategy->SetVirtualWorkerIds(1, LoadBalanceStrategy->GetMinimumRequiredWorkers()); + const UGridBasedLBStrategy::LBStrategyRegions LBStrategyRegions = GridBasedLBStrategy->GetLBStrategyRegions(); + + WorkerRegions.SetNum(LBStrategyRegions.Num()); + for (int i = 0; i < LBStrategyRegions.Num(); i++) + { + const TPair& LBStrategyRegion = LBStrategyRegions[i]; + FWorkerRegionInfo WorkerRegionInfo; + // Generate our own unique worker name as we only need it to generate a unique colour + const PhysicalWorkerName WorkerName = PhysicalWorkerName::Printf(TEXT("WorkerRegion%d%d%d"), i, i, i); + WorkerRegionInfo.Color = GetColorForWorkerName(WorkerName); + WorkerRegionInfo.Extents = LBStrategyRegion.Value; + WorkerRegions[i] = WorkerRegionInfo; + } + } + + // Needed to clean up LoadBalanceStrategy memory, otherwise it gets duplicated exponentially + GEngine->ForceGarbageCollection(true); +} + +void ASpatialDebugger::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (PropertyChangedEvent.Property != nullptr) + { + const FName PropertyName(PropertyChangedEvent.Property->GetFName()); + if (PropertyName == GET_MEMBER_NAME_CHECKED(ASpatialDebugger, WorkerRegionHeight) + || PropertyName == GET_MEMBER_NAME_CHECKED(ASpatialDebugger, WorkerRegionVerticalScale) + || PropertyName == GET_MEMBER_NAME_CHECKED(ASpatialDebugger, WorkerRegionOpacity)) + + { + EditorRefreshWorkerRegions(); + } + } +} +#endif // WITH_EDITOR diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebuggerConfigUI.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebuggerConfigUI.cpp new file mode 100644 index 0000000000..e07d65e6ae --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebuggerConfigUI.cpp @@ -0,0 +1,10 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Utils/SpatialDebuggerConfigUI.h" + +#include "EngineClasses/SpatialNetDriver.h" + +void USpatialDebuggerConfigUI::SetSpatialDebugger_Implementation(ASpatialDebugger* InDebugger) +{ + SpatialDebugger = InDebugger; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLatencyTracer.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLatencyTracer.cpp index d3582718eb..cfa652bc1f 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLatencyTracer.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLatencyTracer.cpp @@ -19,38 +19,35 @@ DECLARE_CYCLE_STAT(TEXT("BeginLatencyTraceRPC_Internal"), STAT_BeginLatencyTrace namespace { - // Stream for piping trace lib output to UE output - class UEStream : public std::stringbuf +// Stream for piping trace lib output to UE output +class UEStream : public std::stringbuf +{ + int sync() override { - int sync() override - { - UE_LOG(LogSpatialLatencyTracing, Verbose, TEXT("%s"), *FString(str().c_str())); - str(""); - return std::stringbuf::sync(); - } + UE_LOG(LogSpatialLatencyTracing, Verbose, TEXT("%s"), *FString(str().c_str())); + str(""); + return std::stringbuf::sync(); + } - public: - virtual ~UEStream() override - { - sync(); - } - }; +public: + virtual ~UEStream() override { sync(); } +}; - UEStream UStream; +UEStream UStream; #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)); +const 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)); + improbable::trace::SpanId _SpanId; + memcpy(&_SpanId[0], SpanBytes, sizeof(improbable::trace::SpanId)); - return improbable::trace::SpanContext(_TraceId, _SpanId); - } + return { _TraceId, _SpanId }; +} #endif -} // anonymous namespace +} // anonymous namespace USpatialLatencyTracer::USpatialLatencyTracer() { @@ -89,7 +86,8 @@ bool USpatialLatencyTracer::SetTraceMetadata(UObject* WorldContextObject, const return false; } -bool USpatialLatencyTracer::BeginLatencyTrace(UObject* WorldContextObject, const FString& TraceDesc, FSpatialLatencyPayload& OutLatencyPayload) +bool USpatialLatencyTracer::BeginLatencyTrace(UObject* WorldContextObject, const FString& TraceDesc, + FSpatialLatencyPayload& OutLatencyPayload) { #if TRACE_LIB_ACTIVE if (USpatialLatencyTracer* Tracer = GetTracer(WorldContextObject)) @@ -100,29 +98,37 @@ bool USpatialLatencyTracer::BeginLatencyTrace(UObject* WorldContextObject, const return false; } -bool USpatialLatencyTracer::ContinueLatencyTraceRPC(UObject* WorldContextObject, const AActor* Actor, const FString& FunctionName, const FString& TraceDesc, const FSpatialLatencyPayload& LatencyPayload, FSpatialLatencyPayload& OutContinuedLatencyPayload) +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); + 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) +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); + 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) +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)) @@ -172,6 +178,17 @@ USpatialLatencyTracer* USpatialLatencyTracer::GetTracer(UObject* WorldContextObj return nullptr; } +FString USpatialLatencyTracer::GetTraceMetadata(UObject* WorldContextObject) +{ +#if TRACE_LIB_ACTIVE + if (USpatialLatencyTracer* Tracer = GetTracer(WorldContextObject)) + { + return Tracer->TraceMetadata; + } +#endif + return TEXT(""); +} + #if TRACE_LIB_ACTIVE bool USpatialLatencyTracer::IsValidKey(const TraceKey Key) { @@ -189,7 +206,7 @@ TraceKey USpatialLatencyTracer::RetrievePendingTrace(const UObject* Obj, const U return ReturnKey; } -TraceKey USpatialLatencyTracer::RetrievePendingTrace(const UObject* Obj, const GDK_PROPERTY(Property)* Property) +TraceKey USpatialLatencyTracer::RetrievePendingTrace(const UObject* Obj, const GDK_PROPERTY(Property) * Property) { FScopeLock Lock(&Mutex); @@ -265,7 +282,7 @@ TraceKey USpatialLatencyTracer::ReadTraceFromSchemaObject(Schema_Object* Obj, co 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); + const improbable::trace::SpanContext DestContext = ReadSpanContext(TraceBytes, SpanBytes); TraceKey Key = InvalidTraceKey; @@ -306,19 +323,19 @@ FSpatialLatencyPayload USpatialLatencyTracer::RetrievePayload_Internal(const UOb { 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(); + 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 {}; + 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() @@ -371,7 +388,7 @@ void USpatialLatencyTracer::OnDequeueMessage(const SpatialGDK::FOutgoingMessage* } 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); @@ -398,7 +415,9 @@ bool USpatialLatencyTracer::BeginLatencyTrace_Internal(const FString& TraceDesc, return true; } -bool USpatialLatencyTracer::ContinueLatencyTrace_Internal(const AActor* Actor, const FString& Target, ETraceType::Type Type, const FString& TraceDesc, const FSpatialLatencyPayload& LatencyPayload, FSpatialLatencyPayload& OutLatencyPayload) +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 @@ -409,7 +428,7 @@ bool USpatialLatencyTracer::ContinueLatencyTrace_Internal(const AActor* Actor, c } // We do minimal internal tracking for native rpcs/properties - const bool bInternalTracking = GetDefault()->UsesSpatialNetworking() || Type == ETraceType::Tagged; + const bool bInternalTracking = Type == ETraceType::Tagged; // GDK now also ends traces in the same way native does, ticket here UNR-4672 FScopeLock Lock(&Mutex); @@ -512,16 +531,16 @@ bool USpatialLatencyTracer::AddTrackingInfo(const AActor* Actor, const FString& } break; case ETraceType::Tagged: + { + ActorTagKey ATKey{ Actor, Target }; + if (TrackingTags.Find(ATKey) == nullptr) { - ActorTagKey ATKey{ Actor, Target }; - if (TrackingTags.Find(ATKey) == nullptr) - { - TrackingTags.Add(ATKey, Key); - return true; - } - UE_LOG(LogSpatialLatencyTracing, Warning, TEXT("(%s) : ActorTag already exists for trace"), *WorkerId); + TrackingTags.Add(ATKey, Key); + return true; } - break; + UE_LOG(LogSpatialLatencyTracing, Warning, TEXT("(%s) : ActorTag already exists for trace"), *WorkerId); + } + break; } } @@ -554,7 +573,7 @@ void USpatialLatencyTracer::ResolveKeyInLatencyPayload(FSpatialLatencyPayload& P // Uninitialized key, generate and add to map Payload.Key = GenerateNewTraceKey(); - improbable::trace::SpanContext DestContext = ReadSpanContext(Payload.TraceId.GetData(), Payload.SpanId.GetData()); + const 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); @@ -589,8 +608,7 @@ FString USpatialLatencyTracer::FormatMessage(const FString& Message, bool bInclu void USpatialLatencyTracer::Debug_SendTestTrace() { #if TRACE_LIB_ACTIVE - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [] - { + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [] { using namespace improbable::trace; std::cout << "Sending test trace" << std::endl; diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLoadBalancingHandler.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLoadBalancingHandler.cpp index ae6f8a24e2..598b1efb70 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLoadBalancingHandler.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLoadBalancingHandler.cpp @@ -4,20 +4,24 @@ #include "EngineClasses/SpatialActorChannel.h" #include "EngineClasses/SpatialPackageMapClient.h" +#include "Interop/SpatialSender.h" #include "LoadBalancing/AbstractLBStrategy.h" #include "LoadBalancing/OwnershipLockingPolicy.h" -#include "Interop/SpatialSender.h" +#include "Schema/AuthorityIntent.h" +#include "Schema/MigrationDiagnostic.h" #include "Schema/SpatialDebugging.h" DEFINE_LOG_CATEGORY(LogSpatialLoadBalancingHandler); +using namespace SpatialGDK; + FSpatialLoadBalancingHandler::FSpatialLoadBalancingHandler(USpatialNetDriver* InNetDriver) : NetDriver(InNetDriver) { - } -FSpatialLoadBalancingHandler::EvaluateActorResult FSpatialLoadBalancingHandler::EvaluateSingleActor(AActor* Actor, AActor*& OutNetOwner, VirtualWorkerId& OutWorkerId) +FSpatialLoadBalancingHandler::EvaluateActorResult FSpatialLoadBalancingHandler::EvaluateSingleActor(AActor* Actor, AActor*& OutNetOwner, + VirtualWorkerId& OutWorkerId) { const Worker_EntityId EntityId = NetDriver->PackageMap->GetEntityIdFromObject(Actor); if (EntityId == SpatialConstants::INVALID_ENTITY_ID) @@ -39,27 +43,53 @@ FSpatialLoadBalancingHandler::EvaluateActorResult FSpatialLoadBalancingHandler:: return EvaluateActorResult::RemoveAdditional; } - if (NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID)) + if (NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID)) { - if (!NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*Actor) && !NetDriver->LockingPolicy->IsLocked(Actor)) - { - AActor* NetOwner = SpatialGDK::GetHierarchyRoot(Actor); + AActor* NetOwner = GetReplicatedHierarchyRoot(Actor); + const bool bNetOwnerHasAuth = NetOwner->HasAuthority(); + // Load balance if we are not supposed to be on this worker, or if we are separated from our owner. + if ((!NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*NetOwner) || !bNetOwnerHasAuth) + && !NetDriver->LockingPolicy->IsLocked(Actor)) + { uint64 HierarchyAuthorityReceivedTimestamp = GetLatestAuthorityChangeFromHierarchy(NetOwner); - const float TimeSinceReceivingAuthInSeconds = double(FPlatformTime::Cycles64() - HierarchyAuthorityReceivedTimestamp) * FPlatformTime::GetSecondsPerCycle64(); + const float TimeSinceReceivingAuthInSeconds = + double(FPlatformTime::Cycles64() - HierarchyAuthorityReceivedTimestamp) * FPlatformTime::GetSecondsPerCycle64(); const float MigrationBackoffTimeInSeconds = 1.0f; if (TimeSinceReceivingAuthInSeconds < MigrationBackoffTimeInSeconds) { - UE_LOG(LogSpatialOSNetDriver, Verbose, TEXT("Tried to change auth too early for actor %s"), *Actor->GetName()); + UE_LOG(LogSpatialLoadBalancingHandler, Verbose, TEXT("Tried to change auth too early for actor %s"), *Actor->GetName()); } else { - const VirtualWorkerId NewAuthVirtualWorkerId = NetDriver->LoadBalanceStrategy->WhoShouldHaveAuthority(*NetOwner); + VirtualWorkerId NewAuthVirtualWorkerId = SpatialConstants::INVALID_VIRTUAL_WORKER_ID; + if (bNetOwnerHasAuth) + { + NewAuthVirtualWorkerId = NetDriver->LoadBalanceStrategy->WhoShouldHaveAuthority(*NetOwner); + } + else + { + // If we are separated from our owner, it could be prevented from migrating (if it has interest over the current actor), + // so the load balancing strategy could give us a worker different from where it should be. + // Instead, we read its currently assigned worker, which will eventually make us land where our owner is. + Worker_EntityId OwnerId = NetDriver->PackageMap->GetEntityIdFromObject(NetOwner); + if (AuthorityIntent* OwnerAuthIntent = NetDriver->StaticComponentView->GetComponentData(OwnerId)) + { + NewAuthVirtualWorkerId = OwnerAuthIntent->VirtualWorkerId; + } + else + { + UE_LOG(LogSpatialLoadBalancingHandler, Error, TEXT("Actor %s (%llu) cannot join its owner %s (%llu)"), + *Actor->GetName(), EntityId, *NetOwner->GetName(), OwnerId); + } + } + if (NewAuthVirtualWorkerId == SpatialConstants::INVALID_VIRTUAL_WORKER_ID) { - UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Load Balancing Strategy returned invalid virtual worker for actor %s"), *Actor->GetName()); + UE_LOG(LogSpatialLoadBalancingHandler, Error, + TEXT("Load Balancing Strategy returned invalid virtual worker for actor %s"), *Actor->GetName()); } else { @@ -93,7 +123,7 @@ void FSpatialLoadBalancingHandler::ProcessMigrations() void FSpatialLoadBalancingHandler::UpdateSpatialDebugInfo(AActor* Actor, Worker_EntityId EntityId) const { - if (SpatialGDK::SpatialDebugging* DebuggingInfo = NetDriver->StaticComponentView->GetComponentData(EntityId)) + if (SpatialDebugging* DebuggingInfo = NetDriver->StaticComponentView->GetComponentData(EntityId)) { const bool bIsLocked = NetDriver->LockingPolicy->IsLocked(Actor); if (DebuggingInfo->IsLocked != bIsLocked) @@ -123,3 +153,66 @@ uint64 FSpatialLoadBalancingHandler::GetLatestAuthorityChangeFromHierarchy(const return LatestTimestamp; } + +void FSpatialLoadBalancingHandler::LogMigrationFailure(EActorMigrationResult ActorMigrationResult, AActor* Actor) +{ + FString FailureReason; + + // Waiting before creating logs to suppress the logs for newly created actors + if (Actor->GetGameTimeSinceCreation() > 1) + { + switch (ActorMigrationResult) + { + case EActorMigrationResult::NotAuthoritative: + FailureReason = TEXT("does not have authority"); + break; + case EActorMigrationResult::NotReady: + FailureReason = TEXT("is not ready"); + break; + case EActorMigrationResult::PendingKill: + FailureReason = TEXT("is pending kill"); + break; + case EActorMigrationResult::NotInitialized: + FailureReason = TEXT("is not initialized"); + break; + case EActorMigrationResult::Streaming: + FailureReason = TEXT("is streaming in or out"); + break; + case EActorMigrationResult::NetDormant: + FailureReason = TEXT("is startup actor and initially net dormant"); + break; + case EActorMigrationResult::NoSpatialClassFlags: + FailureReason = TEXT("does not have spatial class flags"); + break; + case EActorMigrationResult::DormantOnConnection: + FailureReason = TEXT("is dormant on connection"); + break; + default: + break; + } + } + + // If a failure reason is returned log warning + if (!FailureReason.IsEmpty()) + { + Worker_EntityId ActorEntityId = NetDriver->PackageMap->GetEntityIdFromObject(Actor); + + // Check if we have recently logged this actor / reason and if so suppress the log + if (!NetDriver->IsLogged(ActorEntityId, ActorMigrationResult)) + { + if (ActorMigrationResult == EActorMigrationResult::NotAuthoritative) + { + // Request further diagnostics from authoritative server of blocking actor + Worker_CommandRequest MigrationDiagnosticCommandRequest = MigrationDiagnostic::CreateMigrationDiagnosticRequest(); + NetDriver->Connection->SendCommandRequest(ActorEntityId, &MigrationDiagnosticCommandRequest, RETRY_MAX_TIMES, {}); + } + else + { + AActor* HierarchyRoot = GetReplicatedHierarchyRoot(Actor); + UE_LOG(LogSpatialLoadBalancingHandler, Warning, + TEXT("Prevented Actor %s 's hierarchy from migrating because Actor %s (%llu) %s"), *HierarchyRoot->GetName(), + *Actor->GetName(), ActorEntityId, *FailureReason); + } + } + } +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp index c874713576..aae41335d7 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp @@ -151,11 +151,13 @@ 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(); - Connection->SendCommandRequest(ControllerEntityId, &Request, SpatialConstants::DEBUG_METRICS_START_RPC_METRICS_ID); + Connection->SendCommandRequest(ControllerEntityId, &Request, SpatialGDK::RETRY_MAX_TIMES, {}); } else { - UE_LOG(LogSpatialMetrics, Warning, TEXT("SpatialStartRPCMetrics: Could not resolve local PlayerController entity! RPC metrics will not start on the server.")); + UE_LOG( + LogSpatialMetrics, Warning, + TEXT("SpatialStartRPCMetrics: Could not resolve local PlayerController entity! RPC metrics will not start on the server.")); } } } @@ -184,8 +186,7 @@ void USpatialMetrics::SpatialStopRPCMetrics() RecentRPCs.GenerateValueArray(RecentRPCArray); // Show the most frequently called RPCs at the top. - RecentRPCArray.Sort([](const RPCStat& A, const RPCStat& B) - { + RecentRPCArray.Sort([](const RPCStat& A, const RPCStat& B) { if (A.Type != B.Type) { return static_cast(A.Type) < static_cast(B.Type); @@ -204,9 +205,13 @@ void USpatialMetrics::SpatialStopRPCMetrics() UE_LOG(LogSpatialMetrics, Log, TEXT("---------------------------")); 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)); + 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, '-')); + FString SeparatorLine = + FString::Printf(TEXT("-------------------+-%s-+------------+------------+---------------+--------------+------------"), + *FString::ChrN(MaxRPCNameLen, '-')); ERPCType PrevType = ERPCType::Invalid; for (RPCStat& Stat : RecentRPCArray) @@ -218,12 +223,16 @@ void USpatialMetrics::SpatialStopRPCMetrics() PrevType = Stat.Type; UE_LOG(LogSpatialMetrics, Log, TEXT("%s"), *SeparatorLine); } - UE_LOG(LogSpatialMetrics, Log, TEXT("%s | %s | %10d | %10.4f | %13d | %12.4f | %11.4f"), *RPCTypeField.RightPad(18), *Stat.Name.RightPad(MaxRPCNameLen), Stat.Calls, Stat.Calls / TrackRPCInterval, Stat.TotalPayload, (float)Stat.TotalPayload / Stat.Calls, Stat.TotalPayload / TrackRPCInterval); + UE_LOG(LogSpatialMetrics, Log, TEXT("%s | %s | %10d | %10.4f | %13d | %12.4f | %11.4f"), *RPCTypeField.RightPad(18), + *Stat.Name.RightPad(MaxRPCNameLen), Stat.Calls, Stat.Calls / TrackRPCInterval, Stat.TotalPayload, + (float)Stat.TotalPayload / Stat.Calls, Stat.TotalPayload / TrackRPCInterval); TotalCalls += Stat.Calls; TotalPayload += Stat.TotalPayload; } UE_LOG(LogSpatialMetrics, Log, TEXT("%s"), *SeparatorLine); - UE_LOG(LogSpatialMetrics, Log, TEXT("Total | %s | %10d | %10.4f | %13d | %12.4f | %11.4f"), *FString::ChrN(MaxRPCNameLen, ' '), TotalCalls, TotalCalls / TrackRPCInterval, TotalPayload, (float)TotalPayload / TotalCalls, TotalPayload / TrackRPCInterval); + UE_LOG(LogSpatialMetrics, Log, TEXT("Total | %s | %10d | %10.4f | %13d | %12.4f | %11.4f"), + *FString::ChrN(MaxRPCNameLen, ' '), TotalCalls, TotalCalls / TrackRPCInterval, TotalPayload, + (float)TotalPayload / TotalCalls, TotalPayload / TrackRPCInterval); RecentRPCs.Empty(); } @@ -242,11 +251,13 @@ 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(); - Connection->SendCommandRequest(ControllerEntityId, &Request, SpatialConstants::DEBUG_METRICS_STOP_RPC_METRICS_ID); + Connection->SendCommandRequest(ControllerEntityId, &Request, SpatialGDK::RETRY_MAX_TIMES, {}); } else { - UE_LOG(LogSpatialMetrics, Warning, TEXT("SpatialStopRPCMetrics: Could not resolve local PlayerController entity! RPC metrics will not stop on the server.")); + UE_LOG( + LogSpatialMetrics, Warning, + TEXT("SpatialStopRPCMetrics: Could not resolve local PlayerController entity! RPC metrics will not stop on the server.")); } } } @@ -274,11 +285,12 @@ 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); - Connection->SendCommandRequest(ControllerEntityId, &Request, SpatialConstants::DEBUG_METRICS_MODIFY_SETTINGS_ID); + Connection->SendCommandRequest(ControllerEntityId, &Request, SpatialGDK::RETRY_MAX_TIMES, {}); } else { - UE_LOG(LogSpatialMetrics, Warning, TEXT("SpatialModifySetting: Could not resolve local PlayerController entity! Setting will not be sent to server.")); + UE_LOG(LogSpatialMetrics, Warning, + TEXT("SpatialModifySetting: Could not resolve local PlayerController entity! Setting will not be sent to server.")); } } else @@ -292,13 +304,21 @@ void USpatialMetrics::SpatialModifySetting(const FString& Name, float Value) { GetMutableDefault()->EntityCreationRateLimit = static_cast(Value); } - else if (Name == TEXT("PositionUpdateFrequency")) + else if (Name == TEXT("PositionUpdateLowerThresholdSeconds")) + { + GetMutableDefault()->PositionUpdateLowerThresholdSeconds = Value; + } + else if (Name == TEXT("PositionUpdateLowerThresholdCentimeters")) + { + GetMutableDefault()->PositionUpdateLowerThresholdCentimeters = Value; + } + else if (Name == TEXT("PositionUpdateThresholdMaxSeconds")) { - GetMutableDefault()->PositionUpdateFrequency = Value; + GetMutableDefault()->PositionUpdateThresholdMaxSeconds = Value; } - else if (Name == TEXT("PositionDistanceThreshold")) + else if (Name == TEXT("PositionUpdateThresholdMaxCentimeters")) { - GetMutableDefault()->PositionDistanceThreshold = Value; + GetMutableDefault()->PositionUpdateThresholdMaxCentimeters = Value; } else { @@ -349,10 +369,10 @@ void USpatialMetrics::TrackSentRPC(UFunction* Function, ERPCType RPCType, int Pa Stat.TotalPayload += PayloadSize; } -void USpatialMetrics::HandleWorkerMetrics(Worker_Op* Op) +void USpatialMetrics::HandleWorkerMetrics(const Worker_Op& Op) { - int32 NumGaugeMetrics = Op->op.metrics.metrics.gauge_metric_count; - int32 NumHistogramMetrics = Op->op.metrics.metrics.histogram_metric_count; + int32 NumGaugeMetrics = Op.op.metrics.metrics.gauge_metric_count; + int32 NumHistogramMetrics = Op.op.metrics.metrics.histogram_metric_count; if (NumGaugeMetrics > 0 || NumHistogramMetrics > 0) // We store these here so we can forward them with our metrics submission { FString StringTmp; @@ -360,14 +380,14 @@ void USpatialMetrics::HandleWorkerMetrics(Worker_Op* Op) for (int32 i = 0; i < NumGaugeMetrics; i++) { - const Worker_GaugeMetric& WorkerMetric = Op->op.metrics.metrics.gauge_metrics[i]; + const Worker_GaugeMetric& WorkerMetric = Op.op.metrics.metrics.gauge_metrics[i]; StringTmp = WorkerMetric.key; WorkerSDKGaugeMetrics.FindOrAdd(StringTmp) = WorkerMetric.value; } for (int32 i = 0; i < NumHistogramMetrics; i++) { - const Worker_HistogramMetric& WorkerMetric = Op->op.metrics.metrics.histogram_metrics[i]; + const Worker_HistogramMetric& WorkerMetric = Op.op.metrics.metrics.histogram_metrics[i]; StringTmp = WorkerMetric.key; WorkerHistogramValues& HistogramMetrics = WorkerSDKHistogramMetrics.FindOrAdd(StringTmp); HistogramMetrics.Sum = WorkerMetric.sum; @@ -375,7 +395,8 @@ void USpatialMetrics::HandleWorkerMetrics(Worker_Op* Op) HistogramMetrics.Buckets.SetNum(NumBuckets); for (int32 j = 0; j < NumBuckets; j++) { - HistogramMetrics.Buckets[j] = TTuple{ WorkerMetric.buckets[j].upper_bound, WorkerMetric.buckets[j].samples }; + HistogramMetrics.Buckets[j] = + TTuple{ WorkerMetric.buckets[j].upper_bound, WorkerMetric.buckets[j].samples }; } } @@ -388,7 +409,8 @@ void USpatialMetrics::HandleWorkerMetrics(Worker_Op* Op) void USpatialMetrics::SetCustomMetric(const FString& Metric, const UserSuppliedMetric& Delegate) { - UE_LOG(LogSpatialMetrics, Log, TEXT("USpatialMetrics: Adding custom metric %s (%s)"), *Metric, Delegate.GetUObject() ? *GetNameSafe(Delegate.GetUObject()) : TEXT("Not attached to UObject")); + UE_LOG(LogSpatialMetrics, Log, TEXT("USpatialMetrics: Adding custom metric %s (%s)"), *Metric, + Delegate.GetUObject() ? *GetNameSafe(Delegate.GetUObject()) : TEXT("Not attached to UObject")); if (UserSuppliedMetric* ExistingMetric = UserSuppliedMetrics.Find(Metric)) { *ExistingMetric = Delegate; @@ -403,7 +425,8 @@ void USpatialMetrics::RemoveCustomMetric(const FString& Metric) { if (UserSuppliedMetric* ExistingMetric = UserSuppliedMetrics.Find(Metric)) { - UE_LOG(LogSpatialMetrics, Log, TEXT("USpatialMetrics: Removing custom metric %s (%s)"), *Metric, ExistingMetric->GetUObject() ? *GetNameSafe(ExistingMetric->GetUObject()) : TEXT("Not attached to UObject")); + UE_LOG(LogSpatialMetrics, Log, TEXT("USpatialMetrics: Removing custom metric %s (%s)"), *Metric, + ExistingMetric->GetUObject() ? *GetNameSafe(ExistingMetric->GetUObject()) : TEXT("Not attached to UObject")); UserSuppliedMetrics.Remove(Metric); } } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetricsDisplay.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetricsDisplay.cpp index 5cafe07ff4..2f41c0b7b9 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetricsDisplay.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetricsDisplay.cpp @@ -105,7 +105,8 @@ void ASpatialMetricsDisplay::DrawDebug(class UCanvas* Canvas, APlayerController* const uint32 StatDisplayStartX = 25; const uint32 StatDisplayStartY = 80; - const FString StatColumnTitles[StatColumn_Last] = { TEXT("Worker"), TEXT("Frame"), TEXT("Movement Corrections"), TEXT("Replication Limit") }; + const FString StatColumnTitles[StatColumn_Last] = { TEXT("Worker"), TEXT("Frame"), TEXT("Movement Corrections"), + TEXT("Replication Limit") }; const uint32 StatColumnOffsets[StatColumn_Last] = { 0, 160, 80, 160 }; const uint32 StatRowOffset = 20; @@ -142,19 +143,22 @@ void ASpatialMetricsDisplay::DrawDebug(class UCanvas* Canvas, APlayerController* Canvas->DrawText(RenderFont, FString::Printf(TEXT("%s"), *OneWorkerStats.WorkerName), DrawX, DrawY, 1.0f, 1.0f, FontRenderInfo); DrawX += StatColumnOffsets[StatColumn_AverageFrameTime]; - Canvas->DrawText(RenderFont, FString::Printf(TEXT("%.2f ms"), 1000.f / OneWorkerStats.AverageFPS), DrawX, DrawY, 1.0f, 1.0f, FontRenderInfo); + Canvas->DrawText(RenderFont, FString::Printf(TEXT("%.2f ms"), 1000.f / OneWorkerStats.AverageFPS), DrawX, DrawY, 1.0f, 1.0f, + FontRenderInfo); DrawX += StatColumnOffsets[StatColumn_MovementCorrections]; - Canvas->DrawText(RenderFont, FString::Printf(TEXT("%.4f"), OneWorkerStats.ServerMovementCorrections), DrawX, DrawY, 1.0f, 1.0f, FontRenderInfo); + Canvas->DrawText(RenderFont, FString::Printf(TEXT("%.4f"), OneWorkerStats.ServerMovementCorrections), DrawX, DrawY, 1.0f, 1.0f, + FontRenderInfo); DrawX += StatColumnOffsets[StatColumn_ReplicationLimit]; - Canvas->DrawText(RenderFont, FString::Printf(TEXT("%d:%d"), OneWorkerStats.ServerConsiderListSize, OneWorkerStats.ServerReplicationLimit), DrawX, DrawY, 1.0f, 1.0f, FontRenderInfo); + Canvas->DrawText(RenderFont, + FString::Printf(TEXT("%d:%d"), OneWorkerStats.ServerConsiderListSize, OneWorkerStats.ServerReplicationLimit), + DrawX, DrawY, 1.0f, 1.0f, FontRenderInfo); DrawY += StatRowOffset; } } - void ASpatialMetricsDisplay::SpatialToggleStatDisplay() { #if !UE_BUILD_SHIPPING @@ -167,7 +171,8 @@ void ASpatialMetricsDisplay::SpatialToggleStatDisplay() } else { - DrawDebugDelegateHandle = UDebugDrawService::Register(TEXT("Game"), FDebugDrawDelegate::CreateUObject(this, &ASpatialMetricsDisplay::DrawDebug)); + DrawDebugDelegateHandle = + UDebugDrawService::Register(TEXT("Game"), FDebugDrawDelegate::CreateUObject(this, &ASpatialMetricsDisplay::DrawDebug)); } #endif // !UE_BUILD_SHIPPING } @@ -185,9 +190,7 @@ void ASpatialMetricsDisplay::Tick(float DeltaSeconds) USpatialNetDriver* SpatialNetDriver = Cast(GetWorld()->GetNetDriver()); - if (SpatialNetDriver == nullptr || - SpatialNetDriver->Connection == nullptr || - SpatialNetDriver->SpatialMetrics == nullptr) + if (SpatialNetDriver == nullptr || SpatialNetDriver->Connection == nullptr || SpatialNetDriver->SpatialMetrics == nullptr) { return; } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp index 69219f00b0..4e7c67d822 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp @@ -3,16 +3,16 @@ #include "Utils/SpatialStatics.h" #include "Engine/World.h" +#include "EngineClasses/SpatialGameInstance.h" #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" #include "EngineClasses/SpatialWorldSettings.h" -#include "LoadBalancing/SpatialMultiWorkerSettings.h" #include "GeneralProjectSettings.h" #include "Interop/SpatialWorkerFlags.h" #include "Kismet/KismetSystemLibrary.h" -#include "SpatialConstants.h" -#include "EngineClasses/SpatialGameInstance.h" #include "LoadBalancing/LayeredLBStrategy.h" +#include "LoadBalancing/SpatialMultiWorkerSettings.h" +#include "SpatialConstants.h" #include "SpatialGDKSettings.h" #include "Utils/InspectionColors.h" @@ -37,8 +37,7 @@ bool CanProcessActor(const AActor* Actor) if (!Actor->HasAuthority()) { - UE_LOG(LogSpatial, Error, TEXT("Calling locking API functions on a non-auth Actor is invalid. Actor: %s."), - *GetNameSafe(Actor)); + UE_LOG(LogSpatial, Error, TEXT("Calling locking API functions on a non-auth Actor is invalid. Actor: %s."), *GetNameSafe(Actor)); return false; } @@ -48,7 +47,38 @@ bool CanProcessActor(const AActor* Actor) bool USpatialStatics::IsSpatialNetworkingEnabled() { - return GetDefault()->UsesSpatialNetworking(); + return GetDefault()->UsesSpatialNetworking(); +} + +bool USpatialStatics::IsHandoverEnabled(const UObject* WorldContextObject) +{ + const UWorld* World = WorldContextObject->GetWorld(); + if (World == nullptr) + { + return true; + } + + if (World->IsNetMode(NM_Client)) + { + return true; + } + + if (const USpatialNetDriver* SpatialNetDriver = Cast(World->GetNetDriver())) + { + // Calling IsHandoverEnabled before NotifyBeginPlay has been called (when NetDriver is ready) is invalid. + if (!SpatialNetDriver->IsReady()) + { + UE_LOG(LogSpatial, Error, + TEXT("Called IsHandoverEnabled before NotifyBeginPlay has been called is invalid. Returning enabled.")); + return true; + } + + if (const ULayeredLBStrategy* LBStrategy = Cast(SpatialNetDriver->LoadBalanceStrategy)) + { + return LBStrategy->RequiresHandoverData(); + } + } + return true; } FName USpatialStatics::GetCurrentWorkerType(const UObject* WorldContext) @@ -95,21 +125,39 @@ FColor USpatialStatics::GetInspectorColorForWorkerName(const FString& WorkerName return SpatialGDK::GetColorForWorkerName(WorkerName); } -bool USpatialStatics::IsSpatialMultiWorkerEnabled(const UObject* WorldContextObject) +bool USpatialStatics::IsMultiWorkerEnabled() +{ + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + + // Check if multi-worker settings class was overridden from the command line + if (SpatialGDKSettings->OverrideMultiWorkerSettingsClass.IsSet()) + { + // If command line override for Multi Worker Settings is set then enable multi-worker. + return true; + } +#if WITH_EDITOR + else if (!SpatialGDKSettings->IsMultiWorkerEditorEnabled()) + { + // If multi-worker is not enabled in editor then disable multi-worker. + return false; + } +#endif // WITH_EDITOR + return true; +} + +TSubclassOf USpatialStatics::GetSpatialMultiWorkerClass(const UObject* WorldContextObject, + bool bForceNonEditorSettings) { - checkf(WorldContextObject != nullptr, TEXT("Called IsSpatialMultiWorkerEnabled with a nullptr WorldContextObject*")); + checkf(WorldContextObject != nullptr, TEXT("Called GetSpatialMultiWorkerClass with a nullptr WorldContextObject*")); const UWorld* World = WorldContextObject->GetWorld(); - checkf(World != nullptr, TEXT("Called IsSpatialMultiWorkerEnabled with a nullptr World*")); + checkf(World != nullptr, TEXT("Called GetSpatialMultiWorkerClass with a nullptr World*")); - const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); - if (SpatialGDKSettings->bOverrideMultiWorker.IsSet()) + if (ASpatialWorldSettings* WorldSettings = Cast(World->GetWorldSettings())) { - return SpatialGDKSettings->bOverrideMultiWorker.GetValue(); + return WorldSettings->GetMultiWorkerSettingsClass(bForceNonEditorSettings); } - - const ASpatialWorldSettings* WorldSettings = Cast(World->GetWorldSettings()); - return WorldSettings != nullptr && WorldSettings->IsMultiWorkerEnabledInWorldSettings(); + return USpatialMultiWorkerSettings::StaticClass(); } bool USpatialStatics::IsSpatialOffloadingEnabled(const UWorld* World) @@ -118,12 +166,13 @@ bool USpatialStatics::IsSpatialOffloadingEnabled(const UWorld* World) { if (const ASpatialWorldSettings* WorldSettings = Cast(World->GetWorldSettings())) { - if (!IsSpatialMultiWorkerEnabled(World)) + if (!IsMultiWorkerEnabled()) { return false; } - const UAbstractSpatialMultiWorkerSettings* MultiWorkerSettings = WorldSettings->MultiWorkerSettingsClass->GetDefaultObject(); + const UAbstractSpatialMultiWorkerSettings* MultiWorkerSettings = + USpatialStatics::GetSpatialMultiWorkerClass(World)->GetDefaultObject(); return MultiWorkerSettings->WorkerLayers.Num() > 1; } } @@ -166,7 +215,9 @@ bool USpatialStatics::IsActorGroupOwnerForClass(const UObject* WorldContextObjec // Calling IsActorGroupOwnerForClass before NotifyBeginPlay has been called (when NetDriver is ready) is invalid. if (!SpatialNetDriver->IsReady()) { - UE_LOG(LogSpatial, Error, TEXT("Called IsActorGroupOwnerForClass before NotifyBeginPlay has been called is invalid. Actor class: %s"), *GetNameSafe(ActorClass)); + UE_LOG(LogSpatial, Error, + TEXT("Called IsActorGroupOwnerForClass before NotifyBeginPlay has been called is invalid. Actor class: %s"), + *GetNameSafe(ActorClass)); return true; } @@ -178,7 +229,9 @@ bool USpatialStatics::IsActorGroupOwnerForClass(const UObject* WorldContextObjec return true; } -void USpatialStatics::PrintStringSpatial(UObject* WorldContextObject, const FString& InString /*= FString(TEXT("Hello"))*/, bool bPrintToScreen /*= true*/, FLinearColor TextColor /*= FLinearColor(0.0, 0.66, 1.0)*/, float Duration /*= 2.f*/) +void USpatialStatics::PrintStringSpatial(UObject* WorldContextObject, const FString& InString /*= FString(TEXT("Hello"))*/, + bool bPrintToScreen /*= true*/, FLinearColor TextColor /*= FLinearColor(0.0, 0.66, 1.0)*/, + float Duration /*= 2.f*/) { // This will be logged in the SpatialOutput so we don't want to double log this, therefore bPrintToLog is false. UKismetSystemLibrary::PrintString(WorldContextObject, InString, bPrintToScreen, false /*bPrintToLog*/, TextColor, Duration); @@ -187,7 +240,9 @@ void USpatialStatics::PrintStringSpatial(UObject* WorldContextObject, const FStr UE_LOG(LogSpatial, Log, TEXT("%s"), *InString); } -void USpatialStatics::PrintTextSpatial(UObject* WorldContextObject, const FText InText /*= INVTEXT("Hello")*/, bool bPrintToScreen /*= true*/, FLinearColor TextColor /*= FLinearColor(0.0, 0.66, 1.0)*/, float Duration /*= 2.f*/) +void USpatialStatics::PrintTextSpatial(UObject* WorldContextObject, const FText InText /*= INVTEXT("Hello")*/, + bool bPrintToScreen /*= true*/, FLinearColor TextColor /*= FLinearColor(0.0, 0.66, 1.0)*/, + float Duration /*= 2.f*/) { PrintStringSpatial(WorldContextObject, InText.ToString(), bPrintToScreen, TextColor, Duration); } @@ -224,7 +279,7 @@ FString USpatialStatics::GetActorEntityIdAsString(const AActor* Actor) FLockingToken USpatialStatics::AcquireLock(AActor* Actor, const FString& DebugString) { - if (!CanProcessActor(Actor) || !IsSpatialMultiWorkerEnabled(Actor)) + if (!CanProcessActor(Actor) || !IsMultiWorkerEnabled()) { return FLockingToken{ SpatialConstants::INVALID_ACTOR_LOCK_TOKEN }; } @@ -233,15 +288,15 @@ FLockingToken USpatialStatics::AcquireLock(AActor* Actor, const FString& DebugSt const ActorLockToken LockToken = LockingPolicy->AcquireLock(Actor, DebugString); - UE_LOG(LogSpatial, Verbose, TEXT("LockingComponent called AcquireLock. Actor: %s. Token: %lld. New lock count: %d"), - *Actor->GetName(), LockToken, LockingPolicy->GetActorLockCount(Actor)); + UE_LOG(LogSpatial, Verbose, TEXT("LockingComponent called AcquireLock. Actor: %s. Token: %lld. New lock count: %d"), *Actor->GetName(), + LockToken, LockingPolicy->GetActorLockCount(Actor)); return FLockingToken{ LockToken }; } bool USpatialStatics::IsLocked(const AActor* Actor) { - if (!CanProcessActor(Actor) || !IsSpatialMultiWorkerEnabled(Actor)) + if (!CanProcessActor(Actor) || !IsMultiWorkerEnabled()) { return false; } @@ -251,7 +306,7 @@ bool USpatialStatics::IsLocked(const AActor* Actor) void USpatialStatics::ReleaseLock(const AActor* Actor, FLockingToken LockToken) { - if (!CanProcessActor(Actor) || !IsSpatialMultiWorkerEnabled(Actor)) + if (!CanProcessActor(Actor) || !IsMultiWorkerEnabled()) { return; } @@ -260,5 +315,71 @@ void USpatialStatics::ReleaseLock(const AActor* Actor, FLockingToken LockToken) LockingPolicy->ReleaseLock(LockToken.Token); UE_LOG(LogSpatial, Verbose, TEXT("LockingComponent called ReleaseLock. Actor: %s. Token: %lld. Resulting lock count: %d"), - *Actor->GetName(), LockToken.Token, LockingPolicy->GetActorLockCount(Actor)); + *Actor->GetName(), LockToken.Token, LockingPolicy->GetActorLockCount(Actor)); +} + +FName USpatialStatics::GetLayerName(const UObject* WorldContextObject) +{ + const UWorld* World = WorldContextObject->GetWorld(); + if (World == nullptr) + { + UE_LOG(LogSpatial, Error, TEXT("World was nullptr when calling GetLayerName")); + return NAME_None; + } + + if (World->IsNetMode(NM_Client)) + { + return SpatialConstants::DefaultClientWorkerType; + } + + if (!IsSpatialNetworkingEnabled()) + { + return SpatialConstants::DefaultLayer; + } + + const USpatialNetDriver* SpatialNetDriver = Cast(World->GetNetDriver()); + if (SpatialNetDriver == nullptr || !SpatialNetDriver->IsReady()) + { + UE_LOG(LogSpatial, Error, + TEXT("Called GetLayerName before NotifyBeginPlay has been called is invalid. Worker doesn't know its layer yet")); + return NAME_None; + } + + const ULayeredLBStrategy* LBStrategy = Cast(SpatialNetDriver->LoadBalanceStrategy); + check(LBStrategy != nullptr); + return LBStrategy->GetLocalLayerName(); +} + +void USpatialStatics::SpatialDebuggerSetOnConfigUIClosedCallback(const UObject* WorldContextObject, FOnConfigUIClosedDelegate Delegate) +{ + const UWorld* World = WorldContextObject->GetWorld(); + if (World == nullptr) + { + UE_LOG(LogSpatial, Error, TEXT("World was nullptr when calling SpatialDebuggerSetOnConfigUIClosedCallback")); + return; + } + + if (World->GetNetMode() != NM_Client) + { + UE_LOG(LogSpatial, Warning, + TEXT("SpatialDebuggerSetOnConfigUIClosedCallback should only be called on clients. It has no effects on servers.")); + return; + } + + const USpatialNetDriver* SpatialNetDriver = Cast(World->GetNetDriver()); + if (SpatialNetDriver == nullptr) + { + UE_LOG(LogSpatial, Error, TEXT("No spatial net driver found when calling SpatialDebuggerSetOnConfigUIClosedCallback")); + return; + } + + SpatialNetDriver->SpatialDebuggerReady->Await(FOnReady::CreateLambda([SpatialNetDriver, Delegate](const FString& ErrorMessage) { + if (!ErrorMessage.IsEmpty()) + { + UE_LOG(LogSpatial, Error, TEXT("Couldn't set config ui closed callback due to error: %s"), *ErrorMessage); + return; + } + + SpatialNetDriver->SpatialDebugger->OnConfigUIClosed = Delegate; + })); } diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/ActorInterestComponent.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/ActorInterestComponent.h index dbc19e379c..531c1285dc 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/ActorInterestComponent.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/ActorInterestComponent.h @@ -2,8 +2,8 @@ #pragma once -#include "CoreMinimal.h" #include "Components/ActorComponent.h" +#include "CoreMinimal.h" #include "Interop/SpatialInterestConstraints.h" #include "Schema/Interest.h" @@ -14,7 +14,7 @@ class USpatialClassInfoManager; /** * Creates a set of SpatialOS Queries for describing interest that this actor has in other entities. */ -UCLASS(ClassGroup=(SpatialGDK), NotSpatialType, Meta=(BlueprintSpawnableComponent)) +UCLASS(ClassGroup = (SpatialGDK), NotSpatialType, Meta = (BlueprintSpawnableComponent)) class SPATIALGDK_API UActorInterestComponent final : public UActorComponent { GENERATED_BODY() @@ -23,7 +23,8 @@ class SPATIALGDK_API UActorInterestComponent final : public UActorComponent UActorInterestComponent() = default; ~UActorInterestComponent() = default; - void PopulateFrequencyToConstraintsMap(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::FrequencyToConstraintsMap& OutFrequencyToQueryConstraints) 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. @@ -36,5 +37,4 @@ class SPATIALGDK_API UActorInterestComponent final : public UActorComponent */ UPROPERTY(BlueprintReadonly, EditDefaultsOnly, Category = "Interest") TArray Queries; - }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/SpatialPingComponent.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/SpatialPingComponent.h index 85f5bac7a5..1320682df6 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/SpatialPingComponent.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/SpatialPingComponent.h @@ -80,7 +80,8 @@ 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. + // 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; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h index a89e305450..11d26875ca 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h @@ -9,8 +9,8 @@ #include "Interop/SpatialClassInfoManager.h" #include "Interop/SpatialStaticComponentView.h" #include "Runtime/Launch/Resources/Version.h" -#include "Schema/StandardLibrary.h" #include "Schema/RPCPayload.h" +#include "Schema/StandardLibrary.h" #include "SpatialCommonTypes.h" #include "SpatialGDKSettings.h" #include "Utils/GDKPropertyMacros.h" @@ -23,6 +23,14 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialActorChannel, Log, All); +// This is necessary to compile TUniquePtr on Linux with 4.26, +// where the default deleter needs FObjectReferences to be complete, which isn't possible +// because we're still in the middle of its definition. +struct FObjectReferencesMapDeleter +{ + void operator()(FObjectReferencesMap* Ptr) const; +}; + struct FObjectReferences { FObjectReferences() = default; @@ -36,11 +44,18 @@ struct FObjectReferences , Array(MoveTemp(Other.Array)) , ShadowOffset(Other.ShadowOffset) , ParentIndex(Other.ParentIndex) - , Property(Other.Property) {} + , Property(Other.Property) + { + } // Single property constructor - FObjectReferences(const FUnrealObjectRef& InObjectRef, bool bUnresolved, int32 InCmdIndex, int32 InParentIndex, GDK_PROPERTY(Property)* InProperty) - : bSingleProp(true), bFastArrayProp(false), ShadowOffset(InCmdIndex), ParentIndex(InParentIndex), Property(InProperty) + FObjectReferences(const FUnrealObjectRef& InObjectRef, bool bUnresolved, int32 InCmdIndex, int32 InParentIndex, + GDK_PROPERTY(Property) * InProperty) + : bSingleProp(true) + , bFastArrayProp(false) + , ShadowOffset(InCmdIndex) + , ParentIndex(InParentIndex) + , Property(InProperty) { if (bUnresolved) { @@ -53,33 +68,44 @@ struct FObjectReferences } // Struct (memory stream) constructor - FObjectReferences(const TArray& InBuffer, int32 InNumBufferBits, TSet&& InDynamicRefs, TSet&& InUnresolvedRefs, int32 InCmdIndex, int32 InParentIndex, GDK_PROPERTY(Property)* InProperty, bool InFastArrayProp = false) - : MappedRefs(MoveTemp(InDynamicRefs)), UnresolvedRefs(MoveTemp(InUnresolvedRefs)), bSingleProp(false), bFastArrayProp(InFastArrayProp), Buffer(InBuffer), NumBufferBits(InNumBufferBits), ShadowOffset(InCmdIndex), ParentIndex(InParentIndex), Property(InProperty) {} + FObjectReferences(const TArray& InBuffer, int32 InNumBufferBits, TSet&& InDynamicRefs, + TSet&& InUnresolvedRefs, int32 InCmdIndex, int32 InParentIndex, GDK_PROPERTY(Property) * 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, GDK_PROPERTY(Property)* InProperty) - : bSingleProp(false), bFastArrayProp(false), Array(InArray), ShadowOffset(InCmdIndex), ParentIndex(InParentIndex), Property(InProperty) {} - - TSet MappedRefs; - TSet UnresolvedRefs; + FObjectReferences(FObjectReferencesMap* InArray, int32 InCmdIndex, int32 InParentIndex, GDK_PROPERTY(Property) * InProperty) + : bSingleProp(false) + , bFastArrayProp(false) + , Array(InArray) + , ShadowOffset(InCmdIndex) + , ParentIndex(InParentIndex) + , Property(InProperty) + { + } - bool bSingleProp; - bool bFastArrayProp; - TArray Buffer; - int32 NumBufferBits; + TSet MappedRefs; + TSet UnresolvedRefs; - TUniquePtr Array; - int32 ShadowOffset; - int32 ParentIndex; - GDK_PROPERTY(Property)* Property; -}; - -struct FPendingSubobjectAttachment -{ - const FClassInfo* Info; - TWeakObjectPtr Subobject; + bool bSingleProp; + bool bFastArrayProp; + TArray Buffer; + int32 NumBufferBits; - TSet PendingAuthorityDelegations; + TUniquePtr Array; + int32 ShadowOffset; + int32 ParentIndex; + GDK_PROPERTY(Property) * Property; }; // Utility class to manage mapped and unresolved references. @@ -87,8 +113,10 @@ struct FPendingSubobjectAttachment class FSpatialObjectRepState { public: - - FSpatialObjectRepState(FChannelObjectPair InThisObj) : ThisObj(InThisObj) {} + FSpatialObjectRepState(FChannelObjectPair InThisObj) + : ThisObj(InThisObj) + { + } void UpdateRefToRepStateMap(FObjectToRepStateMap& ReplicatorMap); bool MoveMappedObjectToUnmapped(const FUnrealObjectRef& ObjRef); @@ -97,35 +125,29 @@ class FSpatialObjectRepState const FChannelObjectPair& GetChannelObjectPair() const { return ThisObj; } FObjectReferencesMap ReferenceMap; - TSet< FUnrealObjectRef > ReferencedObj; - TSet< FUnrealObjectRef > UnresolvedRefs; + TSet ReferencedObj; + TSet UnresolvedRefs; private: bool MoveMappedObjectToUnmapped_r(const FUnrealObjectRef& ObjRef, FObjectReferencesMap& ObjectReferencesMap); - void GatherObjectRef(TSet& OutReferenced, TSet& OutUnresolved, const FObjectReferences& References) const; + void GatherObjectRef(TSet& OutReferenced, TSet& OutUnresolved, + const FObjectReferences& References) const; FChannelObjectPair ThisObj; }; - UCLASS(Transient) class SPATIALGDK_API USpatialActorChannel : public UActorChannel { GENERATED_BODY() public: - USpatialActorChannel(const FObjectInitializer & ObjectInitializer = FObjectInitializer::Get()); + USpatialActorChannel(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); // SpatialOS Entity ID. - FORCEINLINE Worker_EntityId GetEntityId() const - { - return EntityId; - } + FORCEINLINE Worker_EntityId GetEntityId() const { return EntityId; } - FORCEINLINE void SetEntityId(Worker_EntityId InEntityId) - { - EntityId = InEntityId; - } + FORCEINLINE void SetEntityId(Worker_EntityId InEntityId) { EntityId = InEntityId; } FORCEINLINE bool IsReadyForReplication() { @@ -138,7 +160,7 @@ 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. - if (!bCreatingNewEntity && !NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::POSITION_COMPONENT_ID)) + if (!bCreatingNewEntity && !NetDriver->HasServerAuthority(EntityId)) { return false; } @@ -159,31 +181,24 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel return false; } - return NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::GetClientAuthorityComponent(GetDefault()->UseRPCRingBuffer())); - } - - inline void SetClientAuthority(const bool IsAuth) - { - bIsAuthClient = IsAuth; + return NetDriver->HasClientAuthority(EntityId); } + 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. - inline bool IsAuthoritativeClient() const - { - return bIsAuthClient; - } + inline bool IsAuthoritativeClient() const { return bIsAuthClient; } // Sets the server and client authorities for this SpatialActorChannel based on the StaticComponentView inline void RefreshAuthority() { if (NetDriver->IsServer()) { - SetServerAuthority(NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::POSITION_COMPONENT_ID)); + SetServerAuthority(NetDriver->HasServerAuthority(EntityId)); } else { - SetClientAuthority(NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::GetClientAuthorityComponent(GetDefault()->UseRPCRingBuffer()))); + SetClientAuthority(NetDriver->HasClientAuthority(EntityId)); } } @@ -196,15 +211,9 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel bIsAuthServer = IsAuth; } - uint64 GetAuthorityReceivedTimestamp() const - { - return AuthorityReceivedTimestamp; - } + uint64 GetAuthorityReceivedTimestamp() const { return AuthorityReceivedTimestamp; } - inline bool IsAuthoritativeServer() const - { - return bIsAuthServer; - } + inline bool IsAuthoritativeServer() const { return bIsAuthServer; } FORCEINLINE FRepLayout& GetObjectRepLayout(UObject* Object) { @@ -219,7 +228,7 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel } // Begin UChannel interface - virtual void Init(UNetConnection * InConnection, int32 ChannelIndex, EChannelCreateFlags CreateFlag) override; + virtual void Init(UNetConnection* InConnection, int32 ChannelIndex, EChannelCreateFlags CreateFlag) override; virtual int64 Close(EChannelCloseReason Reason) override; // End UChannel interface @@ -239,18 +248,20 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel FRepChangeState CreateInitialRepChangeState(TWeakObjectPtr Object); FHandoverChangeState CreateInitialHandoverChangeState(const FClassInfo& ClassInfo); - // For an object that is replicated by this channel (i.e. this channel's actor or its component), find out whether a given handle is an array. + // For an object that is replicated by this channel (i.e. this channel's actor or its component), find out whether a given handle is an + // array. bool IsDynamicArrayHandle(UObject* Object, uint16 Handle); FObjectReplicator* PreReceiveSpatialUpdate(UObject* TargetObject); - void PostReceiveSpatialUpdate(UObject* TargetObject, const TArray& RepNotifies); + void PostReceiveSpatialUpdate(UObject* TargetObject, const TArray& RepNotifies, + const TMap& PropertySpanIds); void OnCreateEntityResponse(const Worker_CreateEntityResponseOp& Op); - void RemoveRepNotifiesWithUnresolvedObjs(TArray& RepNotifies, const FRepLayout& RepLayout, const FObjectReferencesMap& RefMap, UObject* Object); + void RemoveRepNotifiesWithUnresolvedObjs(TArray& RepNotifies, const FRepLayout& RepLayout, + const FObjectReferencesMap& RefMap, UObject* Object); void UpdateShadowData(); - void UpdateSpatialPositionWithFrequencyCheck(); void UpdateSpatialPosition(); void ServerProcessOwnershipChange(); @@ -259,13 +270,16 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel FORCEINLINE void MarkInterestDirty() { bInterestDirty = true; } FORCEINLINE bool GetInterestDirty() const { return bInterestDirty; } - 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); + // NB : ObjectPtr might be a dangling pointer. + void OnSubobjectDeleted(const FUnrealObjectRef& ObjectRef, UObject* ObjectPtr, const TWeakObjectPtr& ObjectWeakPtr); static void ResetShadowData(FRepLayout& RepLayout, FRepStateStaticBuffer& StaticBuffer, UObject* TargetObject); + void SetNeedOwnerInterestUpdate(bool bInNeedOwnerInterestUpdate) { bNeedOwnerInterestUpdate = bInNeedOwnerInterestUpdate; } + + bool NeedOwnerInterestUpdate() const { return bNeedOwnerInterestUpdate; } + protected: // Begin UChannel interface virtual bool CleanUp(const bool bForDestroy, EChannelCloseReason CloseReason) override; @@ -281,6 +295,10 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel void InitializeHandoverShadowData(TArray& ShadowData, UObject* Object); FHandoverChangeState GetHandoverChangeList(TArray& ShadowData, UObject* Object); + void UpdateVisibleComponent(AActor* Actor); + + bool SatisfiesSpatialPositionUpdateRequirements(); + 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; @@ -288,9 +306,11 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel // If this actor channel is responsible for creating a new entity, this will be set to true during initial replication. bool bCreatingNewEntity; - TSet> PendingDynamicSubobjects; + TSet, TWeakObjectPtrKeyFuncs, false>> PendingDynamicSubobjects; - TMap, FSpatialObjectRepState> ObjectReferenceMap; + TMap, FSpatialObjectRepState, FDefaultSetAllocator, + TWeakObjectPtrMapKeyFuncs, FSpatialObjectRepState, false>> + ObjectReferenceMap; private: Worker_EntityId EntityId; @@ -302,11 +322,6 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel // Used on the client to track gaining/losing ownership. bool bNetOwned; - // 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; @@ -334,7 +349,9 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel // the state of those properties at the last time we sent them, and is used to detect // when those properties change. TArray* ActorHandoverShadowData; - TMap, TSharedRef>> HandoverShadowDataMap; + TMap, TSharedRef>, FDefaultSetAllocator, + TWeakObjectPtrMapKeyFuncs, TSharedRef>, false>> + HandoverShadowDataMap; // Band-aid until we get Actor Sets. // Used on server-side workers only. @@ -346,4 +363,8 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel // before the actor holding the position for all the hierarchy, it can immediately attempt to migrate back. // Using this timestamp, we can back off attempting migrations for a while. uint64 AuthorityReceivedTimestamp; + + // In case the actor's owner did not have an entity ID when trying to set interest to it + // We set this flag in order to try to add interest as soon as possible. + bool bNeedOwnerInterestUpdate; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialFastArrayNetSerialize.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialFastArrayNetSerialize.h index 3d90fb6b29..64d34d0b6b 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialFastArrayNetSerialize.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialFastArrayNetSerialize.h @@ -15,20 +15,43 @@ namespace SpatialGDK { PRAGMA_DISABLE_DEPRECATION_WARNINGS // TODO: UNR-2371 - Remove when we update our usage of FNetDeltaSerializeInfo -class SpatialFastArrayNetSerializeCB : public INetSerializeCB + class SpatialFastArrayNetSerializeCB : public INetSerializeCB { public: SpatialFastArrayNetSerializeCB(USpatialNetDriver* InNetDriver) : NetDriver(InNetDriver) - { } - virtual void NetSerializeStruct(UScriptStruct* Struct, FBitArchive& Ar, UPackageMap* PackageMap, void* Data, bool& bHasUnmapped) override; - //TODO: UNR-2371 - Look at whether we need to implement these and implement 'NetSerializeStruct(FNetDeltaSerializeInfo& Params)'. - virtual void NetSerializeStruct(FNetDeltaSerializeInfo& Params) override { checkf(false, TEXT("The GDK does not support the new version of NetSerializeStruct yet.")); }; + { + } + virtual void NetSerializeStruct(UScriptStruct* Struct, FBitArchive& Ar, UPackageMap* PackageMap, void* Data, + bool& bHasUnmapped) override; + // TODO: UNR-2371 - Look at whether we need to implement these and implement 'NetSerializeStruct(FNetDeltaSerializeInfo& Params)'. + virtual void NetSerializeStruct(FNetDeltaSerializeInfo& Params) override + { + checkf(false, TEXT("The GDK does not support the new version of NetSerializeStruct yet.")); + }; - virtual void GatherGuidReferencesForFastArray(struct FFastArrayDeltaSerializeParams& Params) override { checkf(false, TEXT("GatherGuidReferencesForFastArray called - the GDK currently does not support delta serialization of structs within fast arrays.")); }; - virtual bool MoveGuidToUnmappedForFastArray(struct FFastArrayDeltaSerializeParams& Params) override { checkf(false, TEXT("MoveGuidToUnmappedForFastArray called - the GDK currently does not support delta serialization of structs within fast arrays.")); return false; }; - virtual void UpdateUnmappedGuidsForFastArray(struct FFastArrayDeltaSerializeParams& Params) override { checkf(false, TEXT("UpdateUnmappedGuidsForFastArray called - the GDK currently does not support delta serialization of structs within fast arrays.")); }; - virtual bool NetDeltaSerializeForFastArray(struct FFastArrayDeltaSerializeParams& Params) override { checkf(false, TEXT("NetDeltaSerializeForFastArray called - the GDK currently does not support delta serialization of structs within fast arrays.")); return false; }; + virtual void GatherGuidReferencesForFastArray(struct FFastArrayDeltaSerializeParams& Params) override + { + checkf(false, TEXT("GatherGuidReferencesForFastArray called - the GDK currently does not support delta serialization of structs " + "within fast arrays.")); + }; + virtual bool MoveGuidToUnmappedForFastArray(struct FFastArrayDeltaSerializeParams& Params) override + { + checkf(false, TEXT("MoveGuidToUnmappedForFastArray called - the GDK currently does not support delta serialization of structs " + "within fast arrays.")); + return false; + }; + virtual void UpdateUnmappedGuidsForFastArray(struct FFastArrayDeltaSerializeParams& Params) override + { + checkf(false, TEXT("UpdateUnmappedGuidsForFastArray called - the GDK currently does not support delta serialization of structs " + "within fast arrays.")); + }; + virtual bool NetDeltaSerializeForFastArray(struct FFastArrayDeltaSerializeParams& Params) override + { + checkf(false, TEXT("NetDeltaSerializeForFastArray called - the GDK currently does not support delta serialization of structs " + "within fast arrays.")); + return false; + }; private: USpatialNetDriver* NetDriver; @@ -36,13 +59,12 @@ class SpatialFastArrayNetSerializeCB : public INetSerializeCB struct FSpatialNetDeltaSerializeInfo : FNetDeltaSerializeInfo { - FSpatialNetDeltaSerializeInfo() - { - bIsSpatialType = true; - } + FSpatialNetDeltaSerializeInfo() { bIsSpatialType = true; } - static bool DeltaSerializeRead(USpatialNetDriver* NetDriver, FSpatialNetBitReader& Reader, UObject* Object, int32 ArrayIndex, GDK_PROPERTY(Property)* ParentProperty, UScriptStruct* NetDeltaStruct); - static bool DeltaSerializeWrite(USpatialNetDriver* NetDriver, FSpatialNetBitWriter& Writer, UObject* Object, int32 ArrayIndex, GDK_PROPERTY(Property)* ParentProperty, UScriptStruct* NetDeltaStruct); + static bool DeltaSerializeRead(USpatialNetDriver* NetDriver, FSpatialNetBitReader& Reader, UObject* Object, int32 ArrayIndex, + GDK_PROPERTY(Property) * ParentProperty, UScriptStruct* NetDeltaStruct); + static bool DeltaSerializeWrite(USpatialNetDriver* NetDriver, FSpatialNetBitWriter& Writer, UObject* Object, int32 ArrayIndex, + GDK_PROPERTY(Property) * ParentProperty, UScriptStruct* NetDeltaStruct); }; PRAGMA_ENABLE_DEPRECATION_WARNINGS // TODO: UNR-2371 - Remove when we update our usage of FNetDeltaSerializeInfo diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialGameInstance.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialGameInstance.h index 844b1beca7..9df34bdffc 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialGameInstance.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialGameInstance.h @@ -4,6 +4,7 @@ #include "CoreMinimal.h" #include "Engine/GameInstance.h" +#include "EngineClasses/SpatialNetDriver.h" #include "SpatialGameInstance.generated.h" @@ -17,6 +18,7 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGameInstance, Log, All); DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnConnectedEvent); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnConnectionFailedEvent, const FString&, Reason); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlayerSpawnFailedEvent, const FString&, Reason); +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnPrepareShutdownEvent); UCLASS(config = Engine) class SPATIALGDK_API USpatialGameInstance : public UGameInstance @@ -27,7 +29,8 @@ class SPATIALGDK_API USpatialGameInstance : public UGameInstance USpatialGameInstance(); #if WITH_EDITOR - virtual FGameInstancePIEResult StartPlayInEditorGameInstance(ULocalPlayer* LocalPlayer, const FGameInstancePIEParameters& Params) override; + virtual FGameInstancePIEResult StartPlayInEditorGameInstance(ULocalPlayer* LocalPlayer, + const FGameInstancePIEParameters& Params) override; #endif virtual void StartGameInstance() override; @@ -40,7 +43,8 @@ class SPATIALGDK_API USpatialGameInstance : public UGameInstance virtual void Init() override; //~ End UGameInstance Interface - // The SpatiaConnectionManager must always be owned by the SpatialGameInstance and so must be created here to prevent TrimMemory from deleting it during Browse. + // 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 SpatialConnectionManager disconnects us from SpatialOS. @@ -51,11 +55,14 @@ class SPATIALGDK_API USpatialGameInstance : public UGameInstance FORCEINLINE UGlobalStateManager* GetGlobalStateManager() { return GlobalStateManager; }; FORCEINLINE USpatialStaticComponentView* GetStaticComponentView() { return StaticComponentView; }; - void HandleOnConnected(); + void HandleOnConnected(const USpatialNetDriver& NetDriver); void HandleOnConnectionFailed(const FString& Reason); void HandleOnPlayerSpawnFailed(const FString& Reason); - void CleanupCachedLevelsAfterConnection(); + UFUNCTION() + void HandlePrepareShutdownWorkerFlagUpdated(const FString& FlagName, const FString& FlagValue); + + bool IsPreparingForShutdown() { return bPreparingForShutdown; } // Invoked when this worker has successfully connected to SpatialOS UPROPERTY(BlueprintAssignable) @@ -66,14 +73,15 @@ class SPATIALGDK_API USpatialGameInstance : public UGameInstance // Invoked when the player could not be spawned UPROPERTY(BlueprintAssignable) FOnPlayerSpawnFailedEvent OnSpatialPlayerSpawnFailed; + // Invoked when the deployment will be shut down soon, and the world should be brought to a consistent state for snapshotting. + UPROPERTY(BlueprintAssignable) + FOnPrepareShutdownEvent OnPrepareShutdown; void DisableShouldConnectUsingCommandLineArgs() { bShouldConnectUsingCommandLineArgs = false; } bool GetShouldConnectUsingCommandLineArgs() const { return bShouldConnectUsingCommandLineArgs; } void TryInjectSpatialLocatorIntoCommandLine(); - void CleanupLevelInitializedNetworkActors(ULevel* LoadedLevel); - protected: // Checks whether the current net driver is a USpatialNetDriver. // Can be used to decide whether to use Unreal networking or SpatialOS networking. @@ -109,8 +117,11 @@ class SPATIALGDK_API USpatialGameInstance : public UGameInstance bool HasPreviouslyConnectedToSpatial() const { return bHasPreviouslyConnectedToSpatial; } UFUNCTION() - void OnLevelInitializedNetworkActors(ULevel* LoadedLevel, UWorld* OwningWorld); + void OnLevelInitializedNetworkActors(ULevel* LoadedLevel, UWorld* OwningWorld) const; // Boolean for whether or not the Spatial connection is ready for normal operations. bool bIsSpatialNetDriverReady; + + // Whether shutdown preparation has been triggered. + bool bPreparingForShutdown; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialLoadBalanceEnforcer.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialLoadBalanceEnforcer.h index d6335b8186..27bf0a1b9b 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialLoadBalanceEnforcer.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialLoadBalanceEnforcer.h @@ -2,52 +2,73 @@ #pragma once -#include "Interop/SpatialStaticComponentView.h" +#include "Schema/AuthorityIntent.h" +#include "Schema/NetOwningClientWorker.h" +#include "Schema/StandardLibrary.h" #include "SpatialCommonTypes.h" -#include - -#include "CoreMinimal.h" +#include "SpatialView/EntityComponentTypes.h" DECLARE_LOG_CATEGORY_EXTERN(LogSpatialLoadBalanceEnforcer, Log, All) class SpatialVirtualWorkerTranslator; -class SPATIALGDK_API SpatialLoadBalanceEnforcer +namespace SpatialGDK { -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); +class FSubView; - bool HandlesComponent(Worker_ComponentId ComponentId) const; +struct LBComponents +{ + AuthorityDelegation Delegation; + AuthorityIntent Intent; + NetOwningClientWorker OwningClientWorker; +}; - 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); +struct AuthorityStateChange +{ + Worker_EntityId EntityId = 0; + TArray ComponentIds; + VirtualWorkerId TargetVirtualWorker; +}; - void MaybeQueueAclAssignmentRequest(const Worker_EntityId EntityId); - // Visible for testing - bool AclAssignmentRequestIsQueued(const Worker_EntityId EntityId) const; +// The load balance enforcer system running on a worker is responsible for updating the authority delegation component +// to the workers indicated in the Authority Intent and Net Owning Client Worker components. +// +// The LB components are: +// - Authority Intent (for authority changes) +// - Net Owning Client Worker (for client authority changes) +// +// The load balance enforcer's view of the world consists of all entities where the authority delegation component +// is delegated to the worker. The passed subview enforces that any entity seen by the enforcer will have all relevant +// LB components present. Each tick, the enforcer reads the deltas for these entities, and if there any changes for any +// of the LB components calculates whether or not an delegation update needs to be sent, and if so, constructs one and +// sends it on to Spatial. If the same worker is authoritative over the authority intent component, a request to construct +// an delegation update will be short circuited locally. +class SpatialLoadBalanceEnforcer +{ +public: + SpatialLoadBalanceEnforcer(const PhysicalWorkerName& InWorkerId, const FSubView& InSubView, + const SpatialVirtualWorkerTranslator* InVirtualWorkerTranslator, + TUniqueFunction InUpdateSender); - TArray ProcessQueuedAclAssignmentRequests(); + void Advance(); + void ShortCircuitMaybeRefreshAuthorityDelegation(const Worker_EntityId EntityId); private: - void QueueAclAssignmentRequest(const Worker_EntityId EntityId); - bool CanEnforce(Worker_EntityId EntityId) const; + void PopulateDataStore(const Worker_EntityId EntityId); + bool ApplyComponentUpdate(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId, Schema_ComponentUpdate* Update); + bool ApplyComponentRefresh(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId, Schema_ComponentData* Data); + + void RefreshAuthority(const Worker_EntityId EntityId); + Worker_ComponentUpdate CreateAuthorityDelegationUpdate(const Worker_EntityId EntityId); const PhysicalWorkerName WorkerId; - TWeakObjectPtr StaticComponentView; + const FSubView* SubView; const SpatialVirtualWorkerTranslator* VirtualWorkerTranslator; - TArray AclWriteAuthAssignmentRequests; + TArray PendingEntityAuthorityChanges; + TMap DataStore; + TUniqueFunction UpdateSender; }; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetBitReader.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetBitReader.h index e2e7569d41..81eb925fdd 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetBitReader.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetBitReader.h @@ -14,18 +14,21 @@ class USpatialPackageMapClient; class SPATIALGDK_API FSpatialNetBitReader : public FNetBitReader { public: - FSpatialNetBitReader(USpatialPackageMapClient* InPackageMap, uint8* Source, int64 CountBits, TSet& InDynamicRefs, TSet& InUnresolvedRefs); + FSpatialNetBitReader(USpatialPackageMapClient* InPackageMap, uint8* Source, int64 CountBits, TSet& InDynamicRefs, + TSet& InUnresolvedRefs); + + ~FSpatialNetBitReader(); using FArchive::operator<<; // For visibility of the overloads we don't override virtual FArchive& operator<<(UObject*& Value) override; - virtual FArchive& operator<<(struct FWeakObjectPtr& Value) override; + virtual FArchive& operator<<(FWeakObjectPtr& Value) override; - UObject* ReadObject(bool& bUnresolved); + static UObject* ReadObject(FArchive& Archive, USpatialPackageMapClient* PackageMap, bool& bUnresolved); protected: - void DeserializeObjectRef(FUnrealObjectRef& ObjectRef); + static void DeserializeObjectRef(FArchive& Archive, FUnrealObjectRef& ObjectRef); TSet& DynamicRefs; TSet& UnresolvedRefs; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetBitWriter.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetBitWriter.h index 42fb928091..244087240f 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetBitWriter.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetBitWriter.h @@ -3,8 +3,8 @@ #pragma once #include "CoreMinimal.h" -#include "UObject/CoreNet.h" #include "Schema/UnrealObjectRef.h" +#include "UObject/CoreNet.h" DECLARE_LOG_CATEGORY_EXTERN(LogSpatialNetSerialize, All, All); @@ -21,6 +21,8 @@ class SPATIALGDK_API FSpatialNetBitWriter : public FNetBitWriter virtual FArchive& operator<<(struct FWeakObjectPtr& Value) override; + static void WriteObject(FArchive& Archive, USpatialPackageMapClient* PackageMap, UObject* Object); + protected: - void SerializeObjectRef(FUnrealObjectRef& ObjectRef); + static void SerializeObjectRef(FArchive& Archive, FUnrealObjectRef& ObjectRef); }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetConnection.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetConnection.h index 5af0b43459..02575eee48 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetConnection.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetConnection.h @@ -5,8 +5,8 @@ #include "Schema/Interest.h" #include "CoreMinimal.h" -#include "Misc/Optional.h" #include "IpConnection.h" +#include "Misc/Optional.h" #include "Runtime/Launch/Resources/Version.h" #include @@ -25,7 +25,8 @@ class SPATIALGDK_API USpatialNetConnection : public UIpConnection // Begin NetConnection Interface virtual void BeginDestroy() override; - virtual void InitBase(UNetDriver* InDriver, class FSocket* InSocket, const FURL& InURL, EConnectionState InState, int32 InMaxPacket = 0, int32 InPacketOverhead = 0) override; + virtual void InitBase(UNetDriver* InDriver, class FSocket* InSocket, const FURL& InURL, EConnectionState InState, int32 InMaxPacket = 0, + int32 InPacketOverhead = 0) override; virtual void LowLevelSend(void* Data, int32 CountBits, FOutPacketTraits& Traits) override; virtual bool ClientHasInitializedLevelFor(const AActor* TestActor) const override; virtual int32 IsNetReady(bool Saturate) override; @@ -59,11 +60,16 @@ class SPATIALGDK_API USpatialNetConnection : public UIpConnection void ClientNotifyClientHasQuit(); + UFUNCTION() + void OnControllerDestroyed(AActor* DestroyedActor); + UPROPERTY() bool bReliableSpatialConnection; - // Only used on the server for client connections. - FString ConnectionOwningWorkerId; + // Store the client system worker entity ID corresponding to this net connection. + // When the corresponding PlayerController is successfully spawned, we will claim + // the PlayerController as a partition entity for the client worker. + Worker_EntityId ConnectionClientWorkerSystemEntityId; class FTimerManager* TimerManager; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h index 2f96e2683d..db0cf16f9a 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h @@ -6,12 +6,13 @@ #include "EngineClasses/SpatialVirtualWorkerTranslationManager.h" #include "EngineClasses/SpatialVirtualWorkerTranslator.h" #include "Interop/Connection/ConnectionConfig.h" +#include "Interop/RPCs/SpatialRPCService.h" #include "Interop/SpatialDispatcher.h" #include "Interop/SpatialOutputDevice.h" -#include "Interop/SpatialRPCService.h" #include "Interop/SpatialSnapshotManager.h" #include "SpatialView/OpList/OpList.h" #include "Utils/InterestFactory.h" +#include "Utils/SpatialBasicAwaiter.h" #include "LoadBalancing/AbstractLockingPolicy.h" #include "SpatialConstants.h" @@ -19,6 +20,7 @@ #include "CoreMinimal.h" #include "GameFramework/OnlineReplStructs.h" +#include "Interop/WellKnownEntitySystem.h" #include "IpNetDriver.h" #include "TimerManager.h" @@ -36,6 +38,7 @@ class USpatialConnectionManager; class USpatialGameInstance; class USpatialMetrics; class USpatialNetConnection; +class USpatialNetDriverDebugContext; class USpatialPackageMapClient; class USpatialPlayerSpawner; class USpatialReceiver; @@ -46,9 +49,22 @@ class USpatialWorkerFlags; DECLARE_LOG_CATEGORY_EXTERN(LogSpatialOSNetDriver, Log, All); -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,); +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, ); + +enum class EActorMigrationResult : uint8 +{ + Success, + NotAuthoritative, + NotReady, + PendingKill, + NotInitialized, + Streaming, + NetDormant, + NoSpatialClassFlags, + DormantOnConnection +}; UCLASS() class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver @@ -56,7 +72,6 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver GENERATED_BODY() public: - USpatialNetDriver(const FObjectInitializer& ObjectInitializer); // Begin UObject Interface @@ -69,10 +84,12 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver // End FExec Interface // Begin UNetDriver interface. - virtual bool InitBase(bool bInitAsClient, FNetworkNotify* InNotify, const FURL& URL, bool bReuseAddressAndPort, FString& Error) override; + virtual bool InitBase(bool bInitAsClient, FNetworkNotify* InNotify, const FURL& URL, bool bReuseAddressAndPort, + FString& Error) override; virtual int32 ServerReplicateActors(float DeltaSeconds) override; virtual void TickDispatch(float DeltaTime) override; - virtual void ProcessRemoteFunction(class AActor* Actor, class UFunction* Function, void* Parameters, struct FOutParmRec* OutParms, struct FFrame* NotStack, class UObject* SubObject = NULL ) override; + virtual void ProcessRemoteFunction(class AActor* Actor, class UFunction* Function, void* Parameters, struct FOutParmRec* OutParms, + struct FFrame* NotStack, class UObject* SubObject = NULL) override; virtual void TickFlush(float DeltaTime) override; virtual bool IsLevelInitializedForActor(const AActor* InActor, const UNetConnection* InConnection) const override; virtual void NotifyActorDestroyed(AActor* Actor, bool IsSeamlessTravel = false) override; @@ -101,8 +118,11 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver 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& ConnectionOwningWorkerId); + void AcceptNewPlayer(const FURL& InUrl, const FUniqueNetIdRepl& UniqueId, const FName& OnlinePlatformName, + const Worker_EntityId& ClientSystemEntityId); + void PostSpawnPlayerController(APlayerController* PlayerController); + + void DisconnectPlayer(Worker_EntityId ClientEntityId); void AddActorChannel(Worker_EntityId EntityId, USpatialActorChannel* Channel); void RemoveActorChannel(Worker_EntityId EntityId, USpatialActorChannel& Channel); @@ -113,6 +133,8 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver void RefreshActorDormancy(AActor* Actor, bool bMakeDormant); + void RefreshActorVisibility(AActor* Actor, bool bMakeVisible); + void AddPendingDormantChannel(USpatialActorChannel* Channel); void RemovePendingDormantChannel(USpatialActorChannel* Channel); void RegisterDormantEntityId(Worker_EntityId EntityId); @@ -123,8 +145,13 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver void SetSpatialMetricsDisplay(ASpatialMetricsDisplay* InSpatialMetricsDisplay); void SetSpatialDebugger(ASpatialDebugger* InSpatialDebugger); - TWeakObjectPtr FindClientConnectionFromWorkerId(const FString& WorkerId); + void RegisterClientConnection(const Worker_EntityId WorkerEntityId, USpatialNetConnection* ClientConnection); + TWeakObjectPtr FindClientConnectionFromWorkerEntityId(const Worker_EntityId InWorkerEntityId); void CleanUpClientConnection(USpatialNetConnection* ClientConnection); + void CleanUpServerConnectionForPC(APlayerController* PC); + + bool HasServerAuthority(Worker_EntityId EntityId) const; + bool HasClientAuthority(Worker_EntityId EntityId) const; UPROPERTY() USpatialWorkerConnection* Connection; @@ -150,22 +177,28 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver ASpatialMetricsDisplay* SpatialMetricsDisplay; UPROPERTY() ASpatialDebugger* SpatialDebugger; + // Fires on a client once is has received the spatial debugger through replication. Does not fire on servers. + UPROPERTY() + USpatialBasicAwaiter* SpatialDebuggerReady; UPROPERTY() UAbstractLBStrategy* LoadBalanceStrategy; UPROPERTY() UAbstractLockingPolicy* LockingPolicy; UPROPERTY() USpatialWorkerFlags* SpatialWorkerFlags; + UPROPERTY() + USpatialNetDriverDebugContext* DebugCtx; + TUniquePtr LoadBalanceEnforcer; TUniquePtr InterestFactory; - TUniquePtr LoadBalanceEnforcer; TUniquePtr VirtualWorkerTranslator; + TUniquePtr WellKnownEntitySystem; + 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; } @@ -194,8 +227,12 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver float GetElapsedTime() { return Time; } #endif -private: + // Check if we have already logged this actor / migration failure, if not update the log record + bool IsLogged(Worker_EntityId ActorEntityId, EActorMigrationResult ActorMigrationFailure); + + virtual int64 GetClientID() const override; +private: TUniquePtr Dispatcher; TUniquePtr SnapshotManager; TUniquePtr SpatialOutputDevice; @@ -203,11 +240,10 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver TUniquePtr RPCService; TMap EntityToActorChannel; - TArray QueuedStartupOpLists; TSet DormantEntities; - TSet> PendingDormantChannels; + TSet, TWeakObjectPtrKeyFuncs, false>> PendingDormantChannels; - TMap> WorkerConnections; + TMap> WorkerConnections; FTimerManager TimerManager; @@ -237,31 +273,32 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver void QueryGSMToLoadMap(); - void HandleStartupOpQueueing(TArray InOpLists); - bool FindAndDispatchStartupOpsServer(const TArray& InOpLists); - bool FindAndDispatchStartupOpsClient(const TArray& InOpLists); - void SelectiveProcessOps(TArray FoundOps); + void TryFinishStartup(); UFUNCTION() void OnMapLoaded(UWorld* LoadedWorld); - UFUNCTION() - void OnLevelAddedToWorld(ULevel* LoadedLevel, UWorld* OwningWorld); - - void OnActorSpawned(AActor* Actor); + void OnActorSpawned(AActor* Actor) const; static void SpatialProcessServerTravel(const FString& URL, bool bAbsolute, AGameModeBase* GameMode); #if WITH_SERVER_CODE // SpatialGDK: These functions all exist in UNetDriver, but we need to modify/simplify them in certain ways. - // Could have marked them virtual in base class but that's a pointless source change as these functions are not meant to be called from anywhere except USpatialNetDriver::ServerReplicateActors. + // Could have marked them virtual in base class but that's a pointless source change as these functions are not meant to be called from + // anywhere except USpatialNetDriver::ServerReplicateActors. int32 ServerReplicateActors_PrepConnections(const float DeltaSeconds); - int32 ServerReplicateActors_PrioritizeActors(UNetConnection* Connection, const TArray& ConnectionViewers, FSpatialLoadBalancingHandler&, const TArray ConsiderList, const bool bCPUSaturated, FActorPriority*& OutPriorityList, FActorPriority**& OutPriorityActors); - void ServerReplicateActors_ProcessPrioritizedActors(UNetConnection* Connection, const TArray& ConnectionViewers, FSpatialLoadBalancingHandler&, FActorPriority** PriorityActors, const int32 FinalSortedCount, int32& OutUpdated); + int32 ServerReplicateActors_PrioritizeActors(UNetConnection* Connection, const TArray& ConnectionViewers, + FSpatialLoadBalancingHandler&, const TArray ConsiderList, + const bool bCPUSaturated, FActorPriority*& OutPriorityList, + FActorPriority**& OutPriorityActors); + void ServerReplicateActors_ProcessPrioritizedActors(UNetConnection* Connection, const TArray& ConnectionViewers, + FSpatialLoadBalancingHandler&, FActorPriority** PriorityActors, + const int32 FinalSortedCount, int32& OutUpdated); #endif void ProcessRPC(AActor* Actor, UObject* SubObject, UFunction* Function, void* Parameters); - bool CreateSpatialNetConnection(const FURL& InUrl, const FUniqueNetIdRepl& UniqueId, const FName& OnlinePlatformName, USpatialNetConnection** OutConn); + bool CreateSpatialNetConnection(const FURL& InUrl, const FUniqueNetIdRepl& UniqueId, const FName& OnlinePlatformName, + const Worker_EntityId& ClientSystemEntityId, USpatialNetConnection** OutConn); void ProcessPendingDormancy(); void PollPendingLoads(); @@ -271,8 +308,6 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver // in the correct order, if needed. int NextRPCIndex; - float TimeWhenPositionLastUpdated; - // Counter for giving each connected client a unique IP address to satisfy Unreal's requirement of // each client having a unique IP address in the UNetDriver::MappedClientConnections map. // The GDK does not use this address for any networked purpose, only bookkeeping. @@ -296,4 +331,16 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver // 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(); + + void ProcessOwnershipChanges(); + + // Has a certain interval (in seconds) been passed since the previous timestamp + bool HasTimedOut(const float Interval, uint64& TimeStamp); + + TSet OwnershipChangedEntities; + uint64 StartupTimestamp; + FString StartupClientDebugString; + + TMultiMap MigrationFailureLogStore; + uint64 MigrationTimestamp; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriverDebugContext.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriverDebugContext.h new file mode 100644 index 0000000000..a233fb02a0 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriverDebugContext.h @@ -0,0 +1,117 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "Schema/DebugComponent.h" +#include "Schema/Interest.h" +#include "SpatialCommonTypes.h" + +#include "SpatialNetDriverDebugContext.generated.h" + +class UDebugLBStrategy; +class USpatialNetDriver; + +/* + * Implement the debug layer from SpatialFunctionalTest, which is enabled by a map flag. + * The goal is to be able to arbitrarily gain interest and manipulate authority of any actors. + * This is done by adding an extra debug component to all actors, which will contain tags. + * All server workers will have interest in all entities with this component, allowing them to inspect tags, and check out extra actors if + * they need to. Arbitrary authority delegation is achieved by wrapping the NetDriver's load balancing strategy into a debug one, which will + * inspect tags first. One caveat that all servers must declare the same delegation at the same time (this requirement could be lifted if + * this object was made to behave like a singleton). + */ + +UCLASS() +class SPATIALGDK_API USpatialNetDriverDebugContext : public UObject +{ + GENERATED_BODY() +public: + static void EnableDebugSpatialGDK(USpatialNetDriver* NetDriver); + static void DisableDebugSpatialGDK(USpatialNetDriver* NetDriver); + + // ------ Startup / Shutdown + void Init(USpatialNetDriver* NetDriver); + void Cleanup(); + void Reset(); + + // ------ Debug Interface + + // Manage tags on the debug component. + // Add/remove extra interest if tags match extra interest queries + void AddActorTag(AActor* Actor, FName Tag); + void RemoveActorTag(AActor* Actor, FName Tag); + + // Manage extra interest. + // This pushes tags to SemanticInterest, which will be compared to the Actor debug components + // to see if we should add extra interest queries to the worker's interest. + void AddInterestOnTag(FName Tag); + void RemoveInterestOnTag(FName Tag); + + // Pin the given Actor to the current worker by setting the local workerId on its debug component + void KeepActorOnLocalWorker(AActor* Actor); + + // Manage Actor authority delegation. + // This pushes an entry to SemanticDelegations, which will be examined by the debug load balancing strategy. + // For this to work properly, all workers should declare the same set of delegations. + void DelegateTagToWorker(FName Tag, uint32 WorkerId); + void RemoveTagDelegation(FName Tag); + + // Used by the debug worker strategy to retrieve + TOptional GetActorHierarchyExplicitDelegation(const AActor* Actor); + + // ----- Utility + + bool IsActorReady(AActor* Actor); + + // ----- NetDriver Integration + + // This will be called from SpatialNetDriver::ServerReplicateActor + // It will create debug components or update them. It also updates the worker's interest query if needed. + void TickServer(); + + // Called from SpatialReveiver when the corresponding Ops are encountered. + void OnDebugComponentUpdateReceived(Worker_EntityId); + void OnDebugComponentAuthLost(Worker_EntityId EntityId); + + void ClearNeedEntityInterestUpdate() { bNeedToUpdateInterest = false; } + + SpatialGDK::QueryConstraint ComputeAdditionalEntityQueryConstraint() const; + + UPROPERTY() + UDebugLBStrategy* DebugStrategy = nullptr; + +protected: + struct DebugComponentView + { + SpatialGDK::DebugComponent Component; + Worker_EntityId Entity = SpatialConstants::INVALID_ENTITY_ID; + bool bAdded = false; + bool bDirty = false; + }; + + DebugComponentView& GetDebugComponentView(AActor* Actor); + + TOptional GetActorExplicitDelegation(const AActor* Actor); + TOptional GetActorHierarchyExplicitDelegation_Traverse(const AActor* Actor); + + void AddEntityToWatch(Worker_EntityId); + void RemoveEntityToWatch(Worker_EntityId); + + bool NeedEntityInterestUpdate() { return bNeedToUpdateInterest; } + + USpatialNetDriver* NetDriver; + + // Collection of actor tag delegations. + TMap SemanticDelegations; + + // Collection of actor tags we should get interest over. + TSet SemanticInterest; + + // Debug info for actors. Only keeps entries for Actors we have authority over. + TMap ActorDebugInfo; + + // Contains a cache of entities computed from the semantic interest. + TSet CachedInterestSet; + bool bNeedToUpdateInterest = false; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h index d0d7379f83..bf8544093e 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h @@ -22,7 +22,7 @@ class FTimerManager; UCLASS() class SPATIALGDK_API USpatialPackageMapClient : public UPackageMapClient { - GENERATED_BODY() + GENERATED_BODY() public: void Init(USpatialNetDriver* NetDriver, FTimerManager* TimerManager); @@ -43,19 +43,23 @@ class SPATIALGDK_API USpatialPackageMapClient : public UPackageMapClient void UnregisterActorObjectRefOnly(const FUnrealObjectRef& ObjectRef); FNetworkGUID ResolveStablyNamedObject(UObject* Object); - + FUnrealObjectRef GetUnrealObjectRefFromNetGUID(const FNetworkGUID& NetGUID) const; FNetworkGUID GetNetGUIDFromUnrealObjectRef(const FUnrealObjectRef& ObjectRef) const; FNetworkGUID GetNetGUIDFromEntityId(const Worker_EntityId& EntityId) const; TWeakObjectPtr GetObjectFromUnrealObjectRef(const FUnrealObjectRef& ObjectRef); - TWeakObjectPtr GetObjectFromEntityId(const Worker_EntityId& EntityId); + TWeakObjectPtr GetObjectFromEntityId(const Worker_EntityId EntityId); FUnrealObjectRef GetUnrealObjectRefFromObject(const UObject* Object); Worker_EntityId GetEntityIdFromObject(const UObject* Object); AActor* GetUniqueActorInstanceByClassRef(const FUnrealObjectRef& ClassRef); AActor* GetUniqueActorInstanceByClass(UClass* Class) const; + FNetworkGUID* GetRemovedDynamicSubobjectNetGUID(const FUnrealObjectRef& ObjectRef); + void AddRemovedDynamicSubobjectObjectRef(const FUnrealObjectRef& ObjectRef, const FNetworkGUID& NetGUID); + void ClearRemovedDynamicSubobjectObjectRefs(const Worker_EntityId& InEntityId); + // Expose FNetGUIDCache::CanClientLoadObject so we can include this info with UnrealObjectRef. bool CanClientLoadObject(UObject* Object); @@ -63,13 +67,15 @@ class SPATIALGDK_API USpatialPackageMapClient : public UPackageMapClient bool IsEntityPoolReady() const; FEntityPoolReadyEvent& GetEntityPoolReadyDelegate(); - virtual bool SerializeObject(FArchive& Ar, UClass* InClass, UObject*& Obj, FNetworkGUID *OutNetGUID = NULL) override; + virtual bool SerializeObject(FArchive& Ar, UClass* InClass, UObject*& Obj, FNetworkGUID* OutNetGUID = NULL) override; const FClassInfo* TryResolveNewDynamicSubobjectAndGetClassInfo(UObject* Object); // Pending object references, being asynchronously loaded. TSet PendingReferences; + Worker_EntityId AllocateNewEntityId() const; + private: UPROPERTY() UEntityPool* EntityPool; @@ -78,13 +84,14 @@ class SPATIALGDK_API USpatialPackageMapClient : public UPackageMapClient // Entities that have been assigned on this server and not created yet TSet PendingCreationEntityIds; + TMap RemovedDynamicSubobjectObjectRefs; }; class SPATIALGDK_API FSpatialNetGUIDCache : public FNetGUIDCache { public: FSpatialNetGUIDCache(class USpatialNetDriver* InDriver); - + FNetworkGUID AssignNewEntityActorNetGUID(AActor* Actor, Worker_EntityId EntityId); void AssignNewSubobjectNetGUID(UObject* Subobject, const FUnrealObjectRef& SubobjectRef); @@ -92,7 +99,7 @@ class SPATIALGDK_API FSpatialNetGUIDCache : public FNetGUIDCache void RemoveSubobjectNetGUID(const FUnrealObjectRef& SubobjectRef); FNetworkGUID AssignNewStablyNamedObjectNetGUID(UObject* Object); - + FNetworkGUID GetNetGUIDFromUnrealObjectRef(const FUnrealObjectRef& ObjectRef); FUnrealObjectRef GetUnrealObjectRefFromNetGUID(const FNetworkGUID& NetGUID) const; FNetworkGUID GetNetGUIDFromEntityId(Worker_EntityId EntityId) const; @@ -108,11 +115,10 @@ class SPATIALGDK_API FSpatialNetGUIDCache : public FNetGUIDCache FNetworkGUID GetOrAssignNetGUID_SpatialGDK(UObject* Object); void RegisterObjectRef(FNetworkGUID NetGUID, const FUnrealObjectRef& ObjectRef); - + FNetworkGUID RegisterNetGUIDFromPathForStaticObject(const FString& PathName, const FNetworkGUID& OuterGUID, bool bNoLoadOnClient); FNetworkGUID GenerateNewNetGUID(const int32 IsStatic); TMap NetGUIDToUnrealObjectRef; TMap UnrealObjectRefToNetGUID; }; - diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPendingNetGame.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPendingNetGame.h index 137b78a0b1..182911b291 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPendingNetGame.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPendingNetGame.h @@ -8,15 +8,14 @@ #include "SpatialPendingNetGame.generated.h" -//UPendingNetGame needs to have its dllexport defined: "class ENGINE_API UPendingNetGame". This can count as a bug, we can submit a PR. +// UPendingNetGame needs to have its dllexport defined: "class ENGINE_API UPendingNetGame". This can count as a bug, we can submit a PR. UCLASS(transient) class USpatialPendingNetGame : public UPendingNetGame { GENERATED_UCLASS_BODY() - - //Made virtual in PendingNetGame.h + + // Made virtual in PendingNetGame.h virtual void InitNetDriver() override; virtual void SendJoin() override; - }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialReplicationGraph.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialReplicationGraph.h index 86bb6fb63e..cc46be2cc8 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialReplicationGraph.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialReplicationGraph.h @@ -2,6 +2,8 @@ #pragma once +#include "Utils/SpatialLoadBalancingHandler.h" + #include "ReplicationGraph.h" #include "SpatialReplicationGraph.generated.h" @@ -15,9 +17,19 @@ class SPATIALGDK_API USpatialReplicationGraph : public UReplicationGraph GENERATED_BODY() public: + virtual void InitForNetDriver(UNetDriver*) override; + virtual void OnOwnerUpdated(AActor* Actor, AActor* OldOwner); + + FGlobalActorReplicationInfoMap& GetGlobalActorReplicationInfoMap() { return GlobalActorReplicationInfoMap; } +protected: //~ Begin UReplicationGraph Interface virtual UActorChannel* GetOrCreateSpatialActorChannel(UObject* TargetObject) override; + + virtual void PreReplicateActors(UNetReplicationGraphConnection* ConnectionManager) override; + + virtual void PostReplicateActors(UNetReplicationGraphConnection* ConnectionManager) override; //~ End UReplicationGraph Interface + TUniquePtr LoadBalancingHandler; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslationManager.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslationManager.h index 4a75a4f192..fd7cd5a757 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslationManager.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslationManager.h @@ -2,18 +2,17 @@ #pragma once -#include "Containers/Queue.h" +#include "EngineClasses/SpatialVirtualWorkerTranslator.h" #include "SpatialCommonTypes.h" #include "SpatialConstants.h" -#include -#include - #include "CoreMinimal.h" +#include +#include + DECLARE_LOG_CATEGORY_EXTERN(LogSpatialVirtualWorkerTranslationManager, Log, All) -class SpatialVirtualWorkerTranslator; class SpatialOSDispatcherInterface; class SpatialOSWorkerInterface; @@ -34,24 +33,35 @@ class SpatialOSWorkerInterface; class SPATIALGDK_API SpatialVirtualWorkerTranslationManager { public: - SpatialVirtualWorkerTranslationManager(SpatialOSDispatcherInterface* InReceiver, - SpatialOSWorkerInterface* InConnection, - SpatialVirtualWorkerTranslator* InTranslator); + struct PartitionInfo + { + Worker_EntityId PartitionEntityId; + VirtualWorkerId VirtualWorker; + Worker_EntityId SimulatingWorkerSystemEntityId; + }; + + SpatialVirtualWorkerTranslationManager(SpatialOSDispatcherInterface* InReceiver, SpatialOSWorkerInterface* InConnection, + SpatialVirtualWorkerTranslator* InTranslator); void SetNumberOfVirtualWorkers(const uint32 NumVirtualWorkers); // The translation manager only cares about changes to the authority of the translation mapping. - void AuthorityChanged(const Worker_AuthorityChangeOp& AuthChangeOp); + void AuthorityChanged(const Worker_ComponentSetAuthorityChangeOp& AuthChangeOp); + + void SpawnPartitionEntitiesForVirtualWorkerIds(); + void ReclaimPartitionEntities(); + const TArray& GetAllPartitions() const { return Partitions; }; + + SpatialVirtualWorkerTranslator* Translator; private: SpatialOSDispatcherInterface* Receiver; SpatialOSWorkerInterface* Connection; - SpatialVirtualWorkerTranslator* Translator; - - TMap> VirtualToPhysicalWorkerMapping; - TMap PhysicalToVirtualWorkerMapping; - TQueue UnassignedVirtualWorkers; + TArray VirtualWorkersToAssign; + TArray Partitions; + TMap VirtualToPhysicalWorkerMapping; + uint32 NumVirtualWorkers; bool bWorkerEntityQueryInFlight; @@ -62,9 +72,13 @@ class SPATIALGDK_API SpatialVirtualWorkerTranslationManager // based on the response. void QueryForServerWorkerEntities(); void ServerWorkerEntityQueryDelegate(const Worker_EntityQueryResponseOp& Op); - void ConstructVirtualWorkerMappingFromQueryResponse(const Worker_EntityQueryResponseOp& Op); + bool AllServerWorkersAreReady(const Worker_EntityQueryResponseOp& Op, uint32& ServerWorkersNotReady); + void AssignPartitionsToEachServerWorkerFromQueryResponse(const Worker_EntityQueryResponseOp& Op); void SendVirtualWorkerMappingUpdate() const; - void AssignWorker(const PhysicalWorkerName& WorkerId, const Worker_EntityId& ServerWorkerEntityId); -}; + void AssignPartitionToWorker(const PhysicalWorkerName& WorkerName, const Worker_EntityId& ServerWorkerEntityId, + const Worker_EntityId& SystemEntityId, const PartitionInfo& Partition); + void SpawnPartitionEntity(Worker_EntityId PartitionEntityId, VirtualWorkerId VirtualWorker); + void OnPartitionEntityCreation(Worker_EntityId PartitionEntityId, VirtualWorkerId VirtualWorker); +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslator.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslator.h index b07f1f2d4d..f9430d48bb 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslator.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslator.h @@ -2,15 +2,16 @@ #pragma once +#include "Interop/SpatialSender.h" #include "SpatialCommonTypes.h" #include "SpatialConstants.h" -#include -#include - #include "Containers/Queue.h" #include "CoreMinimal.h" +#include +#include + DECLARE_LOG_CATEGORY_EXTERN(LogSpatialVirtualWorkerTranslator, Log, All) class UAbstractLBStrategy; @@ -18,9 +19,16 @@ class UAbstractLBStrategy; class SPATIALGDK_API SpatialVirtualWorkerTranslator { public: + struct WorkerInformation + { + PhysicalWorkerName WorkerName; + Worker_EntityId ServerWorkerEntityId; + Worker_PartitionId PartitionEntityId; + }; + SpatialVirtualWorkerTranslator() = delete; - SpatialVirtualWorkerTranslator(UAbstractLBStrategy* InLoadBalanceStrategy, - PhysicalWorkerName InPhysicalWorkerName); + SpatialVirtualWorkerTranslator(UAbstractLBStrategy* InLoadBalanceStrategy, USpatialNetDriver* InNetDriver, + PhysicalWorkerName InLocalPhysicalWorkerName); // 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. @@ -28,31 +36,37 @@ class SPATIALGDK_API SpatialVirtualWorkerTranslator VirtualWorkerId GetLocalVirtualWorkerId() const { return LocalVirtualWorkerId; } PhysicalWorkerName GetLocalPhysicalWorkerName() const { return LocalPhysicalWorkerName; } + Worker_PartitionId GetClaimedPartitionId() const { return LocalPartitionId; } + int32 GetMappingCount() const { return VirtualToPhysicalWorkerMapping.Num(); } // 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 PhysicalWorkerName* GetPhysicalWorkerForVirtualWorker(VirtualWorkerId Id) const; + Worker_PartitionId GetPartitionEntityForVirtualWorker(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); -private: + USpatialNetDriver* NetDriver; + TWeakObjectPtr LoadBalanceStrategy; - TMap> VirtualToPhysicalWorkerMapping; +private: + TMap VirtualToPhysicalWorkerMapping; bool bIsReady; // The WorkerId of this worker, for logging purposes. PhysicalWorkerName LocalPhysicalWorkerName; VirtualWorkerId LocalVirtualWorkerId; + Worker_PartitionId LocalPartitionId; // Serialization and deserialization of the mapping. void ApplyMappingFromSchema(Schema_Object* Object); - bool IsValidMapping(Schema_Object* Object) const; - void UpdateMapping(VirtualWorkerId Id, PhysicalWorkerName Name, Worker_EntityId ServerWorkerEntityId); + void UpdateMapping(VirtualWorkerId Id, PhysicalWorkerName WorkerName, Worker_PartitionId PartitionEntityId, + Worker_EntityId ServerWorkerEntityId); }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialWorldSettings.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialWorldSettings.h index 0cd0955805..f67d759670 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialWorldSettings.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialWorldSettings.h @@ -3,33 +3,86 @@ #pragma once #include "LoadBalancing/SpatialMultiWorkerSettings.h" -#include "SpatialGDKSettings.h" -#include "Utils/LayerInfo.h" -#include "Utils/SpatialStatics.h" #include "GameFramework/WorldSettings.h" #include "Templates/SubclassOf.h" #include "SpatialWorldSettings.generated.h" +/** + * MapTestingMode allows Maps using ASpatialWorldSettings to define how Tests should run by the Automation Manager. + * It will handle it in a way that the current Project Settings will remain untouched. + */ +UENUM() +enum class EMapTestingMode : uint8 +{ + // It will search for ASpatialFunctionalTest, if there are any it forces Spatial otherwise Native + Detect = 0, + // Forces Spatial to be off and Play Offline (1 Client, no network), the default Native behaviour + ForceNativeOffline = 1, + // Forces Spatial to be off and Play As Listen Server (1 Client that is also Server, ie authoritive Client) + ForceNativeAsListenServer = 2, + // Forces Spatial to be off and Play As Client (1 Client + 1 Dedicated Server) + ForceNativeAsClient = 3, + // Forces Spatial to be on. Calculates the number of players needed based on the ASpatialFunctionalTests present, 1 if none exists + ForceSpatial = 4, + // Uses current settings to run the tests + UseCurrentSettings = 5 +}; + +USTRUCT(BlueprintType) +struct FMapTestingSettings +{ + GENERATED_BODY(); + + /* Available Modes to run Tests: + - Detect: It will search for ASpatialFunctionalTest, if there are any it forces Spatial otherwise Native + - Force Native: Forces Spatial to be off and use only 1 Client, the default Native behaviour + - Force Spatial: Forces Spatial to be on. Calculates the number of players needed based on the ASpatialFunctionalTests present, 1 if + none exists + - Use Current Settings: Uses current settings to run the tests */ + UPROPERTY(EditAnywhere, Category = "Default") + EMapTestingMode TestingMode = EMapTestingMode::Detect; +}; + UCLASS() class SPATIALGDK_API ASpatialWorldSettings : public AWorldSettings { - GENERATED_BODY() - -private: - /** Enable running different server worker types to split the simulation. */ - UPROPERTY(EditAnywhere, Config, Category = "Multi-Worker") - bool bEnableMultiWorker; + GENERATED_UCLASS_BODY() + friend class USpatialStatics; public: - UPROPERTY(EditAnywhere, Category = "Multi-Worker", meta = (EditCondition = "bEnableMultiWorker")) + /** If command line override -OverrideMultiWorkerSettingsClass is set then return the specified class from the command line. + * Else if bForceNonEditorSettings is set, return the MultiWorkerSettingsClass. + * Else if the EditorMultiWorkerSettingsOverride is set and we are in the Editor, return the EditorMultiWorkerSettings. + * Else if multi-worker is disabled in the editor, return the single worker settings class + * Else if the MultiWorkerSettingsClass is set return it. + * Otherwise return the single worker settings class. */ + TSubclassOf GetMultiWorkerSettingsClass(bool bForceNonEditorSettings = false); + +#if WITH_EDITORONLY_DATA + /** Defines how Unreal Editor will run the Tests in this map, without changing current Settings. */ + UPROPERTY(EditAnywhere, Category = "Testing") + FMapTestingSettings TestingSettings; + + UPROPERTY(EditAnywhere, Category = "Testing") + bool bEnableDebugInterface = false; +#endif + +#if WITH_EDITOR + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + static void EditorRefreshSpatialDebugger(); +#endif // WITH_EDITOR + +private: + /** Specify the load balancing strategy to be used for multiple workers */ + UPROPERTY(EditAnywhere, Category = "Multi-Worker") TSubclassOf MultiWorkerSettingsClass; - // This function is used to expose the private bool property to SpatialStatics. - // You should call USpatialStatics::IsMultiWorkerEnabled to properly check whether multi-worker is enabled. - bool IsMultiWorkerEnabledInWorldSettings() const - { - return bEnableMultiWorker && *MultiWorkerSettingsClass != nullptr; - } + /** Editor override to specify a different load balancing strategy to run in-editor */ + UPROPERTY(EditAnywhere, Category = "Multi-Worker") + TSubclassOf EditorMultiWorkerSettingsOverride; + + /** Gets MultiWorkerSettingsClass if set, otherwise returns a single worker behaviour. */ + TSubclassOf GetValidWorkerSettings() const; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/ConnectionConfig.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/ConnectionConfig.h index 4f68281f18..b9d9ebf6ee 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/ConnectionConfig.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/ConnectionConfig.h @@ -3,22 +3,33 @@ #pragma once #include "Containers/UnrealString.h" +#include "Engine/EngineBaseTypes.h" #include "Internationalization/Regex.h" #include "Misc/CommandLine.h" #include "Misc/Parse.h" + #include "SpatialConstants.h" #include "SpatialGDKSettings.h" + #include +DECLARE_LOG_CATEGORY_EXTERN(LogConnectionConfig, Log, All); + struct FConnectionConfig { + enum EWorkerType + { + Client, + Server + }; + FConnectionConfig() : UseExternalIp(false) , EnableWorkerSDKProtocolLogging(false) , EnableWorkerSDKOpLogging(false) , WorkerSDKLogFileSize(10 * 1024 * 1024) , WorkerSDKLogLevel(WORKER_LOG_LEVEL_INFO) - , LinkProtocol(WORKER_NETWORK_CONNECTION_TYPE_MODULAR_KCP) + , LinkProtocol(WORKER_NETWORK_CONNECTION_TYPE_TCP) , 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) @@ -44,8 +55,9 @@ struct FConnectionConfig 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); + WorkerType = bConnectAsClient ? SpatialConstants::DefaultClientWorkerType.ToString() + : SpatialConstants::DefaultServerWorkerType.ToString(); + UE_LOG(LogConnectionConfig, Warning, TEXT("No worker type specified through commandline, defaulting to %s"), *WorkerType); } if (WorkerId.IsEmpty()) @@ -55,8 +67,12 @@ struct FConnectionConfig TcpNoDelay = (SpatialGDKSettings->bTcpNoDelay ? 1 : 0); - UdpUpstreamIntervalMS = 10; // Despite flushing on the worker ops thread, WorkerSDK still needs to send periodic data (like ACK, resends and ping). - UdpDownstreamIntervalMS = (bConnectAsClient ? SpatialGDKSettings->UdpClientDownstreamUpdateIntervalMS : SpatialGDKSettings->UdpServerDownstreamUpdateIntervalMS); + UdpUpstreamIntervalMS = + 10; // Despite flushing on the worker ops thread, WorkerSDK still needs to send periodic data (like ACK, resends and ping). + UdpDownstreamIntervalMS = (bConnectAsClient ? SpatialGDKSettings->UdpClientDownstreamUpdateIntervalMS + : SpatialGDKSettings->UdpServerDownstreamUpdateIntervalMS); + + LinkProtocol = ConnectionTypeMap[bConnectAsClient ? EWorkerType::Client : EWorkerType::Server]; } private: @@ -82,7 +98,7 @@ struct FConnectionConfig } else if (!LogLevelString.IsEmpty()) { - UE_LOG(LogTemp, Warning, TEXT("Unknown worker SDK log verbosity %s specified. Defaulting to Info."), *LogLevelString); + UE_LOG(LogConnectionConfig, Warning, TEXT("Unknown worker SDK log verbosity %s specified. Defaulting to Info."), *LogLevelString); } } @@ -92,16 +108,27 @@ struct FConnectionConfig FParse::Value(CommandLine, TEXT("linkProtocol"), LinkProtocolString); if (LinkProtocolString.Compare(TEXT("Tcp"), ESearchCase::IgnoreCase) == 0) { - LinkProtocol = WORKER_NETWORK_CONNECTION_TYPE_MODULAR_TCP; + ConnectionTypeMap[EWorkerType::Client] = WORKER_NETWORK_CONNECTION_TYPE_TCP; + ConnectionTypeMap[EWorkerType::Server] = WORKER_NETWORK_CONNECTION_TYPE_TCP; + return; } else if (LinkProtocolString.Compare(TEXT("Kcp"), ESearchCase::IgnoreCase) == 0) { - LinkProtocol = WORKER_NETWORK_CONNECTION_TYPE_MODULAR_KCP; + ConnectionTypeMap[EWorkerType::Client] = WORKER_NETWORK_CONNECTION_TYPE_KCP; + ConnectionTypeMap[EWorkerType::Server] = WORKER_NETWORK_CONNECTION_TYPE_KCP; + return; } - else if (!LinkProtocolString.IsEmpty()) + + if (!LinkProtocolString.IsEmpty()) { - UE_LOG(LogTemp, Warning, TEXT("Unknown network protocol %s specified for connecting to SpatialOS. Defaulting to KCP."), *LinkProtocolString); + UE_LOG(LogConnectionConfig, Warning, TEXT("Unknown network protocol '%s' specified for connecting to SpatialOS."), + *LinkProtocolString); } + + ConnectionTypeMap[EWorkerType::Client] = WORKER_NETWORK_CONNECTION_TYPE_KCP; + ConnectionTypeMap[EWorkerType::Server] = WORKER_NETWORK_CONNECTION_TYPE_TCP; + + UE_LOG(LogConnectionConfig, Verbose, TEXT("No link protocol set. Defaulting to TCP for server workers, KCP for client workers.")); } public: @@ -114,6 +141,7 @@ struct FConnectionConfig uint32 WorkerSDKLogFileSize; Worker_LogLevel WorkerSDKLogLevel; Worker_NetworkConnectionType LinkProtocol; + Worker_NetworkConnectionType ConnectionTypeMap[2]; Worker_ConnectionParameters ConnectionParams = {}; uint8 TcpMultiplexLevel; uint8 TcpNoDelay; @@ -124,10 +152,7 @@ struct FConnectionConfig class FLocatorConfig : public FConnectionConfig { public: - FLocatorConfig() - { - LoadDefaults(); - } + FLocatorConfig() { LoadDefaults(); } void LoadDefaults() { @@ -141,6 +166,8 @@ class FLocatorConfig : public FConnectionConfig { LocatorHost = SpatialConstants::LOCATOR_HOST; } + + LocatorPort = SpatialConstants::LOCATOR_PORT; } bool TryLoadCommandLineArgs() @@ -154,6 +181,7 @@ class FLocatorConfig : public FConnectionConfig } FString LocatorHost; + int32 LocatorPort; FString PlayerIdentityToken; FString LoginToken; }; @@ -161,10 +189,7 @@ class FLocatorConfig : public FConnectionConfig class FDevAuthConfig : public FLocatorConfig { public: - FDevAuthConfig() - { - LoadDefaults(); - } + FDevAuthConfig() { LoadDefaults(); } void LoadDefaults() { @@ -179,6 +204,8 @@ class FDevAuthConfig : public FLocatorConfig { LocatorHost = SpatialConstants::LOCATOR_HOST; } + + LocatorPort = SpatialConstants::LOCATOR_PORT; } bool TryLoadCommandLineArgs() @@ -203,10 +230,7 @@ class FDevAuthConfig : public FLocatorConfig class FReceptionistConfig : public FConnectionConfig { public: - FReceptionistConfig() - { - LoadDefaults(); - } + FReceptionistConfig() { LoadDefaults(); } void LoadDefaults() { @@ -229,14 +253,19 @@ class FReceptionistConfig : public FConnectionConfig if (!FParse::Value(CommandLine, TEXT("receptionistHost"), Host)) { // 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. + // 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. FString URLAddress; FParse::Token(CommandLine, URLAddress, false /* UseEscape */); const FURL URL(nullptr /* Base */, *URLAddress, TRAVEL_Absolute); - if (URL.Valid) + if (URL.Valid && !URLAddress.IsEmpty()) { SetupFromURL(URL); } + else if (!bReceptionistPortParsed) + { + return false; + } } else { diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/LegacySpatialWorkerConnection.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/LegacySpatialWorkerConnection.h deleted file mode 100644 index 1f92c81878..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/LegacySpatialWorkerConnection.h +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#pragma once - -#include "Interop/Connection/SpatialWorkerConnection.h" -#include "Interop/Connection/OutgoingMessages.h" -#include "Interop/Connection/SpatialOSWorkerInterface.h" -#include "Interop/Connection/WorkerConnectionCoordinator.h" -#include "SpatialCommonTypes.h" -#include "SpatialView/OpList/OpList.h" - -#include "Containers/Queue.h" -#include "HAL/Runnable.h" -#include "HAL/ThreadSafeBool.h" -#include "UObject/WeakObjectPtr.h" - -#include -#include - -#include "LegacySpatialWorkerConnection.generated.h" - -UCLASS() -class SPATIALGDK_API ULegacySpatialWorkerConnection : public USpatialWorkerConnection, public FRunnable -{ - GENERATED_BODY() - -public: - virtual void SetConnection(Worker_Connection* WorkerConnectionIn) override; - virtual void FinishDestroy() override; - virtual void DestroyConnection() override; - - // Worker Connection Interface - 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, FWorkerComponentUpdate* ComponentUpdate) override; - virtual Worker_RequestId SendCommandRequest(Worker_EntityId EntityId, Worker_CommandRequest* Request, uint32_t CommandId) override; - virtual void SendCommandResponse(Worker_RequestId RequestId, 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(SpatialGDK::SpatialMetrics Metrics) override; - - virtual PhysicalWorkerName GetWorkerId() const override; - virtual const TArray& GetWorkerAttributes() const override; - - virtual void ProcessOutgoingMessages() override; - virtual void MaybeFlush() override; - virtual void Flush() override; - -private: - void QueueLatestOpList(); - void CacheWorkerAttributes(); - - // Begin FRunnable Interface - virtual uint32 Run() override; - virtual void Stop() override; - // End FRunnable Interface - - void InitializeOpsProcessingThread(); - - template - void QueueOutgoingMessage(ArgsType&&... Args); - - Worker_Connection* WorkerConnection; - - TArray CachedWorkerAttributes; - - FRunnableThread* OpsProcessingThread; - FThreadSafeBool KeepRunning = true; - - TQueue OpListQueue; - TQueue> OutgoingMessagesQueue; - - // RequestIds per worker connection start at 0 and incrementally go up each command sent. - Worker_RequestId NextRequestId = 0; - - // Coordinates the async worker ops thread. - TOptional ThreadWaitCondition; -}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/OutgoingMessages.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/OutgoingMessages.h index 36d729805e..4702258af1 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/OutgoingMessages.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/OutgoingMessages.h @@ -5,9 +5,10 @@ #include "Containers/Array.h" #include "Containers/UnrealString.h" #include "HAL/Platform.h" +#include "Interop/Connection/SpatialGDKSpanId.h" #include "Misc/Optional.h" -#include "Templates/UnrealTemplate.h" #include "Templates/UniquePtr.h" +#include "Templates/UnrealTemplate.h" #include "UObject/NameTypes.h" #include "Utils/SpatialLatencyTracer.h" @@ -17,7 +18,6 @@ namespace SpatialGDK { - enum class EOutgoingMessageType : int32 { ReserveEntityIdsRequest, @@ -30,14 +30,16 @@ enum class EOutgoingMessageType : int32 CommandResponse, CommandFailure, LogMessage, - ComponentInterest, EntityQueryRequest, Metrics }; struct FOutgoingMessage { - FOutgoingMessage(const EOutgoingMessageType& InType) : Type(InType) {} + FOutgoingMessage(const EOutgoingMessageType& InType) + : Type(InType) + { + } virtual ~FOutgoingMessage() {} EOutgoingMessageType Type; @@ -48,67 +50,83 @@ struct FReserveEntityIdsRequest : FOutgoingMessage FReserveEntityIdsRequest(uint32_t InNumOfEntities) : FOutgoingMessage(EOutgoingMessageType::ReserveEntityIdsRequest) , NumOfEntities(InNumOfEntities) - {} + { + } uint32_t NumOfEntities; }; struct FCreateEntityRequest : FOutgoingMessage { - FCreateEntityRequest(TArray&& InComponents, const Worker_EntityId* InEntityId) + FCreateEntityRequest(TArray&& InComponents, const Worker_EntityId* InEntityId, const FSpatialGDKSpanId& SpanId) : FOutgoingMessage(EOutgoingMessageType::CreateEntityRequest) , Components(MoveTemp(InComponents)) , EntityId(InEntityId != nullptr ? *InEntityId : TOptional()) - {} + , SpanId(SpanId) + { + } TArray Components; TOptional EntityId; + FSpatialGDKSpanId SpanId; }; struct FDeleteEntityRequest : FOutgoingMessage { - FDeleteEntityRequest(Worker_EntityId InEntityId) + FDeleteEntityRequest(Worker_EntityId InEntityId, const FSpatialGDKSpanId& SpanId) : FOutgoingMessage(EOutgoingMessageType::DeleteEntityRequest) , EntityId(InEntityId) - {} + , SpanId(SpanId) + { + } Worker_EntityId EntityId; + const FSpatialGDKSpanId SpanId; }; struct FAddComponent : FOutgoingMessage { - FAddComponent(Worker_EntityId InEntityId, const FWorkerComponentData& InData) + FAddComponent(Worker_EntityId InEntityId, const FWorkerComponentData& InData, const FSpatialGDKSpanId& SpanId) : FOutgoingMessage(EOutgoingMessageType::AddComponent) , EntityId(InEntityId) , Data(InData) - {} + , SpanId(SpanId) + { + } Worker_EntityId EntityId; FWorkerComponentData Data; + FSpatialGDKSpanId SpanId; }; struct FRemoveComponent : FOutgoingMessage { - FRemoveComponent(Worker_EntityId InEntityId, Worker_ComponentId InComponentId) + FRemoveComponent(Worker_EntityId InEntityId, Worker_ComponentId InComponentId, const FSpatialGDKSpanId& SpanId) : FOutgoingMessage(EOutgoingMessageType::RemoveComponent) , EntityId(InEntityId) , ComponentId(InComponentId) - {} + , SpanId(SpanId) + { + } Worker_EntityId EntityId; Worker_ComponentId ComponentId; + FSpatialGDKSpanId SpanId; }; struct FComponentUpdate : FOutgoingMessage { - FComponentUpdate(Worker_EntityId InEntityId, const FWorkerComponentUpdate& InComponentUpdate) + FComponentUpdate(Worker_EntityId InEntityId, const FWorkerComponentUpdate& InComponentUpdate, const FSpatialGDKSpanId& SpanId) : FOutgoingMessage(EOutgoingMessageType::ComponentUpdate) , EntityId(InEntityId) , Update(InComponentUpdate) - {} + , SpanId(SpanId) + { + } Worker_EntityId EntityId; FWorkerComponentUpdate Update; + FSpatialGDKSpanId SpanId; }; struct FCommandRequest : FOutgoingMessage @@ -118,7 +136,8 @@ struct FCommandRequest : FOutgoingMessage , EntityId(InEntityId) , Request(InRequest) , CommandId(InCommandId) - {} + { + } Worker_EntityId EntityId; Worker_CommandRequest Request; @@ -127,26 +146,32 @@ struct FCommandRequest : FOutgoingMessage struct FCommandResponse : FOutgoingMessage { - FCommandResponse(Worker_RequestId InRequestId, const Worker_CommandResponse& InResponse) + FCommandResponse(Worker_RequestId InRequestId, const Worker_CommandResponse& InResponse, const FSpatialGDKSpanId& SpanId) : FOutgoingMessage(EOutgoingMessageType::CommandResponse) , RequestId(InRequestId) , Response(InResponse) - {} + , SpanId(SpanId) + { + } Worker_RequestId RequestId; Worker_CommandResponse Response; + FSpatialGDKSpanId SpanId; }; struct FCommandFailure : FOutgoingMessage { - FCommandFailure(Worker_RequestId InRequestId, const FString& InMessage) + FCommandFailure(Worker_RequestId InRequestId, const FString& InMessage, const FSpatialGDKSpanId& SpanId) : FOutgoingMessage(EOutgoingMessageType::CommandFailure) , RequestId(InRequestId) , Message(InMessage) - {} + , SpanId(SpanId) + { + } Worker_RequestId RequestId; FString Message; + FSpatialGDKSpanId SpanId; }; struct FLogMessage : FOutgoingMessage @@ -156,25 +181,14 @@ struct FLogMessage : FOutgoingMessage , Level(InLevel) , LoggerName(InLoggerName) , Message(InMessage) - {} + { + } uint8_t Level; FName LoggerName; FString Message; }; -struct FComponentInterest : FOutgoingMessage -{ - FComponentInterest(Worker_EntityId InEntityId, TArray&& InInterests) - : FOutgoingMessage(EOutgoingMessageType::ComponentInterest) - , EntityId(InEntityId) - , Interests(MoveTemp(InInterests)) - {} - - Worker_EntityId EntityId; - TArray Interests; -}; - struct FEntityQueryRequest : FOutgoingMessage { FEntityQueryRequest(const Worker_EntityQuery& InEntityQuery) @@ -184,7 +198,8 @@ struct FEntityQueryRequest : FOutgoingMessage if (EntityQuery.snapshot_result_type_component_ids != nullptr) { ComponentIdStorage.SetNum(EntityQuery.snapshot_result_type_component_id_count); - FMemory::Memcpy(static_cast(ComponentIdStorage.GetData()), static_cast(EntityQuery.snapshot_result_type_component_ids), ComponentIdStorage.Num()); + FMemory::Memcpy(static_cast(ComponentIdStorage.GetData()), + static_cast(EntityQuery.snapshot_result_type_component_ids), ComponentIdStorage.Num()); } TraverseConstraint(&EntityQuery.constraint); @@ -244,9 +259,10 @@ struct FMetrics : FOutgoingMessage FMetrics(SpatialMetrics InMetrics) : FOutgoingMessage(EOutgoingMessageType::Metrics) , Metrics(MoveTemp(InMetrics)) - {} + { + } SpatialMetrics Metrics; }; -} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialConnectionManager.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialConnectionManager.h index ca7d790d86..3f7f1fa9fa 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialConnectionManager.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialConnectionManager.h @@ -2,10 +2,15 @@ #pragma once -#include "Interop/Connection/SpatialOSWorkerInterface.h" #include "Interop/Connection/ConnectionConfig.h" +#include "Interop/Connection/SpatialEventTracer.h" +#include "Interop/Connection/SpatialOSWorkerInterface.h" #include "SpatialCommonTypes.h" #include "SpatialGDKSettings.h" +#include "SpatialView/ComponentSetData.h" +#include "Utils/SchemaDatabase.h" + +#include "Engine/EngineBaseTypes.h" #include "SpatialConnectionManager.generated.h" @@ -29,13 +34,14 @@ class SPATIALGDK_API USpatialConnectionManager : public UObject 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;} + + using LoginTokenResponseCallback = TFunction; + using LogCallback = 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); @@ -58,13 +64,16 @@ class SPATIALGDK_API USpatialConnectionManager : public UObject void SetupConnectionConfigFromURL(const FURL& URL, const FString& SpatialWorkerType); USpatialWorkerConnection* GetWorkerConnection() { return WorkerConnection; } + void SetComponentSets(const TMap& InComponentSetMap); void RequestDeploymentLoginTokens(); + static void OnLogCallback(void* UserData, const Worker_LogData* Message); + private: void ConnectToReceptionist(uint32 PlayInEditorID); void ConnectToLocator(FLocatorConfig* InLocatorConfig); - void FinishConnecting(Worker_ConnectionFuture* ConnectionFuture); + void FinishConnecting(Worker_ConnectionFuture* ConnectionFuture, TSharedPtr InEventTracer); void OnConnectionSuccess(); void OnConnectionFailure(uint8_t ConnectionStatusCode, const FString& ErrorMessage); @@ -72,9 +81,11 @@ class SPATIALGDK_API USpatialConnectionManager : public UObject 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); + static void OnPlayerIdentityToken(void* UserData, const Worker_PlayerIdentityTokenResponse* PIToken); + static void OnLoginTokens(void* UserData, const Worker_LoginTokensResponse* LoginTokens); + void ProcessLoginTokensResponse(const Worker_LoginTokensResponse* LoginTokens); + + TSharedPtr CreateEventTracer(const FString& WorkerId); private: UPROPERTY() @@ -87,4 +98,6 @@ class SPATIALGDK_API USpatialConnectionManager : public UObject ESpatialConnectionType ConnectionType = ESpatialConnectionType::Receptionist; LoginTokenResponseCallback LoginTokenResCallback; + LogCallback SpatialLogCallback; + SpatialGDK::FComponentSetData ComponentSetData; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialEventTracer.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialEventTracer.h new file mode 100644 index 0000000000..b6873bb60f --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialEventTracer.h @@ -0,0 +1,90 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Interop/Connection/SpatialGDKSpanId.h" +#include "Interop/Connection/SpatialTraceEvent.h" +#include "Interop/Connection/UserSpanId.h" +#include "SpatialCommonTypes.h" +#include "SpatialView/EntityComponentId.h" + +#include +#include + +// Documentation for event tracing in the GDK can be found here: https://brevi.link/gdk-event-tracing-documentation + +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialEventTracer, Log, All); + +namespace SpatialGDK +{ +// SpatialEventTracer wraps Trace_EventTracer related functionality +class SPATIALGDK_API SpatialEventTracer +{ +public: + explicit SpatialEventTracer(const FString& WorkerId); + ~SpatialEventTracer(); + + const Trace_EventTracer* GetConstWorkerEventTracer() const { return EventTracer; }; + Trace_EventTracer* GetWorkerEventTracer() const { return EventTracer; } + + FSpatialGDKSpanId TraceEvent(const FSpatialTraceEvent& SpatialTraceEvent, const Trace_SpanIdType* Causes = nullptr, + int32 NumCauses = 0); + + void AddComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, const FSpatialGDKSpanId& SpanId); + void RemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + void UpdateComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, const FSpatialGDKSpanId& SpanId); + + FSpatialGDKSpanId GetSpanId(const EntityComponentId& Id) const; + + static FUserSpanId GDKSpanIdToUserSpanId(const FSpatialGDKSpanId& SpanId); + static FSpatialGDKSpanId UserSpanIdToGDKSpanId(const FUserSpanId& UserSpanId); + + const FString& GetFolderPath() const { return FolderPath; } + + void AddToStack(const FSpatialGDKSpanId& SpanId); + FSpatialGDKSpanId PopFromStack(); + FSpatialGDKSpanId GetFromStack() const; + bool IsStackEmpty() const; + + void AddLatentPropertyUpdateSpanId(const TWeakObjectPtr& Object, const FSpatialGDKSpanId& SpanId); + FSpatialGDKSpanId PopLatentPropertyUpdateSpanId(const TWeakObjectPtr& Object); + +private: + struct StreamDeleter + { + void operator()(Io_Stream* StreamToDestroy) const; + }; + + static void TraceCallback(void* UserData, const Trace_Item* Item); + + FString FolderPath; + + TUniquePtr Stream; + Trace_EventTracer* EventTracer = nullptr; + + TArray SpanIdStack; + TMap EntityComponentSpanIds; + TMap, FSpatialGDKSpanId> ObjectSpanIdStacks; + + uint64 BytesWrittenToStream = 0; + uint64 MaxFileSize = 0; +}; + +// SpatialScopedActiveSpanIds are creating prior to calling worker send functions so that worker can use the input SpanId to continue +// traces. +struct SpatialScopedActiveSpanId +{ + explicit SpatialScopedActiveSpanId(SpatialEventTracer* InEventTracer, const FSpatialGDKSpanId& InCurrentSpanId); + ~SpatialScopedActiveSpanId(); + + SpatialScopedActiveSpanId(const SpatialScopedActiveSpanId&) = delete; + SpatialScopedActiveSpanId(SpatialScopedActiveSpanId&&) = delete; + SpatialScopedActiveSpanId& operator=(const SpatialScopedActiveSpanId&) = delete; + SpatialScopedActiveSpanId& operator=(SpatialScopedActiveSpanId&&) = delete; + +private: + const FSpatialGDKSpanId& CurrentSpanId; + Trace_EventTracer* EventTracer; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialEventTracerUserInterface.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialEventTracerUserInterface.h new file mode 100644 index 0000000000..a9f5069ee2 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialEventTracerUserInterface.h @@ -0,0 +1,79 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Interop/Connection/SpatialTraceEvent.h" +#include "Interop/Connection/UserSpanId.h" +#include "Kismet/BlueprintFunctionLibrary.h" + +#include + +#include "SpatialEventTracerUserInterface.generated.h" + +DECLARE_DYNAMIC_DELEGATE(FEventTracerRPCDelegate); + +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialEventTracerUserInterface, Log, All); + +namespace SpatialGDK +{ +class SpatialEventTracer; +} + +class USpatialNetDriver; + +// Docs on how to use the interface can be found: +// https://docs.google.com/document/d/1i0fOdeldqeZ9kgBdmYcTD3fCwYXT9pX1RzpTIupgjcg/edit?usp=sharing + +UCLASS() +class SPATIALGDK_API USpatialEventTracerUserInterface : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** + * EXPERIMENTAL + * Will trace an event using the input data and associate it with the input SpanId + * (This API is subject to change) + */ + UFUNCTION(BlueprintCallable, Category = "SpatialOS|EventTracing", meta = (WorldContext = "WorldContextObject")) + static FUserSpanId TraceEvent(UObject* WorldContextObject, const FSpatialTraceEvent& SpatialTraceEvent); + + /** + * EXPERIMENTAL + * Will trace an event using the input data and associate it with the input SpanId + * (This API is subject to change) + */ + UFUNCTION(BlueprintCallable, Category = "SpatialOS|EventTracing", meta = (WorldContext = "WorldContextObject")) + static FUserSpanId TraceEventWithCauses(UObject* WorldContextObject, const FSpatialTraceEvent& SpatialTraceEvent, + const TArray& Causes); + + /** + * EXPERIMENTAL + * Will ensure that the input SpanId is used to continue the tracing of the RPC flow. + * Use the Delegate to call your RPC + * (This API is subject to change) + */ + UFUNCTION(BlueprintCallable, Category = "SpatialOS|EventTracing", meta = (WorldContext = "WorldContextObject")) + static void TraceRPC(UObject* WorldContextObject, FEventTracerRPCDelegate Delegate, const FUserSpanId& SpanId); + + /** + * EXPERIMENTAL + * Will ensure that the input SpanId is used to continue the tracing of the property update flow. + * The input Object should be the object that contains the property + * (This API is subject to change) + */ + UFUNCTION(BlueprintCallable, Category = "SpatialOS|EventTracing", meta = (WorldContext = "WorldContextObject")) + static void TraceProperty(UObject* WorldContextObject, UObject* Object, const FUserSpanId& UserSpanId); + + /** + * EXPERIMENTAL + * Used to get the active SpanId from the GDK. Use this to cause your own trace events. + * (This API is subject to change) + */ + UFUNCTION(BlueprintCallable, Category = "SpatialOS|EventTracing", meta = (WorldContext = "WorldContextObject")) + static bool GetActiveSpanId(UObject* WorldContextObject, FUserSpanId& OutUserSpanId); + +private: + static SpatialGDK::SpatialEventTracer* GetEventTracer(UObject* WorldContextObject); + static USpatialNetDriver* GetSpatialNetDriver(UObject* WorldContextObject); +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialGDKSpanId.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialGDKSpanId.h new file mode 100644 index 0000000000..1b241a9bd7 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialGDKSpanId.h @@ -0,0 +1,26 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialCommonTypes.h" + +#include + +struct SPATIALGDK_API FSpatialGDKSpanId +{ + FSpatialGDKSpanId(); + explicit FSpatialGDKSpanId(const Trace_SpanIdType* TraceSpanId); + + FString ToString() const; + static FString ToString(const Trace_SpanIdType* TraceSpanId); + + bool IsNull() const { return Trace_SpanId_IsNull(Id) > 0; } + + void WriteId(const Trace_SpanIdType* TraceSpanId); + Trace_SpanIdType* GetId(); + const Trace_SpanIdType* GetConstId() const; + +private: + Trace_SpanIdType Id[TRACE_SPAN_ID_SIZE_BYTES]; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialOSWorkerInterface.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialOSWorkerInterface.h index ba64b04c04..b19ebd1a3d 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialOSWorkerInterface.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialOSWorkerInterface.h @@ -4,31 +4,32 @@ #include "Interop/Connection/OutgoingMessages.h" #include "SpatialCommonTypes.h" -#include "SpatialView/OpList/OpList.h" -#include "Utils/SpatialLatencyTracer.h" - -#include -#include +#include "SpatialView/CommandRetryHandler.h" +#include "SpatialView/ViewDelta.h" class SPATIALGDK_API SpatialOSWorkerInterface { public: -// FORCEINLINE bool IsConnected() { return bIsConnected; } + virtual ~SpatialOSWorkerInterface() = default; // 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, FWorkerComponentUpdate* ComponentUpdate) PURE_VIRTUAL(AbstractSpatialWorkerConnection::SendComponentUpdate, return;); - virtual Worker_RequestId SendCommandRequest(Worker_EntityId EntityId, Worker_CommandRequest* Request, uint32_t CommandId) PURE_VIRTUAL(AbstractSpatialWorkerConnection::SendCommandRequest, return 0;); - virtual void SendCommandResponse(Worker_RequestId RequestId, 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(SpatialGDK::SpatialMetrics Metrics) PURE_VIRTUAL(AbstractSpatialWorkerConnection::SendMetrics, return;); + virtual const TArray& GetEntityDeltas() = 0; + virtual const TArray& GetWorkerMessages() = 0; + virtual Worker_RequestId SendReserveEntityIdsRequest(uint32_t NumOfEntities, const SpatialGDK::FRetryData& Data) = 0; + virtual Worker_RequestId SendCreateEntityRequest(TArray Components, const Worker_EntityId* EntityId, + const SpatialGDK::FRetryData& RetryData, const FSpatialGDKSpanId& SpanId = {}) = 0; + virtual Worker_RequestId SendDeleteEntityRequest(Worker_EntityId EntityId, const SpatialGDK::FRetryData& RetryData, + const FSpatialGDKSpanId& SpanId = {}) = 0; + virtual void SendAddComponent(Worker_EntityId EntityId, FWorkerComponentData* ComponentData, const FSpatialGDKSpanId& SpanId = {}) = 0; + virtual void SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, const FSpatialGDKSpanId& SpanId = {}) = 0; + virtual void SendComponentUpdate(Worker_EntityId EntityId, FWorkerComponentUpdate* ComponentUpdate, + const FSpatialGDKSpanId& SpanId = {}) = 0; + virtual Worker_RequestId SendCommandRequest(Worker_EntityId EntityId, Worker_CommandRequest* Request, + const SpatialGDK::FRetryData& RetryData, const FSpatialGDKSpanId& SpanId) = 0; + virtual void SendCommandResponse(Worker_RequestId RequestId, Worker_CommandResponse* Response, + const FSpatialGDKSpanId& SpanId = {}) = 0; + virtual void SendCommandFailure(Worker_RequestId RequestId, const FString& Message, const FSpatialGDKSpanId& SpanId = {}) = 0; + virtual void SendLogMessage(uint8_t Level, const FName& LoggerName, const TCHAR* Message) = 0; + virtual Worker_RequestId SendEntityQueryRequest(const Worker_EntityQuery* EntityQuery, const SpatialGDK::FRetryData& RetryData) = 0; + virtual void SendMetrics(SpatialGDK::SpatialMetrics Metrics) = 0; }; - diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialTraceEvent.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialTraceEvent.h new file mode 100644 index 0000000000..06932b4fa1 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialTraceEvent.h @@ -0,0 +1,50 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" + +#include "SpatialTraceEvent.generated.h" + +USTRUCT(Blueprintable) +struct FTraceData +{ + GENERATED_BODY() + + FTraceData(){}; + explicit FTraceData(FString InKey, FString InValue) + : Key(MoveTemp(InKey)) + , Value(MoveTemp(InValue)) + { + } + + UPROPERTY(BlueprintReadWrite, Category = "TraceData") + FString Key; + + UPROPERTY(BlueprintReadWrite, Category = "TraceData") + FString Value; +}; + +USTRUCT(Blueprintable) +struct FSpatialTraceEvent +{ + GENERATED_BODY() + + FSpatialTraceEvent(){}; + explicit FSpatialTraceEvent(FName InType, FString InMessage) + : Type(MoveTemp(InType)) + , Message(MoveTemp(InMessage)) + { + } + + void AddData(FString Key, FString Value) { Data.Add(FTraceData(MoveTemp(Key), MoveTemp(Value))); } + + UPROPERTY(BlueprintReadWrite, Category = "SpatialTraceEvent") + FName Type; + + UPROPERTY(BlueprintReadWrite, Category = "SpatialTraceEvent") + FString Message; + + UPROPERTY(BlueprintReadWrite, Category = "SpatialTraceEvent") + TArray Data; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialTraceEventBuilder.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialTraceEventBuilder.h new file mode 100644 index 0000000000..0ccfef3103 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialTraceEventBuilder.h @@ -0,0 +1,76 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Interop/Connection/SpatialTraceEvent.h" +#include "Interop/Connection/SpatialTraceUniqueId.h" +#include "SpatialCommonTypes.h" +#include "WorkerSDK/improbable/c_worker.h" + +#define GDK_EVENT_NAMESPACE "unreal_gdk." + +namespace SpatialGDK +{ +class SPATIALGDK_API FSpatialTraceEventBuilder +{ +public: + FSpatialTraceEventBuilder(FName InType); + FSpatialTraceEventBuilder(FName InType, FString InMessage); + + FSpatialTraceEventBuilder AddObject(FString Key, const UObject* Object); + FSpatialTraceEventBuilder AddFunction(FString Key, const UFunction* Function); + FSpatialTraceEventBuilder AddEntityId(FString Key, const Worker_EntityId EntityId); + FSpatialTraceEventBuilder AddComponentId(FString Key, const Worker_ComponentId ComponentId); + FSpatialTraceEventBuilder AddFieldId(FString Key, const uint32 FieldId); + FSpatialTraceEventBuilder AddNewWorkerId(FString Key, const uint32 NewWorkerId); + FSpatialTraceEventBuilder AddCommand(FString Key, const FString& Command); + FSpatialTraceEventBuilder AddRequestId(FString Key, const int64 RequestId); + FSpatialTraceEventBuilder AddAuthority(FString Key, const Worker_Authority Role); + FSpatialTraceEventBuilder AddKeyValue(FString Key, FString Value); + FSpatialTraceEvent GetEvent() &&; + + static FSpatialTraceEvent CreateProcessRPC(const UObject* Object, UFunction* Function, const EventTraceUniqueId& LinearTraceId); + static FSpatialTraceEvent CreatePushRPC(const UObject* Object, UFunction* Function); + static FSpatialTraceEvent CreateSendRPC(const EventTraceUniqueId& LinearTraceId); + + static FSpatialTraceEvent CreateQueueRPC(); + static FSpatialTraceEvent CreateRetryRPC(); + static FSpatialTraceEvent CreatePropertyChanged(const UObject* Object, const Worker_EntityId EntityId, const FString& PropertyName, + EventTraceUniqueId LinearTraceId); + static FSpatialTraceEvent CreateSendPropertyUpdate(const UObject* Object, const Worker_EntityId EntityId, + const Worker_ComponentId ComponentId); + static FSpatialTraceEvent CreateReceivePropertyUpdate(const UObject* Object, const Worker_EntityId EntityId, + const Worker_ComponentId ComponentId, const FString& PropertyName, + EventTraceUniqueId LinearTraceId); + static FSpatialTraceEvent CreateMergeSendRPCs(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId); + static FSpatialTraceEvent CreateMergeComponentUpdate(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId); + static FSpatialTraceEvent CreateObjectPropertyComponentUpdate(const UObject* Object); + static FSpatialTraceEvent CreateSendCommandRequest(const FString& Command, const int64 RequestId); + static FSpatialTraceEvent CreateReceiveCommandRequest(const FString& Command, const int64 RequestId); + static FSpatialTraceEvent CreateReceiveCommandRequest(const FString& Command, const UObject* Actor, const UObject* TargetObject, + const UFunction* Function, const int32 TraceId, const int64 RequestId); + static FSpatialTraceEvent CreateSendCommandResponse(const int64 RequestId, const bool bSuccess); + static FSpatialTraceEvent CreateReceiveCommandResponse(const FString& Command, const int64 RequestId); + static FSpatialTraceEvent CreateReceiveCommandResponse(const UObject* Actor, const int64 RequestId, const bool bSuccess); + static FSpatialTraceEvent CreateReceiveCommandResponse(const UObject* Actor, const UObject* TargetObject, const UFunction* Function, + int64 RequestId, const bool bSuccess); + static FSpatialTraceEvent CreateSendRemoveEntity(const UObject* Object, const Worker_EntityId EntityId); + static FSpatialTraceEvent CreateReceiveRemoveEntity(const Worker_EntityId EntityId); + static FSpatialTraceEvent CreateSendCreateEntity(const UObject* Object, const Worker_EntityId EntityId); + static FSpatialTraceEvent CreateReceiveCreateEntity(const Worker_EntityId EntityId); + static FSpatialTraceEvent CreateReceiveCreateEntitySuccess(const UObject* Object, const Worker_EntityId EntityId); + static FSpatialTraceEvent CreateSendRetireEntity(const UObject* Object, const Worker_EntityId EntityId); + static FSpatialTraceEvent CreateAuthorityIntentUpdate(VirtualWorkerId WorkerId, const UObject* Object); + static FSpatialTraceEvent CreateAuthorityChange(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId, + const Worker_Authority Authority); + static FSpatialTraceEvent CreateComponentUpdate(const UObject* Object, const UObject* TargetObject, const Worker_EntityId EntityId, + const Worker_ComponentId ComponentId); + static FSpatialTraceEvent CreateGenericMessage(FString Message); + +private: + static FString AuthorityToString(Worker_Authority Authority); + static FString BoolToString(bool bInput); + + FSpatialTraceEvent SpatialTraceEvent; +}; +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialTraceUniqueId.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialTraceUniqueId.h new file mode 100644 index 0000000000..c21d5c1cd8 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialTraceUniqueId.h @@ -0,0 +1,30 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" + +#include + +#include "Utils/GDKPropertyMacros.h" + +class UFunction; + +namespace SpatialGDK +{ +struct EventTraceUniqueId +{ + uint32 Hash = 0; + EventTraceUniqueId(uint32 Hash) + : Hash(Hash) + { + } + + FString ToString() const; + + bool IsValid() const { return Hash != 0; } + + static EventTraceUniqueId GenerateForRPC(Worker_EntityId Entity, uint8 Type, uint64 RPCId); + static EventTraceUniqueId GenerateForProperty(Worker_EntityId Entity, const GDK_PROPERTY(Property) * Property); +}; +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialViewWorkerConnection.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialViewWorkerConnection.h deleted file mode 100644 index 03273f05f4..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialViewWorkerConnection.h +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#pragma once - -#include "Interop/Connection/SpatialWorkerConnection.h" -#include "SpatialCommonTypes.h" -#include "SpatialView/ViewCoordinator.h" -#include "SpatialView/OpList/OpList.h" - -#include "SpatialViewWorkerConnection.generated.h" - -DECLARE_LOG_CATEGORY_EXTERN(LogSpatialViewWorkerConnection, Log, All); - -UCLASS() -class SPATIALGDK_API USpatialViewWorkerConnection : public USpatialWorkerConnection -{ - GENERATED_BODY() - -public: - virtual void SetConnection(Worker_Connection* WorkerConnectionIn) override; - virtual void FinishDestroy() override; - virtual void DestroyConnection() override; - - // Worker Connection Interface - 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, FWorkerComponentUpdate* ComponentUpdate) override; - virtual Worker_RequestId SendCommandRequest(Worker_EntityId EntityId, Worker_CommandRequest* Request, uint32_t CommandId) override; - virtual void SendCommandResponse(Worker_RequestId RequestId, 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(SpatialGDK::SpatialMetrics Metrics) override; - - virtual PhysicalWorkerName GetWorkerId() const override; - virtual const TArray& GetWorkerAttributes() const override; - - virtual void ProcessOutgoingMessages() override; - virtual void MaybeFlush() override; - virtual void Flush() override; -private: - TUniquePtr Coordinator; -}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h index 400d52a6ed..730c065859 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h @@ -2,33 +2,80 @@ #pragma once -#include "Interop/Connection/OutgoingMessages.h" #include "Interop/Connection/SpatialOSWorkerInterface.h" + #include "SpatialCommonTypes.h" +#include "SpatialConstants.h" +#include "SpatialView/EntityView.h" +#include "SpatialView/OpList/ExtractedOpList.h" +#include "SpatialView/OpList/OpList.h" +#include "SpatialView/ViewCoordinator.h" #include "SpatialWorkerConnection.generated.h" DECLARE_LOG_CATEGORY_EXTERN(LogSpatialWorkerConnection, Log, All); -UCLASS(abstract) +UCLASS() class SPATIALGDK_API USpatialWorkerConnection : public UObject, public SpatialOSWorkerInterface { GENERATED_BODY() public: - virtual void SetConnection(Worker_Connection* WorkerConnectionIn) PURE_VIRTUAL(USpatialWorkerConnection::SetConnection, return;); - virtual void FinishDestroy() override - { - Super::FinishDestroy(); - } - virtual void DestroyConnection() PURE_VIRTUAL(USpatialWorkerConnection::DestroyConnection, return;); + void SetConnection(Worker_Connection* WorkerConnectionIn, TSharedPtr EventTracer, + SpatialGDK::FComponentSetData ComponentSetData); + void DestroyConnection(); + + // UObject interface. + virtual void FinishDestroy() override; + + // Worker Connection Interface + virtual const TArray& GetEntityDeltas() override; + virtual const TArray& GetWorkerMessages() override; + + virtual Worker_RequestId SendReserveEntityIdsRequest(uint32_t NumOfEntities, const SpatialGDK::FRetryData& RetryData) override; + virtual Worker_RequestId SendCreateEntityRequest(TArray Components, const Worker_EntityId* EntityId, + const SpatialGDK::FRetryData& RetryData, + const FSpatialGDKSpanId& SpanId = {}) override; + virtual Worker_RequestId SendDeleteEntityRequest(Worker_EntityId EntityId, const SpatialGDK::FRetryData& RetryData, + const FSpatialGDKSpanId& SpanId = {}) override; + virtual void SendAddComponent(Worker_EntityId EntityId, FWorkerComponentData* ComponentData, + const FSpatialGDKSpanId& SpanId = {}) override; + virtual void SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, + const FSpatialGDKSpanId& SpanId = {}) override; + virtual void SendComponentUpdate(Worker_EntityId EntityId, FWorkerComponentUpdate* ComponentUpdate, + const FSpatialGDKSpanId& SpanId = {}) override; + virtual Worker_RequestId SendCommandRequest(Worker_EntityId EntityId, Worker_CommandRequest* Request, + const SpatialGDK::FRetryData& RetryData, const FSpatialGDKSpanId& SpanId) override; + virtual void SendCommandResponse(Worker_RequestId RequestId, Worker_CommandResponse* Response, + const FSpatialGDKSpanId& SpanId = {}) override; + virtual void SendCommandFailure(Worker_RequestId RequestId, const FString& Message, const FSpatialGDKSpanId& SpanId = {}) override; + virtual void SendLogMessage(uint8_t Level, const FName& LoggerName, const TCHAR* Message) override; + virtual Worker_RequestId SendEntityQueryRequest(const Worker_EntityQuery* EntityQuery, + const SpatialGDK::FRetryData& RetryData) override; + virtual void SendMetrics(SpatialGDK::SpatialMetrics Metrics) override; + + void Advance(float DeltaTimeS); + bool HasDisconnected() const; + Worker_ConnectionStatusCode GetConnectionStatus() const; + FString GetDisconnectReason() const; - virtual PhysicalWorkerName GetWorkerId() const PURE_VIRTUAL(USpatialWorkerConnection::GetWorkerId, return PhysicalWorkerName();); - virtual const TArray& GetWorkerAttributes() const PURE_VIRTUAL(USpatialWorkerConnection::GetWorkerAttributes, return ReturnValuePlaceholder;); + const SpatialGDK::EntityView& GetView() const; + SpatialGDK::ViewCoordinator& GetCoordinator() const; - virtual void ProcessOutgoingMessages() PURE_VIRTUAL(USpatialWorkerConnection::ProcessOutgoingMessages, return;); - virtual void MaybeFlush() PURE_VIRTUAL(USpatialWorkerConnection::MaybeFlush, return;); - virtual void Flush() PURE_VIRTUAL(USpatialWorkerConnection::Flush, return;); + PhysicalWorkerName GetWorkerId() const; + Worker_EntityId GetWorkerSystemEntityId() const; + + SpatialGDK::CallbackId RegisterComponentAddedCallback(Worker_ComponentId ComponentId, SpatialGDK::FComponentValueCallback Callback); + SpatialGDK::CallbackId RegisterComponentRemovedCallback(Worker_ComponentId ComponentId, SpatialGDK::FComponentValueCallback Callback); + SpatialGDK::CallbackId RegisterComponentValueCallback(Worker_ComponentId ComponentId, SpatialGDK::FComponentValueCallback Callback); + SpatialGDK::CallbackId RegisterAuthorityGainedCallback(Worker_ComponentId ComponentId, SpatialGDK::FEntityCallback Callback); + SpatialGDK::CallbackId RegisterAuthorityLostCallback(Worker_ComponentId ComponentId, SpatialGDK::FEntityCallback Callback); + SpatialGDK::CallbackId RegisterAuthorityLostTempCallback(Worker_ComponentId ComponentId, SpatialGDK::FEntityCallback Callback); + void RemoveCallback(SpatialGDK::CallbackId Id); + + void Flush(); + + void SetStartupComplete(); DECLARE_MULTICAST_DELEGATE_OneParam(FOnEnqueueMessage, const SpatialGDK::FOutgoingMessage*); FOnEnqueueMessage OnEnqueueMessage; @@ -36,7 +83,12 @@ class SPATIALGDK_API USpatialWorkerConnection : public UObject, public SpatialOS DECLARE_MULTICAST_DELEGATE_OneParam(FOnDequeueMessage, const SpatialGDK::FOutgoingMessage*); FOnDequeueMessage OnDequeueMessage; + SpatialGDK::SpatialEventTracer* GetEventTracer() const { return EventTracer; } + private: - // Exists for the sake of having PURE_VIRTUAL functions returning a const ref. - TArray ReturnValuePlaceholder; + static bool IsStartupComponent(Worker_ComponentId Id); + static void ExtractStartupOps(SpatialGDK::OpList& OpList, SpatialGDK::ExtractedOpListData& ExtractedOpList); + bool StartupComplete = false; + SpatialGDK::SpatialEventTracer* EventTracer; + TUniquePtr Coordinator; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/UserSpanId.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/UserSpanId.h new file mode 100644 index 0000000000..ee692f172d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/UserSpanId.h @@ -0,0 +1,22 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "UserSpanId.generated.h" + +USTRUCT(Blueprintable) +struct FUserSpanId +{ + GENERATED_BODY() + + FUserSpanId() {} + explicit FUserSpanId(const TArray& InData) + : Data(InData) + { + } + + UPROPERTY(BlueprintReadOnly, Category = "UserSpanId") + TArray Data; + + bool IsValid() const { return Data.Num() == 16; } +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/WorkerConnectionCoordinator.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/WorkerConnectionCoordinator.h deleted file mode 100644 index 1a645bf728..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/WorkerConnectionCoordinator.h +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#pragma once - -#include "HAL/Event.h" - -struct FEventDeleter -{ - void operator()(FEvent* Event) const - { - FPlatformProcess::ReturnSynchEventToPool(Event); - } -}; - -/** -* The reason this exists is because FEvent::Wait(Time) is not equivilant for -* FPlatformProcess::Sleep and has overhead which impacts latency. -*/ -class WorkerConnectionCoordinator -{ - TUniquePtr Event; - int32 WaitTimeMs; -public: - WorkerConnectionCoordinator(bool bCanWake, int32 InWaitMs) - : Event(bCanWake ? FGenericPlatformProcess::GetSynchEventFromPool() : nullptr) - , WaitTimeMs(InWaitMs) - { - - } - ~WorkerConnectionCoordinator() = default; - - void Wait() - { - if (Event.IsValid()) - { - Event->Wait(WaitTimeMs); - } - else - { - FPlatformProcess::Sleep(WaitTimeMs*0.001f); - } - } - - void Wake() - { - if (Event.IsValid()) - { - Event->Trigger(); - } - } -}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/EntityRPCType.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/EntityRPCType.h new file mode 100644 index 0000000000..6a6baa7a6e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/EntityRPCType.h @@ -0,0 +1,32 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialConstants.h" +#include "Templates/TypeHash.h" +#include + +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))); + } +}; +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/GlobalStateManager.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/GlobalStateManager.h index 8bf2e12d0d..c67351a4f2 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/GlobalStateManager.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/GlobalStateManager.h @@ -2,11 +2,12 @@ #pragma once +#include "Utils/SchemaUtils.h" + #include "CoreMinimal.h" +#include "EngineUtils.h" #include "UObject/NoExportTypes.h" -#include "Utils/SchemaUtils.h" - #include #include @@ -28,15 +29,16 @@ class SPATIALGDK_API UGlobalStateManager : public UObject public: void Init(USpatialNetDriver* InNetDriver); - void ApplyDeploymentMapData(const Worker_ComponentData& Data); - void ApplyStartupActorManagerData(const Worker_ComponentData& Data); + void ApplyDeploymentMapData(Schema_ComponentData* Data); + void ApplyStartupActorManagerData(Schema_ComponentData* Data); - void ApplyDeploymentMapUpdate(const Worker_ComponentUpdate& Update); - void ApplyStartupActorManagerUpdate(const Worker_ComponentUpdate& Update); + void ApplyDeploymentMapUpdate(Schema_ComponentUpdate* Update); + void ApplyStartupActorManagerUpdate(Schema_ComponentUpdate* Update); DECLARE_DELEGATE_OneParam(QueryDelegate, const Worker_EntityQueryResponseOp&); void QueryGSM(const QueryDelegate& Callback); - bool GetAcceptingPlayersAndSessionIdFromQueryResponse(const Worker_EntityQueryResponseOp& Op, bool& OutAcceptingPlayers, int32& OutSessionId); + static bool GetAcceptingPlayersAndSessionIdFromQueryResponse(const Worker_EntityQueryResponseOp& Op, bool& OutAcceptingPlayers, + int32& OutSessionId); void ApplyVirtualWorkerMappingFromQueryResponse(const Worker_EntityQueryResponseOp& Op) const; void ApplyDeploymentMapDataFromQueryResponse(const Worker_EntityQueryResponseOp& Op); @@ -51,8 +53,7 @@ class SPATIALGDK_API UGlobalStateManager : public UObject 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 AuthorityChanged(const Worker_ComponentSetAuthorityChangeOp& AuthChangeOp); void ResetGSM(); @@ -64,6 +65,11 @@ class SPATIALGDK_API UGlobalStateManager : public UObject bool IsReady() const; + void HandleActorBasedOnLoadBalancer(AActor* ActorIterator) const; + + Worker_EntityId GetLocalServerWorkerEntityId() const; + void ClaimSnapshotPartition() const; + Worker_EntityId GlobalStateManagerEntityId; private: @@ -83,15 +89,14 @@ class SPATIALGDK_API UGlobalStateManager : public UObject void OnPrePIEEnded(bool bValue); void ReceiveShutdownMultiProcessRequest(); - void OnShutdownComponentUpdate(const Worker_ComponentUpdate& Update); + void OnShutdownComponentUpdate(Schema_ComponentUpdate* Update); void ReceiveShutdownAdditionalServersEvent(); #endif // WITH_EDITOR + private: void SetDeploymentMapURL(const FString& MapURL); void SendSessionIdUpdate(); - void BecomeAuthoritativeOverAllActors(); - void SetAllActorRolesBasedOnLBStrategy(); void SendCanBeginPlayUpdate(const bool bInCanBeginPlay); #if WITH_EDITOR diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/ClientServerRPCService.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/ClientServerRPCService.h new file mode 100644 index 0000000000..1f4dfd0f2a --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/ClientServerRPCService.h @@ -0,0 +1,90 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" + +#include "Interop/SpatialClassInfoManager.h" +#include "RPCStore.h" +#include "Schema/ClientEndpoint.h" +#include "Schema/RPCPayload.h" +#include "Schema/ServerEndpoint.h" +#include "SpatialView/SubView.h" +#include "Utils/RPCRingBuffer.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogClientServerRPCService, Log, All); + +class USpatialLatencyTracer; +class USpatialStaticComponentView; +class USpatialNetDriver; +struct RPCRingBuffer; + +namespace SpatialGDK +{ +struct FRPCStore; + +struct ClientServerEndpoints +{ + ClientEndpoint Client; + ServerEndpoint Server; +}; + +class SPATIALGDK_API ClientServerRPCService +{ +public: + ClientServerRPCService(const ExtractRPCDelegate InExtractRPCCallback, const FSubView& InSubView, USpatialNetDriver* InNetDriver, + FRPCStore& InRPCStore); + + void AdvanceView(); + void ProcessChanges(); + + // Public state functions for the main Spatial RPC service to expose bookkeeping around overflows and acks. + // Could be moved into RPCStore. Note: Needs revisiting at some point, this is a strange boundary. + bool ContainsOverflowedRPC(const EntityRPCType& EntityRPC) const; + TMap>& GetOverflowedRPCs(); + void AddOverflowedRPC(EntityRPCType EntityType, PendingRPCPayload&& Payload); + void IncrementAckedRPCID(Worker_EntityId EntityId, ERPCType Type); + uint64 GetAckFromView(Worker_EntityId EntityId, ERPCType Type); + +private: + void SetEntityData(Worker_EntityId EntityId); + // Process relevant view delta changes. + void EntityAdded(const Worker_EntityId EntityId); + void ComponentUpdate(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId, Schema_ComponentUpdate* Update); + + // Maintain local state of client server RPCs. + void PopulateDataStore(Worker_EntityId EntityId); + void ApplyComponentUpdate(Worker_EntityId EntityId, Worker_ComponentId ComponentId, Schema_ComponentUpdate* Update); + + // Client server RPC system responses to state changes. + void OnEndpointAuthorityGained(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + void OnEndpointAuthorityLost(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + void ClearOverflowedRPCs(Worker_EntityId EntityId); + + // The component with the given component ID was updated, and so there is an RPC to be handled. + void HandleRPC(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId); + // 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 ExtractRPCsForType(Worker_EntityId EntityId, ERPCType Type); + + // Helpers + const RPCRingBuffer& GetBufferFromView(Worker_EntityId EntityId, ERPCType Type); + static bool IsClientOrServerEndpoint(Worker_ComponentId ComponentId); + + ExtractRPCDelegate ExtractRPCCallback; + const FSubView* SubView; + USpatialNetDriver* NetDriver; + + FRPCStore* RPCStore; + + // Deserialized state store for client/server RPC components. + TMap ClientServerDataStore; + + // Stored here for things we have authority over. + TMap LastAckedRPCIds; + TMap LastSeenRPCIds; + TMap> OverflowedRPCs; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/MulticastRPCService.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/MulticastRPCService.h new file mode 100644 index 0000000000..1bc9ab7f90 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/MulticastRPCService.h @@ -0,0 +1,62 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" + +#include "RPCStore.h" +#include "Schema/MulticastRPCs.h" +#include "Schema/RPCPayload.h" +#include "SpatialView/SubView.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogMulticastRPCService, Log, All); + +class USpatialLatencyTracer; +class USpatialStaticComponentView; +class USpatialNetDriver; +struct RPCRingBuffer; + +namespace SpatialGDK +{ +class SPATIALGDK_API MulticastRPCService +{ +public: + MulticastRPCService(const ExtractRPCDelegate InExtractRPCCallback, const FSubView& InSubView, FRPCStore& InRPCStore); + + void AdvanceView(); + void ProcessChanges(); + +private: + // Process relevant view delta changes. + void EntityAdded(Worker_EntityId EntityId); + void ComponentUpdate(Worker_EntityId EntityId, Worker_ComponentId ComponentId, Schema_ComponentUpdate* Update); + void AuthorityGained(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + void AuthorityLost(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + + // Maintain local state of multicast RPCs. + void PopulateDataStore(Worker_EntityId EntityId); + void ApplyComponentUpdate(Worker_EntityId EntityId, Schema_ComponentUpdate* Update); + + // Multicast system responses to state changes. + void OnCheckoutMulticastRPCComponentOnEntity(Worker_EntityId EntityId); + void OnRemoveMulticastRPCComponentForEntity(Worker_EntityId EntityId); + void OnEndpointAuthorityGained(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + void OnEndpointAuthorityLost(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + + // Calls ExtractRPCCallback for each RPC it extracts from a given component. If the callback returns false, + // stops retrieving RPCs. + void ExtractRPCs(Worker_EntityId EntityId); + + ExtractRPCDelegate ExtractRPCCallback; + const FSubView* SubView; + + FRPCStore* RPCStore; + + // Deserialized state store for multicast components. + TMap MulticastDataStore; + + // This is local, not written into schema. + TMap LastSeenMulticastRPCIds; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPCStore.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPCStore.h new file mode 100644 index 0000000000..6961584000 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/RPCStore.h @@ -0,0 +1,79 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Interop/Connection/SpatialGDKSpanId.h" +#include "Schema/RPCPayload.h" +#include "SpatialConstants.h" +#include "SpatialView/EntityComponentId.h" + +DECLARE_DELEGATE_ThreeParams(ExtractRPCDelegate, const FUnrealObjectRef&, SpatialGDK::RPCPayload, TOptional); + +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, + EntityBeingCreated +}; + +struct PendingUpdate +{ + PendingUpdate(Schema_ComponentUpdate* InUpdate) + : Update(InUpdate) + { + } + + Schema_ComponentUpdate* Update; + TArray SpanIds; +}; + +struct PendingRPCPayload +{ + PendingRPCPayload(const RPCPayload& InPayload) + : Payload(InPayload) + { + } + + RPCPayload Payload; + FSpatialGDKSpanId SpanId; +}; + +struct FRPCStore +{ + Schema_ComponentUpdate* GetOrCreateComponentUpdate(EntityComponentId EntityComponentIdPair, const FSpatialGDKSpanId& SpanId = {}); + Schema_ComponentData* GetOrCreateComponentData(EntityComponentId EntityComponentIdPair); + void AddSpanIdForComponentUpdate(EntityComponentId EntityComponentIdPair, const FSpatialGDKSpanId& SpanId); + + TMap LastSentRPCIds; + TMap PendingComponentUpdatesToSend; + TMap PendingRPCsOnEntityCreation; +}; +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/SpatialRPCService.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/SpatialRPCService.h new file mode 100644 index 0000000000..10dea42a4b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/RPCs/SpatialRPCService.h @@ -0,0 +1,83 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" + +#include "ClientServerRPCService.h" +#include "Interop/Connection/SpatialEventTracer.h" +#include "Interop/SpatialClassInfoManager.h" +#include "MulticastRPCService.h" +#include "RPCStore.h" +#include "Schema/ClientEndpoint.h" +#include "Schema/RPCPayload.h" +#include "SpatialView/SubView.h" +#include "Utils/RPCContainer.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialRPCService, Log, All); + +class USpatialLatencyTracer; +class USpatialStaticComponentView; +class USpatialNetDriver; +struct RPCRingBuffer; + +namespace SpatialGDK +{ +class SPATIALGDK_API SpatialRPCService +{ +public: + explicit SpatialRPCService(const FSubView& InActorAuthSubView, const FSubView& InActorNonAuthSubView, + USpatialLatencyTracer* InSpatialLatencyTracer, SpatialEventTracer* InEventTracer, + USpatialNetDriver* InNetDriver); + + void AdvanceView(); + void ProcessChanges(const float NetDriverTime); + + void ProcessIncomingRPCs(); + + void ProcessOrQueueIncomingRPC(const FUnrealObjectRef& InTargetObjectRef, RPCPayload InPayload, + TOptional RPCIdForLinearEventTrace); + + EPushRPCResult PushRPC(Worker_EntityId EntityId, ERPCType Type, RPCPayload Payload, bool bCreatedEntity, UObject* Target = nullptr, + UFunction* Function = nullptr); + void PushOverflowedRPCs(); + + struct UpdateToSend + { + Worker_EntityId EntityId; + FWorkerComponentUpdate Update; + FSpatialGDKSpanId SpanId; + }; + TArray GetRPCsAndAcksToSend(); + TArray GetRPCComponentsOnEntityCreation(Worker_EntityId EntityId); + + void ClearPendingRPCs(Worker_EntityId EntityId); + +private: + EPushRPCResult PushRPCInternal(Worker_EntityId EntityId, ERPCType Type, PendingRPCPayload Payload, bool bCreatedEntity); + + FRPCErrorInfo ApplyRPC(const FPendingRPCParams& Params); + // Note: It's like applying an RPC, but more secretive + FRPCErrorInfo ApplyRPCInternal(UObject* TargetObject, UFunction* Function, const FPendingRPCParams& PendingRPCParams); + + USpatialNetDriver* NetDriver; + USpatialLatencyTracer* SpatialLatencyTracer; + SpatialEventTracer* EventTracer; + FRPCContainer IncomingRPCs{ ERPCQueueType::Receive }; + + FRPCStore RPCStore; + ClientServerRPCService ClientServerRPCs; + MulticastRPCService MulticastRPCs; + + // Keep around one of the passed subviews here in order to read the main view. + const FSubView* AuthSubView; + + float LastProcessingTime; + +#if TRACE_LIB_ACTIVE + void ProcessResultToLatencyTrace(const EPushRPCResult Result, const TraceKey Trace); + TMap PendingTraces; +#endif +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h index 88bfaca825..18bf35f1df 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h @@ -43,12 +43,12 @@ struct FHandoverPropertyInfo uint16 Handle; int32 Offset; int32 ArrayIdx; - GDK_PROPERTY(Property)* Property; + GDK_PROPERTY(Property) * Property; }; struct FInterestPropertyInfo { - GDK_PROPERTY(Property)* Property; + GDK_PROPERTY(Property) * Property; int32 Offset; }; @@ -88,7 +88,6 @@ class SPATIALGDK_API USpatialClassInfoManager : public UObject GENERATED_BODY() public: - bool TryInit(USpatialNetDriver* InNetDriver); // Checks whether a class is supported and quits the game if not. This is to avoid crashing @@ -109,7 +108,7 @@ class SPATIALGDK_API USpatialClassInfoManager : public UObject Worker_ComponentId GetComponentIdForClass(const UClass& Class) const; TArray GetComponentIdsForClassHierarchy(const UClass& BaseClass, const bool bIncludeDerivedTypes = true) const; - + const FRPCInfo& GetRPCInfo(UObject* Object, UFunction* Function); Worker_ComponentId GetComponentIdFromLevelPath(const FString& LevelPath) const; @@ -122,13 +121,12 @@ class SPATIALGDK_API USpatialClassInfoManager : public UObject 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); + const FClassInfo* GetClassInfoForNewSubobject(const UObject* Object, Worker_EntityId EntityId, + USpatialPackageMapClient* PackageMapClient); UPROPERTY() USchemaDatabase* SchemaDatabase; @@ -148,7 +146,9 @@ class SPATIALGDK_API USpatialClassInfoManager : public UObject UPROPERTY() USpatialNetDriver* NetDriver; - TMap, TSharedRef> ClassInfoMap; + TMap, TSharedRef, FDefaultSetAllocator, + TWeakObjectPtrMapKeyFuncs, TSharedRef, false>> + ClassInfoMap; TMap> ComponentToClassInfoMap; TMap ComponentToOffsetMap; TMap ComponentToCategoryMap; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialConditionMapFilter.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialConditionMapFilter.h index a1fa82b204..26f763435b 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialConditionMapFilter.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialConditionMapFilter.h @@ -46,7 +46,9 @@ class FSpatialConditionMapFilter ConditionMap[COND_InitialOnly] = bIsInitial; ConditionMap[COND_OwnerOnly] = bIsOwner; - ConditionMap[COND_SkipOwner] = !ActorChannel->IsAuthoritativeClient(); // TODO: UNR-3714, this is a best-effort measure, but SkipOwner is currently quite broken + ConditionMap[COND_SkipOwner] = + !ActorChannel + ->IsAuthoritativeClient(); // TODO: UNR-3714, this is a best-effort measure, but SkipOwner is currently quite broken ConditionMap[COND_SimulatedOnly] = bIsSimulated; ConditionMap[COND_SimulatedOnlyNoReplay] = bIsSimulated && !bIsReplay; @@ -64,12 +66,8 @@ class FSpatialConditionMapFilter ConditionMap[COND_Never] = false; } - bool IsRelevant(ELifetimeCondition Condition) const - { - return ConditionMap[Condition]; - } + bool IsRelevant(ELifetimeCondition Condition) const { return ConditionMap[Condition]; } private: bool ConditionMap[COND_Max]; - }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialDispatcher.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialDispatcher.h index 0159646cdc..353e8d82eb 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialDispatcher.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialDispatcher.h @@ -26,20 +26,17 @@ class SPATIALGDK_API SpatialDispatcher public: using FCallbackId = uint32; - void Init(USpatialReceiver* InReceiver, USpatialStaticComponentView* InStaticComponentView, USpatialMetrics* InSpatialMetrics, USpatialWorkerFlags* InSpatialWorkerFlags); - void ProcessOps(const SpatialGDK::OpList& Ops); - - // The following 2 methods should *only* be used by the Startup OpList Queueing flow - // from the SpatialNetDriver, and should be temporary since an alternative solution will be available via the Worker SDK soon. - void MarkOpToSkip(const Worker_Op* Op); - int GetNumOpsToSkip() const; + void Init(USpatialReceiver* InReceiver, USpatialStaticComponentView* InStaticComponentView, USpatialMetrics* InSpatialMetrics, + USpatialWorkerFlags* InSpatialWorkerFlags); + void ProcessOps(const TArray& Ops); // Each callback method returns a callback ID which is incremented for each registration. // ComponentId must be in the range 1000 - 2000. // Callbacks can be deregistered through passing the corresponding callback ID to the RemoveOpCallback function. FCallbackId OnAddComponent(Worker_ComponentId ComponentId, const TFunction& Callback); FCallbackId OnRemoveComponent(Worker_ComponentId ComponentId, const TFunction& Callback); - FCallbackId OnAuthorityChange(Worker_ComponentId ComponentId, const TFunction& Callback); + FCallbackId OnAuthorityChange(Worker_ComponentId ComponentId, + const TFunction& Callback); FCallbackId OnComponentUpdate(Worker_ComponentId ComponentId, const TFunction& Callback); FCallbackId OnCommandRequest(Worker_ComponentId ComponentId, const TFunction& Callback); FCallbackId OnCommandResponse(Worker_ComponentId ComponentId, const TFunction& Callback); @@ -60,9 +57,10 @@ class SPATIALGDK_API SpatialDispatcher using OpTypeToCallbacksMap = TMap>; - bool IsExternalSchemaOp(Worker_Op* Op) const; - void ProcessExternalSchemaOp(Worker_Op* Op); - FCallbackId AddGenericOpCallback(Worker_ComponentId ComponentId, Worker_OpType OpType, const TFunction& Callback); + bool IsExternalSchemaOp(const Worker_Op& Op) const; + void ProcessExternalSchemaOp(const Worker_Op& Op); + FCallbackId AddGenericOpCallback(Worker_ComponentId ComponentId, Worker_OpType OpType, + const TFunction& Callback); void RunCallbacks(Worker_ComponentId ComponentId, const Worker_Op* Op); TWeakObjectPtr Receiver; @@ -73,11 +71,10 @@ class SPATIALGDK_API SpatialDispatcher 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. - // RunCallbacks is called by the SpatialDispatcher and executes all user registered + // CallbackIds enable you to deregister callbacks using the RemoveOpCallback function. + // RunCallbacks is called by the SpatialDispatcher and executes all user registered // callbacks for the matching component ID and network operation type. FCallbackId NextCallbackId; TMap ComponentOpTypeToCallbacksMap; TMap CallbackIdToDataMap; - TArray OpsToSkip; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialInterestConstraints.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialInterestConstraints.h index d1cadc43e4..10d5d14063 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialInterestConstraints.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialInterestConstraints.h @@ -2,8 +2,8 @@ #pragma once -#include "CoreMinimal.h" #include "Components/ActorComponent.h" +#include "CoreMinimal.h" #include "Templates/SubclassOf.h" #include "SpatialInterestConstraints.generated.h" @@ -63,7 +63,8 @@ class SPATIALGDK_API UAbstractQueryConstraint : public UObject UAbstractQueryConstraint() = default; virtual ~UAbstractQueryConstraint() = default; - virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const PURE_VIRTUAL(UAbstractQueryConstraint::CreateConstraint, ); + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const + PURE_VIRTUAL(UAbstractQueryConstraint::CreateConstraint, ); }; /** @@ -77,11 +78,12 @@ class SPATIALGDK_API UOrConstraint final : public UAbstractQueryConstraint UOrConstraint() = default; ~UOrConstraint() = default; - virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, + SpatialGDK::QueryConstraint& OutConstraint) const override; /** Entities captured by any subconstraints will be included in interest results. */ UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Instanced, Category = "Or Constraint") - TArray Constraints; + TArray Constraints; }; /** @@ -95,11 +97,12 @@ class SPATIALGDK_API UAndConstraint final : public UAbstractQueryConstraint UAndConstraint() = default; ~UAndConstraint() = default; - virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, + SpatialGDK::QueryConstraint& OutConstraint) const override; /** Entities captured by all subconstraints will be included in interest results. */ UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Instanced, Category = "And Constraint") - TArray Constraints; + TArray Constraints; }; /** @@ -113,7 +116,8 @@ class SPATIALGDK_API USphereConstraint final : public UAbstractQueryConstraint USphereConstraint() = default; ~USphereConstraint() = default; - virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, + SpatialGDK::QueryConstraint& OutConstraint) const override; /** The location in the world that this constraint is relative to. */ UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Sphere Constraint") @@ -135,7 +139,8 @@ class SPATIALGDK_API UCylinderConstraint final : public UAbstractQueryConstraint UCylinderConstraint() = default; ~UCylinderConstraint() = default; - virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, + SpatialGDK::QueryConstraint& OutConstraint) const override; /** The location in the world that this constraint is relative to. */ UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Cylinder Constraint") @@ -157,7 +162,8 @@ class SPATIALGDK_API UBoxConstraint final : public UAbstractQueryConstraint UBoxConstraint() = default; ~UBoxConstraint() = default; - virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, + SpatialGDK::QueryConstraint& OutConstraint) const override; /** The location in the world that this constraint is relative to. */ UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Box Constraint") @@ -179,7 +185,8 @@ class SPATIALGDK_API URelativeSphereConstraint final : public UAbstractQueryCons URelativeSphereConstraint() = default; ~URelativeSphereConstraint() = default; - virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, + SpatialGDK::QueryConstraint& OutConstraint) const override; /** The size of the sphere represented by this constraint in centimeters. */ UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Meta = (ClampMin = 0.0), Category = "Relative Sphere Constraint") @@ -197,7 +204,8 @@ class SPATIALGDK_API URelativeCylinderConstraint final : public UAbstractQueryCo URelativeCylinderConstraint() = default; ~URelativeCylinderConstraint() = default; - virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, + SpatialGDK::QueryConstraint& OutConstraint) const override; /** The size of the cylinder represented by this constraint in centimeters. */ UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Meta = (ClampMin = 0.0), Category = "Relative Cylinder Constraint") @@ -215,7 +223,8 @@ class SPATIALGDK_API URelativeBoxConstraint final : public UAbstractQueryConstra URelativeBoxConstraint() = default; ~URelativeBoxConstraint() = default; - virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, + SpatialGDK::QueryConstraint& OutConstraint) const override; /** The size of the box represented by this constraint. */ UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Meta = (ClampMin = 0.0), Category = "Relative Box Constraint") @@ -233,7 +242,8 @@ class SPATIALGDK_API UCheckoutRadiusConstraint final : public UAbstractQueryCons UCheckoutRadiusConstraint() = default; ~UCheckoutRadiusConstraint() = default; - virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, + SpatialGDK::QueryConstraint& OutConstraint) const override; /** The base type of actor that this constraint will capture. */ UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Checkout Radius Constraint") @@ -255,7 +265,8 @@ class SPATIALGDK_API UActorClassConstraint final : public UAbstractQueryConstrai UActorClassConstraint() = default; ~UActorClassConstraint() = default; - virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, + SpatialGDK::QueryConstraint& OutConstraint) const override; /** The base type of actor that this constraint will capture. */ UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Actor Class Constraint") @@ -277,7 +288,8 @@ class SPATIALGDK_API UComponentClassConstraint final : public UAbstractQueryCons UComponentClassConstraint() = default; ~UComponentClassConstraint() = default; - virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, + SpatialGDK::QueryConstraint& OutConstraint) const override; /** The base type of component that this constraint will capture. */ UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Component Class Constraint") diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialOSDispatcherInterface.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialOSDispatcherInterface.h index 8f9a2e48e2..633c7bbc09 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialOSDispatcherInterface.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialOSDispatcherInterface.h @@ -13,6 +13,7 @@ DECLARE_DELEGATE_OneParam(EntityQueryDelegate, const Worker_EntityQueryResponseOp&); DECLARE_DELEGATE_OneParam(ReserveEntityIDsDelegate, const Worker_ReserveEntityIdsResponseOp&); DECLARE_DELEGATE_OneParam(CreateEntityDelegate, const Worker_CreateEntityResponseOp&); +DECLARE_DELEGATE_OneParam(SystemEntityCommandDelegate, const Worker_CommandResponseOp&); DECLARE_MULTICAST_DELEGATE_OneParam(FOnEntityAddedDelegate, const Worker_EntityId); DECLARE_MULTICAST_DELEGATE_OneParam(FOnEntityRemovedDelegate, const Worker_EntityId); @@ -25,21 +26,37 @@ class SpatialOSDispatcherInterface 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 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;); + virtual void DropQueuedRemoveComponentOpsForEntity(Worker_EntityId EntityId) + PURE_VIRTUAL(SpatialOSDispatcherInterface::DropQueuedRemoveComponentOpsForEntity, return;); + virtual void OnAuthorityChange(const Worker_ComponentSetAuthorityChangeOp& 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 void OnSystemEntityCommandResponse(const Worker_CommandResponseOp& Op) + PURE_VIRTUAL(SpatialOSDispatcherInterface::OnSystemEntityCommandResponse, return;); + virtual bool OnExtractIncomingRPC(Worker_EntityId EntityId, ERPCType RPCType, const SpatialGDK::RPCPayload& Payload) + PURE_VIRTUAL(SpatialOSDispatcherInterface::OnExtractIncomingRPC, return false;); + virtual void OnCommandRequest(const Worker_Op& Op) PURE_VIRTUAL(SpatialOSDispatcherInterface::OnCommandRequest, return;); + virtual void OnCommandResponse(const Worker_Op& Op) PURE_VIRTUAL(SpatialOSDispatcherInterface::OnCommandResponse, return;); + virtual void OnReserveEntityIdsResponse(const Worker_ReserveEntityIdsResponseOp& Op) + PURE_VIRTUAL(SpatialOSDispatcherInterface::OnReserveEntityIdsResponse, return;); + virtual void OnCreateEntityResponse(const Worker_Op& 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;); + virtual void AddSystemEntityCommandDelegate(Worker_RequestId RequestId, SystemEntityCommandDelegate Delegate) + PURE_VIRTUAL(SpatialOSDispatcherInterface::AddSystemEntityCommandDelegate, return;); }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialOutputDevice.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialOutputDevice.h index 5421bde591..613c594d94 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialOutputDevice.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialOutputDevice.h @@ -17,13 +17,15 @@ class SPATIALGDK_API FSpatialOutputDevice : public FOutputDevice void AddRedirectCategory(const FName& Category); void RemoveRedirectCategory(const FName& Category); - void SetVerbosityFilterLevel(ELogVerbosity::Type Verbosity); + void SetVerbosityLocalFilterLevel(ELogVerbosity::Type Verbosity); + void SetVerbosityCloudFilterLevel(ELogVerbosity::Type Verbosity); void Serialize(const TCHAR* InData, ELogVerbosity::Type Verbosity, const FName& Category) override; static Worker_LogLevel ConvertLogLevelToSpatial(ELogVerbosity::Type Verbosity); protected: - ELogVerbosity::Type FilterLevel; + ELogVerbosity::Type LocalFilterLevel; + ELogVerbosity::Type CloudFilterLevel; TSet CategoriesToRedirect; USpatialWorkerConnection* Connection; FName LoggerName; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialPlayerSpawner.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialPlayerSpawner.h index 88630ef9c0..d4d2763e8c 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialPlayerSpawner.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialPlayerSpawner.h @@ -9,8 +9,8 @@ #include "Templates/UniquePtr.h" #include "UObject/NoExportTypes.h" -#include #include +#include #include "SpatialPlayerSpawner.generated.h" @@ -27,8 +27,7 @@ class SPATIALGDK_API USpatialPlayerSpawner : public UObject GENERATED_BODY() public: - - void Init(USpatialNetDriver* NetDriver, FTimerManager* TimerManager); + void Init(USpatialNetDriver* NetDriver); // Client void SendPlayerSpawnRequest(); @@ -60,9 +59,11 @@ class SPATIALGDK_API USpatialPlayerSpawner : public UObject SpatialGDK::SpawnPlayerRequest ObtainPlayerParams() const; // Authoritative server worker - void FindPlayerStartAndProcessPlayerSpawn(Schema_Object* Request, const PhysicalWorkerName& ClientWorkerId); - void ForwardSpawnRequestToStrategizedServer(const Schema_Object* OriginalPlayerSpawnRequest, AActor* PlayerStart, const PhysicalWorkerName& ClientWorkerId, const VirtualWorkerId SpawningVirtualWorker); - void RetryForwardSpawnPlayerRequest(const Worker_EntityId EntityId, const Worker_RequestId RequestId, const bool bShouldTryDifferentPlayerStart = false); + void FindPlayerStartAndProcessPlayerSpawn(Schema_Object* Request, const Worker_EntityId& ClientWorkerId); + void ForwardSpawnRequestToStrategizedServer(const Schema_Object* OriginalPlayerSpawnRequest, AActor* PlayerStart, + const Worker_EntityId& ClientWorkerId, const VirtualWorkerId SpawningVirtualWorker); + void RetryForwardSpawnPlayerRequest(const Worker_EntityId EntityId, const Worker_RequestId RequestId, + const bool bShouldTryDifferentPlayerStart = false); // Any server void PassSpawnRequestToNetDriver(const Schema_Object* PlayerSpawnData, AActor* PlayerStart); @@ -70,9 +71,7 @@ class SPATIALGDK_API USpatialPlayerSpawner : public UObject UPROPERTY() USpatialNetDriver* NetDriver; - FTimerManager* TimerManager; - int NumberOfAttempts; TMap> OutgoingForwardPlayerSpawnRequests; - TSet WorkersWithPlayersSpawned; + TSet WorkersWithPlayersSpawned; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialRPCService.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialRPCService.h deleted file mode 100644 index 8425131854..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialRPCService.h +++ /dev/null @@ -1,127 +0,0 @@ -// 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 USpatialLatencyTracer; -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, - EntityBeingCreated -}; - -class SPATIALGDK_API SpatialRPCService -{ -public: - SpatialRPCService(ExtractRPCDelegate ExtractRPCCallback, const USpatialStaticComponentView* View, USpatialLatencyTracer* SpatialLatencyTracer); - - EPushRPCResult PushRPC(Worker_EntityId EntityId, ERPCType Type, RPCPayload Payload, bool bCreatedEntity); - void PushOverflowedRPCs(); - - struct UpdateToSend - { - Worker_EntityId EntityId; - FWorkerComponentUpdate Update; - }; - TArray GetRPCsAndAcksToSend(); - TArray GetRPCComponentsOnEntityCreation(Worker_EntityId EntityId); - - // 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); - - // Will also store acked IDs locally. - void IncrementAckedRPCID(Worker_EntityId EntityId, ERPCType Type); - - 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, bool bCreatedEntity); - - 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; - USpatialLatencyTracer* SpatialLatencyTracer; - - // This is local, not written into schema. - TMap LastSeenMulticastRPCIds; - TMap LastSeenRPCIds; - - // Stored here for things we have authority over. - TMap LastAckedRPCIds; - TMap LastSentRPCIds; - - TMap PendingRPCsOnEntityCreation; - - TMap PendingComponentUpdatesToSend; - TMap> OverflowedRPCs; - -#if TRACE_LIB_ACTIVE - void ProcessResultToLatencyTrace(const EPushRPCResult Result, const TraceKey Trace); - TMap PendingTraces; -#endif -}; - -} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialReceiver.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialReceiver.h index 8395d7a45c..cec130403a 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialReceiver.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialReceiver.h @@ -7,19 +7,17 @@ #include "EngineClasses/SpatialActorChannel.h" #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" +#include "Interop/RPCs/SpatialRPCService.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" #include "Schema/UnrealObjectRef.h" #include "SpatialCommonTypes.h" #include "SpatialView/OpList/EntityComponentOpList.h" #include "Utils/GDKPropertyMacros.h" -#include "Utils/RPCContainer.h" #include #include @@ -33,11 +31,21 @@ class USpatialSender; class UGlobalStateManager; class SpatialLoadBalanceEnforcer; +namespace SpatialGDK +{ +class SpatialEventTracer; +} // namespace SpatialGDK + struct PendingAddComponentWrapper { PendingAddComponentWrapper() = default; - PendingAddComponentWrapper(Worker_EntityId InEntityId, Worker_ComponentId InComponentId, TUniquePtr&& InData) - : EntityId(InEntityId), ComponentId(InComponentId), Data(MoveTemp(InData)) {} + PendingAddComponentWrapper(Worker_EntityId InEntityId, Worker_ComponentId InComponentId, + TUniquePtr&& InData) + : EntityId(InEntityId) + , ComponentId(InComponentId) + , Data(MoveTemp(InData)) + { + } // We define equality to cover just entity and component IDs since duplicated AddComponent ops // will be moved into unique pointers and we cannot equate the underlying Worker_ComponentData. @@ -57,7 +65,8 @@ class USpatialReceiver : public UObject, public SpatialOSDispatcherInterface GENERATED_BODY() public: - void Init(USpatialNetDriver* NetDriver, FTimerManager* InTimerManager, SpatialGDK::SpatialRPCService* InRPCService); + void Init(USpatialNetDriver* NetDriver, FTimerManager* InTimerManager, SpatialGDK::SpatialRPCService* InRPCService, + SpatialGDK::SpatialEventTracer* InEventTracer); // Dispatcher Calls virtual void OnCriticalSection(bool InCriticalSection) override; @@ -67,18 +76,15 @@ class USpatialReceiver : public UObject, public SpatialOSDispatcherInterface 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 OnAuthorityChange(const Worker_ComponentSetAuthorityChangeOp& 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 OnCommandRequest(const Worker_Op& Op) override; + virtual void OnCommandResponse(const Worker_Op& Op) override; virtual void OnReserveEntityIdsResponse(const Worker_ReserveEntityIdsResponseOp& Op) override; - virtual void OnCreateEntityResponse(const Worker_CreateEntityResponseOp& Op) override; + virtual void OnCreateEntityResponse(const Worker_Op& Op) override; virtual void AddPendingActorRequest(Worker_RequestId RequestId, USpatialActorChannel* Channel) override; virtual void AddPendingReliableRPC(Worker_RequestId RequestId, TSharedRef ReliableRPC) override; @@ -86,26 +92,26 @@ class USpatialReceiver : public UObject, public SpatialOSDispatcherInterface 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 AddSystemEntityCommandDelegate(Worker_RequestId RequestId, SystemEntityCommandDelegate Delegate) override; virtual void OnEntityQueryResponse(const Worker_EntityQueryResponseOp& Op) override; + virtual void OnSystemEntityCommandResponse(const Worker_CommandResponseOp& Op) override; + void ResolvePendingOperations(UObject* Object, const FUnrealObjectRef& ObjectRef); void FlushRetryRPCs(); - void OnDisconnect(Worker_DisconnectOp& Op); + void OnDisconnect(uint8 StatusCode, const FString& Reason); void RemoveActor(Worker_EntityId EntityId); bool IsPendingOpsOnChannel(USpatialActorChannel& Channel); - void ClearPendingRPCs(Worker_EntityId EntityId); - void CleanupRepStateMap(FSpatialObjectRepState& Replicator); void MoveMappedObjectToUnmapped(const FUnrealObjectRef&); void RetireWhenAuthoritive(Worker_EntityId EntityId, Worker_ComponentId ActorClassId, bool bIsNetStartup, bool bNeedsTearOff); - FRPCErrorInfo ApplyRPC(const FPendingRPCParams& Params); - + void ProcessActorsFromAsyncLoading(); private: void EnterCriticalSection(); void LeaveCriticalSection(); @@ -113,50 +119,46 @@ class USpatialReceiver : public UObject, public SpatialOSDispatcherInterface void ReceiveActor(Worker_EntityId EntityId); void DestroyActor(AActor* Actor, Worker_EntityId EntityId); - AActor* TryGetOrCreateActor(SpatialGDK::UnrealMetadata* UnrealMetadata, SpatialGDK::SpawnData* SpawnData, SpatialGDK::NetOwningClientWorker* NetOwningClientWorkerData); - AActor* CreateActor(SpatialGDK::UnrealMetadata* UnrealMetadata, SpatialGDK::SpawnData* SpawnData, SpatialGDK::NetOwningClientWorker* NetOwningClientWorkerData); + AActor* TryGetOrCreateActor(SpatialGDK::UnrealMetadata* UnrealMetadata, SpatialGDK::SpawnData* SpawnData, + SpatialGDK::NetOwningClientWorker* NetOwningClientWorkerData); + AActor* CreateActor(SpatialGDK::UnrealMetadata* UnrealMetadata, SpatialGDK::SpawnData* SpawnData, + SpatialGDK::NetOwningClientWorker* NetOwningClientWorkerData); USpatialActorChannel* GetOrRecreateChannelForDomantActor(AActor* Actor, Worker_EntityId EntityID); void ProcessRemoveComponent(const Worker_RemoveComponentOp& Op); static FTransform GetRelativeSpawnTransform(UClass* ActorClass, FTransform SpawnTransform); - void HandlePlayerLifecycleAuthority(const Worker_AuthorityChangeOp& Op, class APlayerController* PlayerController); - void HandleActorAuthority(const Worker_AuthorityChangeOp& Op); - - 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 HandlePlayerLifecycleAuthority(const Worker_ComponentSetAuthorityChangeOp& Op, class APlayerController* PlayerController); + void HandleActorAuthority(const Worker_ComponentSetAuthorityChangeOp& Op); - void ApplyComponentDataOnActorCreation(Worker_EntityId EntityId, const Worker_ComponentData& Data, USpatialActorChannel& Channel, const FClassInfo& ActorClassInfo, TArray& OutObjectsToResolve); + 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); - void HandleIndividualAddComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, TUniquePtr Data); + void HandleIndividualAddComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, + TUniquePtr Data); 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 ApplyRPCInternal(UObject* TargetObject, UFunction* Function, const FPendingRPCParams& PendingRPCParams); - - void ReceiveCommandResponse(const Worker_CommandResponseOp& Op); + void ReceiveCommandResponse(const Worker_Op& Op); bool IsReceivedEntityTornOff(Worker_EntityId EntityId); - void ProcessOrQueueIncomingRPC(const FUnrealObjectRef& InTargetObjectRef, SpatialGDK::RPCPayload InPayload); - void ResolveIncomingOperations(UObject* Object, const FUnrealObjectRef& ObjectRef); - void ResolveObjectReferences(FRepLayout& RepLayout, UObject* ReplicatedObject, FSpatialObjectRepState& RepState, FObjectReferencesMap& ObjectReferencesMap, uint8* RESTRICT StoredData, uint8* RESTRICT Data, int32 MaxAbsOffset, TArray& RepNotifies, bool& bOutSomeObjectsWereMapped); + void ResolveObjectReferences(FRepLayout& RepLayout, UObject* ReplicatedObject, FSpatialObjectRepState& RepState, + FObjectReferencesMap& ObjectReferencesMap, uint8* RESTRICT StoredData, uint8* RESTRICT Data, + int32 MaxAbsOffset, TArray& RepNotifies, bool& bOutSomeObjectsWereMapped); - void ProcessQueuedActorRPCsOnEntityCreation(Worker_EntityId EntityId, SpatialGDK::RPCsOnEntityCreation& QueuedRPCs); void UpdateShadowData(Worker_EntityId EntityId); TWeakObjectPtr PopPendingActorRequest(Worker_RequestId RequestId); void OnHeartbeatComponentUpdate(const Worker_ComponentUpdateOp& Op); void CloseClientConnection(USpatialNetConnection* ClientConnection, Worker_EntityId PlayerControllerEntityId); - void PeriodicallyProcessIncomingRPCs(); - // 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); @@ -168,7 +170,7 @@ class USpatialReceiver : public UObject, public SpatialOSDispatcherInterface void QueueAddComponentOpForAsyncLoad(const Worker_AddComponentOp& Op); void QueueRemoveComponentOpForAsyncLoad(const Worker_RemoveComponentOp& Op); - void QueueAuthorityOpForAsyncLoad(const Worker_AuthorityChangeOp& Op); + void QueueAuthorityOpForAsyncLoad(const Worker_ComponentSetAuthorityChangeOp& Op); void QueueComponentUpdateOpForAsyncLoad(const Worker_ComponentUpdateOp& Op); TArray ExtractAddComponents(Worker_EntityId Entity); @@ -183,20 +185,21 @@ class USpatialReceiver : public UObject, public SpatialOSDispatcherInterface bool bInCriticalSection; TArray PendingAddActors; - TArray PendingAuthorityChanges; + TArray PendingAuthorityChanges; TArray PendingAddComponents; }; void HandleQueuedOpForAsyncLoad(const Worker_Op& Op); // END TODO -public: - TMap, TSharedRef> PendingEntitySubobjectDelegations; + void ReceiveWorkerDisconnectResponse(const Worker_CommandResponseOp& Op); + void ReceiveClaimPartitionResponse(const Worker_CommandResponseOp& Op); +public: FOnEntityAddedDelegate OnEntityAddedDelegate; FOnEntityRemovedDelegate OnEntityRemovedDelegate; - FRPCContainer& GetRPCContainer() { return IncomingRPCs; } + TMap PendingPartitionAssignments; private: UPROPERTY() @@ -217,8 +220,6 @@ class USpatialReceiver : public UObject, public SpatialOSDispatcherInterface UPROPERTY() UGlobalStateManager* GlobalStateManager; - SpatialLoadBalanceEnforcer* LoadBalanceEnforcer; - FTimerManager* TimerManager; SpatialGDK::SpatialRPCService* RPCService; @@ -230,11 +231,9 @@ class USpatialReceiver : public UObject, public SpatialOSDispatcherInterface // Useful to manage entities going in and out of interest, in order to recover references to actors. FObjectToRepStateMap ObjectRefToRepStateMap; - FRPCContainer IncomingRPCs{ ERPCQueueType::Receive }; - bool bInCriticalSection; TArray PendingAddActors; - TArray PendingAuthorityChanges; + TArray PendingAuthorityChanges; TArray PendingAddComponents; TArray QueuedRemoveComponentOps; @@ -244,6 +243,7 @@ class USpatialReceiver : public UObject, public SpatialOSDispatcherInterface TMap EntityQueryDelegates; TMap ReserveEntityIDsDelegates; TMap CreateEntityDelegates; + TMap SystemEntityCommandDelegates; // This will map PlayerController entities to the corresponding SpatialNetConnection // for PlayerControllers that this server has authority over. This is used for player @@ -262,6 +262,7 @@ class USpatialReceiver : public UObject, public SpatialOSDispatcherInterface }; TMap EntitiesWaitingForAsyncLoad; TMap> AsyncLoadingPackages; + TSet LoadedPackages; // END TODO struct DeferredRetire @@ -275,4 +276,7 @@ class USpatialReceiver : public UObject, public SpatialOSDispatcherInterface bool HasEntityBeenRequestedForDelete(Worker_EntityId EntityId); void HandleDeferredEntityDeletion(const DeferredRetire& Retire); void HandleEntityDeletedAuthority(Worker_EntityId EntityId); + bool IsDynamicSubObject(AActor* Actor, uint32 SubObjectOffset); + + SpatialGDK::SpatialEventTracer* EventTracer; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSender.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSender.h index 1e066ed38f..7fe0fd50cd 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSender.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSender.h @@ -2,16 +2,16 @@ #pragma once -#include "CoreMinimal.h" - #include "EngineClasses/SpatialLoadBalanceEnforcer.h" #include "EngineClasses/SpatialNetBitWriter.h" +#include "Interop/RPCs/SpatialRPCService.h" #include "Interop/SpatialClassInfoManager.h" -#include "Interop/SpatialRPCService.h" #include "Schema/RPCPayload.h" -#include "TimerManager.h" -#include "Utils/RepDataUtils.h" #include "Utils/RPCContainer.h" +#include "Utils/RepDataUtils.h" + +#include "CoreMinimal.h" +#include "TimerManager.h" #include #include @@ -29,9 +29,15 @@ class USpatialStaticComponentView; class USpatialClassInfoManager; class USpatialWorkerConnection; +namespace SpatialGDK +{ +class SpatialEventTracer; +} + struct FReliableRPCForRetry { - FReliableRPCForRetry(UObject* InTargetObject, UFunction* InFunction, Worker_ComponentId InComponentId, Schema_FieldId InRPCIndex, const TArray& InPayload, int InRetryIndex); + FReliableRPCForRetry(UObject* InTargetObject, UFunction* InFunction, Worker_ComponentId InComponentId, Schema_FieldId InRPCIndex, + const TArray& InPayload, int InRetryIndex, const FSpatialGDKSpanId& InSpanId); TWeakObjectPtr TargetObject; UFunction* Function; @@ -41,6 +47,7 @@ struct FReliableRPCForRetry int Attempts; // For reliable RPCs int RetryIndex; // Index for ordering reliable RPCs on subsequent tries + FSpatialGDKSpanId SpanId; }; struct FPendingRPC @@ -57,9 +64,9 @@ 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, SpatialGDK::RPCsOnEntityCreation>; using FUpdatesQueuedUntilAuthority = TMap>; -using FChannelsToUpdatePosition = TSet>; +using FChannelsToUpdatePosition = + TSet, TWeakObjectPtrKeyFuncs, false>>; UCLASS() class SPATIALGDK_API USpatialSender : public UObject @@ -67,26 +74,30 @@ class SPATIALGDK_API USpatialSender : public UObject GENERATED_BODY() public: - void Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimerManager, SpatialGDK::SpatialRPCService* InRPCService); + void Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimerManager, SpatialGDK::SpatialRPCService* InRPCService, + SpatialGDK::SpatialEventTracer* InEventTracer); // Actor Updates - void SendComponentUpdates(UObject* Object, const FClassInfo& Info, USpatialActorChannel* Channel, const FRepChangeState* RepChanges, const FHandoverChangeState* HandoverChanges, uint32& OutBytesWritten); + void SendComponentUpdates(UObject* Object, const FClassInfo& Info, USpatialActorChannel* Channel, const FRepChangeState* RepChanges, + const FHandoverChangeState* HandoverChanges, uint32& OutBytesWritten); void SendPositionUpdate(Worker_EntityId EntityId, const FVector& Location); - void SendAuthorityIntentUpdate(const AActor& Actor, VirtualWorkerId NewAuthoritativeVirtualWorkerId); - void SetAclWriteAuthority(const SpatialLoadBalanceEnforcer::AclWriteAuthorityRequest& Request); + + void SendAuthorityIntentUpdate(const AActor& Actor, VirtualWorkerId NewAuthoritativeVirtualWorkerId) const; FRPCErrorInfo SendRPC(const FPendingRPCParams& Params); - void SendOnEntityCreationRPC(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef); - void SendCrossServerRPC(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef); - FRPCErrorInfo SendLegacyRPC(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef); - bool SendRingBufferedRPC(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef); - void SendCommandResponse(Worker_RequestId RequestId, Worker_CommandResponse& Response); - void SendEmptyCommandResponse(Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, Worker_RequestId RequestId); - void SendCommandFailure(Worker_RequestId RequestId, const FString& Message); + void SendCrossServerRPC(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, + USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef); + bool SendRingBufferedRPC(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, + USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef); + void SendCommandResponse(Worker_RequestId RequestId, Worker_CommandResponse& Response, const FSpatialGDKSpanId& CauseSpanId); + void SendEmptyCommandResponse(Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, Worker_RequestId RequestId, + const FSpatialGDKSpanId& CauseSpanId); + void SendCommandFailure(Worker_RequestId RequestId, const FString& Message, const FSpatialGDKSpanId& CauseSpanId); 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 SendInterestBucketComponentChange(const Worker_EntityId EntityId, const Worker_ComponentId OldComponent, + const Worker_ComponentId NewComponent); void SendActorTornOffUpdate(Worker_EntityId EntityId, Worker_ComponentId ComponentId); void SendCreateEntityRequest(USpatialActorChannel* Channel, uint32& OutBytesWritten); @@ -95,12 +106,6 @@ class SPATIALGDK_API USpatialSender : public UObject // 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); - - void SendClientEndpointReadyUpdate(Worker_EntityId EntityId); - void SendServerEndpointReadyUpdate(Worker_EntityId EntityId); - void EnqueueRetryRPC(TSharedRef RetryRPC); void FlushRetryRPCs(); void RetryReliableRPC(TSharedRef RetryRPC); @@ -108,7 +113,6 @@ class SPATIALGDK_API USpatialSender : public UObject void RegisterChannelForPositionUpdate(USpatialActorChannel* Channel); void ProcessPositionUpdates(); - void UpdateClientAuthoritativeComponentAclEntries(Worker_EntityId EntityId, const FString& OwnerWorkerAttribute); void UpdateInterestComponent(AActor* Actor); void ProcessOrQueueOutgoingRPC(const FUnrealObjectRef& InTargetObjectRef, SpatialGDK::RPCPayload&& InPayload); @@ -116,19 +120,22 @@ class SPATIALGDK_API USpatialSender : public UObject void FlushRPCService(); - SpatialGDK::RPCPayload CreateRPCPayloadFromParams(UObject* TargetObject, const FUnrealObjectRef& TargetObjectRef, UFunction* Function, void* Params); - void GainAuthorityThenAddComponent(USpatialActorChannel* Channel, UObject* Object, const FClassInfo* Info); + SpatialGDK::RPCPayload CreateRPCPayloadFromParams(UObject* TargetObject, const FUnrealObjectRef& TargetObjectRef, UFunction* Function, + void* Params); // Creates an entity authoritative on this server worker, ensuring it will be able to receive updates for the GSM. UFUNCTION() void CreateServerWorkerEntity(); - void RetryServerWorkerEntityCreation(Worker_EntityId EntityId, int AttemptCounte); - void UpdateServerWorkerEntityInterestAndPosition(); + void RetryServerWorkerEntityCreation(Worker_EntityId EntityId, int AttemptCounter); + + void UpdatePartitionEntityInterestAndPosition(); void ClearPendingRPCs(const Worker_EntityId EntityId); bool ValidateOrExit_IsSupportedClass(const FString& PathName); + void SendClaimPartitionRequest(Worker_EntityId SystemWorkerEntityId, Worker_PartitionId PartitionId) const; + private: // Create a copy of an array of components. Deep copies all Schema_ComponentData. static TArray CopyEntityComponentData(const TArray& EntityComponents); @@ -149,9 +156,10 @@ class SPATIALGDK_API USpatialSender : public UObject // RPC Construction FSpatialNetBitWriter PackRPCDataToSpatialNetBitWriter(UFunction* Function, void* Parameters) const; - Worker_CommandRequest CreateRPCCommandRequest(UObject* TargetObject, const SpatialGDK::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); - FWorkerComponentUpdate CreateRPCEventUpdate(UObject* TargetObject, const SpatialGDK::RPCPayload& Payload, Worker_ComponentId ComponentId, Schema_FieldId EventIndext); // RPC Tracking #if !UE_BUILD_SHIPPING @@ -182,11 +190,12 @@ class SPATIALGDK_API USpatialSender : public UObject SpatialGDK::SpatialRPCService* RPCService; FRPCContainer OutgoingRPCs{ ERPCQueueType::Send }; - FRPCsOnEntityCreationMap OutgoingOnCreateEntityRPCs; TArray> RetryRPCs; FUpdatesQueuedUntilAuthority UpdatesQueuedUntilAuthorityMap; FChannelsToUpdatePosition ChannelsToUpdatePosition; + + SpatialGDK::SpatialEventTracer* EventTracer; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialStaticComponentView.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialStaticComponentView.h index f533d30668..4e12bfc3ec 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialStaticComponentView.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialStaticComponentView.h @@ -3,7 +3,6 @@ #pragma once #include "Schema/Component.h" -#include "Schema/StandardLibrary.h" #include "SpatialConstants.h" #include @@ -22,6 +21,7 @@ class SPATIALGDK_API USpatialStaticComponentView : public UObject public: bool HasAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId) const; + bool HasEntity(Worker_EntityId EntityId) const; template T* GetComponentData(Worker_EntityId EntityId) const @@ -43,7 +43,7 @@ class SPATIALGDK_API USpatialStaticComponentView : public UObject void OnRemoveComponent(const Worker_RemoveComponentOp& Op); void OnRemoveEntity(Worker_EntityId EntityId); void OnComponentUpdate(const Worker_ComponentUpdateOp& Op); - void OnAuthorityChange(const Worker_AuthorityChangeOp& Op); + void OnAuthorityChange(const Worker_ComponentSetAuthorityChangeOp& Op); void GetEntityIds(TArray& OutEntityIds) const { EntityComponentMap.GetKeys(OutEntityIds); } diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialWorkerFlags.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialWorkerFlags.h index ae5f60df7d..a8521ea4c7 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialWorkerFlags.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialWorkerFlags.h @@ -2,11 +2,16 @@ #pragma once +#include "Utils/SpatialBasicAwaiter.h" #include + #include "SpatialWorkerFlags.generated.h" -DECLARE_DYNAMIC_DELEGATE_TwoParams(FOnWorkerFlagsUpdatedBP, const FString&, FlagName, const FString&, FlagValue); -DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnWorkerFlagsUpdated, const FString&, FlagName, const FString&, FlagValue); +DECLARE_DYNAMIC_DELEGATE_TwoParams(FOnAnyWorkerFlagUpdatedBP, const FString&, FlagName, const FString&, FlagValue); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnAnyWorkerFlagUpdated, const FString&, FlagName, const FString&, FlagValue); + +DECLARE_DYNAMIC_DELEGATE_TwoParams(FOnWorkerFlagUpdatedBP, const FString&, FlagName, const FString&, FlagValue); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnWorkerFlagUpdated, const FString&, FlagName, const FString&, FlagValue); UCLASS() class SPATIALGDK_API USpatialWorkerFlags : public UObject @@ -21,17 +26,30 @@ class SPATIALGDK_API USpatialWorkerFlags : public UObject */ bool GetWorkerFlag(const FString& InFlagName, FString& OutFlagValue) const; + void SetWorkerFlag(const FString FlagName, FString FlagValue); + + void RemoveWorkerFlag(const FString FlagName); + UFUNCTION(BlueprintCallable, Category = "SpatialOS") - void BindToOnWorkerFlagsUpdated(const FOnWorkerFlagsUpdatedBP& InDelegate); + void RegisterAnyFlagUpdatedCallback(const FOnAnyWorkerFlagUpdatedBP& InDelegate); UFUNCTION(BlueprintCallable, Category = "SpatialOS") - void UnbindFromOnWorkerFlagsUpdated(const FOnWorkerFlagsUpdatedBP& InDelegate); + void UnregisterAnyFlagUpdatedCallback(const FOnAnyWorkerFlagUpdatedBP& InDelegate); - void ApplyWorkerFlagUpdate(const Worker_FlagUpdateOp& Op); + UFUNCTION(BlueprintCallable, Category = "SpatialOS") + void RegisterAndInvokeAnyFlagUpdatedCallback(const FOnAnyWorkerFlagUpdatedBP& InDelegate); -private: + UFUNCTION(BlueprintCallable, Category = "SpatialOS") + void RegisterFlagUpdatedCallback(const FString& InFlagName, const FOnWorkerFlagUpdatedBP& InDelegate); - FOnWorkerFlagsUpdated OnWorkerFlagsUpdated; + UFUNCTION(BlueprintCallable, Category = "SpatialOS") + void UnregisterFlagUpdatedCallback(const FString& InFlagName, const FOnWorkerFlagUpdatedBP& InDelegate); + UFUNCTION(BlueprintCallable, Category = "SpatialOS") + void RegisterAndInvokeFlagUpdatedCallback(const FString& InFlagName, const FOnWorkerFlagUpdatedBP& InDelegate); + +private: + FOnAnyWorkerFlagUpdated OnAnyWorkerFlagUpdated; TMap WorkerFlags; + TMap WorkerFlagCallbacks; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/WellKnownEntitySystem.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/WellKnownEntitySystem.h new file mode 100644 index 0000000000..161c700a5a --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/WellKnownEntitySystem.h @@ -0,0 +1,43 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Connection/SpatialWorkerConnection.h" +#include "EngineClasses/SpatialVirtualWorkerTranslationManager.h" +#include "EngineClasses/SpatialVirtualWorkerTranslator.h" +#include "GlobalStateManager.h" +#include "SpatialView/SubView.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogWellKnownEntitySystem, Log, All) + +namespace SpatialGDK +{ +class WellKnownEntitySystem +{ +public: + WellKnownEntitySystem(const FSubView& SubView, USpatialReceiver* InReceiver, USpatialWorkerConnection* InConnection, + int InNumberOfWorkers, SpatialVirtualWorkerTranslator& InVirtualWorkerTranslator, + UGlobalStateManager& InGlobalStateManager); + void Advance(); + +private: + void ProcessComponentUpdate(const Worker_ComponentId ComponentId, Schema_ComponentUpdate* Update); + void ProcessComponentAdd(const Worker_ComponentId ComponentId, Schema_ComponentData* Data); + void ProcessAuthorityGain(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId); + void ProcessEntityAdd(const Worker_EntityId EntityId); + + void InitializeVirtualWorkerTranslationManager(); + void MaybeClaimSnapshotPartition(); + + const FSubView* SubView; + + USpatialReceiver* Receiver; + + TUniquePtr VirtualWorkerTranslationManager; + SpatialVirtualWorkerTranslator* VirtualWorkerTranslator; + UGlobalStateManager* GlobalStateManager; + USpatialWorkerConnection* Connection; + int NumberOfWorkers; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLBStrategy.h b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLBStrategy.h index b311abb343..aae1ca2580 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLBStrategy.h +++ b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLBStrategy.h @@ -16,13 +16,12 @@ * At runtime, all unreal workers will: * 1. Instantiate an instance of the strategy class specified in TODO: where are we adding this? * 2. Call the Init method, passing the current USpatialNetDriver. - * 3. (Translator / Enforcer only): Initialize Worker to VirtualWorkerId mapping with - * VirtualWorkerIds from GetVirtualWorkerIds() and begin assinging workers. + * 3. (Translator authoritative worker only): Initialize Worker to VirtualWorkerId mapping with + * VirtualWorkerIds from GetVirtualWorkerIds() and begin assigning workers. * (Other Workers): SetLocalVirtualWorkerId when assigned a VirtualWorkerId. * 4. For each Actor being replicated: * 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 + * b) If true: Update AuthorityDelegation mapping. */ UCLASS(abstract) class SPATIALGDK_API UAbstractLBStrategy : public UObject @@ -40,32 +39,42 @@ class SPATIALGDK_API UAbstractLBStrategy : public UObject virtual void SetLocalVirtualWorkerId(VirtualWorkerId LocalVirtualWorkerId); // Deprecated: will be removed ASAP. - virtual TSet GetVirtualWorkerIds() const PURE_VIRTUAL(UAbstractLBStrategy::GetVirtualWorkerIds, return {};) + 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;) - /** - * 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 {};) - - /** True if this load balancing strategy requires handover data to be transmitted. */ - virtual bool RequiresHandoverData() const PURE_VIRTUAL(UAbstractLBStrategy::RequiresHandover, return false;) + virtual VirtualWorkerId 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. - */ + * 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; } /** - * GetMinimumRequiredWorkers and SetVirtualWorkerIds are used to assign ranges of virtual worker IDs which will be managed by this strategy. - * LastVirtualWorkerId - FirstVirtualWorkerId + 1 is guaranteed to be >= GetMinimumRequiredWorkers. + * Get the query constraints required by this worker based on the load balancing strategy used. */ - virtual uint32 GetMinimumRequiredWorkers() const PURE_VIRTUAL(UAbstractLBStrategy::GetMinimumRequiredWorkers, return 0;) - virtual void SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtualWorkerId, const VirtualWorkerId& LastVirtualWorkerId) PURE_VIRTUAL(UAbstractLBStrategy::SetVirtualWorkerIds, return;) + virtual SpatialGDK::QueryConstraint GetWorkerInterestQueryConstraint(const VirtualWorkerId VirtualWorker) const + PURE_VIRTUAL(UAbstractLBStrategy::GetWorkerInterestQueryConstraint, return {};); -protected: + /** True if this load balancing strategy requires handover data to be transmitted. */ + virtual bool RequiresHandoverData() const PURE_VIRTUAL(UAbstractLBStrategy::RequiresHandover, return false;); + /** + * GetMinimumRequiredWorkers and SetVirtualWorkerIds are used to assign ranges of virtual worker IDs which will be managed by this + * strategy. LastVirtualWorkerId - FirstVirtualWorkerId + 1 is guaranteed to be >= GetMinimumRequiredWorkers. + */ + virtual uint32 GetMinimumRequiredWorkers() const PURE_VIRTUAL(UAbstractLBStrategy::GetMinimumRequiredWorkers, return 0;); + + virtual void SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtualWorkerId, const VirtualWorkerId& LastVirtualWorkerId) + PURE_VIRTUAL(UAbstractLBStrategy::SetVirtualWorkerIds, return;); + + // This returns the LBStrategy which should be rendered in the SpatialDebugger. + // Currently, this is just the default strategy. + virtual UAbstractLBStrategy* GetLBStrategyForVisualRendering() const + PURE_VIRTUAL(UAbstractLBStrategy::GetLBStrategyForVisualRendering, return nullptr;); + +protected: VirtualWorkerId LocalVirtualWorkerId; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLockingPolicy.h b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLockingPolicy.h index 652e0a9c24..42b70f0c29 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLockingPolicy.h +++ b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLockingPolicy.h @@ -17,18 +17,22 @@ class SPATIALGDK_API UAbstractLockingPolicy : public UObject GENERATED_BODY() public: - virtual void Init(SpatialDelegates::FAcquireLockDelegate& AcquireLockDelegate, SpatialDelegates::FReleaseLockDelegate& ReleaseLockDelegate) + 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 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 int32 GetActorLockCount(const AActor* Actor) const PURE_VIRTUAL(UAbstractLockingPolicy::GetActorLockCount, return 0;); 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;); + 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/DebugLBStrategy.h b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/DebugLBStrategy.h new file mode 100644 index 0000000000..796d352ccb --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/DebugLBStrategy.h @@ -0,0 +1,62 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "LoadBalancing/AbstractLBStrategy.h" + +#include "Utils/LayerInfo.h" + +#include "Containers/Map.h" +#include "CoreMinimal.h" +#include "Math/Vector2D.h" + +#include "DebugLBStrategy.generated.h" + +class UAbstractLockingPolicy; +class UAbstractSpatialMultiWorkerSettings; +class USpatialNetDriverDebugContext; + +DECLARE_LOG_CATEGORY_EXTERN(LogDebugLBStrategy, Log, All) + +/* + * Debug load balancing strategy for SpatialFunctionalTest. + * It is wrapping the load balancing strategy set on the NetDriver, + * and inspecting debug tags before deferring to the wrapped strategy, effectively overriding it when needed. + */ +UCLASS(HideDropdown, NotBlueprintable) +class SPATIALGDK_API UDebugLBStrategy : public UAbstractLBStrategy +{ + GENERATED_BODY() +public: + UDebugLBStrategy(); + void InitDebugStrategy(USpatialNetDriverDebugContext* DebugCtx, UAbstractLBStrategy* WrappedStrategy); + + /* UAbstractLBStrategy Interface */ + virtual void Init() override{}; + + virtual void SetLocalVirtualWorkerId(VirtualWorkerId InLocalVirtualWorkerId) override; + + virtual TSet GetVirtualWorkerIds() const override; + + virtual bool ShouldHaveAuthority(const AActor& Actor) const override; + virtual VirtualWorkerId WhoShouldHaveAuthority(const AActor& Actor) const override; + + virtual SpatialGDK::QueryConstraint GetWorkerInterestQueryConstraint(const VirtualWorkerId VirtualWorker) const override; + + virtual bool RequiresHandoverData() const override { return WrappedStrategy->RequiresHandoverData(); } + + virtual FVector GetWorkerEntityPosition() const override; + + virtual uint32 GetMinimumRequiredWorkers() const override; + virtual void SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtualWorkerId, const VirtualWorkerId& LastVirtualWorkerId) override; + virtual UAbstractLBStrategy* GetLBStrategyForVisualRendering() const override; + /* End UAbstractLBStrategy Interface */ + + UAbstractLBStrategy* GetWrappedStrategy() const { return WrappedStrategy; } + +private: + UPROPERTY() + UAbstractLBStrategy* WrappedStrategy = nullptr; + + USpatialNetDriverDebugContext* DebugCtx = nullptr; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GridBasedLBStrategy.h b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GridBasedLBStrategy.h index 817b0d876d..5b45c92ae9 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GridBasedLBStrategy.h +++ b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GridBasedLBStrategy.h @@ -36,7 +36,7 @@ class SPATIALGDK_API UGridBasedLBStrategy : public UAbstractLBStrategy using LBStrategyRegions = TArray>; -/* UAbstractLBStrategy Interface */ + /* UAbstractLBStrategy Interface */ virtual void Init() override; virtual void SetLocalVirtualWorkerId(VirtualWorkerId InLocalVirtualWorkerId) override; @@ -45,7 +45,7 @@ class SPATIALGDK_API UGridBasedLBStrategy : public UAbstractLBStrategy virtual bool ShouldHaveAuthority(const AActor& Actor) const override; virtual VirtualWorkerId WhoShouldHaveAuthority(const AActor& Actor) const override; - virtual SpatialGDK::QueryConstraint GetWorkerInterestQueryConstraint() const override; + virtual SpatialGDK::QueryConstraint GetWorkerInterestQueryConstraint(const VirtualWorkerId VirtualWorker) const override; virtual bool RequiresHandoverData() const override { return Rows * Cols > 1; } @@ -53,10 +53,14 @@ class SPATIALGDK_API UGridBasedLBStrategy : public UAbstractLBStrategy virtual uint32 GetMinimumRequiredWorkers() const override; virtual void SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtualWorkerId, const VirtualWorkerId& LastVirtualWorkerId) override; -/* End UAbstractLBStrategy Interface */ + /* End UAbstractLBStrategy Interface */ LBStrategyRegions GetLBStrategyRegions() const; +#if WITH_EDITOR + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; +#endif // WITH_EDITOR + protected: UPROPERTY(EditDefaultsOnly, meta = (ClampMin = "1"), Category = "Grid Based Load Balancing") uint32 Rows; diff --git a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/LayeredLBStrategy.h b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/LayeredLBStrategy.h index e9cc419855..e581804b33 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/LayeredLBStrategy.h +++ b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/LayeredLBStrategy.h @@ -35,7 +35,7 @@ class SPATIALGDK_API ULayeredLBStrategy : public UAbstractLBStrategy void SetLayers(const TArray& WorkerLayers); /* UAbstractLBStrategy Interface */ - virtual void Init() override {}; + virtual void Init() override{}; virtual void SetLocalVirtualWorkerId(VirtualWorkerId InLocalVirtualWorkerId) override; @@ -44,23 +44,27 @@ class SPATIALGDK_API ULayeredLBStrategy : public UAbstractLBStrategy virtual bool ShouldHaveAuthority(const AActor& Actor) const override; virtual VirtualWorkerId WhoShouldHaveAuthority(const AActor& Actor) const override; - virtual SpatialGDK::QueryConstraint GetWorkerInterestQueryConstraint() const override; + virtual SpatialGDK::QueryConstraint GetWorkerInterestQueryConstraint(const VirtualWorkerId VirtualWorker) const override; - virtual bool RequiresHandoverData() const override { return GetMinimumRequiredWorkers() > 1; } + virtual bool RequiresHandoverData() const override; virtual FVector GetWorkerEntityPosition() const override; virtual uint32 GetMinimumRequiredWorkers() const override; virtual void SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtualWorkerId, const VirtualWorkerId& LastVirtualWorkerId) override; + + // This returns the LBStrategy which should be rendered in the SpatialDebugger. + // Currently, this is just the default strategy. + UAbstractLBStrategy* GetLBStrategyForVisualRendering() const override; /* End UAbstractLBStrategy Interface */ // This is provided to support the offloading interface in SpatialStatics. It should be removed once users // switch to Load Balancing. bool CouldHaveAuthority(TSubclassOf Class) const; - // This returns the LBStrategy which should be rendered in the SpatialDebugger. - // Currently, this is just the default strategy. - UAbstractLBStrategy* GetLBStrategyForVisualRendering() const; + UAbstractLBStrategy* GetLBStrategyForLayer(FName) const; + + FName GetLocalLayerName() const; private: TArray VirtualWorkerIds; diff --git a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/OwnershipLockingPolicy.h b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/OwnershipLockingPolicy.h index 9ec73551fd..6159fe8be4 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/OwnershipLockingPolicy.h +++ b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/OwnershipLockingPolicy.h @@ -49,7 +49,7 @@ class SPATIALGDK_API UOwnershipLockingPolicy : public UAbstractLockingPolicy UFUNCTION() void OnHierarchyRootActorDeleted(AActor* DestroyedActorRoot); - virtual bool AcquireLockFromDelegate(AActor* ActorToLock, const FString& DelegateLockIdentifier) override; + virtual bool AcquireLockFromDelegate(AActor* ActorToLock, const FString& DelegateLockIdentifier) override; virtual bool ReleaseLockFromDelegate(AActor* ActorToRelease, const FString& DelegateLockIdentifier) override; void RecalculateAllExplicitlyLockedActorsInThisHierarchy(const AActor* HierarchyRoot); diff --git a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/SpatialMultiWorkerSettings.h b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/SpatialMultiWorkerSettings.h index ac91c02ff2..525b9aaf57 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/SpatialMultiWorkerSettings.h +++ b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/SpatialMultiWorkerSettings.h @@ -26,11 +26,13 @@ class SPATIALGDK_API UAbstractSpatialMultiWorkerSettings : public UDataAsset protected: UAbstractSpatialMultiWorkerSettings(TArray InWorkerLayers, TSubclassOf InLockingPolicy) : WorkerLayers(InWorkerLayers) - , LockingPolicy(InLockingPolicy) {} + , LockingPolicy(InLockingPolicy) + { + } public: #if WITH_EDITOR - virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override; + virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override; #endif uint32 GetMinimumRequiredWorkerCount() const; @@ -55,6 +57,7 @@ class SPATIALGDK_API UAbstractSpatialMultiWorkerSettings : public UDataAsset void ValidateAllLayersHaveUniqueNonemptyNames(); void ValidateAllLayersHaveLoadBalancingStrategy(); void ValidateLockingPolicyIsSet(); + void EditorRefreshSpatialDebugger() const; #endif }; @@ -65,6 +68,7 @@ class SPATIALGDK_API USpatialMultiWorkerSettings : public UAbstractSpatialMultiW public: USpatialMultiWorkerSettings() - : Super({ UAbstractSpatialMultiWorkerSettings::GetDefaultLayerInfo()}, UOwnershipLockingPolicy::StaticClass()) - {} + : Super({ UAbstractSpatialMultiWorkerSettings::GetDefaultLayerInfo() }, UOwnershipLockingPolicy::StaticClass()) + { + } }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/WorkerRegion.h b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/WorkerRegion.h index 51efd28a7a..011078fe05 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/WorkerRegion.h +++ b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/WorkerRegion.h @@ -9,7 +9,10 @@ #include "WorkerRegion.generated.h" -UCLASS(NotPlaceable, NotBlueprintable) +class UCanvas; +class UCanvasRenderTarget2D; + +UCLASS(Transient, NotPlaceable, NotBlueprintable) class SPATIALGDK_API AWorkerRegion : public AActor { GENERATED_BODY() @@ -17,13 +20,32 @@ class SPATIALGDK_API AWorkerRegion : public AActor public: AWorkerRegion(const FObjectInitializer& ObjectInitializer); - void Init(UMaterial* Material, const FColor& Color, const FBox2D& Extents, const float VerticalScale); + void Init(UMaterial* BackgroundMaterial, UMaterial* InCombinedMaterial, UFont* InWorkerInfoFont, const FColor& Color, + const float Opacity, const FBox2D& Extents, const float Height, const float VerticalScale, const FString& InWorkerInfo); + + UPROPERTY() + UStaticMeshComponent* Mesh; + + UPROPERTY() + UMaterialInstanceDynamic* BackgroundMaterialInstance; + + UPROPERTY() + UMaterialInstanceDynamic* CombinedMaterialInstance; + + UPROPERTY() + UMaterial* CombinedMaterial; UPROPERTY() - UStaticMeshComponent *Mesh; + UCanvasRenderTarget2D* CanvasRenderTarget; UPROPERTY() - UMaterialInstanceDynamic *MaterialInstance; + UFont* WorkerInfoFont; + + UPROPERTY() + FString WorkerInfo; + + UFUNCTION() + void DrawToCanvasRenderTarget(UCanvas* Canvas, int32 Width, int32 Height); private: void SetOpacity(const float Opacity); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/AuthorityIntent.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/AuthorityIntent.h index 90d14ad365..f331d5dac7 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/AuthorityIntent.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/AuthorityIntent.h @@ -10,35 +10,38 @@ namespace SpatialGDK { - // The AuthorityIntent component is a piece of the Zoning solution for the UnrealGDK. For each // entity in SpatialOS, Unreal will use the AuthorityIntent to indicate which Unreal server worker // should be authoritative for the entity. No Unreal worker should write to an entity if the -// VirtualWorkerId set here doesn't match the worker's Id. +// VirtualWorkerId set here doesn't match the worker's Id. struct AuthorityIntent : Component { static const Worker_ComponentId ComponentId = SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID; AuthorityIntent() : VirtualWorkerId(SpatialConstants::AUTHORITY_INTENT_VIRTUAL_WORKER_ID) - {} + { + } AuthorityIntent(VirtualWorkerId InVirtualWorkerId) : VirtualWorkerId(InVirtualWorkerId) - {} + { + } AuthorityIntent(const Worker_ComponentData& Data) + : AuthorityIntent(Data.schema_type) { - Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); - - VirtualWorkerId = Schema_GetUint32(ComponentObject, SpatialConstants::AUTHORITY_INTENT_VIRTUAL_WORKER_ID); } - Worker_ComponentData CreateAuthorityIntentData() + AuthorityIntent(Schema_ComponentData* Data) { - return CreateAuthorityIntentData(VirtualWorkerId); + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data); + + VirtualWorkerId = Schema_GetUint32(ComponentObject, SpatialConstants::AUTHORITY_INTENT_VIRTUAL_WORKER_ID); } + Worker_ComponentData CreateAuthorityIntentData() { return CreateAuthorityIntentData(VirtualWorkerId); } + static Worker_ComponentData CreateAuthorityIntentData(VirtualWorkerId InVirtualWorkerId) { Worker_ComponentData Data = {}; @@ -51,10 +54,7 @@ struct AuthorityIntent : Component return Data; } - Worker_ComponentUpdate CreateAuthorityIntentUpdate() - { - return CreateAuthorityIntentUpdate(VirtualWorkerId); - } + Worker_ComponentUpdate CreateAuthorityIntentUpdate() { return CreateAuthorityIntentUpdate(VirtualWorkerId); } static Worker_ComponentUpdate CreateAuthorityIntentUpdate(VirtualWorkerId InVirtualWorkerId) { @@ -68,9 +68,11 @@ struct AuthorityIntent : Component return Update; } - void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) + void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) { ApplyComponentUpdate(Update.schema_type); } + + void ApplyComponentUpdate(Schema_ComponentUpdate* Update) { - Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update); VirtualWorkerId = Schema_GetUint32(ComponentObject, SpatialConstants::AUTHORITY_INTENT_VIRTUAL_WORKER_ID); } @@ -80,4 +82,3 @@ struct AuthorityIntent : Component }; } // namespace SpatialGDK - diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/ClientEndpoint.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/ClientEndpoint.h index 7e6dffb52b..d0388ff2e3 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/ClientEndpoint.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/ClientEndpoint.h @@ -11,14 +11,15 @@ namespace SpatialGDK { - struct ClientEndpoint : Component { static const Worker_ComponentId ComponentId = SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID; ClientEndpoint(const Worker_ComponentData& Data); + ClientEndpoint(Schema_ComponentData* Data); void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) override; + void ApplyComponentUpdate(Schema_ComponentUpdate* Update); RPCRingBuffer ReliableRPCBuffer; RPCRingBuffer UnreliableRPCBuffer; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/ClientRPCEndpointLegacy.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/ClientRPCEndpointLegacy.h deleted file mode 100644 index b3a69f1e68..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/ClientRPCEndpointLegacy.h +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#pragma once - -#include "Schema/Component.h" -#include "SpatialConstants.h" -#include "Utils/SchemaUtils.h" - -#include -#include - -namespace SpatialGDK -{ - -struct ClientRPCEndpointLegacy : Component -{ - static const Worker_ComponentId ComponentId = SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY; - - ClientRPCEndpointLegacy() = default; - - ClientRPCEndpointLegacy(const Worker_ComponentData& Data) - { - Schema_Object* EndpointObject = Schema_GetComponentDataFields(Data.schema_type); - bReady = GetBoolFromSchema(EndpointObject, SpatialConstants::UNREAL_RPC_ENDPOINT_READY_ID); - } - - void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) - { - Schema_Object* EndpointObject = Schema_GetComponentUpdateFields(Update.schema_type); - if (Schema_GetBoolCount(EndpointObject, SpatialConstants::UNREAL_RPC_ENDPOINT_READY_ID) > 0) - { - bReady = GetBoolFromSchema(EndpointObject, SpatialConstants::UNREAL_RPC_ENDPOINT_READY_ID); - } - } - - Worker_ComponentData CreateRPCEndpointData() - { - Worker_ComponentData Data{}; - Data.component_id = ComponentId; - Data.schema_type = Schema_CreateComponentData(); - Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); - Schema_AddBool(ComponentObject, SpatialConstants::UNREAL_RPC_ENDPOINT_READY_ID, bReady); - - return Data; - } - - Worker_ComponentUpdate CreateRPCEndpointUpdate() - { - Worker_ComponentUpdate Update{}; - Update.component_id = ComponentId; - Update.schema_type = Schema_CreateComponentUpdate(); - Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update.schema_type); - Schema_AddBool(UpdateObject, SpatialConstants::UNREAL_RPC_ENDPOINT_READY_ID, bReady); - - return Update; - } - - bool bReady = false; -}; - -} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/Component.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/Component.h index 9eb68a6f9a..a843b8025e 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/Component.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/Component.h @@ -2,12 +2,11 @@ #pragma once -#include #include "CoreMinimal.h" +#include namespace SpatialGDK { - struct Component { virtual ~Component() {} diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/ComponentPresence.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/ComponentPresence.h deleted file mode 100644 index e5c180d9d1..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/ComponentPresence.h +++ /dev/null @@ -1,121 +0,0 @@ -// 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 "HAL/UnrealMemory.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); - - uint32 BufferCount = ComponentList.Num(); - uint32 BufferSize = BufferCount * sizeof(uint32); - uint32* Buffer = reinterpret_cast(Schema_AllocateBuffer(ComponentObject, BufferSize)); - FMemory::Memcpy(Buffer, ComponentList.GetData(), BufferSize); - Schema_AddUint32List(ComponentObject, SpatialConstants::COMPONENT_PRESENCE_COMPONENT_LIST_ID, Buffer, BufferCount); - - 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); - - uint32 BufferCount = ComponentList.Num(); - uint32 BufferSize = BufferCount * sizeof(uint32); - uint32* Buffer = reinterpret_cast(Schema_AllocateBuffer(ComponentObject, BufferSize)); - FMemory::Memcpy(Buffer, ComponentList.GetData(), BufferSize); - Schema_AddUint32List(ComponentObject, SpatialConstants::COMPONENT_PRESENCE_COMPONENT_LIST_ID, Buffer, BufferCount); - - 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/DebugComponent.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/DebugComponent.h new file mode 100644 index 0000000000..4df06d75b8 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/DebugComponent.h @@ -0,0 +1,97 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Containers/UnrealString.h" +#include "Schema/Component.h" +#include "SpatialConstants.h" +#include "Utils/SchemaUtils.h" + +#include +#include + +namespace SpatialGDK +{ +struct DebugComponent : Component +{ + static const Worker_ComponentId ComponentId = SpatialConstants::GDK_DEBUG_COMPONENT_ID; + + DebugComponent() {} + + DebugComponent(const Worker_ComponentData& Data) + { + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + ReadFromSchema(ComponentObject); + } + + Worker_ComponentData CreateDebugComponent() const + { + Worker_ComponentData Data = {}; + Data.component_id = ComponentId; + Data.schema_type = Schema_CreateComponentData(); + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + WriteToSchema(ComponentObject); + + return Data; + } + + Worker_ComponentUpdate CreateDebugComponentUpdate() const + { + Worker_ComponentUpdate Data = {}; + Data.component_id = ComponentId; + Data.schema_type = Schema_CreateComponentUpdate(); + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Data.schema_type); + WriteToSchema(ComponentObject); + if (ActorTags.Num() == 0) + { + Schema_AddComponentUpdateClearedField(Data.schema_type, 2); + } + + return Data; + } + + void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) override + { + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); + ReadFromSchema(ComponentObject); + } + + TSchemaOption DelegatedWorkerId; + TSet ActorTags; + +private: + void ReadFromSchema(Schema_Object* ComponentObject) + { + if (Schema_GetObjectCount(ComponentObject, 1) == 1) + { + DelegatedWorkerId = Schema_GetInt32(ComponentObject, 1); + } + else + { + DelegatedWorkerId = TSchemaOption(); + } + + const uint32 TagsCount = Schema_GetObjectCount(ComponentObject, 2); + ActorTags.Empty(); + + for (uint32 i = 0; i < TagsCount; ++i) + { + FString TagString = IndexStringFromSchema(ComponentObject, 2, i); + ActorTags.Add(FName(*TagString)); + } + } + + void WriteToSchema(Schema_Object* ComponentObject) const + { + if (DelegatedWorkerId.IsSet()) + { + Schema_AddInt32(ComponentObject, 1, DelegatedWorkerId.GetValue()); + } + for (const auto& Tag : ActorTags) + { + AddStringToSchema(ComponentObject, 2, Tag.ToString()); + } + } +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/DynamicComponent.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/DynamicComponent.h index 55185acae5..4a9b2b755a 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/DynamicComponent.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/DynamicComponent.h @@ -7,7 +7,6 @@ namespace SpatialGDK { - // Represents any Unreal rep component struct DynamicComponent : Component { diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/Heartbeat.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/Heartbeat.h index 95822d819f..54c971005a 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/Heartbeat.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/Heartbeat.h @@ -11,15 +11,12 @@ namespace SpatialGDK { - struct Heartbeat : Component { static const Worker_ComponentId ComponentId = SpatialConstants::HEARTBEAT_COMPONENT_ID; Heartbeat() = default; - Heartbeat(const Worker_ComponentData& Data) - { - } + Heartbeat(const Worker_ComponentData& Data) {} FORCEINLINE Worker_ComponentData CreateHeartbeatData() { diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/Interest.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/Interest.h index 813127ac78..b2e14bb13c 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/Interest.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/Interest.h @@ -7,7 +7,12 @@ namespace SpatialGDK { using EdgeLength = Coordinates; -using SchemaResultType = TArray; + +struct SchemaResultType +{ + TArray ComponentIds; + TArray ComponentSetsIds; +}; struct SphereConstraint { @@ -54,6 +59,7 @@ struct QueryConstraint TSchemaOption ComponentConstraint; TArray AndConstraint; TArray OrConstraint; + bool bSelfConstraint = false; FORCEINLINE bool IsValid() const { @@ -102,6 +108,11 @@ struct QueryConstraint return true; } + if (bSelfConstraint) + { + return true; + } + return false; } }; @@ -110,9 +121,16 @@ struct Query { QueryConstraint Constraint; - // 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. - SchemaResultType ResultComponentIds; // Which components should be included. + // These three fields determine the set of components that are sent back to the worker interested in the + // query. + // - If FullSnapshotResult is set to false, the query in invalid; + // - If FullSnapshotResult is true and ResultComponentIds and ResultComponentSetIds are empty, all the + // components are sent. + // - If FullSnapshotResult is not set, the set of components sent is the union of ResultComponentIds and + // all the sets in ResultComponentSetIds (these sets are defined in the schema). + TSchemaOption FullSnapshotResult; // Whether all components should be included or none. + TArray ResultComponentIds; // Which components should be included. + TArray ResultComponentSetIds; // Which component sets 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 @@ -145,7 +163,7 @@ using FrequencyToConstraintsMap = TMap>; // A common type for lists of frequency constraints to be converted into queries later using FrequencyConstraints = TArray; -struct ComponentInterest +struct ComponentSetInterest { TArray Queries; }; @@ -154,7 +172,7 @@ inline void AddQueryConstraintToQuerySchema(Schema_Object* QueryObject, Schema_F { Schema_Object* QueryConstraintObject = Schema_AddObject(QueryObject, Id); - //option sphere_constraint = 1; + // option sphere_constraint = 1; if (Constraint.SphereConstraint.IsSet()) { Schema_Object* SphereConstraintObject = Schema_AddObject(QueryConstraintObject, 1); @@ -163,7 +181,7 @@ inline void AddQueryConstraintToQuerySchema(Schema_Object* QueryObject, Schema_F Schema_AddDouble(SphereConstraintObject, 2, Constraint.SphereConstraint->Radius); } - //option cylinder_constraint = 2; + // option cylinder_constraint = 2; if (Constraint.CylinderConstraint.IsSet()) { Schema_Object* CylinderConstraintObject = Schema_AddObject(QueryConstraintObject, 2); @@ -172,7 +190,7 @@ inline void AddQueryConstraintToQuerySchema(Schema_Object* QueryObject, Schema_F Schema_AddDouble(CylinderConstraintObject, 2, Constraint.CylinderConstraint->Radius); } - //option box_constraint = 3; + // option box_constraint = 3; if (Constraint.BoxConstraint.IsSet()) { Schema_Object* BoxConstraintObject = Schema_AddObject(QueryConstraintObject, 3); @@ -180,7 +198,7 @@ inline void AddQueryConstraintToQuerySchema(Schema_Object* QueryObject, Schema_F AddCoordinateToSchema(BoxConstraintObject, 2, Constraint.BoxConstraint->EdgeLength); } - //option relative_sphere_constraint = 4; + // option relative_sphere_constraint = 4; if (Constraint.RelativeSphereConstraint.IsSet()) { Schema_Object* RelativeSphereConstraintObject = Schema_AddObject(QueryConstraintObject, 4); @@ -188,33 +206,33 @@ inline void AddQueryConstraintToQuerySchema(Schema_Object* QueryObject, Schema_F Schema_AddDouble(RelativeSphereConstraintObject, 1, Constraint.RelativeSphereConstraint->Radius); } - //option relative_cylinder_constraint = 5; + // option relative_cylinder_constraint = 5; if (Constraint.RelativeCylinderConstraint.IsSet()) { Schema_Object* RelativeCylinderConstraintObject = Schema_AddObject(QueryConstraintObject, 5); Schema_AddDouble(RelativeCylinderConstraintObject, 1, Constraint.RelativeCylinderConstraint->Radius); } - //option relative_box_constraint = 6; + // option relative_box_constraint = 6; if (Constraint.RelativeBoxConstraint.IsSet()) { Schema_Object* RelativeBoxConstraintObject = Schema_AddObject(QueryConstraintObject, 6); AddCoordinateToSchema(RelativeBoxConstraintObject, 1, Constraint.RelativeBoxConstraint->EdgeLength); } - //option entity_id_constraint = 7; + // option entity_id_constraint = 7; if (Constraint.EntityIdConstraint.IsSet()) { Schema_AddInt64(QueryConstraintObject, 7, *Constraint.EntityIdConstraint); } - //option component_constraint = 8; - if (Constraint.ComponentConstraint) + // option component_constraint = 8; + if (Constraint.ComponentConstraint.IsSet()) { Schema_AddUint32(QueryConstraintObject, 8, *Constraint.ComponentConstraint); } - //list and_constraint = 9; + // list and_constraint = 9; if (Constraint.AndConstraint.Num() > 0) { for (const QueryConstraint& AndConstraintEntry : Constraint.AndConstraint) @@ -223,7 +241,7 @@ inline void AddQueryConstraintToQuerySchema(Schema_Object* QueryObject, Schema_F } } - //list or_constraint = 10; + // list or_constraint = 10; if (Constraint.OrConstraint.Num() > 0) { for (const QueryConstraint& OrConstraintEntry : Constraint.OrConstraint) @@ -231,11 +249,17 @@ inline void AddQueryConstraintToQuerySchema(Schema_Object* QueryObject, Schema_F AddQueryConstraintToQuerySchema(QueryConstraintObject, 10, OrConstraintEntry); } } + + // option self_constraint = 12; + if (Constraint.bSelfConstraint) + { + Schema_AddObject(QueryConstraintObject, 12); + } } inline void AddQueryToComponentInterestSchema(Schema_Object* ComponentInterestObject, Schema_FieldId Id, const Query& Query) { - 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.")); + checkf(!(Query.FullSnapshotResult.IsSet() && !Query.FullSnapshotResult), TEXT("Invalid to set FullSnapshotResult to false")); Schema_Object* QueryObject = Schema_AddObject(ComponentInterestObject, Id); @@ -251,13 +275,18 @@ inline void AddQueryToComponentInterestSchema(Schema_Object* ComponentInterestOb Schema_AddUint32(QueryObject, 3, ComponentId); } + for (uint32 ComponentSetId : Query.ResultComponentSetIds) + { + Schema_AddUint32(QueryObject, 5, ComponentSetId); + } + if (Query.Frequency.IsSet()) { Schema_AddFloat(QueryObject, 4, *Query.Frequency); } } -inline void AddComponentInterestToInterestSchema(Schema_Object* InterestObject, Schema_FieldId Id, const ComponentInterest& Value) +inline void AddComponentInterestToInterestSchema(Schema_Object* InterestObject, Schema_FieldId Id, const ComponentSetInterest& Value) { Schema_Object* ComponentInterestObject = Schema_AddObject(InterestObject, Id); @@ -269,7 +298,7 @@ inline void AddComponentInterestToInterestSchema(Schema_Object* InterestObject, inline QueryConstraint IndexQueryConstraintFromSchema(Schema_Object* Object, Schema_FieldId Id, uint32 Index) { - QueryConstraint NewQueryConstraint; + QueryConstraint NewQueryConstraint{}; Schema_Object* QueryConstraintObject = Schema_IndexObject(Object, Id, Index); @@ -278,6 +307,7 @@ inline QueryConstraint IndexQueryConstraintFromSchema(Schema_Object* Object, Sch { Schema_Object* SphereConstraintObject = Schema_GetObject(QueryConstraintObject, 1); + NewQueryConstraint.SphereConstraint = SphereConstraint{}; NewQueryConstraint.SphereConstraint->Center = GetCoordinateFromSchema(SphereConstraintObject, 1); NewQueryConstraint.SphereConstraint->Radius = Schema_GetDouble(SphereConstraintObject, 2); } @@ -287,6 +317,7 @@ inline QueryConstraint IndexQueryConstraintFromSchema(Schema_Object* Object, Sch { Schema_Object* CylinderConstraintObject = Schema_GetObject(QueryConstraintObject, 2); + NewQueryConstraint.CylinderConstraint = CylinderConstraint{}; NewQueryConstraint.CylinderConstraint->Center = GetCoordinateFromSchema(CylinderConstraintObject, 1); NewQueryConstraint.CylinderConstraint->Radius = Schema_GetDouble(CylinderConstraintObject, 2); } @@ -296,6 +327,7 @@ inline QueryConstraint IndexQueryConstraintFromSchema(Schema_Object* Object, Sch { Schema_Object* BoxConstraintObject = Schema_GetObject(QueryConstraintObject, 3); + NewQueryConstraint.BoxConstraint = BoxConstraint{}; NewQueryConstraint.BoxConstraint->Center = GetCoordinateFromSchema(BoxConstraintObject, 1); NewQueryConstraint.BoxConstraint->EdgeLength = GetCoordinateFromSchema(BoxConstraintObject, 2); } @@ -305,6 +337,7 @@ inline QueryConstraint IndexQueryConstraintFromSchema(Schema_Object* Object, Sch { Schema_Object* RelativeSphereConstraintObject = Schema_GetObject(QueryConstraintObject, 4); + NewQueryConstraint.RelativeSphereConstraint = RelativeSphereConstraint{}; NewQueryConstraint.RelativeSphereConstraint->Radius = Schema_GetDouble(RelativeSphereConstraintObject, 1); } @@ -313,6 +346,7 @@ inline QueryConstraint IndexQueryConstraintFromSchema(Schema_Object* Object, Sch { Schema_Object* RelativeCylinderConstraintObject = Schema_GetObject(QueryConstraintObject, 5); + NewQueryConstraint.RelativeCylinderConstraint = RelativeCylinderConstraint{}; NewQueryConstraint.RelativeCylinderConstraint->Radius = Schema_GetDouble(RelativeCylinderConstraintObject, 1); } @@ -321,23 +355,20 @@ inline QueryConstraint IndexQueryConstraintFromSchema(Schema_Object* Object, Sch { Schema_Object* RelativeBoxConstraintObject = Schema_GetObject(QueryConstraintObject, 6); + NewQueryConstraint.RelativeBoxConstraint = RelativeBoxConstraint{}; NewQueryConstraint.RelativeBoxConstraint->EdgeLength = GetCoordinateFromSchema(RelativeBoxConstraintObject, 1); } - //option entity_id_constraint = 7; - if (Schema_GetObjectCount(QueryConstraintObject, 7) > 0) + // option entity_id_constraint = 7; + if (Schema_GetInt64Count(QueryConstraintObject, 7) > 0) { - Schema_Object* EntityIdConstraintObject = Schema_GetObject(QueryConstraintObject, 7); - - NewQueryConstraint.EntityIdConstraint = Schema_GetInt64(EntityIdConstraintObject, 1); + NewQueryConstraint.EntityIdConstraint = Schema_GetInt64(QueryConstraintObject, 7); } // option component_constraint = 8; - if (Schema_GetObjectCount(QueryConstraintObject, 8) > 0) + if (Schema_GetUint32Count(QueryConstraintObject, 8) > 0) { - Schema_Object* ComponentConstraintObject = Schema_GetObject(QueryConstraintObject, 8); - - NewQueryConstraint.ComponentConstraint = Schema_GetUint32(ComponentConstraintObject, 1); + NewQueryConstraint.ComponentConstraint = Schema_GetUint32(QueryConstraintObject, 8); } // list and_constraint = 9; @@ -358,12 +389,18 @@ inline QueryConstraint IndexQueryConstraintFromSchema(Schema_Object* Object, Sch NewQueryConstraint.OrConstraint.Add(IndexQueryConstraintFromSchema(QueryConstraintObject, 10, OrIndex)); } + // option self_constraint = 12; + if (Schema_GetObjectCount(QueryConstraintObject, 12) > 0) + { + NewQueryConstraint.bSelfConstraint = true; + } + return NewQueryConstraint; } inline QueryConstraint GetQueryConstraintFromSchema(Schema_Object* Object, Schema_FieldId Id) { - return IndexQueryConstraintFromSchema(Object, Id, 1); + return IndexQueryConstraintFromSchema(Object, Id, 0); } inline Query IndexQueryFromSchema(Schema_Object* Object, Schema_FieldId Id, uint32 Index) @@ -374,29 +411,36 @@ inline Query IndexQueryFromSchema(Schema_Object* Object, Schema_FieldId Id, uint NewQuery.Constraint = GetQueryConstraintFromSchema(QueryObject, 1); - if (Schema_GetObjectCount(QueryObject, 2) > 0) + if (Schema_GetBoolCount(QueryObject, 2) > 0) { NewQuery.FullSnapshotResult = GetBoolFromSchema(QueryObject, 2); } - uint32 ResultComponentIdCount = Schema_GetObjectCount(QueryObject, 3); + const uint32 ResultComponentIdCount = Schema_GetUint32Count(QueryObject, 3); NewQuery.ResultComponentIds.Reserve(ResultComponentIdCount); for (uint32 ComponentIdIndex = 0; ComponentIdIndex < ResultComponentIdCount; ComponentIdIndex++) { NewQuery.ResultComponentIds.Add(Schema_IndexUint32(QueryObject, 3, ComponentIdIndex)); } - if (Schema_GetObjectCount(QueryObject, 4) > 0) + if (Schema_GetFloatCount(QueryObject, 4) > 0) { NewQuery.Frequency = Schema_GetFloat(QueryObject, 4); } + const uint32 ResultComponentSetIdCount = Schema_GetUint32Count(QueryObject, 5); + NewQuery.ResultComponentSetIds.Reserve(ResultComponentSetIdCount); + for (uint32 ComponentSetIdIndex = 0; ComponentSetIdIndex < ResultComponentSetIdCount; ComponentSetIdIndex++) + { + NewQuery.ResultComponentSetIds.Add(Schema_IndexUint32(QueryObject, 5, ComponentSetIdIndex)); + } + return NewQuery; } -inline ComponentInterest GetComponentInterestFromSchema(Schema_Object* Object, Schema_FieldId Id) +inline ComponentSetInterest GetComponentInterestFromSchema(Schema_Object* Object, Schema_FieldId Id) { - ComponentInterest NewComponentInterest; + ComponentSetInterest NewComponentInterest; Schema_Object* ComponentInterestObject = Schema_GetObject(Object, Id); @@ -425,16 +469,13 @@ struct Interest : Component { Schema_Object* KVPairObject = Schema_IndexObject(ComponentObject, 1, i); uint32 Key = Schema_GetUint32(KVPairObject, SCHEMA_MAP_KEY_FIELD_ID); - ComponentInterest Value = GetComponentInterestFromSchema(KVPairObject, SCHEMA_MAP_VALUE_FIELD_ID); + ComponentSetInterest Value = GetComponentInterestFromSchema(KVPairObject, SCHEMA_MAP_VALUE_FIELD_ID); ComponentInterestMap.Add(Key, Value); } } - bool IsEmpty() - { - return ComponentInterestMap.Num() == 0; - } + bool IsEmpty() { return ComponentInterestMap.Num() == 0; } void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) { @@ -449,7 +490,7 @@ struct Interest : Component { Schema_Object* KVPairObject = Schema_IndexObject(ComponentObject, 1, i); uint32 Key = Schema_GetUint32(KVPairObject, SCHEMA_MAP_KEY_FIELD_ID); - ComponentInterest Value = GetComponentInterestFromSchema(KVPairObject, SCHEMA_MAP_VALUE_FIELD_ID); + ComponentSetInterest Value = GetComponentInterestFromSchema(KVPairObject, SCHEMA_MAP_VALUE_FIELD_ID); ComponentInterestMap.Add(Key, Value); } @@ -490,7 +531,7 @@ struct Interest : Component } } - TMap ComponentInterestMap; + TMap ComponentInterestMap; }; } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/MigrationDiagnostic.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/MigrationDiagnostic.h new file mode 100644 index 0000000000..87d8cad204 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/MigrationDiagnostic.h @@ -0,0 +1,147 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Containers/UnrealString.h" +#include "Engine/EngineBaseTypes.h" +#include "Engine/GameInstance.h" +#include "GameFramework/OnlineReplStructs.h" +#include "Kismet/GameplayStatics.h" +#include "Schema/Component.h" +#include "SpatialConstants.h" +#include "UObject/CoreNet.h" +#include "Utils/SchemaUtils.h" +#include "Utils/SpatialLoadBalancingHandler.h" + +#include +#include + +namespace SpatialGDK +{ +struct MigrationDiagnostic : Component +{ + static const Worker_ComponentId ComponentId = SpatialConstants::MIGRATION_DIAGNOSTIC_COMPONENT_ID; + + MigrationDiagnostic() = default; + + static Worker_CommandRequest CreateMigrationDiagnosticRequest() + { + // Request information from the worker that has authority over the actor that is blocking the hierarchy from migrating + Worker_CommandRequest CommandRequest = {}; + CommandRequest.component_id = SpatialConstants::MIGRATION_DIAGNOSTIC_COMPONENT_ID; + CommandRequest.command_index = SpatialConstants::MIGRATION_DIAGNOSTIC_COMMAND_ID; + CommandRequest.schema_type = Schema_CreateCommandRequest(); + + return CommandRequest; + } + + // Respond with information on the worker that has authority over the actor that is blocking the hierarchy from migrating + static Worker_CommandResponse CreateMigrationDiagnosticResponse(USpatialNetDriver* NetDriver, Worker_EntityId EntityId, + AActor* BlockingActor) + { + check(NetDriver != nullptr); + check(NetDriver->Connection != nullptr); + check(NetDriver->LockingPolicy != nullptr); + check(NetDriver->VirtualWorkerTranslator != nullptr); + check(NetDriver->PackageMap != nullptr); + + Worker_CommandResponse CommandResponse = {}; + + CommandResponse.component_id = SpatialConstants::MIGRATION_DIAGNOSTIC_COMPONENT_ID; + CommandResponse.command_index = SpatialConstants::MIGRATION_DIAGNOSTIC_COMMAND_ID; + CommandResponse.schema_type = Schema_CreateCommandResponse(); + + Schema_Object* ResponseObject = Schema_GetCommandResponseObject(CommandResponse.schema_type); + Schema_AddEntityId(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_ENTITY_ID, EntityId); + Schema_AddBool(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_REPLICATES_ID, BlockingActor->GetIsReplicated()); + Schema_AddBool(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_HAS_AUTHORITY_ID, BlockingActor->HasAuthority()); + + const VirtualWorkerId LocalVirtualWorkerId = NetDriver->VirtualWorkerTranslator->GetLocalVirtualWorkerId(); + + Schema_AddInt32(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_AUTHORITY_WORKER_ID, + NetDriver->VirtualWorkerTranslator->GetLocalVirtualWorkerId()); + Schema_AddBool(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_LOCKED_ID, NetDriver->LockingPolicy->IsLocked(BlockingActor)); + + AActor* NetOwner; + VirtualWorkerId NewAuthWorkerId; + + FSpatialLoadBalancingHandler MigrationHandler(NetDriver); + FSpatialLoadBalancingHandler::EvaluateActorResult Result = + MigrationHandler.EvaluateSingleActor(BlockingActor, NetOwner, NewAuthWorkerId); + Worker_EntityId OwnerId = NetDriver->PackageMap->GetEntityIdFromObject(NetOwner); + + Schema_AddBool(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_EVALUATION_ID, + Result == FSpatialLoadBalancingHandler::EvaluateActorResult::Migrate); + Schema_AddInt32(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_DESTINATION_WORKER_ID, NewAuthWorkerId); + Schema_AddEntityId(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_OWNER_ID, OwnerId); + + return CommandResponse; + } + + // Compare information from the worker that is blocked from migrating a hierarchy with the information from the authoritative + // worker over the actor that is blocking the migration and log the results. + static FString CreateMigrationDiagnosticLog(USpatialNetDriver* NetDriver, Schema_Object* ResponseObject, AActor* BlockingActor) + { + check(NetDriver != nullptr); + check(NetDriver->Connection != nullptr); + check(NetDriver->LockingPolicy != nullptr); + + if (ResponseObject == nullptr) + { + return FString::Printf(TEXT("Migration diaganostic log failed as response was empty.")); + } + + VirtualWorkerId AuthoritativeWorkerId = Schema_GetInt32(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_AUTHORITY_WORKER_ID); + Worker_EntityId BlockedEntityId = Schema_GetEntityId(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_ENTITY_ID); + bool bIsReplicated = GetBoolFromSchema(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_REPLICATES_ID); + bool bHasAuthority = GetBoolFromSchema(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_HAS_AUTHORITY_ID); + bool bIsLocked = GetBoolFromSchema(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_LOCKED_ID); + bool bCanMigrate = GetBoolFromSchema(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_EVALUATION_ID); + VirtualWorkerId DestinationWorkerId = Schema_GetInt32(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_DESTINATION_WORKER_ID); + Worker_EntityId AuthoritativeNetOwnerId = Schema_GetEntityId(ResponseObject, SpatialConstants::MIGRATION_DIAGNOSTIC_OWNER_ID); + + AActor* NetOwner = SpatialGDK::GetReplicatedHierarchyRoot(BlockingActor); + Worker_EntityId OriginalNetOwnerId = NetDriver->PackageMap->GetEntityIdFromObject(NetOwner); + + FString Reason = FString::Printf(TEXT("Originating worker (%i) does not have authority of blocking actor. "), + NetDriver->VirtualWorkerTranslator->GetLocalVirtualWorkerId()); + + if (BlockingActor->GetIsReplicated() && !bIsReplicated) + { + Reason.Append(FString::Printf(TEXT("Blocking actor replicates on originating worker but not on authoritative worker (%i). "), + AuthoritativeWorkerId)); + } + else if (!bHasAuthority) + { + Reason.Append( + FString::Printf(TEXT("Authoritative worker (%i) does not have authority of blocking actor. "), AuthoritativeWorkerId)); + } + else if (!IsValid(NetOwner)) + { + Reason.Append(FString::Printf(TEXT("Blocking actor owner is not valid. "))); + } + else if (IsValid(NetOwner) && OriginalNetOwnerId != AuthoritativeNetOwnerId) + { + Reason.Append(FString::Printf(TEXT("Blocking actor has different owner (%llu) on authoritative worker (%i). "), + AuthoritativeNetOwnerId, AuthoritativeWorkerId)); + } + else if (bIsLocked) + { + Reason.Append(FString::Printf(TEXT("Blocking actor is locked on authoritative worker (%i). "), AuthoritativeWorkerId)); + } + else if (NetDriver->LockingPolicy->IsLocked(BlockingActor)) + { + Reason.Append(FString::Printf(TEXT("Blocking actor is locked on originating worker. "))); + } + else if (bCanMigrate) + { + Reason.Append(FString::Printf(TEXT("Authoritative worker (%i) believes blocked actor should migrate to worker (%i). "), + AuthoritativeWorkerId, DestinationWorkerId)); + } + + return FString::Printf(TEXT("Prevented owning actor %s (%llu)'s hierarchy from migrating because of blocking actor %s (%llu). %s"), + *GetNameSafe(NetOwner), OriginalNetOwnerId, *BlockingActor->GetName(), BlockedEntityId, *Reason); + } +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/MulticastRPCs.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/MulticastRPCs.h index 820cc993b6..ddb75dc24c 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/MulticastRPCs.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/MulticastRPCs.h @@ -11,14 +11,15 @@ namespace SpatialGDK { - struct MulticastRPCs : Component { static const Worker_ComponentId ComponentId = SpatialConstants::MULTICAST_RPCS_COMPONENT_ID; MulticastRPCs(const Worker_ComponentData& Data); + MulticastRPCs(Schema_ComponentData* Data); void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) override; + void ApplyComponentUpdate(Schema_ComponentUpdate* Update); RPCRingBuffer MulticastRPCBuffer; uint32 InitiallyPresentMulticastRPCsCount = 0; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/NetOwningClientWorker.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/NetOwningClientWorker.h index 46b2e29bb8..b2da6df879 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/NetOwningClientWorker.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/NetOwningClientWorker.h @@ -13,83 +13,92 @@ namespace SpatialGDK { - struct NetOwningClientWorker : Component - { - static const Worker_ComponentId ComponentId = SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID; +struct NetOwningClientWorker : Component +{ + static const Worker_ComponentId ComponentId = SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID; + + NetOwningClientWorker() = default; - NetOwningClientWorker() = default; + NetOwningClientWorker(const TSchemaOption& InPartitionId) + : ClientPartitionId(InPartitionId) + { + } - NetOwningClientWorker(const TSchemaOption& InWorkerId) - : WorkerId(InWorkerId) {} + NetOwningClientWorker(const Worker_ComponentData& Data) + : NetOwningClientWorker(Data.schema_type) + { + } - NetOwningClientWorker(const Worker_ComponentData& Data) + NetOwningClientWorker(Schema_ComponentData* Data) + { + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data); + if (Schema_GetEntityIdCount(ComponentObject, SpatialConstants::NET_OWNING_CLIENT_PARTITION_ENTITY_FIELD_ID) == 1) { - 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); - } + ClientPartitionId = Schema_GetEntityId(ComponentObject, SpatialConstants::NET_OWNING_CLIENT_PARTITION_ENTITY_FIELD_ID); } + } - Worker_ComponentData CreateNetOwningClientWorkerData() + Worker_ComponentData CreateNetOwningClientWorkerData() { return CreateNetOwningClientWorkerData(ClientPartitionId); } + + static Worker_ComponentData CreateNetOwningClientWorkerData(const TSchemaOption& PartitionId) + { + Worker_ComponentData Data = {}; + Data.component_id = ComponentId; + Data.schema_type = Schema_CreateComponentData(); + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + + if (PartitionId.IsSet()) { - return CreateNetOwningClientWorkerData(WorkerId); + Schema_AddEntityId(ComponentObject, SpatialConstants::NET_OWNING_CLIENT_PARTITION_ENTITY_FIELD_ID, *PartitionId); } - 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); + return Data; + } - if (WorkerId.IsSet()) - { - AddStringToSchema(ComponentObject, SpatialConstants::NET_OWNING_CLIENT_WORKER_FIELD_ID, *WorkerId); - } + Worker_ComponentUpdate CreateNetOwningClientWorkerUpdate() const { return CreateNetOwningClientWorkerUpdate(ClientPartitionId); } - return Data; - } + static Worker_ComponentUpdate CreateNetOwningClientWorkerUpdate(const TSchemaOption& PartitionId) + { + Worker_ComponentUpdate Update = {}; + Update.component_id = ComponentId; + Update.schema_type = Schema_CreateComponentUpdate(); + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); - Worker_ComponentUpdate CreateNetOwningClientWorkerUpdate() + if (PartitionId.IsSet()) { - return CreateNetOwningClientWorkerUpdate(WorkerId); + Schema_AddEntityId(ComponentObject, SpatialConstants::NET_OWNING_CLIENT_PARTITION_ENTITY_FIELD_ID, *PartitionId); } - - static Worker_ComponentUpdate CreateNetOwningClientWorkerUpdate(const TSchemaOption& WorkerId) + else { - 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; + Schema_AddComponentUpdateClearedField(Update.schema_type, SpatialConstants::NET_OWNING_CLIENT_PARTITION_ENTITY_FIELD_ID); } - void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) + return Update; + } + + void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) { ApplyComponentUpdate(Update.schema_type); } + + void ApplyComponentUpdate(Schema_ComponentUpdate* Update) + { + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update); + + if (Schema_GetEntityIdCount(ComponentObject, SpatialConstants::NET_OWNING_CLIENT_PARTITION_ENTITY_FIELD_ID) == 1) + { + ClientPartitionId = Schema_GetEntityId(ComponentObject, SpatialConstants::NET_OWNING_CLIENT_PARTITION_ENTITY_FIELD_ID); + } + else if (Schema_IsComponentUpdateFieldCleared(Update, SpatialConstants::NET_OWNING_CLIENT_PARTITION_ENTITY_FIELD_ID)) { - 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(); - } + ClientPartitionId = TSchemaOption(); } + } + + void SetPartitionId(const Worker_PartitionId& InPartitionId) + { + ClientPartitionId = InPartitionId == SpatialConstants::INVALID_ENTITY_ID ? TSchemaOption() : InPartitionId; + } - // Client worker ID corresponding to the owning net connection (if exists). - TSchemaOption WorkerId; - }; + // Client partition entity ID corresponding to the owning net connection (if exists). + TSchemaOption ClientPartitionId; +}; } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/PlayerSpawner.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/PlayerSpawner.h index 76ddbc94c3..849369a2a0 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/PlayerSpawner.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/PlayerSpawner.h @@ -18,13 +18,13 @@ namespace SpatialGDK { - struct SpawnPlayerRequest { FURL LoginURL; FUniqueNetIdRepl UniqueId; FName OnlinePlatformName; bool bIsSimulatedPlayer; + Worker_EntityId ClientSystemEntityId; }; struct PlayerSpawner : Component @@ -65,6 +65,7 @@ struct PlayerSpawner : Component 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); + Schema_AddEntityId(RequestObject, SpatialConstants::SPAWN_PLAYER_CLIENT_SYSTEM_ENTITY_ID, SpawnRequest.ClientSystemEntityId); } static FURL ExtractUrlFromPlayerSpawnParams(const Schema_Object* Payload) @@ -81,20 +82,29 @@ struct PlayerSpawner : Component FNetBitReader UniqueIdReader(nullptr, UniqueIdBytes.GetData(), UniqueIdBytes.Num() * 8); UniqueIdReader << UniqueId; - const FName OnlinePlatformName = FName(*GetStringFromSchema(CommandRequestPayload, SpatialConstants::SPAWN_PLAYER_PLATFORM_NAME_ID)); + 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 }; + const Worker_EntityId ClientPartitionId = + Schema_GetEntityId(CommandRequestPayload, SpatialConstants::SPAWN_PLAYER_CLIENT_SYSTEM_ENTITY_ID); + + return { LoginURL, UniqueId, OnlinePlatformName, bIsSimulated, ClientPartitionId }; } static void CopySpawnDataBetweenObjects(const Schema_Object* SpawnPlayerDataSource, Schema_Object* SpawnPlayerDataDestination) { - AddStringToSchema(SpawnPlayerDataDestination, SpatialConstants::SPAWN_PLAYER_URL_ID, GetStringFromSchema(SpawnPlayerDataSource, SpatialConstants::SPAWN_PLAYER_URL_ID)); + 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)); + 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)); + Schema_AddEntityId(SpawnPlayerDataDestination, SpatialConstants::SPAWN_PLAYER_CLIENT_SYSTEM_ENTITY_ID, + Schema_GetEntityId(SpawnPlayerDataSource, SpatialConstants::SPAWN_PLAYER_CLIENT_SYSTEM_ENTITY_ID)); } }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/RPCPayload.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/RPCPayload.h index 42a89c2316..c5e499db58 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/RPCPayload.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/RPCPayload.h @@ -12,7 +12,6 @@ namespace SpatialGDK { - struct RPCPayload { RPCPayload() = delete; @@ -22,7 +21,8 @@ struct RPCPayload , Index(InIndex) , PayloadData(MoveTemp(Data)) , Trace(InTraceKey) - {} + { + } RPCPayload(Schema_Object* RPCObject) { @@ -38,10 +38,7 @@ struct RPCPayload #endif } - int64 CountDataBits() const - { - return PayloadData.Num() * 8; - } + int64 CountDataBits() const { return PayloadData.Num() * 8; } void WriteToSchemaObject(Schema_Object* RPCObject) const { @@ -68,67 +65,4 @@ struct RPCPayload TraceKey Trace = InvalidTraceKey; }; -struct RPCsOnEntityCreation : Component -{ - static const Worker_ComponentId ComponentId = SpatialConstants::RPCS_ON_ENTITY_CREATION_ID; - - RPCsOnEntityCreation() = default; - - bool HasRPCPayloadData() const - { - return RPCs.Num() > 0; - } - - RPCsOnEntityCreation(const Worker_ComponentData& Data) - { - Schema_Object* ComponentsObject = Schema_GetComponentDataFields(Data.schema_type); - - uint32 RPCCount = Schema_GetObjectCount(ComponentsObject, SpatialConstants::UNREAL_RPC_PAYLOAD_OFFSET_ID); - - for (uint32 i = 0; i < RPCCount; i++) - { - Schema_Object* ComponentObject = Schema_IndexObject(ComponentsObject, SpatialConstants::UNREAL_RPC_PAYLOAD_OFFSET_ID, i); - RPCs.Add(RPCPayload(ComponentObject)); - } - } - - Worker_ComponentData CreateRPCPayloadData() const - { - Worker_ComponentData Data = {}; - Data.component_id = ComponentId; - Data.schema_type = Schema_CreateComponentData(); - Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); - - for (const auto& Payload : RPCs) - { - Schema_Object* Obj = Schema_AddObject(ComponentObject, SpatialConstants::UNREAL_RPC_PAYLOAD_OFFSET_ID); - RPCPayload::WriteToSchemaObject(Obj, Payload.Offset, Payload.Index, Payload.PayloadData.GetData(), Payload.PayloadData.Num()); - } - - return Data; - } - - static Worker_ComponentUpdate CreateClearFieldsUpdate() - { - Worker_ComponentUpdate Update = {}; - Update.component_id = ComponentId; - Update.schema_type = Schema_CreateComponentUpdate(); - Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update.schema_type); - Schema_AddComponentUpdateClearedField(Update.schema_type, SpatialConstants::UNREAL_RPC_PAYLOAD_OFFSET_ID); - - return Update; - } - - static Worker_CommandRequest CreateClearFieldsCommandRequest() - { - Worker_CommandRequest CommandRequest = {}; - CommandRequest.component_id = ComponentId; - CommandRequest.command_index = SpatialConstants::CLEAR_RPCS_ON_ENTITY_CREATION; - CommandRequest.schema_type = Schema_CreateCommandRequest(); - return CommandRequest; - } - - TArray RPCs; -}; - } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/Restricted.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/Restricted.h new file mode 100644 index 0000000000..595db71596 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/Restricted.h @@ -0,0 +1,37 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Schema/Component.h" +#include "SpatialCommonTypes.h" +#include "Utils/SchemaUtils.h" + +#include +#include + +namespace SpatialGDK +{ +struct Partition : Component +{ + static const Worker_ComponentId ComponentId = SpatialConstants::PARTITION_COMPONENT_ID; + + Partition() = default; + + Partition(const Worker_ComponentData& Data) + { + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + + WorkerConnectionId = Schema_GetUint64(ComponentObject, 1); + } + + void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) + { + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); + + WorkerConnectionId = Schema_GetUint64(ComponentObject, 1); + } + + Worker_EntityId_Key WorkerConnectionId; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerEndpoint.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerEndpoint.h index 3a4255069d..97745c82df 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerEndpoint.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerEndpoint.h @@ -11,14 +11,15 @@ namespace SpatialGDK { - struct ServerEndpoint : Component { static const Worker_ComponentId ComponentId = SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID; ServerEndpoint(const Worker_ComponentData& Data); + ServerEndpoint(Schema_ComponentData* Data); void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) override; + void ApplyComponentUpdate(Schema_ComponentUpdate* Update); RPCRingBuffer ReliableRPCBuffer; RPCRingBuffer UnreliableRPCBuffer; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerRPCEndpointLegacy.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerRPCEndpointLegacy.h deleted file mode 100644 index 7f8fc0be9b..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerRPCEndpointLegacy.h +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#pragma once - -#include "Schema/Component.h" -#include "SpatialConstants.h" -#include "Utils/SchemaUtils.h" - -#include -#include - -namespace SpatialGDK -{ - -struct ServerRPCEndpointLegacy : Component -{ - static const Worker_ComponentId ComponentId = SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY; - - ServerRPCEndpointLegacy() = default; - - ServerRPCEndpointLegacy(const Worker_ComponentData& Data) - { - Schema_Object* EndpointObject = Schema_GetComponentDataFields(Data.schema_type); - bReady = GetBoolFromSchema(EndpointObject, SpatialConstants::UNREAL_RPC_ENDPOINT_READY_ID); - } - - void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) - { - Schema_Object* EndpointObject = Schema_GetComponentUpdateFields(Update.schema_type); - if (Schema_GetBoolCount(EndpointObject, SpatialConstants::UNREAL_RPC_ENDPOINT_READY_ID) > 0) - { - bReady = GetBoolFromSchema(EndpointObject, SpatialConstants::UNREAL_RPC_ENDPOINT_READY_ID); - } - } - - Worker_ComponentData CreateRPCEndpointData() - { - Worker_ComponentData Data{}; - Data.component_id = ComponentId; - Data.schema_type = Schema_CreateComponentData(); - Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); - Schema_AddBool(ComponentObject, SpatialConstants::UNREAL_RPC_ENDPOINT_READY_ID, bReady); - - return Data; - } - - Worker_ComponentUpdate CreateRPCEndpointUpdate() - { - Worker_ComponentUpdate Update{}; - Update.component_id = ComponentId; - Update.schema_type = Schema_CreateComponentUpdate(); - Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update.schema_type); - Schema_AddBool(UpdateObject, SpatialConstants::UNREAL_RPC_ENDPOINT_READY_ID, bReady); - - return Update; - } - - bool bReady = false; -}; - -} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerWorker.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerWorker.h index a3a7ffbe71..43c157085e 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerWorker.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerWorker.h @@ -15,7 +15,6 @@ 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. @@ -26,12 +25,15 @@ struct ServerWorker : Component ServerWorker() : WorkerName(SpatialConstants::INVALID_WORKER_NAME) , bReadyToBeginPlay(false) - {} + , SystemEntityId(SpatialConstants::INVALID_ENTITY_ID) + { + } - ServerWorker(const PhysicalWorkerName& InWorkerName, const bool bInReadyToBeginPlay) + ServerWorker(const PhysicalWorkerName& InWorkerName, const bool bInReadyToBeginPlay, const Worker_EntityId InSystemEntityId) { WorkerName = InWorkerName; bReadyToBeginPlay = bInReadyToBeginPlay; + SystemEntityId = InSystemEntityId; } ServerWorker(const Worker_ComponentData& Data) @@ -40,6 +42,7 @@ struct ServerWorker : Component WorkerName = GetStringFromSchema(ComponentObject, SpatialConstants::SERVER_WORKER_NAME_ID); bReadyToBeginPlay = GetBoolFromSchema(ComponentObject, SpatialConstants::SERVER_WORKER_READY_TO_BEGIN_PLAY_ID); + SystemEntityId = Schema_GetEntityId(ComponentObject, SpatialConstants::SERVER_WORKER_SYSTEM_ENTITY_ID); } Worker_ComponentData CreateServerWorkerData() @@ -51,6 +54,7 @@ struct ServerWorker : Component AddStringToSchema(ComponentObject, SpatialConstants::SERVER_WORKER_NAME_ID, WorkerName); Schema_AddBool(ComponentObject, SpatialConstants::SERVER_WORKER_READY_TO_BEGIN_PLAY_ID, bReadyToBeginPlay); + Schema_AddEntityId(ComponentObject, SpatialConstants::SERVER_WORKER_SYSTEM_ENTITY_ID, SystemEntityId); return Data; } @@ -64,6 +68,7 @@ struct ServerWorker : Component AddStringToSchema(ComponentObject, SpatialConstants::SERVER_WORKER_NAME_ID, WorkerName); Schema_AddBool(ComponentObject, SpatialConstants::SERVER_WORKER_READY_TO_BEGIN_PLAY_ID, bReadyToBeginPlay); + Schema_AddEntityId(ComponentObject, SpatialConstants::SERVER_WORKER_SYSTEM_ENTITY_ID, SystemEntityId); return Update; } @@ -74,6 +79,7 @@ struct ServerWorker : Component WorkerName = GetStringFromSchema(ComponentObject, SpatialConstants::SERVER_WORKER_NAME_ID); bReadyToBeginPlay = GetBoolFromSchema(ComponentObject, SpatialConstants::SERVER_WORKER_READY_TO_BEGIN_PLAY_ID); + SystemEntityId = Schema_GetEntityId(ComponentObject, SpatialConstants::SERVER_WORKER_SYSTEM_ENTITY_ID); } static Worker_CommandRequest CreateForwardPlayerSpawnRequest(Schema_CommandRequest* SchemaCommandRequest) @@ -98,7 +104,9 @@ struct ServerWorker : Component return CommandResponse; } - static void CreateForwardPlayerSpawnSchemaRequest(Schema_CommandRequest* Request, const FUnrealObjectRef& PlayerStartObjectRef, const Schema_Object* OriginalPlayerSpawnRequest, const PhysicalWorkerName& ClientWorkerID) + static void CreateForwardPlayerSpawnSchemaRequest(Schema_CommandRequest* Request, const FUnrealObjectRef& PlayerStartObjectRef, + const Schema_Object* OriginalPlayerSpawnRequest, + const Worker_EntityId& ClientWorkerID) { Schema_Object* RequestFields = Schema_GetCommandRequestObject(Request); @@ -107,12 +115,12 @@ struct ServerWorker : Component 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); + Schema_AddEntityId(RequestFields, SpatialConstants::FORWARD_SPAWN_PLAYER_CLIENT_SYSTEM_ENTITY_ID, ClientWorkerID); } PhysicalWorkerName WorkerName; bool bReadyToBeginPlay; + Worker_EntityId SystemEntityId; }; } // namespace SpatialGDK - diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/SpatialDebugging.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/SpatialDebugging.h index 9173405943..6c92e7041e 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/SpatialDebugging.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/SpatialDebugging.h @@ -10,7 +10,6 @@ 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 @@ -23,9 +22,11 @@ struct SpatialDebugging : Component , IntentVirtualWorkerId(SpatialConstants::INVALID_VIRTUAL_WORKER_ID) , IntentColor() , IsLocked(false) - {} + { + } - SpatialDebugging(const VirtualWorkerId AuthoritativeVirtualWorkerIdIn, const FColor& AuthoritativeColorIn, const VirtualWorkerId IntentVirtualWorkerIdIn, const FColor& IntentColorIn, bool IsLockedIn) + SpatialDebugging(const VirtualWorkerId AuthoritativeVirtualWorkerIdIn, const FColor& AuthoritativeColorIn, + const VirtualWorkerId IntentVirtualWorkerIdIn, const FColor& IntentColorIn, bool IsLockedIn) { AuthoritativeVirtualWorkerId = AuthoritativeVirtualWorkerIdIn; AuthoritativeColor = AuthoritativeColorIn; @@ -38,7 +39,8 @@ struct SpatialDebugging : Component { Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); - AuthoritativeVirtualWorkerId = Schema_GetUint32(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_AUTHORITATIVE_VIRTUAL_WORKER_ID); + 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)); @@ -52,7 +54,8 @@ struct SpatialDebugging : Component 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_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()); @@ -68,7 +71,8 @@ struct SpatialDebugging : Component 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_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()); @@ -81,7 +85,8 @@ struct SpatialDebugging : Component { Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); - AuthoritativeVirtualWorkerId = Schema_GetUint32(ComponentObject, SpatialConstants::SPATIAL_DEBUGGING_AUTHORITATIVE_VIRTUAL_WORKER_ID); + 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)); @@ -107,4 +112,3 @@ struct SpatialDebugging : Component }; } // namespace SpatialGDK - diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/SpawnData.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/SpawnData.h index d26d8923e2..1e6fd7a3d0 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/SpawnData.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/SpawnData.h @@ -2,6 +2,9 @@ #pragma once +#include "Engine/EngineTypes.h" +#include "GameFramework/Actor.h" + #include "Schema/Component.h" #include "SpatialConstants.h" #include "Utils/SchemaUtils.h" @@ -11,7 +14,6 @@ namespace SpatialGDK { - struct SpawnData : Component { static const Worker_ComponentId ComponentId = SpatialConstants::SPAWN_DATA_COMPONENT_ID; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/StandardLibrary.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/StandardLibrary.h index 0f2df9b9f5..b8aa332723 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/StandardLibrary.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/StandardLibrary.h @@ -7,8 +7,8 @@ #include "Schema/Component.h" #include "Schema/UnrealObjectRef.h" #include "SpatialCommonTypes.h" -#include "UObject/UObjectGlobals.h" #include "UObject/Package.h" +#include "UObject/UObjectGlobals.h" #include "Utils/SchemaUtils.h" #include @@ -16,7 +16,6 @@ namespace SpatialGDK { - struct Coordinates { double X; @@ -43,10 +42,7 @@ struct Coordinates return Location; } - inline bool operator!=(const Coordinates& Right) const - { - return X != Right.X || Y != Right.Y || Z != Right.Z; - } + inline bool operator!=(const Coordinates& Right) const { return X != Right.X || Y != Right.Y || Z != Right.Z; } }; static const Coordinates DeploymentOrigin{ 0, 0, 0 }; @@ -77,99 +73,6 @@ inline Coordinates GetCoordinateFromSchema(Schema_Object* Object, Schema_FieldId return IndexCoordinateFromSchema(Object, Id, 0); } -struct EntityAcl : Component -{ - static const Worker_ComponentId ComponentId = SpatialConstants::ENTITY_ACL_COMPONENT_ID; - - EntityAcl() = default; - - EntityAcl(const WorkerRequirementSet& InReadAcl, const WriteAclMap& InComponentWriteAcl) - : ReadAcl(InReadAcl), ComponentWriteAcl(InComponentWriteAcl) {} - - EntityAcl(const Worker_ComponentData& Data) - { - Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); - - ReadAcl = GetWorkerRequirementSetFromSchema(ComponentObject, 1); - - uint32 KVPairCount = Schema_GetObjectCount(ComponentObject, 2); - for (uint32 i = 0; i < KVPairCount; i++) - { - Schema_Object* KVPairObject = Schema_IndexObject(ComponentObject, 2, i); - uint32 Key = Schema_GetUint32(KVPairObject, SCHEMA_MAP_KEY_FIELD_ID); - WorkerRequirementSet Value = GetWorkerRequirementSetFromSchema(KVPairObject, SCHEMA_MAP_VALUE_FIELD_ID); - - ComponentWriteAcl.Add(Key, Value); - } - } - - void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) - { - Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); - - if (Schema_GetObjectCount(ComponentObject, 1) > 0) - { - ReadAcl = GetWorkerRequirementSetFromSchema(ComponentObject, 1); - } - - // This is never emptied, so does not need an additional check for cleared fields - uint32 KVPairCount = Schema_GetObjectCount(ComponentObject, 2); - if (KVPairCount > 0) - { - ComponentWriteAcl.Empty(); - for (uint32 i = 0; i < KVPairCount; i++) - { - Schema_Object* KVPairObject = Schema_IndexObject(ComponentObject, 2, i); - uint32 Key = Schema_GetUint32(KVPairObject, SCHEMA_MAP_KEY_FIELD_ID); - WorkerRequirementSet Value = GetWorkerRequirementSetFromSchema(KVPairObject, SCHEMA_MAP_VALUE_FIELD_ID); - - ComponentWriteAcl.Add(Key, Value); - } - } - } - - Worker_ComponentData CreateEntityAclData() - { - Worker_ComponentData Data = {}; - Data.component_id = ComponentId; - Data.schema_type = Schema_CreateComponentData(); - Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); - - AddWorkerRequirementSetToSchema(ComponentObject, 1, ReadAcl); - - for (const auto& KVPair : ComponentWriteAcl) - { - Schema_Object* KVPairObject = Schema_AddObject(ComponentObject, 2); - Schema_AddUint32(KVPairObject, SCHEMA_MAP_KEY_FIELD_ID, KVPair.Key); - AddWorkerRequirementSetToSchema(KVPairObject, SCHEMA_MAP_VALUE_FIELD_ID, KVPair.Value); - } - - return Data; - } - - Worker_ComponentUpdate CreateEntityAclUpdate() - { - Worker_ComponentUpdate ComponentUpdate = {}; - ComponentUpdate.component_id = ComponentId; - ComponentUpdate.schema_type = Schema_CreateComponentUpdate(); - Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(ComponentUpdate.schema_type); - - AddWorkerRequirementSetToSchema(ComponentObject, 1, ReadAcl); - - for (const auto& KVPair : ComponentWriteAcl) - { - Schema_Object* KVPairObject = Schema_AddObject(ComponentObject, 2); - Schema_AddUint32(KVPairObject, SCHEMA_MAP_KEY_FIELD_ID, KVPair.Key); - AddWorkerRequirementSetToSchema(KVPairObject, SCHEMA_MAP_VALUE_FIELD_ID, KVPair.Value); - } - - return ComponentUpdate; - } - - WorkerRequirementSet ReadAcl; - WriteAclMap ComponentWriteAcl; -}; - struct Metadata : Component { static const Worker_ComponentId ComponentId = SpatialConstants::METADATA_COMPONENT_ID; @@ -177,7 +80,9 @@ struct Metadata : Component Metadata() = default; Metadata(const FString& InEntityType) - : EntityType(InEntityType) {} + : EntityType(InEntityType) + { + } Metadata(const Worker_ComponentData& Data) { @@ -208,7 +113,9 @@ struct Position : Component Position() = default; Position(const Coordinates& InCoords) - : Coords(InCoords) {} + : Coords(InCoords) + { + } Position(const Worker_ComponentData& Data) { @@ -258,9 +165,7 @@ struct Persistence : Component static const Worker_ComponentId ComponentId = SpatialConstants::PERSISTENCE_COMPONENT_ID; Persistence() = default; - Persistence(const Worker_ComponentData& Data) - { - } + Persistence(const Worker_ComponentData& Data) {} FORCEINLINE Worker_ComponentData CreatePersistenceData() { @@ -314,6 +219,107 @@ struct Worker : Component FString WorkerId; FString WorkerType; Connection Connection; + + static Worker_CommandRequest CreateClaimPartitionRequest(Worker_PartitionId PartitionId) + { + Worker_CommandRequest CommandRequest = {}; + CommandRequest.component_id = SpatialConstants::WORKER_COMPONENT_ID; + CommandRequest.command_index = SpatialConstants::WORKER_CLAIM_PARTITION_COMMAND_ID; + CommandRequest.schema_type = Schema_CreateCommandRequest(); + Schema_Object* RequestObject = Schema_GetCommandRequestObject(CommandRequest.schema_type); + + Schema_AddInt64(RequestObject, 1, PartitionId); + + return CommandRequest; + } +}; + +struct AuthorityDelegation : Component +{ + static const Worker_ComponentId ComponentId = SpatialConstants::AUTHORITY_DELEGATION_COMPONENT_ID; + + AuthorityDelegation() = default; + + AuthorityDelegation(AuthorityDelegationMap InDelegation) + : Delegations(InDelegation) + { + } + + AuthorityDelegation(const Worker_ComponentData& Data) + : AuthorityDelegation(Data.schema_type) + { + } + + AuthorityDelegation(Schema_ComponentData* Data) + { + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data); + + const uint32 DelegationCount = Schema_GetObjectCount(ComponentObject, 1); + for (uint32 i = 0; i < DelegationCount; i++) + { + Schema_Object* Delegation = Schema_IndexObject(ComponentObject, 1, i); + Worker_ComponentId AssignedComponentId = Schema_GetUint32(Delegation, SCHEMA_MAP_KEY_FIELD_ID); + Worker_PartitionId PartitionId = Schema_GetUint64(Delegation, SCHEMA_MAP_VALUE_FIELD_ID); + + Delegations.Add(AssignedComponentId, PartitionId); + } + } + + void ApplyComponentUpdate(const Worker_ComponentUpdate& Update) + { + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); + + // This is never emptied, so does not need an additional check for cleared fields + const uint32 DelegationCount = Schema_GetObjectCount(ComponentObject, 1); + if (DelegationCount > 0) + { + Delegations.Empty(); + for (uint32 i = 0; i < DelegationCount; i++) + { + Schema_Object* Delegation = Schema_IndexObject(ComponentObject, 1, i); + Worker_ComponentId AssignedComponentId = Schema_GetUint32(Delegation, SCHEMA_MAP_KEY_FIELD_ID); + Worker_PartitionId PartitionId = Schema_GetUint64(Delegation, SCHEMA_MAP_VALUE_FIELD_ID); + + Delegations.Add(AssignedComponentId, PartitionId); + } + } + } + + Worker_ComponentData CreateAuthorityDelegationData() + { + Worker_ComponentData Data = {}; + Data.component_id = ComponentId; + Data.schema_type = Schema_CreateComponentData(); + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + + for (const auto& KVPair : Delegations) + { + Schema_Object* KVPairObject = Schema_AddObject(ComponentObject, 1); + Schema_AddUint32(KVPairObject, SCHEMA_MAP_KEY_FIELD_ID, KVPair.Key); + Schema_AddUint64(KVPairObject, SCHEMA_MAP_VALUE_FIELD_ID, KVPair.Value); + } + + return Data; + } + + Worker_ComponentUpdate CreateAuthorityDelegationUpdate() + { + Worker_ComponentUpdate ComponentUpdate = {}; + ComponentUpdate.component_id = ComponentId; + ComponentUpdate.schema_type = Schema_CreateComponentUpdate(); + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(ComponentUpdate.schema_type); + + for (const auto& KVPair : Delegations) + { + Schema_Object* KVPairObject = Schema_AddObject(ComponentObject, 1); + Schema_AddUint32(KVPairObject, SCHEMA_MAP_KEY_FIELD_ID, KVPair.Key); + Schema_AddUint64(KVPairObject, SCHEMA_MAP_VALUE_FIELD_ID, KVPair.Value); + } + + return ComponentUpdate; + } + + AuthorityDelegationMap Delegations; }; } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/Tombstone.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/Tombstone.h index dedd2504d1..bc014809d7 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/Tombstone.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/Tombstone.h @@ -10,7 +10,6 @@ namespace SpatialGDK { - struct Tombstone : Component { static const Worker_ComponentId ComponentId = SpatialConstants::TOMBSTONE_COMPONENT_ID; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealMetadata.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealMetadata.h index ab9f7ab6d1..f3ae686faf 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealMetadata.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealMetadata.h @@ -10,8 +10,8 @@ #include "Utils/SchemaUtils.h" #include "GameFramework/Actor.h" -#include "UObject/UObjectHash.h" #include "UObject/Package.h" +#include "UObject/UObjectHash.h" #include #include @@ -22,15 +22,19 @@ using SubobjectToOffsetMap = TMap; namespace SpatialGDK { - struct UnrealMetadata : Component { static const Worker_ComponentId ComponentId = SpatialConstants::UNREAL_METADATA_COMPONENT_ID; UnrealMetadata() = default; - UnrealMetadata(const TSchemaOption& InStablyNamedRef, const FString& InClassPath, const TSchemaOption& InbNetStartup) - : StablyNamedRef(InStablyNamedRef), 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) { @@ -83,13 +87,18 @@ struct UnrealMetadata : Component #endif UClass* Class = FindObject(nullptr, *ClassPath, false); - // 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. + // 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); + 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); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h index 66a33e9e52..50c135b2fd 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h @@ -17,7 +17,8 @@ struct SPATIALGDK_API FUnrealObjectRef FUnrealObjectRef(Worker_EntityId Entity, uint32 Offset) : Entity(Entity) , Offset(Offset) - {} + { + } FUnrealObjectRef(Worker_EntityId Entity, uint32 Offset, FString Path, FUnrealObjectRef Outer, bool bNoLoadOnClient = false) : Entity(Entity) @@ -25,14 +26,12 @@ struct SPATIALGDK_API FUnrealObjectRef , Path(Path) , Outer(Outer) , bNoLoadOnClient(bNoLoadOnClient) - {} + { + } FUnrealObjectRef& operator=(const FUnrealObjectRef&) = default; - FORCEINLINE FString ToString() const - { - return FString::Printf(TEXT("(entity ID: %lld, offset: %u)"), Entity, Offset); - } + FORCEINLINE FString ToString() const { return FString::Printf(TEXT("(entity ID: %lld, offset: %u)"), Entity, Offset); } FORCEINLINE FUnrealObjectRef GetLevelReference() const { @@ -53,23 +52,16 @@ struct SPATIALGDK_API FUnrealObjectRef FORCEINLINE bool operator==(const FUnrealObjectRef& Other) const { - return Entity == Other.Entity && - Offset == Other.Offset && - ((!Path && !Other.Path) || (Path && Other.Path && Path->Equals(*Other.Path))) && - ((!Outer && !Other.Outer) || (Outer && Other.Outer && *Outer == *Other.Outer)) && - // Intentionally don't compare bNoLoadOnClient since it does not affect equality. - bUseClassPathToLoadObject == Other.bUseClassPathToLoadObject; + return Entity == Other.Entity && Offset == Other.Offset + && ((!Path && !Other.Path) || (Path && Other.Path && Path->Equals(*Other.Path))) + && ((!Outer && !Other.Outer) || (Outer && Other.Outer && *Outer == *Other.Outer)) && + // Intentionally don't compare bNoLoadOnClient since it does not affect equality. + bUseClassPathToLoadObject == Other.bUseClassPathToLoadObject; } - FORCEINLINE bool operator!=(const FUnrealObjectRef& Other) const - { - return !operator==(Other); - } + FORCEINLINE bool operator!=(const FUnrealObjectRef& Other) const { return !operator==(Other); } - FORCEINLINE bool IsValid() const - { - return (*this != NULL_OBJECT_REF && *this != UNRESOLVED_OBJECT_REF); - } + FORCEINLINE bool IsValid() const { return (*this != NULL_OBJECT_REF && *this != UNRESOLVED_OBJECT_REF); } static UObject* ToObjectPtr(const FUnrealObjectRef& ObjectRef, USpatialPackageMapClient* PackageMap, bool& bOutUnresolved); static FSoftObjectPath ToSoftObjectPath(const FUnrealObjectRef& ObjectRef); diff --git a/SpatialGDK/Source/SpatialGDK/Public/SimulatedPlayers/SimPlayerBPFunctionLibrary.h b/SpatialGDK/Source/SpatialGDK/Public/SimulatedPlayers/SimPlayerBPFunctionLibrary.h index ecda5e8b9c..aa09322688 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SimulatedPlayers/SimPlayerBPFunctionLibrary.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SimulatedPlayers/SimPlayerBPFunctionLibrary.h @@ -17,6 +17,6 @@ class SPATIALGDK_API USimPlayerBPFunctionLibrary : public UBlueprintFunctionLibr * This will return true for clients launched inside simulated player deployments, * or simulated clients launched from the Editor. */ - UFUNCTION(BlueprintPure, Category="SpatialOS|SimulatedPlayer", meta = (WorldContext = WorldContextObject)) + UFUNCTION(BlueprintPure, Category = "SpatialOS|SimulatedPlayer", meta = (WorldContext = WorldContextObject)) static bool IsSimulatedPlayer(const UObject* WorldContextObject); }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialCommonTypes.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialCommonTypes.h index 8c1f67c497..75ffc10e4c 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialCommonTypes.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialCommonTypes.h @@ -2,8 +2,10 @@ #pragma once -#include "CoreMinimal.h" - +#include "Containers/Array.h" +#include "Containers/Map.h" +#include "HAL/Platform.h" +#include "Misc/AssertionMacros.h" #include // IMPORTANT: This is required for Linux builds to succeed - don't remove! @@ -12,37 +14,81 @@ using Worker_EntityId_Key = int64; using Worker_RequestId_Key = int64; +using Worker_PartitionId = Worker_EntityId_Key; +using Trace_SpanIdType = uint8_t; + 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>; using FObjectReferencesMap = TMap; using FReliableRPCMap = TMap>; -using FObjectToRepStateMap = TMap >; +using FObjectToRepStateMap = TMap>; + +using AuthorityDelegationMap = TMap; -template +template struct FTrackableWorkerType : public T { FTrackableWorkerType() = default; FTrackableWorkerType(const T& Update) - : T(Update) {} + : T(Update) + { + } FTrackableWorkerType(T&& Update) - : T(MoveTemp(Update)) {} + : T(MoveTemp(Update)) + { + } #if TRACE_LIB_ACTIVE TraceKey Trace{ InvalidTraceKey }; #endif }; +template +struct TWeakObjectPtrKeyFuncs : DefaultKeyFuncs +{ + using typename DefaultKeyFuncs::KeyInitType; + using typename DefaultKeyFuncs::ElementInitType; + /** + * @return True if the keys match. + */ + static FORCEINLINE bool Matches(KeyInitType A, KeyInitType B) { return A.HasSameIndexAndSerialNumber(B); } +}; + +// TODO: These can be removed once event tracing is enabled UNR-3981 using FWorkerComponentUpdate = FTrackableWorkerType; using FWorkerComponentData = FTrackableWorkerType; + +/* A little helper to ensure no re-entrancy of modifications of data structures. */ +struct FUsageLock +{ + bool bIsSet = false; + + struct Scope + { + bool& bIsSetRef; + Scope(bool& bInIsSetRef) + : bIsSetRef(bInIsSetRef) + { + ensureMsgf(!bIsSetRef, TEXT("Unexpected re-entrancy occured in the Spatial GDK.")); + bIsSetRef = true; + } + ~Scope() { bIsSetRef = false; } + }; +}; + +// A macro to prevent re-entrant calls +#if DO_CHECK +#define __GDK_ENSURE_NO_MODIFICATIONS(x, y) x##y +#define _GDK_ENSURE_NO_MODIFICATIONS(x, y) __GDK_ENSURE_NO_MODIFICATIONS(x, y) +#define GDK_ENSURE_NO_MODIFICATIONS(FLAG) FUsageLock::Scope _GDK_ENSURE_NO_MODIFICATIONS(ScopedUsageCheck, __LINE__)(FLAG.bIsSet); +#else +#define GDK_ENSURE_NO_MODIFICATIONS(FLAG) +#endif diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h index 5bca36b0eb..3b5f348866 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h @@ -43,7 +43,6 @@ enum ESchemaComponentType : int32 namespace SpatialConstants { - inline FString RPCTypeToString(ERPCType RPCType) { switch (RPCType) @@ -68,158 +67,196 @@ inline FString RPCTypeToString(ERPCType RPCType) 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, + INITIAL_SNAPSHOT_PARTITION_ENTITY_ID = 4, + FIRST_AVAILABLE_ENTITY_ID = 5, }; -const Worker_ComponentId INVALID_COMPONENT_ID = 0; +const Worker_PartitionId INVALID_PARTITION_ID = INVALID_ENTITY_ID; + +const Worker_ComponentId INVALID_COMPONENT_ID = 0; + +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; -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 UNREAL_METADATA_COMPONENT_ID = 9996; -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 WORKER_COMPONENT_ID = 60; +const Worker_ComponentId PLAYERIDENTITY_COMPONENT_ID = 61; +const Worker_ComponentId AUTHORITY_DELEGATION_COMPONENT_ID = 65; +const Worker_ComponentId PARTITION_COMPONENT_ID = 66; + +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 UNREAL_METADATA_COMPONENT_ID = 9996; +const Worker_ComponentId GDK_DEBUG_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 SERVER_AUTH_COMPONENT_SET_ID = 9900; +const Worker_ComponentId CLIENT_AUTH_COMPONENT_SET_ID = 9901; +const Worker_ComponentId DATA_COMPONENT_SET_ID = 9902; +const Worker_ComponentId OWNER_ONLY_COMPONENT_SET_ID = 9903; +const Worker_ComponentId HANDOVER_COMPONENT_SET_ID = 9904; +const Worker_ComponentId GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID = 9905; + +const FString SERVER_AUTH_COMPONENT_SET_NAME = TEXT("ServerAuthoritativeComponentSet"); +const FString CLIENT_AUTH_COMPONENT_SET_NAME = TEXT("ClientAuthoritativeComponentSet"); +const FString DATA_COMPONENT_SET_NAME = TEXT("DataComponentSet"); +const FString OWNER_ONLY_COMPONENT_SET_NAME = TEXT("OwnerOnlyComponentSet"); +const FString HANDOVER_COMPONENT_SET_NAME = TEXT("HandoverComponentSet"); + +const Worker_ComponentId NOT_STREAMED_COMPONENT_ID = 9986; +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 VISIBLE_COMPONENT_ID = 9970; +const Worker_ComponentId SERVER_ONLY_ALWAYS_RELEVANT_COMPONENT_ID = 9968; + +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 NET_OWNING_CLIENT_WORKER_COMPONENT_ID = 9971; +const Worker_ComponentId MIGRATION_DIAGNOSTIC_COMPONENT_ID = 9969; +const Worker_ComponentId PARTITION_SHADOW_COMPONENT_ID = 9967; -const Worker_ComponentId STARTING_GENERATED_COMPONENT_ID = 10000; +const Worker_ComponentId STARTING_GENERATED_COMPONENT_ID = 10000; -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; +// System query tags for entity completeness +const Worker_ComponentId FIRST_EC_COMPONENT_ID = 2001; +const Worker_ComponentId ACTOR_AUTH_TAG_COMPONENT_ID = 2001; +const Worker_ComponentId ACTOR_NON_AUTH_TAG_COMPONENT_ID = 2002; +const Worker_ComponentId LB_TAG_COMPONENT_ID = 2005; +const Worker_ComponentId GDK_KNOWN_ENTITY_TAG_COMPONENT_ID = 2007; +const Worker_ComponentId LAST_EC_COMPONENT_ID = 2008; -const Schema_FieldId STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_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 ACTOR_COMPONENT_REPLICATES_ID = 1; -const Schema_FieldId ACTOR_TEAROFF_ID = 3; +const Schema_FieldId STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_ID = 1; -const Schema_FieldId HEARTBEAT_EVENT_ID = 1; -const Schema_FieldId HEARTBEAT_CLIENT_HAS_QUIT_ID = 1; +const Schema_FieldId ACTOR_COMPONENT_REPLICATES_ID = 1; +const Schema_FieldId ACTOR_TEAROFF_ID = 3; -const Schema_FieldId SHUTDOWN_MULTI_PROCESS_REQUEST_ID = 1; -const Schema_FieldId SHUTDOWN_ADDITIONAL_SERVERS_EVENT_ID = 1; +const Schema_FieldId HEARTBEAT_EVENT_ID = 1; +const Schema_FieldId HEARTBEAT_CLIENT_HAS_QUIT_ID = 1; -const Schema_FieldId CLEAR_RPCS_ON_ENTITY_CREATION = 1; +const Schema_FieldId SHUTDOWN_MULTI_PROCESS_REQUEST_ID = 1; +const Schema_FieldId SHUTDOWN_ADDITIONAL_SERVERS_EVENT_ID = 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; +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; +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_CLASS_PATH_TO_LOAD_ID = 6; +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_CLASS_PATH_TO_LOAD_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_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; +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 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; +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 Schema_FieldId VIRTUAL_WORKER_TRANSLATION_MAPPING_ID = 1; +const Schema_FieldId MAPPING_VIRTUAL_WORKER_ID = 1; +const Schema_FieldId MAPPING_PHYSICAL_WORKER_NAME_ID = 2; +const Schema_FieldId MAPPING_SERVER_WORKER_ENTITY_ID = 3; +const Schema_FieldId MAPPING_PARTITION_ID = 4; 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; +const Schema_FieldId WORKER_ID_ID = 1; +const Schema_FieldId WORKER_TYPE_ID = 2; + +// WorkerEntity command IDs +const Schema_FieldId WORKER_DISCONNECT_COMMAND_ID = 1; +const Schema_FieldId WORKER_CLAIM_PARTITION_COMMAND_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; +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; +const Schema_FieldId SERVER_WORKER_NAME_ID = 1; +const Schema_FieldId SERVER_WORKER_READY_TO_BEGIN_PLAY_ID = 2; +const Schema_FieldId SERVER_WORKER_SYSTEM_ENTITY_ID = 3; +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; +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; +const Schema_FieldId SPAWN_PLAYER_CLIENT_SYSTEM_ENTITY_ID = 5; // ForwardSpawnPlayerRequest type IDs. -const Schema_FieldId FORWARD_SPAWN_PLAYER_DATA_ID = 1; -const Schema_FieldId FORWARD_SPAWN_PLAYER_START_ACTOR_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 Schema_FieldId FORWARD_SPAWN_PLAYER_DATA_ID = 1; +const Schema_FieldId FORWARD_SPAWN_PLAYER_START_ACTOR_ID = 2; +const Schema_FieldId FORWARD_SPAWN_PLAYER_CLIENT_SYSTEM_ENTITY_ID = 3; +const Schema_FieldId FORWARD_SPAWN_PLAYER_RESPONSE_SUCCESS_ID = 1; // NetOwningClientWorker Field IDs. -const Schema_FieldId NET_OWNING_CLIENT_WORKER_FIELD_ID = 1; +const Schema_FieldId NET_OWNING_CLIENT_PARTITION_ENTITY_FIELD_ID = 1; // 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 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; + +// Migration diagnostic Field IDs +const Schema_FieldId MIGRATION_DIAGNOSTIC_COMMAND_ID = 1; + +// MigrationDiagnosticRequest type IDs. +const Schema_FieldId MIGRATION_DIAGNOSTIC_AUTHORITY_WORKER_ID = 1; +const Schema_FieldId MIGRATION_DIAGNOSTIC_ENTITY_ID = 2; +const Schema_FieldId MIGRATION_DIAGNOSTIC_REPLICATES_ID = 3; +const Schema_FieldId MIGRATION_DIAGNOSTIC_HAS_AUTHORITY_ID = 4; +const Schema_FieldId MIGRATION_DIAGNOSTIC_LOCKED_ID = 5; +const Schema_FieldId MIGRATION_DIAGNOSTIC_EVALUATION_ID = 6; +const Schema_FieldId MIGRATION_DIAGNOSTIC_DESTINATION_WORKER_ID = 7; +const Schema_FieldId MIGRATION_DIAGNOSTIC_OWNER_ID = 8; // 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; @@ -234,30 +271,29 @@ const FString INVALID_WORKER_NAME = TEXT(""); static const FName DefaultLayer = FName(TEXT("DefaultLayer")); -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 SpatialSessionIdURLOption = TEXT("spatialSessionId="); -const FString LOCATOR_HOST = TEXT("locator.improbable.io"); +const FString LOCATOR_HOST = TEXT("locator.improbable.io"); const FString LOCATOR_HOST_CN = TEXT("locator.spatialoschina.com"); -const uint16 LOCATOR_PORT = 443; +const uint16 LOCATOR_PORT = 443; -const FString CONSOLE_HOST = TEXT("console.improbable.io"); +const FString CONSOLE_HOST = TEXT("console.improbable.io"); const FString CONSOLE_HOST_CN = TEXT("console.spatialoschina.com"); -const FString AssemblyPattern = TEXT("^[a-zA-Z0-9_.-]{5,64}$"); -const FText AssemblyPatternHint = LOCTEXT("AssemblyPatternHint", "Assembly name may only contain alphanumeric characters, '_', '.', or '-', and must be between 5 and 64 characters long."); -const FString ProjectPattern = TEXT("^[a-z0-9_]{3,32}$"); -const FText ProjectPatternHint = LOCTEXT("ProjectPatternHint", "Project name may only contain lowercase alphanumeric characters or '_', and must be between 3 and 32 characters long."); -const FString DeploymentPattern = TEXT("^[a-z0-9_]{2,32}$"); -const FText DeploymentPatternHint = LOCTEXT("DeploymentPatternHint", "Deployment name may only contain lowercase alphanumeric characters or '_', and must be between 2 and 32 characters long."); -const FString Ipv4Pattern = TEXT("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$"); +const FString AssemblyPattern = TEXT("^[a-zA-Z0-9_.-]{5,64}$"); +const FText AssemblyPatternHint = + LOCTEXT("AssemblyPatternHint", + "Assembly name may only contain alphanumeric characters, '_', '.', or '-', and must be between 5 and 64 characters long."); +const FString ProjectPattern = TEXT("^[a-z0-9_]{3,32}$"); +const FText ProjectPatternHint = + LOCTEXT("ProjectPatternHint", + "Project name may only contain lowercase alphanumeric characters or '_', and must be between 3 and 32 characters long."); +const FString DeploymentPattern = TEXT("^[a-z0-9_]{2,32}$"); +const FText DeploymentPatternHint = + LOCTEXT("DeploymentPatternHint", + "Deployment name may only contain lowercase alphanumeric characters or '_', and must be between 2 and 32 characters long."); +const FString Ipv4Pattern = TEXT("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$"); inline float GetCommandRetryWaitTimeSeconds(uint32 NumAttempts) { @@ -266,10 +302,10 @@ inline float GetCommandRetryWaitTimeSeconds(uint32 NumAttempts) return FIRST_COMMAND_RETRY_WAIT_SECONDS * WaitTimeExponentialFactor; } -const FString LOCAL_HOST = TEXT("127.0.0.1"); -const uint16 DEFAULT_PORT = 7777; +const FString LOCAL_HOST = TEXT("127.0.0.1"); +const uint16 DEFAULT_PORT = 7777; -const uint16 DEFAULT_SERVER_RECEPTIONIST_PROXY_PORT = 7777; +const uint16 DEFAULT_SERVER_RECEPTIONIST_PROXY_PORT = 7777; const float ENTITY_QUERY_RETRY_WAIT_SECONDS = 3.0f; @@ -289,114 +325,144 @@ const FString URL_DISPLAY_NAME_OPTION = TEXT("displayname="); const FString URL_METADATA_OPTION = TEXT("metadata="); const FString URL_USE_EXTERNAL_IP_FOR_BRIDGE_OPTION = TEXT("useExternalIpForBridge"); +const FString SHUTDOWN_PREPARATION_WORKER_FLAG = TEXT("PrepareShutdown"); + const FString DEVELOPMENT_AUTH_PLAYER_ID = TEXT("Player Id"); -const FString SCHEMA_DATABASE_FILE_PATH = TEXT("Spatial/SchemaDatabase"); +const FString SCHEMA_DATABASE_FILE_PATH = TEXT("Spatial/SchemaDatabase"); const FString SCHEMA_DATABASE_ASSET_PATH = TEXT("/Game/Spatial/SchemaDatabase"); +// An empty map with the game mode override set to GameModeBase. +const FString EMPTY_TEST_MAP_PATH = TEXT("/SpatialGDK/Maps/Empty"); + const FString DEV_LOGIN_TAG = TEXT("dev_login"); // 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 - DEPLOYMENT_MAP_COMPONENT_ID, - STARTUP_ACTOR_MANAGER_COMPONENT_ID, - GSM_SHUTDOWN_COMPONENT_ID, - - // Debugging information - DEBUG_METRICS_COMPONENT_ID, - SPATIAL_DEBUGGING_COMPONENT_ID -}; +const TArray REQUIRED_COMPONENTS_FOR_NON_AUTH_CLIENT_INTEREST = + TArray{ // Actor components + UNREAL_METADATA_COMPONENT_ID, SPAWN_DATA_COMPONENT_ID, TOMBSTONE_COMPONENT_ID, DORMANT_COMPONENT_ID, -// 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 -}; + // Multicast RPCs + MULTICAST_RPCS_COMPONENT_ID, -// 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 - DEPLOYMENT_MAP_COMPONENT_ID, - STARTUP_ACTOR_MANAGER_COMPONENT_ID, - GSM_SHUTDOWN_COMPONENT_ID, - - // Unreal load balancing components - VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID + // Global state components + DEPLOYMENT_MAP_COMPONENT_ID, STARTUP_ACTOR_MANAGER_COMPONENT_ID, GSM_SHUTDOWN_COMPONENT_ID, + + // Debugging information + DEBUG_METRICS_COMPONENT_ID, SPATIAL_DEBUGGING_COMPONENT_ID, + + // Non auth actor tag + ACTOR_NON_AUTH_TAG_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, + + // Actor auth tag + ACTOR_AUTH_TAG_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 -{ - // RPCs from clients - CLIENT_ENDPOINT_COMPONENT_ID, - CLIENT_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, TOMBSTONE_COMPONENT_ID, DORMANT_COMPONENT_ID, + NET_OWNING_CLIENT_WORKER_COMPONENT_ID, + + // Multicast RPCs + MULTICAST_RPCS_COMPONENT_ID, + + // Global state components + DEPLOYMENT_MAP_COMPONENT_ID, STARTUP_ACTOR_MANAGER_COMPONENT_ID, GSM_SHUTDOWN_COMPONENT_ID, + + // Unreal load balancing components + VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID, + + // Authority intent component to handle scattered hierarchies + AUTHORITY_INTENT_COMPONENT_ID, + + // Tags: Well known entities, and non-auth actors + GDK_KNOWN_ENTITY_TAG_COMPONENT_ID, ACTOR_NON_AUTH_TAG_COMPONENT_ID, + + PARTITION_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{ // RPCs from clients + CLIENT_ENDPOINT_COMPONENT_ID, + + // Heartbeat + HEARTBEAT_COMPONENT_ID, - // Heartbeat - HEARTBEAT_COMPONENT_ID + // Auth actor tag + ACTOR_AUTH_TAG_COMPONENT_ID, + + PARTITION_COMPONENT_ID }; -inline Worker_ComponentId RPCTypeToWorkerComponentIdLegacy(ERPCType RPCType) +inline bool IsEntityCompletenessComponent(Worker_ComponentId ComponentId) { - switch (RPCType) - { - case ERPCType::CrossServer: - { - return SpatialConstants::SERVER_TO_SERVER_COMMAND_ENDPOINT_COMPONENT_ID; - } - case ERPCType::NetMulticast: - { - return SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID_LEGACY; - } - case ERPCType::ClientReliable: - case ERPCType::ClientUnreliable: - { - return SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY; - } - case ERPCType::ServerReliable: - case ERPCType::ServerUnreliable: - { - return SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY; - } - default: - checkNoEntry(); - return SpatialConstants::INVALID_COMPONENT_ID; - } + return ComponentId >= SpatialConstants::FIRST_EC_COMPONENT_ID && ComponentId <= SpatialConstants::LAST_EC_COMPONENT_ID; } -inline Worker_ComponentId GetClientAuthorityComponent(bool bUsingRingBuffers) -{ - return bUsingRingBuffers ? CLIENT_ENDPOINT_COMPONENT_ID : CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY; -} +// TODO: These containers should be cleaned up when we move to reading component set data directly from schema bundle - UNR-4666 +const TArray ServerAuthorityWellKnownSchemaImports = { + "improbable/standard_library.schema", + "unreal/gdk/authority_intent.schema", + "unreal/gdk/debug_component.schema", + "unreal/gdk/debug_metrics.schema", + "unreal/gdk/net_owning_client_worker.schema", + "unreal/gdk/not_streamed.schema", + "unreal/gdk/query_tags.schema", + "unreal/gdk/relevant.schema", + "unreal/gdk/rpc_components.schema", + "unreal/gdk/spatial_debugging.schema", + "unreal/gdk/spawndata.schema", + "unreal/gdk/tombstone.schema", + "unreal/gdk/unreal_metadata.schema", + "unreal/generated/rpc_endpoints.schema", + "unreal/generated/NetCullDistance/ncdcomponents.schema", +}; + +const TMap ServerAuthorityWellKnownComponents = { + { POSITION_COMPONENT_ID, "improbable.Position" }, + { INTEREST_COMPONENT_ID, "improbable.Interest" }, + { AUTHORITY_DELEGATION_COMPONENT_ID, "improbable.AuthorityDelegation" }, + { AUTHORITY_INTENT_COMPONENT_ID, "unreal.AuthorityIntent" }, + { GDK_DEBUG_COMPONENT_ID, "unreal.DebugComponent" }, + { DEBUG_METRICS_COMPONENT_ID, "unreal.DebugMetrics" }, + { NET_OWNING_CLIENT_WORKER_COMPONENT_ID, "unreal.NetOwningClientWorker" }, + { NOT_STREAMED_COMPONENT_ID, "unreal.NotStreamed" }, + { ALWAYS_RELEVANT_COMPONENT_ID, "unreal.AlwaysRelevant" }, + { DORMANT_COMPONENT_ID, "unreal.Dormant" }, + { VISIBLE_COMPONENT_ID, "unreal.Visible" }, + { SERVER_TO_SERVER_COMMAND_ENDPOINT_COMPONENT_ID, "unreal.UnrealServerToServerCommandEndpoint" }, + { SPATIAL_DEBUGGING_COMPONENT_ID, "unreal.SpatialDebugging" }, + { SPAWN_DATA_COMPONENT_ID, "unreal.SpawnData" }, + { TOMBSTONE_COMPONENT_ID, "unreal.Tombstone" }, + { UNREAL_METADATA_COMPONENT_ID, "unreal.UnrealMetadata" }, + { SERVER_ENDPOINT_COMPONENT_ID, "unreal.generated.UnrealServerEndpoint" }, + { MULTICAST_RPCS_COMPONENT_ID, "unreal.generated.UnrealMulticastRPCs" }, +}; + +const TArray ClientAuthorityWellKnownSchemaImports = { "unreal/gdk/heartbeat.schema", "unreal/gdk/rpc_components.schema", + "unreal/generated/rpc_endpoints.schema" }; + +const TMap ClientAuthorityWellKnownComponents = { + { HEARTBEAT_COMPONENT_ID, "unreal.Heartbeat" }, + { CLIENT_ENDPOINT_COMPONENT_ID, "unreal.generated.UnrealClientEndpoint" }, +}; + +const TArray KnownEntityAuthorityComponents = { POSITION_COMPONENT_ID, METADATA_COMPONENT_ID, + INTEREST_COMPONENT_ID, PLAYER_SPAWNER_COMPONENT_ID, + DEPLOYMENT_MAP_COMPONENT_ID, STARTUP_ACTOR_MANAGER_COMPONENT_ID, + GSM_SHUTDOWN_COMPONENT_ID, VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID, + SERVER_WORKER_COMPONENT_ID }; -} // ::SpatialConstants +} // namespace 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 index fc9adf3721..308d25bcc7 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKConsoleCommands.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKConsoleCommands.h @@ -9,6 +9,6 @@ class UWorld; namespace SpatialGDKConsoleCommands { - void ConsoleCommand_ConnectToLocator(const TArray& Args, UWorld* World); +void ConsoleCommand_ConnectToLocator(const TArray& Args, UWorld* World); } // namespace diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKModule.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKModule.h index dd46ec78ae..65dd313b8b 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKModule.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKModule.h @@ -6,6 +6,7 @@ #include "Modules/ModuleManager.h" #include "Utils/EngineVersionCheck.h" +#include "Utils/WorkerVersionCheck.h" #include "SpatialGDKLoader.h" diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h index 116d93030f..e1f97c2131 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h @@ -16,30 +16,31 @@ class ASpatialDebugger; /** * Enum that maps Unreal's log verbosity to allow use in settings. -**/ + */ UENUM() namespace ESettingsWorkerLogVerbosity { - enum Type - { - Fatal = 1, - Error, - Warning, - Display, - Log, - Verbose, - VeryVerbose, - }; +enum Type +{ + NoLogging = 0, + Fatal, + Error, + Warning, + Display, + Log, + Verbose, + VeryVerbose, +}; } UENUM() namespace EServicesRegion { - enum Type - { - Default, - CN - }; +enum Type +{ + Default, + CN +}; } USTRUCT(BlueprintType) @@ -71,76 +72,78 @@ 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 = (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 = (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. - */ + * 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 = (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. */ + /** + * 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 = (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.) - */ + * 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 = (DisplayName = "Heartbeat Timeout (seconds)")) float HeartbeatTimeoutSeconds; /** - * Same as HeartbeatTimeoutSeconds, but used if WITH_EDITOR is defined. - */ + * 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. + * (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 = (DisplayName = "Maximum Actors replicated per tick")) uint32 ActorReplicationRateLimit; /** - * 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) - */ + * 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 = (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. 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. + * 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 = (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 - */ + * 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 = (DisplayName = "SpatialOS Network Update Rate")) float OpsUpdateRate; - /** Replicate handover properties between servers, required for zoned worker deployments. If Unreal Load Balancing is enabled, this will be set based on the load balancing strategy.*/ - UPROPERTY(EditAnywhere, config, Category = "Replication") - bool bEnableHandover; - /** * 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. @@ -149,24 +152,43 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject float MaxNetCullDistanceSquared; /** Seconds to wait before executing a received RPC substituting nullptr for unresolved UObjects*/ - UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (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 attempting to reprocess queued incoming RPCs */ - UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (DisplayName = "Wait Time Before Attempting To Reprocess Queued Incoming RPCs")) + UPROPERTY(EditAnywhere, config, Category = "Replication", + meta = (DisplayName = "Wait Time Before Attempting To Reprocess Queued Incoming RPCs")) float QueuedIncomingRPCRetryTime; /** Seconds to wait before retying all queued outgoing RPCs. If 0 there will not be retried on a timer. */ UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (DisplayName = "Wait Time Before Retrying Outoing RPC")) float QueuedOutgoingRPCRetryTime; - /** Frequency for updating an Actor's SpatialOS Position. Updating position should have a low update rate since it is expensive.*/ + /** + * Minimum time, in seconds, required to pass before an Actor will update its SpatialOS Position, if it has also traveled more than the + * PositionUpdateLowerThresholdCentimeters since its last update. + */ + UPROPERTY(EditAnywhere, config, Category = "SpatialOS Position Updates") + float PositionUpdateLowerThresholdSeconds; + + /** + * Minimum distance, in centimeters, required for an Actor to move before its SpatialOS Position is updated, if more than + * PositionUpdateLowerThresholdSeconds seconds have also passed since its last update. + */ + UPROPERTY(EditAnywhere, config, Category = "SpatialOS Position Updates") + float PositionUpdateLowerThresholdCentimeters; + + /** + * Maximum time, in seconds, that can pass before an Actor will update its SpatialOS Position, if it has also traveled any non-null + * amount of centimeters since its last update. + */ UPROPERTY(EditAnywhere, config, Category = "SpatialOS Position Updates") - float PositionUpdateFrequency; + float PositionUpdateThresholdMaxSeconds; - /** Threshold an Actor needs to move, in centimeters, before its SpatialOS Position is updated.*/ + /** Maximum distance, in centimeters, an Actor can move before its SpatialOS Position is updated.*/ UPROPERTY(EditAnywhere, config, Category = "SpatialOS Position Updates") - float PositionDistanceThreshold; + float PositionUpdateThresholdMaxCentimeters; /** Metrics about client and server performance can be reported to SpatialOS to monitor a deployments health.*/ UPROPERTY(EditAnywhere, config, Category = "Metrics") @@ -181,10 +203,10 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject float MetricsReportRate; /** - * By default the SpatialOS Runtime reports server-worker instance’s load in frames per second (FPS). - * Select this to switch so it reports as seconds per frame. - * This value is visible as 'Load' in the Inspector, next to each worker. - */ + * By default the SpatialOS Runtime reports server-worker instance’s load in frames per second (FPS). + * 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") bool bUseFrameTimeAsLoad; @@ -193,7 +215,8 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject 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 = (DisplayName = "Maximum Dynamically Attached Subobjects Per Class")) + UPROPERTY(EditAnywhere, config, Category = "Schema Generation", + meta = (DisplayName = "Maximum Dynamically Attached Subobjects Per Class")) uint32 MaxDynamicallyAttachedSubobjectsPerClass; /** The receptionist host to use if no 'receptionistHost' argument is passed to the command line. */ @@ -206,36 +229,45 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject bool bPreventClientCloudDeploymentAutoConnect; public: - bool GetPreventClientCloudDeploymentAutoConnect() const; - UPROPERTY(EditAnywhere, Config, Category = "Region settings", meta = (ConfigRestartRequired = true, DisplayName = "Region where services are located")) + UPROPERTY(EditAnywhere, Config, Category = "Region settings", + meta = (ConfigRestartRequired = true, DisplayName = "Region where services are located")) TEnumAsByte ServicesRegion; - /** 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 = (DisplayName = "Worker Log Level")) + /** Deprecated! + Upgraded into the two settings below for local/cloud configurations. + Ticket for removal UNR-4348 */ + UPROPERTY(config, meta = (DeprecatedProperty, DeprecationMessage = "Use LocalWorkerLogLevel or CloudWorkerLogLevel")) TEnumAsByte WorkerLogLevel; + /** 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 = (DisplayName = "Local Worker Log Level")) + TEnumAsByte LocalWorkerLogLevel; + + /** 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 = (DisplayName = "Cloud Worker Log Level")) + TEnumAsByte CloudWorkerLogLevel; + UPROPERTY(EditAnywhere, config, Category = "Debug", meta = (MetaClass = "SpatialDebugger")) TSubclassOf SpatialDebugger; - /** EXPERIMENTAL: Run SpatialWorkerConnection on Game Thread. */ - UPROPERTY(Config) - bool bRunSpatialWorkerConnectionOnGameThread; + /** Enables multi-worker, if false uses single worker strategy in the editor. */ + UPROPERTY(EditAnywhere, config, Category = "Load Balancing", meta = (DisplayName = "Enable multi-worker in editor")) + bool bEnableMultiWorker; - /** RPC ring buffers is enabled when either the matching setting is set, or load balancing is enabled */ - bool UseRPCRingBuffer() const; +#if WITH_EDITOR + void SetMultiWorkerEditorEnabled(const bool bIsEnabled); + FORCEINLINE bool IsMultiWorkerEditorEnabled() const { return bEnableMultiWorker; } +#endif // WITH_EDITOR private: #if WITH_EDITOR - bool CanEditChange(const GDK_PROPERTY(Property)* InProperty) const override; + bool CanEditChange(const GDK_PROPERTY(Property) * InProperty) const override; void UpdateServicesRegionFile(); #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; @@ -250,7 +282,10 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject bool ShouldRPCTypeAllowUnresolvedParameters(const ERPCType Type) 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. */ + /** + * 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; @@ -274,10 +309,12 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject 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.")) + 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.")) + 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; } @@ -316,22 +353,60 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject UPROPERTY(EditAnywhere, Config, Category = "Interest") bool bEnableClientQueriesOnServer; - /** Experimental feature to use SpatialView layer when communicating with the Worker */ - UPROPERTY(Config) - bool bUseSpatialView; - /** - * By default, load balancing config will be read from the WorldSettings, but this can be toggled to override - * the map's config with a 1x1 grid. - */ - TOptional bOverrideMultiWorker; + * By default, load balancing config will be read from the WorldSettings, but this can be toggled to override + * the multi-worker settings class + */ + TOptional OverrideMultiWorkerSettingsClass; /** - * This will enable warning messages for ActorSpawning that could be legitimate but is likely to be an error. - */ + * This will allow Actors to be spawned on a layer different to the intended authoritative layer. + */ UPROPERTY(Config) - bool bEnableMultiWorkerDebuggingWarnings; + bool bEnableCrossLayerActorSpawning; - UPROPERTY(EditAnywhere, Config, Category = "Logging", AdvancedDisplay, meta = (DisplayName = "Whether or not to suppress a warning if an RPC of Type is being called with unresolved references. Default is false. QueuedIncomingWaitRPC time is still respected.")) + // clang-format off + UPROPERTY(EditAnywhere, Config, Category = "Logging", AdvancedDisplay, + meta = (DisplayName = "Whether or not to suppress a warning if an RPC of Type is being called with unresolved references. Default is false. QueuedIncomingWaitRPC time is still respected.")) + // clang-format on TMap RPCTypeAllowUnresolvedParamMap; + + /** + * Time in seconds, controls at which frequency logs related to startup are emitted. + */ + UPROPERTY(EditAnywhere, Config, Category = "Logging", AdvancedDisplay) + float StartupLogRate; + + /** + * Time in seconds, controls at which frequency the logs related to failed actor migration are emitted. + */ + UPROPERTY(EditAnywhere, Config, Category = "Logging", AdvancedDisplay) + float ActorMigrationLogRate; + + /* + * -- EXPERIMENTAL -- + * This will enable event tracing for the Unreal client/worker. + */ + UPROPERTY(EditAnywhere, Config, Category = "Event Tracing") + bool bEventTracingEnabled; + + /* + * Used to set the default sample rate if event tracing is enabled. + */ + UPROPERTY(EditAnywhere, Config, Category = "Event Tracing", + meta = (EditCondition = "bEventTracingEnabled", ClampMin = 0.0f, ClampMax = 1.0f)) + float SamplingProbability; + + /* + * Used to override sample rate for specific trace events. + */ + UPROPERTY(EditAnywhere, Config, Category = "Event Tracing", meta = (EditCondition = "bEventTracingEnabled")) + TMap EventSamplingModeOverrides; + + /* + * -- EXPERIMENTAL -- + * The maximum size of the event tracing file, in bytes + */ + UPROPERTY(Config) + uint64 MaxEventTracingFileSizeBytes; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/AuthorityRecord.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/AuthorityRecord.h index 32b2fbbe3b..03bfee896f 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/AuthorityRecord.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/AuthorityRecord.h @@ -1,16 +1,15 @@ #pragma once -#include "SpatialView/EntityComponentId.h" #include "Containers/Array.h" +#include "SpatialView/EntityComponentId.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. +// 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: diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/Callbacks.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/Callbacks.h new file mode 100644 index 0000000000..e447fee891 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/Callbacks.h @@ -0,0 +1,93 @@ +#pragma once + +#include "Containers/Array.h" +#include "Templates/Function.h" + +namespace SpatialGDK +{ +using CallbackId = int32; + +/** + * Container holding a set of callbacks. + * Callbacks are called in the order in which they were registered. + * Callbacks added or removed during callback invocation will be reconciled once all callbacks have been invoked. + * Nested calls to Invoke are not allowed. + */ +template +class TCallbacks +{ +public: + using CallbackType = TFunction; + + bool IsEmpty() const { return Callbacks.Num() == 0; } + + void Register(CallbackId CallbackId, CallbackType Callback) + { + if (bCurrentlyInvokingCallbacks) + { + CallbacksToAdd.Push({ MoveTemp(Callback), CallbackId }); + } + else + { + Callbacks.Push({ MoveTemp(Callback), CallbackId }); + } + } + + void Remove(CallbackId Id) + { + if (bCurrentlyInvokingCallbacks) + { + CallbacksToRemove.Emplace(Id); + } + else + { + CallbackAndId* Element = Callbacks.FindByPredicate([Id](const CallbackAndId& E) { + return E.Id == Id; + }); + if (Element != nullptr) + { + Callbacks.RemoveAt(Element - Callbacks.GetData()); + } + } + } + + void Invoke(const T& Value) + { + check(!bCurrentlyInvokingCallbacks); + + bCurrentlyInvokingCallbacks = true; + for (const CallbackAndId& Callback : Callbacks) + { + Callback.Callback(Value); + } + bCurrentlyInvokingCallbacks = false; + + // Sort out pending adds and removes. + if (CallbacksToAdd.Num() > 0) + { + Callbacks.Append(MoveTemp(CallbacksToAdd)); + CallbacksToAdd.Empty(); + } + if (CallbacksToRemove.Num() > 0) + { + Callbacks.RemoveAll([this](const CallbackAndId& E) { + return CallbacksToRemove.Contains(E.Id); + }); + CallbacksToRemove.Empty(); + } + } + +private: + struct CallbackAndId + { + CallbackType Callback; + CallbackId Id; + }; + + TArray Callbacks; + bool bCurrentlyInvokingCallbacks = false; + TArray CallbacksToAdd; + TArray CallbacksToRemove; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandRequest.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandRequest.h index 89c3ee843f..5be645f2ef 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandRequest.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandRequest.h @@ -3,12 +3,11 @@ #pragma once #include "Templates/UniquePtr.h" -#include #include +#include namespace SpatialGDK { - struct CommandRequestDeleter { void operator()(Schema_CommandRequest* CommandRequest) const noexcept diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandResponse.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandResponse.h index e25215db54..043024c477 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandResponse.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandResponse.h @@ -3,12 +3,11 @@ #pragma once #include "Templates/UniquePtr.h" -#include #include +#include namespace SpatialGDK { - struct CommandResponseDeleter { void operator()(Schema_CommandResponse* CommandResponse) const noexcept diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandRetryHandler.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandRetryHandler.h new file mode 100644 index 0000000000..5c71bbdf71 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandRetryHandler.h @@ -0,0 +1,133 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialView/OpList/OpList.h" +#include "SpatialView/WorkerView.h" + +#include "Containers/Array.h" +#include "Containers/Map.h" +#include "GenericPlatform/GenericPlatformMath.h" +#include "Math/NumericLimits.h" + +namespace SpatialGDK +{ +struct FRetryData +{ + int32 Retries; + float BackOffTimeS; + float BackOffIncrementS; + float MaximumRetryTimeS; + uint32 TimeoutMillis; + + void RetryAndBackOff() + { + --Retries; + BackOffTimeS = FMath::Min(BackOffTimeS + BackOffIncrementS, MaximumRetryTimeS); + BackOffIncrementS *= 2; + } + + void RetryWithoutBackOff() { --Retries; } + + void StopRetries() { Retries = -1; } +}; + +// Will retry until it's done or no longer makes sense. +constexpr FRetryData RETRY_UNTIL_COMPLETE = { TNumericLimits::Max(), 0, 0.1f, 5.0f, 0 }; +constexpr FRetryData RETRY_MAX_TIMES = { SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS, 0, 0.1f, 5.0f, 0 }; +constexpr FRetryData NO_RETRIES = { 0, 0, 0.f, 0.f, 0 }; + +template +class TCommandRetryHandler +{ +public: + using DataType = typename T::CommandData; + + void ProcessOps(float TimeAdvancedS, OpList& Ops, WorkerView& View) + { + TimeElapsedS += TimeAdvancedS; + + for (uint32 i = 0; i < Ops.Count; ++i) + { + if (T::CanHandleOp(Ops.Ops[i])) + { + HandleResponse(Ops.Ops[i]); + } + } + + while (CommandsToSendHeap.Num() > 0) + { + FDataToSend& Command = CommandsToSendHeap[0]; + if (Command.TimeToSend > TimeElapsedS) + { + return; + } + + SendRequest(Command.RequestId, MoveTemp(Command.Data), Command.Retry, View); + + CommandsToSendHeap.HeapPopDiscard(CompareByTimeToSend); + } + } + + void SendRequest(Worker_RequestId RequestId, DataType Data, const FRetryData& RetryData, WorkerView& View) + { + if (RetryData.Retries > 0) + { + T::SendCommandRequest(RequestId, Data, RetryData.TimeoutMillis, View); + RequestsInFlight.Emplace(RequestId, FDataInFlight{ MoveTemp(Data), RetryData }); + } + else + { + T::SendCommandRequest(RequestId, MoveTemp(Data), RetryData.TimeoutMillis, View); + } + } + +protected: + struct FDataToSend + { + Worker_RequestId RequestId; + DataType Data; + double TimeToSend; + FRetryData Retry; + }; + + struct FDataInFlight + { + DataType Data; + FRetryData Retry; + }; + + static bool CompareByTimeToSend(const FDataToSend& Lhs, const FDataToSend& Rhs) { return Lhs.TimeToSend < Rhs.TimeToSend; } + + void HandleResponse(Worker_Op& Op) + { + Worker_RequestId& RequestId = T::GetRequestId(Op); + auto It = RequestsInFlight.CreateKeyIterator(RequestId); + if (!It) + { + return; + } + + FDataInFlight& Data = It.Value(); + + // Update the retry data and enqueue a retry if appropriate. + T::UpdateRetries(Op, Data.Retry); + if (Data.Retry.Retries >= 0) + { + CommandsToSendHeap.HeapPush(FDataToSend{ RequestId, MoveTemp(Data.Data), TimeElapsedS + Data.Retry.BackOffTimeS, Data.Retry }, + CompareByTimeToSend); + // Effectively remove the op by setting its request ID to something invalid. + RequestId *= -1; + } + + It.RemoveCurrent(); + } + + double TimeElapsedS = 0.0; + TArray CommandsToSendHeap; + TMap RequestsInFlight; +}; + +} // namespace SpatialGDK +// Implementations for specific commands. +#include "SpatialView/CommandRetryHandlerImpl.h" diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandRetryHandlerImpl.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandRetryHandlerImpl.h new file mode 100644 index 0000000000..985296fd80 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandRetryHandlerImpl.h @@ -0,0 +1,212 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialView/CommandRetryHandler.h" + +#include "Algo/Transform.h" +#include "Misc/Optional.h" + +namespace SpatialGDK +{ +struct FDeleteEntityRetryHandlerImpl +{ + struct CommandData + { + Worker_EntityId EntityId; + FSpatialGDKSpanId SpanId; + }; + + static bool CanHandleOp(const Worker_Op& Op) { return Op.op_type == WORKER_OP_TYPE_DELETE_ENTITY_RESPONSE; } + + static Worker_RequestId& GetRequestId(Worker_Op& Op) + { + check(CanHandleOp(Op)); + return Op.op.delete_entity_response.request_id; + } + + static void UpdateRetries(const Worker_Op& Op, FRetryData& RetryData) + { + check(CanHandleOp(Op)); + if (Op.op.delete_entity_response.status_code == WORKER_STATUS_CODE_TIMEOUT) + { + RetryData.RetryAndBackOff(); + } + else + { + RetryData.StopRetries(); + } + } + + static void SendCommandRequest(Worker_RequestId RequestId, const CommandData& Data, uint32 TimeoutMillis, WorkerView& View) + { + View.SendDeleteEntityRequest(DeleteEntityRequest{ RequestId, Data.EntityId, TimeoutMillis, Data.SpanId }); + } + + static void SendCommandRequest(Worker_RequestId RequestId, CommandData&& Data, uint32 TimeoutMillis, WorkerView& View) + { + View.SendDeleteEntityRequest(DeleteEntityRequest{ RequestId, Data.EntityId, TimeoutMillis, Data.SpanId }); + } +}; + +struct FCreateEntityRetryHandlerImpl +{ + struct CommandData + { + TArray Components; + TOptional EntityId; + FSpatialGDKSpanId SpanId; + }; + + static bool CanHandleOp(const Worker_Op& Op) { return Op.op_type == WORKER_OP_TYPE_CREATE_ENTITY_RESPONSE; } + + static Worker_RequestId& GetRequestId(Worker_Op& Op) + { + check(CanHandleOp(Op)); + return Op.op.create_entity_response.request_id; + } + + static void UpdateRetries(const Worker_Op& Op, FRetryData& RetryData) + { + check(CanHandleOp(Op)); + if (Op.op.create_entity_response.status_code == WORKER_STATUS_CODE_TIMEOUT) + { + RetryData.RetryAndBackOff(); + } + else + { + RetryData.StopRetries(); + } + } + + static void SendCommandRequest(Worker_RequestId RequestId, const CommandData& Data, uint32 TimeoutMillis, WorkerView& View) + { + TArray ComponentsCopy; + ComponentsCopy.Reserve(Data.Components.Num()); + Algo::Transform(Data.Components, ComponentsCopy, [](const ComponentData& Component) { + return Component.DeepCopy(); + }); + + View.SendCreateEntityRequest(CreateEntityRequest{ RequestId, MoveTemp(ComponentsCopy), Data.EntityId, TimeoutMillis, Data.SpanId }); + } + + static void SendCommandRequest(Worker_RequestId RequestId, CommandData&& Data, uint32 TimeoutMillis, WorkerView& View) + { + View.SendCreateEntityRequest( + CreateEntityRequest{ RequestId, MoveTemp(Data.Components), Data.EntityId, TimeoutMillis, Data.SpanId }); + } +}; + +struct FReserveEntityIdsRetryHandlerImpl +{ + using CommandData = uint32; + + static bool CanHandleOp(const Worker_Op& Op) { return Op.op_type == WORKER_OP_TYPE_RESERVE_ENTITY_IDS_RESPONSE; } + + static Worker_RequestId& GetRequestId(Worker_Op& Op) + { + check(CanHandleOp(Op)); + return Op.op.reserve_entity_ids_response.request_id; + } + + static void UpdateRetries(const Worker_Op& Op, FRetryData& RetryData) + { + check(CanHandleOp(Op)); + if (Op.op.reserve_entity_ids_response.status_code == WORKER_STATUS_CODE_TIMEOUT) + { + RetryData.RetryAndBackOff(); + } + else + { + RetryData.StopRetries(); + } + } + + static void SendCommandRequest(Worker_RequestId RequestId, CommandData NumberOfIds, uint32 TimeoutMillis, WorkerView& View) + { + View.SendReserveEntityIdsRequest(ReserveEntityIdsRequest{ RequestId, NumberOfIds, TimeoutMillis }); + } +}; + +struct FEntityQueryRetryHandlerImpl +{ + using CommandData = EntityQuery; + + static bool CanHandleOp(const Worker_Op& Op) { return Op.op_type == WORKER_OP_TYPE_ENTITY_QUERY_RESPONSE; } + + static Worker_RequestId& GetRequestId(Worker_Op& Op) + { + check(CanHandleOp(Op)); + return Op.op.entity_query_response.request_id; + } + + static void UpdateRetries(const Worker_Op& Op, FRetryData& RetryData) + { + check(CanHandleOp(Op)); + if (Op.op.entity_query_response.status_code == WORKER_STATUS_CODE_TIMEOUT) + { + RetryData.RetryAndBackOff(); + } + else + { + RetryData.StopRetries(); + } + } + + static void SendCommandRequest(Worker_RequestId RequestId, const CommandData& Query, uint32 TimeoutMillis, WorkerView& View) + { + View.SendEntityQueryRequest(EntityQueryRequest{ RequestId, EntityQuery(Query.GetWorkerQuery()), TimeoutMillis }); + } + + static void SendCommandRequest(Worker_RequestId RequestId, CommandData&& Query, uint32 TimeoutMillis, WorkerView& View) + { + View.SendEntityQueryRequest(EntityQueryRequest{ RequestId, MoveTemp(Query), TimeoutMillis }); + } +}; + +struct FEntityCommandRetryHandlerImpl +{ + struct CommandData + { + Worker_EntityId EntityId; + CommandRequest Request; + FSpatialGDKSpanId SpanId; + }; + + static bool CanHandleOp(const Worker_Op& Op) { return Op.op_type == WORKER_OP_TYPE_COMMAND_RESPONSE; } + + static Worker_RequestId& GetRequestId(Worker_Op& Op) + { + check(CanHandleOp(Op)); + return Op.op.command_response.request_id; + } + + static void UpdateRetries(const Worker_Op& Op, FRetryData& RetryData) + { + check(CanHandleOp(Op)); + switch (static_cast(Op.op.command_response.status_code)) + { + case WORKER_STATUS_CODE_TIMEOUT: + RetryData.RetryAndBackOff(); + break; + case WORKER_STATUS_CODE_AUTHORITY_LOST: + RetryData.RetryWithoutBackOff(); + break; + default: + RetryData.StopRetries(); + } + } + + static void SendCommandRequest(Worker_RequestId RequestId, const CommandData& Query, uint32 TimeoutMillis, WorkerView& View) + { + View.SendEntityCommandRequest( + EntityCommandRequest{ Query.EntityId, RequestId, Query.Request.DeepCopy(), TimeoutMillis, Query.SpanId }); + } + + static void SendCommandRequest(Worker_RequestId RequestId, CommandData&& Query, uint32 TimeoutMillis, WorkerView& View) + { + View.SendEntityCommandRequest( + EntityCommandRequest{ Query.EntityId, RequestId, MoveTemp(Query.Request), TimeoutMillis, Query.SpanId }); + } +}; +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ComponentData.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ComponentData.h index 889f93bafb..2f8e6b4c36 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ComponentData.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ComponentData.h @@ -3,12 +3,11 @@ #pragma once #include "Templates/UniquePtr.h" -#include #include +#include namespace SpatialGDK { - class ComponentUpdate; struct ComponentDataDeleter diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ComponentSetData.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ComponentSetData.h new file mode 100644 index 0000000000..f54cd7d217 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ComponentSetData.h @@ -0,0 +1,15 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Containers/Map.h" +#include "Containers/Set.h" +#include "improbable/c_worker.h" + +namespace SpatialGDK +{ +struct FComponentSetData +{ + TMap> ComponentSets; +}; +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ComponentUpdate.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ComponentUpdate.h index 9201cc6336..b10219c41f 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ComponentUpdate.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ComponentUpdate.h @@ -3,12 +3,11 @@ #pragma once #include "Templates/UniquePtr.h" -#include #include +#include namespace SpatialGDK { - struct ComponentUpdateDeleter { void operator()(Schema_ComponentUpdate* ComponentUpdate) const noexcept diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandler/AbstractConnectionHandler.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandler/AbstractConnectionHandler.h index 2035c37673..b41637d448 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandler/AbstractConnectionHandler.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandler/AbstractConnectionHandler.h @@ -5,11 +5,9 @@ #include "SpatialView/MessagesToSend.h" #include "SpatialView/OpList/OpList.h" #include "Templates/UniquePtr.h" -#include namespace SpatialGDK { - class AbstractConnectionHandler { public: @@ -28,11 +26,11 @@ class AbstractConnectionHandler // Consumes messages and sends them to the deployment. virtual void SendMessages(TUniquePtr Messages) = 0; - // Return the unique ID for the worker. - virtual const FString& GetWorkerId() const = 0; + // Return the unique ID for the worker. + virtual const FString& GetWorkerId() const = 0; - // Returns the attributes for the worker. - virtual const TArray& GetWorkerAttributes() const = 0; + // Returns the worker system entity ID. + virtual Worker_EntityId GetWorkerSystemEntityId() const = 0; }; -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandler/InitialOpListConnectionHandler.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandler/InitialOpListConnectionHandler.h new file mode 100644 index 0000000000..2c3be021fc --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandler/InitialOpListConnectionHandler.h @@ -0,0 +1,43 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialView/ConnectionHandler/AbstractConnectionHandler.h" +#include "SpatialView/OpList/ExtractedOpList.h" +#include "SpatialView/OpList/OpList.h" + +#include "Containers/Array.h" +#include "Templates/UniquePtr.h" + +namespace SpatialGDK +{ +// A connection handler that can present selected ops early, holding back all others. +class InitialOpListConnectionHandler : public AbstractConnectionHandler +{ +public: + explicit InitialOpListConnectionHandler(TUniquePtr InnerHandler, + TFunction OpExtractor); + + virtual void Advance() override; + virtual uint32 GetOpListCount() override; + virtual OpList GetNextOpList() override; + virtual void SendMessages(TUniquePtr Messages) override; + virtual const FString& GetWorkerId() const override; + virtual Worker_EntityId GetWorkerSystemEntityId() const override; + +private: + enum + { + EXTRACTING_OPS, + FLUSHING_QUEUED_OP_LISTS, + PASS_THROUGH + } State; + + OpList QueueAndExtractOps(); + + TUniquePtr InnerHandler; + TFunction OpExtractor; + TArray QueuedOpLists; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandler/SpatialOSConnectionHandler.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandler/SpatialOSConnectionHandler.h index db8db1ba40..bed0cf5b02 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandler/SpatialOSConnectionHandler.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandler/SpatialOSConnectionHandler.h @@ -4,33 +4,36 @@ #include "SpatialView/ConnectionHandler/AbstractConnectionHandler.h" #include "SpatialView/OpList/OpList.h" -#include "SpatialView/OpList/WorkerConnectionOpList.h" namespace SpatialGDK { +class SpatialEventTracer; class SpatialOSConnectionHandler : public AbstractConnectionHandler { public: - explicit SpatialOSConnectionHandler(Worker_Connection* Connection); + explicit SpatialOSConnectionHandler(Worker_Connection* Connection, TSharedPtr EventTracer); + ~SpatialOSConnectionHandler(); virtual void Advance() override; virtual uint32 GetOpListCount() override; virtual OpList GetNextOpList() override; virtual void SendMessages(TUniquePtr Messages) override; virtual const FString& GetWorkerId() const override; - virtual const TArray& GetWorkerAttributes() const override; + virtual Worker_EntityId GetWorkerSystemEntityId() const override; private: - struct ConnectionDeleter + struct WorkerConnectionDeleter { - void operator()(Worker_Connection* Connection) const noexcept; + void operator()(Worker_Connection* ConnectionToDestroy) const noexcept; }; +# + TSharedPtr EventTracer; + TUniquePtr Connection; - TUniquePtr Connection; TMap InternalToUserRequestId; FString WorkerId; - TArray WorkerAttributes; + Worker_EntityId WorkerSystemEntityId; }; -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CriticalSectionFilter.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CriticalSectionFilter.h new file mode 100644 index 0000000000..9044176ec9 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CriticalSectionFilter.h @@ -0,0 +1,21 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Containers/Array.h" +#include "OpList/OpList.h" + +namespace SpatialGDK +{ +// Splits and queues op lists to ensure that we don't partially process open critical sections. +class FCriticalSectionFilter +{ +public: + void AddOpList(OpList Ops); + TArray GetReadyOpLists(); + +private: + TArray ReadyOps; + TArray OpenCriticalSectionOps; +}; +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/Dispatcher.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/Dispatcher.h new file mode 100644 index 0000000000..f742e8c60c --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/Dispatcher.h @@ -0,0 +1,99 @@ +#pragma once + +#include "SpatialView/Callbacks.h" +#include "SpatialView/ViewDelta.h" + +#include "Containers/Array.h" +#include "Templates/Function.h" + +namespace SpatialGDK +{ +struct FEntityComponentChange +{ + Worker_EntityId EntityId; + const ComponentChange& Change; +}; + +using FEntityCallback = TCallbacks::CallbackType; +using FComponentValueCallback = TCallbacks::CallbackType; + +class FDispatcher +{ +public: + FDispatcher(); + + void InvokeCallbacks(const TArray& Deltas); + + CallbackId RegisterAndInvokeComponentAddedCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback, + const EntityView& View); + CallbackId RegisterAndInvokeComponentRemovedCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback, + const EntityView& View); + CallbackId RegisterAndInvokeComponentValueCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback, + const EntityView& View); + CallbackId RegisterAndInvokeAuthorityGainedCallback(Worker_ComponentId ComponentId, FEntityCallback Callback, const EntityView& View); + CallbackId RegisterAndInvokeAuthorityLostCallback(Worker_ComponentId ComponentId, FEntityCallback Callback, const EntityView& View); + + CallbackId RegisterComponentAddedCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback); + CallbackId RegisterComponentRemovedCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback); + CallbackId RegisterComponentValueCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback); + CallbackId RegisterAuthorityGainedCallback(Worker_ComponentId ComponentId, FEntityCallback Callback); + CallbackId RegisterAuthorityLostCallback(Worker_ComponentId ComponentId, FEntityCallback Callback); + CallbackId RegisterAuthorityLostTempCallback(Worker_ComponentId ComponentId, FEntityCallback Callback); + + void RemoveCallback(CallbackId Id); + +private: + struct FComponentCallbacks + { + explicit FComponentCallbacks(Worker_ComponentId Id) + : Id(Id) + { + } + Worker_ComponentId Id; + TCallbacks ComponentAddedCallbacks; + TCallbacks ComponentRemovedCallbacks; + TCallbacks ComponentValueCallbacks; + + struct ComponentIdComparator + { + bool operator()(const FComponentCallbacks& Callbacks, Worker_ComponentId ComponentId) const + { + return Callbacks.Id < ComponentId; + } + }; + }; + + struct FAuthorityCallbacks + { + explicit FAuthorityCallbacks(Worker_ComponentId Id) + : Id(Id) + { + } + Worker_ComponentId Id; + TCallbacks AuthorityGainedCallbacks; + TCallbacks AuthorityLostCallbacks; + TCallbacks AuthorityLostTemporarilyCallbacks; + + struct ComponentIdComparator + { + bool operator()(const FAuthorityCallbacks& Callbacks, Worker_ComponentId ComponentId) const + { + return Callbacks.Id < ComponentId; + } + }; + }; + + static void InvokeWithExistingValues(Worker_ComponentId ComponentId, const FComponentValueCallback& Callback, const EntityView& View); + void HandleComponentPresenceChanges(Worker_EntityId EntityId, const ComponentSpan& ComponentChanges, + TCallbacks FComponentCallbacks::*Callbacks); + void HandleComponentValueChanges(Worker_EntityId EntityId, const ComponentSpan& ComponentChanges); + void HandleAuthorityChange(Worker_EntityId EntityId, const ComponentSpan& AuthorityChanges, + TCallbacks FAuthorityCallbacks::*Callbacks); + + // Component callbacks sorted by component ID; + TArray ComponentCallbacks; + // Authority callbacks sorted by component ID; + TArray AuthorityCallbacks; + CallbackId NextCallbackId; +}; +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentId.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentId.h index 39265791f6..20ec698a40 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentId.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentId.h @@ -5,9 +5,14 @@ namespace SpatialGDK { - struct EntityComponentId { + EntityComponentId(Worker_EntityId InEntityId, Worker_ComponentId InComponentId) + : EntityId(InEntityId) + , ComponentId(InComponentId) + { + } + Worker_EntityId EntityId; Worker_ComponentId ComponentId; @@ -22,4 +27,4 @@ struct EntityComponentId } }; -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentRecord.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentRecord.h index 72cb2371e0..69a82adc2b 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentRecord.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentRecord.h @@ -2,13 +2,12 @@ #pragma once +#include "Containers/Array.h" #include "SpatialView/EntityComponentId.h" #include "SpatialView/EntityComponentUpdateRecord.h" -#include "Containers/Array.h" namespace SpatialGDK { - // Can be recorded as at most one of // added // removed diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentTypes.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentTypes.h index 24417ba13e..669ebdca92 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentTypes.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentTypes.h @@ -2,22 +2,18 @@ #pragma once -#include "SpatialView/EntityComponentId.h" #include "SpatialView/ComponentData.h" #include "SpatialView/ComponentUpdate.h" +#include "SpatialView/EntityComponentId.h" namespace SpatialGDK { - struct EntityComponentUpdate { Worker_EntityId EntityId; ComponentUpdate Update; - EntityComponentId GetEntityComponentId() const - { - return { EntityId, Update.GetComponentId() }; - } + EntityComponentId GetEntityComponentId() const { return { EntityId, Update.GetComponentId() }; } }; struct EntityComponentData @@ -25,10 +21,7 @@ struct EntityComponentData Worker_EntityId EntityId; ComponentData Data; - EntityComponentId GetEntityComponentId() const - { - return { EntityId, Data.GetComponentId() }; - } + EntityComponentId GetEntityComponentId() const { return { EntityId, Data.GetComponentId() }; } }; struct EntityComponentCompleteUpdate @@ -37,30 +30,27 @@ struct EntityComponentCompleteUpdate ComponentData CompleteUpdate; ComponentUpdate Events; - EntityComponentId GetEntityComponentId() const - { - return { EntityId, CompleteUpdate.GetComponentId() }; - } + EntityComponentId GetEntityComponentId() const { return { EntityId, CompleteUpdate.GetComponentId() }; } }; struct EntityComponentIdEquality { EntityComponentId Id; - bool operator()(const EntityComponentUpdate& Element) const - { - return Element.GetEntityComponentId() == Id; - } + bool operator()(const EntityComponentUpdate& Element) const { return Element.GetEntityComponentId() == Id; } + + bool operator()(const EntityComponentData& Element) const { return Element.GetEntityComponentId() == Id; } + + bool operator()(const EntityComponentCompleteUpdate& Element) const { return Element.GetEntityComponentId() == Id; } +}; + +struct ComponentIdEquality +{ + Worker_ComponentId Id; - bool operator()(const EntityComponentData& Element) const - { - return Element.GetEntityComponentId() == Id; - } + bool operator()(const ComponentData& Element) const { return Element.GetComponentId() == Id; } - bool operator()(const EntityComponentCompleteUpdate& Element) const - { - return Element.GetEntityComponentId() == Id; - } + bool operator()(const ComponentUpdate& Element) const { return Element.GetComponentId() == Id; } }; -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentUpdateRecord.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentUpdateRecord.h index 8c4459aca9..396d0b530d 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentUpdateRecord.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentUpdateRecord.h @@ -2,13 +2,12 @@ #pragma once +#include "Containers/Array.h" #include "SpatialView/EntityComponentId.h" #include "SpatialView/EntityComponentTypes.h" -#include "Containers/Array.h" namespace SpatialGDK { - class EntityComponentUpdateRecord { public: diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityDelta.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityDelta.h new file mode 100644 index 0000000000..ada75d6bb7 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityDelta.h @@ -0,0 +1,129 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Containers/Array.h" +#include +#include + +namespace SpatialGDK +{ +struct CompleteUpdateData +{ + Schema_ComponentData* Data; + Schema_Object* Events; +}; + +struct ComponentChange +{ + explicit ComponentChange(Worker_ComponentId Id, Schema_ComponentData* Data) + : ComponentId(Id) + , Type(ADD) + , Data(Data) + { + } + + explicit ComponentChange(Worker_ComponentId Id, Schema_ComponentUpdate* Update) + : ComponentId(Id) + , Type(UPDATE) + , Update(Update) + { + } + + explicit ComponentChange(Worker_ComponentId Id, Schema_ComponentData* Data, Schema_Object* Events) + : ComponentId(Id) + , Type(COMPLETE_UPDATE) + , CompleteUpdate{ Data, Events } + { + } + + explicit ComponentChange(Worker_ComponentId id) + : ComponentId(id) + , Type(REMOVE) + { + } + + Worker_ComponentId ComponentId; + enum + { + ADD, + REMOVE, + UPDATE, + COMPLETE_UPDATE + } Type; + union + { + Schema_ComponentData* Data; + Schema_ComponentUpdate* Update; + CompleteUpdateData CompleteUpdate; + }; +}; + +struct AuthorityChange +{ + AuthorityChange(Worker_ComponentId Id, int Type) + : ComponentId(Id) + , Type(static_cast(Type)) + { + } + + Worker_ComponentId ComponentId; + enum AuthorityType + { + AUTHORITY_GAINED = 1, + AUTHORITY_LOST = 2, + AUTHORITY_LOST_TEMPORARILY = 3 + } Type; +}; + +/** Pointer to an array and a size that can be used in a ranged based for. */ +template +class ComponentSpan +{ +public: + ComponentSpan() + : Elements(nullptr) + , Count(0) + { + } + ComponentSpan(const T* Elements, int32 Count) + : Elements(Elements) + , Count(Count) + { + } + + const T* begin() const { return Elements; } + + const T* end() const { return Elements + Count; } + + int32 Num() const { return Count; } + + const T& operator[](int32 Index) const { return Elements[Index]; } + + const T* GetData() const { return Elements; } + +private: + const T* Elements; + int32 Count; +}; + +struct EntityDelta +{ + Worker_EntityId EntityId; + enum + { + UPDATE, + ADD, + REMOVE, + TEMPORARILY_REMOVED + } Type; + ComponentSpan ComponentsAdded; + ComponentSpan ComponentsRemoved; + ComponentSpan ComponentUpdates; + ComponentSpan ComponentsRefreshed; + ComponentSpan AuthorityGained; + ComponentSpan AuthorityLost; + ComponentSpan AuthorityLostTemporarily; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityPresenceRecord.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityPresenceRecord.h index 6bd102bda9..85f8c86a92 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityPresenceRecord.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityPresenceRecord.h @@ -7,7 +7,6 @@ namespace SpatialGDK { - // An entity can be recorded as at most one of // added // removed diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityQuery.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityQuery.h index 4ef7be2ee8..f6898300c3 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityQuery.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityQuery.h @@ -1,4 +1,4 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved #pragma once @@ -7,7 +7,6 @@ namespace SpatialGDK { - // A wrapper around a Worker_EntityQuery that allows it to be stored and moved. class EntityQuery { @@ -35,7 +34,6 @@ class EntityQuery TArray SnapshotComponentIds; TArray Constraints; // Stable pointer storage. - uint8 ResultType; }; -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityView.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityView.h new file mode 100644 index 0000000000..9b237e5a84 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityView.h @@ -0,0 +1,22 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialCommonTypes.h" +#include "SpatialView/ComponentData.h" + +#include "Containers/Array.h" +#include "Containers/Map.h" +#include + +namespace SpatialGDK +{ +struct EntityViewElement +{ + TArray Components; + TArray Authority; +}; + +using EntityView = TMap; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/MessagesToSend.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/MessagesToSend.h index b3c66ff48c..efc6811d03 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/MessagesToSend.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/MessagesToSend.h @@ -2,14 +2,13 @@ #pragma once +#include "Containers/Array.h" #include "Interop/Connection/OutgoingMessages.h" #include "SpatialView/OutgoingComponentMessage.h" #include "SpatialView/OutgoingMessages.h" -#include "Containers/Array.h" namespace SpatialGDK { - struct MessagesToSend { TArray ComponentMessages; @@ -25,4 +24,4 @@ struct MessagesToSend TArray Logs; }; -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/EntityComponentOpList.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/EntityComponentOpList.h index 0ec7f52932..023037c975 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/EntityComponentOpList.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/EntityComponentOpList.h @@ -1,15 +1,21 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved #pragma once +#include "Containers/Array.h" #include "SpatialView/ComponentData.h" #include "SpatialView/ComponentUpdate.h" #include "SpatialView/OpList/OpList.h" -#include "Containers/Array.h" +#include "StringStorage.h" #include "Templates/UniquePtr.h" namespace SpatialGDK { +struct OpListEntity +{ + Worker_EntityId EntityId; + TArray Components; +}; // Data for a set of ops representing struct EntityComponentOpListData : OpListData @@ -17,6 +23,9 @@ struct EntityComponentOpListData : OpListData TArray Ops; TArray DataStorage; TArray UpdateStorage; + TArray MessageStorage; + TArray> QueriedEntities; + TArray> ComponentArrayStorage; }; class EntityComponentOpListBuilder @@ -24,15 +33,33 @@ class EntityComponentOpListBuilder public: EntityComponentOpListBuilder(); + EntityComponentOpListBuilder& AddEntity(Worker_EntityId EntityId); + EntityComponentOpListBuilder& RemoveEntity(Worker_EntityId EntityId); EntityComponentOpListBuilder& AddComponent(Worker_EntityId EntityId, ComponentData Data); EntityComponentOpListBuilder& UpdateComponent(Worker_EntityId EntityId, ComponentUpdate Update); EntityComponentOpListBuilder& RemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId); - EntityComponentOpListBuilder& SetAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId, Worker_Authority Authority); + EntityComponentOpListBuilder& SetAuthority(Worker_EntityId EntityId, Worker_ComponentSetId ComponentSetId, Worker_Authority Authority, + TArray Components); + EntityComponentOpListBuilder& SetDisconnect(Worker_ConnectionStatusCode StatusCode, StringStorage DisconnectReason); + EntityComponentOpListBuilder& AddCreateEntityCommandResponse(Worker_EntityId EntityID, Worker_RequestId RequestId, + Worker_StatusCode StatusCode, StringStorage Message); + EntityComponentOpListBuilder& AddEntityQueryCommandResponse(Worker_RequestId RequestId, TArray Results, + Worker_StatusCode StatusCode, StringStorage Message); + EntityComponentOpListBuilder& AddEntityCommandResponse(Worker_EntityId EntityID, Worker_RequestId RequestId, + Worker_StatusCode StatusCode, StringStorage Message); + EntityComponentOpListBuilder& AddDeleteEntityCommandResponse(Worker_EntityId EntityID, Worker_RequestId RequestId, + Worker_StatusCode StatusCode, StringStorage Message); + EntityComponentOpListBuilder& AddReserveEntityIdsCommandResponse(Worker_EntityId EntityID, uint32 NumberOfEntities, + Worker_RequestId RequestId, Worker_StatusCode StatusCode, + StringStorage Message); OpList CreateOpList() &&; private: TUniquePtr OpListData; + const char* StoreString(StringStorage Message) const; + const Worker_Entity* StoreQueriedEntities(TArray Entities) const; + const Worker_ComponentData* StoreComponentDataArray(TArray Components) const; }; -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/ExtractedOpList.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/ExtractedOpList.h new file mode 100644 index 0000000000..8309892386 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/ExtractedOpList.h @@ -0,0 +1,21 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once +#include "OpList.h" + +#include "Containers/Array.h" + +namespace SpatialGDK +{ +struct ExtractedOpListData : OpListData +{ + TArray ExtractedOps; + + void AddOp(Worker_Op& Op) + { + ExtractedOps.Push(Op); + Op.op_type = 0; + } +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/OpList.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/OpList.h index 4b835fd04a..9b1607dd40 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/OpList.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/OpList.h @@ -7,8 +7,8 @@ namespace SpatialGDK { - -struct OpListData { +struct OpListData +{ virtual ~OpListData() = default; }; @@ -19,4 +19,4 @@ struct OpList TUniquePtr Storage; }; -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/SplitOpList.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/SplitOpList.h index 66ff3726b6..16c14fe0bf 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/SplitOpList.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/SplitOpList.h @@ -6,7 +6,6 @@ namespace SpatialGDK { - struct SplitOpListData : OpListData { TSharedPtr Data; @@ -24,12 +23,13 @@ struct SplitOpListPair check(InitialOpListCount <= OriginalOpList.Count); // Transfer ownership to a shared pointer. TSharedPtr SplitData(OriginalOpList.Storage.Release()); - Head = {OriginalOpList.Ops, InitialOpListCount, MakeUnique(SplitData)}; - Tail = {OriginalOpList.Ops + InitialOpListCount, OriginalOpList.Count - InitialOpListCount, MakeUnique(MoveTemp(SplitData))}; + Head = { OriginalOpList.Ops, InitialOpListCount, MakeUnique(SplitData) }; + Tail = { OriginalOpList.Ops + InitialOpListCount, OriginalOpList.Count - InitialOpListCount, + MakeUnique(MoveTemp(SplitData)) }; } OpList Head; OpList Tail; }; -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/StringStorage.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/StringStorage.h new file mode 100644 index 0000000000..6707236410 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/StringStorage.h @@ -0,0 +1,38 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Containers/Array.h" +#include "Containers/StringConv.h" +#include "Templates/UniquePtr.h" +#include +#include + +namespace SpatialGDK +{ +class StringStorage +{ +public: + StringStorage(const FString& InString) + { + const int32 SourceLength = TCString::Strlen(*InString); + // Add one to include the null terminator. + const int32 BufferSize = FTCHARToUTF8_Convert::ConvertedLength(*InString, SourceLength) + 1; + Storage = MakeUnique(BufferSize); + FTCHARToUTF8_Convert::Convert(Storage.Get(), BufferSize, *InString, SourceLength + 1); + } + + StringStorage(const char* InString) + { + // Add one to include the null terminator. + const size_t BufferSize = std::strlen(InString) + 1; + Storage = MakeUnique(BufferSize); + std::memcpy(Storage.Get(), InString, BufferSize); + } + + const char* Get() const { return Storage.Get(); } + +private: + TUniquePtr Storage; +}; +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/ViewDeltaLegacyOpList.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/ViewDeltaLegacyOpList.h index fda02aff3e..ef2e951f07 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/ViewDeltaLegacyOpList.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/ViewDeltaLegacyOpList.h @@ -2,21 +2,13 @@ #pragma once +#include "Containers/Array.h" #include "SpatialView/OpList/OpList.h" #include "SpatialView/ViewDelta.h" -#include "Containers/Array.h" namespace SpatialGDK { - -struct ViewDeltaLegacyOpListData: OpListData -{ - TArray Ops; - // Used to store UTF8 disconnect string. - ViewDelta Delta; - TUniquePtr DisconnectReason; -}; - /** Creates an OpList from a ViewDelta. */ -OpList GetOpListFromViewDelta(ViewDelta Delta); -} // namespace SpatialGDK +TArray GetOpsFromEntityDeltas(const TArray& Deltas); + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/WorkerConnectionOpList.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/WorkerConnectionOpList.h index 762f6a6151..52e6cf7ba3 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/WorkerConnectionOpList.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/WorkerConnectionOpList.h @@ -8,7 +8,6 @@ namespace SpatialGDK { - struct WorkerConnectionOpListData : OpListData { struct Deleter @@ -24,7 +23,8 @@ struct WorkerConnectionOpListData : OpListData TUniquePtr OpList; - explicit WorkerConnectionOpListData(Worker_OpList* OpList) : OpList(OpList) + explicit WorkerConnectionOpListData(Worker_OpList* OpList) + : OpList(OpList) { } }; @@ -32,7 +32,7 @@ struct WorkerConnectionOpListData : OpListData inline OpList GetOpListFromConnection(Worker_Connection* Connection) { Worker_OpList* Ops = Worker_Connection_GetOpList(Connection, 0); - return {Ops->ops, Ops->op_count, MakeUnique(Ops)}; + return { Ops->ops, Ops->op_count, MakeUnique(Ops) }; } -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OutgoingComponentMessage.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OutgoingComponentMessage.h index ea3c59abc8..9e9e70f005 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OutgoingComponentMessage.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OutgoingComponentMessage.h @@ -8,31 +8,51 @@ namespace SpatialGDK { - // Represents one of a component addition, update, or removal. -// Internally schema data is stored using raw pointers. However the interface exclusively uses explicitly owning objects to denote ownership. +// Internally schema data is stored using raw pointers. However the interface exclusively uses explicitly owning objects to denote +// ownership. class OutgoingComponentMessage { public: - enum MessageType {NONE, ADD, UPDATE, REMOVE}; + enum MessageType + { + NONE, + ADD, + UPDATE, + REMOVE + }; explicit OutgoingComponentMessage() - : EntityId(0), ComponentId(0), Type(NONE) + : EntityId(0) + , ComponentId(0) + , SpanId() + , Type(NONE) { } - explicit OutgoingComponentMessage(Worker_EntityId EntityId, ComponentData ComponentAdded) - : EntityId(EntityId), ComponentId(ComponentAdded.GetComponentId()), ComponentAdded(MoveTemp(ComponentAdded).Release()), Type(ADD) + explicit OutgoingComponentMessage(Worker_EntityId EntityId, ComponentData ComponentAdded, const FSpatialGDKSpanId& SpanId) + : EntityId(EntityId) + , ComponentId(ComponentAdded.GetComponentId()) + , SpanId(SpanId) + , ComponentAdded(MoveTemp(ComponentAdded).Release()) + , Type(ADD) { } - explicit OutgoingComponentMessage(Worker_EntityId EntityId, ComponentUpdate ComponentUpdated) - : EntityId(EntityId), ComponentId(ComponentUpdated.GetComponentId()), ComponentUpdated(MoveTemp(ComponentUpdated).Release()), Type(UPDATE) + explicit OutgoingComponentMessage(Worker_EntityId EntityId, ComponentUpdate ComponentUpdated, const FSpatialGDKSpanId& SpanId) + : EntityId(EntityId) + , ComponentId(ComponentUpdated.GetComponentId()) + , SpanId(SpanId) + , ComponentUpdated(MoveTemp(ComponentUpdated).Release()) + , Type(UPDATE) { } - explicit OutgoingComponentMessage(Worker_EntityId EntityId, Worker_ComponentId RemovedComponentId) - : EntityId(EntityId), ComponentId(RemovedComponentId), Type(REMOVE) + explicit OutgoingComponentMessage(Worker_EntityId EntityId, Worker_ComponentId RemovedComponentId, const FSpatialGDKSpanId& SpanId) + : EntityId(EntityId) + , ComponentId(RemovedComponentId) + , SpanId(SpanId) + , Type(REMOVE) { } @@ -47,7 +67,10 @@ class OutgoingComponentMessage OutgoingComponentMessage& operator=(const OutgoingComponentMessage& Other) = delete; OutgoingComponentMessage(OutgoingComponentMessage&& Other) noexcept - : EntityId(Other.EntityId), ComponentId(Other.ComponentId), Type(Other.Type) + : EntityId(Other.EntityId) + , ComponentId(Other.ComponentId) + , SpanId(Other.SpanId) + , Type(Other.Type) { switch (Other.Type) { @@ -67,10 +90,11 @@ class OutgoingComponentMessage Other.Type = NONE; } - OutgoingComponentMessage& operator=(OutgoingComponentMessage&& Other) noexcept { - + OutgoingComponentMessage& operator=(OutgoingComponentMessage&& Other) noexcept + { EntityId = Other.EntityId; ComponentId = Other.ComponentId; + SpanId = Other.SpanId; // As data is stored in owning raw pointers we need to make sure resources are released. DeleteSchemaObjects(); @@ -95,10 +119,7 @@ class OutgoingComponentMessage return *this; } - MessageType GetType() const - { - return Type; - } + MessageType GetType() const { return Type; } ComponentData ReleaseComponentAdded() && { @@ -119,6 +140,8 @@ class OutgoingComponentMessage Worker_EntityId EntityId; Worker_ComponentId ComponentId; + FSpatialGDKSpanId SpanId; + private: void DeleteSchemaObjects() { @@ -146,4 +169,4 @@ class OutgoingComponentMessage MessageType Type; }; -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OutgoingMessages.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OutgoingMessages.h index e670237879..2552811ec3 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OutgoingMessages.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OutgoingMessages.h @@ -2,19 +2,17 @@ #pragma once -#include "SpatialView/ComponentData.h" -#include "SpatialView/CommandResponse.h" -#include "SpatialView/CommandRequest.h" -#include "SpatialView/EntityQuery.h" #include "Containers/UnrealString.h" #include "Misc/Optional.h" +#include "SpatialView/CommandRequest.h" +#include "SpatialView/CommandResponse.h" +#include "SpatialView/ComponentData.h" +#include "SpatialView/EntityQuery.h" #include "UObject/NameTypes.h" #include - namespace SpatialGDK { - struct ReserveEntityIdsRequest { Worker_RequestId RequestId; @@ -28,6 +26,7 @@ struct CreateEntityRequest TArray EntityComponents; TOptional EntityId; TOptional TimeoutMillis; + FSpatialGDKSpanId SpanId; }; struct DeleteEntityRequest @@ -35,6 +34,7 @@ struct DeleteEntityRequest Worker_RequestId RequestId; Worker_EntityId EntityId; TOptional TimeoutMillis; + FSpatialGDKSpanId SpanId; }; struct EntityQueryRequest @@ -50,18 +50,21 @@ struct EntityCommandRequest Worker_RequestId RequestId; CommandRequest Request; TOptional TimeoutMillis; + FSpatialGDKSpanId SpanId; }; struct EntityCommandResponse { Worker_RequestId RequestId; CommandResponse Response; + FSpatialGDKSpanId SpanId; }; struct EntityCommandFailure { Worker_RequestId RequestId; FString Message; + FSpatialGDKSpanId SpanId; }; struct LogMessage @@ -71,4 +74,4 @@ struct LogMessage FString Message; }; -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ReceivedOpEventHandler.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ReceivedOpEventHandler.h new file mode 100644 index 0000000000..8eb84ea5b8 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ReceivedOpEventHandler.h @@ -0,0 +1,19 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Interop/Connection/SpatialEventTracer.h" +#include "OpList/OpList.h" + +namespace SpatialGDK +{ +class FReceivedOpEventHandler +{ +public: + explicit FReceivedOpEventHandler(TSharedPtr EventTracer = nullptr); + void ProcessOpLists(const OpList& Ops); + +private: + TSharedPtr EventTracer; +}; +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/SubView.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/SubView.h new file mode 100644 index 0000000000..8f21ace39b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/SubView.h @@ -0,0 +1,81 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Dispatcher.h" +#include "EntityView.h" +#include "Templates/Function.h" + +using FFilterPredicate = TFunction; +using FRefreshCallback = TFunction; +using FDispatcherRefreshCallback = TFunction; +using FComponentChangeRefreshPredicate = TFunction; +using FAuthorityChangeRefreshPredicate = TFunction; + +namespace SpatialGDK +{ +class FSubView +{ +public: + static const FFilterPredicate NoFilter; + static const TArray NoDispatcherCallbacks; + static const FComponentChangeRefreshPredicate NoComponentChangeRefreshPredicate; + static const FAuthorityChangeRefreshPredicate NoAuthorityChangeRefreshPredicate; + + // The subview constructor takes the filter and the dispatcher refresh callbacks for the subview, rather than + // adding them to the subview later. This is to maintain the invariant that a subview always has the correct + // full set of complete entities. During construction, it calculates the initial set of complete entities, + // and registers the passed dispatcher callbacks in order to ensure all possible changes which could change + // the state of completeness for any entity are picked up by the subview to maintain this invariant. + FSubView(const Worker_ComponentId InTagComponentId, FFilterPredicate InFilter, const EntityView* InView, FDispatcher& Dispatcher, + const TArray& DispatcherRefreshCallbacks); + + ~FSubView() = default; + + // Non-copyable, non-movable + FSubView(const FSubView&) = delete; + FSubView(FSubView&&) = default; + FSubView& operator=(const FSubView&) = delete; + FSubView& operator=(FSubView&&) = default; + + void Advance(const ViewDelta& Delta); + const FSubViewDelta& GetViewDelta() const; + void RefreshEntity(const Worker_EntityId EntityId); + + const EntityView& GetView() const; + bool HasComponent(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) const; + bool HasAuthority(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) const; + + // Helper functions for creating dispatcher refresh callbacks for use when constructing a subview. + // Takes an optional predicate argument to further filter what causes a refresh. Example: Only trigger + // a refresh if the received component change has a change for a certain field. + static FDispatcherRefreshCallback CreateComponentExistenceRefreshCallback(FDispatcher& Dispatcher, const Worker_ComponentId ComponentId, + const FComponentChangeRefreshPredicate& RefreshPredicate); + static FDispatcherRefreshCallback CreateComponentChangedRefreshCallback(FDispatcher& Dispatcher, const Worker_ComponentId ComponentId, + const FComponentChangeRefreshPredicate& RefreshPredicate); + static FDispatcherRefreshCallback CreateAuthorityChangeRefreshCallback(FDispatcher& Dispatcher, const Worker_ComponentId ComponentId, + const FAuthorityChangeRefreshPredicate& RefreshPredicate); + +private: + void RegisterTagCallbacks(FDispatcher& Dispatcher); + void RegisterRefreshCallbacks(const TArray& DispatcherRefreshCallbacks); + void OnTaggedEntityAdded(const Worker_EntityId EntityId); + void OnTaggedEntityRemoved(const Worker_EntityId EntityId); + void CheckEntityAgainstFilter(const Worker_EntityId EntityId); + void EntityComplete(const Worker_EntityId EntityId); + void EntityIncomplete(const Worker_EntityId EntityId); + + Worker_ComponentId TagComponentId; + FFilterPredicate Filter; + const EntityView* View; + + FSubViewDelta SubViewDelta; + + TArray TaggedEntities; + TArray CompleteEntities; + TArray NewlyCompleteEntities; + TArray NewlyIncompleteEntities; + TArray TemporarilyIncompleteEntities; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewCoordinator.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewCoordinator.h index 4bc68bbcf2..23aaebcb47 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewCoordinator.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewCoordinator.h @@ -2,51 +2,116 @@ #pragma once -#include "SpatialView/WorkerView.h" +#include "SpatialView/CommandRetryHandler.h" +#include "SpatialView/ComponentSetData.h" #include "SpatialView/ConnectionHandler/AbstractConnectionHandler.h" +#include "SpatialView/CriticalSectionFilter.h" +#include "SpatialView/Dispatcher.h" +#include "SpatialView/ReceivedOpEventHandler.h" +#include "SpatialView/SubView.h" +#include "SpatialView/WorkerView.h" + #include "Templates/UniquePtr.h" namespace SpatialGDK { +class SpatialEventTracer; class ViewCoordinator { public: - explicit ViewCoordinator(TUniquePtr ConnectionHandler); + explicit ViewCoordinator(TUniquePtr ConnectionHandler, TSharedPtr EventTracer, + FComponentSetData ComponentSetData); ~ViewCoordinator(); - // Moveable, not copyable. + // Movable, Non-copyable. ViewCoordinator(const ViewCoordinator&) = delete; - ViewCoordinator(ViewCoordinator&&) = delete; + ViewCoordinator(ViewCoordinator&&) = default; ViewCoordinator& operator=(const ViewCoordinator&) = delete; - ViewCoordinator& operator=(ViewCoordinator&&) = delete; + ViewCoordinator& operator=(ViewCoordinator&&) = default; - OpList Advance(); + void Advance(float DeltaTimeS); + const ViewDelta& GetViewDelta() const; + const EntityView& GetView() const; void FlushMessagesToSend(); + // Create a subview with the specified tag, filter, and refresh callbacks. + FSubView& CreateSubView(Worker_ComponentId Tag, const FFilterPredicate& Filter, + const TArray& DispatcherRefreshCallbacks); + // Force a refresh of the given entity ID across all subviews. Used when local state changes which could + // change any subview's filter's truth value for the given entity. Conceptually this can be thought of + // as marking the entity dirty for all subviews, although the refresh is immediate. + // Note: It would be possible to only refresh the subviews that require refreshing instead of globally refreshing. + // This could be achieved by either having systems understand which subviews need refreshing, or by having systems + // broadcast events which subviews subscribe to in order to trigger a refresh. This global refresh may only be a + // temporary solution which keeps the API simple while the main systems are Actors and the load balancer. + // In the future when there could be an unbounded number of user systems this should probably be revisited. + void RefreshEntityCompleteness(Worker_EntityId EntityId); + const FString& GetWorkerId() const; - const TArray& GetWorkerAttributes() const; + Worker_EntityId GetWorkerSystemEntityId() const; - void SendAddComponent(Worker_EntityId EntityId, ComponentData Data); - void SendComponentUpdate(Worker_EntityId EntityId, ComponentUpdate Update); - void SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + void SendAddComponent(Worker_EntityId EntityId, ComponentData Data, const FSpatialGDKSpanId& SpanId); + void SendComponentUpdate(Worker_EntityId EntityId, ComponentUpdate Update, const FSpatialGDKSpanId& SpanId); + void SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, const FSpatialGDKSpanId& SpanId); Worker_RequestId SendReserveEntityIdsRequest(uint32 NumberOfEntityIds, TOptional TimeoutMillis = {}); - Worker_RequestId SendCreateEntityRequest(TArray EntityComponents, - TOptional EntityId, TOptional TimeoutMillis = {}); - Worker_RequestId SendDeleteEntityRequest(Worker_EntityId EntityId, TOptional TimeoutMillis = {}); + Worker_RequestId SendCreateEntityRequest(TArray EntityComponents, TOptional EntityId, + TOptional TimeoutMillis = {}, const FSpatialGDKSpanId& SpanId = {}); + Worker_RequestId SendDeleteEntityRequest(Worker_EntityId EntityId, TOptional TimeoutMillis = {}, + const FSpatialGDKSpanId& SpanId = {}); Worker_RequestId SendEntityQueryRequest(EntityQuery Query, TOptional TimeoutMillis = {}); - Worker_RequestId SendEntityCommandRequest(Worker_EntityId EntityId, CommandRequest Request, - TOptional TimeoutMillis = {}); - void SendEntityCommandResponse(Worker_RequestId RequestId, CommandResponse Response); - void SendEntityCommandFailure(Worker_RequestId RequestId, FString Message); + Worker_RequestId SendEntityCommandRequest(Worker_EntityId EntityId, CommandRequest Request, TOptional TimeoutMillis = {}, + const FSpatialGDKSpanId& SpanId = {}); + void SendEntityCommandResponse(Worker_RequestId RequestId, CommandResponse Response, const FSpatialGDKSpanId& SpanId); + void SendEntityCommandFailure(Worker_RequestId RequestId, FString Message, const FSpatialGDKSpanId& SpanId); void SendMetrics(SpatialMetrics Metrics); void SendLogMessage(Worker_LogLevel Level, const FName& LoggerName, FString Message); + Worker_RequestId SendReserveEntityIdsRequest(uint32 NumberOfEntityIds, FRetryData RetryData); + Worker_RequestId SendCreateEntityRequest(TArray EntityComponents, TOptional EntityId, + FRetryData RetryData, const FSpatialGDKSpanId& SpanId); + Worker_RequestId SendDeleteEntityRequest(Worker_EntityId EntityId, FRetryData RetryData, const FSpatialGDKSpanId& SpanId); + Worker_RequestId SendEntityQueryRequest(EntityQuery Query, FRetryData RetryData); + Worker_RequestId SendEntityCommandRequest(Worker_EntityId EntityId, CommandRequest Request, FRetryData RetryData, + const FSpatialGDKSpanId& SpanId); + + CallbackId RegisterComponentAddedCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback); + CallbackId RegisterComponentRemovedCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback); + CallbackId RegisterComponentValueCallback(Worker_ComponentId ComponentId, FComponentValueCallback Callback); + CallbackId RegisterAuthorityGainedCallback(Worker_ComponentId ComponentId, FEntityCallback Callback); + CallbackId RegisterAuthorityLostCallback(Worker_ComponentId ComponentId, FEntityCallback Callback); + CallbackId RegisterAuthorityLostTempCallback(Worker_ComponentId ComponentId, FEntityCallback Callback); + void RemoveCallback(CallbackId Id); + + FDispatcherRefreshCallback CreateComponentExistenceRefreshCallback( + Worker_ComponentId ComponentId, + const FComponentChangeRefreshPredicate& RefreshPredicate = FSubView::NoComponentChangeRefreshPredicate); + FDispatcherRefreshCallback CreateComponentChangedRefreshCallback( + Worker_ComponentId ComponentId, + const FComponentChangeRefreshPredicate& RefreshPredicate = FSubView::NoComponentChangeRefreshPredicate); + FDispatcherRefreshCallback CreateAuthorityChangeRefreshCallback( + Worker_ComponentId ComponentId, + const FAuthorityChangeRefreshPredicate& RefreshPredicate = FSubView::NoAuthorityChangeRefreshPredicate); + private: WorkerView View; TUniquePtr ConnectionHandler; + + FCriticalSectionFilter CriticalSectionFilter; + Worker_RequestId NextRequestId; + FDispatcher Dispatcher; + + TArray> SubViews; + + FReceivedOpEventHandler ReceivedOpEventHandler; + + TCommandRetryHandler ReserveEntityIdRetryHandler; + TCommandRetryHandler CreateEntityRetryHandler; + TCommandRetryHandler DeleteEntityRetryHandler; + TCommandRetryHandler EntityQueryRetryHandler; + TCommandRetryHandler EntityCommandRetryHandler; }; } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewDelta.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewDelta.h index 1b54f11db4..cb52d5d78e 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewDelta.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewDelta.h @@ -1,63 +1,165 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved - #pragma once +#include "Containers/Array.h" -#include -#include "SpatialView/AuthorityRecord.h" -#include "SpatialView/EntityComponentRecord.h" -#include "SpatialView/EntityPresenceRecord.h" +#include "SpatialView/ComponentSetData.h" +#include "SpatialView/EntityDelta.h" +#include "SpatialView/EntityView.h" #include "SpatialView/OpList/OpList.h" -#include "Containers/Array.h" -#include "Containers/Queue.h" -#include "Containers/Set.h" -#include "Containers/UnrealString.h" -#include - namespace SpatialGDK { +struct FSubViewDelta +{ + TArray EntityDeltas; + const TArray* WorkerMessages; +}; +/** + * Lists of changes made to a view as a list of EntityDeltas and miscellaneous other messages. + * EntityDeltas are sorted by entity ID. + * Within an EntityDelta the component and authority changes are ordered by component ID. + * + * Rough outline of how it works. + * Input a set of op lists. These should not have any unfinished critical sections in them. + * Take all ops corresponding to entity components and sort them into entity order then component + * order within that. Put all other ops in some other list to be read without pre-processing. For + * ops related to components added, updated, and removed: For each entity-component look at the last + * op received and check if the component is currently in the view. From this you can work out if + * the net effect on component was added, removed or updated. If updated read whichever ops are + * needed to work out what the total update was. For ops related to authority do the same but + * checking the view to see the current authority state. For add and remove entity ops it's the same + * again. If an entity is removed, skip reading the received ops and check the view to see what + * components or authority to remove. + */ class ViewDelta { public: - void AddOpList(OpList Ops, TSet& ComponentsPresent); - - bool HasDisconnected() const; - uint8 GetConnectionStatus() const; - FString GetDisconnectReason() const; - - const TArray& GetEntitiesAdded() const; - const TArray& GetEntitiesRemoved() const; - const TArray& GetAuthorityGained() const; - const TArray& GetAuthorityLost() const; - const TArray& GetAuthorityLostTemporarily() const; - const TArray& GetComponentsAdded() const; - const TArray& GetComponentsRemoved() const; - const TArray& GetUpdates() const; - const TArray& GetCompleteUpdates() const; - - const TArray& GetWorkerMessages() const; - + void SetFromOpList(TArray OpLists, EntityView& View, const FComponentSetData& ComponentSetData); + // Produces a projection of a given main view delta to a sub view delta. The passed SubViewDelta is populated with + // the projection. The given arrays represent the state of the sub view and dictates the projection. + // Entity ID arrays are assumed to be sorted for view delta projection. + void Project(FSubViewDelta& SubDelta, const TArray& CompleteEntities, + const TArray& NewlyCompleteEntities, const TArray& NewlyIncompleteEntities, + const TArray& TemporarilyIncompleteEntities) const; void Clear(); + const TArray& GetEntityDeltas() const; + const TArray& GetWorkerMessages() const; + bool HasConnectionStatusChanged() const; + Worker_ConnectionStatusCode GetConnectionStatusChange() const; + FString GetConnectionStatusChangeMessage() const; private: - void ProcessOp(const Worker_Op& Op, TSet& ComponentsPresent); - - void HandleAuthorityChange(const Worker_AuthorityChangeOp& Op); - void HandleAddComponent(const Worker_AddComponentOp& Op, TSet& ComponentsPresent); - void HandleComponentUpdate(const Worker_ComponentUpdateOp& Op); - void HandleRemoveComponent(const Worker_RemoveComponentOp& Op, TSet& ComponentsPresent); - + struct ReceivedComponentChange + { + explicit ReceivedComponentChange(const Worker_AddComponentOp& Op); + explicit ReceivedComponentChange(const Worker_ComponentUpdateOp& Op); + explicit ReceivedComponentChange(const Worker_RemoveComponentOp& Op); + Worker_EntityId EntityId; + Worker_ComponentId ComponentId; + enum + { + ADD, + UPDATE, + REMOVE + } Type; + union + { + Schema_ComponentData* ComponentAdded; + Schema_ComponentUpdate* ComponentUpdate; + }; + }; + struct ReceivedEntityChange + { + Worker_EntityId EntityId; + bool bAdded; + }; + // Comparator that will return true when the entity change in question is not for the same entity ID as stored. + struct DifferentEntity + { + Worker_EntityId EntityId; + bool operator()(const ReceivedEntityChange& E) const; + bool operator()(const ReceivedComponentChange& Op) const; + bool operator()(const Worker_ComponentSetAuthorityChangeOp& Op) const; + }; + // Comparator that will return true when the entity change in question is not for the same entity-component as stored. + struct DifferentEntityComponent + { + Worker_EntityId EntityId; + Worker_ComponentId ComponentId; + bool operator()(const ReceivedComponentChange& Op) const; + bool operator()(const Worker_ComponentSetAuthorityChangeOp& Op) const; + }; + // Comparator that will return true when the entity ID of Lhs is less than that of Rhs. + // If the entity IDs are the same it will return true when the component ID of Lhs is less than that of Rhs. + struct EntityComponentComparison + { + bool operator()(const ReceivedComponentChange& Lhs, const ReceivedComponentChange& Rhs) const; + bool operator()(const Worker_ComponentSetAuthorityChangeOp& Lhs, const Worker_ComponentSetAuthorityChangeOp& Rhs) const; + }; + // Comparator that will return true when the entity ID of Lhs is less than that of Rhs. + struct EntityComparison + { + bool operator()(const ReceivedEntityChange& Lhs, const ReceivedEntityChange& Rhs) const; + }; + // Calculate and return the net component added in [`Start`, `End`). + // Also add the resulting component to `Components`. + // The accumulated component change in this range must be a component add. + static ComponentChange CalculateAdd(ReceivedComponentChange* Start, ReceivedComponentChange* End, TArray& Components); + // Calculate and return the net complete update in [`Start`, `End`). + // Also set `Component` to match. + // The accumulated component change in this range must be a complete-update or + // `Data` and `Events` should be non null. + static ComponentChange CalculateCompleteUpdate(ReceivedComponentChange* Start, ReceivedComponentChange* End, Schema_ComponentData* Data, + Schema_ComponentUpdate* Events, ComponentData& Component); + // Calculate and return the net update in [`Start`, `End`). + // Also apply the update to `Component`. + // The accumulated component change in this range must be an update or a complete-update. + static ComponentChange CalculateUpdate(ReceivedComponentChange* Start, ReceivedComponentChange* End, ComponentData& Component); + + void ProcessOpList(const OpList& Ops, const EntityView& View, const FComponentSetData& ComponentSetData); + void GenerateComponentChangesFromSetData(const Worker_ComponentSetAuthorityChangeOp& Op, const EntityView& View, + const FComponentSetData& ComponentSetData); + void PopulateEntityDeltas(EntityView& View); + + // Adds component changes to `Delta` and updates `Components` accordingly. + // `It` must point to the first element with a given entity ID. + // Returns a pointer to the next entity in the component changes list. + ReceivedComponentChange* ProcessEntityComponentChanges(ReceivedComponentChange* It, ReceivedComponentChange* End, + TArray& Components, EntityDelta& Delta); + // Adds authority changes to `Delta` and updates `EntityAuthority` accordingly. + // `It` must point to the first element with a given entity ID. + // Returns a pointer to the next entity in the authority changes list. + Worker_ComponentSetAuthorityChangeOp* ProcessEntityAuthorityChanges(Worker_ComponentSetAuthorityChangeOp* It, + Worker_ComponentSetAuthorityChangeOp* End, + TArray& EntityAuthority, EntityDelta& Delta); + // Sets `bAdded` and `bRemoved` fields in the `Delta`. + // `It` must point to the first element with a given entity ID. + // `ViewElement` must point to the same entity in the view or end if it doesn't exist. + // Returns a pointer to the next entity in the authority changes list. + // After returning `*ViewElement` will point to that entity in the view or nullptr if it doesn't exist. + ReceivedEntityChange* ProcessEntityExistenceChange(ReceivedEntityChange* It, ReceivedEntityChange* End, EntityDelta& Delta, + bool bAlreadyInView, EntityView& View); + + // The sentinel entity ID has the property that when converted to a uint64 it will be greater than INT64_MAX. + // If we convert all entity IDs to uint64s before comparing them we can then be assured that the sentinel values + // will be greater than all valid IDs. + static const Worker_EntityId SENTINEL_ENTITY_ID = -1; + + TArray EntityChanges; + TArray ComponentChanges; + TArray AuthorityChanges; + uint8 ConnectionStatusCode = 0; + FString ConnectionStatusMessage; + TArray EntityDeltas; TArray WorkerMessages; - - AuthorityRecord AuthorityChanges; - EntityPresenceRecord EntityPresenceChanges; - EntityComponentRecord EntityComponentChanges; - - TArray OpLists; - - uint8 ConnectionStatus = 0; - FString DisconnectReason; + TArray AuthorityGainedForDelta; + TArray AuthorityLostForDelta; + TArray AuthorityLostTempForDelta; + TArray ComponentsAddedForDelta; + TArray ComponentsRemovedForDelta; + TArray ComponentUpdatesForDelta; + TArray ComponentsRefreshedForDelta; + TArray OpListStorage; }; - -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/WorkerView.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/WorkerView.h index 835c8eb7fb..c314002f1e 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/WorkerView.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/WorkerView.h @@ -2,32 +2,31 @@ #pragma once +#include "SpatialView/ComponentSetData.h" #include "SpatialView/MessagesToSend.h" -#include "SpatialView/ViewDelta.h" #include "SpatialView/OpList/OpList.h" -#include "Containers/Set.h" +#include "SpatialView/ViewDelta.h" namespace SpatialGDK { - class WorkerView { public: - WorkerView(); + explicit WorkerView(FComponentSetData ComponentSetData); - // Process queued op lists to create a new view delta. - // The view delta will exist until the next call to advance. - ViewDelta GenerateViewDelta(); + // Process op lists to create a new view delta. + // The view delta will exist until the next call to AdvanceViewDelta. + void AdvanceViewDelta(TArray OpLists); - // Add an OpList to generate the next ViewDelta. - void EnqueueOpList(OpList Ops); + const ViewDelta& GetViewDelta() const; + const EntityView& GetView() const; // Ensure all local changes have been applied and return the resulting MessagesToSend. TUniquePtr FlushLocalChanges(); - void SendAddComponent(Worker_EntityId EntityId, ComponentData Data); - void SendComponentUpdate(Worker_EntityId EntityId, ComponentUpdate Update); - void SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + void SendAddComponent(Worker_EntityId EntityId, ComponentData Data, const FSpatialGDKSpanId& SpanId); + void SendComponentUpdate(Worker_EntityId EntityId, ComponentUpdate Update, const FSpatialGDKSpanId& SpanId); + void SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, const FSpatialGDKSpanId& SpanId); void SendReserveEntityIdsRequest(ReserveEntityIdsRequest Request); void SendCreateEntityRequest(CreateEntityRequest Request); void SendDeleteEntityRequest(DeleteEntityRequest Request); @@ -39,11 +38,10 @@ class WorkerView void SendLogMessage(LogMessage Log); private: - TArray QueuedOps; - TArray OpenCriticalSectionOps; + FComponentSetData ComponentSetData; + EntityView View; + ViewDelta Delta; TUniquePtr LocalChanges; - TSet AddedComponents; }; - -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/CommandTestUtils.h b/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/CommandTestUtils.h new file mode 100644 index 0000000000..4909bc7146 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/CommandTestUtils.h @@ -0,0 +1,194 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "ComponentTestUtils.h" +#include "SpatialView/OutgoingMessages.h" + +namespace SpatialGDK +{ +bool CompareListOfWorkerConstraints(const Worker_Constraint* Lhs, const Worker_Constraint* Rhs, const int32 LhsNum, const int32 RhsNum); +inline bool CompareReseverEntityIdsRequests(const ReserveEntityIdsRequest& Lhs, const ReserveEntityIdsRequest& Rhs) +{ + if (Lhs.RequestId != Rhs.RequestId) + { + return false; + } + + if (Lhs.NumberOfEntityIds != Rhs.NumberOfEntityIds) + { + return false; + } + + return Lhs.TimeoutMillis == Rhs.TimeoutMillis; +} + +inline bool CompareCreateEntityRequests(const CreateEntityRequest& Lhs, const CreateEntityRequest& Rhs) +{ + if (Lhs.EntityId != Rhs.EntityId) + { + return false; + } + + if (Lhs.RequestId != Rhs.RequestId) + { + return false; + } + + if (Lhs.TimeoutMillis != Rhs.TimeoutMillis) + { + return false; + } + + return AreEquivalent(Lhs.EntityComponents, Rhs.EntityComponents, CompareComponentData); +} + +inline bool CompareDeleteEntityRequests(const DeleteEntityRequest& Lhs, const DeleteEntityRequest& Rhs) +{ + if (Lhs.EntityId != Rhs.EntityId) + { + return false; + } + + if (Lhs.RequestId != Rhs.RequestId) + { + return false; + } + + return Lhs.TimeoutMillis == Rhs.TimeoutMillis; +} + +inline bool CompareWorkerConstraints(const Worker_Constraint& Lhs, const Worker_Constraint Rhs) +{ + if (Lhs.constraint_type != Rhs.constraint_type) + { + return false; + } + + switch (Lhs.constraint_type) + { + case WORKER_CONSTRAINT_TYPE_ENTITY_ID: + return Lhs.constraint.entity_id_constraint.entity_id == Rhs.constraint.entity_id_constraint.entity_id; + case WORKER_CONSTRAINT_TYPE_COMPONENT: + return Lhs.constraint.component_constraint.component_id == Rhs.constraint.component_constraint.component_id; + case WORKER_CONSTRAINT_TYPE_SPHERE: + return Lhs.constraint.sphere_constraint.radius == Rhs.constraint.sphere_constraint.radius + && Lhs.constraint.sphere_constraint.x == Rhs.constraint.sphere_constraint.x + && Lhs.constraint.sphere_constraint.y == Rhs.constraint.sphere_constraint.y + && Lhs.constraint.sphere_constraint.z == Rhs.constraint.sphere_constraint.z; + case WORKER_CONSTRAINT_TYPE_AND: + return CompareListOfWorkerConstraints(Lhs.constraint.and_constraint.constraints, Rhs.constraint.and_constraint.constraints, + Lhs.constraint.and_constraint.constraint_count, + Rhs.constraint.and_constraint.constraint_count); + case WORKER_CONSTRAINT_TYPE_OR: + return CompareListOfWorkerConstraints(Lhs.constraint.or_constraint.constraints, Rhs.constraint.or_constraint.constraints, + Lhs.constraint.or_constraint.constraint_count, Rhs.constraint.or_constraint.constraint_count); + case WORKER_CONSTRAINT_TYPE_NOT: + return CompareWorkerConstraints(*Lhs.constraint.not_constraint.constraint, *Rhs.constraint.not_constraint.constraint); + default: + return false; + } +} + +inline bool CompareListOfWorkerConstraints(const Worker_Constraint* Lhs, const Worker_Constraint* Rhs, const int32 LhsNum, + const int32 RhsNum) +{ + if (LhsNum != RhsNum) + { + return false; + } + + return std::is_permutation(Lhs, Lhs + LhsNum, Rhs, CompareWorkerConstraints); +} + +inline bool CompareEntityQueryRequests(const EntityQueryRequest& Lhs, const EntityQueryRequest& Rhs) +{ + if (Lhs.RequestId != Rhs.RequestId) + { + return false; + } + + if (Lhs.Query.GetWorkerQuery().snapshot_result_type_component_id_count + != Rhs.Query.GetWorkerQuery().snapshot_result_type_component_id_count) + { + return false; + } + + if (Lhs.TimeoutMillis != Rhs.TimeoutMillis) + { + return false; + } + + return CompareWorkerConstraints(Lhs.Query.GetWorkerQuery().constraint, Rhs.Query.GetWorkerQuery().constraint); +} + +inline bool CompareCommandRequests(const CommandRequest& Lhs, const CommandRequest& Rhs) +{ + if (Lhs.GetComponentId() != Rhs.GetComponentId()) + { + return false; + } + + if (Lhs.GetCommandIndex() != Rhs.GetCommandIndex()) + { + return false; + } + + return CompareSchemaObjects(Lhs.GetRequestObject(), Rhs.GetRequestObject()); +} + +inline bool CompareCommandResponses(const CommandResponse& Lhs, const CommandResponse& Rhs) +{ + if (Lhs.GetComponentId() != Rhs.GetComponentId()) + { + return false; + } + + if (Lhs.GetCommandIndex() != Rhs.GetCommandIndex()) + { + return false; + } + + return CompareSchemaObjects(Lhs.GetResponseObject(), Rhs.GetResponseObject()); +} + +inline bool CompareEntityCommandRequests(const EntityCommandRequest& Lhs, const EntityCommandRequest& Rhs) +{ + if (Lhs.EntityId != Rhs.EntityId) + { + return false; + } + + if (Lhs.RequestId != Rhs.RequestId) + { + return false; + } + + if (Lhs.TimeoutMillis != Rhs.TimeoutMillis) + { + return false; + } + + return CompareCommandRequests(Lhs.Request, Rhs.Request); +} + +inline bool CompareEntityCommandResponses(const EntityCommandResponse& Lhs, const EntityCommandResponse& Rhs) +{ + if (Lhs.RequestId != Rhs.RequestId) + { + return false; + } + + return CompareCommandResponses(Lhs.Response, Rhs.Response); +} + +inline bool CompareEntityCommandFailuers(const EntityCommandFailure& Lhs, const EntityCommandFailure& Rhs) +{ + if (Lhs.RequestId != Rhs.RequestId) + { + return false; + } + + return Lhs.Message.Equals(Rhs.Message); +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentTestUtils.h b/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/ComponentTestUtils.h similarity index 59% rename from SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentTestUtils.h rename to SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/ComponentTestUtils.h index 13a8f92152..710998172d 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentTestUtils.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/ComponentTestUtils.h @@ -3,12 +3,12 @@ #pragma once #include "SpatialView/EntityComponentTypes.h" +#include "SpatialView/EntityDelta.h" #include namespace SpatialGDK { - namespace EntityComponentTestUtils { const Schema_FieldId EVENT_ID = 1; @@ -21,7 +21,13 @@ inline ComponentData CreateTestComponentData(const Worker_ComponentId Id, const ComponentData Data{ Id }; Schema_Object* Fields = Data.GetFields(); Schema_AddDouble(Fields, EntityComponentTestUtils::TEST_DOUBLE_FIELD_ID, Value); - return Data; + return MoveTemp(Data); +} + +// Assumes the passed data has the TEST_DOUBLE_FIELD_ID field populated. +inline double GetValueFromTestComponentData(Schema_ComponentData* Data) +{ + return Schema_GetDouble(Schema_GetComponentDataFields(Data), EntityComponentTestUtils::TEST_DOUBLE_FIELD_ID); } inline ComponentUpdate CreateTestComponentUpdate(const Worker_ComponentId Id, const double Value) @@ -32,14 +38,14 @@ inline ComponentUpdate CreateTestComponentUpdate(const Worker_ComponentId Id, co return Update; } -inline void AddTestEvent(ComponentUpdate* Update, int Value) +inline void AddTestEvent(ComponentUpdate* Update, const int Value) { Schema_Object* events = Update->GetEvents(); Schema_Object* eventData = Schema_AddObject(events, EntityComponentTestUtils::EVENT_ID); Schema_AddInt32(eventData, EntityComponentTestUtils::EVENT_INT_FIELD_ID, Value); } -inline ComponentUpdate CreateTestComponentEvent(const Worker_ComponentId Id, int Value) +inline ComponentUpdate CreateTestComponentEvent(const Worker_ComponentId Id, const int Value) { ComponentUpdate Update{ Id }; AddTestEvent(&Update, Value); @@ -49,6 +55,16 @@ inline ComponentUpdate CreateTestComponentEvent(const Worker_ComponentId Id, int /** Returns true if Lhs and Rhs have the same serialized form. */ inline bool CompareSchemaObjects(const Schema_Object* Lhs, const Schema_Object* Rhs) { + if (Lhs == Rhs) + { + return true; + } + + if (Lhs == nullptr || Rhs == nullptr) + { + return false; + } + const auto Length = Schema_GetWriteBufferLength(Lhs); if (Schema_GetWriteBufferLength(Rhs) != Length) { @@ -61,6 +77,46 @@ inline bool CompareSchemaObjects(const Schema_Object* Lhs, const Schema_Object* return FMemory::Memcmp(LhsBuffer.Get(), RhsBuffer.Get(), Length) == 0; } +inline bool CompareSchemaComponentData(Schema_ComponentData* Lhs, Schema_ComponentData* Rhs) +{ + return CompareSchemaObjects(Schema_GetComponentDataFields(Lhs), Schema_GetComponentDataFields(Rhs)); +} + +inline bool CompareSchemaComponentUpdate(Schema_ComponentUpdate* Lhs, Schema_ComponentUpdate* Rhs) +{ + if (!CompareSchemaObjects(Schema_GetComponentUpdateFields(Lhs), Schema_GetComponentUpdateFields(Rhs))) + { + return false; + } + + return CompareSchemaObjects(Schema_GetComponentUpdateEvents(Lhs), Schema_GetComponentUpdateEvents(Rhs)); +} + +inline bool CompareSchemaComponentRefresh(const CompleteUpdateData& Lhs, const CompleteUpdateData& Rhs) +{ + if (!CompareSchemaObjects(Schema_GetComponentDataFields(Lhs.Data), Schema_GetComponentDataFields(Rhs.Data))) + { + return false; + } + + if (Lhs.Events == nullptr) + { + if (Lhs.Events == Rhs.Events) + { + return true; + } + + return Schema_GetWriteBufferLength(Rhs.Events) == 0; + } + + if (Rhs.Events == nullptr) + { + return Schema_GetWriteBufferLength(Lhs.Events) == 0; + } + + return CompareSchemaObjects(Lhs.Events, Rhs.Events); +} + /** Returns true if Lhs and Rhs have the same component ID and state. */ inline bool CompareComponentData(const ComponentData& Lhs, const ComponentData& Rhs) { @@ -71,6 +127,60 @@ inline bool CompareComponentData(const ComponentData& Lhs, const ComponentData& return CompareSchemaObjects(Lhs.GetFields(), Rhs.GetFields()); } +inline bool CompareComponentChangeById(const ComponentChange& Lhs, const ComponentChange& Rhs) +{ + return Lhs.ComponentId < Rhs.ComponentId; +} + +inline bool CompareComponentChanges(const ComponentChange& Lhs, const ComponentChange& Rhs) +{ + if (Lhs.ComponentId != Rhs.ComponentId) + { + return false; + } + + if (Lhs.Type != Rhs.Type) + { + return false; + } + + switch (Lhs.Type) + { + case ComponentChange::ADD: + return CompareSchemaComponentData(Lhs.Data, Rhs.Data); + case ComponentChange::UPDATE: + return CompareSchemaComponentUpdate(Lhs.Update, Rhs.Update); + case ComponentChange::COMPLETE_UPDATE: + return CompareSchemaComponentRefresh(Lhs.CompleteUpdate, Rhs.CompleteUpdate); + case ComponentChange::REMOVE: + break; + default: + checkNoEntry(); + } + + return true; +} + +inline bool CompareAuthorityChangeById(const AuthorityChange& Lhs, const AuthorityChange& Rhs) +{ + return Lhs.ComponentId < Rhs.ComponentId; +} + +inline bool CompareAuthorityChanges(const AuthorityChange& Lhs, const AuthorityChange& Rhs) +{ + if (Lhs.ComponentId != Rhs.ComponentId) + { + return false; + } + + if (Lhs.Type != Rhs.Type) + { + return false; + } + + return true; +} + /** Returns true if Lhs and Rhs have the same component ID and events. */ inline bool CompareComponentUpdateEvents(const ComponentUpdate& Lhs, const ComponentUpdate& Rhs) { @@ -88,8 +198,7 @@ inline bool CompareComponentUpdates(const ComponentUpdate& Lhs, const ComponentU { return false; } - return CompareSchemaObjects(Lhs.GetFields(), Rhs.GetFields()) && - CompareSchemaObjects(Lhs.GetEvents(), Rhs.GetEvents()); + return CompareSchemaObjects(Lhs.GetFields(), Rhs.GetFields()) && CompareSchemaObjects(Lhs.GetEvents(), Rhs.GetEvents()); } /** Returns true if Lhs and Rhs have the same entity ID, component ID, and state. */ @@ -132,15 +241,35 @@ inline bool CompareEntityComponentCompleteUpdates(const EntityComponentCompleteU return CompareComponentData(Lhs.CompleteUpdate, Rhs.CompleteUpdate) && CompareComponentUpdateEvents(Lhs.Events, Rhs.Events); } -inline bool CompareEntityComponentId(const EntityComponentId& Lhs, const EntityComponentId& Rhs) +inline bool EntityComponentIdEquality(const EntityComponentId& Lhs, const EntityComponentId& Rhs) +{ + return Lhs == Rhs; +} + +inline bool WorkerComponentIdEquality(const Worker_ComponentId Lhs, const Worker_ComponentId Rhs) +{ + return Lhs == Rhs; +} + +inline bool WorkerEntityIdEquality(const Worker_EntityId Lhs, const Worker_EntityId Rhs) { return Lhs == Rhs; } -template +inline bool CompareWorkerEntityId(const Worker_EntityId Lhs, const Worker_EntityId Rhs) +{ + return Lhs < Rhs; +} + +template bool AreEquivalent(const TArray& Lhs, const TArray& Rhs, Predicate&& Compare) { - return std::is_permutation(Lhs.GetData(), Lhs.GetData() + Lhs.Num(), Rhs.GetData(), std::forward(Compare)); + if (Lhs.Num() != Rhs.Num()) + { + return false; + } + + return std::is_permutation(Lhs.GetData(), Lhs.GetData() + Lhs.Num(), Rhs.GetData(), Forward(Compare)); } inline bool AreEquivalent(const TArray& Lhs, const TArray& Rhs) @@ -160,7 +289,7 @@ inline bool AreEquivalent(const TArray& Lhs, const TArray& Lhs, const TArray& Rhs) { - return AreEquivalent(Lhs, Rhs, CompareEntityComponentId); + return AreEquivalent(Lhs, Rhs, EntityComponentIdEquality); } -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/ExpectedEntityDelta.h b/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/ExpectedEntityDelta.h new file mode 100644 index 0000000000..8c458a8f80 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/ExpectedEntityDelta.h @@ -0,0 +1,29 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once +#include "SpatialView/ComponentData.h" +#include "SpatialView/EntityDelta.h" + +namespace SpatialGDK +{ +struct ExpectedEntityDelta +{ + Worker_EntityId EntityId; + enum + { + UPDATE, + ADD, + REMOVE, + TEMPORARILY_REMOVED + } Type; + TArray DataStorage; + TArray UpdateStorage; + TArray ComponentsAdded; + TArray ComponentsRemoved; + TArray ComponentUpdates; + TArray ComponentsRefreshed; + TArray AuthorityGained; + TArray AuthorityLost; + TArray AuthorityLostTemporarily; +}; +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/ExpectedMessagesToSend.h b/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/ExpectedMessagesToSend.h new file mode 100644 index 0000000000..905637fb74 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/ExpectedMessagesToSend.h @@ -0,0 +1,36 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "ComponentTestUtils.h" +#include "SpatialView/MessagesToSend.h" +#include "SpatialView/OutgoingMessages.h" + +namespace SpatialGDK +{ +class ExpectedMessagesToSend +{ +public: + ExpectedMessagesToSend& AddCreateEntityRequest(Worker_RequestId RequestId, Worker_EntityId EntityId, + TArray ComponentData); + ExpectedMessagesToSend& AddEntityCommandRequest(Worker_RequestId RequestId, Worker_EntityId EntityId, Worker_ComponentId ComponentId, + Worker_CommandIndex CommandIndex); + ExpectedMessagesToSend& AddDeleteEntityCommandRequest(Worker_RequestId RequestId, Worker_EntityId EntityId); + ExpectedMessagesToSend& AddReserveEntityIdsRequest(Worker_RequestId RequestId, uint32 NumOfEntities); + ExpectedMessagesToSend& AddEntityQueryRequest(Worker_RequestId RequestId, EntityQuery Query); + ExpectedMessagesToSend& AddEntityCommandResponse(Worker_RequestId RequestId, Worker_ComponentId ComponentId, + Worker_CommandIndex CommandIndex); + ExpectedMessagesToSend& AddEntityCommandFailure(Worker_RequestId RequestId, FString Message); + bool Compare(const MessagesToSend& MessagesToSend) const; + +private: + TArray ReserveEntityIdsRequests; + TArray CreateEntityRequests; + TArray DeleteEntityRequests; + TArray EntityQueryRequests; + TArray EntityCommandRequests; + TArray EntityCommandResponses; + TArray EntityCommandFailures; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/ExpectedViewDelta.h b/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/ExpectedViewDelta.h new file mode 100644 index 0000000000..f0f1728efc --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/ExpectedViewDelta.h @@ -0,0 +1,67 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once +#include "CoreMinimal.h" +#include "ExpectedEntityDelta.h" + +#include "SpatialView/ViewDelta.h" + +namespace SpatialGDK +{ +class ExpectedViewDelta +{ +public: + enum EntityChangeType + { + UPDATE, + ADD, + REMOVE, + TEMPORARILY_REMOVED + }; + + ExpectedViewDelta& AddEntityDelta(const Worker_EntityId EntityId, const EntityChangeType ChangeType); + ExpectedViewDelta& AddComponentAdded(const Worker_EntityId EntityId, ComponentData Data); + ExpectedViewDelta& AddComponentRemoved(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId); + ExpectedViewDelta& AddComponentUpdate(const Worker_EntityId EntityId, ComponentUpdate Update); + ExpectedViewDelta& AddComponentRefreshed(const Worker_EntityId EntityId, ComponentUpdate Update, ComponentData Data); + ExpectedViewDelta& AddAuthorityGained(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId); + ExpectedViewDelta& AddAuthorityLost(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId); + ExpectedViewDelta& AddAuthorityLostTemporarily(const Worker_EntityId EntityId, const Worker_ComponentId ComponentId); + ExpectedViewDelta& AddDisconnect(const uint8 StatusCode, FString StatusMessage); + + // Compares the stored Entity Deltas + bool Compare(const ViewDelta& Other); + bool Compare(const FSubViewDelta& Other); + +private: + void SortEntityDeltas(); + TMap EntityDeltas; + uint8 ConnectionStatusCode = 0; + FString ConnectionStatusMessage; + + bool CompareDeltas(const TArray& Other); + + template + bool CompareData(const TArray& Lhs, const ComponentSpan& Rhs, Predicate&& Comparator) + { + if (Lhs.Num() != Rhs.Num()) + { + return false; + } + + auto LhsFirst = Lhs.GetData(); + auto LhsLast = Lhs.GetData() + Lhs.Num(); + auto RhsFirst = Rhs.GetData(); + while (LhsFirst != LhsLast) + { + if (!Comparator(*LhsFirst, *RhsFirst)) + { + return false; + } + ++LhsFirst, ++RhsFirst; + } + + return true; + } +}; +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/SpatialViewUtils.h b/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/SpatialViewUtils.h new file mode 100644 index 0000000000..858d7d71fd --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Tests/SpatialView/SpatialViewUtils.h @@ -0,0 +1,124 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "ComponentTestUtils.h" +#include "SpatialView/OpList/EntityComponentOpList.h" +#include "SpatialView/ViewDelta.h" +#include "SpatialView/WorkerView.h" + +namespace SpatialGDK +{ +inline TArray CopyComponentSetOnEntity(Worker_EntityId EntityId, Worker_ComponentSetId ComponentSetId, + const EntityView& View, const FComponentSetData& ComponentSetData) +{ + TArray Components; + const TSet& ComponentSet = ComponentSetData.ComponentSets[ComponentSetId]; + for (const ComponentData& Component : View[EntityId].Components) + { + if (ComponentSet.Contains(Component.GetComponentId())) + { + Components.Emplace(Component.DeepCopy()); + } + } + return Components; +} + +inline void SetFromOpList(ViewDelta& Delta, EntityView& View, EntityComponentOpListBuilder OpListBuilder, + const FComponentSetData& ComponentSetData) +{ + OpList Ops = MoveTemp(OpListBuilder).CreateOpList(); + TArray OpLists; + OpLists.Push(MoveTemp(Ops)); + Delta.SetFromOpList(MoveTemp(OpLists), View, ComponentSetData); +} + +inline void AddEntityToView(EntityView& View, const Worker_EntityId EntityId) +{ + View.Add(EntityId, EntityViewElement()); +} + +inline void AddComponentToView(EntityView& View, const Worker_EntityId EntityId, ComponentData Data) +{ + View[EntityId].Components.Push(MoveTemp(Data)); +} + +inline void AddAuthorityToView(EntityView& View, const Worker_EntityId EntityId, const Worker_ComponentId ComponentId) +{ + View[EntityId].Authority.Push(ComponentId); +} + +inline void PopulateViewDeltaWithComponentAdded(ViewDelta& Delta, EntityView& View, const Worker_EntityId EntityId, ComponentData Data) +{ + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.AddComponent(EntityId, MoveTemp(Data)); + SetFromOpList(Delta, View, MoveTemp(OpListBuilder), FComponentSetData()); +} + +inline void PopulateViewDeltaWithComponentUpdated(ViewDelta& Delta, EntityView& View, const Worker_EntityId EntityId, + ComponentUpdate Update) +{ + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.UpdateComponent(EntityId, MoveTemp(Update)); + SetFromOpList(Delta, View, MoveTemp(OpListBuilder), FComponentSetData()); +} + +inline void PopulateViewDeltaWithComponentRemoved(ViewDelta& Delta, EntityView& View, const Worker_EntityId EntityId, + const Worker_ComponentId ComponentId) +{ + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.RemoveComponent(EntityId, ComponentId); + SetFromOpList(Delta, View, MoveTemp(OpListBuilder), FComponentSetData()); +} + +inline void PopulateViewDeltaWithAuthorityChange(ViewDelta& Delta, EntityView& View, const Worker_EntityId EntityId, + const Worker_ComponentSetId ComponentSetId, const Worker_Authority Authority, + const FComponentSetData& ComponentSetData) +{ + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.SetAuthority(EntityId, ComponentSetId, Authority, + CopyComponentSetOnEntity(EntityId, ComponentSetId, View, ComponentSetData)); + SetFromOpList(Delta, View, MoveTemp(OpListBuilder), ComponentSetData); +} + +inline void PopulateViewDeltaWithAuthorityLostTemp(ViewDelta& Delta, EntityView& View, const Worker_EntityId EntityId, + const Worker_ComponentSetId ComponentSetId, const FComponentSetData& ComponentSetData) +{ + EntityComponentOpListBuilder OpListBuilder; + OpListBuilder.SetAuthority(EntityId, ComponentSetId, WORKER_AUTHORITY_NOT_AUTHORITATIVE, + CopyComponentSetOnEntity(EntityId, ComponentSetId, View, ComponentSetData)); + OpListBuilder.SetAuthority(EntityId, ComponentSetId, WORKER_AUTHORITY_AUTHORITATIVE, + CopyComponentSetOnEntity(EntityId, ComponentSetId, View, ComponentSetData)); + SetFromOpList(Delta, View, MoveTemp(OpListBuilder), ComponentSetData); +} + +inline bool CompareViews(const EntityView& Lhs, const EntityView& Rhs) +{ + TArray LhsKeys; + TArray RhsKeys; + Lhs.GetKeys(LhsKeys); + Rhs.GetKeys(RhsKeys); + if (!AreEquivalent(LhsKeys, RhsKeys, WorkerEntityIdEquality)) + { + return false; + } + + for (const auto& Pair : Lhs) + { + const long EntityId = Pair.Key; + const EntityViewElement* LhsElement = &Pair.Value; + const EntityViewElement* RhsElement = &Rhs[EntityId]; + if (!AreEquivalent(LhsElement->Components, RhsElement->Components, CompareComponentData)) + { + return false; + } + + if (!AreEquivalent(LhsElement->Authority, RhsElement->Authority, WorkerComponentIdEquality)) + { + return false; + } + } + + return true; +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Tests/TestActor.h b/SpatialGDK/Source/SpatialGDK/Public/Tests/TestActor.h index 6ad0db8cea..6b396348e0 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Tests/TestActor.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Tests/TestActor.h @@ -12,13 +12,10 @@ class ATestActor : public AActor { GENERATED_BODY() - ATestActor() - { - SetReplicates(true); - } + ATestActor() { bReplicates = true; } public: UFUNCTION(Server, Reliable) void TestServerRPC(); - void TestServerRPC_Implementation() {}; + void TestServerRPC_Implementation(){}; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Tests/TestDefinitions.h b/SpatialGDK/Source/SpatialGDK/Public/Tests/TestDefinitions.h index c417d86b1f..d879aa857f 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Tests/TestDefinitions.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Tests/TestDefinitions.h @@ -4,16 +4,20 @@ #include "Misc/AutomationTest.h" -#define GDK_TEST(ModuleName, ComponentName, TestName) \ - IMPLEMENT_SIMPLE_AUTOMATION_TEST(TestName, "SpatialGDK."#ModuleName"."#ComponentName"."#TestName, EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::EngineFilter) \ +#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) +#define GDK_COMPLEX_TEST(ModuleName, ComponentName, TestName) \ + IMPLEMENT_COMPLEX_AUTOMATION_TEST(TestName, "SpatialGDK." #ModuleName "." #ComponentName "." #TestName, \ + EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::EngineFilter) -#define GDK_SLOW_TEST(ModuleName, ComponentName, TestName) \ - IMPLEMENT_SIMPLE_AUTOMATION_TEST(TestName, "SpatialGDKSlow."#ModuleName"."#ComponentName"."#TestName, EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::EngineFilter) \ +#define GDK_SLOW_TEST(ModuleName, ComponentName, TestName) \ + IMPLEMENT_SIMPLE_AUTOMATION_TEST(TestName, "SpatialGDKSlow." #ModuleName "." #ComponentName "." #TestName, \ + EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::EngineFilter) \ bool TestName::RunTest(const FString& Parameters) -#define GDK_SLOW_COMPLEX_TEST(ModuleName, ComponentName, TestName) \ - IMPLEMENT_COMPLEX_AUTOMATION_TEST(TestName, "SpatialGDKSlow."#ModuleName"."#ComponentName"."#TestName, EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::EngineFilter) +#define GDK_SLOW_COMPLEX_TEST(ModuleName, ComponentName, TestName) \ + IMPLEMENT_COMPLEX_AUTOMATION_TEST(TestName, "SpatialGDKSlow." #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 deleted file mode 100644 index fc42a46cff..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Public/Tests/TestingComponentViewHelpers.h +++ /dev/null @@ -1,24 +0,0 @@ -// 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 index 04680334dd..53081eeae7 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Tests/TestingSchemaHelpers.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Tests/TestingSchemaHelpers.h @@ -13,5 +13,6 @@ 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); + static void AddTranslationComponentDataMapping(Schema_Object* ComponentDataFields, VirtualWorkerId VWId, + const PhysicalWorkerName& WorkerName, Worker_PartitionId PartitionId); }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentFactory.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentFactory.h index aeaed8cfe8..f70ac635ee 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentFactory.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentFactory.h @@ -24,30 +24,41 @@ enum EReplicatedPropertyGroup : uint32; namespace SpatialGDK { - class SPATIALGDK_API ComponentFactory { public: ComponentFactory(bool bInterestDirty, USpatialNetDriver* InNetDriver, USpatialLatencyTracer* LatencyTracer); - 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); + 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); - FWorkerComponentData CreateHandoverComponentData(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes, uint32& OutBytesWritten); + FWorkerComponentData CreateHandoverComponentData(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, + const FHandoverChangeState& Changes, uint32& OutBytesWritten); static FWorkerComponentData CreateEmptyComponentData(Worker_ComponentId ComponentId); private: - 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); + 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); - uint32 FillSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FRepChangeState& Changes, ESchemaComponentType PropertyGroup, bool bIsInitialData, TraceKey* OutLatencyTraceId, TArray* ClearedIds = nullptr); + uint32 FillSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FRepChangeState& Changes, + ESchemaComponentType PropertyGroup, bool bIsInitialData, TraceKey* OutLatencyTraceId, + TArray* ClearedIds = nullptr); - FWorkerComponentUpdate CreateHandoverComponentUpdate(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes, uint32& OutBytesWritten); + FWorkerComponentUpdate CreateHandoverComponentUpdate(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, + const FHandoverChangeState& Changes, uint32& OutBytesWritten); - uint32 FillHandoverSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes, bool bIsInitialData, TraceKey* OutLatencyTraceId, TArray* ClearedIds = nullptr); + 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, GDK_PROPERTY(Property)* Property, const uint8* Data, TArray* ClearedIds); + void AddProperty(Schema_Object* Object, Schema_FieldId FieldId, GDK_PROPERTY(Property) * Property, const uint8* Data, + TArray* ClearedIds); USpatialNetDriver* NetDriver; USpatialPackageMapClient* PackageMap; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentReader.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentReader.h index 00bf3dd7d7..cee37810bb 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentReader.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentReader.h @@ -10,28 +10,39 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialComponentReader, All, All); namespace SpatialGDK { +class SpatialEventTracer; class ComponentReader { public: - ComponentReader(class USpatialNetDriver* InNetDriver, FObjectReferencesMap& InObjectReferencesMap); + explicit ComponentReader(class USpatialNetDriver* InNetDriver, FObjectReferencesMap& InObjectReferencesMap, + SpatialEventTracer* InEventTracer); - 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); + 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, bool& bOutReferencesChanged); - void ApplyHandoverSchemaObject(Schema_Object* ComponentObject, UObject& Object, USpatialActorChannel& Channel, bool bIsInitialData, const TArray& UpdatedIds, Worker_ComponentId ComponentId, bool& bOutReferencesChanged); + void ApplySchemaObject(Schema_Object* ComponentObject, UObject& Object, USpatialActorChannel& Channel, bool bIsInitialData, + const TArray& UpdatedIds, Worker_ComponentId ComponentId, bool& bOutReferencesChanged); + void ApplyHandoverSchemaObject(Schema_Object* ComponentObject, UObject& Object, USpatialActorChannel& Channel, bool bIsInitialData, + const TArray& UpdatedIds, Worker_ComponentId ComponentId, bool& bOutReferencesChanged); - void ApplyProperty(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, uint32 Index, GDK_PROPERTY(Property)* Property, uint8* Data, int32 Offset, int32 CmdIndex, int32 ParentIndex, bool& bOutReferencesChanged); - void ApplyArray(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, GDK_PROPERTY(ArrayProperty)* Property, uint8* Data, int32 Offset, int32 CmdIndex, int32 ParentIndex, bool& bOutReferencesChanged); + void ApplyProperty(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, uint32 Index, + GDK_PROPERTY(Property) * Property, uint8* Data, int32 Offset, int32 CmdIndex, int32 ParentIndex, + bool& bOutReferencesChanged); + void ApplyArray(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, + GDK_PROPERTY(ArrayProperty) * Property, uint8* Data, int32 Offset, int32 CmdIndex, int32 ParentIndex, + bool& bOutReferencesChanged); - uint32 GetPropertyCount(const Schema_Object* Object, Schema_FieldId Id, GDK_PROPERTY(Property)* Property); + uint32 GetPropertyCount(const Schema_Object* Object, Schema_FieldId Id, GDK_PROPERTY(Property) * Property); private: class USpatialPackageMapClient* PackageMap; class USpatialNetDriver* NetDriver; class USpatialClassInfoManager* ClassInfoManager; + class SpatialEventTracer* EventTracer; FObjectReferencesMap& RootObjectReferencesMap; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/EngineVersionCheck.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/EngineVersionCheck.h index 7a6e8f555e..049ad3bf18 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/EngineVersionCheck.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/EngineVersionCheck.h @@ -7,9 +7,10 @@ // 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 25 +#define SPATIAL_GDK_VERSION 32 // Check if GDK is compatible with the current version of Unreal Engine // SPATIAL_ENGINE_VERSION is incremented in engine when breaking changes // are made that make previous versions of the GDK incompatible -static_assert(SPATIAL_ENGINE_VERSION == SPATIAL_GDK_VERSION, "GDK Version is incompatible with the Engine Version. Check both the GDK and Engine are up to date"); +static_assert(SPATIAL_ENGINE_VERSION == SPATIAL_GDK_VERSION, + "GDK Version is incompatible with the Engine Version. Check both the GDK and Engine are up to date"); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityFactory.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityFactory.h index 8dcfeb1ec3..f809177583 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityFactory.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityFactory.h @@ -1,38 +1,47 @@ // 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 UAbstractLBStrategy; class USpatialActorChannel; class USpatialNetDriver; class USpatialPackageMap; class USpatialClassInfoManager; class USpatialPackageMapClient; - + namespace SpatialGDK { -class SpatialRPCService; +class InterestFactory; +class SpatialRPCService; struct RPCsOnEntityCreation; -using FRPCsOnEntityCreationMap = TMap, RPCsOnEntityCreation>; - +using FRPCsOnEntityCreationMap = TMap, RPCsOnEntityCreation, FDefaultSetAllocator, + TWeakObjectPtrMapKeyFuncs, SpatialGDK::RPCsOnEntityCreation, false>>; + class SPATIALGDK_API EntityFactory { public: - EntityFactory(USpatialNetDriver* InNetDriver, USpatialPackageMapClient* InPackageMap, USpatialClassInfoManager* InClassInfoManager, SpatialRPCService* InRPCService); - - TArray CreateEntityComponents(USpatialActorChannel* Channel, FRPCsOnEntityCreationMap& OutgoingOnCreateEntityRPCs, uint32& OutBytesWritten); + EntityFactory(USpatialNetDriver* InNetDriver, USpatialPackageMapClient* InPackageMap, USpatialClassInfoManager* InClassInfoManager, + SpatialRPCService* InRPCService); + + TArray CreateEntityComponents(USpatialActorChannel* Channel, uint32& OutBytesWritten); TArray CreateTombstoneEntityComponents(AActor* Actor); - static TArray GetComponentPresenceList(const TArray& ComponentDatas); + static TArray CreatePartitionEntityComponents(const Worker_EntityId EntityId, + const InterestFactory* InterestFactory, + const UAbstractLBStrategy* LbStrategy, + VirtualWorkerId VirtualWorker, bool bDebugContexValid); + + static inline bool IsClientAuthoritativeComponent(Worker_ComponentId ComponentId) + { + return ComponentId == SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID || ComponentId == SpatialConstants::HEARTBEAT_COMPONENT_ID; + } private: USpatialNetDriver* NetDriver; @@ -40,4 +49,4 @@ class SPATIALGDK_API EntityFactory USpatialClassInfoManager* ClassInfoManager; SpatialRPCService* RPCService; }; -} // namepsace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityPool.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityPool.h index 39e4a83f81..9c481e8f3a 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityPool.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityPool.h @@ -38,10 +38,7 @@ class SPATIALGDK_API UEntityPool : public UObject Worker_EntityId GetNextEntityId(); FEntityPoolReadyEvent& GetEntityPoolReadyDelegate(); - FORCEINLINE bool IsReady() const - { - return bIsReady; - } + FORCEINLINE bool IsReady() const { return bIsReady; } private: void OnEntityRangeExpired(uint32 ExpiringEntityRangeId); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/ErrorCodeRemapping.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/ErrorCodeRemapping.h index e970a36ce9..262a902ea0 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/ErrorCodeRemapping.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/ErrorCodeRemapping.h @@ -7,34 +7,34 @@ namespace ENetworkFailure { - static inline ENetworkFailure::Type FromDisconnectOpStatusCode(uint8_t StatusCode) +static inline ENetworkFailure::Type FromDisconnectOpStatusCode(uint8_t StatusCode) +{ + // For full status code descriptions, see WorkerSDK\improbable\c_worker.h + switch (StatusCode) { - // For full status code descriptions, see WorkerSDK\improbable\c_worker.h - switch (StatusCode) - { - case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_TIMEOUT: - return ENetworkFailure::ConnectionTimeout; + case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_TIMEOUT: + return ENetworkFailure::ConnectionTimeout; - case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_INTERNAL_ERROR: - case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_NETWORK_ERROR: - case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_SERVER_SHUTDOWN: - return ENetworkFailure::ConnectionLost; + case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_INTERNAL_ERROR: + case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_NETWORK_ERROR: + case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_SERVER_SHUTDOWN: + return ENetworkFailure::ConnectionLost; - case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_INVALID_ARGUMENT: - case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_CANCELLED: - case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_REJECTED: - case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_PLAYER_IDENTITY_TOKEN_EXPIRED: - case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_LOGIN_TOKEN_EXPIRED: - case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_CAPACITY_EXCEEDED: - case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_RATE_EXCEEDED: - return ENetworkFailure::PendingConnectionFailure; + case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_INVALID_ARGUMENT: + case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_CANCELLED: + case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_REJECTED: + case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_PLAYER_IDENTITY_TOKEN_EXPIRED: + case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_LOGIN_TOKEN_EXPIRED: + case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_CAPACITY_EXCEEDED: + case Worker_ConnectionStatusCode::WORKER_CONNECTION_STATUS_CODE_RATE_EXCEEDED: + return ENetworkFailure::PendingConnectionFailure; - default: - // Execution of this code path should be considered an error as all worker status codes map to an ENetworkFailure - // The only exception to this is WORKER_CONNECTION_STATUS_CODE_SUCCESS which does not indicate an error and will never - // be received in a WORKER_OP_TYPE_DISCONNECT - checkNoEntry(); - return ENetworkFailure::FailureReceived; - } + default: + // Execution of this code path should be considered an error as all worker status codes map to an ENetworkFailure + // The only exception to this is WORKER_CONNECTION_STATUS_CODE_SUCCESS which does not indicate an error and will never + // be received in a WORKER_OP_TYPE_DISCONNECT + checkNoEntry(); + return ENetworkFailure::FailureReceived; } } +} // namespace ENetworkFailure diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/GDKPropertyMacros.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/GDKPropertyMacros.h index ea60194d32..1ffa5d38b0 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/GDKPropertyMacros.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/GDKPropertyMacros.h @@ -2,13 +2,13 @@ #pragma once -#include "UObject/UnrealType.h" #include "Runtime/Launch/Resources/Version.h" +#include "UObject/UnrealType.h" #if ENGINE_MINOR_VERSION <= 24 - #define GDK_PROPERTY(Type) U##Type - #define GDK_CASTFIELD Cast +#define GDK_PROPERTY(Type) U##Type +#define GDK_CASTFIELD Cast #else - #define GDK_PROPERTY(Type) F##Type - #define GDK_CASTFIELD CastField +#define GDK_PROPERTY(Type) F##Type +#define GDK_CASTFIELD CastField #endif diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/ISpatialAwaitable.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/ISpatialAwaitable.h new file mode 100644 index 0000000000..77d6a2cee5 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/ISpatialAwaitable.h @@ -0,0 +1,43 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "Templates/SharedPointer.h" +#include "UObject/Interface.h" + +#include "ISpatialAwaitable.generated.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogAwaitable, Log, Log); + +DECLARE_DELEGATE_OneParam(FOnReady, const FString& /* ErrorMessage */); + +USTRUCT(BlueprintType) +struct FSpatialAwaitableDelegateHandleBPWrapper +{ + GENERATED_BODY() + +public: + FDelegateHandle Handle; +}; + +UINTERFACE(MinimalAPI) +class USpatialAwaitable : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Generic awaitable interface. + */ +class SPATIALGDK_API ISpatialAwaitable +{ + GENERATED_BODY() + +public: + virtual FDelegateHandle Await(const FOnReady& OnReadyDelegate, const float Timeout) = 0; + virtual bool StopAwaiting(FDelegateHandle& Handle) = 0; + + DECLARE_EVENT(ISpatialAwaitable, FSpatialAwaitableOnResetEvent); + virtual FSpatialAwaitableOnResetEvent& OnReset() = 0; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/InspectionColors.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/InspectionColors.h index de6c7e249d..8beea99e1a 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/InspectionColors.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/InspectionColors.h @@ -2,13 +2,13 @@ #pragma once -#include "SpatialCommonTypes.h" #include "Math/Color.h" +#include "SpatialCommonTypes.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); -} +// Argument expected in the form: UnrealWorker1a2s3d4f... +FColor GetColorForWorkerName(const PhysicalWorkerName& WorkerName); +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/Interest/NetCullDistanceInterest.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/Interest/NetCullDistanceInterest.h index 3934ea979b..1a3b5d30a7 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/Interest/NetCullDistanceInterest.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/Interest/NetCullDistanceInterest.h @@ -10,9 +10,9 @@ * 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. + * 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 @@ -30,29 +30,29 @@ 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 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); + 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 f5e030d26e..6c7728222e 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/InterestFactory.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/InterestFactory.h @@ -26,8 +26,8 @@ * 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. + * 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; @@ -38,7 +38,6 @@ DECLARE_LOG_CATEGORY_EXTERN(LogInterestFactory, Log, All); namespace SpatialGDK { - class SPATIALGDK_API InterestFactory { public: @@ -47,7 +46,12 @@ class SPATIALGDK_API InterestFactory 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; - Interest CreateServerWorkerInterest(const UAbstractLBStrategy* LBStrategy); + Interest CreateServerWorkerInterest(const UAbstractLBStrategy* LBStrategy) const; + Interest CreatePartitionInterest(const UAbstractLBStrategy* LBStrategy, VirtualWorkerId VirtualWorker, bool bDebug) const; + void AddLoadBalancingInterestQuery(const UAbstractLBStrategy* LBStrategy, VirtualWorkerId VirtualWorker, Interest& OutInterest) const; + + // Returns false if we could not get an owner's entityId in the Actor's owner chain. + bool DoOwnersHaveEntityId(const AActor* Actor) const; private: // Shared constraints and result types are created at initialization and reused throughout the lifetime of the factory. @@ -64,32 +68,39 @@ class SPATIALGDK_API InterestFactory // Defined Constraint AND Level Constraint 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 clients need to see on entities they have authority over that they don't already see through authority. + void AddClientSelfInterest(Interest& OutInterest) const; // The components servers need to see on entities they have authority over that they don't already see through authority. - void AddServerSelfInterest(Interest& OutInterest, const Worker_EntityId& EntityId) const; + void AddServerSelfInterest(Interest& OutInterest) const; + // Add interest to the actor's owner. + void AddOwnerInterestOnServer(Interest& OutInterest, const AActor* InActor, const Worker_EntityId& EntityId) const; // Add the always relevant and the always interested query. - void AddAlwaysRelevantAndInterestedQuery(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo, const QueryConstraint& LevelConstraint) const; + void AddAlwaysRelevantAndInterestedQuery(Interest& OutInterest, const AActor* InActor, const FClassInfo& InInfo, + const QueryConstraint& LevelConstraint) 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; + void GetActorUserDefinedQueryConstraints(const AActor* InActor, FrequencyToConstraintsMap& OutFrequencyToConstraints, + bool bRecurseChildren) const; void AddNetCullDistanceQueries(Interest& OutInterest, const QueryConstraint& LevelConstraint) const; - void AddComponentQueryPairToInterestComponent(Interest& OutInterest, const Worker_ComponentId ComponentId, const Query& QueryToAdd) const; + void AddComponentQueryPairToInterestComponent(Interest& OutInterest, const Worker_ComponentId ComponentId, + const Query& QueryToAdd) const; // System Defined Constraints bool ShouldAddNetCullDistanceInterest(const AActor* InActor) const; QueryConstraint CreateAlwaysInterestedConstraint(const AActor* InActor, const FClassInfo& InInfo) const; - QueryConstraint CreateAlwaysRelevantConstraint() const; + QueryConstraint CreateGDKSnapshotEntitiesConstraint() const; + QueryConstraint CreateClientAlwaysRelevantConstraint() const; + QueryConstraint CreateServerAlwaysRelevantConstraint() const; + QueryConstraint CreateActorVisibilityConstraint() const; // Only checkout entities that are in loaded sub-levels QueryConstraint CreateLevelConstraints(const AActor* InActor) const; - void AddObjectToConstraint(GDK_PROPERTY(ObjectPropertyBase)* Property, uint8* Data, QueryConstraint& OutConstraint) const; + void AddObjectToConstraint(GDK_PROPERTY(ObjectPropertyBase) * Property, uint8* Data, QueryConstraint& OutConstraint) const; USpatialClassInfoManager* ClassInfoManager; USpatialPackageMapClient* PackageMap; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/LayerInfo.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/LayerInfo.h index b6147b40f4..69984997e0 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/LayerInfo.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/LayerInfo.h @@ -5,6 +5,7 @@ #include "LoadBalancing/GridBasedLBStrategy.h" #include "CoreMinimal.h" +#include "Templates/SubclassOf.h" #include "LayerInfo.generated.h" @@ -26,7 +27,9 @@ struct FLayerInfo FLayerInfo(FName InName, TSet> InActorClasses, UClass* InLoadBalanceStrategy) : Name(InName) , ActorClasses(InActorClasses) - , LoadBalanceStrategy(InLoadBalanceStrategy) {} + , LoadBalanceStrategy(InLoadBalanceStrategy) + { + } UPROPERTY(EditAnywhere, Category = "Load Balancing") FName Name; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/OpUtils.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/OpUtils.h index ebc0f906f1..530f26a0e3 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/OpUtils.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/OpUtils.h @@ -10,10 +10,6 @@ namespace SpatialGDK { - -Worker_Op* FindFirstOpOfType(const TArray& InOpLists, const Worker_OpType OpType); -void AppendAllOpsOfType(const TArray& InOpLists, const Worker_OpType OpType, TArray& FoundOps); -Worker_Op* FindFirstOpOfTypeForComponent(const TArray& InOpLists, const Worker_OpType OpType, const Worker_ComponentId ComponentId); -Worker_ComponentId GetComponentId(const Worker_Op* Op); +Worker_ComponentId GetComponentId(const Worker_Op& Op); } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/RPCContainer.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/RPCContainer.h index a02cca076a..f07e016ef9 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/RPCContainer.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/RPCContainer.h @@ -18,32 +18,30 @@ struct FPendingRPCParams; struct FRPCErrorInfo; DECLARE_DELEGATE_RetVal_OneParam(FRPCErrorInfo, FProcessRPCDelegate, const FPendingRPCParams&) -UENUM() -enum class ERPCResult : uint8 -{ - Success, + UENUM() enum class ERPCResult : uint8 { + Success, - // Shared across Sender and Receiver - UnresolvedTargetObject, - MissingFunctionInfo, - UnresolvedParameters, - NoAuthority, + // Shared across Sender and Receiver + UnresolvedTargetObject, + MissingFunctionInfo, + UnresolvedParameters, + NoAuthority, - // Sender specific - NoActorChannel, - SpatialActorChannelNotListening, - NoNetConnection, - InvalidRPCType, + // Sender specific + NoActorChannel, + SpatialActorChannelNotListening, + NoNetConnection, + InvalidRPCType, - // Specific to packing - NoOwningController, - NoControllerChannel, - ControllerChannelNotListening, + // Specific to packing + NoOwningController, + NoControllerChannel, + ControllerChannelNotListening, - RPCServiceFailure, + RPCServiceFailure, - Unknown -}; + Unknown + }; enum class ERPCQueueProcessResult : uint8_t { @@ -61,10 +59,7 @@ enum class ERPCQueueType : uint8_t struct FRPCErrorInfo { - bool Success() const - { - return (ErrorCode == ERPCResult::Success); - } + bool Success() const { return (ErrorCode == ERPCResult::Success); } TWeakObjectPtr TargetObject = nullptr; TWeakObjectPtr Function = nullptr; @@ -73,8 +68,9 @@ struct FRPCErrorInfo }; struct SPATIALGDK_API FPendingRPCParams -{ - FPendingRPCParams(const FUnrealObjectRef& InTargetObjectRef, ERPCType InType, SpatialGDK::RPCPayload&& InPayload); +{ + FPendingRPCParams(const FUnrealObjectRef& InTargetObjectRef, ERPCType InType, SpatialGDK::RPCPayload&& InPayload, + TOptional RPCIdForLinearEventTrace); // Moveable, not copyable. FPendingRPCParams() = delete; @@ -89,6 +85,8 @@ struct SPATIALGDK_API FPendingRPCParams FDateTime Timestamp; ERPCType Type; + + TOptional RPCIdForLinearEventTrace; }; class SPATIALGDK_API FRPCContainer @@ -104,7 +102,8 @@ class SPATIALGDK_API FRPCContainer ~FRPCContainer() = default; void BindProcessingFunction(const FProcessRPCDelegate& Function); - void ProcessOrQueueRPC(const FUnrealObjectRef& InTargetObjectRef, ERPCType InType, SpatialGDK::RPCPayload&& InPayload); + void ProcessOrQueueRPC(const FUnrealObjectRef& InTargetObjectRef, ERPCType InType, SpatialGDK::RPCPayload&& InPayload, + TOptional RPCIdForLinearEventTrace); void ProcessRPCs(); void DropForEntity(const Worker_EntityId& EntityId); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/RPCRingBuffer.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/RPCRingBuffer.h index 45a32f07f0..151e15eef8 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/RPCRingBuffer.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/RPCRingBuffer.h @@ -11,15 +11,11 @@ namespace SpatialGDK { - struct RPCRingBuffer { RPCRingBuffer(ERPCType InType); - const TOptional& GetRingBufferElement(uint64 RPCId) const - { - return RingBuffer[(RPCId - 1) % RingBuffer.Num()]; - } + const TOptional& GetRingBufferElement(uint64 RPCId) const { return RingBuffer[(RPCId - 1) % RingBuffer.Num()]; } ERPCType Type; TArray> RingBuffer; @@ -28,15 +24,9 @@ struct RPCRingBuffer struct RPCRingBufferDescriptor { - uint32 GetRingBufferElementIndex(uint64 RPCId) const - { - return (RPCId - 1) % RingBufferSize; - } + uint32 GetRingBufferElementIndex(uint64 RPCId) const { return (RPCId - 1) % RingBufferSize; } - Schema_FieldId GetRingBufferElementFieldId(uint64 RPCId) const - { - return SchemaFieldStart + GetRingBufferElementIndex(RPCId); - } + Schema_FieldId GetRingBufferElementFieldId(uint64 RPCId) const { return SchemaFieldStart + GetRingBufferElementIndex(RPCId); } uint32 RingBufferSize; Schema_FieldId SchemaFieldStart; @@ -45,12 +35,13 @@ struct RPCRingBufferDescriptor namespace RPCRingBufferUtils { - Worker_ComponentId GetRingBufferComponentId(ERPCType Type); +Worker_ComponentId GetRingBufferAuthComponentSetId(ERPCType Type); RPCRingBufferDescriptor GetRingBufferDescriptor(ERPCType Type); uint32 GetRingBufferSize(ERPCType Type); Worker_ComponentId GetAckComponentId(ERPCType Type); +Worker_ComponentId GetAckAuthComponentSetId(ERPCType Type); Schema_FieldId GetAckFieldId(ERPCType Type); Schema_FieldId GetInitiallyPresentMulticastRPCsCountFieldId(); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/RepLayoutUtils.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/RepLayoutUtils.h index 0812065e2a..22bd8c32a9 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/RepLayoutUtils.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/RepLayoutUtils.h @@ -13,10 +13,11 @@ namespace SpatialGDK { +void RepLayout_SerializeProperties(FRepLayout& RepLayout, FArchive& Ar, UPackageMap* Map, const int32 CmdStart, const int32 CmdEnd, + void* Data, bool& bHasUnmapped); -void RepLayout_SerializeProperties(FRepLayout& RepLayout, FArchive& Ar, UPackageMap* Map, const int32 CmdStart, const int32 CmdEnd, void* Data, bool& bHasUnmapped); - -inline void RepLayout_SerializeProperties_DynamicArray(FRepLayout& RepLayout, FArchive& Ar, UPackageMap* Map, const int32 CmdIndex, uint8* Data, bool& bHasUnmapped) +inline void RepLayout_SerializeProperties_DynamicArray(FRepLayout& RepLayout, FArchive& Ar, UPackageMap* Map, const int32 CmdIndex, + uint8* Data, bool& bHasUnmapped) { const FRepLayoutCmd& Cmd = RepLayout.Cmds[CmdIndex]; @@ -44,7 +45,8 @@ inline void RepLayout_SerializeProperties_DynamicArray(FRepLayout& RepLayout, FA } } -inline void RepLayout_SerializeProperties(FRepLayout& RepLayout, FArchive& Ar, UPackageMap* Map, const int32 CmdStart, const int32 CmdEnd, void* Data, bool& bHasUnmapped) +inline void RepLayout_SerializeProperties(FRepLayout& RepLayout, FArchive& Ar, UPackageMap* Map, const int32 CmdStart, const int32 CmdEnd, + void* Data, bool& bHasUnmapped) { for (int32 CmdIndex = CmdStart; CmdIndex < CmdEnd && !Ar.IsError(); CmdIndex++) { @@ -55,7 +57,7 @@ inline void RepLayout_SerializeProperties(FRepLayout& RepLayout, FArchive& Ar, U if (Cmd.Type == ERepLayoutCmdType::DynamicArray) { RepLayout_SerializeProperties_DynamicArray(RepLayout, Ar, Map, CmdIndex, (uint8*)Data + Cmd.Offset, bHasUnmapped); - CmdIndex = Cmd.EndCmd - 1; // The -1 to handle the ++ in the for loop + CmdIndex = Cmd.EndCmd - 1; // The -1 to handle the ++ in the for loop continue; } @@ -130,7 +132,8 @@ inline void RepLayout_ReceivePropertiesForRPC(FRepLayout& RepLayout, FNetBitRead } } -inline void ReadStructProperty(FSpatialNetBitReader& Reader, GDK_PROPERTY(StructProperty)* Property, USpatialNetDriver* NetDriver, uint8* Data, bool& bOutHasUnmapped) +inline void ReadStructProperty(FSpatialNetBitReader& Reader, GDK_PROPERTY(StructProperty) * Property, USpatialNetDriver* NetDriver, + uint8* Data, bool& bOutHasUnmapped) { UScriptStruct* Struct = Property->Struct; @@ -164,13 +167,11 @@ inline TArray GetClassRPCFunctions(const UClass* Class) TArray AllClassFunctions; TFieldIterator RemoteFunction(Class, EFieldIteratorFlags::IncludeSuper, EFieldIteratorFlags::IncludeDeprecated, - EFieldIteratorFlags::IncludeInterfaces); + EFieldIteratorFlags::IncludeInterfaces); for (; RemoteFunction; ++RemoteFunction) { - if (RemoteFunction->FunctionFlags & FUNC_NetClient || - RemoteFunction->FunctionFlags & FUNC_NetServer || - RemoteFunction->FunctionFlags & FUNC_NetCrossServer || - RemoteFunction->FunctionFlags & FUNC_NetMulticast) + if (RemoteFunction->FunctionFlags & FUNC_NetClient || RemoteFunction->FunctionFlags & FUNC_NetServer + || RemoteFunction->FunctionFlags & FUNC_NetCrossServer || RemoteFunction->FunctionFlags & FUNC_NetMulticast) { AllClassFunctions.Add(*RemoteFunction); } @@ -195,15 +196,14 @@ inline TArray GetClassRPCFunctions(const UClass* Class) } // When using multiple EventGraphs in blueprints, the functions could be iterated in different order, so just sort them alphabetically. - RelevantClassFunctions.Sort([](const UFunction& A, const UFunction& B) - { + RelevantClassFunctions.Sort([](const UFunction& A, const UFunction& B) { return FNameLexicalLess()(A.GetFName(), B.GetFName()); }); return RelevantClassFunctions; } -inline UScriptStruct* GetFastArraySerializerProperty(GDK_PROPERTY(ArrayProperty)* Property) +inline UScriptStruct* GetFastArraySerializerProperty(GDK_PROPERTY(ArrayProperty) * Property) { // Check if this array property conforms to the pattern of what we expect for a FFastArraySerializer. We do // this be ensuring that the owner struct has the NetDeltaSerialize flag, and that the array's internal item diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaDatabase.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaDatabase.h index fc4610fd7c..2ae1646a4a 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaDatabase.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaDatabase.h @@ -74,14 +74,37 @@ struct FSubobjectSchemaData } }; +USTRUCT() +struct FComponentIDs +{ + GENERATED_BODY() + + UPROPERTY() + TArray ComponentIDs; +}; + +UENUM() +enum class ESchemaDatabaseVersion : uint8 +{ + BeforeVersionSupportAdded = 0, + VersionSupportAdded, + + // Add new versions here + + LatestVersionPlusOne, + LatestVersion = LatestVersionPlusOne - 1 +}; + UCLASS() class SPATIALGDK_API USchemaDatabase : public UDataAsset { GENERATED_BODY() public: - - USchemaDatabase() : NextAvailableComponentId(SpatialConstants::STARTING_GENERATED_COMPONENT_ID) {} + USchemaDatabase() + : NextAvailableComponentId(SpatialConstants::STARTING_GENERATED_COMPONENT_ID) + { + } UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) TMap ActorClassPathToSchema; @@ -118,6 +141,11 @@ class SPATIALGDK_API USchemaDatabase : public UDataAsset uint32 NextAvailableComponentId; UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) - uint32 SchemaDescriptorHash; -}; + uint32 SchemaBundleHash; + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) + TMap ComponentSetIdToComponentIds; + + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) + ESchemaDatabaseVersion SchemaDatabaseVersion; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaOption.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaOption.h index c70074238d..b1d1e79134 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaOption.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaOption.h @@ -6,7 +6,6 @@ namespace SpatialGDK { - template class TSchemaOption { @@ -16,17 +15,16 @@ class TSchemaOption TSchemaOption(const T& InValue) : Value(MakeUnique(InValue)) - {} + { + } TSchemaOption(T&& InValue) : Value(MakeUnique(MoveTemp(InValue))) - {} - - TSchemaOption(const TSchemaOption& InValue) { - *this = InValue; } + TSchemaOption(const TSchemaOption& InValue) { *this = InValue; } + TSchemaOption(TSchemaOption&&) = default; TSchemaOption& operator=(const TSchemaOption& InValue) @@ -44,15 +42,9 @@ class TSchemaOption TSchemaOption& operator=(TSchemaOption&&) = default; - FORCEINLINE bool IsSet() const - { - return Value.IsValid(); - } + FORCEINLINE bool IsSet() const { return Value.IsValid(); } - FORCEINLINE explicit operator bool() const - { - return IsSet(); - } + FORCEINLINE explicit operator bool() const { return IsSet(); } const T& GetValue() const { @@ -81,25 +73,13 @@ class TSchemaOption return GetValue() == InValue.GetValue(); } - bool operator!=(const TSchemaOption& InValue) const - { - return !operator==(InValue); - } + bool operator!=(const TSchemaOption& InValue) const { return !operator==(InValue); } - T& operator*() const - { - return *Value; - } + T& operator*() const { return *Value; } - const T* operator->() const - { - return Value.Get(); - } + const T* operator->() const { return Value.Get(); } - T* operator->() - { - return Value.Get(); - } + T* operator->() { return Value.Get(); } private: TUniquePtr Value; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaUtils.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaUtils.h index 1d6556d250..63b51cb3d1 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaUtils.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaUtils.h @@ -15,7 +15,6 @@ using StringToEntityMap = TMap; namespace SpatialGDK { - inline void AddStringToSchema(Schema_Object* Object, Schema_FieldId Id, const FString& Value) { FTCHARToUTF8 CStrConversion(*Value); @@ -66,52 +65,6 @@ inline TArray GetBytesFromSchema(const Schema_Object* Object, Schema_Fiel return IndexBytesFromSchema(Object, Id, 0); } -inline void AddWorkerRequirementSetToSchema(Schema_Object* Object, Schema_FieldId Id, const WorkerRequirementSet& Value) -{ - Schema_Object* RequirementSetObject = Schema_AddObject(Object, Id); - for (const WorkerAttributeSet& AttributeSet : Value) - { - Schema_Object* AttributeSetObject = Schema_AddObject(RequirementSetObject, 1); - - for (const FString& Attribute : AttributeSet) - { - AddStringToSchema(AttributeSetObject, 1, Attribute); - } - } -} - -inline WorkerRequirementSet IndexWorkerRequirementSetFromSchema(Schema_Object* Object, Schema_FieldId Id, uint32 Index) -{ - Schema_Object* RequirementSetObject = Schema_IndexObject(Object, Id, Index); - - int32 AttributeSetCount = (int32)Schema_GetObjectCount(RequirementSetObject, 1); - WorkerRequirementSet RequirementSet; - RequirementSet.Reserve(AttributeSetCount); - - for (int32 i = 0; i < AttributeSetCount; i++) - { - Schema_Object* AttributeSetObject = Schema_IndexObject(RequirementSetObject, 1, i); - - int32 AttributeCount = (int32)Schema_GetBytesCount(AttributeSetObject, 1); - WorkerAttributeSet AttributeSet; - AttributeSet.Reserve(AttributeCount); - - for (int32 j = 0; j < AttributeCount; j++) - { - AttributeSet.Add(IndexStringFromSchema(AttributeSetObject, 1, j)); - } - - RequirementSet.Add(AttributeSet); - } - - 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; @@ -226,7 +179,7 @@ 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); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SnapshotGenerationTemplate.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SnapshotGenerationTemplate.h index e720ff4641..040fc3241f 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SnapshotGenerationTemplate.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SnapshotGenerationTemplate.h @@ -15,10 +15,11 @@ class SPATIALGDK_API USnapshotGenerationTemplate : public UObject ~USnapshotGenerationTemplate() = default; /** - * Write to the snapshot generation output stream - * @param OutputStream the output stream for the snapshot being created. - * @param NextEntityId the next available entity ID in the snapshot, this reference should be incremented appropriately. - * @return bool the success of writing to the snapshot output stream, this is returned to the overall snapshot generation. - */ - virtual bool WriteToSnapshotOutput(Worker_SnapshotOutputStream* OutputStream, Worker_EntityId& NextEntityId) PURE_VIRTUAL(USnapshotGenerationTemplate::WriteToSnapshotOutput, return false;); + * Write to the snapshot generation output stream + * @param OutputStream the output stream for the snapshot being created. + * @param NextEntityId the next available entity ID in the snapshot, this reference should be incremented appropriately. + * @return bool the success of writing to the snapshot output stream, this is returned to the overall snapshot generation. + */ + virtual bool WriteToSnapshotOutput(Worker_SnapshotOutputStream* OutputStream, Worker_EntityId& NextEntityId) + PURE_VIRTUAL(USnapshotGenerationTemplate::WriteToSnapshotOutput, return false;); }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialActorUtils.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialActorUtils.h index 6ace5d2ce3..4fee2172d6 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialActorUtils.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialActorUtils.h @@ -3,30 +3,34 @@ #pragma once #include "EngineClasses/SpatialNetConnection.h" +#include "EngineClasses/SpatialNetDriver.h" #include "Components/SceneComponent.h" -#include "Containers/Array.h" #include "Containers/UnrealString.h" #include "Engine/EngineTypes.h" #include "GameFramework/Actor.h" #include "GameFramework/Controller.h" +#include "GameFramework/GameMode.h" #include "GameFramework/PlayerController.h" #include "Math/Vector.h" +#if WITH_UNREAL_DEVELOPER_TOOLS || (!UE_BUILD_SHIPPING && !UE_BUILD_TEST) +#include "GameplayDebuggerCategoryReplicator.h" +#endif + namespace SpatialGDK { - -inline AActor* GetTopmostOwner(const AActor* Actor) +inline AActor* GetTopmostReplicatedOwner(const AActor* Actor) { check(Actor != nullptr); AActor* Owner = Actor->GetOwner(); - if (Owner == nullptr || Owner->IsPendingKillPending()) + if (Owner == nullptr || Owner->IsPendingKillPending() || !Owner->GetIsReplicated()) { return nullptr; } - while (Owner->GetOwner() != nullptr && !Owner->GetOwner()->IsPendingKillPending()) + while (Owner->GetOwner() != nullptr && !Owner->GetOwner()->IsPendingKillPending() && Owner->GetIsReplicated()) { Owner = Owner->GetOwner(); } @@ -34,20 +38,37 @@ inline AActor* GetTopmostOwner(const AActor* Actor) return Owner; } -inline AActor* GetHierarchyRoot(const AActor* Actor) +inline AActor* GetReplicatedHierarchyRoot(const AActor* Actor) { - AActor* TopmostOwner = GetTopmostOwner(Actor); + AActor* TopmostOwner = GetTopmostReplicatedOwner(Actor); return TopmostOwner != nullptr ? TopmostOwner : const_cast(Actor); } -inline FString GetConnectionOwningWorkerId(const AActor* Actor) +// Effectively, if this Actor is in a player hierarchy, get the PlayerController entity ID. +inline Worker_PartitionId GetConnectionOwningPartitionId(const AActor* Actor) { if (const USpatialNetConnection* NetConnection = Cast(Actor->GetNetConnection())) { - return NetConnection->ConnectionOwningWorkerId; + return NetConnection->PlayerControllerEntity; } - return FString(); + return SpatialConstants::INVALID_ENTITY_ID; +} + +inline Worker_EntityId GetConnectionOwningClientSystemEntityId(const APlayerController* PC) +{ + const USpatialNetConnection* NetConnection = Cast(PC->GetNetConnection()); + checkf(NetConnection != nullptr, TEXT("PlayerController did not have NetConnection when trying to find client system entity ID.")); + + if (NetConnection->ConnectionClientWorkerSystemEntityId == SpatialConstants::INVALID_ENTITY_ID) + { + UE_LOG(LogTemp, Error, + TEXT("Client system entity ID was invalid on a PlayerController. " + "This is expected after the PlayerController migrates, the client system entity ID is currently only " + "used on the spawning server.")); + } + + return NetConnection->ConnectionClientWorkerSystemEntityId; } inline FVector GetActorSpatialPosition(const AActor* InActor) @@ -89,4 +110,29 @@ inline FVector GetActorSpatialPosition(const AActor* InActor) return FRepMovement::RebaseOntoZeroOrigin(Location, InActor); } +inline bool DoesActorClassIgnoreVisibilityCheck(AActor* InActor) +{ + if (InActor->IsA(APlayerController::StaticClass()) || InActor->IsA(AGameModeBase::StaticClass()) +#if WITH_UNREAL_DEVELOPER_TOOLS || (!UE_BUILD_SHIPPING && !UE_BUILD_TEST) + || InActor->IsA(AGameplayDebuggerCategoryReplicator::StaticClass()) +#endif + ) + + { + return true; + } + + return false; +} + +inline bool ShouldActorHaveVisibleComponent(AActor* InActor) +{ + if (InActor->bAlwaysRelevant || !InActor->IsHidden() || DoesActorClassIgnoreVisibilityCheck(InActor)) + { + return true; + } + + return false; +} + } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialBasicAwaiter.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialBasicAwaiter.h new file mode 100644 index 0000000000..d5ff148945 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialBasicAwaiter.h @@ -0,0 +1,57 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/EngineTypes.h" +#include "ISpatialAwaitable.h" +#include "UObject/NoExportTypes.h" + +#include "SpatialBasicAwaiter.generated.h" + +DECLARE_DERIVED_EVENT(USpatialBasicAwaiter, ISpatialAwaitable::FSpatialAwaitableOnResetEvent, FSpatialAwaitableOnResetEvent); + +/** + * Object to await a single condition becoming true. + * If the awaiter is not ready yet, delegates passed via Await are stored, and called once the awaiter becomes ready (by having Ready called + * on it). Once the awaiter is ready, any delegate passed via Await will be called immediately. A common example of a condition would be an + * object being ready for use by other objects. + */ +UCLASS(Blueprintable, DefaultToInstanced) +class SPATIALGDK_API USpatialBasicAwaiter : public UObject, public ISpatialAwaitable +{ + GENERATED_BODY() + +public: + virtual ~USpatialBasicAwaiter() {} + + /** ISpatialAwaitable Implementation */ + // A Timeout value of 0 means the await should never time out. + virtual FDelegateHandle Await(const FOnReady& OnReadyDelegate, const float Timeout = 0.f) override; + virtual bool StopAwaiting(FDelegateHandle& Handle) override; + virtual FSpatialAwaitableOnResetEvent& OnReset() override; + /** End ISpatialAwaitable Implementation */ + + virtual void BeginDestroy() override; + + UFUNCTION(BlueprintCallable, Category = Awaiter) + void Ready(); + + UFUNCTION(BlueprintCallable, Category = Awaiter) + void Reset(); + + static const FString TIMEOUT_MESSAGE; + +protected: + DECLARE_EVENT_OneParam(USpatialBasicAwaiter, FOnReadyEvent, const FString&); + + FOnReadyEvent OnReadyEvent; + +private: + bool bIsReady; + + void InvokeQueuedDelegates(const FString& ErrorStatus = FString{}); + FSpatialAwaitableOnResetEvent OnResetEvent; + + TSet TimeoutHandles; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialDebugger.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialDebugger.h index 1f0030bd30..f8ccf92144 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialDebugger.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialDebugger.h @@ -4,6 +4,7 @@ #include "LoadBalancing/WorkerRegion.h" #include "SpatialCommonTypes.h" +#include "SpatialDebuggerConfigUI.h" #include "Containers/Map.h" #include "CoreMinimal.h" @@ -46,16 +47,36 @@ struct FWorkerRegionInfo UPROPERTY() FBox2D Extents; + + UPROPERTY() + FString WorkerName; + + UPROPERTY() + uint32 VirtualWorkerID; }; -UCLASS(SpatialType=(NotPersistent), Blueprintable, NotPlaceable) -class SPATIALGDK_API ASpatialDebugger : - public AInfo +UENUM() +namespace EActorTagDrawMode +{ +enum Type +{ + None, + LocalPlayer, + All +}; +} // namespace EActorTagDrawMode + +DECLARE_DYNAMIC_DELEGATE(FOnConfigUIClosedDelegate); + +/** + * Visualise spatial information at runtime and in the editor + */ +UCLASS(SpatialType = (NotPersistent), Blueprintable, NotPlaceable, Transient) +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; @@ -66,20 +87,49 @@ class SPATIALGDK_API ASpatialDebugger : UFUNCTION(Exec, Category = "SpatialGDK", BlueprintCallable) void SpatialToggleDebugger(); + UFUNCTION(Category = "SpatialGDK", BlueprintCallable, BlueprintPure) + bool IsEnabled(); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = UI, + meta = (ToolTip = "Key to open configuration UI for the debugger at runtime")) + FKey ConfigUIToggleKey = EKeys::F9; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = UI, meta = (ToolTip = "Key to select actor when debugging in game")) + FKey SelectActorKey = EKeys::RightMouseButton; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = UI, + meta = (ToolTip = "Key to highlight next actor under cursor when debugging in game")) + FKey HighlightActorKey = EKeys::MouseWheelAxis; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = UI, meta = (ToolTip = "In-game configuration UI widget")) + TSubclassOf ConfigUIClass; + + FOnConfigUIClosedDelegate OnConfigUIClosed; + // 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; + int PlayerPanelStartY = 64; - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = General, meta = (ToolTip = "Maximum range from local player that tags will be drawn out to")) + 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")) + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Visualization, meta = (Tooltip = "Which Actor tags to show")) + TEnumAsByte ActorTagDrawMode = EActorTagDrawMode::All; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Visualization, + meta = (Tooltip = "Show all replicated Actors in the player controller's hierarchy, or just state/controller/pawn")) + bool bShowPlayerHierarchy = false; + + 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")) + 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")) @@ -91,45 +141,99 @@ class SPATIALGDK_API ASpatialDebugger : UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Visualization, meta = (ToolTip = "Show Actor Name for every entity in range")) bool bShowActorName = false; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Visualization, meta = (ToolTip = "Show glowing mesh when selecting actors.")) + bool bShowHighlight = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Visualization, + meta = (ToolTip = "Select the object types you want to query when selecting actors")) + TArray> SelectCollisionTypesToQuery = { + ECollisionChannel::ECC_WorldStatic, ECollisionChannel::ECC_WorldDynamic, ECollisionChannel::ECC_Pawn, + ECollisionChannel::ECC_Destructible, ECollisionChannel::ECC_Vehicle, ECollisionChannel::ECC_PhysicsBody + }; + 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")) + 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 = "Height at which the origin of each worker region cuboid is placed")) + float WorkerRegionHeight = 30.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Visualization, + meta = (ToolTip = "Vertical scale to apply to each worker region cuboid")) + float WorkerRegionVerticalScale = 1.0f; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Visualization, meta = (ToolTip = "Opacity of the worker region cuboids")) + float WorkerRegionOpacity = 0.7f; + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Visualization, meta = (ToolTip = "Texture to use for the Auth Icon")) - UTexture2D *AuthTexture; + UTexture2D* AuthTexture; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Visualization, meta = (ToolTip = "Texture to use for the Auth Intent Icon")) - UTexture2D *AuthIntentTexture; + UTexture2D* AuthIntentTexture; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Visualization, meta = (ToolTip = "Texture to use for the Unlocked Icon")) - UTexture2D *UnlockedTexture; + UTexture2D* UnlockedTexture; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Visualization, meta = (ToolTip = "Texture to use for the Locked Icon")) - UTexture2D *LockedTexture; + UTexture2D* LockedTexture; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Visualization, meta = (ToolTip = "Texture to use for the Box Icon")) - UTexture2D *BoxTexture; + UTexture2D* BoxTexture; + + // This will be drawn instead of the mouse cursor when selecting an actor + UPROPERTY(EditDefaultsOnly, Category = Visualization, meta = (ToolTip = "Texture to use when selecting an actor")) + UTexture2D* CrosshairTexture; 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")) + 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; + UFUNCTION() + void OnToggleConfigUI(); + + UFUNCTION(BlueprintCallable, Category = Visualization) + void ToggleSelectActor(); + + UFUNCTION() + void OnSelectActor(); + + UFUNCTION() + void OnHighlightActor(); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = Visualization) + bool IsSelectActorEnabled() const; + +private: + UFUNCTION() + void DefaultOnConfigUIClosed(); + +public: + UFUNCTION(BlueprintCallable, Category = Visualization) + void SetShowWorkerRegions(const bool bNewShow); + + void ActorAuthorityChanged(const Worker_ComponentSetAuthorityChangeOp& AuthOp) const; void ActorAuthorityIntentChanged(Worker_EntityId EntityId, VirtualWorkerId NewIntentVirtualWorkerId) const; +#if WITH_EDITOR + void EditorRefreshWorkerRegions(); + static void EditorRefreshDisplay(); + bool EditorAllowWorkerBoundaries() const; + void EditorSpatialToggleDebugger(bool bEnabled); +#endif + private: void LoadIcons(); @@ -140,7 +244,24 @@ class SPATIALGDK_API ASpatialDebugger : // FDebugDrawDelegate void DrawDebug(UCanvas* Canvas, APlayerController* Controller); - void DrawTag(UCanvas* Canvas, const FVector2D& ScreenLocation, const Worker_EntityId EntityId, const FString& ActorName); + FVector GetLocalPawnLocation(); + + // Allow user to select actor(s) for debugging - the mesh on the actor must have collision presets enabled to block on at least one of + // the object channels + void SelectActorsToTag(UCanvas* Canvas); + + void HighlightActorUnderCursor(TWeakObjectPtr& NewHoverActor); + + TWeakObjectPtr GetActorAtPosition(const FVector2D& MousePosition); + + TWeakObjectPtr GetHitActor(); + + FVector2D ProjectActorToScreen(const TWeakObjectPtr Actor, const FVector& PlayerLocation); + + void RevertHoverMaterials(); + + void DrawTag(UCanvas* Canvas, const FVector2D& ScreenLocation, const Worker_EntityId EntityId, const FString& ActorName, + const bool bCentre); void DrawDebugLocalPlayer(UCanvas* Canvas); void CreateWorkerRegions(); @@ -149,6 +270,11 @@ class SPATIALGDK_API ASpatialDebugger : FColor GetTextColorForBackgroundColor(const FColor& BackgroundColor) const; int32 GetNumberOfDigitsIn(int32 SomeNumber) const; +#if WITH_EDITOR + void EditorInitialiseWorkerRegions(); + void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent); +#endif + static const int ENTITY_ACTOR_MAP_RESERVATION_COUNT = 512; static const int PLAYER_TAG_VERTICAL_OFFSET = 18; @@ -179,4 +305,33 @@ class SPATIALGDK_API ASpatialDebugger : FFontRenderInfo FontRenderInfo; FCanvasIcon Icons[ICON_MAX]; + + USpatialDebuggerConfigUI* ConfigUIWidget; + + // Mode for selecting actors under the cursor - should only be visible in the runtime config UI + bool bSelectActor = false; + + // Actors selected by user for debugging + TArray> SelectedActors; + + // Highlighted actor under the mouse cursor + TWeakObjectPtr HoverActor; + // Highlighted actor original materials and components + TArray> ActorMeshMaterials; + TArray> ActorMeshComponents; + // Material for highlighting actor + UPROPERTY() + UMaterialInterface* WireFrameMaterial; + + // All actors under the mouse cursor + TArray> HitActors; + + // Index for selecting the highlighted actor when multiple are under the mouse cursor + int32 HoverIndex; + + // Mouse position to avoid unnecessary raytracing when mouse has not moved + FVector2D MousePosition; + + // Select actor object types to query + FCollisionObjectQueryParams CollisionObjectParams; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialDebuggerConfigUI.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialDebuggerConfigUI.h new file mode 100644 index 0000000000..950782a1da --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialDebuggerConfigUI.h @@ -0,0 +1,30 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Blueprint/UserWidget.h" +#include "CoreMinimal.h" + +#include "SpatialDebuggerConfigUI.generated.h" + +class ASpatialDebugger; + +/** + * UI to change visualization settings of the spatial debugger in-game. + */ +UCLASS(Abstract) +class SPATIALGDK_API USpatialDebuggerConfigUI : public UUserWidget +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintNativeEvent, Category = "Debugger") + void SetSpatialDebugger(ASpatialDebugger* InDebugger); + + UFUNCTION(BlueprintImplementableEvent, Category = "Debugger") + void OnShow(); + +protected: + UPROPERTY(BlueprintReadOnly, Category = "Debugger") + ASpatialDebugger* SpatialDebugger; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyPayload.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyPayload.h index f9769f4588..d6b845370f 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyPayload.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyPayload.h @@ -2,15 +2,15 @@ #pragma once -#include "CoreMinimal.h" #include "Containers/Array.h" +#include "CoreMinimal.h" #include "Hash/CityHash.h" #include "SpatialCommonTypes.h" #include "SpatialLatencyPayload.generated.h" USTRUCT(BlueprintType) -struct SPATIALGDK_API FSpatialLatencyPayload +struct SPATIALGDK_API FSpatialLatencyPayload { GENERATED_BODY() @@ -20,7 +20,8 @@ struct SPATIALGDK_API FSpatialLatencyPayload : TraceId(MoveTemp(TraceBytes)) , SpanId(MoveTemp(SpanBytes)) , Key(InKey) - {} + { + } UPROPERTY() TArray TraceId; @@ -32,13 +33,11 @@ struct SPATIALGDK_API FSpatialLatencyPayload int32 Key = InvalidTraceKey; // Required for TMap hash - bool operator == (const FSpatialLatencyPayload& Other) const - { - return TraceId == Other.TraceId && SpanId == Other.SpanId; - } + 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()); + 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 index a08a44e4ed..80479cdae4 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyTracer.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyTracer.h @@ -4,14 +4,15 @@ #include "CoreMinimal.h" -#include "SpatialConstants.h" #include "Containers/Map.h" #include "Containers/StaticArray.h" +#include "SpatialConstants.h" #include "SpatialLatencyPayload.h" #include "Utils/GDKPropertyMacros.h" #if TRACE_LIB_ACTIVE -#include "WorkerSDK/improbable/trace.h" +#include +#include #endif #include "SpatialLatencyTracer.generated.h" @@ -24,21 +25,21 @@ class USpatialGameInstance; namespace SpatialGDK { - struct FOutgoingMessage; -} // 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 - }; +enum Type +{ + RPC, + Property, + Tagged +}; } UCLASS() @@ -47,7 +48,6 @@ 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. @@ -89,31 +89,38 @@ class SPATIALGDK_API USpatialLatencyTracer : public UObject 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)". + // 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. + // 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); + 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. + // 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); + 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); + 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. + // 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); @@ -124,11 +131,14 @@ class SPATIALGDK_API USpatialLatencyTracer : public UObject // Internal GDK usage, shouldn't be used by game code static USpatialLatencyTracer* GetTracer(UObject* WorldContextObject); + UFUNCTION(BlueprintPure, Category = "SpatialOS", meta = (WorldContext = "WorldContextObject")) + static FString GetTraceMetadata(UObject* WorldContextObject); + #if TRACE_LIB_ACTIVE bool IsValidKey(TraceKey Key); TraceKey RetrievePendingTrace(const UObject* Obj, const UFunction* Function); - TraceKey RetrievePendingTrace(const UObject* Obj, const GDK_PROPERTY(Property)* Property); + TraceKey RetrievePendingTrace(const UObject* Obj, const GDK_PROPERTY(Property) * Property); TraceKey RetrievePendingTrace(const UObject* Obj, const FString& Tag); void WriteToLatencyTrace(const TraceKey Key, const FString& TraceDesc); @@ -144,14 +154,14 @@ class SPATIALGDK_API USpatialLatencyTracer : public UObject 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 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); @@ -179,7 +189,6 @@ class SPATIALGDK_API USpatialLatencyTracer : public UObject TSet RootTraces; public: - #endif // TRACE_LIB_ACTIVE // Used for testing trace functionality, will send a debug trace in three parts from this worker diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLoadBalancingHandler.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLoadBalancingHandler.h similarity index 83% rename from SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLoadBalancingHandler.h rename to SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLoadBalancingHandler.h index f76f1ba574..68a9cb7350 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLoadBalancingHandler.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLoadBalancingHandler.h @@ -3,6 +3,7 @@ #pragma once #include "EngineClasses/SpatialNetDriver.h" +#include "EngineClasses/SpatialPackageMapClient.h" #include "Utils/SpatialActorUtils.h" DECLARE_LOG_CATEGORY_EXTERN(LogSpatialLoadBalancingHandler, Log, All); @@ -10,7 +11,6 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialLoadBalancingHandler, Log, All); class FSpatialLoadBalancingHandler { public: - FSpatialLoadBalancingHandler(USpatialNetDriver* InNetDriver); // Iterates over the list of actors to replicate, to check if they should migrate to another worker @@ -49,48 +49,40 @@ class FSpatialLoadBalancingHandler } } - const TMap& GetActorsToMigrate() const - { - return ActorsToMigrate; - } + const TMap& GetActorsToMigrate() const { return ActorsToMigrate; } // Sends the migration instructions and update actor authority. void ProcessMigrations(); -protected: - - void UpdateSpatialDebugInfo(AActor* Actor, Worker_EntityId EntityId) const; - - uint64 GetLatestAuthorityChangeFromHierarchy(const AActor* HierarchyActor) const; - enum class EvaluateActorResult { - None, // Actor not concerned by load balancing - Migrate, // Actor should migrate - RemoveAdditional // Actor is already marked as migrating. + None, // Actor not concerned by load balancing + Migrate, // Actor should migrate + RemoveAdditional // Actor is already marked as migrating. }; EvaluateActorResult EvaluateSingleActor(AActor* Actor, AActor*& OutNetOwner, VirtualWorkerId& OutWorkerId); +protected: + void UpdateSpatialDebugInfo(AActor* Actor, Worker_EntityId EntityId) const; + + uint64 GetLatestAuthorityChangeFromHierarchy(const AActor* HierarchyActor) const; + template bool CollectActorsToMigrate(ReplicationContext& iCtx, AActor* Actor, bool bNetOwnerHasAuth) { - if(Actor->GetIsReplicated()) + if (Actor->GetIsReplicated()) { - if (!iCtx.IsActorReadyForMigration(Actor)) + EActorMigrationResult ActorMigration = iCtx.IsActorReadyForMigration(Actor); + if (ActorMigration != EActorMigrationResult::Success) { // Prevents an Actor hierarchy from migrating if one of its actor is not ready. // Child Actors are always allowed to join the owner. // This is a band aid to prevent Actors from being left behind, // although it has the risk of creating an infinite lock if the child is unable to become ready. - if(bNetOwnerHasAuth) + if (bNetOwnerHasAuth) { - AActor* HierarchyRoot = SpatialGDK::GetHierarchyRoot(Actor); - UE_LOG(LogSpatialLoadBalancingHandler, Warning, - TEXT("Prevented Actor %s 's hierarchy from migrating because Actor %s is not ready."), - *HierarchyRoot->GetName(), - *Actor->GetName()); - + LogMigrationFailure(ActorMigration, Actor); return false; } } @@ -111,6 +103,8 @@ class FSpatialLoadBalancingHandler return true; } + void LogMigrationFailure(EActorMigrationResult ActorMigrationResult, AActor* Actor); + USpatialNetDriver* NetDriver; TMap ActorsToMigrate; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h index 339381e674..8f2442b6ad 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h @@ -46,7 +46,7 @@ class SPATIALGDK_API USpatialMetrics : public UObject void TrackSentRPC(UFunction* Function, ERPCType RPCType, int PayloadSize); - void HandleWorkerMetrics(Worker_Op* Op); + void HandleWorkerMetrics(const Worker_Op& Op); // The user can bind their own delegate to handle worker metrics. typedef TMap WorkerGaugeMetric; @@ -57,7 +57,7 @@ class SPATIALGDK_API USpatialMetrics : public UObject }; typedef TMap WorkerHistogramMetrics; DECLARE_MULTICAST_DELEGATE_TwoParams(WorkerMetricsDelegate, const WorkerGaugeMetric&, const WorkerHistogramMetrics&); - WorkerMetricsDelegate WorkerMetricsUpdated; + WorkerMetricsDelegate WorkerMetricsUpdated; // Delegate used to poll for the current player controller's reference DECLARE_DELEGATE_RetVal(FUnrealObjectRef, FControllerRefProviderDelegate); @@ -66,8 +66,8 @@ class SPATIALGDK_API USpatialMetrics : public UObject void SetWorkerLoadDelegate(const UserSuppliedMetric& Delegate) { WorkerLoadDelegate = Delegate; } void SetCustomMetric(const FString& Metric, const UserSuppliedMetric& Delegate); void RemoveCustomMetric(const FString& Metric); -private: +private: // Worker SDK metrics WorkerGaugeMetric WorkerSDKGaugeMetrics; WorkerHistogramMetrics WorkerSDKHistogramMetrics; @@ -104,4 +104,3 @@ class SPATIALGDK_API USpatialMetrics : public UObject bool bRPCTrackingEnabled; float RPCTrackingStartTime; }; - diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetricsDisplay.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetricsDisplay.h index 3ea64a26c2..6a8117856b 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetricsDisplay.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetricsDisplay.h @@ -2,8 +2,8 @@ #pragma once -#include "CoreMinimal.h" #include "Containers/Queue.h" +#include "CoreMinimal.h" #include "GameFramework/Info.h" #include "SpatialMetricsDisplay.generated.h" @@ -24,20 +24,15 @@ struct FWorkerStats UPROPERTY() uint32 ServerReplicationLimit; - bool operator==(const FWorkerStats& other) const - { - return (WorkerName.Equals(other.WorkerName)); - } + bool operator==(const FWorkerStats& other) const { return (WorkerName.Equals(other.WorkerName)); } }; UCLASS(SpatialType) -class SPATIALGDK_API ASpatialMetricsDisplay : - public AInfo +class SPATIALGDK_API ASpatialMetricsDisplay : public AInfo { GENERATED_UCLASS_BODY() public: - virtual void Tick(float DeltaSeconds) override; virtual void BeginPlay() override; @@ -47,7 +42,6 @@ class SPATIALGDK_API ASpatialMetricsDisplay : void SpatialToggleStatDisplay(); private: - FDelegateHandle DrawDebugDelegateHandle; UPROPERTY(Replicated) diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialStatics.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialStatics.h index 96de033fe4..c33239735e 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialStatics.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialStatics.h @@ -9,6 +9,7 @@ #include "UObject/TextProperty.h" #include "SpatialGDKSettings.h" +#include "Utils/SpatialDebugger.h" #include "SpatialStatics.generated.h" @@ -32,7 +33,6 @@ class SPATIALGDK_API USpatialStatics : public UBlueprintFunctionLibrary GENERATED_BODY() public: - /** * Returns true if SpatialOS Networking is enabled. */ @@ -40,14 +40,27 @@ class SPATIALGDK_API USpatialStatics : public UBlueprintFunctionLibrary static bool IsSpatialNetworkingEnabled(); /** - * Returns true if spatial networking and multi worker are enabled. - */ + * Returns whether handover is disabled in offloading scenarios. + */ UFUNCTION(BlueprintPure, Category = "SpatialOS", meta = (WorldContext = "WorldContextObject")) - static bool IsSpatialMultiWorkerEnabled(const UObject* WorldContextObject); + static bool IsHandoverEnabled(const UObject* WorldContextObject); /** - * Returns true if there is more than one worker layer in the SpatialWorldSettings and IsMultiWorkerEnabled. - */ + * Returns true if spatial networking and multi worker are enabled. + */ + UFUNCTION(BlueprintPure, Category = "SpatialOS", meta = (WorldContext = "WorldContextObject")) + static bool IsMultiWorkerEnabled(); + + /** + * Returns the multi worker settings class. + */ + UFUNCTION(BlueprintPure, Category = "SpatialOS", meta = (WorldContext = "WorldContextObject")) + static TSubclassOf GetSpatialMultiWorkerClass(const UObject* WorldContextObject, + bool bForceNonEditorSettings = false); + + /** + * Returns true if there is more than one worker layer in the SpatialWorldSettings and IsMultiWorkerEnabled. + */ UFUNCTION(BlueprintPure, Category = "SpatialOS|Offloading") static bool IsSpatialOffloadingEnabled(const UWorld* World); @@ -68,14 +81,22 @@ class SPATIALGDK_API USpatialStatics : public UBlueprintFunctionLibrary /** * Functionally the same as the native Unreal PrintString but also logs to the spatial runtime. */ - UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject", CallableWithoutWorldContext, Keywords = "log print spatial", AdvancedDisplay = "2", DevelopmentOnly), Category = "Utilities|String") - static void PrintStringSpatial(UObject* WorldContextObject, const FString& InString = FString(TEXT("Hello")), bool bPrintToScreen = true, FLinearColor TextColor = FLinearColor(0.0, 0.66, 1.0), float Duration = 2.f); + UFUNCTION(BlueprintCallable, + meta = (WorldContext = "WorldContextObject", CallableWithoutWorldContext, Keywords = "log print spatial", + AdvancedDisplay = "2", DevelopmentOnly), + Category = "Utilities|String") + static void PrintStringSpatial(UObject* WorldContextObject, const FString& InString = FString(TEXT("Hello")), + bool bPrintToScreen = true, FLinearColor TextColor = FLinearColor(0.0, 0.66, 1.0), float Duration = 2.f); /** * Functionally the same as the native Unreal PrintText but also logs to the spatial runtime. */ - 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); + 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. @@ -126,22 +147,32 @@ class SPATIALGDK_API USpatialStatics : public UBlueprintFunctionLibrary * If Spatial networking or multi-worker is disabled, this will return an invalid locking token. */ UFUNCTION(BlueprintCallable, Category = "SpatialGDK|Locking") - static FLockingToken AcquireLock(AActor* Actor, const FString& DebugString = TEXT("")); + static FLockingToken AcquireLock(AActor* Actor, const FString& DebugString = TEXT("")); /** - * ReleaseLock should only be called for an authoritative Actor from a server where the LockToken argument - * was previously returned from a call to AcquireLock. - * If Spatial networking or multi-worker is disabled, this will early. - */ + * ReleaseLock should only be called for an authoritative Actor from a server where the LockToken argument + * was previously returned from a call to AcquireLock. + * If Spatial networking or multi-worker is disabled, this will early. + */ UFUNCTION(BlueprintCallable, Category = "SpatialGDK|Locking") - static void ReleaseLock(const AActor* Actor, FLockingToken LockToken); + static void ReleaseLock(const AActor* Actor, FLockingToken LockToken); /** - * IsLocked should only be called for an authoritative Actor from a server. - * If Spatial networking or multi-worker is disabled, this will early. - */ + * IsLocked should only be called for an authoritative Actor from a server. + * If Spatial networking or multi-worker is disabled, this will early. + */ UFUNCTION(BlueprintPure, Category = "SpatialGDK|Locking") - static bool IsLocked(const AActor* Actor); + static bool IsLocked(const AActor* Actor); + + /** + * Returns the local layer name for this worker. Returns client worker type for all clients, + * and default layer for native servers. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "SpatialOS", meta = (WorldContext = "WorldContextObject")) + static FName GetLayerName(const UObject* WorldContextObject); + + UFUNCTION(BlueprintCallable, Category = "SpatialGDK|Spatial Debugger", meta = (WorldContext = "WorldContextObject")) + static void SpatialDebuggerSetOnConfigUIClosedCallback(const UObject* WorldContextObject, FOnConfigUIClosedDelegate Delegate); private: static FName GetCurrentWorkerType(const UObject* WorldContext); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/WorkerVersionCheck.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/WorkerVersionCheck.h new file mode 100644 index 0000000000..a81ef03b04 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/WorkerVersionCheck.h @@ -0,0 +1,18 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "improbable/c_worker.h" + +#define WORKER_SDK_VERSION "15.0.0" + +constexpr bool StringsEqual(char const* A, char const* B) +{ + return *A == *B && (*A == '\0' || StringsEqual(A + 1, B + 1)); +} + +// Check if the current version of the Worker SDK is compatible with the current version of UnrealGDK +// WORKER_SDK_VERSION is incremented here when breaking changes are made that make previous versions of the SDK +// incompatible +static_assert(StringsEqual(WORKER_API_VERSION_STR, WORKER_SDK_VERSION), + "Worker SDK version is incompatible with the UnrealGDK version. Check both the Worker SDK and GDK are up to date"); diff --git a/SpatialGDK/Source/SpatialGDK/SpatialGDK.Build.cs b/SpatialGDK/Source/SpatialGDK/SpatialGDK.Build.cs index af7beb57db..4ba85ba7b7 100644 --- a/SpatialGDK/Source/SpatialGDK/SpatialGDK.Build.cs +++ b/SpatialGDK/Source/SpatialGDK/SpatialGDK.Build.cs @@ -13,7 +13,7 @@ public class SpatialGDK : ModuleRules { public SpatialGDK(ReadOnlyTargetRules Target) : base(Target) { - bLegacyPublicIncludePaths = false; + bLegacyPublicIncludePaths = false; PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; #pragma warning disable 0618 bFasterWithoutUnity = true; // Deprecated in 4.24, replace with bUseUnity = false; once we drop support for 4.23 @@ -37,13 +37,21 @@ public SpatialGDK(ReadOnlyTargetRules Target) : base(Target) "CoreUObject", "Engine", "EngineSettings", - "Projects", - "OnlineSubsystemUtils", "InputCore", + "OnlineSubsystemUtils", + "Projects", + "ReplicationGraph", "Sockets", - "ReplicationGraph" + "Slate", + "UMG" }); + if (Target.bBuildDeveloperTools || (Target.Configuration != UnrealTargetConfiguration.Shipping && + Target.Configuration != UnrealTargetConfiguration.Test)) + { + PublicDependencyModuleNames.Add("GameplayDebugger"); + } + if (Target.bBuildEditor) { PublicDependencyModuleNames.Add("UnrealEd"); @@ -55,7 +63,7 @@ public SpatialGDK(ReadOnlyTargetRules Target) : base(Target) PublicDependencyModuleNames.Add("PerfCounters"); } - var WorkerLibraryDir = Path.GetFullPath(Path.Combine(ModuleDirectory, "..", "..", "Binaries", "ThirdParty", "Improbable", Target.Platform.ToString())); + var WorkerLibraryDir = Path.Combine(ModuleDirectory, "..", "..", "Binaries", "ThirdParty", "Improbable", Target.Platform.ToString()); var WorkerLibraryPaths = new List { @@ -132,12 +140,15 @@ public SpatialGDK(ReadOnlyTargetRules Target) : base(Target) } WorkerImportLib = Path.Combine(WorkerLibraryDir, WorkerImportLib); + PublicRuntimeLibraryPaths.Add(WorkerLibraryDir); + + } + else + { + PublicLibraryPaths.AddRange(WorkerLibraryPaths); } 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 = ""; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/CloudDeploymentConfiguration.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/CloudDeploymentConfiguration.cpp index 5ae9ce5fc4..94086ff426 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/CloudDeploymentConfiguration.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/CloudDeploymentConfiguration.cpp @@ -2,8 +2,8 @@ #include "CloudDeploymentConfiguration.h" -#include "SpatialGDKSettings.h" #include "SpatialGDKEditorSettings.h" +#include "SpatialGDKSettings.h" void FCloudDeploymentConfiguration::InitFromSettings() { diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.cpp index f6d9e780b6..77a08949cb 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.cpp @@ -8,12 +8,12 @@ #include "UObject/TextProperty.h" #include "Interop/SpatialClassInfoManager.h" +#include "SpatialGDKEditorSchemaGenerator.h" #include "SpatialGDKSettings.h" #include "Utils/CodeWriter.h" #include "Utils/ComponentIdGenerator.h" #include "Utils/DataTypeUtilities.h" #include "Utils/GDKPropertyMacros.h" -#include "SpatialGDKEditorSchemaGenerator.h" using namespace SpatialGDKEditor::Schema; @@ -21,7 +21,6 @@ DEFINE_LOG_CATEGORY(LogSchemaGenerator); namespace { - ESchemaComponentType PropertyGroupToSchemaComponentType(EReplicatedPropertyGroup Group) { if (Group == REP_MultiClient) @@ -41,7 +40,7 @@ ESchemaComponentType PropertyGroupToSchemaComponentType(EReplicatedPropertyGroup // Given a RepLayout cmd type (a data type supported by the replication system). Generates the corresponding // type used in schema. -FString PropertyToSchemaType(GDK_PROPERTY(Property)* Property) +FString PropertyToSchemaType(GDK_PROPERTY(Property) * Property) { FString DataType; @@ -95,7 +94,8 @@ FString PropertyToSchemaType(GDK_PROPERTY(Property)* Property) { DataType = TEXT("uint64"); } - else if (Property->IsA(GDK_PROPERTY(NameProperty)::StaticClass()) || Property->IsA(GDK_PROPERTY(StrProperty)::StaticClass()) || Property->IsA(GDK_PROPERTY(TextProperty)::StaticClass())) + else if (Property->IsA(GDK_PROPERTY(NameProperty)::StaticClass()) || Property->IsA(GDK_PROPERTY(StrProperty)::StaticClass()) + || Property->IsA(GDK_PROPERTY(TextProperty)::StaticClass())) { DataType = TEXT("string"); } @@ -122,24 +122,19 @@ FString PropertyToSchemaType(GDK_PROPERTY(Property)* Property) void WriteSchemaRepField(FCodeWriter& Writer, const TSharedPtr RepProp, const int FieldCounter) { - Writer.Printf("{0} {1} = {2};", - *PropertyToSchemaType(RepProp->Property), - *SchemaFieldName(RepProp), - FieldCounter - ); + Writer.Printf("{0} {1} = {2};", *PropertyToSchemaType(RepProp->Property), *SchemaFieldName(RepProp), FieldCounter); } void WriteSchemaHandoverField(FCodeWriter& Writer, const TSharedPtr HandoverProp, const int FieldCounter) { - Writer.Printf("{0} {1} = {2};", - *PropertyToSchemaType(HandoverProp->Property), - *SchemaFieldName(HandoverProp), - FieldCounter - ); + Writer.Printf("{0} {1} = {2};", *PropertyToSchemaType(HandoverProp->Property), *SchemaFieldName(HandoverProp), FieldCounter); } // Generates schema for a statically attached subobject on an Actor. -FActorSpecificSubobjectSchemaData GenerateSchemaForStaticallyAttachedSubobject(FCodeWriter& Writer, FComponentIdGenerator& IdGenerator, FString PropertyName, TSharedPtr& TypeInfo, UClass* ComponentClass, UClass* ActorClass, int MapIndex, const FActorSpecificSubobjectSchemaData* ExistingSchemaData) +FActorSpecificSubobjectSchemaData GenerateSchemaForStaticallyAttachedSubobject(FCodeWriter& Writer, FComponentIdGenerator& IdGenerator, + FString PropertyName, TSharedPtr& TypeInfo, + UClass* ComponentClass, UClass* ActorClass, int MapIndex, + const FActorSpecificSubobjectSchemaData* ExistingSchemaData) { FUnrealFlatRepData RepData = GetFlatRepData(TypeInfo); @@ -194,7 +189,8 @@ FActorSpecificSubobjectSchemaData GenerateSchemaForStaticallyAttachedSubobject(F Writer.PrintNewLine(); // Handover (server to server) replicated properties. - Writer.Printf("component {0} {", *(PropertyName + TEXT("Handover"))); + FString ComponentName = PropertyName + TEXT("Handover"); + Writer.Printf("component {0} {", *ComponentName); Writer.Indent(); Writer.Printf("id = {0};", ComponentId); Writer.Printf("data unreal.generated.{0};", *SchemaHandoverDataName(ComponentClass)); @@ -236,7 +232,8 @@ void GenerateSubobjectSchemaForActorIncludes(FCodeWriter& Writer, TSharedPtr TypeInfo, FString SchemaPath, FActorSchemaData& ActorSchemaData, const FActorSchemaData* ExistingSchemaData) +void GenerateSubobjectSchemaForActor(FComponentIdGenerator& IdGenerator, UClass* ActorClass, TSharedPtr TypeInfo, + FString SchemaPath, FActorSchemaData& ActorSchemaData, const FActorSchemaData* ExistingSchemaData) { FCodeWriter Writer; @@ -244,7 +241,7 @@ void GenerateSubobjectSchemaForActor(FComponentIdGenerator& IdGenerator, UClass* // 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()); + *ClassPathToSchemaName[ActorClass->GetPathName()].ToLower()); Writer.PrintNewLine(); @@ -277,7 +274,9 @@ void GenerateSubobjectSchemaForActor(FComponentIdGenerator& IdGenerator, UClass* } } } - SubobjectData = GenerateSchemaForStaticallyAttachedSubobject(Writer, IdGenerator, UnrealNameToSchemaComponentName(SubobjectTypeInfo->Name.ToString()), SubobjectTypeInfo, SubobjectClass, ActorClass, 0, ExistingSubobjectSchemaData); + SubobjectData = GenerateSchemaForStaticallyAttachedSubobject( + Writer, IdGenerator, UnrealNameToSchemaComponentName(SubobjectTypeInfo->Name.ToString()), SubobjectTypeInfo, SubobjectClass, + ActorClass, 0, ExistingSubobjectSchemaData); } else { @@ -317,10 +316,12 @@ FString GetRPCFieldPrefix(ERPCType RPCType) return FString(); } -void GenerateRPCEndpoint(FCodeWriter& Writer, FString EndpointName, Worker_ComponentId ComponentId, TArray SentRPCTypes, TArray AckedRPCTypes) +void GenerateRPCEndpoint(FCodeWriter& Writer, FString EndpointName, Worker_ComponentId ComponentId, TArray SentRPCTypes, + TArray AckedRPCTypes) { + FString ComponentName = TEXT("Unreal") + EndpointName; Writer.PrintNewLine(); - Writer.Printf("component Unreal{0} {", *EndpointName).Indent(); + Writer.Printf("component {0} {", *ComponentName).Indent(); Writer.Printf("id = {0};", ComponentId); Schema_FieldId FieldId = 1; @@ -385,6 +386,25 @@ void GenerateSubobjectSchema(FComponentIdGenerator& IdGenerator, UClass* Class, } } + // Also check the HandoverData + FCmdHandlePropertyMap HandoverData = GetFlatHandoverData(TypeInfo); + for (auto& PropertyPair : HandoverData) + { + GDK_PROPERTY(Property)* Property = PropertyPair.Value->Property; + if (Property->IsA()) + { + bShouldIncludeCoreTypes = true; + } + + if (Property->IsA()) + { + if (GDK_CASTFIELD(Property)->Inner->IsA()) + { + bShouldIncludeCoreTypes = true; + } + } + } + if (bShouldIncludeCoreTypes) { Writer.PrintNewLine(); @@ -404,14 +424,16 @@ void GenerateSubobjectSchema(FComponentIdGenerator& IdGenerator, UClass* Class, // If this class is an Actor Component, it MUST have bReplicates at field ID 1. if (Group == REP_MultiClient && Class->IsChildOf()) { - TSharedPtr ExpectedReplicatesPropData = RepData[Group].FindRef(SpatialConstants::ACTOR_COMPONENT_REPLICATES_ID); + TSharedPtr ExpectedReplicatesPropData = + RepData[Group].FindRef(SpatialConstants::ACTOR_COMPONENT_REPLICATES_ID); const GDK_PROPERTY(Property)* ReplicatesProp = UActorComponent::StaticClass()->FindPropertyByName("bReplicates"); if (!(ExpectedReplicatesPropData.IsValid() && ExpectedReplicatesPropData->Property == ReplicatesProp)) { - UE_LOG(LogSchemaGenerator, Error, TEXT("Did not find ActorComponent->bReplicates at field %d for class %s. Modifying the base Actor Component class is currently not supported."), - SpatialConstants::ACTOR_COMPONENT_REPLICATES_ID, - *Class->GetName()); + UE_LOG(LogSchemaGenerator, Error, + TEXT("Did not find ActorComponent->bReplicates at field %d for class %s. Modifying the base Actor Component class " + "is currently not supported."), + SpatialConstants::ACTOR_COMPONENT_REPLICATES_ID, *Class->GetName()); } } @@ -420,14 +442,11 @@ void GenerateSubobjectSchema(FComponentIdGenerator& IdGenerator, UClass* Class, Writer.Indent(); for (auto& RepProp : RepData[Group]) { - WriteSchemaRepField(Writer, - RepProp.Value, - RepProp.Value->ReplicationData->Handle); + WriteSchemaRepField(Writer, RepProp.Value, RepProp.Value->ReplicationData->Handle); } Writer.Outdent().Print("}"); } - FCmdHandlePropertyMap HandoverData = GetFlatHandoverData(TypeInfo); if (HandoverData.Num() > 0) { Writer.PrintNewLine(); @@ -438,9 +457,7 @@ void GenerateSubobjectSchema(FComponentIdGenerator& IdGenerator, UClass* Class, for (auto& Prop : HandoverData) { FieldCounter++; - WriteSchemaHandoverField(Writer, - Prop.Value, - FieldCounter); + WriteSchemaHandoverField(Writer, Prop.Value, FieldCounter); } Writer.Outdent().Print("}"); } @@ -456,9 +473,11 @@ void GenerateSubobjectSchema(FComponentIdGenerator& IdGenerator, UClass* Class, if (ExistingSchemaData != nullptr && !ExistingSchemaData->GeneratedSchemaName.IsEmpty() && ExistingSchemaData->GeneratedSchemaName != ClassPathToSchemaName[Class->GetPathName()]) { - UE_LOG(LogSchemaGenerator, Error, TEXT("Saved generated schema name does not match in-memory version for class %s - schema %s : %s"), - *Class->GetPathName(), *ExistingSchemaData->GeneratedSchemaName, *ClassPathToSchemaName[Class->GetPathName()]); - UE_LOG(LogSchemaGenerator, Error, TEXT("Schema generation may have resulted in component name clash, recommend you perform a full schema generation")); + UE_LOG(LogSchemaGenerator, Error, + TEXT("Saved generated schema name does not match in-memory version for class %s - schema %s : %s"), *Class->GetPathName(), + *ExistingSchemaData->GeneratedSchemaName, *ClassPathToSchemaName[Class->GetPathName()]); + UE_LOG(LogSchemaGenerator, Error, + TEXT("Schema generation may have resulted in component name clash, recommend you perform a full schema generation")); } for (uint32 i = 1; i <= DynamicComponentsPerClass; i++) @@ -541,7 +560,7 @@ void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSha // Copyright (c) Improbable Worlds Ltd, All Rights Reserved // Note that this file has been generated automatically package unreal.generated.{0};)""", - *ClassPathToSchemaName[Class->GetPathName()].ToLower()); + *ClassPathToSchemaName[Class->GetPathName()].ToLower()); // Will always be included since AActor has replicated pointers to other actors Writer.PrintNewLine(); @@ -560,7 +579,7 @@ void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSha continue; } - // If this class is an Actor, it MUST have bTearOff at field ID 3. + // If this class is an Actor, it MUST have bTearOff at field ID 3. if (Group == REP_MultiClient && Class->IsChildOf()) { TSharedPtr ExpectedReplicatesPropData = RepData[Group].FindRef(SpatialConstants::ACTOR_TEAROFF_ID); @@ -568,9 +587,10 @@ void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSha if (!(ExpectedReplicatesPropData.IsValid() && ExpectedReplicatesPropData->Property == ReplicatesProp)) { - UE_LOG(LogSchemaGenerator, Error, TEXT("Did not find Actor->bTearOff at field %d for class %s. Modifying the base Actor class is currently not supported."), - SpatialConstants::ACTOR_TEAROFF_ID, - *Class->GetName()); + UE_LOG(LogSchemaGenerator, Error, + TEXT("Did not find Actor->bTearOff at field %d for class %s. Modifying the base Actor class is currently not " + "supported."), + SpatialConstants::ACTOR_TEAROFF_ID, *Class->GetName()); } } @@ -586,7 +606,8 @@ void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSha Writer.PrintNewLine(); - Writer.Printf("component {0} {", *SchemaReplicatedDataName(Group, Class)); + FString ComponentName = SchemaReplicatedDataName(Group, Class); + Writer.Printf("component {0} {", *ComponentName); Writer.Indent(); Writer.Printf("id = {0};", ComponentId); @@ -596,9 +617,7 @@ void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSha for (auto& RepProp : RepData[Group]) { FieldCounter++; - WriteSchemaRepField(Writer, - RepProp.Value, - RepProp.Value->ReplicationData->Handle); + WriteSchemaRepField(Writer, RepProp.Value, RepProp.Value->ReplicationData->Handle); } Writer.Outdent().Print("}"); @@ -620,7 +639,8 @@ void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSha Writer.PrintNewLine(); // Handover (server to server) replicated properties. - Writer.Printf("component {0} {", *SchemaHandoverDataName(Class)); + FString ComponentName = SchemaHandoverDataName(Class); + Writer.Printf("component {0} {", *ComponentName); Writer.Indent(); Writer.Printf("id = {0};", ComponentId); @@ -630,14 +650,13 @@ void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSha for (auto& Prop : HandoverData) { FieldCounter++; - WriteSchemaHandoverField(Writer, - Prop.Value, - FieldCounter); + WriteSchemaHandoverField(Writer, Prop.Value, FieldCounter); } Writer.Outdent().Print("}"); } - GenerateSubobjectSchemaForActor(IdGenerator, Class, TypeInfo, SchemaPath, ActorSchemaData, ActorClassPathToSchema.Find(Class->GetPathName())); + GenerateSubobjectSchemaForActor(IdGenerator, Class, TypeInfo, SchemaPath, ActorSchemaData, + ActorClassPathToSchema.Find(Class->GetPathName())); ActorClassPathToSchema.Add(Class->GetPathName(), ActorSchemaData); @@ -649,8 +668,10 @@ void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSha { 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); + 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); } NetCullDistanceToComponentId.Add(NCD, 0); @@ -672,8 +693,10 @@ void GenerateRPCEndpointsSchema(FString SchemaPath) Writer.Print("import \"unreal/gdk/core_types.schema\";"); Writer.Print("import \"unreal/gdk/rpc_payload.schema\";"); - 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("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 }, {}); Writer.WriteToFile(FString::Printf(TEXT("%srpc_endpoints.schema"), *SchemaPath)); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp index 35e5434a27..33e3d71257 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp @@ -18,6 +18,7 @@ #include "Misc/FileHelper.h" #include "Misc/MessageDialog.h" #include "Misc/MonitoredProcess.h" +#include "Runtime/Launch/Resources/Version.h" #include "Templates/SharedPointer.h" #include "UObject/UObjectIterator.h" @@ -30,6 +31,7 @@ #include "SpatialGDKEditorSettings.h" #include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesModule.h" +#include "SpatialGDKSettings.h" #include "TypeStructure.h" #include "UObject/StrongObjectPtr.h" #include "Utils/CodeWriter.h" @@ -59,7 +61,8 @@ TMap> PotentialSchemaNameCollisions; // QBI TMap NetCullDistanceToComponentId; -const FString RelativeSchemaDatabaseFilePath = FPaths::SetExtension(FPaths::Combine(FPaths::ProjectContentDir(), SpatialConstants::SCHEMA_DATABASE_FILE_PATH), FPackageName::GetAssetPackageExtension()); +const FString RelativeSchemaDatabaseFilePath = FPaths::SetExtension( + FPaths::Combine(FPaths::ProjectContentDir(), SpatialConstants::SCHEMA_DATABASE_FILE_PATH), FPackageName::GetAssetPackageExtension()); namespace SpatialGDKEditor { @@ -93,13 +96,17 @@ bool CheckSchemaNameValidity(const FString& Name, const FString& Identifier, con { if (Name.IsEmpty()) { - UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("%s %s is empty after removing non-alphanumeric characters, schema not generated."), *Category, *Identifier); + UE_LOG(LogSpatialGDKSchemaGenerator, Error, + TEXT("%s %s is empty after removing non-alphanumeric characters, schema not generated."), *Category, *Identifier); return false; } if (FChar::IsDigit(Name[0])) { - UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("%s names should not start with digits. %s %s (%s) has leading digits (potentially after removing non-alphanumeric characters), schema not generated."), *Category, *Category, *Name, *Identifier); + UE_LOG(LogSpatialGDKSchemaGenerator, Error, + TEXT("%s names should not start with digits. %s %s (%s) has leading digits (potentially after removing non-alphanumeric " + "characters), schema not generated."), + *Category, *Category, *Name, *Identifier); return false; } @@ -124,8 +131,11 @@ void CheckIdentifierNameValidity(TSharedPtr TypeInfo, bool& bOutSuc if (TSharedPtr* ExistingReplicatedProperty = SchemaReplicatedDataNames.Find(NextSchemaReplicatedDataName)) { - UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Replicated property name collision after removing non-alphanumeric characters, schema not generated. Name '%s' collides for '%s' and '%s'"), - *NextSchemaReplicatedDataName, *ExistingReplicatedProperty->Get()->Property->GetPathName(), *RepProp.Value->Property->GetPathName()); + UE_LOG(LogSpatialGDKSchemaGenerator, Error, + TEXT("Replicated property name collision after removing non-alphanumeric characters, schema not generated. Name " + "'%s' collides for '%s' and '%s'"), + *NextSchemaReplicatedDataName, *ExistingReplicatedProperty->Get()->Property->GetPathName(), + *RepProp.Value->Property->GetPathName()); bOutSuccess = false; } else @@ -149,8 +159,11 @@ void CheckIdentifierNameValidity(TSharedPtr TypeInfo, bool& bOutSuc if (TSharedPtr* ExistingHandoverData = SchemaHandoverDataNames.Find(NextSchemaHandoverDataName)) { - UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Handover data name collision after removing non-alphanumeric characters, schema not generated. Name '%s' collides for '%s' and '%s'"), - *NextSchemaHandoverDataName, *ExistingHandoverData->Get()->Property->GetPathName(), *Prop.Value->Property->GetPathName()); + UE_LOG(LogSpatialGDKSchemaGenerator, Error, + TEXT("Handover data name collision after removing non-alphanumeric characters, schema not generated. Name '%s' collides " + "for '%s' and '%s'"), + *NextSchemaHandoverDataName, *ExistingHandoverData->Get()->Property->GetPathName(), + *Prop.Value->Property->GetPathName()); bOutSuccess = false; } else @@ -174,8 +187,10 @@ void CheckIdentifierNameValidity(TSharedPtr TypeInfo, bool& bOutSuc if (TSharedPtr* ExistingSubobject = SchemaSubobjectNames.Find(NextSchemaSubobjectName)) { - UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Subobject name collision after removing non-alphanumeric characters, schema not generated. Name '%s' collides for '%s' and '%s'"), - *NextSchemaSubobjectName, *ExistingSubobject->Get()->Object->GetPathName(), *SubobjectTypeInfo->Object->GetPathName()); + UE_LOG(LogSpatialGDKSchemaGenerator, Error, + TEXT("Subobject name collision after removing non-alphanumeric characters, schema not generated. Name '%s' collides for " + "'%s' and '%s'"), + *NextSchemaSubobjectName, *ExistingSubobject->Get()->Object->GetPathName(), *SubobjectTypeInfo->Object->GetPathName()); bOutSuccess = false; } else @@ -230,8 +245,9 @@ bool ValidateIdentifierNames(TArray>& TypeInfos) { if (Collision.Value.Num() > 1) { - UE_LOG(LogSpatialGDKSchemaGenerator, Display, TEXT("Class name collision after removing non-alphanumeric characters. Name '%s' collides for classes [%s]"), - *Collision.Key, *FString::Join(Collision.Value, TEXT(", "))); + UE_LOG(LogSpatialGDKSchemaGenerator, Display, + TEXT("Class name collision after removing non-alphanumeric characters. Name '%s' collides for classes [%s]"), + *Collision.Key, *FString::Join(Collision.Value, TEXT(", "))); } } @@ -244,10 +260,11 @@ bool ValidateIdentifierNames(TArray>& TypeInfos) return bSuccess; } -void GenerateSchemaFromClasses(const TArray>& TypeInfos, const FString& CombinedSchemaPath, FComponentIdGenerator& IdGenerator) +void GenerateSchemaFromClasses(const TArray>& TypeInfos, const FString& CombinedSchemaPath, + FComponentIdGenerator& IdGenerator) { // Generate the actual schema. - FScopedSlowTask Progress((float)TypeInfos.Num(), LOCTEXT("GenerateSchemaFromClasses", "Generating Schema...")); + FScopedSlowTask Progress((float)TypeInfos.Num(), LOCTEXT("GenerateSchemaFromClasses", "Generating schema...")); for (const auto& TypeInfo : TypeInfos) { Progress.EnterProgressFrame(1.f); @@ -257,9 +274,10 @@ void GenerateSchemaFromClasses(const TArray>& TypeInfos, void WriteLevelComponent(FCodeWriter& Writer, const FString& LevelName, Worker_ComponentId ComponentId, const FString& ClassPath) { + FString ComponentName = UnrealNameToSchemaComponentName(LevelName); Writer.PrintNewLine(); Writer.Printf("// {0}", *ClassPath); - Writer.Printf("component {0} {", *UnrealNameToSchemaComponentName(LevelName)); + Writer.Printf("component {0} {", *ComponentName); Writer.Indent(); Writer.Printf("id = {0};", ComponentId); Writer.Outdent().Print("}"); @@ -325,7 +343,6 @@ void GenerateSchemaForSublevels(const FString& SchemaOutputPath, const TMultiMap LevelPathToComponentId.Add(LevelPaths[i].ToString(), ComponentId); } WriteLevelComponent(Writer, FString::Printf(TEXT("%sInd%d"), *LevelNameString, i), ComponentId, LevelPaths[i].ToString()); - } } else @@ -380,11 +397,14 @@ void GenerateSchemaForNCDs(const FString& SchemaOutputPath) NCDComponent.Value = IdGenerator.Next(); } + FString SchemaComponentName = UnrealNameToSchemaComponentName(ComponentName); + Worker_ComponentId ComponentId = NCDComponent.Value; + Writer.PrintNewLine(); Writer.Printf("// distance {0}", NCDComponent.Key); - Writer.Printf("component {0} {", *UnrealNameToSchemaComponentName(ComponentName)); + Writer.Printf("component {0} {", *SchemaComponentName); Writer.Indent(); - Writer.Printf("id = {0};", NCDComponent.Value); + Writer.Printf("id = {0};", ComponentId); Writer.Outdent().Print("}"); } @@ -395,7 +415,8 @@ void GenerateSchemaForNCDs(const FString& SchemaOutputPath) FString GenerateIntermediateDirectory() { - const FString CombinedIntermediatePath = FPaths::Combine(*FPaths::GetPath(FPaths::GetProjectFilePath()), TEXT("Intermediate/Improbable/"), *FGuid::NewGuid().ToString(), TEXT("/")); + const FString CombinedIntermediatePath = FPaths::Combine(*FPaths::GetPath(FPaths::GetProjectFilePath()), + TEXT("Intermediate/Improbable/"), *FGuid::NewGuid().ToString(), TEXT("/")); FString AbsoluteCombinedIntermediatePath = FPaths::ConvertRelativePathToFull(CombinedIntermediatePath); FPlatformFileManager::Get().GetPlatformFile().CreateDirectoryTree(*AbsoluteCombinedIntermediatePath); @@ -408,15 +429,13 @@ TMap CreateComponentIdToClassPathMap() for (const auto& ActorSchemaData : ActorClassPathToSchema) { - ForAllSchemaComponentTypes([&](ESchemaComponentType Type) - { + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { ComponentIdToClassPath.Add(ActorSchemaData.Value.SchemaComponents[Type], ActorSchemaData.Key); }); for (const auto& SubobjectSchemaData : ActorSchemaData.Value.SubobjectData) { - ForAllSchemaComponentTypes([&](ESchemaComponentType Type) - { + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { ComponentIdToClassPath.Add(SubobjectSchemaData.Value.SchemaComponents[Type], SubobjectSchemaData.Value.ClassPath); }); } @@ -426,8 +445,7 @@ TMap CreateComponentIdToClassPathMap() { for (const auto& DynamicSubobjectData : SubobjectSchemaData.Value.DynamicSubobjectComponents) { - ForAllSchemaComponentTypes([&](ESchemaComponentType Type) - { + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { ComponentIdToClassPath.Add(DynamicSubobjectData.SchemaComponents[Type], SubobjectSchemaData.Key); }); } @@ -438,15 +456,388 @@ TMap CreateComponentIdToClassPathMap() return ComponentIdToClassPath; } -bool SaveSchemaDatabase(const FString& PackagePath) +FString GetComponentSetNameBySchemaType(ESchemaComponentType SchemaType) +{ + switch (SchemaType) + { + case SCHEMA_Data: + return SpatialConstants::DATA_COMPONENT_SET_NAME; + case SCHEMA_OwnerOnly: + return SpatialConstants::OWNER_ONLY_COMPONENT_SET_NAME; + case SCHEMA_Handover: + return SpatialConstants::HANDOVER_COMPONENT_SET_NAME; + default: + UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Could not return component set name. Schema component type was invalid: %d"), + SchemaType); + return FString(); + } +} + +Worker_ComponentId GetComponentSetIdBySchemaType(ESchemaComponentType SchemaType) +{ + switch (SchemaType) + { + case SCHEMA_Data: + return SpatialConstants::DATA_COMPONENT_SET_ID; + case SCHEMA_OwnerOnly: + return SpatialConstants::OWNER_ONLY_COMPONENT_SET_ID; + case SCHEMA_Handover: + return SpatialConstants::HANDOVER_COMPONENT_SET_ID; + default: + UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Could not return component set ID. Schema component type was invalid: %d"), + SchemaType); + return SpatialConstants::INVALID_COMPONENT_ID; + } +} + +FString GetComponentSetOutputPathBySchemaType(ESchemaComponentType SchemaType) +{ + const FString ComponentSetName = GetComponentSetNameBySchemaType(SchemaType); + return FString::Printf(TEXT("%sComponentSets/%s.schema"), *GetDefault()->GetGeneratedSchemaOutputFolder(), + *ComponentSetName); +} + +void WriteServerAuthorityComponentSet(const USchemaDatabase* SchemaDatabase, TArray& ServerAuthoritativeComponentIds) +{ + const FString SchemaOutputPath = GetDefault()->GetGeneratedSchemaOutputFolder(); + + FCodeWriter Writer; + Writer.Printf(R"""( + // Copyright (c) Improbable Worlds Ltd, All Rights Reserved + // Note that this file has been generated automatically + package unreal.generated;)"""); + Writer.PrintNewLine(); + + // Write all import statements. + { + // Well-known SpatialOS and handwritten GDK schema files. + for (const auto& WellKnownSchemaImport : SpatialConstants::ServerAuthorityWellKnownSchemaImports) + { + Writer.Printf("import \"{0}\";", WellKnownSchemaImport); + } + + const FString IncludePath = TEXT("unreal/generated"); + for (const auto& GeneratedActorClass : SchemaDatabase->ActorClassPathToSchema) + { + const FString ActorClassName = UnrealNameToSchemaName(GeneratedActorClass.Value.GeneratedSchemaName); + Writer.Printf("import \"{0}/{1}.schema\";", IncludePath, ActorClassName); + if (GeneratedActorClass.Value.SubobjectData.Num() > 0) + { + Writer.Printf("import \"{0}/{1}Components.schema\";", IncludePath, ActorClassName); + } + } + + for (const auto& GeneratedSubObjectClass : SchemaDatabase->SubobjectClassPathToSchema) + { + const FString SubObjectClassName = UnrealNameToSchemaName(GeneratedSubObjectClass.Value.GeneratedSchemaName); + Writer.Printf("import \"{0}/Subobjects/{1}.schema\";", IncludePath, SubObjectClassName); + } + } + + Writer.PrintNewLine(); + Writer.Printf("component_set {0} {", SpatialConstants::SERVER_AUTH_COMPONENT_SET_NAME).Indent(); + Writer.Printf("id = {0};", SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID); + Writer.Printf("components = [").Indent(); + + // Write all components. + { + // Well-known SpatialOS and handwritten GDK components. + for (const auto& WellKnownComponent : SpatialConstants::ServerAuthorityWellKnownComponents) + { + Writer.Printf("{0},", WellKnownComponent.Value); + } + + // NCDs. + for (auto& NCDComponent : NetCullDistanceToComponentId) + { + const FString NcdComponentName = FString::Printf(TEXT("NetCullDistanceSquared%lld"), static_cast(NCDComponent.Key)); + Writer.Printf("unreal.ncdcomponents.{0},", NcdComponentName); + } + + for (const auto& GeneratedActorClass : SchemaDatabase->ActorClassPathToSchema) + { + // Actor components. + const FString& ActorClassName = UnrealNameToSchemaComponentName(GeneratedActorClass.Value.GeneratedSchemaName); + ForAllSchemaComponentTypes([&](ESchemaComponentType SchemaType) { + const Worker_ComponentId ComponentId = GeneratedActorClass.Value.SchemaComponents[SchemaType]; + ServerAuthoritativeComponentIds.Push(ComponentId); + if (ComponentId != 0) + { + switch (SchemaType) + { + case SCHEMA_Data: + Writer.Printf("unreal.generated.{0}.{1},", ActorClassName.ToLower(), ActorClassName); + break; + case SCHEMA_OwnerOnly: + Writer.Printf("unreal.generated.{0}.{1}OwnerOnly,", ActorClassName.ToLower(), ActorClassName); + break; + case SCHEMA_Handover: + Writer.Printf("unreal.generated.{0}.{1}Handover,", ActorClassName.ToLower(), ActorClassName); + break; + default: + break; + } + } + }); + + // Actor static subobjects. + for (const auto& ActorSubObjectData : GeneratedActorClass.Value.SubobjectData) + { + const FString ActorSubObjectName = UnrealNameToSchemaComponentName(ActorSubObjectData.Value.Name.ToString()); + ForAllSchemaComponentTypes([&](ESchemaComponentType SchemaType) { + const Worker_ComponentId& ComponentId = ActorSubObjectData.Value.SchemaComponents[SchemaType]; + ServerAuthoritativeComponentIds.Push(ComponentId); + if (ComponentId != 0) + { + switch (SchemaType) + { + case SCHEMA_Data: + Writer.Printf("unreal.generated.{0}.subobjects.{1},", ActorClassName.ToLower(), ActorSubObjectName); + break; + case SCHEMA_OwnerOnly: + Writer.Printf("unreal.generated.{0}.subobjects.{1}OwnerOnly,", ActorClassName.ToLower(), ActorSubObjectName); + break; + case SCHEMA_Handover: + Writer.Printf("unreal.generated.{0}.subobjects.{1}Handover,", ActorClassName.ToLower(), ActorSubObjectName); + break; + default: + break; + } + } + }); + } + } + + // Dynamic subobjects. + for (const auto& GeneratedSubObjectClass : SchemaDatabase->SubobjectClassPathToSchema) + { + const FString& SubObjectClassName = UnrealNameToSchemaComponentName(GeneratedSubObjectClass.Value.GeneratedSchemaName); + for (auto SubObjectNumber = 0; SubObjectNumber < GeneratedSubObjectClass.Value.DynamicSubobjectComponents.Num(); + ++SubObjectNumber) + { + const FDynamicSubobjectSchemaData& SubObjectSchemaData = + GeneratedSubObjectClass.Value.DynamicSubobjectComponents[SubObjectNumber]; + ForAllSchemaComponentTypes([&](ESchemaComponentType SchemaType) { + const Worker_ComponentId& ComponentId = SubObjectSchemaData.SchemaComponents[SchemaType]; + ServerAuthoritativeComponentIds.Push(ComponentId); + if (ComponentId != 0) + { + switch (SchemaType) + { + case SCHEMA_Data: + Writer.Printf("unreal.generated.{0}Dynamic{1},", SubObjectClassName, SubObjectNumber + 1); + break; + case SCHEMA_OwnerOnly: + Writer.Printf("unreal.generated.{0}OwnerOnlyDynamic{1},", SubObjectClassName, SubObjectNumber + 1); + break; + case SCHEMA_Handover: + Writer.Printf("unreal.generated.{0}HandoverDynamic{1},", SubObjectClassName, SubObjectNumber + 1); + break; + default: + break; + } + } + }); + } + } + } + + Writer.RemoveTrailingComma(); + + Writer.Outdent().Print("];"); + Writer.Outdent().Print("}"); + + Writer.WriteToFile(FString::Printf(TEXT("%sComponentSets/ServerAuthoritativeComponentSet.schema"), *SchemaOutputPath)); +} + +void WriteClientAuthorityComponentSet() +{ + const FString SchemaOutputPath = GetDefault()->GetGeneratedSchemaOutputFolder(); + + FCodeWriter Writer; + Writer.Printf(R"""( + // Copyright (c) Improbable Worlds Ltd, All Rights Reserved + // Note that this file has been generated automatically + package unreal.generated;)"""); + Writer.PrintNewLine(); + + // Write all import statements. + for (const auto& WellKnownSchemaImport : SpatialConstants::ClientAuthorityWellKnownSchemaImports) + { + Writer.Printf("import \"{0}\";", WellKnownSchemaImport); + } + + Writer.PrintNewLine(); + Writer.Printf("component_set {0} {", SpatialConstants::CLIENT_AUTH_COMPONENT_SET_NAME).Indent(); + Writer.Printf("id = {0};", SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID); + Writer.Printf("components = [").Indent(); + + // Write all import components. + for (const auto& WellKnownComponent : SpatialConstants::ClientAuthorityWellKnownComponents) + { + Writer.Printf("{0},", WellKnownComponent.Value); + } + + Writer.RemoveTrailingComma(); + + Writer.Outdent().Print("];"); + Writer.Outdent().Print("}"); + + Writer.WriteToFile(FString::Printf(TEXT("%sComponentSets/ClientAuthoritativeComponentSet.schema"), *SchemaOutputPath)); +} + +void WriteComponentSetBySchemaType(const USchemaDatabase* SchemaDatabase, ESchemaComponentType SchemaType) { - UPackage *Package = CreatePackage(nullptr, *PackagePath); + FCodeWriter Writer; + Writer.Printf(R"""( + // Copyright (c) Improbable Worlds Ltd, All Rights Reserved + // Note that this file has been generated automatically + package unreal.generated;)"""); + Writer.PrintNewLine(); + + // Write all import statements. + { + const FString IncludePath = TEXT("unreal/generated"); + for (const auto& GeneratedActorClass : SchemaDatabase->ActorClassPathToSchema) + { + const FString ActorClassName = UnrealNameToSchemaName(GeneratedActorClass.Value.GeneratedSchemaName); + if (GeneratedActorClass.Value.SchemaComponents[SchemaType] != 0) + { + Writer.Printf("import \"{0}/{1}.schema\";", IncludePath, ActorClassName); + } + for (const auto& SubObjectData : GeneratedActorClass.Value.SubobjectData) + { + if (SubObjectData.Value.SchemaComponents[SchemaType] != 0) + { + Writer.Printf("import \"{0}/{1}Components.schema\";", IncludePath, ActorClassName); + break; + } + } + } + for (const auto& GeneratedSubObjectClass : SchemaDatabase->SubobjectClassPathToSchema) + { + const FString SubObjectClassName = UnrealNameToSchemaName(GeneratedSubObjectClass.Value.GeneratedSchemaName); + for (const auto& SubObjectData : GeneratedSubObjectClass.Value.DynamicSubobjectComponents) + { + if (SubObjectData.SchemaComponents[SchemaType] != 0) + { + Writer.Printf("import \"{0}/Subobjects/{1}.schema\";", IncludePath, SubObjectClassName); + break; + } + } + } + } + + Writer.PrintNewLine(); + Writer.Printf("component_set {0} {", GetComponentSetNameBySchemaType(SchemaType)).Indent(); + Writer.Printf("id = {0};", GetComponentSetIdBySchemaType(SchemaType)); + Writer.Printf("components = [").Indent(); + + // Write all components. + { + for (const auto& GeneratedActorClass : SchemaDatabase->ActorClassPathToSchema) + { + // Actor components. + const FString& ActorClassName = UnrealNameToSchemaComponentName(GeneratedActorClass.Value.GeneratedSchemaName); + if (GeneratedActorClass.Value.SchemaComponents[SchemaType] != 0) + { + switch (SchemaType) + { + case SCHEMA_Data: + Writer.Printf("unreal.generated.{0}.{1},", ActorClassName.ToLower(), ActorClassName); + break; + case SCHEMA_OwnerOnly: + Writer.Printf("unreal.generated.{0}.{1}OwnerOnly,", ActorClassName.ToLower(), ActorClassName); + break; + case SCHEMA_Handover: + Writer.Printf("unreal.generated.{0}.{1}Handover,", ActorClassName.ToLower(), ActorClassName); + break; + default: + break; + } + } + // Actor static subobjects. + for (const auto& ActorSubObjectData : GeneratedActorClass.Value.SubobjectData) + { + const FString ActorSubObjectName = UnrealNameToSchemaComponentName(ActorSubObjectData.Value.Name.ToString()); + if (ActorSubObjectData.Value.SchemaComponents[SchemaType] != 0) + { + switch (SchemaType) + { + case SCHEMA_Data: + Writer.Printf("unreal.generated.{0}.subobjects.{1},", ActorClassName.ToLower(), ActorSubObjectName); + break; + case SCHEMA_OwnerOnly: + Writer.Printf("unreal.generated.{0}.subobjects.{1}OwnerOnly,", ActorClassName.ToLower(), ActorSubObjectName); + break; + case SCHEMA_Handover: + Writer.Printf("unreal.generated.{0}.subobjects.{1}Handover,", ActorClassName.ToLower(), ActorSubObjectName); + break; + default: + break; + } + } + } + } + // Dynamic subobjects. + for (const auto& GeneratedSubObjectClass : SchemaDatabase->SubobjectClassPathToSchema) + { + const FString& SubObjectClassName = UnrealNameToSchemaComponentName(GeneratedSubObjectClass.Value.GeneratedSchemaName); + for (auto SubObjectNumber = 0; SubObjectNumber < GeneratedSubObjectClass.Value.DynamicSubobjectComponents.Num(); + ++SubObjectNumber) + { + const FDynamicSubobjectSchemaData& SubObjectSchemaData = + GeneratedSubObjectClass.Value.DynamicSubobjectComponents[SubObjectNumber]; + if (SubObjectSchemaData.SchemaComponents[SchemaType] != 0) + { + switch (SchemaType) + { + case SCHEMA_Data: + Writer.Printf("unreal.generated.{0}Dynamic{1},", SubObjectClassName, SubObjectNumber + 1); + break; + case SCHEMA_OwnerOnly: + Writer.Printf("unreal.generated.{0}OwnerOnlyDynamic{1},", SubObjectClassName, SubObjectNumber + 1); + break; + case SCHEMA_Handover: + Writer.Printf("unreal.generated.{0}HandoverDynamic{1},", SubObjectClassName, SubObjectNumber + 1); + break; + default: + break; + } + } + } + } + } + + Writer.RemoveTrailingComma(); - 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; }); + Writer.Outdent().Print("];"); + Writer.Outdent().Print("}"); + + const FString OutputPath = GetComponentSetOutputPathBySchemaType(SchemaType); + Writer.WriteToFile(OutputPath); +} - USchemaDatabase* SchemaDatabase = NewObject(Package, USchemaDatabase::StaticClass(), FName("SchemaDatabase"), EObjectFlags::RF_Public | EObjectFlags::RF_Standalone); +USchemaDatabase* InitialiseSchemaDatabase(const FString& PackagePath) +{ +#if ENGINE_MINOR_VERSION >= 26 + UPackage* Package = CreatePackage(*PackagePath); +#else + UPackage* Package = CreatePackage(nullptr, *PackagePath); +#endif + + 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; @@ -466,16 +857,38 @@ bool SaveSchemaDatabase(const FString& PackagePath) SchemaDatabase->LevelComponentIds.Reset(LevelPathToComponentId.Num()); LevelPathToComponentId.GenerateValueArray(SchemaDatabase->LevelComponentIds); + SchemaDatabase->ComponentSetIdToComponentIds.Reset(); + for (const auto& WellKnownComponent : SpatialConstants::ServerAuthorityWellKnownComponents) + { + SchemaDatabase->ComponentSetIdToComponentIds.FindOrAdd(SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) + .ComponentIDs.Push(WellKnownComponent.Key); + } + for (const auto& WellKnownComponent : SpatialConstants::ClientAuthorityWellKnownComponents) + { + SchemaDatabase->ComponentSetIdToComponentIds.FindOrAdd(SpatialConstants::CLIENT_AUTH_COMPONENT_SET_ID) + .ComponentIDs.Push(WellKnownComponent.Key); + } + SchemaDatabase->ComponentSetIdToComponentIds.FindOrAdd(SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID) + .ComponentIDs.Append(SpatialConstants::KnownEntityAuthorityComponents); + SchemaDatabase->ComponentSetIdToComponentIds.FindOrAdd(SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) + .ComponentIDs.Append(NetCullDistanceComponentIds); + + SchemaDatabase->SchemaDatabaseVersion = ESchemaDatabaseVersion::LatestVersion; + + return SchemaDatabase; +} +bool SaveSchemaDatabase(USchemaDatabase* SchemaDatabase) +{ FString CompiledSchemaDir = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("build/assembly/schema")); // Generate hash { - SchemaDatabase->SchemaDescriptorHash = 0; - FString DescriptorPath = FPaths::Combine(CompiledSchemaDir, TEXT("schema.descriptor")); + SchemaDatabase->SchemaBundleHash = 0; + FString SchemaBundlePath = FPaths::Combine(CompiledSchemaDir, TEXT("schema.sb")); IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); - TUniquePtr FileHandle(PlatformFile.OpenRead(DescriptorPath.GetCharArray().GetData())); + TUniquePtr FileHandle(PlatformFile.OpenRead(SchemaBundlePath.GetCharArray().GetData())); if (FileHandle) { // Create our byte buffer @@ -484,17 +897,20 @@ bool SaveSchemaDatabase(const FString& PackagePath) 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); + SchemaDatabase->SchemaBundleHash = CityHash32(reinterpret_cast(ByteArray.Get()), FileSize); + UE_LOG(LogSpatialGDKSchemaGenerator, Display, TEXT("Generated schema bundle hash for database %u"), + SchemaDatabase->SchemaBundleHash); } else { - UE_LOG(LogSpatialGDKSchemaGenerator, Warning, TEXT("Failed to fully read schema.descriptor. Schema not saved. Location: %s"), *DescriptorPath); + UE_LOG(LogSpatialGDKSchemaGenerator, Warning, TEXT("Failed to fully read schema.sb. Schema not saved. Location: %s"), + *SchemaBundlePath); } } else { - UE_LOG(LogSpatialGDKSchemaGenerator, Warning, TEXT("Failed to open schema.descriptor generated by the schema compiler! Location: %s"), *DescriptorPath); + UE_LOG(LogSpatialGDKSchemaGenerator, Warning, TEXT("Failed to open schema.sb generated by the schema compiler! Location: %s"), + *SchemaBundlePath); } } @@ -504,19 +920,26 @@ bool SaveSchemaDatabase(const FString& PackagePath) // NOTE: UPackage::GetMetaData() has some code where it will auto-create the metadata if it's missing // UPackage::SavePackage() calls UPackage::GetMetaData() at some point, and will cause an exception to get thrown // if the metadata auto-creation branch needs to be taken. This is the case when generating the schema from the - // command line, so we just pre-empt it here. + // command line, so we just preempt it here. + UPackage* Package = SchemaDatabase->GetOutermost(); + const FString& PackagePath = Package->GetPathName(); Package->GetMetaData(); FString FilePath = FString::Printf(TEXT("%s%s"), *PackagePath, *FPackageName::GetAssetPackageExtension()); - bool bSuccess = UPackage::SavePackage(Package, SchemaDatabase, EObjectFlags::RF_Public | EObjectFlags::RF_Standalone, *FPackageName::LongPackageNameToFilename(PackagePath, FPackageName::GetAssetPackageExtension()), GError, nullptr, false, true, SAVE_NoError); + bool bSuccess = UPackage::SavePackage(Package, SchemaDatabase, EObjectFlags::RF_Public | EObjectFlags::RF_Standalone, + *FPackageName::LongPackageNameToFilename(PackagePath, FPackageName::GetAssetPackageExtension()), + GError, nullptr, false, true, SAVE_NoError); if (!bSuccess) { FString FullPath = FPaths::ConvertRelativePathToFull(FilePath); FPaths::MakePlatformFilename(FullPath); - FMessageDialog::Debugf(FText::Format(LOCTEXT("SchemaDatabaseLocked_Error", "Unable to save Schema Database to '{0}'! The file may be locked by another process."), FText::FromString(FullPath))); + FMessageDialog::Debugf(FText::Format( + LOCTEXT("SchemaDatabaseLocked_Error", "Unable to save schema database to '{0}'! The file may be locked by another process."), + FText::FromString(FullPath))); return false; } + return true; } @@ -524,13 +947,15 @@ bool IsSupportedClass(const UClass* SupportedClass) { if (!IsValid(SupportedClass)) { - UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("[%s] Invalid Class not supported for schema gen."), *GetPathNameSafe(SupportedClass)); + UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("[%s] Invalid Class not supported for schema gen."), + *GetPathNameSafe(SupportedClass)); return false; } if (SupportedClass->IsEditorOnly()) { - UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("[%s] Editor-only Class not supported for schema gen."), *GetPathNameSafe(SupportedClass)); + UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("[%s] Editor-only Class not supported for schema gen."), + *GetPathNameSafe(SupportedClass)); return false; } @@ -538,11 +963,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)); + UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("[%s] Has NotSpatialType flag, not supported for schema gen."), + *GetPathNameSafe(SupportedClass)); } else { - UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("[%s] Has neither a SpatialType or NotSpatialType flag."), *GetPathNameSafe(SupportedClass)); + UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("[%s] Has neither a SpatialType or NotSpatialType flag."), + *GetPathNameSafe(SupportedClass)); } return false; @@ -563,7 +990,8 @@ bool IsSupportedClass(const UClass* SupportedClass) || SupportedClass->GetName().StartsWith(TEXT("PLACEHOLDER-CLASS_"), ESearchCase::CaseSensitive) || SupportedClass->GetName().StartsWith(TEXT("ORPHANED_DATA_ONLY_"), ESearchCase::CaseSensitive)) { - UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("[%s] Transient Class not supported for schema gen"), *GetPathNameSafe(SupportedClass)); + UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("[%s] Transient Class not supported for schema gen"), + *GetPathNameSafe(SupportedClass)); return false; } @@ -571,12 +999,12 @@ bool IsSupportedClass(const UClass* SupportedClass) // Avoid processing classes contained in Directories to Never Cook const FString& ClassPath = SupportedClass->GetPathName(); - if (DirectoriesToNeverCook.ContainsByPredicate([&ClassPath](const FDirectoryPath& Directory) + if (DirectoriesToNeverCook.ContainsByPredicate([&ClassPath](const FDirectoryPath& Directory) { + return ClassPath.StartsWith(Directory.Path); + })) { - return ClassPath.StartsWith(Directory.Path); - })) - { - UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("[%s] Inside Directory to never cook for schema gen"), *GetPathNameSafe(SupportedClass)); + UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("[%s] Inside Directory to never cook for schema gen"), + *GetPathNameSafe(SupportedClass)); return false; } @@ -584,7 +1012,6 @@ bool IsSupportedClass(const UClass* SupportedClass) return true; } - TSet GetAllSupportedClasses(const TArray& AllClasses) { TSet Classes; @@ -614,31 +1041,37 @@ void CopyWellKnownSchemaFiles(const FString& GDKSchemaCopyDir, const FString& Co RefreshSchemaFiles(*GDKSchemaCopyDir); if (!PlatformFile.CopyDirectoryTree(*GDKSchemaCopyDir, *GDKSchemaDir, true /*bOverwriteExisting*/)) { - UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Could not copy gdk schema to '%s'! Please make sure the directory is writeable."), *GDKSchemaCopyDir); + UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Could not copy gdk schema to '%s'! Please make sure the directory is writeable."), + *GDKSchemaCopyDir); } RefreshSchemaFiles(*CoreSDKSchemaCopyDir); if (!PlatformFile.CopyDirectoryTree(*CoreSDKSchemaCopyDir, *CoreSDKSchemaDir, true /*bOverwriteExisting*/)) { - UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Could not copy standard library schema to '%s'! Please make sure the directory is writeable."), *CoreSDKSchemaCopyDir); + UE_LOG(LogSpatialGDKSchemaGenerator, Error, + TEXT("Could not copy standard library schema to '%s'! Please make sure the directory is writeable."), *CoreSDKSchemaCopyDir); } } -bool RefreshSchemaFiles(const FString& SchemaOutputPath) +bool RefreshSchemaFiles(const FString& SchemaOutputPath, const bool bDeleteExistingSchema /*= true*/, + const bool bCreateDirectoryTree /*= true*/) { IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); - if (PlatformFile.DirectoryExists(*SchemaOutputPath)) + if (bDeleteExistingSchema && PlatformFile.DirectoryExists(*SchemaOutputPath)) { if (!PlatformFile.DeleteDirectoryRecursively(*SchemaOutputPath)) { - UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Could not clean the schema directory '%s'! Please make sure the directory and the files inside are writeable."), *SchemaOutputPath); + UE_LOG(LogSpatialGDKSchemaGenerator, Error, + TEXT("Could not clean the schema directory '%s'! Please make sure the directory and the files inside are writeable."), + *SchemaOutputPath); return false; } } - if (!PlatformFile.CreateDirectoryTree(*SchemaOutputPath)) + if (bCreateDirectoryTree && !PlatformFile.CreateDirectoryTree(*SchemaOutputPath)) { - UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Could not create schema directory '%s'! Please make sure the parent directory is writeable."), *SchemaOutputPath); + UE_LOG(LogSpatialGDKSchemaGenerator, Error, + TEXT("Could not create schema directory '%s'! Please make sure the parent directory is writeable."), *SchemaOutputPath); return false; } return true; @@ -649,8 +1082,7 @@ void ResetSchemaGeneratorState() ActorClassPathToSchema.Empty(); SubobjectClassPathToSchema.Empty(); SchemaComponentTypeToComponents.Empty(); - ForAllSchemaComponentTypes([&](ESchemaComponentType Type) - { + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { SchemaComponentTypeToComponents.Add(Type, TSet()); }); LevelPathToComponentId.Empty(); @@ -659,7 +1091,7 @@ void ResetSchemaGeneratorState() NetCullDistanceToComponentId.Empty(); } - void ResetSchemaGeneratorStateAndCleanupFolders() +void ResetSchemaGeneratorStateAndCleanupFolders() { ResetSchemaGeneratorState(); RefreshSchemaFiles(GetDefault()->GetGeneratedSchemaOutputFolder()); @@ -673,7 +1105,9 @@ bool LoadGeneratorStateFromSchemaDatabase(const FString& FileName) if (IsAssetReadOnly(FileName)) { FString AbsoluteFilePath = FPaths::ConvertRelativePathToFull(RelativeFileName); - UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Schema Generation failed: Schema Database at %s is read only. Make it writable before generating schema"), *AbsoluteFilePath); + UE_LOG(LogSpatialGDKSchemaGenerator, Error, + TEXT("Schema generation failed: Schema Database at %s is read only. Make it writable before generating schema"), + *AbsoluteFilePath); return false; } @@ -687,7 +1121,9 @@ bool LoadGeneratorStateFromSchemaDatabase(const FString& FileName) if (SchemaDatabase == nullptr) { - UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Schema Generation failed: Failed to load existing schema database. If this continues, delete the schema database and try again.")); + UE_LOG(LogSpatialGDKSchemaGenerator, Error, + TEXT("Schema generation failed: Failed to load existing schema database. If this continues, delete the schema database " + "and try again.")); return false; } @@ -695,8 +1131,10 @@ bool LoadGeneratorStateFromSchemaDatabase(const FString& FileName) SubobjectClassPathToSchema = SchemaDatabase->SubobjectClassPathToSchema; 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)); + 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; @@ -741,21 +1179,23 @@ bool DeleteSchemaDatabase(const FString& PackagePath) { FString DatabaseAssetPath = ""; - - DatabaseAssetPath = FPaths::SetExtension(FPaths::Combine(FPaths::ProjectContentDir(), PackagePath), FPackageName::GetAssetPackageExtension()); + DatabaseAssetPath = + FPaths::SetExtension(FPaths::Combine(FPaths::ProjectContentDir(), PackagePath), FPackageName::GetAssetPackageExtension()); FFileStatData StatData = FPlatformFileManager::Get().GetPlatformFile().GetStatData(*DatabaseAssetPath); if (StatData.bIsValid) { if (IsAssetReadOnly(PackagePath)) { - UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Unable to delete schema database at %s because it is read-only."), *DatabaseAssetPath); + UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Unable to delete schema database at %s because it is read-only."), + *DatabaseAssetPath); return false; } if (!FPlatformFileManager::Get().GetPlatformFile().DeleteFile(*DatabaseAssetPath)) { - // This should never run, since DeleteFile should only return false if the file does not exist which we have already checked for. + // This should never run, since DeleteFile should only return false if the file does not exist which we have already checked + // for. UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Unable to delete schema database at %s"), *DatabaseAssetPath); return false; } @@ -771,6 +1211,30 @@ bool GeneratedSchemaDatabaseExists() return PlatformFile.FileExists(*RelativeSchemaDatabaseFilePath); } +FSpatialGDKEditor::ESchemaDatabaseValidationResult ValidateSchemaDatabase() +{ + FFileStatData StatData = FPlatformFileManager::Get().GetPlatformFile().GetStatData(*RelativeSchemaDatabaseFilePath); + if (!StatData.bIsValid) + { + return FSpatialGDKEditor::NotFound; + } + + const FString DatabaseAssetPath = FPaths::SetExtension(SpatialConstants::SCHEMA_DATABASE_ASSET_PATH, TEXT(".SchemaDatabase")); + const USchemaDatabase* const SchemaDatabase = Cast(FSoftObjectPath(DatabaseAssetPath).TryLoad()); + + if (SchemaDatabase == nullptr) + { + return FSpatialGDKEditor::NotFound; + } + + if (SchemaDatabase->SchemaDatabaseVersion < ESchemaDatabaseVersion::LatestVersion) + { + return FSpatialGDKEditor::OldVersion; + } + + return FSpatialGDKEditor::Ok; +} + void ResolveClassPathToSchemaName(const FString& ClassPath, const FString& SchemaName) { if (SchemaName.IsEmpty()) @@ -801,10 +1265,10 @@ void ResetUsedNames() ResolveClassPathToSchemaName(Entry.Key, Entry.Value.GeneratedSchemaName); } - for (const TPair< FString, FSubobjectSchemaData>& Entry : SubobjectClassPathToSchema) - { + for (const TPair& Entry : SubobjectClassPathToSchema) + { ResolveClassPathToSchemaName(Entry.Key, Entry.Value.GeneratedSchemaName); - } + } } bool RunSchemaCompiler() @@ -815,21 +1279,27 @@ bool RunSchemaCompiler() FString SchemaCompilerExe = FPaths::Combine(PluginDir, TEXT("SpatialGDK/Binaries/ThirdParty/Improbable/Programs/schema_compiler.exe")); FString SchemaDir = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("schema")); - FString CoreSDKSchemaDir = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("build/dependencies/schema/standard_library")); + 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")); + FString SchemaBundleOutput = FPaths::Combine(CompiledSchemaDir, TEXT("schema.sb")); + FString SchemaBundleJsonOutput = FPaths::Combine(CompiledSchemaDir, TEXT("schema.json")); 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); + const FString& SchemaCompilerBaseArgs = FString::Printf(TEXT("--schema_path=\"%s\" --schema_path=\"%s\" --bundle_out=\"%s\" " + "--bundle_json_out=\"%s\" --load_all_schema_on_schema_path "), + *SchemaDir, *CoreSDKSchemaDir, *SchemaBundleOutput, *SchemaBundleJsonOutput); // 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)) { if (!PlatformFile.DeleteDirectoryRecursively(*CompiledSchemaDir)) { - UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Could not delete pre-existing compiled schema directory '%s'! Please make sure the directory is writeable."), *CompiledSchemaDir); + UE_LOG(LogSpatialGDKSchemaGenerator, Error, + TEXT("Could not delete pre-existing compiled schema directory '%s'! Please make sure the directory is writeable."), + *CompiledSchemaDir); return false; } } @@ -837,7 +1307,9 @@ bool RunSchemaCompiler() // 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); + UE_LOG(LogSpatialGDKSchemaGenerator, Error, + TEXT("Could not create compiled schema directory '%s'! Please make sure the parent directory is writeable."), + *CompiledSchemaDir); return false; } @@ -847,37 +1319,46 @@ bool RunSchemaCompiler() TArray Switches; FCommandLine::Parse(FCommandLine::Get(), Tokens, Switches); - if (const FString* SchemaCompileArgsCLSwitchPtr = Switches.FindByPredicate([](const FString& ClSwitch) { return ClSwitch.StartsWith(FString{ TEXT("AdditionalSchemaCompilerArgs") }); })) + 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 (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); + 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); + FString SchemaCompilerArgs = FString::Printf(TEXT("%s %s"), *SchemaCompilerBaseArgs, *AdditionalSchemaCompilerArgs.TrimQuotes()); - UE_LOG(LogSpatialGDKSchemaGenerator, Log, TEXT("Starting '%s' with `%s` arguments."), *SpatialGDKServicesConstants::SchemaCompilerExe, *SchemaCompilerArgs); + UE_LOG(LogSpatialGDKSchemaGenerator, Log, TEXT("Starting '%s' with `%s` arguments."), *SpatialGDKServicesConstants::SchemaCompilerExe, + *SchemaCompilerArgs); int32 ExitCode = 1; FString SchemaCompilerOut; FString SchemaCompilerErr; - FPlatformProcess::ExecProcess(*SpatialGDKServicesConstants::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 compiled schema with arguments `%s`: %s"), *SchemaCompilerArgs, *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 compiled schema for arguments `%s`: %s"), *SchemaCompilerArgs, *SchemaCompilerErr); + UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("schema_compiler failed to generate compiled schema for arguments `%s`: %s"), + *SchemaCompilerArgs, *SchemaCompilerErr); return false; } } @@ -890,21 +1371,40 @@ bool SpatialGDKGenerateSchema() TArray AllClasses; GetObjectsOfClass(UClass::StaticClass(), AllClasses); - if (!SpatialGDKGenerateSchemaForClasses(GetAllSupportedClasses(AllClasses), GetDefault()->GetGeneratedSchemaOutputFolder())) + if (!SpatialGDKGenerateSchemaForClasses(GetAllSupportedClasses(AllClasses))) { return false; } + SpatialGDKSanitizeGeneratedSchema(); GenerateSchemaForSublevels(); GenerateSchemaForRPCEndpoints(); GenerateSchemaForNCDs(); + USchemaDatabase* SchemaDatabase = InitialiseSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_ASSET_PATH); + + // Needs to happen before RunSchemaCompiler + // We construct the list of all server authoritative components while writing the file. + TArray GeneratedServerAuthoritativeComponentIds{}; + WriteServerAuthorityComponentSet(SchemaDatabase, GeneratedServerAuthoritativeComponentIds); + WriteClientAuthorityComponentSet(); + WriteComponentSetBySchemaType(SchemaDatabase, SCHEMA_Data); + WriteComponentSetBySchemaType(SchemaDatabase, SCHEMA_OwnerOnly); + WriteComponentSetBySchemaType(SchemaDatabase, SCHEMA_Handover); + + // Finish initializing the schema database through updating the server authoritative component set. + for (const auto& ComponentId : GeneratedServerAuthoritativeComponentIds) + { + SchemaDatabase->ComponentSetIdToComponentIds.FindOrAdd(SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) + .ComponentIDs.Push(ComponentId); + } + if (!RunSchemaCompiler()) { return false; } - if (!SaveSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_ASSET_PATH)) // This requires RunSchemaCompiler to run first + if (!SaveSchemaDatabase(SchemaDatabase)) // This requires RunSchemaCompiler to run first { return false; } @@ -915,8 +1415,7 @@ bool SpatialGDKGenerateSchema() bool SpatialGDKGenerateSchemaForClasses(TSet Classes, FString SchemaOutputPath /*= ""*/) { ResetUsedNames(); - Classes.Sort([](const UClass& A, const UClass& B) - { + Classes.Sort([](const UClass& A, const UClass& B) { return A.GetPathName() < B.GetPathName(); }); @@ -934,8 +1433,7 @@ bool SpatialGDKGenerateSchemaForClasses(TSet Classes, FString SchemaOut // Parent and static array index start at 0 for checksum calculations. TSharedPtr TypeInfo = CreateUnrealTypeInfo(Class, 0, 0); TypeInfos.Add(TypeInfo); - VisitAllObjects(TypeInfo, [&](TSharedPtr TypeNode) - { + VisitAllObjects(TypeInfo, [&](TSharedPtr TypeNode) { if (UClass* NestedClass = Cast(TypeNode->Type)) { if (!SchemaGeneratedClasses.Contains(NestedClass) && IsSupportedClass(NestedClass)) @@ -976,7 +1474,52 @@ bool SpatialGDKGenerateSchemaForClasses(TSet Classes, FString SchemaOut return true; } -} // Schema -} // SpatialGDKEditor +template +void SanitizeClassMap(TMap& Map, const TSet& ValidClassNames) +{ + for (auto Item = Map.CreateIterator(); Item; ++Item) + { + FString SanitizeName = Item->Key; + SanitizeName.RemoveFromEnd(TEXT("_C")); + if (!ValidClassNames.Contains(FName(*SanitizeName))) + { + UE_LOG(LogSpatialGDKSchemaGenerator, Log, TEXT("Found stale class (%s), removing from schema database."), *Item->Key); + Item.RemoveCurrent(); + } + } +} + +void SpatialGDKSanitizeGeneratedSchema() +{ + // Sanitize schema database, removing assets that no longer exist + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + + TArray Assets; + AssetRegistryModule.Get().GetAllAssets(Assets, false); + TSet ValidClassNames; + for (const auto& Asset : Assets) + { + FAssetDataTagMapSharedView::FFindTagResult GeneratedClassPathResult = Asset.TagsAndValues.FindTag(TEXT("GeneratedClass")); + if (GeneratedClassPathResult.IsSet()) + { + FString SanitizedClassPath = FPackageName::ExportTextPathToObjectPath(GeneratedClassPathResult.GetValue()); + SanitizedClassPath.RemoveFromEnd(TEXT("_C")); + ValidClassNames.Add(FName(*SanitizedClassPath)); + } + } + + TArray AllClasses; + GetObjectsOfClass(UClass::StaticClass(), AllClasses); + for (const auto& SupportedClass : GetAllSupportedClasses(AllClasses)) + { + ValidClassNames.Add(FName(*SupportedClass->GetPathName())); + } + + SanitizeClassMap(ActorClassPathToSchema, ValidClassNames); + SanitizeClassMap(SubobjectClassPathToSchema, ValidClassNames); +} + +} // namespace Schema +} // namespace SpatialGDKEditor #undef LOCTEXT_NAMESPACE diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.cpp index 62690c02c3..322f37b08c 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.cpp @@ -12,7 +12,7 @@ using namespace SpatialGDKEditor::Schema; TArray GetAllReplicatedPropertyGroups() { - static TArray Groups = {REP_MultiClient, REP_SingleClient}; + static TArray Groups = { REP_MultiClient, REP_SingleClient }; return Groups; } @@ -52,16 +52,18 @@ void VisitAllProperties(TSharedPtr TypeNode, TFunctionGetName().ToLower(), ParentChecksum); // Evolve checksum on name - Checksum = FCrc::StrCrc32(*Property->GetCPPType(nullptr, 0).ToLower(), Checksum); // Evolve by property type - Checksum = FCrc::MemCrc32(&StaticArrayIndex, sizeof(StaticArrayIndex), Checksum); // Evolve by StaticArrayIndex (to make all unrolled static array elements unique) + Checksum = FCrc::StrCrc32(*Property->GetName().ToLower(), ParentChecksum); // Evolve checksum on name + Checksum = FCrc::StrCrc32(*Property->GetCPPType(nullptr, 0).ToLower(), Checksum); // Evolve by property type + Checksum = FCrc::MemCrc32(&StaticArrayIndex, sizeof(StaticArrayIndex), + Checksum); // Evolve by StaticArrayIndex (to make all unrolled static array elements unique) return Checksum; } -TSharedPtr CreateUnrealProperty(TSharedPtr TypeNode, GDK_PROPERTY(Property)* Property, uint32 ParentChecksum, uint32 StaticArrayIndex) +TSharedPtr CreateUnrealProperty(TSharedPtr TypeNode, GDK_PROPERTY(Property) * Property, uint32 ParentChecksum, + uint32 StaticArrayIndex) { TSharedPtr PropertyNode = MakeShared(); PropertyNode->Property = Property; @@ -119,23 +121,26 @@ TSharedPtr CreateUnrealTypeInfo(UStruct* Type, uint32 ParentChecksu TSharedPtr StaticStructArrayPropertyNode = CreateUnrealProperty(TypeNode, Property, ParentChecksum, i); // Generate Type information on the inner struct. - // Note: The parent checksum of the properties within a struct that is a member of a static struct array, is the checksum for the struct itself after index modification. - StaticStructArrayPropertyNode->Type = CreateUnrealTypeInfo(StructProperty->Struct, StaticStructArrayPropertyNode->CompatibleChecksum, 0); + // Note: The parent checksum of the properties within a struct that is a member of a static struct array, is the checksum + // for the struct itself after index modification. + StaticStructArrayPropertyNode->Type = + CreateUnrealTypeInfo(StructProperty->Struct, StaticStructArrayPropertyNode->CompatibleChecksum, 0); StaticStructArrayPropertyNode->Type->ParentProperty = StaticStructArrayPropertyNode; } continue; } // If this is an object property, then we need to do two things: - // 1) Determine whether this property is a strong or weak reference to the object. Some subobjects (such as the CharacterMovementComponent) - // are in fact owned by the character, and can be stored in the same entity as the character itself. Some subobjects (such as the Controller - // field in AActor) is a weak reference, and should just store a reference to the real object. We inspect the CDO to determine whether - // the owner of the property value is equal to itself. As structs don't have CDOs, we assume that all object properties in structs are - // weak references. // - // 2) Obtain the concrete object type stored in this property. For example, the property containing the CharacterMovementComponent - // might be a property which stores a MovementComponent pointer, so we'd need to somehow figure out the real type being stored there - // during runtime. This is determined by getting the CDO of this class to determine what is stored in that property. + // 1) Determine whether this property is a strong or weak reference to the object. Some subobjects (such as the + // CharacterMovementComponent) are in fact owned by the character, and can be stored in the same entity as the character itself. + // Some subobjects (such as the Controller field in AActor) is a weak reference, and should just store a reference to the real + // object. We inspect the CDO to determine whether the owner of the property value is equal to itself. As structs don't have CDOs, + // we assume that all object properties in structs are weak references. + // + // 2) Obtain the concrete object type stored in this property. For example, the property containing the CharacterMovementComponent + // might be a property which stores a MovementComponent pointer, so we'd need to somehow figure out the real type being stored + // there during runtime. This is determined by getting the CDO of this class to determine what is stored in that property. GDK_PROPERTY(ObjectProperty)* ObjectProperty = GDK_CASTFIELD(Property); check(ObjectProperty); @@ -167,11 +172,10 @@ TSharedPtr CreateUnrealTypeInfo(UStruct* Type, uint32 ParentChecksu // or the CDO of any of its parent classes. // (this also covers generating schema for a Blueprint derived from the outer's class) UObject* Outer = Value->GetOuter(); - if ((Outer != nullptr) && - Outer->HasAnyFlags(RF_ClassDefaultObject) && - ContainerCDO->IsA(Outer->GetClass())) + if ((Outer != nullptr) && Outer->HasAnyFlags(RF_ClassDefaultObject) && ContainerCDO->IsA(Outer->GetClass())) { - UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("Property Class: %s Instance Class: %s"), *ObjectProperty->PropertyClass->GetName(), *Value->GetClass()->GetName()); + UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("Property Class: %s Instance Class: %s"), + *ObjectProperty->PropertyClass->GetName(), *Value->GetClass()->GetName()); // This property is definitely a strong reference, recurse into it. PropertyNode->Type = CreateUnrealTypeInfo(Value->GetClass(), ParentChecksum, 0); @@ -193,13 +197,15 @@ TSharedPtr CreateUnrealTypeInfo(UStruct* Type, uint32 ParentChecksu else { // The values outer is not us, store as weak reference. - UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("%s - %s weak reference (outer not this)"), *Property->GetName(), *ObjectProperty->PropertyClass->GetName()); + UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("%s - %s weak reference (outer not this)"), *Property->GetName(), + *ObjectProperty->PropertyClass->GetName()); } } else { // If value is just nullptr, then we clearly don't own it. - UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("%s - %s weak reference (null init)"), *Property->GetName(), *ObjectProperty->PropertyClass->GetName()); + UE_LOG(LogSpatialGDKSchemaGenerator, Verbose, TEXT("%s - %s weak reference (null init)"), *Property->GetName(), + *ObjectProperty->PropertyClass->GetName()); } // Weak reference static arrays are handled as a single UObjectRef per static array member. @@ -229,7 +235,8 @@ TSharedPtr CreateUnrealTypeInfo(UStruct* Type, uint32 ParentChecksu for (auto& PropertyPair : TypeNode->Properties) { GDK_PROPERTY(ObjectProperty)* ObjectProperty = GDK_CASTFIELD(PropertyPair.Key); - if (ObjectProperty == nullptr) continue; + if (ObjectProperty == nullptr) + continue; TSharedPtr PropertyNode = PropertyPair.Value; if (ObjectProperty->GetName().Equals(Node->GetVariableName().ToString())) @@ -256,7 +263,7 @@ TSharedPtr CreateUnrealTypeInfo(UStruct* Type, uint32 ParentChecksu // Based on inspection in InitFromObjectClass, the RepLayout will always replicate object properties using NetGUIDs, regardless of // ownership. However, the rep layout will recurse into structs and allocate rep handles for their properties, unless the condition // "Struct->StructFlags & STRUCT_NetSerializeNative" is true. In this case, the entire struct is replicated as a whole. - TSharedPtr RepLayoutPtr = FRepLayout::CreateFromClass(Class, nullptr/*ServerConnection*/, ECreateRepLayoutFlags::None); + TSharedPtr RepLayoutPtr = FRepLayout::CreateFromClass(Class, nullptr /*ServerConnection*/, ECreateRepLayoutFlags::None); FRepLayout& RepLayout = *RepLayoutPtr.Get(); for (int CmdIndex = 0; CmdIndex < RepLayout.Cmds.Num(); ++CmdIndex) { @@ -267,7 +274,8 @@ TSharedPtr CreateUnrealTypeInfo(UStruct* Type, uint32 ParentChecksu } // Jump over invalid replicated property types - if (Cmd.Property->IsA() || Cmd.Property->IsA() || Cmd.Property->IsA()) + if (Cmd.Property->IsA() || Cmd.Property->IsA() + || Cmd.Property->IsA()) { continue; } @@ -280,7 +288,8 @@ TSharedPtr CreateUnrealTypeInfo(UStruct* Type, uint32 ParentChecksu // multiple Cmds map to the structs properties, but they all have the same ParentIndex (which points to the root replicated property // which contains them. // - // This might be problematic if we have a property which is inside a struct, nested in another struct which is replicated. For example: + // This might be problematic if we have a property which is inside a struct, nested in another struct which is replicated. For + // example: // // class Foo // { @@ -293,8 +302,8 @@ TSharedPtr CreateUnrealTypeInfo(UStruct* Type, uint32 ParentChecksu // } Bar; // } // - // The parents array will contain "Bar", and the cmds array will contain "Nested", but we have no reference to "Baz" anywhere in the RepLayout. - // What we do here is recurse into all of Bar's properties in the AST until we find Baz. + // The parents array will contain "Bar", and the cmds array will contain "Nested", but we have no reference to "Baz" anywhere in the + // RepLayout. What we do here is recurse into all of Bar's properties in the AST until we find Baz. TSharedPtr PropertyNode = nullptr; @@ -312,19 +321,21 @@ TSharedPtr CreateUnrealTypeInfo(UStruct* Type, uint32 ParentChecksu } else { - // It's possible to have duplicate parent properties (they are distinguished by ArrayIndex), so we make sure to look at them all. + // It's possible to have duplicate parent properties (they are distinguished by ArrayIndex), so we make sure to look at them + // all. TArray> RootProperties; TypeNode->Properties.MultiFind(Parent.Property, RootProperties); for (TSharedPtr& RootProperty : RootProperties) { - checkf(RootProperty->Type.IsValid(), TEXT("Properties in the AST which are parent properties in the rep layout must have child properties")); - VisitAllProperties(RootProperty->Type, [&PropertyNode, &Cmd](TSharedPtr Property) - { + checkf(RootProperty->Type.IsValid(), + TEXT("Properties in the AST which are parent properties in the rep layout must have child properties")); + VisitAllProperties(RootProperty->Type, [&PropertyNode, &Cmd](TSharedPtr Property) { if (Property->CompatibleChecksum == Cmd.CompatibleChecksum) { - checkf(!PropertyNode.IsValid(), TEXT("We've already found a previous property node with the same property. This indicates that we have a 'diamond of death' style situation.")) - PropertyNode = Property; + checkf(!PropertyNode.IsValid(), TEXT("We've already found a previous property node with the same property. This " + "indicates that we have a 'diamond of death' style situation.")) PropertyNode = + Property; } return true; }); @@ -376,8 +387,7 @@ TSharedPtr CreateUnrealTypeInfo(UStruct* Type, uint32 ParentChecksu // Find the handover properties. uint16 HandoverDataHandle = 1; - VisitAllProperties(TypeNode, [&HandoverDataHandle, &Class](TSharedPtr PropertyInfo) - { + VisitAllProperties(TypeNode, [&HandoverDataHandle, &Class](TSharedPtr PropertyInfo) { if (PropertyInfo->Property->PropertyFlags & CPF_Handover) { if (GDK_PROPERTY(StructProperty)* StructProp = GDK_CASTFIELD(PropertyInfo->Property)) @@ -385,8 +395,10 @@ TSharedPtr CreateUnrealTypeInfo(UStruct* Type, uint32 ParentChecksu if (StructProp->Struct->StructFlags & STRUCT_NetDeltaSerializeNative) { // Warn about delta serialization - UE_LOG(LogSpatialGDKSchemaGenerator, Warning, TEXT("%s in %s uses delta serialization. " \ - "This is not supported and standard serialization will be used instead."), *PropertyInfo->Property->GetName(), *Class->GetName()); + UE_LOG(LogSpatialGDKSchemaGenerator, Warning, + TEXT("%s in %s uses delta serialization. " + "This is not supported and standard serialization will be used instead."), + *PropertyInfo->Property->GetName(), *Class->GetName()); } } PropertyInfo->HandoverData = MakeShared(); @@ -404,8 +416,7 @@ FUnrealFlatRepData GetFlatRepData(TSharedPtr TypeInfo) RepData.Add(REP_MultiClient); RepData.Add(REP_SingleClient); - VisitAllProperties(TypeInfo, [&RepData](TSharedPtr PropertyInfo) - { + VisitAllProperties(TypeInfo, [&RepData](TSharedPtr PropertyInfo) { if (PropertyInfo->ReplicationData.IsValid()) { EReplicatedPropertyGroup Group = REP_MultiClient; @@ -423,12 +434,10 @@ FUnrealFlatRepData GetFlatRepData(TSharedPtr TypeInfo) }); // Sort by replication handle. - RepData[REP_MultiClient].KeySort([](uint16 A, uint16 B) - { + RepData[REP_MultiClient].KeySort([](uint16 A, uint16 B) { return A < B; }); - RepData[REP_SingleClient].KeySort([](uint16 A, uint16 B) - { + RepData[REP_SingleClient].KeySort([](uint16 A, uint16 B) { return A < B; }); return RepData; @@ -437,8 +446,7 @@ FUnrealFlatRepData GetFlatRepData(TSharedPtr TypeInfo) FCmdHandlePropertyMap GetFlatHandoverData(TSharedPtr TypeInfo) { FCmdHandlePropertyMap HandoverData; - VisitAllProperties(TypeInfo, [&HandoverData](TSharedPtr PropertyInfo) - { + VisitAllProperties(TypeInfo, [&HandoverData](TSharedPtr PropertyInfo) { if (PropertyInfo->HandoverData.IsValid()) { HandoverData.Add(PropertyInfo->HandoverData->Handle, PropertyInfo); @@ -447,8 +455,7 @@ FCmdHandlePropertyMap GetFlatHandoverData(TSharedPtr TypeInfo) }); // Sort by property handle. - HandoverData.KeySort([](uint16 A, uint16 B) - { + HandoverData.KeySort([](uint16 A, uint16 B) { return A < B; }); return HandoverData; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.h b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.h index 00bdc574d1..936e2747e5 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.h @@ -77,7 +77,7 @@ struct FUnrealType { UStruct* Type; UObject* Object; // Actual instance of the object. Could be the CDO or a Subobject on the CDO/BlueprintGeneratedClass - FName Name; // Name for the object. This is either the name of the object itself, or the name of the property in the blueprint + FName Name; // Name for the object. This is either the name of the object itself, or the name of the property in the blueprint TMultiMap> Properties; TWeakPtr ParentProperty; }; @@ -85,14 +85,15 @@ struct FUnrealType // A node which represents a single property. struct FUnrealProperty { - GDK_PROPERTY(Property)* Property; - TSharedPtr Type; // Only set if strong reference to object/struct property. - TSharedPtr ReplicationData; // Only set if property is replicated. + GDK_PROPERTY(Property) * Property; + TSharedPtr Type; // Only set if strong reference to object/struct property. + TSharedPtr ReplicationData; // Only set if property is replicated. TSharedPtr HandoverData; // Only set if property is marked for handover (and not replicated). TWeakPtr ContainerType; // These variables are used for unique variable checksum generation. We do this to accurately match properties at run-time. - // They are used in the function GenerateChecksum which will use all three variables and the UProperty itself to create a checksum for each FUnrealProperty. + // They are used in the function GenerateChecksum which will use all three variables and the UProperty itself to create a checksum for + // each FUnrealProperty. int32 StaticArrayIndex; uint32 CompatibleChecksum; uint32 ParentChecksum; @@ -125,20 +126,21 @@ TArray GetAllReplicatedPropertyGroups(); // Convert a replicated property group to a string. Used to generate component names. FString GetReplicatedPropertyGroupName(EReplicatedPropertyGroup Group); -// Given an AST, this applies the function 'Visitor' to all FUnrealType's contained transitively within the properties. bRecurseIntoObjects will control -// whether this function will recurse into a UObject's properties, which may not always be desirable. However, it will always recurse into substructs. -// If the Visitor function returns false, it will not recurse any further into that part of the tree. +// Given an AST, this applies the function 'Visitor' to all FUnrealType's contained transitively within the properties. bRecurseIntoObjects +// will control whether this function will recurse into a UObject's properties, which may not always be desirable. However, it will always +// recurse into substructs. If the Visitor function returns false, it will not recurse any further into that part of the tree. void VisitAllObjects(TSharedPtr TypeNode, TFunction)> Visitor); -// Given an AST, this applies the function 'Visitor' to all properties contained transitively within the type. This will recurse into substructs. -// If the Visitor function returns false, it will not recurse any further into that part of the tree. +// Given an AST, this applies the function 'Visitor' to all properties contained transitively within the type. This will recurse into +// substructs. If the Visitor function returns false, it will not recurse any further into that part of the tree. void VisitAllProperties(TSharedPtr TypeNode, TFunction)> Visitor); // Generates a unique checksum for the Property that allows matching to Unreal's RepLayout Cmds. -uint32 GenerateChecksum(GDK_PROPERTY(Property)* Property, uint32 ParentChecksum, int32 StaticArrayIndex); +uint32 GenerateChecksum(GDK_PROPERTY(Property) * Property, uint32 ParentChecksum, int32 StaticArrayIndex); // Creates a new FUnrealProperty for the included UProperty, generates a checksum for it and then adds it to the TypeNode included. -TSharedPtr CreateUnrealProperty(TSharedPtr TypeNode, GDK_PROPERTY(Property)* Property, uint32 ParentChecksum, uint32 StaticArrayIndex); +TSharedPtr CreateUnrealProperty(TSharedPtr TypeNode, GDK_PROPERTY(Property) * Property, uint32 ParentChecksum, + uint32 StaticArrayIndex); // Generates an AST from an Unreal UStruct or UClass. TSharedPtr CreateUnrealTypeInfo(UStruct* Type, uint32 ParentChecksum, int32 StaticArrayIndex); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/CodeWriter.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/CodeWriter.cpp index b9db6f5563..eb87fbb033 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/CodeWriter.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/CodeWriter.cpp @@ -1,9 +1,10 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved -#include "CodeWriter.h" +#include "Utils/CodeWriter.h" #include "Misc/FileHelper.h" -FCodeWriter::FCodeWriter() : Scope(0) +FCodeWriter::FCodeWriter() + : Scope(0) { } @@ -111,6 +112,17 @@ FCodeWriter& FCodeWriter::End() return *this; } +void FCodeWriter::RemoveTrailingComma() +{ + const int32 TrailingCommaIndex = OutputSource.FindLastCharByPredicate([](TCHAR c) { + return c == TEXT(','); + }); + if (TrailingCommaIndex != INDEX_NONE) + { + OutputSource.RemoveAt(TrailingCommaIndex); + } +} + void FCodeWriter::WriteToFile(const FString& Filename) { check(Scope == 0); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/ComponentIdGenerator.h b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/ComponentIdGenerator.h index 837d3951cc..4b5fe4541d 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/ComponentIdGenerator.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/ComponentIdGenerator.h @@ -20,13 +20,9 @@ struct FComponentIdGenerator return Result; } - uint32 Peek() const - { - return NextId; - } + uint32 Peek() const { return NextId; } private: - void ValidateNextId() { if (RESERVED_COMPONENT_ID_START <= NextId && NextId <= RESERVED_COMPONENT_ID_END) diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.cpp index 4bb3f2a73b..8fbd03f03f 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.cpp @@ -11,7 +11,7 @@ // Regex pattern matcher to match alphanumeric characters. const FRegexPattern AlphanumericPattern(TEXT("[A-Za-z0-9]")); -FString GetEnumDataType(const GDK_PROPERTY(EnumProperty)* EnumProperty) +FString GetEnumDataType(const GDK_PROPERTY(EnumProperty) * EnumProperty) { FString DataType; @@ -36,7 +36,10 @@ FString UnrealNameToSchemaName(const FString& UnrealName, bool bWarnAboutRename FString Result = TEXT("ZZ") + Sanitized; if (bWarnAboutRename) { - UE_LOG(LogSpatialGDKSchemaGenerator, Warning, TEXT("%s starts with a digit (potentially after removing non-alphanumeric characters), so its schema name was changed to %s instead. To remove this warning, rename your asset."), *UnrealName, *Result); + UE_LOG(LogSpatialGDKSchemaGenerator, Warning, + TEXT("%s starts with a digit (potentially after removing non-alphanumeric characters), so its schema name was changed " + "to %s instead. To remove this warning, rename your asset."), + *UnrealName, *Result); } return Result; } @@ -70,7 +73,8 @@ FString UnrealNameToSchemaComponentName(const FString& UnrealName) FString SchemaReplicatedDataName(EReplicatedPropertyGroup Group, UClass* Class) { - return FString::Printf(TEXT("%s%s"), *UnrealNameToSchemaComponentName(ClassPathToSchemaName[Class->GetPathName()]), *GetReplicatedPropertyGroupName(Group)); + return FString::Printf(TEXT("%s%s"), *UnrealNameToSchemaComponentName(ClassPathToSchemaName[Class->GetPathName()]), + *GetReplicatedPropertyGroupName(Group)); } FString SchemaHandoverDataName(UClass* Class) @@ -82,8 +86,7 @@ FString SchemaFieldName(const TSharedPtr Property) { // Transform the property chain into a chain of names. TArray ChainNames; - Algo::Transform(GetPropertyChain(Property), ChainNames, [](const TSharedPtr& Property) -> FString - { + Algo::Transform(GetPropertyChain(Property), ChainNames, [](const TSharedPtr& Property) -> FString { FString PropName = Property->Property->GetName().ToLower(); if (Property->Property->ArrayDim > 1) { diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.h b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.h index 3484bc0403..1c5e2d03d7 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.h @@ -10,12 +10,13 @@ extern TMap ClassPathToSchemaName; // Return the string representation of the underlying data type of an enum property -FString GetEnumDataType(const GDK_PROPERTY(EnumProperty)* EnumProperty); +FString GetEnumDataType(const GDK_PROPERTY(EnumProperty) * EnumProperty); // Given a class or function name, generates the name used for naming schema components and types. Removes all non-alphanumeric characters. FString UnrealNameToSchemaName(const FString& UnrealName, bool bWarnAboutRename = false); -// Given an object name, generates the name used for naming schema components. Removes all non-alphanumeric characters and capitalizes the first letter. +// Given an object name, generates the name used for naming schema components. Removes all non-alphanumeric characters and capitalizes the +// first letter. FString UnrealNameToSchemaComponentName(const FString& UnrealName); // Given a replicated property group and Unreal type, generates the name of the corresponding schema component. diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp index 66a2c4f6ad..6b3dabd83e 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp @@ -4,15 +4,14 @@ #include "Engine/LevelScriptActor.h" #include "Interop/SpatialClassInfoManager.h" -#include "Schema/ComponentPresence.h" #include "Schema/Interest.h" #include "Schema/SpawnData.h" #include "Schema/StandardLibrary.h" #include "Schema/UnrealMetadata.h" #include "SpatialConstants.h" #include "SpatialGDKSettings.h" -#include "Utils/EntityFactory.h" #include "Utils/ComponentFactory.h" +#include "Utils/EntityFactory.h" #include "Utils/RepDataUtils.h" #include "Utils/RepLayoutUtils.h" #include "Utils/SchemaUtils.h" @@ -23,8 +22,8 @@ #include "HAL/PlatformFilemanager.h" #include "UObject/UObjectIterator.h" -#include #include +#include using namespace SpatialGDK; @@ -59,22 +58,27 @@ bool CreateSpawnerEntity(Worker_SnapshotOutputStream* OutputStream) PlayerSpawnerData.component_id = SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID; PlayerSpawnerData.schema_type = Schema_CreateComponentData(); + Interest SelfInterest; + Query AuthoritySelfQuery = {}; + AuthoritySelfQuery.ResultComponentIds = { SpatialConstants::GDK_KNOWN_ENTITY_TAG_COMPONENT_ID }; + AuthoritySelfQuery.Constraint.bSelfConstraint = true; + SelfInterest.ComponentInterestMap.Add(SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID); + SelfInterest.ComponentInterestMap[SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID].Queries.Add(AuthoritySelfQuery); + TArray Components; - 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::PLAYER_SPAWNER_COMPONENT_ID, SpatialConstants::UnrealServerPermission); - ComponentWriteAcl.Add(SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID, SpatialConstants::UnrealServerPermission); + AuthorityDelegationMap DelegationMap; + DelegationMap.Add(SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID, SpatialConstants::INITIAL_SNAPSHOT_PARTITION_ENTITY_ID); 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()); + Components.Add(SelfInterest.CreateInterestData()); + + // GDK known entities completeness tags. + Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::GDK_KNOWN_ENTITY_TAG_COMPONENT_ID)); + Components.Add(AuthorityDelegation(DelegationMap).CreateAuthorityDelegationData()); SetEntityData(SpawnerEntity, Components); @@ -99,7 +103,7 @@ Worker_ComponentData CreateDeploymentData() Worker_ComponentData CreateGSMShutdownData() { - Worker_ComponentData GSMShutdownData; + Worker_ComponentData GSMShutdownData{}; GSMShutdownData.component_id = SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID; GSMShutdownData.schema_type = Schema_CreateComponentData(); return GSMShutdownData; @@ -122,19 +126,17 @@ bool CreateGlobalStateManager(Worker_SnapshotOutputStream* OutputStream) Worker_Entity GSM; GSM.entity_id = SpatialConstants::INITIAL_GLOBAL_STATE_MANAGER_ENTITY_ID; - TArray Components; + Interest SelfInterest; + Query AuthoritySelfQuery = {}; + AuthoritySelfQuery.ResultComponentIds = { SpatialConstants::GDK_KNOWN_ENTITY_TAG_COMPONENT_ID }; + AuthoritySelfQuery.Constraint.bSelfConstraint = true; + SelfInterest.ComponentInterestMap.Add(SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID); + SelfInterest.ComponentInterestMap[SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID].Queries.Add(AuthoritySelfQuery); - 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::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::COMPONENT_PRESENCE_COMPONENT_ID, SpatialConstants::UnrealServerPermission); + TArray Components; - WorkerRequirementSet ReadACL = { SpatialConstants::UnrealServerAttributeSet }; + AuthorityDelegationMap DelegationMap; + DelegationMap.Add(SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID, SpatialConstants::INITIAL_SNAPSHOT_PARTITION_ENTITY_ID); Components.Add(Position(DeploymentOrigin).CreatePositionData()); Components.Add(Metadata(TEXT("GlobalStateManager")).CreateMetadataData()); @@ -142,8 +144,12 @@ bool CreateGlobalStateManager(Worker_SnapshotOutputStream* OutputStream) Components.Add(CreateDeploymentData()); Components.Add(CreateGSMShutdownData()); Components.Add(CreateStartupActorManagerData()); - Components.Add(EntityAcl(ReadACL, ComponentWriteAcl).CreateEntityAclData()); - Components.Add(ComponentPresence(EntityFactory::GetComponentPresenceList(Components)).CreateComponentPresenceData()); + Components.Add(SelfInterest.CreateInterestData()); + + // GDK known entities completeness tags. + Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::GDK_KNOWN_ENTITY_TAG_COMPONENT_ID)); + + Components.Add(AuthorityDelegation(DelegationMap).CreateAuthorityDelegationData()); SetEntityData(GSM, Components); @@ -164,24 +170,28 @@ bool CreateVirtualWorkerTranslator(Worker_SnapshotOutputStream* OutputStream) Worker_Entity VirtualWorkerTranslator; VirtualWorkerTranslator.entity_id = SpatialConstants::INITIAL_VIRTUAL_WORKER_TRANSLATOR_ENTITY_ID; - TArray Components; + Interest SelfInterest; + Query AuthoritySelfQuery = {}; + AuthoritySelfQuery.ResultComponentIds = { SpatialConstants::GDK_KNOWN_ENTITY_TAG_COMPONENT_ID }; + AuthoritySelfQuery.Constraint.bSelfConstraint = true; + SelfInterest.ComponentInterestMap.Add(SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID); + SelfInterest.ComponentInterestMap[SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID].Queries.Add(AuthoritySelfQuery); - 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); + TArray Components; - WorkerRequirementSet ReadACL = { SpatialConstants::UnrealServerAttributeSet }; + AuthorityDelegationMap DelegationMap; + DelegationMap.Add(SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID, SpatialConstants::INITIAL_SNAPSHOT_PARTITION_ENTITY_ID); Components.Add(Position(DeploymentOrigin).CreatePositionData()); Components.Add(Metadata(TEXT("VirtualWorkerTranslator")).CreateMetadataData()); Components.Add(Persistence().CreatePersistenceData()); Components.Add(CreateVirtualWorkerTranslatorData()); - Components.Add(EntityAcl(ReadACL, ComponentWriteAcl).CreateEntityAclData()); - Components.Add(ComponentPresence(EntityFactory::GetComponentPresenceList(Components)).CreateComponentPresenceData()); + Components.Add(SelfInterest.CreateInterestData()); + + // GDK known entities completeness tags. + Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::GDK_KNOWN_ENTITY_TAG_COMPONENT_ID)); + + Components.Add(AuthorityDelegation(DelegationMap).CreateAuthorityDelegationData()); SetEntityData(VirtualWorkerTranslator, Components); @@ -189,6 +199,28 @@ bool CreateVirtualWorkerTranslator(Worker_SnapshotOutputStream* OutputStream) return Worker_SnapshotOutputStream_GetState(OutputStream).stream_state == WORKER_STREAM_STATE_GOOD; } +bool CreateSnapshotPartitionEntity(Worker_SnapshotOutputStream* OutputStream) +{ + Worker_Entity SnapshotPartitionEntity; + SnapshotPartitionEntity.entity_id = SpatialConstants::INITIAL_SNAPSHOT_PARTITION_ENTITY_ID; + + TArray Components; + + AuthorityDelegationMap DelegationMap; + DelegationMap.Add(SpatialConstants::GDK_KNOWN_ENTITY_AUTH_COMPONENT_SET_ID, SpatialConstants::INITIAL_SNAPSHOT_PARTITION_ENTITY_ID); + + Components.Add(Position(DeploymentOrigin).CreatePositionData()); + Components.Add(Metadata(TEXT("SnapshotPartitionEntity")).CreateMetadataData()); + Components.Add(Persistence().CreatePersistenceData()); + Components.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::PARTITION_SHADOW_COMPONENT_ID)); + Components.Add(AuthorityDelegation(DelegationMap).CreateAuthorityDelegationData()); + + SetEntityData(SnapshotPartitionEntity, Components); + + Worker_SnapshotOutputStream_WriteEntity(OutputStream, &SnapshotPartitionEntity); + return Worker_SnapshotOutputStream_GetState(OutputStream).stream_state == WORKER_STREAM_STATE_GOOD; +} + bool ValidateAndCreateSnapshotGenerationPath(FString& SavePath) { FString DirectoryPath = FPaths::GetPath(SavePath); @@ -214,13 +246,16 @@ bool RunUserSnapshotGenerationOverrides(Worker_SnapshotOutputStream* OutputStrea { for (TObjectIterator SnapshotGenerationClass; SnapshotGenerationClass; ++SnapshotGenerationClass) { - if (SnapshotGenerationClass->IsChildOf(USnapshotGenerationTemplate::StaticClass()) && *SnapshotGenerationClass != USnapshotGenerationTemplate::StaticClass()) + if (SnapshotGenerationClass->IsChildOf(USnapshotGenerationTemplate::StaticClass()) + && *SnapshotGenerationClass != USnapshotGenerationTemplate::StaticClass()) { UE_LOG(LogSpatialGDKSnapshot, Log, TEXT("Found user snapshot generation class: %s"), *SnapshotGenerationClass->GetName()); - USnapshotGenerationTemplate *SnapshotGenerationObj = NewObject(GetTransientPackage(), *SnapshotGenerationClass); + USnapshotGenerationTemplate* SnapshotGenerationObj = + NewObject(GetTransientPackage(), *SnapshotGenerationClass); if (!SnapshotGenerationObj->WriteToSnapshotOutput(OutputStream, NextAvailableEntityID)) { - UE_LOG(LogSpatialGDKSnapshot, Error, TEXT("Failure returned in user snapshot generation override method from class: %s"), *SnapshotGenerationClass->GetName()); + UE_LOG(LogSpatialGDKSnapshot, Error, TEXT("Failure returned in user snapshot generation override method from class: %s"), + *SnapshotGenerationClass->GetName()); return false; } } @@ -232,26 +267,37 @@ bool FillSnapshot(Worker_SnapshotOutputStream* OutputStream, UWorld* World) { if (!CreateSpawnerEntity(OutputStream)) { - UE_LOG(LogSpatialGDKSnapshot, Error, TEXT("Error generating Spawner in snapshot: %s"), UTF8_TO_TCHAR(Worker_SnapshotOutputStream_GetState(OutputStream).error_message)); + UE_LOG(LogSpatialGDKSnapshot, Error, TEXT("Error generating Spawner in snapshot: %s"), + UTF8_TO_TCHAR(Worker_SnapshotOutputStream_GetState(OutputStream).error_message)); return false; } if (!CreateGlobalStateManager(OutputStream)) { - UE_LOG(LogSpatialGDKSnapshot, Error, TEXT("Error generating GlobalStateManager in snapshot: %s"), UTF8_TO_TCHAR(Worker_SnapshotOutputStream_GetState(OutputStream).error_message)); + UE_LOG(LogSpatialGDKSnapshot, Error, TEXT("Error generating GlobalStateManager in snapshot: %s"), + UTF8_TO_TCHAR(Worker_SnapshotOutputStream_GetState(OutputStream).error_message)); 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)); + UE_LOG(LogSpatialGDKSnapshot, Error, TEXT("Error generating VirtualWorkerTranslator in snapshot: %s"), + UTF8_TO_TCHAR(Worker_SnapshotOutputStream_GetState(OutputStream).error_message)); + return false; + } + + if (!CreateSnapshotPartitionEntity(OutputStream)) + { + UE_LOG(LogSpatialGDKSnapshot, Error, TEXT("Error generating SnapshotPartitionEntity 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)) { - UE_LOG(LogSpatialGDKSnapshot, Error, TEXT("Error running user defined snapshot generation overrides in snapshot: %s"), UTF8_TO_TCHAR(Worker_SnapshotOutputStream_GetState(OutputStream).error_message)); + UE_LOG(LogSpatialGDKSnapshot, Error, TEXT("Error running user defined snapshot generation overrides in snapshot: %s"), + UTF8_TO_TCHAR(Worker_SnapshotOutputStream_GetState(OutputStream).error_message)); return false; } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultLaunchConfigGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultLaunchConfigGenerator.cpp index 6f8d96a395..81b0d60f19 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultLaunchConfigGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultLaunchConfigGenerator.cpp @@ -5,10 +5,12 @@ #include "EngineClasses/SpatialWorldSettings.h" #include "LoadBalancing/AbstractLBStrategy.h" #include "SpatialGDKEditorModule.h" -#include "SpatialGDKSettings.h" #include "SpatialGDKEditorSettings.h" +#include "SpatialGDKSettings.h" +#include "Utils/SpatialStatics.h" #include "Editor.h" +#include "GenericPlatform/GenericPlatformFile.h" #include "ISettingsModule.h" #include "Misc/FileHelper.h" #include "Misc/MessageDialog.h" @@ -26,74 +28,61 @@ namespace bool WriteFlagSection(TSharedRef> Writer, const FString& Key, const FString& Value) { Writer->WriteObjectStart(); - Writer->WriteValue(TEXT("name"), Key); - Writer->WriteValue(TEXT("value"), Value); + Writer->WriteValue(TEXT("name"), Key); + Writer->WriteValue(TEXT("value"), Value); Writer->WriteObjectEnd(); return true; } -bool WriteWorkerSection(TSharedRef> Writer, const FName& WorkerTypeName, const FWorkerTypeLaunchSection& WorkerConfig) +bool WriteLoadbalancingSection(TSharedRef> Writer, const FName& WorkerType, uint32 NumEditorInstances, + const bool bManualWorkerConnectionOnly) { - Writer->WriteObjectStart(); - Writer->WriteValue(TEXT("worker_type"), *WorkerTypeName.ToString()); - Writer->WriteArrayStart(TEXT("flags")); - for (const auto& Flag : WorkerConfig.Flags) - { - WriteFlagSection(Writer, Flag.Key, Flag.Value); - } - Writer->WriteArrayEnd(); - Writer->WriteArrayStart(TEXT("permissions")); - Writer->WriteObjectStart(); - if (WorkerConfig.WorkerPermissions.bAllPermissions) - { - Writer->WriteObjectStart(TEXT("all")); - Writer->WriteObjectEnd(); - } - else - { - Writer->WriteObjectStart(TEXT("entity_creation")); - Writer->WriteValue(TEXT("allow"), WorkerConfig.WorkerPermissions.bAllowEntityCreation); - Writer->WriteObjectEnd(); - Writer->WriteObjectStart(TEXT("entity_deletion")); - Writer->WriteValue(TEXT("allow"), WorkerConfig.WorkerPermissions.bAllowEntityDeletion); - Writer->WriteObjectEnd(); - Writer->WriteObjectStart(TEXT("entity_query")); - Writer->WriteValue(TEXT("allow"), WorkerConfig.WorkerPermissions.bAllowEntityQuery); - Writer->WriteArrayStart("components"); - for (const FString& Component : WorkerConfig.WorkerPermissions.Components) - { - Writer->WriteValue(Component); - } - Writer->WriteArrayEnd(); - Writer->WriteObjectEnd(); - } - Writer->WriteObjectEnd(); - Writer->WriteArrayEnd(); + Writer->WriteObjectStart(TEXT("load_balancing")); + Writer->WriteObjectStart("rectangle_grid"); + Writer->WriteValue(TEXT("cols"), 1); + Writer->WriteValue(TEXT("rows"), static_cast(NumEditorInstances)); + Writer->WriteObjectEnd(); + Writer->WriteValue(TEXT("manual_worker_connection_only"), bManualWorkerConnectionOnly); Writer->WriteObjectEnd(); return true; } -bool WriteLoadbalancingSection(TSharedRef> Writer, const FName& WorkerType, uint32 NumEditorInstances, const bool ManualWorkerConnectionOnly) +bool WriteWorkerSection(TSharedRef> Writer, const FWorkerTypeLaunchSection& WorkerConfig) { Writer->WriteObjectStart(); - Writer->WriteValue(TEXT("layer"), *WorkerType.ToString()); - Writer->WriteObjectStart("rectangle_grid"); - Writer->WriteValue(TEXT("cols"), 1); - Writer->WriteValue(TEXT("rows"), (int32) NumEditorInstances); - Writer->WriteObjectEnd(); - Writer->WriteObjectStart(TEXT("options")); - Writer->WriteValue(TEXT("manual_worker_connection_only"), ManualWorkerConnectionOnly); - Writer->WriteObjectEnd(); + + Writer->WriteValue(TEXT("worker_type"), *WorkerConfig.WorkerTypeName.ToString()); + + Writer->WriteArrayStart(TEXT("flags")); + for (const auto& Flag : WorkerConfig.Flags) + { + WriteFlagSection(Writer, Flag.Key, Flag.Value); + } + Writer->WriteArrayEnd(); + + Writer->WriteObjectStart(TEXT("permissions")); + Writer->WriteValue(TEXT("entity_creation"), WorkerConfig.WorkerPermissions.bAllowEntityCreation); + Writer->WriteValue(TEXT("entity_deletion"), WorkerConfig.WorkerPermissions.bAllowEntityDeletion); + Writer->WriteValue(TEXT("disconnect_worker"), WorkerConfig.WorkerPermissions.bDisconnectWorker); + Writer->WriteValue(TEXT("reserve_entity_id"), WorkerConfig.WorkerPermissions.bReserveEntityID); + Writer->WriteValue(TEXT("entity_query"), WorkerConfig.WorkerPermissions.bAllowEntityQuery); Writer->WriteObjectEnd(); + if (WorkerConfig.NumEditorInstances > 0) + { + WriteLoadbalancingSection(Writer, SpatialConstants::DefaultServerWorkerType, WorkerConfig.NumEditorInstances, + WorkerConfig.bManualWorkerConnectionOnly); + } + + Writer->WriteObjectEnd(); return true; } } // anonymous namespace -uint32 GetWorkerCountFromWorldSettings(const UWorld& World) +uint32 GetWorkerCountFromWorldSettings(const UWorld& World, bool bForceNonEditorSettings) { const ASpatialWorldSettings* WorldSettings = Cast(World.GetWorldSettings()); if (WorldSettings == nullptr) @@ -102,35 +91,13 @@ uint32 GetWorkerCountFromWorldSettings(const UWorld& World) return 1; } - const bool bIsMultiWorkerEnabled = USpatialStatics::IsSpatialMultiWorkerEnabled(&World); - if (!bIsMultiWorkerEnabled) - { - return 1; - } - - return WorldSettings->MultiWorkerSettingsClass->GetDefaultObject()->GetMinimumRequiredWorkerCount(); + return USpatialStatics::GetSpatialMultiWorkerClass(&World, bForceNonEditorSettings) + ->GetDefaultObject() + ->GetMinimumRequiredWorkerCount(); } -bool FillWorkerConfigurationFromCurrentMap(FWorkerTypeLaunchSection& OutWorker, FIntPoint& OutWorldDimensions) -{ - if (GEditor == nullptr || GEditor->GetWorldContexts().Num() == 0) - { - return false; - } - - const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); - const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); - - UWorld* EditorWorld = GEditor->GetEditorWorldContext().World(); - check(EditorWorld != nullptr); - - OutWorker = SpatialGDKEditorSettings->LaunchConfigDesc.ServerWorkerConfig; - OutWorker.NumEditorInstances = GetWorkerCountFromWorldSettings(*EditorWorld); - - return true; -} - -bool GenerateLaunchConfig(const FString& LaunchConfigPath, const FSpatialLaunchConfigDescription* InLaunchConfigDescription, const FWorkerTypeLaunchSection& InWorker) +bool GenerateLaunchConfig(const FString& LaunchConfigPath, const FSpatialLaunchConfigDescription* InLaunchConfigDescription, + bool bGenerateCloudConfig) { if (InLaunchConfigDescription != nullptr) { @@ -140,73 +107,161 @@ bool GenerateLaunchConfig(const FString& LaunchConfigPath, const FSpatialLaunchC TSharedRef> Writer = TJsonWriterFactory<>::Create(&Text); // Populate json file for launch config - Writer->WriteObjectStart(); // Start of json - Writer->WriteValue(TEXT("template"), LaunchConfigDescription.GetTemplate()); // Template section - Writer->WriteObjectStart(TEXT("world")); // World section begin - Writer->WriteObjectStart(TEXT("dimensions")); - Writer->WriteValue(TEXT("x_meters"), LaunchConfigDescription.World.Dimensions.X); - Writer->WriteValue(TEXT("z_meters"), LaunchConfigDescription.World.Dimensions.Y); - Writer->WriteObjectEnd(); - Writer->WriteValue(TEXT("chunk_edge_length_meters"), LaunchConfigDescription.World.ChunkEdgeLengthMeters); - Writer->WriteArrayStart(TEXT("legacy_flags")); - for (auto& Flag : LaunchConfigDescription.World.LegacyFlags) - { - WriteFlagSection(Writer, Flag.Key, Flag.Value); - } - Writer->WriteArrayEnd(); - Writer->WriteArrayStart(TEXT("legacy_javaparams")); - for (auto& Parameter : LaunchConfigDescription.World.LegacyJavaParams) - { - WriteFlagSection(Writer, Parameter.Key, Parameter.Value); - } - Writer->WriteArrayEnd(); - Writer->WriteObjectStart(TEXT("snapshots")); - Writer->WriteValue(TEXT("snapshot_write_period_seconds"), LaunchConfigDescription.World.SnapshotWritePeriodSeconds); - Writer->WriteObjectEnd(); - Writer->WriteObjectEnd(); // World section end - Writer->WriteObjectStart(TEXT("load_balancing")); // Load balancing section begin - Writer->WriteArrayStart("layer_configurations"); - if (InWorker.NumEditorInstances > 0) - { - WriteLoadbalancingSection(Writer, SpatialConstants::DefaultServerWorkerType, InWorker.NumEditorInstances, InWorker.bManualWorkerConnectionOnly); - } - Writer->WriteArrayEnd(); - Writer->WriteObjectEnd(); // Load balancing section end - Writer->WriteArrayStart(TEXT("workers")); // Workers section begin - if (InWorker.NumEditorInstances > 0) - { - WriteWorkerSection(Writer, SpatialConstants::DefaultServerWorkerType, InWorker); - } - // Write the client worker section - FWorkerTypeLaunchSection ClientWorker; - ClientWorker.WorkerPermissions.bAllPermissions = true; - WriteWorkerSection(Writer, SpatialConstants::DefaultClientWorkerType, ClientWorker); - Writer->WriteArrayEnd(); // Worker section end + Writer->WriteObjectStart(); + Writer->WriteArrayStart(TEXT("runtime_flags")); + for (const auto& Flag : LaunchConfigDescription.RuntimeFlags) + { + WriteFlagSection(Writer, Flag.Key, Flag.Value); + } + Writer->WriteArrayEnd(); + + Writer->WriteArrayStart(TEXT("workers")); // Workers section begin + + // Write the default server worker config (UnrealWorker) + WriteWorkerSection(Writer, InLaunchConfigDescription->ServerWorkerConfiguration); + + // Write the client worker section (UnrealClient) + FWorkerTypeLaunchSection ClientWorker; + ClientWorker.NumEditorInstances = 0; + ClientWorker.WorkerTypeName = SpatialConstants::DefaultClientWorkerType; + WriteWorkerSection(Writer, ClientWorker); + + // For cloud configs we always add the SimulatedPlayerCoordinator and DeploymentManager. + if (bGenerateCloudConfig) + { + // Write the Simulated Player Coordinator section + FWorkerTypeLaunchSection SimulatedPlayerCoordinator; + SimulatedPlayerCoordinator.NumEditorInstances = 0; + SimulatedPlayerCoordinator.WorkerTypeName = TEXT("SimulatedPlayerCoordinator"); + WriteWorkerSection(Writer, SimulatedPlayerCoordinator); + + // Write the Deployment Manager section + FWorkerTypeLaunchSection DeploymentManagerConfig; + DeploymentManagerConfig.NumEditorInstances = 0; + DeploymentManagerConfig.WorkerTypeName = TEXT("DeploymentManager"); + WriteWorkerSection(Writer, DeploymentManagerConfig); + } + + // Write any additional worker configs that may have been added. + for (const FWorkerTypeLaunchSection& AdditionalWorkerConfig : LaunchConfigDescription.AdditionalWorkerConfigs) + { + WriteWorkerSection(Writer, AdditionalWorkerConfig); + } + + Writer->WriteArrayEnd(); // Worker section end + + Writer->WriteObjectStart(TEXT("world_dimensions")); + Writer->WriteValue(TEXT("x_size"), LaunchConfigDescription.World.Dimensions.X); + Writer->WriteValue(TEXT("z_size"), LaunchConfigDescription.World.Dimensions.Y); + Writer->WriteObjectEnd(); // World section end + + Writer->WriteValue(TEXT("max_concurrent_workers"), LaunchConfigDescription.MaxConcurrentWorkers); + Writer->WriteObjectEnd(); // End of json Writer->Close(); if (!FFileHelper::SaveStringToFile(Text, *LaunchConfigPath)) { - UE_LOG(LogSpatialGDKDefaultLaunchConfigGenerator, Error, TEXT("Failed to write output file '%s'. It might be that the file is read-only."), *LaunchConfigPath); + UE_LOG(LogSpatialGDKDefaultLaunchConfigGenerator, Error, + TEXT("Failed to write output file '%s'. It might be that the file is read-only."), *LaunchConfigPath); return false; } + if (bGenerateCloudConfig) + { + // TODO: UNR-4471 - Remove classic config conversion when new cloud platform exists. + return ConvertToClassicConfig(LaunchConfigPath, InLaunchConfigDescription); + } + return true; } return false; } -bool ValidateGeneratedLaunchConfig(const FSpatialLaunchConfigDescription& LaunchConfigDesc, const FWorkerTypeLaunchSection& InWorker) +bool ConvertToClassicConfig(const FString& LaunchConfigPath, const FSpatialLaunchConfigDescription* InLaunchConfigDescription) { - const USpatialGDKSettings* SpatialGDKRuntimeSettings = GetDefault(); + // The new runtime binary handles the config conversion. We just need to pass it the parameters. + // All `runtime_flags` are converted to `legacy_flags` in the conversion process. + // `max_concurrent_workers` is also converted to a legacy flag. + // The output config file is called `launch_config.json` when printing and we must provide a path to a folder for it to be saved to. + // The output config file will replace the generated standalone config file. + // We do not export the worker configurations as we already generate these for the user still. + + FString LaunchConfigDir = FPaths::GetPath(LaunchConfigPath); + + // runtime.exe --export-classic-config=classic_config_dir --config=Game\Intermediate\Improbable\Control_Small_LocalLaunchConfig.json + // --launch-template=w2_r0500_e5 --export-worker-configuration=true + FString ConversionArgs = + FString::Printf(TEXT("--export-classic-config=\"%s\" --config=\"%s\" --launch-template=%s --export-worker-configuration=false"), + *LaunchConfigDir, *LaunchConfigPath, *InLaunchConfigDescription->GetTemplate()); + + FString Output; + int32 ExitCode; - if (const FString* EnableChunkInterest = LaunchConfigDesc.World.LegacyFlags.Find(TEXT("enable_chunk_interest"))) + const USpatialGDKEditorSettings* SpatialGDKEditor = GetDefault(); + FString RuntimePath = + SpatialGDKServicesConstants::GetRuntimeExecutablePath(SpatialGDKEditor->GetSelectedRuntimeVariantVersion().GetVersionForLocal()); + + FSpatialGDKServicesModule::ExecuteAndReadOutput(RuntimePath, ConversionArgs, FPlatformProcess::BaseDir(), Output, ExitCode); + + if (ExitCode != SpatialGDKServicesConstants::ExitCodeSuccess) + { + UE_LOG(LogSpatialGDKDefaultLaunchConfigGenerator, Error, + TEXT("Failed to convert generated launch config to classic style config for config '%s'. It might " + "be that the file is read-only. Conversion output: %s"), + *LaunchConfigPath, *Output); + return false; + } + + UE_LOG(LogSpatialGDKDefaultLaunchConfigGenerator, Verbose, + TEXT("Successfully converted generated launch config to classic style config for config '%s'. Conversion output: %s"), + *LaunchConfigPath, *Output); + + FString GeneratedClassicConfigFilePath = FPaths::Combine(LaunchConfigDir, TEXT("launch_config.json")); + + IPlatformFile& PlatformFile = IPlatformFile::GetPlatformPhysical(); + + bool bSuccess = true; + + // Delete the previously generated config that is not classic format. + if (FPaths::FileExists(LaunchConfigPath)) + { + bSuccess = PlatformFile.DeleteFile(*LaunchConfigPath); + } + + if (!bSuccess) + { + UE_LOG(LogSpatialGDKDefaultLaunchConfigGenerator, Error, + TEXT("Failed to remove local launch configuration '%s' when converting to cloud launch configuration."), *LaunchConfigPath); + return bSuccess; + } + + bSuccess = PlatformFile.MoveFile(*LaunchConfigPath, *GeneratedClassicConfigFilePath); + if (!bSuccess) + { + UE_LOG(LogSpatialGDKDefaultLaunchConfigGenerator, Error, + TEXT("Failed to rename converted classic style launch config. From: %s. To: %s"), *GeneratedClassicConfigFilePath, + *LaunchConfigPath); + } + + return bSuccess; +} + +bool ValidateGeneratedLaunchConfig(const FSpatialLaunchConfigDescription& LaunchConfigDesc) +{ + const USpatialGDKSettings* SpatialGDKRuntimeSettings = GetDefault(); + if (const FString* EnableChunkInterest = LaunchConfigDesc.RuntimeFlags.Find(TEXT("enable_chunk_interest"))) { if (*EnableChunkInterest == TEXT("true")) { - const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, LOCTEXT("ChunkInterestNotSupported_Prompt", "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?")); + const EAppReturnType::Type Result = FMessageDialog::Open( + EAppMsgType::YesNo, + LOCTEXT( + "ChunkInterestNotSupported_Prompt", + "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) { @@ -216,6 +271,7 @@ bool ValidateGeneratedLaunchConfig(const FSpatialLaunchConfigDescription& Launch return false; } } + return true; } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultWorkerJsonGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultWorkerJsonGenerator.cpp index b656a8c5b6..61be53cd01 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultWorkerJsonGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultWorkerJsonGenerator.cpp @@ -2,22 +2,22 @@ #include "SpatialGDKDefaultWorkerJsonGenerator.h" -#include "SpatialGDKSettings.h" #include "SpatialGDKServicesConstants.h" +#include "SpatialGDKSettings.h" #include "Misc/FileHelper.h" DEFINE_LOG_CATEGORY(LogSpatialGDKDefaultWorkerJsonGenerator); #define LOCTEXT_NAMESPACE "SpatialGDKDefaultWorkerJsonGenerator" -bool GenerateDefaultWorkerJson(const FString& JsonPath, const FString& WorkerTypeName, bool& bOutRedeployRequired) +bool GenerateDefaultWorkerJson(const FString& JsonPath, bool& bOutRedeployRequired) { - const FString TemplateWorkerJsonPath = FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(TEXT("SpatialGDK/Extras/templates/WorkerJsonTemplate.json")); + const FString TemplateWorkerJsonPath = + FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(TEXT("SpatialGDK/Extras/templates/WorkerJsonTemplate.json")); FString Contents; if (FFileHelper::LoadFileToString(Contents, *TemplateWorkerJsonPath)) { - Contents.ReplaceInline(TEXT("{{WorkerTypeName}}"), *WorkerTypeName); if (FFileHelper::SaveStringToFile(Contents, *JsonPath)) { bOutRedeployRequired = true; @@ -32,7 +32,8 @@ bool GenerateDefaultWorkerJson(const FString& JsonPath, const FString& WorkerTyp } else { - UE_LOG(LogSpatialGDKDefaultWorkerJsonGenerator, Error, TEXT("Failed to read default worker json template at %s"), *TemplateWorkerJsonPath) + UE_LOG(LogSpatialGDKDefaultWorkerJsonGenerator, Error, TEXT("Failed to read default worker json template at %s"), + *TemplateWorkerJsonPath) } return false; @@ -51,7 +52,7 @@ bool GenerateAllDefaultWorkerJsons(bool& bOutRedeployRequired) { UE_LOG(LogSpatialGDKDefaultWorkerJsonGenerator, Verbose, TEXT("Could not find worker json at %s"), *JsonPath); - if (!GenerateDefaultWorkerJson(JsonPath, Worker.ToString(), bOutRedeployRequired)) + if (!GenerateDefaultWorkerJson(JsonPath, bOutRedeployRequired)) { bAllJsonsGeneratedSuccessfully = false; } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDevAuthTokenGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDevAuthTokenGenerator.cpp index de589861bd..95072be8ec 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDevAuthTokenGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDevAuthTokenGenerator.cpp @@ -19,10 +19,8 @@ FSpatialGDKDevAuthTokenGenerator::FSpatialGDKDevAuthTokenGenerator() void FSpatialGDKDevAuthTokenGenerator::DoGenerateDevAuthTokenTasks() { bool bIsRunningInChina = GetDefault()->IsRunningInChina(); - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, bIsRunningInChina] - { - AsyncTask(ENamedThreads::GameThread, [this]() - { + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, bIsRunningInChina] { + AsyncTask(ENamedThreads::GameThread, [this]() { ShowTaskStartedNotification(TEXT("Generating Development Authentication Token")); }); @@ -30,17 +28,16 @@ void FSpatialGDKDevAuthTokenGenerator::DoGenerateDevAuthTokenTasks() FText ErrorMessage; if (SpatialCommandUtils::GenerateDevAuthToken(bIsRunningInChina, DevAuthToken, ErrorMessage)) { - AsyncTask(ENamedThreads::GameThread, [this, DevAuthToken]() - { + AsyncTask(ENamedThreads::GameThread, [this, DevAuthToken]() { GetMutableDefault()->SetDevelopmentAuthenticationToken(DevAuthToken); EndTask(/* bSuccess */ true); }); } else { - UE_LOG(LogSpatialGDKDevAuthTokenGenerator, Error, TEXT("Failed to generate a Development Authentication Token: %s"), *ErrorMessage.ToString()); - AsyncTask(ENamedThreads::GameThread, [this]() - { + UE_LOG(LogSpatialGDKDevAuthTokenGenerator, Error, TEXT("Failed to generate a Development Authentication Token: %s"), + *ErrorMessage.ToString()); + AsyncTask(ENamedThreads::GameThread, [this]() { EndTask(/* bSuccess */ false); }); } @@ -56,7 +53,8 @@ void FSpatialGDKDevAuthTokenGenerator::AsyncGenerateDevAuthToken() } else { - UE_LOG(LogSpatialGDKDevAuthTokenGenerator, Display, TEXT("A previous Development Authentication Token request is still pending. New request for generation ignored.")); + UE_LOG(LogSpatialGDKDevAuthTokenGenerator, Display, + TEXT("A previous Development Authentication Token request is still pending. New request for generation ignored.")); } } @@ -88,7 +86,8 @@ void FSpatialGDKDevAuthTokenGenerator::EndTask(bool bSuccess) bIsGenerating = false; } -void FSpatialGDKDevAuthTokenGenerator::ShowTaskEndedNotification(const FString& NotificationText, SNotificationItem::ECompletionState CompletionState) +void FSpatialGDKDevAuthTokenGenerator::ShowTaskEndedNotification(const FString& NotificationText, + SNotificationItem::ECompletionState CompletionState) { TSharedPtr Notification = TaskNotificationPtr.Pin(); if (Notification.IsValid()) diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp index 4525d99b34..aa0034487d 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp @@ -8,14 +8,14 @@ #include "Editor.h" #include "FileHelpers.h" #include "GeneralProjectSettings.h" -#include "Internationalization/Regex.h" #include "IUATHelperModule.h" +#include "Internationalization/Regex.h" #include "Misc/MessageDialog.h" #include "Misc/ScopedSlowTask.h" #include "PackageTools.h" #include "Settings/ProjectPackagingSettings.h" -#include "UnrealEdMisc.h" #include "UObject/StrongObjectPtr.h" +#include "UnrealEdMisc.h" #include "SpatialGDKDevAuthTokenGenerator.h" #include "SpatialGDKEditorCloudLauncher.h" @@ -31,9 +31,8 @@ DEFINE_LOG_CATEGORY(LogSpatialGDKEditor); #define LOCTEXT_NAMESPACE "FSpatialGDKEditor" -namespace +namespace { - bool CheckAutomationToolsUpToDate() { #if PLATFORM_WINDOWS @@ -48,12 +47,14 @@ bool CheckAutomationToolsUpToDate() #endif FString UatPath = FPaths::ConvertRelativePathToFull(FPaths::EngineDir() / TEXT("Build/BatchFiles") / RunUATScriptName); - + if (!FPaths::FileExists(UatPath)) { FFormatNamedArguments Arguments; Arguments.Add(TEXT("File"), FText::FromString(UatPath)); - FMessageDialog::Open(EAppMsgType::Ok, FText::Format(LOCTEXT("RequiredFileNotFoundMessage", "A required file could not be found:\n{File}"), Arguments)); + FMessageDialog::Open( + EAppMsgType::Ok, + FText::Format(LOCTEXT("RequiredFileNotFoundMessage", "A required file could not be found:\n{File}"), Arguments)); return false; } @@ -79,14 +80,15 @@ bool CheckAutomationToolsUpToDate() return true; } - FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("GenerateSchemaUATOutOfDate", - "Could not generate Schema because the AutomationTool is out of date.\n" - "Please rebuild the AutomationTool project which can be found alongside the UE4 project files")); + FMessageDialog::Open(EAppMsgType::Ok, + LOCTEXT("GenerateSchemaUATOutOfDate", + "Could not generate Schema because the AutomationTool is out of date.\n" + "Please rebuild the AutomationTool project which can be found alongside the UE4 project files")); return false; } -} +} // namespace FSpatialGDKEditor::FSpatialGDKEditor() : bSchemaGeneratorRunning(false) @@ -95,18 +97,20 @@ FSpatialGDKEditor::FSpatialGDKEditor() { } -bool FSpatialGDKEditor::GenerateSchema(ESchemaGenerationMethod Method) +void FSpatialGDKEditor::GenerateSchema(ESchemaGenerationMethod Method, TFunction ResultCallback) { if (bSchemaGeneratorRunning) { UE_LOG(LogSpatialGDKEditor, Warning, TEXT("Schema generation is already running")); - return false; + ResultCallback(false); + return; } if (!FPaths::IsProjectFilePathSet()) { UE_LOG(LogSpatialGDKEditor, Error, TEXT("Schema generation called when no project was opened")); - return false; + ResultCallback(false); + return; } // If this has been run from an open editor then prompt the user to save dirty packages and maps. @@ -118,23 +122,27 @@ bool FSpatialGDKEditor::GenerateSchema(ESchemaGenerationMethod Method) const bool bFastSave = false; const bool bNotifyNoPackagesSaved = false; const bool bCanBeDeclined = true; - if (!FEditorFileUtils::SaveDirtyPackages(bPromptUserToSave, bSaveMapPackages, bSaveContentPackages, bFastSave, bNotifyNoPackagesSaved, bCanBeDeclined)) + if (!FEditorFileUtils::SaveDirtyPackages(bPromptUserToSave, bSaveMapPackages, bSaveContentPackages, bFastSave, + bNotifyNoPackagesSaved, bCanBeDeclined)) { // User hit cancel don't generate schema. - return false; + ResultCallback(false); + return; } } if (Schema::IsAssetReadOnly(SpatialConstants::SCHEMA_DATABASE_FILE_PATH)) { - return false; + ResultCallback(false); + return; } if (Method == FullAssetScan) { if (!CheckAutomationToolsUpToDate()) { - return false; + ResultCallback(false); + return; } // Make sure SchemaDatabase is not loaded. @@ -152,34 +160,36 @@ bool FSpatialGDKEditor::GenerateSchema(ESchemaGenerationMethod Method) if (PlatformName.IsEmpty()) { UE_LOG(LogSpatialGDKEditor, Error, TEXT("Empty platform passed to CookAndGenerateSchema")); - return false; + ResultCallback(false); + return; } FString OptionalParams = EditorSettings->GetCookAndGenerateSchemaAdditionalArgs(); OptionalParams += FString::Printf(TEXT(" -targetplatform=%s"), *PlatformName); FString ProjectPath = FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath()); - FString UATCommandLine = FString::Printf(TEXT("-ScriptsForProject=\"%s\" CookAndGenerateSchema -nocompile -nocompileeditor -server -noclient %s -nop4 -project=\"%s\" -cook -skipstage -ue4exe=\"%s\" %s -utf8output"), - *ProjectPath, - FApp::IsEngineInstalled() ? TEXT(" -installed") : TEXT(""), - *ProjectPath, - *FUnrealEdMisc::Get().GetExecutableForCommandlets(), - *OptionalParams - ); - - IUATHelperModule::Get().CreateUatTask(UATCommandLine, - FText::FromString(PlatformName), - LOCTEXT("CookAndGenerateSchemaTaskName", "Cook and generate project schema"), - LOCTEXT("CookAndGenerateSchemaTaskShortName", "Generating Schema"), - FEditorStyle::GetBrush(TEXT("MainFrame.PackageProject"))); + FString UATCommandLine = FString::Printf(TEXT("-ScriptsForProject=\"%s\" CookAndGenerateSchema -nocompile -nocompileeditor -server " + "-noclient %s -nop4 -project=\"%s\" -cook -skipstage -ue4exe=\"%s\" %s -utf8output"), + *ProjectPath, FApp::IsEngineInstalled() ? TEXT(" -installed") : TEXT(""), *ProjectPath, + *FUnrealEdMisc::Get().GetExecutableForCommandlets(), *OptionalParams); - return true; + bSchemaGeneratorRunning = true; + TFunction Callback = [this, ResultCallback = MoveTemp(ResultCallback)](const FString& UATResult, double) { + ResultCallback(UATResult == FString(TEXT("Completed"))); + bSchemaGeneratorRunning = false; + }; + IUATHelperModule::Get().CreateUatTask(UATCommandLine, FText::FromString(PlatformName), + LOCTEXT("CookAndGenerateSchemaTaskName", "Cook and generate project schema"), + LOCTEXT("CookAndGenerateSchemaTaskShortName", "Generating schema"), + FEditorStyle::GetBrush(TEXT("MainFrame.PackageProject")), MoveTemp(Callback)); + + return; } else { bSchemaGeneratorRunning = true; - FScopedSlowTask Progress(100.f, LOCTEXT("GeneratingSchema", "Generating Schema...")); + FScopedSlowTask Progress(100.f, LOCTEXT("GeneratingSchema", "Generating schema...")); Progress.MakeDialog(true); RemoveEditorAssetLoadedCallback(); @@ -204,7 +214,9 @@ bool FSpatialGDKEditor::GenerateSchema(ESchemaGenerationMethod Method) // We delay printing this error until after the schema spam to make it have a higher chance of being noticed. if (ErroredBlueprints.Num() > 0) { - UE_LOG(LogSpatialGDKEditor, Error, TEXT("Errors compiling blueprints during schema generation! The following blueprints did not have schema generated for them:")); + UE_LOG(LogSpatialGDKEditor, Error, + TEXT("Errors compiling blueprints during schema generation! The following blueprints did not have schema generated for " + "them:")); for (const auto& Blueprint : ErroredBlueprints) { UE_LOG(LogSpatialGDKEditor, Error, TEXT("%s"), *GetPathNameSafe(Blueprint)); @@ -215,22 +227,27 @@ bool FSpatialGDKEditor::GenerateSchema(ESchemaGenerationMethod Method) if (bResult) { - UE_LOG(LogSpatialGDKEditor, Display, TEXT("Schema Generation succeeded!")); + UE_LOG(LogSpatialGDKEditor, Display, TEXT("Schema generation succeeded!")); } else { - UE_LOG(LogSpatialGDKEditor, Error, TEXT("Schema Generation failed. View earlier log messages for errors.")); + UE_LOG(LogSpatialGDKEditor, Error, TEXT("Schema generation failed. View earlier log messages for errors.")); } - return bResult; + ResultCallback(bResult); + return; } } -bool FSpatialGDKEditor::IsSchemaGenerated() +FSpatialGDKEditor::ESchemaDatabaseValidationResult FSpatialGDKEditor::ValidateSchemaDatabase() { - 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(); + if (!FPaths::DirectoryExists(GdkFolderPath)) + { + return ESchemaDatabaseValidationResult::NotFound; + } + + return SpatialGDKEditor::Schema::ValidateSchemaDatabase(); } bool FSpatialGDKEditor::LoadPotentialAssets(TArray>& OutAssets) @@ -246,8 +263,7 @@ bool FSpatialGDKEditor::LoadPotentialAssets(TArray>& O const TArray& DirectoriesToNeverCook = GetDefault()->DirectoriesToNeverCook; // Filter assets to game blueprint classes that are not loaded and not inside DirectoriesToNeverCook. - FoundAssets = FoundAssets.FilterByPredicate([&DirectoriesToNeverCook](const FAssetData& Data) - { + FoundAssets = FoundAssets.FilterByPredicate([&DirectoriesToNeverCook](const FAssetData& Data) { if (Data.IsAssetLoaded()) { return false; @@ -268,7 +284,9 @@ bool FSpatialGDKEditor::LoadPotentialAssets(TArray>& O return true; }); - FScopedSlowTask Progress(static_cast(FoundAssets.Num()), FText::Format(LOCTEXT("LoadingAssets_Text", "Loading {0} Assets before generating schema"), FoundAssets.Num())); + FScopedSlowTask Progress( + static_cast(FoundAssets.Num()), + FText::Format(LOCTEXT("LoadingAssets_Text", "Loading {0} assets before generating schema"), FoundAssets.Num())); for (const FAssetData& Data : FoundAssets) { @@ -298,10 +316,11 @@ bool FSpatialGDKEditor::LoadPotentialAssets(TArray>& O return true; } -void FSpatialGDKEditor::GenerateSnapshot(UWorld* World, FString SnapshotFilename, FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback, FSpatialGDKEditorErrorHandler ErrorCallback) +void FSpatialGDKEditor::GenerateSnapshot(UWorld* World, FString SnapshotFilename, FSimpleDelegate SuccessCallback, + FSimpleDelegate FailureCallback, FSpatialGDKEditorErrorHandler ErrorCallback) { const USpatialGDKEditorSettings* Settings = GetDefault(); - FString SavePath = FPaths::Combine(Settings->GetSpatialOSSnapshotFolderPath(), SnapshotFilename); + FString SavePath = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSSnapshotFolderPath, SnapshotFilename); const bool bSuccess = SpatialGDKGenerateSnapshot(World, SavePath); if (bSuccess) @@ -314,11 +333,15 @@ void FSpatialGDKEditor::GenerateSnapshot(UWorld* World, FString SnapshotFilename } } -void FSpatialGDKEditor::StartCloudDeployment(const FCloudDeploymentConfiguration& Configuration, FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback) +void FSpatialGDKEditor::StartCloudDeployment(const FCloudDeploymentConfiguration& Configuration, FSimpleDelegate SuccessCallback, + FSimpleDelegate FailureCallback) { - LaunchCloudResult = Async(EAsyncExecution::Thread, [&Configuration]() { return SpatialGDKCloudLaunch(Configuration); }, - [this, SuccessCallback, FailureCallback] - { + LaunchCloudResult = Async( + EAsyncExecution::Thread, + [&Configuration]() { + return SpatialGDKCloudLaunch(Configuration); + }, + [this, SuccessCallback, FailureCallback] { if (!LaunchCloudResult.IsReady() || LaunchCloudResult.Get() != true) { FailureCallback.ExecuteIfBound(); @@ -332,23 +355,21 @@ void FSpatialGDKEditor::StartCloudDeployment(const FCloudDeploymentConfiguration void FSpatialGDKEditor::StopCloudDeployment(FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback) { - StopCloudResult = Async(EAsyncExecution::Thread, SpatialGDKCloudStop, - [this, SuccessCallback, FailureCallback] + StopCloudResult = Async(EAsyncExecution::Thread, SpatialGDKCloudStop, [this, SuccessCallback, FailureCallback] { + if (!StopCloudResult.IsReady() || StopCloudResult.Get() != true) { - if (!StopCloudResult.IsReady() || StopCloudResult.Get() != true) - { - FailureCallback.ExecuteIfBound(); - } - else - { - SuccessCallback.ExecuteIfBound(); - } - }); + FailureCallback.ExecuteIfBound(); + } + else + { + SuccessCallback.ExecuteIfBound(); + } + }); } bool FSpatialGDKEditor::FullScanRequired() { - return !Schema::GeneratedSchemaFolderExists() || !Schema::GeneratedSchemaDatabaseExists(); + return !Schema::GeneratedSchemaFolderExists() || (Schema::ValidateSchemaDatabase() != FSpatialGDKEditor::Ok); } void FSpatialGDKEditor::SetProjectName(const FString& InProjectName) @@ -371,12 +392,12 @@ void FSpatialGDKEditor::RemoveEditorAssetLoadedCallback() { UE_LOG(LogSpatialGDKEditor, Verbose, TEXT("Removing UEditorEngine::OnAssetLoaded.")); FCoreUObjectDelegates::OnAssetLoaded.RemoveAll(GEditor); - UE_LOG(LogSpatialGDKEditor, Verbose, TEXT("Replacing UEditorEngine::OnAssetLoaded with spatial version that won't run during schema gen.")); + UE_LOG(LogSpatialGDKEditor, Verbose, + TEXT("Replacing UEditorEngine::OnAssetLoaded with spatial version that won't run during schema gen.")); OnAssetLoadedHandle = FCoreUObjectDelegates::OnAssetLoaded.AddLambda([this](UObject* Asset) { OnAssetLoaded(Asset); }); } - } // This callback is copied from UEditorEngine::OnAssetLoaded so that we can turn it off during schema gen in editor. @@ -393,14 +414,14 @@ void FSpatialGDKEditor::OnAssetLoaded(UObject* Asset) // Init inactive worlds here instead of UWorld::PostLoad because it is illegal to call UpdateWorldComponents while IsRoutingPostLoad if (!World->bIsWorldInitialized && World->WorldType == EWorldType::Inactive) { - // Create the world without a physics scene because creating too many physics scenes causes deadlock issues in PhysX. The scene will be created when it is opened in the level editor. - // Also, don't create an FXSystem because it consumes too much video memory. This is also created when the level editor opens this world. + // Create the world without a physics scene because creating too many physics scenes causes deadlock issues in PhysX. The scene + // will be created when it is opened in the level editor. Also, don't create an FXSystem because it consumes too much video + // memory. This is also created when the level editor opens this world. World->InitWorld(UWorld::InitializationValues() - .ShouldSimulatePhysics(false) - .EnableTraceCollision(true) - .CreatePhysicsScene(false) - .CreateFXSystem(false) - ); + .ShouldSimulatePhysics(false) + .EnableTraceCollision(true) + .CreatePhysicsScene(false) + .CreateFXSystem(false)); // Update components so the scene is populated World->UpdateWorldComponents(true, true); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCloudLauncher.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCloudLauncher.cpp index f87dad6f63..d9fe5a5066 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCloudLauncher.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCloudLauncher.cpp @@ -11,35 +11,24 @@ DEFINE_LOG_CATEGORY(LogSpatialGDKEditorCloudLauncher); namespace { - const FString LauncherExe = FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(TEXT("SpatialGDK/Binaries/ThirdParty/Improbable/Programs/DeploymentLauncher/DeploymentLauncher.exe")); +const FString LauncherExe = FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory( + TEXT("SpatialGDK/Binaries/ThirdParty/Improbable/Programs/DeploymentLauncher/DeploymentLauncher.exe")); } bool SpatialGDKCloudLaunch(const FCloudDeploymentConfiguration& Configuration) { - FString LauncherCreateArguments = FString::Printf( - TEXT("create %s %s %s %s \"%s\" \"%s\" %s \"%s\" \"%s\""), - *FSpatialGDKServicesModule::GetProjectName(), - *Configuration.AssemblyName, - *Configuration.RuntimeVersion, - *Configuration.PrimaryDeploymentName, - *Configuration.PrimaryLaunchConfigPath, - *Configuration.SnapshotPath, - *Configuration.PrimaryRegionCode, - *Configuration.MainDeploymentCluster, - *Configuration.DeploymentTags - ); + FString LauncherCreateArguments = + FString::Printf(TEXT("create %s %s %s %s \"%s\" \"%s\" %s \"%s\" \"%s\""), *FSpatialGDKServicesModule::GetProjectName(), + *Configuration.AssemblyName, *Configuration.RuntimeVersion, *Configuration.PrimaryDeploymentName, + *Configuration.PrimaryLaunchConfigPath, *Configuration.SnapshotPath, *Configuration.PrimaryRegionCode, + *Configuration.MainDeploymentCluster, *Configuration.DeploymentTags); if (Configuration.bSimulatedPlayersEnabled) { - LauncherCreateArguments = FString::Printf( - TEXT("%s %s \"%s\" %s \"%s\" %u"), - *LauncherCreateArguments, - *Configuration.SimulatedPlayerDeploymentName, - *Configuration.SimulatedPlayerLaunchConfigPath, - *Configuration.SimulatedPlayerRegionCode, - *Configuration.SimulatedPlayerCluster, - Configuration.NumberOfSimulatedPlayers - ); + LauncherCreateArguments = + FString::Printf(TEXT("%s %s \"%s\" %s \"%s\" %u"), *LauncherCreateArguments, *Configuration.SimulatedPlayerDeploymentName, + *Configuration.SimulatedPlayerLaunchConfigPath, *Configuration.SimulatedPlayerRegionCode, + *Configuration.SimulatedPlayerCluster, Configuration.NumberOfSimulatedPlayers); } if (Configuration.bUseChinaPlatform) @@ -64,7 +53,7 @@ bool SpatialGDKCloudLaunch(const FCloudDeploymentConfiguration& Configuration) return bSuccess; } - + bool SpatialGDKCloudStop() { UE_LOG(LogSpatialGDKEditorCloudLauncher, Error, TEXT("Function not available")); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCommandLineArgsManager.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCommandLineArgsManager.cpp index ce1904b042..6d4d396428 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCommandLineArgsManager.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCommandLineArgsManager.cpp @@ -84,14 +84,15 @@ void FSpatialGDKEditorCommandLineArgsManager::OnCreateLauncher(ILauncherRef Laun namespace { - FString GetAdbExePath() { FString AndroidHome = FPlatformMisc::GetEnvironmentVariable(TEXT("ANDROID_HOME")); if (AndroidHome.IsEmpty()) { - UE_LOG(LogSpatialGDKEditorCommandLineArgsManager, Error, TEXT("Environment variable ANDROID_HOME is not set. Please make sure to configure this.")); - FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("AndroidHomeNotSet_Error", "Environment variable ANDROID_HOME is not set. Please make sure to configure this.")); + UE_LOG(LogSpatialGDKEditorCommandLineArgsManager, Error, + TEXT("Environment variable ANDROID_HOME is not set. Please make sure to configure this.")); + FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("AndroidHomeNotSet_Error", + "Environment variable ANDROID_HOME is not set. Please make sure to configure this.")); return TEXT(""); } @@ -116,8 +117,11 @@ FReply FSpatialGDKEditorCommandLineArgsManager::PushCommandLineToIOSDevice() 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); + 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); @@ -147,7 +151,8 @@ FReply FSpatialGDKEditorCommandLineArgsManager::PushCommandLineToAndroidDevice() return FReply::Unhandled(); } - const FString AndroidCommandLineFile = FString::Printf(TEXT("/mnt/sdcard/UE4Game/%s/UE4CommandLine.txt"), *FString(FApp::GetProjectName())); + 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 (!TryPushCommandLineArgsToDevice(AdbExe, AdbArguments, OutCommandLineArgsFile)) @@ -175,11 +180,15 @@ FReply FSpatialGDKEditorCommandLineArgsManager::RemoveCommandLineFromAndroidDevi FPlatformProcess::ExecProcess(*AdbExe, *ExeArguments, &ExitCode, &ExeOutput, &StdErr); if (ExitCode != 0) { - UE_LOG(LogSpatialGDKEditorCommandLineArgsManager, Error, TEXT("Failed to remove settings from the mobile client. %s %s"), *ExeOutput, *StdErr); - FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("FailedToRemoveMobileSettings_Error", "Failed to remove settings from the mobile client. See the Output log for more information.")); + UE_LOG(LogSpatialGDKEditorCommandLineArgsManager, Error, TEXT("Failed to remove settings from the mobile client. %s %s"), + *ExeOutput, *StdErr); + FMessageDialog::Open(EAppMsgType::Ok, + LOCTEXT("FailedToRemoveMobileSettings_Error", + "Failed to remove settings from the mobile client. See the Output log for more information.")); return FReply::Unhandled(); } - UE_LOG(LogSpatialGDKEditorCommandLineArgsManager, Log, TEXT("Remove ue4commandline.txt from the Android device. %s %s"), *ExeOutput, *StdErr); + UE_LOG(LogSpatialGDKEditorCommandLineArgsManager, Log, TEXT("Remove ue4commandline.txt from the Android device. %s %s"), *ExeOutput, + *StdErr); return FReply::Handled(); } @@ -189,7 +198,8 @@ bool FSpatialGDKEditorCommandLineArgsManager::TryConstructMobileCommandLineArgum 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 + // 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)); @@ -210,8 +220,11 @@ bool FSpatialGDKEditorCommandLineArgsManager::TryConstructMobileCommandLineArgum if (RuntimeIP.IsEmpty()) { - UE_LOG(LogSpatialGDKEditorCommandLineArgsManager, Error, TEXT("The Runtime IP is currently not set. Please make sure to specify a Runtime IP.")); - FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("RuntimeIPNotSet_Error", "The Runtime IP is currently not set. Please make sure to specify a Runtime IP.")); + UE_LOG(LogSpatialGDKEditorCommandLineArgsManager, Error, + TEXT("The Runtime IP is currently not set. Please make sure to specify a Runtime IP.")); + FMessageDialog::Open( + EAppMsgType::Ok, + LOCTEXT("RuntimeIPNotSet_Error", "The Runtime IP is currently not set. Please make sure to specify a Runtime IP.")); return false; } @@ -222,7 +235,8 @@ bool FSpatialGDKEditorCommandLineArgsManager::TryConstructMobileCommandLineArgum else if (ConnectionFlow == ESpatialOSNetFlow::CloudDeployment) { // 127.0.0.1 is only used to indicate that we want to connect to a deployment. - // This address won't be used when actually trying to connect, but Unreal will try to resolve the address and close the connection if it fails. + // This address won't be used when actually trying to connect, but Unreal will try to resolve the address and close the connection + // if it fails. TravelUrl = TEXT("127.0.0.1"); if (SpatialGDKSettings->DevelopmentAuthenticationToken.IsEmpty()) @@ -241,13 +255,18 @@ bool FSpatialGDKEditorCommandLineArgsManager::TryConstructMobileCommandLineArgum } } - const FString SpatialOSCommandLineArgs = FString::Printf(TEXT("%s %s %s %s"), *MobileProjectPath, *TravelUrl, *SpatialOSOptions, *(SpatialGDKSettings->MobileExtraCommandLineArgs)); + const FString SpatialOSCommandLineArgs = FString::Printf(TEXT("%s %s %s %s"), *MobileProjectPath, *TravelUrl, *SpatialOSOptions, + *(SpatialGDKSettings->MobileExtraCommandLineArgs)); OutCommandLineArgsFile = FPaths::ConvertRelativePathToFull(FPaths::Combine(*FPaths::ProjectLogDir(), TEXT("ue4commandline.txt"))); - if (!FFileHelper::SaveStringToFile(SpatialOSCommandLineArgs, *OutCommandLineArgsFile, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) + if (!FFileHelper::SaveStringToFile(SpatialOSCommandLineArgs, *OutCommandLineArgsFile, + FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) { - UE_LOG(LogSpatialGDKEditorCommandLineArgsManager, Error, TEXT("Failed to write command line args to file: %s"), *OutCommandLineArgsFile); - FMessageDialog::Open(EAppMsgType::Ok, FText::Format(LOCTEXT("FailedToWriteCommandLine_Error", "Failed to write command line args to file: {0}"), FText::FromString(OutCommandLineArgsFile))); + UE_LOG(LogSpatialGDKEditorCommandLineArgsManager, Error, TEXT("Failed to write command line args to file: %s"), + *OutCommandLineArgsFile); + FMessageDialog::Open(EAppMsgType::Ok, + FText::Format(LOCTEXT("FailedToWriteCommandLine_Error", "Failed to write command line args to file: {0}"), + FText::FromString(OutCommandLineArgsFile))); return false; } @@ -258,7 +277,8 @@ FReply FSpatialGDKEditorCommandLineArgsManager::GenerateDevAuthToken() { FString DevAuthToken; FText ErrorMessage; - if (!SpatialCommandUtils::GenerateDevAuthToken(GetMutableDefault()->IsRunningInChina(), DevAuthToken, ErrorMessage)) + if (!SpatialCommandUtils::GenerateDevAuthToken(GetMutableDefault()->IsRunningInChina(), DevAuthToken, + ErrorMessage)) { FMessageDialog::Open(EAppMsgType::Ok, ErrorMessage); return FReply::Unhandled(); @@ -269,7 +289,8 @@ FReply FSpatialGDKEditorCommandLineArgsManager::GenerateDevAuthToken() return FReply::Handled(); } -bool FSpatialGDKEditorCommandLineArgsManager::TryPushCommandLineArgsToDevice(const FString& Executable, const FString& ExeArguments, const FString& CommandLineArgsFile) +bool FSpatialGDKEditorCommandLineArgsManager::TryPushCommandLineArgsToDevice(const FString& Executable, const FString& ExeArguments, + const FString& CommandLineArgsFile) { FString ExeOutput; FString StdErr; @@ -279,7 +300,8 @@ bool FSpatialGDKEditorCommandLineArgsManager::TryPushCommandLineArgsToDevice(con if (ExitCode != 0) { UE_LOG(LogSpatialGDKEditorCommandLineArgsManager, Error, TEXT("Failed to update the mobile client. %s %s"), *ExeOutput, *StdErr); - FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("FailedToPushCommandLine_Error", "Failed to update the mobile client. See the Output log for more information.")); + FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("FailedToPushCommandLine_Error", + "Failed to update the mobile client. See the Output log for more information.")); return false; } @@ -288,7 +310,8 @@ bool FSpatialGDKEditorCommandLineArgsManager::TryPushCommandLineArgsToDevice(con if (!PlatformFile.DeleteFile(*CommandLineArgsFile)) { UE_LOG(LogSpatialGDKEditorCommandLineArgsManager, Error, TEXT("Failed to delete file %s"), *CommandLineArgsFile); - FMessageDialog::Open(EAppMsgType::Ok, FText::Format(LOCTEXT("FailedToDeleteFile_Error", "Failed to delete file {0}"), FText::FromString(CommandLineArgsFile))); + FMessageDialog::Open(EAppMsgType::Ok, FText::Format(LOCTEXT("FailedToDeleteFile_Error", "Failed to delete file {0}"), + FText::FromString(CommandLineArgsFile))); return false; } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorLayoutDetails.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorLayoutDetails.cpp index d70d369a68..46a5fb649c 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorLayoutDetails.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorLayoutDetails.cpp @@ -50,95 +50,57 @@ void FSpatialGDKEditorLayoutDetails::CustomizeDetails(IDetailLayoutBuilder& Deta IDetailCategoryBuilder& CloudConnectionCategory = DetailBuilder.EditCategory("Cloud Connection"); CloudConnectionCategory.AddCustomRow(LOCTEXT("ProjectName_Filter", "Project Name")) - .NameContent() - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("ProjectName_Label", "Project Name")) - .ToolTipText(LOCTEXT("ProjectName_Tooltip", "The name of the SpatialOS project.")) - ] - ] + .NameContent()[SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(STextBlock) + .Text(LOCTEXT("ProjectName_Label", "Project Name")) + .ToolTipText(LOCTEXT("ProjectName_Tooltip", "The name of the SpatialOS project."))]] .ValueContent() .VAlign(VAlign_Center) - .MinDesiredWidth(250) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SEditableTextBox) - .Text(FText::FromString(ProjectName)) - .ToolTipText(LOCTEXT("ProjectName_Tooltip", "The name of the SpatialOS project.")) - .OnTextCommitted(this, &FSpatialGDKEditorLayoutDetails::OnProjectNameCommitted) - .ErrorReporting(ProjectNameInputErrorReporting) - ] - ]; + .MinDesiredWidth(250)[SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(SEditableTextBox) + .Text(FText::FromString(ProjectName)) + .ToolTipText(LOCTEXT("ProjectName_Tooltip", "The name of the SpatialOS project.")) + .OnTextCommitted(this, &FSpatialGDKEditorLayoutDetails::OnProjectNameCommitted) + .ErrorReporting(ProjectNameInputErrorReporting)]]; CloudConnectionCategory.AddCustomRow(LOCTEXT("GenerateDevAuthToken_Filter", "Generate Development Authentication Token")) .ValueContent() .VAlign(VAlign_Center) - .MinDesiredWidth(250) - [ - SNew(SButton) - .VAlign(VAlign_Center) - .OnClicked_Static(FSpatialGDKEditorCommandLineArgsManager::GenerateDevAuthToken) - .Content() - [ - SNew(STextBlock) - .Text(LOCTEXT("GenerateDevAuthToken_Label", "Generate Dev Auth Token")) - ] - ]; - + .MinDesiredWidth(250)[SNew(SButton) + .VAlign(VAlign_Center) + .OnClicked_Static(FSpatialGDKEditorCommandLineArgsManager::GenerateDevAuthToken) + .Content()[SNew(STextBlock).Text(LOCTEXT("GenerateDevAuthToken_Label", "Generate Dev Auth Token"))]]; + IDetailCategoryBuilder& MobileCategory = DetailBuilder.EditCategory("Mobile"); MobileCategory.AddCustomRow(LOCTEXT("PushCommandLineAndroid_Filter", "Push SpatialOS settings to Android device")) .ValueContent() .VAlign(VAlign_Center) - .MinDesiredWidth(550) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SButton) - .VAlign(VAlign_Center) - .OnClicked_Static(FSpatialGDKEditorCommandLineArgsManager::PushCommandLineToAndroidDevice) - .Content() - [ - SNew(STextBlock) - .Text(LOCTEXT("PushCommandLineAndroid_Label", "Push SpatialOS settings to Android device")) - ] - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SButton) - .VAlign(VAlign_Center) - .OnClicked_Static(FSpatialGDKEditorCommandLineArgsManager::RemoveCommandLineFromAndroidDevice) - .Content() - [ - SNew(STextBlock) - .Text(LOCTEXT("RemoveCommandLineAndroid_Label", "Remove SpatialOS settings from Android device")) - ] - ] - ]; + .MinDesiredWidth( + 550)[SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(SButton) + .VAlign(VAlign_Center) + .OnClicked_Static(FSpatialGDKEditorCommandLineArgsManager::PushCommandLineToAndroidDevice) + .Content()[SNew(STextBlock) + .Text(LOCTEXT("PushCommandLineAndroid_Label", "Push SpatialOS settings to Android device"))]] + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(SButton) + .VAlign(VAlign_Center) + .OnClicked_Static(FSpatialGDKEditorCommandLineArgsManager::RemoveCommandLineFromAndroidDevice) + .Content()[SNew(STextBlock) + .Text(LOCTEXT("RemoveCommandLineAndroid_Label", + "Remove SpatialOS settings from Android device"))]]]; MobileCategory.AddCustomRow(LOCTEXT("PushCommandLineIOS_Filter", "Push SpatialOS settings to iOS device")) .ValueContent() .VAlign(VAlign_Center) - .MinDesiredWidth(275) - [ - SNew(SButton) - .VAlign(VAlign_Center) - .OnClicked_Static(FSpatialGDKEditorCommandLineArgsManager::PushCommandLineToIOSDevice) - .Content() - [ - SNew(STextBlock) - .Text(LOCTEXT("PushCommandLineIOS_Label", "Push SpatialOS settings to iOS device")) - ] - ]; + .MinDesiredWidth( + 275)[SNew(SButton) + .VAlign(VAlign_Center) + .OnClicked_Static(FSpatialGDKEditorCommandLineArgsManager::PushCommandLineToIOSDevice) + .Content()[SNew(STextBlock).Text(LOCTEXT("PushCommandLineIOS_Label", "Push SpatialOS settings to iOS device"))]]; } void FSpatialGDKEditorLayoutDetails::OnProjectNameCommitted(const FText& InText, ETextCommit::Type InCommitType) @@ -151,7 +113,8 @@ void FSpatialGDKEditorLayoutDetails::OnProjectNameCommitted(const FText& InText, } ProjectNameInputErrorReporting->SetError(TEXT("")); - TSharedPtr SpatialGDKEditorInstance = FModuleManager::GetModuleChecked("SpatialGDKEditor").GetSpatialGDKEditorInstance(); + TSharedPtr SpatialGDKEditorInstance = + FModuleManager::GetModuleChecked("SpatialGDKEditor").GetSpatialGDKEditorInstance(); SpatialGDKEditorInstance->SetProjectName(NewProjectName); } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorModule.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorModule.cpp index 840c54f0b2..d2863b20aa 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorModule.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorModule.cpp @@ -2,8 +2,8 @@ #include "SpatialGDKEditorModule.h" -#include "GeneralProjectSettings.h" #include "Editor.h" +#include "GeneralProjectSettings.h" #include "ISettingsContainer.h" #include "ISettingsModule.h" #include "ISettingsSection.h" @@ -19,10 +19,17 @@ #include "SpatialGDKEditorPackageAssembly.h" #include "SpatialGDKEditorSchemaGenerator.h" #include "SpatialGDKEditorSettings.h" +#include "SpatialGDKFunctionalTests/Public/SpatialFunctionalTest.h" #include "SpatialGDKSettings.h" #include "SpatialLaunchConfigCustomization.h" -#include "Utils/LaunchConfigurationEditor.h" #include "SpatialRuntimeVersionCustomization.h" + +#include "Engine/World.h" +#include "EngineClasses/SpatialWorldSettings.h" +#include "EngineUtils.h" +#include "IAutomationControllerModule.h" +#include "SpatialFunctionalTest.h" +#include "Utils/LaunchConfigurationEditor.h" #include "WorkerTypeCustomization.h" DEFINE_LOG_CATEGORY(LogSpatialGDKEditorModule); @@ -32,7 +39,6 @@ DEFINE_LOG_CATEGORY(LogSpatialGDKEditorModule); FSpatialGDKEditorModule::FSpatialGDKEditorModule() : CommandLineArgsManager(MakeUnique()) { - } void FSpatialGDKEditorModule::StartupModule() @@ -45,6 +51,24 @@ void FSpatialGDKEditorModule::StartupModule() // This is relying on the module loading phase - SpatialGDKServices module should be already loaded FSpatialGDKServicesModule& GDKServices = FModuleManager::GetModuleChecked("SpatialGDKServices"); LocalReceptionistProxyServerManager = GDKServices.GetLocalReceptionistProxyServerManager(); + + // Allow Spatial Plugin to stop PIE after Automation Manager completes the tests + IAutomationControllerModule& AutomationControllerModule = + FModuleManager::LoadModuleChecked(TEXT("AutomationController")); + IAutomationControllerManagerPtr AutomationController = AutomationControllerModule.GetAutomationController(); + AutomationController->OnTestsComplete().AddLambda([]() { + // Make sure to clear the snapshot in case something happened with Tests (or they weren't ran properly). + ASpatialFunctionalTest::ClearAllTakenSnapshots(); + +#if ENGINE_MINOR_VERSION < 25 + if (GetDefault()->bStopPIEOnTestingCompleted && GEditor->EditorWorld != nullptr) +#else + if (GetDefault()->bStopPIEOnTestingCompleted && GEditor->IsPlayingSessionInEditor()) +#endif + { + GEditor->EndPlayMap(); + } + }); } void FSpatialGDKEditorModule::ShutdownModule() @@ -55,9 +79,16 @@ void FSpatialGDKEditorModule::ShutdownModule() } } +void FSpatialGDKEditorModule::TakeSnapshot(UWorld* World, FSpatialSnapshotTakenFunc OnSnapshotTaken) +{ + FSpatialGDKServicesModule& GDKServices = FModuleManager::GetModuleChecked("SpatialGDKServices"); + GDKServices.GetLocalDeploymentManager()->TakeSnapshot(World, OnSnapshotTaken); +} + bool FSpatialGDKEditorModule::ShouldConnectToLocalDeployment() const { - return GetDefault()->UsesSpatialNetworking() && GetDefault()->SpatialOSNetFlowType == ESpatialOSNetFlow::LocalDeployment; + return GetDefault()->UsesSpatialNetworking() + && GetDefault()->SpatialOSNetFlowType == ESpatialOSNetFlow::LocalDeployment; } FString FSpatialGDKEditorModule::GetSpatialOSLocalDeploymentIP() const @@ -72,7 +103,8 @@ bool FSpatialGDKEditorModule::ShouldStartPIEClientsWithLocalLaunchOnDevice() con bool FSpatialGDKEditorModule::ShouldConnectToCloudDeployment() const { - return GetDefault()->UsesSpatialNetworking() && GetDefault()->SpatialOSNetFlowType == ESpatialOSNetFlow::CloudDeployment; + return GetDefault()->UsesSpatialNetworking() + && GetDefault()->SpatialOSNetFlowType == ESpatialOSNetFlow::CloudDeployment; } FString FSpatialGDKEditorModule::GetDevAuthToken() const @@ -95,7 +127,9 @@ bool FSpatialGDKEditorModule::TryStartLocalReceptionistProxyServer() const if (ShouldConnectToCloudDeployment() && ShouldConnectServerToCloud()) { const USpatialGDKEditorSettings* EditorSettings = GetDefault(); - bool bSuccess = LocalReceptionistProxyServerManager->TryStartReceptionistProxyServer(GetDefault()->IsRunningInChina(), EditorSettings->GetPrimaryDeploymentName(), EditorSettings->ListeningAddress, EditorSettings->LocalReceptionistPort); + bool bSuccess = LocalReceptionistProxyServerManager->TryStartReceptionistProxyServer( + GetDefault()->IsRunningInChina(), EditorSettings->GetPrimaryDeploymentName(), + EditorSettings->ListeningAddress, EditorSettings->LocalReceptionistPort); if (bSuccess) { @@ -103,7 +137,9 @@ bool FSpatialGDKEditorModule::TryStartLocalReceptionistProxyServer() const } else { - FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("ReceptionistProxyFailure", "Failed to start local receptionist proxy server. See the logs for more information.")); + FMessageDialog::Open( + EAppMsgType::Ok, + LOCTEXT("ReceptionistProxyFailure", "Failed to start local receptionist proxy server. See the logs for more information.")); } return bSuccess; @@ -119,9 +155,19 @@ bool FSpatialGDKEditorModule::CanExecuteLaunch() const bool FSpatialGDKEditorModule::CanStartSession(FText& OutErrorMessage) const { - if (!SpatialGDKEditorInstance->IsSchemaGenerated()) + FSpatialGDKEditor::ESchemaDatabaseValidationResult SchemaCheck = SpatialGDKEditorInstance->ValidateSchemaDatabase(); + if (SchemaCheck == FSpatialGDKEditor::NotFound) + { + OutErrorMessage = LOCTEXT("MissingSchema", + "Attempted to start a local deployment but schema is not generated. You can generate it by clicking on " + "the Schema button in the toolbar."); + return false; + } + else if (SchemaCheck == FSpatialGDKEditor::OldVersion) { - OutErrorMessage = LOCTEXT("MissingSchema", "Attempted to start a local deployment but schema is not generated. You can generate it by clicking on the Schema button in the toolbar."); + OutErrorMessage = LOCTEXT("OldSchema", + "Attempted to start a local deployment but schema is out of date. You can generate it by clicking on " + "the Schema button in the toolbar."); return false; } @@ -129,13 +175,16 @@ bool FSpatialGDKEditorModule::CanStartSession(FText& OutErrorMessage) const { if (GetDevAuthToken().IsEmpty()) { - OutErrorMessage = LOCTEXT("MissingDevelopmentAuthenticationToken", "You have to generate or provide a development authentication token in the SpatialOS GDK Editor Settings section to enable connecting to a cloud deployment."); + OutErrorMessage = LOCTEXT("MissingDevelopmentAuthenticationToken", + "You have to generate or provide a development authentication token in the SpatialOS GDK Editor " + "Settings section to enable connecting to a cloud deployment."); return false; } const USpatialGDKEditorSettings* Settings = GetDefault(); bool bIsRunningInChina = GetDefault()->IsRunningInChina(); - if (!Settings->GetPrimaryDeploymentName().IsEmpty() && !SpatialCommandUtils::HasDevLoginTag(Settings->GetPrimaryDeploymentName(), bIsRunningInChina, OutErrorMessage)) + if (!Settings->GetPrimaryDeploymentName().IsEmpty() + && !SpatialCommandUtils::HasDevLoginTag(Settings->GetPrimaryDeploymentName(), bIsRunningInChina, OutErrorMessage)) { return false; } @@ -163,7 +212,9 @@ bool FSpatialGDKEditorModule::CanStartLaunchSession(FText& OutErrorMessage) cons if (ShouldConnectToLocalDeployment() && GetSpatialOSLocalDeploymentIP().IsEmpty()) { - OutErrorMessage = LOCTEXT("MissingLocalDeploymentIP", "You have to enter this machine's local network IP in the 'Local Deployment IP' field to enable connecting to a local deployment."); + OutErrorMessage = LOCTEXT("MissingLocalDeploymentIP", + "You have to enter this machine's local network IP in the 'Local Deployment IP' field to enable " + "connecting to a local deployment."); return false; } @@ -180,7 +231,8 @@ FString FSpatialGDKEditorModule::GetMobileClientCommandLineArgs() const else if (ShouldConnectToCloudDeployment()) { // 127.0.0.1 is only used to indicate that we want to connect to a deployment. - // This address won't be used when actually trying to connect, but Unreal will try to resolve the address and close the connection if it fails. + // This address won't be used when actually trying to connect, but Unreal will try to resolve the address and close the connection + // if it fails. CommandLine = TEXT("127.0.0.1 -devAuthToken ") + GetDevAuthToken(); FString CloudDeploymentName = GetSpatialOSCloudDeploymentName(); if (!CloudDeploymentName.IsEmpty()) @@ -189,7 +241,9 @@ FString FSpatialGDKEditorModule::GetMobileClientCommandLineArgs() const } else { - UE_LOG(LogSpatialGDKEditorModule, Display, TEXT("Cloud deployment name is empty. If there are multiple running deployments with 'dev_login' tag, the game will choose one randomly.")); + UE_LOG(LogSpatialGDKEditorModule, Display, + TEXT("Cloud deployment name is empty. If there are multiple running deployments with 'dev_login' tag, the game will " + "choose one randomly.")); } } return CommandLine; @@ -203,15 +257,15 @@ bool FSpatialGDKEditorModule::ShouldPackageMobileCommandLineArgs() const uint32 GetPIEServerWorkers() { const USpatialGDKEditorSettings* EditorSettings = GetDefault(); - if (EditorSettings->bGenerateDefaultLaunchConfig && EditorSettings->LaunchConfigDesc.ServerWorkerConfig.bAutoNumEditorInstances) + if (EditorSettings->bGenerateDefaultLaunchConfig && !EditorSettings->LaunchConfigDesc.ServerWorkerConfiguration.bAutoNumEditorInstances) { - UWorld* EditorWorld = GEditor->GetEditorWorldContext().World(); - check(EditorWorld); - return GetWorkerCountFromWorldSettings(*EditorWorld); + return EditorSettings->LaunchConfigDesc.ServerWorkerConfiguration.NumEditorInstances; } else { - return EditorSettings->LaunchConfigDesc.ServerWorkerConfig.NumEditorInstances; + UWorld* EditorWorld = GEditor->GetEditorWorldContext().World(); + check(EditorWorld); + return GetWorkerCountFromWorldSettings(*EditorWorld); } } @@ -232,6 +286,80 @@ bool FSpatialGDKEditorModule::ForEveryServerWorker(TFunction(World->GetWorldSettings())) + { + EMapTestingMode TestingMode = SpatialWorldSettings->TestingSettings.TestingMode; + if (TestingMode != EMapTestingMode::UseCurrentSettings) + { + TActorIterator SpatialTestIt(World); + if (TestingMode == EMapTestingMode::Detect) + { + if (SpatialTestIt) + { + TestingMode = EMapTestingMode::ForceSpatial; + } + else + { + TActorIterator NativeTestIt(World); + if (!NativeTestIt) + { + // if there's no AFunctionalTests assume it's a Unit Test, so use current settings + return PIESettingsOverride; + } + TestingMode = EMapTestingMode::ForceNativeOffline; + } + } + + int NumberOfClients = 1; + + PIESettingsOverride.bUseSpatial = false; // turn off by default + + switch (TestingMode) + { + case EMapTestingMode::ForceNativeOffline: + PIESettingsOverride.PlayNetMode = EPlayNetMode::PIE_Standalone; + break; + case EMapTestingMode::ForceNativeAsListenServer: + PIESettingsOverride.PlayNetMode = EPlayNetMode::PIE_ListenServer; + break; + case EMapTestingMode::ForceNativeAsClient: + PIESettingsOverride.PlayNetMode = EPlayNetMode::PIE_Client; + break; + case EMapTestingMode::ForceSpatial: + PIESettingsOverride.bUseSpatial = true; // turn on for Spatial + PIESettingsOverride.PlayNetMode = EPlayNetMode::PIE_Client; + for (; SpatialTestIt; ++SpatialTestIt) + { + NumberOfClients = FMath::Max(SpatialTestIt->GetNumRequiredClients(), NumberOfClients); + } + { + FString SnapshotForMap = ASpatialFunctionalTest::GetTakenSnapshotPath(World); + + if (!SnapshotForMap.IsEmpty()) + { + PIESettingsOverride.ForceUseSnapshot = SnapshotForMap; + // Set that we're loading from taken snapshot. + ASpatialFunctionalTest::SetLoadedFromTakenSnapshot(); + } + } + break; + default: + checkf(false, TEXT("Unsupported Testing Mode")); + break; + } + + PIESettingsOverride.NumberOfClients = NumberOfClients; + } + } + return PIESettingsOverride; +} + bool FSpatialGDKEditorModule::ShouldStartLocalServer() const { if (!GetDefault()->UsesSpatialNetworking()) @@ -260,10 +388,10 @@ void FSpatialGDKEditorModule::RegisterSettings() ISettingsContainerPtr SettingsContainer = SettingsModule->GetContainer("Project"); SettingsContainer->DescribeCategory("SpatialGDKEditor", LOCTEXT("RuntimeWDCategoryName", "SpatialOS GDK for Unreal"), - LOCTEXT("RuntimeWDCategoryDescription", "Configuration for the SpatialOS GDK for Unreal")); + LOCTEXT("RuntimeWDCategoryDescription", "Configuration for the SpatialOS GDK for Unreal")); - ISettingsSectionPtr EditorSettingsSection = SettingsModule->RegisterSettings("Project", "SpatialGDKEditor", "Editor Settings", - LOCTEXT("SpatialEditorGeneralSettingsName", "Editor Settings"), + ISettingsSectionPtr EditorSettingsSection = SettingsModule->RegisterSettings( + "Project", "SpatialGDKEditor", "Editor Settings", LOCTEXT("SpatialEditorGeneralSettingsName", "Editor Settings"), LOCTEXT("SpatialEditorGeneralSettingsDescription", "Editor configuration for the SpatialOS GDK for Unreal"), GetMutableDefault()); @@ -272,8 +400,8 @@ void FSpatialGDKEditorModule::RegisterSettings() EditorSettingsSection->OnModified().BindRaw(this, &FSpatialGDKEditorModule::HandleEditorSettingsSaved); } - ISettingsSectionPtr RuntimeSettingsSection = SettingsModule->RegisterSettings("Project", "SpatialGDKEditor", "Runtime Settings", - LOCTEXT("SpatialRuntimeGeneralSettingsName", "Runtime Settings"), + ISettingsSectionPtr RuntimeSettingsSection = SettingsModule->RegisterSettings( + "Project", "SpatialGDKEditor", "Runtime Settings", LOCTEXT("SpatialRuntimeGeneralSettingsName", "Runtime Settings"), LOCTEXT("SpatialRuntimeGeneralSettingsDescription", "Runtime configuration for the SpatialOS GDK for Unreal"), GetMutableDefault()); @@ -284,10 +412,16 @@ void FSpatialGDKEditorModule::RegisterSettings() } FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); - PropertyModule.RegisterCustomPropertyTypeLayout("WorkerType", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FWorkerTypeCustomization::MakeInstance)); - PropertyModule.RegisterCustomPropertyTypeLayout("SpatialLaunchConfigDescription", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FSpatialLaunchConfigCustomization::MakeInstance)); - PropertyModule.RegisterCustomPropertyTypeLayout("RuntimeVariantVersion", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FSpatialRuntimeVersionCustomization::MakeInstance)); - PropertyModule.RegisterCustomClassLayout(USpatialGDKEditorSettings::StaticClass()->GetFName(), FOnGetDetailCustomizationInstance::CreateStatic(&FSpatialGDKEditorLayoutDetails::MakeInstance)); + PropertyModule.RegisterCustomPropertyTypeLayout( + "WorkerType", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FWorkerTypeCustomization::MakeInstance)); + PropertyModule.RegisterCustomPropertyTypeLayout( + "SpatialLaunchConfigDescription", + FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FSpatialLaunchConfigCustomization::MakeInstance)); + PropertyModule.RegisterCustomPropertyTypeLayout( + "RuntimeVariantVersion", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FSpatialRuntimeVersionCustomization::MakeInstance)); + PropertyModule.RegisterCustomClassLayout( + USpatialGDKEditorSettings::StaticClass()->GetFName(), + FOnGetDetailCustomizationInstance::CreateStatic(&FSpatialGDKEditorLayoutDetails::MakeInstance)); } void FSpatialGDKEditorModule::UnregisterSettings() diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorPackageAssembly.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorPackageAssembly.cpp index 4dfe4d9c1e..90aaa3e45c 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorPackageAssembly.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorPackageAssembly.cpp @@ -3,6 +3,7 @@ #include "SpatialGDKEditorPackageAssembly.h" #include "Async/Async.h" +#include "DesktopPlatformModule.h" #include "Framework/Notifications/NotificationManager.h" #include "Misc/App.h" #include "Misc/FileHelper.h" @@ -21,9 +22,10 @@ DEFINE_LOG_CATEGORY(LogSpatialGDKEditorPackageAssembly); namespace { - const FString SpatialBuildExe = FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(TEXT("SpatialGDK/Binaries/ThirdParty/Improbable/Programs/Build.exe")); - const FString LinuxPlatform = TEXT("Linux"); - const FString Win64Platform = TEXT("Win64"); +const FString SpatialBuildExe = + FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(TEXT("SpatialGDK/Binaries/ThirdParty/Improbable/Programs/Build.exe")); +const FString LinuxPlatform = TEXT("Linux"); +const FString Win64Platform = TEXT("Win64"); } // anonymous namespace void FSpatialGDKPackageAssembly::LaunchTask(const FString& Exe, const FString& Args, const FString& WorkingDir) @@ -35,11 +37,12 @@ void FSpatialGDKPackageAssembly::LaunchTask(const FString& Exe, const FString& A PackageAssemblyTask->Launch(); } -void FSpatialGDKPackageAssembly::BuildAssembly(const FString& ProjectName, const FString& Platform, const FString& Configuration, const FString& AdditionalArgs) +void FSpatialGDKPackageAssembly::BuildAssembly(const FString& TargetName, const FString& Platform, const FString& Configuration, + const FString& AdditionalArgs) { FString WorkingDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir()); FString Project = FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath()); - FString Args = FString::Printf(TEXT("%s %s %s \"%s\" %s"), *ProjectName, *Platform, *Configuration, *Project, *AdditionalArgs); + FString Args = FString::Printf(TEXT("%s %s %s \"%s\" %s"), *TargetName, *Platform, *Configuration, *Project, *AdditionalArgs); LaunchTask(SpatialBuildExe, Args, WorkingDir); } @@ -78,8 +81,7 @@ void FSpatialGDKPackageAssembly::BuildAndUploadAssembly(const FCloudDeploymentCo } Steps.Enqueue(EPackageAssemblyStep::UPLOAD_ASSEMBLY); - AsyncTask(ENamedThreads::GameThread, [this]() - { + AsyncTask(ENamedThreads::GameThread, [this]() { ShowTaskStartedNotification(TEXT("Building Assembly")); NextStep(); }); @@ -101,26 +103,26 @@ bool FSpatialGDKPackageAssembly::NextStep() switch (Target) { case EPackageAssemblyStep::BUILD_SERVER: - AsyncTask(ENamedThreads::GameThread, [this]() - { - BuildAssembly(FString::Printf(TEXT("%sServer"), FApp::GetProjectName()), LinuxPlatform, CloudDeploymentConfiguration.BuildConfiguration, CloudDeploymentConfiguration.BuildServerExtraArgs); + AsyncTask(ENamedThreads::GameThread, [this]() { + BuildAssembly(FString::Printf(TEXT("%sServer"), FApp::GetProjectName()), LinuxPlatform, + CloudDeploymentConfiguration.BuildConfiguration, CloudDeploymentConfiguration.BuildServerExtraArgs); }); break; case EPackageAssemblyStep::BUILD_CLIENT: - AsyncTask(ENamedThreads::GameThread, [this]() - { - BuildAssembly(FApp::GetProjectName(), Win64Platform, CloudDeploymentConfiguration.BuildConfiguration, CloudDeploymentConfiguration.BuildClientExtraArgs); + AsyncTask(ENamedThreads::GameThread, [this]() { + BuildAssembly(FApp::GetProjectName(), Win64Platform, CloudDeploymentConfiguration.BuildConfiguration, + CloudDeploymentConfiguration.BuildClientExtraArgs + GetExtraArgsForClientTarget()); }); break; case EPackageAssemblyStep::BUILD_SIMULATED_PLAYERS: - AsyncTask(ENamedThreads::GameThread, [this]() - { - BuildAssembly(FString::Printf(TEXT("%sSimulatedPlayer"), FApp::GetProjectName()), LinuxPlatform, CloudDeploymentConfiguration.BuildConfiguration, CloudDeploymentConfiguration.BuildSimulatedPlayerExtraArgs); + AsyncTask(ENamedThreads::GameThread, [this]() { + BuildAssembly(FString::Printf(TEXT("%sSimulatedPlayer"), FApp::GetProjectName()), LinuxPlatform, + CloudDeploymentConfiguration.BuildConfiguration, + CloudDeploymentConfiguration.BuildSimulatedPlayerExtraArgs + GetExtraArgsForClientTarget()); }); break; case EPackageAssemblyStep::UPLOAD_ASSEMBLY: - AsyncTask(ENamedThreads::GameThread, [this]() - { + AsyncTask(ENamedThreads::GameThread, [this]() { UploadAssembly(CloudDeploymentConfiguration.AssemblyName, CloudDeploymentConfiguration.bForceAssemblyOverwrite); }); break; @@ -137,9 +139,9 @@ void FSpatialGDKPackageAssembly::OnTaskCompleted(int32 TaskResult) { if (!NextStep()) { - AsyncTask(ENamedThreads::GameThread, [this]() - { - FString NotificationMessage = FString::Printf(TEXT("Assembly successfully uploaded to project: %s"), *FSpatialGDKServicesModule::GetProjectName()); + AsyncTask(ENamedThreads::GameThread, [this]() { + FString NotificationMessage = + FString::Printf(TEXT("Assembly successfully uploaded to project: %s"), *FSpatialGDKServicesModule::GetProjectName()); ShowTaskEndedNotification(NotificationMessage, SNotificationItem::CS_Success); OnSuccess.ExecuteIfBound(); }); @@ -147,17 +149,25 @@ void FSpatialGDKPackageAssembly::OnTaskCompleted(int32 TaskResult) } else { - AsyncTask(ENamedThreads::GameThread, [this]() - { - FString NotificationMessage = FString::Printf(TEXT("Failed assembly upload to project: %s"), *FSpatialGDKServicesModule::GetProjectName()); + AsyncTask(ENamedThreads::GameThread, [this]() { + FString NotificationMessage = + FString::Printf(TEXT("Failed assembly upload to project: %s"), *FSpatialGDKServicesModule::GetProjectName()); ShowTaskEndedNotification(NotificationMessage, SNotificationItem::CS_Fail); if (Status == EPackageAssemblyStatus::ASSEMBLY_EXISTS) { - FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("AssemblyExists_Error", "The assembly with the specified name has previously been uploaded. Enable the 'Force Overwrite on Upload' option in the Cloud Deployment dialog to overwrite the existing assembly or specify a different assembly name.")); + FMessageDialog::Open( + EAppMsgType::Ok, + LOCTEXT( + "AssemblyExists_Error", + "The assembly with the specified name has previously been uploaded. Enable the 'Force Overwrite on Upload' option " + "in the Cloud Deployment dialog to overwrite the existing assembly or specify a different assembly name.")); } else if (Status == EPackageAssemblyStatus::BAD_PROJECT_NAME) { - FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("BadProjectName_Error", "The project name appears to be incorrect or you do not have permissions for this project. You can edit the project name from the Cloud Deployment dialog.")); + FMessageDialog::Open(EAppMsgType::Ok, + LOCTEXT("BadProjectName_Error", + "The project name appears to be incorrect or you do not have permissions for this project. " + "You can edit the project name from the Cloud Deployment dialog.")); } else if (Status == EPackageAssemblyStatus::NONE) { @@ -170,8 +180,8 @@ void FSpatialGDKPackageAssembly::OnTaskCompleted(int32 TaskResult) void FSpatialGDKPackageAssembly::OnTaskOutput(FString Message) { - //UNR-3486 parse for assembly name conflict so we can display a message to the user - //because the spatial cli doesn't return error codes this is done via string matching + // UNR-3486 parse for assembly name conflict so we can display a message to the user + // because the spatial cli doesn't return error codes this is done via string matching if (Message.Find(TEXT("Either change the name or use the '--force' flag")) >= 0) { Status = EPackageAssemblyStatus::ASSEMBLY_EXISTS; @@ -187,13 +197,37 @@ void FSpatialGDKPackageAssembly::OnTaskCanceled() { Steps.Empty(); Status = EPackageAssemblyStatus::CANCELED; - FString NotificationMessage = FString::Printf(TEXT("Cancelled assembly upload to project: %s"), *FSpatialGDKServicesModule::GetProjectName()); - AsyncTask(ENamedThreads::GameThread, [this, NotificationMessage]() - { + FString NotificationMessage = + FString::Printf(TEXT("Cancelled assembly upload to project: %s"), *FSpatialGDKServicesModule::GetProjectName()); + AsyncTask(ENamedThreads::GameThread, [this, NotificationMessage]() { ShowTaskEndedNotification(NotificationMessage, SNotificationItem::CS_Fail); }); } +FString FSpatialGDKPackageAssembly::GetExtraArgsForClientTarget() +{ + // This will determine if the target with the project name (.Target.cs) has the TargetType.Client, + // in which case we will pass '-client -noserver' to the build executable when packaging the assembly. + IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get(); + + TArray Targets = DesktopPlatform->GetTargetsForCurrentProject(); + const FString TargetName = FApp::GetProjectName(); + + for (const FTargetInfo& Target : Targets) + { + if (Target.Name == TargetName) + { + if (Target.Type == EBuildTargetType::Client) + { + return TEXT(" -client -noserver"); + } + break; + } + } + + return FString(); +} + void FSpatialGDKPackageAssembly::HandleCancelButtonClicked() { if (PackageAssemblyTask.IsValid()) @@ -205,14 +239,9 @@ void FSpatialGDKPackageAssembly::HandleCancelButtonClicked() void FSpatialGDKPackageAssembly::ShowTaskStartedNotification(const FString& NotificationText) { FNotificationInfo Info(FText::AsCultureInvariant(NotificationText)); - Info.ButtonDetails.Add( - FNotificationButtonInfo( - LOCTEXT("PackageAssemblyTaskCancel", "Cancel"), - LOCTEXT("PackageAssemblyTaskCancel_ToolTip", "Cancels execution of this task."), - FSimpleDelegate::CreateRaw(this, &FSpatialGDKPackageAssembly::HandleCancelButtonClicked), - SNotificationItem::CS_Pending - ) - ); + Info.ButtonDetails.Add(FNotificationButtonInfo( + LOCTEXT("PackageAssemblyTaskCancel", "Cancel"), LOCTEXT("PackageAssemblyTaskCancel_ToolTip", "Cancels execution of this task."), + FSimpleDelegate::CreateRaw(this, &FSpatialGDKPackageAssembly::HandleCancelButtonClicked), SNotificationItem::CS_Pending)); Info.ExpireDuration = 5.0f; Info.bFireAndForget = false; @@ -224,7 +253,8 @@ void FSpatialGDKPackageAssembly::ShowTaskStartedNotification(const FString& Noti } } -void FSpatialGDKPackageAssembly::ShowTaskEndedNotification(const FString& NotificationText, SNotificationItem::ECompletionState CompletionState) +void FSpatialGDKPackageAssembly::ShowTaskEndedNotification(const FString& NotificationText, + SNotificationItem::ECompletionState CompletionState) { TSharedPtr Notification = TaskNotificationPtr.Pin(); if (Notification.IsValid()) diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorSettings.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorSettings.cpp index 4846443825..574ec56217 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorSettings.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorSettings.cpp @@ -2,9 +2,9 @@ #include "SpatialGDKEditorSettings.h" +#include "ISettingsModule.h" #include "Interfaces/ITargetPlatformManagerModule.h" #include "Internationalization/Regex.h" -#include "ISettingsModule.h" #include "Misc/FileHelper.h" #include "Misc/MessageDialog.h" #include "Modules/ModuleManager.h" @@ -39,28 +39,29 @@ const FString& FRuntimeVariantVersion::GetVersionForCloud() const USpatialGDKEditorSettings::USpatialGDKEditorSettings(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) - , bShowSpatialServiceButton(false) , bDeleteDynamicEntities(true) , bGenerateDefaultLaunchConfig(true) - , RuntimeVariant(ESpatialOSRuntimeVariant::Standard) , StandardRuntimeVersion(SpatialGDKServicesConstants::SpatialOSRuntimePinnedStandardVersion) - , CompatibilityModeRuntimeVersion(SpatialGDKServicesConstants::SpatialOSRuntimePinnedCompatbilityModeVersion) + , bUseGDKPinnedInspectorVersion(true) + , InspectorVersionOverride(TEXT("")) , ExposedRuntimeIP(TEXT("")) - , bStopLocalDeploymentOnEndPIE(false) - , bStopSpatialOnExit(false) , bAutoStartLocalDeployment(true) + , bSpatialDebuggerEditorEnabled(false) + , AutoStopLocalDeployment(EAutoStopLocalDeploymentMode::OnEndPIE) + , bStopPIEOnTestingCompleted(true) , CookAndGeneratePlatform("") , CookAndGenerateAdditionalArguments("-cookall -unversioned") , PrimaryDeploymentRegionCode(ERegionCode::US) , bIsAutoGenerateCloudConfigEnabled(true) - , SimulatedPlayerLaunchConfigPath(FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(TEXT("SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/SpatialConfig/cloud_launch_sim_player_deployment.json"))) + , SimulatedPlayerLaunchConfigPath(FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(TEXT( + "SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/SpatialConfig/cloud_launch_sim_player_deployment.json"))) , bBuildAndUploadAssembly(true) , AssemblyBuildConfiguration(TEXT("Development")) , bConnectServerToCloud(false) , LocalReceptionistPort(SpatialConstants::DEFAULT_SERVER_RECEPTIONIST_PROXY_PORT) , ListeningAddress(SpatialConstants::LOCAL_HOST) , SimulatedPlayerDeploymentRegionCode(ERegionCode::US) - , bPackageMobileCommandLineArgs(false) + , bPackageMobileCommandLineArgs(true) , bStartPIEClientsWithLocalLaunchOnDevice(false) , SpatialOSNetFlowType(ESpatialOSNetFlow::LocalDeployment) { @@ -68,17 +69,15 @@ USpatialGDKEditorSettings::USpatialGDKEditorSettings(const FObjectInitializer& O SpatialOSSnapshotToSave = GetSpatialOSSnapshotToSave(); SpatialOSSnapshotToLoad = GetSpatialOSSnapshotToLoad(); SnapshotPath.FilePath = GetSpatialOSSnapshotToSavePath(); + + // TODO: UNR-4472 - Remove this WorkerTypeName renaming when refactoring FLaunchConfigDescription. + // Force update users settings in-case they have a bad server worker name saved. + LaunchConfigDesc.ServerWorkerConfiguration.WorkerTypeName = SpatialConstants::DefaultServerWorkerType; } FRuntimeVariantVersion& USpatialGDKEditorSettings::GetRuntimeVariantVersion(ESpatialOSRuntimeVariant::Type Variant) { - switch (Variant) - { - case ESpatialOSRuntimeVariant::CompatibilityMode: - return CompatibilityModeRuntimeVersion; - default: - return StandardRuntimeVersion; - } + return StandardRuntimeVersion; } void USpatialGDKEditorSettings::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) @@ -96,7 +95,7 @@ void USpatialGDKEditorSettings::PostEditChangeProperty(struct FPropertyChangedEv PlayInSettings->PostEditChange(); PlayInSettings->SaveConfig(); } - else if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, RuntimeVariant)) + else if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, StandardRuntimeVersion)) { FSpatialGDKServicesModule& GDKServices = FModuleManager::GetModuleChecked("SpatialGDKServices"); GDKServices.GetLocalDeploymentManager()->SetRedeployRequired(); @@ -118,6 +117,26 @@ void USpatialGDKEditorSettings::PostEditChangeProperty(struct FPropertyChangedEv return; } } + else if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, LaunchConfigDesc)) + { + // TODO: UNR-4472 - Remove this WorkerTypeName renaming when refactoring FLaunchConfigDescription. + // Force override the server worker name as it MUST be UnrealWorker. + LaunchConfigDesc.ServerWorkerConfiguration.WorkerTypeName = SpatialConstants::DefaultServerWorkerType; + + if (PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(FSpatialLaunchConfigDescription, RuntimeFlags)) + { + USpatialGDKEditorSettings::TrimTMap(LaunchConfigDesc.RuntimeFlags); + } + if (PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(FWorkerTypeLaunchSection, Flags)) + { + USpatialGDKEditorSettings::TrimTMap(LaunchConfigDesc.ServerWorkerConfiguration.Flags); + + for (auto& WorkerConfig : LaunchConfigDesc.AdditionalWorkerConfigs) + { + USpatialGDKEditorSettings::TrimTMap(WorkerConfig.Flags); + } + } + } } void USpatialGDKEditorSettings::PostInitProperties() @@ -245,6 +264,11 @@ void USpatialGDKEditorSettings::SetSimulatedPlayersEnabledState(bool IsEnabled) SaveConfig(); } +void USpatialGDKEditorSettings::SetSpatialDebuggerEditorEnabled(bool IsEnabled) +{ + bSpatialDebuggerEditorEnabled = IsEnabled; +} + void USpatialGDKEditorSettings::SetAutoGenerateCloudLaunchConfigEnabledState(bool IsEnabled) { bIsAutoGenerateCloudConfigEnabled = IsEnabled; @@ -327,7 +351,7 @@ bool USpatialGDKEditorSettings::IsManualWorkerConnectionSet(const FString& Launc if (!ConfigFile) { - UE_LOG(LogSpatialEditorSettings, Error, TEXT("Could not open configuration file %s"), *LaunchConfigPath); + UE_LOG(LogSpatialEditorSettings, Error, TEXT("Configuration file is missing at path %s"), *LaunchConfigPath); return false; } @@ -345,13 +369,13 @@ bool USpatialGDKEditorSettings::IsManualWorkerConnectionSet(const FString& Launc } const TSharedPtr* LoadBalancingField; - if (!(*LaunchConfigJsonRootObject)->TryGetObjectField("load_balancing", LoadBalancingField)) + if (!(*LaunchConfigJsonRootObject)->TryGetObjectField("loadBalancing", LoadBalancingField)) { return false; } const TArray>* LayerConfigurations; - if (!(*LoadBalancingField)->TryGetArrayField("layer_configurations", LayerConfigurations)) + if (!(*LoadBalancingField)->TryGetArrayField("layerConfigurations", LayerConfigurations)) { return false; } @@ -365,8 +389,7 @@ bool USpatialGDKEditorSettings::IsManualWorkerConnectionSet(const FString& Launc // Check manual_worker_connection flag, if it exists. if (LayerConfiguration->TryGetObjectField("options", OptionsField) - && (*OptionsField)->TryGetBoolField("manual_worker_connection_only", ManualWorkerConnectionFlag) - && ManualWorkerConnectionFlag) + && (*OptionsField)->TryGetBoolField("manualWorkerConnectionOnly", ManualWorkerConnectionFlag) && ManualWorkerConnectionFlag) { FString WorkerName; if (LayerConfiguration->TryGetStringField("layer", WorkerName)) @@ -375,7 +398,8 @@ bool USpatialGDKEditorSettings::IsManualWorkerConnectionSet(const FString& Launc } else { - UE_LOG(LogSpatialEditorSettings, Error, TEXT("Invalid configuration file %s, Layer configuration missing its layer field"), *LaunchConfigPath); + UE_LOG(LogSpatialEditorSettings, Error, + TEXT("Invalid configuration file %s, Layer configuration missing its layer field"), *LaunchConfigPath); } } } @@ -399,7 +423,8 @@ bool USpatialGDKEditorSettings::IsDeploymentConfigurationValid() const } if (!IsDeploymentNameValid(PrimaryDeploymentName)) { - UE_LOG(LogSpatialEditorSettings, Error, TEXT("Deployment name is invalid. %s"), *SpatialConstants::DeploymentPatternHint.ToString()); + UE_LOG(LogSpatialEditorSettings, Error, TEXT("Deployment name is invalid. %s"), + *SpatialConstants::DeploymentPatternHint.ToString()); bValid = false; } if (!IsRegionCodeValid(PrimaryDeploymentRegionCode)) @@ -422,7 +447,8 @@ bool USpatialGDKEditorSettings::IsDeploymentConfigurationValid() const { if (!IsDeploymentNameValid(SimulatedPlayerDeploymentName)) { - UE_LOG(LogSpatialEditorSettings, Error, TEXT("Simulated player deployment name is invalid. %s"), *SpatialConstants::DeploymentPatternHint.ToString()); + UE_LOG(LogSpatialEditorSettings, Error, TEXT("Simulated player deployment name is invalid. %s"), + *SpatialConstants::DeploymentPatternHint.ToString()); bValid = false; } if (!IsRegionCodeValid(SimulatedPlayerDeploymentRegionCode)) @@ -437,10 +463,18 @@ bool USpatialGDKEditorSettings::IsDeploymentConfigurationValid() const } } + return bValid; +} + +bool USpatialGDKEditorSettings::CheckManualWorkerConnectionOnLaunch() 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()); + 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) { @@ -453,7 +487,7 @@ bool USpatialGDKEditorSettings::IsDeploymentConfigurationValid() const } } - return bValid; + return true; } void USpatialGDKEditorSettings::SetDevelopmentAuthenticationToken(const FString& Token) @@ -492,6 +526,15 @@ FString USpatialGDKEditorSettings::GetCookAndGenerateSchemaTargetPlatform() cons return FPlatformProcess::GetBinariesSubdirectory(); } +void USpatialGDKEditorSettings::TrimTMap(TMap& Map) +{ + for (auto& Flag : Map) + { + Flag.Key.TrimStartAndEndInline(); + Flag.Value.TrimStartAndEndInline(); + } +} + const FString& FSpatialLaunchConfigDescription::GetTemplate() const { if (bUseDefaultTemplateForRuntimeVariant) @@ -504,26 +547,13 @@ const FString& FSpatialLaunchConfigDescription::GetTemplate() const const FString& FSpatialLaunchConfigDescription::GetDefaultTemplateForRuntimeVariant() const { - switch (GetDefault()->GetSpatialOSRuntimeVariant()) + if (GetDefault()->IsRunningInChina()) { - case ESpatialOSRuntimeVariant::CompatibilityMode: - if (GetDefault()->IsRunningInChina()) - { - return SpatialGDKServicesConstants::PinnedChinaCompatibilityModeRuntimeTemplate; - } - else - { - return SpatialGDKServicesConstants::PinnedCompatibilityModeRuntimeTemplate; - } - default: - if (GetDefault()->IsRunningInChina()) - { - return SpatialGDKServicesConstants::PinnedChinaStandardRuntimeTemplate; - } - else - { - return SpatialGDKServicesConstants::PinnedStandardRuntimeTemplate; - } + return SpatialGDKServicesConstants::PinnedChinaStandardRuntimeTemplate; + } + else + { + return SpatialGDKServicesConstants::PinnedStandardRuntimeTemplate; } } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialLaunchConfigCustomization.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialLaunchConfigCustomization.cpp index 7ceac8db6b..a8af6effbf 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialLaunchConfigCustomization.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialLaunchConfigCustomization.cpp @@ -2,8 +2,8 @@ #include "SpatialLaunchConfigCustomization.h" -#include "SpatialGDKSettings.h" #include "SpatialGDKEditorSettings.h" +#include "SpatialGDKSettings.h" #include "IDetailChildrenBuilder.h" #include "IDetailGroup.h" @@ -19,17 +19,21 @@ TSharedRef FSpatialLaunchConfigCustomization::MakeIn return MakeShared(); } -void FSpatialLaunchConfigCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +void FSpatialLaunchConfigCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, + class FDetailWidgetRow& HeaderRow, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) { - } -void FSpatialLaunchConfigCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +void FSpatialLaunchConfigCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, + class IDetailChildrenBuilder& StructBuilder, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) { TArray EditedObject; StructPropertyHandle->GetOuterObjects(EditedObject); - const FName& PinnedGDKRuntimeLocalPropertyName = GET_MEMBER_NAME_CHECKED(FSpatialLaunchConfigDescription, bUseDefaultTemplateForRuntimeVariant); + const FName& PinnedGDKRuntimeLocalPropertyName = + GET_MEMBER_NAME_CHECKED(FSpatialLaunchConfigDescription, bUseDefaultTemplateForRuntimeVariant); if (EditedObject.Num() == 0) { @@ -53,33 +57,14 @@ void FSpatialLaunchConfigCustomization::CustomizeChildren(TSharedRef(StructPtr); - FText PinnedTemplateDisplay = FText::Format(LOCTEXT("DefaultTemplate", "Default: {0}"), FText::FromString(LaunchConfigDesc->GetDefaultTemplateForRuntimeVariant())); + FText PinnedTemplateDisplay = FText::Format(LOCTEXT("DefaultTemplate", "Default: {0}"), + FText::FromString(LaunchConfigDesc->GetDefaultTemplateForRuntimeVariant())); IDetailPropertyRow& CustomRow = StructBuilder.AddProperty(ChildProperty.ToSharedRef()); - CustomRow.CustomWidget() - .NameContent() - [ - ChildProperty->CreatePropertyNameWidget() - ] - .ValueContent() - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .HAlign(HAlign_Left) - .AutoWidth() - [ - ChildProperty->CreatePropertyValueWidget() - ] - + SHorizontalBox::Slot() - .Padding(5) - .HAlign(HAlign_Center) - .AutoWidth() - [ - SNew(STextBlock) - .Text(PinnedTemplateDisplay) - ] - ]; + CustomRow.CustomWidget().NameContent()[ChildProperty->CreatePropertyNameWidget()].ValueContent() + [SNew(SHorizontalBox) + SHorizontalBox::Slot().HAlign(HAlign_Left).AutoWidth()[ChildProperty->CreatePropertyValueWidget()] + + SHorizontalBox::Slot().Padding(5).HAlign(HAlign_Center).AutoWidth()[SNew(STextBlock).Text(PinnedTemplateDisplay)]]; } else { diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialRuntimeVersionCustomization.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialRuntimeVersionCustomization.cpp index 159ecd3391..cf8188c8db 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialRuntimeVersionCustomization.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialRuntimeVersionCustomization.cpp @@ -16,16 +16,15 @@ TSharedRef FSpatialRuntimeVersionCustomization::Make return MakeShareable(new FSpatialRuntimeVersionCustomization); } -void FSpatialRuntimeVersionCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +void FSpatialRuntimeVersionCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, FDetailWidgetRow& HeaderRow, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) { - HeaderRow - .NameContent() - [ - StructPropertyHandle->CreatePropertyNameWidget() - ]; + HeaderRow.NameContent()[StructPropertyHandle->CreatePropertyNameWidget()]; } -void FSpatialRuntimeVersionCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +void FSpatialRuntimeVersionCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, + IDetailChildrenBuilder& StructBuilder, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) { const FName& PinnedGDKRuntimeLocalPropertyName = GET_MEMBER_NAME_CHECKED(FRuntimeVariantVersion, bUseGDKPinnedRuntimeVersionForLocal); const FName& PinnedGDKRuntimeCloudPropertyName = GET_MEMBER_NAME_CHECKED(FRuntimeVariantVersion, bUseGDKPinnedRuntimeVersionForCloud); @@ -38,7 +37,8 @@ void FSpatialRuntimeVersionCustomization::CustomizeChildren(TSharedRef ChildProperty = StructPropertyHandle->GetChildHandle(ChildIdx); // Layout other properties as usual. - if (ChildProperty->GetProperty()->GetFName() != PinnedGDKRuntimeLocalPropertyName && ChildProperty->GetProperty()->GetFName() != PinnedGDKRuntimeCloudPropertyName) + if (ChildProperty->GetProperty()->GetFName() != PinnedGDKRuntimeLocalPropertyName + && ChildProperty->GetProperty()->GetFName() != PinnedGDKRuntimeCloudPropertyName) { StructBuilder.AddProperty(ChildProperty.ToSharedRef()); continue; @@ -54,27 +54,12 @@ void FSpatialRuntimeVersionCustomization::CustomizeChildren(TSharedRefGetPinnedVersion()); CustomRow.CustomWidget() - .NameContent() - [ - ChildProperty->CreatePropertyNameWidget() - ] - .ValueContent() - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .HAlign(HAlign_Left) - .AutoWidth() - [ - ChildProperty->CreatePropertyValueWidget() - ] - + SHorizontalBox::Slot() - .Padding(5) - .HAlign(HAlign_Center) - .AutoWidth() - [ - SNew(STextBlock) - .Text(FText::FromString(PinnedVersionDisplay)) - ] - ]; + .NameContent()[ChildProperty->CreatePropertyNameWidget()] + .ValueContent()[SNew(SHorizontalBox) + + SHorizontalBox::Slot().HAlign(HAlign_Left).AutoWidth()[ChildProperty->CreatePropertyValueWidget()] + + SHorizontalBox::Slot() + .Padding(5) + .HAlign(HAlign_Center) + .AutoWidth()[SNew(STextBlock).Text(FText::FromString(PinnedVersionDisplay))]]; } } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/LaunchConfigurationEditor.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/LaunchConfigurationEditor.cpp index 93e761a30d..c4905b3d3e 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/LaunchConfigurationEditor.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/LaunchConfigurationEditor.cpp @@ -5,8 +5,8 @@ #include "SpatialGDKDefaultLaunchConfigGenerator.h" #include "SpatialGDKSettings.h" -#include "Editor.h" #include "DesktopPlatformModule.h" +#include "Editor.h" #include "Framework/Application/SlateApplication.h" #include "IDesktopPlatform.h" #include "MainFrame/Public/Interfaces/IMainFrameModule.h" @@ -31,12 +31,27 @@ void ULaunchConfigurationEditor::PostInitProperties() const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); LaunchConfiguration = SpatialGDKEditorSettings->LaunchConfigDesc; - LaunchConfiguration.ServerWorkerConfig.NumEditorInstances = GetWorkerCountFromWorldSettings(*EditorWorld); + LaunchConfiguration.ServerWorkerConfiguration.NumEditorInstances = GetWorkerCountFromWorldSettings(*EditorWorld); +} + +void ULaunchConfigurationEditor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + // Use MemberProperty here so we report the correct member name for nested changes + const FName Name = (PropertyChangedEvent.MemberProperty != nullptr) ? PropertyChangedEvent.MemberProperty->GetFName() : NAME_None; + + if (Name == GET_MEMBER_NAME_CHECKED(ULaunchConfigurationEditor, LaunchConfiguration)) + { + // TODO: UNR-4472 - Remove this WorkerTypeName renaming when refactoring FLaunchConfigDescription. + // Force override the server worker name as it MUST be UnrealWorker. + LaunchConfiguration.ServerWorkerConfiguration.WorkerTypeName = SpatialConstants::DefaultServerWorkerType; + } } void ULaunchConfigurationEditor::SaveConfiguration() { - if (!ValidateGeneratedLaunchConfig(LaunchConfiguration, LaunchConfiguration.ServerWorkerConfig)) + if (!ValidateGeneratedLaunchConfig(LaunchConfiguration)) { return; } @@ -46,18 +61,13 @@ void ULaunchConfigurationEditor::SaveConfiguration() 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); + 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) { - if (GenerateLaunchConfig(Filenames[0], &LaunchConfiguration, LaunchConfiguration.ServerWorkerConfig)) + if (GenerateLaunchConfig(Filenames[0], &LaunchConfiguration, bIsCloudConfiguration)) { OnConfigurationSaved.ExecuteIfBound(Filenames[0]); } @@ -66,40 +76,40 @@ void ULaunchConfigurationEditor::SaveConfiguration() namespace { - // Copied from FPropertyEditorModule::CreateFloatingDetailsView. - bool ShouldShowProperty(const FPropertyAndParent& PropertyAndParent, bool bHaveTemplate) - { - const GDK_PROPERTY(Property)& Property = PropertyAndParent.Property; +// Copied from FPropertyEditorModule::CreateFloatingDetailsView. +bool ShouldShowProperty(const FPropertyAndParent& PropertyAndParent, bool bHaveTemplate) +{ + const GDK_PROPERTY(Property)& Property = PropertyAndParent.Property; - if (bHaveTemplate) - { + if (bHaveTemplate) + { #if ENGINE_MINOR_VERSION <= 24 - const UClass* PropertyOwnerClass = Cast(Property.GetOuter()); + const UClass* PropertyOwnerClass = Cast(Property.GetOuter()); #else - const UClass* PropertyOwnerClass = Property.GetOwner(); + const UClass* PropertyOwnerClass = Property.GetOwner(); #endif - const bool bDisableEditOnTemplate = PropertyOwnerClass - && PropertyOwnerClass->IsNative() - && Property.HasAnyPropertyFlags(CPF_DisableEditOnTemplate); - if (bDisableEditOnTemplate) - { - return false; - } + const bool bDisableEditOnTemplate = + PropertyOwnerClass && PropertyOwnerClass->IsNative() && Property.HasAnyPropertyFlags(CPF_DisableEditOnTemplate); + if (bDisableEditOnTemplate) + { + return false; } - return true; } + return true; +} - FReply ExecuteEditorCommand(ULaunchConfigurationEditor* Instance, UFunction* MethodToExecute) - { - Instance->CallFunctionByNameWithArguments(*MethodToExecute->GetName(), *GLog, nullptr, true); +FReply ExecuteEditorCommand(ULaunchConfigurationEditor* Instance, UFunction* MethodToExecute) +{ + Instance->CallFunctionByNameWithArguments(*MethodToExecute->GetName(), *GLog, nullptr, true); - return FReply::Handled(); - } + return FReply::Handled(); } +} // namespace void ULaunchConfigurationEditor::OpenModalWindow(TSharedPtr InParentWindow, OnLaunchConfigurationSaved InSaved) { - ULaunchConfigurationEditor* ObjectInstance = NewObject(GetTransientPackage(), ULaunchConfigurationEditor::StaticClass()); + ULaunchConfigurationEditor* ObjectInstance = + NewObject(GetTransientPackage(), ULaunchConfigurationEditor::StaticClass()); ObjectInstance->AddToRoot(); FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked("PropertyEditor"); @@ -129,13 +139,7 @@ void ULaunchConfigurationEditor::OpenModalWindow(TSharedPtr InParentWin DetailView->SetObjects(ObjectsToView); - TSharedRef VBoxBuilder = SNew(SVerticalBox) - + SVerticalBox::Slot() - .AutoHeight() - .FillHeight(1.0) - [ - DetailView - ]; + 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(ULaunchConfigurationEditor::StaticClass()); FuncIt; ++FuncIt) @@ -149,31 +153,19 @@ void ULaunchConfigurationEditor::OpenModalWindow(TSharedPtr InParentWin .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)) - ] - ]; + .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(LOCTEXT("LaunchConfigurationEditor_Title", "Launch Configuration Editor")) - .ClientSize(FVector2D(600, 400)) - [ - SNew(SBorder) - .BorderImage(FEditorStyle::GetBrush(TEXT("PropertyWindow.WindowBorder"))) - [ - VBoxBuilder - ] - ]; + TSharedRef NewSlateWindow = + SNew(SWindow) + .Title(LOCTEXT("LaunchConfigurationEditor_Title", "Launch Configuration Editor")) + .ClientSize( + FVector2D(600, 400))[SNew(SBorder).BorderImage(FEditorStyle::GetBrush(TEXT("PropertyWindow.WindowBorder")))[VBoxBuilder]]; if (!InParentWindow.IsValid() && FModuleManager::Get().IsModuleLoaded("MainFrame")) { diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/TransientUObjectEditor.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/TransientUObjectEditor.cpp index 54fe68c340..fe7b9f935f 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/TransientUObjectEditor.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/TransientUObjectEditor.cpp @@ -13,45 +13,44 @@ namespace { +void OnTransientUObjectEditorWindowClosed(const TSharedRef& Window, UTransientUObjectEditor* Instance) +{ + Instance->RemoveFromRoot(); +} - void OnTransientUObjectEditorWindowClosed(const TSharedRef& Window, UTransientUObjectEditor* Instance) - { - Instance->RemoveFromRoot(); - } +// Copied from FPropertyEditorModule::CreateFloatingDetailsView. +bool ShouldShowProperty(const FPropertyAndParent& PropertyAndParent, bool bHaveTemplate) +{ + const GDK_PROPERTY(Property)& Property = PropertyAndParent.Property; - // Copied from FPropertyEditorModule::CreateFloatingDetailsView. - bool ShouldShowProperty(const FPropertyAndParent& PropertyAndParent, bool bHaveTemplate) + if (bHaveTemplate) { - const GDK_PROPERTY(Property)& Property = PropertyAndParent.Property; - - if (bHaveTemplate) - { #if ENGINE_MINOR_VERSION <= 24 - const UClass* PropertyOwnerClass = Cast(Property.GetOuter()); + const UClass* PropertyOwnerClass = Cast(Property.GetOuter()); #else - const UClass* PropertyOwnerClass = Property.GetOwner(); + const UClass* PropertyOwnerClass = Property.GetOwner(); #endif - const bool bDisableEditOnTemplate = PropertyOwnerClass - && PropertyOwnerClass->IsNative() - && Property.HasAnyPropertyFlags(CPF_DisableEditOnTemplate); - if (bDisableEditOnTemplate) - { - return false; - } + const bool bDisableEditOnTemplate = + PropertyOwnerClass && PropertyOwnerClass->IsNative() && Property.HasAnyPropertyFlags(CPF_DisableEditOnTemplate); + if (bDisableEditOnTemplate) + { + return false; } - return true; } + return true; +} - FReply ExecuteEditorCommand(UTransientUObjectEditor* Instance, UFunction* MethodToExecute) - { - Instance->CallFunctionByNameWithArguments(*MethodToExecute->GetName(), *GLog, nullptr, true); +FReply ExecuteEditorCommand(UTransientUObjectEditor* Instance, UFunction* MethodToExecute) +{ + Instance->CallFunctionByNameWithArguments(*MethodToExecute->GetName(), *GLog, nullptr, true); - return FReply::Handled(); - } + return FReply::Handled(); } +} // namespace // Rewrite of FPropertyEditorModule::CreateFloatingDetailsView to use the detail property view in a new window. -UTransientUObjectEditor* UTransientUObjectEditor::LaunchTransientUObjectEditor(const FText& EditorName, UClass* ObjectClass, TSharedPtr ParentWindow) +UTransientUObjectEditor* UTransientUObjectEditor::LaunchTransientUObjectEditor(const FText& EditorName, UClass* ObjectClass, + TSharedPtr ParentWindow) { if (!ObjectClass) { @@ -93,13 +92,7 @@ UTransientUObjectEditor* UTransientUObjectEditor::LaunchTransientUObjectEditor(c DetailView->SetObjects(ObjectsToView); - TSharedRef VBoxBuilder = SNew(SVerticalBox) - + SVerticalBox::Slot() - .AutoHeight() - .FillHeight(1.0) - [ - DetailView - ]; + 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) @@ -113,31 +106,17 @@ UTransientUObjectEditor* UTransientUObjectEditor::LaunchTransientUObjectEditor(c .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)) - ] - ]; + .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(EditorName) - [ - SNew(SBorder) - .BorderImage(FEditorStyle::GetBrush(TEXT("PropertyWindow.WindowBorder"))) - [ - VBoxBuilder - ] - ]; - + TSharedRef NewSlateWindow = SNew(SWindow).Title( + EditorName)[SNew(SBorder).BorderImage(FEditorStyle::GetBrush(TEXT("PropertyWindow.WindowBorder")))[VBoxBuilder]]; + if (!ParentWindow.IsValid() && FModuleManager::Get().IsModuleLoaded("MainFrame")) { // If the main frame exists parent the window to it @@ -154,11 +133,10 @@ UTransientUObjectEditor* UTransientUObjectEditor::LaunchTransientUObjectEditor(c FSlateApplication::Get().AddWindow(NewSlateWindow); } - NewSlateWindow->RegisterActiveTimer(0.5, FWidgetActiveTimerDelegate::CreateLambda([NewSlateWindow](double, float) - { - NewSlateWindow->Resize(NewSlateWindow->GetDesiredSize()); - return EActiveTimerReturnType::Stop; - })); + NewSlateWindow->RegisterActiveTimer(0.5, FWidgetActiveTimerDelegate::CreateLambda([NewSlateWindow](double, float) { + NewSlateWindow->Resize(NewSlateWindow->GetDesiredSize()); + return EActiveTimerReturnType::Stop; + })); return ObjectInstance; } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/WorkerTypeCustomization.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/WorkerTypeCustomization.cpp index bc39b3053f..64f0e2fcfe 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/WorkerTypeCustomization.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/WorkerTypeCustomization.cpp @@ -13,31 +13,29 @@ TSharedRef FWorkerTypeCustomization::MakeInstance() return MakeShared(); } -void FWorkerTypeCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +void FWorkerTypeCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, class FDetailWidgetRow& HeaderRow, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) { TSharedPtr WorkerTypeNameProperty = StructPropertyHandle->GetChildHandle("WorkerTypeName"); if (WorkerTypeNameProperty->IsValidHandle()) { - HeaderRow.NameContent() - [ - StructPropertyHandle->CreatePropertyNameWidget() - ] - .ValueContent() - [ - PropertyCustomizationHelpers::MakePropertyComboBox(WorkerTypeNameProperty, - FOnGetPropertyComboBoxStrings::CreateStatic(&FWorkerTypeCustomization::OnGetStrings), + HeaderRow.NameContent()[StructPropertyHandle->CreatePropertyNameWidget()] + .ValueContent()[PropertyCustomizationHelpers::MakePropertyComboBox( + WorkerTypeNameProperty, FOnGetPropertyComboBoxStrings::CreateStatic(&FWorkerTypeCustomization::OnGetStrings), FOnGetPropertyComboBoxValue::CreateStatic(&FWorkerTypeCustomization::OnGetValue, WorkerTypeNameProperty), - FOnPropertyComboBoxValueSelected::CreateStatic(&FWorkerTypeCustomization::OnValueSelected, WorkerTypeNameProperty)) - ]; + FOnPropertyComboBoxValueSelected::CreateStatic(&FWorkerTypeCustomization::OnValueSelected, WorkerTypeNameProperty))]; } } -void FWorkerTypeCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +void FWorkerTypeCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, + class IDetailChildrenBuilder& StructBuilder, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) { } -void FWorkerTypeCustomization::OnGetStrings(TArray>& OutComboBoxStrings, TArray>& OutToolTips, TArray& OutRestrictedItems) +void FWorkerTypeCustomization::OnGetStrings(TArray>& OutComboBoxStrings, + TArray>& OutToolTips, TArray& OutRestrictedItems) { if (const USpatialGDKSettings* Settings = GetDefault()) { diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDefaultLaunchConfigGenerator.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDefaultLaunchConfigGenerator.h index a76408069e..4912f20323 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDefaultLaunchConfigGenerator.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDefaultLaunchConfigGenerator.h @@ -4,18 +4,20 @@ #include "Logging/LogMacros.h" -#include "SpatialGDKSettings.h" #include "SpatialGDKEditorSettings.h" +#include "SpatialGDKSettings.h" DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKDefaultLaunchConfigGenerator, Log, All); class UAbstractRuntimeLoadBalancingStrategy; struct FSpatialLaunchConfigDescription; -uint32 SPATIALGDKEDITOR_API GetWorkerCountFromWorldSettings(const UWorld& World); +uint32 SPATIALGDKEDITOR_API GetWorkerCountFromWorldSettings(const UWorld& World, bool bForceNonEditorSettings = false); -bool SPATIALGDKEDITOR_API FillWorkerConfigurationFromCurrentMap(FWorkerTypeLaunchSection& OutWorker, FIntPoint& OutWorldDimensions); +bool SPATIALGDKEDITOR_API GenerateLaunchConfig(const FString& LaunchConfigPath, + const FSpatialLaunchConfigDescription* InLaunchConfigDescription, bool bGenerateCloudConfig); -bool SPATIALGDKEDITOR_API GenerateLaunchConfig(const FString& LaunchConfigPath, const FSpatialLaunchConfigDescription* InLaunchConfigDescription, const FWorkerTypeLaunchSection& InWorker); +bool SPATIALGDKEDITOR_API ConvertToClassicConfig(const FString& LaunchConfigPath, + const FSpatialLaunchConfigDescription* InLaunchConfigDescription); -bool SPATIALGDKEDITOR_API ValidateGeneratedLaunchConfig(const FSpatialLaunchConfigDescription& LaunchConfigDesc, const FWorkerTypeLaunchSection& InWorker); +bool SPATIALGDKEDITOR_API ValidateGeneratedLaunchConfig(const FSpatialLaunchConfigDescription& LaunchConfigDesc); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDefaultWorkerJsonGenerator.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDefaultWorkerJsonGenerator.h index 3942d8b250..73533a1dca 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDefaultWorkerJsonGenerator.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDefaultWorkerJsonGenerator.h @@ -7,4 +7,4 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKDefaultWorkerJsonGenerator, Log, All); SPATIALGDKEDITOR_API bool GenerateAllDefaultWorkerJsons(bool& bOutRedeployRequired); -SPATIALGDKEDITOR_API bool GenerateDefaultWorkerJson(const FString& WorkerJsonName, const FString& WorkerTypeName, bool& bOutRedeployRequired); +SPATIALGDKEDITOR_API bool GenerateDefaultWorkerJson(const FString& WorkerJsonName, bool& bOutRedeployRequired); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditor.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditor.h index e5da0b0627..e097aa8cc9 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditor.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditor.h @@ -25,14 +25,24 @@ class SPATIALGDKEDITOR_API FSpatialGDKEditor FullAssetScan }; - bool GenerateSchema(ESchemaGenerationMethod Method); - void GenerateSnapshot(UWorld* World, FString SnapshotFilename, FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback, FSpatialGDKEditorErrorHandler ErrorCallback); - void StartCloudDeployment(const FCloudDeploymentConfiguration& Configuration, FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback); + enum ESchemaDatabaseValidationResult + { + Ok, + NotFound, + OldVersion, + }; + + void GenerateSchema(ESchemaGenerationMethod Method, TFunction ResultCallback); + void GenerateSnapshot(UWorld* World, FString SnapshotFilename, FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback, + FSpatialGDKEditorErrorHandler ErrorCallback); + void StartCloudDeployment(const FCloudDeploymentConfiguration& Configuration, FSimpleDelegate SuccessCallback, + FSimpleDelegate FailureCallback); void StopCloudDeployment(FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback); bool IsSchemaGeneratorRunning() { return bSchemaGeneratorRunning; } bool FullScanRequired(); - bool IsSchemaGenerated(); + + ESchemaDatabaseValidationResult ValidateSchemaDatabase(); void SetProjectName(const FString& InProjectName); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorModule.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorModule.h index 9cdfc8e51d..356e263481 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorModule.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorModule.h @@ -14,21 +14,21 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKEditorModule, Log, All); class FSpatialGDKEditorModule : public ISpatialGDKEditorModule { public: - FSpatialGDKEditorModule(); virtual void StartupModule() override; virtual void ShutdownModule() override; - virtual bool SupportsDynamicReloading() override - { - return true; - } + virtual bool SupportsDynamicReloading() override { return true; } + + TSharedPtr GetSpatialGDKEditorInstance() const { return SpatialGDKEditorInstance; } - TSharedPtr GetSpatialGDKEditorInstance() const - { - return SpatialGDKEditorInstance; - } + virtual void TakeSnapshot(UWorld* World, FSpatialSnapshotTakenFunc OnSnapshotTaken) override; + + /* Way to force a deployment to be launched with a specific snapshot. This is meant to be override-able only + * at runtime, specifically for Functional Testing purposes. + */ + FString ForceUseSnapshotAtPath; private: // Local deployment connection flow @@ -52,6 +52,8 @@ class FSpatialGDKEditorModule : public ISpatialGDKEditorModule virtual bool ForEveryServerWorker(TFunction Function) const override; + virtual FPlayInEditorSettingsOverride GetPlayInEditorSettingsOverrideForTesting(UWorld* World) const; + private: void RegisterSettings(); void UnregisterSettings(); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorPackageAssembly.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorPackageAssembly.h index 1ec5b2cac5..5686a0a27f 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorPackageAssembly.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorPackageAssembly.h @@ -51,7 +51,7 @@ class SPATIALGDKEDITOR_API FSpatialGDKPackageAssembly : public TSharedFromThis GetAllSupportedClasses(const TArray& AllClasses); - - SPATIALGDKEDITOR_API bool SpatialGDKGenerateSchema(); - - SPATIALGDKEDITOR_API bool SpatialGDKGenerateSchemaForClasses(TSet Classes, FString SchemaOutputPath = ""); - - 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); - - SPATIALGDKEDITOR_API bool IsAssetReadOnly(const FString& FileName); - - SPATIALGDKEDITOR_API bool GeneratedSchemaDatabaseExists(); - - SPATIALGDKEDITOR_API bool SaveSchemaDatabase(const FString& PackagePath); - - SPATIALGDKEDITOR_API bool DeleteSchemaDatabase(const FString& PackagePath); - - SPATIALGDKEDITOR_API void ResetSchemaGeneratorState(); - - SPATIALGDKEDITOR_API void ResetSchemaGeneratorStateAndCleanupFolders(); - - SPATIALGDKEDITOR_API bool GeneratedSchemaFolderExists(); - - SPATIALGDKEDITOR_API bool RefreshSchemaFiles(const FString& SchemaOutputPath); - - SPATIALGDKEDITOR_API void CopyWellKnownSchemaFiles(const FString& GDKSchemaCopyDir, const FString& CoreSDKSchemaCopyDir); - - SPATIALGDKEDITOR_API bool RunSchemaCompiler(); - } -} +namespace Schema +{ +SPATIALGDKEDITOR_API bool IsSupportedClass(const UClass* SupportedClass); + +SPATIALGDKEDITOR_API TSet GetAllSupportedClasses(const TArray& AllClasses); + +SPATIALGDKEDITOR_API bool SpatialGDKGenerateSchema(); + +SPATIALGDKEDITOR_API bool SpatialGDKGenerateSchemaForClasses(TSet Classes, FString SchemaOutputPath = ""); + +SPATIALGDKEDITOR_API void SpatialGDKSanitizeGeneratedSchema(); + +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); + +SPATIALGDKEDITOR_API bool IsAssetReadOnly(const FString& FileName); + +SPATIALGDKEDITOR_API bool GeneratedSchemaDatabaseExists(); + +SPATIALGDKEDITOR_API FSpatialGDKEditor::ESchemaDatabaseValidationResult ValidateSchemaDatabase(); + +SPATIALGDKEDITOR_API USchemaDatabase* InitialiseSchemaDatabase(const FString& PackagePath); + +SPATIALGDKEDITOR_API bool SaveSchemaDatabase(USchemaDatabase* SchemaDatabase); + +SPATIALGDKEDITOR_API bool DeleteSchemaDatabase(const FString& PackagePath); + +SPATIALGDKEDITOR_API void ResetSchemaGeneratorState(); + +SPATIALGDKEDITOR_API void ResetSchemaGeneratorStateAndCleanupFolders(); + +SPATIALGDKEDITOR_API bool GeneratedSchemaFolderExists(); + +SPATIALGDKEDITOR_API bool RefreshSchemaFiles(const FString& SchemaOutputPath, const bool bDeleteExistingSchema = true, + const bool bCreateDirectoryTree = true); + +SPATIALGDKEDITOR_API void CopyWellKnownSchemaFiles(const FString& GDKSchemaCopyDir, const FString& CoreSDKSchemaCopyDir); + +SPATIALGDKEDITOR_API bool RunSchemaCompiler(); + +SPATIALGDKEDITOR_API void WriteServerAuthorityComponentSet(const USchemaDatabase* SchemaDatabase, + TArray& ServerAuthoritativeComponentIds); + +SPATIALGDKEDITOR_API void WriteClientAuthorityComponentSet(); + +SPATIALGDKEDITOR_API void WriteComponentSetBySchemaType(const USchemaDatabase* SchemaDatabase, ESchemaComponentType SchemaType); + +} // namespace Schema +} // namespace SpatialGDKEditor diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h index b48f98a046..4e0c3b3e43 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h @@ -28,33 +28,12 @@ struct FWorldLaunchSection FWorldLaunchSection() : Dimensions(2000, 2000) - , ChunkEdgeLengthMeters(50) - , SnapshotWritePeriodSeconds(0) { - 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 = (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 = (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 = (DisplayName = "Snapshot write period in seconds")) - int32 SnapshotWritePeriodSeconds; - - /** Legacy non-worker flag configurations. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config) - TMap LegacyFlags; - - /** Legacy JVM configurations. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Legacy Java parameters")) - TMap LegacyJavaParams; }; USTRUCT() @@ -63,53 +42,34 @@ struct FWorkerPermissionsSection GENERATED_BODY() FWorkerPermissionsSection() - : bAllPermissions(true) - , bAllowEntityCreation(true) + : bAllowEntityCreation(true) , bAllowEntityDeletion(true) + , bDisconnectWorker(true) + , bReserveEntityID(true) , bAllowEntityQuery(true) - , Components() { } - /** Gives all permissions to a worker instance. */ - 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", DisplayName = "Allow entity creation")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Allow entity creation")) bool bAllowEntityCreation; /** Enables a worker instance to delete entities. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "!bAllPermissions", DisplayName = "Allow entity deletion")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (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", DisplayName = "Allow entity query")) - bool bAllowEntityQuery; - - /** Specifies which components can be returned in the query result. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "!bAllPermissions", DisplayName = "Component queries")) - TArray Components; -}; - -USTRUCT() -struct FLoginRateLimitSection -{ - GENERATED_BODY() + /** Enables a worker instance to disconnect other workers. */ + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Allow worker disconnections")) + bool bDisconnectWorker; - FLoginRateLimitSection() - : Duration() - , RequestsPerDuration(0) - { - } + /** Enables a worker instance to reserve entity IDs. */ + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Allow entity ID reservations")) + bool bReserveEntityID; - /** The duration for which worker connection requests will be limited. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config) - FString Duration; - - /** The connection request limit for the duration. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ClampMin = "1", UIMin = "1")) - int32 RequestsPerDuration; + /** 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 = (DisplayName = "Allow entity queries")) + bool bAllowEntityQuery; }; USTRUCT() @@ -125,26 +85,29 @@ struct FWorkerTypeLaunchSection { } - /** Worker type name, deprecated in favor of defining them in the runtime settings.*/ - UPROPERTY(config) - FName WorkerTypeName_DEPRECATED; + /** Worker type name. */ + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config) + FName WorkerTypeName; + + /** Flags defined for a worker instance. */ + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Flags")) + TMap Flags; /** Defines the worker instance's permissions. */ UPROPERTY(Category = "SpatialGDK", EditAnywhere, config) FWorkerPermissionsSection WorkerPermissions; /** Automatically or manually specifies the number of worker instances to launch in editor. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Automatically compute number of instances to launch in Editor")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, + meta = (DisplayName = "Automatically compute number of instances to launch in Editor")) bool bAutoNumEditorInstances; /** Number of instances to launch when playing in editor. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Instances to launch in editor", ClampMin = "0", UIMin = "0", EditCondition = "!bAutoNumEditorInstances")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, + meta = (DisplayName = "Manual number of instances to launch in Editor", ClampMin = "0", UIMin = "0", + EditCondition = "!bAutoNumEditorInstances")) int32 NumEditorInstances; - /** Flags defined for a worker instance. */ - 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 = (DisplayName = "Manual worker connection only")) bool bManualWorkerConnectionOnly; @@ -161,7 +124,10 @@ struct FSpatialLaunchConfigDescription : bUseDefaultTemplateForRuntimeVariant(true) , Template() , World() - {} + , MaxConcurrentWorkers(1000) + { + ServerWorkerConfiguration.WorkerTypeName = SpatialConstants::DefaultServerWorkerType; + } const FString& GetTemplate() const; @@ -175,51 +141,59 @@ struct FSpatialLaunchConfigDescription UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "!bUseDefaultTemplateForRuntimeVariant")) FString Template; + /** Runtime flag configurations. */ + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config) + TMap RuntimeFlags; + + /** Main server worker configuration, usually known as the UnrealWorker */ + UPROPERTY(Category = "SpatialGDK", EditAnywhere, EditFixedSize, config) + FWorkerTypeLaunchSection ServerWorkerConfiguration; + + /** Additional worker configurations used for testing and cloud deploying */ + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Additional Workers")) + TArray AdditionalWorkerConfigs; + /** Configuration for the simulated world. */ UPROPERTY(Category = "SpatialGDK", EditAnywhere, config) FWorldLaunchSection World; - /** Worker-specific configuration parameters. */ - UPROPERTY(config) - TArray ServerWorkers_DEPRECATED; - - UPROPERTY(Category = "SpatialGDK", EditAnywhere, EditFixedSize, config) - FWorkerTypeLaunchSection ServerWorkerConfig; + /** The connection request limit for the deployment. */ + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ClampMin = "1", UIMin = "1")) + int32 MaxConcurrentWorkers; }; /** -* Enumerates available Region Codes -*/ + * Enumerates available Region Codes + */ UENUM() namespace ERegionCode { - enum Type - { - US = 1, - EU, - AP, - CN UMETA(Hidden) - }; +enum Type +{ + US = 1, + EU, + AP, + CN UMETA(Hidden) +}; } UENUM() namespace ESpatialOSNetFlow { - enum Type - { - LocalDeployment, - CloudDeployment - }; +enum Type +{ + LocalDeployment, + CloudDeployment +}; } UENUM() namespace ESpatialOSRuntimeVariant { - enum Type - { - Standard, - CompatibilityMode - }; +enum Type +{ + Standard +}; } USTRUCT() @@ -230,16 +204,22 @@ struct SPATIALGDKEDITOR_API FRuntimeVariantVersion GENERATED_BODY() - FRuntimeVariantVersion() : PinnedVersion(SpatialGDKServicesConstants::SpatialOSRuntimePinnedStandardVersion) - {} + FRuntimeVariantVersion() + : PinnedVersion(SpatialGDKServicesConstants::SpatialOSRuntimePinnedStandardVersion) + { + } - FRuntimeVariantVersion(const FString& InPinnedVersion) : PinnedVersion(InPinnedVersion) - {} + FRuntimeVariantVersion(const FString& InPinnedVersion) + : PinnedVersion(InPinnedVersion) + { + } - /** Returns the Runtime version to use for cloud deployments, either the pinned one, or the user-specified one depending on the settings. */ + /** Returns the Runtime version to use for cloud deployments, either the pinned one, or the user-specified one depending on the + * settings. */ const FString& GetVersionForCloud() const; - /** Returns the Runtime version to use for local deployments, either the pinned one, or the user-specified one depending on the settings. */ + /** Returns the Runtime version to use for local deployments, either the pinned one, or the user-specified one depending on the + * settings. */ const FString& GetVersionForLocal() const; bool GetUseGDKPinnedRuntimeVersionForLocal() const { return bUseGDKPinnedRuntimeVersionForLocal; } @@ -249,12 +229,13 @@ struct SPATIALGDKEDITOR_API FRuntimeVariantVersion const FString& GetPinnedVersion() const { return PinnedVersion; } private: - - /** Whether to use the GDK-associated SpatialOS runtime version for local deployments, or to use the one specified in the RuntimeVersion field. */ + /** Whether to use the GDK-associated SpatialOS runtime version for local deployments, or to use the one specified in the RuntimeVersion + * field. */ UPROPERTY(EditAnywhere, config, Category = "Runtime", meta = (DisplayName = "Use GDK pinned runtime version for local")) bool bUseGDKPinnedRuntimeVersionForLocal = true; - /** Whether to use the GDK-associated SpatialOS runtime version for cloud deployments, or to use the one specified in the RuntimeVersion field. */ + /** Whether to use the GDK-associated SpatialOS runtime version for cloud deployments, or to use the one specified in the RuntimeVersion + * field. */ UPROPERTY(EditAnywhere, config, Category = "Runtime", meta = (DisplayName = "Use GDK pinned runtime version for cloud")) bool bUseGDKPinnedRuntimeVersionForCloud = true; @@ -271,6 +252,18 @@ struct SPATIALGDKEDITOR_API FRuntimeVariantVersion FString PinnedVersion; }; +/** Different modes to automatically stop of the local SpatialOS deployment. */ +UENUM() +enum class EAutoStopLocalDeploymentMode : uint8 +{ + /** Never stop the local SpatialOS deployment automatically. */ + Never = 0, + /** Automatically stop the local SpatialOS deployment on end of play in editor. */ + OnEndPIE = 1, + /** Only stop the local SpatialOS deployment automatically when exiting the editor. */ + OnExitEditor = 2 +}; + UCLASS(config = SpatialGDKEditorSettings, defaultconfig, HideCategories = LoadBalancing) class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject { @@ -283,62 +276,76 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject virtual void PostInitProperties() override; 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 = (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. */ + /** 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 = (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. */ + /** 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 = (DisplayName = "Auto-generate launch configuration file")) bool bGenerateDefaultLaunchConfig; /** Returns which runtime variant we should use. */ - TEnumAsByte GetSpatialOSRuntimeVariant() const { return RuntimeVariant; } + TEnumAsByte GetSpatialOSRuntimeVariant() const { return ESpatialOSRuntimeVariant::Standard; } - /** Returns the version information for the currently set variant*/ + /** Returns the version information for the currently set runtime variant*/ const FRuntimeVariantVersion& GetSelectedRuntimeVariantVersion() const { - return const_cast(this)->GetRuntimeVariantVersion(RuntimeVariant); + return const_cast(this)->GetRuntimeVariantVersion(ESpatialOSRuntimeVariant::Standard); } - UPROPERTY(EditAnywhere, config, Category = "Runtime") - TEnumAsByte RuntimeVariant; - - UPROPERTY(EditAnywhere, config, Category = "Runtime", AdvancedDisplay) + UPROPERTY(EditAnywhere, config, Category = "Runtime", meta = (DisplayName = "Runtime versions")) FRuntimeVariantVersion StandardRuntimeVersion; - UPROPERTY(EditAnywhere, config, Category = "Runtime", AdvancedDisplay) - FRuntimeVariantVersion CompatibilityModeRuntimeVersion; + /** Whether to use the GDK-associated SpatialOS Inspector version for local deployments, or to use the one specified in the + * InspectorVersion field. */ + UPROPERTY(EditAnywhere, config, Category = "Inspector", meta = (DisplayName = "Use GDK pinned Inspector version")) + bool bUseGDKPinnedInspectorVersion; + + /** Runtime version to use for local deployments, if not using the GDK pinned version. */ + UPROPERTY(EditAnywhere, config, Category = "Inspector", meta = (EditCondition = "!bUseGDKPinnedInspectorVersion")) + FString InspectorVersionOverride; + + /** Returns the version information for the currently set inspector*/ + const FString& GetInspectorVersion() const + { + return bUseGDKPinnedInspectorVersion ? SpatialGDKServicesConstants::InspectorPinnedVersion : InspectorVersionOverride; + } mutable FOnDefaultTemplateNameRequireUpdate OnDefaultTemplateNameRequireUpdate; private: - FRuntimeVariantVersion& GetRuntimeVariantVersion(ESpatialOSRuntimeVariant::Type); /** 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", DisplayName = "Launch configuration file path")) + UPROPERTY(EditAnywhere, config, Category = "Launch", + meta = (EditCondition = "!bGenerateDefaultLaunchConfig", DisplayName = "Launch configuration file path")) FFilePath SpatialOSLaunchConfig; public: - /** Specify on which IP address the local runtime should be reachable. If empty, the local runtime will not be exposed. Changes are applied on next local deployment startup. */ + /** Specify on which IP address the local runtime should be reachable. If empty, the local runtime will not be exposed. Changes are + * applied on next local deployment startup. */ UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (DisplayName = "Exposed local runtime IP address")) FString ExposedRuntimeIP; - UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (DisplayName = "Stop local deployment on stop play in editor")) - bool bStopLocalDeploymentOnEndPIE; - - /** Select the check box to stop your game’s local deployment when you shut down Unreal Editor. */ - 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 = (DisplayName = "Auto-start local deployment")) bool bAutoStartLocalDeployment; + /** Show worker boundaries in the editor. */ + UPROPERTY(EditAnywhere, config, Category = "Debug", meta = (DisplayName = "Enable spatial debugger in editor")) + bool bSpatialDebuggerEditorEnabled; + + /** Allows the local SpatialOS deployment to be automatically stopped. */ + UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (DisplayName = "Auto-stop local deployment")) + EAutoStopLocalDeploymentMode AutoStopLocalDeployment; + + /** Stop play in editor when Automation Manager finishes running Tests. If false, the native Unreal Engine behaviour maintains of + * leaving the last map PIE running. */ + UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (DisplayName = "Stop play in editor on Testing completed")) + bool bStopPIEOnTestingCompleted; + private: /** Name of your SpatialOS snapshot file that will be generated. */ UPROPERTY(EditAnywhere, config, Category = "Snapshots", meta = (DisplayName = "Snapshot to save")) @@ -348,13 +355,16 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject UPROPERTY(EditAnywhere, config, Category = "Snapshots", meta = (DisplayName = "Snapshot to load")) FString SpatialOSSnapshotToLoad; - UPROPERTY(EditAnywhere, config, Category = "Schema Generation", meta = (Tooltip = "Platform to target when using Cook And Generate Schema (if empty, defaults to Editor's platform)")) + UPROPERTY(EditAnywhere, config, Category = "Schema Generation", + meta = (Tooltip = "Platform to target when using Cook And Generate Schema (if empty, defaults to Editor's platform)")) FString CookAndGeneratePlatform; - UPROPERTY(EditAnywhere, config, Category = "Schema Generation", meta = (Tooltip = "Additional arguments passed to Cook And Generate Schema")) + UPROPERTY(EditAnywhere, config, Category = "Schema Generation", + meta = (Tooltip = "Additional arguments passed to Cook And Generate Schema")) FString CookAndGenerateAdditionalArguments; - /** Add flags to the `spatial local launch` command; they alter the deployment’s behavior. Select the trash icon to remove all the flags.*/ + /** 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 = (DisplayName = "Command line flags for local launch")) TArray SpatialOSCommandLineLaunchFlags; @@ -427,17 +437,22 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject UPROPERTY(EditAnywhere, config, Category = "Cloud Connection") FString DevelopmentAuthenticationToken; - /** Whether to start local server worker when connecting to cloud deployment. If selected, make sure that the cloud deployment you want to connect to is not automatically launching Server-workers. (That your workers have "manual_connection_only" enabled) */ - UPROPERTY(EditAnywhere, config, Category = "Cloud Connection", meta = (DisplayName = "Connect local server worker to the cloud deployment")) + /** Whether to start local server worker when connecting to cloud deployment. If selected, make sure that the cloud deployment you want + * to connect to is not automatically launching Server-workers. (That your workers have "manual_connection_only" enabled) */ + UPROPERTY(EditAnywhere, config, Category = "Cloud Connection", + meta = (DisplayName = "Connect local server worker to the cloud deployment")) bool bConnectServerToCloud; /** Port on which the receptionist proxy will be available. */ - UPROPERTY(EditAnywhere, config, Category = "Cloud Connection", meta = (EditCondition = "bConnectServerToCloud", DisplayName = "Local Receptionist Port")) + UPROPERTY(EditAnywhere, config, Category = "Cloud Connection", + meta = (EditCondition = "bConnectServerToCloud", DisplayName = "Local Receptionist Port")) int32 LocalReceptionistPort; /** Network address to bind the receptionist proxy to. */ - UPROPERTY(EditAnywhere, config, Category = "Cloud Connection", meta = (EditCondition = "bConnectServerToCloud", DisplayName = "Listening Address")) + UPROPERTY(EditAnywhere, config, Category = "Cloud Connection", + meta = (EditCondition = "bConnectServerToCloud", DisplayName = "Listening Address")) FString ListeningAddress; + private: UPROPERTY(config) TEnumAsByte SimulatedPlayerDeploymentRegionCode; @@ -458,15 +473,19 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject static bool IsManualWorkerConnectionSet(const FString& LaunchConfigPath, TArray& OutWorkersManuallyLaunched); public: - /** If checked, use the connection flow override below instead of the one selected in the editor when building the command line for mobile. */ - UPROPERTY(EditAnywhere, config, Category = "Mobile", meta = (DisplayName = "Override Mobile Connection Flow (only for Push settings to device)")) + /** If checked, use the connection flow override below instead of the one selected in the editor when building the command line for + * mobile. */ + UPROPERTY(EditAnywhere, config, Category = "Mobile", + meta = (DisplayName = "Override Mobile Connection Flow (only for Push settings to device)")) bool bMobileOverrideConnectionFlow; /** The connection flow that should be used when pushing command line to the mobile device. */ - UPROPERTY(EditAnywhere, config, Category = "Mobile", meta = (EditCondition = "bMobileOverrideConnectionFlow", DisplayName = "Mobile Connection Flow")) + UPROPERTY(EditAnywhere, config, Category = "Mobile", + meta = (EditCondition = "bMobileOverrideConnectionFlow", DisplayName = "Mobile Connection Flow")) TEnumAsByte MobileConnectionFlow; - /** If specified, use this IP instead of 'Exposed local runtime IP address' when building the command line to push to the mobile device. */ + /** If specified, use this IP instead of 'Exposed local runtime IP address' when building the command line to push to the mobile device. + */ UPROPERTY(EditAnywhere, config, Category = "Mobile", meta = (DisplayName = "Local Runtime IP Override")) FString MobileRuntimeIPOverride; @@ -480,57 +499,45 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject bool bPackageMobileCommandLineArgs; /** If checked, PIE clients will be automatically started when launching on a device and connecting to local deployment. */ - UPROPERTY(EditAnywhere, config, Category = "Mobile", meta = (DisplayName = "Start PIE Clients when launching on a device with local deployment flow")) + UPROPERTY(EditAnywhere, config, Category = "Mobile", + meta = (DisplayName = "Start PIE Clients when launching on a device with local deployment flow")) bool bStartPIEClientsWithLocalLaunchOnDevice; 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", DisplayName = "Launch configuration file options")) + /** 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", DisplayName = "Launch configuration file options")) FSpatialLaunchConfigDescription LaunchConfigDesc; /** Select the connection flow that should be used when starting the game with Spatial networking enabled. */ UPROPERTY(EditAnywhere, config, Category = "Connection Flow", meta = (DisplayName = "SpatialOS Connection Flow Type")) TEnumAsByte SpatialOSNetFlowType; - FORCEINLINE FString GetSpatialOSLaunchConfig() const - { - return SpatialOSLaunchConfig.FilePath; - } + FORCEINLINE FString GetSpatialOSLaunchConfig() const { return SpatialOSLaunchConfig.FilePath; } FORCEINLINE FString GetSpatialOSSnapshotToSave() const { - return SpatialOSSnapshotToSave.IsEmpty() - ? FString(TEXT("default.snapshot")) - : SpatialOSSnapshotToSave; + return SpatialOSSnapshotToSave.IsEmpty() ? FString(TEXT("default.snapshot")) : SpatialOSSnapshotToSave; } FORCEINLINE FString GetSpatialOSSnapshotToSavePath() const { - return FPaths::Combine(GetSpatialOSSnapshotFolderPath(), GetSpatialOSSnapshotToSave()); + return FPaths::Combine(SpatialGDKServicesConstants::SpatialOSSnapshotFolderPath, GetSpatialOSSnapshotToSave()); } FORCEINLINE FString GetSpatialOSSnapshotToLoad() const { - return SpatialOSSnapshotToLoad.IsEmpty() - ? FString(TEXT("default.snapshot")) - : SpatialOSSnapshotToLoad; + return SpatialOSSnapshotToLoad.IsEmpty() ? FString(TEXT("default.snapshot")) : SpatialOSSnapshotToLoad; } FString GetCookAndGenerateSchemaTargetPlatform() const; - FORCEINLINE FString GetCookAndGenerateSchemaAdditionalArgs() const - { - return CookAndGenerateAdditionalArguments; - } + FORCEINLINE FString GetCookAndGenerateSchemaAdditionalArgs() const { return CookAndGenerateAdditionalArguments; } FORCEINLINE FString GetSpatialOSSnapshotToLoadPath() const { - return FPaths::Combine(GetSpatialOSSnapshotFolderPath(), GetSpatialOSSnapshotToLoad()); - } - - FORCEINLINE FString GetSpatialOSSnapshotFolderPath() const - { - return FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("snapshots")); + return FPaths::Combine(SpatialGDKServicesConstants::SpatialOSSnapshotFolderPath, GetSpatialOSSnapshotToLoad()); } FORCEINLINE FString GetGeneratedSchemaOutputFolder() const @@ -557,29 +564,16 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject } void SetPrimaryDeploymentName(const FString& Name); - FORCEINLINE FString GetPrimaryDeploymentName() const - { - return PrimaryDeploymentName; - } + FORCEINLINE FString GetPrimaryDeploymentName() const { return PrimaryDeploymentName; } void SetAssemblyName(const FString& Name); - FORCEINLINE FString GetAssemblyName() const - { - return AssemblyName; - } + FORCEINLINE FString GetAssemblyName() const { return AssemblyName; } void SetPrimaryLaunchConfigPath(const FString& Path); - FORCEINLINE FString GetPrimaryLaunchConfigPath() const - { - - return PrimaryLaunchConfigPath.FilePath; - } + FORCEINLINE FString GetPrimaryLaunchConfigPath() const { return PrimaryLaunchConfigPath.FilePath; } void SetSnapshotPath(const FString& Path); - FORCEINLINE FString GetSnapshotPath() const - { - return SnapshotPath.FilePath; - } + FORCEINLINE FString GetSnapshotPath() const { return SnapshotPath.FilePath; } void SetPrimaryRegionCode(const ERegionCode::Type RegionCode); FORCEINLINE FText GetPrimaryRegionCodeText() const @@ -594,28 +588,16 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject return Region->GetDisplayNameTextByValue(static_cast(PrimaryDeploymentRegionCode.GetValue())); } - const ERegionCode::Type GetPrimaryRegionCode() const - { - return PrimaryDeploymentRegionCode; - } + const ERegionCode::Type GetPrimaryRegionCode() const { return PrimaryDeploymentRegionCode; } void SetMainDeploymentCluster(const FString& NewCluster); - FORCEINLINE FString GetMainDeploymentCluster() const - { - return MainDeploymentCluster; - } + FORCEINLINE FString GetMainDeploymentCluster() const { return MainDeploymentCluster; } void SetDeploymentTags(const FString& Tags); - FORCEINLINE FString GetDeploymentTags() const - { - return DeploymentTags; - } + FORCEINLINE FString GetDeploymentTags() const { return DeploymentTags; } void SetAssemblyBuildConfiguration(const FString& Configuration); - FORCEINLINE FText GetAssemblyBuildConfiguration() const - { - return FText::FromString(AssemblyBuildConfiguration); - } + FORCEINLINE FText GetAssemblyBuildConfiguration() const { return FText::FromString(AssemblyBuildConfiguration); } void SetSimulatedPlayerRegionCode(const ERegionCode::Type RegionCode); FORCEINLINE FText GetSimulatedPlayerRegionCode() const @@ -631,86 +613,55 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject } void SetSimulatedPlayersEnabledState(bool IsEnabled); - FORCEINLINE bool IsSimulatedPlayersEnabled() const - { - return bSimulatedPlayersIsEnabled; - } + FORCEINLINE bool IsSimulatedPlayersEnabled() const { return bSimulatedPlayersIsEnabled; } + + void SetSpatialDebuggerEditorEnabled(bool IsEnabled); + FORCEINLINE bool IsSpatialDebuggerEditorEnabled() const { return bSpatialDebuggerEditorEnabled; } void SetAutoGenerateCloudLaunchConfigEnabledState(bool IsEnabled); - FORCEINLINE bool ShouldAutoGenerateCloudLaunchConfig() const - { - return bIsAutoGenerateCloudConfigEnabled; - } + FORCEINLINE bool ShouldAutoGenerateCloudLaunchConfig() const { return bIsAutoGenerateCloudConfigEnabled; } void SetBuildAndUploadAssembly(bool bBuildAndUpload); - FORCEINLINE bool ShouldBuildAndUploadAssembly() const - { - return bBuildAndUploadAssembly; - } + FORCEINLINE bool ShouldBuildAndUploadAssembly() const { return bBuildAndUploadAssembly; } void SetForceAssemblyOverwrite(bool bForce); - FORCEINLINE bool IsForceAssemblyOverwriteEnabled() const - { - return bForceAssemblyOverwrite; - } + FORCEINLINE bool IsForceAssemblyOverwriteEnabled() const { return bForceAssemblyOverwrite; } void SetBuildClientWorker(bool bBuild); - FORCEINLINE bool IsBuildClientWorkerEnabled() const - { - return bBuildClientWorker; - } + FORCEINLINE bool IsBuildClientWorkerEnabled() const { return bBuildClientWorker; } void SetGenerateSchema(bool bGenerate); - FORCEINLINE bool IsGenerateSchemaEnabled() const - { - return bGenerateSchema; - } + FORCEINLINE bool IsGenerateSchemaEnabled() const { return bGenerateSchema; } void SetGenerateSnapshot(bool bGenerate); - FORCEINLINE bool IsGenerateSnapshotEnabled() const - { - return bGenerateSnapshot; - } + FORCEINLINE bool IsGenerateSnapshotEnabled() const { return bGenerateSnapshot; } void SetUseGDKPinnedRuntimeVersionForLocal(ESpatialOSRuntimeVariant::Type Variant, bool IsEnabled); void SetUseGDKPinnedRuntimeVersionForCloud(ESpatialOSRuntimeVariant::Type Variant, bool IsEnabled); void SetCustomCloudSpatialOSRuntimeVersion(ESpatialOSRuntimeVariant::Type Variant, const FString& Version); void SetSimulatedPlayerDeploymentName(const FString& Name); - FORCEINLINE FString GetSimulatedPlayerDeploymentName() const - { - return SimulatedPlayerDeploymentName; - } + FORCEINLINE FString GetSimulatedPlayerDeploymentName() const { return SimulatedPlayerDeploymentName; } void SetConnectServerToCloud(bool bIsEnabled); - FORCEINLINE bool IsConnectServerToCloudEnabled() const - { - return bConnectServerToCloud; - } + FORCEINLINE bool IsConnectServerToCloudEnabled() const { return bConnectServerToCloud; } void SetSimulatedPlayerCluster(const FString& NewCluster); - FORCEINLINE FString GetSimulatedPlayerCluster() const - { - return SimulatedPlayerCluster; - } + FORCEINLINE FString GetSimulatedPlayerCluster() const { return SimulatedPlayerCluster; } - FORCEINLINE FString GetSimulatedPlayerLaunchConfigPath() const - { - return SimulatedPlayerLaunchConfigPath; - } + FORCEINLINE FString GetSimulatedPlayerLaunchConfigPath() const { return SimulatedPlayerLaunchConfigPath; } void SetNumberOfSimulatedPlayers(uint32 Number); - FORCEINLINE uint32 GetNumberOfSimulatedPlayers() const - { - return NumberOfSimulatedPlayers; - } + FORCEINLINE uint32 GetNumberOfSimulatedPlayers() const { return NumberOfSimulatedPlayers; } FORCEINLINE FString GetDeploymentLauncherPath() const { - return FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(TEXT("SpatialGDK/Binaries/ThirdParty/Improbable/Programs/DeploymentLauncher")); + return FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory( + TEXT("SpatialGDK/Binaries/ThirdParty/Improbable/Programs/DeploymentLauncher")); } bool IsDeploymentConfigurationValid() const; + bool CheckManualWorkerConnectionOnLaunch() const; void SetDevelopmentAuthenticationToken(const FString& Token); @@ -722,4 +673,5 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject static bool IsProjectNameValid(const FString& Name); static bool IsAssemblyNameValid(const FString& Name); static bool IsDeploymentNameValid(const FString& Name); + static void TrimTMap(TMap& Map); }; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialLaunchConfigCustomization.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialLaunchConfigCustomization.h index 72b1724117..dc0d0c6cbb 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialLaunchConfigCustomization.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialLaunchConfigCustomization.h @@ -8,10 +8,11 @@ class FSpatialLaunchConfigCustomization : public IPropertyTypeCustomization { public: - static TSharedRef MakeInstance(); /** IPropertyTypeCustomization interface */ - virtual void CustomizeHeader(TSharedRef StructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; - virtual void CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + virtual void CustomizeHeader(TSharedRef StructPropertyHandle, class FDetailWidgetRow& HeaderRow, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + virtual void CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; }; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialRuntimeVersionCustomization.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialRuntimeVersionCustomization.h index cf7f052e3d..e68af7017f 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialRuntimeVersionCustomization.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialRuntimeVersionCustomization.h @@ -5,12 +5,14 @@ #include "CoreMinimal.h" #include "IPropertyTypeCustomization.h" -class FSpatialRuntimeVersionCustomization :public IPropertyTypeCustomization +class FSpatialRuntimeVersionCustomization : public IPropertyTypeCustomization { public: static TSharedRef MakeInstance(); /** IPropertyTypeCustomization interface */ - virtual void CustomizeHeader(TSharedRef StructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; - virtual void CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + virtual void CustomizeHeader(TSharedRef StructPropertyHandle, class FDetailWidgetRow& HeaderRow, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + virtual void CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; }; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/CodeWriter.h b/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/CodeWriter.h similarity index 71% rename from SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/CodeWriter.h rename to SpatialGDK/Source/SpatialGDKEditor/Public/Utils/CodeWriter.h index 5042adb7dd..a00da471cd 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/CodeWriter.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/CodeWriter.h @@ -9,20 +9,11 @@ struct FFunctionSignature FString Type; FString NameAndParams; - FString Declaration() const - { - return FString::Printf(TEXT("%s %s;"), *Type, *NameAndParams); - } + FString Declaration() const { return FString::Printf(TEXT("%s %s;"), *Type, *NameAndParams); } - FString Definition() const - { - return FString::Printf(TEXT("%s %s"), *Type, *NameAndParams); - } + FString Definition() const { return FString::Printf(TEXT("%s %s"), *Type, *NameAndParams); } - FString Definition(const FString& TypeName) const - { - return FString::Printf(TEXT("%s %s::%s"), *Type, *TypeName, *NameAndParams); - } + FString Definition(const FString& TypeName) const { return FString::Printf(TEXT("%s %s::%s"), *Type, *TypeName, *NameAndParams); } }; class FCodeWriter @@ -33,7 +24,7 @@ class FCodeWriter template FCodeWriter& Printf(const FString& Format, const T&... Args) { - return Print(FString::Format(*Format, TArray{Args...})); + return Print(FString::Format(*Format, TArray{ Args... })); } FCodeWriter& PrintNewLine(); @@ -46,6 +37,8 @@ class FCodeWriter FCodeWriter& BeginFunction(const FFunctionSignature& Signature, const FString& TypeName); FCodeWriter& End(); + void RemoveTrailingComma(); + void WriteToFile(const FString& Filename); void Dump(); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/LaunchConfigurationEditor.h b/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/LaunchConfigurationEditor.h index f6dd78eac9..14e979d56b 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/LaunchConfigurationEditor.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/LaunchConfigurationEditor.h @@ -12,8 +12,7 @@ class SWindow; DECLARE_DELEGATE_OneParam(FOnSpatialOSLaunchConfigurationSaved, const FString&) -UCLASS(Transient, CollapseCategories) -class SPATIALGDKEDITOR_API ULaunchConfigurationEditor : public UObject + UCLASS(Transient, CollapseCategories) class SPATIALGDKEDITOR_API ULaunchConfigurationEditor : public UObject { GENERATED_BODY() public: @@ -22,12 +21,19 @@ class SPATIALGDKEDITOR_API ULaunchConfigurationEditor : public UObject UPROPERTY(EditAnywhere, Category = "Launch Configuration") FSpatialLaunchConfigDescription LaunchConfiguration; - typedef void(*OnLaunchConfigurationSaved)(const FString&); + /** Tick this if this configuration will be used for cloud deployments. */ + UPROPERTY(EditAnywhere, Category = "Launch Configuration") + bool bIsCloudConfiguration = true; + + typedef void (*OnLaunchConfigurationSaved)(const FString&); static void OpenModalWindow(TSharedPtr InParentWindow, OnLaunchConfigurationSaved InSaved = nullptr); + protected: void PostInitProperties() override; + virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override; + UFUNCTION(Exec) void SaveConfiguration(); }; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/TransientUObjectEditor.h b/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/TransientUObjectEditor.h index 62dd5a7ddd..649d67ae5e 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/TransientUObjectEditor.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/TransientUObjectEditor.h @@ -17,7 +17,6 @@ class SPATIALGDKEDITOR_API UTransientUObjectEditor : public UObject { GENERATED_BODY() public: - template static T* LaunchTransientUObjectEditor(const FText& EditorName, TSharedPtr ParentWindow) { @@ -25,5 +24,6 @@ class SPATIALGDKEDITOR_API UTransientUObjectEditor : public UObject } private: - static UTransientUObjectEditor* LaunchTransientUObjectEditor(const FText& EditorName, UClass* ObjectClass, TSharedPtr ParentWindow); + static UTransientUObjectEditor* LaunchTransientUObjectEditor(const FText& EditorName, UClass* ObjectClass, + TSharedPtr ParentWindow); }; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/WorkerTypeCustomization.h b/SpatialGDK/Source/SpatialGDKEditor/Public/WorkerTypeCustomization.h index 06b4baea1e..0d40de8499 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/WorkerTypeCustomization.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/WorkerTypeCustomization.h @@ -8,15 +8,17 @@ class FWorkerTypeCustomization : public IPropertyTypeCustomization { public: - static TSharedRef MakeInstance(); /** IPropertyTypeCustomization interface */ - virtual void CustomizeHeader(TSharedRef StructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; - virtual void CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + virtual void CustomizeHeader(TSharedRef StructPropertyHandle, class FDetailWidgetRow& HeaderRow, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + virtual void CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; private: - static void OnGetStrings(TArray>& OutComboBoxStrings, TArray>& OutToolTips, TArray& OutRestrictedItems); + static void OnGetStrings(TArray>& OutComboBoxStrings, TArray>& OutToolTips, + TArray& OutRestrictedItems); static FString OnGetValue(TSharedPtr WorkerTypeNameHandle); static void OnValueSelected(const FString& SelectedValue, TSharedPtr WorkerTypeNameHandle); }; diff --git a/SpatialGDK/Source/SpatialGDKEditor/SpatialGDKEditor.Build.cs b/SpatialGDK/Source/SpatialGDKEditor/SpatialGDKEditor.Build.cs index 5d6d5c3ff5..2854824b5c 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/SpatialGDKEditor.Build.cs +++ b/SpatialGDK/Source/SpatialGDKEditor/SpatialGDKEditor.Build.cs @@ -20,16 +20,19 @@ public SpatialGDKEditor(ReadOnlyTargetRules Target) : base(Target) new string[] { "Core", "CoreUObject", + "DesktopPlatform", "EditorStyle", "Engine", "EngineSettings", - "IOSRuntimeSettings", + "FunctionalTesting", + "IOSRuntimeSettings", "LauncherServices", "Json", "PropertyEditor", "Slate", "SlateCore", "SpatialGDK", + "SpatialGDKFunctionalTests", "SpatialGDKServices", "UATHelper", "UnrealEd", diff --git a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/CookAndGenerateSchemaCommandlet.cpp b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/CookAndGenerateSchemaCommandlet.cpp index b24e91ac1a..d3accdd6a1 100644 --- a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/CookAndGenerateSchemaCommandlet.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/CookAndGenerateSchemaCommandlet.cpp @@ -22,10 +22,7 @@ struct FObjectListener : public FUObjectArray::FUObjectCreateListener GUObjectArray.AddUObjectCreateListener(this); } - void StopListening() - { - GUObjectArray.RemoveUObjectCreateListener(this); - } + void StopListening() { GUObjectArray.RemoveUObjectCreateListener(this); } virtual void NotifyUObjectCreated(const UObjectBase* Object, int32 Index) override { @@ -39,7 +36,7 @@ struct FObjectListener : public FUObjectArray::FUObjectCreateListener if (IsSupportedClass(Object->GetClass())) { UE_LOG(LogCookAndGenerateSchemaCommandlet, Verbose, TEXT("Object [%s] Created, Consider Class [%s] For Schema."), - *Object->GetFName().ToString(), *GetPathNameSafe(Object->GetClass())); + *Object->GetFName().ToString(), *GetPathNameSafe(Object->GetClass())); VisitedClasses->Add(SoftClass); } else @@ -49,10 +46,7 @@ struct FObjectListener : public FUObjectArray::FUObjectCreateListener } } - virtual void OnUObjectArrayShutdown() override - { - GUObjectArray.RemoveUObjectCreateListener(this); - } + virtual void OnUObjectArrayShutdown() override { GUObjectArray.RemoveUObjectCreateListener(this); } private: TSet* VisitedClasses; @@ -77,24 +71,13 @@ int32 UCookAndGenerateSchemaCommandlet::Main(const FString& CmdLineParams) TSet ReferencedClasses; ObjectListener.StartListening(&ReferencedClasses); - UE_LOG(LogCookAndGenerateSchemaCommandlet, Display, TEXT("Try Load Schema Database.")); + UE_LOG(LogCookAndGenerateSchemaCommandlet, Display, TEXT("Try load schema database.")); if (IsAssetReadOnly(SpatialConstants::SCHEMA_DATABASE_FILE_PATH)) { - UE_LOG(LogCookAndGenerateSchemaCommandlet, Error, TEXT("Failed to load Schema Database.")); + UE_LOG(LogCookAndGenerateSchemaCommandlet, Error, TEXT("Failed to load schema database.")); return 0; } - // 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(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("schema/unreal/gdk")); - FString CoreSDKSchemaCopyDir = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("build/dependencies/schema/standard_library")); - SpatialGDKEditor::Schema::CopyWellKnownSchemaFiles(GDKSchemaCopyDir, CoreSDKSchemaCopyDir); - SpatialGDKEditor::Schema::RefreshSchemaFiles(GetDefault()->GetGeneratedSchemaOutputFolder()); - - if (!LoadGeneratorStateFromSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_FILE_PATH)) - { - ResetSchemaGeneratorStateAndCleanupFolders(); - } - UE_LOG(LogCookAndGenerateSchemaCommandlet, Display, TEXT("Finding supported C++ and in-memory Classes.")); TArray AllClasses; @@ -118,13 +101,27 @@ int32 UCookAndGenerateSchemaCommandlet::Main(const FString& CmdLineParams) ObjectListener.StopListening(); - // Sort classes here so that batching does not have an effect on ordering. - ReferencedClasses.Sort([](const FSoftClassPath& A, const FSoftClassPath& B) + // 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(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("schema/unreal/gdk")); + FString CoreSDKSchemaCopyDir = + FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("build/dependencies/schema/standard_library")); + SpatialGDKEditor::Schema::CopyWellKnownSchemaFiles(GDKSchemaCopyDir, CoreSDKSchemaCopyDir); + const bool bDeleteExistingSchema = Switches.Contains(TEXT("DeleteExistingGeneratedSchema")); + SpatialGDKEditor::Schema::RefreshSchemaFiles(GetDefault()->GetGeneratedSchemaOutputFolder(), + bDeleteExistingSchema); + + if (!LoadGeneratorStateFromSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_FILE_PATH)) { + ResetSchemaGeneratorStateAndCleanupFolders(); + } + + // Sort classes here so that batching does not have an effect on ordering. + ReferencedClasses.Sort([](const FSoftClassPath& A, const FSoftClassPath& B) { return FNameLexicalLess()(A.GetAssetPathName(), B.GetAssetPathName()); }); - UE_LOG(LogCookAndGenerateSchemaCommandlet, Display, TEXT("Start Schema Generation for discovered assets.")); + UE_LOG(LogCookAndGenerateSchemaCommandlet, Display, TEXT("Start schema generation for discovered assets.")); FDateTime StartTime = FDateTime::Now(); TSet Classes; const int BatchSize = 100; @@ -146,6 +143,7 @@ int32 UCookAndGenerateSchemaCommandlet::Main(const FString& CmdLineParams) } } SpatialGDKGenerateSchemaForClasses(Classes); + SpatialGDKSanitizeGeneratedSchema(); GenerateSchemaForSublevels(); GenerateSchemaForRPCEndpoints(); @@ -155,13 +153,31 @@ int32 UCookAndGenerateSchemaCommandlet::Main(const FString& CmdLineParams) UE_LOG(LogCookAndGenerateSchemaCommandlet, Display, TEXT("Schema Generation Finished in %.2f seconds"), Duration.GetTotalSeconds()); + USchemaDatabase* SchemaDatabase = InitialiseSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_ASSET_PATH); + + // Needs to happen before RunSchemaCompiler + // We construct the list of all server authoritative components while writing the file. + TArray GeneratedServerAuthoritativeComponentIds{}; + WriteServerAuthorityComponentSet(SchemaDatabase, GeneratedServerAuthoritativeComponentIds); + WriteClientAuthorityComponentSet(); + WriteComponentSetBySchemaType(SchemaDatabase, SCHEMA_Data); + WriteComponentSetBySchemaType(SchemaDatabase, SCHEMA_OwnerOnly); + WriteComponentSetBySchemaType(SchemaDatabase, SCHEMA_Handover); + + // Finish initializing the schema database through updating the server authoritative component set. + for (const auto& ComponentId : GeneratedServerAuthoritativeComponentIds) + { + SchemaDatabase->ComponentSetIdToComponentIds.FindOrAdd(SpatialConstants::SERVER_AUTH_COMPONENT_SET_ID) + .ComponentIDs.Push(ComponentId); + } + if (!RunSchemaCompiler()) { UE_LOG(LogCookAndGenerateSchemaCommandlet, Error, TEXT("Failed to run schema compiler.")); return 0; } - if (!SaveSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_ASSET_PATH)) + if (!SaveSchemaDatabase(SchemaDatabase)) { UE_LOG(LogCookAndGenerateSchemaCommandlet, Error, TEXT("Failed to save schema database.")); return 0; diff --git a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/CookAndGenerateSchemaCommandlet.h b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/CookAndGenerateSchemaCommandlet.h index 164dbdfadc..33777ef543 100644 --- a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/CookAndGenerateSchemaCommandlet.h +++ b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/CookAndGenerateSchemaCommandlet.h @@ -2,8 +2,8 @@ #pragma once -#include "CoreMinimal.h" #include "Commandlets/CookCommandlet.h" +#include "CoreMinimal.h" #include "CookAndGenerateSchemaCommandlet.generated.h" DECLARE_LOG_CATEGORY_EXTERN(LogCookAndGenerateSchemaCommandlet, Log, All); @@ -16,11 +16,13 @@ struct FObjectListener; * schema for blueprints required by the cook. * * Usage: - * Engine\Binaries\Win64\UE4Editor-Cmd.exe .uproject -run=CookAndGenerateSchema -targetplatform=LinuxServer -SkipShaderCompile <...Native Cook Params> + * Engine\Binaries\Win64\UE4Editor-Cmd.exe .uproject -run=CookAndGenerateSchema -targetplatform=LinuxServer -SkipShaderCompile + * <...Native Cook Params> * * Known Issues: * - SchemaDatabase.uasset will need to be cooked again after running this Commandlet, potentially with [-iterate] flag. - * - [-iterate] flag will result in schema only generated for dirty packages, you maintain previous .schema files if you want to use this flag. + * - [-iterate] flag will result in schema only generated for dirty packages, you maintain previous .schema files if you want to use this + * flag. * * Recommended Workflow: * 1. Run CookAndGenerateSchema for a LinuxServer platform with [-SkipShaderCompile] for needed maps WITHOUT [-iterate] diff --git a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaAndSnapshotsCommandlet.cpp b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaAndSnapshotsCommandlet.cpp index 7565292ac4..2eb20b9c27 100644 --- a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaAndSnapshotsCommandlet.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaAndSnapshotsCommandlet.cpp @@ -73,7 +73,7 @@ int32 UGenerateSchemaAndSnapshotsCommandlet::Main(const FString& Args) { if (!GenerateSnapshotForPath(SpatialGDKEditor, ThisMapName)) { - return 1; // Error + return 1; // Error } } // When we get to this point, one of two things is true: @@ -81,7 +81,7 @@ int32 UGenerateSchemaAndSnapshotsCommandlet::Main(const FString& Args) // 2) RemainingMapPaths was split n times, and the last map that needs to be run after the loop is still in it if (!GenerateSnapshotForPath(SpatialGDKEditor, RemainingMapPaths)) { - return 1; // Error + return 1; // Error } } else @@ -89,7 +89,7 @@ int32 UGenerateSchemaAndSnapshotsCommandlet::Main(const FString& Args) // Default to everything in the project if (!GenerateSnapshotForPath(SpatialGDKEditor, TEXT(""))) { - return 1; // Error + return 1; // Error } } @@ -141,7 +141,7 @@ bool UGenerateSchemaAndSnapshotsCommandlet::GenerateSnapshotForPath(FSpatialGDKE UObjectLibrary* ObjectLibrary = UObjectLibrary::CreateLibrary(UWorld::StaticClass(), false, true); // Convert InPath into a format acceptable by LoadAssetDataFromPath(). - FString DirPath = CorrectedPath.LeftChop(1); // Remove the final '/' character + FString DirPath = CorrectedPath.LeftChop(1); // Remove the final '/' character ObjectLibrary->LoadAssetDataFromPath(DirPath); @@ -164,8 +164,9 @@ bool UGenerateSchemaAndSnapshotsCommandlet::GenerateSnapshotForPath(FSpatialGDKE FString CorrectedLongPackageNameError; FString Dummy; FPackageName::TryConvertFilenameToLongPackageName(CorrectedPath, Dummy, &CorrectedLongPackageNameError); - UE_LOG(LogSpatialGDKEditorCommandlet, Error, TEXT("Requested path \"%s\" is not in the expected format. %s"), *InPath, *CorrectedLongPackageNameError); - return false; // Future-proofing + UE_LOG(LogSpatialGDKEditorCommandlet, Error, TEXT("Requested path \"%s\" is not in the expected format. %s"), *InPath, + *CorrectedLongPackageNameError); + return false; // Future-proofing } return true; @@ -176,13 +177,14 @@ bool UGenerateSchemaAndSnapshotsCommandlet::GenerateSnapshotForMap(FSpatialGDKEd // Check if this map path has already been generated and early exit if so if (GeneratedMapPaths.Contains(InMapName)) { - UE_LOG(LogSpatialGDKEditorCommandlet, Warning, TEXT("Map %s has already been generated against. Skipping duplicate generation."), *InMapName); + UE_LOG(LogSpatialGDKEditorCommandlet, Warning, TEXT("Map %s has already been generated against. Skipping duplicate generation."), + *InMapName); return true; } GeneratedMapPaths.Add(InMapName); // Load persistent Level (this will load over any previously loaded levels) - if (!FEditorFileUtils::LoadMap(InMapName)) // This loads the world into GWorld + if (!FEditorFileUtils::LoadMap(InMapName)) // This loads the world into GWorld { UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Failed to load map %s"), *InMapName); return false; @@ -220,7 +222,9 @@ bool UGenerateSchemaAndSnapshotsCommandlet::GenerateSnapshotForMap(FSpatialGDKEd bool UGenerateSchemaAndSnapshotsCommandlet::GenerateSchema(FSpatialGDKEditor& InSpatialGDKEditor) { - UE_LOG(LogSpatialGDKEditorCommandlet, Error, TEXT("Commandlet GenerateSchemaAndSnapshots without -SkipSchema has been deprecated in favor of CookAndGenerateSchemaCommandlet.")); + UE_LOG( + LogSpatialGDKEditorCommandlet, Error, + TEXT("Commandlet GenerateSchemaAndSnapshots without -SkipSchema has been deprecated in favor of CookAndGenerateSchemaCommandlet.")); return false; } @@ -229,14 +233,16 @@ bool UGenerateSchemaAndSnapshotsCommandlet::GenerateSnapshotForLoadedMap(FSpatia { // Generate the Snapshot! bool bSnapshotGenSuccess = false; - InSpatialGDKEditor.GenerateSnapshot( - GWorld, FPaths::SetExtension(FPaths::GetCleanFilename(MapName), TEXT(".snapshot")), - FSimpleDelegate::CreateLambda([&bSnapshotGenSuccess]() - { - UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Snapshot Generation Completed!")); - bSnapshotGenSuccess = true; - }), - FSimpleDelegate::CreateLambda([]() { UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Snapshot Generation Failed")); }), - FSpatialGDKEditorErrorHandler::CreateLambda([](FString ErrorText) { UE_LOG(LogSpatialGDKEditorCommandlet, Error, TEXT("%s"), *ErrorText); })); + InSpatialGDKEditor.GenerateSnapshot(GWorld, FPaths::SetExtension(FPaths::GetCleanFilename(MapName), TEXT(".snapshot")), + FSimpleDelegate::CreateLambda([&bSnapshotGenSuccess]() { + UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Snapshot Generation Completed!")); + bSnapshotGenSuccess = true; + }), + FSimpleDelegate::CreateLambda([]() { + UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Snapshot Generation Failed")); + }), + FSpatialGDKEditorErrorHandler::CreateLambda([](FString ErrorText) { + UE_LOG(LogSpatialGDKEditorCommandlet, Error, TEXT("%s"), *ErrorText); + })); return bSnapshotGenSuccess; } diff --git a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaAndSnapshotsCommandlet.h b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaAndSnapshotsCommandlet.h index 815357531c..c68ee2198a 100644 --- a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaAndSnapshotsCommandlet.h +++ b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaAndSnapshotsCommandlet.h @@ -20,8 +20,9 @@ class UGenerateSchemaAndSnapshotsCommandlet : public UGenerateSchemaCommandlet virtual int32 Main(const FString& Params) override; private: - const FString MapPathsParamName = TEXT("MapPaths"); // Commandline Argument Name used to declare the paths to generate schema/snapshots against - const FString AssetPathGameDirName = TEXT("/Game"); // Root asset path directory name that maps will ultimately be found in + const FString MapPathsParamName = + TEXT("MapPaths"); // Commandline Argument Name used to declare the paths to generate schema/snapshots against + const FString AssetPathGameDirName = TEXT("/Game"); // Root asset path directory name that maps will ultimately be found in TArray GeneratedMapPaths; diff --git a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaCommandlet.cpp b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaCommandlet.cpp index 051d186e81..619e330886 100644 --- a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaCommandlet.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaCommandlet.cpp @@ -35,7 +35,7 @@ bool UGenerateSchemaCommandlet::HandleOptions(const TArray& Switches) int32 UGenerateSchemaCommandlet::Main(const FString& Args) { - UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Schema Generation Commandlet Started")); + UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Schema generation commandlet started")); TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, GIsRunningUnattendedScript || IsRunningCommandlet()); @@ -50,9 +50,10 @@ int32 UGenerateSchemaCommandlet::Main(const FString& Args) return 1; } - UE_LOG(LogSpatialGDKEditorCommandlet, Error, TEXT("Commandlet GenerateSchema has been deprecated in favor of CookAndGenerateSchemaCommandlet.")); - - UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Schema Generation Commandlet Complete")); + UE_LOG(LogSpatialGDKEditorCommandlet, Error, + TEXT("Commandlet GenerateSchema has been deprecated in favor of CookAndGenerateSchemaCommandlet.")); + + UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Schema generation commandlet complete")); return false; } diff --git a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSnapshotCommandlet.cpp b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSnapshotCommandlet.cpp index da32e224bf..a0cf06a930 100644 --- a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSnapshotCommandlet.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSnapshotCommandlet.cpp @@ -1,13 +1,13 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "GenerateSnapshotCommandlet.h" -#include "SpatialGDKEditorCommandletPrivate.h" #include "SpatialGDKEditor.h" +#include "SpatialGDKEditorCommandletPrivate.h" -#include "Kismet/GameplayStatics.h" #include "Engine/ObjectLibrary.h" #include "Engine/World.h" #include "FileHelpers.h" +#include "Kismet/GameplayStatics.h" #include "Misc/Paths.h" UGenerateSnapshotCommandlet::UGenerateSnapshotCommandlet() @@ -65,15 +65,17 @@ bool UGenerateSnapshotCommandlet::GenerateSnapshotForMap(FString MapPath) // Generate the Snapshot! bool bSnapshotGenSuccess = false; FSpatialGDKEditor SpatialGDKEditor; - SpatialGDKEditor.GenerateSnapshot( - GWorld, FPaths::SetExtension(FPaths::GetCleanFilename(MapPath), TEXT(".snapshot")), - FSimpleDelegate::CreateLambda([&bSnapshotGenSuccess]() - { - UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Success!")); - bSnapshotGenSuccess = true; - }), - FSimpleDelegate::CreateLambda([]() { UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Failed")); }), - FSpatialGDKEditorErrorHandler::CreateLambda([](FString ErrorText) { UE_LOG(LogSpatialGDKEditorCommandlet, Error, TEXT("%s"), *ErrorText); })); + SpatialGDKEditor.GenerateSnapshot(GWorld, FPaths::SetExtension(FPaths::GetCleanFilename(MapPath), TEXT(".snapshot")), + FSimpleDelegate::CreateLambda([&bSnapshotGenSuccess]() { + UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Success!")); + bSnapshotGenSuccess = true; + }), + FSimpleDelegate::CreateLambda([]() { + UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Failed")); + }), + FSpatialGDKEditorErrorHandler::CreateLambda([](FString ErrorText) { + UE_LOG(LogSpatialGDKEditorCommandlet, Error, TEXT("%s"), *ErrorText); + })); return bSnapshotGenSuccess; } diff --git a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/SpatialGDKEditorCommandletModule.cpp b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/SpatialGDKEditorCommandletModule.cpp index 73333be4e0..2db8022ab2 100644 --- a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/SpatialGDKEditorCommandletModule.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/SpatialGDKEditorCommandletModule.cpp @@ -8,11 +8,7 @@ DEFINE_LOG_CATEGORY(LogSpatialGDKEditorCommandlet); IMPLEMENT_MODULE(FSpatialGDKEditorCommandletModule, SpatialGDKEditorCommandlet); -void FSpatialGDKEditorCommandletModule::StartupModule() -{ -} -void FSpatialGDKEditorCommandletModule::ShutdownModule() -{ -} +void FSpatialGDKEditorCommandletModule::StartupModule() {} +void FSpatialGDKEditorCommandletModule::ShutdownModule() {} #undef LOCTEXT_NAMESPACE diff --git a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Public/SpatialGDKEditorCommandletModule.h b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Public/SpatialGDKEditorCommandletModule.h index e90a876293..fc2bd4d23b 100644 --- a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Public/SpatialGDKEditorCommandletModule.h +++ b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Public/SpatialGDKEditorCommandletModule.h @@ -13,8 +13,5 @@ class FSpatialGDKEditorCommandletModule : public IModuleInterface virtual void StartupModule() override; virtual void ShutdownModule() override; - virtual bool SupportsDynamicReloading() override - { - return true; - } + virtual bool SupportsDynamicReloading() override { return true; } }; diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKCloudDeploymentConfiguration.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKCloudDeploymentConfiguration.cpp index e296816492..64a3127947 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKCloudDeploymentConfiguration.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKCloudDeploymentConfiguration.cpp @@ -38,10 +38,10 @@ #include "SpatialConstants.h" #include "SpatialGDKDefaultLaunchConfigGenerator.h" #include "SpatialGDKDevAuthTokenGenerator.h" -#include "SpatialGDKEditorSettings.h" -#include "SpatialGDKEditorToolbar.h" #include "SpatialGDKEditorPackageAssembly.h" +#include "SpatialGDKEditorSettings.h" #include "SpatialGDKEditorSnapshotGenerator.h" +#include "SpatialGDKEditorToolbar.h" #include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesModule.h" #include "SpatialGDKSettings.h" @@ -52,12 +52,12 @@ DEFINE_LOG_CATEGORY(LogSpatialGDKCloudDeploymentConfiguration); namespace { - //Build Configurations - const FString DebugConfiguration(TEXT("Debug")); - const FString DebugGameConfiguration(TEXT("DebugGame")); - const FString DevelopmentConfiguration(TEXT("Development")); - const FString TestConfiguration(TEXT("Test")); - const FString ShippingConfiguration(TEXT("Shipping")); +// Build Configurations +const FString DebugConfiguration(TEXT("Debug")); +const FString DebugGameConfiguration(TEXT("DebugGame")); +const FString DevelopmentConfiguration(TEXT("Development")); +const FString TestConfiguration(TEXT("Test")); +const FString ShippingConfiguration(TEXT("Shipping")); } // anonymous namespace void SSpatialGDKCloudDeploymentConfiguration::Construct(const FArguments& InArgs) @@ -69,25 +69,15 @@ void SSpatialGDKCloudDeploymentConfiguration::Construct(const FArguments& InArgs ParentWindowPtr = InArgs._ParentWindow; SpatialGDKEditorPtr = InArgs._SpatialGDKEditor; - auto AddRequiredFieldAsterisk = [](TSharedRef TextBlock) - { - return SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .AutoWidth() - .HAlign(HAlign_Left) - [ - TextBlock - ] - + SHorizontalBox::Slot() - .AutoWidth() - .HAlign(HAlign_Left) - .Padding(2.0f, 0.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("RequiredFieldAsterisk", "*")) - .ToolTipText(LOCTEXT("RequiredField_Tooltip", "Required field")) - .ColorAndOpacity(FLinearColor(1.0f, 0.0f, 0.0f)) - ]; + auto AddRequiredFieldAsterisk = [](TSharedRef TextBlock) { + return SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().HAlign(HAlign_Left)[TextBlock] + + SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Left) + .Padding(2.0f, 0.0f)[SNew(STextBlock) + .Text(LOCTEXT("RequiredFieldAsterisk", "*")) + .ToolTipText(LOCTEXT("RequiredField_Tooltip", "Required field")) + .ColorAndOpacity(FLinearColor(1.0f, 0.0f, 0.0f))]; }; ProjectNameInputErrorReporting = SNew(SPopupErrorText); @@ -96,661 +86,531 @@ void SSpatialGDKCloudDeploymentConfiguration::Construct(const FArguments& InArgs AssemblyNameInputErrorReporting->SetError(TEXT("")); DeploymentNameInputErrorReporting = SNew(SPopupErrorText); DeploymentNameInputErrorReporting->SetError(TEXT("")); + SimulatedPlayersDeploymentNameInputErrorReporting = SNew(SPopupErrorText); + SimulatedPlayersDeploymentNameInputErrorReporting->SetError(TEXT("")); ChildSlot - [ - SNew(SBorder) - .HAlign(HAlign_Fill) - .BorderImage(FEditorStyle::GetBrush("ChildWindow.Background")) - .Padding(4.0f) - [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .FillHeight(1.0f) - .Padding(0.0f, 6.0f, 0.0f, 0.0f) - [ - SNew(SBorder) - .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) - .Padding(4.0f) - [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .AutoHeight() - .Padding(1.0f) - [ - SNew(SVerticalBox) - // Project - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - AddRequiredFieldAsterisk( - SNew(STextBlock) - .Text(LOCTEXT("ProjectName_Label", "Project Name")) - .ToolTipText(LOCTEXT("ProjectName_Tooltip", "The name of the SpatialOS project.")) - ) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SEditableTextBox) - .Text(FText::FromString(ProjectName)) - .ToolTipText(LOCTEXT("ProjectName_Tooltip", "The name of the SpatialOS project.")) - .OnTextCommitted(this, &SSpatialGDKCloudDeploymentConfiguration::OnProjectNameCommitted) - .ErrorReporting(ProjectNameInputErrorReporting) - ] - ] - // Assembly Name - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - AddRequiredFieldAsterisk( - SNew(STextBlock) - .Text(LOCTEXT("AssemblyName_Label", "Assembly Name")) - .ToolTipText(LOCTEXT("AssemblyName_Tooltip", "The name of the assembly.")) - ) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SEditableTextBox) - .Text(FText::FromString(SpatialGDKSettings->GetAssemblyName())) - .ToolTipText(LOCTEXT("AssemblyName_Tooltip", "The name of the assembly.")) - .OnTextCommitted(this, &SSpatialGDKCloudDeploymentConfiguration::OnDeploymentAssemblyCommited) - .ErrorReporting(AssemblyNameInputErrorReporting) - ] - ] - // RuntimeVersion - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("UseGDKPinnedRuntime_Label", "Use GDK Pinned Version For Cloud")) - .ToolTipText(LOCTEXT("UseGDKPinnedRuntime_Tooltip", "Whether to use the SpatialOS Runtime version associated to the current GDK version for cloud deployments")) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SCheckBox) - .IsChecked(this, &SSpatialGDKCloudDeploymentConfiguration::IsUsingGDKPinnedRuntimeVersion) - .OnCheckStateChanged(this, &SSpatialGDKCloudDeploymentConfiguration::OnCheckedUsePinnedVersion) - ] - ] - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("RuntimeVersion_Label", "Runtime Version")) - .ToolTipText(LOCTEXT("RuntimeVersion_Tooltip", "User supplied version of the SpatialOS runtime to use")) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SEditableTextBox) - .Text(this, &SSpatialGDKCloudDeploymentConfiguration::GetSpatialOSRuntimeVersionToUseText) - .OnTextCommitted(this, &SSpatialGDKCloudDeploymentConfiguration::OnRuntimeCustomVersionCommited) - .OnTextChanged(this, &SSpatialGDKCloudDeploymentConfiguration::OnRuntimeCustomVersionCommited, ETextCommit::Default) - .IsEnabled(this, &SSpatialGDKCloudDeploymentConfiguration::IsUsingCustomRuntimeVersion) - ] - ] - // Primary Deployment Name - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - AddRequiredFieldAsterisk( - SNew(STextBlock) - .Text(LOCTEXT("PrimaryDeploymentName_Label", "Deployment Name")) - .ToolTipText(LOCTEXT("PrimaryDeploymentName_Tooltip", "The name of the cloud deployment. Must be unique.")) - ) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SEditableTextBox) - .Text(this, &SSpatialGDKCloudDeploymentConfiguration::GetPrimaryDeploymentNameText) - .ToolTipText(LOCTEXT("PrimaryDeploymentName_Tooltip", "The name of the cloud deployment. Must be unique.")) - .OnTextCommitted(this, &SSpatialGDKCloudDeploymentConfiguration::OnPrimaryDeploymentNameCommited) - .ErrorReporting(DeploymentNameInputErrorReporting) - ] - ] - // Snapshot File + File Picker - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - AddRequiredFieldAsterisk( - SNew(STextBlock) - .Text(LOCTEXT("SnapshotFile_Label", "Snapshot File")) - .ToolTipText(LOCTEXT("SnapshotFile_Tooltip", "The relative path to the snapshot file.")) - ) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SFilePathPicker) - .BrowseButtonImage(FEditorStyle::GetBrush("PropertyWindow.Button_Ellipsis")) - .BrowseButtonStyle(FEditorStyle::Get(), "HoverHintOnly") - .BrowseButtonToolTip(LOCTEXT("SnapshotFilePicker_Tooltip", "Path to the snapshot file.")) - .BrowseDirectory(SpatialGDKSettings->GetSpatialOSSnapshotFolderPath()) - .BrowseTitle(LOCTEXT("SnapshotFilePicker_Title", "File picker...")) - .FilePath_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::GetSnapshotPath) - .FileTypeFilter(TEXT("Snapshot files (*.snapshot)|*.snapshot")) - .OnPathPicked(this, &SSpatialGDKCloudDeploymentConfiguration::OnSnapshotPathPicked) - ] - ] - // Automatically Generate Launch Configuration - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("AutoGenerateCloudLaunchConfig_Label", "Automatically Generate Launch Configuration")) - .ToolTipText(LOCTEXT("AutoGenerateCloudLaunchConfig_Tooltip", "Whether to automatically generate the launch configuration from the current map when a cloud deployment is started.")) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SCheckBox) - .IsChecked(this, &SSpatialGDKCloudDeploymentConfiguration::IsAutoGenerateCloudLaunchConfigEnabled) - .OnCheckStateChanged(this, &SSpatialGDKCloudDeploymentConfiguration::OnCheckedAutoGenerateCloudLaunchConfig) - ] - ] - // Primary Launch Config + File Picker - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - AddRequiredFieldAsterisk( - SNew(STextBlock) - .Text(LOCTEXT("LaunchConfigFile_Label", "Launch Config File")) - .ToolTipText(LOCTEXT("LaunchConfigFile_Tooltip", "The relative path to the launch configuration file.")) - ) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SFilePathPicker) - .BrowseButtonImage(FEditorStyle::GetBrush("PropertyWindow.Button_Ellipsis")) - .BrowseButtonStyle(FEditorStyle::Get(), "HoverHintOnly") - .BrowseButtonToolTip(LOCTEXT("LaunchConfigFilePicker_Tooltip", "Path to the launch configuration file.")) - .BrowseDirectory(SpatialGDKServicesConstants::SpatialOSDirectory) - .BrowseTitle(LOCTEXT("LaunchConfigFilePicker_Title", "File picker...")) - .FilePath_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::GetPrimaryLaunchConfigPath) - .FileTypeFilter(TEXT("Launch configuration files (*.json)|*.json")) - .OnPathPicked(this, &SSpatialGDKCloudDeploymentConfiguration::OnPrimaryLaunchConfigPathPicked) - .IsEnabled(this, &SSpatialGDKCloudDeploymentConfiguration::CanPickOrEditCloudLaunchConfig) - ] - ] - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SButton) - .Text(LOCTEXT("OpenLaunchConfig_Label", "Open Launch Configuration editor")) - .OnClicked(this, &SSpatialGDKCloudDeploymentConfiguration::OnOpenLaunchConfigEditor) - .IsEnabled(this, &SSpatialGDKCloudDeploymentConfiguration::CanPickOrEditCloudLaunchConfig) - ] - ] - // Primary Deployment Region Picker - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - .Visibility(this, &SSpatialGDKCloudDeploymentConfiguration::GetRegionPickerVisibility) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("PrimaryDeploymentRegion_Label", "Region")) - .ToolTipText(LOCTEXT("PrimaryDeploymentRegion_Tooltip", "The region in which the deployment will be deployed.")) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SComboButton) - .OnGetMenuContent(this, &SSpatialGDKCloudDeploymentConfiguration::OnGetPrimaryDeploymentRegionCode) - .ContentPadding(FMargin(2.0f, 2.0f)) - .IsEnabled(this, &SSpatialGDKCloudDeploymentConfiguration::IsPrimaryRegionPickerEnabled) - .ButtonContent() - [ - SNew(STextBlock) - .Text_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::GetPrimaryRegionCodeText) - ] - ] - ] - // Main Deployment Cluster - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("PrimaryDeploymentCluster_Label", "Deployment Cluster")) - .ToolTipText(LOCTEXT("PrimaryDeploymentCluster_Tooltip", "The name of the cluster to deploy to. Region code will be ignored if this is specified.")) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SEditableTextBox) - .Text(FText::FromString(SpatialGDKSettings->GetMainDeploymentCluster())) - .ToolTipText(LOCTEXT("PrimaryDeploymentCluster_Tooltip", "The name of the cluster to deploy to. Region code will be ignored if this is specified.")) - .OnTextCommitted(this, &SSpatialGDKCloudDeploymentConfiguration::OnDeploymentClusterCommited) - .OnTextChanged(this, &SSpatialGDKCloudDeploymentConfiguration::OnDeploymentClusterCommited, ETextCommit::Default) - ] - ] - // Deployment Tags - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("DeploymentTags_Label", "Deployment Tags")) - .ToolTipText(LOCTEXT("DeploymentTags_Tooltip", "Tags for the deployment (separated by spaces).")) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SEditableTextBox) - .Text(FText::FromString(SpatialGDKSettings->GetDeploymentTags())) - .ToolTipText(LOCTEXT("DeploymentTags_Tooltip", "Tags for the deployment (separated by spaces).")) - .OnTextCommitted(this, &SSpatialGDKCloudDeploymentConfiguration::OnDeploymentTagsCommitted) - .OnTextChanged(this, &SSpatialGDKCloudDeploymentConfiguration::OnDeploymentTagsCommitted, ETextCommit::Default) - ] - ] - // Separator - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - .VAlign(VAlign_Center) - [ - SNew(SSeparator) - ] - // Explanation text - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - .VAlign(VAlign_Center) - .HAlign(HAlign_Center) - [ - SNew(STextBlock) - .Text(LOCTEXT("SimulatedPlayers_Label", "Simulated Players")) - ] - // Toggle Simulated Players - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("EnableSimulatedPlayers_Label", "Add simulated players")) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SCheckBox) - .IsChecked(this, &SSpatialGDKCloudDeploymentConfiguration::IsSimulatedPlayersEnabled) - .OnCheckStateChanged(this, &SSpatialGDKCloudDeploymentConfiguration::OnCheckedSimulatedPlayers) - ] - ] - // Simulated Players Deployment Name - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("SimPlayerDeploymentName_Label", "Deployment Name")) - .ToolTipText(LOCTEXT("SimPlayerDeploymentName_Tooltip", "The name of the simulated player deployment.")) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SEditableTextBox) - .Text(FText::FromString(SpatialGDKSettings->GetSimulatedPlayerDeploymentName())) - .ToolTipText(LOCTEXT("SimPlayerDeploymentName_Tooltip", "The name of the simulated player deployment.")) - .OnTextCommitted(this, &SSpatialGDKCloudDeploymentConfiguration::OnSimulatedPlayerDeploymentNameCommited) - .OnTextChanged(this, &SSpatialGDKCloudDeploymentConfiguration::OnSimulatedPlayerDeploymentNameCommited, ETextCommit::Default) - .IsEnabled_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::IsSimulatedPlayersEnabled) - ] - ] - // Simulated Players Number - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("NumberOfSimulatedPlayers_Label", "Number of Simulated Players")) - .ToolTipText(LOCTEXT("NumberOfSimulatedPlayers_Tooltip", "The number of Simulated Players to launch and connect to the game.")) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SSpinBox) - .ToolTipText(LOCTEXT("NumberOfSimulatedPlayers_Tooltip", "The number of Simulated Players to launch and connect to the game.")) - .MinValue(1) - .MaxValue(8192) - .Value(SpatialGDKSettings->GetNumberOfSimulatedPlayers()) - .OnValueChanged(this, &SSpatialGDKCloudDeploymentConfiguration::OnNumberOfSimulatedPlayersCommited) - .IsEnabled_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::IsSimulatedPlayersEnabled) - ] - ] - // Simulated Players Deployment Region Picker - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - .Visibility(this, &SSpatialGDKCloudDeploymentConfiguration::GetRegionPickerVisibility) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("SimPlayerRegion_Label", "Region")) - .ToolTipText(LOCTEXT("SimPlayerRegion_Tooltip", "The region in which the simulated player deployment will be deployed.")) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SComboButton) - .OnGetMenuContent(this, &SSpatialGDKCloudDeploymentConfiguration::OnGetSimulatedPlayerDeploymentRegionCode) - .ContentPadding(FMargin(2.0f, 2.0f)) - .IsEnabled(this, &SSpatialGDKCloudDeploymentConfiguration::IsSimulatedPlayerRegionPickerEnabled) - .ButtonContent() - [ - SNew(STextBlock) - .Text_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::GetSimulatedPlayerRegionCode) - ] - ] - ] - // Simulated Player Cluster - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("SimPlayerCluster_Label", "Deployment Cluster")) - .ToolTipText(LOCTEXT("SimPlayerCluster_Tooltip", "The name of the cluster to deploy to. Region code will be ignored if this is specified.")) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SEditableTextBox) - .Text(FText::FromString(SpatialGDKSettings->GetSimulatedPlayerCluster())) - .ToolTipText(LOCTEXT("SimPlayerCluster_Tooltip", "The name of the cluster to deploy to. Region code will be ignored if this is specified.")) - .OnTextCommitted(this, &SSpatialGDKCloudDeploymentConfiguration::OnSimulatedPlayerClusterCommited) - .OnTextChanged(this, &SSpatialGDKCloudDeploymentConfiguration::OnSimulatedPlayerClusterCommited, ETextCommit::Default) - .IsEnabled_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::IsSimulatedPlayersEnabled) - ] - ] - // Separator - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - .VAlign(VAlign_Center) - [ - SNew(SSeparator) - ] - // Explanation text - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - .VAlign(VAlign_Center) - .HAlign(HAlign_Center) - [ - SNew(STextBlock) - .Text(LOCTEXT("AssemblyConfiguration_Label", "Assembly Configuration")) - ] - // Build and Upload Assembly - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("BuildAndUploadAssembly_Label", "Build and Upload Assembly")) - .ToolTipText(LOCTEXT("BuildAndUploadAssembly_Tooltip", "Whether to build and upload the assembly when starting the cloud deployment.")) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SCheckBox) - .IsChecked(this, &SSpatialGDKCloudDeploymentConfiguration::IsBuildAndUploadAssemblyEnabled) - .OnCheckStateChanged(this, &SSpatialGDKCloudDeploymentConfiguration::OnCheckedBuildAndUploadAssembly) - ] - ] - // Generate Schema - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("GenerateSchema_Label", "Generate Schema")) - .ToolTipText(LOCTEXT("GenerateSchema_Tooltip", "Whether to generate the schema automatically when building the assembly.")) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SCheckBox) - .IsChecked(this, &SSpatialGDKCloudDeploymentConfiguration::IsGenerateSchemaEnabled) - .OnCheckStateChanged(this, &SSpatialGDKCloudDeploymentConfiguration::OnCheckedGenerateSchema) - .IsEnabled_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::ShouldBuildAndUploadAssembly) - ] - ] - // Generate Snapshot - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("GenerateSnapshot_Label", "Generate Snapshot")) - .ToolTipText(LOCTEXT("GenerateSnapshot_Tooltip", "Whether to generate the snapshot automatically when building the assembly.")) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SCheckBox) - .IsChecked(this, &SSpatialGDKCloudDeploymentConfiguration::IsGenerateSnapshotEnabled) - .OnCheckStateChanged(this, &SSpatialGDKCloudDeploymentConfiguration::OnCheckedGenerateSnapshot) - .IsEnabled_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::ShouldBuildAndUploadAssembly) - ] - ] - // Build Configuration - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("BuildConfiguration_Label", "Build Configuration")) - .ToolTipText(LOCTEXT("BuildConfiguration_Tooltip", "The configuration to build.")) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SComboButton) - .OnGetMenuContent(this, &SSpatialGDKCloudDeploymentConfiguration::OnGetBuildConfiguration) - .ContentPadding(FMargin(2.0f, 2.0f)) - .ButtonContent() - [ - SNew(STextBlock) - .Text_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::GetAssemblyBuildConfiguration) - ] - .IsEnabled_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::ShouldBuildAndUploadAssembly) - ] - ] - // Enable/Disable Build Client Worker - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("BuildClientWorker_Label", "Build Client Worker")) - .ToolTipText(LOCTEXT("BuildClientWorker_Tooltip", "Whether to build the client worker as part of the assembly.")) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SCheckBox) - .IsChecked(this, &SSpatialGDKCloudDeploymentConfiguration::IsBuildClientWorkerEnabled) - .OnCheckStateChanged(this, &SSpatialGDKCloudDeploymentConfiguration::OnCheckedBuildClientWorker) - .IsEnabled_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::ShouldBuildAndUploadAssembly) - ] - ] - // Force Overwrite on Upload - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(LOCTEXT("ForceOverwriteAssembly_Label", "Force Overwrite on Upload")) - .ToolTipText(LOCTEXT("ForceOverwriteAssembly_Tooltip", "Whether to overwrite an existing assembly when uploading.")) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SCheckBox) - .IsChecked(this, &SSpatialGDKCloudDeploymentConfiguration::ForceAssemblyOverwrite) - .OnCheckStateChanged(this, &SSpatialGDKCloudDeploymentConfiguration::OnCheckedForceAssemblyOverwrite) - .IsEnabled_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::ShouldBuildAndUploadAssembly) - ] - ] - // Separator - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - .VAlign(VAlign_Center) - [ - SNew(SSeparator) - ] - // Buttons - + SVerticalBox::Slot() - .FillHeight(1.0f) - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - .HAlign(HAlign_Left) - [ - // Open Deployment Page - SNew(SUniformGridPanel) - .SlotPadding(FMargin(2.0f, 20.0f, 0.0f, 0.0f)) - + SUniformGridPanel::Slot(0, 0) - [ - SNew(SButton) - .HAlign(HAlign_Center) - .Text(LOCTEXT("OpenDeploymentPage_Label", "Open Deployment Page")) - .OnClicked(this, &SSpatialGDKCloudDeploymentConfiguration::OnOpenCloudDeploymentPageClicked) - .IsEnabled(this, &SSpatialGDKCloudDeploymentConfiguration::CanOpenCloudDeploymentPage) - ] - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - .HAlign(HAlign_Right) - [ - // Start Deployment Button - SNew(SUniformGridPanel) - .SlotPadding(FMargin(2.0f, 20.0f, 0.0f, 0.0f)) - + SUniformGridPanel::Slot(1, 0) - [ - SNew(SButton) - .HAlign(HAlign_Center) - .Text(LOCTEXT("StartDeployment_Label", "Start Deployment")) - .OnClicked_Raw(ToolbarPtr, &FSpatialGDKEditorToolbarModule::OnStartCloudDeployment) - .IsEnabled_Raw(ToolbarPtr, &FSpatialGDKEditorToolbarModule::CanStartCloudDeployment) - ] - ] - ] - ] - ] - ] - ] - ]; + [SNew(SBorder) + .HAlign(HAlign_Fill) + .BorderImage(FEditorStyle::GetBrush("ChildWindow.Background")) + .Padding(4.0f) + [SNew(SVerticalBox) + + SVerticalBox::Slot().FillHeight(1.0f).Padding(0.0f, 6.0f, 0.0f, 0.0f) + [SNew(SBorder) + .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) + .Padding(4.0f)[SNew(SVerticalBox) + + SVerticalBox::Slot().AutoHeight().Padding( + 1.0f)[SNew(SVerticalBox) + // Project + + SVerticalBox::Slot().AutoHeight().Padding( + 2.0f)[SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f)[AddRequiredFieldAsterisk( + SNew(STextBlock) + .Text(LOCTEXT("ProjectName_Label", "Project Name")) + .ToolTipText(LOCTEXT("ProjectName_Tooltip", + "The name of the SpatialOS project.")))] + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(SEditableTextBox) + .Text(FText::FromString(ProjectName)) + .ToolTipText(LOCTEXT("ProjectName_Tooltip", + "The name of the SpatialOS project.")) + .OnTextCommitted(this, &SSpatialGDKCloudDeploymentConfiguration:: + OnProjectNameCommitted) + .ErrorReporting(ProjectNameInputErrorReporting)]] + // Assembly Name + + SVerticalBox::Slot().AutoHeight().Padding( + 2.0f)[SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f)[AddRequiredFieldAsterisk( + SNew(STextBlock) + .Text(LOCTEXT("AssemblyName_Label", "Assembly Name")) + .ToolTipText( + LOCTEXT("AssemblyName_Tooltip", "The name of the assembly.")))] + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(SEditableTextBox) + .Text(FText::FromString(SpatialGDKSettings->GetAssemblyName())) + .ToolTipText( + LOCTEXT("AssemblyName_Tooltip", "The name of the assembly.")) + .OnTextCommitted(this, &SSpatialGDKCloudDeploymentConfiguration:: + OnDeploymentAssemblyCommited) + .ErrorReporting(AssemblyNameInputErrorReporting)]] + // RuntimeVersion + + SVerticalBox::Slot().AutoHeight().Padding( + 2.0f)[SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(STextBlock) + .Text(LOCTEXT("UseGDKPinnedRuntime_Label", + "Use GDK Pinned Version For Cloud")) + .ToolTipText(LOCTEXT("UseGDKPinnedRuntime_Tooltip", + "Whether to use the SpatialOS Runtime " + "version associated to the current GDK " + "version for cloud deployments"))] + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(SCheckBox) + .IsChecked(this, &SSpatialGDKCloudDeploymentConfiguration:: + IsUsingGDKPinnedRuntimeVersion) + .OnCheckStateChanged( + this, &SSpatialGDKCloudDeploymentConfiguration:: + OnCheckedUsePinnedVersion)]] + + SVerticalBox::Slot().AutoHeight().Padding( + 2.0f)[SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(STextBlock) + .Text(LOCTEXT("RuntimeVersion_Label", "Runtime Version")) + .ToolTipText(LOCTEXT("RuntimeVersion_Tooltip", + "User supplied version of the SpatialOS " + "runtime to use"))] + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(SEditableTextBox) + .Text(this, &SSpatialGDKCloudDeploymentConfiguration:: + GetSpatialOSRuntimeVersionToUseText) + .OnTextCommitted(this, &SSpatialGDKCloudDeploymentConfiguration:: + OnRuntimeCustomVersionCommited) + .OnTextChanged(this, + &SSpatialGDKCloudDeploymentConfiguration:: + OnRuntimeCustomVersionCommited, + ETextCommit::Default) + .IsEnabled(this, &SSpatialGDKCloudDeploymentConfiguration:: + IsUsingCustomRuntimeVersion)]] + // Primary Deployment Name + + SVerticalBox::Slot().AutoHeight().Padding( + 2.0f)[SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f)[AddRequiredFieldAsterisk( + SNew(STextBlock) + .Text(LOCTEXT("PrimaryDeploymentName_Label", "Deployment Name")) + .ToolTipText(LOCTEXT("PrimaryDeploymentName_Tooltip", + "The name of the cloud deployment. Must be " + "unique.")))] + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(SEditableTextBox) + .Text(this, &SSpatialGDKCloudDeploymentConfiguration:: + GetPrimaryDeploymentNameText) + .ToolTipText(LOCTEXT("PrimaryDeploymentName_Tooltip", + "The name of the cloud deployment. Must be " + "unique.")) + .OnTextCommitted(this, &SSpatialGDKCloudDeploymentConfiguration:: + OnPrimaryDeploymentNameCommited) + .ErrorReporting(DeploymentNameInputErrorReporting)]] + // Snapshot File + File Picker + + SVerticalBox::Slot().AutoHeight().Padding(2.0f) + [SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f)[AddRequiredFieldAsterisk( + SNew(STextBlock) + .Text(LOCTEXT("SnapshotFile_Label", "Snapshot File")) + .ToolTipText(LOCTEXT("SnapshotFile_Tooltip", + "The relative path to the snapshot file.")))] + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(SFilePathPicker) + .BrowseButtonImage(FEditorStyle::GetBrush("PropertyWindow.Button_" + "Ellipsis")) + .BrowseButtonStyle(FEditorStyle::Get(), "HoverHintOnly") + .BrowseButtonToolTip(LOCTEXT("SnapshotFilePicker_Tooltip", + "Path to the snapshot file.")) + .BrowseDirectory( + SpatialGDKServicesConstants::SpatialOSSnapshotFolderPath) + .BrowseTitle(LOCTEXT("SnapshotFilePicker_Title", "File picker...")) + .FilePath_UObject(SpatialGDKSettings, + &USpatialGDKEditorSettings::GetSnapshotPath) + .FileTypeFilter(TEXT("Snapshot files (*.snapshot)|*.snapshot")) + .OnPathPicked(this, &SSpatialGDKCloudDeploymentConfiguration:: + OnSnapshotPathPicked)]] + // Automatically Generate Launch Configuration + + SVerticalBox::Slot().AutoHeight().Padding( + 2.0f)[SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(STextBlock) + .Text(LOCTEXT("AutoGenerateCloudLaunchConfig_Label", + "Automatically Generate Launch Configuration")) + .ToolTipText(LOCTEXT("AutoGenerateCloudLaunchConfig_Tooltip", + "Whether to automatically generate the " + "launch configuration from the current map " + "when a cloud deployment is started."))] + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(SCheckBox) + .IsChecked(this, &SSpatialGDKCloudDeploymentConfiguration:: + IsAutoGenerateCloudLaunchConfigEnabled) + .OnCheckStateChanged( + this, &SSpatialGDKCloudDeploymentConfiguration:: + OnCheckedAutoGenerateCloudLaunchConfig)]] + // Primary Launch Config + File Picker + + SVerticalBox::Slot().AutoHeight().Padding(2.0f) + [SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f)[AddRequiredFieldAsterisk( + SNew(STextBlock) + .Text(LOCTEXT("LaunchConfigFile_Label", "Launch Config File")) + .ToolTipText(LOCTEXT("LaunchConfigFile_Tooltip", + "The relative path to the launch configuration " + "file.")))] + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(SFilePathPicker) + .BrowseButtonImage(FEditorStyle::GetBrush("PropertyWindow.Button_" + "Ellipsis")) + .BrowseButtonStyle(FEditorStyle::Get(), "HoverHintOnly") + .BrowseButtonToolTip(LOCTEXT("LaunchConfigFilePicker_Tooltip", + "Path to the launch configuration " + "file.")) + .BrowseDirectory(SpatialGDKServicesConstants::SpatialOSDirectory) + .BrowseTitle( + LOCTEXT("LaunchConfigFilePicker_Title", "File picker...")) + .FilePath_UObject( + SpatialGDKSettings, + &USpatialGDKEditorSettings::GetPrimaryLaunchConfigPath) + .FileTypeFilter(TEXT("Launch configuration files (*.json)|*.json")) + .OnPathPicked(this, &SSpatialGDKCloudDeploymentConfiguration:: + OnPrimaryLaunchConfigPathPicked) + .IsEnabled(this, &SSpatialGDKCloudDeploymentConfiguration:: + CanPickOrEditCloudLaunchConfig)]] + + SVerticalBox::Slot().AutoHeight().Padding( + 2.0f)[SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(1.0f) + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(SButton) + .Text(LOCTEXT("OpenLaunchConfig_Label", + "Open Launch Configuration editor")) + .OnClicked(this, &SSpatialGDKCloudDeploymentConfiguration:: + OnOpenLaunchConfigEditor) + .IsEnabled(this, &SSpatialGDKCloudDeploymentConfiguration:: + CanPickOrEditCloudLaunchConfig)]] + // Primary Deployment Region Picker + + SVerticalBox::Slot().AutoHeight().Padding(2.0f) + [SNew(SHorizontalBox) + .Visibility(this, &SSpatialGDKCloudDeploymentConfiguration:: + GetRegionPickerVisibility) + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(STextBlock) + .Text(LOCTEXT("PrimaryDeploymentRegion_Label", "Region")) + .ToolTipText(LOCTEXT("PrimaryDeploymentRegion_Tooltip", + "The region in which the deployment will be " + "deployed."))] + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(SComboButton) + .OnGetMenuContent(this, &SSpatialGDKCloudDeploymentConfiguration:: + OnGetPrimaryDeploymentRegionCode) + .ContentPadding(FMargin(2.0f, 2.0f)) + .IsEnabled(this, &SSpatialGDKCloudDeploymentConfiguration:: + IsPrimaryRegionPickerEnabled) + .ButtonContent()[SNew(STextBlock) + .Text_UObject(SpatialGDKSettings, + &USpatialGDKEditorSettings:: + GetPrimaryRegionCodeText)]]] + // Main Deployment Cluster + + SVerticalBox::Slot().AutoHeight().Padding(2.0f) + [SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(STextBlock) + .Text( + LOCTEXT("PrimaryDeploymentCluster_Label", "Deployment Cluster")) + .ToolTipText(LOCTEXT("PrimaryDeploymentCluster_Tooltip", + "The name of the cluster to deploy to. Region " + "code will be ignored if this is specified."))] + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(SEditableTextBox) + .Text(FText::FromString( + SpatialGDKSettings->GetMainDeploymentCluster())) + .ToolTipText(LOCTEXT("PrimaryDeploymentCluster_Tooltip", + "The name of the cluster to deploy to. Region " + "code will be ignored if this is specified.")) + .OnTextCommitted(this, &SSpatialGDKCloudDeploymentConfiguration:: + OnDeploymentClusterCommited) + .OnTextChanged(this, + &SSpatialGDKCloudDeploymentConfiguration:: + OnDeploymentClusterCommited, + ETextCommit::Default)]] + // Deployment Tags + + SVerticalBox::Slot().AutoHeight().Padding( + 2.0f)[SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(STextBlock) + .Text(LOCTEXT("DeploymentTags_Label", "Deployment Tags")) + .ToolTipText(LOCTEXT("DeploymentTags_Tooltip", + "Tags for the deployment (separated by " + "spaces)."))] + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(SEditableTextBox) + .Text(FText::FromString(SpatialGDKSettings->GetDeploymentTags())) + .ToolTipText(LOCTEXT("DeploymentTags_Tooltip", + "Tags for the deployment (separated by " + "spaces).")) + .OnTextCommitted(this, &SSpatialGDKCloudDeploymentConfiguration:: + OnDeploymentTagsCommitted) + .OnTextChanged(this, + &SSpatialGDKCloudDeploymentConfiguration:: + OnDeploymentTagsCommitted, + ETextCommit::Default)]] + // Separator + + SVerticalBox::Slot().AutoHeight().Padding(2.0f).VAlign( + VAlign_Center)[SNew(SSeparator)] + // Explanation text + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center)[SNew(STextBlock) + .Text(LOCTEXT("SimulatedPlayers_Label", + "Simulated Players"))] + // Toggle Simulated Players + + SVerticalBox::Slot().AutoHeight().Padding( + 2.0f)[SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(STextBlock) + .Text(LOCTEXT("EnableSimulatedPlayers_Label", + "Add simulated players"))] + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(SCheckBox) + .IsChecked(this, &SSpatialGDKCloudDeploymentConfiguration:: + IsSimulatedPlayersEnabled) + .OnCheckStateChanged( + this, &SSpatialGDKCloudDeploymentConfiguration:: + OnCheckedSimulatedPlayers)]] + // Simulated Players Deployment Name + + SVerticalBox::Slot().AutoHeight().Padding(2.0f) + [SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(STextBlock) + .Text(LOCTEXT("SimPlayerDeploymentName_Label", "Deployment Name")) + .ToolTipText(LOCTEXT("SimPlayerDeploymentName_Tooltip", + "The name of the simulated player " + "deployment."))] + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(SEditableTextBox) + .Text(FText::FromString( + SpatialGDKSettings->GetSimulatedPlayerDeploymentName())) + .ToolTipText(LOCTEXT("SimPlayerDeploymentName_Tooltip", + "The name of the simulated player " + "deployment.")) + .OnTextCommitted(this, &SSpatialGDKCloudDeploymentConfiguration:: + OnSimulatedPlayerDeploymentNameCommited) + .OnTextChanged(this, + &SSpatialGDKCloudDeploymentConfiguration:: + OnSimulatedPlayerDeploymentNameCommited, + ETextCommit::Default) + .ErrorReporting(SimulatedPlayersDeploymentNameInputErrorReporting) + .IsEnabled_UObject( + SpatialGDKSettings, + &USpatialGDKEditorSettings::IsSimulatedPlayersEnabled)]] + // Simulated Players Number + + SVerticalBox::Slot().AutoHeight().Padding( + 2.0f)[SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(STextBlock) + .Text(LOCTEXT("NumberOfSimulatedPlayers_Label", + "Number of Simulated Players")) + .ToolTipText(LOCTEXT("NumberOfSimulatedPlayers_Tooltip", + "The number of Simulated Players to " + "launch and connect to the game."))] + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(SSpinBox) + .ToolTipText(LOCTEXT("NumberOfSimulatedPlayers_Tooltip", + "The number of Simulated Players to launch " + "and connect to the game.")) + .MinValue(1) + .MaxValue(8192) + .Value(SpatialGDKSettings->GetNumberOfSimulatedPlayers()) + .OnValueChanged(this, &SSpatialGDKCloudDeploymentConfiguration:: + OnNumberOfSimulatedPlayersCommited) + .IsEnabled_UObject( + SpatialGDKSettings, + &USpatialGDKEditorSettings::IsSimulatedPlayersEnabled)]] + // Simulated Players Deployment Region Picker + + SVerticalBox::Slot().AutoHeight().Padding( + 2.0f)[SNew(SHorizontalBox) + .Visibility(this, &SSpatialGDKCloudDeploymentConfiguration:: + GetRegionPickerVisibility) + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(STextBlock) + .Text(LOCTEXT("SimPlayerRegion_Label", "Region")) + .ToolTipText(LOCTEXT("SimPlayerRegion_Tooltip", + "The region in which the simulated " + "player deployment will be deployed."))] + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(SComboButton) + .OnGetMenuContent( + this, &SSpatialGDKCloudDeploymentConfiguration:: + OnGetSimulatedPlayerDeploymentRegionCode) + .ContentPadding(FMargin(2.0f, 2.0f)) + .IsEnabled(this, &SSpatialGDKCloudDeploymentConfiguration:: + IsSimulatedPlayerRegionPickerEnabled) + .ButtonContent()[SNew(STextBlock) + .Text_UObject( + SpatialGDKSettings, + &USpatialGDKEditorSettings:: + GetSimulatedPlayerRegionCode)]]] + // Simulated Player Cluster + + SVerticalBox::Slot().AutoHeight().Padding(2.0f) + [SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(STextBlock) + .Text(LOCTEXT("SimPlayerCluster_Label", "Deployment Cluster")) + .ToolTipText(LOCTEXT("SimPlayerCluster_Tooltip", + "The name of the cluster to deploy to. Region " + "code will be ignored if this is specified."))] + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(SEditableTextBox) + .Text(FText::FromString( + SpatialGDKSettings->GetSimulatedPlayerCluster())) + .ToolTipText(LOCTEXT("SimPlayerCluster_Tooltip", + "The name of the cluster to deploy to. Region " + "code will be ignored if this is specified.")) + .OnTextCommitted(this, &SSpatialGDKCloudDeploymentConfiguration:: + OnSimulatedPlayerClusterCommited) + .OnTextChanged(this, + &SSpatialGDKCloudDeploymentConfiguration:: + OnSimulatedPlayerClusterCommited, + ETextCommit::Default) + .IsEnabled_UObject( + SpatialGDKSettings, + &USpatialGDKEditorSettings::IsSimulatedPlayersEnabled)]] + // Separator + + SVerticalBox::Slot().AutoHeight().Padding(2.0f).VAlign( + VAlign_Center)[SNew(SSeparator)] + // Explanation text + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center)[SNew(STextBlock) + .Text(LOCTEXT("AssemblyConfiguration_Label", + "Assembly Configuration"))] + // Build and Upload Assembly + + SVerticalBox::Slot().AutoHeight().Padding( + 2.0f)[SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(STextBlock) + .Text(LOCTEXT("BuildAndUploadAssembly_Label", + "Build and Upload Assembly")) + .ToolTipText(LOCTEXT("BuildAndUploadAssembly_Tooltip", + "Whether to build and upload the assembly " + "when starting the cloud deployment."))] + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(SCheckBox) + .IsChecked(this, &SSpatialGDKCloudDeploymentConfiguration:: + IsBuildAndUploadAssemblyEnabled) + .OnCheckStateChanged( + this, &SSpatialGDKCloudDeploymentConfiguration:: + OnCheckedBuildAndUploadAssembly)]] + // Generate Schema + + SVerticalBox::Slot().AutoHeight().Padding(2.0f) + [SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(STextBlock) + .Text(LOCTEXT("GenerateSchema_Label", "Generate Schema")) + .ToolTipText(LOCTEXT("GenerateSchema_Tooltip", + "Whether to generate the schema automatically " + "when building the assembly."))] + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(SCheckBox) + .IsChecked(this, &SSpatialGDKCloudDeploymentConfiguration:: + IsGenerateSchemaEnabled) + .OnCheckStateChanged(this, + &SSpatialGDKCloudDeploymentConfiguration:: + OnCheckedGenerateSchema) + .IsEnabled_UObject( + SpatialGDKSettings, + &USpatialGDKEditorSettings::ShouldBuildAndUploadAssembly)]] + // Generate Snapshot + + SVerticalBox::Slot().AutoHeight().Padding(2.0f) + [SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(STextBlock) + .Text(LOCTEXT("GenerateSnapshot_Label", "Generate Snapshot")) + .ToolTipText(LOCTEXT("GenerateSnapshot_Tooltip", + "Whether to generate the snapshot " + "automatically when building the assembly."))] + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(SCheckBox) + .IsChecked(this, &SSpatialGDKCloudDeploymentConfiguration:: + IsGenerateSnapshotEnabled) + .OnCheckStateChanged(this, + &SSpatialGDKCloudDeploymentConfiguration:: + OnCheckedGenerateSnapshot) + .IsEnabled_UObject( + SpatialGDKSettings, + &USpatialGDKEditorSettings::ShouldBuildAndUploadAssembly)]] + // Build Configuration + + SVerticalBox::Slot().AutoHeight().Padding(2.0f) + [SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(STextBlock) + .Text(LOCTEXT("BuildConfiguration_Label", "Build Configuration")) + .ToolTipText(LOCTEXT("BuildConfiguration_Tooltip", + "The configuration to build."))] + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(SComboButton) + .OnGetMenuContent(this, &SSpatialGDKCloudDeploymentConfiguration:: + OnGetBuildConfiguration) + .ContentPadding(FMargin(2.0f, 2.0f)) + .ButtonContent()[SNew(STextBlock) + .Text_UObject( + SpatialGDKSettings, + &USpatialGDKEditorSettings:: + GetAssemblyBuildConfiguration)] + .IsEnabled_UObject( + SpatialGDKSettings, + &USpatialGDKEditorSettings::ShouldBuildAndUploadAssembly)]] + // Enable/Disable Build Client Worker + + SVerticalBox::Slot().AutoHeight().Padding( + 2.0f)[SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(STextBlock) + .Text(LOCTEXT("BuildClientWorker_Label", "Build Client Worker")) + .ToolTipText(LOCTEXT("BuildClientWorker_Tooltip", + "Whether to build the client worker as " + "part of the assembly."))] + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(SCheckBox) + .IsChecked(this, &SSpatialGDKCloudDeploymentConfiguration:: + IsBuildClientWorkerEnabled) + .OnCheckStateChanged(this, + &SSpatialGDKCloudDeploymentConfiguration:: + OnCheckedBuildClientWorker) + .IsEnabled_UObject( + SpatialGDKSettings, + &USpatialGDKEditorSettings::ShouldBuildAndUploadAssembly)]] + // Force Overwrite on Upload + + SVerticalBox::Slot().AutoHeight().Padding( + 2.0f)[SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth( + 1.0f)[SNew(STextBlock) + .Text(LOCTEXT("ForceOverwriteAssembly_Label", + "Force Overwrite on Upload")) + .ToolTipText(LOCTEXT("ForceOverwriteAssembly_Tooltip", + "Whether to overwrite an existing " + "assembly when uploading."))] + + SHorizontalBox::Slot().FillWidth(1.0f) + [SNew(SCheckBox) + .IsChecked(this, &SSpatialGDKCloudDeploymentConfiguration:: + ForceAssemblyOverwrite) + .OnCheckStateChanged(this, + &SSpatialGDKCloudDeploymentConfiguration:: + OnCheckedForceAssemblyOverwrite) + .IsEnabled_UObject( + SpatialGDKSettings, + &USpatialGDKEditorSettings::ShouldBuildAndUploadAssembly)]] + // Separator + + SVerticalBox::Slot().AutoHeight().Padding(2.0f).VAlign( + VAlign_Center)[SNew(SSeparator)] + // Buttons + + SVerticalBox::Slot().FillHeight(1.0f).Padding(2.0f) + [SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f).HAlign(HAlign_Left)[ + // Open Deployment Page + SNew(SUniformGridPanel).SlotPadding(FMargin(2.0f, 20.0f, 0.0f, 0.0f)) + + SUniformGridPanel::Slot(0, 0) + [SNew(SButton) + .HAlign(HAlign_Center) + .Text(LOCTEXT("OpenDeploymentPage_Label", "Open Deployment Page")) + .OnClicked(this, &SSpatialGDKCloudDeploymentConfiguration:: + OnOpenCloudDeploymentPageClicked) + .IsEnabled(this, &SSpatialGDKCloudDeploymentConfiguration:: + CanOpenCloudDeploymentPage)]] + + SHorizontalBox::Slot().FillWidth(1.0f).HAlign(HAlign_Right)[ + // Start Deployment Button + SNew(SUniformGridPanel).SlotPadding(FMargin(2.0f, 20.0f, 0.0f, 0.0f)) + + SUniformGridPanel::Slot(1, 0) + [SNew(SButton) + .HAlign(HAlign_Center) + .Text(LOCTEXT("StartDeployment_Label", "Start Deployment")) + .OnClicked_Raw(ToolbarPtr, &FSpatialGDKEditorToolbarModule:: + OnStartCloudDeployment) + .IsEnabled_Raw(ToolbarPtr, &FSpatialGDKEditorToolbarModule:: + CanStartCloudDeployment)]]]]]]]]; } void SSpatialGDKCloudDeploymentConfiguration::OnDeploymentAssemblyCommited(const FText& InText, ETextCommit::Type InCommitType) @@ -798,7 +658,7 @@ void SSpatialGDKCloudDeploymentConfiguration::OnPrimaryDeploymentNameCommited(co return; } DeploymentNameInputErrorReporting->SetError(TEXT("")); - + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); SpatialGDKSettings->SetPrimaryDeploymentName(InputDeploymentName); } @@ -806,13 +666,14 @@ void SSpatialGDKCloudDeploymentConfiguration::OnPrimaryDeploymentNameCommited(co void SSpatialGDKCloudDeploymentConfiguration::OnCheckedUsePinnedVersion(ECheckBoxState NewCheckedState) { USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); - SpatialGDKSettings->SetUseGDKPinnedRuntimeVersionForCloud(SpatialGDKSettings->RuntimeVariant, NewCheckedState == ECheckBoxState::Checked); + SpatialGDKSettings->SetUseGDKPinnedRuntimeVersionForCloud(SpatialGDKSettings->GetSpatialOSRuntimeVariant(), + NewCheckedState == ECheckBoxState::Checked); } void SSpatialGDKCloudDeploymentConfiguration::OnRuntimeCustomVersionCommited(const FText& InText, ETextCommit::Type InCommitType) { USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); - SpatialGDKSettings->SetCustomCloudSpatialOSRuntimeVersion(SpatialGDKSettings->RuntimeVariant, InText.ToString()); + SpatialGDKSettings->SetCustomCloudSpatialOSRuntimeVersion(SpatialGDKSettings->GetSpatialOSRuntimeVariant(), InText.ToString()); } void SSpatialGDKCloudDeploymentConfiguration::OnSnapshotPathPicked(const FString& PickedPath) @@ -845,7 +706,8 @@ TSharedRef SSpatialGDKCloudDeploymentConfiguration::OnGetPrimaryDeploym if (!pEnum->HasMetaData(TEXT("Hidden"), EnumIdx)) { int64 CurrentEnumValue = pEnum->GetValueByIndex(EnumIdx); - FUIAction ItemAction(FExecuteAction::CreateSP(this, &SSpatialGDKCloudDeploymentConfiguration::OnPrimaryDeploymentRegionCodePicked, CurrentEnumValue)); + FUIAction ItemAction(FExecuteAction::CreateSP( + this, &SSpatialGDKCloudDeploymentConfiguration::OnPrimaryDeploymentRegionCodePicked, CurrentEnumValue)); MenuBuilder.AddMenuEntry(pEnum->GetDisplayNameTextByValue(CurrentEnumValue), TAttribute(), FSlateIcon(), ItemAction); } } @@ -890,7 +752,8 @@ TSharedRef SSpatialGDKCloudDeploymentConfiguration::OnGetSimulatedPlaye if (!pEnum->HasMetaData(TEXT("Hidden"), EnumIdx)) { int64 CurrentEnumValue = pEnum->GetValueByIndex(EnumIdx); - FUIAction ItemAction(FExecuteAction::CreateSP(this, &SSpatialGDKCloudDeploymentConfiguration::OnSimulatedPlayerDeploymentRegionCodePicked, CurrentEnumValue)); + FUIAction ItemAction(FExecuteAction::CreateSP( + this, &SSpatialGDKCloudDeploymentConfiguration::OnSimulatedPlayerDeploymentRegionCodePicked, CurrentEnumValue)); MenuBuilder.AddMenuEntry(pEnum->GetDisplayNameTextByValue(CurrentEnumValue), TAttribute(), FSlateIcon(), ItemAction); } } @@ -908,20 +771,27 @@ void SSpatialGDKCloudDeploymentConfiguration::OnSimulatedPlayerClusterCommited(c void SSpatialGDKCloudDeploymentConfiguration::OnPrimaryDeploymentRegionCodePicked(const int64 RegionCodeEnumValue) { USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); - SpatialGDKSettings->SetPrimaryRegionCode((ERegionCode::Type) RegionCodeEnumValue); - + SpatialGDKSettings->SetPrimaryRegionCode((ERegionCode::Type)RegionCodeEnumValue); } void SSpatialGDKCloudDeploymentConfiguration::OnSimulatedPlayerDeploymentRegionCodePicked(const int64 RegionCodeEnumValue) { USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); - SpatialGDKSettings->SetSimulatedPlayerRegionCode((ERegionCode::Type) RegionCodeEnumValue); + SpatialGDKSettings->SetSimulatedPlayerRegionCode((ERegionCode::Type)RegionCodeEnumValue); } void SSpatialGDKCloudDeploymentConfiguration::OnSimulatedPlayerDeploymentNameCommited(const FText& InText, ETextCommit::Type InCommitType) { + const FString& InputSimulatedPlayersDeploymentName = InText.ToString(); + if (!USpatialGDKEditorSettings::IsDeploymentNameValid(InputSimulatedPlayersDeploymentName)) + { + SimulatedPlayersDeploymentNameInputErrorReporting->SetError(SpatialConstants::DeploymentPatternHint); + return; + } + SimulatedPlayersDeploymentNameInputErrorReporting->SetError(TEXT("")); + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); - SpatialGDKSettings->SetSimulatedPlayerDeploymentName(InText.ToString()); + SpatialGDKSettings->SetSimulatedPlayerDeploymentName(InputSimulatedPlayersDeploymentName); } void SSpatialGDKCloudDeploymentConfiguration::OnNumberOfSimulatedPlayersCommited(uint32 NewValue) @@ -938,25 +808,26 @@ FReply SSpatialGDKCloudDeploymentConfiguration::OnRefreshClicked() FReply SSpatialGDKCloudDeploymentConfiguration::OnStopClicked() { - if (TSharedPtr SpatialGDKEditorSharedPtr = SpatialGDKEditorPtr.Pin()) { - - if (FSpatialGDKEditorToolbarModule* ToolbarPtr = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) + if (TSharedPtr SpatialGDKEditorSharedPtr = SpatialGDKEditorPtr.Pin()) + { + if (FSpatialGDKEditorToolbarModule* ToolbarPtr = + FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) { ToolbarPtr->OnShowTaskStartNotification("Stopping cloud deployment ..."); } SpatialGDKEditorSharedPtr->StopCloudDeployment( - FSimpleDelegate::CreateLambda([]() - { - if (FSpatialGDKEditorToolbarModule* ToolbarPtr = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) + FSimpleDelegate::CreateLambda([]() { + if (FSpatialGDKEditorToolbarModule* ToolbarPtr = + FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) { ToolbarPtr->OnShowSuccessNotification("Successfully stopped cloud deployment."); } }), - FSimpleDelegate::CreateLambda([]() - { - if (FSpatialGDKEditorToolbarModule* ToolbarPtr = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) + FSimpleDelegate::CreateLambda([]() { + if (FSpatialGDKEditorToolbarModule* ToolbarPtr = + FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) { ToolbarPtr->OnShowFailedNotification("Failed to stop cloud deployment."); } @@ -968,7 +839,9 @@ FReply SSpatialGDKCloudDeploymentConfiguration::OnStopClicked() void SSpatialGDKCloudDeploymentConfiguration::OnCloudDocumentationClicked() { FString WebError; - FPlatformProcess::LaunchURL(TEXT("https://documentation.improbable.io/gdk-for-unreal/docs/cloud-deployment-workflow#section-build-server-worker-assembly"), TEXT(""), &WebError); + FPlatformProcess::LaunchURL( + TEXT("https://documentation.improbable.io/gdk-for-unreal/docs/cloud-deployment-workflow#section-build-server-worker-assembly"), + TEXT(""), &WebError); if (!WebError.IsEmpty()) { FNotificationInfo Info(FText::FromString(WebError)); @@ -1059,24 +932,24 @@ TSharedRef SSpatialGDKCloudDeploymentConfiguration::OnGetBuildConfigura FMenuBuilder MenuBuilder(true, nullptr); MenuBuilder.AddMenuEntry(FText::FromString(DebugConfiguration), TAttribute(), FSlateIcon(), - FUIAction(FExecuteAction::CreateSP(this, &SSpatialGDKCloudDeploymentConfiguration::OnBuildConfigurationPicked, DebugConfiguration)) - ); + FUIAction(FExecuteAction::CreateSP(this, &SSpatialGDKCloudDeploymentConfiguration::OnBuildConfigurationPicked, + DebugConfiguration))); MenuBuilder.AddMenuEntry(FText::FromString(DebugGameConfiguration), TAttribute(), FSlateIcon(), - FUIAction(FExecuteAction::CreateSP(this, &SSpatialGDKCloudDeploymentConfiguration::OnBuildConfigurationPicked, DebugGameConfiguration)) - ); + FUIAction(FExecuteAction::CreateSP(this, &SSpatialGDKCloudDeploymentConfiguration::OnBuildConfigurationPicked, + DebugGameConfiguration))); MenuBuilder.AddMenuEntry(FText::FromString(DevelopmentConfiguration), TAttribute(), FSlateIcon(), - FUIAction(FExecuteAction::CreateSP(this, &SSpatialGDKCloudDeploymentConfiguration::OnBuildConfigurationPicked, DevelopmentConfiguration)) - ); + FUIAction(FExecuteAction::CreateSP(this, &SSpatialGDKCloudDeploymentConfiguration::OnBuildConfigurationPicked, + DevelopmentConfiguration))); - MenuBuilder.AddMenuEntry(FText::FromString(TestConfiguration), TAttribute(), FSlateIcon(), - FUIAction(FExecuteAction::CreateSP(this, &SSpatialGDKCloudDeploymentConfiguration::OnBuildConfigurationPicked, TestConfiguration)) - ); + MenuBuilder.AddMenuEntry( + FText::FromString(TestConfiguration), TAttribute(), FSlateIcon(), + FUIAction(FExecuteAction::CreateSP(this, &SSpatialGDKCloudDeploymentConfiguration::OnBuildConfigurationPicked, TestConfiguration))); MenuBuilder.AddMenuEntry(FText::FromString(ShippingConfiguration), TAttribute(), FSlateIcon(), - FUIAction(FExecuteAction::CreateSP(this, &SSpatialGDKCloudDeploymentConfiguration::OnBuildConfigurationPicked, ShippingConfiguration)) - ); + FUIAction(FExecuteAction::CreateSP(this, &SSpatialGDKCloudDeploymentConfiguration::OnBuildConfigurationPicked, + ShippingConfiguration))); return MenuBuilder.MakeWidget(); } @@ -1138,7 +1011,8 @@ void SSpatialGDKCloudDeploymentConfiguration::OnCheckedGenerateSnapshot(ECheckBo FReply SSpatialGDKCloudDeploymentConfiguration::OnOpenCloudDeploymentPageClicked() { FString ProjectName = FSpatialGDKServicesModule::GetProjectName(); - FString ConsoleHost = GetDefault()->IsRunningInChina() ? SpatialConstants::CONSOLE_HOST_CN : SpatialConstants::CONSOLE_HOST; + FString ConsoleHost = + GetDefault()->IsRunningInChina() ? SpatialConstants::CONSOLE_HOST_CN : SpatialConstants::CONSOLE_HOST; FString Url = FString::Printf(TEXT("https://%s/projects/%s"), *ConsoleHost, *ProjectName); FString WebError; diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp index 57e1666d97..cdef1eb566 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp @@ -8,21 +8,23 @@ #include "Editor/EditorEngine.h" #include "EditorStyleSet.h" #include "EngineClasses/SpatialWorldSettings.h" +#include "EngineUtils.h" #include "Framework/Application/SlateApplication.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Framework/Notifications/NotificationManager.h" #include "GeneralProjectSettings.h" #include "HAL/FileManager.h" #include "HAL/PlatformFilemanager.h" -#include "Interfaces/IProjectManager.h" -#include "Internationalization/Regex.h" #include "IOSRuntimeSettings.h" #include "ISettingsContainer.h" #include "ISettingsModule.h" #include "ISettingsSection.h" +#include "Interfaces/IProjectManager.h" +#include "Internationalization/Regex.h" #include "LevelEditor.h" #include "Misc/FileHelper.h" #include "Misc/MessageDialog.h" +#include "Runtime/Launch/Resources/Version.h" #include "Sound/SoundBase.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/Input/SEditableTextBox.h" @@ -32,32 +34,34 @@ #include "CloudDeploymentConfiguration.h" #include "SpatialCommandUtils.h" #include "SpatialConstants.h" +#include "SpatialGDKCloudDeploymentConfiguration.h" #include "SpatialGDKDefaultLaunchConfigGenerator.h" #include "SpatialGDKDefaultWorkerJsonGenerator.h" #include "SpatialGDKDevAuthTokenGenerator.h" #include "SpatialGDKEditor.h" #include "SpatialGDKEditorModule.h" +#include "SpatialGDKEditorPackageAssembly.h" #include "SpatialGDKEditorSchemaGenerator.h" #include "SpatialGDKEditorSettings.h" -#include "SpatialGDKServicesConstants.h" -#include "SpatialGDKServicesModule.h" -#include "SpatialGDKSettings.h" -#include "SpatialGDKEditorPackageAssembly.h" #include "SpatialGDKEditorSnapshotGenerator.h" #include "SpatialGDKEditorToolbarCommands.h" #include "SpatialGDKEditorToolbarStyle.h" -#include "SpatialGDKCloudDeploymentConfiguration.h" +#include "SpatialGDKServicesConstants.h" +#include "SpatialGDKServicesModule.h" +#include "SpatialGDKSettings.h" #include "Utils/GDKPropertyMacros.h" #include "Utils/LaunchConfigurationEditor.h" +#include "Utils/SpatialDebugger.h" +#include "Utils/SpatialStatics.h" DEFINE_LOG_CATEGORY(LogSpatialGDKEditorToolbar); #define LOCTEXT_NAMESPACE "FSpatialGDKEditorToolbarModule" FSpatialGDKEditorToolbarModule::FSpatialGDKEditorToolbarModule() -: bStopSpatialOnExit(false) -, bSchemaBuildError(false) -, bStartingCloudDeployment(false) + : AutoStopLocalDeployment(EAutoStopLocalDeploymentMode::Never) + , bStartingCloudDeployment(false) + , SpatialDebugger(nullptr) { } @@ -75,19 +79,21 @@ void FSpatialGDKEditorToolbarModule::StartupModule() // load sounds ExecutionStartSound = LoadObject(nullptr, TEXT("/Engine/EditorSounds/Notifications/CompileStart_Cue.CompileStart_Cue")); ExecutionStartSound->AddToRoot(); - ExecutionSuccessSound = LoadObject(nullptr, TEXT("/Engine/EditorSounds/Notifications/CompileSuccess_Cue.CompileSuccess_Cue")); + ExecutionSuccessSound = + LoadObject(nullptr, TEXT("/Engine/EditorSounds/Notifications/CompileSuccess_Cue.CompileSuccess_Cue")); ExecutionSuccessSound->AddToRoot(); ExecutionFailSound = LoadObject(nullptr, TEXT("/Engine/EditorSounds/Notifications/CompileFailed_Cue.CompileFailed_Cue")); ExecutionFailSound->AddToRoot(); const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); - OnPropertyChangedDelegateHandle = FCoreUObjectDelegates::OnObjectPropertyChanged.AddRaw(this, &FSpatialGDKEditorToolbarModule::OnPropertyChanged); - bStopSpatialOnExit = SpatialGDKEditorSettings->bStopSpatialOnExit; - bStopLocalDeploymentOnEndPIE = SpatialGDKEditorSettings->bStopLocalDeploymentOnEndPIE; + OnPropertyChangedDelegateHandle = + FCoreUObjectDelegates::OnObjectPropertyChanged.AddRaw(this, &FSpatialGDKEditorToolbarModule::OnPropertyChanged); + AutoStopLocalDeployment = SpatialGDKEditorSettings->AutoStopLocalDeployment; // Check for UseChinaServicesRegion file in the plugin directory to determine the services region. - bool bUseChinaServicesRegion = FPaths::FileExists(FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(SpatialGDKServicesConstants::UseChinaServicesRegionFilename)); + bool bUseChinaServicesRegion = FPaths::FileExists( + FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(SpatialGDKServicesConstants::UseChinaServicesRegionFilename)); GetMutableDefault()->SetServicesRegion(bUseChinaServicesRegion ? EServicesRegion::CN : EServicesRegion::Default); // This is relying on the module loading phase - SpatialGDKServices module should be already loaded @@ -102,32 +108,51 @@ void FSpatialGDKEditorToolbarModule::StartupModule() // This code block starts a local deployment when loading maps for automation testing // However, it is no longer required in 4.25 and beyond, due to the editor flow refactors. #if ENGINE_MINOR_VERSION < 25 - FEditorDelegates::PreBeginPIE.AddLambda([this](bool bIsSimulatingInEditor) - { - if (GIsAutomationTesting && GetDefault()->UsesSpatialNetworking()) + FEditorDelegates::PreBeginPIE.AddLambda([this](bool bIsSimulatingInEditor) { + if (GetDefault()->bAutoStartLocalDeployment && GIsAutomationTesting + && GetDefault()->UsesSpatialNetworking()) { - LocalDeploymentManager->IsServiceRunningAndInCorrectDirectory(); - LocalDeploymentManager->GetLocalDeploymentStatus(); - - VerifyAndStartDeployment(); + VerifyAndStartDeployment(GetDefault()->GetSnapshotOverride()); } }); #endif // We try to stop a local deployment either when the appropriate setting is selected, or when running with automation tests - // TODO: Reuse local deployment between test maps: UNR-2488 - FEditorDelegates::EndPIE.AddLambda([this](bool bIsSimulatingInEditor) - { - if ((GIsAutomationTesting || bStopLocalDeploymentOnEndPIE) && GetDefault()->UsesSpatialNetworking()) + FEditorDelegates::EndPIE.AddLambda([this](bool bIsSimulatingInEditor) { + if ((GIsAutomationTesting || AutoStopLocalDeployment == EAutoStopLocalDeploymentMode::OnEndPIE) + && LocalDeploymentManager->IsLocalDeploymentRunning()) { LocalDeploymentManager->TryStopLocalDeployment(); } }); - LocalDeploymentManager->Init(GetOptionalExposedRuntimeIP()); + LocalDeploymentManager->Init(); LocalReceptionistProxyServerManager->Init(GetDefault()->LocalReceptionistPort); SpatialGDKEditorInstance = FModuleManager::GetModuleChecked("SpatialGDKEditor").GetSpatialGDKEditorInstance(); + + // Get notified of map changed events to update worker boundaries in the editor + FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); + FDelegateHandle OnMapChangedHandle = LevelEditorModule.OnMapChanged().AddRaw(this, &FSpatialGDKEditorToolbarModule::MapChanged); + + if (USpatialStatics::IsSpatialNetworkingEnabled()) + { + // Grab the runtime and inspector binaries ahead of time so they are ready when the user wants them. + const FString RuntimeVersion = SpatialGDKEditorSettings->GetSelectedRuntimeVariantVersion().GetVersionForLocal(); + const FString InspectorVersion = SpatialGDKEditorSettings->GetInspectorVersion(); + + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, RuntimeVersion, InspectorVersion] { + if (!FetchRuntimeBinaryWrapper(RuntimeVersion)) + { + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Attempted to cache the local runtime binary but failed!")); + } + + if (!FetchInspectorBinaryWrapper(InspectorVersion)) + { + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Attempted to cache the local inspector binary but failed!")); + } + }); + } } void FSpatialGDKEditorToolbarModule::ShutdownModule() @@ -161,6 +186,11 @@ void FSpatialGDKEditorToolbarModule::ShutdownModule() ExecutionFailSound = nullptr; } + if (FLevelEditorModule* LevelEditor = FModuleManager::GetModulePtr("LevelEditor")) + { + LevelEditor->OnMapChanged().RemoveAll(this); + } + FSpatialGDKEditorToolbarStyle::Shutdown(); FSpatialGDKEditorToolbarCommands::Unregister(); } @@ -169,15 +199,17 @@ void FSpatialGDKEditorToolbarModule::PreUnloadCallback() { LocalReceptionistProxyServerManager->TryStopReceptionistProxyServer(); - if (bStopSpatialOnExit) + if (AutoStopLocalDeployment != EAutoStopLocalDeploymentMode::Never) { + if (InspectorProcess.IsSet() && InspectorProcess->Update()) + { + InspectorProcess->Cancel(); + } LocalDeploymentManager->TryStopLocalDeployment(); } } -void FSpatialGDKEditorToolbarModule::Tick(float DeltaTime) -{ -} +void FSpatialGDKEditorToolbarModule::Tick(float DeltaTime) {} bool FSpatialGDKEditorToolbarModule::CanExecuteSchemaGenerator() const { @@ -191,130 +223,105 @@ bool FSpatialGDKEditorToolbarModule::CanExecuteSnapshotGenerator() const void FSpatialGDKEditorToolbarModule::MapActions(TSharedPtr InPluginCommands) { - InPluginCommands->MapAction( - FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSchema, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::SchemaGenerateButtonClicked), - FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CanExecuteSchemaGenerator)); + InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSchema, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::SchemaGenerateButtonClicked), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CanExecuteSchemaGenerator)); - InPluginCommands->MapAction( - FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSchemaFull, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::SchemaGenerateFullButtonClicked), - FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CanExecuteSchemaGenerator)); + InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSchemaFull, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::SchemaGenerateFullButtonClicked), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CanExecuteSchemaGenerator)); - InPluginCommands->MapAction( - FSpatialGDKEditorToolbarCommands::Get().DeleteSchemaDatabase, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::DeleteSchemaDatabaseButtonClicked)); + InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().DeleteSchemaDatabase, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::DeleteSchemaDatabaseButtonClicked)); - InPluginCommands->MapAction( - FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSnapshot, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CreateSnapshotButtonClicked), - FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CanExecuteSnapshotGenerator)); + InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSnapshot, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CreateSnapshotButtonClicked), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CanExecuteSnapshotGenerator)); - InPluginCommands->MapAction( - FSpatialGDKEditorToolbarCommands::Get().StartNative, - FExecuteAction(), - FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartNativeCanExecute), - FIsActionChecked(), - FIsActionButtonVisible::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartNativeIsVisible)); + InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().StartNative, FExecuteAction(), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartNativeCanExecute), + FIsActionChecked(), + FIsActionButtonVisible::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartNativeIsVisible)); InPluginCommands->MapAction( FSpatialGDKEditorToolbarCommands::Get().StartLocalSpatialDeployment, FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartLocalSpatialDeploymentButtonClicked), - FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartLocalSpatialDeploymentCanExecute), - FIsActionChecked(), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartLocalSpatialDeploymentCanExecute), FIsActionChecked(), FIsActionButtonVisible::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartLocalSpatialDeploymentIsVisible)); InPluginCommands->MapAction( FSpatialGDKEditorToolbarCommands::Get().StartCloudSpatialDeployment, FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::LaunchOrShowCloudDeployment), - FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartCloudSpatialDeploymentCanExecute), - FIsActionChecked(), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartCloudSpatialDeploymentCanExecute), FIsActionChecked(), FIsActionButtonVisible::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartCloudSpatialDeploymentIsVisible)); - InPluginCommands->MapAction( - FSpatialGDKEditorToolbarCommands::Get().StopSpatialDeployment, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialDeploymentButtonClicked), - FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialDeploymentCanExecute), - FIsActionChecked(), - FIsActionButtonVisible::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialDeploymentIsVisible)); - - InPluginCommands->MapAction( - FSpatialGDKEditorToolbarCommands::Get().LaunchInspectorWebPageAction, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::LaunchInspectorWebpageButtonClicked), - FCanExecuteAction()); + InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().StopSpatialDeployment, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialDeploymentButtonClicked), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialDeploymentCanExecute), + FIsActionChecked(), + FIsActionButtonVisible::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialDeploymentIsVisible)); - InPluginCommands->MapAction( - FSpatialGDKEditorToolbarCommands::Get().EnableBuildClientWorker, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::OnCheckedBuildClientWorker), - FCanExecuteAction::CreateStatic(&FSpatialGDKEditorToolbarModule::AreCloudDeploymentPropertiesEditable), - FIsActionChecked::CreateRaw(this, &FSpatialGDKEditorToolbarModule::IsBuildClientWorkerEnabled)); + InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().LaunchInspectorWebPageAction, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::LaunchInspectorWebpageButtonClicked), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::LaunchInspectorWebpageCanExecute)); - InPluginCommands->MapAction( - FSpatialGDKEditorToolbarCommands::Get().EnableBuildSimulatedPlayer, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::OnCheckedSimulatedPlayers), - FCanExecuteAction::CreateStatic(&FSpatialGDKEditorToolbarModule::AreCloudDeploymentPropertiesEditable), - FIsActionChecked::CreateRaw(this, &FSpatialGDKEditorToolbarModule::IsSimulatedPlayersEnabled)); + InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().EnableBuildClientWorker, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::OnCheckedBuildClientWorker), + FCanExecuteAction::CreateStatic(&FSpatialGDKEditorToolbarModule::AreCloudDeploymentPropertiesEditable), + FIsActionChecked::CreateRaw(this, &FSpatialGDKEditorToolbarModule::IsBuildClientWorkerEnabled)); - InPluginCommands->MapAction( - FSpatialGDKEditorToolbarCommands::Get().OpenCloudDeploymentWindowAction, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::ShowCloudDeploymentDialog), - FCanExecuteAction()); + InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().EnableBuildSimulatedPlayer, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::OnCheckedSimulatedPlayers), + FCanExecuteAction::CreateStatic(&FSpatialGDKEditorToolbarModule::AreCloudDeploymentPropertiesEditable), + FIsActionChecked::CreateRaw(this, &FSpatialGDKEditorToolbarModule::IsSimulatedPlayersEnabled)); - InPluginCommands->MapAction( - FSpatialGDKEditorToolbarCommands::Get().OpenLaunchConfigurationEditorAction, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::OpenLaunchConfigurationEditor), - FCanExecuteAction()); + InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().OpenCloudDeploymentWindowAction, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::ShowCloudDeploymentDialog), + FCanExecuteAction()); - InPluginCommands->MapAction( - FSpatialGDKEditorToolbarCommands::Get().StartSpatialService, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartSpatialServiceButtonClicked), - FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartSpatialServiceCanExecute), - FIsActionChecked(), - FIsActionButtonVisible::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartSpatialServiceIsVisible)); - - InPluginCommands->MapAction( - FSpatialGDKEditorToolbarCommands::Get().StopSpatialService, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialServiceButtonClicked), - FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialServiceCanExecute), - FIsActionChecked(), - FIsActionButtonVisible::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialServiceIsVisible)); + InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().OpenLaunchConfigurationEditorAction, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::OpenLaunchConfigurationEditor), + FCanExecuteAction()); InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().EnableSpatialNetworking, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::OnToggleSpatialNetworking), - FCanExecuteAction(), - FIsActionChecked::CreateRaw(this, &FSpatialGDKEditorToolbarModule::OnIsSpatialNetworkingEnabled) - ); + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::OnToggleSpatialNetworking), + FCanExecuteAction(), + FIsActionChecked::CreateRaw(this, &FSpatialGDKEditorToolbarModule::OnIsSpatialNetworkingEnabled)); InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().LocalDeployment, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::LocalDeploymentClicked), - FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::OnIsSpatialNetworkingEnabled), - FIsActionChecked::CreateRaw(this, &FSpatialGDKEditorToolbarModule::IsLocalDeploymentSelected) - ); + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::LocalDeploymentClicked), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::OnIsSpatialNetworkingEnabled), + FIsActionChecked::CreateRaw(this, &FSpatialGDKEditorToolbarModule::IsLocalDeploymentSelected)); InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().CloudDeployment, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CloudDeploymentClicked), - FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::IsSpatialOSNetFlowConfigurable), - FIsActionChecked::CreateRaw(this, &FSpatialGDKEditorToolbarModule::IsCloudDeploymentSelected) - ); + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CloudDeploymentClicked), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::IsSpatialOSNetFlowConfigurable), + FIsActionChecked::CreateRaw(this, &FSpatialGDKEditorToolbarModule::IsCloudDeploymentSelected)); InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().GDKEditorSettings, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::GDKEditorSettingsClicked) - ); + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::GDKEditorSettingsClicked)); InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().GDKRuntimeSettings, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::GDKRuntimeSettingsClicked) - ); + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::GDKRuntimeSettingsClicked)); + + InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().ToggleSpatialDebuggerEditor, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::ToggleSpatialDebuggerEditor), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::AllowWorkerBoundaries), + FIsActionChecked::CreateRaw(this, &FSpatialGDKEditorToolbarModule::IsSpatialDebuggerEditorEnabled)); + + InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().ToggleMultiWorkerEditor, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::ToggleMultiworkerEditor), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::OnIsSpatialNetworkingEnabled), + FIsActionChecked::CreateRaw(this, &FSpatialGDKEditorToolbarModule::IsMultiWorkerEnabled)); } void FSpatialGDKEditorToolbarModule::SetupToolbar(TSharedPtr InPluginCommands) { - FLevelEditorModule& LevelEditorModule = - FModuleManager::LoadModuleChecked("LevelEditor"); + FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); { TSharedPtr MenuExtender = MakeShareable(new FExtender()); - MenuExtender->AddMenuExtension( - "General", EExtensionHook::After, InPluginCommands, - FMenuExtensionDelegate::CreateRaw(this, &FSpatialGDKEditorToolbarModule::AddMenuExtension)); + MenuExtender->AddMenuExtension("General", EExtensionHook::After, InPluginCommands, + FMenuExtensionDelegate::CreateRaw(this, &FSpatialGDKEditorToolbarModule::AddMenuExtension)); LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender); } @@ -323,8 +330,7 @@ void FSpatialGDKEditorToolbarModule::SetupToolbar(TSharedPtr ToolbarExtender = MakeShareable(new FExtender); ToolbarExtender->AddToolBarExtension( "Game", EExtensionHook::After, InPluginCommands, - FToolBarExtensionDelegate::CreateRaw(this, - &FSpatialGDKEditorToolbarModule::AddToolbarExtension)); + FToolBarExtensionDelegate::CreateRaw(this, &FSpatialGDKEditorToolbarModule::AddToolbarExtension)); LevelEditorModule.GetToolBarExtensibilityManager()->AddExtender(ToolbarExtender); } @@ -344,8 +350,6 @@ void FSpatialGDKEditorToolbarModule::AddMenuExtension(FMenuBuilder& Builder) #endif Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSchema); Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSnapshot); - Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StartSpatialService); - Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StopSpatialService); } Builder.EndSection(); } @@ -357,38 +361,21 @@ void FSpatialGDKEditorToolbarModule::AddToolbarExtension(FToolBarBuilder& Builde Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StartLocalSpatialDeployment); Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StartCloudSpatialDeployment); Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StopSpatialDeployment); - Builder.AddComboButton( - FUIAction(), - FOnGetContent::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CreateStartDropDownMenuContent), - LOCTEXT("StartDropDownMenu_Label", "SpatialOS Network Options"), - TAttribute(), - FSlateIcon(FEditorStyle::GetStyleSetName(), "GDK.Start"), - true - ); + Builder.AddComboButton(FUIAction(), FOnGetContent::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CreateStartDropDownMenuContent), + LOCTEXT("StartDropDownMenu_Label", "SpatialOS Network Options"), TAttribute(), + FSlateIcon(FEditorStyle::GetStyleSetName(), "GDK.Start"), true); Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().LaunchInspectorWebPageAction); #if PLATFORM_WINDOWS Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().OpenCloudDeploymentWindowAction); - Builder.AddComboButton( - FUIAction(), - FOnGetContent::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CreateLaunchDeploymentMenuContent), - LOCTEXT("GDKDeploymentCombo_Label", "Deployment Tools"), - TAttribute(), - FSlateIcon(FEditorStyle::GetStyleSetName(), "GDK.Cloud"), - true - ); + 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().CreateSpatialGDKSchema); - Builder.AddComboButton( - FUIAction(), - FOnGetContent::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CreateGenerateSchemaMenuContent), - LOCTEXT("GDKSchemaCombo_Label", "Schema Generation Options"), - TAttribute(), - FSlateIcon(FEditorStyle::GetStyleSetName(), "GDK.Schema"), - true - ); + Builder.AddComboButton(FUIAction(), FOnGetContent::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CreateGenerateSchemaMenuContent), + LOCTEXT("GDKSchemaCombo_Label", "Schema Generation Options"), TAttribute(), + FSlateIcon(FEditorStyle::GetStyleSetName(), "GDK.Schema"), true); Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSnapshot); - Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StartSpatialService); - Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StopSpatialService); } TSharedRef FSpatialGDKEditorToolbarModule::CreateGenerateSchemaMenuContent() @@ -448,7 +435,9 @@ void OnCloudDeploymentNameChanged(const FText& InText, ETextCommit::Type InCommi FRegexMatcher DeploymentNameRegexMatcher(DeploymentNamePatternRegex, InputDeploymentName); if (!InputDeploymentName.IsEmpty() && !DeploymentNameRegexMatcher.FindNext()) { - FMessageDialog::Open(EAppMsgType::Ok, FText::Format(LOCTEXT("InputValidDeploymentName_Prompt", "Please input a valid deployment name. {0}"), SpatialConstants::DeploymentPatternHint)); + FMessageDialog::Open(EAppMsgType::Ok, + FText::Format(LOCTEXT("InputValidDeploymentName_Prompt", "Please input a valid deployment name. {0}"), + SpatialConstants::DeploymentPatternHint)); UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Invalid deployment name: %s"), *InputDeploymentName); return; } @@ -479,23 +468,17 @@ TSharedRef FSpatialGDKEditorToolbarModule::CreateStartDropDownMenuConte MenuBuilder.BeginSection("AdditionalProperties"); { - MenuBuilder.AddWidget(CreateBetterEditableTextWidget( - LOCTEXT("LocalDeploymentIP_Label", "Local Deployment IP: "), - FText::FromString(GetDefault()->ExposedRuntimeIP), - OnLocalDeploymentIPChanged, - FSpatialGDKEditorToolbarModule::IsLocalDeploymentIPEditable - ), - FText() - ); - - MenuBuilder.AddWidget(CreateBetterEditableTextWidget( - LOCTEXT("CloudDeploymentName_Label", "Cloud Deployment Name: "), - FText::FromString(SpatialGDKEditorSettings->GetPrimaryDeploymentName()), - OnCloudDeploymentNameChanged, - FSpatialGDKEditorToolbarModule::AreCloudDeploymentPropertiesEditable - ), - FText() - ); + MenuBuilder.AddWidget( + CreateBetterEditableTextWidget(LOCTEXT("LocalDeploymentIP_Label", "Local Deployment IP: "), + FText::FromString(GetDefault()->ExposedRuntimeIP), + OnLocalDeploymentIPChanged, FSpatialGDKEditorToolbarModule::IsLocalDeploymentIPEditable), + FText()); + + MenuBuilder.AddWidget(CreateBetterEditableTextWidget(LOCTEXT("CloudDeploymentName_Label", "Cloud Deployment Name: "), + FText::FromString(SpatialGDKEditorSettings->GetPrimaryDeploymentName()), + OnCloudDeploymentNameChanged, + FSpatialGDKEditorToolbarModule::AreCloudDeploymentPropertiesEditable), + FText()); MenuBuilder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().EnableBuildClientWorker); MenuBuilder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().EnableBuildSimulatedPlayer); } @@ -508,31 +491,29 @@ TSharedRef FSpatialGDKEditorToolbarModule::CreateStartDropDownMenuConte } MenuBuilder.EndSection(); + MenuBuilder.BeginSection("SpatialDebuggerEditorSettings"); + { + MenuBuilder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().ToggleSpatialDebuggerEditor); + MenuBuilder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().ToggleMultiWorkerEditor); + } + MenuBuilder.EndSection(); + return MenuBuilder.MakeWidget(); } -TSharedRef FSpatialGDKEditorToolbarModule::CreateBetterEditableTextWidget(const FText& Label, const FText& Text, FOnTextCommitted::TFuncType OnTextCommitted, IsEnabledFunc IsEnabled) +TSharedRef FSpatialGDKEditorToolbarModule::CreateBetterEditableTextWidget(const FText& Label, const FText& Text, + FOnTextCommitted::TFuncType OnTextCommitted, + IsEnabledFunc IsEnabled) { return SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .AutoWidth() - .VAlign(VAlign_Center) - [ - SNew(STextBlock) - .Text(Label) - .IsEnabled_Static(IsEnabled) - ] - + SHorizontalBox::Slot() - .FillWidth(1.f) - .VAlign(VAlign_Bottom) - [ - SNew(SEditableTextBox) - .OnTextCommitted_Static(OnTextCommitted) - .Text(Text) - .SelectAllTextWhenFocused(true) - .IsEnabled_Static(IsEnabled) - .Font(FEditorStyle::GetFontStyle(TEXT("SourceControl.LoginWindow.Font"))) - ]; + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)[SNew(STextBlock).Text(Label).IsEnabled_Static(IsEnabled)] + + SHorizontalBox::Slot().FillWidth(1.f).VAlign( + VAlign_Bottom)[SNew(SEditableTextBox) + .OnTextCommitted_Static(OnTextCommitted) + .Text(Text) + .SelectAllTextWhenFocused(true) + .IsEnabled_Static(IsEnabled) + .Font(FEditorStyle::GetFontStyle(TEXT("SourceControl.LoginWindow.Font")))]; } void FSpatialGDKEditorToolbarModule::CreateSnapshotButtonClicked() @@ -541,16 +522,23 @@ void FSpatialGDKEditorToolbarModule::CreateSnapshotButtonClicked() const USpatialGDKEditorSettings* Settings = GetDefault(); - SpatialGDKEditorInstance->GenerateSnapshot( - GEditor->GetEditorWorldContext().World(), Settings->GetSpatialOSSnapshotToSave(), - FSimpleDelegate::CreateLambda([this]() { OnShowSuccessNotification("Snapshot successfully generated!"); }), - FSimpleDelegate::CreateLambda([this]() { OnShowFailedNotification("Snapshot generation failed!"); }), - FSpatialGDKEditorErrorHandler::CreateLambda([](FString ErrorText) { FMessageDialog::Debugf(FText::FromString(ErrorText)); })); + SpatialGDKEditorInstance->GenerateSnapshot(GEditor->GetEditorWorldContext().World(), Settings->GetSpatialOSSnapshotToSave(), + FSimpleDelegate::CreateLambda([this]() { + OnShowSuccessNotification("Snapshot successfully generated!"); + }), + FSimpleDelegate::CreateLambda([this]() { + OnShowFailedNotification("Snapshot generation failed!"); + }), + FSpatialGDKEditorErrorHandler::CreateLambda([](FString ErrorText) { + FMessageDialog::Debugf(FText::FromString(ErrorText)); + })); } void FSpatialGDKEditorToolbarModule::DeleteSchemaDatabaseButtonClicked() { - if (FMessageDialog::Open(EAppMsgType::YesNo, LOCTEXT("DeleteSchemaDatabase_Prompt", "Are you sure you want to delete the schema database?")) == EAppReturnType::Yes) + if (FMessageDialog::Open(EAppMsgType::YesNo, + LOCTEXT("DeleteSchemaDatabase_Prompt", "Are you sure you want to delete the schema database?")) + == EAppReturnType::Yes) { OnShowTaskStartNotification(TEXT("Deleting schema database")); if (SpatialGDKEditor::Schema::DeleteSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_FILE_PATH)) @@ -576,9 +564,9 @@ void FSpatialGDKEditorToolbarModule::SchemaGenerateFullButtonClicked() void FSpatialGDKEditorToolbarModule::OnShowSingleFailureNotification(const FString& NotificationText) { - AsyncTask(ENamedThreads::GameThread, [NotificationText] - { - if (FSpatialGDKEditorToolbarModule* Module = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) + AsyncTask(ENamedThreads::GameThread, [NotificationText] { + if (FSpatialGDKEditorToolbarModule* Module = + FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) { Module->ShowSingleFailureNotification(NotificationText); } @@ -604,9 +592,9 @@ void FSpatialGDKEditorToolbarModule::ShowSingleFailureNotification(const FString void FSpatialGDKEditorToolbarModule::OnShowTaskStartNotification(const FString& NotificationText) { - AsyncTask(ENamedThreads::GameThread, [NotificationText] - { - if (FSpatialGDKEditorToolbarModule* Module = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) + AsyncTask(ENamedThreads::GameThread, [NotificationText] { + if (FSpatialGDKEditorToolbarModule* Module = + FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) { Module->ShowTaskStartNotification(NotificationText); } @@ -641,9 +629,9 @@ void FSpatialGDKEditorToolbarModule::ShowTaskStartNotification(const FString& No void FSpatialGDKEditorToolbarModule::OnShowSuccessNotification(const FString& NotificationText) { - AsyncTask(ENamedThreads::GameThread, [NotificationText] - { - if (FSpatialGDKEditorToolbarModule* Module = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) + AsyncTask(ENamedThreads::GameThread, [NotificationText] { + if (FSpatialGDKEditorToolbarModule* Module = + FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) { Module->ShowSuccessNotification(NotificationText); } @@ -671,9 +659,9 @@ void FSpatialGDKEditorToolbarModule::ShowSuccessNotification(const FString& Noti void FSpatialGDKEditorToolbarModule::OnShowFailedNotification(const FString& NotificationText) { - AsyncTask(ENamedThreads::GameThread, [NotificationText] - { - if (FSpatialGDKEditorToolbarModule* Module = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) + AsyncTask(ENamedThreads::GameThread, [NotificationText] { + if (FSpatialGDKEditorToolbarModule* Module = + FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) { Module->ShowFailedNotification(NotificationText); } @@ -699,51 +687,107 @@ void FSpatialGDKEditorToolbarModule::ShowFailedNotification(const FString& Notif } } -void FSpatialGDKEditorToolbarModule::StartSpatialServiceButtonClicked() +void FSpatialGDKEditorToolbarModule::ToggleSpatialDebuggerEditor() { - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] + if (SpatialDebugger.IsValid()) + { + USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetMutableDefault(); + SpatialGDKEditorSettings->SetSpatialDebuggerEditorEnabled(!SpatialGDKEditorSettings->IsSpatialDebuggerEditorEnabled()); + GDK_PROPERTY(Property)* SpatialDebuggerEditorEnabledProperty = + USpatialGDKEditorSettings::StaticClass()->FindPropertyByName(FName("bSpatialDebuggerEditorEnabled")); + SpatialGDKEditorSettings->UpdateSinglePropertyInConfigFile(SpatialDebuggerEditorEnabledProperty, + SpatialGDKEditorSettings->GetDefaultConfigFilename()); + + SpatialDebugger->EditorSpatialToggleDebugger(SpatialGDKEditorSettings->IsSpatialDebuggerEditorEnabled()); + } + else { - FDateTime StartTime = FDateTime::Now(); - OnShowTaskStartNotification(TEXT("Starting spatial service...")); + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("There was no SpatialDebugger setup when the map was loaded.")); + } +} + +void FSpatialGDKEditorToolbarModule::ToggleMultiworkerEditor() +{ + USpatialGDKSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetMultiWorkerEditorEnabled(!SpatialGDKSettings->IsMultiWorkerEditorEnabled()); + GDK_PROPERTY(Property)* EnableMultiWorkerProperty = USpatialGDKSettings::StaticClass()->FindPropertyByName(FName("bEnableMultiWorker")); + SpatialGDKSettings->UpdateSinglePropertyInConfigFile(EnableMultiWorkerProperty, SpatialGDKSettings->GetDefaultConfigFilename()); + + if (SpatialDebugger.IsValid()) + { + SpatialDebugger->EditorRefreshWorkerRegions(); + } +} - // If the runtime IP is to be exposed, pass it to the spatial service on startup - const bool bSpatialServiceStarted = LocalDeploymentManager->TryStartSpatialService(GetOptionalExposedRuntimeIP()); - if (!bSpatialServiceStarted) +void FSpatialGDKEditorToolbarModule::MapChanged(UWorld* World, EMapChangeType MapChangeType) +{ + if (MapChangeType == EMapChangeType::LoadMap || MapChangeType == EMapChangeType::NewMap) + { + // If Spatial networking is enabled then initialize the editor debugging facilities. + if (GetDefault()->UsesSpatialNetworking()) { - OnShowFailedNotification(TEXT("Spatial service failed to start")); - UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Could not start spatial service.")); - return; + InitialiseSpatialDebuggerEditor(World); } + } + else if (MapChangeType == EMapChangeType::TearDownWorld) + { + // Destroy spatial debugger when changing map as it will be invalid + DestroySpatialDebuggerEditor(); + } +} - FTimespan Span = FDateTime::Now() - StartTime; +bool FSpatialGDKEditorToolbarModule::FetchRuntimeBinaryWrapper(FString RuntimeVersion) +{ + bFetchingRuntimeBinary = true; - OnShowSuccessNotification(TEXT("Spatial service started!")); - UE_LOG(LogSpatialGDKEditorToolbar, Log, TEXT("Spatial service started in %f seconds."), Span.GetTotalSeconds()); - }); + const bool bSuccess = SpatialCommandUtils::FetchRuntimeBinary(RuntimeVersion, GetDefault()->IsRunningInChina()); + + if (!bSuccess) + { + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Could not fetch the local runtime for version %s"), *RuntimeVersion); + OnShowFailedNotification(TEXT("Failed to fetch local runtime!")); + } + + bFetchingRuntimeBinary = false; + + return bSuccess; } -void FSpatialGDKEditorToolbarModule::StopSpatialServiceButtonClicked() +bool FSpatialGDKEditorToolbarModule::FetchInspectorBinaryWrapper(FString InspectorVersion) { - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] + bFetchingInspectorBinary = true; + + bool bSuccess = SpatialCommandUtils::FetchInspectorBinary(InspectorVersion, GetDefault()->IsRunningInChina()); + + if (!bSuccess) { - FDateTime StartTime = FDateTime::Now(); - OnShowTaskStartNotification(TEXT("Stopping spatial service...")); + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Could not fetch the Inspector for version %s"), *InspectorVersion); + OnShowFailedNotification(TEXT("Failed to fetch local inspector!")); + bFetchingInspectorBinary = false; + return false; + } - if (!LocalDeploymentManager->TryStopSpatialService()) - { - OnShowFailedNotification(TEXT("Spatial service failed to stop")); - UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Could not stop spatial service.")); - return; - } +#if PLATFORM_MAC + int32 OutCode = 0; + FString OutString; + FString OutErr; + FString ChmodCommand = FPaths::Combine(SpatialGDKServicesConstants::BinPath, TEXT("chmod")); + FString ChmodArguments = FString::Printf(TEXT("+x \"%s\""), *SpatialGDKServicesConstants::GetInspectorExecutablePath(InspectorVersion)); + bSuccess = FPlatformProcess::ExecProcess(*ChmodCommand, *ChmodArguments, &OutCode, &OutString, &OutErr); + if (!bSuccess) + { + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Could not make the Inspector executable for version %s. %s %s"), *InspectorVersion, + *OutString, *OutErr); + OnShowFailedNotification(TEXT("Failed to fetch local inspector!")); + } +#endif - FTimespan Span = FDateTime::Now() - StartTime; + bFetchingInspectorBinary = false; - OnShowSuccessNotification(TEXT("Spatial service stopped!")); - UE_LOG(LogSpatialGDKEditorToolbar, Log, TEXT("Spatial service stopped in %f secoonds."), Span.GetTotalSeconds()); - }); + return bSuccess; } -void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment() +void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment(FString ForceSnapshot /* = ""*/) { // Don't try and start a local deployment if spatial networking is disabled. if (!GetDefault()->UsesSpatialNetworking()) @@ -781,31 +825,36 @@ void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment() UWorld* EditorWorld = GEditor->GetEditorWorldContext().World(); check(EditorWorld); - LaunchConfig = FPaths::Combine(FPaths::ConvertRelativePathToFull(FPaths::ProjectIntermediateDir()), FString::Printf(TEXT("Improbable/%s_LocalLaunchConfig.json"), *EditorWorld->GetMapName())); + LaunchConfig = FPaths::Combine(FPaths::ConvertRelativePathToFull(FPaths::ProjectIntermediateDir()), + FString::Printf(TEXT("Improbable/%s_LocalLaunchConfig.json"), *EditorWorld->GetMapName())); FSpatialLaunchConfigDescription LaunchConfigDescription = SpatialGDKEditorSettings->LaunchConfigDesc; - FWorkerTypeLaunchSection Conf = SpatialGDKEditorSettings->LaunchConfigDesc.ServerWorkerConfig; // Force manual connection to true as this is the config for PIE. - Conf.bManualWorkerConnectionOnly = true; - if (Conf.bAutoNumEditorInstances) + LaunchConfigDescription.ServerWorkerConfiguration.bManualWorkerConnectionOnly = true; + if (LaunchConfigDescription.ServerWorkerConfiguration.bAutoNumEditorInstances) { - Conf.NumEditorInstances = GetWorkerCountFromWorldSettings(*EditorWorld); + LaunchConfigDescription.ServerWorkerConfiguration.NumEditorInstances = GetWorkerCountFromWorldSettings(*EditorWorld); } - if (!ValidateGeneratedLaunchConfig(LaunchConfigDescription, Conf)) + if (!ValidateGeneratedLaunchConfig(LaunchConfigDescription)) { return; } - GenerateLaunchConfig(LaunchConfig, &LaunchConfigDescription, Conf); + GenerateLaunchConfig(LaunchConfig, &LaunchConfigDescription, /*bGenerateCloudConfig*/ false); // Also create default launch config for cloud deployments. { // Revert to the setting's flag value for manual connection. - Conf.bManualWorkerConnectionOnly = SpatialGDKEditorSettings->LaunchConfigDesc.ServerWorkerConfig.bManualWorkerConnectionOnly; - FString CloudLaunchConfig = FPaths::Combine(FPaths::ConvertRelativePathToFull(FPaths::ProjectIntermediateDir()), FString::Printf(TEXT("Improbable/%s_CloudLaunchConfig.json"), *EditorWorld->GetMapName())); - GenerateLaunchConfig(CloudLaunchConfig, &LaunchConfigDescription, Conf); + LaunchConfigDescription.ServerWorkerConfiguration.bManualWorkerConnectionOnly = + SpatialGDKEditorSettings->LaunchConfigDesc.ServerWorkerConfiguration.bManualWorkerConnectionOnly; + FString CloudLaunchConfig = + FPaths::Combine(FPaths::ConvertRelativePathToFull(FPaths::ProjectIntermediateDir()), + FString::Printf(TEXT("Improbable/%s_CloudLaunchConfig.json"), *EditorWorld->GetMapName())); + LaunchConfigDescription.ServerWorkerConfiguration.NumEditorInstances = GetWorkerCountFromWorldSettings(*EditorWorld, true); + + GenerateLaunchConfig(CloudLaunchConfig, &LaunchConfigDescription, /*bGenerateCloudConfig*/ true); } } else @@ -814,11 +863,18 @@ void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment() } const FString LaunchFlags = SpatialGDKEditorSettings->GetSpatialOSCommandLineLaunchFlags(); - const FString SnapshotName = SpatialGDKEditorSettings->GetSpatialOSSnapshotToLoad(); + const FString SnapshotName = ForceSnapshot.IsEmpty() ? SpatialGDKEditorSettings->GetSpatialOSSnapshotToLoad() : ForceSnapshot; + const FString SnapshotPath = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSSnapshotFolderPath, SnapshotName); + const FString RuntimeVersion = SpatialGDKEditorSettings->GetSelectedRuntimeVariantVersion().GetVersionForLocal(); - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, LaunchConfig, LaunchFlags, SnapshotName, RuntimeVersion] - { + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, LaunchConfig, LaunchFlags, SnapshotPath, RuntimeVersion] { + if (!FetchRuntimeBinaryWrapper(RuntimeVersion)) + { + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Attempted to start a local deployment but could not fetch the local runtime.")); + return; + } + // If the last local deployment is still stopping then wait until it's finished. while (LocalDeploymentManager->IsDeploymentStopping()) { @@ -829,7 +885,6 @@ void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment() if (LocalDeploymentManager->IsRedeployRequired() && LocalDeploymentManager->IsLocalDeploymentRunning()) { UE_LOG(LogSpatialGDKEditorToolbar, Display, TEXT("Local deployment must restart.")); - OnShowTaskStartNotification(TEXT("Local deployment restarting.")); LocalDeploymentManager->TryStopLocalDeployment(); } else if (LocalDeploymentManager->IsLocalDeploymentRunning()) @@ -838,20 +893,15 @@ void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment() return; } - FLocalDeploymentManager::LocalDeploymentCallback CallBack = [this](bool bSuccess) - { - if (bSuccess) - { - OnShowSuccessNotification(TEXT("Local deployment started!")); - } - else + FLocalDeploymentManager::LocalDeploymentCallback CallBack = [this](bool bSuccess) { + if (!bSuccess) { OnShowFailedNotification(TEXT("Local deployment failed to start")); } }; - OnShowTaskStartNotification(TEXT("Starting local deployment...")); - LocalDeploymentManager->TryStartLocalDeployment(LaunchConfig, RuntimeVersion, LaunchFlags, SnapshotName, GetOptionalExposedRuntimeIP(), CallBack); + LocalDeploymentManager->TryStartLocalDeployment(LaunchConfig, RuntimeVersion, LaunchFlags, SnapshotPath, + GetOptionalExposedRuntimeIP(), CallBack); }); } @@ -862,35 +912,18 @@ void FSpatialGDKEditorToolbarModule::StartLocalSpatialDeploymentButtonClicked() void FSpatialGDKEditorToolbarModule::StopSpatialDeploymentButtonClicked() { - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] - { - OnShowTaskStartNotification(TEXT("Stopping local deployment...")); - if (LocalDeploymentManager->TryStopLocalDeployment()) - { - OnShowSuccessNotification(TEXT("Successfully stopped local deployment")); - } - else + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] { + if (!LocalDeploymentManager->TryStopLocalDeployment()) { OnShowFailedNotification(TEXT("Failed to stop local deployment!")); } }); } -void FSpatialGDKEditorToolbarModule::LaunchInspectorWebpageButtonClicked() +void FSpatialGDKEditorToolbarModule::OpenInspectorURL() { - // Get the runtime variant currently being used as this affects which Inspector to use. - FString InspectorURL; - if (GetDefault()->GetSpatialOSRuntimeVariant() == ESpatialOSRuntimeVariant::Standard) - { - InspectorURL = SpatialGDKServicesConstants::InspectorV2URL; - } - else - { - InspectorURL = SpatialGDKServicesConstants::InspectorURL; - } - FString WebError; - FPlatformProcess::LaunchURL(*InspectorURL, TEXT(""), &WebError); + FPlatformProcess::LaunchURL(*SpatialGDKServicesConstants::InspectorV2URL, TEXT(""), &WebError); if (!WebError.IsEmpty()) { FNotificationInfo Info(FText::FromString(WebError)); @@ -902,6 +935,60 @@ void FSpatialGDKEditorToolbarModule::LaunchInspectorWebpageButtonClicked() } } +void FSpatialGDKEditorToolbarModule::LaunchInspectorWebpageButtonClicked() +{ + const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); + const FString InspectorVersion = SpatialGDKEditorSettings->GetInspectorVersion(); + + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, InspectorVersion] { + if (InspectorProcess && InspectorProcess->Update()) + { + // We already have an inspector running. Just open the URL. + OpenInspectorURL(); + return; + } + + // Check for any old inspector processes that may be leftover from previous runs. Kill any we find. + SpatialCommandUtils::TryKillProcessWithName(SpatialGDKServicesConstants::InspectorExe); + + // Grab the inspector binary + if (!SpatialCommandUtils::FetchInspectorBinary(InspectorVersion, GetDefault()->IsRunningInChina())) + { + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Attempted to fetch the local inspector binary but failed!")); + OnShowFailedNotification(TEXT("Failed to fetch local inspector!")); + return; + } + + FString InspectorArgs = FString::Printf( + TEXT("--grpc_addr=%s --http_addr=%s --schema_bundle=\"%s\""), *SpatialGDKServicesConstants::InspectorGRPCAddress, + *SpatialGDKServicesConstants::InspectorHTTPAddress, *SpatialGDKServicesConstants::SchemaBundlePath); + + InspectorProcess = { *SpatialGDKServicesConstants::GetInspectorExecutablePath(InspectorVersion), *InspectorArgs, + SpatialGDKServicesConstants::SpatialOSDirectory, /*InHidden*/ true, + /*InCreatePipes*/ true }; + + FSpatialGDKServicesModule& GDKServices = FModuleManager::GetModuleChecked("SpatialGDKServices"); + TWeakPtr SpatialOutputLog = GDKServices.GetSpatialOutputLog(); + + InspectorProcess->OnOutput().BindLambda([this](const FString& Output) { + UE_LOG(LogSpatialGDKEditorToolbar, Log, TEXT("Inspector: %s"), *Output) + }); + + InspectorProcess->OnCanceled().BindLambda([this] { + if (InspectorProcess.IsSet() && InspectorProcess->GetReturnCode() != SpatialGDKServicesConstants::ExitCodeSuccess) + { + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Inspector crashed! Please check logs for more details. Exit code: %s"), + *FString::FromInt(InspectorProcess->GetReturnCode())); + OnShowFailedNotification(TEXT("Inspector crashed!")); + } + }); + + InspectorProcess->Launch(); + + OpenInspectorURL(); + }); +} + bool FSpatialGDKEditorToolbarModule::StartNativeIsVisible() const { return !GetDefault()->UsesSpatialNetworking(); @@ -914,17 +1001,19 @@ bool FSpatialGDKEditorToolbarModule::StartNativeCanExecute() const bool FSpatialGDKEditorToolbarModule::StartLocalSpatialDeploymentIsVisible() const { - return !LocalDeploymentManager->IsLocalDeploymentRunning() && GetDefault()->UsesSpatialNetworking() && GetDefault()->SpatialOSNetFlowType == ESpatialOSNetFlow::LocalDeployment; + return !LocalDeploymentManager->IsLocalDeploymentRunning() && GetDefault()->UsesSpatialNetworking() + && GetDefault()->SpatialOSNetFlowType == ESpatialOSNetFlow::LocalDeployment; } bool FSpatialGDKEditorToolbarModule::StartLocalSpatialDeploymentCanExecute() const { - return !LocalDeploymentManager->IsServiceStarting() && !LocalDeploymentManager->IsDeploymentStarting(); + return !LocalDeploymentManager->IsDeploymentStarting() && !bFetchingRuntimeBinary; } bool FSpatialGDKEditorToolbarModule::StartCloudSpatialDeploymentIsVisible() const { - return GetDefault()->UsesSpatialNetworking() && GetDefault()->SpatialOSNetFlowType == ESpatialOSNetFlow::CloudDeployment; + return GetDefault()->UsesSpatialNetworking() + && GetDefault()->SpatialOSNetFlowType == ESpatialOSNetFlow::CloudDeployment; } bool FSpatialGDKEditorToolbarModule::StartCloudSpatialDeploymentCanExecute() const @@ -938,42 +1027,41 @@ bool FSpatialGDKEditorToolbarModule::StartCloudSpatialDeploymentCanExecute() con #endif } -bool FSpatialGDKEditorToolbarModule::StopSpatialDeploymentIsVisible() const -{ - return LocalDeploymentManager->IsSpatialServiceRunning() && LocalDeploymentManager->IsLocalDeploymentRunning(); -} - -bool FSpatialGDKEditorToolbarModule::StopSpatialDeploymentCanExecute() const +bool FSpatialGDKEditorToolbarModule::LaunchInspectorWebpageCanExecute() const { - return !LocalDeploymentManager->IsDeploymentStopping(); + return !bFetchingInspectorBinary; } -bool FSpatialGDKEditorToolbarModule::StartSpatialServiceIsVisible() const -{ - const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); - - return SpatialGDKSettings->bShowSpatialServiceButton && !LocalDeploymentManager->IsSpatialServiceRunning(); -} - -bool FSpatialGDKEditorToolbarModule::StartSpatialServiceCanExecute() const +bool FSpatialGDKEditorToolbarModule::StopSpatialDeploymentIsVisible() const { - return !LocalDeploymentManager->IsServiceStarting(); + return LocalDeploymentManager->IsLocalDeploymentRunning(); } -bool FSpatialGDKEditorToolbarModule::StopSpatialServiceIsVisible() const +bool FSpatialGDKEditorToolbarModule::StopSpatialDeploymentCanExecute() const { - const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); - - return SpatialGDKSettings->bShowSpatialServiceButton && LocalDeploymentManager->IsSpatialServiceRunning(); + return !LocalDeploymentManager->IsDeploymentStopping(); } void FSpatialGDKEditorToolbarModule::OnToggleSpatialNetworking() { UGeneralProjectSettings* GeneralProjectSettings = GetMutableDefault(); - GDK_PROPERTY(Property)* SpatialNetworkingProperty = UGeneralProjectSettings::StaticClass()->FindPropertyByName(FName("bSpatialNetworking")); + GDK_PROPERTY(Property)* SpatialNetworkingProperty = + UGeneralProjectSettings::StaticClass()->FindPropertyByName(FName("bSpatialNetworking")); GeneralProjectSettings->SetUsesSpatialNetworking(!GeneralProjectSettings->UsesSpatialNetworking()); GeneralProjectSettings->UpdateSinglePropertyInConfigFile(SpatialNetworkingProperty, GeneralProjectSettings->GetDefaultConfigFilename()); + + // If Spatial networking is enabled then initialise the SpatialDebugger, otherwise destroy it + if (GeneralProjectSettings->UsesSpatialNetworking()) + { + UWorld* EditorWorld = GEditor->GetEditorWorldContext().World(); + check(EditorWorld); + InitialiseSpatialDebuggerEditor(EditorWorld); + } + else + { + DestroySpatialDebuggerEditor(); + } } bool FSpatialGDKEditorToolbarModule::OnIsSpatialNetworkingEnabled() const @@ -1032,41 +1120,32 @@ void FSpatialGDKEditorToolbarModule::CloudDeploymentClicked() bool FSpatialGDKEditorToolbarModule::IsLocalDeploymentIPEditable() { const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); - return GetDefault()->UsesSpatialNetworking() && (SpatialGDKEditorSettings->SpatialOSNetFlowType == ESpatialOSNetFlow::LocalDeployment); + return GetDefault()->UsesSpatialNetworking() + && (SpatialGDKEditorSettings->SpatialOSNetFlowType == ESpatialOSNetFlow::LocalDeployment); } bool FSpatialGDKEditorToolbarModule::AreCloudDeploymentPropertiesEditable() { const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); - return GetDefault()->UsesSpatialNetworking() && (SpatialGDKEditorSettings->SpatialOSNetFlowType == ESpatialOSNetFlow::CloudDeployment); -} - -bool FSpatialGDKEditorToolbarModule::StopSpatialServiceCanExecute() const -{ - return !LocalDeploymentManager->IsServiceStopping(); + return GetDefault()->UsesSpatialNetworking() + && (SpatialGDKEditorSettings->SpatialOSNetFlowType == ESpatialOSNetFlow::CloudDeployment); } void FSpatialGDKEditorToolbarModule::OnPropertyChanged(UObject* ObjectBeingModified, FPropertyChangedEvent& PropertyChangedEvent) { if (USpatialGDKEditorSettings* Settings = Cast(ObjectBeingModified)) { - FName PropertyName = PropertyChangedEvent.Property != nullptr - ? PropertyChangedEvent.Property->GetFName() - : NAME_None; + FName PropertyName = PropertyChangedEvent.Property != nullptr ? PropertyChangedEvent.Property->GetFName() : NAME_None; FString PropertyNameStr = PropertyName.ToString(); - if (PropertyName == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, bStopSpatialOnExit)) + if (PropertyName == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, AutoStopLocalDeployment)) { /* - * This updates our own local copy of bStopSpatialOnExit as Settings change. - * We keep the copy of the variable as all the USpatialGDKEditorSettings references get - * cleaned before all the available callbacks that IModuleInterface exposes. This means that we can't access - * this variable through its references after the engine is closed. - */ - bStopSpatialOnExit = Settings->bStopSpatialOnExit; - } - else if (PropertyName == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, bStopLocalDeploymentOnEndPIE)) - { - bStopLocalDeploymentOnEndPIE = Settings->bStopLocalDeploymentOnEndPIE; + * This updates our own local copy of AutoStopLocalDeployment as Settings change. + * We keep the copy of the variable as all the USpatialGDKEditorSettings references get + * cleaned before all the available callbacks that IModuleInterface exposes. This means that we can't access + * this variable through its references after the engine is closed. + */ + AutoStopLocalDeployment = Settings->AutoStopLocalDeployment; } else if (PropertyName == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, bAutoStartLocalDeployment)) { @@ -1076,6 +1155,26 @@ void FSpatialGDKEditorToolbarModule::OnPropertyChanged(UObject* ObjectBeingModif { LocalReceptionistProxyServerManager->TryStopReceptionistProxyServer(); } + else if (PropertyName == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, bSpatialDebuggerEditorEnabled)) + { + if (SpatialDebugger.IsValid()) + { + SpatialDebugger->EditorSpatialToggleDebugger(Settings->bSpatialDebuggerEditorEnabled); + } + } + } + if (USpatialGDKSettings* Settings = Cast(ObjectBeingModified)) + { + FName PropertyName = PropertyChangedEvent.Property != nullptr ? PropertyChangedEvent.Property->GetFName() : NAME_None; + FString PropertyNameStr = PropertyName.ToString(); + if (PropertyName == GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, bEnableMultiWorker)) + { + // Update multi-worker settings + if (SpatialDebugger.IsValid()) + { + SpatialDebugger->EditorRefreshWorkerRegions(); + } + } } } @@ -1089,23 +1188,17 @@ void FSpatialGDKEditorToolbarModule::ShowCloudDeploymentDialog() else { CloudDeploymentSettingsWindowPtr = SNew(SWindow) - .Title(LOCTEXT("CloudDeploymentConfigurationTitle", "Cloud Deployment Configuration")) - .HasCloseButton(true) - .SupportsMaximize(false) - .SupportsMinimize(false) - .SizingRule(ESizingRule::Autosized); + .Title(LOCTEXT("CloudDeploymentConfigurationTitle", "Cloud Deployment Configuration")) + .HasCloseButton(true) + .SupportsMaximize(false) + .SupportsMinimize(false) + .SizingRule(ESizingRule::Autosized); CloudDeploymentSettingsWindowPtr->SetContent( - SNew(SBox) - .WidthOverride(700.0f) - [ - SAssignNew(CloudDeploymentConfigPtr, SSpatialGDKCloudDeploymentConfiguration) - .SpatialGDKEditor(SpatialGDKEditorInstance) - .ParentWindow(CloudDeploymentSettingsWindowPtr) - ] - ); - CloudDeploymentSettingsWindowPtr->SetOnWindowClosed(FOnWindowClosed::CreateLambda([=](const TSharedRef& WindowArg) - { + SNew(SBox).WidthOverride(700.0f)[SAssignNew(CloudDeploymentConfigPtr, SSpatialGDKCloudDeploymentConfiguration) + .SpatialGDKEditor(SpatialGDKEditorInstance) + .ParentWindow(CloudDeploymentSettingsWindowPtr)]); + CloudDeploymentSettingsWindowPtr->SetOnWindowClosed(FOnWindowClosed::CreateLambda([=](const TSharedRef& WindowArg) { CloudDeploymentSettingsWindowPtr = nullptr; })); FSlateApplication::Get().AddWindow(CloudDeploymentSettingsWindowPtr.ToSharedRef()); @@ -1133,50 +1226,41 @@ void FSpatialGDKEditorToolbarModule::GenerateSchema(bool bFullScan) { LocalDeploymentManager->SetRedeployRequired(); - bSchemaBuildError = false; + const bool bFullScanRequired = SpatialGDKEditorInstance->FullScanRequired(); - if (SpatialGDKEditorInstance->FullScanRequired()) + FSpatialGDKEditor::ESchemaGenerationMethod GenerationMethod; + FString OnTaskStartMessage; + FString OnTaskCompleteMessage; + FString OnTaskFailMessage; + if (bFullScanRequired || bFullScan) { - OnShowTaskStartNotification("Initial Schema Generation"); - - if (SpatialGDKEditorInstance->GenerateSchema(FSpatialGDKEditor::FullAssetScan)) - { - OnShowSuccessNotification("Initial Schema Generation completed!"); - } - else - { - OnShowFailedNotification("Initial Schema Generation failed"); - bSchemaBuildError = true; - } - } - else if (bFullScan) - { - OnShowTaskStartNotification("Generating Schema (Full)"); - - if (SpatialGDKEditorInstance->GenerateSchema(FSpatialGDKEditor::FullAssetScan)) - { - OnShowSuccessNotification("Full Schema Generation completed!"); - } - else - { - OnShowFailedNotification("Full Schema Generation failed"); - bSchemaBuildError = true; - } + GenerationMethod = FSpatialGDKEditor::FullAssetScan; + const TCHAR* RequiredStr = bFullScanRequired ? TEXT(" required") : TEXT(""); + OnTaskStartMessage = FString::Printf(TEXT("Generating schema (full scan%s)"), RequiredStr); + OnTaskCompleteMessage = TEXT("Full schema generation complete"); + OnTaskFailMessage = TEXT("Full schema generation failed"); } else { - OnShowTaskStartNotification("Generating Schema (Incremental)"); + GenerationMethod = FSpatialGDKEditor::InMemoryAsset; + OnTaskStartMessage = TEXT("Generating schema (incremental)"); + OnTaskCompleteMessage = TEXT("Incremental schema generation completed!"); + OnTaskFailMessage = TEXT("Incremental schema generation failed"); + } - if (SpatialGDKEditorInstance->GenerateSchema(FSpatialGDKEditor::InMemoryAsset)) + OnShowTaskStartNotification(OnTaskStartMessage); + SpatialGDKEditorInstance->GenerateSchema(GenerationMethod, [this, OnTaskCompleteMessage = MoveTemp(OnTaskCompleteMessage), + OnTaskFailMessage = MoveTemp(OnTaskFailMessage)](bool bResult) { + if (bResult) { - OnShowSuccessNotification("Incremental Schema Generation completed!"); + OnShowSuccessNotification(OnTaskCompleteMessage); } else { - OnShowFailedNotification("Incremental Schema Generation failed"); - bSchemaBuildError = true; + OnShowFailedNotification(OnTaskFailMessage); } - } + }); + ; } bool FSpatialGDKEditorToolbarModule::IsSnapshotGenerated() const @@ -1203,7 +1287,8 @@ void FSpatialGDKEditorToolbarModule::OnAutoStartLocalDeploymentChanged() const USpatialGDKEditorSettings* Settings = GetDefault(); // Only auto start local deployment when the setting is checked AND local deployment connection flow is selected. - bool bShouldAutoStartLocalDeployment = (Settings->bAutoStartLocalDeployment && Settings->SpatialOSNetFlowType == ESpatialOSNetFlow::LocalDeployment); + bool bShouldAutoStartLocalDeployment = + (Settings->bAutoStartLocalDeployment && Settings->SpatialOSNetFlowType == ESpatialOSNetFlow::LocalDeployment); // TODO: UNR-1776 Workaround for SpatialNetDriver requiring editor settings. LocalDeploymentManager->SetAutoDeploy(bShouldAutoStartLocalDeployment); @@ -1213,11 +1298,11 @@ void FSpatialGDKEditorToolbarModule::OnAutoStartLocalDeploymentChanged() if (!UEditorEngine::TryStartSpatialDeployment.IsBound()) { // Bind the TryStartSpatialDeployment delegate if autostart is enabled. - UEditorEngine::TryStartSpatialDeployment.BindLambda([this] - { - if (GetDefault()->UsesSpatialNetworking()) + UEditorEngine::TryStartSpatialDeployment.BindLambda([this](FString ForceSnapshot) { + if (GetDefault()->bAutoStartLocalDeployment + && GetDefault()->UsesSpatialNetworking()) { - VerifyAndStartDeployment(); + VerifyAndStartDeployment(ForceSnapshot); } }); } @@ -1232,20 +1317,21 @@ void FSpatialGDKEditorToolbarModule::OnAutoStartLocalDeploymentChanged() } } -void FSpatialGDKEditorToolbarModule::GenerateConfigFromCurrentMap() +void FSpatialGDKEditorToolbarModule::GenerateCloudConfigFromCurrentMap() { USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetMutableDefault(); UWorld* EditorWorld = GEditor->GetEditorWorldContext().World(); check(EditorWorld != nullptr); - const FString LaunchConfig = FPaths::Combine(FPaths::ConvertRelativePathToFull(FPaths::ProjectIntermediateDir()), FString::Printf(TEXT("Improbable/%s_CloudLaunchConfig.json"), *EditorWorld->GetMapName())); + const FString LaunchConfig = FPaths::Combine(FPaths::ConvertRelativePathToFull(FPaths::ProjectIntermediateDir()), + FString::Printf(TEXT("Improbable/%s_CloudLaunchConfig.json"), *EditorWorld->GetMapName())); FSpatialLaunchConfigDescription LaunchConfiguration = SpatialGDKEditorSettings->LaunchConfigDesc; - FWorkerTypeLaunchSection& ServerWorkerConfig = LaunchConfiguration.ServerWorkerConfig; - ServerWorkerConfig.NumEditorInstances = GetWorkerCountFromWorldSettings(*EditorWorld); - GenerateLaunchConfig(LaunchConfig, &LaunchConfiguration, ServerWorkerConfig); + LaunchConfiguration.ServerWorkerConfiguration.NumEditorInstances = GetWorkerCountFromWorldSettings(*EditorWorld, true); + + GenerateLaunchConfig(LaunchConfig, &LaunchConfiguration, /*bGenerateCloudConfig*/ true); SpatialGDKEditorSettings->SetPrimaryLaunchConfigPath(LaunchConfig); } @@ -1263,7 +1349,14 @@ FReply FSpatialGDKEditorToolbarModule::OnStartCloudDeployment() if (SpatialGDKSettings->ShouldAutoGenerateCloudLaunchConfig()) { - GenerateConfigFromCurrentMap(); + GenerateCloudConfigFromCurrentMap(); + } + + if (!SpatialGDKSettings->CheckManualWorkerConnectionOnLaunch()) + { + OnShowFailedNotification(TEXT("Launch halted because of unexpected workers requiring manual launch.")); + + return FReply::Unhandled(); } AddDeploymentTagIfMissing(SpatialConstants::DEV_LOGIN_TAG); @@ -1279,12 +1372,22 @@ FReply FSpatialGDKEditorToolbarModule::OnStartCloudDeployment() { if (SpatialGDKEditorInstance->FullScanRequired()) { - FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("FullSchemaGenRequired_Prompt", "A full schema generation is required at least once before you can start a cloud deployment. Press the Schema button before starting a cloud deployment.")); + FMessageDialog::Open(EAppMsgType::Ok, + LOCTEXT("FullSchemaGenRequired_Prompt", + "A full schema generation is required at least once before you can start a cloud deployment. " + "Press the Schema button before starting a cloud deployment.")); OnShowSingleFailureNotification(TEXT("Generate schema failed.")); return FReply::Unhandled(); } - if (!SpatialGDKEditorInstance->GenerateSchema(FSpatialGDKEditor::InMemoryAsset)) + bool bHasResult{ false }; + bool bResult{ false }; + SpatialGDKEditorInstance->GenerateSchema(FSpatialGDKEditor::InMemoryAsset, [&bHasResult, &bResult](bool bTaskResult) { + bResult = bTaskResult; + bHasResult = true; + }); + checkf(bHasResult, TEXT("Result is expected to be returned synchronously.")); + if (!bResult) { OnShowSingleFailureNotification(TEXT("Generate schema failed.")); return FReply::Unhandled(); @@ -1300,6 +1403,11 @@ FReply FSpatialGDKEditorToolbarModule::OnStartCloudDeployment() } } +#if ENGINE_MINOR_VERSION >= 26 + FGlobalTabmanager::Get()->TryInvokeTab(FName(TEXT("OutputLog"))); +#else + FGlobalTabmanager::Get()->InvokeTab(FName(TEXT("OutputLog"))); +#endif TSharedRef PackageAssembly = SpatialGDKEditorInstance->GetPackageAssemblyRef(); PackageAssembly->OnSuccess.BindRaw(this, &FSpatialGDKEditorToolbarModule::OnBuildSuccess); PackageAssembly->BuildAndUploadAssembly(CloudDeploymentConfiguration); @@ -1317,43 +1425,41 @@ void FSpatialGDKEditorToolbarModule::OnBuildSuccess() { bStartingCloudDeployment = true; - auto StartCloudDeployment = [this]() - { - OnShowTaskStartNotification(FString::Printf(TEXT("Starting cloud deployment: %s"), *CloudDeploymentConfiguration.PrimaryDeploymentName)); + auto StartCloudDeployment = [this]() { + OnShowTaskStartNotification( + FString::Printf(TEXT("Starting cloud deployment: %s"), *CloudDeploymentConfiguration.PrimaryDeploymentName)); SpatialGDKEditorInstance->StartCloudDeployment( - CloudDeploymentConfiguration, - FSimpleDelegate::CreateLambda([this]() - { + CloudDeploymentConfiguration, FSimpleDelegate::CreateLambda([this]() { OnStartCloudDeploymentFinished(); OnShowSuccessNotification("Successfully started cloud deployment."); }), - FSimpleDelegate::CreateLambda([this]() - { + FSimpleDelegate::CreateLambda([this]() { OnStartCloudDeploymentFinished(); OnShowFailedNotification("Failed to start cloud deployment. See output logs for details."); - }) - ); + })); }; - AttemptSpatialAuthResult = Async(EAsyncExecution::Thread, []() { return SpatialCommandUtils::AttemptSpatialAuth(GetDefault()->IsRunningInChina()); }, - [this, StartCloudDeployment]() - { - if (AttemptSpatialAuthResult.IsReady() && AttemptSpatialAuthResult.Get() == true) - { - StartCloudDeployment(); - } - else - { - OnStartCloudDeploymentFinished(); - OnShowFailedNotification(TEXT("Failed to launch cloud deployment. Unable to authenticate with SpatialOS.")); - } - }); + AttemptSpatialAuthResult = Async( + EAsyncExecution::Thread, + []() { + return SpatialCommandUtils::AttemptSpatialAuth(GetDefault()->IsRunningInChina()); + }, + [this, StartCloudDeployment]() { + if (AttemptSpatialAuthResult.IsReady() && AttemptSpatialAuthResult.Get() == true) + { + StartCloudDeployment(); + } + else + { + OnStartCloudDeploymentFinished(); + OnShowFailedNotification(TEXT("Failed to launch cloud deployment. Unable to authenticate with SpatialOS.")); + } + }); } void FSpatialGDKEditorToolbarModule::OnStartCloudDeploymentFinished() { - AsyncTask(ENamedThreads::GameThread, [this] - { + AsyncTask(ENamedThreads::GameThread, [this] { bStartingCloudDeployment = false; }); } @@ -1361,11 +1467,9 @@ void FSpatialGDKEditorToolbarModule::OnStartCloudDeploymentFinished() bool FSpatialGDKEditorToolbarModule::IsDeploymentConfigurationValid() const { const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); - return !FSpatialGDKServicesModule::GetProjectName().IsEmpty() - && !SpatialGDKSettings->GetPrimaryDeploymentName().IsEmpty() - && !SpatialGDKSettings->GetAssemblyName().IsEmpty() - && !SpatialGDKSettings->GetSnapshotPath().IsEmpty() - && (!SpatialGDKSettings->GetPrimaryLaunchConfigPath().IsEmpty() || SpatialGDKSettings->ShouldAutoGenerateCloudLaunchConfig()); + return !FSpatialGDKServicesModule::GetProjectName().IsEmpty() && !SpatialGDKSettings->GetPrimaryDeploymentName().IsEmpty() + && !SpatialGDKSettings->GetAssemblyName().IsEmpty() && !SpatialGDKSettings->GetSnapshotPath().IsEmpty() + && (!SpatialGDKSettings->GetPrimaryLaunchConfigPath().IsEmpty() || SpatialGDKSettings->ShouldAutoGenerateCloudLaunchConfig()); } bool FSpatialGDKEditorToolbarModule::CanBuildAndUpload() const @@ -1393,6 +1497,48 @@ bool FSpatialGDKEditorToolbarModule::IsBuildClientWorkerEnabled() const return GetDefault()->IsBuildClientWorkerEnabled(); } +void FSpatialGDKEditorToolbarModule::DestroySpatialDebuggerEditor() +{ + if (SpatialDebugger.IsValid()) + { + SpatialDebugger->Destroy(); + SpatialDebugger = nullptr; + ASpatialDebugger::EditorRefreshDisplay(); + } +} + +void FSpatialGDKEditorToolbarModule::InitialiseSpatialDebuggerEditor(UWorld* World) +{ + const USpatialGDKSettings* SpatialSettings = GetDefault(); + + if (SpatialSettings->SpatialDebugger != nullptr) + { + // If spatial debugger set then create the SpatialDebugger for this map to be used in the editor + FActorSpawnParameters SpawnParameters; + SpawnParameters.bHideFromSceneOutliner = true; + SpatialDebugger = World->SpawnActor(SpatialSettings->SpatialDebugger, SpawnParameters); + const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); + SpatialDebugger->EditorSpatialToggleDebugger(SpatialGDKEditorSettings->bSpatialDebuggerEditorEnabled); + } +} + +bool FSpatialGDKEditorToolbarModule::IsSpatialDebuggerEditorEnabled() const +{ + const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); + return AllowWorkerBoundaries() && SpatialGDKEditorSettings->bSpatialDebuggerEditorEnabled; +} + +bool FSpatialGDKEditorToolbarModule::IsMultiWorkerEnabled() const +{ + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + return SpatialGDKSettings->bEnableMultiWorker; +} + +bool FSpatialGDKEditorToolbarModule::AllowWorkerBoundaries() const +{ + return SpatialDebugger.IsValid() && SpatialDebugger->EditorAllowWorkerBoundaries(); +} + void FSpatialGDKEditorToolbarModule::OnCheckedBuildClientWorker() { GetMutableDefault()->SetBuildClientWorker(!IsBuildClientWorkerEnabled()); diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp index 6c03b4f9e0..58f53bc9c5 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp @@ -6,26 +6,51 @@ void FSpatialGDKEditorToolbarCommands::RegisterCommands() { - UI_COMMAND(CreateSpatialGDKSchema, "Schema", "Creates SpatialOS Unreal GDK schema for assets in memory.", EUserInterfaceActionType::Button, FInputGesture()); - UI_COMMAND(CreateSpatialGDKSchemaFull, "Schema (Full Scan)", "Creates SpatialOS Unreal GDK schema for all assets.", EUserInterfaceActionType::Button, FInputGesture()); - UI_COMMAND(DeleteSchemaDatabase, "Delete schema database", "Deletes the schema database file", EUserInterfaceActionType::Button, FInputGesture()); - UI_COMMAND(CreateSpatialGDKSnapshot, "Snapshot", "Creates SpatialOS Unreal GDK snapshot.", EUserInterfaceActionType::Button, FInputGesture()); + UI_COMMAND(CreateSpatialGDKSchema, "Schema", "Creates SpatialOS Unreal GDK schema for assets in memory.", + EUserInterfaceActionType::Button, FInputGesture()); + UI_COMMAND(CreateSpatialGDKSchemaFull, "Schema (Full Scan)", "Creates SpatialOS Unreal GDK schema for all assets.", + EUserInterfaceActionType::Button, FInputGesture()); + UI_COMMAND(DeleteSchemaDatabase, "Delete schema database", "Deletes the schema database file", EUserInterfaceActionType::Button, + FInputGesture()); + UI_COMMAND(CreateSpatialGDKSnapshot, "Snapshot", "Creates SpatialOS Unreal GDK snapshot.", EUserInterfaceActionType::Button, + FInputGesture()); UI_COMMAND(StartNative, "Start Deployment", "Use native Unreal networking", EUserInterfaceActionType::Button, FInputGesture()); - UI_COMMAND(StartLocalSpatialDeployment, "Start Deployment", "Start a local deployment", EUserInterfaceActionType::Button, FInputGesture()); - UI_COMMAND(StartCloudSpatialDeployment, "Start Deployment", "Start a cloud deployment (Not available for macOS)", EUserInterfaceActionType::Button, FInputGesture()); + UI_COMMAND(StartLocalSpatialDeployment, "Start Deployment", "Start a local deployment", EUserInterfaceActionType::Button, + FInputGesture()); + UI_COMMAND(StartCloudSpatialDeployment, "Start Deployment", "Start a cloud deployment (Not available for macOS)", + EUserInterfaceActionType::Button, FInputGesture()); UI_COMMAND(StopSpatialDeployment, "Stop Deployment", "Stops SpatialOS.", EUserInterfaceActionType::Button, FInputGesture()); - UI_COMMAND(LaunchInspectorWebPageAction, "Inspector", "Launches default web browser to SpatialOS Inspector.", EUserInterfaceActionType::Button, FInputGesture()); - UI_COMMAND(OpenCloudDeploymentWindowAction, "Cloud", "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(EnableBuildClientWorker, "Build Client Worker", "If checked, an UnrealClient worker will be built and uploaded before launching the cloud deployment.", EUserInterfaceActionType::ToggleButton, FInputChord()); - UI_COMMAND(EnableBuildSimulatedPlayer, "Build Simulated Player", "If checked, a SimulatedPlayer worker will be built and uploaded before launching the cloud deployment.", EUserInterfaceActionType::ToggleButton, FInputChord()); - UI_COMMAND(StartSpatialService, "Start Service", "Starts the Spatial service daemon.", EUserInterfaceActionType::Button, FInputGesture()); + UI_COMMAND(LaunchInspectorWebPageAction, "Inspector", "Launches default web browser to SpatialOS Inspector.", + EUserInterfaceActionType::Button, FInputGesture()); + UI_COMMAND(OpenCloudDeploymentWindowAction, "Cloud", "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(EnableBuildClientWorker, "Build Client Worker", + "If checked, an UnrealClient worker will be built and uploaded before launching the cloud deployment.", + EUserInterfaceActionType::ToggleButton, FInputChord()); + UI_COMMAND(EnableBuildSimulatedPlayer, "Build Simulated Player", + "If checked, a SimulatedPlayer worker will be built and uploaded before launching the cloud deployment.", + EUserInterfaceActionType::ToggleButton, FInputChord()); + 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()); - UI_COMMAND(EnableSpatialNetworking, "SpatialOS Networking", "If checked, the SpatialOS networking is used. Otherwise, native Unreal networking is used.", EUserInterfaceActionType::ToggleButton, FInputChord()); - UI_COMMAND(GDKEditorSettings, "Editor Settings", "Open the SpatialOS GDK Editor Settings", EUserInterfaceActionType::Button, FInputChord()); - UI_COMMAND(GDKRuntimeSettings, "Runtime Settings", "Open the SpatialOS GDK Runtime Settings", EUserInterfaceActionType::Button, FInputChord()); - UI_COMMAND(LocalDeployment, "Connect to a local deployment", "Automatically connect to a local deployment", EUserInterfaceActionType::RadioButton, FInputChord()); - UI_COMMAND(CloudDeployment, "Connect to a cloud deployment", "Automatically connect to a cloud deployment", EUserInterfaceActionType::RadioButton, FInputChord()); + UI_COMMAND(ToggleSpatialDebuggerEditor, "Spatial Editor Debugger", "Show worker boundaries in editor", + EUserInterfaceActionType::ToggleButton, FInputChord()); + UI_COMMAND(ToggleMultiWorkerEditor, "Enable Multi-Worker", + "If checked, multi-worker is enabled. Otherwise, a single worker strategy is used in the editor", + EUserInterfaceActionType::ToggleButton, FInputChord()); + UI_COMMAND(EnableSpatialNetworking, "SpatialOS Networking", + "If checked, the SpatialOS networking is used. Otherwise, native Unreal networking is used.", + EUserInterfaceActionType::ToggleButton, FInputChord()); + UI_COMMAND(GDKEditorSettings, "Editor Settings", "Open the SpatialOS GDK Editor Settings", EUserInterfaceActionType::Button, + FInputChord()); + UI_COMMAND(GDKRuntimeSettings, "Runtime Settings", "Open the SpatialOS GDK Runtime Settings", EUserInterfaceActionType::Button, + FInputChord()); + UI_COMMAND(LocalDeployment, "Connect to a local deployment", "Automatically connect to a local deployment", + EUserInterfaceActionType::RadioButton, FInputChord()); + UI_COMMAND(CloudDeployment, "Connect to a cloud deployment", "Automatically connect to a cloud deployment", + EUserInterfaceActionType::RadioButton, FInputChord()); } #undef LOCTEXT_NAMESPACE diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarStyle.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarStyle.cpp index dce343adb6..0a07e320d5 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarStyle.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarStyle.cpp @@ -31,8 +31,7 @@ FName FSpatialGDKEditorToolbarStyle::GetStyleSetName() return StyleSetName; } -#define IMAGE_BRUSH(RelativePath, ...) \ - FSlateImageBrush(Style->RootToContentDir(RelativePath, TEXT(".png")), __VA_ARGS__) +#define IMAGE_BRUSH(RelativePath, ...) FSlateImageBrush(Style->RootToContentDir(RelativePath, TEXT(".png")), __VA_ARGS__) namespace { @@ -40,77 +39,54 @@ const FVector2D Icon16x16(16.0f, 16.0f); const FVector2D Icon20x20(20.0f, 20.0f); const FVector2D Icon40x40(40.0f, 40.0f); const FVector2D Icon100x22(100.0f, 22.0f); -} +} // namespace TSharedRef FSpatialGDKEditorToolbarStyle::Create() { - TSharedRef Style = - MakeShareable(new FSlateStyleSet("SpatialGDKEditorToolbarStyle")); - Style->SetContentRoot(IPluginManager::Get().FindPlugin("SpatialGDK")->GetBaseDir() / - TEXT("Resources")); + TSharedRef Style = MakeShareable(new FSlateStyleSet("SpatialGDKEditorToolbarStyle")); + Style->SetContentRoot(IPluginManager::Get().FindPlugin("SpatialGDK")->GetBaseDir() / TEXT("Resources")); + + Style->Set("SpatialGDKEditorToolbar.CreateSpatialGDKSnapshot", new IMAGE_BRUSH(TEXT("Snapshot"), Icon40x40)); + + Style->Set("SpatialGDKEditorToolbar.CreateSpatialGDKSnapshot.Small", new IMAGE_BRUSH(TEXT("Snapshot@0.5x"), Icon20x20)); + + Style->Set("SpatialGDKEditorToolbar.CreateSpatialGDKSchema", new IMAGE_BRUSH(TEXT("Schema"), Icon40x40)); - Style->Set("SpatialGDKEditorToolbar.CreateSpatialGDKSnapshot", - new IMAGE_BRUSH(TEXT("Snapshot"), Icon40x40)); + Style->Set("SpatialGDKEditorToolbar.CreateSpatialGDKSchema.Small", new IMAGE_BRUSH(TEXT("Schema@0.5x"), Icon20x20)); - Style->Set("SpatialGDKEditorToolbar.CreateSpatialGDKSnapshot.Small", - new IMAGE_BRUSH(TEXT("Snapshot@0.5x"), Icon20x20)); + Style->Set("SpatialGDKEditorToolbar.StartNative", new IMAGE_BRUSH(TEXT("None"), Icon40x40)); - Style->Set("SpatialGDKEditorToolbar.CreateSpatialGDKSchema", - new IMAGE_BRUSH(TEXT("Schema"), Icon40x40)); + Style->Set("SpatialGDKEditorToolbar.StartNative.Small", new IMAGE_BRUSH(TEXT("None@0.5x"), Icon20x20)); - Style->Set("SpatialGDKEditorToolbar.CreateSpatialGDKSchema.Small", - new IMAGE_BRUSH(TEXT("Schema@0.5x"), Icon20x20)); + Style->Set("SpatialGDKEditorToolbar.StartLocalSpatialDeployment", new IMAGE_BRUSH(TEXT("StartLocal"), Icon40x40)); - Style->Set("SpatialGDKEditorToolbar.StartNative", - new IMAGE_BRUSH(TEXT("None"), Icon40x40)); + Style->Set("SpatialGDKEditorToolbar.StartLocalSpatialDeployment.Small", new IMAGE_BRUSH(TEXT("StartLocal@0.5x"), Icon20x20)); - Style->Set("SpatialGDKEditorToolbar.StartNative.Small", - new IMAGE_BRUSH(TEXT("None@0.5x"), Icon20x20)); + Style->Set("SpatialGDKEditorToolbar.StartCloudSpatialDeployment", new IMAGE_BRUSH(TEXT("StartCloud"), Icon40x40)); - Style->Set("SpatialGDKEditorToolbar.StartLocalSpatialDeployment", - new IMAGE_BRUSH(TEXT("StartLocal"), Icon40x40)); + Style->Set("SpatialGDKEditorToolbar.StartCloudSpatialDeployment.Small", new IMAGE_BRUSH(TEXT("StartCloud@0.5x"), Icon20x20)); - Style->Set("SpatialGDKEditorToolbar.StartLocalSpatialDeployment.Small", - new IMAGE_BRUSH(TEXT("StartLocal@0.5x"), Icon20x20)); + Style->Set("SpatialGDKEditorToolbar.StopSpatialDeployment", new IMAGE_BRUSH(TEXT("StopLocal"), Icon40x40)); - Style->Set("SpatialGDKEditorToolbar.StartCloudSpatialDeployment", - new IMAGE_BRUSH(TEXT("StartCloud"), Icon40x40)); + Style->Set("SpatialGDKEditorToolbar.StopSpatialDeployment.Small", new IMAGE_BRUSH(TEXT("StopLocal@0.5x"), Icon20x20)); - Style->Set("SpatialGDKEditorToolbar.StartCloudSpatialDeployment.Small", - new IMAGE_BRUSH(TEXT("StartCloud@0.5x"), Icon20x20)); - - Style->Set("SpatialGDKEditorToolbar.StopSpatialDeployment", - new IMAGE_BRUSH(TEXT("StopLocal"), Icon40x40)); + Style->Set("SpatialGDKEditorToolbar.LaunchInspectorWebPageAction", new IMAGE_BRUSH(TEXT("Inspector"), Icon40x40)); - Style->Set("SpatialGDKEditorToolbar.StopSpatialDeployment.Small", - new IMAGE_BRUSH(TEXT("StopLocal@0.5x"), Icon20x20)); - - Style->Set("SpatialGDKEditorToolbar.LaunchInspectorWebPageAction", - new IMAGE_BRUSH(TEXT("Inspector"), Icon40x40)); - - Style->Set("SpatialGDKEditorToolbar.LaunchInspectorWebPageAction.Small", - new IMAGE_BRUSH(TEXT("Inspector@0.5x"), Icon20x20)); + Style->Set("SpatialGDKEditorToolbar.LaunchInspectorWebPageAction.Small", new IMAGE_BRUSH(TEXT("Inspector@0.5x"), Icon20x20)); - Style->Set("SpatialGDKEditorToolbar.OpenCloudDeploymentWindowAction", - new IMAGE_BRUSH(TEXT("Cloud"), Icon40x40)); + Style->Set("SpatialGDKEditorToolbar.OpenCloudDeploymentWindowAction", new IMAGE_BRUSH(TEXT("Cloud"), Icon40x40)); - Style->Set("SpatialGDKEditorToolbar.OpenCloudDeploymentWindowAction.Small", - new IMAGE_BRUSH(TEXT("Cloud@0.5x"), Icon20x20)); + Style->Set("SpatialGDKEditorToolbar.OpenCloudDeploymentWindowAction.Small", new IMAGE_BRUSH(TEXT("Cloud@0.5x"), Icon20x20)); - Style->Set("SpatialGDKEditorToolbar.StartSpatialService", - new IMAGE_BRUSH(TEXT("StartLocal"), Icon40x40)); + Style->Set("SpatialGDKEditorToolbar.StartSpatialService", new IMAGE_BRUSH(TEXT("StartLocal"), Icon40x40)); - Style->Set("SpatialGDKEditorToolbar.StartSpatialService.Small", - new IMAGE_BRUSH(TEXT("StartLocal@0.5x"), Icon20x20)); + Style->Set("SpatialGDKEditorToolbar.StartSpatialService.Small", new IMAGE_BRUSH(TEXT("StartLocal@0.5x"), Icon20x20)); - Style->Set("SpatialGDKEditorToolbar.StopSpatialService", - new IMAGE_BRUSH(TEXT("StopLocal"), Icon40x40)); + Style->Set("SpatialGDKEditorToolbar.StopSpatialService", new IMAGE_BRUSH(TEXT("StopLocal"), Icon40x40)); - Style->Set("SpatialGDKEditorToolbar.StopSpatialService.Small", - new IMAGE_BRUSH(TEXT("StopLocal@0.5x"), Icon20x20)); + Style->Set("SpatialGDKEditorToolbar.StopSpatialService.Small", new IMAGE_BRUSH(TEXT("StopLocal@0.5x"), Icon20x20)); - Style->Set("SpatialGDKEditorToolbar.SpatialOSLogo", - new IMAGE_BRUSH(TEXT("SPATIALOS_LOGO_WHITE"), Icon100x22)); + Style->Set("SpatialGDKEditorToolbar.SpatialOSLogo", new IMAGE_BRUSH(TEXT("SPATIALOS_LOGO_WHITE"), Icon100x22)); return Style; } diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKCloudDeploymentConfiguration.h b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKCloudDeploymentConfiguration.h index d436b6dcb6..bd175ea57a 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKCloudDeploymentConfiguration.h +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKCloudDeploymentConfiguration.h @@ -22,7 +22,6 @@ enum class ECheckBoxState : uint8; class SSpatialGDKCloudDeploymentConfiguration : public SCompoundWidget { public: - SLATE_BEGIN_ARGS(SSpatialGDKCloudDeploymentConfiguration) {} /** A reference to the parent window */ @@ -32,11 +31,9 @@ class SSpatialGDKCloudDeploymentConfiguration : public SCompoundWidget SLATE_END_ARGS() public: - void Construct(const FArguments& InArgs); private: - /** The parent window of this widget */ TWeakPtr ParentWindowPtr; @@ -47,6 +44,7 @@ class SSpatialGDKCloudDeploymentConfiguration : public SCompoundWidget TSharedPtr ProjectNameInputErrorReporting; TSharedPtr AssemblyNameInputErrorReporting; TSharedPtr DeploymentNameInputErrorReporting; + TSharedPtr SimulatedPlayersDeploymentNameInputErrorReporting; /** Delegate to commit project name */ void OnProjectNameCommitted(const FText& InText, ETextCommit::Type InCommitType); diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h index dad03fcd66..42be8f9ba0 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h @@ -15,6 +15,7 @@ #include "CloudDeploymentConfiguration.h" #include "LocalDeploymentManager.h" #include "LocalReceptionistProxyServerManager.h" +#include "SpatialGDKEditorSettings.h" class FMenuBuilder; class FSpatialGDKEditor; @@ -26,6 +27,7 @@ class USoundBase; struct FWorkerTypeLaunchSection; class UAbstractRuntimeLoadBalancingStrategy; +class ASpatialDebugger; DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKEditorToolbar, Log, All); @@ -40,15 +42,9 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable /** FTickableEditorObject interface */ void Tick(float DeltaTime) override; - bool IsTickable() const override - { - return true; - } + bool IsTickable() const override { return true; } - TStatId GetStatId() const override - { - RETURN_QUICK_DECLARE_CYCLE_STAT(FSpatialGDKEditorToolbarModule, STATGROUP_Tickables); - } + TStatId GetStatId() const override { RETURN_QUICK_DECLARE_CYCLE_STAT(FSpatialGDKEditorToolbarModule, STATGROUP_Tickables); } void OnShowSingleFailureNotification(const FString& NotificationText); void OnShowSuccessNotification(const FString& NotificationText); @@ -71,14 +67,23 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable void AddToolbarExtension(FToolBarBuilder& Builder); void AddMenuExtension(FMenuBuilder& Builder); - void VerifyAndStartDeployment(); + bool FetchRuntimeBinaryWrapper(FString RuntimeVersion); + bool FetchInspectorBinaryWrapper(FString InspectorVersion); + + void VerifyAndStartDeployment(FString ForceSnapshot = ""); void StartLocalSpatialDeploymentButtonClicked(); void StopSpatialDeploymentButtonClicked(); - void StartSpatialServiceButtonClicked(); - void StopSpatialServiceButtonClicked(); + void MapChanged(UWorld* World, EMapChangeType MapChangeType); + void DestroySpatialDebuggerEditor(); + void InitialiseSpatialDebuggerEditor(UWorld* World); + bool IsSpatialDebuggerEditorEnabled() const; + bool IsMultiWorkerEnabled() const; + bool AllowWorkerBoundaries() const; + void ToggleSpatialDebuggerEditor(); + void ToggleMultiworkerEditor(); bool StartNativeIsVisible() const; bool StartNativeCanExecute() const; @@ -88,15 +93,11 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable bool StartCloudSpatialDeploymentIsVisible() const; bool StartCloudSpatialDeploymentCanExecute() const; + bool LaunchInspectorWebpageCanExecute() const; + bool StopSpatialDeploymentIsVisible() const; bool StopSpatialDeploymentCanExecute() const; - bool StartSpatialServiceIsVisible() const; - bool StartSpatialServiceCanExecute() const; - - bool StopSpatialServiceIsVisible() const; - bool StopSpatialServiceCanExecute() const; - void OnToggleSpatialNetworking(); bool OnIsSpatialNetworkingEnabled() const; @@ -114,6 +115,7 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable static bool IsLocalDeploymentIPEditable(); static bool AreCloudDeploymentPropertiesEditable(); + void OpenInspectorURL(); void LaunchInspectorWebpageButtonClicked(); void CreateSnapshotButtonClicked(); void SchemaGenerateButtonClicked(); @@ -143,7 +145,8 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable TSharedRef CreateStartDropDownMenuContent(); using IsEnabledFunc = bool(); - TSharedRef CreateBetterEditableTextWidget(const FText& Label, const FText& Text, FOnTextCommitted::TFuncType OnTextCommitted, IsEnabledFunc IsEnabled); + TSharedRef CreateBetterEditableTextWidget(const FText& Label, const FText& Text, FOnTextCommitted::TFuncType OnTextCommitted, + IsEnabledFunc IsEnabled); void ShowSingleFailureNotification(const FString& NotificationText); void ShowTaskStartNotification(const FString& NotificationText); @@ -163,10 +166,7 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable TSharedPtr PluginCommands; FDelegateHandle OnPropertyChangedDelegateHandle; - bool bStopSpatialOnExit; - bool bStopLocalDeploymentOnEndPIE; - - bool bSchemaBuildError; + EAutoStopLocalDeploymentMode AutoStopLocalDeployment; TWeakPtr TaskNotificationPtr; @@ -180,7 +180,7 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable TSharedPtr CloudDeploymentSettingsWindowPtr; TSharedPtr CloudDeploymentConfigPtr; - + FLocalDeploymentManager* LocalDeploymentManager; FLocalReceptionistProxyServerManager* LocalReceptionistProxyServerManager; @@ -189,6 +189,13 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable FCloudDeploymentConfiguration CloudDeploymentConfiguration; bool bStartingCloudDeployment; + bool bFetchingRuntimeBinary; + bool bFetchingInspectorBinary; + + void GenerateCloudConfigFromCurrentMap(); + + // Used to show worker boundaries in the editor + TWeakObjectPtr SpatialDebugger; - void GenerateConfigFromCurrentMap(); + TOptional InspectorProcess = {}; }; diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h index 85559b4d95..3d07c62e7a 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h @@ -10,10 +10,9 @@ class FSpatialGDKEditorToolbarCommands : public TCommands( - TEXT("SpatialGDKEditorToolbar"), - NSLOCTEXT("Contexts", "SpatialGDKEditorToolbar", "SpatialGDKEditorToolbar Plugin"), NAME_None, - FSpatialGDKEditorToolbarStyle::GetStyleSetName()) + : TCommands(TEXT("SpatialGDKEditorToolbar"), + NSLOCTEXT("Contexts", "SpatialGDKEditorToolbar", "SpatialGDKEditorToolbar Plugin"), + NAME_None, FSpatialGDKEditorToolbarStyle::GetStyleSetName()) { } @@ -29,7 +28,7 @@ class FSpatialGDKEditorToolbarCommands : public TCommands StartCloudSpatialDeployment; TSharedPtr StopSpatialDeployment; TSharedPtr LaunchInspectorWebPageAction; - + TSharedPtr OpenCloudDeploymentWindowAction; TSharedPtr OpenLaunchConfigurationEditorAction; TSharedPtr EnableBuildClientWorker; @@ -42,4 +41,6 @@ class FSpatialGDKEditorToolbarCommands : public TCommands GDKRuntimeSettings; TSharedPtr LocalDeployment; TSharedPtr CloudDeployment; + TSharedPtr ToggleSpatialDebuggerEditor; + TSharedPtr ToggleMultiWorkerEditor; }; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTest.cpp index 2041835e6b..c32e64e83f 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTest.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTest.cpp @@ -2,17 +2,34 @@ #include "SpatialFunctionalTest.h" +#include "AutomationBlueprintFunctionLibrary.h" +#include "Engine/Engine.h" #include "Engine/World.h" +#include "EngineClasses/SpatialNetDriver.h" +#include "EngineClasses/SpatialNetDriverDebugContext.h" +#include "EngineClasses/SpatialPackageMapClient.h" +#include "GameFramework/Actor.h" +#include "GameFramework/GameModeBase.h" #include "GameFramework/PlayerController.h" -#include "Net/UnrealNetwork.h" -#include "Engine/Engine.h" +#include "HAL/FileManagerGeneric.h" +#include "HttpModule.h" +#include "Improbable/SpatialGDKSettingsBridge.h" +#include "Interfaces/IHttpBase.h" +#include "Interfaces/IHttpResponse.h" #include "LoadBalancing/AbstractLBStrategy.h" -#include "EngineClasses/SpatialNetDriver.h" +#include "LoadBalancing/DebugLBStrategy.h" +#include "LoadBalancing/LayeredLBStrategy.h" +#include "Net/UnrealNetwork.h" +#include "SpatialFunctionalTestAutoDestroyComponent.h" #include "SpatialFunctionalTestFlowController.h" #include "SpatialGDKFunctionalTestsPrivate.h" -#include "SpatialFunctionalTestAutoDestroyComponent.h" -#include "LoadBalancing/LayeredLBStrategy.h" +namespace +{ +// Maximum time that the test authority will wait after deciding to FinishTest in order for all the Workers +// to have enough time to acknowledge it. If this time is exceeded, the test authority will force FinishTest. +constexpr float FINISH_TEST_GRACE_PERIOD_DURATION = 2.0f; +} // namespace ASpatialFunctionalTest::ASpatialFunctionalTest() : Super() @@ -21,25 +38,54 @@ ASpatialFunctionalTest::ASpatialFunctionalTest() bReplicates = true; NetPriority = 3.0f; NetUpdateFrequency = 100.0f; - + bAlwaysRelevant = true; PrimaryActorTick.TickInterval = 0.0f; + + PreparationTimeLimit = 30.0f; } -void ASpatialFunctionalTest::GetLifetimeReplicatedProps(TArray< FLifetimeProperty >& OutLifetimeProps) const +void ASpatialFunctionalTest::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(ASpatialFunctionalTest, bReadyToSpawnServerControllers); DOREPLIFETIME(ASpatialFunctionalTest, FlowControllers); DOREPLIFETIME(ASpatialFunctionalTest, CurrentStepIndex); + DOREPLIFETIME(ASpatialFunctionalTest, bPreparedTest); + DOREPLIFETIME(ASpatialFunctionalTest, bFinishedTest); } -void ASpatialFunctionalTest::BeginPlay() +void ASpatialFunctionalTest::BeginPlay() { Super::BeginPlay(); - // by default expect 1 server + // Setup built-in step definitions. + TakeSnapshotStepDefinition = FSpatialFunctionalTestStepDefinition(true); + TakeSnapshotStepDefinition.StepName = TEXT("Take SpatialOS Snapshot"); + TakeSnapshotStepDefinition.NativeStartEvent.BindLambda([this]() { + TakeSnapshot([this](bool bSuccess) { + if (bSuccess) + { + FinishStep(); + } + else + { + FinishTest(EFunctionalTestResult::Failed, TEXT("Failed to take SpatialOS Snapshot.")); + } + }); + }); + + ClearSnapshotStepDefinition = FSpatialFunctionalTestStepDefinition(true); + ClearSnapshotStepDefinition.StepName = TEXT("Clear SpatialOS Snapshot"); + ClearSnapshotStepDefinition.NativeStartEvent.BindLambda([this]() { + ClearSnapshot(); + FinishStep(); + }); + + RequireHandler.SetOwnerTest(this); + + // By default expect 1 server. NumExpectedServers = 1; USpatialNetDriver* SpatialNetDriver = Cast(GetNetDriver()); @@ -74,18 +120,42 @@ void ASpatialFunctionalTest::Tick(float DeltaSeconds) FinishTest(EFunctionalTestResult::Succeeded, ""); } } - else + else { TimeRunningStep += DeltaSeconds; float CurrentStepTimeLimit = StepDefinitions[CurrentStepIndex].TimeLimit; - + if (CurrentStepTimeLimit > 0.0f && TimeRunningStep >= CurrentStepTimeLimit) { FinishTest(EFunctionalTestResult::Failed, TEXT("Step time limit reached")); } } } + else if (CurrentStepIndex == SPATIAL_FUNCTIONAL_TEST_FINISHED && FinishTestTimerHandle.IsValid()) + { + bool bAllAcknowledgedFinishedTest = true; + for (const auto* FlowController : FlowControllers) + { + if (!FlowController->HasAckFinishedTest()) + { + bAllAcknowledgedFinishedTest = false; + break; + } + } + if (bAllAcknowledgedFinishedTest) + { + GetWorld()->GetTimerManager().ClearTimer(FinishTestTimerHandle); + Super::FinishTest(CachedTestResult, CachedTestMessage); + + // This will call NotifyTestFinishedObserver on other workers. + bFinishedTest = true; + + // Clear cached variables + CachedTestResult = EFunctionalTestResult::Default; + CachedTestMessage.Empty(); + } + } } void ASpatialFunctionalTest::OnAuthorityGained() @@ -99,13 +169,39 @@ void ASpatialFunctionalTest::RegisterAutoDestroyActor(AActor* ActorToAutoDestroy if (ActorToAutoDestroy != nullptr && ActorToAutoDestroy->HasAuthority()) { // Add component to actor to auto destroy when test finishes - USpatialFunctionalTestAutoDestroyComponent* AutoDestroyComponent = NewObject(ActorToAutoDestroy); + USpatialFunctionalTestAutoDestroyComponent* AutoDestroyComponent = + NewObject(ActorToAutoDestroy); AutoDestroyComponent->AttachToComponent(ActorToAutoDestroy->GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform); AutoDestroyComponent->RegisterComponent(); } else { - UE_LOG(LogSpatialGDKFunctionalTests, Error, TEXT("Should only register to auto destroy from the authoritative worker of the actor: %s"), *GetNameSafe(ActorToAutoDestroy)); + UE_LOG(LogSpatialGDKFunctionalTests, Error, + TEXT("Should only register to auto destroy from the authoritative worker of the actor: %s"), + *GetNameSafe(ActorToAutoDestroy)); + } +} + +void ASpatialFunctionalTest::LogStep(ELogVerbosity::Type Verbosity, const FString& Message) +{ + Super::LogStep(Verbosity, Message); + + if (Verbosity == ELogVerbosity::Error || Verbosity == ELogVerbosity::Fatal) + { + FinishTest(EFunctionalTestResult::Failed, TEXT("Failed assertions")); + } +} + +void ASpatialFunctionalTest::PrepareTest() +{ + StepDefinitions.Empty(); + + Super::PrepareTest(); + + if (HasAuthority()) + { + bPreparedTest = true; + OnReplicated_bPreparedTest(); } } @@ -143,11 +239,19 @@ void ASpatialFunctionalTest::StartTest() void ASpatialFunctionalTest::FinishStep() { + // We can only FinishStep if there are no Require fails. + if (RequireHandler.HasFails()) + { + return; + } + + RequireHandler.LogAndClearStepRequires(); + auto* AuxLocalFlowController = GetLocalFlowController(); ensureMsgf(AuxLocalFlowController != nullptr, TEXT("Can't Find LocalFlowController")); - if(AuxLocalFlowController != nullptr) + if (AuxLocalFlowController != nullptr) { - AuxLocalFlowController->NotifyStepFinished(); + AuxLocalFlowController->NotifyStepFinished(CurrentStepIndex); } } @@ -189,104 +293,95 @@ int ASpatialFunctionalTest::GetNumberOfClientWorkers() return Counter; } -void ASpatialFunctionalTest::AddActorDelegation_Implementation(AActor* Actor, int ServerWorkerId, bool bPersistOnTestFinished /*= false*/) -{ - ISpatialFunctionalTestLBDelegationInterface* DelegationInterface = GetDelegationInterface(); - - if (DelegationInterface != nullptr) - { - bool bAddedDelegation = DelegationInterface->AddActorDelegation(Actor, ServerWorkerId, bPersistOnTestFinished); - ensureMsgf(bAddedDelegation, TEXT("Tried to delegate Actor %s to Server Worker %d but couldn't"), *GetNameSafe(Actor), ServerWorkerId); - } -} - -void ASpatialFunctionalTest::RemoveActorDelegation_Implementation(AActor* Actor) +void ASpatialFunctionalTest::FinishTest(EFunctionalTestResult TestResult, const FString& Message) { - ISpatialFunctionalTestLBDelegationInterface* DelegationInterface = GetDelegationInterface(); - - if (DelegationInterface != nullptr) + if (HasAuthority()) { - bool bRemovedDelegation = DelegationInterface->RemoveActorDelegation(Actor); - ensureMsgf(bRemovedDelegation, TEXT("Tried to remove Delegation from Actor %s but couldn't"), *GetNameSafe(Actor)); - } -} - -bool ASpatialFunctionalTest::HasActorDelegation(AActor* Actor, int& WorkerId, bool& bIsPersistent) -{ - WorkerId = 0; - bIsPersistent = 0; - - ISpatialFunctionalTestLBDelegationInterface* DelegationInterface = GetDelegationInterface(); - - bool bHasDelegation = false; + // Make sure we don't FinishTest multiple times. + if (CurrentStepIndex != SPATIAL_FUNCTIONAL_TEST_FINISHED) + { + bPreparedTest = false; // Clear for PrepareTest to run on all again if the test re-runs. + OnReplicated_bPreparedTest(); - if (DelegationInterface != nullptr) - { - VirtualWorkerId AuxWorkerId; + UE_LOG(LogSpatialGDKFunctionalTests, Display, TEXT("Test %s finished! Result: %s ; Message: %s"), *GetName(), + *UEnum::GetValueAsString(TestResult), *Message); - bHasDelegation = DelegationInterface->HasActorDelegation(Actor, AuxWorkerId, bIsPersistent); + if (TestResult == TimesUpResult) + { + int NumRegisteredClients = 0; + int NumRegisteredServers = 0; - WorkerId = AuxWorkerId; - } + for (ASpatialFunctionalTestFlowController* FlowController : FlowControllers) + { + if (FlowController->IsReadyToRunTest()) // Check if the owner already finished initialization + { + if (FlowController->WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server) + { + ++NumRegisteredServers; + } + else + { + ++NumRegisteredClients; + } + } + } - return bHasDelegation; -} + if (NumRegisteredClients < NumRequiredClients) + { + UE_LOG( + LogSpatialGDKFunctionalTests, Warning, + TEXT("In %s, the number of connected clients is less than the number of required clients: Connected clients: %d, " + "Required clients: %d!"), + *GetName(), NumRegisteredClients, NumRequiredClients); + } -ISpatialFunctionalTestLBDelegationInterface* ASpatialFunctionalTest::GetDelegationInterface() const -{ - USpatialNetDriver* SpatialNetDriver = Cast(GetNetDriver()); - if (SpatialNetDriver) - { - ULayeredLBStrategy* LayeredLBStrategy = Cast(SpatialNetDriver->LoadBalanceStrategy); - if(LayeredLBStrategy != nullptr) - { - return Cast(LayeredLBStrategy->GetLBStrategyForVisualRendering()); - } - } - return nullptr; -} + if (NumRegisteredServers < NumExpectedServers) + { + UE_LOG( + LogSpatialGDKFunctionalTests, Warning, + TEXT("In %s, the number of connected servers is less than the number of required servers: Connected servers: %d, " + "Required servers: %d!"), + *GetName(), NumRegisteredServers, NumExpectedServers); + } + } -void ASpatialFunctionalTest::FinishTest(EFunctionalTestResult TestResult, const FString& Message) -{ - if (HasAuthority()) - { - UE_LOG(LogSpatialGDKFunctionalTests, Display, TEXT("Test %s finished! Result: %s ; Message: %s"), *GetName(), *UEnum::GetValueAsString(TestResult), *Message); + CurrentStepIndex = SPATIAL_FUNCTIONAL_TEST_FINISHED; + OnReplicated_CurrentStepIndex(); // need to call it in Authority manually - if (TestResult == TimesUpResult) - { - int NumRegisteredClients = 0; - int NumRegisteredServers = 0; + CachedTestResult = TestResult; + CachedTestMessage = Message; - for (ASpatialFunctionalTestFlowController* FlowController : FlowControllers) - { - if (FlowController->IsReadyToRunTest()) // Check if the owner already finished initialization - { - if (FlowController->WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server) + GetWorld()->GetTimerManager().SetTimer( + FinishTestTimerHandle, + [this]() { + // If this timer triggers, then it means that something went wrong with one of the Workers. The + // expected behaviour is that the Super::FinishTest will be called from Tick(). Let's double check + // which failed to reply. + FString WorkersDidntAck; + for (const auto* FlowController : FlowControllers) { - ++NumRegisteredServers; + if (!FlowController->HasAckFinishedTest()) + { + WorkersDidntAck += FString::Printf(TEXT("%s, "), *(FlowController->GetDisplayName())); + } } - else + if (!WorkersDidntAck.IsEmpty()) { - ++NumRegisteredClients; + WorkersDidntAck.RemoveFromEnd(TEXT(", ")); + UE_LOG(LogSpatialGDKFunctionalTests, Warning, + TEXT("The following Workers failed to acknowledge FinishTest in time: %s"), *WorkersDidntAck); } - } - } - if (NumRegisteredClients < NumRequiredClients) - { - UE_LOG(LogSpatialGDKFunctionalTests, Warning, TEXT("In %s, the number of connected clients is less than the number of required clients: Connected clients: %d, Required clients: %d!"), *GetName(), NumRegisteredClients, NumRequiredClients); - } - - if (NumRegisteredServers < NumExpectedServers) - { - UE_LOG(LogSpatialGDKFunctionalTests, Warning, TEXT("In %s, the number of connected servers is less than the number of required servers: Connected servers: %d, Required servers: %d!"), *GetName(), NumRegisteredServers, NumExpectedServers); - } - } + Super::FinishTest(CachedTestResult, CachedTestMessage); - CurrentStepIndex = SPATIAL_FUNCTIONAL_TEST_FINISHED; - OnReplicated_CurrentStepIndex(); // need to call it in Authority manually + FinishTestTimerHandle.Invalidate(); - Super::FinishTest(TestResult, Message); + // Clear cached values. + CachedTestResult = EFunctionalTestResult::Default; + CachedTestMessage.Empty(); + }, + FINISH_TEST_GRACE_PERIOD_DURATION, false /* InbLoop */); + } } else { @@ -298,6 +393,12 @@ void ASpatialFunctionalTest::FinishTest(EFunctionalTestResult TestResult, const } } +void ASpatialFunctionalTest::AddExpectedLogError(const FString& ExpectedPatternString, int32 Occurrences /*= 1*/, + bool bExactMatch /*= false*/) +{ + UAutomationBlueprintFunctionLibrary::AddExpectedLogError(ExpectedPatternString, Occurrences, bExactMatch); +} + void ASpatialFunctionalTest::CrossServerFinishTest_Implementation(EFunctionalTestResult TestResult, const FString& Message) { FinishTest(TestResult, Message); @@ -307,13 +408,19 @@ void ASpatialFunctionalTest::RegisterFlowController(ASpatialFunctionalTestFlowCo { if (FlowController->IsLocalController()) { - checkf(LocalFlowController == nullptr, TEXT("Already had LocalFlowController, this shouldn't happen")); + if (LocalFlowController != nullptr) + { + checkf(LocalFlowController == FlowController, + TEXT("OwningTest already had different LocalFlowController, this shouldn't happen")); + return; + } + LocalFlowController = FlowController; } if (!HasAuthority()) { - //FlowControllers invoke this on each worker's local context when checkout and ready, we only want to act in the authority + // FlowControllers invoke this on each worker's local context when checkout and ready, we only want to act in the authority return; } @@ -322,7 +429,7 @@ void ASpatialFunctionalTest::RegisterFlowController(ASpatialFunctionalTestFlowCo // Since Clients can spawn on any worker we need to centralize the assignment of their ids to the Test Authority. FlowControllerSpawner.AssignClientFlowControllerId(FlowController); } - + FlowControllers.Add(FlowController); } @@ -332,10 +439,29 @@ ASpatialFunctionalTestFlowController* ASpatialFunctionalTest::GetLocalFlowContro return LocalFlowController; } +ESpatialFunctionalTestWorkerType ASpatialFunctionalTest::GetLocalWorkerType() +{ + ASpatialFunctionalTestFlowController* AuxFlowController = GetLocalFlowController(); + return AuxFlowController != nullptr ? AuxFlowController->WorkerDefinition.Type : ESpatialFunctionalTestWorkerType::Invalid; +} + +int ASpatialFunctionalTest::GetLocalWorkerId() +{ + ASpatialFunctionalTestFlowController* AuxFlowController = GetLocalFlowController(); + return AuxFlowController != nullptr ? AuxFlowController->WorkerDefinition.Id : INVALID_FLOW_CONTROLLER_ID; +} + // Add Steps for Blueprints -void ASpatialFunctionalTest::AddStepBlueprint(const FString& StepName, const FWorkerDefinition& Worker, const FStepIsReadyDelegate& IsReadyEvent, const FStepStartDelegate& StartEvent, const FStepTickDelegate& TickEvent, float StepTimeLimit /*= 0.0f*/) +void ASpatialFunctionalTest::AddStepBlueprint(const FString& StepName, const FWorkerDefinition& Worker, + const FStepIsReadyDelegate& IsReadyEvent, const FStepStartDelegate& StartEvent, + const FStepTickDelegate& TickEvent, float StepTimeLimit /*= 0.0f*/) { + if (StepName.IsEmpty()) + { + UE_LOG(LogSpatialGDKFunctionalTests, Warning, TEXT("Adding a Step without a name")); + } + FSpatialFunctionalTestStepDefinition StepDefinition; StepDefinition.bIsNativeDefinition = false; StepDefinition.StepName = StepName; @@ -349,8 +475,13 @@ void ASpatialFunctionalTest::AddStepBlueprint(const FString& StepName, const FWo StepDefinitions.Add(StepDefinition); } -void ASpatialFunctionalTest::AddStepFromDefinition(const FSpatialFunctionalTestStepDefinition& StepDefinition, const FWorkerDefinition& Worker) +void ASpatialFunctionalTest::AddStepFromDefinition(const FSpatialFunctionalTestStepDefinition& StepDefinition, + const FWorkerDefinition& Worker) { + if (StepDefinition.StepName.IsEmpty()) + { + UE_LOG(LogSpatialGDKFunctionalTests, Warning, TEXT("Adding a Step without a name")); + } FSpatialFunctionalTestStepDefinition StepDefinitionCopy = StepDefinition; StepDefinitionCopy.Workers.Add(Worker); @@ -358,8 +489,13 @@ void ASpatialFunctionalTest::AddStepFromDefinition(const FSpatialFunctionalTestS StepDefinitions.Add(StepDefinitionCopy); } -void ASpatialFunctionalTest::AddStepFromDefinitionMulti(const FSpatialFunctionalTestStepDefinition& StepDefinition, const TArray& Workers) +void ASpatialFunctionalTest::AddStepFromDefinitionMulti(const FSpatialFunctionalTestStepDefinition& StepDefinition, + const TArray& Workers) { + if (StepDefinition.StepName.IsEmpty()) + { + UE_LOG(LogSpatialGDKFunctionalTests, Warning, TEXT("Adding a Step without a name")); + } FSpatialFunctionalTestStepDefinition StepDefinitionCopy = StepDefinition; StepDefinitionCopy.Workers.Append(Workers); @@ -371,6 +507,9 @@ void ASpatialFunctionalTest::StartStep(const int StepIndex) { if (HasAuthority()) { + // Log Requires from previous step. + RequireHandler.LogAndClearStepRequires(); + CurrentStepIndex = StepIndex; TimeRunningStep = 0.0f; @@ -389,7 +528,8 @@ void ASpatialFunctionalTest::StartStep(const int StepIndex) for (auto* FlowController : FlowControllers) { if (WorkerType == ESpatialFunctionalTestWorkerType::All - || ( FlowController->WorkerDefinition.Type == WorkerType && (WorkerId <= FWorkerDefinition::ALL_WORKERS_ID || FlowController->WorkerDefinition.Id == WorkerId))) + || (FlowController->WorkerDefinition.Type == WorkerType + && (WorkerId <= FWorkerDefinition::ALL_WORKERS_ID || FlowController->WorkerDefinition.Id == WorkerId))) { FlowControllersExecutingStep.AddUnique(FlowController); } @@ -418,15 +558,25 @@ void ASpatialFunctionalTest::StartStep(const int StepIndex) } else { - FinishTest(EFunctionalTestResult::Error, FString::Printf(TEXT("Trying to start Step %s without any Worker"), *StepDefinition.StepName)); + FinishTest(EFunctionalTestResult::Error, + FString::Printf(TEXT("Trying to start Step %s without any Worker"), *StepDefinition.StepName)); } } } // Add Steps for C++ -FSpatialFunctionalTestStepDefinition& ASpatialFunctionalTest::AddStep(const FString& StepName, const FWorkerDefinition& Worker, FIsReadyEventFunc IsReadyEvent /*= nullptr*/, FStartEventFunc StartEvent /*= nullptr*/, FTickEventFunc TickEvent /*= nullptr*/, float StepTimeLimit /*= 0.0f*/) +FSpatialFunctionalTestStepDefinition& ASpatialFunctionalTest::AddStep(const FString& StepName, const FWorkerDefinition& Worker, + FIsReadyEventFunc IsReadyEvent /*= nullptr*/, + FStartEventFunc StartEvent /*= nullptr*/, + FTickEventFunc TickEvent /*= nullptr*/, + float StepTimeLimit /*= 0.0f*/) { + if (StepName.IsEmpty()) + { + UE_LOG(LogSpatialGDKFunctionalTests, Warning, TEXT("Adding a Step without a name")); + } + FSpatialFunctionalTestStepDefinition StepDefinition; StepDefinition.bIsNativeDefinition = true; StepDefinition.StepName = StepName; @@ -451,7 +601,6 @@ FSpatialFunctionalTestStepDefinition& ASpatialFunctionalTest::AddStep(const FStr return StepDefinitions[StepDefinitions.Num() - 1]; } - ASpatialFunctionalTestFlowController* ASpatialFunctionalTest::GetFlowController(ESpatialFunctionalTestWorkerType WorkerType, int WorkerId) { ensureMsgf(WorkerType != ESpatialFunctionalTestWorkerType::All, TEXT("Trying to call GetFlowController with All WorkerType")); @@ -465,20 +614,21 @@ ASpatialFunctionalTestFlowController* ASpatialFunctionalTest::GetFlowController( return nullptr; } -void ASpatialFunctionalTest::CrossServerNotifyStepFinished_Implementation(ASpatialFunctionalTestFlowController* FlowController) +void ASpatialFunctionalTest::CrossServerNotifyStepFinished_Implementation(ASpatialFunctionalTestFlowController* FlowController, + const int StepIndex) { - if (CurrentStepIndex < 0) + if (CurrentStepIndex < 0 || StepIndex != CurrentStepIndex) { return; } - const FString FLowControllerDisplayName = FlowController->GetDisplayName(); - - UE_LOG(LogSpatialGDKFunctionalTests, Display, TEXT("%s finished Step"), *FLowControllerDisplayName); - + const FString FlowControllerDisplayName = FlowController->GetDisplayName(); + + UE_LOG(LogSpatialGDKFunctionalTests, Display, TEXT("%s finished Step"), *FlowControllerDisplayName); + if (FlowControllersExecutingStep.RemoveSwap(FlowController) == 0) { - FString ErrorMsg = FString::Printf(TEXT("%s was not in list of workers executing"), *FLowControllerDisplayName); + FString ErrorMsg = FString::Printf(TEXT("%s was not in list of workers executing"), *FlowControllerDisplayName); ensureMsgf(false, TEXT("%s"), *ErrorMsg); FinishTest(EFunctionalTestResult::Error, ErrorMsg); } @@ -488,34 +638,53 @@ void ASpatialFunctionalTest::OnReplicated_CurrentStepIndex() { if (CurrentStepIndex == SPATIAL_FUNCTIONAL_TEST_FINISHED) { - //test finished - if(StartTime > 0) + RequireHandler.LogAndClearStepRequires(); + // if we ever started in first place + ASpatialFunctionalTestFlowController* AuxLocalFlowController = GetLocalFlowController(); + if (AuxLocalFlowController != nullptr) { - //if we ever started in first place - ASpatialFunctionalTestFlowController* AuxLocalFlowController = GetLocalFlowController(); - if (AuxLocalFlowController != nullptr) + AuxLocalFlowController->OnTestFinished(); + if (AuxLocalFlowController->WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server) { - AuxLocalFlowController->OnTestFinished(); - if (AuxLocalFlowController->WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server) - { - ISpatialFunctionalTestLBDelegationInterface* DelegationInterface = GetDelegationInterface(); - - if (DelegationInterface != nullptr) - { - DelegationInterface->RemoveAllActorDelegations(GetWorld()); - } - } + ClearTagDelegationAndInterest(); } } - if (!HasAuthority()) // Authority already does this on Super::FinishTest - { - NotifyTestFinishedObserver(); - } DeleteActorsRegisteredForAutoDestroy(); } } +void ASpatialFunctionalTest::OnReplicated_bPreparedTest() +{ + if (bPreparedTest) + { + // We need to delay until next Tick since on non-Authority + // OnReplicated_bPreparedTest() will be called before BeginPlay(). + GetWorld()->GetTimerManager().SetTimerForNextTick([this]() { + if (!HasAuthority()) + { + PrepareTest(); + } + + // Currently PrepareTest() happens before FlowControllers are registered, + // but that is most likely because of the bug that forces us to delay their registration. + if (LocalFlowController != nullptr) + { + LocalFlowController->SetReadyToRunTest(true); + } + }); + } +} + +void ASpatialFunctionalTest::OnReplicated_bFinishedTest() +{ + if (!HasAuthority()) + { + // The server that started this test has to call this in order for the test to properly finish. + NotifyTestFinishedObserver(); + } +} + void ASpatialFunctionalTest::StartServerFlowControllerSpawn() { if (!bReadyToSpawnServerControllers) @@ -533,18 +702,22 @@ void ASpatialFunctionalTest::StartServerFlowControllerSpawn() void ASpatialFunctionalTest::SetupClientPlayerRegistrationFlow() { - GetWorld()->AddOnActorSpawnedHandler(FOnActorSpawned::FDelegate::CreateLambda( - [this](AActor* Spawned) + PostLoginDelegate = FGameModeEvents::GameModePostLoginEvent.AddLambda([this](AGameModeBase* GameMode, APlayerController* NewPlayer) { + // NB : the delegate is a global one, have to filter in case we are running from PIE <==> multiple worlds. + if (NewPlayer->GetWorld() == GetWorld() && NewPlayer->HasAuthority()) { - if (APlayerController* PlayerController = Cast(Spawned)) - { - if(PlayerController->HasAuthority()) - { - this->FlowControllerSpawner.SpawnClientFlowController(PlayerController); - } - } + this->FlowControllerSpawner.SpawnClientFlowController(NewPlayer); } - )); + }); +} + +void ASpatialFunctionalTest::EndPlay(const EEndPlayReason::Type Reason) +{ + if (PostLoginDelegate.IsValid()) + { + FGameModeEvents::GameModePostLoginEvent.Remove(PostLoginDelegate); + PostLoginDelegate.Reset(); + } } void ASpatialFunctionalTest::DeleteActorsRegisteredForAutoDestroy() @@ -560,5 +733,267 @@ void ASpatialFunctionalTest::DeleteActorsRegisteredForAutoDestroy() FoundActor->SetLifeSpan(0.01f); } } - +} + +namespace +{ +USpatialNetDriver* GetNetDriverAndCheckDebuggingEnabled(AActor* Actor) +{ + if (!ensureMsgf(Actor != nullptr, TEXT("Actor is null"))) + { + return nullptr; + } + + USpatialNetDriver* NetDriver = Cast(Actor->GetNetDriver()); + if (!ensureMsgf(NetDriver != nullptr, TEXT("Using SpatialFunctionalTest Debug facilities while the NetDriver is not Spatial"))) + { + return nullptr; + } + if (!ensureMsgf(NetDriver->DebugCtx != nullptr, + TEXT("SpatialFunctionalTest Debug facilities are not enabled. Enable them in your map's world settings."))) + { + return nullptr; + } + + return NetDriver; +} +} // namespace + +ULayeredLBStrategy* ASpatialFunctionalTest::GetLoadBalancingStrategy() +{ + UWorld* World = GetWorld(); + USpatialNetDriver* NetDriver = Cast(World->GetNetDriver()); + if (ensureMsgf(NetDriver != nullptr, TEXT("Trying to get a load balancing strategy while the NetDriver is not Spatial"))) + { + if (NetDriver->DebugCtx != nullptr) + { + return Cast(NetDriver->DebugCtx->DebugStrategy->GetWrappedStrategy()); + } + else + { + return Cast(NetDriver->LoadBalanceStrategy); + } + } + return nullptr; +} + +void ASpatialFunctionalTest::AddDebugTag(AActor* Actor, FName Tag) +{ + if (Actor == nullptr) + { + return; + } + + if (USpatialNetDriver* NetDriver = GetNetDriverAndCheckDebuggingEnabled(this)) + { + NetDriver->DebugCtx->AddActorTag(Actor, Tag); + } +} + +void ASpatialFunctionalTest::RemoveDebugTag(AActor* Actor, FName Tag) +{ + if (Actor == nullptr) + { + return; + } + + if (USpatialNetDriver* NetDriver = GetNetDriverAndCheckDebuggingEnabled(this)) + { + NetDriver->DebugCtx->RemoveActorTag(Actor, Tag); + } +} + +void ASpatialFunctionalTest::AddInterestOnTag(FName Tag) +{ + if (USpatialNetDriver* NetDriver = GetNetDriverAndCheckDebuggingEnabled(this)) + { + NetDriver->DebugCtx->AddInterestOnTag(Tag); + } +} + +void ASpatialFunctionalTest::RemoveInterestOnTag(FName Tag) +{ + if (USpatialNetDriver* NetDriver = GetNetDriverAndCheckDebuggingEnabled(this)) + { + NetDriver->DebugCtx->RemoveInterestOnTag(Tag); + } +} + +void ASpatialFunctionalTest::KeepActorOnCurrentWorker(AActor* Actor) +{ + if (Actor == nullptr) + { + return; + } + + if (USpatialNetDriver* NetDriver = GetNetDriverAndCheckDebuggingEnabled(this)) + { + NetDriver->DebugCtx->KeepActorOnLocalWorker(Actor); + } +} + +void ASpatialFunctionalTest::AddStepSetTagDelegation(FName Tag, int32 ServerWorkerId /*= 1*/) +{ + if (!ensureMsgf(ServerWorkerId > 0, TEXT("Invalid Server Worker Id"))) + { + return; + } + if (ServerWorkerId >= GetNumExpectedServers()) + { + ServerWorkerId = 1; // Support for single worker environments. + } + AddStep(FString::Printf(TEXT("Set Delegation of Tag '%s' to Server Worker %d"), *Tag.ToString(), ServerWorkerId), + FWorkerDefinition::AllServers, nullptr, [this, Tag, ServerWorkerId] { + SetTagDelegation(Tag, ServerWorkerId); + FinishStep(); + }); +} + +void ASpatialFunctionalTest::AddStepClearTagDelegation(FName Tag) +{ + AddStep(FString::Printf(TEXT("Clear Delegation of Tag '%s'"), *Tag.ToString()), FWorkerDefinition::AllServers, nullptr, [this, Tag] { + ClearTagDelegation(Tag); + FinishStep(); + }); +} + +void ASpatialFunctionalTest::AddStepClearTagDelegationAndInterest() +{ + AddStep(TEXT("Clear Delegation of all Tags and extra Interest"), FWorkerDefinition::AllServers, nullptr, [this] { + ClearTagDelegationAndInterest(); + FinishStep(); + }); +} + +void ASpatialFunctionalTest::SetTagDelegation(FName Tag, int32 ServerWorkerId) +{ + if (USpatialNetDriver* NetDriver = GetNetDriverAndCheckDebuggingEnabled(this)) + { + NetDriver->DebugCtx->DelegateTagToWorker(Tag, ServerWorkerId); + } +} + +void ASpatialFunctionalTest::ClearTagDelegation(FName Tag) +{ + if (USpatialNetDriver* NetDriver = GetNetDriverAndCheckDebuggingEnabled(this)) + { + NetDriver->DebugCtx->RemoveTagDelegation(Tag); + } +} + +void ASpatialFunctionalTest::ClearTagDelegationAndInterest() +{ + UWorld* World = GetWorld(); + USpatialNetDriver* NetDriver = Cast(World->GetNetDriver()); + if (NetDriver && NetDriver->DebugCtx) + { + NetDriver->DebugCtx->Reset(); + } +} + +void ASpatialFunctionalTest::TakeSnapshot(const FSpatialFunctionalTestSnapshotTakenDelegate& BlueprintCallback) +{ + ISpatialGDKEditorModule* SpatialGDKEditorModule = FModuleManager::GetModulePtr("SpatialGDKEditor"); + if (SpatialGDKEditorModule != nullptr) + { + UWorld* World = GetWorld(); + SpatialGDKEditorModule->TakeSnapshot(World, [World, BlueprintCallback](bool bSuccess, const FString& PathToSnapshot) { + if (bSuccess) + { + bSuccess = SetSnapshotForMap(World, PathToSnapshot); + } + BlueprintCallback.ExecuteIfBound(bSuccess); + }); + } +} + +void ASpatialFunctionalTest::TakeSnapshot(const FSnapshotTakenFunc& CppCallback) +{ + ISpatialGDKEditorModule* SpatialGDKEditorModule = FModuleManager::GetModulePtr("SpatialGDKEditor"); + if (SpatialGDKEditorModule != nullptr) + { + UWorld* World = GetWorld(); + SpatialGDKEditorModule->TakeSnapshot(World, [World, CppCallback](bool bSuccess, const FString& PathToSnapshot) { + if (bSuccess) + { + bSuccess = SetSnapshotForMap(World, PathToSnapshot); + } + if (CppCallback != nullptr) + { + CppCallback(bSuccess); + } + }); + } +} + +void ASpatialFunctionalTest::ClearSnapshot() +{ + SetSnapshotForMap(GetWorld(), FString() /* PathToSnapshot */); +} + +bool ASpatialFunctionalTest::SetSnapshotForMap(UWorld* World, const FString& PathToSnapshot) +{ + check(World != nullptr); + + FString MapName = World->GetMapName(); + MapName.RemoveFromStart(World->StreamingLevelsPrefix); + + bool bSuccess = true; + + if (PathToSnapshot.IsEmpty()) + { + TakenSnapshots.Remove(MapName); + } + else + { + FString SnapshotFileName = FString::Printf(TEXT("functional_testing_%s.snapshot"), *MapName); + FString SnapshotSavePath = FPaths::ProjectDir() + TEXT("../spatial/snapshots/") + SnapshotFileName; + if (FFileManagerGeneric::Get().Copy(*SnapshotSavePath, *PathToSnapshot, true, true) == 0) + { + TakenSnapshots.Add(MapName, SnapshotFileName); + } + else + { + bSuccess = false; + UE_LOG(LogSpatialGDKFunctionalTests, Error, TEXT("Failed to copy snapshot file '%s' to '%s'"), *PathToSnapshot, + *SnapshotSavePath); + } + } + return bSuccess; +} + +TMap ASpatialFunctionalTest::TakenSnapshots = TMap(); + +FString ASpatialFunctionalTest::GetTakenSnapshotPath(UWorld* World) +{ + if (World == nullptr) + { + return FString(); + } + FString MapName = World->GetMapName(); + MapName.RemoveFromStart(World->StreamingLevelsPrefix); + return TakenSnapshots.FindRef(MapName); +} + +bool ASpatialFunctionalTest::bWasLoadedFromTakenSnapshot = false; + +void ASpatialFunctionalTest::SetLoadedFromTakenSnapshot() +{ + bWasLoadedFromTakenSnapshot = true; +} + +void ASpatialFunctionalTest::ClearLoadedFromTakenSnapshot() +{ + bWasLoadedFromTakenSnapshot = false; +} + +bool ASpatialFunctionalTest::WasLoadedFromTakenSnapshot() +{ + return bWasLoadedFromTakenSnapshot; +} + +void ASpatialFunctionalTest::ClearAllTakenSnapshots() +{ + bWasLoadedFromTakenSnapshot = false; + TakenSnapshots.Empty(); } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestAutoDestroyComponent.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestAutoDestroyComponent.cpp index 204766f18a..d6c7464052 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestAutoDestroyComponent.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestAutoDestroyComponent.cpp @@ -1,6 +1,5 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved - #include "SpatialFunctionalTestAutoDestroyComponent.h" USpatialFunctionalTestAutoDestroyComponent::USpatialFunctionalTestAutoDestroyComponent() @@ -8,10 +7,7 @@ USpatialFunctionalTestAutoDestroyComponent::USpatialFunctionalTestAutoDestroyCom { #if ENGINE_MINOR_VERSION <= 23 bReplicates = true; -#else +#else SetIsReplicatedByDefault(true); #endif } - - - diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestBlueprintLibrary.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestBlueprintLibrary.cpp index 2d03b27505..909838a26d 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestBlueprintLibrary.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestBlueprintLibrary.cpp @@ -2,7 +2,11 @@ #include "SpatialFunctionalTestBlueprintLibrary.h" -FSpatialFunctionalTestStepDefinition USpatialFunctionalTestBlueprintLibrary::MakeStepDefinition(const FString& StepName, const FStepIsReadyDelegate& IsReadyEvent, const FStepStartDelegate& StartEvent, const FStepTickDelegate& TickEvent, const float StepTimeLimit) +FSpatialFunctionalTestStepDefinition USpatialFunctionalTestBlueprintLibrary::MakeStepDefinition(const FString& StepName, + const FStepIsReadyDelegate& IsReadyEvent, + const FStepStartDelegate& StartEvent, + const FStepTickDelegate& TickEvent, + const float StepTimeLimit) { FSpatialFunctionalTestStepDefinition StepDefinition; StepDefinition.StepName = StepName; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestFlowController.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestFlowController.cpp index 98f129dd55..ef166524da 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestFlowController.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestFlowController.cpp @@ -2,14 +2,18 @@ #include "SpatialFunctionalTestFlowController.h" +#include "EngineClasses/SpatialNetDriver.h" +#include "EngineClasses/SpatialPackageMapClient.h" #include "GameFramework/PlayerController.h" +#include "Interop/SpatialSender.h" +#include "LoadBalancing/LayeredLBStrategy.h" #include "Net/UnrealNetwork.h" #include "SpatialFunctionalTest.h" #include "SpatialGDKFunctionalTestsPrivate.h" ASpatialFunctionalTestFlowController::ASpatialFunctionalTestFlowController(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) -{ +{ bReplicates = true; bAlwaysRelevant = true; @@ -25,28 +29,50 @@ ASpatialFunctionalTestFlowController::ASpatialFunctionalTestFlowController(const #endif } -void ASpatialFunctionalTestFlowController::GetLifetimeReplicatedProps(TArray< FLifetimeProperty >& OutLifetimeProps) const +void ASpatialFunctionalTestFlowController::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(ASpatialFunctionalTestFlowController, bReadyToRegisterWithTest); DOREPLIFETIME(ASpatialFunctionalTestFlowController, bIsReadyToRunTest); + DOREPLIFETIME(ASpatialFunctionalTestFlowController, bHasAckFinishedTest); DOREPLIFETIME(ASpatialFunctionalTestFlowController, OwningTest); DOREPLIFETIME(ASpatialFunctionalTestFlowController, WorkerDefinition); } -void ASpatialFunctionalTestFlowController::OnAuthorityGained() +void ASpatialFunctionalTestFlowController::BeginPlay() { - bReadyToRegisterWithTest = true; - OnReadyToRegisterWithTest(); + Super::BeginPlay(); + + if (HasAuthority()) + { + // Super hack + FTimerHandle Handle; + GetWorldTimerManager().SetTimer( + Handle, + [this]() { + bReadyToRegisterWithTest = true; + OnReadyToRegisterWithTest(); + }, + 1.0f, false); + } } +void ASpatialFunctionalTestFlowController::OnAuthorityGained() {} + void ASpatialFunctionalTestFlowController::Tick(float DeltaSeconds) { if (CurrentStep.bIsRunning) { CurrentStep.Tick(DeltaSeconds); } + + // Did it stop now or before the Tick was called? + if (!CurrentStep.bIsRunning) + { + SetActorTickEnabled(false); + CurrentStep.Reset(); + } } void ASpatialFunctionalTestFlowController::CrossServerSetWorkerId_Implementation(int NewWorkerId) @@ -56,33 +82,40 @@ void ASpatialFunctionalTestFlowController::CrossServerSetWorkerId_Implementation void ASpatialFunctionalTestFlowController::OnReadyToRegisterWithTest() { - if(!bReadyToRegisterWithTest || OwningTest == nullptr) + TryRegisterFlowControllerWithOwningTest(); +} + +void ASpatialFunctionalTestFlowController::OnRep_OwningTest() +{ + TryRegisterFlowControllerWithOwningTest(); +} + +void ASpatialFunctionalTestFlowController::TryRegisterFlowControllerWithOwningTest() +{ + if (!bReadyToRegisterWithTest || OwningTest == nullptr) { return; } OwningTest->RegisterFlowController(this); - if (IsLocalController()) + if (OwningTest->HasPreparedTest()) { - if (WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server) - { - bIsReadyToRunTest = true; - } - else - { - ServerSetReadyToRunTest(); - } + SetReadyToRunTest(true); } } -void ASpatialFunctionalTestFlowController::ServerSetReadyToRunTest_Implementation() +void ASpatialFunctionalTestFlowController::ServerSetReadyToRunTest_Implementation(bool bIsReady) { - bIsReadyToRunTest = true; + bIsReadyToRunTest = bIsReady; } void ASpatialFunctionalTestFlowController::CrossServerStartStep_Implementation(int StepIndex) { + // Since we're starting a step, we mark as not Ack that we've finished the test. This is needed + // for the cases when we run multiple times the same test without a map reload. + bHasAckFinishedTest = false; + OwningTest->SetCurrentStepIndex(StepIndex); // Just in case we do not get the replication fast enough if (WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server) { StartStepInternal(StepIndex); @@ -93,25 +126,23 @@ void ASpatialFunctionalTestFlowController::CrossServerStartStep_Implementation(i } } -void ASpatialFunctionalTestFlowController::NotifyStepFinished() +void ASpatialFunctionalTestFlowController::NotifyStepFinished(const int StepIndex) { - ensureMsgf(CurrentStep.bIsRunning, TEXT("Trying to Notify Step Finished when it wasn't running. Either the Test ended prematurely or it's logic is calling FinishStep multiple times")); if (CurrentStep.bIsRunning) { if (WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server) { - CrossServerNotifyStepFinished(); + CrossServerNotifyStepFinished(StepIndex); } else { - ServerNotifyStepFinished(); + ServerNotifyStepFinished(StepIndex); } StopStepInternal(); } } - bool ASpatialFunctionalTestFlowController::IsLocalController() const { ENetMode NetMode = GetNetMode(); @@ -123,43 +154,78 @@ bool ASpatialFunctionalTestFlowController::IsLocalController() const { // if no PlayerController owns it it's ours. // @note keep in mind that this only works because each server worker has authority (by locking) their FlowController! - return GetOwner() == nullptr; + return GetOwner() == nullptr; } - else if(GetNetMode() == ENetMode::NM_Client) + else if (GetNetMode() == ENetMode::NM_Client) { // @note Clients only know their own PlayerController return GetOwner() != nullptr; } - + return false; } void ASpatialFunctionalTestFlowController::NotifyFinishTest(EFunctionalTestResult TestResult, const FString& Message) { - StopStepInternal(); - - if (WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server) - { - ServerNotifyFinishTestInternal(TestResult, Message); - } - else + // To prevent trying to Notify multiple times, which can happen for example with multiple asserts in a row. + if (CurrentStep.bIsRunning) { - ServerNotifyFinishTest(TestResult, Message); + StopStepInternal(); + + if (WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server) + { + ServerNotifyFinishTestInternal(TestResult, Message); + } + else + { + ServerNotifyFinishTest(TestResult, Message); + } } } -const FString ASpatialFunctionalTestFlowController::GetDisplayName() +const FString ASpatialFunctionalTestFlowController::GetDisplayName() const { - return FString::Printf(TEXT("[%s:%d]"), (WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server ? TEXT("Server") : TEXT("Client")), WorkerDefinition.Id); + return FString::Printf(TEXT("[%s:%d]"), + (WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server ? TEXT("Server") : TEXT("Client")), + WorkerDefinition.Id); } void ASpatialFunctionalTestFlowController::OnTestFinished() { StopStepInternal(); + if (HasAuthority()) + { + bHasAckFinishedTest = true; + } + else + { + ServerAckFinishedTest(); + } + SetReadyToRunTest(false); +} + +void ASpatialFunctionalTestFlowController::SetReadyToRunTest(bool bIsReady) +{ + if (bIsReady == bIsReadyToRunTest) + { + return; + } + if (IsLocalController()) + { + if (WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server) + { + bIsReadyToRunTest = bIsReady; + } + else + { + ServerSetReadyToRunTest(bIsReady); + } + } } void ASpatialFunctionalTestFlowController::ClientStartStep_Implementation(int StepIndex) { + OwningTest->SetCurrentStepIndex(StepIndex); // Just in case we do not get the replication fast enough StartStepInternal(StepIndex); } @@ -168,14 +234,12 @@ void ASpatialFunctionalTestFlowController::StartStepInternal(const int StepIndex const FSpatialFunctionalTestStepDefinition& StepDefinition = OwningTest->GetStepDefinition(StepIndex); UE_LOG(LogSpatialGDKFunctionalTests, Log, TEXT("Executing step %s on %s"), *StepDefinition.StepName, *GetDisplayName()); SetActorTickEnabled(true); - CurrentStep.Owner = OwningTest; CurrentStep.Start(StepDefinition); } void ASpatialFunctionalTestFlowController::StopStepInternal() { - SetActorTickEnabled(false); - CurrentStep.Reset(); + CurrentStep.bIsRunning = false; } void ASpatialFunctionalTestFlowController::ServerNotifyFinishTest_Implementation(EFunctionalTestResult TestResult, const FString& Message) @@ -188,12 +252,17 @@ void ASpatialFunctionalTestFlowController::ServerNotifyFinishTestInternal(EFunct OwningTest->CrossServerFinishTest(TestResult, Message); } -void ASpatialFunctionalTestFlowController::ServerNotifyStepFinished_Implementation() +void ASpatialFunctionalTestFlowController::ServerAckFinishedTest_Implementation() +{ + bHasAckFinishedTest = true; +} + +void ASpatialFunctionalTestFlowController::ServerNotifyStepFinished_Implementation(const int StepIndex) { - OwningTest->CrossServerNotifyStepFinished(this); + OwningTest->CrossServerNotifyStepFinished(this, StepIndex); } -void ASpatialFunctionalTestFlowController::CrossServerNotifyStepFinished_Implementation() +void ASpatialFunctionalTestFlowController::CrossServerNotifyStepFinished_Implementation(const int StepIndex) { - OwningTest->CrossServerNotifyStepFinished(this); + OwningTest->CrossServerNotifyStepFinished(this, StepIndex); } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestFlowControllerSpawner.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestFlowControllerSpawner.cpp index 3ade7de970..ce2ab47dfa 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestFlowControllerSpawner.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestFlowControllerSpawner.cpp @@ -1,27 +1,29 @@ #include "SpatialFunctionalTestFlowControllerSpawner.h" - -#include "Engine/World.h" #include "Engine/NetDriver.h" +#include "Engine/World.h" +#include "EngineClasses/SpatialNetDriver.h" #include "GameFramework/PlayerController.h" #include "LoadBalancing/AbstractLBStrategy.h" -#include "EngineClasses/SpatialNetDriver.h" -#include "SpatialFunctionalTestFlowController.h" #include "SpatialFunctionalTest.h" +#include "SpatialFunctionalTestFlowController.h" SpatialFunctionalTestFlowControllerSpawner::SpatialFunctionalTestFlowControllerSpawner() - : SpatialFunctionalTestFlowControllerSpawner(nullptr, TSubclassOf(ASpatialFunctionalTestFlowController::StaticClass())) + : SpatialFunctionalTestFlowControllerSpawner( + nullptr, TSubclassOf(ASpatialFunctionalTestFlowController::StaticClass())) { } -SpatialFunctionalTestFlowControllerSpawner::SpatialFunctionalTestFlowControllerSpawner(ASpatialFunctionalTest* ControllerOwningTest, TSubclassOf FlowControllerClassToSpawn) - : OwningTest(ControllerOwningTest), - FlowControllerClass(FlowControllerClassToSpawn), - NextClientControllerId(1) +SpatialFunctionalTestFlowControllerSpawner::SpatialFunctionalTestFlowControllerSpawner( + ASpatialFunctionalTest* ControllerOwningTest, TSubclassOf FlowControllerClassToSpawn) + : OwningTest(ControllerOwningTest) + , FlowControllerClass(FlowControllerClassToSpawn) + , NextClientControllerId(1) { } -void SpatialFunctionalTestFlowControllerSpawner::ModifyFlowControllerClassToSpawn(TSubclassOf FlowControllerClassToSpawn) +void SpatialFunctionalTestFlowControllerSpawner::ModifyFlowControllerClassToSpawn( + TSubclassOf FlowControllerClassToSpawn) { FlowControllerClass = FlowControllerClassToSpawn; } @@ -32,11 +34,12 @@ ASpatialFunctionalTestFlowController* SpatialFunctionalTestFlowControllerSpawner UNetDriver* NetDriver = World->GetNetDriver(); if (NetDriver != nullptr && !NetDriver->IsServer()) { - //Not a server, quit + // Not a server, quit return nullptr; } - ASpatialFunctionalTestFlowController* ServerFlowController = World->SpawnActorDeferred(FlowControllerClass, FTransform()); + ASpatialFunctionalTestFlowController* ServerFlowController = + World->SpawnActorDeferred(FlowControllerClass, FTransform()); ServerFlowController->OwningTest = OwningTest; ServerFlowController->WorkerDefinition = FWorkerDefinition{ ESpatialFunctionalTestWorkerType::Server, OwningServerIntanceId(World) }; @@ -51,20 +54,23 @@ ASpatialFunctionalTestFlowController* SpatialFunctionalTestFlowControllerSpawner { UWorld* World = OwningTest->GetWorld(); - ASpatialFunctionalTestFlowController* FlowController = World->SpawnActorDeferred(FlowControllerClass, OwningTest->GetActorTransform(), OwningClient); + ASpatialFunctionalTestFlowController* FlowController = + World->SpawnActorDeferred(FlowControllerClass, OwningTest->GetActorTransform(), OwningClient); FlowController->OwningTest = OwningTest; - FlowController->WorkerDefinition = FWorkerDefinition{ ESpatialFunctionalTestWorkerType::Client , INVALID_FLOW_CONTROLLER_ID}; // by default have invalid id, Test Authority will set it to ensure uniqueness - + FlowController->WorkerDefinition = + FWorkerDefinition{ ESpatialFunctionalTestWorkerType::Client, + INVALID_FLOW_CONTROLLER_ID }; // by default have invalid id, Test Authority will set it to ensure uniqueness + FlowController->FinishSpawning(OwningTest->GetActorTransform(), true); - // TODO: Replace locking with custom LB strategy - UNR-3393 - LockFlowControllerDelegations(FlowController); return FlowController; } void SpatialFunctionalTestFlowControllerSpawner::AssignClientFlowControllerId(ASpatialFunctionalTestFlowController* ClientFlowController) { - check(OwningTest->HasAuthority() && ClientFlowController != nullptr && ClientFlowController->WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Client && ClientFlowController->WorkerDefinition.Id == INVALID_FLOW_CONTROLLER_ID); + check(OwningTest->HasAuthority() && ClientFlowController != nullptr + && ClientFlowController->WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Client + && ClientFlowController->WorkerDefinition.Id == INVALID_FLOW_CONTROLLER_ID); ClientFlowController->CrossServerSetWorkerId(NextClientControllerId++); } @@ -74,7 +80,7 @@ uint8 SpatialFunctionalTestFlowControllerSpawner::OwningServerIntanceId(UWorld* USpatialNetDriver* SpatialNetDriver = Cast(World->GetNetDriver()); if (SpatialNetDriver == nullptr || SpatialNetDriver->LoadBalanceStrategy == nullptr) { - //not load balanced test, default instance 1 + // not load balanced test, default instance 1 return 1; } else @@ -86,7 +92,7 @@ uint8 SpatialFunctionalTestFlowControllerSpawner::OwningServerIntanceId(UWorld* void SpatialFunctionalTestFlowControllerSpawner::LockFlowControllerDelegations(ASpatialFunctionalTestFlowController* FlowController) const { USpatialNetDriver* SpatialNetDriver = Cast(FlowController->GetNetDriver()); - if(SpatialNetDriver == nullptr || SpatialNetDriver->LoadBalanceStrategy == nullptr) + if (SpatialNetDriver == nullptr || SpatialNetDriver->LoadBalanceStrategy == nullptr) { return; } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestGridLBStrategy.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestGridLBStrategy.cpp deleted file mode 100644 index abb95e80cf..0000000000 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestGridLBStrategy.cpp +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - - -#include "SpatialFunctionalTestGridLBStrategy.h" -#include "GameFramework/Actor.h" -#include "SpatialFunctionalTestWorkerDelegationComponent.h" - -USpatialFunctionalTestGridLBStrategy::USpatialFunctionalTestGridLBStrategy() - : Super() -{ - Rows = 2; - Cols = 2; -} - -bool USpatialFunctionalTestGridLBStrategy::ShouldHaveAuthority(const AActor& Actor) const -{ - USpatialFunctionalTestWorkerDelegationComponent* DelegationComponent = Actor.FindComponentByClass(); - - if (DelegationComponent != nullptr) - { - return GetLocalVirtualWorkerId() == DelegationComponent->WorkerId; - } - return Super::ShouldHaveAuthority(Actor); -} - -VirtualWorkerId USpatialFunctionalTestGridLBStrategy::WhoShouldHaveAuthority(const AActor& Actor) const -{ - USpatialFunctionalTestWorkerDelegationComponent* DelegationComponent = Actor.FindComponentByClass(); - - if (DelegationComponent != nullptr) - { - return DelegationComponent->WorkerId; - } - return Super::WhoShouldHaveAuthority(Actor); -} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestLBDelegationInterface.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestLBDelegationInterface.cpp deleted file mode 100644 index 2f466f1284..0000000000 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestLBDelegationInterface.cpp +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - - -#include "SpatialFunctionalTestLBDelegationInterface.h" -#include "GameFramework/Actor.h" -#include "SpatialCommonTypes.h" -#include "Utils/SpatialStatics.h" -#include "SpatialFunctionalTestWorkerDelegationComponent.h" -#include "EngineUtils.h" - -bool ISpatialFunctionalTestLBDelegationInterface::AddActorDelegation(AActor* Actor, VirtualWorkerId WorkerId, bool bPersistOnTestFinished /*= false*/) -{ - if (Actor == nullptr) - { - return false; - } - - if (!Actor->HasAuthority()) - { - ensureMsgf(false, TEXT("Only the worker authoritative over an Actor can delegate it to another worker. Tried to delegate %s to %d"), *Actor->GetName(), WorkerId); - return false; - } - - USpatialFunctionalTestWorkerDelegationComponent* DelegationComponent = Cast(Actor->GetComponentByClass(USpatialFunctionalTestWorkerDelegationComponent::StaticClass())); - - if( DelegationComponent == nullptr ) - { - DelegationComponent = NewObject(Actor, "Delegation Component"); - DelegationComponent->RegisterComponent(); - } - - DelegationComponent->WorkerId = WorkerId; - DelegationComponent->bIsPersistent = bPersistOnTestFinished; - - return true; -} - -bool ISpatialFunctionalTestLBDelegationInterface::RemoveActorDelegation(AActor* Actor) -{ - if (Actor == nullptr) - { - return false; - } - - USpatialFunctionalTestWorkerDelegationComponent* DelegationComponent = Actor->FindComponentByClass(); - if (DelegationComponent == nullptr) - { - return false; - } - - DelegationComponent->DestroyComponent(); - - return true; -} - -bool ISpatialFunctionalTestLBDelegationInterface::HasActorDelegation(AActor* Actor, VirtualWorkerId& WorkerId, bool& bIsPersistent) -{ - WorkerId = 0; - bIsPersistent = false; - - if (Actor == nullptr) - { - return false; - } - USpatialFunctionalTestWorkerDelegationComponent* DelegationComponent = Actor->FindComponentByClass(); - - if (DelegationComponent != nullptr) - { - WorkerId = DelegationComponent->WorkerId; - bIsPersistent = DelegationComponent->bIsPersistent; - return true; - } - - return false; -} - -void ISpatialFunctionalTestLBDelegationInterface::RemoveAllActorDelegations(UWorld* World, bool bRemovePersistent /*= false*/) -{ - for(TActorIterator It(World); It; ++It) - { - if( It->HasAuthority() ) - { - USpatialFunctionalTestWorkerDelegationComponent* DelegationComponent = It->FindComponentByClass(); - if(DelegationComponent != nullptr) - { - if (!DelegationComponent->bIsPersistent || bRemovePersistent) - { - DelegationComponent->DestroyComponent(); - } - } - } - } -} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestRequireHandler.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestRequireHandler.cpp new file mode 100644 index 0000000000..a12505b364 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestRequireHandler.cpp @@ -0,0 +1,398 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialFunctionalTestRequireHandler.h" +#include "Logging/LogMacros.h" +#include "Misc/AssertionMacros.h" +#include "SpatialFunctionalTest.h" +#include "SpatialFunctionalTestFlowController.h" +#include "SpatialGDKFunctionalTestsPrivate.h" +#include "VisualLogger/VisualLogger.h" + +namespace +{ +template +bool Compare(const T& A, EComparisonMethod Operator, const T& B) +{ + bool bPassed = false; + switch (Operator) + { + case EComparisonMethod::Equal_To: + bPassed = A == B; + break; + case EComparisonMethod::Not_Equal_To: + bPassed = A != B; + break; + case EComparisonMethod::Greater_Than_Or_Equal_To: + bPassed = A >= B; + break; + case EComparisonMethod::Less_Than_Or_Equal_To: + bPassed = A <= B; + break; + case EComparisonMethod::Greater_Than: + bPassed = A > B; + break; + case EComparisonMethod::Less_Than: + bPassed = A < B; + break; + default: + checkNoEntry(); + break; + } + return bPassed; +} + +FString GetComparisonMethodAsString(EComparisonMethod Operator) +{ + switch (Operator) + { + case EComparisonMethod::Equal_To: + return TEXT("=="); + case EComparisonMethod::Not_Equal_To: + return TEXT("!="); + case EComparisonMethod::Greater_Than_Or_Equal_To: + return TEXT(">="); + case EComparisonMethod::Less_Than_Or_Equal_To: + return TEXT("<="); + case EComparisonMethod::Greater_Than: + return TEXT(">"); + case EComparisonMethod::Less_Than: + return TEXT("<"); + default: + checkNoEntry(); + break; + } + + return FString(); // For compilation. +} + +FString GetTransformAsString(const FTransform& Transform) +{ + FVector T = Transform.GetLocation(); + FRotator R = Transform.Rotator(); + FVector S = Transform.GetScale3D(); + return FString::Printf(TEXT("T(%f,%f,%f) | R(%f, %f, %f), S(%f, %f, %f)"), T.X, T.Y, T.Z, R.Pitch, R.Yaw, R.Roll, S.X, S.Y, S.Z); +} +} // namespace + +SpatialFunctionalTestRequireHandler::SpatialFunctionalTestRequireHandler() + : NextOrder(0) +{ +} + +void SpatialFunctionalTestRequireHandler::RequireTrue(bool bCheckTrue, const FString& Msg) +{ + GenericRequire(Msg, bCheckTrue, FString()); +} + +void SpatialFunctionalTestRequireHandler::RequireFalse(bool bCheckFalse, const FString& Msg) +{ + GenericRequire(Msg, !bCheckFalse, FString()); +} + +void SpatialFunctionalTestRequireHandler::RequireCompare(int A, EComparisonMethod Operator, int B, const FString& Msg) +{ + bool bPassed = Compare(A, Operator, B); + + FString ErrorMsg; + + if (!bPassed) + { + FString OperatorStr = GetComparisonMethodAsString(Operator); + ErrorMsg = FString::Printf(TEXT("Received %d %s %d but was expecting A %s B"), A, *OperatorStr, B, *OperatorStr); + } + + GenericRequire(Msg, bPassed, ErrorMsg); +} + +void SpatialFunctionalTestRequireHandler::RequireCompare(float A, EComparisonMethod Operator, float B, const FString& Msg) +{ + bool bPassed = Compare(A, Operator, B); + + FString ErrorMsg; + + if (!bPassed) + { + FString OperatorStr = GetComparisonMethodAsString(Operator); + ErrorMsg = FString::Printf(TEXT("Received %f %s %f but was expected A %s B"), A, *OperatorStr, B, *OperatorStr); + } + + GenericRequire(Msg, bPassed, ErrorMsg); +} + +void SpatialFunctionalTestRequireHandler::RequireEqual(bool bValue, bool bExpected, const FString& Msg) +{ + bool bPassed = bValue == bExpected; + FString ErrorMsg; + + if (!bPassed) + { + FString ValueStr = bValue ? TEXT("True") : TEXT("False"); + FString ExpectedStr = bExpected ? TEXT("True") : TEXT("False"); + ErrorMsg = FString::Printf(TEXT("Received %s but was expecting %s"), *ValueStr, *ExpectedStr); + } + + GenericRequire(Msg, bPassed, ErrorMsg); +} + +void SpatialFunctionalTestRequireHandler::RequireEqual(int Value, int Expected, const FString& Msg) +{ + bool bPassed = Value == Expected; + FString ErrorMsg; + + if (!bPassed) + { + ErrorMsg = FString::Printf(TEXT("Received %d but was expecting %d"), Value, Expected); + } + + GenericRequire(Msg, bPassed, ErrorMsg); +} + +void SpatialFunctionalTestRequireHandler::RequireEqual(float Value, float Expected, const FString& Msg, float Tolerance) +{ + bool bPassed = FMath::Abs(Value - Expected) <= Tolerance; + FString ErrorMsg; + + if (!bPassed) + { + ErrorMsg = FString::Printf(TEXT("Received %f but was expecting %f (tolerance %f)"), Value, Expected, Tolerance); + } + + GenericRequire(Msg, bPassed, ErrorMsg); +} + +void SpatialFunctionalTestRequireHandler::RequireEqual(const FString& Value, const FString& Expected, const FString& Msg) +{ + bool bPassed = Value == Expected; + FString ErrorMsg; + + if (!bPassed) + { + ErrorMsg = FString::Printf(TEXT("Received %s but was expecting %s"), *Value, *Expected); + } + + GenericRequire(Msg, bPassed, ErrorMsg); +} + +void SpatialFunctionalTestRequireHandler::RequireEqual(const FName& Value, const FName& Expected, const FString& Msg) +{ + bool bPassed = Value == Expected; + FString ErrorMsg; + + if (!bPassed) + { + ErrorMsg = FString::Printf(TEXT("Received %s but was expecting %s"), *Value.ToString(), *Expected.ToString()); + } + + GenericRequire(Msg, bPassed, ErrorMsg); +} + +void SpatialFunctionalTestRequireHandler::RequireEqual(const FVector& Value, const FVector& Expected, const FString& Msg, float Tolerance) +{ + bool bPassed = Value.Equals(Expected, Tolerance); + FString ErrorMsg; + + if (!bPassed) + { + ErrorMsg = FString::Printf(TEXT("Received (%s) but was expecting (%s) (tolerance %f)"), *Value.ToString(), *Expected.ToString(), + Tolerance); + } + + GenericRequire(Msg, bPassed, ErrorMsg); +} + +void SpatialFunctionalTestRequireHandler::RequireEqual(const FRotator& Value, const FRotator& Expected, const FString& Msg, float Tolerance) +{ + bool bPassed = Value.Equals(Expected, Tolerance); + FString ErrorMsg; + + if (!bPassed) + { + ErrorMsg = FString::Printf(TEXT("Received (%s) but was expecting (%s) (tolerance %f)"), *Value.ToString(), *Expected.ToString(), + Tolerance); + } + + GenericRequire(Msg, bPassed, ErrorMsg); +} + +void SpatialFunctionalTestRequireHandler::RequireEqual(const FTransform& Value, const FTransform& Expected, const FString& Msg, + float Tolerance) +{ + bool bPassed = Value.Equals(Expected, Tolerance); + FString ErrorMsg; + + if (!bPassed) + { + ErrorMsg = FString::Printf(TEXT("Received {%s} but was expecting {%s} (tolerance %f)"), *GetTransformAsString(Value), + *GetTransformAsString(Expected), Tolerance); + } + + GenericRequire(Msg, bPassed, ErrorMsg); +} + +void SpatialFunctionalTestRequireHandler::RequireNotEqual(bool bValue, bool bNotExpected, const FString& Msg) +{ + bool bPassed = bValue != bNotExpected; + FString ErrorMsg; + + if (!bPassed) + { + FString ValueStr = bValue ? TEXT("True") : TEXT("False"); + ErrorMsg = FString::Printf(TEXT("Received %s but wasn't expecting it"), *ValueStr); + } + + GenericRequire(Msg, bPassed, ErrorMsg); +} + +void SpatialFunctionalTestRequireHandler::RequireNotEqual(int Value, int NotExpected, const FString& Msg) +{ + bool bPassed = Value != NotExpected; + FString ErrorMsg; + + if (!bPassed) + { + ErrorMsg = FString::Printf(TEXT("Received %d but wasn't expecting it"), Value); + } + + GenericRequire(Msg, bPassed, ErrorMsg); +} + +void SpatialFunctionalTestRequireHandler::RequireNotEqual(float Value, float NotExpected, const FString& Msg) +{ + bool bPassed = Value != NotExpected; + FString ErrorMsg; + + if (!bPassed) + { + ErrorMsg = FString::Printf(TEXT("Received %f but wasn't expecting it"), Value); + } + + GenericRequire(Msg, bPassed, ErrorMsg); +} + +void SpatialFunctionalTestRequireHandler::RequireNotEqual(const FString& Value, const FString& NotExpected, const FString& Msg) +{ + bool bPassed = Value != NotExpected; + FString ErrorMsg; + + if (!bPassed) + { + ErrorMsg = FString::Printf(TEXT("Received %s but wasn't expecting it"), *Value); + } + + GenericRequire(Msg, bPassed, ErrorMsg); +} + +void SpatialFunctionalTestRequireHandler::RequireNotEqual(const FName& Value, const FName& NotExpected, const FString& Msg) +{ + bool bPassed = Value != NotExpected; + FString ErrorMsg; + + if (!bPassed) + { + ErrorMsg = FString::Printf(TEXT("Received %s but wasn't expecting it"), *Value.ToString()); + } + + GenericRequire(Msg, bPassed, ErrorMsg); +} + +void SpatialFunctionalTestRequireHandler::RequireNotEqual(const FVector& Value, const FVector& NotExpected, const FString& Msg) +{ + bool bPassed = Value != NotExpected; + FString ErrorMsg; + + if (!bPassed) + { + ErrorMsg = FString::Printf(TEXT("Received (%s) but wasn't expecting it"), *Value.ToString()); + } + + GenericRequire(Msg, bPassed, ErrorMsg); +} + +void SpatialFunctionalTestRequireHandler::RequireNotEqual(const FRotator& Value, const FRotator& NotExpected, const FString& Msg) +{ + bool bPassed = Value != NotExpected; + FString ErrorMsg; + + if (!bPassed) + { + ErrorMsg = FString::Printf(TEXT("Received (%s) but wasn't expecting it"), *Value.ToString()); + } + + GenericRequire(Msg, bPassed, ErrorMsg); +} + +void SpatialFunctionalTestRequireHandler::RequireNotEqual(const FTransform& Value, const FTransform& NotExpected, const FString& Msg) +{ + bool bPassed = !Value.Equals(NotExpected); + FString ErrorMsg; + + if (!bPassed) + { + ErrorMsg = FString::Printf(TEXT("Received {%s} but wasn't expecting it"), *GetTransformAsString(Value)); + } + + GenericRequire(Msg, bPassed, ErrorMsg); +} + +void SpatialFunctionalTestRequireHandler::GenericRequire(const FString& Msg, bool bPassed, const FString& ErrorMsg) +{ + ensureMsgf(!Msg.IsEmpty(), TEXT("Requires cannot have an empty message")); + + FSpatialFunctionalTestRequire Require; + Require.Msg = Msg; + Require.bPassed = bPassed; + Require.ErrorMsg = ErrorMsg; + Require.Order = NextOrder++; + + Requires.Add(Msg, Require); +} + +void SpatialFunctionalTestRequireHandler::LogAndClearStepRequires() +{ + // Since it's a TMap, we need to order them for better readability. + TArray RequiresOrdered; + RequiresOrdered.Reserve(Requires.Num()); + + for (const auto& RequireEntry : Requires) + { + RequiresOrdered.Add(RequireEntry.Value); + } + + RequiresOrdered.Sort([](const FSpatialFunctionalTestRequire& A, const FSpatialFunctionalTestRequire& B) -> bool { + return A.Order < B.Order; + }); + + const FString& WorkerName = OwnerTest->GetLocalFlowController()->GetDisplayName(); + + for (const auto& Require : RequiresOrdered) + { + FString Msg; + if (Require.bPassed) + { + Msg = FString::Printf(TEXT("%s [Passed] %s"), *WorkerName, *Require.Msg); + UE_VLOG(nullptr, LogSpatialGDKFunctionalTests, Display, TEXT("%s"), *Msg); + UE_LOG(LogSpatialGDKFunctionalTests, Display, TEXT("%s"), *Msg); + } + else + { + Msg = FString::Printf(TEXT("%s [Failed] %s : %s"), *WorkerName, *Require.Msg, *Require.ErrorMsg); + UE_VLOG(nullptr, LogSpatialGDKFunctionalTests, Error, TEXT("%s"), *Msg); + UE_LOG(LogSpatialGDKFunctionalTests, Error, TEXT("%s"), *Msg); + } + } + + NextOrder = 0; + Requires.Empty(); +} + +bool SpatialFunctionalTestRequireHandler::HasFails() +{ + for (const auto& RequireEntry : Requires) + { + if (!RequireEntry.Value.bPassed) + { + return true; + } + } + return false; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestStep.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestStep.cpp index 097fd1fc96..9e569bea9a 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestStep.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestStep.cpp @@ -2,13 +2,16 @@ #include "SpatialFunctionalTestStep.h" -const FWorkerDefinition FWorkerDefinition::AllWorkers = FWorkerDefinition{ ESpatialFunctionalTestWorkerType::All, FWorkerDefinition::ALL_WORKERS_ID }; -const FWorkerDefinition FWorkerDefinition::AllServers = FWorkerDefinition{ ESpatialFunctionalTestWorkerType::Server, FWorkerDefinition::ALL_WORKERS_ID }; -const FWorkerDefinition FWorkerDefinition::AllClients = FWorkerDefinition{ ESpatialFunctionalTestWorkerType::Client, FWorkerDefinition::ALL_WORKERS_ID }; +const FWorkerDefinition FWorkerDefinition::AllWorkers = + FWorkerDefinition{ ESpatialFunctionalTestWorkerType::All, FWorkerDefinition::ALL_WORKERS_ID }; +const FWorkerDefinition FWorkerDefinition::AllServers = + FWorkerDefinition{ ESpatialFunctionalTestWorkerType::Server, FWorkerDefinition::ALL_WORKERS_ID }; +const FWorkerDefinition FWorkerDefinition::AllClients = + FWorkerDefinition{ ESpatialFunctionalTestWorkerType::Client, FWorkerDefinition::ALL_WORKERS_ID }; FWorkerDefinition FWorkerDefinition::Server(int ServerId) { - return FWorkerDefinition{ESpatialFunctionalTestWorkerType::Server, ServerId}; + return FWorkerDefinition{ ESpatialFunctionalTestWorkerType::Server, ServerId }; } FWorkerDefinition FWorkerDefinition::Client(int ClientId) @@ -44,7 +47,7 @@ void SpatialFunctionalTestStep::Tick(float DeltaTime) { if (StepDefinition.bIsNativeDefinition) { - bIsReady = StepDefinition.NativeIsReadyEvent.Execute(Owner); + bIsReady = StepDefinition.NativeIsReadyEvent.Execute(); } else { @@ -60,7 +63,7 @@ void SpatialFunctionalTestStep::Tick(float DeltaTime) { if (StepDefinition.bIsNativeDefinition) { - StepDefinition.NativeStartEvent.ExecuteIfBound(Owner); + StepDefinition.NativeStartEvent.ExecuteIfBound(); } else { @@ -68,12 +71,12 @@ void SpatialFunctionalTestStep::Tick(float DeltaTime) } } } - + if (bIsReady) { if (StepDefinition.bIsNativeDefinition) { - StepDefinition.NativeTickEvent.ExecuteIfBound(Owner, DeltaTime); + StepDefinition.NativeTickEvent.ExecuteIfBound(DeltaTime); } else { diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestWorkerDelegationComponent.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestWorkerDelegationComponent.cpp deleted file mode 100644 index aa92db0b8f..0000000000 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialFunctionalTestWorkerDelegationComponent.cpp +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#include "SpatialFunctionalTestWorkerDelegationComponent.h" -#include "Net/UnrealNetwork.h" - -USpatialFunctionalTestWorkerDelegationComponent::USpatialFunctionalTestWorkerDelegationComponent() - : Super() -{ -#if ENGINE_MINOR_VERSION <= 23 - bReplicates = true; -#else - SetIsReplicatedByDefault(true); -#endif -} - -void USpatialFunctionalTestWorkerDelegationComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const -{ - Super::GetLifetimeReplicatedProps(OutLifetimeProps); - - DOREPLIFETIME(USpatialFunctionalTestWorkerDelegationComponent, WorkerId); - DOREPLIFETIME(USpatialFunctionalTestWorkerDelegationComponent, bIsPersistent); -} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialGDKFunctionalTestsModule.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialGDKFunctionalTestsModule.cpp index ecd15a9714..9cdab916e8 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialGDKFunctionalTestsModule.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/SpatialGDKFunctionalTestsModule.cpp @@ -10,12 +10,8 @@ DEFINE_LOG_CATEGORY(LogSpatialGDKFunctionalTests); IMPLEMENT_MODULE(FSpatialGDKFunctionalTestsModule, SpatialGDKFunctionalTests); -void FSpatialGDKFunctionalTestsModule::StartupModule() -{ -} +void FSpatialGDKFunctionalTestsModule::StartupModule() {} -void FSpatialGDKFunctionalTestsModule::ShutdownModule() -{ -} +void FSpatialGDKFunctionalTestsModule::ShutdownModule() {} #undef LOCTEXT_NAMESPACE diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/Test1x2GridStrategy.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/Test1x2GridStrategy.cpp index d08f0a2273..8d075b82f2 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/Test1x2GridStrategy.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Private/Test1x2GridStrategy.cpp @@ -1,6 +1,5 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved - #include "Test1x2GridStrategy.h" UTest1x2GridStrategy::UTest1x2GridStrategy() diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTest.h index 08d1d1acec..f755a13aee 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTest.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTest.h @@ -3,31 +3,41 @@ #pragma once #include "CoreMinimal.h" -#include "FunctionalTest.h" #include "Engine/World.h" #include "EngineUtils.h" +#include "FunctionalTest.h" +#include "Improbable/SpatialGDKSettingsBridge.h" #include "SpatialFunctionalTestFlowControllerSpawner.h" +#include "SpatialFunctionalTestRequireHandler.h" #include "SpatialFunctionalTestStep.h" -#include "SpatialFunctionalTestLBDelegationInterface.h" #include "SpatialFunctionalTest.generated.h" -namespace +// Blueprint Delegate +DECLARE_DYNAMIC_DELEGATE_OneParam(FSpatialFunctionalTestSnapshotTakenDelegate, bool, bSuccess); + +namespace { - typedef TFunction FIsReadyEventFunc; - typedef TFunction FStartEventFunc; - typedef TFunction FTickEventFunc; +// C++ hooks for Lambdas. +typedef TFunction FIsReadyEventFunc; +typedef TFunction FStartEventFunc; +typedef TFunction FTickEventFunc; + +typedef TFunction FSnapshotTakenFunc; + +// We need 2 values since the way we clean up tests is based on replication of variables, +// so if the test fails to start, the cleanup process would never be triggered. +constexpr int SPATIAL_FUNCTIONAL_TEST_NOT_STARTED = -1; // Represents test waiting to run. +constexpr int SPATIAL_FUNCTIONAL_TEST_FINISHED = -2; // Represents test already ran. +} // namespace - // we need 2 values since the way we clean up tests is based on replication of variables, - // so if the test fails to start, the cleanup process would never be triggered - constexpr int SPATIAL_FUNCTIONAL_TEST_NOT_STARTED = -1; // represents test waiting to run - constexpr int SPATIAL_FUNCTIONAL_TEST_FINISHED = -2; // represents test already ran -} +class ULayeredLBStrategy; /* * A Spatial Functional NetTest allows you to define a series of steps, and control which server/client context they execute on * Servers and Clients are registered as Test Players by the framework, and request individual steps to be executed in the correct Player */ -UCLASS(Blueprintable, hidecategories = (Input, Movement, Collision, Rendering, Replication, LOD, "Utilities|Transformation")) +UCLASS(Blueprintable, SpatialType = NotPersistent, + hidecategories = (Input, Movement, Collision, Rendering, Replication, LOD, "Utilities|Transformation")) class SPATIALGDKFUNCTIONALTESTS_API ASpatialFunctionalTest : public AFunctionalTest { GENERATED_BODY() @@ -38,13 +48,13 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialFunctionalTest : public AFunctionalT private: SpatialFunctionalTestFlowControllerSpawner FlowControllerSpawner; - UPROPERTY(ReplicatedUsing=StartServerFlowControllerSpawn) + UPROPERTY(ReplicatedUsing = StartServerFlowControllerSpawn) uint8 bReadyToSpawnServerControllers : 1; public: ASpatialFunctionalTest(); - virtual void GetLifetimeReplicatedProps(TArray< FLifetimeProperty >& OutLifetimeProps) const override; + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; virtual void BeginPlay() override; @@ -52,107 +62,282 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialFunctionalTest : public AFunctionalT virtual void OnAuthorityGained() override; - // Should be called from the server with authority over this actor + // Should be called from the server with authority over this actor. virtual void RegisterAutoDestroyActor(AActor* ActorToAutoDestroy) override; + virtual void LogStep(ELogVerbosity::Type Verbosity, const FString& Message) override; + // # Test APIs int GetNumRequiredClients() const { return NumRequiredClients; } - // Starts being called after PrepareTest, until it returns true + // Called at the beginning of the test, use it to setup your steps. Contrary to AFunctionalTest, this will + // run on all Workers (Server and Client). + virtual void PrepareTest() override; + + // Lets you know if PrepareTest() has been called. + bool HasPreparedTest() const { return bPreparedTest; } + + // Starts being called after PrepareTest, until it returns true. This is only called on Authority. virtual bool IsReady_Implementation() override; - // Called once after IsReady is true - virtual void StartTest() override; + // Called once after IsReady is true. This is only called on Authority. + virtual void StartTest() override; // Ends the Test, can be called from any place. virtual void FinishTest(EFunctionalTestResult TestResult, const FString& Message) override; + // Add expected log errors in C++. This can only be called when setting up the steps in PrepareTest() or in + // the steps themselves. Keep in mind that if the expected number of occurrences aren't met, the test fails. + // The same pattern can only be added once, so make sure to execute only in the test Authority or in a step that + // is running only on one worker. + void AddExpectedLogError(const FString& ExpectedPatternString, int32 Occurrences = 1, bool bExactMatch = false); + UFUNCTION(CrossServer, Reliable) void CrossServerFinishTest(EFunctionalTestResult TestResult, const FString& Message); - + UFUNCTION(CrossServer, Reliable) - void CrossServerNotifyStepFinished(ASpatialFunctionalTestFlowController* FlowController); + void CrossServerNotifyStepFinished(ASpatialFunctionalTestFlowController* FlowController, const int StepIndex); + + // # FlowController related APIs. - // # FlowController related APIs - void RegisterFlowController(ASpatialFunctionalTestFlowController* FlowController); // Get all the FlowControllers registered in this Test. const TArray& GetFlowControllers() const { return FlowControllers; } - UFUNCTION(BlueprintPure, Category = "Spatial Functional Test", meta = (WorkerId = "1", ToolTip = "Returns the FlowController for a specific Server / Client.\nKeep in mind that WorkerIds start from 1, and the Server's WorkerId will match their VirtualWorkerId while the Client's will be based on the order they connect.\n\n'All' Worker type will soft assert as it isn't supported.")) + // clang-format off + UFUNCTION(BlueprintPure, Category = "Spatial Functional Test", meta = (WorkerId = "1", + ToolTip = "Returns the FlowController for a specific Server / Client.\nKeep in mind that WorkerIds start from 1, and the Server's WorkerId will match their VirtualWorkerId while the Client's will be based on the order they connect.\n\n'All' Worker type will soft assert as it isn't supported.")) + // clang-format on ASpatialFunctionalTestFlowController* GetFlowController(ESpatialFunctionalTestWorkerType WorkerType, int WorkerId); - // Get the FlowController that is Local to this instance + // Get the FlowController that is Local to this instance. UFUNCTION(BlueprintPure, Category = "Spatial Functional Test") ASpatialFunctionalTestFlowController* GetLocalFlowController(); - // # Step APIs + // Helper to get the local Worker Type. + UFUNCTION(BlueprintPure, Category = "Spatial Functional Test") + ESpatialFunctionalTestWorkerType GetLocalWorkerType(); + + // Helper to get the local Worker Id. + UFUNCTION(BlueprintPure, Category = "Spatial Functional Test") + int GetLocalWorkerId(); + + // # Step APIs. - // Add Steps for Blueprints - - UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test", meta = (DisplayName = "Add Step", AutoCreateRefTerm = "IsReadyEvent,StartEvent,TickEvent", ToolTip = "Adds a Test Step. Check GetAllWorkers(), GetAllServerWorkers() and GetAllClientWorkers() for convenience.\n\nIf you split the Worker pin you can define if you want to run on Server, Client or All.\n\nWorker Ids start from 1.\nIf you pass 0 it will run on all the Servers / Clients (there's also a convenience function GetAllWorkersId())\n\nIf you choose WorkerType 'All' it runs on all Servers and Clients (hence WorkerId is ignored).\n\nKeep in mind you can split the Worker pin for convenience.")) - void AddStepBlueprint(const FString& StepName, const FWorkerDefinition& Worker, const FStepIsReadyDelegate& IsReadyEvent, const FStepStartDelegate& StartEvent, const FStepTickDelegate& TickEvent, float StepTimeLimit = 0.0f); + // Add Steps for Blueprints. - // Add Steps for Blueprints and C++ + // clang-format off + UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test", meta = (DisplayName = "Add Step", AutoCreateRefTerm = "IsReadyEvent,StartEvent,TickEvent", + ToolTip = "Adds a Test Step. Check GetAllWorkers(), GetAllServerWorkers() and GetAllClientWorkers() for convenience.\n\nIf you split the Worker pin you can define if you want to run on Server, Client or All.\n\nWorker Ids start from 1.\nIf you pass 0 it will run on all the Servers / Clients (there's also a convenience function GetAllWorkersId())\n\nIf you choose WorkerType 'All' it runs on all Servers and Clients (hence WorkerId is ignored).\n\nKeep in mind you can split the Worker pin for convenience.")) + // clang-format on + void AddStepBlueprint(const FString& StepName, const FWorkerDefinition& Worker, const FStepIsReadyDelegate& IsReadyEvent, + const FStepStartDelegate& StartEvent, const FStepTickDelegate& TickEvent, float StepTimeLimit = 0.0f); - UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test", meta = (ToolTip = "Adds a Step from a Definition. Allows you to define a Step and add it / re-use it multiple times.\n\nKeep in mind you can split the Worker pin for convenience.")) + // Add Steps for Blueprints and C++. + + // clang-format off + UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test", + meta = (ToolTip = "Adds a Step from a Definition. Allows you to define a Step and add it / re-use it multiple times.\n\nKeep in mind you can split the Worker pin for convenience.")) + // clang-format on void AddStepFromDefinition(const FSpatialFunctionalTestStepDefinition& StepDefinition, const FWorkerDefinition& Worker); - UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test", meta = (ToolTip = "Adds a Step from a Definition. Allows you to define a Step and add it / re-use it multiple times.\n\nKeep in mind you can split the Worker pin for convenience.\nIt is a more extensible version of AddStepFromDefinition(), where you can pass an array with multiple specific Workers.")) + // clang-format off + UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test", + meta = (ToolTip = "Adds a Step from a Definition. Allows you to define a Step and add it / re-use it multiple times.\n\nKeep in mind you can split the Worker pin for convenience.\nIt is a more extensible version of AddStepFromDefinition(), where you can pass an array with multiple specific Workers.")) + // clang-format on void AddStepFromDefinitionMulti(const FSpatialFunctionalTestStepDefinition& StepDefinition, const TArray& Workers); - // Add Steps for C++ + // Add Steps for C++. /** * Adds a Step to the Test. You can define if you want to run on Server, Client or All. * There's helpers in FWorkerDefinition to make it easier / more concise. If you want to make a FWorkerDefinition from scratch, * keep in mind that Worker Ids start from 1. If you pass FWorkerDefinition::ALL_WORKERS_ID (GetAllWorkersId()) it will * run on all the Servers / Clients. If you pass WorkerType 'All' it runs on all Servers and Clients (hence WorkerId is ignored). */ - FSpatialFunctionalTestStepDefinition& AddStep(const FString& StepName, const FWorkerDefinition& Worker, FIsReadyEventFunc IsReadyEvent = nullptr, FStartEventFunc StartEvent = nullptr, FTickEventFunc TickEvent = nullptr, float StepTimeLimit = 0.0f); + FSpatialFunctionalTestStepDefinition& AddStep(const FString& StepName, const FWorkerDefinition& Worker, + FIsReadyEventFunc IsReadyEvent = nullptr, FStartEventFunc StartEvent = nullptr, + FTickEventFunc TickEvent = nullptr, float StepTimeLimit = 0.0f); - // Start Running a Step + // Start Running a Step. void StartStep(const int StepIndex); - // Terminate current Running Step (called once per FlowController executing it) + // Terminate the current running step (called once per FlowController executing it) if you have no failing Requires. + // If you have failed Requires it will be ignored, making it easier for you to build tests without + // having to manually check that there's no failed Requires before finishing the step. UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test") - void FinishStep(); + virtual void FinishStep(); const FSpatialFunctionalTestStepDefinition GetStepDefinition(int StepIndex) const; - + int GetCurrentStepIndex() { return CurrentStepIndex; } + void SetCurrentStepIndex(const int StepIndex) { CurrentStepIndex = StepIndex; } - // Convenience function that goes over all FlowControllers and counts how many are Servers + // Convenience function that goes over all FlowControllers and counts how many are Servers. UFUNCTION(BlueprintPure, Category = "Spatial Functional Test") int GetNumberOfServerWorkers(); - // Convenience function that goes over all FlowControllers and counts how many are Clients + // Convenience function that goes over all FlowControllers and counts how many are Clients. UFUNCTION(BlueprintPure, Category = "Spatial Functional Test") int GetNumberOfClientWorkers(); - // Convenience function that returns the Id used for executing steps on all Servers / Clients - UFUNCTION(BlueprintPure, meta = (ToolTip = "Returns the Id (0) that represents all Workers (ie Server / Client), useful for when you want to have a Server / Client Step run on all of them"), Category = "Spatial Functional Test") + // Convenience function that returns the Id used for executing steps on all Servers / Clients. + // clang-format off + UFUNCTION(BlueprintPure, + meta = (ToolTip = "Returns the Id (0) that represents all Workers (ie Server / Client), useful for when you want to have a Server / Client Step run on all of them"), + Category = "Spatial Functional Test") + // clang-format on int GetAllWorkersId() { return FWorkerDefinition::ALL_WORKERS_ID; } - UFUNCTION(BlueprintPure, meta = (ToolTip = "Returns a Worker Defnition that represents all of the Servers and Clients"), Category = "Spatial Functional Test") + UFUNCTION(BlueprintPure, meta = (ToolTip = "Returns a Worker Defnition that represents all of the Servers and Clients"), + Category = "Spatial Functional Test") FWorkerDefinition GetAllWorkers() { return FWorkerDefinition::AllWorkers; } - UFUNCTION(BlueprintPure, meta = (ToolTip = "Returns a Worker Defnition that represents all of the Servers"), Category = "Spatial Functional Test") + UFUNCTION(BlueprintPure, meta = (ToolTip = "Returns a Worker Defnition that represents all of the Servers"), + Category = "Spatial Functional Test") FWorkerDefinition GetAllServers() { return FWorkerDefinition::AllServers; } - UFUNCTION(BlueprintPure, meta = (ToolTip = "Returns a Worker Defnition that represents all of the Clients"), Category = "Spatial Functional Test") + UFUNCTION(BlueprintPure, meta = (ToolTip = "Returns a Worker Defnition that represents all of the Clients"), + Category = "Spatial Functional Test") FWorkerDefinition GetAllClients() { return FWorkerDefinition::AllClients; } - // # Actor Delegation APIs - UFUNCTION(CrossServer, Reliable, BlueprintCallable, Category = "Spatial Functional Test", meta=(ToolTip="Allows you to delegate authority over this Actor to a specific Server Worker. \n\nKeep in mind that currently this functionality only works in single layer Load Balancing Strategies, and your Default Load Balancing Strategy needs to implement ISpatialFunctionalTestLBDelegationInterface.", ServerWorkerId = "1")) - void AddActorDelegation(AActor* Actor, int ServerWorkerId, bool bPersistOnTestFinished = false); + ULayeredLBStrategy* GetLoadBalancingStrategy(); + + UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test", + meta = (ToolTip = "Add a debug tag to the given Actor that will be matched with interest and delegation declarations.")) + void AddDebugTag(AActor* Actor, FName Tag); + + UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test", meta = (ToolTip = "Add a debug tag from the given Actor.")) + void RemoveDebugTag(AActor* Actor, FName Tag); + + UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test", + meta = (ToolTip = "Add extra interest queries, allowing the current worker to see all Actors having the given tag.")) + void AddInterestOnTag(FName Tag); + + UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test", + meta = (ToolTip = "Remove the extra interest query on the given tag.")) + void RemoveInterestOnTag(FName Tag); + + UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test", + meta = (ToolTip = "Prevent the given actor from losing authority from this server worker.")) + void KeepActorOnCurrentWorker(AActor* Actor); + + // clang-format off + UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test", meta = (ToolTip = "Sets a Debug Tag to be delegated to a specific Server Worker, forcing the Authority to belong to it preventing the Load-Balancing Strategy from changing it.")) + // clang-format on + void AddStepSetTagDelegation(FName Tag, int32 ServerWorkerId = 1); + + // clang-format off + UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test", meta = (ToolTip = "Clears delegation of a Debug Tag. If there's no delegation set, the Load-Balancing Strategy will decide which Server Worker should have Authority.")) + // clang-format on + void AddStepClearTagDelegation(FName Tag); + + // clang-format off + UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test", meta = (ToolTip = "Clears all Debug Tag delegations and extra interest. Note that this is called automatically when a test ends, so if you use delegation / interest in the test, you don't need to clear it manually at the end.")) + // clang-format on + void AddStepClearTagDelegationAndInterest(); + + // # Require Functions. Requires mimic the assert behaviour but without the immediate failure. Since when you're + // running networked tests you generally need to wait for state to be synced if you simply call asserts you'd get false + // negatives. These functions work in a way that they record the expected behaviour, and when we FinishStep / FinishTest + // it will let you know which of them passed and which failed. Keep in mind that failed requires will prevent FinishStep + // from moving forward, so this allows you to make tests in a simpler way without having to keep track if anything failed + // before calling FinishStep. + + // clang-format off + UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test") + void RequireTrue(bool bCheckTrue, const FString& Msg) { RequireHandler.RequireTrue(bCheckTrue, Msg); } + + UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test") + void RequireFalse(bool bCheckFalse, const FString& Msg) { RequireHandler.RequireFalse(bCheckFalse, Msg); } + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Compare (Int)"), Category = "Spatial Functional Test") + void RequireCompare_Int(int A, EComparisonMethod Operator, int B, const FString& Msg) { RequireHandler.RequireCompare(A, Operator, B, Msg); } + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Compare (Float)"), Category = "Spatial Functional Test") + void RequireCompare_Float(float A, EComparisonMethod Operator, float B, const FString& Msg) { RequireHandler.RequireCompare(A, Operator, B, Msg); } + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Equal (Bool)"), Category = "Spatial Functional Test") + void RequireEqual_Bool(bool bValue, bool bExpected, const FString& Msg) { RequireHandler.RequireEqual(bValue, bExpected, Msg); } + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Equal (Int)"), Category = "Spatial Functional Test") + void RequireEqual_Int(int Value, int Expected, const FString& Msg) { RequireHandler.RequireEqual(Value, Expected, Msg); } + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Equal (Float)"), Category = "Spatial Functional Test") + void RequireEqual_Float(float Value, float Expected, const FString& Msg, float Tolerance = 1.e-4) { RequireHandler.RequireEqual(Value, Expected, Msg, Tolerance); } + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Equal (String)"), Category = "Spatial Functional Test") + void RequireEqual_String(const FString& Value, const FString& Expected, const FString& Msg) { RequireHandler.RequireEqual(Value, Expected, Msg); } + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Equal (Name)"), Category = "Spatial Functional Test") + void RequireEqual_Name(const FName& Value, const FName& Expected, const FString& Msg) { RequireHandler.RequireEqual(Value, Expected, Msg); } + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Equal (Vector)"), Category = "Spatial Functional Test") + void RequireEqual_Vector(const FVector& Value, const FVector& Expected, const FString& Msg, float Tolerance = 1.e-4) { RequireHandler.RequireEqual(Value, Expected, Msg, Tolerance); } - UFUNCTION(CrossServer, Reliable, BlueprintCallable, Category = "Spatial Functional Test", meta = (ToolTip = "Remove Actor authority delegation, making it fallback to the Default Load Balacing Strategy. \n\nKeep in mind that currently this functionality only works in single layer Load Balancing Strategies, and your Default Load Balancing Strategy needs to implement ISpatialFunctionalTestLBDelegationInterface.")) - void RemoveActorDelegation(AActor* Actor); - - UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test", meta = (ToolTip = "Check is the Actor has it's authority delegated to a specific Server Worker. \n\nKeep in mind that currently this functionality only works in single layer Load Balancing Strategies, and your Default Load Balancing Strategy needs to implement ISpatialFunctionalTestLBDelegationInterface.")) - bool HasActorDelegation(AActor* Actor, int& WorkerId, bool& bIsPersistent); + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Equal (Rotator)"), Category = "Spatial Functional Test") + void RequireEqual_Rotator(const FRotator& Value, const FRotator& Expected, const FString& Msg, float Tolerance = 1.e-4) { RequireHandler.RequireEqual(Value, Expected, Msg, Tolerance); } + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Equal (Transform)"), Category = "Spatial Functional Test") + void RequireEqual_Transform(const FTransform& Value, const FTransform& Expected, const FString& Msg, float Tolerance = 1.e-4) { RequireHandler.RequireEqual(Value, Expected, Msg, Tolerance); } + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Not Equal (Bool)"), Category = "Spatial Functional Test") + void RequireNotEqual_Bool(bool bValue, bool bNotExpected, const FString& Msg) { RequireHandler.RequireNotEqual(bValue, bNotExpected, Msg); } + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Not Equal (Int)"), Category = "Spatial Functional Test") + void RequireNotEqual_Int(int Value, int Expected, const FString& Msg) { RequireHandler.RequireNotEqual(Value, Expected, Msg); } + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Not Equal (Float)"), Category = "Spatial Functional Test") + void RequireNotEqual_Float(float Value, float Expected, const FString& Msg) { RequireHandler.RequireNotEqual(Value, Expected, Msg); } + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Not Equal (String)"), Category = "Spatial Functional Test") + void RequireNotEqual_String(const FString& Value, const FString& Expected, const FString& Msg) { RequireHandler.RequireNotEqual(Value, Expected, Msg); } + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Not Equal (Name)"), Category = "Spatial Functional Test") + void RequireNotEqual_Name(const FName& Value, const FName& Expected, const FString& Msg) { RequireHandler.RequireNotEqual(Value, Expected, Msg); } + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Not Equal (Vector)"), Category = "Spatial Functional Test") + void RequireNotEqual_Vector(const FVector& Value, const FVector& Expected, const FString& Msg) { RequireHandler.RequireNotEqual(Value, Expected, Msg); } + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Not Equal (Rotator)"), Category = "Spatial Functional Test") + void RequireNotEqual_Rotator(const FRotator& Value, const FRotator& Expected, const FString& Msg) { RequireHandler.RequireNotEqual(Value, Expected, Msg); } + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Require Not Equal (Transform)"), Category = "Spatial Functional Test") + void RequireNotEqual_Transform(const FTransform& Value, const FTransform& Expected, const FString& Msg) { RequireHandler.RequireNotEqual(Value, Expected, Msg); } + // clang-format on + + // # Snapshot APIs. + // clang-format off + UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test", + meta = (ToolTip = "Allows a Server Worker to request a SpatialOS snapshot to be taken. Keep in mind that this should be done at the last Step of your Test. Keep in mind that if you take a snapshot, you should eventually call ClearLoadedFromTakenSnapshot.")) + // clang-format on + void TakeSnapshot(const FSpatialFunctionalTestSnapshotTakenDelegate& BlueprintCallback); + + // C++ version that allows you to hook up a lambda. + void TakeSnapshot(const FSnapshotTakenFunc& CppCallback); + + // clang-format off + UFUNCTION(BlueprintCallable, Category = "Spatial Functional Test", + meta = (Tooltip = "Clears the snapshot, making it start deployments with the default snapshot again. Tests that call TakeSnapshot should eventually also call ClearSnapshot.")) + // clang-format on + void ClearSnapshot(); + + // Allows you to know if the current deployment was started from a previously taken snapshot. + UFUNCTION(BlueprintPure, Category = "Spatial Functional Test") + bool WasLoadedFromTakenSnapshot(); + + // Get the path of the taken snapshot for this world's map. Returns an empty string if it's using the default snapshot. + static FString GetTakenSnapshotPath(UWorld* World); + + // Sets that this map was loaded by a taken snapshot, not meant to be used directly. + static void SetLoadedFromTakenSnapshot(); + + // Clears that this map was loaded by a taken snapshot, not meant to be used directly. + static void ClearLoadedFromTakenSnapshot(); + + // Clears all the snapshots taken, not meant to be used directly. + static void ClearAllTakenSnapshots(); protected: void SetNumRequiredClients(int NewNumRequiredClients) { NumRequiredClients = FMath::Max(NewNumRequiredClients, 0); } @@ -160,37 +345,99 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialFunctionalTest : public AFunctionalT int GetNumExpectedServers() const { return NumExpectedServers; } void DeleteActorsRegisteredForAutoDestroy(); - ISpatialFunctionalTestLBDelegationInterface* GetDelegationInterface() const; + // Force Actors having the given tag to migrate and gain authority on the given worker. All server workers must declare + // the same delegation at the same time, so we highly recommend that you use the AddStepSetTagDelegation() instead. + void SetTagDelegation(FName Tag, int32 ServerWorkerId); + + // Remove the forced authority delegation. All server workers must declare the same delegation at the same time, + // so we highly recommend that you use the AddStepClearTagDelegation() instead. + void ClearTagDelegation(FName Tag); + + // Remove all the actor tags, extra interest, and authority delegation, resetting the Debug layer. All server workers must + // call it at the same time to guarantee consistency, so we again highly recommend you use AddStepClearTagDelegationAndInterest(). + // Whenever a test finishes this will be called automatically. + void ClearTagDelegationAndInterest(); + + // # Built-in StepDefinitions for convenience + + // Step Definition that will take a SpatialOS snapshot for the current map. This snapshot will become the + // default snapshot for the map it was taken with until you either clear it or the Automation Manager finishes + // running the tests. + UPROPERTY(BlueprintReadOnly, Category = "Spatial Functional Test") + FSpatialFunctionalTestStepDefinition TakeSnapshotStepDefinition; + + // Step Definition that will clear the SpatialOS snapshot for the current map. + UPROPERTY(BlueprintReadOnly, Category = "Spatial Functional Test") + FSpatialFunctionalTestStepDefinition ClearSnapshotStepDefinition; private: UPROPERTY(EditAnywhere, meta = (ClampMin = "0"), Category = "Spatial Functional Test") int NumRequiredClients = 2; - // number of servers that should be running in the world - int NumExpectedServers = 0; + // Number of servers that should be running in the world. + int NumExpectedServers = 0; - // FlowController which is locally owned + // FlowController which is locally owned. ASpatialFunctionalTestFlowController* LocalFlowController = nullptr; TArray StepDefinitions; TArray FlowControllersExecutingStep; - // Time current step has been running for, used if Step Definition has TimeLimit >= 0 + // Time current step has been running for, used if Step Definition has TimeLimit >= 0. float TimeRunningStep = 0.0f; - // Current Step Index, < 0 if not executing any, check consts at the top - UPROPERTY(ReplicatedUsing=OnReplicated_CurrentStepIndex, Transient) + // Cached test result while we wait all Workers to acknowledge they finished the test. + EFunctionalTestResult CachedTestResult; + + // Cached test result message while we wait all Workers to acknowledge they finished the test. + FString CachedTestMessage; + + // Handle for waiting for acknowledgment from all workers that the test is finished. + FTimerHandle FinishTestTimerHandle; + + // Current Step Index, < 0 if not executing any, check consts at the top. + UPROPERTY(ReplicatedUsing = OnReplicated_CurrentStepIndex, Transient) int CurrentStepIndex = SPATIAL_FUNCTIONAL_TEST_NOT_STARTED; UFUNCTION() void OnReplicated_CurrentStepIndex(); + UPROPERTY(ReplicatedUsing = OnReplicated_bPreparedTest, Transient) + bool bPreparedTest = false; + + UFUNCTION() + void OnReplicated_bPreparedTest(); + + UPROPERTY(ReplicatedUsing = OnReplicated_bFinishedTest, Transient) + bool bFinishedTest = false; + + UFUNCTION() + void OnReplicated_bFinishedTest(); + UPROPERTY(Replicated, Transient) TArray FlowControllers; - + + // Holds all the Requires calls / results for printing at the end of the step. + SpatialFunctionalTestRequireHandler RequireHandler; + UFUNCTION() void StartServerFlowControllerSpawn(); void SetupClientPlayerRegistrationFlow(); + void EndPlay(const EEndPlayReason::Type Reason) override; + + FDelegateHandle PostLoginDelegate; + + // Sets the snapshot for the map loaded by this world. When launching the test maps, the AutomationManager will + // check if there's a snapshot for that map and if so use it instead of the default snapshot. If PathToSnapshot + // is empty, it clears the entry for that map. + static bool SetSnapshotForMap(UWorld* World, const FString& PathToSnapshot); + + // Holds if currently we're running from a taken snapshot and not the default snapshot. + static bool bWasLoadedFromTakenSnapshot; + + // Holds the paths of all the snapshots taken during tests. Test maps before running in the AutomationManager will + // will check if there's a snapshot for them, and if so launch with it instead of the default snapshot. + static TMap TakenSnapshots; }; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestAutoDestroyComponent.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestAutoDestroyComponent.h index fb86a552da..fce5faa2ad 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestAutoDestroyComponent.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestAutoDestroyComponent.h @@ -2,15 +2,14 @@ #pragma once -#include "CoreMinimal.h" #include "Components/ActorComponent.h" +#include "CoreMinimal.h" #include "SpatialFunctionalTestAutoDestroyComponent.generated.h" - /* -* Empty component to be added to actors so that they can be automatically destroyed when the tests finish -*/ -UCLASS( NotBlueprintable, ClassGroup=SpatialFunctionalTest ) + * Empty component to be added to actors so that they can be automatically destroyed when the tests finish + */ +UCLASS(NotBlueprintable, ClassGroup = SpatialFunctionalTest) class SPATIALGDKFUNCTIONALTESTS_API USpatialFunctionalTestAutoDestroyComponent : public USceneComponent { GENERATED_BODY() diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestBlueprintLibrary.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestBlueprintLibrary.h index 97fec9361e..9676f17ec3 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestBlueprintLibrary.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestBlueprintLibrary.h @@ -7,14 +7,15 @@ #include "SpatialFunctionalTestStep.h" #include "SpatialFunctionalTestBlueprintLibrary.generated.h" - UCLASS(meta = (ScriptName = "SpatialFunctionalTestLibrary")) class SPATIALGDKFUNCTIONALTESTS_API USpatialFunctionalTestBlueprintLibrary : public UBlueprintFunctionLibrary { GENERATED_BODY() public: - - UFUNCTION(BlueprintPure, Category = "Spatial Functional Test", meta = (AutoCreateRefTerm = "IsReadyEvent,StartEvent,TickEvent", NativeMakeFunc)) - static FSpatialFunctionalTestStepDefinition MakeStepDefinition(const FString& StepName, const FStepIsReadyDelegate& IsReadyEvent, const FStepStartDelegate& StartEvent, const FStepTickDelegate& TickEvent, const float StepTimeLimit); + UFUNCTION(BlueprintPure, Category = "Spatial Functional Test", + meta = (AutoCreateRefTerm = "IsReadyEvent,StartEvent,TickEvent", NativeMakeFunc)) + static FSpatialFunctionalTestStepDefinition MakeStepDefinition(const FString& StepName, const FStepIsReadyDelegate& IsReadyEvent, + const FStepStartDelegate& StartEvent, const FStepTickDelegate& TickEvent, + const float StepTimeLimit); }; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestFlowController.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestFlowController.h index d1d6d08b52..6886384211 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestFlowController.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestFlowController.h @@ -9,21 +9,22 @@ namespace { - constexpr int INVALID_FLOW_CONTROLLER_ID = 0; +constexpr int INVALID_FLOW_CONTROLLER_ID = 0; } class ASpatialFunctionalTest; -UCLASS() +UCLASS(SpatialType = NotPersistent) class SPATIALGDKFUNCTIONALTESTS_API ASpatialFunctionalTestFlowController : public AActor { GENERATED_BODY() public: - ASpatialFunctionalTestFlowController(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); - void GetLifetimeReplicatedProps(TArray< FLifetimeProperty >& OutLifetimeProps) const override; + void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + virtual void BeginPlay() override; virtual void OnAuthorityGained() override; @@ -31,7 +32,7 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialFunctionalTestFlowController : publi // Convenience function to know if this FlowController is locally owned bool IsLocalController() const; - + // # Testing APIs // Locally triggers StepIndex Test Step to start @@ -39,12 +40,12 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialFunctionalTestFlowController : publi void CrossServerStartStep(int StepIndex); // Tells Test owner that the current Step is finished locally - void NotifyStepFinished(); + void NotifyStepFinished(const int StepIndex); - // Tell the Test owner that we want to end the Test + // Tell the Test owner that we want to end the Test void NotifyFinishTest(EFunctionalTestResult TestResult, const FString& Message); - - UPROPERTY(Replicated) + + UPROPERTY(ReplicatedUsing = OnRep_OwningTest) ASpatialFunctionalTest* OwningTest; // Holds WorkerType and WorkerId. Type should be only Server or Client, and Id >= 1 (after registered) @@ -53,12 +54,17 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialFunctionalTestFlowController : publi FWorkerDefinition WorkerDefinition; // Prettier way to display type+id combo since it can be quite useful - const FString GetDisplayName(); + const FString GetDisplayName() const; // When Test is finished, this gets triggered. It's mostly important for when a Test was failed during runtime void OnTestFinished(); - // Returns if the data regarding the FlowControllers has been replicated to their owners + // Marks the Flow Controller to be ready or not for the test to start, which means that PrepareTest() + // has been called locally on the OwningTest. + UFUNCTION() + void SetReadyToRunTest(bool bIsReady); + + // Returns if the data regarding the FlowControllers has been replicated PrepareTest() has run on locally on the OwningTest. bool IsReadyToRunTest() { return WorkerDefinition.Id != INVALID_FLOW_CONTROLLER_ID && bIsReadyToRunTest; } // Each server worker will assign local client ids, this function will be used by @@ -69,6 +75,9 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialFunctionalTestFlowController : publi UFUNCTION(BlueprintPure, Category = "Spatial Functional Test") FWorkerDefinition GetWorkerDefinition() { return WorkerDefinition; } + // Let's you know if the owning worker has acknowledged the FinishTest flow. + bool HasAckFinishedTest() const { return bHasAckFinishedTest; } + private: // Current Step being executed SpatialFunctionalTestStep CurrentStep; @@ -79,28 +88,38 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialFunctionalTestFlowController : publi UPROPERTY(Replicated) bool bIsReadyToRunTest; + UPROPERTY(Replicated) + bool bHasAckFinishedTest; + UFUNCTION() void OnReadyToRegisterWithTest(); + UFUNCTION() + void OnRep_OwningTest(); + + void TryRegisterFlowControllerWithOwningTest(); + UFUNCTION(Server, Reliable) - void ServerSetReadyToRunTest(); + void ServerSetReadyToRunTest(bool bIsReady); UFUNCTION(Client, Reliable) void ClientStartStep(int StepIndex); void StartStepInternal(const int StepIndex); - + void StopStepInternal(); UFUNCTION(Server, Reliable) - void ServerNotifyStepFinished(); - + void ServerNotifyStepFinished(const int StepIndex); UFUNCTION(CrossServer, Reliable) - void CrossServerNotifyStepFinished(); + void CrossServerNotifyStepFinished(const int StepIndex); UFUNCTION(Server, Reliable) void ServerNotifyFinishTest(EFunctionalTestResult TestResult, const FString& Message); - + void ServerNotifyFinishTestInternal(EFunctionalTestResult TestResult, const FString& Message); + + UFUNCTION(Server, Reliable) + void ServerAckFinishedTest(); }; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestFlowControllerSpawner.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestFlowControllerSpawner.h index 9ecffc7b91..e00ba3e220 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestFlowControllerSpawner.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestFlowControllerSpawner.h @@ -10,12 +10,13 @@ class UWorld; class SpatialFunctionalTestFlowControllerSpawner { public: - //default constructor has to exist for generated code, shouldn't be used in user code + // default constructor has to exist for generated code, shouldn't be used in user code SpatialFunctionalTestFlowControllerSpawner(); - SpatialFunctionalTestFlowControllerSpawner(ASpatialFunctionalTest* ControllerOwningTest, TSubclassOf FlowControllerClassToSpawn); + SpatialFunctionalTestFlowControllerSpawner(ASpatialFunctionalTest* ControllerOwningTest, + TSubclassOf FlowControllerClassToSpawn); void ModifyFlowControllerClassToSpawn(TSubclassOf FlowControllerClassToSpawn); - + ASpatialFunctionalTestFlowController* SpawnServerFlowController(); ASpatialFunctionalTestFlowController* SpawnClientFlowController(APlayerController* OwningClient); @@ -25,7 +26,7 @@ class SpatialFunctionalTestFlowControllerSpawner ASpatialFunctionalTest* OwningTest; TSubclassOf FlowControllerClass; uint8 NextClientControllerId; - + uint8 OwningServerIntanceId(UWorld* World) const; void LockFlowControllerDelegations(ASpatialFunctionalTestFlowController* FlowController) const; -}; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestGridLBStrategy.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestGridLBStrategy.h deleted file mode 100644 index 95f76c6a50..0000000000 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestGridLBStrategy.h +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#pragma once - -#include "CoreMinimal.h" -#include "LoadBalancing/GridBasedLBStrategy.h" -#include "SpatialFunctionalTestLBDelegationInterface.h" -#include "SpatialFunctionalTestGridLBStrategy.generated.h" - -/** - * A 2 by 2 (rows by columns) load balancing strategy for testing zoning features. - * You should use this Grid LBS instead of the UGridBasedLBStrategy because it allows you to - * do runtime delegations of Actors to specific Server Workers. - */ -UCLASS() -class SPATIALGDKFUNCTIONALTESTS_API USpatialFunctionalTestGridLBStrategy : public UGridBasedLBStrategy, public ISpatialFunctionalTestLBDelegationInterface -{ - GENERATED_BODY() - -public: - USpatialFunctionalTestGridLBStrategy(); - - virtual bool ShouldHaveAuthority(const AActor& Actor) const override; - virtual VirtualWorkerId WhoShouldHaveAuthority(const AActor& Actor) const override; -}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestLBDelegationInterface.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestLBDelegationInterface.h deleted file mode 100644 index b6c95e5a89..0000000000 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestLBDelegationInterface.h +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#pragma once - -#include "CoreMinimal.h" -#include "SpatialCommonTypes.h" -#include "SpatialFunctionalTestLBDelegationInterface.generated.h" - -/** - * Interface that can be added to Spatial's Load Balancing Strategies to allow you to - * delegate Actors to specific Server Workers at runtime. - * To guarantee that this delegation system works even when Server Workers don't have - * complete World area of interest, we add USpatialFunctionalTestDelegationComponent - * to Actors. - */ -UINTERFACE(MinimalAPI, Blueprintable) -class USpatialFunctionalTestLBDelegationInterface : public UInterface -{ - GENERATED_BODY() -}; - -class ISpatialFunctionalTestLBDelegationInterface -{ - GENERATED_BODY() - -public: - // Adds or changes the current Actor Delegation to WorkerId - bool AddActorDelegation(AActor* Actor, VirtualWorkerId WorkerId, bool bPersistOnTestFinished = false); - - // Removes an Actor Delegation, which means that it will fallback to the Load Balancing Strategy - bool RemoveActorDelegation(AActor* Actor); - - // If there's an Actor Delegation it will return True, and WorkerId and bIsPersistent will be set accordingly - bool HasActorDelegation(AActor* Actor, VirtualWorkerId& WorkerId, bool& bIsPersistent); - - void RemoveAllActorDelegations(UWorld* World, bool bRemovePersistent = false); -}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestRequireHandler.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestRequireHandler.h new file mode 100644 index 0000000000..a2b8b18a34 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestRequireHandler.h @@ -0,0 +1,67 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "FunctionalTest.h" + +class ASpatialFunctionalTest; + +struct FSpatialFunctionalTestRequire +{ + FString Msg; + bool bPassed; + FString ErrorMsg; + uint32 Order; // Used to be able to log the messages in the same order they occurred at the end. +}; + +/* This class handles all the Require functionality used by the ASpatialFunctionalTest. Because of the way networked tests + * work we can't simply assert when checking values since we need to wait for them to propagate through the network. This + * handler provides an API that is similar to asserts but doesn't actually abort on failure, and instead gathers data to + * print at the end of a step / test. + * Note that if there's fails in the Requires, ASpatialFunctionalTest::FinishStep() will not proceed. + */ +class SpatialFunctionalTestRequireHandler +{ +public: + SpatialFunctionalTestRequireHandler(); + + void SetOwnerTest(ASpatialFunctionalTest* SpatialFunctionalTest) { OwnerTest = SpatialFunctionalTest; } + + void RequireTrue(bool bCheckTrue, const FString& Msg); + void RequireFalse(bool bCheckFalse, const FString& Msg); + + void RequireCompare(int A, EComparisonMethod Operator, int B, const FString& Msg); + void RequireCompare(float A, EComparisonMethod Operator, float B, const FString& Msg); + + void RequireEqual(bool bValue, bool bExpected, const FString& Msg); + void RequireEqual(int Value, int Expected, const FString& Msg); + void RequireEqual(float Value, float Expected, const FString& Msg, float Tolerance); + void RequireEqual(const FString& Value, const FString& Expected, const FString& Msg); + void RequireEqual(const FName& Value, const FName& Expected, const FString& Msg); + void RequireEqual(const FVector& Value, const FVector& Expected, const FString& Msg, float Tolerance); + void RequireEqual(const FRotator& Value, const FRotator& Expected, const FString& Msg, float Tolerance); + void RequireEqual(const FTransform& Value, const FTransform& Expected, const FString& Msg, float Tolerance); + + void RequireNotEqual(bool bValue, bool bNotExpected, const FString& Msg); + void RequireNotEqual(int Value, int NotExpected, const FString& Msg); + void RequireNotEqual(float Value, float NotExpected, const FString& Msg); + void RequireNotEqual(const FString& Value, const FString& NotExpected, const FString& Msg); + void RequireNotEqual(const FName& Value, const FName& NotExpected, const FString& Msg); + void RequireNotEqual(const FVector& Value, const FVector& NotExpected, const FString& Msg); + void RequireNotEqual(const FRotator& Value, const FRotator& NotExpected, const FString& Msg); + void RequireNotEqual(const FTransform& Value, const FTransform& NotExpected, const FString& Msg); + + void GenericRequire(const FString& Key, bool bPassed, const FString& ErrorMsg); + + void LogAndClearStepRequires(); + + bool HasFails(); + +private: + ASpatialFunctionalTest* OwnerTest; + + uint32 NextOrder; + + TMap Requires; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestStep.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestStep.h index 62447e963b..85e7a2b1e1 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestStep.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestStep.h @@ -16,26 +16,26 @@ DECLARE_DYNAMIC_DELEGATE(FStepStartDelegate); DECLARE_DYNAMIC_DELEGATE_OneParam(FStepTickDelegate, float, DeltaTime); // C++ Delegates -DECLARE_DELEGATE_RetVal_OneParam(bool, FNativeStepIsReadyDelegate, ASpatialFunctionalTest*); -DECLARE_DELEGATE_OneParam(FNativeStepStartDelegate, ASpatialFunctionalTest*); -DECLARE_DELEGATE_TwoParams(FNativeStepTickDelegate, ASpatialFunctionalTest*, float /*DeltaTime*/); +DECLARE_DELEGATE_RetVal(bool, FNativeStepIsReadyDelegate); +DECLARE_DELEGATE(FNativeStepStartDelegate); +DECLARE_DELEGATE_OneParam(FNativeStepTickDelegate, float /*DeltaTime*/); UENUM() enum class ESpatialFunctionalTestWorkerType : uint8 { Server, Client, - All // Special type that allows you to reference all the Servers and Clients + All, // Special type that allows you to reference all the Servers and Clients + Invalid = 0xff UMETA(Hidden) }; - USTRUCT(BlueprintType) struct FWorkerDefinition { GENERATED_BODY() // Type of Worker, usually Server or Client. - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Spatial Functional Test") + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spatial Functional Test") ESpatialFunctionalTestWorkerType Type = ESpatialFunctionalTestWorkerType::Server; // Ids of Workers start from 1. @@ -60,15 +60,9 @@ struct FWorkerDefinition // Helper for Client Worker Definition static FWorkerDefinition Client(int ClientId); - bool operator == (const FWorkerDefinition& Other) - { - return Type == Other.Type && Id == Other.Id; - }; + bool operator==(const FWorkerDefinition& Other) { return Type == Other.Type && Id == Other.Id; }; - bool operator != (const FWorkerDefinition& Other) - { - return Type != Other.Type || Id != Other.Id; - }; + bool operator!=(const FWorkerDefinition& Other) { return Type != Other.Type || Id != Other.Id; }; }; USTRUCT(BlueprintType, meta = (HasNativeMake = "")) @@ -129,9 +123,8 @@ class SpatialFunctionalTestStep bool HasReadyEvent(); - ASpatialFunctionalTest* Owner; bool bIsRunning; bool bIsReady; - + FSpatialFunctionalTestStepDefinition StepDefinition; }; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestWorkerDelegationComponent.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestWorkerDelegationComponent.h deleted file mode 100644 index cabb8886f5..0000000000 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialFunctionalTestWorkerDelegationComponent.h +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#pragma once - -#include "CoreMinimal.h" -#include "SpatialCommonTypes.h" -#include "SpatialFunctionalTestWorkerDelegationComponent.generated.h" - -/* - * Actor Component for Functional Testing purposes only that allows you to delegate its Actor to a specific Server Worker. - * Note that currently this functionality only works in single layer Load Balancing Strategies, and your Default Load - * Balancing Strategy needs to implement ISpatialFunctionalTestLBDelegationInterface. - */ -UCLASS(BlueprintType) -class SPATIALGDKFUNCTIONALTESTS_API USpatialFunctionalTestWorkerDelegationComponent : public UActorComponent -{ - GENERATED_BODY() - -public: - USpatialFunctionalTestWorkerDelegationComponent(); - - virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; - - UPROPERTY(Replicated, VisibleAnywhere, BlueprintReadOnly, Category="Default") - int WorkerId = 0; - - UPROPERTY(Replicated, VisibleAnywhere, BlueprintReadOnly, Category="Default") - bool bIsPersistent = false; -}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialGDKFunctionalTestsModule.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialGDKFunctionalTestsModule.h index a287c39b17..2382d9f978 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialGDKFunctionalTestsModule.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/Public/SpatialGDKFunctionalTestsModule.h @@ -11,8 +11,5 @@ class SPATIALGDKFUNCTIONALTESTS_API FSpatialGDKFunctionalTestsModule : public IM virtual void StartupModule() override; virtual void ShutdownModule() override; - virtual bool SupportsDynamicReloading() override - { - return true; - } + virtual bool SupportsDynamicReloading() override { return true; } }; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/CrossServerAndClientOrchestrationTest/CrossServerAndClientOrchestrationFlowController.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/CrossServerAndClientOrchestrationTest/CrossServerAndClientOrchestrationFlowController.cpp index ad3105ff1f..5b71af6258 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/CrossServerAndClientOrchestrationTest/CrossServerAndClientOrchestrationFlowController.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/CrossServerAndClientOrchestrationTest/CrossServerAndClientOrchestrationFlowController.cpp @@ -1,6 +1,5 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved - #include "CrossServerAndClientOrchestrationFlowController.h" #include "CrossServerAndClientOrchestrationTest.h" diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/CrossServerAndClientOrchestrationTest/CrossServerAndClientOrchestrationFlowController.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/CrossServerAndClientOrchestrationTest/CrossServerAndClientOrchestrationFlowController.h index 95cb656925..3c385bec7e 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/CrossServerAndClientOrchestrationTest/CrossServerAndClientOrchestrationFlowController.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/CrossServerAndClientOrchestrationTest/CrossServerAndClientOrchestrationFlowController.h @@ -7,7 +7,7 @@ #include "CrossServerAndClientOrchestrationFlowController.generated.h" /** - * + * */ UCLASS() class SPATIALGDKFUNCTIONALTESTS_API ACrossServerAndClientOrchestrationFlowController : public ASpatialFunctionalTestFlowController diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/CrossServerAndClientOrchestrationTest/CrossServerAndClientOrchestrationTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/CrossServerAndClientOrchestrationTest/CrossServerAndClientOrchestrationTest.cpp index b5e5faefee..7ca8faf75e 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/CrossServerAndClientOrchestrationTest/CrossServerAndClientOrchestrationTest.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/CrossServerAndClientOrchestrationTest/CrossServerAndClientOrchestrationTest.cpp @@ -1,18 +1,17 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved - #include "CrossServerAndClientOrchestrationTest.h" -#include "LoadBalancing/AbstractLBStrategy.h" +#include "CrossServerAndClientOrchestrationFlowController.h" #include "EngineClasses/SpatialNetDriver.h" +#include "LoadBalancing/AbstractLBStrategy.h" #include "Net/UnrealNetwork.h" -#include "CrossServerAndClientOrchestrationFlowController.h" /** * This test tests server and client steps run in the right worker and can modify test data via CrossServer RPCs. * - * The test includes 2 servers and 2 client workers. All workers will try to set some state in the Test actor via CrossServer RPC and ensure they have received updates done by other workers. - * The flow is as follows: + * The test includes 2 servers and 2 client workers. All workers will try to set some state in the Test actor via CrossServer RPC and ensure + * they have received updates done by other workers. The flow is as follows: * - Setup: * - All server and clients set a flag in the test actor via CrossServer RPC * - Test: @@ -27,102 +26,103 @@ ACrossServerAndClientOrchestrationTest::ACrossServerAndClientOrchestrationTest() FlowControllerActorClass = ACrossServerAndClientOrchestrationFlowController::StaticClass(); } -void ACrossServerAndClientOrchestrationTest::BeginPlay() +void ACrossServerAndClientOrchestrationTest::PrepareTest() { - Super::BeginPlay(); + Super::PrepareTest(); ClientWorkerSetValues.SetNum(GetNumRequiredClients()); ServerWorkerSetValues.SetNum(GetNumExpectedServers()); { - //Step 1 - Set all server values - AddStep(TEXT("Servers_SetupSetValue"), FWorkerDefinition::AllServers, nullptr, [](ASpatialFunctionalTest* NetTest) { - //Send CrossServer RPC to Test actor to set the flag for this server flow controller instance - ACrossServerAndClientOrchestrationTest* CrossServerTest = Cast(NetTest); - ASpatialFunctionalTestFlowController* FlowController = CrossServerTest->GetLocalFlowController(); - CrossServerTest->CrossServerSetTestValue(FlowController->WorkerDefinition.Type, FlowController->WorkerDefinition.Id); - CrossServerTest->FinishStep(); - }); + // Step 1 - Set all server values + AddStep(TEXT("Servers_SetupSetValue"), FWorkerDefinition::AllServers, nullptr, [this]() { + // Send CrossServer RPC to Test actor to set the flag for this server flow controller instance + ASpatialFunctionalTestFlowController* FlowController = GetLocalFlowController(); + CrossServerSetTestValue(FlowController->WorkerDefinition.Type, FlowController->WorkerDefinition.Id); + FinishStep(); + }); } { - //Step 2 - Set all client values - AddStep(TEXT("Clients_SetupSetValue"), FWorkerDefinition::AllClients, nullptr, [](ASpatialFunctionalTest* NetTest) { - //Send Server RPC via flow controller to set the Test actor flag for this client flow controller instance - ACrossServerAndClientOrchestrationFlowController* FlowController = Cast(NetTest->GetLocalFlowController()); + // Step 2 - Set all client values + AddStep(TEXT("Clients_SetupSetValue"), FWorkerDefinition::AllClients, nullptr, [this]() { + // Send Server RPC via flow controller to set the Test actor flag for this client flow controller instance + ACrossServerAndClientOrchestrationFlowController* FlowController = + Cast(GetLocalFlowController()); FlowController->ServerClientReadValueAck(); - NetTest->FinishStep(); - }); + FinishStep(); + }); } { - //Step 3 - Verify steps for server 1 run in right context and can read all values set in test by other workers - AddStep(TEXT("Server1_Validate"), FWorkerDefinition::Server(1), nullptr, - [](ASpatialFunctionalTest* NetTest) { - ACrossServerAndClientOrchestrationTest* CrossServerTest = Cast(NetTest); - CrossServerTest->Assert_ServerStepIsRunningInExpectedEnvironment(1, CrossServerTest->GetLocalFlowController()); - }, - [](ASpatialFunctionalTest* NetTest, float DeltaTime) { - ACrossServerAndClientOrchestrationTest* CrossServerTest = Cast(NetTest); - if (CrossServerTest->CheckAllValuesHaveBeenSetAndCanBeLocallyRead()) + // Step 3 - Verify steps for server 1 run in right context and can read all values set in test by other workers + AddStep( + TEXT("Server1_Validate"), FWorkerDefinition::Server(1), nullptr, + [this]() { + Assert_ServerStepIsRunningInExpectedEnvironment(1, GetLocalFlowController()); + }, + [this](float DeltaTime) { + if (CheckAllValuesHaveBeenSetAndCanBeLocallyRead()) { - CrossServerTest->FinishStep(); + FinishStep(); } - }, 5.0f); + }, + 5.0f); } { - //Step 4 - Verify steps for server 2 run in right context and can read all values set in test by other workers - AddStep(TEXT("Server2_Validate"), FWorkerDefinition::Server(2), nullptr, - [](ASpatialFunctionalTest* NetTest) { - ACrossServerAndClientOrchestrationTest* CrossServerTest = Cast(NetTest); - CrossServerTest->Assert_ServerStepIsRunningInExpectedEnvironment(2, CrossServerTest->GetLocalFlowController()); + // Step 4 - Verify steps for server 2 run in right context and can read all values set in test by other workers + AddStep( + TEXT("Server2_Validate"), FWorkerDefinition::Server(2), nullptr, + [this]() { + Assert_ServerStepIsRunningInExpectedEnvironment(2, GetLocalFlowController()); }, - [](ASpatialFunctionalTest* NetTest, float DeltaTime) { - ACrossServerAndClientOrchestrationTest* CrossServerTest = Cast(NetTest); - if (CrossServerTest->CheckAllValuesHaveBeenSetAndCanBeLocallyRead()) + [this](float DeltaTime) { + if (CheckAllValuesHaveBeenSetAndCanBeLocallyRead()) { - CrossServerTest->FinishStep(); + FinishStep(); } - }, 5.0f); + }, + 5.0f); } { - //Step 5 - Verify steps for client 1 run in right context and can read all values set in test by other workers - AddStep(TEXT("Client1_Validate"), FWorkerDefinition::Client(1), nullptr, - [](ASpatialFunctionalTest* NetTest) { - ACrossServerAndClientOrchestrationTest* CrossServerTest = Cast(NetTest); - CrossServerTest->Assert_ClientStepIsRunningInExpectedEnvironment(1, CrossServerTest->GetLocalFlowController()); - }, - [](ASpatialFunctionalTest* NetTest, float DeltaTime) { - ACrossServerAndClientOrchestrationTest* CrossServerTest = Cast(NetTest); - if (CrossServerTest->CheckAllValuesHaveBeenSetAndCanBeLocallyRead()) + // Step 5 - Verify steps for client 1 run in right context and can read all values set in test by other workers + AddStep( + TEXT("Client1_Validate"), FWorkerDefinition::Client(1), nullptr, + [this]() { + Assert_ClientStepIsRunningInExpectedEnvironment(1, GetLocalFlowController()); + }, + [this](float DeltaTime) { + if (CheckAllValuesHaveBeenSetAndCanBeLocallyRead()) { - CrossServerTest->FinishStep(); + FinishStep(); } - }, 5.0f); + }, + 5.0f); } { - //Step 6 - Verify steps for client 2 run in right context and can read all values set in test by other workers - AddStep(TEXT("Client2_Validate"), FWorkerDefinition::Client(2), nullptr, - [](ASpatialFunctionalTest* NetTest) { - ACrossServerAndClientOrchestrationTest* CrossServerTest = Cast(NetTest); - CrossServerTest->Assert_ClientStepIsRunningInExpectedEnvironment(2, CrossServerTest->GetLocalFlowController()); + // Step 6 - Verify steps for client 2 run in right context and can read all values set in test by other workers + AddStep( + TEXT("Client2_Validate"), FWorkerDefinition::Client(2), nullptr, + [this]() { + Assert_ClientStepIsRunningInExpectedEnvironment(2, GetLocalFlowController()); }, - [](ASpatialFunctionalTest* NetTest, float DeltaTime) { - ACrossServerAndClientOrchestrationTest* CrossServerTest = Cast(NetTest); - if (CrossServerTest->CheckAllValuesHaveBeenSetAndCanBeLocallyRead()) + [this](float DeltaTime) { + if (CheckAllValuesHaveBeenSetAndCanBeLocallyRead()) { - CrossServerTest->FinishStep(); + FinishStep(); } - }, 5.0f); + }, + 5.0f); } } -void ACrossServerAndClientOrchestrationTest::CrossServerSetTestValue_Implementation(ESpatialFunctionalTestWorkerType ControllerType, uint8 ChangedInstance) +void ACrossServerAndClientOrchestrationTest::CrossServerSetTestValue_Implementation(ESpatialFunctionalTestWorkerType ControllerType, + uint8 ChangedInstance) { uint8 FlagIndex = ChangedInstance - 1; - if(ControllerType == ESpatialFunctionalTestWorkerType::Client) + if (ControllerType == ESpatialFunctionalTestWorkerType::Client) { - if(FlagIndex >= ClientWorkerSetValues.Num()) + if (FlagIndex >= ClientWorkerSetValues.Num()) { - //ignore + // ignore return; } ClientWorkerSetValues[FlagIndex] = true; @@ -131,14 +131,13 @@ void ACrossServerAndClientOrchestrationTest::CrossServerSetTestValue_Implementat { if (FlagIndex >= ServerWorkerSetValues.Num()) { - //ignore + // ignore return; } ServerWorkerSetValues[FlagIndex] = true; } } - void ACrossServerAndClientOrchestrationTest::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); @@ -147,7 +146,8 @@ void ACrossServerAndClientOrchestrationTest::GetLifetimeReplicatedProps(TArray(GetNetDriver()); @@ -156,7 +156,7 @@ void ACrossServerAndClientOrchestrationTest::Assert_ServerStepIsRunningInExpecte VirtualWorkerId LocalWorkerId = bUsingLoadbalancing ? SpatialNetDriver->LoadBalanceStrategy->GetLocalVirtualWorkerId() : 1; InstanceToRunIn = GetNumExpectedServers() == 1 ? 1 : InstanceToRunIn; - + // Check Step is running in expected controller instance AssertEqual_Int(FlowController->WorkerDefinition.Id, InstanceToRunIn, TEXT("Step executing in expected FlowController instance"), this); @@ -164,7 +164,8 @@ void ACrossServerAndClientOrchestrationTest::Assert_ServerStepIsRunningInExpecte AssertEqual_Int(LocalWorkerId, InstanceToRunIn, TEXT("Step executing in expected Worker instance"), this); } -void ACrossServerAndClientOrchestrationTest::Assert_ClientStepIsRunningInExpectedEnvironment(int InstanceToRunIn, ASpatialFunctionalTestFlowController* FlowController) +void ACrossServerAndClientOrchestrationTest::Assert_ClientStepIsRunningInExpectedEnvironment( + int InstanceToRunIn, ASpatialFunctionalTestFlowController* FlowController) { // Check Step is running in expected controller instance // We can't check against clients as clients don't have natural logical IDs, Controllers are mapped by login order diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/CrossServerAndClientOrchestrationTest/CrossServerAndClientOrchestrationTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/CrossServerAndClientOrchestrationTest/CrossServerAndClientOrchestrationTest.h index 1a8a2dcbb3..566b6b5bb4 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/CrossServerAndClientOrchestrationTest/CrossServerAndClientOrchestrationTest.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/CrossServerAndClientOrchestrationTest/CrossServerAndClientOrchestrationTest.h @@ -7,7 +7,7 @@ #include "CrossServerAndClientOrchestrationTest.generated.h" /** - * + * */ UCLASS() class SPATIALGDKFUNCTIONALTESTS_API ACrossServerAndClientOrchestrationTest : public ASpatialFunctionalTest @@ -17,7 +17,7 @@ class SPATIALGDKFUNCTIONALTESTS_API ACrossServerAndClientOrchestrationTest : pub public: ACrossServerAndClientOrchestrationTest(); - virtual void BeginPlay() override; + virtual void PrepareTest() override; UPROPERTY(Replicated) TArray ServerWorkerSetValues; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DebugInterface/TestDebugInterface.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DebugInterface/TestDebugInterface.cpp new file mode 100644 index 0000000000..a052531e6c --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DebugInterface/TestDebugInterface.cpp @@ -0,0 +1,397 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "TestDebugInterface.h" +#include "SpatialFunctionalTestFlowController.h" + +#include "LoadBalancing/GridBasedLBStrategy.h" +#include "LoadBalancing/LayeredLBStrategy.h" + +#include "Kismet/GameplayStatics.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h" + +/* +Test for coverage of the USpatialGDKDebugInterface. + + +*/ + +ATestDebugInterface::ATestDebugInterface() + : Super() +{ + Author = "Nicolas"; + Description = TEXT("Test Debug interface"); +} + +namespace +{ +FName GetTestTag() +{ + static const FName TestTag(TEXT("TestActorToFollow")); + return TestTag; +} +} // namespace + +bool ATestDebugInterface::WaitToSeeActors(UClass* ActorClass, int32 NumActors) +{ + if (bIsOnDefaultLayer) + { + UWorld* World = GetWorld(); + + TArray TestActors; + UGameplayStatics::GetAllActorsOfClass(World, ActorClass, TestActors); + if (TestActors.Num() != NumActors) + { + return false; + } + } + return true; +} + +void ATestDebugInterface::PrepareTest() +{ + Super::PrepareTest(); + + AddStep(TEXT("SetupStep"), FWorkerDefinition::AllServers, nullptr, nullptr, [this](float DeltaTime) { + UWorld* World = GetWorld(); + DelegationStep = 0; + Workers.Empty(); + bIsOnDefaultLayer = false; + + ULayeredLBStrategy* RootStrategy = GetLoadBalancingStrategy(); + + bIsOnDefaultLayer = RootStrategy->CouldHaveAuthority(AReplicatedTestActorBase::StaticClass()); + if (bIsOnDefaultLayer) + { + FName LocalLayer = RootStrategy->GetLocalLayerName(); + UAbstractLBStrategy* LocalStrategy = RootStrategy->GetLBStrategyForLayer(LocalLayer); + + AssertTrue(LocalStrategy->IsA(), TEXT("")); + + UGridBasedLBStrategy* GridStrategy = Cast(LocalStrategy); + + for (auto& WorkerRegion : GridStrategy->GetLBStrategyRegions()) + { + Workers.Add(WorkerRegion.Key); + } + LocalWorker = GridStrategy->GetLocalVirtualWorkerId(); + + WorkerEntityPosition = GridStrategy->GetWorkerEntityPosition(); + AReplicatedTestActorBase* Actor = World->SpawnActor(WorkerEntityPosition, FRotator()); + AddDebugTag(Actor, GetTestTag()); + RegisterAutoDestroyActor(Actor); + TimeStampSpinning = FPlatformTime::Cycles64(); + } + + FinishStep(); + }); + + AddStep( + TEXT("Wait for actor ready and add extra interest"), FWorkerDefinition::AllServers, + [this]() -> bool { + if (double(FPlatformTime::Cycles64() - TimeStampSpinning) * FPlatformTime::GetSecondsPerCycle() < 2.0) + { + return false; + } + + if (!bIsOnDefaultLayer) + { + return true; + } + + UWorld* World = GetWorld(); + + TArray TestActors; + UGameplayStatics::GetAllActorsOfClass(World, AReplicatedTestActorBase::StaticClass(), TestActors); + if (!AssertTrue(TestActors.Num() == 1, "We should only see a single actor at this point!!")) + { + return false; + } + return TestActors[0]->IsActorReady(); + }, + [this]() { + if (!bIsOnDefaultLayer) + { + FinishStep(); + } + + AddInterestOnTag(GetTestTag()); + FinishStep(); + }, + nullptr, 10.0f); + + AddStep( + TEXT("Wait for extra actors"), FWorkerDefinition::AllServers, + [this]() -> bool { + return WaitToSeeActors(AReplicatedTestActorBase::StaticClass(), Workers.Num()); + }, + [this]() { + if (!bIsOnDefaultLayer) + { + FinishStep(); + } + UWorld* World = GetWorld(); + + TArray TestActors; + UGameplayStatics::GetAllActorsOfClass(World, AReplicatedTestActorBase::StaticClass(), TestActors); + + AssertTrue(TestActors.Num() == Workers.Num(), TEXT("Not the expected number of actors")); + + FinishStep(); + }, + nullptr, 5.0f); + + AddStep( + TEXT("Force actor delegation"), FWorkerDefinition::AllServers, nullptr, nullptr, + [this](float DeltaTime) { + if (!bIsOnDefaultLayer) + { + FinishStep(); + } + int32 CurAuthWorker = DelegationStep / 2; + int32 WorkerSubStep = DelegationStep % 2; + switch (WorkerSubStep) + { + case 0: + SetTagDelegation(GetTestTag(), Workers[CurAuthWorker]); + ++DelegationStep; + break; + case 1: + bool bExpectedAuth = Workers[CurAuthWorker] == LocalWorker; + bool bExpectedResult = true; + + TArray TestActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase::StaticClass(), TestActors); + for (AActor* Actor : TestActors) + { + bExpectedResult &= Actor->HasAuthority() == bExpectedAuth; + } + + if (bExpectedResult) + { + ++DelegationStep; + } + break; + } + + if (DelegationStep >= Workers.Num() * 2) + { + UWorld* World = GetWorld(); + + AReplicatedTestActorBase* Actor = World->SpawnActor(WorkerEntityPosition, FRotator()); + AddDebugTag(Actor, GetTestTag()); + RegisterAutoDestroyActor(Actor); + + FinishStep(); + } + }, + 5.0f); + + AddStep( + TEXT("Check new actors interest and delegation"), FWorkerDefinition::AllServers, + [this]() -> bool { + return WaitToSeeActors(AReplicatedTestActorBase::StaticClass(), Workers.Num() * 2); + }, + nullptr, + [this](float DeltaTime) { + if (!bIsOnDefaultLayer) + { + FinishStep(); + } + int32_t CurAuthWorker = Workers.Num() - 1; + + bool bExpectedAuth = Workers[CurAuthWorker] == LocalWorker; + bool bExpectedResult = true; + + TArray TestActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase::StaticClass(), TestActors); + for (AActor* Actor : TestActors) + { + bExpectedResult &= Actor->HasAuthority() == bExpectedAuth; + } + + if (bExpectedResult) + { + FinishStep(); + } + }, + 5.0f); + + AddStep( + TEXT("Remove extra interest"), FWorkerDefinition::AllServers, nullptr, + [this]() { + if (!bIsOnDefaultLayer) + { + FinishStep(); + } + + RemoveInterestOnTag(GetTestTag()); + FinishStep(); + }, + nullptr); + + AddStep( + TEXT("Check extra interest removed"), FWorkerDefinition::AllServers, + [this] { + int32_t CurAuthWorker = Workers.Num() - 1; + bool bExpectedAuth = Workers[CurAuthWorker] == LocalWorker; + return WaitToSeeActors(AReplicatedTestActorBase::StaticClass(), bExpectedAuth ? Workers.Num() * 2 : 2); + }, + [this] { + FinishStep(); + }, + nullptr, 5.0f); + + AddStep( + TEXT("Add extra interest again"), FWorkerDefinition::AllServers, nullptr, + [this]() { + if (!bIsOnDefaultLayer) + { + FinishStep(); + } + AddInterestOnTag(GetTestTag()); + FinishStep(); + }, + nullptr, 5.0f); + + AddStep( + TEXT("Remove actor tags"), FWorkerDefinition::AllServers, + [this] { + return WaitToSeeActors(AReplicatedTestActorBase::StaticClass(), Workers.Num() * 2); + }, + [this] { + if (!bIsOnDefaultLayer) + { + FinishStep(); + } + + TArray TestActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase::StaticClass(), TestActors); + for (AActor* Actor : TestActors) + { + if (Actor->HasAuthority()) + { + RemoveDebugTag(Actor, GetTestTag()); + } + } + + FinishStep(); + }, + nullptr, 5.0f); + + AddStep( + TEXT("Check state after tags removed"), FWorkerDefinition::AllServers, + [this]() -> bool { + return WaitToSeeActors(AReplicatedTestActorBase::StaticClass(), 2); + }, + nullptr, + [this](float DeltaTime) { + if (!bIsOnDefaultLayer) + { + FinishStep(); + } + + bool bExpectedResult = true; + + TArray TestActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase::StaticClass(), TestActors); + for (AActor* Actor : TestActors) + { + bExpectedResult &= Actor->HasAuthority(); + } + + if (bExpectedResult) + { + FinishStep(); + } + }, + 5.0f); + + AddStep( + TEXT("Add tag and remove delegation"), FWorkerDefinition::AllServers, nullptr, + [this]() { + if (!bIsOnDefaultLayer) + { + FinishStep(); + } + bool bExpectedResult = true; + + TArray TestActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase::StaticClass(), TestActors); + for (AActor* Actor : TestActors) + { + if (Actor->HasAuthority()) + { + AddDebugTag(Actor, GetTestTag()); + } + } + + ClearTagDelegation(GetTestTag()); + FinishStep(); + }, + nullptr, 5.0f); + + AddStep( + TEXT("Check state after delegation removal"), FWorkerDefinition::AllServers, + [this] { + return WaitToSeeActors(AReplicatedTestActorBase::StaticClass(), Workers.Num() * 2); + }, + [this]() { + if (!bIsOnDefaultLayer) + { + FinishStep(); + } + + bool bExpectedResult = true; + TArray TestActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase::StaticClass(), TestActors); + for (AActor* Actor : TestActors) + { + bExpectedResult &= (Actor->HasAuthority() == WorkerEntityPosition.Equals(Actor->GetActorLocation())); + } + + if (bExpectedResult) + { + FinishStep(); + } + }, + nullptr, 5.0f); + + AddStep( + TEXT("Shutdown debugging"), FWorkerDefinition::AllServers, nullptr, + [this]() { + if (bIsOnDefaultLayer) + { + ClearTagDelegationAndInterest(); + FinishStep(); + } + }, + nullptr, 5.0f); + + AddStep( + TEXT("Check state after debug reset"), FWorkerDefinition::AllServers, + [this]() -> bool { + return WaitToSeeActors(AReplicatedTestActorBase::StaticClass(), 2); + }, + nullptr, + [this](float DeltaTime) { + if (!bIsOnDefaultLayer) + { + FinishStep(); + } + + bool bExpectedResult = true; + + TArray TestActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase::StaticClass(), TestActors); + for (AActor* Actor : TestActors) + { + bExpectedResult &= Actor->HasAuthority(); + } + + if (bExpectedResult) + { + FinishStep(); + } + }, + 5.0f); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DebugInterface/TestDebugInterface.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DebugInterface/TestDebugInterface.h new file mode 100644 index 0000000000..2e3d22a3d5 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DebugInterface/TestDebugInterface.h @@ -0,0 +1,28 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialCommonTypes.h" +#include "SpatialFunctionalTest.h" +#include "TestDebugInterface.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ATestDebugInterface : public ASpatialFunctionalTest +{ + GENERATED_BODY() +public: + ATestDebugInterface(); + + virtual void PrepareTest() override; + +protected: + bool WaitToSeeActors(UClass* ActorClass, int32 NumActors); + + TArray Workers; + VirtualWorkerId LocalWorker; + FVector WorkerEntityPosition; + bool bIsOnDefaultLayer = false; + int32 DelegationStep = 0; + int64 TimeStampSpinning; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DormancyAndTombstoneTest/DormancyAndTombstoneTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DormancyAndTombstoneTest/DormancyAndTombstoneTest.cpp index a84013e0e9..3fdbff62f4 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DormancyAndTombstoneTest/DormancyAndTombstoneTest.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DormancyAndTombstoneTest/DormancyAndTombstoneTest.cpp @@ -7,9 +7,9 @@ /** * This test tests dormancy and tombstoning of bNetLoadOnClient actors placed in the level. * - * The test includes a single server and two client workers. The client workers begin with a player controller and their default pawns, which they initially possess. - * The test also REQUIRES the presence of a ADormancyTestActor (this actor is initially dormant) in the level where it is placed. - * The flow is as follows: + * The test includes a single server and two client workers. The client workers begin with a player controller and their default pawns, + * which they initially possess. The test also REQUIRES the presence of a ADormancyTestActor (this actor is initially dormant) in the level + * where it is placed. The flow is as follows: * - Setup: * - (Refer to above about placing instructions). * - Test: @@ -18,7 +18,8 @@ * - The server deletes the dormant actor. * - The clients check that the actor has been deleted in their local world. * - Cleanup: - * - No cleanup required, as the actor is deleted as part of the test. Note that the actor exists in the world if other tests are run before this one. + * - No cleanup required, as the actor is deleted as part of the test. Note that the actor exists in the world if other tests are run + * before this one. * - Note that this test cannot be rerun, as it relies on an actor placed in the level being deleted as part of the test. */ ADormancyAndTombstoneTest::ADormancyAndTombstoneTest() @@ -27,78 +28,76 @@ ADormancyAndTombstoneTest::ADormancyAndTombstoneTest() Description = TEXT("Test Actor Dormancy and Tombstones"); } -void ADormancyAndTombstoneTest::BeginPlay() +void ADormancyAndTombstoneTest::PrepareTest() { - Super::BeginPlay(); + Super::PrepareTest(); - { // Step 1 - Set TestIntProp to 1. - AddStep(TEXT("ServerSetTestIntPropTo1"), FWorkerDefinition::Server(1), nullptr, [](ASpatialFunctionalTest* NetTest) { + { // Step 1 - Set TestIntProp to 1. + AddStep(TEXT("ServerSetTestIntPropTo1"), FWorkerDefinition::Server(1), nullptr, [this]() { int Counter = 0; int ExpectedDormancyActors = 1; - for (TActorIterator Iter(NetTest->GetWorld()); Iter; ++Iter) + for (TActorIterator Iter(GetWorld()); Iter; ++Iter) { Counter++; - NetTest->AssertEqual_Int(Iter->NetDormancy, DORM_Initial, TEXT("Dormancy on ADormancyTestActor (should be DORM_Initial)"), NetTest); + RequireEqual_Int(Iter->NetDormancy, DORM_Initial, TEXT("Dormancy on ADormancyTestActor (should be DORM_Initial)")); Iter->TestIntProp = 1; } - NetTest->AssertEqual_Int(Counter, ExpectedDormancyActors, TEXT("Number of TestDormancyActors in the server world"), NetTest); + RequireEqual_Int(Counter, ExpectedDormancyActors, TEXT("Number of TestDormancyActors in the server world")); - NetTest->FinishStep(); - }); + FinishStep(); + }); } - { // Step 2 - Observe TestIntProp on client should still be 0. - AddStep(TEXT("ClientCheckValue"), FWorkerDefinition::AllClients, nullptr, nullptr, [](ASpatialFunctionalTest* NetTest, float DeltaTime) { - - bool bPassesChecks = true; - - int Counter = 0; - int ExpectedDormancyActors = 1; - for (TActorIterator Iter(NetTest->GetWorld()); Iter; ++Iter) - { - Counter++; - if (Iter->TestIntProp != 0 || Iter->NetDormancy != DORM_Initial) + { // Step 2 - Observe TestIntProp on client should still be 0. + AddStep( + TEXT("ClientCheckValue"), FWorkerDefinition::AllClients, nullptr, nullptr, + [this](float DeltaTime) { + int Counter = 0; + int ExpectedDormancyActors = 1; + for (TActorIterator Iter(GetWorld()); Iter; ++Iter) { - bPassesChecks = false; - break; + if (Iter->TestIntProp == 0 && Iter->NetDormancy == DORM_Initial) + { + Counter++; + } } - } - if (Counter == ExpectedDormancyActors && bPassesChecks) - { - NetTest->AssertEqual_Int(Counter, ExpectedDormancyActors, TEXT("Number of TestDormancyActors in client world"), NetTest); - NetTest->FinishStep(); - } - }, 5.0f); + RequireEqual_Int(Counter, ExpectedDormancyActors, TEXT("Number of TestDormancyActors in client world")); + + FinishStep(); + }, + 5.0f); } - { // Step 3 - Delete the test actor on the server. - AddStep(TEXT("ServerDeleteActor"), FWorkerDefinition::Server(1), nullptr, [](ASpatialFunctionalTest* NetTest) { + { // Step 3 - Delete the test actor on the server. + AddStep(TEXT("ServerDeleteActor"), FWorkerDefinition::Server(1), nullptr, [this]() { int Counter = 0; int ExpectedDormancyActors = 1; - for (TActorIterator Iter(NetTest->GetWorld()); Iter; ++Iter) + for (TActorIterator Iter(GetWorld()); Iter; ++Iter) { Counter++; Iter->Destroy(); } - NetTest->AssertEqual_Int(Counter, ExpectedDormancyActors, TEXT("Number of TestDormancyActors in the server world"), NetTest); + RequireEqual_Int(Counter, ExpectedDormancyActors, TEXT("Number of TestDormancyActors in the server world")); - NetTest->FinishStep(); - }); + FinishStep(); + }); } - { // Step 4 - Observe the test actor has been deleted on the client. - AddStep(TEXT("ClientCheckActorDestroyed"), FWorkerDefinition::AllClients, nullptr, nullptr, [](ASpatialFunctionalTest* NetTest, float DeltaTime) { - int Counter = 0; - int ExpectedDormancyActors = 0; - for (TActorIterator Iter(NetTest->GetWorld()); Iter; ++Iter) - { - Counter++; - } - if (Counter == ExpectedDormancyActors) - { - NetTest->AssertEqual_Int(Counter, ExpectedDormancyActors, TEXT("Number of TestDormancyActors in client world"), NetTest); - NetTest->FinishStep(); - } - }, 5.0f); + { // Step 4 - Observe the test actor has been deleted on the client. + AddStep( + TEXT("ClientCheckActorDestroyed"), FWorkerDefinition::AllClients, nullptr, nullptr, + [this](float DeltaTime) { + int Counter = 0; + int ExpectedDormancyActors = 0; + for (TActorIterator Iter(GetWorld()); Iter; ++Iter) + { + Counter++; + } + + RequireEqual_Int(Counter, ExpectedDormancyActors, TEXT("Number of TestDormancyActors in client world")); + + FinishStep(); + }, + 5.0f); } } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DormancyAndTombstoneTest/DormancyAndTombstoneTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DormancyAndTombstoneTest/DormancyAndTombstoneTest.h index 85f2e6aaf1..1380e063b8 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DormancyAndTombstoneTest/DormancyAndTombstoneTest.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DormancyAndTombstoneTest/DormancyAndTombstoneTest.h @@ -10,9 +10,9 @@ UCLASS() class SPATIALGDKFUNCTIONALTESTS_API ADormancyAndTombstoneTest : public ASpatialFunctionalTest { GENERATED_BODY() - + public: ADormancyAndTombstoneTest(); - virtual void BeginPlay() override; + virtual void PrepareTest() override; }; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DormancyAndTombstoneTest/DormancyTestActor.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DormancyAndTombstoneTest/DormancyTestActor.cpp index c9441628e4..017d2e6242 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DormancyAndTombstoneTest/DormancyTestActor.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DormancyAndTombstoneTest/DormancyTestActor.cpp @@ -8,21 +8,11 @@ ADormancyTestActor::ADormancyTestActor() { - bReplicates = true; TestIntProp = 0; - GetStaticMeshComponent()->SetStaticMesh(LoadObject(nullptr, TEXT("StaticMesh'/Engine/BasicShapes/Sphere.Sphere'"))); - GetStaticMeshComponent()->SetMaterial(0, LoadObject(nullptr, TEXT("Material'/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial'"))); - NetDormancy = DORM_Initial; // By default dormant initially, as we have no way to correctly set this at runtime. -#if ENGINE_MINOR_VERSION < 24 - bHidden = true; -#else - SetHidden(true); -#endif } - void ADormancyTestActor::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DormancyAndTombstoneTest/DormancyTestActor.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DormancyAndTombstoneTest/DormancyTestActor.h index d547bd530d..5e23d924a2 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DormancyAndTombstoneTest/DormancyTestActor.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DormancyAndTombstoneTest/DormancyTestActor.h @@ -3,7 +3,7 @@ #pragma once #include "CoreMinimal.h" -#include "Engine/StaticMeshActor.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h" #include "DormancyTestActor.generated.h" /** @@ -11,10 +11,10 @@ * Has a TestIntProp to see if it replicates when it should/shouldn't. */ UCLASS() -class SPATIALGDKFUNCTIONALTESTS_API ADormancyTestActor : public AStaticMeshActor +class SPATIALGDKFUNCTIONALTESTS_API ADormancyTestActor : public AReplicatedTestActorBase { GENERATED_BODY() - + public: ADormancyTestActor(); diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubObjectsTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubObjectsTest.h new file mode 100644 index 0000000000..7eeaf5326a --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubObjectsTest.h @@ -0,0 +1,36 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialFunctionalTest.h" +#include "DynamicSubobjectsTest.generated.h" + +class ATestMovementCharacter; +class AReplicatedGASTestActor; + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ADynamicSubobjectsTest : public ASpatialFunctionalTest +{ + GENERATED_BODY() + +public: + ADynamicSubobjectsTest(); + + virtual void PrepareTest() override; + + // A reference to the Default Pawn of Client 1 to allow for repossession in the final step of the test. + APawn* ClientOneDefaultPawn; + + ATestMovementCharacter* ClientOneSpawnedPawn; + + AReplicatedGASTestActor* TestActor; + + // The spawn location for Client 1's Pawn; + FVector CharacterSpawnLocation; + + // A remote location where Client 1's Pawn will be moved in order to not see the AReplicatedVisibilityTestActor. + FVector CharacterRemoteLocation; + + float StepTimer; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubobjectsTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubobjectsTest.cpp new file mode 100644 index 0000000000..3645db4f86 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/DynamicSubobjectsTest.cpp @@ -0,0 +1,249 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "DynamicSubobjectsTest.h" +#include "ReplicatedGASTestActor.h" +#include "SpatialFunctionalTestFlowController.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/TestMovementCharacter.h" +#include "SpatialGDKSettings.h" + +#include "GameFramework/PlayerController.h" +#include "Kismet/GameplayStatics.h" + +/** + * Tests if the dynamic Subobject of the AReplicatedGASTestActor is not duplicated on Clients when leaving + * and re-entering interest. + * + * The test includes a single server and one client worker. + * The flow is as follows: + * - Setup: + * - One cube actor already placed in the level at Location FVector(0.0f, 0.0f, 80.0f) needs to be a startup actor - bNetLoadOnClient = + *true. + * - The Server spawns a TestMovementCharacter and makes Client 1 possess it. + * - Test: + * - Each worker tests if it can initially see the AReplicatedGASTestActor. + * - Repeat the following steps MaxDynamicallyAttachedSubobjectsPerClass + 1 times: + * - After ensuring possession happened, the Server moves Client 1's Character to a remote location, so it cannot see the + *AReplicatedGASTestActor. + * - After ensuring movement replicated correctly, Client 1 checks it can no longer see the AReplicatedGASTestActor. + * - The Server moves the character of Client 1 back close to its spawn location, so that the AReplicatedGASTestActor is + *in its interest area. + * - If the "Too many dynamic sub objects" error does not appears in the log the test is successful. + * - Cleanup: + * - Client 1 repossesses its default pawn. + * - The spawned Character is destroyed. + */ + +const static float StepTimeLimit = 10.0f; + +ADynamicSubobjectsTest::ADynamicSubobjectsTest() + : Super() +{ + Author = "Evi"; + Description = TEXT("Test Dynamic Subobjects Duplication in Client"); + + CharacterSpawnLocation = FVector(0.0f, 120.0f, 40.0f); + CharacterRemoteLocation = FVector(20000.0f, 20000.0f, 40.0f); // Outside of the interest range of the client +} + +void ADynamicSubobjectsTest::PrepareTest() +{ + Super::PrepareTest(); + + const int DynamicComponentsPerClass = GetDefault()->MaxDynamicallyAttachedSubobjectsPerClass; + StepTimer = 0.0f; + + { // Step 0 - The server spawn a TestMovementCharacter and makes Client 1 possess it. + AddStep(TEXT("DynamicSubobjectsTestSetup"), FWorkerDefinition::Server(1), nullptr, [this]() { + ASpatialFunctionalTestFlowController* ClientOneFlowController = GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1); + APlayerController* PlayerController = Cast(ClientOneFlowController->GetOwner()); + + if (IsValid(PlayerController)) + { + ClientOneSpawnedPawn = + GetWorld()->SpawnActor(CharacterSpawnLocation, FRotator::ZeroRotator, FActorSpawnParameters()); + RegisterAutoDestroyActor(ClientOneSpawnedPawn); + + ClientOneDefaultPawn = PlayerController->GetPawn(); + + PlayerController->Possess(ClientOneSpawnedPawn); + + FinishStep(); + } + }); + } + + { // Step 1 - All workers check if they have one AReplicatedGASTestActor in the world, and set a reference to it. + AddStep( + TEXT("DynamicSubobjectsTestAllWorkers"), FWorkerDefinition::AllWorkers, nullptr, nullptr, + [this](float DeltaTime) { + TArray FoundActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedGASTestActor::StaticClass(), FoundActors); + + if (FoundActors.Num() == 1) + { + TestActor = Cast(FoundActors[0]); + + if (IsValid(TestActor)) + { + FinishStep(); + } + } + }, + StepTimeLimit); + } + + { // Step 2 - Client 1 checks if it has correctly possessed the TestMovementCharacter. + AddStep( + TEXT("DynamicSubobjectsTestClientCheckPossesion"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this](float DeltaTime) { + ASpatialFunctionalTestFlowController* FlowController = GetLocalFlowController(); + APlayerController* PlayerController = Cast(FlowController->GetOwner()); + ATestMovementCharacter* PlayerCharacter = Cast(PlayerController->GetPawn()); + if (IsValid(PlayerController)) + { + if (PlayerCharacter == PlayerController->AcknowledgedPawn) + { + FinishStep(); + } + } + }, + StepTimeLimit); + } + + for (int i = 0; i < DynamicComponentsPerClass + 1; ++i) + { + { // Step 3 - Server moves the TestMovementCharacter of Client 1 to a remote location, so that it does not see the + // AReplicatedGASTestActor. + AddStep(TEXT("DynamicSubobjectsTestServerMoveClient1"), FWorkerDefinition::Server(1), nullptr, [this, i]() { + if (ClientOneSpawnedPawn->SetActorLocation(CharacterRemoteLocation)) + { + if (ClientOneSpawnedPawn->GetActorLocation().Equals(CharacterRemoteLocation, 1.0f)) + { + FinishStep(); + } + } + }); + } + + { // Step 4 - Client 1 makes sure that the movement was correctly replicated + AddStep( + TEXT("DynamicSubobjectsTestClientCheckFirstMovement"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this](float DeltaTime) { + ASpatialFunctionalTestFlowController* FlowController = GetLocalFlowController(); + APlayerController* PlayerController = Cast(FlowController->GetOwner()); + ATestMovementCharacter* PlayerCharacter = Cast(PlayerController->GetPawn()); + + if (IsValid(PlayerCharacter)) + { + if (PlayerCharacter->GetActorLocation().Equals(CharacterRemoteLocation, 1.0f)) + { + FinishStep(); + } + } + }, + StepTimeLimit); + } + + { // Step 5 - Server increases AReplicatedGASTestActor's TestIntProperty to enable checking if the client is out of interest later. + AddStep(TEXT("DynamicSubobjectsTestServerIncreasesIntValue"), FWorkerDefinition::Server(1), nullptr, [this, i]() { + TArray FoundActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedGASTestActor::StaticClass(), FoundActors); + if (FoundActors.Num() == 1) + { + TestActor = Cast(FoundActors[0]); + TestActor->TestIntProperty = i + 1; + } + if (TestActor->TestIntProperty == i + 1) + { + FinishStep(); + } + }); + } + + { // Step 6 - Client 1 checks it can no longer see the AReplicatedGASTestActor + AddStep( + TEXT("DynamicSubobjectsTestClientCheckIntValueIncreased"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this, i](float DeltaTime) { + TArray FoundActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedGASTestActor::StaticClass(), FoundActors); + if (FoundActors.Num() == 1) + { + TestActor = Cast(FoundActors[0]); + + RequireNotEqual_Int(TestActor->TestIntProperty, i + 1, TEXT("Check TestIntProperty didn't get replicated")); + StepTimer += DeltaTime; + if (StepTimer >= 0.5f) + { + FinishStep(); + StepTimer = 0.0f; // reset for the next time + } + } + }, + StepTimeLimit); + } + + { // Step7 - Server moves Client 1 close to the cube. + AddStep(TEXT("DynamicSubobjectsTestServerMoveClient1CloseToCube"), FWorkerDefinition::Server(1), nullptr, [this]() { + if (ClientOneSpawnedPawn->SetActorLocation(CharacterSpawnLocation)) + { + if (ClientOneSpawnedPawn->GetActorLocation().Equals(CharacterSpawnLocation, 1.0f)) + { + FinishStep(); + } + } + }); + } + + { // Step 8 - Client 1 checks that the movement was replicated correctly. + AddStep( + TEXT("DynamicSubobjectsTestClientCheckSecondMovement"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this](float DeltaTime) { + ASpatialFunctionalTestFlowController* FlowController = GetLocalFlowController(); + APlayerController* PlayerController = Cast(FlowController->GetOwner()); + ATestMovementCharacter* PlayerCharacter = Cast(PlayerController->GetPawn()); + + if (IsValid(PlayerCharacter)) + { + if (PlayerCharacter->GetActorLocation().Equals(CharacterSpawnLocation, 1.0f)) + { + FinishStep(); + } + } + }, + StepTimeLimit); + } + + { // Step 9 - Client 1 checks it can see the AReplicatedGASTestActor + AddStep( + TEXT("DynamicSubobjectsTestClientCheckIntValueIncreased2"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this, i](float DeltaTime) { + TArray FoundActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedGASTestActor::StaticClass(), FoundActors); + if (FoundActors.Num() == 1) + { + TestActor = Cast(FoundActors[0]); + if (TestActor->TestIntProperty == i + 1) + { + FinishStep(); + } + } + }, + StepTimeLimit); + } + } + + { // Step 10 - Server Cleanup. + AddStep(TEXT("DynamicSubobjectsTestServerCleanup"), FWorkerDefinition::Server(1), nullptr, [this]() { + // Possess the original pawn, so that the spawned character can get destroyed correctly + ASpatialFunctionalTestFlowController* ClientOneFlowController = GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1); + APlayerController* PlayerController = Cast(ClientOneFlowController->GetOwner()); + + if (IsValid(PlayerController)) + { + PlayerController->Possess(ClientOneDefaultPawn); + + FinishStep(); + } + }); + } +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/ReplicatedGASTestActor.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/ReplicatedGASTestActor.cpp new file mode 100644 index 0000000000..dfc20c3bfe --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/ReplicatedGASTestActor.cpp @@ -0,0 +1,19 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "ReplicatedGASTestActor.h" +#include "Net/UnrealNetwork.h" + +AReplicatedGASTestActor::AReplicatedGASTestActor() +{ + TestIntProperty = 0; + bNetLoadOnClient = true; + bNetLoadOnNonAuthServer = true; + bReplicates = true; +} + +void AReplicatedGASTestActor::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(AReplicatedGASTestActor, TestIntProperty); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/ReplicatedGASTestActor.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/ReplicatedGASTestActor.h new file mode 100644 index 0000000000..ef5c3ed4f4 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/DynamicSubobjectsTest/ReplicatedGASTestActor.h @@ -0,0 +1,22 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h" + +#include "ReplicatedGASTestActor.generated.h" + +UCLASS() +class AReplicatedGASTestActor : public AReplicatedTestActorBase +{ + GENERATED_BODY() + +public: + AReplicatedGASTestActor(); + + void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + UPROPERTY(Replicated) + int TestIntProperty; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/ComponentUpdateEventTracingTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/ComponentUpdateEventTracingTest.cpp new file mode 100644 index 0000000000..763f04994a --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/ComponentUpdateEventTracingTest.cpp @@ -0,0 +1,42 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "ComponentUpdateEventTracingTest.h" + +AComponentUpdateEventTracingTest::AComponentUpdateEventTracingTest() +{ + Author = "Matthew Sandford"; + Description = TEXT("Test checking the component update trace events have appropriate causes"); + + FilterEventNames = { ComponentUpdateEventName, ReceiveOpEventName, MergeComponentUpdateEventName }; + WorkerDefinition = FWorkerDefinition::Client(1); +} + +void AComponentUpdateEventTracingTest::FinishEventTraceTest() +{ + int EventsTested = 0; + int EventsFailed = 0; + for (const auto& Pair : TraceEvents) + { + const FString& SpanIdString = Pair.Key; + const FName& EventName = Pair.Value; + + if (EventName != ComponentUpdateEventName) + { + continue; + } + + EventsTested++; + + if (!CheckEventTraceCause(SpanIdString, { ReceiveOpEventName, MergeComponentUpdateEventName })) + { + EventsFailed++; + } + } + + bool bSuccess = EventsTested > 0 && EventsFailed == 0; + AssertTrue(bSuccess, + FString::Printf(TEXT("Component update trace events have the expected causes. Events Tested: %d, Events Failed: %d"), + EventsTested, EventsFailed)); + + FinishStep(); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/ComponentUpdateEventTracingTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/ComponentUpdateEventTracingTest.h new file mode 100644 index 0000000000..c715072f31 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/ComponentUpdateEventTracingTest.h @@ -0,0 +1,20 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "EventTracingTest.h" + +#include "ComponentUpdateEventTracingTest.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API AComponentUpdateEventTracingTest : public AEventTracingTest +{ + GENERATED_BODY() + +public: + AComponentUpdateEventTracingTest(); + +private: + virtual void FinishEventTraceTest() override; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/EventTracingTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/EventTracingTest.cpp new file mode 100644 index 0000000000..64d7893ae0 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/EventTracingTest.cpp @@ -0,0 +1,238 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "EventTracingTest.h" + +#include "EngineClasses/SpatialGameInstance.h" +#include "Interop/Connection/SpatialConnectionManager.h" +#include "Interop/Connection/SpatialEventTracer.h" +#include "Interop/Connection/SpatialWorkerConnection.h" +#include "SpatialCommonTypes.h" + +#include +#include + +DEFINE_LOG_CATEGORY(LogEventTracingTest); + +using namespace SpatialGDK; + +const FName AEventTracingTest::ReceiveOpEventName = "worker.receive_op"; +const FName AEventTracingTest::PropertyChangedEventName = "unreal_gdk.property_changed"; +const FName AEventTracingTest::ReceivePropertyUpdateEventName = "unreal_gdk.receive_property_update"; +const FName AEventTracingTest::PushRPCEventName = "unreal_gdk.push_rpc"; +const FName AEventTracingTest::ProcessRPCEventName = "unreal_gdk.process_rpc"; +const FName AEventTracingTest::ComponentUpdateEventName = "unreal_gdk.component_update"; +const FName AEventTracingTest::MergeComponentUpdateEventName = "unreal_gdk.merge_component_update"; +const FName AEventTracingTest::UserProcessRPCEventName = "user.process_rpc"; +const FName AEventTracingTest::UserReceivePropertyEventName = "user.receive_property"; +const FName AEventTracingTest::UserReceiveComponentPropertyEventName = "user.receive_component_property"; +const FName AEventTracingTest::UserSendPropertyEventName = "user.send_property"; +const FName AEventTracingTest::UserSendComponentPropertyEventName = "user.send_component_property"; +const FName AEventTracingTest::UserSendRPCEventName = "user.send_rpc"; + +AEventTracingTest::AEventTracingTest() +{ + Author = "Matthew Sandford"; + Description = TEXT("Base class for event tracing tests"); + + SetNumRequiredClients(1); +} + +void AEventTracingTest::PrepareTest() +{ + Super::PrepareTest(); + + AddStep( + TEXT("StartEventTracingTest"), WorkerDefinition, nullptr, + [this]() { + StartEventTracingTest(); + }, + nullptr); + + AddStep(TEXT("WaitForTestToEnd"), WorkerDefinition, nullptr, nullptr, [this](float DeltaTime) { + WaitForTestToEnd(); + }); + + AddStep( + TEXT("GatherData"), WorkerDefinition, nullptr, + [this]() { + GatherData(); + }, + nullptr); + + AddStep( + TEXT("FinishEventTraceTest"), WorkerDefinition, nullptr, + [this]() { + FinishEventTraceTest(); + }, + nullptr); +} + +void AEventTracingTest::StartEventTracingTest() +{ + TestStartTime = FDateTime::Now(); + FinishStep(); +} + +void AEventTracingTest::WaitForTestToEnd() +{ + if (TestStartTime + FTimespan::FromSeconds(TestTime) > FDateTime::Now()) + { + return; + } + + FinishStep(); +} + +void AEventTracingTest::FinishEventTraceTest() +{ + FinishStep(); +} + +void AEventTracingTest::GatherData() +{ + USpatialGameInstance* GameInstance = GetGameInstance(); + USpatialConnectionManager* ConnectionManager = GameInstance->GetSpatialConnectionManager(); + SpatialEventTracer* EventTracer = ConnectionManager->GetWorkerConnection()->GetEventTracer(); + if (EventTracer == nullptr) + { + return; + } + + FString EventsFolderPath = EventTracer->GetFolderPath(); + + IFileManager& FileManager = IFileManager::Get(); + + TArray Files; + FileManager.FindFiles(Files, *EventsFolderPath, *FString(".trace")); + + if (Files.Num() < 2) + { + UE_LOG(LogEventTracingTest, Error, TEXT("Could not find all required event tracing files")); + return; + } + + struct FileCreationTime + { + FString FilePath; + FDateTime CreationTime; + }; + + TArray FileCreationTimes; + for (const FString& File : Files) + { + FString FilePath = FPaths::Combine(EventsFolderPath, File); + FileCreationTimes.Add({ FilePath, FileManager.GetTimeStamp(*FilePath) }); + } + + FileCreationTimes.Sort([](const FileCreationTime& A, const FileCreationTime& B) { + return A.CreationTime > B.CreationTime; + }); + + bool bFoundClient = false; + bool bFoundWorker = false; + for (const FileCreationTime& FileCreation : FileCreationTimes) + { + if (!bFoundClient && FileCreation.FilePath.Contains("UnrealClient")) + { + GatherDataFromFile(FileCreation.FilePath); + bFoundClient = true; + } + + if (!bFoundWorker && FileCreation.FilePath.Contains("UnrealWorker")) + { + GatherDataFromFile(FileCreation.FilePath); + bFoundWorker = true; + } + + if (bFoundClient && bFoundWorker) + { + break; + } + } + + if (!bFoundClient || !bFoundWorker) + { + UE_LOG(LogEventTracingTest, Error, TEXT("Could not find all required event tracing files")); + return; + } + + FinishStep(); +} + +void AEventTracingTest::GatherDataFromFile(const FString& FilePath) +{ + struct StreamDeleter + { + void operator()(Io_Stream* StreamToDestroy) const { Io_Stream_Destroy(StreamToDestroy); }; + }; + + TUniquePtr Stream; + Stream.Reset(Io_CreateFileStream(TCHAR_TO_ANSI(*FilePath), Io_OpenMode::IO_OPEN_MODE_READ)); + + uint32_t BytesToRead = 1; + int8_t ReturnCode = 1; + while (BytesToRead != 0 && ReturnCode == 1) + { + BytesToRead = Trace_GetNextSerializedItemSize(Stream.Get()); + + Trace_Item* Item = Trace_Item_GetThreadLocal(); + if (BytesToRead != 0) + { + ReturnCode = Trace_DeserializeItemFromStream(Stream.Get(), Item, BytesToRead); + } + + if (Item != nullptr) + { + if (Item->item_type == TRACE_ITEM_TYPE_EVENT) + { + const Trace_Event& Event = Item->item.event; + FName EventName = FName(*FString(Event.type)); + + if (FilterEventNames.Num() == 0 || FilterEventNames.Contains(EventName)) + { + FString SpanIdString = FSpatialGDKSpanId::ToString(Event.span_id); + FName& CachedEventName = TraceEvents.FindOrAdd(SpanIdString); + CachedEventName = EventName; + } + } + else if (Item->item_type == TRACE_ITEM_TYPE_SPAN) + { + const Trace_Span& Span = Item->item.span; + + FString SpanIdString = FSpatialGDKSpanId::ToString(Span.id); + TArray& Causes = TraceSpans.FindOrAdd(SpanIdString); + for (uint64 i = 0; i < Span.cause_count; ++i) + { + const int32 ByteOffset = i * TRACE_SPAN_ID_SIZE_BYTES; + Causes.Add(FSpatialGDKSpanId::ToString(Span.causes + ByteOffset)); + } + } + } + } + + Stream = nullptr; +} + +bool AEventTracingTest::CheckEventTraceCause(const FString& SpanIdString, const TArray& CauseEventNames, int MinimumCauses /*= 1*/) +{ + TArray* Causes = TraceSpans.Find(SpanIdString); + if (Causes == nullptr || Causes->Num() < MinimumCauses) + { + return false; + } + + for (const FString& CauseSpanIdString : *Causes) + { + const FName* CauseEventName = TraceEvents.Find(CauseSpanIdString); + if (CauseEventName == nullptr) + { + return false; + } + if (!CauseEventNames.Contains(*CauseEventName)) + { + return false; + } + } + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/EventTracingTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/EventTracingTest.h new file mode 100644 index 0000000000..af0b8a95d2 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/EventTracingTest.h @@ -0,0 +1,64 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialFunctionalTest.h" + +#include "EventTracingTest.generated.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogEventTracingTest, Log, All); + +namespace worker +{ +namespace c +{ +struct Trace_Item; +} // namespace c +} // namespace worker + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API AEventTracingTest : public ASpatialFunctionalTest +{ + GENERATED_BODY() + +public: + AEventTracingTest(); + + virtual void PrepareTest() override; + +protected: + const static FName ReceiveOpEventName; + const static FName PropertyChangedEventName; + const static FName ReceivePropertyUpdateEventName; + const static FName PushRPCEventName; + const static FName ProcessRPCEventName; + const static FName ComponentUpdateEventName; + const static FName MergeComponentUpdateEventName; + const static FName UserProcessRPCEventName; + const static FName UserReceivePropertyEventName; + const static FName UserReceiveComponentPropertyEventName; + const static FName UserSendPropertyEventName; + const static FName UserSendComponentPropertyEventName; + const static FName UserSendRPCEventName; + + FWorkerDefinition WorkerDefinition; + TArray FilterEventNames; + + float TestTime = 8.0f; + + TMap TraceEvents; + TMap> TraceSpans; + + bool CheckEventTraceCause(const FString& SpanIdString, const TArray& CauseEventNames, int MinimumCauses = 1); + + virtual void FinishEventTraceTest(); + +private: + FDateTime TestStartTime; + + void StartEventTracingTest(); + void WaitForTestToEnd(); + void GatherData(); + void GatherDataFromFile(const FString& FilePath); +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/MergeComponentEventTracingTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/MergeComponentEventTracingTest.cpp new file mode 100644 index 0000000000..45709f912d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/MergeComponentEventTracingTest.cpp @@ -0,0 +1,43 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "MergeComponentEventTracingTest.h" + +AMergeComponentEventTracingTest::AMergeComponentEventTracingTest() +{ + Author = "Matthew Sandford"; + Description = TEXT("Test checking the merge component field trace events have appropriate causes"); + + FilterEventNames = { MergeComponentUpdateEventName, ReceiveOpEventName }; + WorkerDefinition = FWorkerDefinition::Client(1); +} + +void AMergeComponentEventTracingTest::FinishEventTraceTest() +{ + int EventsTested = 0; + int EventsFailed = 0; + + for (const auto& Pair : TraceEvents) + { + const FString& SpanIdString = Pair.Key; + const FName& EventName = Pair.Value; + + if (EventName != MergeComponentUpdateEventName) + { + continue; + } + + EventsTested++; + + if (!CheckEventTraceCause(SpanIdString, { MergeComponentUpdateEventName, ReceiveOpEventName }, 2)) + { + EventsFailed++; + } + } + + bool bSuccess = EventsTested > 0 && EventsFailed == 0; + AssertTrue(bSuccess, + FString::Printf(TEXT("Merge component field trace events have the expected causes. Events Tested: %d, Events Failed: %d"), + EventsTested, EventsFailed)); + + FinishStep(); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/MergeComponentEventTracingTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/MergeComponentEventTracingTest.h new file mode 100644 index 0000000000..751164137a --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/MergeComponentEventTracingTest.h @@ -0,0 +1,20 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "EventTracingTest.h" + +#include "MergeComponentEventTracingTest.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API AMergeComponentEventTracingTest : public AEventTracingTest +{ + GENERATED_BODY() + +public: + AMergeComponentEventTracingTest(); + +private: + virtual void FinishEventTraceTest() override; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/ProcessRPCEventTracingTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/ProcessRPCEventTracingTest.cpp new file mode 100644 index 0000000000..f9c4100d6b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/ProcessRPCEventTracingTest.cpp @@ -0,0 +1,41 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "ProcessRPCEventTracingTest.h" + +AProcessRPCEventTracingTest::AProcessRPCEventTracingTest() +{ + Author = "Matthew Sandford"; + Description = TEXT("Test checking the process RPC trace events have appropriate causes"); + + FilterEventNames = { ProcessRPCEventName, ReceiveOpEventName, MergeComponentUpdateEventName }; + WorkerDefinition = FWorkerDefinition::Server(1); +} + +void AProcessRPCEventTracingTest::FinishEventTraceTest() +{ + int EventsTested = 0; + int EventsFailed = 0; + for (const auto& Pair : TraceEvents) + { + const FString& SpanIdString = Pair.Key; + const FName& EventName = Pair.Value; + + if (EventName != ProcessRPCEventName) + { + continue; + } + + EventsTested++; + + if (!CheckEventTraceCause(SpanIdString, { ReceiveOpEventName, MergeComponentUpdateEventName })) + { + EventsFailed++; + } + } + + bool bSuccess = EventsTested > 0 && EventsFailed == 0; + AssertTrue(bSuccess, FString::Printf(TEXT("Process RPC trace events have the expected causes. Events Tested: %d, Events Failed: %d"), + EventsTested, EventsFailed)); + + FinishStep(); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/ProcessRPCEventTracingTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/ProcessRPCEventTracingTest.h new file mode 100644 index 0000000000..7044394bbe --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/ProcessRPCEventTracingTest.h @@ -0,0 +1,20 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "EventTracingTest.h" + +#include "ProcessRPCEventTracingTest.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API AProcessRPCEventTracingTest : public AEventTracingTest +{ + GENERATED_BODY() + +public: + AProcessRPCEventTracingTest(); + +private: + virtual void FinishEventTraceTest() override; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/PropertyUpdateEventTracingTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/PropertyUpdateEventTracingTest.cpp new file mode 100644 index 0000000000..59bef3188d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/PropertyUpdateEventTracingTest.cpp @@ -0,0 +1,42 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "PropertyUpdateEventTracingTest.h" + +APropertyUpdateEventTracingTest::APropertyUpdateEventTracingTest() +{ + Author = "Matthew Sandford"; + Description = TEXT("Test checking the property update trace events have appropriate causes"); + + FilterEventNames = { ReceivePropertyUpdateEventName, ReceiveOpEventName, MergeComponentUpdateEventName }; + WorkerDefinition = FWorkerDefinition::Client(1); +} + +void APropertyUpdateEventTracingTest::FinishEventTraceTest() +{ + int EventsTested = 0; + int EventsFailed = 0; + for (const auto& Pair : TraceEvents) + { + const FString& SpanIdString = Pair.Key; + const FName& EventName = Pair.Value; + + if (EventName != ReceivePropertyUpdateEventName) + { + continue; + } + + EventsTested++; + + if (!CheckEventTraceCause(SpanIdString, { ReceiveOpEventName, MergeComponentUpdateEventName })) + { + EventsFailed++; + } + } + + bool bSuccess = EventsTested > 0 && EventsFailed == 0; + AssertTrue(bSuccess, + FString::Printf(TEXT("Process property update events have the expected causes. Events Tested: %d, Events Failed: %d"), + EventsTested, EventsFailed)); + + FinishStep(); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/PropertyUpdateEventTracingTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/PropertyUpdateEventTracingTest.h new file mode 100644 index 0000000000..6d16e72f23 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/PropertyUpdateEventTracingTest.h @@ -0,0 +1,20 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "EventTracingTest.h" + +#include "PropertyUpdateEventTracingTest.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API APropertyUpdateEventTracingTest : public AEventTracingTest +{ + GENERATED_BODY() + +public: + APropertyUpdateEventTracingTest(); + +private: + virtual void FinishEventTraceTest() override; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserProcessRPCEventTracingTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserProcessRPCEventTracingTest.cpp new file mode 100644 index 0000000000..c1f6bdb457 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserProcessRPCEventTracingTest.cpp @@ -0,0 +1,42 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "UserProcessRPCEventTracingTest.h" + +AUserProcessRPCEventTracingTest::AUserProcessRPCEventTracingTest() +{ + Author = "Matthew Sandford"; + Description = TEXT("Test checking user event traces can be caused by rpcs process events"); + + FilterEventNames = { UserProcessRPCEventName, ProcessRPCEventName }; + WorkerDefinition = FWorkerDefinition::Client(1); +} + +void AUserProcessRPCEventTracingTest::FinishEventTraceTest() +{ + int EventsTested = 0; + int EventsFailed = 0; + for (const auto& Pair : TraceEvents) + { + const FString& SpanIdString = Pair.Key; + const FName& EventName = Pair.Value; + + if (EventName != UserProcessRPCEventName) + { + continue; + } + + EventsTested++; + + if (!CheckEventTraceCause(SpanIdString, { ProcessRPCEventName }, 1)) + { + EventsFailed++; + } + } + + bool bSuccess = EventsTested > 0 && EventsFailed == 0; + AssertTrue(bSuccess, + FString::Printf(TEXT("User event have been caused by the expected process RPC events. Events Tested: %d, Events Failed: %d"), + EventsTested, EventsFailed)); + + FinishStep(); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserProcessRPCEventTracingTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserProcessRPCEventTracingTest.h new file mode 100644 index 0000000000..3c9c53692d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserProcessRPCEventTracingTest.h @@ -0,0 +1,20 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "EventTracingTest.h" + +#include "UserProcessRPCEventTracingTest.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API AUserProcessRPCEventTracingTest : public AEventTracingTest +{ + GENERATED_BODY() + +public: + AUserProcessRPCEventTracingTest(); + +private: + virtual void FinishEventTraceTest() override; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserReceivePropertyEventTracingTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserReceivePropertyEventTracingTest.cpp new file mode 100644 index 0000000000..3b79af54e9 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserReceivePropertyEventTracingTest.cpp @@ -0,0 +1,44 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "UserReceivePropertyEventTracingTest.h" + +AUserReceivePropertyEventTracingTest::AUserReceivePropertyEventTracingTest() +{ + Author = "Matthew Sandford"; + Description = TEXT("Test checking user event traces can be caused by receive property update events"); + + FilterEventNames = { UserReceivePropertyEventName, UserReceiveComponentPropertyEventName, ReceivePropertyUpdateEventName }; + WorkerDefinition = FWorkerDefinition::Client(1); +} + +void AUserReceivePropertyEventTracingTest::FinishEventTraceTest() +{ + int EventsTested = 0; + int EventsFailed = 0; + for (const auto& Pair : TraceEvents) + { + const FString& SpanIdString = Pair.Key; + const FName& EventName = Pair.Value; + + if (EventName != UserReceivePropertyEventName && EventName != UserReceiveComponentPropertyEventName) + { + continue; + } + + EventsTested++; + + if (!CheckEventTraceCause(SpanIdString, { ReceivePropertyUpdateEventName }, 1)) + { + EventsFailed++; + } + } + + bool bSuccess = EventsTested > 0 && EventsFailed == 0; + AssertTrue( + bSuccess, + FString::Printf( + TEXT("User events have been caused by the expected receive property update events. Events Tested: %d, Events Failed: %d"), + EventsTested, EventsFailed)); + + FinishStep(); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserReceivePropertyEventTracingTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserReceivePropertyEventTracingTest.h new file mode 100644 index 0000000000..281c6c5ffe --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserReceivePropertyEventTracingTest.h @@ -0,0 +1,20 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "EventTracingTest.h" + +#include "UserReceivePropertyEventTracingTest.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API AUserReceivePropertyEventTracingTest : public AEventTracingTest +{ + GENERATED_BODY() + +public: + AUserReceivePropertyEventTracingTest(); + +private: + virtual void FinishEventTraceTest() override; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserSendPropertyEventTracingTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserSendPropertyEventTracingTest.cpp new file mode 100644 index 0000000000..e9e0a30182 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserSendPropertyEventTracingTest.cpp @@ -0,0 +1,52 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "UserSendPropertyEventTracingTest.h" + +AUserSendPropertyEventTracingTest::AUserSendPropertyEventTracingTest() +{ + Author = "Matthew Sandford"; + Description = TEXT("Test checking user event traces can cause send property update events"); + + FilterEventNames = { UserSendPropertyEventName, UserSendComponentPropertyEventName, PropertyChangedEventName }; + WorkerDefinition = FWorkerDefinition::Client(1); +} + +void AUserSendPropertyEventTracingTest::FinishEventTraceTest() +{ + TArray UserEventSpanIds; + TArray SendPropertyUpdatesCauseSpanIds; + + for (const auto& Pair : TraceEvents) + { + const FString& SpanIdString = Pair.Key; + const FName& EventName = Pair.Value; + + if (EventName == UserSendPropertyEventName || EventName == UserSendComponentPropertyEventName) + { + UserEventSpanIds.Add(SpanIdString); + } + else if (EventName == PropertyChangedEventName) + { + TArray* Causes = TraceSpans.Find(SpanIdString); + if (Causes != nullptr) + { + SendPropertyUpdatesCauseSpanIds += *Causes; + } + } + } + + int EventsTested = UserEventSpanIds.Num(); + + for (const FString& CauseSpanId : SendPropertyUpdatesCauseSpanIds) + { + UserEventSpanIds.Remove(CauseSpanId); + } + + int EventsFailed = UserEventSpanIds.Num(); + bool bSuccess = EventsTested > 0 && EventsFailed == 0; + AssertTrue(bSuccess, FString::Printf( + TEXT("User event have caused the expected send property update events. Events Tested: %d, Events Failed: %d"), + EventsTested, EventsFailed)); + + FinishStep(); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserSendPropertyEventTracingTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserSendPropertyEventTracingTest.h new file mode 100644 index 0000000000..43fc47f719 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserSendPropertyEventTracingTest.h @@ -0,0 +1,20 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "EventTracingTest.h" + +#include "UserSendPropertyEventTracingTest.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API AUserSendPropertyEventTracingTest : public AEventTracingTest +{ + GENERATED_BODY() + +public: + AUserSendPropertyEventTracingTest(); + +private: + virtual void FinishEventTraceTest() override; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserSendRPCEventTracingTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserSendRPCEventTracingTest.cpp new file mode 100644 index 0000000000..054654d7a9 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserSendRPCEventTracingTest.cpp @@ -0,0 +1,51 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "UserSendRPCEventTracingTest.h" + +AUserSendRPCEventTracingTest::AUserSendRPCEventTracingTest() +{ + Author = "Matthew Sandford"; + Description = TEXT("Test checking user event traces can cause rpcs send events"); + + FilterEventNames = { PushRPCEventName, UserSendRPCEventName }; + WorkerDefinition = FWorkerDefinition::Client(1); +} + +void AUserSendRPCEventTracingTest::FinishEventTraceTest() +{ + TArray UserEventSpanIds; + TArray SendRPCCauseSpanIds; + + for (const auto& Pair : TraceEvents) + { + const FString& SpanIdString = Pair.Key; + const FName& EventName = Pair.Value; + + if (EventName == UserSendRPCEventName) + { + UserEventSpanIds.Add(SpanIdString); + } + else if (EventName == PushRPCEventName) + { + TArray* Causes = TraceSpans.Find(SpanIdString); + if (Causes != nullptr) + { + SendRPCCauseSpanIds += *Causes; + } + } + } + + int EventsTested = UserEventSpanIds.Num(); + + for (const FString& CauseSpanId : SendRPCCauseSpanIds) + { + UserEventSpanIds.Remove(CauseSpanId); + } + + int EventsFailed = UserEventSpanIds.Num(); + bool bSuccess = EventsTested > 0 && EventsFailed == 0; + AssertTrue(bSuccess, FString::Printf(TEXT("User event have caused the expected send RPC events. Events Tested: %d, Events Failed: %d"), + EventsTested, EventsFailed)); + + FinishStep(); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserSendRPCEventTracingTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserSendRPCEventTracingTest.h new file mode 100644 index 0000000000..81884d5d57 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/EventTracingTests/UserSendRPCEventTracingTest.h @@ -0,0 +1,20 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "EventTracingTest.h" + +#include "UserSendRPCEventTracingTest.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API AUserSendRPCEventTracingTest : public AEventTracingTest +{ + GENERATED_BODY() + +public: + AUserSendRPCEventTracingTest(); + +private: + virtual void FinishEventTraceTest() override; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/RegisterAutoDestroyActorsTest/RegisterAutoDestroyActorsTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/RegisterAutoDestroyActorsTest/RegisterAutoDestroyActorsTest.cpp index 3fea0687cc..6db2c139a8 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/RegisterAutoDestroyActorsTest/RegisterAutoDestroyActorsTest.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/RegisterAutoDestroyActorsTest/RegisterAutoDestroyActorsTest.cpp @@ -1,9 +1,8 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved - #include "RegisterAutoDestroyActorsTest.h" -#include "GameFramework/Character.h" #include "EngineClasses/SpatialNetDriver.h" +#include "GameFramework/Character.h" #include "LoadBalancing/AbstractLBStrategy.h" #include "SpatialFunctionalTestFlowController.h" @@ -13,13 +12,13 @@ ARegisterAutoDestroyActorsTestPart1::ARegisterAutoDestroyActorsTestPart1() Description = TEXT("Part1: Verify that server spawned a character and that is is visible to the clients"); } -void ARegisterAutoDestroyActorsTestPart1::BeginPlay() +void ARegisterAutoDestroyActorsTestPart1::PrepareTest() { - Super::BeginPlay(); - { // Step 1 - Spawn Actor On Auth - AddStep(TEXT("SERVER_1_Spawn"), FWorkerDefinition::Server(1), nullptr, [](ASpatialFunctionalTest* NetTest){ - UWorld* World = NetTest->GetWorld(); - int NumVirtualWorkers = NetTest->GetNumberOfServerWorkers(); + Super::PrepareTest(); + { // Step 1 - Spawn Actor On Auth + AddStep(TEXT("SERVER_1_Spawn"), FWorkerDefinition::Server(1), nullptr, [this]() { + UWorld* World = GetWorld(); + int NumVirtualWorkers = GetNumberOfServerWorkers(); // spawn 1 per server worker // since the information about positioning of the virtual workers is currently hidden, will assume they are all around zero @@ -29,61 +28,66 @@ void ARegisterAutoDestroyActorsTestPart1::BeginPlay() for (int i = 0; i != NumVirtualWorkers; ++i) { ACharacter* Character = World->SpawnActor(SpawnPosition, FRotator::ZeroRotator); - NetTest->AssertTrue(IsValid(Character), FString::Printf(TEXT("Spawned ACharacter %s in worker %s"), *GetNameSafe(Character), *NetTest->GetFlowController(ESpatialFunctionalTestWorkerType::Server, i + 1)->GetDisplayName())); + AssertTrue(IsValid(Character), + FString::Printf(TEXT("Spawned ACharacter %s in worker %s"), *GetNameSafe(Character), + *GetFlowController(ESpatialFunctionalTestWorkerType::Server, i + 1)->GetDisplayName())); SpawnPosition = SpawnPositionRotator.RotateVector(SpawnPosition); } - - NetTest->FinishStep(); + FinishStep(); }); - - } { // Step 2 - Check If Clients have it - AddStep(TEXT("CLIENT_ALL_CheckActorsSpawned"), FWorkerDefinition::AllClients, nullptr, nullptr, [](ASpatialFunctionalTest* NetTest, float DeltaTime){ + AddStep( + TEXT("CLIENT_ALL_CheckActorsSpawned"), FWorkerDefinition::AllClients, nullptr, nullptr, + [this](float DeltaTime) { int NumCharactersFound = 0; - int NumCharactersExpected = NetTest->GetNumberOfServerWorkers(); - UWorld* World = NetTest->GetWorld(); + int NumCharactersExpected = GetNumberOfServerWorkers(); + UWorld* World = GetWorld(); for (TActorIterator It(World); It; ++It) { ++NumCharactersFound; } - if(NumCharactersFound == NumCharactersExpected) + if (NumCharactersFound == NumCharactersExpected) { - NetTest->FinishStep(); + FinishStep(); } - }, 5.0f); + }, + 5.0f); } { // Step 3 - Destroy by all servers that have authority - AddStep(TEXT("SERVER_ALL_RegisterAutoDestroyActors"), FWorkerDefinition::AllServers, [](ASpatialFunctionalTest* NetTest) -> bool { - int NumCharactersFound = 0; - int NumCharactersExpected = 1; - UWorld* World = NetTest->GetWorld(); - for (TActorIterator It(World, ACharacter::StaticClass()); It; ++It) - { - if (It->HasAuthority()) + AddStep( + TEXT("SERVER_ALL_RegisterAutoDestroyActors"), FWorkerDefinition::AllServers, + [this]() -> bool { + int NumCharactersFound = 0; + int NumCharactersExpected = 1; + UWorld* World = GetWorld(); + for (TActorIterator It(World, ACharacter::StaticClass()); It; ++It) { - ++NumCharactersFound; + if (It->HasAuthority()) + { + ++NumCharactersFound; + } } - } - return NumCharactersFound == NumCharactersExpected; - }, - [](ASpatialFunctionalTest* NetTest) { - UWorld* World = NetTest->GetWorld(); - for (TActorIterator It(World); It; ++It) - { - if (It->HasAuthority()) + return NumCharactersFound == NumCharactersExpected; + }, + [this]() { + UWorld* World = GetWorld(); + for (TActorIterator It(World); It; ++It) { - NetTest->AssertTrue(IsValid(*It), FString::Printf(TEXT("Registering ACharacter for destruction: %s"), *GetNameSafe(*It))); - NetTest->RegisterAutoDestroyActor(*It); + if (It->HasAuthority()) + { + AssertTrue(IsValid(*It), FString::Printf(TEXT("Registering ACharacter for destruction: %s"), *GetNameSafe(*It))); + RegisterAutoDestroyActor(*It); + } } - } - NetTest->FinishStep(); - }, nullptr, 5.0f); + FinishStep(); + }, + nullptr, 5.0f); } } @@ -93,20 +97,21 @@ ARegisterAutoDestroyActorsTestPart2::ARegisterAutoDestroyActorsTestPart2() Description = TEXT("Part2: Verify that the actors have been destroyed across all workers"); } -void ARegisterAutoDestroyActorsTestPart2::BeginPlay() +void ARegisterAutoDestroyActorsTestPart2::PrepareTest() { - Super::BeginPlay(); + Super::PrepareTest(); { // Check nobody has characters - FSpatialFunctionalTestStepDefinition StepDefinition; - StepDefinition.bIsNativeDefinition = true; + FSpatialFunctionalTestStepDefinition StepDefinition(/*bIsNativeDefinition*/ true); + StepDefinition.StepName = TEXT("Check No Worker Has Characters"); StepDefinition.TimeLimit = 0.0f; - StepDefinition.NativeStartEvent.BindLambda([](ASpatialFunctionalTest* NetTest) { - UWorld* World = NetTest->GetWorld(); + StepDefinition.NativeStartEvent.BindLambda([this]() { + UWorld* World = GetWorld(); TActorIterator It(World); bool bHasCharacter = static_cast(It); - NetTest->AssertFalse(bHasCharacter, FString::Printf(TEXT("Cleanup of ACharacter successful, no ACharacter found by %s"), *NetTest->GetLocalFlowController()->GetDisplayName())); - NetTest->FinishStep(); + AssertFalse(bHasCharacter, FString::Printf(TEXT("Cleanup of ACharacter successful, no ACharacter found by %s"), + *GetLocalFlowController()->GetDisplayName())); + FinishStep(); }); AddStepFromDefinition(StepDefinition, FWorkerDefinition::AllWorkers); diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/RegisterAutoDestroyActorsTest/RegisterAutoDestroyActorsTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/RegisterAutoDestroyActorsTest/RegisterAutoDestroyActorsTest.h index 3f2b825267..82065bb597 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/RegisterAutoDestroyActorsTest/RegisterAutoDestroyActorsTest.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/RegisterAutoDestroyActorsTest/RegisterAutoDestroyActorsTest.h @@ -9,8 +9,8 @@ /** * These Tests are meant to test the functionality of RegisterAutoDestroyActor in Test environments. * Keep in mind that for it to run correctly you need to run both part 1 and 2, and in that order, - * since the auto destruction happens at the end of the test, so you need the next test to check - * that it is working. This test should work both with and without load-balancing, as long as + * since the auto destruction happens at the end of the test, so you need the next test to check + * that it is working. This test should work both with and without load-balancing, as long as * the servers have global interest area (limitation at this time). */ UCLASS() @@ -20,7 +20,7 @@ class SPATIALGDKFUNCTIONALTESTS_API ARegisterAutoDestroyActorsTestPart1 : public ARegisterAutoDestroyActorsTestPart1(); - virtual void BeginPlay() override; + virtual void PrepareTest() override; }; UCLASS() @@ -30,6 +30,5 @@ class SPATIALGDKFUNCTIONALTESTS_API ARegisterAutoDestroyActorsTestPart2 : public ARegisterAutoDestroyActorsTestPart2(); - virtual void BeginPlay() override; - + virtual void PrepareTest() override; }; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/RelevancyTest/RelevancyTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/RelevancyTest/RelevancyTest.cpp new file mode 100644 index 0000000000..f976cbfc9d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/RelevancyTest/RelevancyTest.cpp @@ -0,0 +1,109 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "RelevancyTest.h" +#include "SpatialFunctionalTestFlowController.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/RelevancyTestActors.h" + +#include "LoadBalancing/LayeredLBStrategy.h" + +/** + * This test tests that actors with bAlwaysRelevant are replicated to clients and servers correctly. + * + * The test should include two servers and two clients + * The flow is as follows: + * - Setup: + * - Each server spawns an AAlwaysRelevantTestActor and an AAlwaysRelevantServerOnlyTestActor + * - Test: + * - Each server validates they can see all AAlwaysRelevantTestActor and all AAlwaysRelevantServerOnlyTestActor + * - Each client validates they can see all AAlwaysRelevantTestActor and no AAlwaysRelevantServerOnlyTestActor + * - Cleanup: + * - Destroy the actors + */ + +const static float StepTimeLimit = 5.0f; + +ARelevancyTest::ARelevancyTest() + : Super() +{ + Author = "Mike"; + Description = TEXT("Test Actor Relevancy"); +} + +template +int GetNumberOfActorsOfType(UWorld* World) +{ + int Counter = 0; + for (TActorIterator Iter(World); Iter; ++Iter) + { + Counter++; + } + + return Counter; +} + +void ARelevancyTest::PrepareTest() +{ + Super::PrepareTest(); + + { // Step 0 - Spawn actors on each server + AddStep(TEXT("RelevancyTestSpawnActors"), FWorkerDefinition::AllServers, nullptr, [this]() { + ULayeredLBStrategy* RootStrategy = GetLoadBalancingStrategy(); + UAbstractLBStrategy* DefaultStrategy = RootStrategy->GetLBStrategyForLayer(SpatialConstants::DefaultLayer); + UGridBasedLBStrategy* GridStrategy = Cast(DefaultStrategy); + AssertIsValid(GridStrategy, TEXT("Invalid LBS")); + const FVector WorkerPos = GridStrategy->GetWorkerEntityPosition(); + + AlwaysRelevantActor = + GetWorld()->SpawnActor(WorkerPos, FRotator::ZeroRotator, FActorSpawnParameters()); + AlwaysRelevantServerOnlyActor = + GetWorld()->SpawnActor(WorkerPos, FRotator::ZeroRotator, FActorSpawnParameters()); + + RegisterAutoDestroyActor(AlwaysRelevantActor); + RegisterAutoDestroyActor(AlwaysRelevantServerOnlyActor); + + FinishStep(); + }); + } + + { // Step 1 - Check actors are ready on each server + AddStep( + TEXT("RelevancyTestReadyActors"), FWorkerDefinition::AllServers, + [this]() -> bool { + return (AlwaysRelevantActor->IsActorReady() && AlwaysRelevantServerOnlyActor->IsActorReady()); + }, + [this]() { + FinishStep(); + }); + } + + { // Step 2 - Check actors count is correct on servers + AddStep( + TEXT("RelevancyTestCountActorsOnServers"), FWorkerDefinition::AllServers, nullptr, nullptr, + [this](float DeltaTime) { + int NumAlwaysRelevantActors = GetNumberOfActorsOfType(GetWorld()); + int NumAlwaysServerOnlyRelevantActors = GetNumberOfActorsOfType(GetWorld()); + int NumServers = GetNumberOfServerWorkers(); + + RequireEqual_Int(NumAlwaysRelevantActors, NumServers, TEXT("Servers see expected number of always relevant actors")); + RequireEqual_Int(NumAlwaysServerOnlyRelevantActors, NumServers, + TEXT("Servers see expected number of server-only always relevant actors")); + FinishStep(); // This will only actually finish if requires are satisfied + }, + StepTimeLimit); + } + + { // Step 3 - Check actors count is correct on clients + AddStep( + TEXT("RelevancyTestCountActorsOnClients"), FWorkerDefinition::AllClients, nullptr, nullptr, + [this](float DeltaTime) { + int NumAlwaysRelevantActors = GetNumberOfActorsOfType(GetWorld()); + int NumAlwaysServerOnlyRelevantActors = GetNumberOfActorsOfType(GetWorld()); + int NumServers = GetNumberOfServerWorkers(); + + RequireEqual_Int(NumAlwaysRelevantActors, NumServers, TEXT("Client see expected number of always relevant actors")); + RequireEqual_Int(NumAlwaysServerOnlyRelevantActors, 0, TEXT("Client see no always relevant server-only actors")); + FinishStep(); // This will only actually finish if requires are satisfied + }, + StepTimeLimit); + } +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/RelevancyTest/RelevancyTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/RelevancyTest/RelevancyTest.h new file mode 100644 index 0000000000..3f5a38668e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/RelevancyTest/RelevancyTest.h @@ -0,0 +1,24 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialFunctionalTest.h" +#include "RelevancyTest.generated.h" + +class AAlwaysRelevantTestActor; +class AAlwaysRelevantServerOnlyTestActor; + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ARelevancyTest : public ASpatialFunctionalTest +{ + GENERATED_BODY() + +public: + ARelevancyTest(); + + virtual void PrepareTest() override; + + AAlwaysRelevantTestActor* AlwaysRelevantActor; + AAlwaysRelevantServerOnlyTestActor* AlwaysRelevantServerOnlyActor; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTest.cpp new file mode 100644 index 0000000000..7c452f0ef6 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTest.cpp @@ -0,0 +1,428 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialAuthorityTest.h" +#include "Engine/World.h" +#include "Net/UnrealNetwork.h" +#include "SpatialAuthorityTestActor.h" +#include "SpatialAuthorityTestActorComponent.h" +#include "SpatialAuthorityTestGameMode.h" +#include "SpatialAuthorityTestGameState.h" +#include "SpatialAuthorityTestReplicatedActor.h" +#include "SpatialFunctionalTestFlowController.h" +#include "SpatialGDK/Public/EngineClasses/SpatialNetDriver.h" + +/** This Test is meant to check that HasAuthority() rules are respected on different occasions. We check + * in BeginPlay and Tick, and in the following use cases: + * - replicated level actor + * - non-replicated level actor + * - dynamic replicated actor (one time with spatial authority and another without) + * - dynamic non-replicated actor + * - GameMode (which only exists on Servers) + * - GameState + * Keep in mind that we're assuming a 1x2 Grid Load-Balancing Strategy, otherwise the ownership of + * these actors may be something completely different (specially important for actors placed in the Level). + * You have some flexibility to change the Server1/2Position properties to test in different Load-Balancing Strategies. + */ +ASpatialAuthorityTest::ASpatialAuthorityTest() +{ + Author = "Nuno Afonso"; + Description = TEXT("Test HasAuthority under multi-worker setups. It also ensures it works in Native"); + + Server1Position = FVector(-250.0f, -250.0f, 0.0f); + Server2Position = FVector(-250.0f, 250.0f, 0.0f); +} + +void ASpatialAuthorityTest::PrepareTest() +{ + Super::PrepareTest(); + + ResetTimer(); + + if (HasAuthority()) + { + NumHadAuthorityOverGameMode = 0; + NumHadAuthorityOverGameState = 0; + } + + // Replicated Level Actor. Server 1 should have Authority, again assuming that the Level is setup accordingly. + { + AddStep( + TEXT("Replicated Level Actor - Server 1 Has Authority"), FWorkerDefinition::AllWorkers, nullptr, nullptr, + [this](float DeltaTime) { + Timer -= DeltaTime; + if (Timer <= 0) + { + const FWorkerDefinition& LocalWorkerDefinition = GetLocalFlowController()->WorkerDefinition; + if (LocalWorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server && LocalWorkerDefinition.Id == 1) + { + if (VerifyTestActor(LevelReplicatedActor, 1, 1, 1, 0)) + { + FinishStep(); + } + } + else + { + if (VerifyTestActor(LevelReplicatedActor, 0, 0, 0, 0)) + { + FinishStep(); + } + } + } + }, + 5.0f); + } + + // Non-replicated Level Actor. Each Server should have Authority over their instance, Clients don't. + { + AddStep( + TEXT("Non-replicated Level Actor - Each Server has Authority, Client doesn't know"), FWorkerDefinition::AllWorkers, nullptr, + nullptr, + [this](float DeltaTime) { + // Since this actor already was in level and we wait for timer in the previous step, we don't need to wait + // in this one again. + const FWorkerDefinition& LocalWorkerDefinition = GetLocalFlowController()->WorkerDefinition; + if (LocalWorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server) + { + // Note: Non-replicated actors never get OnAuthorityGained() called. + if (VerifyTestActor(LevelActor, LocalWorkerDefinition.Id, LocalWorkerDefinition.Id, 0, 0)) + { + FinishStep(); + } + } + else + { + if (VerifyTestActor(LevelActor, 0, 0, 0, 0)) + { + FinishStep(); // Clients don't have authority over non-replicated Level Actors. + } + } + }, + 5.0f); + } + + // Replicated Dynamic Actor Spawned On Same Server. Server 1 should have Authority. + { + AddStep(TEXT("Replicated Dynamic Actor Spawned On Same Server - Spawn"), FWorkerDefinition::Server(1), nullptr, [this]() { + ASpatialAuthorityTestReplicatedActor* Actor = + GetWorld()->SpawnActor(Server1Position, FRotator::ZeroRotator); + CrossServerSetDynamicReplicatedActor(Actor); + FinishStep(); + }); + + AddStep( + TEXT("Replicated Dynamic Actor Spawned On Same Server - Verify Server 1 Has Authority"), FWorkerDefinition::AllWorkers, nullptr, + nullptr, + [this](float DeltaTime) { + Timer -= DeltaTime; + if (Timer <= 0) + { + const FWorkerDefinition& LocalWorkerDefinition = GetLocalFlowController()->WorkerDefinition; + if (LocalWorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server && LocalWorkerDefinition.Id == 1) + { + if (VerifyTestActor(DynamicReplicatedActor, 1, 1, 1, 0)) + { + FinishStep(); + } + } + else + { + if (VerifyTestActor(DynamicReplicatedActor, 0, 0, 0, 0)) + { + FinishStep(); + } + } + } + }, + 5.0f); + + AddStep(TEXT("Replicated Dynamic Actor Spawned On Same Server - Destroy"), FWorkerDefinition::Server(1), nullptr, [this]() { + DynamicReplicatedActor->Destroy(); + CrossServerSetDynamicReplicatedActor(nullptr); + FinishStep(); + }); + } + + // Replicated Dynamic Actor Spawned On Different Server. Server 1 should have Authority on BeginPlay, Server 2 on Tick + { + AddStep(TEXT("Replicated Dynamic Actor Spawned On Different Server - Spawn"), FWorkerDefinition::Server(1), nullptr, [this]() { + ASpatialAuthorityTestReplicatedActor* Actor = + GetWorld()->SpawnActor(Server2Position, FRotator::ZeroRotator); + CrossServerSetDynamicReplicatedActor(Actor); + FinishStep(); + }); + + AddStep( + TEXT("Replicated Dynamic Actor Spawned On Different Server - Verify Server 1 Has Authority on BeginPlay and Server 2 on Tick"), + FWorkerDefinition::AllWorkers, nullptr, nullptr, + [this](float DeltaTime) { + Timer -= DeltaTime; + if (Timer <= 0) + { + const FWorkerDefinition& LocalWorkerDefinition = GetLocalFlowController()->WorkerDefinition; + if (LocalWorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server) + { + // Allow it to continue working in Native / Single worker setups. + if (GetNumberOfServerWorkers() > 1) + { + if (LocalWorkerDefinition.Id == 1) + { + // Note: An Actor always ticks on the spawning Worker before migrating. + if (VerifyTestActor(DynamicReplicatedActor, 1, 1, 1, 1)) + { + FinishStep(); + } + } + else if (LocalWorkerDefinition.Id == 2) + { + if (VerifyTestActor(DynamicReplicatedActor, 0, 2, 1, 0) + && DynamicReplicatedActor->AuthorityComponent->ReplicatedAuthWorkerIdOnBeginPlay == 1) + { + FinishStep(); + } + } + else + { + if (VerifyTestActor(DynamicReplicatedActor, 0, 0, 0, 0)) + { + FinishStep(); + } + } + } + else // Support for Native / Single Worker. + { + if (VerifyTestActor(DynamicReplicatedActor, 1, 1, 1, 0)) + { + FinishStep(); + } + } + } + else // Clients. + { + if (VerifyTestActor(DynamicReplicatedActor, 0, 0, 0, 0)) + { + FinishStep(); + } + } + } + }, + 5.0f); + + // Now it's Server 2 destroying, since it has Authority over it. + AddStep(TEXT("Replicated Dynamic Actor Spawned On Different Server - Destroy"), FWorkerDefinition::Server(2), nullptr, [this]() { + DynamicReplicatedActor->Destroy(); + CrossServerSetDynamicReplicatedActor(nullptr); + FinishStep(); + }); + } + + // Non-replicated Dynamic Actor. Server 1 should have Authority. + { + AddStep(TEXT("Non-replicated Dynamic Actor - Spawn"), FWorkerDefinition::Server(1), nullptr, [this]() { + // Spawning directly on Server 2, but since it's non-replicated it shouldn't migrate to Server 2. + DynamicNonReplicatedActor = GetWorld()->SpawnActor(Server2Position, FRotator::ZeroRotator); + FinishStep(); + }); + + AddStep( + TEXT("Non-replicated Dynamic Actor - Verify Authority on Server 1"), FWorkerDefinition::Server(1), nullptr, nullptr, + [this](float DeltaTime) { + // Not replicated so OnAuthorityGained() is not called. + if (VerifyTestActor(DynamicNonReplicatedActor, 1, 1, 0, 0)) + { + FinishStep(); + } + }, + 5.0f); + + AddStep(TEXT("Non-replicated Dynamic Actor - Verify Dynamic Actor doesn't exist on others"), FWorkerDefinition::AllWorkers, nullptr, + nullptr, [this](float DeltaTime) { + const FWorkerDefinition& LocalWorkerDefinition = GetLocalFlowController()->WorkerDefinition; + if (LocalWorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server && LocalWorkerDefinition.Id == 1) + { + FinishStep(); + } + else + { + Timer -= DeltaTime; + if (Timer <= 0) + { + int NumNonReplicatedActorsExpected = 1; // The one that is in the Map itself + int NumNonReplicatedActorsInLevel = 0; + for (TActorIterator It(GetWorld()); It; ++It) + { + if (!It->GetIsReplicated()) + { + NumNonReplicatedActorsInLevel += 1; + } + } + + if (NumNonReplicatedActorsInLevel == NumNonReplicatedActorsExpected) + { + FinishStep(); + } + else + { + FinishTest(EFunctionalTestResult::Failed, + FString::Printf(TEXT("Was expecting only %d non replicated Actors, but found %d"), + NumNonReplicatedActorsExpected, NumNonReplicatedActorsInLevel)); + } + } + } + }); + + // Destroy to be able to re-run. + AddStep(TEXT("Non-replicated Dynamic Actor - Destroy"), FWorkerDefinition::Server(1), nullptr, [this]() { + DynamicNonReplicatedActor->Destroy(); + DynamicNonReplicatedActor = nullptr; + FinishStep(); + }); + } + + // GameMode. + { + AddStep( + TEXT("GameMode - Determine Authority by every Server"), FWorkerDefinition::AllServers, nullptr, nullptr, + [this](float DeltaTime) { + // This is running very far from the start, no need to wait more time. + ASpatialAuthorityTestGameMode* GameMode = GetWorld()->GetAuthGameMode(); + if (GameMode == nullptr) + { + FinishTest(EFunctionalTestResult::Failed, TEXT("This test requires ASpatialAuthorityTestGameMode")); + return; + } + + bool bIsStateValid; + + USpatialAuthorityTestActorComponent* AuthorityComponent = GameMode->AuthorityComponent; + // Either it's bigger than 0 and all match (in the Authoritative Server), or all equal to zero (except for replicated). + if (AuthorityComponent->AuthWorkerIdOnBeginPlay > 0) + { + bool bIsUsingSpatial = Cast(GetNetDriver()) != nullptr; + + // Currently the behaviour is that if you're using native, OnAuthorityGained is never called. + int NumExpectedAuthorityGains = bIsUsingSpatial ? 1 : 0; + + bIsStateValid = AuthorityComponent->AuthWorkerIdOnBeginPlay == AuthorityComponent->ReplicatedAuthWorkerIdOnBeginPlay + && AuthorityComponent->AuthWorkerIdOnBeginPlay == AuthorityComponent->AuthWorkerIdOnTick + && AuthorityComponent->NumAuthorityGains == NumExpectedAuthorityGains + && AuthorityComponent->NumAuthorityLosses == 0; + } + else + { + bIsStateValid = AuthorityComponent->ReplicatedAuthWorkerIdOnBeginPlay != 0 + && AuthorityComponent->AuthWorkerIdOnBeginPlay == 0 && AuthorityComponent->AuthWorkerIdOnTick == 0 + && AuthorityComponent->NumAuthorityGains == 0 && AuthorityComponent->NumAuthorityLosses == 0; + } + + if (bIsStateValid) + { + if (AuthorityComponent->AuthWorkerIdOnBeginPlay) + { + CrossServerNotifyHadAuthorityOverGameMode(); + } + FinishStep(); + } + }, + 50.0f); + } + + // GameState. + { + AddStep( + TEXT("GameState - Determine Authority by all Workers"), FWorkerDefinition::AllWorkers, nullptr, nullptr, + [this](float DeltaTime) { + // Again, no need to wait for more time. + ASpatialAuthorityTestGameState* GameState = GetWorld()->GetGameState(); + if (GameState == nullptr) // GameMode already checked on previous step. + { + FinishTest(EFunctionalTestResult::Failed, TEXT("This test requires ASpatialAuthorityTestGameState")); + return; + } + + bool bIsStateValid; + + USpatialAuthorityTestActorComponent* AuthorityComponent = GameState->AuthorityComponent; + // Either it's bigger than 0 and all match the GameMode Authority (in the Authoritative Server), + // or all equal to zero (except for replicated). + if (AuthorityComponent->AuthWorkerIdOnBeginPlay > 0) + { + ASpatialAuthorityTestGameMode* GameMode = GetWorld()->GetAuthGameMode(); + int GameModeAuthority = GameMode->AuthorityComponent->ReplicatedAuthWorkerIdOnBeginPlay; + bIsStateValid = AuthorityComponent->AuthWorkerIdOnBeginPlay == GameModeAuthority + && AuthorityComponent->ReplicatedAuthWorkerIdOnBeginPlay == GameModeAuthority + && AuthorityComponent->AuthWorkerIdOnTick == GameModeAuthority + && AuthorityComponent->NumAuthorityGains == 1 && AuthorityComponent->NumAuthorityLosses == 0; + } + else + { + bIsStateValid = AuthorityComponent->ReplicatedAuthWorkerIdOnBeginPlay != 0 + && AuthorityComponent->AuthWorkerIdOnBeginPlay == 0 && AuthorityComponent->AuthWorkerIdOnTick == 0 + && AuthorityComponent->NumAuthorityGains == 0 && AuthorityComponent->NumAuthorityLosses == 0; + } + + if (bIsStateValid) + { + if (AuthorityComponent->AuthWorkerIdOnBeginPlay > 0) + { + CrossServerNotifyHadAuthorityOverGameState(); + } + FinishStep(); + } + }, + 5.0f); + } + + // Verify GameMode/State Unique Authority + { + AddStep( + TEXT("Verify GameMode/State Unique Authority"), FWorkerDefinition::Server(1), nullptr, nullptr, + [this](float DeltaTime) { + if (NumHadAuthorityOverGameMode == 1 && NumHadAuthorityOverGameState == 1) + { + // Reset in case we run the Test multiple times in a row. + NumHadAuthorityOverGameMode = 0; + NumHadAuthorityOverGameState = 0; + FinishStep(); + } + }, + 5.0f); + } +} + +void ASpatialAuthorityTest::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ASpatialAuthorityTest, DynamicReplicatedActor); + DOREPLIFETIME(ASpatialAuthorityTest, NumHadAuthorityOverGameMode); + DOREPLIFETIME(ASpatialAuthorityTest, NumHadAuthorityOverGameState); +} + +void ASpatialAuthorityTest::CrossServerSetDynamicReplicatedActor_Implementation(ASpatialAuthorityTestReplicatedActor* Actor) +{ + DynamicReplicatedActor = Actor; +} + +void ASpatialAuthorityTest::CrossServerNotifyHadAuthorityOverGameMode_Implementation() +{ + NumHadAuthorityOverGameMode += 1; +} + +void ASpatialAuthorityTest::CrossServerNotifyHadAuthorityOverGameState_Implementation() +{ + NumHadAuthorityOverGameState += 1; +} + +bool ASpatialAuthorityTest::VerifyTestActor(ASpatialAuthorityTestActor* Actor, int AuthorityOnBeginPlay, int AuthorityOnTick, + int NumAuthorityGains, int NumAuthorityLosses) +{ + if (!IsValid(Actor) || !Actor->HasActorBegunPlay()) + { + return false; + } + + return Actor->AuthorityComponent->AuthWorkerIdOnBeginPlay == AuthorityOnBeginPlay + && Actor->AuthorityComponent->AuthWorkerIdOnTick == AuthorityOnTick + && Actor->AuthorityComponent->NumAuthorityGains == NumAuthorityGains + && Actor->AuthorityComponent->NumAuthorityLosses == NumAuthorityLosses; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTest.h new file mode 100644 index 0000000000..2695677b78 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTest.h @@ -0,0 +1,73 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialFunctionalTest.h" +#include "SpatialAuthorityTest.generated.h" + +class ASpatialAuthorityTestActor; +class ASpatialAuthorityTestReplicatedActor; + +/** Check SpatialAuthorityTest.cpp for Test explanation. */ +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialAuthorityTest : public ASpatialFunctionalTest +{ + GENERATED_BODY() + +public: + ASpatialAuthorityTest(); + + virtual void PrepareTest() override; + + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + virtual void FinishStep() override + { + ResetTimer(); + Super::FinishStep(); + }; + + void ResetTimer() { Timer = 0.5; }; + + bool VerifyTestActor(ASpatialAuthorityTestActor* Actor, int AuthorityOnBeginPlay, int AuthorityOnTick, int NumAuthorityGains, + int NumAuthorityLosses); + + UFUNCTION(CrossServer, Reliable) + void CrossServerSetDynamicReplicatedActor(ASpatialAuthorityTestReplicatedActor* Actor); + + UFUNCTION(CrossServer, Reliable) + void CrossServerNotifyHadAuthorityOverGameMode(); + + UFUNCTION(CrossServer, Reliable) + void CrossServerNotifyHadAuthorityOverGameState(); + + UPROPERTY(EditAnywhere, Category = "Default") + ASpatialAuthorityTestActor* LevelActor; + + UPROPERTY(EditAnywhere, Category = "Default") + ASpatialAuthorityTestReplicatedActor* LevelReplicatedActor; + + // This needs to be a position that belongs to Server 1. + UPROPERTY(EditAnywhere, Category = "Default") + FVector Server1Position; + + // This needs to be a position that belongs to Server 2. + UPROPERTY(EditAnywhere, Category = "Default") + FVector Server2Position; + + UPROPERTY(Replicated) + ASpatialAuthorityTestReplicatedActor* DynamicReplicatedActor; + + UPROPERTY() + ASpatialAuthorityTestActor* DynamicNonReplicatedActor; + + UPROPERTY(Replicated) + int NumHadAuthorityOverGameMode; + + UPROPERTY(Replicated) + int NumHadAuthorityOverGameState; + + // Local timer used for some active waits. + float Timer; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestActor.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestActor.cpp new file mode 100644 index 0000000000..7682e1ff4d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestActor.cpp @@ -0,0 +1,20 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialAuthorityTestActor.h" + +#include "EngineClasses/SpatialNetDriver.h" +#include "LoadBalancing/AbstractLBStrategy.h" +#include "Net/UnrealNetwork.h" +#include "SpatialAuthorityTestActorComponent.h" +#include "SpatialFunctionalTest.h" +#include "SpatialFunctionalTestFlowController.h" + +ASpatialAuthorityTestActor::ASpatialAuthorityTestActor() +{ + PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.TickInterval = 0.0f; + + AuthorityComponent = CreateDefaultSubobject(FName("AuthorityComponent")); + + RootComponent = AuthorityComponent; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestActor.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestActor.h new file mode 100644 index 0000000000..2668410990 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestActor.h @@ -0,0 +1,21 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "SpatialAuthorityTestActor.generated.h" + +class USpatialAuthorityTestActorComponent; + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialAuthorityTestActor : public AActor +{ + GENERATED_BODY() + +public: + ASpatialAuthorityTestActor(); + + UPROPERTY() + USpatialAuthorityTestActorComponent* AuthorityComponent; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestActorComponent.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestActorComponent.cpp new file mode 100644 index 0000000000..16c38fe2cb --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestActorComponent.cpp @@ -0,0 +1,68 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialAuthorityTestActorComponent.h" +#include "EngineClasses/SpatialNetDriver.h" +#include "LoadBalancing/AbstractLBStrategy.h" +#include "Net/UnrealNetwork.h" +#include "SpatialFunctionalTest.h" +#include "SpatialFunctionalTestFlowController.h" + +USpatialAuthorityTestActorComponent::USpatialAuthorityTestActorComponent() +{ + PrimaryComponentTick.bCanEverTick = true; + PrimaryComponentTick.TickInterval = 0.0f; + + SetIsReplicatedByDefault(true); +} + +void USpatialAuthorityTestActorComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(USpatialAuthorityTestActorComponent, ReplicatedAuthWorkerIdOnBeginPlay); +} + +void USpatialAuthorityTestActorComponent::OnAuthorityGained() +{ + NumAuthorityGains += 1; +} + +void USpatialAuthorityTestActorComponent::OnAuthorityLost() +{ + NumAuthorityLosses += 1; +} + +void USpatialAuthorityTestActorComponent::BeginPlay() +{ + Super::BeginPlay(); + + AActor* Owner = GetOwner(); + + if (Owner->HasAuthority() && AuthWorkerIdOnBeginPlay == 0) + { + USpatialNetDriver* SpatialNetDriver = Cast(Owner->GetNetDriver()); + + AuthWorkerIdOnBeginPlay = SpatialNetDriver != nullptr && SpatialNetDriver->LoadBalanceStrategy != nullptr + ? SpatialNetDriver->LoadBalanceStrategy->GetLocalVirtualWorkerId() + : 1; + + ReplicatedAuthWorkerIdOnBeginPlay = AuthWorkerIdOnBeginPlay; + } +} + +void USpatialAuthorityTestActorComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, + FActorComponentTickFunction* ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + AActor* Owner = GetOwner(); + + // Need to check HasActorBegunPlay because GameState will tick before BeginPlay. + if (Owner->HasActorBegunPlay() && Owner->HasAuthority() && AuthWorkerIdOnTick == 0) + { + USpatialNetDriver* SpatialNetDriver = Cast(Owner->GetNetDriver()); + AuthWorkerIdOnTick = SpatialNetDriver != nullptr && SpatialNetDriver->LoadBalanceStrategy != nullptr + ? SpatialNetDriver->LoadBalanceStrategy->GetLocalVirtualWorkerId() + : 1; + } +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestActorComponent.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestActorComponent.h new file mode 100644 index 0000000000..edf7b54356 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestActorComponent.h @@ -0,0 +1,41 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Components/SceneComponent.h" +#include "CoreMinimal.h" +#include "SpatialAuthorityTestActorComponent.generated.h" + +class ASpatialFunctionalTest; +class ASpatialFunctionalTestFlowController; + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API USpatialAuthorityTestActorComponent : public USceneComponent +{ + GENERATED_BODY() + +public: + USpatialAuthorityTestActorComponent(); + + void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + virtual void OnAuthorityGained() override; + + virtual void OnAuthorityLost() override; + + virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction); + + UPROPERTY(Replicated) + int ReplicatedAuthWorkerIdOnBeginPlay = 0; + + int AuthWorkerIdOnBeginPlay = 0; + + int AuthWorkerIdOnTick = 0; + + int NumAuthorityGains = 0; + + int NumAuthorityLosses = 0; + +protected: + virtual void BeginPlay() override; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestGameMode.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestGameMode.cpp new file mode 100644 index 0000000000..dc79cdc3f1 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestGameMode.cpp @@ -0,0 +1,18 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialAuthorityTestGameMode.h" +#include "SpatialAuthorityTestActorComponent.h" +#include "SpatialAuthorityTestGameState.h" + +ASpatialAuthorityTestGameMode::ASpatialAuthorityTestGameMode() + : Super() +{ + PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.TickInterval = 0.0f; // Ensure fast Tick + + GameStateClass = ASpatialAuthorityTestGameState::StaticClass(); + + AuthorityComponent = CreateDefaultSubobject(FName("AuthorityComponent")); + + RootComponent = AuthorityComponent; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestGameMode.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestGameMode.h new file mode 100644 index 0000000000..ab15f80c97 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestGameMode.h @@ -0,0 +1,21 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/GameModeBase.h" +#include "SpatialAuthorityTestGameMode.generated.h" + +class USpatialAuthorityTestActorComponent; + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialAuthorityTestGameMode : public AGameModeBase +{ + GENERATED_BODY() + +public: + ASpatialAuthorityTestGameMode(); + + UPROPERTY() + USpatialAuthorityTestActorComponent* AuthorityComponent; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestGameState.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestGameState.cpp new file mode 100644 index 0000000000..c6132417ac --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestGameState.cpp @@ -0,0 +1,15 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialAuthorityTestGameState.h" +#include "SpatialAuthorityTestActorComponent.h" + +ASpatialAuthorityTestGameState::ASpatialAuthorityTestGameState() + : Super() +{ + PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.TickInterval = 0.0f; // Ensure fast Tick + + AuthorityComponent = CreateDefaultSubobject(FName("AuthorityComponent")); + + RootComponent = AuthorityComponent; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestGameState.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestGameState.h new file mode 100644 index 0000000000..dc6418b799 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestGameState.h @@ -0,0 +1,21 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/GameStateBase.h" +#include "SpatialAuthorityTestGameState.generated.h" + +class USpatialAuthorityTestActorComponent; + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialAuthorityTestGameState : public AGameStateBase +{ + GENERATED_BODY() + +public: + ASpatialAuthorityTestGameState(); + + UPROPERTY() + USpatialAuthorityTestActorComponent* AuthorityComponent; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestReplicatedActor.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestReplicatedActor.cpp new file mode 100644 index 0000000000..b175e7d814 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestReplicatedActor.cpp @@ -0,0 +1,9 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialAuthorityTestReplicatedActor.h" + +ASpatialAuthorityTestReplicatedActor::ASpatialAuthorityTestReplicatedActor() + : Super() +{ + bReplicates = true; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestReplicatedActor.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestReplicatedActor.h new file mode 100644 index 0000000000..26953c00c7 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialAuthorityTest/SpatialAuthorityTestReplicatedActor.h @@ -0,0 +1,16 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialAuthorityTestActor.h" +#include "SpatialAuthorityTestReplicatedActor.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialAuthorityTestReplicatedActor : public ASpatialAuthorityTestActor +{ + GENERATED_BODY() + +public: + ASpatialAuthorityTestReplicatedActor(); +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotDummyTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotDummyTest.cpp new file mode 100644 index 0000000000..fd8b026a15 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotDummyTest.cpp @@ -0,0 +1,28 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialSnapshotDummyTest.h" + +/** + * This test is made to always just pass. The way we have to test snapshots, you need to have an additional + * map with at least some test because of the way the whole automation works. + * Please check ASpatialSnapshotTest for a full explanation. + */ + +ASpatialSnapshotDummyTest::ASpatialSnapshotDummyTest() + : Super() +{ + Author = "Nuno"; + Description = TEXT("Dummy Test that just passes"); + SetNumRequiredClients(1); +} + +void ASpatialSnapshotDummyTest::PrepareTest() +{ + Super::PrepareTest(); + + { + AddStep(TEXT("Always Pass"), FWorkerDefinition::Server(1), nullptr, [this]() { + FinishStep(); + }); + } +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotDummyTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotDummyTest.h new file mode 100644 index 0000000000..6a000513b9 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotDummyTest.h @@ -0,0 +1,16 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialFunctionalTest.h" +#include "SpatialSnapshotDummyTest.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialSnapshotDummyTest : public ASpatialFunctionalTest +{ + GENERATED_BODY() +public: + ASpatialSnapshotDummyTest(); + + virtual void PrepareTest() override; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotTest.cpp new file mode 100644 index 0000000000..db497b829e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotTest.cpp @@ -0,0 +1,194 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialSnapshotTest.h" +#include "GameFramework/GameStateBase.h" +#include "SpatialSnapshotTestActor.h" +#include "SpatialSnapshotTestGameMode.h" + +/** + * This test handles SpatialOS Snapshots. + * + * Here's what you should expect: + * - ASpatialSnapshotTest is in a map with testing mode ForceSpatial + * - ASpatialSnapshotTest will be expected to run 2 times, the first setting up the data and taking the Snapshot + * and the second time checking that the data from the Snapshot is properly loaded and clearing the Snapshot. + * - ASpatialSnapshotDummyTest is in another map with testing mode ForceNative. + * - ASpatialSnapshotDummyTest will be expected to run 2 times, each time just passing. + * + * Caveats that you should be aware to understand the way this needs to be setup: + * - (1) Currently passing a Snapshot to a deployment is only allowed at launch time. + * - (2) UE Automation Manager only loads maps / launches SpatialOS deployments whenever it wants to run tests in different maps. + * - (3) A Snapshot taken from a specific map is only guaranteed to be valid for that same map. + * - (4) UE Automation Manager only loads maps if they have a test inside. + * + * Because of (1) we need to first launch the test with a clean Snapshot where we setup the data for taking the Snapshot, + * and the second launch to be able to have a deployment with the Snapshot Taken to verify the data was properly loaded. Because of + * (2) to have maps / deployments reload we need to have 2 maps. However, then (3) forces us to have the second map be a dummy + * map which runs in Native to prevent errors of launching with Snapshots from a different map, and in order to be picked up + * by the Automation Manager (4) it needs to have a dummy test that always passes. + * + * So given this setup, Automation Manager will: + * - Loads and starts map A with SpatialOS and runs ASpatialSnapshotTest which setups up our data and takes snapshot. + * - Stops map A and shuts down SpatialOS deployment. + * - Loads and starts map B without SpatialOS and runs ASpatialSnapshotDummyTest which just passes. + * - Stops map B. + * + * This means that we still are missing a crucial part of the test, we still didn't verify that the Snapshot loading works. + * Remember how (3) requires us to load a Snapshot with the map it was created, so we make the Automation Manager run + * these tests an even amount of times (more than 2 if you want to stress test it). + * + * The second time will be exact the same way as above, the 2 differences are that (a) map A will be launched with the + * Snapshot taken in the first run, and (b) ASpatialSnapshotTest will know that it is running from a custom Snapshot + * and will execute different steps. + * + * Keep in mind that we do support running multiple snapshot testing maps, as we save snapshots tied to the map name. + * This means you can bulk run multiple different snapshot test maps, so you can run more than one of these maps + * and not have that dummy map that was built purely for GDK testing. + */ + +ASpatialSnapshotTest::ASpatialSnapshotTest() + : Super() +{ + Author = "Nuno"; + Description = TEXT( + "Test SpatialOS Snapshots. This test is expected to run twice, the first time sets up the data and takes a Snapshot and the second " + "time loads from it and verifies the data is set."); + SetNumRequiredClients(1); +} + +void ASpatialSnapshotTest::PrepareTest() +{ + Super::PrepareTest(); + + // First we need to know if we're launching from the default Snapshot or from a taken Snapshot. + bool bIsRunningFirstTime = !WasLoadedFromTakenSnapshot(); + + FString VerifyActorDataStepName = TEXT("Verify Actor Data Properly Set"); + + FSpatialFunctionalTestStepDefinition VerifyActorDataStepDef = FSpatialFunctionalTestStepDefinition(true); + VerifyActorDataStepDef.TimeLimit = 5.0f; + VerifyActorDataStepDef.NativeTickEvent.BindLambda([this](float DeltaTime) { + ASpatialSnapshotTestActor* Actor = nullptr; + int NumActors = 0; + for (TActorIterator It(GetWorld()); It; ++It) + { + if (NumActors == 1) + { + FinishTest(EFunctionalTestResult::Failed, TEXT("There's more than one ASpatialSnapshotTestActor")); + return; + } + Actor = *It; + ++NumActors; + } + + if (IsValid(Actor)) + { + RequireTrue(Actor->VerifyBool(), TEXT("Bool Replication")); + RequireTrue(Actor->VerifyInt32(), TEXT("Int32 Replication")); + RequireTrue(Actor->VerifyInt64(), TEXT("Int64 Replication")); + RequireTrue(Actor->VerifyFloat(), TEXT("Float Replication")); + RequireTrue(Actor->VerifyString(), TEXT("String Replication")); + RequireTrue(Actor->VerifyName(), TEXT("Name Replication")); + RequireTrue(Actor->VerifyIntArray(), TEXT("Int Array Replication")); + FinishStep(); + } + }); + + FString VerifyGameModeDataStepName = TEXT("Verify GameMode Data Properly Set"); + + FSpatialFunctionalTestStepDefinition VerifyGameModeDataStepDef = FSpatialFunctionalTestStepDefinition(true); + VerifyGameModeDataStepDef.TimeLimit = 5.0f; + VerifyGameModeDataStepDef.NativeTickEvent.BindLambda([this](float DeltaTime) { + if (GetNetDriver()->IsServer()) + { + ASpatialSnapshotTestGameMode* GameMode = nullptr; + int NumGameModes = 0; + for (TActorIterator It(GetWorld()); It; ++It) + { + if (NumGameModes == 1) + { + FinishTest(EFunctionalTestResult::Failed, TEXT("There's more than one ASpatialSnapshotTestGameMode")); + return; + } + GameMode = *It; + ++NumGameModes; + } + + if (IsValid(GameMode)) + { + RequireTrue(GameMode->VerifyBool(), TEXT("Bool Replication")); + RequireTrue(GameMode->VerifyInt32(), TEXT("Int32 Replication")); + RequireTrue(GameMode->VerifyInt64(), TEXT("Int64 Replication")); + RequireTrue(GameMode->VerifyFloat(), TEXT("Float Replication")); + RequireTrue(GameMode->VerifyString(), TEXT("String Replication")); + RequireTrue(GameMode->VerifyName(), TEXT("Name Replication")); + RequireTrue(GameMode->VerifyIntArray(), TEXT("Int Array Replication")); + FinishStep(); + } + } + else + { + int NumGameStates = 0; + for (TActorIterator It(GetWorld()); It; ++It) + { + if (NumGameStates == 1) + { + FinishTest(EFunctionalTestResult::Failed, TEXT("There's more than one GameState")); + return; + } + ++NumGameStates; + } + + if (NumGameStates > 0) + { + FinishStep(); + } + } + }); + + if (bIsRunningFirstTime) + { + // The first run we want to setup the data, verify it, and take snapshot. + AddStep(TEXT("First Run - Spawn Replicated Actor and Set Properties"), FWorkerDefinition::Server(1), nullptr, [this]() { + ASpatialSnapshotTestActor* Actor = GetWorld()->SpawnActor(); + + Actor->CrossServerSetProperties(); + + FinishStep(); + }); + + VerifyActorDataStepDef.StepName = FString::Printf(TEXT("%s - %s"), TEXT("First Run"), *VerifyActorDataStepName); + AddStepFromDefinition(VerifyActorDataStepDef, FWorkerDefinition::AllWorkers); + + AddStep(TEXT("First Run - GameMode Set Properties"), FWorkerDefinition::Server(1), nullptr, [this]() { + ASpatialSnapshotTestGameMode* GameMode = GetWorld()->GetAuthGameMode(); + + if (GameMode == nullptr) + { + FinishTest(EFunctionalTestResult::Failed, TEXT("This test requires ASpatialSnapshotTestGameMode to be set as Game Mode")); + return; + } + GameMode->CrossServerSetProperties(); + FinishStep(); + }); + + VerifyGameModeDataStepDef.StepName = FString::Printf(TEXT("%s - %s"), TEXT("First Run"), *VerifyGameModeDataStepName); + AddStepFromDefinition(VerifyGameModeDataStepDef, FWorkerDefinition::AllWorkers); + + // Take snapshot. + AddStepFromDefinition(TakeSnapshotStepDefinition, FWorkerDefinition::Server(1)); + } + else + { + // The second run we want to verify the data loaded from snapshot was correct, clear the snapshot. + + VerifyActorDataStepDef.StepName = FString::Printf(TEXT("%s - %s"), TEXT("Second Run"), *VerifyActorDataStepName); + AddStepFromDefinition(VerifyActorDataStepDef, FWorkerDefinition::AllWorkers); + + VerifyGameModeDataStepDef.StepName = FString::Printf(TEXT("%s - %s"), TEXT("Second Run"), *VerifyGameModeDataStepName); + AddStepFromDefinition(VerifyGameModeDataStepDef, FWorkerDefinition::AllWorkers); + + // Clear snapshot. + AddStepFromDefinition(ClearSnapshotStepDefinition, FWorkerDefinition::Server(1)); + } +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotTest.h new file mode 100644 index 0000000000..aad23a0a8e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotTest.h @@ -0,0 +1,16 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialFunctionalTest.h" +#include "SpatialSnapshotTest.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialSnapshotTest : public ASpatialFunctionalTest +{ + GENERATED_BODY() +public: + ASpatialSnapshotTest(); + + virtual void PrepareTest() override; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotTestActor.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotTestActor.cpp new file mode 100644 index 0000000000..e6137b97ee --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotTestActor.cpp @@ -0,0 +1,115 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialSnapshotTestActor.h" +#include "Net/UnrealNetwork.h" + +namespace +{ +constexpr bool bTargetBoolProperty = true; +constexpr int32 TargetInt32Property = -1050; +constexpr int64 TargetInt64Property = 8000000; +constexpr float TargetFloatProperty = 1000.0f; +const FString TargetStringProperty = TEXT("Some String"); +const FName TargetNameProperty = TEXT("Some Name"); + +TArray GetTargetIntArrayProperty() +{ + TArray Array; + for (int i = 0; i != 10; ++i) + { + Array.Add(i); + } + return Array; +} +} // namespace + +ASpatialSnapshotTestActor::ASpatialSnapshotTestActor() + : Super() +{ + bReplicates = true; + PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.TickInterval = 0.0f; +} + +void ASpatialSnapshotTestActor::BeginPlay() +{ + Super::BeginPlay(); +} + +void ASpatialSnapshotTestActor::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ASpatialSnapshotTestActor, bBoolProperty); + DOREPLIFETIME(ASpatialSnapshotTestActor, Int32Property); + DOREPLIFETIME(ASpatialSnapshotTestActor, Int64Property); + DOREPLIFETIME(ASpatialSnapshotTestActor, FloatProperty); + DOREPLIFETIME(ASpatialSnapshotTestActor, StringProperty); + DOREPLIFETIME(ASpatialSnapshotTestActor, NameProperty); + DOREPLIFETIME(ASpatialSnapshotTestActor, IntArrayProperty); +} + +void ASpatialSnapshotTestActor::CrossServerSetProperties_Implementation() +{ + bBoolProperty = bTargetBoolProperty; + Int32Property = TargetInt32Property; + Int64Property = TargetInt64Property; + FloatProperty = TargetFloatProperty; + StringProperty = TargetStringProperty; + NameProperty = TargetNameProperty; + + IntArrayProperty.Empty(); + + TArray TargetArray = GetTargetIntArrayProperty(); + for (int i : TargetArray) + { + IntArrayProperty.Add(i); + } +} + +bool ASpatialSnapshotTestActor::VerifyBool() +{ + return bBoolProperty == bTargetBoolProperty; +} + +bool ASpatialSnapshotTestActor::VerifyInt32() +{ + return Int32Property == TargetInt32Property; +} + +bool ASpatialSnapshotTestActor::VerifyInt64() +{ + return Int64Property == TargetInt64Property; +} + +bool ASpatialSnapshotTestActor::VerifyFloat() +{ + return FMath::IsNearlyEqual(FloatProperty, TargetFloatProperty); +} + +bool ASpatialSnapshotTestActor::VerifyString() +{ + return StringProperty == TargetStringProperty; +} + +bool ASpatialSnapshotTestActor::VerifyName() +{ + return NameProperty == TargetNameProperty; +} + +bool ASpatialSnapshotTestActor::VerifyIntArray() +{ + TArray TargetIntArray = GetTargetIntArrayProperty(); + if (IntArrayProperty.Num() != TargetIntArray.Num()) + { + return false; + } + for (int i = 0; i != IntArrayProperty.Num(); ++i) + { + if (IntArrayProperty[i] != TargetIntArray[i]) + { + return false; + } + } + return true; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotTestActor.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotTestActor.h new file mode 100644 index 0000000000..76c9ede915 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotTestActor.h @@ -0,0 +1,63 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "GameFramework/Actor.h" +#include "SpatialSnapshotTestActor.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialSnapshotTestActor : public AActor +{ + GENERATED_BODY() +public: + ASpatialSnapshotTestActor(); + + virtual void BeginPlay() override; + + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + UFUNCTION(CrossServer, Reliable) + void CrossServerSetProperties(); + + UFUNCTION() + bool VerifyBool(); + + UFUNCTION() + bool VerifyInt32(); + + UFUNCTION() + bool VerifyInt64(); + + UFUNCTION() + bool VerifyFloat(); + + UFUNCTION() + bool VerifyString(); + + UFUNCTION() + bool VerifyName(); + + UFUNCTION() + bool VerifyIntArray(); + + UPROPERTY(Replicated) + bool bBoolProperty = false; + + UPROPERTY(Replicated) + int32 Int32Property = 0; + + UPROPERTY(Replicated) + int64 Int64Property = 0; + + UPROPERTY(Replicated) + float FloatProperty = 0.0f; + + UPROPERTY(Replicated) + FString StringProperty = ""; + + UPROPERTY(Replicated) + FName NameProperty = ""; + + UPROPERTY(Replicated) + TArray IntArrayProperty; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotTestGameMode.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotTestGameMode.cpp new file mode 100644 index 0000000000..11ea322789 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotTestGameMode.cpp @@ -0,0 +1,107 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialSnapshotTestGameMode.h" +#include "Net/UnrealNetwork.h" + +namespace +{ +constexpr bool bTargetBoolProperty = true; +constexpr int32 TargetInt32Property = -1050; +constexpr int64 TargetInt64Property = 8000000; +constexpr float TargetFloatProperty = 1000.0f; +const FString TargetStringProperty = TEXT("Some String"); +const FName TargetNameProperty = TEXT("Some Name"); + +TArray GetTargetIntArrayProperty() +{ + TArray Array; + for (int i = 0; i != 10; ++i) + { + Array.Add(i); + } + return Array; +} +} // namespace + +ASpatialSnapshotTestGameMode::ASpatialSnapshotTestGameMode() + : Super() +{ +} + +void ASpatialSnapshotTestGameMode::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ASpatialSnapshotTestGameMode, bBoolProperty); + DOREPLIFETIME(ASpatialSnapshotTestGameMode, Int32Property); + DOREPLIFETIME(ASpatialSnapshotTestGameMode, Int64Property); + DOREPLIFETIME(ASpatialSnapshotTestGameMode, FloatProperty); + DOREPLIFETIME(ASpatialSnapshotTestGameMode, StringProperty); + DOREPLIFETIME(ASpatialSnapshotTestGameMode, NameProperty); + DOREPLIFETIME(ASpatialSnapshotTestGameMode, IntArrayProperty); +} + +void ASpatialSnapshotTestGameMode::CrossServerSetProperties_Implementation() +{ + bBoolProperty = bTargetBoolProperty; + Int32Property = TargetInt32Property; + Int64Property = TargetInt64Property; + FloatProperty = TargetFloatProperty; + StringProperty = TargetStringProperty; + NameProperty = TargetNameProperty; + + IntArrayProperty.Empty(); + + TArray TargetArray = GetTargetIntArrayProperty(); + for (int i : TargetArray) + { + IntArrayProperty.Add(i); + } +} + +bool ASpatialSnapshotTestGameMode::VerifyBool() +{ + return bBoolProperty == bTargetBoolProperty; +} + +bool ASpatialSnapshotTestGameMode::VerifyInt32() +{ + return Int32Property == TargetInt32Property; +} + +bool ASpatialSnapshotTestGameMode::VerifyInt64() +{ + return Int64Property == TargetInt64Property; +} + +bool ASpatialSnapshotTestGameMode::VerifyFloat() +{ + return FMath::IsNearlyEqual(FloatProperty, TargetFloatProperty); +} + +bool ASpatialSnapshotTestGameMode::VerifyString() +{ + return StringProperty == TargetStringProperty; +} + +bool ASpatialSnapshotTestGameMode::VerifyName() +{ + return NameProperty == TargetNameProperty; +} + +bool ASpatialSnapshotTestGameMode::VerifyIntArray() +{ + TArray TargetIntArray = GetTargetIntArrayProperty(); + if (IntArrayProperty.Num() != TargetIntArray.Num()) + { + return false; + } + for (int i = 0; i != IntArrayProperty.Num(); ++i) + { + if (IntArrayProperty[i] != TargetIntArray[i]) + { + return false; + } + } + return true; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotTestGameMode.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotTestGameMode.h new file mode 100644 index 0000000000..df92536ca3 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialSnapshotTest/SpatialSnapshotTestGameMode.h @@ -0,0 +1,61 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "GameFramework/GameModeBase.h" +#include "SpatialSnapshotTestGameMode.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialSnapshotTestGameMode : public AGameModeBase +{ + GENERATED_BODY() +public: + ASpatialSnapshotTestGameMode(); + + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + UFUNCTION(CrossServer, Reliable) + void CrossServerSetProperties(); + + UFUNCTION() + bool VerifyBool(); + + UFUNCTION() + bool VerifyInt32(); + + UFUNCTION() + bool VerifyInt64(); + + UFUNCTION() + bool VerifyFloat(); + + UFUNCTION() + bool VerifyString(); + + UFUNCTION() + bool VerifyName(); + + UFUNCTION() + bool VerifyIntArray(); + + UPROPERTY(Replicated) + bool bBoolProperty = false; + + UPROPERTY(Replicated) + int32 Int32Property = 0; + + UPROPERTY(Replicated) + int64 Int64Property = 0; + + UPROPERTY(Replicated) + float FloatProperty = 0.0f; + + UPROPERTY(Replicated) + FString StringProperty = ""; + + UPROPERTY(Replicated) + FName NameProperty = ""; + + UPROPERTY(Replicated) + TArray IntArrayProperty; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMigration/SpatialTestCharacterMigration.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMigration/SpatialTestCharacterMigration.cpp new file mode 100644 index 0000000000..22ab82d51a --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMigration/SpatialTestCharacterMigration.cpp @@ -0,0 +1,141 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialTestCharacterMigration.h" +#include "Components/BoxComponent.h" +#include "Engine/TriggerBox.h" +#include "GameFramework/PlayerController.h" +#include "Kismet/GameplayStatics.h" +#include "SpatialFunctionalTestFlowController.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/TestMovementCharacter.h" + +namespace +{ +float GetTargetDistanceOnLine(const FVector& From, const FVector& Target, const FVector& Location) +{ + FVector Norm = (Target - From); + Norm.Normalize(); + FVector RelativePosition = Location - Target; + return FVector::DotProduct(Norm, RelativePosition); +} +} // namespace + +/** + * This test moves a character backward and forward repeatedly between two workers, adding actors. Based on the SpatialTestCharacterMovement + * test. This test requires the CharacterMovementTestGameMode, trying to run this test on a different game mode will fail. + * + * The test includes two servers and one client worker. The client worker begins with a PlayerController and a TestCharacterMovement + * + */ + +ASpatialTestCharacterMigration::ASpatialTestCharacterMigration() + : Super() +{ + Author = "Victoria"; + Description = TEXT("Test Character Migration"); + TimeLimit = 300; +} + +void ASpatialTestCharacterMigration::PrepareTest() +{ + Super::PrepareTest(); + + // Reset test + FSpatialFunctionalTestStepDefinition ResetStepDefinition(/*bIsNativeDefinition*/ true); + ResetStepDefinition.StepName = TEXT("Reset"); + ResetStepDefinition.TimeLimit = 0.0f; + ResetStepDefinition.NativeStartEvent.BindLambda([this]() { + bCharacterReachedDestination = false; + bCharacterReachedOrigin = false; + FinishStep(); + }); + + // Add actor to controller + FSpatialFunctionalTestStepDefinition AddActorStepDefinition(/*bIsNativeDefinition*/ true); + AddActorStepDefinition.StepName = TEXT("Add actor to player controller"); + AddActorStepDefinition.TimeLimit = 0.0f; + AddActorStepDefinition.NativeStartEvent.BindLambda([this]() { + for (ASpatialFunctionalTestFlowController* FlowController : GetFlowControllers()) + { + AController* PlayerController = Cast(FlowController->GetOwner()); + + FActorSpawnParameters SpawnParams; + SpawnParams.Owner = PlayerController; + AActor* TestActor = GetWorld()->SpawnActor(AActor::StaticClass(), FTransform(), SpawnParams); + TestActor->SetReplicates( + true); // NOTE: this currently causes parent not to migrate after a delay and outputs a warning in the test + RegisterAutoDestroyActor(TestActor); + } + FinishStep(); + }); + + // Move character forward + FSpatialFunctionalTestStepDefinition MoveForwardStepDefinition(/*bIsNativeDefinition*/ true); + MoveForwardStepDefinition.StepName = TEXT("Client1MoveForward"); + MoveForwardStepDefinition.TimeLimit = 0.0f; + MoveForwardStepDefinition.NativeTickEvent.BindLambda([this](float DeltaTime) { + AController* PlayerController = Cast(GetLocalFlowController()->GetOwner()); + ATestMovementCharacter* PlayerCharacter = Cast(PlayerController->GetPawn()); + + PlayerCharacter->AddMovementInput(FVector(1, 0, 0), 10.0f, true); + + bCharacterReachedDestination = + GetTargetDistanceOnLine(Origin, Destination, PlayerCharacter->GetActorLocation()) > -20.0f; // 20cm overlap + + if (bCharacterReachedDestination) + { + AssertTrue(bCharacterReachedDestination, TEXT("Player character has reached the destination on the autonomous proxy.")); + FinishStep(); + } + }); + + // Move character backward + FSpatialFunctionalTestStepDefinition MoveBackwardStepDefinition(/*bIsNativeDefinition*/ true); + MoveBackwardStepDefinition.StepName = TEXT("Client1MoveBackward"); + MoveBackwardStepDefinition.TimeLimit = 0.0f; + MoveBackwardStepDefinition.NativeTickEvent.BindLambda([this](float DeltaTime) { + AController* PlayerController = Cast(GetLocalFlowController()->GetOwner()); + ATestMovementCharacter* PlayerCharacter = Cast(PlayerController->GetPawn()); + + PlayerCharacter->AddMovementInput(FVector(-1, 0, 0), 10.0f, true); + + bCharacterReachedOrigin = GetTargetDistanceOnLine(Destination, Origin, PlayerCharacter->GetActorLocation()) > -20.0f; + ; // 20cm overlap + + if (bCharacterReachedOrigin) + { + AssertTrue(bCharacterReachedOrigin, TEXT("Player character has reached the origin on the autonomous proxy.")); + FinishStep(); + } + }); + + // Universal setup step to create the TriggerBox and to set the helper variable + AddStep(TEXT("UniversalSetupStep"), FWorkerDefinition::AllWorkers, nullptr, [this]() { + bCharacterReachedDestination = false; + bCharacterReachedOrigin = false; + + Destination = FVector(132.0f, 0.0f, 40.0f); + Origin = FVector(-132.0f, 0.0f, 40.0f); + + FinishStep(); + }); + + // Repeatedly move character forwards and backwards over the worker boundary and adding actors every time + for (int i = 0; i < 5; i++) + { + if (i < 1) + { + AddStepFromDefinition(AddActorStepDefinition, FWorkerDefinition::AllServers); + } + + AddStepFromDefinition(MoveForwardStepDefinition, FWorkerDefinition::Client(1)); + + if (i < 1) + { + AddStepFromDefinition(AddActorStepDefinition, FWorkerDefinition::AllServers); + } + + AddStepFromDefinition(MoveBackwardStepDefinition, FWorkerDefinition::Client(1)); + + AddStepFromDefinition(ResetStepDefinition, FWorkerDefinition::AllWorkers); + } +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMigration/SpatialTestCharacterMigration.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMigration/SpatialTestCharacterMigration.h new file mode 100644 index 0000000000..8281e58ef9 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMigration/SpatialTestCharacterMigration.h @@ -0,0 +1,24 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialFunctionalTest.h" +#include "SpatialTestCharacterMigration.generated.h" + +UCLASS() +class ASpatialTestCharacterMigration : public ASpatialFunctionalTest +{ + GENERATED_BODY() + +public: + ASpatialTestCharacterMigration(); + + virtual void PrepareTest() override; + + FVector Origin; + FVector Destination; + + bool bCharacterReachedDestination; + bool bCharacterReachedOrigin; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMovement/CharacterMovementTestGameMode.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMovement/CharacterMovementTestGameMode.cpp index 04e121e6a3..176e78e699 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMovement/CharacterMovementTestGameMode.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMovement/CharacterMovementTestGameMode.cpp @@ -1,8 +1,7 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved - #include "CharacterMovementTestGameMode.h" -#include "TestMovementCharacter.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/TestMovementCharacter.h" ACharacterMovementTestGameMode::ACharacterMovementTestGameMode(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMovement/SpatialTestCharacterMovement.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMovement/SpatialTestCharacterMovement.cpp index 68e9ccfbf1..d064eda48f 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMovement/SpatialTestCharacterMovement.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMovement/SpatialTestCharacterMovement.cpp @@ -1,25 +1,25 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved - #include "SpatialTestCharacterMovement.h" -#include "TestMovementCharacter.h" -#include "SpatialFunctionalTestFlowController.h" -#include "GameFramework/PlayerController.h" -#include "Kismet/GameplayStatics.h" #include "Components/BoxComponent.h" #include "Engine/TriggerBox.h" +#include "GameFramework/PlayerController.h" +#include "Kismet/GameplayStatics.h" +#include "SpatialFunctionalTestFlowController.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/TestMovementCharacter.h" /** - * This test tests if the movement of a character from a starting point to a Destination, performed on a client, is correctly replicated on the server and on all other clients. - * Note: The Destination is a TriggerBox spawned locally on each connected worker, either client or server. - * This test requires the CharacterMovementTestGameMode, trying to run this test on a different game mode will fail. + * This test tests if the movement of a character from a starting point to a Destination, performed on a client, is correctly replicated on + *the server and on all other clients. Note: The Destination is a TriggerBox spawned locally on each connected worker, either client or + *server. This test requires the CharacterMovementTestGameMode, trying to run this test on a different game mode will fail. * * The test includes a single server and two client workers. The client workers begin with a PlayerController and a TestCharacterMovement * * The flow is as follows: * - Setup: * - The server and each client create a TriggerBox locally. - * - The server checks if the clients received a TestCharacterMovement and sets their position to (0.0f, 0.0f, 50.0f) for the first client and (100.0f, 300.0f, 50.0f) for the second. + * - The server checks if the clients received a TestCharacterMovement and sets their position to (0.0f, 0.0f, 50.0f) for the first + *client and (100.0f, 300.0f, 50.0f) for the second. * - The client with ID 1 moves its character as an autonomous proxy towards the Destination. * - Test: * - The owning client asserts that his character has reached the Destination. @@ -46,101 +46,107 @@ void ASpatialTestCharacterMovement::OnOverlapBegin(AActor* OverlappedActor, AAct } } -void ASpatialTestCharacterMovement::BeginPlay() +void ASpatialTestCharacterMovement::PrepareTest() { - Super::BeginPlay(); + Super::PrepareTest(); // Universal setup step to create the TriggerBox and to set the helper variable - AddStep(TEXT("UniversalSetupStep"), FWorkerDefinition::AllWorkers, nullptr, [this](ASpatialFunctionalTest* NetTest) + AddStep(TEXT("UniversalSetupStep"), FWorkerDefinition::AllWorkers, nullptr, [this]() { + bCharacterReachedDestination = false; + + ATriggerBox* TriggerBox = + GetWorld()->SpawnActor(FVector(232.0f, 0.0f, 40.0f), FRotator::ZeroRotator, FActorSpawnParameters()); + + UBoxComponent* BoxComponent = Cast(TriggerBox->GetCollisionComponent()); + if (BoxComponent) { - bCharacterReachedDestination = false; + BoxComponent->SetBoxExtent(FVector(10.0f, 1.0f, 1.0f)); + } + + TriggerBox->OnActorBeginOverlap.AddDynamic(this, &ASpatialTestCharacterMovement::OnOverlapBegin); + RegisterAutoDestroyActor(TriggerBox); - ATriggerBox* TriggerBox = GetWorld()->SpawnActor(FVector(232.0f, 0.0f, 40.0f), FRotator::ZeroRotator, FActorSpawnParameters()); + FinishStep(); + }); - UBoxComponent* BoxComponent = Cast(TriggerBox->GetCollisionComponent()); - if (BoxComponent) + // The server checks if the clients received a TestCharacterMovement and moves them to the mentioned locations + AddStep(TEXT("SpatialTestCharacterMovementServerSetupStep"), FWorkerDefinition::Server(1), nullptr, [this]() { + for (ASpatialFunctionalTestFlowController* FlowController : GetFlowControllers()) + { + if (FlowController->WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server) { - BoxComponent->SetBoxExtent(FVector(10.0f, 1.0f, 1.0f)); + continue; } - TriggerBox->OnActorBeginOverlap.AddDynamic(this, &ASpatialTestCharacterMovement::OnOverlapBegin); - RegisterAutoDestroyActor(TriggerBox); + AController* PlayerController = Cast(FlowController->GetOwner()); + ATestMovementCharacter* PlayerCharacter = Cast(PlayerController->GetPawn()); - FinishStep(); - }); + checkf(PlayerCharacter, TEXT("Client did not receive a TestMovementCharacter")); - // The server checks if the clients received a TestCharacterMovement and moves them to the mentioned locations - AddStep(TEXT("SpatialTestCharacterMovementServerSetupStep"), FWorkerDefinition::Server(1), nullptr, [this](ASpatialFunctionalTest* NetTest) - { - for (ASpatialFunctionalTestFlowController* FlowController : GetFlowControllers()) + int FlowControllerId = FlowController->WorkerDefinition.Id; + + if (FlowControllerId == 1) { - if (FlowController->WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server) - { - continue; - } - - AController* PlayerController = Cast(FlowController->GetOwner()); - ATestMovementCharacter* PlayerCharacter = Cast(PlayerController->GetPawn()); - - checkf(PlayerCharacter, TEXT("Client did not receive a TestMovementCharacter")); - - int FlowControllerId = FlowController->WorkerDefinition.Id; - - if (FlowControllerId == 1) - { - PlayerCharacter->SetActorLocation(FVector(0.0f, 0.0f, 50.0f)); - } - else - { - PlayerCharacter->SetActorLocation(FVector(100.0f + 100*FlowControllerId, 300.0f, 50.0f)); - } + PlayerCharacter->SetActorLocation(FVector(0.0f, 0.0f, 50.0f)); } + else + { + PlayerCharacter->SetActorLocation(FVector(100.0f + 100 * FlowControllerId, 300.0f, 50.0f)); + } + } - FinishStep(); - }); + FinishStep(); + }); // Client 1 moves his character and asserts that it reached the Destination locally. - AddStep(TEXT("SpatialTestCharacterMovementClient1Move"), FWorkerDefinition::Client(1), - [](ASpatialFunctionalTest* NetTest) -> bool - { - AController* PlayerController = Cast(NetTest->GetLocalFlowController()->GetOwner()); + AddStep( + TEXT("SpatialTestCharacterMovementClient1Move"), FWorkerDefinition::Client(1), + [this]() -> bool { + AController* PlayerController = Cast(GetLocalFlowController()->GetOwner()); ATestMovementCharacter* PlayerCharacter = Cast(PlayerController->GetPawn()); - // Since the character is simulating gravity, it will drop from the original position close to (0, 0, 40), depending on the size of the CapsuleComponent in the TestMovementCharacter - return IsValid(PlayerCharacter) && PlayerCharacter->GetActorLocation().Equals(FVector(0.0f,0.0f,40.0f), 2.0f); - }, + // Since the character is simulating gravity, it will drop from the original position close to (0, 0, 40), depending on the size + // of the CapsuleComponent in the TestMovementCharacter. However, depending on physics is not good for tests, so I'm + // changing this test to only compare Z (height) coordinate. + return IsValid(PlayerCharacter) && FMath::IsNearlyEqual(PlayerCharacter->GetActorLocation().Z, 40.0f, 2.0f); + }, nullptr, - [this](ASpatialFunctionalTest* NetTest, float DeltaTime) - { + [this](float DeltaTime) { AController* PlayerController = Cast(GetLocalFlowController()->GetOwner()); ATestMovementCharacter* PlayerCharacter = Cast(PlayerController->GetPawn()); - PlayerCharacter->AddMovementInput(FVector(1,0,0), 1.0f); + PlayerCharacter->AddMovementInput(FVector(1, 0, 0), 1.0f); - if(bCharacterReachedDestination) + if (bCharacterReachedDestination) { AssertTrue(bCharacterReachedDestination, TEXT("Player character has reached the destination on the autonomous proxy.")); FinishStep(); } - }, 3.0f); + }, + 10.0f); // Server asserts that the character of client 1 has reached the Destination. - AddStep(TEXT("SpatialTestChracterMovementServerCheckMovementVisibility"), FWorkerDefinition::Server(1), nullptr, nullptr, [this](ASpatialFunctionalTest* NetTest, float DeltaTime) - { + AddStep( + TEXT("SpatialTestChracterMovementServerCheckMovementVisibility"), FWorkerDefinition::Server(1), nullptr, nullptr, + [this](float DeltaTime) { if (bCharacterReachedDestination) { AssertTrue(bCharacterReachedDestination, TEXT("Player character has reached the destination on the server.")); FinishStep(); } - }, 1.0f); + }, + + 5.0f); // Client 2 asserts that the character of client 1 has reached the Destination. - AddStep(TEXT("SpatialTestCharacterMovementClient2CheckMovementVisibility"), FWorkerDefinition::Client(2), nullptr, nullptr, [this](ASpatialFunctionalTest* NetTest, float DeltaTime) - { + AddStep( + TEXT("SpatialTestCharacterMovementClient2CheckMovementVisibility"), FWorkerDefinition::Client(2), nullptr, nullptr, + [this](float DeltaTime) { if (bCharacterReachedDestination) { AssertTrue(bCharacterReachedDestination, TEXT("Player character has reached the destination on the simulated proxy")); FinishStep(); } - }, 1.0f); + }, + 5.0f); } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMovement/SpatialTestCharacterMovement.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMovement/SpatialTestCharacterMovement.h index 8e7f1abef9..ecd27080db 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMovement/SpatialTestCharacterMovement.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMovement/SpatialTestCharacterMovement.h @@ -10,11 +10,11 @@ UCLASS() class ASpatialTestCharacterMovement : public ASpatialFunctionalTest { GENERATED_BODY() - -public: + +public: ASpatialTestCharacterMovement(); - virtual void BeginPlay() override; + virtual void PrepareTest() override; bool bCharacterReachedDestination; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/DynamicReplicationHandoverCube.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/DynamicReplicationHandoverCube.cpp new file mode 100644 index 0000000000..f0d2ef1f56 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/DynamicReplicationHandoverCube.cpp @@ -0,0 +1,8 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "DynamicReplicationHandoverCube.h" + +ADynamicReplicationHandoverCube::ADynamicReplicationHandoverCube() +{ + bReplicates = false; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/DynamicReplicationHandoverCube.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/DynamicReplicationHandoverCube.h new file mode 100644 index 0000000000..5b25982df8 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/DynamicReplicationHandoverCube.h @@ -0,0 +1,20 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestHandover/HandoverCube.h" +#include "Utils/SpatialStatics.h" +#include "DynamicReplicationHandoverCube.generated.h" + +/** + * A replicated Actor with a Cube Mesh, used as a base for Actors in spatial tests. + */ +UCLASS() +class ADynamicReplicationHandoverCube : public AHandoverCube +{ + GENERATED_BODY() + +public: + ADynamicReplicationHandoverCube(); +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/SpatialTestHandoverReplication.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/SpatialTestHandoverReplication.cpp new file mode 100644 index 0000000000..9d7db0a462 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/SpatialTestHandoverReplication.cpp @@ -0,0 +1,223 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialTestHandoverReplication.h" +#include "EngineClasses/SpatialNetDriver.h" +#include "Kismet/GameplayStatics.h" +#include "LoadBalancing/LayeredLBStrategy.h" + +#include "DynamicReplicationHandoverCube.h" +#include "SpatialFunctionalTestFlowController.h" + +/** + * This tests that an Actor's bReplicate flag is properly handed over when + * dynamically set. + * Tests UNR-4441 + * This test contains 4 Server and 2 Client workers. + * + * The flow is as follows: + * - Setup: + * - Server 1 spawns a HandoverCube (called ADynamicReplicationHandoverCube) + * which has bReplicates set to false in it's + * constructor. + * - The bReplicates flag is set to true after the end of the Actor's + * initialization. + * - All servers set a reference to the HandoverCube and reset their local copy + * of the LocationIndex and the AuthorityCheckIndex. + * - Test: + * - At this stage, Server 1 should have authority over the HandoverCube. + * - The HandoverCube moves into the authority area of Server 2. + * - At this stage, Server 2 should have authority over the HandoverCube. + * - Server 2 acquires a lock on the HandoverCube and moves it into the + * authority area of Server 3. + * - Since Server 2 has the lock on the HandoverCube it should still be + * authoritative over it. + * - Server 2 releases the lock on the HandoverCube. + * - At this point, Server 3 should become authoritative over the HandoverCube. + * - The HandoverCube moves into the authority area of Server 4. + * - At this point, Server 4 should be authoritative over the Handover Cube. + * - Clean-up: + * - The HandoverCube is destroyed. + */ + +ASpatialTestHandoverReplication::ASpatialTestHandoverReplication() : Super() { + Author = "Antoine Cordelle"; + Description = TEXT("Test dynamically set replication for an actor"); + + Server1Position = FVector(-500.0f, -500.0f, 50.0f); + Server2Position = FVector(500.0f, -500.0f, 50.0f); + Server3Position = FVector(-500.0f, 500.0f, 50.0f); + Server4Position = FVector(500.0f, 500.0f, 50.0f); +} + +void ASpatialTestHandoverReplication::PrepareTest() { + Super::PrepareTest(); + + AddStep( + TEXT("Server 1 spawns a HandoverCube (called " + "ADynamicReplicationHandoverCube) with bReplicates set to false " + "inside its authority area."), + FWorkerDefinition::Server(1), nullptr, [this]() { + HandoverCube = GetWorld()->SpawnActor( + Server1Position, FRotator::ZeroRotator, FActorSpawnParameters()); + RegisterAutoDestroyActor(HandoverCube); + FinishStep(); + }); + + AddStep(TEXT("Server sets Actor's replication to true"), + FWorkerDefinition::Server(1), nullptr, [this]() { + HandoverCube->SetReplicates(true); + FinishStep(); + }); + + const float StepTimeLimit = 10.0f; + + // All servers set a reference to the HandoverCube and reset the LocationIndex + // and AuthorityCheckIndex. + AddStep( + TEXT("All servers set a reference to the HandoverCube and reset their " + "local copy of the LocationIndex and the AuthorityCheckIndex."), + FWorkerDefinition::AllServers, nullptr, nullptr, [this](float DeltaTime) { + TArray HandoverCubes; + UGameplayStatics::GetAllActorsOfClass( + GetWorld(), ADynamicReplicationHandoverCube::StaticClass(), + HandoverCubes); + + if (HandoverCubes.Num() == 1) { + HandoverCube = + Cast(HandoverCubes[0]); + + USpatialNetDriver *NetDriver = + Cast(GetWorld()->GetNetDriver()); + + AssertTrue(IsValid(NetDriver), + TEXT("This test should be run with Spatial Networking")); + + LoadBalancingStrategy = + Cast(NetDriver->LoadBalanceStrategy); + + AssertTrue(IsValid(HandoverCube) && IsValid(LoadBalancingStrategy), + TEXT("All servers should have a valid reference to the " + "HandoverCube and the strategy")); + FinishStep(); + } + }, StepTimeLimit); + + // Check that Server 1 is authoritative over the HandoverCube. + AddStep(TEXT("Check that Server 1 is authoritative over the HandoverCube."), + FWorkerDefinition::AllServers, nullptr, + nullptr, [this](float DeltaTime) { + RequireHandoverCubeAuthorityAndPosition(1, Server1Position); + FinishStep(); + }, StepTimeLimit); + + // Move the HandoverCube to the next location, which is inside the authority + // area of Server 2. + AddStep(TEXT("Move the HandoverCube to the next location, which is inside " + "the authority area of Server 2."), + FWorkerDefinition::Server(1), nullptr, + nullptr, [this](float DeltaTime) { + if (MoveHandoverCube(Server2Position)) { + FinishStep(); + } + }, StepTimeLimit); + + // Check that Server 2 is authoritative over the HandoverCube. + AddStep(TEXT("Check that Server 2 is authoritative over the HandoverCube."), + FWorkerDefinition::AllServers, nullptr, + nullptr, [this](float DeltaTime) { + RequireHandoverCubeAuthorityAndPosition(2, Server2Position); + FinishStep(); + }, StepTimeLimit); + + // Server 2 acquires a lock on the HandoverCube. + AddStep(TEXT("Server 2 acquires a lock on the HandoverCube."), + FWorkerDefinition::Server(2), nullptr, [this]() { + HandoverCube->AcquireLock(2); + FinishStep(); + }); + + // Move the HandoverCube to the next location, which is inside the authority + // area of Server 3. + AddStep(TEXT("Move the HandoverCube to the next location, which is inside " + "the authority area of Server 3."), + FWorkerDefinition::Server(2), nullptr, + nullptr, [this](float DeltaTime) { + if (MoveHandoverCube(Server3Position)) { + FinishStep(); + } + }, StepTimeLimit); + + // Check that Server 2 is still authoritative over the HandoverCube due to + // acquiring the lock earlier. + AddStep(TEXT("Check that Server 2 is still authoritative over the " + "HandoverCube due to acquiring the lock earlier."), + FWorkerDefinition::AllServers, nullptr, + nullptr, [this](float DeltaTime) { + RequireHandoverCubeAuthorityAndPosition(2, Server3Position); + FinishStep(); + }, StepTimeLimit); + + // Server 2 releases the lock on the HandoverCube. + AddStep(TEXT("Server 2 releases the lock on the HandoverCube."), + FWorkerDefinition::Server(2), nullptr, [this]() { + HandoverCube->ReleaseLock(); + FinishStep(); + }); + + // Check that Server 3 is now authoritative over the HandoverCube. + AddStep( + TEXT("Check that Server 3 is now authoritative over the HandoverCube."), + FWorkerDefinition::AllServers, nullptr, nullptr, [this](float DeltaTime) { + RequireHandoverCubeAuthorityAndPosition(3, Server3Position); + FinishStep(); + }, StepTimeLimit); + + // Move the HandoverCube to the next location, which is inside the authority + // area of Server 4. + AddStep(TEXT("Move the HandoverCube to the next location, which is inside " + "the authority area of Server 4."), + FWorkerDefinition::Server(3), nullptr, + nullptr, [this](float DeltaTime) { + if (MoveHandoverCube(Server4Position)) { + FinishStep(); + } + }, StepTimeLimit); + + // Check that Server 4 is now authoritative over the HandoverCube. + AddStep( + TEXT("Check that Server 4 is now authoritative over the HandoverCube."), + FWorkerDefinition::AllServers, nullptr, nullptr, [this](float DeltaTime) { + RequireHandoverCubeAuthorityAndPosition(4, Server4Position); + FinishStep(); + }, StepTimeLimit); +} + +void ASpatialTestHandoverReplication::RequireHandoverCubeAuthorityAndPosition( + int WorkerShouldHaveAuthority, const FVector& ExpectedPosition) { + if (!ensureMsgf(GetLocalWorkerType() == + ESpatialFunctionalTestWorkerType::Server, + TEXT("Should only be called in Servers"))) { + return; + } + + RequireEqual_Vector(HandoverCube->GetActorLocation(), ExpectedPosition, + FString::Printf(TEXT("HandoverCube in %s"), + *ExpectedPosition.ToCompactString()), + 1.0f); + + if (WorkerShouldHaveAuthority == GetLocalWorkerId()) { + RequireTrue(HandoverCube->HasAuthority(), TEXT("Has Authority")); + } else { + RequireFalse(HandoverCube->HasAuthority(), TEXT("Doesn't Have Authority")); + } +} + +bool ASpatialTestHandoverReplication::MoveHandoverCube( + const FVector& Position) { + if (HandoverCube->HasAuthority()) { + HandoverCube->SetActorLocation(Position); + return true; + } + + return false; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/SpatialTestHandoverReplication.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/SpatialTestHandoverReplication.h new file mode 100644 index 0000000000..323eb449fc --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestHandoverReplication/SpatialTestHandoverReplication.h @@ -0,0 +1,36 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" + +#include "SpatialFunctionalTest.h" +#include "SpatialTestHandoverReplication.generated.h" + +class ADynamicReplicationHandoverCube; + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialTestHandoverReplication : public ASpatialFunctionalTest +{ + GENERATED_BODY() +public: + ASpatialTestHandoverReplication(); + + virtual void PrepareTest() override; + +private: + ADynamicReplicationHandoverCube* HandoverCube; + + // The Load Balancing used by the test, needed to decide what Server should have authority over the TestActor. + ULayeredLBStrategy* LoadBalancingStrategy; + + void RequireHandoverCubeAuthorityAndPosition(int WorkerShouldHaveAuthority, const FVector& ExpectedPosition); + + bool MoveHandoverCube(const FVector& Position); + + // Positions that belong to specific server according to 2x2 Grid LBS. + FVector Server1Position; + FVector Server2Position; + FVector Server3Position; + FVector Server4Position; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPlayerControllerMigration/SpatialTestPlayerControllerHandover.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPlayerControllerMigration/SpatialTestPlayerControllerHandover.cpp new file mode 100644 index 0000000000..8c3a06d724 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPlayerControllerMigration/SpatialTestPlayerControllerHandover.cpp @@ -0,0 +1,274 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialTestPlayerControllerHandover.h" +#include "EngineClasses/SpatialNetDriver.h" +#include "LoadBalancing/LayeredLBStrategy.h" +#include "SpatialFunctionalTestFlowController.h" + +#include "GameFramework/Character.h" +#include "GameFramework/PlayerState.h" +#include "Kismet/GameplayStatics.h" +#include "Net/UnrealNetwork.h" + +ASpatialTestPlayerControllerHandoverGameMode::ASpatialTestPlayerControllerHandoverGameMode(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + DefaultPawnClass = ACharacter::StaticClass(); + bStartPlayersAsSpectators = true; +} + +void ASpatialTestPlayerControllerHandover::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ASpatialTestPlayerControllerHandover, DestinationWorker); +} + +/** + * This test checks that APlayerController state is properly handed over when worker migration happens. + * + * We test alternating between possessing a pawn and spectating, and checking that no state is lots when migrating between workers. + * + * - The gamemode is set to start the player as a spectator. + * - We make the player spawn for the client, migrate to another worker, and check that the "Playing" state has been handed over. + * - Then we set the player as spectating only, which will also set the player as "not ready to spawn" + * - We migrate to another worker, and check that the "spectating" state has been handed over. + * - Then we try to respawn from the client, and check that no pawn has been spawned (because the player should still not be ready). + * - We set the player as ready, migrate to another worker, and check that we can spawn. + * + */ + +ASpatialTestPlayerControllerHandover::ASpatialTestPlayerControllerHandover() + : Super() +{ + Author = "Nicolas"; + Description = TEXT("Test player controller handover"); +} + +static const FName GDebugTag(TEXT("PlayerController")); + +void ASpatialTestPlayerControllerHandover::OnRep_DestinationWorker() +{ + bReceivedNewDestination = true; +} + +APlayerController* ASpatialTestPlayerControllerHandover::GetPlayerController() +{ + ASpatialFunctionalTestFlowController* FlowController = GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1); + check(FlowController != nullptr); + return Cast(FlowController->GetOwner()); +} + +void ASpatialTestPlayerControllerHandover::PrepareTest() +{ + Super::PrepareTest(); + + FSpatialFunctionalTestStepDefinition NextDestination(true); + NextDestination.StepName = TEXT("DetermineNextDestination"); + NextDestination.NativeStartEvent = FNativeStepStartDelegate::CreateLambda([this]() { + if (HasAuthority()) + { + TArray Workers; + WorkerPositions.GenerateKeyArray(Workers); + int32 i = Workers.Find(DestinationWorker); + check(i != -1); + + i = (i + 1) % Workers.Num(); + + DestinationWorker = Workers[i]; + } + + FinishStep(); + }); + + FSpatialFunctionalTestStepDefinition AuthMove(true); + AuthMove.StepName = TEXT("PerformAuthMove"); + AuthMove.NativeTickEvent = FNativeStepTickDelegate::CreateLambda([this](float) { + if (HasAuthority() || bReceivedNewDestination) + { + bReceivedNewDestination = false; + SetTagDelegation(GDebugTag, DestinationWorker); + FinishStep(); + } + }); + + FSpatialFunctionalTestStepDefinition WaitAuth(true); + WaitAuth.StepName = TEXT("WaitForAuthChange"); + WaitAuth.NativeTickEvent = FNativeStepTickDelegate::CreateLambda([this](float) { + if (LocalWorker == DestinationWorker) + { + APlayerController* PlayerController = GetPlayerController(); + if (PlayerController && PlayerController->HasAuthority()) + { + FinishStep(); + } + } + else + { + FinishStep(); + } + }); + + auto AddStepChangePlayerControllerAuthWorker = [&] { + AddStepFromDefinition(NextDestination, FWorkerDefinition::AllServers); + AddStepFromDefinition(AuthMove, FWorkerDefinition::AllServers); + AddStepFromDefinition(WaitAuth, FWorkerDefinition::AllServers); + }; + + FSpatialFunctionalTestStepDefinition ClientRespawn(true); + ClientRespawn.StepName = TEXT("SpawnPlayerPawn"); + ClientRespawn.NativeStartEvent = FNativeStepStartDelegate::CreateLambda([this] { + ASpatialFunctionalTestFlowController* FlowController = GetLocalFlowController(); + APlayerController* PlayerController = Cast(FlowController->GetOwner()); + + PlayerController->ServerRestartPlayer(); + + FinishStep(); + }); + + FSpatialFunctionalTestStepDefinition WaitPlayerSpawn(true); + WaitPlayerSpawn.StepName = TEXT("WaitForPlayerPawn"); + WaitPlayerSpawn.NativeStartEvent = FNativeStepStartDelegate::CreateLambda([this] { + APlayerController* PlayerController = GetPlayerController(); + if (PlayerController && PlayerController->HasAuthority()) + { + if (PlayerController->GetPawn() != nullptr) + { + AssertTrue(PlayerController->GetStateName() == NAME_Playing, TEXT("State was changed on posession")); + FinishStep(); + } + } + else + { + FinishStep(); + } + }); + + AddStep( + TEXT("SetupStep"), FWorkerDefinition::AllServers, nullptr, + [this]() { + UWorld* World = GetWorld(); + + WorkerPositions.Empty(); + bIsOnDefaultLayer = false; + bReceivedNewDestination = false; + + ULayeredLBStrategy* RootStrategy = GetLoadBalancingStrategy(); + + bIsOnDefaultLayer = RootStrategy->CouldHaveAuthority(ACharacter::StaticClass()); + if (bIsOnDefaultLayer) + { + FName LocalLayer = RootStrategy->GetLocalLayerName(); + UAbstractLBStrategy* LocalStrategy = RootStrategy->GetLBStrategyForLayer(LocalLayer); + + AssertTrue(LocalStrategy->IsA(), TEXT("")); + + UGridBasedLBStrategy* GridStrategy = Cast(LocalStrategy); + + for (auto& WorkerRegion : GridStrategy->GetLBStrategyRegions()) + { + FVector2D RegionCenter = WorkerRegion.Value.GetCenter(); + WorkerPositions.Add(WorkerRegion.Key, FVector(RegionCenter.X, RegionCenter.Y, 0)); + } + LocalWorker = GridStrategy->GetLocalVirtualWorkerId(); + } + + FinishStep(); + }, + nullptr); + + AddStep(TEXT("GetClientController"), FWorkerDefinition::AllServers, nullptr, [this] { + if (APlayerController* PlayerController = GetPlayerController()) + { + AssertTrue(PlayerController->GetStateName() == NAME_Spectating, TEXT("Client started in spectator mode")); + + if (PlayerController->HasAuthority()) + { + AddDebugTag(PlayerController, GDebugTag); + DestinationWorker = LocalWorker; + } + } + FinishStep(); + }); + + AddStepFromDefinition(ClientRespawn, FWorkerDefinition::Client(1)); + AddStepFromDefinition(WaitPlayerSpawn, FWorkerDefinition::AllServers); + + AddStepChangePlayerControllerAuthWorker(); + + AddStep(TEXT("CheckStateHandover"), FWorkerDefinition::AllServers, nullptr, [this] { + APlayerController* PlayerController = GetPlayerController(); + if (PlayerController && PlayerController->HasAuthority()) + { + AssertTrue(PlayerController->GetStateName() == NAME_Playing, TEXT("State handed over")); + } + FinishStep(); + }); + + AddStep(TEXT("SetToSpectateOnly"), FWorkerDefinition::AllServers, nullptr, [this] { + APlayerController* PlayerController = GetPlayerController(); + if (PlayerController && PlayerController->HasAuthority()) + { + // This will set the Player as "not ready" + PlayerController->StartSpectatingOnly(); + } + FinishStep(); + }); + + AddStepChangePlayerControllerAuthWorker(); + + AddStep(TEXT("CheckStateHandover"), FWorkerDefinition::AllServers, nullptr, [this] { + APlayerController* PlayerController = GetPlayerController(); + if (PlayerController && PlayerController->HasAuthority()) + { + AssertTrue(PlayerController->GetStateName() == NAME_Spectating, TEXT("State was handed over pn player controller")); +#if ENGINE_MINOR_VERSION <= 24 + AssertTrue(PlayerController->PlayerState->bOnlySpectator, TEXT("State was handed over on player state")); + PlayerController->PlayerState->bOnlySpectator = false; +#else + AssertTrue(PlayerController->PlayerState->IsOnlyASpectator(), TEXT("State was handed over on player state")); + PlayerController->PlayerState->SetIsOnlyASpectator(false); +#endif + } + + FinishStep(); + }); + + AddStepFromDefinition(ClientRespawn, FWorkerDefinition::Client(1)); + + AddStep( + TEXT("CheckNoPlayerSpawned"), FWorkerDefinition::AllServers, nullptr, + [this] { + CheckNoPlayerSpawnTime = GetWorld()->GetTimeSeconds(); + }, + [this](float) { + APlayerController* PlayerController = GetPlayerController(); + if (PlayerController && PlayerController->HasAuthority()) + { + AssertTrue(PlayerController->GetPawn() == nullptr, TEXT("No pawn was spawned")); + if (GetWorld()->GetTimeSeconds() - CheckNoPlayerSpawnTime > 3.0) + { + FinishStep(); + } + } + else + { + FinishStep(); + } + }); + + AddStep(TEXT("SetPlayerReady"), FWorkerDefinition::AllServers, nullptr, [this] { + APlayerController* PlayerController = GetPlayerController(); + if (PlayerController && PlayerController->HasAuthority()) + { + PlayerController->ServerSetSpectatorWaiting(true); + PlayerController->ClientSetSpectatorWaiting(true); + } + FinishStep(); + }); + + AddStepChangePlayerControllerAuthWorker(); + + AddStepFromDefinition(ClientRespawn, FWorkerDefinition::Client(1)); + AddStepFromDefinition(WaitPlayerSpawn, FWorkerDefinition::AllServers); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPlayerControllerMigration/SpatialTestPlayerControllerHandover.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPlayerControllerMigration/SpatialTestPlayerControllerHandover.h new file mode 100644 index 0000000000..0312a9f027 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPlayerControllerMigration/SpatialTestPlayerControllerHandover.h @@ -0,0 +1,55 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialCommonTypes.h" +#include "SpatialFunctionalTest.h" + +#include "GameFramework/GameMode.h" +#include "GameFramework/PlayerController.h" + +#include "SpatialTestPlayerControllerHandover.generated.h" + +class ULayeredLBStrategy; + +UCLASS() +class ASpatialTestPlayerControllerHandoverGameMode : public AGameModeBase +{ + GENERATED_UCLASS_BODY() +}; + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialTestPlayerControllerHandover : public ASpatialFunctionalTest +{ + GENERATED_BODY() + +public: + ASpatialTestPlayerControllerHandover(); + + virtual void PrepareTest() override; + + void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + +private: + using ASpatialFunctionalTest::SetTagDelegation; + + APlayerController* GetPlayerController(); + + UFUNCTION() + void OnRep_DestinationWorker(); + + // The Load Balancing used by the test, needed to decide what Server should have authority over the HandoverCube. + ULayeredLBStrategy* LoadBalancingStrategy; + + TMap WorkerPositions; + VirtualWorkerId LocalWorker; + bool bIsOnDefaultLayer; + + UPROPERTY(ReplicatedUsing = OnRep_DestinationWorker) + int32 DestinationWorker; + + float CheckNoPlayerSpawnTime; + + bool bReceivedNewDestination; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestPossession.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestPossession.cpp index 75b33e449d..9fb4eb2cd6 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestPossession.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestPossession.cpp @@ -1,16 +1,16 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "SpatialTestPossession.h" -#include "TestPossessionPawn.h" -#include "GameFramework/PlayerController.h" #include "Containers/Array.h" +#include "GameFramework/PlayerController.h" #include "SpatialFunctionalTestFlowController.h" +#include "TestPossessionPawn.h" /** * This test tests client possession over pawns. - * - * The test includes a single server and two client workers. The client workers begin with a player controller and their default pawns, which they initially possess. - * The flow is as follows: + * + * The test includes a single server and two client workers. The client workers begin with a player controller and their default pawns, + * which they initially possess. The flow is as follows: * - Setup: * - Two test pawn actors are spawned, one for each client, with an offset in the y direction for easy visualisation * - The controllers for each client possess the spawned test pawn actors @@ -28,63 +28,62 @@ ASpatialTestPossession::ASpatialTestPossession() Description = TEXT("Test Actor Possession"); } -void ASpatialTestPossession::BeginPlay() +void ASpatialTestPossession::PrepareTest() { - Super::BeginPlay(); - - AddStep(TEXT("SpatialTestPossessionServerSetupStep"), FWorkerDefinition::Server(1), nullptr, nullptr, [](ASpatialFunctionalTest* NetTest, float DeltaTime) { - ASpatialTestPossession* Test = Cast(NetTest); + Super::PrepareTest(); + AddStep(TEXT("SpatialTestPossessionServerSetupStep"), FWorkerDefinition::Server(1), nullptr, nullptr, [this](float DeltaTime) { float YToSpawnAt = -60.0f; float YSpawnIncrement = 120.0f; - for (ASpatialFunctionalTestFlowController* FlowController : Test->GetFlowControllers()) + for (ASpatialFunctionalTestFlowController* FlowController : GetFlowControllers()) { if (FlowController->WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server) { continue; } // Spawn the actor for the client to possess, and increment the y variable for the next spawn - ATestPossessionPawn* TestPawn = Test->GetWorld()->SpawnActor(FVector(0.0f, YToSpawnAt, 50.0f), FRotator::ZeroRotator, FActorSpawnParameters()); + ATestPossessionPawn* TestPawn = GetWorld()->SpawnActor(FVector(0.0f, YToSpawnAt, 50.0f), + FRotator::ZeroRotator, FActorSpawnParameters()); YToSpawnAt += YSpawnIncrement; - Test->RegisterAutoDestroyActor(TestPawn); + RegisterAutoDestroyActor(TestPawn); AController* PlayerController = Cast(FlowController->GetOwner()); // Save old one to put it back in the final step - Test->OriginalPawns.Add(TPair(PlayerController, PlayerController->GetPawn())); + OriginalPawns.Add(TPair(PlayerController, PlayerController->GetPawn())); // Actually do the possession of the test pawn PlayerController->Possess(TestPawn); } - Test->FinishStep(); - }); + FinishStep(); + }); - AddStep(TEXT("SpatialTestPossessionClientCheckStep"), FWorkerDefinition::AllClients, - [](ASpatialFunctionalTest* NetTest) -> bool { - AController* PlayerController = Cast(NetTest->GetLocalFlowController()->GetOwner()); + AddStep( + TEXT("SpatialTestPossessionClientCheckStep"), FWorkerDefinition::AllClients, + [this]() -> bool { + AController* PlayerController = Cast(GetLocalFlowController()->GetOwner()); return IsValid(PlayerController->GetPawn()); }, - [](ASpatialFunctionalTest* NetTest) { - ASpatialTestPossession* Test = Cast(NetTest); - ASpatialFunctionalTestFlowController* FlowController = Test->GetLocalFlowController(); + [this]() { + ASpatialFunctionalTestFlowController* FlowController = GetLocalFlowController(); AController* PlayerController = Cast(FlowController->GetOwner()); // Run the assertion. This checks the currently possessed pawn is a TestPossessionPawn, and fails if not - Test->AssertTrue(PlayerController->GetPawn()->GetClass() == ATestPossessionPawn::StaticClass(), TEXT("Player has possessed test pawn"), PlayerController); + AssertTrue(PlayerController->GetPawn()->GetClass() == ATestPossessionPawn::StaticClass(), + TEXT("Player has possessed test pawn"), PlayerController); - Test->FinishStep(); + FinishStep(); }); - AddStep(TEXT("SpatialTestPossessionServerPossessOldPawns"), FWorkerDefinition::Server(1), nullptr, nullptr, [](ASpatialFunctionalTest* NetTest, float DeltaTime) { - ASpatialTestPossession* Test = Cast(NetTest); - for (const auto& OriginalPawnPair : Test->OriginalPawns) + AddStep(TEXT("SpatialTestPossessionServerPossessOldPawns"), FWorkerDefinition::Server(1), nullptr, nullptr, [this](float DeltaTime) { + for (const auto& OriginalPawnPair : OriginalPawns) { OriginalPawnPair.Key->Possess(OriginalPawnPair.Value); } - Test->FinishStep(); - }); + FinishStep(); + }); } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestPossession.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestPossession.h index e849220459..4caaa20325 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestPossession.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestPossession.h @@ -13,7 +13,7 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialTestPossession : public ASpatialFunc public: ASpatialTestPossession(); - virtual void BeginPlay() override; + virtual void PrepareTest() override; // To save original Pawns and possess them back at the end TArray> OriginalPawns; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestRepossession.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestRepossession.cpp index 7f510a2d44..11fb57e127 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestRepossession.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestRepossession.cpp @@ -4,9 +4,9 @@ #include "GameFramework/PlayerController.h" #include "Net/UnrealNetwork.h" -#include "TestPossessionPawn.h" -#include "SpatialTestPossession.h" #include "SpatialFunctionalTestFlowController.h" +#include "SpatialTestPossession.h" +#include "TestPossessionPawn.h" void ASpatialTestRepossession::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const { @@ -23,55 +23,55 @@ ASpatialTestRepossession::ASpatialTestRepossession() Description = TEXT("Test Actor Repossession"); } -void ASpatialTestRepossession::BeginPlay() +void ASpatialTestRepossession::PrepareTest() { - Super::BeginPlay(); + Super::PrepareTest(); - AddStep(TEXT("SpatialTestRepossessionServerSetupStep"), FWorkerDefinition::Server(1), nullptr, nullptr, [](ASpatialFunctionalTest* NetTest, float DeltaTime) { - ASpatialTestRepossession* Test = Cast(NetTest); - ASpatialFunctionalTestFlowController* LocalFlowController = Test->GetLocalFlowController(); + AddStep(TEXT("SpatialTestRepossessionServerSetupStep"), FWorkerDefinition::Server(1), nullptr, nullptr, [this](float DeltaTime) { + ASpatialFunctionalTestFlowController* LocalFlowController = GetLocalFlowController(); checkf(LocalFlowController, TEXT("Can't be running test without valid FlowControl.")); - Test->TestPawns.Empty(); - Test->Controllers.Empty(); + TestPawns.Empty(); + Controllers.Empty(); float YToSpawnAt = -60.0f; float YSpawnIncrement = 120.0f; - for (ASpatialFunctionalTestFlowController* FlowController : Test->GetFlowControllers()) + for (ASpatialFunctionalTestFlowController* FlowController : GetFlowControllers()) { if (FlowController->WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Server) { continue; } - ATestPossessionPawn* TestPawn = Test->GetWorld()->SpawnActor(FVector(0.0f, YToSpawnAt, 50.0f), FRotator::ZeroRotator, FActorSpawnParameters()); - Test->RegisterAutoDestroyActor(TestPawn); + ATestPossessionPawn* TestPawn = GetWorld()->SpawnActor(FVector(0.0f, YToSpawnAt, 50.0f), + FRotator::ZeroRotator, FActorSpawnParameters()); + RegisterAutoDestroyActor(TestPawn); - Test->TestPawns.Add(TestPawn); + TestPawns.Add(TestPawn); YToSpawnAt += YSpawnIncrement; APlayerController* PlayerController = Cast(FlowController->GetOwner()); - Test->Controllers.Add(PlayerController); + Controllers.Add(PlayerController); // Save original Pawn - Test->OriginalPawns.Add(TPair(PlayerController, PlayerController->GetPawn())); + OriginalPawns.Add(TPair(PlayerController, PlayerController->GetPawn())); PlayerController->Possess(TestPawn); } - Test->FinishStep(); - }); + FinishStep(); + }); - auto ClientCheckPossessionTickLambda = [](ASpatialFunctionalTest* NetTest, float DeltaTime) { - ASpatialTestRepossession* Test = Cast(NetTest); - ASpatialFunctionalTestFlowController* LocalFlowController = Test->GetLocalFlowController(); + auto ClientCheckPossessionTickLambda = [this](float DeltaTime) { + ASpatialFunctionalTestFlowController* LocalFlowController = GetLocalFlowController(); - Test->AssertTrue(Test->Controllers.Num() == Test->TestPawns.Num(), TEXT("Number of players is equal to the number of pawns spawned"), LocalFlowController); + AssertTrue(Controllers.Num() == TestPawns.Num(), TEXT("Number of players is equal to the number of pawns spawned"), + LocalFlowController); APlayerController* PlayerController = Cast(LocalFlowController->GetOwner()); bool bFoundCorrectPair = false; - for (int i = 0; i < Test->TestPawns.Num(); i++) + for (int i = 0; i < TestPawns.Num(); i++) { - if (PlayerController->GetPawn() == Test->TestPawns[i] && PlayerController == Test->Controllers[i]) + if (PlayerController->GetPawn() == TestPawns[i] && PlayerController == Controllers[i]) { bFoundCorrectPair = true; break; @@ -79,47 +79,50 @@ void ASpatialTestRepossession::BeginPlay() } if (bFoundCorrectPair) { - Test->AssertTrue(bFoundCorrectPair, TEXT("Player has possessed correct test pawn"), PlayerController); + AssertTrue(bFoundCorrectPair, TEXT("Player has possessed correct test pawn"), PlayerController); - Test->FinishStep(); + FinishStep(); } }; - AddStep(TEXT("SpatialTestRepossessionClientCheckPossessionStep"), FWorkerDefinition::AllClients, [](ASpatialFunctionalTest* NetTest) -> bool { - ASpatialTestRepossession* Test = Cast(NetTest); - int NumClients = Test->GetNumRequiredClients(); - return Test->Controllers.Num() == NumClients && Test->TestPawns.Num() == NumClients; - }, nullptr, ClientCheckPossessionTickLambda); + AddStep( + TEXT("SpatialTestRepossessionClientCheckPossessionStep"), FWorkerDefinition::AllClients, + [this]() -> bool { + int NumClients = GetNumRequiredClients(); + return Controllers.Num() == NumClients && TestPawns.Num() == NumClients; + }, + nullptr, ClientCheckPossessionTickLambda); - AddStep(TEXT("SpatialTestRepossessionServerSwitchStep"), FWorkerDefinition::Server(1), nullptr, nullptr, [](ASpatialFunctionalTest* NetTest, float DeltaTime) { - ASpatialTestRepossession* Test = Cast(NetTest); - ASpatialFunctionalTestFlowController* LocalFlowController = Test->GetLocalFlowController(); + AddStep(TEXT("SpatialTestRepossessionServerSwitchStep"), FWorkerDefinition::Server(1), nullptr, nullptr, [this](float DeltaTime) { + ASpatialFunctionalTestFlowController* LocalFlowController = GetLocalFlowController(); checkf(LocalFlowController, TEXT("Can't be running test without valid FlowControl.")); - Test->AssertTrue(Test->Controllers.Num() == Test->TestPawns.Num(), TEXT("Number of players is equal to the number of pawns spawned"), LocalFlowController); - int NumPawns = Test->Controllers.Num(); - APlayerController* FirstController = Test->Controllers[0]; + AssertTrue(Controllers.Num() == TestPawns.Num(), TEXT("Number of players is equal to the number of pawns spawned"), + LocalFlowController); + int NumPawns = Controllers.Num(); + APlayerController* FirstController = Controllers[0]; for (int i = 0; i < NumPawns; i++) { - Test->Controllers[i]->Possess(Test->TestPawns[(i + 1) % NumPawns]); + Controllers[i]->Possess(TestPawns[(i + 1) % NumPawns]); if (i > 0) { - Test->Controllers[i - 1] = Test->Controllers[i]; + Controllers[i - 1] = Controllers[i]; } } - Test->Controllers[NumPawns - 1] = FirstController; - - Test->FinishStep(); - }); - - AddStep(TEXT("SpatialTestRepossessionClientCheckRepossessionStep"), FWorkerDefinition::AllClients, nullptr, nullptr, ClientCheckPossessionTickLambda); - - AddStep(TEXT("SpatialTestRepossessionServerPossessOriginalPawn"), FWorkerDefinition::Server(1), nullptr, nullptr, [](ASpatialFunctionalTest* NetTest, float DeltaTime) { - ASpatialTestRepossession* Test = Cast(NetTest); - for (const auto& OriginalPawnPair : Test->OriginalPawns) - { - OriginalPawnPair.Key->Possess(OriginalPawnPair.Value); - } - Test->FinishStep(); - }); + Controllers[NumPawns - 1] = FirstController; + + FinishStep(); + }); + + AddStep(TEXT("SpatialTestRepossessionClientCheckRepossessionStep"), FWorkerDefinition::AllClients, nullptr, nullptr, + ClientCheckPossessionTickLambda); + + AddStep(TEXT("SpatialTestRepossessionServerPossessOriginalPawn"), FWorkerDefinition::Server(1), nullptr, nullptr, + [this](float DeltaTime) { + for (const auto& OriginalPawnPair : OriginalPawns) + { + OriginalPawnPair.Key->Possess(OriginalPawnPair.Value); + } + FinishStep(); + }); } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestRepossession.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestRepossession.h index 3cf22714da..66f2aa6224 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestRepossession.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/SpatialTestRepossession.h @@ -15,7 +15,7 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialTestRepossession : public ASpatialFu public: ASpatialTestRepossession(); - virtual void BeginPlay() override; + virtual void PrepareTest() override; void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/TestPossessionPawn.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/TestPossessionPawn.cpp index 6e2b7afb75..5f3f42c1fa 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/TestPossessionPawn.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/TestPossessionPawn.cpp @@ -1,16 +1,17 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "TestPossessionPawn.h" -#include "Engine/World.h" -#include "Engine/Classes/Camera/CameraComponent.h" #include "Components/StaticMeshComponent.h" +#include "Engine/Classes/Camera/CameraComponent.h" +#include "Engine/World.h" #include "Materials/Material.h" ATestPossessionPawn::ATestPossessionPawn() { SphereComponent = CreateDefaultSubobject(TEXT("SphereComponent")); SphereComponent->SetStaticMesh(LoadObject(nullptr, TEXT("StaticMesh'/Engine/BasicShapes/Sphere.Sphere'"))); - SphereComponent->SetMaterial(0, LoadObject(nullptr, TEXT("Material'/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial'"))); + SphereComponent->SetMaterial( + 0, LoadObject(nullptr, TEXT("Material'/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial'"))); SphereComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision); SphereComponent->SetVisibility(true); RootComponent = SphereComponent; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/TestPossessionPawn.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/TestPossessionPawn.h index d13494ac81..fd6f2afba6 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/TestPossessionPawn.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestPossession/TestPossessionPawn.h @@ -18,6 +18,7 @@ class ATestPossessionPawn : public APawn UPROPERTY(VisibleAnywhere, Category = "Spatial Functional Test") UCameraComponent* CameraComponent; + public: ATestPossessionPawn(); }; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestRepNotify/SpatialTestRepNotify.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestRepNotify/SpatialTestRepNotify.cpp new file mode 100644 index 0000000000..ff441f1fe3 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestRepNotify/SpatialTestRepNotify.cpp @@ -0,0 +1,278 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialTestRepNotify.h" +#include "SpatialGDK/Public/EngineClasses/SpatialNetDriver.h" + +#include "Net/UnrealNetwork.h" + +/** + * This test tests RepNotifies and shadow data implementation. + * + * The test includes 1 Server and 2 Client workers. + * The flow is as follows: + * - Setup: + * - The test itself is used as the test Actor. + * - It contains a number of replicated properties and OnRep functions that either set boolean flags confirming they have been called, or + *save the old value of the argument passed to the OnRep function. + * - Test: + * - The Server changes the values of the replicated properties multiple times. + * - The clients check whether the RepNotifies have or have not been called correctly. + * - Cleanup: + * - None. + */ + +ASpatialTestRepNotify::ASpatialTestRepNotify() + : Super() +{ + Author = "Miron + Andrei"; + Description = TEXT("Test RepNotify replication and shadow data"); +} + +void ASpatialTestRepNotify::PrepareTest() +{ + Super::PrepareTest(); + + // The Server sets some initial values to the replicated variables. + AddStep(TEXT("SpatialTestRepNotifyServerSetReplicatedVariables"), FWorkerDefinition::Server(1), nullptr, [this]() { + OnChangedRepNotifyInt1 = 1; + AlwaysRepNotifyInt1 = 2; + OnChangedRepNotifyInt2 = 3; + AlwaysRepNotifyInt2 = 4; + TestArray.Empty(); + TestArray.Add(1); + TestArray.Add(2); + + FinishStep(); + }); + + // All clients check they received the correct values for the replicated variables. + AddStep( + TEXT("SpatialTestRepNotifyAllClientsCheckReplicatedVariables"), FWorkerDefinition::AllClients, nullptr, nullptr, + [this](float DeltaTime) { + if (bOnRepOnChangedRepNotifyInt1Called && bOnRepAlwaysRepNotifyInt1Called && OnChangedRepNotifyInt1 == 1 + && AlwaysRepNotifyInt1 == 2 && OnChangedRepNotifyInt2 == 3 && AlwaysRepNotifyInt2 == 4 && TestArray.Num() == 2 + && TestArray[0] == 1 && TestArray[1] == 2) + { + FinishStep(); + } + }, + 5.0f); + + // All clients reset the values modified by the RepNotifies called due to the Server modifying the replicated variables. + AddStep(TEXT("SpatialTestRepNotifiyAllClientsLocallyChangeVariables"), FWorkerDefinition::AllClients, nullptr, [this]() { + bOnRepOnChangedRepNotifyInt1Called = false; + bOnRepAlwaysRepNotifyInt1Called = false; + OldOnChangedRepNotifyInt2 = -3; + OldAlwaysRepNotifyInt2 = -4; + OldTestArray.Empty(); + + FinishStep(); + }); + + // All clients modify 3 of the replicated variables. + AddStep( + TEXT("SpatialTestRepNotifyAllClientsModifyReplicatedVariables"), FWorkerDefinition::AllClients, nullptr, + [this]() { + // Note that OnChangedRepNotifyInt2 is specifically not modified, this will be relevant in the + // SpatialTestRepNotifyAllClientsCheckValuesAndRepNotifies step. + + OnChangedRepNotifyInt1 = 10; + AlwaysRepNotifyInt1 = 20; + AlwaysRepNotifyInt2 = 50; + + FinishStep(); + }, + nullptr, 5.0f); + + // The Server modifies the replicated variables once again. + AddStep(TEXT("SpatialTestRepNotifyServerChangeReplicatedVariables"), FWorkerDefinition::Server(1), nullptr, [this]() { + OnChangedRepNotifyInt1 = 10; + OnChangedRepNotifyInt2 = 30; + + AlwaysRepNotifyInt1 = 20; + AlwaysRepNotifyInt2 = 40; + + TestArray.Add(30); + FinishStep(); + }); + + // All clients check that the replicated variables were received correctly and that RepNotify acted as expected. + AddStep( + TEXT("SpatialTestRepNotifyAllClientsCheckValuesAndRepNotifies"), FWorkerDefinition::AllClients, + [this]() -> bool { + // First make sure that we have correctly received the replicated variables, before checking the RepNotify behaviour. + if (!(OnChangedRepNotifyInt1 == 10 && AlwaysRepNotifyInt1 == 20 && OnChangedRepNotifyInt2 == 30 && AlwaysRepNotifyInt2 == 40)) + { + return false; + } + + if (!(TestArray.Num() == 3 && TestArray[0] == 1 && TestArray[1] == 2 && TestArray[2] == 30)) + { + return false; + } + + return true; + }, + [this]() { + // At this point, we have correctly received the values of all replicated variables, therefore the RepNotify behaviour can be + // checked. + + // Since the RepNotify for this variable is using the default REPNOTIFY_OnChanged, we expect it not to get called on the + // clients. + if (bOnRepOnChangedRepNotifyInt1Called) + { + FinishTest(EFunctionalTestResult::Failed, TEXT("OnRepOnChangedRepNotifyInt1 should not be called on the clients")); + return; + } + + // In this case, the RepNotify uses REPNOTIFY_Always so we expect it to be called on the clients. + if (!bOnRepAlwaysRepNotifyInt1Called) + { + FinishTest(EFunctionalTestResult::Failed, TEXT("OnRepOnChangedRepNotifyInt1 should be called on the clients")); + return; + } + + // From the Clients, we have not modified the value of this particular variable, so we still expect the old value to be the one + // initially set by the Server in the first step. + if (OldOnChangedRepNotifyInt2 != 3) + { + FinishTest(EFunctionalTestResult::Failed, + TEXT("OnRepOnChangedRepNotifyInt2 should have been called with the old value of 3")); + return; + } + + // Since we have modified this value from the Clients, we expect its value to be the same as set in + // SpatialTestRepNotifyAllClientsModifyReplicatedVariables. + if (OldAlwaysRepNotifyInt2 != 50) + { + FinishTest(EFunctionalTestResult::Failed, + TEXT("OnRepAlwaysRepNotifyInt2 should have been called with the old value of 40")); + return; + } + + // We consciously differ from native UE here + if (GetNetDriver()->IsA(USpatialNetDriver::StaticClass())) + { + if (OldTestArray.Num() != 2) + { + FinishTest(EFunctionalTestResult::Failed, + TEXT("OnRepTestArray should have been called with 2 entries in the old Array on Spatial")); + return; + } + } + else + { + if (OldTestArray.Num() != 3) + { + FinishTest(EFunctionalTestResult::Failed, + TEXT("OnRepTestArray should have been called with 3 entries in the old Array on Native")); + return; + } + + if (OldTestArray[2] != 0) + { + FinishTest(EFunctionalTestResult::Failed, + TEXT("OnRepTestArray should have been called with 0 as its third entry in the old Array on Native")); + return; + } + } + + if (OldTestArray[0] != 1) + { + FinishTest(EFunctionalTestResult::Failed, + TEXT("OnRepTestArray should have been called with 1 as its first entry in the old Array")); + return; + } + + if (OldTestArray[1] != 2) + { + FinishTest(EFunctionalTestResult::Failed, + TEXT("OnRepTestArray should have been called with 2 as its second entry in the old Array")); + return; + } + + FinishStep(); + }, + nullptr, 5.0f); + + AddStep(TEXT("SpatialTestRepNotifyServerTestRemovalFromArray"), FWorkerDefinition::Server(1), nullptr, [this]() { + TestArray.Pop(true); + + FinishStep(); + }); + + AddStep( + TEXT("SpatialTestRepNotifyClientsCheckArrayRemoval"), FWorkerDefinition::AllClients, + [this]() -> bool { + // First make sure that we have correctly received the replicated variables, before checking RepNotify behaviour. + return TestArray.Num() == 2; + }, + [this]() { + // At this point, we have received the update for the TestArray, so it makes sense to check RepNotify beahviour. + + // We consciously differ from native UE here + if (GetNetDriver()->IsA(USpatialNetDriver::StaticClass())) + { + if (OldTestArray.Num() != 3) + { + FinishTest(EFunctionalTestResult::Failed, + TEXT("OnRepTestArray should have been called with 3 elements after shrinking on Spatial")); + return; + } + if (OldTestArray[2] != 30) + { + FinishTest(EFunctionalTestResult::Failed, + TEXT("OnRepTestArray should have been called with 30 as its third entry after shrinking on Spatial")); + return; + } + } + else + { + if (OldTestArray.Num() != 2) + { + FinishTest(EFunctionalTestResult::Failed, + TEXT("OnRepTestArray should have been called with 2 elements after shrinking on Native")); + return; + } + } + + FinishStep(); + }, + nullptr, 5.0f); +} + +void ASpatialTestRepNotify::OnRep_OnChangedRepNotifyInt1(int32 OldOnChangedRepNotifyInt1) +{ + bOnRepOnChangedRepNotifyInt1Called = true; +} + +void ASpatialTestRepNotify::OnRep_AlwaysRepNotifyInt1(int32 OldAlwaysRepNotifyInt1) +{ + bOnRepAlwaysRepNotifyInt1Called = true; +} + +void ASpatialTestRepNotify::OnRep_OnChangedRepNotifyInt2(int32 InOldOnChangedRepNotifyInt2) +{ + OldOnChangedRepNotifyInt2 = InOldOnChangedRepNotifyInt2; +} + +void ASpatialTestRepNotify::OnRep_AlwaysRepNotifyInt2(int32 InOldAlwaysRepNotifyInt2) +{ + OldAlwaysRepNotifyInt2 = InOldAlwaysRepNotifyInt2; +} + +void ASpatialTestRepNotify::OnRep_TestArray(TArray InOldTestArray) +{ + OldTestArray = InOldTestArray; +} + +void ASpatialTestRepNotify::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ASpatialTestRepNotify, OnChangedRepNotifyInt1); + DOREPLIFETIME_CONDITION_NOTIFY(ASpatialTestRepNotify, AlwaysRepNotifyInt1, COND_None, REPNOTIFY_Always); + DOREPLIFETIME(ASpatialTestRepNotify, OnChangedRepNotifyInt2); + DOREPLIFETIME_CONDITION_NOTIFY(ASpatialTestRepNotify, AlwaysRepNotifyInt2, COND_None, REPNOTIFY_Always); + DOREPLIFETIME(ASpatialTestRepNotify, TestArray); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestRepNotify/SpatialTestRepNotify.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestRepNotify/SpatialTestRepNotify.h new file mode 100644 index 0000000000..08659f74b6 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestRepNotify/SpatialTestRepNotify.h @@ -0,0 +1,60 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialFunctionalTest.h" +#include "SpatialTestRepNotify.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialTestRepNotify : public ASpatialFunctionalTest +{ + GENERATED_BODY() + +public: + ASpatialTestRepNotify(); + + virtual void PrepareTest() override; + + void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + bool bOnRepOnChangedRepNotifyInt1Called; + + bool bOnRepAlwaysRepNotifyInt1Called; + + int32 OldOnChangedRepNotifyInt2; + + int32 OldAlwaysRepNotifyInt2; + + TArray OldTestArray; + + UPROPERTY(ReplicatedUsing = OnRep_OnChangedRepNotifyInt1) + int32 OnChangedRepNotifyInt1; + + UPROPERTY(ReplicatedUsing = OnRep_AlwaysRepNotifyInt1) + int32 AlwaysRepNotifyInt1; + + UPROPERTY(ReplicatedUsing = OnRep_OnChangedRepNotifyInt2) + int32 OnChangedRepNotifyInt2; + + UPROPERTY(ReplicatedUsing = OnRep_AlwaysRepNotifyInt2) + int32 AlwaysRepNotifyInt2; + + UPROPERTY(ReplicatedUsing = OnRep_TestArray) + TArray TestArray; + + UFUNCTION() + void OnRep_OnChangedRepNotifyInt1(int32 OldOnChangedRepNotifyInt1); + + UFUNCTION() + void OnRep_AlwaysRepNotifyInt1(int32 OldAlwaysRepNotifyInt1); + + UFUNCTION() + void OnRep_OnChangedRepNotifyInt2(int32 OldOnChangedRepNotifyInt2); + + UFUNCTION() + void OnRep_AlwaysRepNotifyInt2(int32 OldAlwaysRepNotifyInt2); + + UFUNCTION() + void OnRep_TestArray(TArray OldTestArray); +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestShutdownPreparation/SpatialTestShutdownPreparationTrigger.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestShutdownPreparation/SpatialTestShutdownPreparationTrigger.cpp new file mode 100644 index 0000000000..6bf04eb630 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestShutdownPreparation/SpatialTestShutdownPreparationTrigger.cpp @@ -0,0 +1,163 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialTestShutdownPreparationTrigger.h" +#include "EngineClasses/SpatialNetDriver.h" +#include "SpatialConstants.h" +#include "TestPrepareShutdownListener.h" + +ASpatialTestShutdownPreparationTrigger::ASpatialTestShutdownPreparationTrigger() +{ + Author = "Tilman Schmidt"; + Description = TEXT("Trigger shutdown preparation via worker flags and make sure callbacks get called in C++ and Blueprints"); + TriggerEventWaitTime = 5.0f; + StepTimer = 0.0f; + LocalListener = nullptr; + + // set up the request to set the worker flag in the standard runtime. + // Being lazy here and constructing one request on all workers, even though it will only be used on server worker 1 + LocalShutdownRequest = FHttpModule::Get().CreateRequest(); + LocalShutdownRequest->SetVerb(TEXT("PUT")); + LocalShutdownRequest->SetURL(TEXT("http://localhost:5006/worker_flag/workers/UnrealWorker/flags/") + + SpatialConstants::SHUTDOWN_PREPARATION_WORKER_FLAG); + LocalShutdownRequest->SetContentAsString(TEXT("")); +} + +void ASpatialTestShutdownPreparationTrigger::PrepareTest() +{ + Super::PrepareTest(); + { // Step 1 - Test print on all workers + AddStep(TEXT("AllWorkers_SetupListener"), FWorkerDefinition::AllWorkers, nullptr, [this]() { + UWorld* World = GetWorld(); + + // Spawn a non-replicated actor that will listen for the shutdown event. + // Using a non-replicated actor since this is the easiest way to make sure that every worker has exactly one instance of it. + LocalListener = + World->SpawnActor(PrepareShutdownListenerClass, FVector::ZeroVector, FRotator::ZeroRotator); + AssertTrue(IsValid(LocalListener), TEXT("Listener actor is valid.")); + RegisterAutoDestroyActor(LocalListener); + + LocalListener->RegisterCallback(); + if (LocalListener->NativePrepareShutdownEventCount != 0 || LocalListener->BlueprintPrepareShutdownEventCount != 0) + { + UE_LOG(LogTemp, Log, TEXT("Failing test due to event counts starting out wrong. native: %d, blueprint: %d"), + LocalListener->NativePrepareShutdownEventCount, LocalListener->BlueprintPrepareShutdownEventCount); + FinishTest(EFunctionalTestResult::Failed, TEXT("Number of triggered events should start out at 0")); + return; + } + + FinishStep(); + }); + + AddStep( + TEXT("Server1_TriggerShutdownPreparation1"), FWorkerDefinition::Server(1), nullptr, + [this]() { + LocalShutdownRequest->SetContentAsString( + TEXT("ValueA")); // The value doesn't matter. It's just here to set something that we can change later, so we're sure we + // trigger another notification from spatial + if (!LocalShutdownRequest->ProcessRequest()) + { + FinishTest(EFunctionalTestResult::Failed, TEXT("Failed to start the request to set the worker flag.")); + return; + } + }, + [this](float DeltaTime) { + switch (LocalShutdownRequest->GetStatus()) + { + case EHttpRequestStatus::Processing: + break; + case EHttpRequestStatus::Succeeded: + FinishStep(); + break; + default: + FinishTest(EFunctionalTestResult::Failed, TEXT("Request to set the worker flag failed or was never started.")); + break; + } + }); + + AddStep(TEXT("AllServers_CheckEventHasTriggered"), FWorkerDefinition::AllServers, nullptr, nullptr, [this](float DeltaTime) { + // On servers, we expect the event to have been triggered + if (LocalListener->NativePrepareShutdownEventCount == 1 && LocalListener->BlueprintPrepareShutdownEventCount == 1) + { + FinishStep(); + return; + } + // If the count is 0, we might not have received the event yet. We will keep checking by ticking this function. + }); + + AddStep(TEXT("AllClients_CheckEventHasNotTriggered"), FWorkerDefinition::AllClients, nullptr, nullptr, [this](float DeltaTime) { + // On clients, the event should not be triggered + if (LocalListener->NativePrepareShutdownEventCount != 0 || LocalListener->BlueprintPrepareShutdownEventCount != 0) + { + FinishTest(EFunctionalTestResult::Failed, TEXT("The prepare shutdown event was received on a client")); + return; + } + + // The callback may take some time to be called on workers after being triggered. So we should wait a while before claiming that + // it hasn't been called on a client. + StepTimer += DeltaTime; + if (StepTimer > TriggerEventWaitTime) + { + StepTimer = 0.0f; + FinishStep(); + } + }); + + AddStep( + TEXT("Server1_TriggerShutdownPreparation2"), FWorkerDefinition::Server(1), nullptr, + [this]() { + LocalShutdownRequest->SetContentAsString(TEXT("ValueB")); // again, the value doesn't matter + if (!LocalShutdownRequest->ProcessRequest()) + { + FinishTest(EFunctionalTestResult::Failed, TEXT("Failed to start the request to set the worker flag.")); + return; + } + }, + [this](float DeltaTime) { + switch (LocalShutdownRequest->GetStatus()) + { + case EHttpRequestStatus::Processing: + break; + case EHttpRequestStatus::Succeeded: + FinishStep(); + break; + default: + FinishTest(EFunctionalTestResult::Failed, TEXT("Request to set the worker flag failed or was never started.")); + break; + } + }); + + AddStep(TEXT("AllServers_CheckEventHasTriggeredOnce"), FWorkerDefinition::AllServers, nullptr, nullptr, [this](float DeltaTime) { + if (LocalListener->NativePrepareShutdownEventCount != 1 || LocalListener->BlueprintPrepareShutdownEventCount != 1) + { + FinishTest(EFunctionalTestResult::Failed, TEXT("The prepare shutdown event has been received more than once.")); + return; + } + + StepTimer += DeltaTime; + if (StepTimer > TriggerEventWaitTime) + { + StepTimer = 0.0f; + FinishStep(); + } + }); + + AddStep(TEXT("AllClients_CheckEventStillHasNotTriggered"), FWorkerDefinition::AllClients, nullptr, nullptr, + [this](float DeltaTime) { + // On clients, the event should not be triggered + if (LocalListener->NativePrepareShutdownEventCount != 0 || LocalListener->BlueprintPrepareShutdownEventCount != 0) + { + FinishTest(EFunctionalTestResult::Failed, TEXT("The prepare shutdown event was received on a client")); + return; + } + + // The callback may take some time to be called on workers after being triggered. So we should wait a while before + // claiming that it hasn't been called on a client. + StepTimer += DeltaTime; + if (StepTimer > TriggerEventWaitTime) + { + StepTimer = 0.0f; + FinishStep(); + } + }); + } +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestShutdownPreparation/SpatialTestShutdownPreparationTrigger.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestShutdownPreparation/SpatialTestShutdownPreparationTrigger.h new file mode 100644 index 0000000000..f9d574ac21 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestShutdownPreparation/SpatialTestShutdownPreparationTrigger.h @@ -0,0 +1,29 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "Http.h" +#include "SpatialFunctionalTest.h" +#include "TestPrepareShutdownListener.h" +#include "SpatialTestShutdownPreparationTrigger.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialTestShutdownPreparationTrigger : public ASpatialFunctionalTest +{ + GENERATED_BODY() + + ASpatialTestShutdownPreparationTrigger(); + + virtual void PrepareTest() override; + + UPROPERTY(EditInstanceOnly, Category = "Test Settings") + TSubclassOf PrepareShutdownListenerClass; + + UPROPERTY(EditInstanceOnly, Category = "Test Settings") + float TriggerEventWaitTime; + + float StepTimer; + ATestPrepareShutdownListener* LocalListener; + FHttpRequestPtr LocalShutdownRequest; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestShutdownPreparation/TestPrepareShutdownListener.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestShutdownPreparation/TestPrepareShutdownListener.cpp new file mode 100644 index 0000000000..8d124ea486 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestShutdownPreparation/TestPrepareShutdownListener.cpp @@ -0,0 +1,27 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "TestPrepareShutdownListener.h" +#include "EngineClasses/SpatialGameInstance.h" + +ATestPrepareShutdownListener::ATestPrepareShutdownListener() + : NativePrepareShutdownEventCount(0) + , BlueprintPrepareShutdownEventCount(0) +{ +} + +bool ATestPrepareShutdownListener::RegisterCallback_Implementation() +{ + USpatialGameInstance* GameInstance = GetGameInstance(); + if (GameInstance == nullptr) + { + return false; + } + + GameInstance->OnPrepareShutdown.AddDynamic(this, &ATestPrepareShutdownListener::OnPrepareShutdownNative); + return true; +} + +void ATestPrepareShutdownListener::OnPrepareShutdownNative() +{ + NativePrepareShutdownEventCount++; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestShutdownPreparation/TestPrepareShutdownListener.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestShutdownPreparation/TestPrepareShutdownListener.h new file mode 100644 index 0000000000..1e8a1378d2 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestShutdownPreparation/TestPrepareShutdownListener.h @@ -0,0 +1,32 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "TestPrepareShutdownListener.generated.h" + +/** + * Non-replicated actor that listens for the shutdown preparation trigger in C++ and reacts to it by incrementing the native event count. + * Intended to be subclassed as a blueprint actor that listens to the event from blueprint logic as well, and increments the blueprint event + * count. + */ +UCLASS() +class ATestPrepareShutdownListener : public AActor +{ + GENERATED_BODY() + +public: + ATestPrepareShutdownListener(); + + UFUNCTION(BlueprintNativeEvent) + bool RegisterCallback(); + + UFUNCTION() + void OnPrepareShutdownNative(); + + int NativePrepareShutdownEventCount; + + UPROPERTY(BlueprintReadWrite, Category = "Test Values") + int BlueprintPrepareShutdownEventCount; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/SpatialTestSingleServerDynamicComponents.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/SpatialTestSingleServerDynamicComponents.cpp new file mode 100644 index 0000000000..4f052dec1e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/SpatialTestSingleServerDynamicComponents.cpp @@ -0,0 +1,198 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialTestSingleServerDynamicComponents.h" +#include "TestDynamicComponent.h" +#include "TestDynamicComponentActor.h" + +#include "Kismet/GameplayStatics.h" +#include "Net/UnrealNetwork.h" + +/** + * This test tests dynamic component creation, attachment, removal and replication of properties in a single-server context. + * + * The test includes 1 Server and 2 Clients. + * The flow is as follows: + * - Setup: + * - The TestActor is spawned and a dynamic component is immediately created and attached to it. + * - The TestActor by itself attaches another dynamic component as part of ATestDynamicComponentActor::PostInitializeComponents. + * - After one second, the Server creates and attaches one more dynamic component to the TestActor. + * - All the components have a replicated array that contains references to the TestActor and to the test itself. + * - Test: + * - The Clients check that the dynamic components exist and that the replicated references are correct. + * - The Server removes the dynamic components from the TestActor. + * - The Clients check that the components were properly removed. + * - The Server creates and attaches 2 more dynamic components to the TestActor. + * - The Clients check that the newly attached components exist and they correctly replicate the references. + * - Clean-up: + * - The TestActor is destroyed. + */ +ASpatialTestSingleServerDynamicComponents::ASpatialTestSingleServerDynamicComponents() + : Super() +{ + Author = "Miron + Andrei"; + Description = TEXT("Test Dynamic Component Replication in a Single Server Context"); +} + +void ASpatialTestSingleServerDynamicComponents::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ASpatialTestSingleServerDynamicComponents, TestActor); +} + +void ASpatialTestSingleServerDynamicComponents::PrepareTest() +{ + Super::PrepareTest(); + + // The Server spawns the TestActor and immediately after it creates and attaches the OnSpawnComponent. + AddStep(TEXT("SpatialTestSingleServerDynamicComponentsServerSpawnTestActor"), FWorkerDefinition::Server(1), nullptr, [this]() { + TestActor = GetWorld()->SpawnActor(ActorSpawnPosition, FRotator::ZeroRotator, FActorSpawnParameters()); + TestActor->OnSpawnComponent = CreateAndAttachTestDynamicComponentToActor(TestActor, TEXT("OnSpawnDynamicComponent1")); + + FinishStep(); + }); + + // After a second, the Server sets the references of the PostInitializeComponent and creates and attaches the LateAddedComponent. + AddStep( + TEXT("SpatialTestSingleServerDynamicComponentsServerAddDynamicComponentsAndReferences"), FWorkerDefinition::Server(1), + [this]() -> bool { + return GetWorld()->GetTimeSeconds() - TestActor->CreationTime >= 1.0f; + }, + [this]() { + // Make sure the PostInitializeComponent was created and it does not have any reference at this stage. + if (TestActor->PostInitializeComponent == nullptr || TestActor->PostInitializeComponent->ReferencesArray.Num() != 0) + { + FinishTest(EFunctionalTestResult::Failed, TEXT("The PostInitializedComponent was not created correctly!")); + return; + } + + // Set the references for the PostInitializeComponent which is created from + // ATestDynamicComponentActor::PostInitializeComponents. + TestActor->PostInitializeComponent->ReferencesArray.Add(TestActor); + TestActor->PostInitializeComponent->ReferencesArray.Add(this); + + // Create and attach the LateAddedComponent. + TestActor->LateAddedComponent = CreateAndAttachTestDynamicComponentToActor(TestActor, TEXT("LateAddedDynamicComponent1")); + + FinishStep(); + }); + + // The Clients check if they have correctly received the TestActor, its components and the references array of the components. + AddStep( + TEXT("SpatialTestSingleServerDynamicComponentsClientCheck"), FWorkerDefinition::AllClients, + [this]() -> bool { + // Make sure we have the received the TestActor an its replicated components before checking their references. + return TestActor != nullptr && TestActor->OnSpawnComponent != nullptr && TestActor->PostInitializeComponent != nullptr + && TestActor->LateAddedComponent != nullptr; + }, + [this]() { + // At this point the Actor and its replicated components were received, therefore the references can be checked. + + // Check the references for the OnSpawnComponent + AssertTrue(TestActor->OnSpawnComponent->ReferencesArray[0] == TestActor, + TEXT("Reference from the on-spawn dynamic component to its parent works.")); + AssertTrue(TestActor->OnSpawnComponent->ReferencesArray[1] == this, + TEXT("Reference from the on-spawn dynamic component to the test works.")); + + // Check the references for the PostInitializeComponent + AssertTrue(TestActor->PostInitializeComponent->ReferencesArray[0] == TestActor, + TEXT("Reference from the post-init dynamic component to its parent works.")); + AssertTrue(TestActor->PostInitializeComponent->ReferencesArray[1] == this, + TEXT("Reference from the post-init dynamic component to the test works.")); + + // Check the references for the LateAddedComponent + AssertTrue(TestActor->LateAddedComponent->ReferencesArray[0] == TestActor, + TEXT("Reference from the late-created dynamic component to its parent works.")); + AssertTrue(TestActor->LateAddedComponent->ReferencesArray[1] == this, + TEXT("Reference from the late-created dynamic component to the test works.")); + + FinishStep(); + }, + nullptr, 5.0f); + + // The Server destroys all the components of the TestActor. + AddStep(TEXT("SpatialTestSingleServerDynamicComponentsServerRemoveDynamiComponents"), FWorkerDefinition::Server(1), nullptr, [this]() { + TestActor->OnSpawnComponent->DestroyComponent(); + TestActor->PostInitializeComponent->DestroyComponent(); + TestActor->LateAddedComponent->DestroyComponent(); + TestActor->OnSpawnComponent = nullptr; + TestActor->PostInitializeComponent = nullptr; + TestActor->LateAddedComponent = nullptr; + + FinishStep(); + }); + + // The Clients check if the components were correctly destroyed. + AddStep( + TEXT("SpatialTestSingleServerDynamicComponentsClientCheckDynamicComponentsRemoved"), FWorkerDefinition::AllClients, nullptr, + nullptr, + [this](float DeltaTime) { + if (TestActor->GetComponents().Num() == 0) + { + if (TestActor->OnSpawnComponent == nullptr && TestActor->PostInitializeComponent == nullptr + && TestActor->LateAddedComponent == nullptr) + { + FinishStep(); + } + } + }, + 5.0f); + + // The Server creates two 2 components and adds them to the TestActor, using the existing replicated properties. + AddStep(TEXT("SpatialTestSingleServerDynamicComponentsServerReCreateComponents"), FWorkerDefinition::Server(1), nullptr, [this]() { + TestActor->OnSpawnComponent = CreateAndAttachTestDynamicComponentToActor(TestActor, TEXT("OnSpawnDynamicComponent2")); + TestActor->OnSpawnComponent->ReferencesArray.SetNum(4); + TestActor->OnSpawnComponent->ReferencesArray[2] = this; + TestActor->OnSpawnComponent->ReferencesArray[3] = TestActor; + + TestActor->LateAddedComponent = CreateAndAttachTestDynamicComponentToActor(TestActor, TEXT("LateAddedDynamicComponent2")); + TestActor->LateAddedComponent->ReferencesArray.SetNum(4); + TestActor->LateAddedComponent->ReferencesArray[2] = TestActor; + TestActor->LateAddedComponent->ReferencesArray[3] = this; + + FinishStep(); + }); + + // The Clients check that the components were correctly replicated. + AddStep( + TEXT("SpatialTestSingleServerDynamicComponentsClientCheckDynamicComponentsReCreated"), FWorkerDefinition::AllClients, + [this]() -> bool { + return TestActor != nullptr && TestActor->OnSpawnComponent != nullptr && TestActor->PostInitializeComponent == nullptr + && TestActor->LateAddedComponent != nullptr; + }, + [this]() { + AssertTrue(TestActor->OnSpawnComponent->ReferencesArray[2] == this, + TEXT("Reference from the on-spawn dynamic component to the test works after swapping.")); + AssertTrue(TestActor->OnSpawnComponent->ReferencesArray[3] == TestActor, + TEXT("Reference from the on-spawn dynamic component to its parent works after swapping.")); + + AssertTrue(TestActor->LateAddedComponent->ReferencesArray[2] == TestActor, + TEXT("Reference from the late-created dynamic component to its parent works.")); + AssertTrue(TestActor->LateAddedComponent->ReferencesArray[3] == this, + TEXT("Reference from the late-created dynamic component to the test works.")); + + FinishStep(); + }, + nullptr, 5.0f); + + // Since calling RegisterAutoDestroy adds a component to the Actor, the clean-up is done manually. + AddStep(TEXT("SpatialTestSingleServerDynamicComponentsServerCleanup"), FWorkerDefinition::Server(1), nullptr, [this]() { + RegisterAutoDestroyActor(TestActor); + + FinishStep(); + }); +} + +// Helper function that creates and attaches a UTestDynamicComponent to the TestActor and also sets the component's references accordingly. +UTestDynamicComponent* ASpatialTestSingleServerDynamicComponents::CreateAndAttachTestDynamicComponentToActor(AActor* Actor, FName Name) +{ + UTestDynamicComponent* NewDynamicComponent = + NewObject(Actor, UTestDynamicComponent::StaticClass(), Name, RF_Transient); + + NewDynamicComponent->SetupAttachment(Actor->GetRootComponent()); + NewDynamicComponent->RegisterComponent(); + NewDynamicComponent->ReferencesArray.Add(Actor); + NewDynamicComponent->ReferencesArray.Add(this); + + return NewDynamicComponent; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/SpatialTestSingleServerDynamicComponents.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/SpatialTestSingleServerDynamicComponents.h new file mode 100644 index 0000000000..c747f3f0cb --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/SpatialTestSingleServerDynamicComponents.h @@ -0,0 +1,30 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialFunctionalTest.h" +#include "SpatialTestSingleServerDynamicComponents.generated.h" + +class ATestDynamicComponentActor; +class UTestDynamicComponent; + +UCLASS() +class ASpatialTestSingleServerDynamicComponents : public ASpatialFunctionalTest +{ + GENERATED_BODY() + +public: + ASpatialTestSingleServerDynamicComponents(); + + virtual void PrepareTest() override; + + void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + UPROPERTY(Replicated) + ATestDynamicComponentActor* TestActor; + + UTestDynamicComponent* CreateAndAttachTestDynamicComponentToActor(AActor* Actor, FName Name); + + const FVector ActorSpawnPosition = FVector(0.0f, 0.0f, 50.0f); +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/TestDynamicComponent.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/TestDynamicComponent.cpp new file mode 100644 index 0000000000..c0513d36f3 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/TestDynamicComponent.cpp @@ -0,0 +1,18 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "TestDynamicComponent.h" + +#include "Net/UnrealNetwork.h" + +UTestDynamicComponent::UTestDynamicComponent() +{ + PrimaryComponentTick.bCanEverTick = true; + SetIsReplicatedByDefault(true); +} + +void UTestDynamicComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(UTestDynamicComponent, ReferencesArray); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/TestDynamicComponent.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/TestDynamicComponent.h new file mode 100644 index 0000000000..5a928a52c8 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/TestDynamicComponent.h @@ -0,0 +1,24 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Components/SceneComponent.h" +#include "CoreMinimal.h" +#include "TestDynamicComponent.generated.h" + +/** + * Simple replicated component with a replicated array of references, used in SpatialTestSingleServerDynamicComponents + */ +UCLASS() +class UTestDynamicComponent : public USceneComponent +{ + GENERATED_BODY() + +public: + UTestDynamicComponent(); + + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + UPROPERTY(Replicated) + TArray ReferencesArray; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/TestDynamicComponentActor.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/TestDynamicComponentActor.cpp new file mode 100644 index 0000000000..4cc6ded014 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/TestDynamicComponentActor.cpp @@ -0,0 +1,33 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "TestDynamicComponentActor.h" +#include "TestDynamicComponent.h" + +#include "Net/UnrealNetwork.h" + +ATestDynamicComponentActor::ATestDynamicComponentActor() +{ + bReplicates = true; + bAlwaysRelevant = true; +} + +void ATestDynamicComponentActor::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ATestDynamicComponentActor, OnSpawnComponent); + DOREPLIFETIME(ATestDynamicComponentActor, PostInitializeComponent); + DOREPLIFETIME(ATestDynamicComponentActor, LateAddedComponent); +} + +void ATestDynamicComponentActor::PostInitializeComponents() +{ + Super::PostInitializeComponents(); + + if (HasAuthority() && PostInitializeComponent == nullptr) + { + PostInitializeComponent = NewObject(this, TEXT("PostInitializeDynamicComponent")); + PostInitializeComponent->SetupAttachment(GetRootComponent()); + PostInitializeComponent->RegisterComponent(); + } +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/TestDynamicComponentActor.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/TestDynamicComponentActor.h new file mode 100644 index 0000000000..d0f759b19a --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestSingleServerDynamicComponents/TestDynamicComponentActor.h @@ -0,0 +1,39 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "TestDynamicComponentActor.generated.h" + +class UTestDynamicComponent; + +/** + * A replicated, always relevant Actor used in SpatialTestSingleServerDynamicComponents to test dynamic components. + * Contains 3 replicated dynamic components attached at various points in its lifecycle. + */ + +UCLASS() +class ATestDynamicComponentActor : public AActor +{ + GENERATED_BODY() + +public: + ATestDynamicComponentActor(); + + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + // In the test, this Component is created and attached after spawning the ATestDynamicComponentActor. + UPROPERTY(Replicated) + UTestDynamicComponent* OnSpawnComponent; + + // In the test, this Component is created and attached as part of PostInitializeComponents. + UPROPERTY(Replicated) + UTestDynamicComponent* PostInitializeComponent; + + // In the test, this Component is created and attached one second after spawning the ATestDynamicComponentActor. + UPROPERTY(Replicated) + UTestDynamicComponent* LateAddedComponent; + + virtual void PostInitializeComponents() override; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestTearOff/ReplicatedTearOffActor.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestTearOff/ReplicatedTearOffActor.cpp new file mode 100644 index 0000000000..c420e4c4c4 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestTearOff/ReplicatedTearOffActor.cpp @@ -0,0 +1,26 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "ReplicatedTearOffActor.h" +#include "Net/UnrealNetwork.h" + +AReplicatedTearOffActor::AReplicatedTearOffActor() +{ + bNetLoadOnClient = true; + SetReplicatingMovement(true); + TestInteger = 0; +} + +void AReplicatedTearOffActor::BeginPlay() +{ + Super::BeginPlay(); + + // Note: calling TearOff inside the constructor will not prevent the Actor from replicating. + TearOff(); +} + +void AReplicatedTearOffActor::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(AReplicatedTearOffActor, TestInteger); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestTearOff/ReplicatedTearOffActor.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestTearOff/ReplicatedTearOffActor.h new file mode 100644 index 0000000000..7c73fe1063 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestTearOff/ReplicatedTearOffActor.h @@ -0,0 +1,28 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h" +#include "ReplicatedTearOffActor.generated.h" + +class ASpatialTestTearOff; + +/** + * A replicated Actor that calls TearOff on BeginPlay(). Used in the SpatialTearoffMap and inside the SpatialTestTearOff class. + */ +UCLASS() +class AReplicatedTearOffActor : public AReplicatedTestActorBase +{ + GENERATED_BODY() + +public: + AReplicatedTearOffActor(); + + void BeginPlay() override; + + void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + UPROPERTY(Replicated) + int TestInteger; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestTearOff/SpatialTestTearOff.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestTearOff/SpatialTestTearOff.cpp new file mode 100644 index 0000000000..b8aa5dfdb3 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestTearOff/SpatialTestTearOff.cpp @@ -0,0 +1,224 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialTestTearOff.h" + +#include "ReplicatedTearOffActor.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h" + +#include "Kismet/GameplayStatics.h" + +/** + * This test automates the QA flow that checks if Actors can be prevented from replicating with bTearOff. + * NOTE: Due to adding wait steps before checking certain parts of the test, this test runs slow, in roughly 30 seconds. This can be + * improved by modifying the WorkerWaitForTimeStepDefinition to wait for a smaller amount of time. + * + * The test includes 1 Server and 2 Client workers. + * The flow is as follows: + * - Setup: + * - A ReplicatedTearOffActor must be placed in the map that is running the test, as a start-up actor. + * - All workers set a reference to the start-up actor. + * - Test: + * - The server modifies a replicated property of the start-up Actor. + * - The clients check that the modification was ignored. + * - The server spawns a ReplicatedTearOffActor. + * - The clients check that the spawned ReplicatedTearOffActor was not replicated at all. + * - The server spawns a ReplicatedTestActorBase, which is not torn off by deafult. + * - The clients check that the spawned ReplicatedTestActorBase is correctly replicated. + * - The server calls TearOff on the ReplicatedTestActorBase and changes its location. + * - The clients check that the location update is ignored. + * - Clean-up: + * - The 2 spawned Actors are deleted. + */ + +ASpatialTestTearOff::ASpatialTestTearOff() + : Super() +{ + Author = "Andrei"; + Description = TEXT("Test TearOff prevents Actors from replicating"); + + ReplicatedTestActorBaseInitialLocation = FVector(150.0f, 150.0f, 80.0f); + ReplicatedTestActorBaseMoveLocationBeforeTearOff = FVector(-150.0f, -150.0f, 80.0f); + ReplicatedTestActorBaseMoveLocationAfterTearOff = FVector(30.0f, -20.0f, 80.0f); +} + +void ASpatialTestTearOff::PrepareTest() +{ + Super::PrepareTest(); + + // Step definition for a 5 second timer + FSpatialFunctionalTestStepDefinition WorkerWaitForTimeStepDefinition; + WorkerWaitForTimeStepDefinition.StepName = TEXT("SpatialTestTearOffUniversalWorkerWaitForTime"); + WorkerWaitForTimeStepDefinition.bIsNativeDefinition = true; + WorkerWaitForTimeStepDefinition.TimeLimit = 15.0f; + WorkerWaitForTimeStepDefinition.NativeStartEvent.BindLambda([this]() { + TimerHelper = 0.0f; + }); + WorkerWaitForTimeStepDefinition.NativeTickEvent.BindLambda([this](float DeltaTime) { + TimerHelper += DeltaTime; + if (TimerHelper > 5.0f) + { + FinishStep(); + } + }); + + // All workers set a reference to the ReplicatedTearOffActor placed in the map. + AddStep(TEXT("SpatialTestTearOffUniversalStartupActorReferenceSetup"), FWorkerDefinition::AllWorkers, nullptr, [this]() { + TArray ReplicatedTearOffActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTearOffActor::StaticClass(), ReplicatedTearOffActors); + + checkf(ReplicatedTearOffActors.Num() == 1, TEXT("Exactly one ReplicatedTearOffActor is expected at this stage")); + + StartupTearOffActor = Cast(ReplicatedTearOffActors[0]); + if (IsValid(StartupTearOffActor)) + { + FinishStep(); + } + }); + + // This is required to make the test pass in Native since calling TearOff does not immediately stop the Actor from replicating. + AddStepFromDefinition(WorkerWaitForTimeStepDefinition, FWorkerDefinition::AllWorkers); + + // The server changes the value of the replicated property of the ReplicatedTearOffActor that was initially placed in the map. + AddStep(TEXT("SpatialTestTearOffServerChangeStartupActorDefaultProperty"), FWorkerDefinition::Server(1), nullptr, [this]() { + if (StartupTearOffActor) + { + StartupTearOffActor->TestInteger = 1; + FinishStep(); + } + }); + + // Allow for potential replication to propagate before checking that the property update was ignored. + AddStepFromDefinition(WorkerWaitForTimeStepDefinition, FWorkerDefinition::AllWorkers); + + // Clients check that the server update to the replicated property was ignored, as expected. + AddStep(TEXT("SpatialTestTearOffClientsCheckStartupActorPropertyValue"), FWorkerDefinition::AllClients, nullptr, [this]() { + if (StartupTearOffActor && StartupTearOffActor->TestInteger == 0) + { + FinishStep(); + } + }); + + // The server dynamically spawns a ReplicatedTearOffActor. + AddStep(TEXT("SpatialTestTearOffServerSpawnReplicatedTearOffActor"), FWorkerDefinition::Server(1), nullptr, [this]() { + AReplicatedTearOffActor* SpawnedTearOffActor = + GetWorld()->SpawnActor(FVector(50.0f, 50.0f, 80.0f), FRotator::ZeroRotator, FActorSpawnParameters()); + RegisterAutoDestroyActor(SpawnedTearOffActor); + + // Make sure the Actor was correctly spawned + if (IsValid(SpawnedTearOffActor)) + { + FinishStep(); + } + }); + + // Allow for potential replication to propagate before checking that the SpawnedTearOffActor was not received. + AddStepFromDefinition(WorkerWaitForTimeStepDefinition, FWorkerDefinition::AllWorkers); + + // Clients check that they have not received the newly spawned Actor + AddStep(TEXT("SpatialTestTearOffUniversalSpawnedActorReferenceSetup"), FWorkerDefinition::AllClients, nullptr, [this]() { + TArray ReplicatedTearOffActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTearOffActor::StaticClass(), ReplicatedTearOffActors); + + // Only the StartupTearOffActor is expected to exist in the world at this point. + if (ReplicatedTearOffActors.Num() == 1) + { + FinishStep(); + } + }); + + // Spawn a replicated Actor that does not call TearOff on BeginPlay(). + AddStep(TEXT("SpatialTestTearOffServerSpawnReplicatedActor"), FWorkerDefinition::Server(1), nullptr, [this]() { + SpawnedReplicatedActorBase = GetWorld()->SpawnActor(ReplicatedTestActorBaseInitialLocation, + FRotator::ZeroRotator, FActorSpawnParameters()); + RegisterAutoDestroyActor(SpawnedReplicatedActorBase); + FinishStep(); + }); + + // Make sure the spawned actor is correctly replicated and set a reference to it. + AddStep( + TEXT("SpatialTestTearOffClientsSpawnedActorReferenceSetup"), FWorkerDefinition::AllClients, nullptr, nullptr, + [this](float DeltaTime) { + TArray ReplicatedActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase::StaticClass(), ReplicatedActors); + + // Since ReplicatedTearOffActor inherits from AReplicatedTestActorBase, 2 AReplicatedTestActorBase are expected at this point: + // The start-up ReplicatedTearOfActor, and the newly spawned AReplicatedTestActorBase object. + if (ReplicatedActors.Num() == 2) + { + // Ensure the reference to the correct Actor is set. + if (ReplicatedActors[0]->IsA(AReplicatedTearOffActor::StaticClass())) + { + SpawnedReplicatedActorBase = Cast(ReplicatedActors[1]); + } + else + { + SpawnedReplicatedActorBase = Cast(ReplicatedActors[0]); + } + + if (IsValid(SpawnedReplicatedActorBase) + && SpawnedReplicatedActorBase->GetActorLocation().Equals(ReplicatedTestActorBaseInitialLocation, 1.0f)) + { + FinishStep(); + } + } + }, + 5.0f); + + // Move the spawned actor to make sure its movement is being replicated correctly. + AddStep(TEXT("SpatialTestTearOffSeverMoveSpawnedActor"), FWorkerDefinition::Server(1), nullptr, [this]() { + SpawnedReplicatedActorBase->SetActorLocation(ReplicatedTestActorBaseMoveLocationBeforeTearOff); + + FinishStep(); + }); + + // Clients check that the spawned actor correctly replicates movement before calling TearOff. + AddStep( + TEXT("SpatialTestTearOffClientsCheckMovement"), FWorkerDefinition::AllClients, nullptr, nullptr, + [this](float DeltaTime) { + if (SpawnedReplicatedActorBase->GetActorLocation().Equals(ReplicatedTestActorBaseMoveLocationBeforeTearOff, 1.0f)) + { + FinishStep(); + } + }, + 5.0f); + + // Tear off the spawned replicated ReplicatedTestActorBase. + AddStep(TEXT("SpatialTestTearOffServerSetTearOffAndMoveActor"), FWorkerDefinition::Server(1), nullptr, [this]() { + SpawnedReplicatedActorBase->TearOff(); + + FinishStep(); + }); + + // Wait for the TearOff to propagate. + AddStep( + TEXT("SpatialTestTearOffAllWorkersCheckTearOff"), FWorkerDefinition::AllWorkers, nullptr, nullptr, + [this](float DeltaTime) { + if (SpawnedReplicatedActorBase->GetTearOff()) + { + FinishStep(); + } + }, + 5.0f); + + // Now that the Actor is TornOff, move it from the server. + AddStep(TEXT("SpatialTestTearOfFServerMoveTornOffActor"), FWorkerDefinition::Server(1), nullptr, [this]() { + if (SpawnedReplicatedActorBase->SetActorLocation(ReplicatedTestActorBaseMoveLocationAfterTearOff)) + { + FinishStep(); + } + }); + + // Allow for potential replication to propagate before checking that the location update for SpawnedReplicatedActorBase was ignored. + AddStepFromDefinition(WorkerWaitForTimeStepDefinition, FWorkerDefinition::AllWorkers); + + // Clients check that the Location update was ignored. + AddStep( + TEXT("SpatialTestTearOffClientsLocationUpdateWasIgnored"), FWorkerDefinition::AllClients, nullptr, + [this]() { + if (SpawnedReplicatedActorBase->GetActorLocation().Equals(ReplicatedTestActorBaseMoveLocationBeforeTearOff, 1.0f)) + { + FinishStep(); + } + }, + nullptr, 2.0f); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestTearOff/SpatialTestTearOff.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestTearOff/SpatialTestTearOff.h new file mode 100644 index 0000000000..3cc72d96c4 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestTearOff/SpatialTestTearOff.h @@ -0,0 +1,40 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialFunctionalTest.h" +#include "SpatialTestTearOff.generated.h" + +class AReplicatedTearOffActor; +class AReplicatedTestActorBase; + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialTestTearOff : public ASpatialFunctionalTest +{ + GENERATED_BODY() + +public: + ASpatialTestTearOff(); + + virtual void PrepareTest() override; + + // Variable used to reference the ReplicatedTearOffActor placed in the map. + AReplicatedTearOffActor* StartupTearOffActor; + + // Variable used to reference the spawned AReplicatedTestActorBase. + UPROPERTY() + AReplicatedTestActorBase* SpawnedReplicatedActorBase; + + // The location where the ReplicatedActorBase is spawned. + FVector ReplicatedTestActorBaseInitialLocation; + + // The location where the ReplicatedActorBase is moved before calling TearOff. + FVector ReplicatedTestActorBaseMoveLocationBeforeTearOff; + + // THe location where the ReplicatedActorBase is moved after calling TearOff. + FVector ReplicatedTestActorBaseMoveLocationAfterTearOff; + + // Helper variable used for implementing the WorkerWaitForTimeStepDefinition. + float TimerHelper; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/InitiallyDormantTestActor.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/InitiallyDormantTestActor.cpp new file mode 100644 index 0000000000..74767e6f83 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/InitiallyDormantTestActor.cpp @@ -0,0 +1,8 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "InitiallyDormantTestActor.h" + +AInitiallyDormantTestActor::AInitiallyDormantTestActor() +{ + NetDormancy = ENetDormancy::DORM_Initial; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/InitiallyDormantTestActor.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/InitiallyDormantTestActor.h new file mode 100644 index 0000000000..9206ffb174 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/InitiallyDormantTestActor.h @@ -0,0 +1,19 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "ReplicatedTestActorBase.h" +#include "InitiallyDormantTestActor.generated.h" + +/** + * An initially dormant, replicated Actor. + */ +UCLASS() +class AInitiallyDormantTestActor : public AReplicatedTestActorBase +{ + GENERATED_BODY() + +public: + AInitiallyDormantTestActor(); +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/RelevancyTestActors.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/RelevancyTestActors.cpp new file mode 100644 index 0000000000..48aff5b8a0 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/RelevancyTestActors.cpp @@ -0,0 +1,15 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "RelevancyTestActors.h" + +AAlwaysRelevantTestActor::AAlwaysRelevantTestActor() +{ + bAlwaysRelevant = true; + NetCullDistanceSquared = 1; +} + +AAlwaysRelevantServerOnlyTestActor::AAlwaysRelevantServerOnlyTestActor() +{ + bAlwaysRelevant = true; + NetCullDistanceSquared = 1; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/RelevancyTestActors.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/RelevancyTestActors.h new file mode 100644 index 0000000000..b0ac411bca --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/RelevancyTestActors.h @@ -0,0 +1,31 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "ReplicatedTestActorBase.h" +#include "RelevancyTestActors.generated.h" + +/** + * An always relevant, replicated Actor. NCD set small to ensure AlwaysRelevant used + */ +UCLASS() +class AAlwaysRelevantTestActor : public AReplicatedTestActorBase +{ + GENERATED_BODY() + +public: + AAlwaysRelevantTestActor(); +}; + +/** + * An always relevant, server only, replicated Actor. + */ +UCLASS(SpatialType = ServerOnly) +class AAlwaysRelevantServerOnlyTestActor : public AReplicatedTestActorBase +{ + GENERATED_BODY() + +public: + AAlwaysRelevantServerOnlyTestActor(); +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.cpp index 8b0950f2a0..55bf6b1fde 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.cpp @@ -1,6 +1,5 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved - #include "ReplicatedTestActorBase.h" #include "Components/StaticMeshComponent.h" #include "Materials/Material.h" @@ -8,12 +7,13 @@ AReplicatedTestActorBase::AReplicatedTestActorBase() { bReplicates = true; + SetReplicateMovement(true); CubeComponent = CreateDefaultSubobject(TEXT("CubeComponent")); CubeComponent->SetStaticMesh(LoadObject(nullptr, TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"))); - CubeComponent->SetMaterial(0, LoadObject(nullptr, TEXT("Material'/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial'"))); + CubeComponent->SetMaterial(0, + LoadObject(nullptr, TEXT("Material'/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial'"))); CubeComponent->SetVisibility(true); RootComponent = CubeComponent; } - diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h index 069155720d..097bddc3de 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h @@ -7,7 +7,7 @@ #include "ReplicatedTestActorBase.generated.h" /** - * A replicated Actor with a Cube Mesh, used as a base for Actors used in tests. + * A replicated Actor with a Cube Mesh, used as a base for Actors used in spatial tests. */ UCLASS() class AReplicatedTestActorBase : public AActor diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMovement/TestMovementCharacter.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/TestMovementCharacter.cpp similarity index 92% rename from SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMovement/TestMovementCharacter.cpp rename to SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/TestMovementCharacter.cpp index 896837b2f4..bc879673f5 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMovement/TestMovementCharacter.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/TestMovementCharacter.cpp @@ -1,12 +1,11 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved - #include "TestMovementCharacter.h" -#include "Engine/Classes/Camera/CameraComponent.h" +#include "Components/CapsuleComponent.h" #include "Components/StaticMeshComponent.h" +#include "Engine/Classes/Camera/CameraComponent.h" #include "Materials/Material.h" #include "Net/UnrealNetwork.h" -#include "Components/CapsuleComponent.h" ATestMovementCharacter::ATestMovementCharacter() { @@ -21,7 +20,8 @@ ATestMovementCharacter::ATestMovementCharacter() SphereComponent = CreateDefaultSubobject(TEXT("SphereComponent")); SphereComponent->SetStaticMesh(LoadObject(nullptr, TEXT("StaticMesh'/Engine/BasicShapes/Sphere.Sphere'"))); - SphereComponent->SetMaterial(0, LoadObject(nullptr, TEXT("Material'/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial'"))); + SphereComponent->SetMaterial( + 0, LoadObject(nullptr, TEXT("Material'/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial'"))); SphereComponent->SetVisibility(true); SphereComponent->SetupAttachment(GetCapsuleComponent()); diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMovement/TestMovementCharacter.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/TestMovementCharacter.h similarity index 100% rename from SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMovement/TestMovementCharacter.h rename to SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/TestActors/TestMovementCharacter.h diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3066/OwnerOnlyPropertyReplication.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3066/OwnerOnlyPropertyReplication.cpp index 7f09241cba..d9401f5552 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3066/OwnerOnlyPropertyReplication.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3066/OwnerOnlyPropertyReplication.cpp @@ -1,26 +1,26 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "OwnerOnlyPropertyReplication.h" +#include "EngineUtils.h" #include "GameFramework/Controller.h" +#include "GameFramework/PlayerController.h" #include "GameFramework/PlayerState.h" -#include "EngineUtils.h" #include "Net/UnrealNetwork.h" #include "SpatialFunctionalTestFlowController.h" -#include "GameFramework/PlayerController.h" namespace { - FString AssertStep(const FSpatialFunctionalTestStepDefinition& StepDefinition, const FString& Text) - { - return FString::Printf(TEXT("[%s] %s"), *StepDefinition.StepName, *Text); - } +FString AssertStep(const FSpatialFunctionalTestStepDefinition& StepDefinition, const FString& Text) +{ + return FString::Printf(TEXT("[%s] %s"), *StepDefinition.StepName, *Text); } +} // namespace /** * This test tests replication of owner-only properties on an actor. * - * The test includes a single server and two client workers. The client workers begin with a player controller and their default pawns, which they initially possess. - * The flow is as follows: + * The test includes a single server and two client workers. The client workers begin with a player controller and their default pawns, + * which they initially possess. The flow is as follows: * - Setup: * - No setup required * - Test: @@ -41,139 +41,142 @@ AOwnerOnlyPropertyReplication::AOwnerOnlyPropertyReplication() Description = TEXT("UNR-3066 OwnerOnly replication test"); } -void AOwnerOnlyPropertyReplication::BeginPlay() +void AOwnerOnlyPropertyReplication::PrepareTest() { - Super::BeginPlay(); + Super::PrepareTest(); - { // Step 1 - Set TestIntProp to 42. - AddStep(TEXT("ServerCreateActor"), FWorkerDefinition::Server(1), nullptr, [](ASpatialFunctionalTest* NetTest) { - AOwnerOnlyPropertyReplication* Test = Cast(NetTest); - Test->Pawn = Test->GetWorld()->SpawnActor(FVector::ZeroVector, FRotator::ZeroRotator); - Test->Pawn->SetReplicates(true); - Test->Pawn->TestInt = 42; - Test->RegisterAutoDestroyActor(Test->Pawn); + { // Step 1 - Set TestIntProp to 42. + AddStep(TEXT("ServerCreateActor"), FWorkerDefinition::Server(1), nullptr, [this]() { + Pawn = GetWorld()->SpawnActor(FVector::ZeroVector, FRotator::ZeroRotator); + Pawn->SetReplicates(true); + Pawn->TestInt = 42; + RegisterAutoDestroyActor(Pawn); - Test->FinishStep(); - }); + FinishStep(); + }); } - { // Step 2 - Check on client that TestInt didn't replicate. - AddStep(TEXT("ClientNoReplicationBeforePossess"), FWorkerDefinition::AllClients, - [](ASpatialFunctionalTest* NetTest) -> bool { - AOwnerOnlyPropertyReplication* Test = Cast(NetTest); - return IsValid(Test->Pawn); - }, - [](ASpatialFunctionalTest* NetTest) { - AOwnerOnlyPropertyReplication* Test = Cast(NetTest); - const FSpatialFunctionalTestStepDefinition StepDefinition = Test->GetStepDefinition(Test->GetCurrentStepIndex()); - if (Test->Pawn) + { // Step 2 - Check on client that TestInt didn't replicate. + AddStep( + TEXT("ClientNoReplicationBeforePossess"), FWorkerDefinition::AllClients, + [this]() -> bool { + return IsValid(Pawn); + }, + [this]() { + const FSpatialFunctionalTestStepDefinition StepDefinition = GetStepDefinition(GetCurrentStepIndex()); + if (Pawn) { - Test->AssertEqual_Int(Test->Pawn->TestInt, 0, AssertStep(StepDefinition, TEXT("Pawn has default value")), Test); + AssertEqual_Int(Pawn->TestInt, 0, AssertStep(StepDefinition, TEXT("Pawn has default value")), this); } - Test->FinishStep(); + FinishStep(); }); } - { // Step 3 - Possess actor. - AddStep(TEXT("ServerPossessActor"), FWorkerDefinition::Server(1), nullptr, [](ASpatialFunctionalTest* NetTest) { - AOwnerOnlyPropertyReplication* Test = Cast(NetTest); - if (Test->Pawn) + { // Step 3 - Possess actor. + AddStep(TEXT("ServerPossessActor"), FWorkerDefinition::Server(1), nullptr, [this]() { + if (Pawn) { - ASpatialFunctionalTestFlowController* FlowController = Test->GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1); + ASpatialFunctionalTestFlowController* FlowController = GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1); APlayerController* PlayerController = Cast(FlowController->GetOwner()); - Test->OriginalPawns.Add(TPair(PlayerController, PlayerController->GetPawn())); + OriginalPawns.Add(TPair(PlayerController, PlayerController->GetPawn())); - PlayerController->Possess(Test->Pawn); + PlayerController->Possess(Pawn); } - Test->FinishStep(); - }); + FinishStep(); + }); } - { // Step 4 - Check on client that TestInt did replicate now on owning client. - AddStep(TEXT("ClientCheckReplicationAfterPossess"), FWorkerDefinition::AllClients, - [](ASpatialFunctionalTest* NetTest) -> bool { - AOwnerOnlyPropertyReplication* Test = Cast(NetTest); - return IsValid(Test->Pawn); - }, nullptr, - [](ASpatialFunctionalTest* NetTest, float DeltaTime) { - AOwnerOnlyPropertyReplication* Test = Cast(NetTest); - const FSpatialFunctionalTestStepDefinition StepDefinition = Test->GetStepDefinition(Test->GetCurrentStepIndex()); - if (Test->Pawn) + { // Step 4 - Check on client that TestInt did replicate now on owning client. + AddStep( + TEXT("ClientCheckReplicationAfterPossess"), FWorkerDefinition::AllClients, + [this]() -> bool { + return IsValid(Pawn); + }, + nullptr, + [this](float DeltaTime) { + const FSpatialFunctionalTestStepDefinition StepDefinition = GetStepDefinition(GetCurrentStepIndex()); + if (Pawn) { - ASpatialFunctionalTestFlowController* FlowController = Test->GetLocalFlowController(); + ASpatialFunctionalTestFlowController* FlowController = GetLocalFlowController(); if (FlowController->WorkerDefinition.Id == 1) { - if (Test->Pawn->GetController() == FlowController->GetOwner() && Test->Pawn->TestInt == 42) + if (Pawn->GetController() == FlowController->GetOwner() && Pawn->TestInt == 42) { - Test->AssertTrue(Test->Pawn->GetController() == FlowController->GetOwner(), AssertStep(StepDefinition, TEXT("Client is in possession of pawn"))); - Test->AssertEqual_Int(Test->Pawn->TestInt, 42, AssertStep(StepDefinition, TEXT("Pawn's TestInt was replicated to owning client")), Test); - Test->FinishStep(); + AssertTrue(Pawn->GetController() == FlowController->GetOwner(), + AssertStep(StepDefinition, TEXT("Client is in possession of pawn"))); + AssertEqual_Int(Pawn->TestInt, 42, + AssertStep(StepDefinition, TEXT("Pawn's TestInt was replicated to owning client")), this); + FinishStep(); } } else { - Test->StepTimer += DeltaTime; - if (Test->StepTimer >= 1.0f) + StepTimer += DeltaTime; + if (StepTimer >= 1.0f) { - Test->StepTimer = 0.0f; - Test->AssertTrue(Test->Pawn->GetController() != FlowController->GetOwner(), AssertStep(StepDefinition, TEXT("Client is not in possession of pawn"))); - Test->AssertEqual_Int(Test->Pawn->TestInt, 0, AssertStep(StepDefinition, TEXT("Pawn's TestInt was not replicated to non-owning client")), Test); - Test->FinishStep(); + StepTimer = 0.0f; + AssertTrue(Pawn->GetController() != FlowController->GetOwner(), + AssertStep(StepDefinition, TEXT("Client is not in possession of pawn"))); + AssertEqual_Int(Pawn->TestInt, 0, + AssertStep(StepDefinition, TEXT("Pawn's TestInt was not replicated to non-owning client")), + this); + FinishStep(); } } } }); } - { // Step 5 - Change value on server. - AddStep(TEXT("ServerChangeValue"), FWorkerDefinition::Server(1), nullptr, [](ASpatialFunctionalTest* NetTest) { - AOwnerOnlyPropertyReplication* Test = Cast(NetTest); - if (Test->Pawn) + { // Step 5 - Change value on server. + AddStep(TEXT("ServerChangeValue"), FWorkerDefinition::Server(1), nullptr, [this]() { + if (Pawn) { - Test->Pawn->TestInt = 666; + Pawn->TestInt = 666; } - Test->FinishStep(); - }); + FinishStep(); + }); } - { // Step 6 - Check that value was replicated on owning client. - AddStep(TEXT("ClientCheckReplicationAfterChange"), FWorkerDefinition::AllClients, nullptr, nullptr, - [](ASpatialFunctionalTest* NetTest, float DeltaTime) { - AOwnerOnlyPropertyReplication* Test = Cast(NetTest); - const FSpatialFunctionalTestStepDefinition StepDefinition = Test->GetStepDefinition(Test->GetCurrentStepIndex()); + { // Step 6 - Check that value was replicated on owning client. + AddStep(TEXT("ClientCheckReplicationAfterChange"), FWorkerDefinition::AllClients, nullptr, nullptr, [this](float DeltaTime) { + const FSpatialFunctionalTestStepDefinition StepDefinition = GetStepDefinition(GetCurrentStepIndex()); - if (Test->Pawn) + if (Pawn) + { + ASpatialFunctionalTestFlowController* FlowController = GetLocalFlowController(); + if (FlowController->WorkerDefinition.Id == 1) { - ASpatialFunctionalTestFlowController* FlowController = Test->GetLocalFlowController(); - if (FlowController->WorkerDefinition.Id == 1) + if (Pawn->TestInt == 666) { - if (Test->Pawn->TestInt == 666) - { - Test->AssertEqual_Int(Test->Pawn->TestInt, 666, AssertStep(StepDefinition, TEXT("Pawn's TestInt was replicated to owning client after being changed")), Test); - Test->FinishStep(); - } + AssertEqual_Int( + Pawn->TestInt, 666, + AssertStep(StepDefinition, TEXT("Pawn's TestInt was replicated to owning client after being changed")), this); + FinishStep(); } - else + } + else + { + StepTimer += DeltaTime; + if (StepTimer >= 1.0f) { - Test->StepTimer += DeltaTime; - if (Test->StepTimer >= 1.0f) - { - Test->StepTimer = 0.0f; - Test->AssertEqual_Int(Test->Pawn->TestInt, 0, AssertStep(StepDefinition, TEXT("Pawn's TestInt was not replicated to non-owning client after being changed")), Test); - Test->FinishStep(); - } + StepTimer = 0.0f; + AssertEqual_Int( + Pawn->TestInt, 0, + AssertStep(StepDefinition, TEXT("Pawn's TestInt was not replicated to non-owning client after being changed")), + this); + FinishStep(); } } - }); + } + }); } { // Step 7 - Put back original Pawns - AddStep(TEXT("ServerPossessOriginalPawns"), FWorkerDefinition::Server(1), nullptr, [](ASpatialFunctionalTest* NetTest) { - AOwnerOnlyPropertyReplication* Test = Cast(NetTest); - for (const auto& OriginalPawnPair : Test->OriginalPawns) + AddStep(TEXT("ServerPossessOriginalPawns"), FWorkerDefinition::Server(1), nullptr, [this]() { + for (const auto& OriginalPawnPair : OriginalPawns) { OriginalPawnPair.Key->Possess(OriginalPawnPair.Value); } - Test->FinishStep(); - }); + FinishStep(); + }); } } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3066/OwnerOnlyPropertyReplication.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3066/OwnerOnlyPropertyReplication.h index c54fff33e3..bdc2807c3c 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3066/OwnerOnlyPropertyReplication.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3066/OwnerOnlyPropertyReplication.h @@ -15,7 +15,7 @@ class SPATIALGDKFUNCTIONALTESTS_API AOwnerOnlyPropertyReplication : public ASpat public: AOwnerOnlyPropertyReplication(); - virtual void BeginPlay() override; + virtual void PrepareTest() override; void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3157/RPCInInterfaceTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3157/RPCInInterfaceTest.cpp index b632499dfb..7e0bde60c0 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3157/RPCInInterfaceTest.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3157/RPCInInterfaceTest.cpp @@ -1,11 +1,10 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved - #include "RPCInInterfaceTest.h" -#include "Net/UnrealNetwork.h" #include "Engine/World.h" -#include "GameFramework/PlayerState.h" #include "GameFramework/Controller.h" +#include "GameFramework/PlayerState.h" +#include "Net/UnrealNetwork.h" #include "SpatialFunctionalTestFlowController.h" /** @@ -18,70 +17,68 @@ ARPCInInterfaceTest::ARPCInInterfaceTest() Description = TEXT("Test RPCs in interfaces"); } -void ARPCInInterfaceTest::BeginPlay() +void ARPCInInterfaceTest::PrepareTest() { - Super::BeginPlay(); - - { // Step 1 - Create actor - AddStep(TEXT("ServerCreateActor"), FWorkerDefinition::Server(1), nullptr, [](ASpatialFunctionalTest* NetTest) { - ARPCInInterfaceTest* Test = Cast(NetTest); + Super::PrepareTest(); - ASpatialFunctionalTestFlowController* Client1FlowController = Test->GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1); + { // Step 1 - Create actor + AddStep(TEXT("ServerCreateActor"), FWorkerDefinition::Server(1), nullptr, [this]() { + ASpatialFunctionalTestFlowController* Client1FlowController = GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1); - Test->TestActor = Test->GetWorld()->SpawnActor(); - Test->AssertIsValid(Test->TestActor, "Actor exists", Test); - if (Test->TestActor) + TestActor = GetWorld()->SpawnActor(); + AssertIsValid(TestActor, "Actor exists", this); + if (TestActor) { - Test->TestActor->SetReplicates(true); - Test->TestActor->SetOwner(Client1FlowController->GetOwner()); + TestActor->SetReplicates(true); + TestActor->SetOwner(Client1FlowController->GetOwner()); } - Test->FinishStep(); - }); + FinishStep(); + }); } { // Step 2 - Make sure client has ownership of Actor - AddStep(TEXT("ClientCheckOwnership"), FWorkerDefinition::Client(1), nullptr, nullptr, [](ASpatialFunctionalTest* NetTest, float DeltaTime) { - ARPCInInterfaceTest* Test = Cast(NetTest); - if (IsValid(Test->TestActor) && Test->TestActor->GetOwner() == Test->GetLocalFlowController()->GetOwner()) + AddStep(TEXT("ClientCheckOwnership"), FWorkerDefinition::Client(1), nullptr, nullptr, [this](float DeltaTime) { + if (IsValid(TestActor) && TestActor->GetOwner() == GetLocalFlowController()->GetOwner()) { - Test->FinishStep(); + FinishStep(); } - }); + }); } - { // Step 3 - Call client RPC on interface - AddStep(TEXT("ServerCallRPC"), FWorkerDefinition::Server(1), [](ASpatialFunctionalTest* NetTest) -> bool { - ARPCInInterfaceTest* Test = Cast(NetTest); - return IsValid(Test->TestActor); - }, [](ASpatialFunctionalTest* NetTest) { - ARPCInInterfaceTest* Test = Cast(NetTest); - Test->TestActor->RPCInInterface(); - Test->FinishStep(); + { // Step 3 - Call client RPC on interface + AddStep( + TEXT("ServerCallRPC"), FWorkerDefinition::Server(1), + [this]() -> bool { + return IsValid(TestActor); + }, + [this]() { + TestActor->RPCInInterface(); + FinishStep(); }); } - { // Step 4 - Check RPC was received on client - AddStep(TEXT("ClientCheckRPC"), FWorkerDefinition::AllClients, [](ASpatialFunctionalTest* NetTest) -> bool { - ARPCInInterfaceTest* Test = Cast(NetTest); - return IsValid(Test->TestActor); - }, + { // Step 4 - Check RPC was received on client + AddStep( + TEXT("ClientCheckRPC"), FWorkerDefinition::AllClients, + [this]() -> bool { + return IsValid(TestActor); + }, nullptr, - [](ASpatialFunctionalTest* NetTest, float DeltaTime) { - ARPCInInterfaceTest* Test = Cast(NetTest); - if (Test->TestActor->GetOwner() == Test->GetLocalFlowController()->GetOwner()) + [this](float DeltaTime) { + if (TestActor->GetOwner() == GetLocalFlowController()->GetOwner()) { - if (Test->TestActor->bRPCReceived) + if (TestActor->bRPCReceived) { - Test->AssertTrue(Test->TestActor->bRPCReceived, "RPC was received", Test); - Test->FinishStep(); + AssertTrue(TestActor->bRPCReceived, "RPC was received", this); + FinishStep(); } } else { - Test->StepTimer += DeltaTime; - if (Test->StepTimer > 1.0f) // we give it up to 1s to make sure it wasn't received + StepTimer += DeltaTime; + if (StepTimer > 1.0f) // we give it up to 1s to make sure it wasn't received { - Test->StepTimer = 0.0f; - Test->AssertFalse(Test->TestActor->bRPCReceived, "RPC not received on non-owning client", Test); - Test->FinishStep(); + StepTimer = 0.0f; + AssertFalse(TestActor->bRPCReceived, "RPC not received on non-owning client", this); + FinishStep(); } } }); diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3157/RPCInInterfaceTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3157/RPCInInterfaceTest.h index 7700d13d6f..1e185a0ef6 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3157/RPCInInterfaceTest.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3157/RPCInInterfaceTest.h @@ -15,9 +15,9 @@ class SPATIALGDKFUNCTIONALTESTS_API ARPCInInterfaceTest : public ASpatialFunctio public: ARPCInInterfaceTest(); - virtual void BeginPlay() override; + virtual void PrepareTest() override; - void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; private: float StepTimer = 0.0f; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3157/RPCTestInterface.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3157/RPCTestInterface.h index 09d5feedca..2d74467b8c 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3157/RPCTestInterface.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3157/RPCTestInterface.h @@ -2,8 +2,8 @@ #pragma once -#include "UObject/Interface.h" #include "Engine/Classes/Engine/EngineTypes.h" +#include "UObject/Interface.h" #include "RPCTestInterface.generated.h" UINTERFACE(Blueprintable) diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestClientNetOwnership/NetOwnershipCube.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestClientNetOwnership/NetOwnershipCube.cpp new file mode 100644 index 0000000000..a3dd7bd2ad --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestClientNetOwnership/NetOwnershipCube.cpp @@ -0,0 +1,24 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "NetOwnershipCube.h" + +#include "Net/UnrealNetwork.h" + +ANetOwnershipCube::ANetOwnershipCube() +{ + SetReplicateMovement(true); + + ReceivedRPCs = 0; +} + +void ANetOwnershipCube::ServerIncreaseRPCCount_Implementation() +{ + ReceivedRPCs++; +} + +void ANetOwnershipCube::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ANetOwnershipCube, ReceivedRPCs); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestClientNetOwnership/NetOwnershipCube.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestClientNetOwnership/NetOwnershipCube.h new file mode 100644 index 0000000000..ea7a352211 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestClientNetOwnership/NetOwnershipCube.h @@ -0,0 +1,26 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h" + +#include "CoreMinimal.h" + +#include "NetOwnershipCube.generated.h" + +UCLASS() +class ANetOwnershipCube : public AReplicatedTestActorBase +{ + GENERATED_BODY() + +public: + ANetOwnershipCube(); + + void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + UFUNCTION(Server, Reliable) + void ServerIncreaseRPCCount(); + + UPROPERTY(Replicated) + int ReceivedRPCs; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestClientNetOwnership/SpatialTestNetOwnership.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestClientNetOwnership/SpatialTestNetOwnership.cpp new file mode 100644 index 0000000000..eb1fed4737 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestClientNetOwnership/SpatialTestNetOwnership.cpp @@ -0,0 +1,171 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialTestNetOwnership.h" +#include "NetOwnershipCube.h" +#include "SpatialFunctionalTestFlowController.h" + +#include "GameFramework/PlayerController.h" +#include "Kismet/GameplayStatics.h" + +/** + * This test automates the Client Net Ownership gym which demonstrates that in a zoned environment, setting client net-ownership of an Actor + * allows server RPCs to be sent correctly. The test runs with the BP_QuadrantZoningSettings and therefore includes 4 servers and 2 client + * workers. NOTE: This test requires the map it runs in to use the BP_QuadrantZoningSettings or any other setting that creates a zoned + * environment in order to be relevant. + * + * The flow of the test has 2 phases, as follows: + * - Phase 1 setup: + * - Server 1 spawns a NetOwnershipCube. + * - All workers set the reference to the spawned NetOwnershipCube. + * - The server that is authoritative over the NetOwnershipCube sets its owner to the PlayerController of Client 1. + * - The NetOwnershipCube, toghether with Client1's possesed Pawn, is moved by the server that has authority over it to 2 different test + * locations, such that it changes the authoritative server once. + * - Client 1 sends an RPC from the NetOwnershipCube at each test location. + * - Phase 1 test: + * - All workers test that all sent RPCs have been received by the NetOwnershipCube. + * + * - Phase 2 setup + * - The authoritative server sets the NetOwnershipCube's Owner to null. + * - Client 1 sends an RPC from the NetOwnershipCube. + * - Phase 2 test: + * - All workers test that the RPC sent was ignored. + * + * - Test Cleanup + * - The NetOwnershipCube is destroyed. + */ + +ASpatialTestNetOwnership::ASpatialTestNetOwnership() + : Super() +{ + Author = "Andrei"; + Description = TEXT("Test Net Ownership"); +} + +void ASpatialTestNetOwnership::PrepareTest() +{ + Super::PrepareTest(); + + if (HasAuthority()) + { + AddExpectedLogError(TEXT("No owning connection for actor NetOwnershipCube"), 1, false); + } + + // Step definition for Client 1 to send a Server RPC + FSpatialFunctionalTestStepDefinition ClientSendRPCStepDefinition(/*bIsNativeDefinition*/ true); + ClientSendRPCStepDefinition.StepName = TEXT("SpatialTestNetOwnershipClientSendRPC"); + ClientSendRPCStepDefinition.TimeLimit = 5.0f; + ClientSendRPCStepDefinition.NativeStartEvent.BindLambda([this]() { + NetOwnershipCube->ServerIncreaseRPCCount(); + + FinishStep(); + }); + + // Test Phase 1 + + // Server 1 spawns the NetOwnershipCube and registers it for auto-destroy. + AddStep(TEXT("SpatialTestNetOwnershipServerSpawnCube"), FWorkerDefinition::Server(1), nullptr, [this]() { + ANetOwnershipCube* Cube = + GetWorld()->SpawnActor(FVector::ZeroVector, FRotator::ZeroRotator, FActorSpawnParameters()); + RegisterAutoDestroyActor(Cube); + + FinishStep(); + }); + + AddStep( + TEXT("SpatialTestNetOwnershipAllWorkersSetReference"), FWorkerDefinition::AllWorkers, nullptr, nullptr, + [this](float DeltaTime) { + TArray NetOwnershipCubes; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), ANetOwnershipCube::StaticClass(), NetOwnershipCubes); + + // Make sure the cube is visible before trying to set the Test's variable. + if (NetOwnershipCubes.Num() != 1) + { + return; + } + + NetOwnershipCube = Cast(NetOwnershipCubes[0]); + + FinishStep(); + }, + 10.0f); + + // The authoritative server sets the owner of the NetOwnershipCube to Client's 1 PlayerController + AddStep(TEXT("SpatialTestNetOwnershipServerSetOwner"), FWorkerDefinition::AllServers, nullptr, [this]() { + if (NetOwnershipCube->HasAuthority()) + { + APlayerController* PlayerController = + Cast(GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1)->GetOwner()); + NetOwnershipCube->SetOwner(PlayerController); + } + + FinishStep(); + }); + + // The locations where the NetOwnershipCube will be when Client 1 will send an RPC. These are specifically set to make the + // NetOwnershipCube's authoritative server change according to the BP_QuadrantZoningSettings. + TArray TestLocations; + TestLocations.Add(FVector(250.0f, -250.0f, 0.0f)); + TestLocations.Add(FVector(-250.0f, -250.0f, 0.0f)); + + for (int i = 1; i <= TestLocations.Num(); ++i) + { + // The authoritative server moves the cube and Client1's Pawn to the corresponding test location + AddStep(TEXT("SpatialTestNetOwnershipServerMoveCube"), FWorkerDefinition::AllServers, nullptr, [this, i, TestLocations]() { + APlayerController* PlayerController = + Cast(GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1)->GetOwner()); + APawn* PlayerPawn = PlayerController->GetPawn(); + + if (PlayerPawn->HasAuthority()) + { + PlayerPawn->SetActorLocation(TestLocations[i - 1]); + } + + if (NetOwnershipCube->HasAuthority()) + { + NetOwnershipCube->SetActorLocation(TestLocations[i - 1]); + } + + FinishStep(); + }); + + // Client 1 sends a ServerRPC from the Cube. + AddStepFromDefinition(ClientSendRPCStepDefinition, FWorkerDefinition::Client(1)); + + // All workers check that the number of RPCs received by the server authoritative over NetOwnershipCube the is correct. + AddStep( + TEXT("SpatialTestNetOwnershipAllWorkersTestCount"), FWorkerDefinition::AllWorkers, nullptr, nullptr, + [this, i](float DeltaTime) { + if (NetOwnershipCube->ReceivedRPCs == i) + { + FinishStep(); + } + }, + 10.0f); + } + + // Test Phase 2 + + // The authoritative server sets the owner of the cube to be nullptr + AddStep(TEXT("SpatialTestNetOwnershipServerSetOwnerToNull"), FWorkerDefinition::AllServers, nullptr, [this]() { + if (NetOwnershipCube->HasAuthority()) + { + NetOwnershipCube->SetOwner(nullptr); + } + + FinishStep(); + }); + + // Client 1 sends a ServerRPC from the NetOwnershipCube that should be ignored. + AddStepFromDefinition(ClientSendRPCStepDefinition, FWorkerDefinition::Client(1)); + + // All workers check that the number of RPCs received by the authoritative server is correct. + AddStep( + TEXT("SpatialTestNetOwnershipAllWorkersTestCount2"), FWorkerDefinition::AllWorkers, nullptr, nullptr, + [this, TestLocations](float DeltaTime) { + if (NetOwnershipCube->ReceivedRPCs == TestLocations.Num()) + { + FinishStep(); + } + }, + 10.0f); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestClientNetOwnership/SpatialTestNetOwnership.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestClientNetOwnership/SpatialTestNetOwnership.h new file mode 100644 index 0000000000..9c6c89240a --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestClientNetOwnership/SpatialTestNetOwnership.h @@ -0,0 +1,25 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialFunctionalTest.h" + +#include "CoreMinimal.h" + +#include "SpatialTestNetOwnership.generated.h" + +class ANetOwnershipCube; +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialTestNetOwnership : public ASpatialFunctionalTest +{ + GENERATED_BODY() + +public: + ASpatialTestNetOwnership(); + + virtual void PrepareTest() override; + + // Reference to the NetOwnershipCube, used to avoid using GetAllActorsOfClass() in every step to get a reference to the + // NetOwnershipCube. + ANetOwnershipCube* NetOwnershipCube; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestCrossServerRPC/CrossServerRPCCube.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestCrossServerRPC/CrossServerRPCCube.cpp new file mode 100644 index 0000000000..7e3e841d76 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestCrossServerRPC/CrossServerRPCCube.cpp @@ -0,0 +1,23 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "CrossServerRPCCube.h" +#include "Net/UnrealNetwork.h" + +ACrossServerRPCCube::ACrossServerRPCCube() +{ + bAlwaysRelevant = true; + bNetLoadOnClient = true; + bNetLoadOnNonAuthServer = true; +} + +void ACrossServerRPCCube::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ACrossServerRPCCube, ReceivedCrossServerRPCS); +} + +void ACrossServerRPCCube::CrossServerTestRPC_Implementation(int SendingServerID) +{ + ReceivedCrossServerRPCS.Add(SendingServerID); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestCrossServerRPC/CrossServerRPCCube.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestCrossServerRPC/CrossServerRPCCube.h new file mode 100644 index 0000000000..925ebbdf76 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestCrossServerRPC/CrossServerRPCCube.h @@ -0,0 +1,25 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h" +#include "CrossServerRPCCube.generated.h" + +UCLASS() +class ACrossServerRPCCube : public AReplicatedTestActorBase +{ + GENERATED_BODY() + +public: + ACrossServerRPCCube(); + + void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + // Array storing the IDs of the servers from which this cube has successfully received a CrossServer RPC. + UPROPERTY(Replicated) + TArray ReceivedCrossServerRPCS; + + UFUNCTION(CrossServer, Reliable) + void CrossServerTestRPC(int SendingServerID); +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestCrossServerRPC/SpatialTestCrossServerRPC.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestCrossServerRPC/SpatialTestCrossServerRPC.cpp new file mode 100644 index 0000000000..42ad2e2305 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestCrossServerRPC/SpatialTestCrossServerRPC.cpp @@ -0,0 +1,145 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialTestCrossServerRPC.h" +#include "CrossServerRPCCube.h" +#include "EngineClasses/SpatialNetDriver.h" +#include "Kismet/GameplayStatics.h" +#include "LoadBalancing/AbstractLBStrategy.h" +#include "SpatialFunctionalTestFlowController.h" + +/** + * This test automates the Server to server RPC gym, that was used to demonstrate that actors owned by different servers correctly send + * server-to-server RPCs. The test includes 4 server workers and 2 clients. NOTE: This test requires the map it runs in to have the + * BP_QuadrantLBStrategy and OwnershipLockingPolicy set in order to be relevant. + * + * The flow is as follows: + * - Setup: + * - Each server spawns one CrossServerRPCCube. + * - Each server sends RPCs to all the cubes that are not under his authority. + * - Test + * - Server 1 checks that all the expected RPCs were received by all cubes. + * - Clean-up + * - The previously spawned cubes are destroyed. + */ + +ASpatialTestCrossServerRPC::ASpatialTestCrossServerRPC() + : Super() +{ + Author = "Andrei"; + Description = TEXT("Test CrossServer RPCs"); +} + +void ASpatialTestCrossServerRPC::PrepareTest() +{ + Super::PrepareTest(); + + TArray CubesLocations; + CubesLocations.Add(FVector(250.0f, 250.0f, 75.0f)); + CubesLocations.Add(FVector(250.0f, -250.0f, 75.0f)); + CubesLocations.Add(FVector(-250.0f, -250.0f, 75.0f)); + CubesLocations.Add(FVector(-250.0f, 250.0f, 75.0f)); + + AddStep(TEXT("EnsureSpatialOS"), FWorkerDefinition::Server(1), nullptr, [this]() { + USpatialNetDriver* SpatialNetDriver = Cast(GetNetDriver()); + + if (SpatialNetDriver == nullptr || SpatialNetDriver->LoadBalanceStrategy == nullptr) + { + FinishTest(EFunctionalTestResult::Error, TEXT("Test requires SpatialOS enabled with Load-Balancing Strategy")); + } + else + { + FinishStep(); + } + }); + + for (int i = 1; i <= 4; ++i) + { + FVector SpawnPosition = CubesLocations[i - 1]; + // Each server spawns a cube + AddStep(TEXT("ServerSetupStep"), FWorkerDefinition::Server(i), nullptr, [this, SpawnPosition]() { + ACrossServerRPCCube* TestCube = + GetWorld()->SpawnActor(SpawnPosition, FRotator::ZeroRotator, FActorSpawnParameters()); + RegisterAutoDestroyActor(TestCube); + + FinishStep(); + }); + } + + int NumCubes = CubesLocations.Num(); + + // Each server sends an RPC to all cubes that it is NOT authoritive over. + AddStep( + TEXT("ServerSendRPCs"), FWorkerDefinition::AllServers, + [this, NumCubes]() -> bool { + // Make sure that all cubes were spawned and are visible to all servers before trying to send the RPCs. + TArray TestCubes; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), ACrossServerRPCCube::StaticClass(), TestCubes); + + UAbstractLBStrategy* LBStrategy = Cast(GetNetDriver())->LoadBalanceStrategy; + + // Since the servers are spawning the cubes in positions that don't belong to them + // we need to wait for all the authority changes to happen, and this can take a bit. + int LocalWorkerId = GetLocalWorkerId(); + int NumCubesWithAuthority = 0; + int NumCubesShouldHaveAuthority = 0; + for (AActor* Cube : TestCubes) + { + if (Cube->HasAuthority()) + { + NumCubesWithAuthority += 1; + } + if (LBStrategy->WhoShouldHaveAuthority(*Cube) == LocalWorkerId) + { + NumCubesShouldHaveAuthority += 1; + } + } + + // So only when we have all cubes present and we only have authority over the one we should we can progress. + return TestCubes.Num() == NumCubes && NumCubesWithAuthority == 1 && NumCubesShouldHaveAuthority == 1; + }, + [this]() { + TArray TestCubes; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), ACrossServerRPCCube::StaticClass(), TestCubes); + + int LocalWorkerId = GetLocalWorkerId(); + + for (AActor* Cube : TestCubes) + { + if (!Cube->HasAuthority()) + { + ACrossServerRPCCube* CrossServerRPCCube = Cast(Cube); + CrossServerRPCCube->CrossServerTestRPC(LocalWorkerId); + } + } + + FinishStep(); + }); + + // Server 1 checks if all cubes received the expected number of RPCs. + AddStep( + TEXT("Server1CheckRPCs"), FWorkerDefinition::Server(1), nullptr, nullptr, + [this](float DeltaTime) { + TArray TestCubes; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), ACrossServerRPCCube::StaticClass(), TestCubes); + + int CorrectCubes = 0; + + for (AActor* Cube : TestCubes) + { + ACrossServerRPCCube* CrossServerRPCCube = Cast(Cube); + + int ReceivedRPCS = CrossServerRPCCube->ReceivedCrossServerRPCS.Num(); + + if (ReceivedRPCS == TestCubes.Num() - 1) + { + CorrectCubes++; + } + } + + if (CorrectCubes == TestCubes.Num()) + { + FinishStep(); + } + }, + 10.0f); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestCrossServerRPC/SpatialTestCrossServerRPC.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestCrossServerRPC/SpatialTestCrossServerRPC.h new file mode 100644 index 0000000000..8e11cba8c4 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestCrossServerRPC/SpatialTestCrossServerRPC.h @@ -0,0 +1,18 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialFunctionalTest.h" +#include "SpatialTestCrossServerRPC.generated.h" + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialTestCrossServerRPC : public ASpatialFunctionalTest +{ + GENERATED_BODY() + +public: + ASpatialTestCrossServerRPC(); + + virtual void PrepareTest() override; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestHandover/HandoverCube.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestHandover/HandoverCube.cpp new file mode 100644 index 0000000000..1dff9321d8 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestHandover/HandoverCube.cpp @@ -0,0 +1,44 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "HandoverCube.h" +#include "Net/UnrealNetwork.h" +#include "Utils/SpatialStatics.h" + +AHandoverCube::AHandoverCube() +{ + LockingServerID = 0; + AuthorityChanges = 0; +} + +void AHandoverCube::AcquireLock_Implementation(int ServerID) +{ + if (!USpatialStatics::IsLocked(this)) + { + LockTocken = USpatialStatics::AcquireLock(this); + LockingServerID = ServerID; + } +} + +void AHandoverCube::ReleaseLock_Implementation() +{ + if (USpatialStatics::IsLocked(this)) + { + USpatialStatics::ReleaseLock(this, LockTocken); + LockingServerID = 0; + } +} + +void AHandoverCube::OnAuthorityGained() +{ + Super::OnAuthorityGained(); + + ++AuthorityChanges; +} + +void AHandoverCube::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(AHandoverCube, LockingServerID); + DOREPLIFETIME(AHandoverCube, AuthorityChanges); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestHandover/HandoverCube.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestHandover/HandoverCube.h new file mode 100644 index 0000000000..f3b895e49f --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestHandover/HandoverCube.h @@ -0,0 +1,36 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h" +#include "Utils/SpatialStatics.h" +#include "HandoverCube.generated.h" + +UCLASS() +class AHandoverCube : public AReplicatedTestActorBase +{ + GENERATED_BODY() + +public: + AHandoverCube(); + + void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + void OnAuthorityGained() override; + + UFUNCTION(CrossServer, Reliable) + void AcquireLock(int ServerID); + + UFUNCTION(CrossServer, Reliable) + void ReleaseLock(); + + UPROPERTY(Replicated) + int LockingServerID; + + UPROPERTY(Replicated) + int AuthorityChanges; + +private: + FLockingToken LockTocken; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestHandover/SpatialTestHandover.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestHandover/SpatialTestHandover.cpp new file mode 100644 index 0000000000..1170cef76b --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestHandover/SpatialTestHandover.cpp @@ -0,0 +1,204 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialTestHandover.h" +#include "EngineClasses/SpatialNetDriver.h" +#include "Kismet/GameplayStatics.h" +#include "LoadBalancing/LayeredLBStrategy.h" + +#include "HandoverCube.h" +#include "SpatialFunctionalTestFlowController.h" + +/** + * This test automated the Handover Gym which demonstrates that Entities correctly migrate between areas of authority. + * This test requires the map it runs in to have the Multi worker enabled, with the BP_QuadrantZoningSettings set in order to be relevant. + * The test includes 4 servers and 2 client workers. + * + * The flow is as follows: + * - Setup: + * - Server 1 spawns a HandoverCube inside its authority area. + * - All servers set a reference to the HandoverCube and reset their local copy of the LocationIndex and AuthorityCheckIndex. + * - Test: + * - At this stage, Server 1 should have authority over the HandoverCube. + * - The HandoverCube is moved in the authority area of Server 2. + * - At this stage, Server 2 should have authority over the HandoverCube. + * - Server 2 acquires a lock on the HandoverCube and moves it into the authority area of Server 4. + * - Since Server 2 has the lock on the HandoverCube it should still be authoritative over it. + * - Server 2 releases the lock on the HandoverCube. + * - At this point, Server 4 should become authoritative over the HandoverCube. + * - The HandoverCube is moved in the authority area of Server 3. + * - At this point, Server 3 should be authoritative over the HandoverCube. + * - Clean-up: + * - The HandoverCube is destroyed. + */ +ASpatialTestHandover::ASpatialTestHandover() + : Super() +{ + Author = "Andrei"; + Description = TEXT("Test Actor handover"); + + Server1Position = FVector(-500.0f, -500.0f, 50.0f); + Server2Position = FVector(500.0f, -500.0f, 50.0f); + Server3Position = FVector(-500.0f, 500.0f, 50.0f); + Server4Position = FVector(500.0f, 500.0f, 50.0f); +} + +void ASpatialTestHandover::PrepareTest() +{ + Super::PrepareTest(); + + // Server 1 spawns the HandoverCube under its authority area. + AddStep(TEXT("SpatialTestHandoverServer1SpawnCube"), FWorkerDefinition::Server(1), nullptr, [this]() { + HandoverCube = GetWorld()->SpawnActor(Server1Position, FRotator::ZeroRotator, FActorSpawnParameters()); + RegisterAutoDestroyActor(HandoverCube); + + FinishStep(); + }); + + float StepTimeLimit = 10.0f; + + // All servers set a reference to the HandoverCube and reset the LocationIndex and AuthorityCheckIndex. + AddStep( + TEXT("SpatialTestHandoverAllServersSetupStep"), FWorkerDefinition::AllServers, nullptr, nullptr, + [this](float DeltaTime) { + TArray HandoverCubes; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AHandoverCube::StaticClass(), HandoverCubes); + + if (HandoverCubes.Num() == 1) + { + HandoverCube = Cast(HandoverCubes[0]); + + USpatialNetDriver* NetDriver = Cast(GetWorld()->GetNetDriver()); + + AssertTrue(IsValid(NetDriver), TEXT("This test should be run with Spatial Networking")); + + LoadBalancingStrategy = Cast(NetDriver->LoadBalanceStrategy); + + if (IsValid(HandoverCube) && IsValid(LoadBalancingStrategy)) + { + FinishStep(); + } + } + }, + StepTimeLimit); + + // Check that Server 1 is authoritative over the HandoverCube. + AddStep( + TEXT("SpatialTestHandoverServer1AuthorityAndPosition"), FWorkerDefinition::AllServers, nullptr, nullptr, + [this](float DeltaTime) { + RequireHandoverCubeAuthorityAndPosition(1, Server1Position); + FinishStep(); + }, + StepTimeLimit); + + // Move the HandoverCube to the next location, which is inside the authority area of Server 2. + AddStep( + TEXT("SpatialTestHandoverServer1MoveToServer2"), FWorkerDefinition::Server(1), nullptr, nullptr, + [this](float DeltaTime) { + if (MoveHandoverCube(Server2Position)) + { + FinishStep(); + } + }, + StepTimeLimit); + + // Check that Server 2 is authoritative over the HandoverCube. + AddStep( + TEXT("SpatialTestHandoverServer2AuthorityAndPosition"), FWorkerDefinition::AllServers, nullptr, nullptr, + [this](float DeltaTime) { + RequireHandoverCubeAuthorityAndPosition(2, Server2Position); + FinishStep(); + }, + StepTimeLimit); + + // Server 2 acquires a lock on the HandoverCube. + AddStep(TEXT("SpatialTestHandoverServer2AcquireLock"), FWorkerDefinition::Server(2), nullptr, [this]() { + HandoverCube->AcquireLock(2); + FinishStep(); + }); + + // Move the HandoverCube to the next location, which is inside the authority area of Server 4. + AddStep( + TEXT("SpatialTestHandoverServer2MoveToServer4"), FWorkerDefinition::Server(2), nullptr, nullptr, + [this](float DeltaTime) { + if (MoveHandoverCube(Server4Position)) + { + FinishStep(); + } + }, + StepTimeLimit); + + // Check that Server 2 is still authoritative over the HandoverCube due to acquiring the lock earlier. + AddStep( + TEXT("SpatialTestHandoverServer2AuthorityAndServer4Position"), FWorkerDefinition::AllServers, nullptr, nullptr, + [this](float DeltaTime) { + RequireHandoverCubeAuthorityAndPosition(2, Server4Position); + FinishStep(); + }, + StepTimeLimit); + + // Server 2 releases the lock on the HandoverCube. + AddStep(TEXT("SpatialTestHandoverServer2ReleaseLock"), FWorkerDefinition::Server(2), nullptr, [this]() { + HandoverCube->ReleaseLock(); + FinishStep(); + }); + + // Check that Server 4 is now authoritative over the HandoverCube. + AddStep( + TEXT("SpatialTestHandoverServer4AuthorityAndPosition"), FWorkerDefinition::AllServers, nullptr, nullptr, + [this](float DeltaTime) { + RequireHandoverCubeAuthorityAndPosition(4, Server4Position); + FinishStep(); + }, + StepTimeLimit); + + // Move the HandoverCube to the next location, which is inside the authority area of Server 3. + AddStep( + TEXT("SpatialTestHandoverServer4MoveToServer3"), FWorkerDefinition::Server(4), nullptr, nullptr, + [this](float DeltaTime) { + if (MoveHandoverCube(Server3Position)) + { + FinishStep(); + } + }, + StepTimeLimit); + + // Check that Server 3 is now authoritative over the HandoverCube. + AddStep( + TEXT("SpatialTestHandoverServer3AuthorityAndPosition"), FWorkerDefinition::AllServers, nullptr, nullptr, + [this](float DeltaTime) { + RequireHandoverCubeAuthorityAndPosition(3, Server3Position); + FinishStep(); + }, + StepTimeLimit); +} + +void ASpatialTestHandover::RequireHandoverCubeAuthorityAndPosition(int WorkerShouldHaveAuthority, FVector ExpectedPosition) +{ + if (!ensureMsgf(GetLocalWorkerType() == ESpatialFunctionalTestWorkerType::Server, TEXT("Should only be called in Servers"))) + { + return; + } + + RequireEqual_Vector(HandoverCube->GetActorLocation(), ExpectedPosition, + FString::Printf(TEXT("HandoverCube in %s"), *ExpectedPosition.ToCompactString()), 1.0f); + + if (WorkerShouldHaveAuthority == GetLocalWorkerId()) + { + RequireTrue(HandoverCube->HasAuthority(), TEXT("Has Authority")); + } + else + { + RequireFalse(HandoverCube->HasAuthority(), TEXT("Doesn't Have Authority")); + } +} + +bool ASpatialTestHandover::MoveHandoverCube(FVector Position) +{ + if (HandoverCube->HasAuthority()) + { + HandoverCube->SetActorLocation(Position); + return true; + } + + return false; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestHandover/SpatialTestHandover.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestHandover/SpatialTestHandover.h new file mode 100644 index 0000000000..e128568ecb --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestHandover/SpatialTestHandover.h @@ -0,0 +1,37 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialFunctionalTest.h" +#include "SpatialTestHandover.generated.h" + +class AHandoverCube; +class ULayeredLBStrategy; + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialTestHandover : public ASpatialFunctionalTest +{ + GENERATED_BODY() + +public: + ASpatialTestHandover(); + + virtual void PrepareTest() override; + +private: + AHandoverCube* HandoverCube; + + // The Load Balancing used by the test, needed to decide what Server should have authority over the HandoverCube. + ULayeredLBStrategy* LoadBalancingStrategy; + + void RequireHandoverCubeAuthorityAndPosition(int WorkerShouldHaveAuthority, FVector ExpectedPosition); + + bool MoveHandoverCube(FVector Position); + + // Positions that belong to specific server according to 2x2 Grid LBS. + FVector Server1Position; + FVector Server2Position; + FVector Server3Position; + FVector Server4Position; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestNetReference/CubeWithReferences.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestNetReference/CubeWithReferences.cpp index 8c1c118329..5891daf761 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestNetReference/CubeWithReferences.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestNetReference/CubeWithReferences.cpp @@ -1,6 +1,5 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved - #include "CubeWithReferences.h" #include "Net/UnrealNetwork.h" @@ -16,7 +15,7 @@ int ACubeWithReferences::CountValidNeighbours() if (IsValid(Neighbour1)) { - ValidNeighbours++; + ValidNeighbours++; } if (IsValid(Neighbour2)) diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestNetReference/SpatialTestNetReference.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestNetReference/SpatialTestNetReference.cpp index bc543f1616..ce058e8ecc 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestNetReference/SpatialTestNetReference.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestNetReference/SpatialTestNetReference.cpp @@ -1,28 +1,29 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "SpatialTestNetReference.h" +#include "CubeWithReferences.h" #include "GameFramework/PlayerController.h" -#include "SpatialFunctionalTestFlowController.h" -#include "SpatialGDKFunctionalTests/SpatialGDK/SpatialTestCharacterMovement/TestMovementCharacter.h" #include "Kismet/GameplayStatics.h" -#include "CubeWithReferences.h" +#include "SpatialFunctionalTestFlowController.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/TestMovementCharacter.h" #include "SpatialGDKSettings.h" /** - * This test automates the Net Reference Test gym, which tested that references to replicated actors are stable when actors go in and out of relevance. - * This test also adds an interest check on top of the previously mentioned Gym. - * NOTE: The test also includes support for visual debugging. If desired, it is suggested to comment the line that is updating the PositionUpdateFrequency before trying to visually debug the test. + * This test automates the Net Reference Test gym, which tested that references to replicated actors are stable when actors go in and out of + *relevance. This test also adds an interest check on top of the previously mentioned Gym. NOTE: The test also includes support for visual + *debugging. If desired, it is suggested to comment the line that is updating the PositionUpdateFrequency before trying to visually debug + *the test. * - * The test includes a single server and two client workers. For performance considerations, the only client that is executing the test is Client 1. - * The flow is as follows: + * The test includes a single server and two client workers. For performance considerations, the only client that is executing the test is + *Client 1. The flow is as follows: * - Setup: * - The Server spawns 4 CubeWithReferences objects and sets up their references. * - Test: * - The test contains 2 runs of the same flow: * 1) The Server moves the character of Client 1 to 4 specific locations * 2) After arriving at each location on the Client, the test checks that: - * 2.1) The correct amount of cubes are present in the world, based on the default NetCullDistanceSquared of the PlayerController. - * 2.2) The references to the replicated actors are correct. + * 2.1) The correct amount of cubes are present in the world, based on the default NetCullDistanceSquared of the + *PlayerController. 2.2) The references to the replicated actors are correct. * - Clean-up: * - The previously spawned CubeWithReferences and TestMovementCharacter are destroyed */ @@ -33,29 +34,35 @@ ASpatialTestNetReference::ASpatialTestNetReference() Author = "Andrei"; Description = TEXT("Test Net Reference"); - // The test locations are specifically set so that the specified number of cubes are visible, based on the default NetCullDistanceSquared. - // To be more specific, in this setup, a cube will be visible if the distance from it to the PlayerCharacter is less than 15000 units. - TestLocations.Add(TPair (FVector(0.0f, -15000.0f, 40.0f), 1)); - TestLocations.Add(TPair (FVector(5000.0f, -5000.0f, 40.0f), 2)); - TestLocations.Add(TPair (FVector(5000.0f, 1000.0f, 40.0f), 3)); - TestLocations.Add(TPair (FVector(100.0f, 100.0f, 40.0f), 4)); + // The test locations are specifically set so that the specified number of cubes are visible, based on the default + // NetCullDistanceSquared. To be more specific, in this setup, a cube will be visible if the distance from it to the PlayerCharacter is + // less than 15000 units. + TestLocations.Add(TPair(FVector(0.0f, -15000.0f, 40.0f), 1)); + TestLocations.Add(TPair(FVector(5000.0f, -5000.0f, 40.0f), 2)); + TestLocations.Add(TPair(FVector(5000.0f, 1000.0f, 40.0f), 3)); + TestLocations.Add(TPair(FVector(100.0f, 100.0f, 40.0f), 4)); + + /* Uncomment these lines, together with the line in the SpatialTestNetReferenceServerMove step related to the camera movement to enable + visual debugging. However, note that uncommenting these lines will make the test fail if running with Native Unreal networking + // The camera relative locations are set so that the camera is always at the location (8500.0f, 13000.0f, 40.f), in order to have all 4 + possible cubes in its view for ease of visual debugging - // The camera relative locations are set so that the camera is always at the location (8500.0f, 13000.0f, 40.f), in order to have all 4 possible cubes in its view for ease of visual debugging CameraRelativeLocations.Add(FVector(8500.0f, 28000.0f, 0.0f)); CameraRelativeLocations.Add(FVector(3500.0f, 18000.0f, 0.0f)); CameraRelativeLocations.Add(FVector(3500.0f, 12000.0f, 0.0f)); CameraRelativeLocations.Add(FVector(8400.0f, 12900.0f, 0.0f)); CameraRelativeRotation = FRotator::MakeFromEuler(FVector(0.0f, 0.0f, 240.0f)); + */ } -void ASpatialTestNetReference::BeginPlay() +void ASpatialTestNetReference::PrepareTest() { - Super::BeginPlay(); + Super::PrepareTest(); - PreviousPositionUpdateFrequency = GetDefault()->PositionUpdateFrequency; + PreviousMaximumDistanceThreshold = GetDefault()->PositionUpdateThresholdMaxCentimeters; - AddStep(TEXT("SpatialTestNetReferenceServerSetup"), FWorkerDefinition::Server(1), nullptr, [this](ASpatialFunctionalTest* NetTest) { + AddStep(TEXT("SpatialTestNetReferenceServerSetup"), FWorkerDefinition::Server(1), nullptr, [this]() { // Set up the cubes' spawn locations TArray CubeLocations; CubeLocations.Add(FVector(0.0f, -11000.0f, 40.0f)); @@ -69,10 +76,11 @@ void ASpatialTestNetReference::BeginPlay() for (int i = 0; i < NumberOfCubes; ++i) { - ACubeWithReferences* CubeWithReferences = GetWorld()->SpawnActor(CubeLocations[i], FRotator::ZeroRotator, FActorSpawnParameters()); + ACubeWithReferences* CubeWithReferences = + GetWorld()->SpawnActor(CubeLocations[i], FRotator::ZeroRotator, FActorSpawnParameters()); // Cubes are scaled so that they can be seen by the camera, used for easing visual debugging - CubeWithReferences->SetActorScale3D(FVector(10.0f,30.0f,30.0f)); + CubeWithReferences->SetActorScale3D(FVector(10.0f, 30.0f, 30.0f)); TestCubes.Add(CubeWithReferences); @@ -86,81 +94,83 @@ void ASpatialTestNetReference::BeginPlay() TestCubes[i]->Neighbour2 = TestCubes[(i + NumberOfCubes - 1) % NumberOfCubes]; } - // Set the PositionUpdateFrequency to a higher value so that the amount of waiting time before checking the references can be smaller, decreasing the overall duration of the test - PreviousPositionUpdateFrequency = GetDefault()->PositionUpdateFrequency; - GetMutableDefault()->PositionUpdateFrequency = 10000.0f; + // Set the PositionUpdateThresholdMaxCentimeeters to a lower value so that the spatial position updates can be sent every time the + // character moves, decreasing the overall duration of the test + PreviousMaximumDistanceThreshold = GetDefault()->PositionUpdateThresholdMaxCentimeters; + GetMutableDefault()->PositionUpdateThresholdMaxCentimeters = 0.0f; - // Spawn the TestMovementCharacter actor for client 1 to possess. - for (ASpatialFunctionalTestFlowController* FlowController : GetFlowControllers()) - { - if (FlowController->WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Client && FlowController->WorkerDefinition.Id == 1) - { - ATestMovementCharacter* TestCharacter = GetWorld()->SpawnActor(FVector::ZeroVector, FRotator::ZeroRotator, FActorSpawnParameters()); - APlayerController* PlayerController = Cast(FlowController->GetOwner()); - OriginalPawn = TPair(PlayerController, PlayerController->GetPawn()); - - RegisterAutoDestroyActor(TestCharacter); - PlayerController->Possess(TestCharacter); - } - } + // Spawn the TestMovementCharacter actor for Client 1 to possess. + ASpatialFunctionalTestFlowController* FlowController = GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1); + ATestMovementCharacter* TestCharacter = + GetWorld()->SpawnActor(FVector::ZeroVector, FRotator::ZeroRotator, FActorSpawnParameters()); + APlayerController* PlayerController = Cast(FlowController->GetOwner()); + + // Set a reference to the previous Pawn so that it can be possessed back in the last step of the test + OriginalPawn = TPair(PlayerController, PlayerController->GetPawn()); + + RegisterAutoDestroyActor(TestCharacter); + PlayerController->Possess(TestCharacter); FinishStep(); - }); + }); - for(int i = 0; i < 2 * TestLocations.Num(); ++ i) + for (int i = 0; i < 2 * TestLocations.Num(); ++i) { - - // The mod is required since the test goes over each test location twice + // The mod is required since the test goes over each test location twice int CurrentMoveIndex = i % TestLocations.Num(); - AddStep(TEXT("SpatialTestNetReferenceServerMove"), FWorkerDefinition::Server(1), nullptr, [this, CurrentMoveIndex](ASpatialFunctionalTest* NetTest) - { - ASpatialFunctionalTestFlowController* FlowController = GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1); - APlayerController* PlayerController = Cast(FlowController->GetOwner()); - ATestMovementCharacter* PlayerCharacter = Cast(PlayerController->GetPawn()); + AddStep(TEXT("SpatialTestNetReferenceServerMove"), FWorkerDefinition::Server(1), nullptr, [this, CurrentMoveIndex]() { + ASpatialFunctionalTestFlowController* FlowController = GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1); + APlayerController* PlayerController = Cast(FlowController->GetOwner()); + ATestMovementCharacter* PlayerCharacter = Cast(PlayerController->GetPawn()); - // Move the character to the correct location - PlayerCharacter->SetActorLocation(TestLocations[CurrentMoveIndex].Key); + // Move the character to the correct location + PlayerCharacter->SetActorLocation(TestLocations[CurrentMoveIndex].Key); - // Update the camera location for visual debugging - PlayerCharacter->UpdateCameraLocationAndRotation(CameraRelativeLocations[CurrentMoveIndex], CameraRelativeRotation); + /* Uncomment this line to allow for visual debugging, together with the lines in the constructor. + However, note that uncommenting these lines will make the test fail if running with Native Unreal networking - FinishStep(); - }); + // Update the camera location for visual debugging + PlayerCharacter->UpdateCameraLocationAndRotation(CameraRelativeLocations[CurrentMoveIndex], CameraRelativeRotation); + */ + + FinishStep(); + }); - AddStep(TEXT("SpatialTestNetReferenceClientCheckMovement"), FWorkerDefinition::Client(1), nullptr, nullptr, [this, CurrentMoveIndex](ASpatialFunctionalTest* NetTest, float DeltaTime) - { + AddStep( + TEXT("SpatialTestNetReferenceClientCheckMovement"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this, CurrentMoveIndex](float DeltaTime) { AController* PlayerController = Cast(GetLocalFlowController()->GetOwner()); ATestMovementCharacter* PlayerCharacter = Cast(PlayerController->GetPawn()); - if (PlayerCharacter->GetActorLocation().Equals(TestLocations[CurrentMoveIndex].Key, 1.0f)) + if (PlayerCharacter != nullptr && PlayerCharacter->GetActorLocation().Equals(TestLocations[CurrentMoveIndex].Key, 1.0f)) { FinishStep(); } + }, + 10.0f); - }, 5.0f); - - AddStep(TEXT("SpatialTestNetReferenceClientCheckNumberOfReferences"), FWorkerDefinition::Client(1), nullptr, nullptr, [this, CurrentMoveIndex](ASpatialFunctionalTest* NetTest, float DeltaTime) - { + AddStep( + TEXT("SpatialTestNetReferenceClientCheckNumberOfReferences"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this, CurrentMoveIndex](float DeltaTime) { TArray CubesWithReferences; UGameplayStatics::GetAllActorsOfClass(GetWorld(), ACubeWithReferences::StaticClass(), CubesWithReferences); bool bHasCorrectNumberOfCubes = CubesWithReferences.Num() == TestLocations[CurrentMoveIndex].Value; - if(bHasCorrectNumberOfCubes) + if (bHasCorrectNumberOfCubes) { - AssertTrue(bHasCorrectNumberOfCubes, FString::Printf(TEXT("For location with index %d the correct number of cubes are visible"), CurrentMoveIndex)); FinishStep(); } - }, 5.0f); + }, + 10.0f); - AddStep(TEXT("SpatialTestNetReferenceClientCheckReferences"), FWorkerDefinition::Client(1), nullptr, nullptr, [this, CurrentMoveIndex](ASpatialFunctionalTest* NetTest, float DeltaTime) - { + AddStep( + TEXT("SpatialTestNetReferenceClientCheckReferences"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this, CurrentMoveIndex](float DeltaTime) { TArray CubesWithReferences; UGameplayStatics::GetAllActorsOfClass(GetWorld(), ACubeWithReferences::StaticClass(), CubesWithReferences); - checkf(CubesWithReferences.Num() != 0, TEXT("There should never be 0 visible cubes")) - bool bHasCorrectReferences = true; for (AActor* ArrayObject : CubesWithReferences) @@ -175,8 +185,11 @@ void ASpatialTestNetReference::BeginPlay() ACubeWithReferences* OtherCube = Cast(OtherObject); FVector OtherCubeLocation = OtherObject->GetActorLocation(); - // If the cube is the current one or the diagonally opposed one, then ignore it as it should never be a neigbhour of the current cube - if (OtherCubeLocation.Equals(CurrentCubeLocation) || (FMath::IsNearlyEqual(OtherCubeLocation.X,-CurrentCubeLocation.X) && FMath::IsNearlyEqual(OtherCubeLocation.Y, -CurrentCubeLocation.Y))) + // If the cube is the current one or the diagonally opposed one, then ignore it as it should never be a neigbhour of + // the current cube + if (OtherCubeLocation.Equals(CurrentCubeLocation) + || (FMath::IsNearlyEqual(OtherCubeLocation.X, -CurrentCubeLocation.X) + && FMath::IsNearlyEqual(OtherCubeLocation.Y, -CurrentCubeLocation.Y))) { continue; } @@ -197,24 +210,22 @@ void ASpatialTestNetReference::BeginPlay() } else if (ExpectedValidReferences == 1) { - // We have previously checked that one neighbour reference is correctly pointing to the neighbour cube, also check that the other reference is null + // We have previously checked that one neighbour reference is correctly pointing to the neighbour cube, also check + // that the other reference is null bHasCorrectReferences &= !IsValid(CurrentCube->Neighbour1) || !IsValid(CurrentCube->Neighbour2); } - - checkf(ExpectedValidReferences <= 2, TEXT("There should never be more than 2 valid references for a cube")); - - AssertEqual_Bool(bHasCorrectReferences, true, FString::Printf(TEXT("At location with index %d, for the cube at location %f, %f, %f, the references are correct"), CurrentMoveIndex, CurrentCubeLocation.X, CurrentCubeLocation.Y, CurrentCubeLocation.Z)); } - if(bHasCorrectReferences) + if (bHasCorrectReferences) { FinishStep(); } - }, 5.0f); + }, + 15.0f); } - - AddStep(TEXT("SpatialTestNetReferenceServerCleanup"), FWorkerDefinition::Server(1), nullptr, [this](ASpatialFunctionalTest* NetTest) { - // Possess the original pawn, so that the spawned character can get destroyed correctly + + AddStep(TEXT("SpatialTestNetReferenceServerCleanup"), FWorkerDefinition::Server(1), nullptr, [this]() { + // Possess the original pawn, so that other tests start from the expected, default set-up OriginalPawn.Key->Possess(OriginalPawn.Value); FinishStep(); @@ -225,6 +236,7 @@ void ASpatialTestNetReference::FinishTest(EFunctionalTestResult TestResult, cons { Super::FinishTest(TestResult, Message); - // Restoring the PositionUpdateFrequency here catches most but not all of the cases when the test failing would cause PositionUpdateFrequency to be changed. - GetMutableDefault()->PositionUpdateFrequency = PreviousPositionUpdateFrequency; + // Restoring the PositionUpdateThresholdMaxCentimeters here catches most but not all of the cases when the test failing would cause + // PositionUpdateThresholdMaxCentimeters to be changed. + GetMutableDefault()->PositionUpdateThresholdMaxCentimeters = PreviousMaximumDistanceThreshold; } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestNetReference/SpatialTestNetReference.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestNetReference/SpatialTestNetReference.h index 67def37fab..29511e5830 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestNetReference/SpatialTestNetReference.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestNetReference/SpatialTestNetReference.h @@ -16,18 +16,21 @@ class SPATIALGDKFUNCTIONALTESTS_API ASpatialTestNetReference : public ASpatialFu virtual void FinishTest(EFunctionalTestResult TestResult, const FString& Message) override; - virtual void BeginPlay() override; + virtual void PrepareTest() override; - // Array used to store the locations in which the character will perform the references check and the number of cubes that should be visible at that location + // Array used to store the locations in which the character will perform the references check and the number of cubes that should be + // visible at that location TArray> TestLocations; - // Helper array used to store the relative locations of the camera, so that it can see all cubes from every test location, used for visual debugging + // Helper array used to store the relative locations of the camera, so that it can see all cubes from every test location, used for + // visual debugging TArray CameraRelativeLocations; - // Helper rotator used to store the relative rotation of the camera so that it can see all cubes from every test location, used for visual debugging + // Helper rotator used to store the relative rotation of the camera so that it can see all cubes from every test location, used for + // visual debugging FRotator CameraRelativeRotation; TPair OriginalPawn; - float PreviousPositionUpdateFrequency; + float PreviousMaximumDistanceThreshold; }; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/ReplicatedStartupActor.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/ReplicatedStartupActor.cpp new file mode 100644 index 0000000000..e6513915ca --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/ReplicatedStartupActor.cpp @@ -0,0 +1,18 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "ReplicatedStartupActor.h" +#include "Net/UnrealNetwork.h" + +AReplicatedStartupActor::AReplicatedStartupActor() +{ + SetReplicateMovement(true); +} + +void AReplicatedStartupActor::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(AReplicatedStartupActor, TestIntProperty); + DOREPLIFETIME(AReplicatedStartupActor, TestArrayProperty); + DOREPLIFETIME(AReplicatedStartupActor, TestArrayStructProperty); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/ReplicatedStartupActor.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/ReplicatedStartupActor.h new file mode 100644 index 0000000000..8012317cb2 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/ReplicatedStartupActor.h @@ -0,0 +1,41 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h" +#include "ReplicatedStartupActor.generated.h" + +USTRUCT() +struct FTestStruct +{ + GENERATED_BODY() + + UPROPERTY() + int Int; +}; + +/** + * Helper actor used in SpatialTestReplicatedStartupActor. + * Contains 3 replicated properties: an int, an array of ints, and an array of FTestStruct. + */ + +UCLASS() +class AReplicatedStartupActor : public AReplicatedTestActorBase +{ + GENERATED_BODY() + +public: + AReplicatedStartupActor(); + + void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + UPROPERTY(Replicated) + int TestIntProperty; + + UPROPERTY(Replicated) + TArray TestArrayProperty; + + UPROPERTY(Replicated) + TArray TestArrayStructProperty; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/ReplicatedStartupActorGameMode.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/ReplicatedStartupActorGameMode.cpp index 5cba2f1f94..ec108e5fb7 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/ReplicatedStartupActorGameMode.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/ReplicatedStartupActorGameMode.cpp @@ -1,6 +1,5 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved - #include "ReplicatedStartupActorGameMode.h" #include "ReplicatedStartupActorPlayerController.h" diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/ReplicatedStartupActorPlayerController.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/ReplicatedStartupActorPlayerController.cpp index bf26beafd0..c7f345a22e 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/ReplicatedStartupActorPlayerController.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/ReplicatedStartupActorPlayerController.cpp @@ -1,10 +1,10 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved - #include "ReplicatedStartupActorPlayerController.h" #include "SpatialTestReplicatedStartupActor.h" -void AReplicatedStartupActorPlayerController::ClientToServerRPC_Implementation(ASpatialTestReplicatedStartupActor* Test, AActor* ReplicatedActor) +void AReplicatedStartupActorPlayerController::ClientToServerRPC_Implementation(ASpatialTestReplicatedStartupActor* Test, + AActor* ReplicatedActor) { if (IsValid(ReplicatedActor)) { diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/ReplicatedStartupActorPlayerController.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/ReplicatedStartupActorPlayerController.h index f0f4914b29..d0595c0d5d 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/ReplicatedStartupActorPlayerController.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/ReplicatedStartupActorPlayerController.h @@ -11,8 +11,8 @@ UCLASS() class AReplicatedStartupActorPlayerController : public APlayerController { GENERATED_BODY() - -public: + +public: UFUNCTION(Server, Reliable) void ClientToServerRPC(ASpatialTestReplicatedStartupActor* Test, AActor* ReplicatedActor); diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/SpatialTestReplicatedStartupActor.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/SpatialTestReplicatedStartupActor.cpp index 303abe8a84..8ad4924c8a 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/SpatialTestReplicatedStartupActor.cpp +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/SpatialTestReplicatedStartupActor.cpp @@ -1,36 +1,47 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved - #include "SpatialTestReplicatedStartupActor.h" + +#include "ReplicatedStartupActor.h" +#include "ReplicatedStartupActorPlayerController.h" #include "SpatialFunctionalTestFlowController.h" + #include "Kismet/GameplayStatics.h" -#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h" -#include "SpatialFunctionalTestFlowController.h" #include "Net/UnrealNetwork.h" -#include "ReplicatedStartupActorPlayerController.h" - /** - * This test automates the ReplicatedStartupActor gym. The gym was used for: - * - QA workflows Test Replicated startup actors are correctly spawned on all clients - * - To support QA test case "C1944 Replicated startup actors are correctly spawned on all clients" - * NOTE: This test requires a specific Map with a ReplicatedTestActorBase placed on the map and in the interest of the players and - * a custom GameMode and PlayerController, trying to run this test on a different Map will make it fail. + * This test automates the ReplicatedStartupActor gym. The gym was used to support QA test case "C1944 Replicated startup actors are + * correctly spawned on all clients". The test also covers the QA work-flow "Startup actors correctly replicate arbitrary properties". + * NOTE: 1. This test requires a specific Map with a ReplicatedStartupActor placed on the map and in the interest of the players and a + * custom GameMode and PlayerController, trying to run this test on a different Map will make it fail. + * 2. After UNR-3128 is solved, this test should be updated to also include the check for the case mentioned in the ticket. This would + * require applying the suggestions found in the last 2 steps of the test. + * + * The test contains two main phases: + * - Common Setup: + * - Each worker sets a reference to the ReplicatedStartupActor. + * - Phase 1: + * - Test: + * - Each client sends a server RPC from the ReplicatedStartupActor. + * - Each client tests that the server has a valid reference to its ReplicatedStartupActor. * - * The flow is as follows: - * - Setup: - * - Each client sets its reference to the replicated actor and sends a server RPC. - * - Test: - * - Each client tests that the server has a valid reference to its replicated actor. + * - Phase 2: + * - Test: + * - The server sets some default values for the replicated properties whilst the ReplicatedStartupActor is in view of the clients. + * - All workers check that the properties were replicated correctly. + * - The server moves the ReplicatedStartupActor out of view. + * - All workers check the movement is visible. + * - The server updates the replicated properties and moves the ReplicatedStartupActor back into the view of the clients. + * - All workers check that the ReplicatedStartupActor is in view and all its replicated properties were replicated correctly. + * - Common Clean-up: + * - None. */ ASpatialTestReplicatedStartupActor::ASpatialTestReplicatedStartupActor() : Super() { Author = "Andrei"; - Description = TEXT("Test Replicated Startup Actor"); - - bIsValidReference = false; + Description = TEXT("Test Replicated Startup Actor Reference And Property Replication"); } void ASpatialTestReplicatedStartupActor::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const @@ -40,40 +51,161 @@ void ASpatialTestReplicatedStartupActor::GetLifetimeReplicatedProps(TArray(GetLocalFlowController()->GetOwner()); - return IsValid(PlayerController); - }, - [this](ASpatialFunctionalTest* NetTest) - { - TArray ReplicatedActors; - UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase::StaticClass(), ReplicatedActors); + // Common Setup + + // All workers set a reference to the ReplicatedStartupActor. + AddStep( + TEXT("SpatialTestReplicatedStartupActorUniversalReferenceSetup"), FWorkerDefinition::AllWorkers, nullptr, nullptr, + [this](float DeltaTime) { + TArray ReplicatedStartupActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedStartupActor::StaticClass(), ReplicatedStartupActors); + + if (ReplicatedStartupActors.Num() == 1) + { + ReplicatedStartupActor = Cast(ReplicatedStartupActors[0]); + + // Reset the variables to allow for relevant consecutive runs of the same test. + ASpatialFunctionalTestFlowController* FlowController = GetLocalFlowController(); + if (FlowController->WorkerDefinition.Type == ESpatialFunctionalTestWorkerType::Client) + { + AReplicatedStartupActorPlayerController* PlayerController = + Cast(FlowController->GetOwner()); + PlayerController->ResetBoolean(this); + } + else + { + bIsValidReference = false; + } - checkf(ReplicatedActors.Num() == 1, TEXT("There should be exactly 1 replicated actor")); + FinishStep(); + } + }, + 5.0f); - AReplicatedStartupActorPlayerController* PlayerController = Cast(GetLocalFlowController()->GetOwner()); + // Phase 1 - PlayerController->ClientToServerRPC(this, ReplicatedActors[0]); + // All clients send a server RPC from the ReplicatedStartupActor. + AddStep( + TEXT("SpatialTestReplicatedStartupActorClientsSendRPC"), FWorkerDefinition::AllClients, nullptr, nullptr, + [this](float DeltaTime) { + AReplicatedStartupActorPlayerController* PlayerController = + Cast(GetLocalFlowController()->GetOwner()); - FinishStep(); - }); + // Make sure that the PlayerController has been set before trying to do anything with it, this might prevent Null Pointer + // exceptions being thrown when UE ticks at a relatively slow rate + if (IsValid(PlayerController)) + { + PlayerController->ClientToServerRPC(this, ReplicatedStartupActor); + FinishStep(); + } + }, + 5.0f); - AddStep(TEXT("SpatialTestReplicatedStarupActorClientsCheckStep"), FWorkerDefinition::AllClients, nullptr, nullptr, [this](ASpatialFunctionalTest* NetTest, float DeltaTime) - { + // All clients check that the RPC was received and correctly applied. + AddStep( + TEXT("SpatialTestReplicatedStarupActorClientsCheckRPC"), FWorkerDefinition::AllClients, nullptr, nullptr, + [this](float DeltaTime) { if (bIsValidReference) { - AssertTrue(bIsValidReference, TEXT("The server has a valid reference to this client's replicated actor")); + FinishStep(); + } + }, + 5.0f); + + // Phase 2 - AReplicatedStartupActorPlayerController* PlayerController = Cast(GetLocalFlowController()->GetOwner()); - PlayerController->ResetBoolean(this); + // The server sets default values for the replicated properties. + AddStep(TEXT("SpatialTestReplicatedStartupActorServerSetDefaultProperties"), FWorkerDefinition::Server(1), nullptr, [this]() { + ReplicatedStartupActor->TestIntProperty = 1; + ReplicatedStartupActor->TestArrayProperty.Empty(); + ReplicatedStartupActor->TestArrayProperty.Add(1); + + ReplicatedStartupActor->TestArrayStructProperty.Empty(); + ReplicatedStartupActor->TestArrayStructProperty.Add(FTestStruct{ 1 }); + + FinishStep(); + }); + + // All workers check that the properties were replicated correctly. + AddStep( + TEXT("SpatialTestReplicatedStartupActorAllWorkersCheckDefaultProperties"), FWorkerDefinition::AllWorkers, nullptr, nullptr, + [this](float DeltaTime) { + if (ReplicatedStartupActor->TestIntProperty == 1 && ReplicatedStartupActor->TestArrayProperty.Num() == 1 + && ReplicatedStartupActor->TestArrayProperty[0] == 1 && ReplicatedStartupActor->TestArrayStructProperty.Num() == 1 + && ReplicatedStartupActor->TestArrayStructProperty[0].Int == 1) + { + FinishStep(); + } + }, + 5.0f); + + // The server moves the ReplicatedStartupActor out of the clients' view. + AddStep(TEXT("SpatialTestReplicatedStartupActorServerMoveActorOutOfView"), FWorkerDefinition::Server(1), nullptr, [this]() { + ReplicatedStartupActor->SetActorLocation(FVector(15000.0f, 15000.0f, 50.0f)); + + FinishStep(); + }); + + // All workers check that the movement is visible. + AddStep( + TEXT("SpatialTestReplicatedStartupActorAllWorkersCheckMovement"), FWorkerDefinition::AllWorkers, nullptr, nullptr, + [this](float DeltaTime) { + // Make sure the Actor was moved out of view of the clients before updating its properties + if (ReplicatedStartupActor->GetActorLocation().Equals(FVector(15000.0f, 15000.0f, 50.0f), 1)) + { FinishStep(); } - }, 2.0); + }, + 5.0f); + + // The server updates the replicated properties whilst the ReplicatedStartupActor is out of the clients' view. + AddStep(TEXT("SpatialTestReplicatedStartupActorServerUpdateProperties"), FWorkerDefinition::Server(1), nullptr, [this]() { + ReplicatedStartupActor->TestIntProperty = 0; + + ReplicatedStartupActor->TestArrayProperty.Add(2); + + ReplicatedStartupActor->TestArrayStructProperty.Add(FTestStruct{ 2 }); + + /* TODO: After UNR-3128 is solved, replace the 2 uncommented lines above with the commented ones, also do the same in the next step. + ReplicatedStartupActor->TestArrayProperty.Empty(); + + ReplicatedStartupActor->TestArrayStructProperty.Empty(); + */ + + ReplicatedStartupActor->SetActorLocation(FVector(250.0f, -250.0f, 50.0f)); + + FinishStep(); + }); + + // All workers check that the ReplicatedStartupActor is back in view and that properties were replicated correctly. + AddStep( + TEXT("SpatialTestReplicatedStartupActorAllWorkersCheckModifiedProperties"), FWorkerDefinition::AllWorkers, nullptr, nullptr, + [this](float DeltaTime) { + if (ReplicatedStartupActor->GetActorLocation().Equals(FVector(250.0f, -250.0f, 50.0f), 1)) + { + if (ReplicatedStartupActor->TestIntProperty == 0 && ReplicatedStartupActor->TestArrayProperty.Num() == 2 + && ReplicatedStartupActor->TestArrayProperty[0] == 1 && ReplicatedStartupActor->TestArrayProperty[1] == 2 + && ReplicatedStartupActor->TestArrayStructProperty.Num() == 2 + && ReplicatedStartupActor->TestArrayStructProperty[0].Int == 1 + && ReplicatedStartupActor->TestArrayStructProperty[1].Int == 2) + { + FinishStep(); + } + + /* TODO: After UNR-3128 is solved, replace the if statement above with the commented version below. + if (ReplicatedStartupActor->TestIntProperty == 0 + && ReplicatedStartupActor->TestArrayProperty.Num() == 0 + && ReplicatedStartupActor->TestArrayStructProperty.Num() == 0) + { + FinishStep(); + } + */ + } + }, + 5.0f); } diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/SpatialTestReplicatedStartupActor.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/SpatialTestReplicatedStartupActor.h index 803206ba4f..c5189ef0b7 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/SpatialTestReplicatedStartupActor.h +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestReplicatedStartupActor/SpatialTestReplicatedStartupActor.h @@ -6,20 +6,22 @@ #include "SpatialFunctionalTest.h" #include "SpatialTestReplicatedStartupActor.generated.h" +class AReplicatedStartupActor; + UCLASS() class SPATIALGDKFUNCTIONALTESTS_API ASpatialTestReplicatedStartupActor : public ASpatialFunctionalTest { GENERATED_BODY() - -public: + +public: ASpatialTestReplicatedStartupActor(); - virtual void BeginPlay() override; + virtual void PrepareTest() override; void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; UPROPERTY(Replicated) bool bIsValidReference; - AActor* ReplicatedActor; + AReplicatedStartupActor* ReplicatedStartupActor; }; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestWorldComposition/SpatialTestWorldComposition.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestWorldComposition/SpatialTestWorldComposition.cpp new file mode 100644 index 0000000000..d916981206 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestWorldComposition/SpatialTestWorldComposition.cpp @@ -0,0 +1,153 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialTestWorldComposition.h" + +#include "GameFramework/PlayerController.h" +#include "SpatialFunctionalTestFlowController.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/InitiallyDormantTestActor.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h" + +#include "Kismet/GameplayStatics.h" + +/** + * This test automates the World Composition Gym which tested level loading and unloading. This test requires to be ran inside a custom map, + * trying to run it in another map will make the test fail. NOTE: Currently, the test will fail if ran with Native networking, due to the + * issue described in UNR-4066. + * + * The test includes 1 server and 1 client, with all test logic ran only by the Client. + * The flow is as follows: + * - Setup: + * - The Client sets a reference to its Pawn and resets the TestLocation index. + * - Test: + * - The test contains 2 runs of the same flow: + * - The Client moves its Pawn to each TestLocation. + * - The Client checks its Pawn has arrived at the correct location and checks if the corresponding level was loaded correctly, and all + * previously loaded levels were unloaded. + * - Clean-up: + * - No clean-up is required. + */ +ASpatialTestWorldComposition::ASpatialTestWorldComposition() + : Super() +{ + Author = "Andrei"; + Description = TEXT("Test World Composition"); + + TArray ExpectedConditionsAtStep0; + ExpectedConditionsAtStep0.Add(FExpectedActor{ FVector(330.0f, -1200.0f, 80.0f), AInitiallyDormantTestActor::StaticClass() }); + + TArray ExpectedConditionsAtStep1; + ExpectedConditionsAtStep1.Add(FExpectedActor{ FVector(330.0f, -400.0f, 80.0f), AReplicatedTestActorBase::StaticClass() }); + + TArray ExpectedConditionsAtStep2; + ExpectedConditionsAtStep2.Add(FExpectedActor{ FVector(330.0f, 400.0f, 80.0f), AReplicatedTestActorBase::StaticClass() }); + + TArray ExpectedConditionsAtStep3; + ExpectedConditionsAtStep3.Add(FExpectedActor{ FVector(330.0f, 1200.0f, 80.0f), AInitiallyDormantTestActor::StaticClass() }); + + TArray ExpectedConditionsAtStep4; + ExpectedConditionsAtStep4.Add(ExpectedConditionsAtStep1[0]); + ExpectedConditionsAtStep4.Add(ExpectedConditionsAtStep2[0]); + + TestStepsData.Add(TPair>(FVector(-150.0f, -1200.0f, 60.0f), ExpectedConditionsAtStep0)); + TestStepsData.Add(TPair>(FVector(-150.0f, -400.0f, 60.0f), ExpectedConditionsAtStep1)); + TestStepsData.Add(TPair>(FVector(-150.0f, 400.0f, 60.0f), ExpectedConditionsAtStep2)); + TestStepsData.Add(TPair>(FVector(-150.0f, 1200.0f, 60.0f), ExpectedConditionsAtStep3)); + TestStepsData.Add(TPair>(FVector(0.0f, 0.0f, 60.0f), ExpectedConditionsAtStep4)); + + SetNumRequiredClients(1); +} + +void ASpatialTestWorldComposition::PrepareTest() +{ + Super::PrepareTest(); + + // Step definition for Client 1 to move its Pawn and check if the levels loaded correctly. + FSpatialFunctionalTestStepDefinition ClientCheckLocationStepDefinition(/*bIsNativeDefinition*/ true); + ClientCheckLocationStepDefinition.StepName = TEXT("SpatialTestWorldCompositionClientCheckLocation"); + ClientCheckLocationStepDefinition.TimeLimit = 10.0f; + ClientCheckLocationStepDefinition.NativeStartEvent.BindLambda([this]() { + ClientOnePawn->SetActorLocation(TestStepsData[TestLocationIndex].Key); + }); + ClientCheckLocationStepDefinition.NativeTickEvent.BindLambda([this](float DeltaTime) { + if (IsCorrectAtLocation(TestLocationIndex)) + { + TestLocationIndex++; + FinishStep(); + } + }); + + // Run through each of the test locations twice to ensure that levels can be loaded and unloaded successfully multiple times. + for (int i = 0; i < 2; ++i) + { + // Setup step that sets a reference to the Pawn and resets the TestLocationIndex. + AddStep(TEXT("SpatialTestWorldCompositionClientSetupStep"), FWorkerDefinition::Client(1), nullptr, [this]() { + ASpatialFunctionalTestFlowController* FlowController = GetLocalFlowController(); + APlayerController* PlayerController = Cast(FlowController->GetOwner()); + ClientOnePawn = PlayerController->GetPawn(); + TestLocationIndex = 0; + + FinishStep(); + }); + + // Move the Pawn to the TestLocation with index 0 and test that the InitiallyDormantActorLevel was loaded correctly and the + // ReplicatedActorLevel and ReplicatedAndNetLoadOnClientLevel were unloaded. Note that since initially, the pawn spawns close to the + // origin, it will load the ReplicatedActorLevel and ReplicatedAndNetLoadOnClientLevel, failing to unload those will first be seen + // in this step. + AddStepFromDefinition(ClientCheckLocationStepDefinition, FWorkerDefinition::Client(1)); + + // Move the Pawn to the TestLocation with index 1 and test that the ReplicatedActorLevel was loaded correctly and the + // IntiallyDormantActorLevel was unloaded. + AddStepFromDefinition(ClientCheckLocationStepDefinition, FWorkerDefinition::Client(1)); + + // Move the Pawn to the TestLocation with index 2 and test that the ReplicatedAndNetLoadOnClientLevel was loaded correctly and the + // ReplicatedActorLevel was unloaded. + AddStepFromDefinition(ClientCheckLocationStepDefinition, FWorkerDefinition::Client(1)); + + // Move the Pawn to the TestLocation with index 3 and test that the InitiallyDormantAndNetLoadLevel was loaded correctly and the + // ReplicatedAndNetLoadOnClientLevel was unloaded. + AddStepFromDefinition(ClientCheckLocationStepDefinition, FWorkerDefinition::Client(1)); + + // Move the Pawn to the TestLocation with index 4 and test that both ReplicatedActorLevel and ReplicatedAndNetLoadOnClientLevel were + // loaded correctly, and that the InitiallyDormantAndNetLoadLevel was unloaded. + AddStepFromDefinition(ClientCheckLocationStepDefinition, FWorkerDefinition::Client(1)); + } +} + +bool ASpatialTestWorldComposition::IsCorrectAtLocation(int TestLocation) +{ + // Check that the movement was correctly applied before checking if levels loaded correctly. + if (!ClientOnePawn->GetActorLocation().Equals(TestStepsData[TestLocation].Key, 1.0f)) + { + return false; + } + + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedTestActorBase::StaticClass(), FoundReplicatedBaseActors); + TArray ExpectedLocationConditions = TestStepsData[TestLocation].Value; + + if (ExpectedLocationConditions.Num() != FoundReplicatedBaseActors.Num()) + { + return false; + } + + int CorrectActors = 0; + + for (AActor* FoundActor : FoundReplicatedBaseActors) + { + for (auto Condition : ExpectedLocationConditions) + { + if (FoundActor->GetActorLocation().Equals(Condition.ExpectedActorLocation, 1.0f) + && FoundActor->IsA(Condition.ExpectedActorClass)) + { + CorrectActors++; + break; + } + } + } + + if (CorrectActors != ExpectedLocationConditions.Num()) + { + return false; + } + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestWorldComposition/SpatialTestWorldComposition.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestWorldComposition/SpatialTestWorldComposition.h new file mode 100644 index 0000000000..1d1576007a --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/UNR-3761/SpatialTestWorldComposition/SpatialTestWorldComposition.h @@ -0,0 +1,46 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialFunctionalTest.h" +#include "SpatialTestWorldComposition.generated.h" + +USTRUCT() +/** + * Struct used to store the expected location and class of an Actor. + */ +struct FExpectedActor +{ + GENERATED_BODY() + + UPROPERTY() + FVector ExpectedActorLocation; + + UPROPERTY() + UClass* ExpectedActorClass; +}; + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API ASpatialTestWorldComposition : public ASpatialFunctionalTest +{ + GENERATED_BODY() + +public: + ASpatialTestWorldComposition(); + + virtual void PrepareTest() override; + + bool IsCorrectAtLocation(int TestLocation); + + // Helper array used to store all the references returned by GetAllActorsOfClass. + TArray FoundReplicatedBaseActors; + + // Array storing the Pawn's testing locations and the Actors that must be present at every location for the test to pass. + TArray>> TestStepsData; + + // A reference to the Client's Pawn to avoid code duplication. + APawn* ClientOnePawn; + + int TestLocationIndex; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/VisibilityTest/ReplicatedVisibilityTestActor.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/VisibilityTest/ReplicatedVisibilityTestActor.cpp new file mode 100644 index 0000000000..41c23b6dcc --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/VisibilityTest/ReplicatedVisibilityTestActor.cpp @@ -0,0 +1,10 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "ReplicatedVisibilityTestActor.h" + +AReplicatedVisibilityTestActor::AReplicatedVisibilityTestActor() +{ + bNetLoadOnClient = false; + bNetLoadOnNonAuthServer = true; + SetActorEnableCollision(false); +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/VisibilityTest/ReplicatedVisibilityTestActor.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/VisibilityTest/ReplicatedVisibilityTestActor.h new file mode 100644 index 0000000000..8e0cb139ad --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/VisibilityTest/ReplicatedVisibilityTestActor.h @@ -0,0 +1,16 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/ReplicatedTestActorBase.h" +#include "ReplicatedVisibilityTestActor.generated.h" + +UCLASS() +class AReplicatedVisibilityTestActor : public AReplicatedTestActorBase +{ + GENERATED_BODY() + +public: + AReplicatedVisibilityTestActor(); +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/VisibilityTest/VisibilityTest.cpp b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/VisibilityTest/VisibilityTest.cpp new file mode 100644 index 0000000000..a28ecacca4 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/VisibilityTest/VisibilityTest.cpp @@ -0,0 +1,262 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "VisibilityTest.h" +#include "ReplicatedVisibilityTestActor.h" +#include "SpatialFunctionalTestFlowController.h" +#include "SpatialGDKFunctionalTests/SpatialGDK/TestActors/TestMovementCharacter.h" + +#include "GameFramework/PlayerController.h" +#include "Kismet/GameplayStatics.h" + +/** + * This test tests if a bHidden Actor is replicating properly to Server and Clients. + * + * The test includes a single server and two client workers. + * The flow is as follows: + * - Setup: + * - One cube actor already placed in the level at Location FVector(0.0f, 0.0f, 80.0f). + * - The Server spawns a TestMovementCharacter and makes Client 1 possess it. + * - Test: + * - Each worker tests if it can initially see the AReplicatedVisibilityTestActor. + * - After ensuring possession happened, the Server moves Client 1's Character to a remote location, so it cannot see the + *AReplicatedVisibilityTestActor. + * - After ensuring movement replicated correctly, Client 1 checks it can no longer see the AReplicatedVisibilityTestActor. + * - The Server sets the AReplicatedVisibilityTestActor to hidden. + * - All Clients check they can no longer see the AReplicatedVisibilityTestActor. + * - The Server moves the character of Client 1 back close to its spawn location, so that the AReplicatedVisibilityTestActor is in its + *interest area. + * - All Clients check they can still not see the AReplicatedVisibilityTestActor. + * - The Server sets the AReplicatedVisibilityTestActor to not be hidden. + * - All Clients check they can now see the AReplicatedVisibilityTestActor. + * - Cleanup: + * - Client 1 repossesses its default pawn. + * - The spawned Character is destroyed. + */ + +const static float StepTimeLimit = 10.0f; + +AVisibilityTest::AVisibilityTest() + : Super() +{ + Author = "Evi"; + Description = TEXT("Test Actor Visibility"); + + CharacterSpawnLocation = FVector(0.0f, 120.0f, 40.0f); + CharacterRemoteLocation = FVector(20000.0f, 20000.0f, 50.0f); +} + +int AVisibilityTest::GetNumberOfVisibilityTestActors() +{ + int Counter = 0; + for (TActorIterator Iter(GetWorld()); Iter; ++Iter) + { + Counter++; + } + + return Counter; +} + +void AVisibilityTest::PrepareTest() +{ + Super::PrepareTest(); + + { // Step 0 - The server spawn a TestMovementCharacter and makes Client 1 possess it. + AddStep(TEXT("VisibilityTestServerSetup"), FWorkerDefinition::Server(1), nullptr, [this]() { + ASpatialFunctionalTestFlowController* ClientOneFlowController = GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1); + APlayerController* PlayerController = Cast(ClientOneFlowController->GetOwner()); + + if (IsValid(PlayerController)) + { + ClientOneSpawnedPawn = + GetWorld()->SpawnActor(CharacterSpawnLocation, FRotator::ZeroRotator, FActorSpawnParameters()); + RegisterAutoDestroyActor(ClientOneSpawnedPawn); + + ClientOneDefaultPawn = PlayerController->GetPawn(); + + PlayerController->Possess(ClientOneSpawnedPawn); + + FinishStep(); + } + }); + } + + { // Step 1 - All workers check if they have one ReplicatedVisibilityTestActor in the world, and set a reference to it. + AddStep( + TEXT("VisibilityTestAllWorkersCheckVisibility"), FWorkerDefinition::AllWorkers, nullptr, nullptr, + [this](float DeltaTime) { + TArray FoundActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AReplicatedVisibilityTestActor::StaticClass(), FoundActors); + + if (FoundActors.Num() == 1) + { + TestActor = Cast(FoundActors[0]); + + if (IsValid(TestActor)) + { + FinishStep(); + } + } + }, + StepTimeLimit); + } + + { // Step 2 - Client 1 checks if it has correctly possessed the TestMovementCharacter. + AddStep( + TEXT("VisibilityTestClientCheckPossesion"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this](float DeltaTime) { + ASpatialFunctionalTestFlowController* FlowController = GetLocalFlowController(); + APlayerController* PlayerController = Cast(FlowController->GetOwner()); + ATestMovementCharacter* PlayerCharacter = Cast(PlayerController->GetPawn()); + if (IsValid(PlayerController)) + { + if (PlayerCharacter == PlayerController->AcknowledgedPawn) + { + FinishStep(); + } + } + }, + StepTimeLimit); + } + + { // Step 4 - Server moves the TestMovementCharacter of Client 1 to a remote location, so that it does not see the + // AReplicatedVisibilityTestActor. + AddStep(TEXT("VisibilityTestServerMoveClient1"), FWorkerDefinition::Server(1), nullptr, [this]() { + if (ClientOneSpawnedPawn->SetActorLocation(CharacterRemoteLocation)) + { + if (ClientOneSpawnedPawn->GetActorLocation().Equals(CharacterRemoteLocation, 1.0f)) + { + FinishStep(); + } + } + }); + } + + { // Step 5 - Client 1 makes sure that the movement was correctly replicated + AddStep( + TEXT("VisibilityTestClientCheckFirstMovement"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this](float DeltaTime) { + ASpatialFunctionalTestFlowController* FlowController = GetLocalFlowController(); + APlayerController* PlayerController = Cast(FlowController->GetOwner()); + ATestMovementCharacter* PlayerCharacter = Cast(PlayerController->GetPawn()); + + if (IsValid(PlayerCharacter)) + { + if (PlayerCharacter->GetActorLocation().Equals(CharacterRemoteLocation, 1.0f)) + { + FinishStep(); + } + } + }, + StepTimeLimit); + } + + { // Step 6 - Client 1 checks that it can no longer see the AReplicatedVisibilityTestActor. + AddStep( + TEXT("VisibilityTestClient1CheckReplicatedActorsBeforeActorHidden"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this](float DeltaTime) { + if (GetNumberOfVisibilityTestActors() == 0 && !IsValid(TestActor)) + { + FinishStep(); + } + }, + StepTimeLimit); + } + + { // Step 7 - Server Set AReplicatedVisibilityTestActor to be hidden. + AddStep(TEXT("VisibilityTestServerSetActorHidden"), FWorkerDefinition::Server(1), nullptr, [this]() { + if (IsValid(TestActor)) + { + TestActor->SetHidden(true); + FinishStep(); + } + }); + } + + { // Step 8 - Clients check that the AReplicatedVisibilityTestActor is no longer replicated. + AddStep(TEXT("VisibilityTestClientCheckReplicatedActorsAfterSetActorHidden"), FWorkerDefinition::AllClients, nullptr, nullptr, + [this](float DeltaTime) { + if (GetNumberOfVisibilityTestActors() == 0 && !IsValid(TestActor)) + { + FinishStep(); + } + }); + } + + { // Step 9 - Server moves Client 1 close to the cube. + AddStep(TEXT("VisibilityTestServerMoveClient1CloseToCube"), FWorkerDefinition::Server(1), nullptr, [this]() { + if (ClientOneSpawnedPawn->SetActorLocation(CharacterSpawnLocation)) + { + if (ClientOneSpawnedPawn->GetActorLocation().Equals(CharacterSpawnLocation, 1.0f)) + { + FinishStep(); + } + } + }); + } + + { // Step 10 - Client 1 checks that the movement was replicated correctly. + AddStep( + TEXT("VisibilityTestClientCheckSecondMovement"), FWorkerDefinition::Client(1), nullptr, nullptr, + [this](float DeltaTime) { + ASpatialFunctionalTestFlowController* FlowController = GetLocalFlowController(); + APlayerController* PlayerController = Cast(FlowController->GetOwner()); + ATestMovementCharacter* PlayerCharacter = Cast(PlayerController->GetPawn()); + + if (IsValid(PlayerCharacter)) + { + if (PlayerCharacter->GetActorLocation().Equals(CharacterSpawnLocation, 1.0f)) + { + FinishStep(); + } + } + }, + StepTimeLimit); + } + + { // Step 11 - Clients check that they can still not see the AReplicatedVisibilityTestActor + AddStep( + TEXT("VisibilityTestClientCheckFinalReplicatedActors"), FWorkerDefinition::AllClients, nullptr, nullptr, + [this](float DeltaTime) { + if (GetNumberOfVisibilityTestActors() == 0 && !IsValid(TestActor)) + { + FinishStep(); + } + }, + StepTimeLimit); + } + + { // Step 12 - Server Set AReplicatedVisibilityTestActor to not be hidden anymore. + AddStep(TEXT("VisibilityTestServerSetActorNotHidden"), FWorkerDefinition::Server(1), nullptr, [this]() { + if (IsValid(TestActor)) + { + TestActor->SetHidden(false); + FinishStep(); + } + }); + } + + { // Step 13 - Clients check that the AReplicatedVisibilityTestActor is being replicated again. + AddStep(TEXT("VisibilityTestClientCheckFinalReplicatedNonHiddenActors"), FWorkerDefinition::AllClients, nullptr, nullptr, + [this](float DeltaTime) { + if (GetNumberOfVisibilityTestActors() == 1) + { + FinishStep(); + } + }); + } + + { // Step 14 - Server Cleanup. + AddStep(TEXT("VisibilityTestServerCleanup"), FWorkerDefinition::Server(1), nullptr, [this]() { + // Possess the original pawn, so that the spawned character can get destroyed correctly + ASpatialFunctionalTestFlowController* ClientOneFlowController = GetFlowController(ESpatialFunctionalTestWorkerType::Client, 1); + APlayerController* PlayerController = Cast(ClientOneFlowController->GetOwner()); + + if (IsValid(PlayerController)) + { + PlayerController->Possess(ClientOneDefaultPawn); + + FinishStep(); + } + }); + } +} diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/VisibilityTest/VisibilityTest.h b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/VisibilityTest/VisibilityTest.h new file mode 100644 index 0000000000..a2a8aa35b6 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDK/VisibilityTest/VisibilityTest.h @@ -0,0 +1,36 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialFunctionalTest.h" +#include "VisibilityTest.generated.h" + +class ATestMovementCharacter; +class AReplicatedVisibilityTestActor; + +UCLASS() +class SPATIALGDKFUNCTIONALTESTS_API AVisibilityTest : public ASpatialFunctionalTest +{ + GENERATED_BODY() + +public: + AVisibilityTest(); + + virtual void PrepareTest() override; + + int GetNumberOfVisibilityTestActors(); + + // A reference to the Default Pawn of Client 1 to allow for repossession in the final step of the test. + APawn* ClientOneDefaultPawn; + + ATestMovementCharacter* ClientOneSpawnedPawn; + + AReplicatedVisibilityTestActor* TestActor; + + // The spawn location for Client 1's Pawn; + FVector CharacterSpawnLocation; + + // A remote location where Client 1's Pawn will be moved in order to not see the AReplicatedVisibilityTestActor. + FVector CharacterRemoteLocation; +}; diff --git a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDKFunctionalTests.Build.cs b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDKFunctionalTests.Build.cs index 4d5795b1b3..1a52a22f4b 100644 --- a/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDKFunctionalTests.Build.cs +++ b/SpatialGDK/Source/SpatialGDKFunctionalTests/SpatialGDKFunctionalTests.Build.cs @@ -19,10 +19,12 @@ public SpatialGDKFunctionalTests(ReadOnlyTargetRules Target) : base(Target) PrivateDependencyModuleNames.AddRange( new string[] { "SpatialGDK", + "SpatialGDKServices", "Core", "CoreUObject", "Engine", - "FunctionalTesting" + "FunctionalTesting", + "HTTP" }); } } diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp index 178ad0b069..62733ece7b 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp +++ b/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp @@ -6,15 +6,23 @@ #include "Async/Async.h" #include "DirectoryWatcherModule.h" #include "Editor.h" +#include "Engine/World.h" #include "FileCache.h" #include "GeneralProjectSettings.h" -#include "Internationalization/Regex.h" -#include "Internationalization/Internationalization.h" +#include "HAL/FileManagerGeneric.h" +#include "HttpModule.h" #include "IPAddress.h" +#include "Improbable/SpatialGDKSettingsBridge.h" +#include "Interfaces/IHttpResponse.h" +#include "Internationalization/Internationalization.h" +#include "Internationalization/Regex.h" #include "Json/Public/Dom/JsonObject.h" +#include "Misc/FileHelper.h" #include "Misc/MessageDialog.h" -#include "Sockets.h" +#include "Misc/MonitoredProcess.h" +#include "SSpatialOutputLog.h" #include "SocketSubsystem.h" +#include "Sockets.h" #include "SpatialCommandUtils.h" #include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesModule.h" @@ -24,38 +32,15 @@ DEFINE_LOG_CATEGORY(LogSpatialDeploymentManager); #define LOCTEXT_NAMESPACE "FLocalDeploymentManager" -static const FString SpatialServiceVersion(TEXT("20200611.170527.924b1f1c45")); - FLocalDeploymentManager::FLocalDeploymentManager() : bLocalDeploymentRunning(false) - , bSpatialServiceRunning(false) - , bSpatialServiceInProjectDirectory(false) , bStartingDeployment(false) , bStoppingDeployment(false) - , bStartingSpatialService(false) - , bStoppingSpatialService(false) { } void FLocalDeploymentManager::PreInit(bool bChinaEnabled) { - bIsInChina = bChinaEnabled; - // Don't kick off background processes when running commandlets - 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) - { - if (!bSpatialServicesAvailable) - { - 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(); @@ -63,32 +48,12 @@ void FLocalDeploymentManager::PreInit(bool bChinaEnabled) StartUpWorkerConfigDirectoryWatcher(); } -void FLocalDeploymentManager::Init(FString RuntimeIPToExpose) +void FLocalDeploymentManager::Init() { - 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.")); - - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, 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()->UsesSpatialNetworking()) - { - TryStartSpatialService(RuntimeIPToExpose); - } - else - { - UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("SpatialOS daemon not started because spatial networking is disabled.")); - } - - // Ensure we have an up to date state of the spatial service and local deployment. - RefreshServiceStatus(); - }); - } + // Kill any existing runtime processes. + // We cannot attach to old runtime processes as they may be 'zombie' and not killable (even if they are not blocking ports). + // Usually caused by a driver bug, see: https://stackoverflow.com/questions/49988/really-killing-a-process-in-windows + SpatialCommandUtils::TryKillProcessWithName(SpatialGDKServicesConstants::RuntimeExe); } void FLocalDeploymentManager::StartUpWorkerConfigDirectoryWatcher() @@ -101,29 +66,42 @@ void FLocalDeploymentManager::StartUpWorkerConfigDirectoryWatcher() if (FPaths::DirectoryExists(WorkerConfigDirectory)) { - WorkerConfigDirectoryChangedDelegate = IDirectoryWatcher::FDirectoryChanged::CreateRaw(this, &FLocalDeploymentManager::OnWorkerConfigDirectoryChanged); - DirectoryWatcher->RegisterDirectoryChangedCallback_Handle(WorkerConfigDirectory, WorkerConfigDirectoryChangedDelegate, WorkerConfigDirectoryChangedDelegateHandle); + WorkerConfigDirectoryChangedDelegate = + IDirectoryWatcher::FDirectoryChanged::CreateRaw(this, &FLocalDeploymentManager::OnWorkerConfigDirectoryChanged); + DirectoryWatcher->RegisterDirectoryChangedCallback_Handle(WorkerConfigDirectory, WorkerConfigDirectoryChangedDelegate, + WorkerConfigDirectoryChangedDelegateHandle); } else { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Worker config directory does not exist! Please ensure you have your worker configurations at %s"), *WorkerConfigDirectory); + UE_LOG(LogSpatialDeploymentManager, Error, + TEXT("Worker config directory does not exist! Please ensure you have your worker configurations at %s"), + *WorkerConfigDirectory); } } } void FLocalDeploymentManager::OnWorkerConfigDirectoryChanged(const TArray& FileChanges) { - UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Worker config files updated. Regenerating worker descriptors ('spatial worker build build-config').")); - WorkerBuildConfigAsync(); + const bool ShouldRebuild = FileChanges.ContainsByPredicate([](FFileChangeData& FileChange) { + return FileChange.Filename.EndsWith(".worker.json"); + }); + + if (ShouldRebuild) + { + UE_LOG(LogSpatialDeploymentManager, Log, + TEXT("Worker config files updated. Regenerating worker descriptors ('spatial worker build build-config').")); + + WorkerBuildConfigAsync(); + } } void FLocalDeploymentManager::WorkerBuildConfigAsync() { - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] - { + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] { FString WorkerBuildConfigResult; int32 ExitCode; - bool bSuccess = SpatialCommandUtils::BuildWorkerConfig(bIsInChina, SpatialGDKServicesConstants::SpatialOSDirectory, WorkerBuildConfigResult, ExitCode); + bool bSuccess = SpatialCommandUtils::BuildWorkerConfig(bIsInChina, SpatialGDKServicesConstants::SpatialOSDirectory, + WorkerBuildConfigResult, ExitCode); if (bSuccess) { @@ -131,40 +109,13 @@ void FLocalDeploymentManager::WorkerBuildConfigAsync() } else { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Building worker configurations failed. Please ensure your .worker.json files are correct. Result: %s"), *WorkerBuildConfigResult); + UE_LOG(LogSpatialDeploymentManager, Error, + TEXT("Building worker configurations failed. Please ensure your .worker.json files are correct. Result: %s"), + *WorkerBuildConfigResult); } }); } -void FLocalDeploymentManager::RefreshServiceStatus() -{ - if(!bLocalDeploymentManagerEnabled) - { - return; - } - - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] - { - IsServiceRunningAndInCorrectDirectory(); - GetLocalDeploymentStatus(); - - // Timers must be started on the game thread. - AsyncTask(ENamedThreads::GameThread, [this] - { - // It's possible that GEditor won't exist when shutting down. - if (GEditor != nullptr) - { - // Start checking for the service status. - FTimerHandle RefreshTimer; - GEditor->GetTimerManager()->SetTimer(RefreshTimer, [this]() - { - RefreshServiceStatus(); - }, RefreshFrequency, false); - } - }); - }); -} - bool FLocalDeploymentManager::CheckIfPortIsBound(int32 Port) { ISocketSubsystem* SocketSubsystem = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM); @@ -191,12 +142,14 @@ bool FLocalDeploymentManager::CheckIfPortIsBound(int32 Port) // Bind to our listen port. if (ListenSocket->Bind(*ListenAddr)) { - bCanBindToPort = ListenSocket->Listen(0 /* MaxBacklog*/ ); + bCanBindToPort = ListenSocket->Listen(0 /* MaxBacklog*/); ListenSocket->Close(); } else { - UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Failed to bind listen socket to addr (%s) for %s, the port is likely in use"), *ListenAddr->ToString(true), *SocketName); + UE_LOG(LogSpatialDeploymentManager, Verbose, + TEXT("Failed to bind listen socket to addr (%s) for %s, the port is likely in use"), *ListenAddr->ToString(true), + *SocketName); } } else @@ -227,125 +180,38 @@ bool FLocalDeploymentManager::LocalDeploymentPreRunChecks() { bool bSuccess = true; - // Check for the known runtime port (5301) which could be blocked. - if (CheckIfPortIsBound(RequiredRuntimePort)) - { - // If it exists offer the user the ability to kill it. - if (FMessageDialog::Open(EAppMsgType::YesNo, LOCTEXT("KillPortBlockingProcess", "A required port is blocked by another process (potentially by an old deployment). Would you like to kill this process?")) == EAppReturnType::Yes) - { - bSuccess = KillProcessBlockingPort(RequiredRuntimePort); - } - else - { - bSuccess = false; - } - } - - if (!bSpatialServiceInProjectDirectory && bSpatialServiceRunning) - { - 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 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\" --runtime-version=%s %s"), *LaunchConfig, *FSpatialGDKServicesModule::GetProjectName(), *SnapshotName, *RuntimeVersion, *LaunchArgs); - - FDateTime SpotCreateStart = FDateTime::Now(); - - FString SpotCreateResult; - FString StdErr; - int32 ExitCode; - FPlatformProcess::ExecProcess(*SpatialGDKServicesConstants::SpotExe, *SpotCreateArgs, &ExitCode, &SpotCreateResult, &StdErr); - bStartingDeployment = false; - - if (ExitCode != ExitCodeSuccess) - { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Creation of local deployment failed. Result: %s - Error: %s"), *SpotCreateResult, *StdErr); - return false; - } - - bool bSuccess = false; + // Check for the known runtime ports which could be blocked by other processes. + TArray RequiredRuntimePorts = { RequiredRuntimePort, WorkerPort, HTTPPort, SpatialGDKServicesConstants::RuntimeGRPCPort }; - TSharedPtr SpotJsonResult; - bool bParsingSuccess = FSpatialGDKServicesModule::ParseJson(SpotCreateResult, SpotJsonResult); - if (!bParsingSuccess) + for (int32 RuntimePort : RequiredRuntimePorts) { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Json parsing of spot create result failed. Result: %s"), *SpotCreateResult); - } - - const TSharedPtr* SpotJsonContent = nullptr; - if (bParsingSuccess && !SpotJsonResult->TryGetObjectField(TEXT("content"), SpotJsonContent)) - { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("'content' does not exist in Json result from 'spot create': %s"), *SpotCreateResult); - bParsingSuccess = false; - } - - FString DeploymentStatus; - if (bParsingSuccess && SpotJsonContent->Get()->TryGetStringField(TEXT("status"), DeploymentStatus)) - { - if (DeploymentStatus == TEXT("RUNNING")) + if (CheckIfPortIsBound(RequiredRuntimePort)) { - FString DeploymentID = SpotJsonContent->Get()->GetStringField(TEXT("id")); - LocalRunningDeploymentID = DeploymentID; - bLocalDeploymentRunning = true; - - FDateTime SpotCreateEnd = FDateTime::Now(); - FTimespan Span = SpotCreateEnd - SpotCreateStart; - - AsyncTask(ENamedThreads::GameThread, [this] + // If it exists offer the user the ability to kill it. + FText DialogMessage = LOCTEXT("KillPortBlockingProcess", + "A required port is blocked by another process (potentially by an old " + "deployment). Would you like to kill this process?"); + if (FMessageDialog::Open(EAppMsgType::YesNo, DialogMessage) == EAppReturnType::Yes) { - 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. Please check the 'Spatial Output' window for more details."), *DeploymentStatus); + bSuccess &= KillProcessBlockingPort(RequiredRuntimePort); + } + else + { + bSuccess = false; + } } } - else - { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("'status' does not exist in Json result from 'spot create': %s"), *SpotCreateResult); - } - return true; + return bSuccess; } -void FLocalDeploymentManager::TryStartLocalDeployment(FString LaunchConfig, FString RuntimeVersion, 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; - } - + RuntimeStartTime = FDateTime::Now(); bRedeployRequired = false; - if (bStoppingDeployment) - { - UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Local deployment is in the process of stopping. New deployment will start when previous one has stopped.")); - while (bStoppingDeployment) - { - FPlatformProcess::Sleep(0.1f); - } - } - if (bLocalDeploymentRunning) { UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Tried to start a local deployment but one is already running.")); @@ -358,7 +224,8 @@ void FLocalDeploymentManager::TryStartLocalDeployment(FString LaunchConfig, FStr if (!LocalDeploymentPreRunChecks()) { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Tried to start a local deployment but a required port is already bound by another process.")); + UE_LOG(LogSpatialDeploymentManager, Error, + TEXT("Tried to start a local deployment but a required port is already bound by another process.")); if (CallBack) { CallBack(false); @@ -366,336 +233,159 @@ void FLocalDeploymentManager::TryStartLocalDeployment(FString LaunchConfig, FStr return; } - LocalRunningDeploymentID.Empty(); - bStartingDeployment = true; - // Stop the currently running service if the runtime IP is to be exposed, but is different from the one specified - if (ExposedRuntimeIP != RuntimeIPToExpose) - { - UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Settings for exposing runtime IP have changed since service startup. Restarting service to reflect changes.")); - TryStopSpatialService(); - } + FString SchemaBundle = SpatialGDKServicesConstants::SchemaBundlePath; - // If the service is not running then start it. - if (!bSpatialServiceRunning) - { - TryStartSpatialService(RuntimeIPToExpose); - } + // Give the snapshot path a timestamp to ensure we don't overwrite snapshots from older deployments. + // The snapshot service saves snapshots with the name `snapshot-n.snapshot` for a given deployment, + // where 'n' is the number of snapshots taken since starting the deployment. + FString SnapshotPath = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSSnapshotFolderPath, *RuntimeStartTime.ToString()); - SnapshotName.RemoveFromEnd(TEXT(".snapshot")); + // Create the folder for storing the snapshots. + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + PlatformFile.CreateDirectoryTree(*SnapshotPath); + // Use the runtime start timestamp as the log directory, e.g. `/spatial/localdeployment//` + FString LocalDeploymentLogsDir = FPaths::Combine(SpatialGDKServicesConstants::LocalDeploymentLogsDir, RuntimeStartTime.ToString()); - AttemptSpatialAuthResult = Async(EAsyncExecution::Thread, [this]() { return SpatialCommandUtils::AttemptSpatialAuth(bIsInChina); }, - [this, LaunchConfig, RuntimeVersion, LaunchArgs, SnapshotName, RuntimeIPToExpose, CallBack]() - { - bool bSuccess = AttemptSpatialAuthResult.IsReady() && AttemptSpatialAuthResult.Get() == true; - if (bSuccess) - { - bSuccess = FinishLocalDeployment(LaunchConfig, RuntimeVersion, LaunchArgs, SnapshotName, RuntimeIPToExpose); - } - else - { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Failed to authenticate against SpatialOS while attempting to start a local deployment.")); - } - bStartingDeployment = false; + // runtime.exe --config=squid_config.json --snapshot=snapshots/default.snapshot --worker-port 8018 --http-port 5006 --grpc-port 7777 + // --worker-external-host 127.0.0.1 --snapshots-directory=spatial/snapshots/ + // --schema-bundle=spatial/build/assembly/schema/schema.sb + // --event-tracing-logs-directory=`/spatial/localdeployment//` + FString RuntimeArgs = + FString::Printf(TEXT("--config=\"%s\" --snapshot=\"%s\" --worker-port %s --http-port=%s --grpc-port=%s " + "--snapshots-directory=\"%s\" --schema-bundle=\"%s\" --event-tracing-logs-directory=\"%s\" %s"), + *LaunchConfig, *SnapshotName, *FString::FromInt(WorkerPort), *FString::FromInt(HTTPPort), + *FString::FromInt(SpatialGDKServicesConstants::RuntimeGRPCPort), *SnapshotPath, *SchemaBundle, + *LocalDeploymentLogsDir, *LaunchArgs); - if (CallBack) - { - CallBack(bSuccess); - } - }); - - return; -} - -bool FLocalDeploymentManager::TryStopLocalDeployment() -{ - if (!bLocalDeploymentRunning || LocalRunningDeploymentID.IsEmpty()) + if (!RuntimeIPToExpose.IsEmpty()) { - UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Tried to stop local deployment but no active deployment exists.")); - return false; + RuntimeArgs.Append(FString::Printf(TEXT(" --worker-external-host %s"), *RuntimeIPToExpose)); } - bStoppingDeployment = true; + // Setup the runtime file logger. + SetupRuntimeFileLogger(LocalDeploymentLogsDir); - FString SpotDeleteArgs = FString::Printf(TEXT("alpha deployment delete --id=%s --json"), *LocalRunningDeploymentID); + FString RuntimePath = SpatialGDKServicesConstants::GetRuntimeExecutablePath(RuntimeVersion); - FString SpotDeleteResult; - FString StdErr; - int32 ExitCode; - FPlatformProcess::ExecProcess(*SpatialGDKServicesConstants::SpotExe, *SpotDeleteArgs, &ExitCode, &SpotDeleteResult, &StdErr); - bStoppingDeployment = false; + RuntimeProcess = { *RuntimePath, *RuntimeArgs, SpatialGDKServicesConstants::SpatialOSDirectory, /*InHidden*/ true, + /*InCreatePipes*/ true }; - if (ExitCode != ExitCodeSuccess) - { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Failed to stop local deployment! Result: %s - Error: %s"), *SpotDeleteResult, *StdErr); - } + FSpatialGDKServicesModule& GDKServices = FModuleManager::GetModuleChecked("SpatialGDKServices"); + TWeakPtr SpatialOutputLog = GDKServices.GetSpatialOutputLog(); - bool bSuccess = false; + RuntimeProcess->OnOutput().BindLambda([&RuntimeLogFileHandle = RuntimeLogFileHandle, &bStartingDeployment = bStartingDeployment, + SpatialOutputLog](const FString& Output) { + if (SpatialOutputLog.IsValid()) + { + // Format and output the log to the editor window `SpatialOutputLog` + SpatialOutputLog.Pin()->FormatAndPrintRawLogLine(Output); + } - TSharedPtr SpotJsonResult; - bool bParsingSuccess = FSpatialGDKServicesModule::ParseJson(SpotDeleteResult, SpotJsonResult); - if (!bParsingSuccess) - { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Json parsing of spot delete result failed. Result: %s"), *SpotDeleteResult); - } + // Save the raw runtime output to disk. + if (RuntimeLogFileHandle.IsValid()) + { + // In order to get the correct length of the ANSI converted string, we must create the converted string here. + auto OutputANSI = StringCast(*Output); - const TSharedPtr* SpotJsonContent = nullptr; - if (bParsingSuccess && !SpotJsonResult->TryGetObjectField(TEXT("content"), SpotJsonContent)) - { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("'content' does not exist in Json result from 'spot delete': %s"), *SpotDeleteResult); - bParsingSuccess = false; - } + RuntimeLogFileHandle->Write((const uint8*)OutputANSI.Get(), OutputANSI.Length()); - FString DeploymentStatus; - if (bParsingSuccess && SpotJsonContent->Get()->TryGetStringField(TEXT("status"), DeploymentStatus)) - { - if (DeploymentStatus == TEXT("STOPPED")) - { - UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Successfully stopped local deplyoment")); - LocalRunningDeploymentID.Empty(); - bLocalDeploymentRunning = false; - bSuccess = true; + // Always add a newline + RuntimeLogFileHandle->Write((const uint8*)LINE_TERMINATOR_ANSI, 1); } - else + + // Timeout detection. + if (bStartingDeployment && Output.Contains(TEXT("startup completed"))) { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Stopping local deployment failed. Deployment status: %s"), *DeploymentStatus); + bStartingDeployment = false; } - } - else - { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("'status' does not exist in Json result from 'spot delete': %s"), *SpotDeleteResult); - } - - return bSuccess; -} + }); -bool FLocalDeploymentManager::TryStartSpatialService(FString RuntimeIPToExpose) -{ - if (!bLocalDeploymentManagerEnabled) - { - UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Local deployment manager is disabled because spatial services are unavailable.")); - return false; - } + RuntimeProcess->Launch(); - if (bSpatialServiceRunning) - { - UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Tried to start spatial service but it is already running.")); - return false; - } - else if (bStartingSpatialService) + while (bStartingDeployment && RuntimeProcess->Update()) { - UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Tried to start spatial service but it is already being started.")); - return false; + if (RuntimeProcess->GetDuration().GetTotalSeconds() > RuntimeTimeout) + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Timed out waiting for the Runtime to start.")); + bStartingDeployment = false; + break; + } } - bStartingSpatialService = true; + bStartingDeployment = false; + bLocalDeploymentRunning = true; - FString ServiceStartResult; - int32 ExitCode; - bool bSuccess = SpatialCommandUtils::StartSpatialService(*SpatialServiceVersion, *RuntimeIPToExpose, bIsInChina, - SpatialGDKServicesConstants::SpatialOSDirectory, ServiceStartResult, ExitCode); + FTimespan Span = FDateTime::Now() - RuntimeStartTime; + UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Successfully created local deployment in %f seconds."), Span.GetTotalSeconds()); - bStartingSpatialService = false; + AsyncTask(ENamedThreads::GameThread, [this] { + OnDeploymentStart.Broadcast(); + }); - if (bSuccess && ServiceStartResult.Contains(TEXT("RUNNING"))) - { - UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Spatial service started!")); - ExposedRuntimeIP = RuntimeIPToExpose; - bSpatialServiceRunning = true; - return true; - } - else - { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Spatial service failed to start! %s"), *ServiceStartResult); - ExposedRuntimeIP = TEXT(""); - bSpatialServiceRunning = false; - bLocalDeploymentRunning = false; - return false; - } + return; } -bool FLocalDeploymentManager::TryStopSpatialService() +bool FLocalDeploymentManager::SetupRuntimeFileLogger(const FString& RuntimeLogDir) { - if (!bLocalDeploymentManagerEnabled) - { - return false; - } - - if (bStoppingSpatialService) - { - UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Tried to stop spatial service but it is already being stopped.")); - return false; - } - - bStoppingSpatialService = true; + // Ensure any old log file is cleaned up. + RuntimeLogFileHandle.Reset(); - FString ServiceStopResult; - int32 ExitCode; - bool bSuccess = SpatialCommandUtils::StopSpatialService(bIsInChina, SpatialGDKServicesConstants::SpatialOSDirectory, ServiceStopResult, ExitCode); + FString RuntimeLogFilePath = FPaths::Combine(RuntimeLogDir, TEXT("runtime.log")); + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); - bStoppingSpatialService = false; + const bool bSuccess = PlatformFile.CreateDirectoryTree(*RuntimeLogDir); if (bSuccess) { - UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Spatial service stopped!")); - ExposedRuntimeIP = TEXT(""); - bSpatialServiceRunning = false; - bSpatialServiceInProjectDirectory = true; - bLocalDeploymentRunning = false; - return true; + RuntimeLogFileHandle.Reset(PlatformFile.OpenWrite(*RuntimeLogFilePath, /*bAppend*/ false, /*bAllowRead*/ true)); } - return false; -} - -bool FLocalDeploymentManager::GetLocalDeploymentStatus() -{ - if (!bSpatialServiceRunning) + if (!bSuccess || RuntimeLogFileHandle == nullptr) { - bLocalDeploymentRunning = false; - return bLocalDeploymentRunning; - } - - FString SpotListArgs = FString::Printf(TEXT("alpha deployment list --project-name=%s --json --view BASIC --status-filter NOT_STOPPED_DEPLOYMENTS"), *FSpatialGDKServicesModule::GetProjectName()); - - FString SpotListResult; - FString StdErr; - int32 ExitCode; - FPlatformProcess::ExecProcess(*SpatialGDKServicesConstants::SpotExe, *SpotListArgs, &ExitCode, &SpotListResult, &StdErr); - - if (ExitCode != ExitCodeSuccess) - { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Failed to check local deployment status. Result: %s - Error: %s"), *SpotListResult, *StdErr); + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Could not create runtime log file at '%s'. Saving logs to disk will be disabled."), + *RuntimeLogFilePath); return false; } - TSharedPtr SpotJsonResult; - bool bParsingSuccess = FSpatialGDKServicesModule::ParseJson(SpotListResult, SpotJsonResult); - - if (!bParsingSuccess) - { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Json parsing of spot list result failed. Result: %s"), *SpotListResult); - } - - const TSharedPtr* SpotJsonContent = nullptr; - if (bParsingSuccess && SpotJsonResult->TryGetObjectField(TEXT("content"), SpotJsonContent)) - { - const TArray>* JsonDeployments; - if (!SpotJsonContent->Get()->TryGetArrayField(TEXT("deployments"), JsonDeployments)) - { - UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("No local deployments running.")); - return false; - } - - for (TSharedPtr JsonDeployment : *JsonDeployments) - { - FString DeploymentStatus; - if (JsonDeployment->AsObject()->TryGetStringField(TEXT("status"), DeploymentStatus)) - { - if (DeploymentStatus == TEXT("RUNNING")) - { - FString DeploymentId = JsonDeployment->AsObject()->GetStringField(TEXT("id")); - - UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Running deployment found: %s"), *DeploymentId); - - LocalRunningDeploymentID = DeploymentId; - bLocalDeploymentRunning = true; - return true; - } - } - } - } - else - { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Json parsing of spot list result failed. Can't check deployment status.")); - } - - LocalRunningDeploymentID.Empty(); - bLocalDeploymentRunning = false; - return false; + UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Runtime logs will be saved to %s"), *RuntimeLogFilePath); + return true; } -bool FLocalDeploymentManager::IsServiceRunningAndInCorrectDirectory() +bool FLocalDeploymentManager::TryStopLocalDeployment() { - if (!bLocalDeploymentManagerEnabled) + if (!bLocalDeploymentRunning) { + UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Tried to stop local deployment but no active deployment exists.")); return false; } - FString SpotProjectInfoArgs = TEXT("alpha service project-info --json"); - FString SpotProjectInfoResult; - FString StdErr; - int32 ExitCode; + bStoppingDeployment = true; + RuntimeProcess->Stop(); - FPlatformProcess::ExecProcess(*SpatialGDKServicesConstants::SpotExe, *SpotProjectInfoArgs, &ExitCode, &SpotProjectInfoResult, &StdErr); + double RuntimeStopTime = RuntimeProcess->GetDuration().GetTotalSeconds(); - if (ExitCode != ExitCodeSuccess) + // Update returns true while the process is still running. Wait for it to finish. + while (RuntimeProcess->Update()) { - if (ExitCode == ExitCodeNotRunning) + // If the runtime did not stop after some timeout then inform the user as something is amiss. + if (RuntimeProcess->GetDuration().GetTotalSeconds() > RuntimeStopTime + RuntimeTimeout) { - UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Spatial service is not running: %s"), *SpotProjectInfoResult); - } - else - { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Failed to get spatial service project info: %s"), *SpotProjectInfoResult); + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Timed out waiting for the Runtime to stop.")); + bStoppingDeployment = false; + return false; } - - bSpatialServiceInProjectDirectory = false; - bSpatialServiceRunning = false; - bLocalDeploymentRunning = false; - return false; } - TSharedPtr SpotJsonResult; - bool bParsingSuccess = FSpatialGDKServicesModule::ParseJson(SpotProjectInfoResult, SpotJsonResult); - if (!bParsingSuccess) - { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Json parsing of spot project info result failed. Result: %s"), *SpotProjectInfoResult); - } - - const TSharedPtr* SpotJsonContent = nullptr; - if (bParsingSuccess && !SpotJsonResult->TryGetObjectField(TEXT("content"), SpotJsonContent)) - { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("'content' does not exist in Json result from 'spot service project-info': %s"), *SpotProjectInfoResult); - bParsingSuccess = false; - } + // Kill the log file handle. + RuntimeLogFileHandle.Reset(); - FString SpatialServiceProjectPath; - // 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(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("spatialos.json")); - FPaths::NormalizeDirectoryName(SpatialServiceProjectPath); - FPaths::RemoveDuplicateSlashes(SpatialServiceProjectPath); - - UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Spatial service running at path: %s "), *SpatialServiceProjectPath); - - if (CurrentProjectSpatialPath.Equals(SpatialServiceProjectPath, ESearchCase::IgnoreCase)) - { - bSpatialServiceInProjectDirectory = true; - bSpatialServiceRunning = true; - return true; - } - else - { - UE_LOG(LogSpatialDeploymentManager, Error, - TEXT("Spatial service running in a different project! Please run 'spatial service stop' if you wish to start deployments in the current project. Service at: %s"), *SpatialServiceProjectPath); - - ExposedRuntimeIP = TEXT(""); - bSpatialServiceInProjectDirectory = false; - bSpatialServiceRunning = false; - bLocalDeploymentRunning = false; - return false; - } - } - else - { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("'status' does not exist in Json result from 'spot service project-info': %s"), *SpotProjectInfoResult); - } + bLocalDeploymentRunning = false; + bStoppingDeployment = false; - return false; + return true; } bool FLocalDeploymentManager::IsLocalDeploymentRunning() const @@ -703,11 +393,6 @@ bool FLocalDeploymentManager::IsLocalDeploymentRunning() const return bLocalDeploymentRunning; } -bool FLocalDeploymentManager::IsSpatialServiceRunning() const -{ - return bSpatialServiceRunning; -} - bool FLocalDeploymentManager::IsDeploymentStarting() const { return bStartingDeployment; @@ -718,16 +403,6 @@ bool FLocalDeploymentManager::IsDeploymentStopping() const return bStoppingDeployment; } -bool FLocalDeploymentManager::IsServiceStarting() const -{ - return bStartingSpatialService; -} - -bool FLocalDeploymentManager::IsServiceStopping() const -{ - return bStoppingSpatialService; -} - bool FLocalDeploymentManager::IsRedeployRequired() const { return bRedeployRequired; @@ -755,4 +430,53 @@ void FLocalDeploymentManager::SetAutoDeploy(bool bInAutoDeploy) bAutoDeploy = bInAutoDeploy; } +void SPATIALGDKSERVICES_API FLocalDeploymentManager::TakeSnapshot(UWorld* World, FSpatialSnapshotTakenFunc OnSnapshotTaken) +{ + FHttpModule& HttpModule = FModuleManager::LoadModuleChecked("HTTP"); +#if ENGINE_MINOR_VERSION >= 26 + TSharedRef HttpRequest = HttpModule.Get().CreateRequest(); +#else + TSharedRef HttpRequest = HttpModule.Get().CreateRequest(); +#endif + + HttpRequest->OnProcessRequestComplete().BindLambda( + [World, OnSnapshotTaken](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { + if (!bSucceeded) + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Failed to trigger snapshot at '%s'; received '%s'"), + *HttpRequest->GetURL(), *HttpResponse->GetContentAsString()); + if (OnSnapshotTaken != nullptr) + { + OnSnapshotTaken(false /* bSuccess */, FString() /* PathToSnapshot */); + } + return; + } + + FSpatialGDKServicesModule& GDKServices = FModuleManager::GetModuleChecked("SpatialGDKServices"); + FLocalDeploymentManager* LocalDeploymentManager = GDKServices.GetLocalDeploymentManager(); + + IFileManager& FileManager = FFileManagerGeneric::Get(); + + FString NewestSnapshotFilePath = HttpResponse->GetContentAsString(); + FPaths::NormalizeFilename(NewestSnapshotFilePath); + + bool bSuccess = FPaths::FileExists(NewestSnapshotFilePath); + + if (!bSuccess) + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Failed find snapshot file at '%s'"), *NewestSnapshotFilePath); + } + + if (OnSnapshotTaken != nullptr) + { + OnSnapshotTaken(bSuccess, NewestSnapshotFilePath); + } + }); + + HttpRequest->SetURL(TEXT("http://localhost:5006/snapshot")); + HttpRequest->SetVerb("GET"); + + HttpRequest->ProcessRequest(); +} + #undef LOCTEXT_NAMESPACE diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/LocalReceptionistProxyServerManager.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/LocalReceptionistProxyServerManager.cpp index 76323145b0..99ca939119 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Private/LocalReceptionistProxyServerManager.cpp +++ b/SpatialGDK/Source/SpatialGDKServices/Private/LocalReceptionistProxyServerManager.cpp @@ -8,8 +8,8 @@ #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" #include "Serialization/JsonWriter.h" -#include "Sockets.h" #include "SocketSubsystem.h" +#include "Sockets.h" #include "UObject/CoreNet.h" #include "SpatialCommandUtils.h" @@ -65,7 +65,8 @@ bool FLocalReceptionistProxyServerManager::LocalReceptionistProxyServerPreRunChe } else { - UE_LOG(LogLocalReceptionistProxyServerManager, Warning, TEXT("Failed to kill the process that is blocking the port. %s"), *OutLogMessage); + UE_LOG(LogLocalReceptionistProxyServerManager, Warning, TEXT("Failed to kill the process that is blocking the port. %s"), + *OutLogMessage); } return bProcessKilled; @@ -76,7 +77,6 @@ bool FLocalReceptionistProxyServerManager::LocalReceptionistProxyServerPreRunChe return false; } - void FLocalReceptionistProxyServerManager::Init(int32 Port) { if (!IsRunningCommandlet()) @@ -85,7 +85,6 @@ void FLocalReceptionistProxyServerManager::Init(int32 Port) } } - bool FLocalReceptionistProxyServerManager::TryStopReceptionistProxyServer() { if (ProxyServerProcHandle.IsValid()) @@ -100,7 +99,6 @@ bool FLocalReceptionistProxyServerManager::TryStopReceptionistProxyServer() return false; } - TSharedPtr FLocalReceptionistProxyServerManager::ParsePIDFile() { FString ProxyInfoFileResult; @@ -113,7 +111,8 @@ TSharedPtr FLocalReceptionistProxyServerManager::ParsePIDFile() return JsonParsedProxyInfoFile; } - UE_LOG(LogLocalReceptionistProxyServerManager, Error, TEXT("Json parsing of %s failed. Can't get proxy's PID."), *SpatialGDKServicesConstants::ProxyInfoFilePath); + UE_LOG(LogLocalReceptionistProxyServerManager, Error, TEXT("Json parsing of %s failed. Can't get proxy's PID."), + *SpatialGDKServicesConstants::ProxyInfoFilePath); } return nullptr; @@ -128,12 +127,14 @@ void FLocalReceptionistProxyServerManager::SavePIDInJson(const FString& PID) TSharedRef> JsonWriter = TJsonWriterFactory<>::Create(&ProxyInfoFileResult); if (!FJsonSerializer::Serialize(JsonParsedProxyInfoFile.ToSharedRef(), JsonWriter)) { - UE_LOG(LogLocalReceptionistProxyServerManager, Error, TEXT("Failed to write PID to parsed proxy info file. Unable toS serialize content to json file.")); + UE_LOG(LogLocalReceptionistProxyServerManager, Error, + TEXT("Failed to write PID to parsed proxy info file. Unable toS serialize content to json file.")); return; } if (!FFileHelper::SaveStringToFile(ProxyInfoFileResult, *SpatialGDKServicesConstants::ProxyInfoFilePath)) { - UE_LOG(LogLocalReceptionistProxyServerManager, Error, TEXT("Failed to write file content to %s"), *SpatialGDKServicesConstants::ProxyInfoFilePath); + UE_LOG(LogLocalReceptionistProxyServerManager, Error, TEXT("Failed to write file content to %s"), + *SpatialGDKServicesConstants::ProxyInfoFilePath); } } @@ -146,11 +147,14 @@ bool FLocalReceptionistProxyServerManager::GetPreviousReceptionistProxyPID(FStri return true; } - UE_LOG(LogLocalReceptionistProxyServerManager, Error, TEXT("Local Receptionist Proxy is running but 'pid' does not exist in %s. Can't read proxy's PID."), *SpatialGDKServicesConstants::ProxyInfoFilePath); + UE_LOG(LogLocalReceptionistProxyServerManager, Error, + TEXT("Local Receptionist Proxy is running but 'pid' does not exist in %s. Can't read proxy's PID."), + *SpatialGDKServicesConstants::ProxyInfoFilePath); return false; } - UE_LOG(LogLocalReceptionistProxyServerManager, Log, TEXT("Local Receptionist Proxy is not running or the %s file got deleted."), *SpatialGDKServicesConstants::ProxyInfoFilePath); + UE_LOG(LogLocalReceptionistProxyServerManager, Log, TEXT("Local Receptionist Proxy is not running or the %s file got deleted."), + *SpatialGDKServicesConstants::ProxyInfoFilePath); OutPID.Empty(); return false; @@ -164,7 +168,8 @@ void FLocalReceptionistProxyServerManager::DeletePIDFile() } } -bool FLocalReceptionistProxyServerManager::TryStartReceptionistProxyServer(bool bIsRunningInChina, const FString& CloudDeploymentName, const FString& ListeningAddress, const int32 ReceptionistPort) +bool FLocalReceptionistProxyServerManager::TryStartReceptionistProxyServer(bool bIsRunningInChina, const FString& CloudDeploymentName, + const FString& ListeningAddress, const int32 ReceptionistPort) { FString StartResult; int32 ExitCode; @@ -177,7 +182,8 @@ bool FLocalReceptionistProxyServerManager::TryStartReceptionistProxyServer(bool } // Do not restart the same proxy if you have already a proxy running for the same cloud deployment - if (bProxyIsRunning && ProxyServerProcHandle.IsValid() && RunningCloudDeploymentName == CloudDeploymentName && RunningProxyListeningAddress == ListeningAddress && RunningProxyReceptionistPort == ReceptionistPort) + if (bProxyIsRunning && ProxyServerProcHandle.IsValid() && RunningCloudDeploymentName == CloudDeploymentName + && RunningProxyListeningAddress == ListeningAddress && RunningProxyReceptionistPort == ReceptionistPort) { UE_LOG(LogLocalReceptionistProxyServerManager, Log, TEXT("The local receptionist proxy server is already running!")); @@ -196,12 +202,14 @@ bool FLocalReceptionistProxyServerManager::TryStartReceptionistProxyServer(bool UE_LOG(LogLocalReceptionistProxyServerManager, Log, TEXT("Stopped previous proxy server successfully!")); } - ProxyServerProcHandle = SpatialCommandUtils::StartLocalReceptionistProxyServer(bIsRunningInChina, CloudDeploymentName, ListeningAddress, ReceptionistPort, StartResult, ExitCode); + ProxyServerProcHandle = SpatialCommandUtils::StartLocalReceptionistProxyServer(bIsRunningInChina, CloudDeploymentName, ListeningAddress, + ReceptionistPort, StartResult, ExitCode); // Check if process run successfully if (!ProxyServerProcHandle.IsValid()) { - UE_LOG(LogLocalReceptionistProxyServerManager, Error, TEXT("Starting the local receptionist proxy server failed. Error Code: %d, Error Message: %s"), ExitCode, *StartResult); + UE_LOG(LogLocalReceptionistProxyServerManager, Error, + TEXT("Starting the local receptionist proxy server failed. Error Code: %d, Error Message: %s"), ExitCode, *StartResult); ProxyServerProcHandle.Reset(); return false; } diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/SSpatialOutputLog.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/SSpatialOutputLog.cpp index 8a4284b8e9..986984d32a 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Private/SSpatialOutputLog.cpp +++ b/SpatialGDK/Source/SpatialGDKServices/Private/SSpatialOutputLog.cpp @@ -5,42 +5,20 @@ #include "Async/Async.h" #include "DirectoryWatcherModule.h" #include "Editor.h" +#include "Internationalization/Regex.h" #include "Misc/CoreDelegates.h" #include "Misc/FileHelper.h" #include "Modules/ModuleManager.h" #include "SlateOptMacros.h" #include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesModule.h" -#include "Internationalization/Regex.h" #define LOCTEXT_NAMESPACE "SSpatialOutputLog" DEFINE_LOG_CATEGORY(LogSpatialOutputLog); -static const FString LocalDeploymentLogsDir(FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("logs/localdeployment"))); -static const FString LaunchLogFilename(TEXT("launch.log")); -static const float PollTimeInterval(0.05f); - -void FArchiveLogFileReader::UpdateFileSize() -{ - Size = IFileManager::Get().GetStatData(*Filename).FileSize; -} - -TUniquePtr SSpatialOutputLog::CreateLogFileReader(const TCHAR* InFilename, uint32 Flags, uint32 BufferSize) -{ - IFileHandle* Handle = FPlatformFileManager::Get().GetPlatformFile().OpenRead(InFilename, !!(Flags & FILEREAD_AllowWrite)); - if (Handle == nullptr) - { - if (!(Flags & FILEREAD_NoFail)) - { - UE_LOG(LogSpatialOutputLog, Error, TEXT("Failed to read file: %s"), InFilename); - } - - return nullptr; - } - - return MakeUnique(Handle, InFilename, Handle->Size(), BufferSize); -} +TTuple ErrorLogFlagInfo; +const FString DefaultLogCategory = TEXT("Runtime"); BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void SSpatialOutputLog::Construct(const FArguments& InArgs) @@ -49,97 +27,9 @@ void SSpatialOutputLog::Construct(const FArguments& InArgs) // Remove ourselves as the constructor of our parent (SOutputLog) added 'this' as a remote output device. GLog->RemoveOutputDevice(this); - - LogReader.Reset(); - - StartUpLogDirectoryWatcher(LocalDeploymentLogsDir); - - // Set the LogReader to the latest launch.log if we can. - ReadLatestLogFile(); } END_SLATE_FUNCTION_BUILD_OPTIMIZATION -void SSpatialOutputLog::ReadLatestLogFile() -{ - FString LatestLogDir; - FDateTime LatestLogDirTime; - - // Go through all log directories in the spatial logs and find the most recently created (if one exists) and print the log file to the Spatial Output. - bool bGetLatestLogDir = IFileManager::Get().IterateDirectoryStat(*LocalDeploymentLogsDir, [&LatestLogDir, &LatestLogDirTime](const TCHAR* FileName, const FFileStatData& FileStats) - { - if (FileStats.bIsDirectory) - { - if (FileStats.CreationTime > LatestLogDirTime) - { - LatestLogDir = FString(FileName); - LatestLogDirTime = FileStats.CreationTime; - } - } - - return true; - }); - - if (bGetLatestLogDir) - { - ResetPollingLogFile(FPaths::Combine(LatestLogDir, LaunchLogFilename)); - } -} - -SSpatialOutputLog::~SSpatialOutputLog() -{ - CloseLogReader(); - - ShutdownLogDirectoryWatcher(LocalDeploymentLogsDir); -} - -void SSpatialOutputLog::StartUpLogDirectoryWatcher(const FString& LogDirectory) -{ - // This function will be called from the Slate thread and thus we must switch to the Game thread to create the Directory Watcher. - AsyncTask(ENamedThreads::GameThread, [this, LogDirectory] - { - FDirectoryWatcherModule& DirectoryWatcherModule = FModuleManager::LoadModuleChecked(TEXT("DirectoryWatcher")); - if (IDirectoryWatcher* DirectoryWatcher = DirectoryWatcherModule.Get()) - { - // Watch the log directory for changes. - 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.")); - return; - } - } - - LogDirectoryChangedDelegate = IDirectoryWatcher::FDirectoryChanged::CreateRaw(this, &SSpatialOutputLog::OnLogDirectoryChanged); - DirectoryWatcher->RegisterDirectoryChangedCallback_Handle(LogDirectory, LogDirectoryChangedDelegate, LogDirectoryChangedDelegateHandle, IDirectoryWatcher::WatchOptions::IncludeDirectoryChanges | IDirectoryWatcher::WatchOptions::IgnoreChangesInSubtree); - } - }); -} - -void SSpatialOutputLog::OnLogDirectoryChanged(const TArray& FileChanges) -{ - // If this is a new folder creation then switch to watching the log files in that new log folder. - for (const FFileChangeData& FileChange : FileChanges) - { - if (FileChange.Action == FFileChangeData::FCA_Added) - { -#if PLATFORM_MAC - // Unreal does not support IDirectoryWatcher::WatchOptions::IgnoreChangesInSubtree for macOS. - // We need to double-check whether the current file really is a directory. - if (!FPaths::DirectoryExists(FileChange.Filename)) - { - continue; - } -#endif - // Now we can start reading the new log file in the new log folder. - ResetPollingLogFile(FPaths::Combine(FileChange.Filename, LaunchLogFilename)); - return; - } - } -} - void SSpatialOutputLog::OnClearLog() { // SOutputLog will clear the messages and the SelectedLogCategories. @@ -150,154 +40,25 @@ void SSpatialOutputLog::OnClearLog() Filter.SelectedLogCategories.Reset(); } -void SSpatialOutputLog::ShutdownLogDirectoryWatcher(const FString& LogDirectory) -{ - AsyncTask(ENamedThreads::GameThread, [LogDirectory, LogDirectoryChangedDelegateHandle = LogDirectoryChangedDelegateHandle] - { - FDirectoryWatcherModule& DirectoryWatcherModule = FModuleManager::LoadModuleChecked(TEXT("DirectoryWatcher")); - if (IDirectoryWatcher* DirectoryWatcher = DirectoryWatcherModule.Get()) - { - DirectoryWatcher->UnregisterDirectoryChangedCallback_Handle(LogDirectory, LogDirectoryChangedDelegateHandle); - } - }); -} - -void SSpatialOutputLog::CloseLogReader() -{ - if (GEditor != nullptr) - { - // Delete the old timer if one exists. - GEditor->GetTimerManager()->ClearTimer(PollTimer); - } - - FScopeLock CloseLock(&LogReaderMutex); - - // Clean up the the previous file reader if it existed. - if (LogReader.IsValid()) - { - LogReader->Close(); - LogReader = nullptr; - } -} - -void SSpatialOutputLog::ResetPollingLogFile(const FString& LogFilePath) -{ - CloseLogReader(); - - FScopeLock CreateLock(&LogReaderMutex); - - // FILEREAD_AllowWrite is required as we must match the permissions of the other processes writing to our log file in order to read from it. - LogReader = CreateLogFileReader(*LogFilePath, FILEREAD_AllowWrite, PLATFORM_FILE_READER_BUFFER_SIZE); - - if (LogReader.IsValid()) - { - PollLogFile(LogFilePath); - } - else - { - UE_LOG(LogSpatialOutputLog, Error, TEXT("Could not set up log file reader for %s"), *LogFilePath); - } -} - -void SSpatialOutputLog::PollLogFile(const FString& LogFilePath) -{ - // Poll log files in a background thread since we are doing a lot of string operations. - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, LogFilePath] - { - FScopeLock PollLock(&LogReaderMutex); - - if (!LogReader.IsValid()) - { - UE_LOG(LogSpatialOutputLog, Error, TEXT("Attempted to read from log file but LogReader is not valid.")); - return; - } - - FScopedLoadingState ScopedLoadingState(*LogFilePath); - - // Find out the current size of the log file. This is a cheaper operation than opening a new file reader on every poll. - LogReader->UpdateFileSize(); - - const int32 SizeDifference = LogReader->TotalSize() - LogReader->Tell(); - - // New log lines have been added, serialize them. - if (SizeDifference > 0) - { - uint8* Ch = static_cast(FMemory::Malloc(SizeDifference)); - - LogReader->Serialize(Ch, SizeDifference); - - FString ReadResult; - FFileHelper::BufferToString(ReadResult, Ch, SizeDifference); - - TArray LogLines; - - // All log lines begin with 'time='. We use this as our log line delimiter. - ReadResult.ParseIntoArray(LogLines, TEXT("time="), true); - - for (const FString& LogLine : LogLines) - { - FormatAndPrintRawLogLine(LogLine); - } - - FMemory::Free(Ch); - } - - StartPollTimer(LogFilePath); - }); -} - -void SSpatialOutputLog::StartPollTimer(const FString& LogFilePath) -{ - // Start a timer to read the log file every PollTimeInterval seconds - // Timers must be started on the game thread. - AsyncTask(ENamedThreads::GameThread, [this, LogFilePath] - { - // It's possible that GEditor won't exist when shutting down. - if (GEditor != nullptr) - { - GEditor->GetTimerManager()->SetTimer(PollTimer, [this, LogFilePath]() - { - PollLogFile(LogFilePath); - }, PollTimeInterval, false); - } - }); -} - void SSpatialOutputLog::FormatAndPrintRawErrorLine(const FString& LogLine) { - const FRegexPattern ErrorPattern = FRegexPattern(TEXT("level=(.*) msg=(.*) code=(.*) code_string=(.*) error=(.*) stack=(.*)")); - FRegexMatcher ErrorMatcher(ErrorPattern, LogLine); + FString LogCategory = DefaultLogCategory; - if (!ErrorMatcher.FindNext()) + if (ErrorLogFlagInfo.Key) { - UE_LOG(LogSpatialOutputLog, Error, TEXT("Failed to parse log line: %s"), *LogLine); - return; + LogCategory = ErrorLogFlagInfo.Value; } - 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"))); + AsyncTask(ENamedThreads::GameThread, [this, LogLine, LogCategory] { + Serialize(*LogLine, ELogVerbosity::Error, FName(*LogCategory)); }); } 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=(.*) msg=\"(.*)\" loggerName=(.*\\.)?(.*)")); + // Log line format [time] [category] [level] [message] or [time] [category] [level] [UnrealWorkerCF00FF...5B:UnrealWorker] [message] + const FRegexPattern LogPattern = FRegexPattern(TEXT("\\[(\\w*)\\] \\[(\\w*)\\] (.*)")); FRegexMatcher LogMatcher(LogPattern, LogLine); if (!LogMatcher.FindNext()) @@ -306,38 +67,34 @@ void SSpatialOutputLog::FormatAndPrintRawLogLine(const FString& LogLine) FormatAndPrintRawErrorLine(LogLine); return; } + FString LogCategory = LogMatcher.GetCaptureGroup(1); + FString LogLevelText = LogMatcher.GetCaptureGroup(2); + FString LogMessage = LogMatcher.GetCaptureGroup(3); - FString LogLevelText = LogMatcher.GetCaptureGroup(1); - FString LogMessage = LogMatcher.GetCaptureGroup(2); - FString LogCategory = LogMatcher.GetCaptureGroup(4); + // Log message could have the format [UnrealWorkerCF00FF5D420435E4C4827D8AAC7FFA5B:UnrealWorker] [message] + const FRegexPattern WorkerLogPattern = FRegexPattern(TEXT("\\[(.*)\\] (.*)")); + FRegexMatcher WorkerLogMatcher(WorkerLogPattern, LogMessage); - // 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] - if (LogCategory == TEXT("WorkerLogMessageHandler") || LogCategory == TEXT("Runtime")) + if (WorkerLogMatcher.FindNext()) { - const FRegexPattern WorkerLogPattern = FRegexPattern(TEXT("\\[([^:]*):([^\\]]*)\\] (.*)")); - FRegexMatcher WorkerLogMatcher(WorkerLogPattern, LogMessage); - - if (WorkerLogMatcher.FindNext()) - { - LogCategory = WorkerLogMatcher.GetCaptureGroup(1); // Worker Name - FString WorkerType = WorkerLogMatcher.GetCaptureGroup(2); // Worker Type - - if (LogCategory.StartsWith(WorkerType)) - { - // We shorten the category name to make it more human readable. e.g. UnrealWorkerF5C56 - LogCategory = LogCategory.Left(WorkerType.Len() + 5); - } - - LogMessage = WorkerLogMatcher.GetCaptureGroup(3); - } + FString LogMessageCategory = WorkerLogMatcher.GetCaptureGroup(1); + LogMessage = WorkerLogMatcher.GetCaptureGroup(2); + LogCategory = LogMessageCategory.Left(20); + } + else + { + // If the Log Category is not of type Worker, then it should be categorised as Runtime instead. + LogCategory = DefaultLogCategory; } - ELogVerbosity::Type LogVerbosity = ELogVerbosity::Display; + ELogVerbosity::Type LogVerbosity; + ErrorLogFlagInfo.Key = false; if (LogLevelText.Contains(TEXT("error"))) { LogVerbosity = ELogVerbosity::Error; + ErrorLogFlagInfo.Key = true; + ErrorLogFlagInfo.Value = LogCategory; } else if (LogLevelText.Contains(TEXT("warn"))) { @@ -347,9 +104,9 @@ void SSpatialOutputLog::FormatAndPrintRawLogLine(const FString& LogLine) { LogVerbosity = ELogVerbosity::Verbose; } - else if (LogLevelText.Contains(TEXT("verbose"))) + else if (LogLevelText.Contains(TEXT("trace"))) { - LogVerbosity = ELogVerbosity::Verbose; + LogVerbosity = ELogVerbosity::VeryVerbose; } else { @@ -357,8 +114,7 @@ void SSpatialOutputLog::FormatAndPrintRawLogLine(const FString& LogLine) } // Serialization must be done on the game thread. - AsyncTask(ENamedThreads::GameThread, [this, LogMessage, LogVerbosity, LogCategory] - { + AsyncTask(ENamedThreads::GameThread, [this, LogMessage, LogVerbosity, LogCategory] { Serialize(*LogMessage, LogVerbosity, FName(*LogCategory)); }); } diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/SpatialCommandUtils.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/SpatialCommandUtils.cpp index e1178ffac9..946898f583 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Private/SpatialCommandUtils.cpp +++ b/SpatialGDK/Source/SpatialGDKServices/Private/SpatialCommandUtils.cpp @@ -3,6 +3,7 @@ #include "SpatialCommandUtils.h" #include "Internationalization/Regex.h" +#include "Misc/MonitoredProcess.h" #include "Serialization/JsonSerializer.h" #include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesModule.h" @@ -20,7 +21,8 @@ bool SpatialCommandUtils::SpatialVersion(bool bIsRunningInChina, const FString& Command += SpatialGDKServicesConstants::ChinaEnvironmentArgument; } - FSpatialGDKServicesModule::ExecuteAndReadOutput(*SpatialGDKServicesConstants::SpatialExe, Command, DirectoryToRun, OutResult, OutExitCode); + FSpatialGDKServicesModule::ExecuteAndReadOutput(*SpatialGDKServicesConstants::SpatialExe, Command, DirectoryToRun, OutResult, + OutExitCode); bool bSuccess = OutExitCode == 0; if (!bSuccess) @@ -49,13 +51,15 @@ bool SpatialCommandUtils::AttemptSpatialAuth(bool bIsRunningInChina) 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); + UE_LOG(LogSpatialCommandUtils, Warning, TEXT("Spatial auth login failed. Error Code: %d, StdOut Message: %s, StdErr Message: %s"), + OutExitCode, *OutStdOut, *OutStdErr); } return bSuccess; } -bool SpatialCommandUtils::StartSpatialService(const FString& Version, const FString& RuntimeIP, bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode) +bool SpatialCommandUtils::StartSpatialService(const FString& Version, const FString& RuntimeIP, bool bIsRunningInChina, + const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode) { FString Command = TEXT("service start"); @@ -75,12 +79,14 @@ bool SpatialCommandUtils::StartSpatialService(const FString& Version, const FStr UE_LOG(LogSpatialCommandUtils, Verbose, TEXT("Trying to start spatial service with exposed runtime ip: %s"), *RuntimeIP); } - FSpatialGDKServicesModule::ExecuteAndReadOutput(*SpatialGDKServicesConstants::SpatialExe, Command, DirectoryToRun, OutResult, OutExitCode); + 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); + UE_LOG(LogSpatialCommandUtils, Warning, TEXT("Spatial start service failed. Error Code: %d, Error Message: %s"), OutExitCode, + *OutResult); } return bSuccess; @@ -95,12 +101,14 @@ bool SpatialCommandUtils::StopSpatialService(bool bIsRunningInChina, const FStri Command += SpatialGDKServicesConstants::ChinaEnvironmentArgument; } - FSpatialGDKServicesModule::ExecuteAndReadOutput(*SpatialGDKServicesConstants::SpatialExe, Command, DirectoryToRun, OutResult, OutExitCode); + 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); + UE_LOG(LogSpatialCommandUtils, Warning, TEXT("Spatial stop service failed. Error Code: %d, Error Message: %s"), OutExitCode, + *OutResult); } return bSuccess; @@ -115,18 +123,21 @@ bool SpatialCommandUtils::BuildWorkerConfig(bool bIsRunningInChina, const FStrin Command += SpatialGDKServicesConstants::ChinaEnvironmentArgument; } - FSpatialGDKServicesModule::ExecuteAndReadOutput(*SpatialGDKServicesConstants::SpatialExe, Command, DirectoryToRun, OutResult, OutExitCode); + FSpatialGDKServicesModule::ExecuteAndReadOutput(*SpatialGDKServicesConstants::SpatialExe, Command, DirectoryToRun, OutResult, + OutExitCode); bool bSuccess = OutExitCode == 0; if (!bSuccess) { - UE_LOG(LogSpatialCommandUtils, Warning, TEXT("Spatial build worker config failed. Error Code: %d, Error Message: %s"), OutExitCode, *OutResult); + 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) +FProcHandle SpatialCommandUtils::LocalWorkerReplace(const FString& ServicePort, const FString& OldWorker, const FString& NewWorker, + bool bIsRunningInChina, uint32* OutProcessID) { check(!ServicePort.IsEmpty()); check(!OldWorker.IsEmpty()); @@ -137,8 +148,8 @@ FProcHandle SpatialCommandUtils::LocalWorkerReplace(const FString& 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); + return FPlatformProcess::CreateProc(*SpatialGDKServicesConstants::SpatialExe, *Command, false, true, true, OutProcessID, + 2 /*PriorityModifier*/, nullptr, nullptr, nullptr); } bool SpatialCommandUtils::GenerateDevAuthToken(bool bIsRunningInChina, FString& OutTokenSecret, FText& OutErrorMessage) @@ -151,7 +162,8 @@ bool SpatialCommandUtils::GenerateDevAuthToken(bool bIsRunningInChina, FString& FString CreateDevAuthTokenResult; int32 ExitCode; - FSpatialGDKServicesModule::ExecuteAndReadOutput(SpatialGDKServicesConstants::SpatialExe, Arguments, SpatialGDKServicesConstants::SpatialOSDirectory, CreateDevAuthTokenResult, ExitCode); + FSpatialGDKServicesModule::ExecuteAndReadOutput(SpatialGDKServicesConstants::SpatialExe, Arguments, + SpatialGDKServicesConstants::SpatialOSDirectory, CreateDevAuthTokenResult, ExitCode); if (ExitCode != 0) { @@ -162,16 +174,20 @@ bool SpatialCommandUtils::GenerateDevAuthToken(bool bIsRunningInChina, FString& { JsonRootObject->TryGetStringField("error", ErrorMessage); } - OutErrorMessage = FText::Format(LOCTEXT("UnableToGenerateDevAuthToken_Error", "Unable to generate a development authentication token. Result: {0}"), FText::FromString(ErrorMessage)); + OutErrorMessage = FText::Format( + LOCTEXT("UnableToGenerateDevAuthToken_Error", "Unable to generate a development authentication token. Result: {0}"), + FText::FromString(ErrorMessage)); return false; }; FString AuthResult; FString DevAuthTokenResult; - bool bFoundNewline = CreateDevAuthTokenResult.TrimEnd().Split(TEXT("\n"), &AuthResult, &DevAuthTokenResult, ESearchCase::IgnoreCase, ESearchDir::FromEnd); + bool bFoundNewline = CreateDevAuthTokenResult.TrimEnd().Split(TEXT("\n"), &AuthResult, &DevAuthTokenResult, ESearchCase::IgnoreCase, + ESearchDir::FromEnd); if (!bFoundNewline || DevAuthTokenResult.IsEmpty()) { - // This is necessary because spatial might return multiple json structs depending on whether you are already authenticated against spatial and are on the latest version of it. + // This is necessary because spatial might return multiple json structs depending on whether you are already authenticated against + // spatial and are on the latest version of it. DevAuthTokenResult = CreateDevAuthTokenResult; } @@ -179,7 +195,9 @@ bool SpatialCommandUtils::GenerateDevAuthToken(bool bIsRunningInChina, FString& TSharedPtr JsonRootObject; if (!(FJsonSerializer::Deserialize(JsonReader, JsonRootObject) && JsonRootObject.IsValid())) { - OutErrorMessage = FText::Format(LOCTEXT("UnableToParseDevAuthToken_Error", "Unable to parse the received development authentication token. Result: {0}"), FText::FromString(DevAuthTokenResult)); + OutErrorMessage = FText::Format( + LOCTEXT("UnableToParseDevAuthToken_Error", "Unable to parse the received development authentication token. Result: {0}"), + FText::FromString(DevAuthTokenResult)); return false; } @@ -187,14 +205,17 @@ bool SpatialCommandUtils::GenerateDevAuthToken(bool bIsRunningInChina, FString& const TSharedPtr* JsonDataObject; if (!(JsonRootObject->TryGetObjectField("json_data", JsonDataObject))) { - OutErrorMessage = FText::Format(LOCTEXT("UnableToParseJson_Error", "Unable to parse the received json data. Result: {0}"), FText::FromString(DevAuthTokenResult)); + OutErrorMessage = FText::Format(LOCTEXT("UnableToParseJson_Error", "Unable to parse the received json data. Result: {0}"), + FText::FromString(DevAuthTokenResult)); return false; } FString TokenSecret; if (!(*JsonDataObject)->TryGetStringField("token_secret", TokenSecret)) { - OutErrorMessage = FText::Format(LOCTEXT("UnableToParseTokenSecretFromJson_Error", "Unable to parse the token_secret field inside the received json data. Result: {0}"), FText::FromString(DevAuthTokenResult)); + OutErrorMessage = FText::Format(LOCTEXT("UnableToParseTokenSecretFromJson_Error", + "Unable to parse the token_secret field inside the received json data. Result: {0}"), + FText::FromString(DevAuthTokenResult)); return false; } @@ -218,7 +239,8 @@ bool SpatialCommandUtils::HasDevLoginTag(const FString& DeploymentName, bool bIs FString DeploymentCheckResult; int32 ExitCode; - FSpatialGDKServicesModule::ExecuteAndReadOutput(*SpatialGDKServicesConstants::SpatialExe, TagsCommand, SpatialGDKServicesConstants::SpatialOSDirectory, DeploymentCheckResult, ExitCode); + FSpatialGDKServicesModule::ExecuteAndReadOutput(*SpatialGDKServicesConstants::SpatialExe, TagsCommand, + SpatialGDKServicesConstants::SpatialOSDirectory, DeploymentCheckResult, ExitCode); if (ExitCode != 0) { FString ErrorMessage = DeploymentCheckResult; @@ -228,16 +250,20 @@ bool SpatialCommandUtils::HasDevLoginTag(const FString& DeploymentName, bool bIs { JsonRootObject->TryGetStringField("error", ErrorMessage); } - OutErrorMessage = FText::Format(LOCTEXT("DeploymentTagsRetrievalFailed", "Unable to retrieve deployment tags. Is the deployment {0} running?\nResult: {1}"), FText::FromString(DeploymentName), FText::FromString(ErrorMessage)); + OutErrorMessage = FText::Format( + LOCTEXT("DeploymentTagsRetrievalFailed", "Unable to retrieve deployment tags. Is the deployment {0} running?\nResult: {1}"), + FText::FromString(DeploymentName), FText::FromString(ErrorMessage)); return false; }; FString AuthResult; FString RetrieveTagsResult; - bool bFoundNewline = DeploymentCheckResult.TrimEnd().Split(TEXT("\n"), &AuthResult, &RetrieveTagsResult, ESearchCase::IgnoreCase, ESearchDir::FromEnd); + bool bFoundNewline = + DeploymentCheckResult.TrimEnd().Split(TEXT("\n"), &AuthResult, &RetrieveTagsResult, ESearchCase::IgnoreCase, ESearchDir::FromEnd); if (!bFoundNewline || RetrieveTagsResult.IsEmpty()) { - // This is necessary because spatial might return multiple json structs depending on whether you are already authenticated against spatial and are on the latest version of it. + // This is necessary because spatial might return multiple json structs depending on whether you are already authenticated against + // spatial and are on the latest version of it. RetrieveTagsResult = DeploymentCheckResult; } @@ -245,15 +271,17 @@ bool SpatialCommandUtils::HasDevLoginTag(const FString& DeploymentName, bool bIs TSharedPtr JsonRootObject; if (!(FJsonSerializer::Deserialize(JsonReader, JsonRootObject) && JsonRootObject.IsValid())) { - OutErrorMessage = FText::Format(LOCTEXT("DeploymentTagsJsonInvalid", "Unable to parse the received tags.\nResult: {0}"), FText::FromString(RetrieveTagsResult)); + OutErrorMessage = FText::Format(LOCTEXT("DeploymentTagsJsonInvalid", "Unable to parse the received tags.\nResult: {0}"), + FText::FromString(RetrieveTagsResult)); return false; } - FString JsonMessage; if (!JsonRootObject->TryGetStringField("msg", JsonMessage)) { - OutErrorMessage = FText::Format(LOCTEXT("DeploymentTagsMsgInvalid", "Unable to parse the msg field inside the received json data.\nResult: {0}"), FText::FromString(RetrieveTagsResult)); + OutErrorMessage = + FText::Format(LOCTEXT("DeploymentTagsMsgInvalid", "Unable to parse the msg field inside the received json data.\nResult: {0}"), + FText::FromString(RetrieveTagsResult)); return false; } @@ -264,7 +292,8 @@ bool SpatialCommandUtils::HasDevLoginTag(const FString& DeploymentName, bool bIs */ if (JsonMessage[6] != '[' || JsonMessage[JsonMessage.Len() - 1] != ']') { - OutErrorMessage = FText::Format(LOCTEXT("DeploymentTagsInvalid", "Could not parse the tags.\nMessage: {0}"), FText::FromString(JsonMessage)); + OutErrorMessage = + FText::Format(LOCTEXT("DeploymentTagsInvalid", "Could not parse the tags.\nMessage: {0}"), FText::FromString(JsonMessage)); return false; } @@ -277,13 +306,20 @@ bool SpatialCommandUtils::HasDevLoginTag(const FString& DeploymentName, bool bIs return true; } - OutErrorMessage = FText::Format(LOCTEXT("DevLoginTagNotAvailable", "The cloud deployment {0} does not have the {1} tag associated with it. The client won't be able to connect to the deployment."), FText::FromString(DeploymentName), FText::FromString(SpatialGDKServicesConstants::DevLoginDeploymentTag)); + OutErrorMessage = + FText::Format(LOCTEXT("DevLoginTagNotAvailable", + "The cloud deployment {0} does not have the {1} tag associated with it. The client won't be able to connect " + "to the deployment."), + FText::FromString(DeploymentName), FText::FromString(SpatialGDKServicesConstants::DevLoginDeploymentTag)); return false; } -FProcHandle SpatialCommandUtils::StartLocalReceptionistProxyServer(bool bIsRunningInChina, const FString& CloudDeploymentName, const FString& ListeningAddress, const int32 Port , FString &OutResult, int32 &OutExitCode) +FProcHandle SpatialCommandUtils::StartLocalReceptionistProxyServer(bool bIsRunningInChina, const FString& CloudDeploymentName, + const FString& ListeningAddress, const int32 Port, FString& OutResult, + int32& OutExitCode) { - FString Command = FString::Printf(TEXT("cloud connect external %s --listening_address %s --local_receptionist_port %i"), *CloudDeploymentName, *ListeningAddress, Port); + FString Command = FString::Printf(TEXT("cloud connect external %s --listening_address %s --local_receptionist_port %i"), + *CloudDeploymentName, *ListeningAddress, Port); if (bIsRunningInChina) { @@ -296,7 +332,8 @@ FProcHandle SpatialCommandUtils::StartLocalReceptionistProxyServer(bool bIsRunni void* WritePipe = nullptr; ensure(FPlatformProcess::CreatePipe(ReadPipe, WritePipe)); - ProcHandle = FPlatformProcess::CreateProc(*SpatialGDKServicesConstants::SpatialExe, *Command, false, true, true, nullptr, 1 /*PriorityModifer*/, *SpatialGDKServicesConstants::SpatialOSDirectory, WritePipe); + ProcHandle = FPlatformProcess::CreateProc(*SpatialGDKServicesConstants::SpatialExe, *Command, false, true, true, nullptr, + 1 /*PriorityModifer*/, *SpatialGDKServicesConstants::SpatialOSDirectory, WritePipe); bool bProcessSucceeded = false; bool bProcessFinished = false; @@ -314,7 +351,8 @@ FProcHandle SpatialCommandUtils::StartLocalReceptionistProxyServer(bool bIsRunni } else { - UE_LOG(LogSpatialCommandUtils, Error, TEXT("Execution failed. '%s' with arguments '%s' in directory '%s'"), *SpatialGDKServicesConstants::SpatialExe, *Command, *SpatialGDKServicesConstants::SpatialOSDirectory); + UE_LOG(LogSpatialCommandUtils, Error, TEXT("Execution failed. '%s' with arguments '%s' in directory '%s'"), + *SpatialGDKServicesConstants::SpatialExe, *Command, *SpatialGDKServicesConstants::SpatialOSDirectory); } if (!bProcessSucceeded) @@ -340,14 +378,16 @@ void SpatialCommandUtils::StopLocalReceptionistProxyServer(FProcHandle& ProcHand bool SpatialCommandUtils::GetProcessName(const FString& PID, FString& OutProcessName) { #if PLATFORM_MAC - UE_LOG(LogSpatialCommandUtils, Warning, TEXT("Failed to get the name of the process that is blocking the required port. To get the name of the process in MacOS you need to use SpatialCommandUtils::GetProcessInfoFromPort.")); + UE_LOG(LogSpatialCommandUtils, Warning, + TEXT("Failed to get the name of the process that is blocking the required port. To get the name of the process in MacOS you " + "need to use SpatialCommandUtils::GetProcessInfoFromPort.")); return false; #else bool bSuccess = false; OutProcessName = TEXT(""); const FString TaskListCmd = TEXT("tasklist"); - // Get the task list line for the process with PID + // Get the task list line for the process with PID const FString TaskListArgs = FString::Printf(TEXT(" /fi \"PID eq %s\" /nh /fo:csv"), *PID); FString TaskListResult; int32 ExitCode; @@ -379,7 +419,7 @@ bool SpatialCommandUtils::TryKillProcessWithPID(const FString& PID) const FString KillCmd = TEXT("taskkill"); const FString KillArgs = FString::Printf(TEXT("/F /PID %s"), *PID); #elif PLATFORM_MAC - const FString KillCmd = FPaths::Combine(SpatialGDKServicesConstants::KillCmdFilePath, TEXT("kill")); + const FString KillCmd = FPaths::Combine(SpatialGDKServicesConstants::BinPath, TEXT("kill")); const FString KillArgs = FString::Printf(TEXT("%s"), *PID); #endif @@ -394,6 +434,21 @@ bool SpatialCommandUtils::TryKillProcessWithPID(const FString& PID) return bSuccess; } +void SpatialCommandUtils::TryKillProcessWithName(const FString& ProcessName) +{ + FPlatformProcess::FProcEnumerator ProcessIt; + while (ProcessIt.MoveNext()) + { + if (ProcessIt.GetCurrent().GetName().Equals(ProcessName)) + { + UE_LOG(LogSpatialCommandUtils, Log, TEXT("Killing process: %s with process ID : %d"), *ProcessName, + ProcessIt.GetCurrent().GetPID()); + auto Handle = FPlatformProcess::OpenProcess(ProcessIt.GetCurrent().GetPID()); + FPlatformProcess::TerminateProc(Handle); + } + } +} + bool SpatialCommandUtils::GetProcessInfoFromPort(int32 Port, FString& OutPid, FString& OutState, FString& OutProcessName) { #if PLATFORM_WINDOWS @@ -424,7 +479,6 @@ bool SpatialCommandUtils::GetProcessInfoFromPort(int32 Port, FString& OutPid, FS FRegexMatcher PidMatcher(PidMatcherPattern, Result); if (PidMatcher.FindNext()) { - #if PLATFORM_WINDOWS OutState = PidMatcher.GetCaptureGroup(2 /* Get the State of the process, which is the second group. */); OutPid = PidMatcher.GetCaptureGroup(3 /* Get the PID, which is the third group. */); @@ -444,7 +498,6 @@ bool SpatialCommandUtils::GetProcessInfoFromPort(int32 Port, FString& OutPid, FS UE_LOG(LogSpatialCommandUtils, Log, TEXT("The required port %i is not blocked!"), Port); return false; #endif - } #if PLATFORM_MAC @@ -459,4 +512,66 @@ bool SpatialCommandUtils::GetProcessInfoFromPort(int32 Port, FString& OutPid, FS return false; } +bool SpatialCommandUtils::FetchRuntimeBinary(const FString& RuntimeVersion, const bool bIsRunningInChina) +{ + FString RuntimePath = + FPaths::Combine(SpatialGDKServicesConstants::GDKProgramPath, SpatialGDKServicesConstants::RuntimePackageName, RuntimeVersion); + return FetchPackageBinary(RuntimeVersion, SpatialGDKServicesConstants::RuntimeExe, SpatialGDKServicesConstants::RuntimePackageName, + RuntimePath, bIsRunningInChina, true); +} + +bool SpatialCommandUtils::FetchInspectorBinary(const FString& InspectorVersion, const bool bIsRunningInChina) +{ + FString InspectorPath = FPaths::Combine(SpatialGDKServicesConstants::GDKProgramPath, SpatialGDKServicesConstants::InspectorPackageName, + InspectorVersion, SpatialGDKServicesConstants::InspectorExe); + return FetchPackageBinary(InspectorVersion, SpatialGDKServicesConstants::InspectorExe, + SpatialGDKServicesConstants::InspectorPackageName, InspectorPath, bIsRunningInChina, false); +} + +bool SpatialCommandUtils::FetchPackageBinary(const FString& PackageVersion, const FString& PackageExe, const FString& PackageName, + const FString& SaveLocation, const bool bIsRunningInChina, const bool bUnzip) +{ + FString PackagePath = FPaths::Combine(SpatialGDKServicesConstants::GDKProgramPath, *PackageName, PackageVersion); + + // Check if the binary already exists for a given version + if (FPaths::FileExists(FPaths::Combine(PackagePath, PackageExe))) + { + UE_LOG(LogSpatialCommandUtils, Verbose, TEXT("%s binary already exists."), *PackageName); + return true; + } + + // If it does not exist then fetch the binary using `spatial worker package retrieve` + UE_LOG(LogSpatialCommandUtils, Log, TEXT("Trying to fetch %s version %s"), *PackageName, *PackageVersion); + FString Params = FString::Printf(TEXT("package retrieve %s %s %s %s"), *PackageName, *SpatialGDKServicesConstants::PlatformVersion, + *PackageVersion, *SaveLocation); + if (bUnzip) + { + Params += TEXT(" --unzip"); + } + + if (bIsRunningInChina) + { + Params += SpatialGDKServicesConstants::ChinaEnvironmentArgument; + } + + TOptional FetchingProcess; + const FString& ExePath = SpatialGDKServicesConstants::SpatialExe; + FetchingProcess = { ExePath, Params, true, true }; + FetchingProcess->OnOutput().BindLambda([](const FString& Output) { + UE_LOG(LogSpatialCommandUtils, Display, TEXT("FetchingProcess: %s"), *Output); + }); + FetchingProcess->Launch(); + + while (FetchingProcess->Update()) + { + if (FetchingProcess->GetDuration().GetTotalSeconds() > ProcessTimeoutTime) + { + UE_LOG(LogSpatialCommandUtils, Error, TEXT("Timed out waiting for the %s process fetching to start."), *PackageName); + + FetchingProcess->Exit(); + return false; + } + } + return true; +} #undef LOCTEXT_NAMESPACE diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/SpatialGDKServicesModule.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/SpatialGDKServicesModule.cpp index 6da65cb83a..1d67267e59 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Private/SpatialGDKServicesModule.cpp +++ b/SpatialGDK/Source/SpatialGDKServices/Private/SpatialGDKServicesModule.cpp @@ -8,11 +8,11 @@ #include "Framework/Application/SlateApplication.h" #include "Framework/Docking/TabManager.h" #include "Misc/FileHelper.h" -#include "SpatialCommandUtils.h" #include "SSpatialOutputLog.h" #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" #include "Serialization/JsonWriter.h" +#include "SpatialCommandUtils.h" #include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesPrivate.h" #include "Widgets/Docking/SDockTab.h" @@ -24,21 +24,23 @@ DEFINE_LOG_CATEGORY(LogSpatialGDKServices); IMPLEMENT_MODULE(FSpatialGDKServicesModule, SpatialGDKServices); static const FName SpatialOutputLogTabName = FName(TEXT("SpatialOutputLog")); +TWeakPtr SpatialOutputLog; TSharedRef SpawnSpatialOutputLog(const FSpawnTabArgs& Args) { + TSharedRef SpatialOutputLogRef = SNew(SSpatialOutputLog); + SpatialOutputLog = TWeakPtr(SpatialOutputLogRef); + return SNew(SDockTab) .Icon(FEditorStyle::GetBrush("Log.TabIcon")) .TabRole(ETabRole::NomadTab) - .Label(NSLOCTEXT("SpatialOutputLog", "TabTitle", "Spatial Output")) - [ - SNew(SSpatialOutputLog) - ]; + .Label(NSLOCTEXT("SpatialOutputLog", "TabTitle", "Spatial Output"))[SpatialOutputLogRef]; } void FSpatialGDKServicesModule::StartupModule() { - FGlobalTabmanager::Get()->RegisterNomadTabSpawner(SpatialOutputLogTabName, FOnSpawnTab::CreateStatic(&SpawnSpatialOutputLog)) + FGlobalTabmanager::Get() + ->RegisterNomadTabSpawner(SpatialOutputLogTabName, FOnSpawnTab::CreateStatic(&SpawnSpatialOutputLog)) .SetDisplayName(NSLOCTEXT("UnrealEditor", "SpatialOutputLogTab", "Spatial Output Log")) .SetTooltipText(NSLOCTEXT("UnrealEditor", "SpatialOutputLogTooltipText", "Open the Spatial Output Log tab.")) .SetGroup(WorkspaceMenu::GetMenuStructure().GetDeveloperToolsLogCategory()) @@ -65,6 +67,11 @@ FLocalReceptionistProxyServerManager* FSpatialGDKServicesModule::GetLocalRecepti return &LocalReceptionistProxyServerManager; } +TWeakPtr FSpatialGDKServicesModule::GetSpatialOutputLog() +{ + return SpatialOutputLog; +} + FString FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(const FString& AppendPath) { FString PluginDir = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectPluginsDir(), TEXT("UnrealGDK"))); @@ -83,21 +90,15 @@ bool FSpatialGDKServicesModule::SpatialPreRunChecks(bool bIsInChina) { FString SpatialExistenceCheckResult; int32 ExitCode; - bool bSuccess = SpatialCommandUtils::SpatialVersion(bIsInChina, SpatialGDKServicesConstants::SpatialOSDirectory, SpatialExistenceCheckResult, ExitCode); + bool bSuccess = SpatialCommandUtils::SpatialVersion(bIsInChina, SpatialGDKServicesConstants::SpatialOSDirectory, + SpatialExistenceCheckResult, ExitCode); if (!bSuccess) { - 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(*SpatialGDKServicesConstants::SpotExe, TEXT("version"), &ExitCode, &SpotExistenceCheckResult, &StdErr); - - if (ExitCode != 0) - { - 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); + 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; } @@ -110,21 +111,24 @@ bool FSpatialGDKServicesModule::ParseJson(const FString& RawJsonString, TSharedP return FJsonSerializer::Deserialize(JsonReader, JsonParsed); } -// ExecuteAndReadOutput exists so that a spatial command window does not spawn when using 'spatial.exe'. It does not however allow reading from StdErr. -// For other processes which do not spawn cmd windows, use ExecProcess instead. -void FSpatialGDKServicesModule::ExecuteAndReadOutput(const FString& Executable, const FString& Arguments, const FString& DirectoryToRun, FString& OutResult, int32& ExitCode) +// ExecuteAndReadOutput exists so that a spatial command window does not spawn when using 'spatial.exe'. It does not however allow reading +// from StdErr. For other processes which do not spawn cmd windows, use ExecProcess instead. +void FSpatialGDKServicesModule::ExecuteAndReadOutput(const FString& Executable, const FString& Arguments, const FString& DirectoryToRun, + FString& OutResult, int32& ExitCode) { - UE_LOG(LogSpatialGDKServices, Verbose, TEXT("Executing '%s' with arguments '%s' in directory '%s'"), *Executable, *Arguments, *DirectoryToRun); + UE_LOG(LogSpatialGDKServices, Verbose, TEXT("Executing '%s' with arguments '%s' in directory '%s'"), *Executable, *Arguments, + *DirectoryToRun); void* ReadPipe = nullptr; void* WritePipe = nullptr; ensure(FPlatformProcess::CreatePipe(ReadPipe, WritePipe)); - FProcHandle ProcHandle = FPlatformProcess::CreateProc(*Executable, *Arguments, false, true, true, nullptr, 1 /*PriorityModifer*/, *DirectoryToRun, WritePipe); + FProcHandle ProcHandle = FPlatformProcess::CreateProc(*Executable, *Arguments, false, true, true, nullptr, 1 /*PriorityModifer*/, + *DirectoryToRun, WritePipe); if (ProcHandle.IsValid()) { - for (bool bProcessFinished = false; !bProcessFinished; ) + for (bool bProcessFinished = false; !bProcessFinished;) { bProcessFinished = FPlatformProcess::GetProcReturnCode(ProcHandle, &ExitCode); @@ -136,7 +140,8 @@ void FSpatialGDKServicesModule::ExecuteAndReadOutput(const FString& Executable, } else { - UE_LOG(LogSpatialGDKServices, Error, TEXT("Execution failed. '%s' with arguments '%s' in directory '%s' "), *Executable, *Arguments, *DirectoryToRun); + UE_LOG(LogSpatialGDKServices, Error, TEXT("Execution failed. '%s' with arguments '%s' in directory '%s' "), *Executable, *Arguments, + *DirectoryToRun); } FPlatformProcess::ClosePipe(0, ReadPipe); @@ -150,7 +155,8 @@ void FSpatialGDKServicesModule::SetProjectName(const FString& InProjectName) TSharedPtr JsonParsedSpatialFile = ParseProjectFile(); if (!JsonParsedSpatialFile.IsValid()) { - UE_LOG(LogSpatialGDKServices, Error, TEXT("Failed to update project name(%s). Please ensure that the following file exists: %s"), *InProjectName, *SpatialGDKServicesConstants::SpatialOSConfigFileName); + UE_LOG(LogSpatialGDKServices, Error, TEXT("Failed to update project name(%s). Please ensure that the following file exists: %s"), + *InProjectName, *SpatialGDKServicesConstants::SpatialOSConfigFileName); return; } JsonParsedSpatialFile->SetStringField("name", InProjectName); @@ -158,12 +164,15 @@ void FSpatialGDKServicesModule::SetProjectName(const FString& InProjectName) TSharedRef> JsonWriter = TJsonWriterFactory<>::Create(&SpatialFileResult); if (!FJsonSerializer::Serialize(JsonParsedSpatialFile.ToSharedRef(), JsonWriter)) { - UE_LOG(LogSpatialGDKServices, Error, TEXT("Failed to write project name to parsed spatial file. Unable to serialize content to json file.")); + UE_LOG(LogSpatialGDKServices, Error, + TEXT("Failed to write project name to parsed spatial file. Unable to serialize content to json file.")); return; } - if (!FFileHelper::SaveStringToFile(SpatialFileResult, *FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, SpatialGDKServicesConstants::SpatialOSConfigFileName))) + if (!FFileHelper::SaveStringToFile(SpatialFileResult, *FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, + SpatialGDKServicesConstants::SpatialOSConfigFileName))) { - UE_LOG(LogSpatialGDKServices, Error, TEXT("Failed to write file content to %s"), *SpatialGDKServicesConstants::SpatialOSConfigFileName); + UE_LOG(LogSpatialGDKServices, Error, TEXT("Failed to write file content to %s"), + *SpatialGDKServicesConstants::SpatialOSConfigFileName); } ProjectName = InProjectName; } @@ -193,7 +202,8 @@ TSharedPtr FSpatialGDKServicesModule::ParseProjectFile() FString SpatialFileResult; TSharedPtr JsonParsedSpatialFile; - if (FFileHelper::LoadFileToString(SpatialFileResult, *FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, SpatialGDKServicesConstants::SpatialOSConfigFileName))) + if (FFileHelper::LoadFileToString(SpatialFileResult, *FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, + SpatialGDKServicesConstants::SpatialOSConfigFileName))) { if (ParseJson(SpatialFileResult, JsonParsedSpatialFile)) { diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/LocalDeploymentManager.h b/SpatialGDK/Source/SpatialGDKServices/Public/LocalDeploymentManager.h index 3ecd1a9381..4b9f0e32bb 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Public/LocalDeploymentManager.h +++ b/SpatialGDK/Source/SpatialGDKServices/Public/LocalDeploymentManager.h @@ -5,6 +5,8 @@ #include "Async/Future.h" #include "CoreMinimal.h" #include "FileCache.h" +#include "Improbable/SpatialGDKSettingsBridge.h" +#include "Misc/MonitoredProcess.h" #include "Modules/ModuleManager.h" #include "Templates/SharedPointer.h" #include "TimerManager.h" @@ -20,34 +22,24 @@ class FLocalDeploymentManager void SPATIALGDKSERVICES_API PreInit(bool bChinaEnabled); - void SPATIALGDKSERVICES_API Init(FString RuntimeIPToExpose); - - void SPATIALGDKSERVICES_API RefreshServiceStatus(); + void SPATIALGDKSERVICES_API Init(); bool CheckIfPortIsBound(int32 Port); bool KillProcessBlockingPort(int32 Port); bool LocalDeploymentPreRunChecks(); - using LocalDeploymentCallback = TFunction; + using LocalDeploymentCallback = TFunction; - void SPATIALGDKSERVICES_API TryStartLocalDeployment(FString LaunchConfig, FString RuntimeVersion, 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); - bool SPATIALGDKSERVICES_API TryStopSpatialService(); - - bool SPATIALGDKSERVICES_API GetLocalDeploymentStatus(); - bool SPATIALGDKSERVICES_API IsServiceRunningAndInCorrectDirectory(); - bool SPATIALGDKSERVICES_API IsLocalDeploymentRunning() const; - bool SPATIALGDKSERVICES_API IsSpatialServiceRunning() const; bool SPATIALGDKSERVICES_API IsDeploymentStarting() const; bool SPATIALGDKSERVICES_API IsDeploymentStopping() const; - bool SPATIALGDKSERVICES_API IsServiceStarting() const; - bool SPATIALGDKSERVICES_API IsServiceStopping() const; - bool SPATIALGDKSERVICES_API IsRedeployRequired() const; void SPATIALGDKSERVICES_API SetRedeployRequired(); @@ -56,6 +48,8 @@ class FLocalDeploymentManager void SPATIALGDKSERVICES_API SetAutoDeploy(bool bAutoDeploy); + void SPATIALGDKSERVICES_API TakeSnapshot(UWorld* World, FSpatialSnapshotTakenFunc OnSnapshotTaken); + void WorkerBuildConfigAsync(); FSimpleMulticastDelegate OnSpatialShutdown; @@ -68,33 +62,29 @@ class FLocalDeploymentManager void StartUpWorkerConfigDirectoryWatcher(); void OnWorkerConfigDirectoryChanged(const TArray& FileChanges); - bool FinishLocalDeployment(FString LaunchConfig, FString RuntimeVersion, FString LaunchArgs, FString SnapshotName, FString RuntimeIPToExpose); + bool SetupRuntimeFileLogger(const FString& SpatialLogsSubDirectoryName); TFuture AttemptSpatialAuthResult; - static const int32 ExitCodeSuccess = 0; - static const int32 ExitCodeNotRunning = 4; + TOptional RuntimeProcess = {}; + TUniquePtr RuntimeLogFileHandle; + FDateTime RuntimeStartTime; + static const int32 RequiredRuntimePort = 5301; + static const int32 WorkerPort = 8018; + static const int32 HTTPPort = 5006; - // 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; + static constexpr double RuntimeTimeout = 10.0; bool bLocalDeploymentManagerEnabled = true; bool bLocalDeploymentRunning; - bool bSpatialServiceRunning; - bool bSpatialServiceInProjectDirectory; bool bStartingDeployment; bool bStoppingDeployment; - bool bStartingSpatialService; - bool bStoppingSpatialService; - FString ExposedRuntimeIP; - FString LocalRunningDeploymentID; - bool bRedeployRequired = false; bool bAutoDeploy = false; bool bIsInChina = false; diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/LocalReceptionistProxyServerManager.h b/SpatialGDK/Source/SpatialGDKServices/Public/LocalReceptionistProxyServerManager.h index a7e6db7a4b..77cf8e8b11 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Public/LocalReceptionistProxyServerManager.h +++ b/SpatialGDK/Source/SpatialGDKServices/Public/LocalReceptionistProxyServerManager.h @@ -15,9 +15,10 @@ class FLocalReceptionistProxyServerManager bool LocalReceptionistProxyServerPreRunChecks(int32 ReceptionistPort); void SPATIALGDKSERVICES_API Init(int32 ReceptionistPort); - bool SPATIALGDKSERVICES_API TryStartReceptionistProxyServer(bool bIsRunningInChina, const FString& CloudDeploymentName, const FString& ListeningAddress, int32 ReceptionistPort); + bool SPATIALGDKSERVICES_API TryStartReceptionistProxyServer(bool bIsRunningInChina, const FString& CloudDeploymentName, + const FString& ListeningAddress, int32 ReceptionistPort); bool SPATIALGDKSERVICES_API TryStopReceptionistProxyServer(); - + private: static TSharedPtr ParsePIDFile(); static void SavePIDInJson(const FString& PID); diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/SSpatialOutputLog.h b/SpatialGDK/Source/SpatialGDKServices/Public/SSpatialOutputLog.h index b5320adc2c..44fb0a11e4 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Public/SSpatialOutputLog.h +++ b/SpatialGDK/Source/SpatialGDKServices/Public/SSpatialOutputLog.h @@ -10,17 +10,6 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialOutputLog, Log, All); -// Child class of the file reader used by Unreal but with the ability to update the known file size. -// This allows us to read a log file while it is being written to. -class FArchiveLogFileReader : public FArchiveFileReaderGeneric -{ -public: - FArchiveLogFileReader(IFileHandle* InHandle, const TCHAR* InFilename, int64 InSize, uint32 InBufferSize /*= PLATFORM_FILE_READER_BUFFER_SIZE*/) - : FArchiveFileReaderGeneric(InHandle, InFilename, InSize, InBufferSize) - {} - void UpdateFileSize(); -}; - class SSpatialOutputLog : public SOutputLog { public: @@ -28,32 +17,11 @@ class SSpatialOutputLog : public SOutputLog SLATE_END_ARGS() - ~SSpatialOutputLog(); - void Construct(const FArguments& InArgs); - TUniquePtr CreateLogFileReader(const TCHAR* InFilename, uint32 Flags, uint32 BufferSize); - -private: - void ReadLatestLogFile(); - void ResetPollingLogFile(const FString& LogFilePath); - void StartPollTimer(const FString& LogFilePath); - void PollLogFile(const FString& LogFilePath); - void CloseLogReader(); - void FormatAndPrintRawLogLine(const FString& LogLine); void FormatAndPrintRawErrorLine(const FString& LogLine); - void StartUpLogDirectoryWatcher(const FString& LogDirectory); - void ShutdownLogDirectoryWatcher(const FString& LogDirectory); - void OnLogDirectoryChanged(const TArray& FileChanges); - +private: void OnClearLog() override; - - FDelegateHandle LogDirectoryChangedDelegateHandle; - IDirectoryWatcher::FDirectoryChanged LogDirectoryChangedDelegate; - - FTimerHandle PollTimer; - TUniquePtr LogReader; - FCriticalSection LogReaderMutex; }; diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialCommandUtils.h b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialCommandUtils.h index 57862e4f79..5ad3120de9 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialCommandUtils.h +++ b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialCommandUtils.h @@ -10,17 +10,35 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialCommandUtils, Log, All); class SpatialCommandUtils { public: - SPATIALGDKSERVICES_API static bool SpatialVersion(bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode); + SPATIALGDKSERVICES_API static bool SpatialVersion(bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, + int32& OutExitCode); SPATIALGDKSERVICES_API static bool AttemptSpatialAuth(bool bIsRunningInChina); - SPATIALGDKSERVICES_API static bool StartSpatialService(const FString& Version, const FString& RuntimeIP, bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode); - SPATIALGDKSERVICES_API static bool StopSpatialService(bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode); - SPATIALGDKSERVICES_API static bool BuildWorkerConfig(bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode); - SPATIALGDKSERVICES_API static FProcHandle LocalWorkerReplace(const FString& ServicePort, const FString& OldWorker, const FString& NewWorker, bool bIsRunningInChina, uint32* OutProcessID); + SPATIALGDKSERVICES_API static bool StartSpatialService(const FString& Version, const FString& RuntimeIP, bool bIsRunningInChina, + const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode); + SPATIALGDKSERVICES_API static bool StopSpatialService(bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, + int32& OutExitCode); + SPATIALGDKSERVICES_API static bool BuildWorkerConfig(bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, + int32& OutExitCode); + SPATIALGDKSERVICES_API static FProcHandle LocalWorkerReplace(const FString& ServicePort, const FString& OldWorker, + const FString& NewWorker, bool bIsRunningInChina, uint32* OutProcessID); SPATIALGDKSERVICES_API static bool GenerateDevAuthToken(bool bIsRunningInChina, FString& OutTokenSecret, FText& OutErrorMessage); SPATIALGDKSERVICES_API static bool HasDevLoginTag(const FString& DeploymentName, bool bIsRunningInChina, FText& OutErrorMessage); - SPATIALGDKSERVICES_API static FProcHandle StartLocalReceptionistProxyServer(bool bIsRunningInChina, const FString& CloudDeploymentName, const FString& ListeningAddress, const int32 ReceptionistPort, FString& OutResult, int32& OutExitCode); + SPATIALGDKSERVICES_API static FProcHandle StartLocalReceptionistProxyServer(bool bIsRunningInChina, const FString& CloudDeploymentName, + const FString& ListeningAddress, + const int32 ReceptionistPort, FString& OutResult, + int32& OutExitCode); SPATIALGDKSERVICES_API static void StopLocalReceptionistProxyServer(FProcHandle& ProcessHandle); SPATIALGDKSERVICES_API static bool GetProcessInfoFromPort(int32 Port, FString& OutPid, FString& OutState, FString& OutProcessName); SPATIALGDKSERVICES_API static bool GetProcessName(const FString& PID, FString& OutProcessName); SPATIALGDKSERVICES_API static bool TryKillProcessWithPID(const FString& PID); + SPATIALGDKSERVICES_API static void TryKillProcessWithName(const FString& ProcessName); + SPATIALGDKSERVICES_API static bool FetchRuntimeBinary(const FString& RuntimeVersion, const bool bIsRunningInChina); + SPATIALGDKSERVICES_API static bool FetchInspectorBinary(const FString& InspectorVersion, const bool bIsRunningInChina); + SPATIALGDKSERVICES_API static bool FetchPackageBinary(const FString& PackageVersion, const FString& PackageExe, + const FString& PackageName, const FString& SaveLocation, + const bool bIsRunningInChina, const bool bUnzip); + +private: + // Timeout given in seconds. + static constexpr double ProcessTimeoutTime = 60.0; }; diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesConstants.h b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesConstants.h index 04fab494df..75a931bbc7 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesConstants.h +++ b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesConstants.h @@ -7,51 +7,78 @@ namespace SpatialGDKServicesConstants { #if PLATFORM_WINDOWS - // Assumes that spatial is installed and in the PATH - const FString SpatialPath = TEXT(""); - const FString Extension = TEXT("exe"); +// Assumes that spatial is installed and in the PATH +const FString SpatialPath = TEXT(""); +const FString Extension = TEXT("exe"); +const FString PlatformVersion = TEXT("x86_64-win32"); #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(""); +// 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(""); +const FString PlatformVersion = TEXT("x86_64-macos"); #endif - static inline const FString CreateExePath(FString Path, FString ExecutableName) - { - FString ExecutableFile = FPaths::SetExtension(ExecutableName, Extension); - return FPaths::Combine(Path, ExecutableFile); - } +static 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 SpatialOSConfigFileName = TEXT("spatialos.json"); - const FString ChinaEnvironmentArgument = TEXT(" --environment=cn-production"); +const FString GDKProgramPath = + FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(TEXT("SpatialGDK/Binaries/ThirdParty/Improbable/Programs")); +const FString SpatialExe = CreateExePath(SpatialPath, TEXT("spatial")); +const FString SchemaCompilerExe = CreateExePath(GDKProgramPath, TEXT("schema_compiler")); +const FString SpatialOSDirectory = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectDir(), TEXT("/../spatial/"))); +const FString SpatialOSConfigFileName = TEXT("spatialos.json"); +const FString CompiledSchemaDir = FPaths::Combine(SpatialOSDirectory, TEXT("build/assembly/schema")); +const FString SchemaBundlePath = FPaths::Combine(CompiledSchemaDir, TEXT("schema.sb")); +const FString SpatialOSSnapshotFolderPath = FPaths::Combine(SpatialOSDirectory, TEXT("snapshots")); +const FString ChinaEnvironmentArgument = TEXT(" --environment=cn-production"); +const FString RuntimePackageName = TEXT("runtime"); +const FString InspectorPackageName = TEXT("inspector"); +const FString RuntimeExe = FPaths::SetExtension(RuntimePackageName, Extension); +const FString InspectorExe = FPaths::SetExtension(InspectorPackageName, Extension); +const FString LocalDeploymentLogsDir = FPaths::Combine(SpatialOSDirectory, TEXT("logs/localdeployment")); - const FString SpatialOSRuntimePinnedStandardVersion = TEXT("0.4.3"); - const FString SpatialOSRuntimePinnedCompatbilityModeVersion = TEXT("14.5.4"); +static const FString GetRuntimeExecutablePath(const FString& RuntimeVersion) +{ + return FPaths::Combine(GDKProgramPath, RuntimePackageName, RuntimeVersion, RuntimeExe); +} + +static const FString GetInspectorExecutablePath(const FString& InspectorVersion) +{ + return FPaths::Combine(GDKProgramPath, InspectorPackageName, InspectorVersion, InspectorExe); +} - const FString InspectorURL = TEXT("http://localhost:31000/inspector"); - const FString InspectorV2URL = TEXT("http://localhost:31000/inspector-v2"); +const FString SpatialOSRuntimePinnedStandardVersion = TEXT("15.0.0"); - const FString PinnedStandardRuntimeTemplate = TEXT("n1standard4_std40_action1g1"); - const FString PinnedCompatibilityModeRuntimeTemplate = TEXT("n1standard4_std40_r0500"); - const FString PinnedChinaStandardRuntimeTemplate = TEXT("s5large16_std50_action1g1"); - const FString PinnedChinaCompatibilityModeRuntimeTemplate = TEXT("s5large16_std50_r0500"); - - const FString DevLoginDeploymentTag = TEXT("dev_login"); +const int32 RuntimeGRPCPort = 7777; +const int32 RuntimeHTTPPort = 5006; - const FString UseChinaServicesRegionFilename = TEXT("UseChinaServicesRegion"); +const FString InspectorGRPCAddress = FString::Printf(TEXT("localhost:%s"), *FString::FromInt(RuntimeGRPCPort)); +const FString InspectorHTTPAddress = FString::Printf(TEXT("localhost:%s"), *FString::FromInt(RuntimeHTTPPort)); +const FString InspectorV2URL = TEXT("http://localhost:33333/inspector-v2"); +const FString InspectorPinnedVersion = TEXT("15.0.1"); - const FString ProxyFileDirectory = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectIntermediateDir(), TEXT("Improbable"))); - const FString ProxyInfoFilePath = FPaths::Combine(ProxyFileDirectory, TEXT("ServerReceptionistProxyInfo.json")); +const FString PinnedStandardRuntimeTemplate = TEXT("n1standard4_std40_action1g1"); +const FString PinnedCompatibilityModeRuntimeTemplate = TEXT("n1standard4_std40_r0500"); +const FString PinnedChinaStandardRuntimeTemplate = TEXT("s5large16_std50_action1g1"); +const FString PinnedChinaCompatibilityModeRuntimeTemplate = TEXT("s5large16_std50_r0500"); + +const FString DevLoginDeploymentTag = TEXT("dev_login"); + +const FString UseChinaServicesRegionFilename = TEXT("UseChinaServicesRegion"); + +const FString ProxyFileDirectory = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectIntermediateDir(), TEXT("Improbable"))); +const FString ProxyInfoFilePath = FPaths::Combine(ProxyFileDirectory, TEXT("ServerReceptionistProxyInfo.json")); #if PLATFORM_MAC - const FString LsofCmdFilePath = TEXT("/usr/sbin/"); - const FString KillCmdFilePath = TEXT("/bin/"); +const FString LsofCmdFilePath = TEXT("/usr/sbin/"); +const FString BinPath = TEXT("/bin/"); #endif -} + +const int32 ExitCodeSuccess = 0; + +} // namespace SpatialGDKServicesConstants diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesModule.h b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesModule.h index b80512f204..fc4167e6e6 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesModule.h +++ b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesModule.h @@ -7,6 +7,8 @@ #include "Modules/ModuleInterface.h" #include "Modules/ModuleManager.h" +class SSpatialOutputLog; + class SPATIALGDKSERVICES_API FSpatialGDKServicesModule : public IModuleInterface { public: @@ -15,27 +17,23 @@ class SPATIALGDKSERVICES_API FSpatialGDKServicesModule : public IModuleInterface virtual void StartupModule() override; virtual void ShutdownModule() override; - virtual bool SupportsDynamicReloading() override - { - return true; - } + virtual bool SupportsDynamicReloading() override { return true; } FLocalDeploymentManager* GetLocalDeploymentManager(); FLocalReceptionistProxyServerManager* GetLocalReceptionistProxyServerManager(); + TWeakPtr GetSpatialOutputLog(); static FString GetSpatialGDKPluginDirectory(const FString& AppendPath = TEXT("")); - + static bool SpatialPreRunChecks(bool bIsInChina); - FORCEINLINE static FString GetProjectName() - { - return ProjectName; - } + FORCEINLINE static FString GetProjectName() { return ProjectName; } static void SetProjectName(const FString& InProjectName); static bool ParseJson(const FString& RawJsonString, TSharedPtr& JsonParsed); - static void ExecuteAndReadOutput(const FString& Executable, const FString& Arguments, const FString& DirectoryToRun, FString& OutResult, int32& ExitCode); + static void ExecuteAndReadOutput(const FString& Executable, const FString& Arguments, const FString& DirectoryToRun, FString& OutResult, + int32& ExitCode); private: FLocalDeploymentManager LocalDeploymentManager; diff --git a/SpatialGDK/Source/SpatialGDKServices/SpatialGDKServices.Build.cs b/SpatialGDK/Source/SpatialGDKServices/SpatialGDKServices.Build.cs index 3c3670859d..e80a1abea6 100644 --- a/SpatialGDK/Source/SpatialGDKServices/SpatialGDKServices.Build.cs +++ b/SpatialGDK/Source/SpatialGDKServices/SpatialGDKServices.Build.cs @@ -9,7 +9,7 @@ public SpatialGDKServices(ReadOnlyTargetRules Target) : base(Target) bLegacyPublicIncludePaths = false; PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; #pragma warning disable 0618 - bFasterWithoutUnity = true; // Deprecated in 4.24, replace with bUseUnity = false; once we drop support for 4.23 + 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; @@ -29,7 +29,8 @@ public SpatialGDKServices(ReadOnlyTargetRules Target) : base(Target) "Json", "JsonUtilities", "UnrealEd", - "Sockets" + "Sockets", + "HTTP" } ); } diff --git a/SpatialGDK/Source/SpatialGDKTests/Examples/SpatialGDKExampleTest.cpp b/SpatialGDK/Source/SpatialGDKTests/Examples/SpatialGDKExampleTest.cpp index 6229bad510..3b95901c94 100644 --- a/SpatialGDK/Source/SpatialGDKTests/Examples/SpatialGDKExampleTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/Examples/SpatialGDKExampleTest.cpp @@ -2,16 +2,13 @@ #include "Tests/TestDefinitions.h" -#include "HAL/IPlatformFileProfilerWrapper.h" #include "HAL/PlatformFilemanager.h" -#include "Misc/ScopeTryLock.h" #include "Misc/Paths.h" +#include "Misc/ScopeTryLock.h" -#define EXAMPLE_SIMPLE_TEST(TestName) \ - GDK_TEST(SpatialGDKExamples, SimpleExamples, TestName) +#define EXAMPLE_SIMPLE_TEST(TestName) GDK_TEST(SpatialGDKExamples, SimpleExamples, TestName) -#define EXAMPLE_COMPLEX_TEST(TestName) \ - GDK_COMPLEX_TEST(SpatialGDKExamples, ComplexExamples, TestName) +#define EXAMPLE_COMPLEX_TEST(TestName) GDK_COMPLEX_TEST(SpatialGDKExamples, ComplexExamples, TestName) DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKExamples, Log, All); DEFINE_LOG_CATEGORY(LogSpatialGDKExamples); @@ -35,8 +32,7 @@ DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FStartBackgroundThreadComputation bool FStartBackgroundThreadComputation::Update() { TSharedPtr LocalResult = InResult; - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [LocalResult] - { + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [LocalResult] { FScopeLock BackgroundComputationLock(&LocalResult->Mutex); FPlatformProcess::Sleep(COMPUTATION_DURATION); LocalResult->Value = 42; @@ -45,7 +41,8 @@ bool FStartBackgroundThreadComputation::Update() return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER(FWaitForComputationAndCheckResult, FAutomationTestBase*, Test, TSharedPtr, InResult); +DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER(FWaitForComputationAndCheckResult, FAutomationTestBase*, Test, TSharedPtr, + InResult); bool FWaitForComputationAndCheckResult::Update() { const double TimePassed = FPlatformTime::Seconds() - StartTime; @@ -138,17 +135,10 @@ const FString ExampleTestFolder = FPaths::Combine(FPaths::ProjectContentDir(), T class ExampleTestFixture { public: - ExampleTestFixture() - { - CreateTestFolders(); - } - ~ExampleTestFixture() - { - DeleteTestFolders(); - } + ExampleTestFixture() { CreateTestFolders(); } + ~ExampleTestFixture() { DeleteTestFolders(); } private: - void CreateTestFolders() { IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); diff --git a/SpatialGDK/Source/SpatialGDKTests/Examples/SpatialGDKTestGuidelines.h b/SpatialGDK/Source/SpatialGDKTests/Examples/SpatialGDKTestGuidelines.h index 2bac817b25..71be08fd1c 100644 --- a/SpatialGDK/Source/SpatialGDKTests/Examples/SpatialGDKTestGuidelines.h +++ b/SpatialGDK/Source/SpatialGDKTests/Examples/SpatialGDKTestGuidelines.h @@ -39,7 +39,7 @@ 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: + - 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). @@ -52,10 +52,12 @@ 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). + 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. + 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. @@ -63,7 +65,8 @@ 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. + 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. diff --git a/SpatialGDK/Source/SpatialGDKTests/Private/SpatialGDKTestsModule.cpp b/SpatialGDK/Source/SpatialGDKTests/Private/SpatialGDKTestsModule.cpp index 4a3531cf90..dc1198ca3a 100644 --- a/SpatialGDK/Source/SpatialGDKTests/Private/SpatialGDKTestsModule.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/Private/SpatialGDKTestsModule.cpp @@ -17,8 +17,6 @@ void FSpatialGDKTestsModule::StartupModule() InitializeSpatialFlagEarlyValues(); } -void FSpatialGDKTestsModule::ShutdownModule() -{ -} +void FSpatialGDKTestsModule::ShutdownModule() {} #undef LOCTEXT_NAMESPACE diff --git a/SpatialGDK/Source/SpatialGDKTests/Public/GDKAutomationTestBase.h b/SpatialGDK/Source/SpatialGDKTests/Public/GDKAutomationTestBase.h new file mode 100644 index 0000000000..bc2ada49df --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/Public/GDKAutomationTestBase.h @@ -0,0 +1,109 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once +#include "Misc/AutomationTest.h" +#include "SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.h" + +DEFINE_LOG_CATEGORY_STATIC(LogGDKTestBase, Log, All); + +/** + * This class extends the Unreal AutomationTestBase to allow unit tests to be augmented with a set-up step, called + * before each test. + * This class is then offered through a macro, in a similar way to `IMPLEMENT_SIMPLE_AUTOMATION_TEST`. + * + * To use this test base, the GDK_AUTOMATION_TEST macro should be used, followed by the test body: + * ``` + * GDK_AUTOMATION_TEST(MyModule, MyComponent, MyTestName) + * { + * // do some testing here... + * + * return true; + * } + * ``` + * + * Returning `true` indicates a test pass and returning `false` indicates test failure. + */ +class FGDKAutomationTestBase : public FAutomationTestBase +{ +public: + FGDKAutomationTestBase(const FString& Name, bool bInComplexTask, FString TestSrcFileName, uint32 TestSrcFileLine) + : FAutomationTestBase(Name, bInComplexTask) + , TestName(Name) + , TestSourceFileName(TestSrcFileName) + , TestSourceFileLine(TestSrcFileLine) + { + } + + virtual uint32 GetTestFlags() const override + { + return EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::EngineFilter; + } + + virtual bool IsStressTest() { return false; } + virtual uint32 GetRequiredDeviceNum() const override { return 1; } + +protected: + /** + * Checks for an existing deployment and stops it if one exists (also killing the associated workers). + * Ran before each test. + */ + virtual void SetUp() + { + FLocalDeploymentManager* LocalDeploymentManager = SpatialGDK::GetLocalDeploymentManager(); + if (LocalDeploymentManager->IsLocalDeploymentRunning() || LocalDeploymentManager->IsDeploymentStarting()) + { + UE_LOG(LogGDKTestBase, Warning, TEXT("Deployment found! (Was this left over from another test?)")) + UE_LOG(LogGDKTestBase, Warning, TEXT("Ending PIE session")) + GEditor->RequestEndPlayMap(); + ADD_LATENT_AUTOMATION_COMMAND(FStopDeployment); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForDeployment(this, EDeploymentState::IsNotRunning)); + } + } + + /** + * Implement this method with the test body + */ + virtual bool RunGDKTest(const FString& Parameters) = 0; + + virtual bool RunTest(const FString& Parameters) override + { + SetUp(); + return RunGDKTest(Parameters); + } + + virtual FString GetTestSourceFileName() const override { return TestSourceFileName; } + + virtual int32 GetTestSourceFileLine() const override { return TestSourceFileLine; } + + virtual FString GetBeautifiedTestName() const override { return TestName; } + + virtual void GetTests(TArray& OutBeautifiedNames, TArray& OutTestCommands) const override + { + OutBeautifiedNames.Add(TestName); + OutTestCommands.Add(FString()); + } + +private: + FString TestName; + FString TestSourceFileName; + uint32 TestSourceFileLine; +}; + +#define GDK_AUTOMATION_TEST(ModuleName, ComponentName, TestName) \ + IMPLEMENT_GDK_AUTOMATION_TEST(TestName, "SpatialGDK." #ModuleName "." #ComponentName "." #TestName) + +#define IMPLEMENT_GDK_AUTOMATION_TEST(TestName, PrettyName) \ + class TestName : public FGDKAutomationTestBase \ + { \ + public: \ + TestName(const FString& InName, bool bInComplexTask, FString TestSourceFileName, uint32 TestSourceFileLine) \ + : FGDKAutomationTestBase(InName, bInComplexTask, TestSourceFileName, TestSourceFileLine) \ + { \ + } \ + virtual bool RunGDKTest(const FString& Parameters) override; \ + }; \ + namespace \ + { \ + TestName TestName##__AutomationTestInstance(TEXT(PrettyName), false, __FILE__, __LINE__); \ + } \ + bool TestName::RunGDKTest(const FString& Parameters) diff --git a/SpatialGDK/Source/SpatialGDKTests/Public/SpatialGDKTestsModule.h b/SpatialGDK/Source/SpatialGDKTests/Public/SpatialGDKTestsModule.h index 562e21dc5c..6aedd6d053 100644 --- a/SpatialGDK/Source/SpatialGDKTests/Public/SpatialGDKTestsModule.h +++ b/SpatialGDK/Source/SpatialGDKTests/Public/SpatialGDKTestsModule.h @@ -12,8 +12,5 @@ class SPATIALGDKTESTS_API FSpatialGDKTestsModule : public IModuleInterface virtual void StartupModule() override; virtual void ShutdownModule() override; - virtual bool SupportsDynamicReloading() override - { - return true; - } + virtual bool SupportsDynamicReloading() override { return true; } }; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/EngineClasses/SpatialVirtualWorkerTranslationManager/SpatialVirtualWorkerTranslationManagerTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/EngineClasses/SpatialVirtualWorkerTranslationManager/SpatialVirtualWorkerTranslationManagerTest.cpp deleted file mode 100644 index 6f038c3a48..0000000000 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/EngineClasses/SpatialVirtualWorkerTranslationManager/SpatialVirtualWorkerTranslationManagerTest.cpp +++ /dev/null @@ -1,142 +0,0 @@ -// 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"; - - Manager->SetNumberOfVirtualWorkers(1); - - 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. - Manager->SetNumberOfVirtualWorkers(1); - - 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. - Manager->SetNumberOfVirtualWorkers(1); - - 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 53a10e985a..ad25a0f0c5 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/EngineClasses/SpatialVirtualWorkerTranslator/SpatialVirtualWorkerTranslatorTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/EngineClasses/SpatialVirtualWorkerTranslator/SpatialVirtualWorkerTranslatorTest.cpp @@ -1,6 +1,8 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "SpatialGDKTests/SpatialGDK/LoadBalancing/AbstractLBStrategy/LBStrategyStub.h" + +#include "SpatialGDKTests/Public/GDKAutomationTestBase.h" #include "Tests/TestingSchemaHelpers.h" #include "Tests/TestDefinitions.h" @@ -16,23 +18,35 @@ #include #include -#define VIRTUALWORKERTRANSLATOR_TEST(TestName) \ - GDK_TEST(Core, SpatialVirtualWorkerTranslator, TestName) +namespace +{ +const PhysicalWorkerName ValidWorkerOne = TEXT("ValidWorkerOne"); +const PhysicalWorkerName ValidWorkerTwo = TEXT("ValidWorkerTwo"); +const PhysicalWorkerName ValidWorkerThree = TEXT("ValidWorkerThree"); + +const Worker_PartitionId WorkerOneId = 101; +const Worker_PartitionId WorkerTwoId = 102; +const Worker_PartitionId WorkerThreeId = 103; +} // namespace + +#define VIRTUALWORKERTRANSLATOR_TEST(TestName) GDK_AUTOMATION_TEST(Core, SpatialVirtualWorkerTranslator, TestName) VIRTUALWORKERTRANSLATOR_TEST(GIVEN_init_is_not_called_THEN_return_not_ready) { ULBStrategyStub* LBStrategyStub = NewObject(); - TUniquePtr Translator = MakeUnique(LBStrategyStub, SpatialConstants::TRANSLATOR_UNSET_PHYSICAL_NAME); + TUniquePtr Translator = + MakeUnique(LBStrategyStub, nullptr, 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); + 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(nullptr, "my_worker_name"); + TUniquePtr Translator = MakeUnique(nullptr, nullptr, "my_worker_name"); TestEqual("Local physical worker name returned correctly", Translator->GetLocalPhysicalWorkerName(), "my_worker_name"); @@ -42,12 +56,15 @@ VIRTUALWORKERTRANSLATOR_TEST(GIVEN_worker_name_specified_in_constructor_THEN_ret VIRTUALWORKERTRANSLATOR_TEST(GIVEN_no_mapping_WHEN_nothing_has_changed_THEN_return_no_mappings_and_unintialized_state) { ULBStrategyStub* LBStrategyStub = NewObject(); - TUniquePtr Translator = MakeUnique(LBStrategyStub, SpatialConstants::TRANSLATOR_UNSET_PHYSICAL_NAME); + TUniquePtr Translator = + MakeUnique(LBStrategyStub, nullptr, SpatialConstants::TRANSLATOR_UNSET_PHYSICAL_NAME); TestNull("Worker 1 doesn't exist", Translator->GetPhysicalWorkerForVirtualWorker(1)); - TestEqual("Local virtual worker ID is not known.", Translator->GetLocalVirtualWorkerId(), SpatialConstants::INVALID_VIRTUAL_WORKER_ID); + 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); + TestEqual("LBStrategy stub reports an invalid virtual worker ID.", LBStrategyStub->GetVirtualWorkerId(), + SpatialConstants::INVALID_VIRTUAL_WORKER_ID); return true; } @@ -55,7 +72,8 @@ VIRTUALWORKERTRANSLATOR_TEST(GIVEN_no_mapping_WHEN_nothing_has_changed_THEN_retu VIRTUALWORKERTRANSLATOR_TEST(GIVEN_no_mapping_WHEN_receiving_empty_mapping_THEN_ignore_it) { ULBStrategyStub* LBStrategyStub = NewObject(); - TUniquePtr Translator = MakeUnique(LBStrategyStub, SpatialConstants::TRANSLATOR_UNSET_PHYSICAL_NAME); + TUniquePtr Translator = + MakeUnique(LBStrategyStub, nullptr, SpatialConstants::TRANSLATOR_UNSET_PHYSICAL_NAME); // Create an empty mapping. Schema_Object* DataObject = TestingSchemaHelpers::CreateTranslationComponentDataFields(); @@ -64,35 +82,11 @@ VIRTUALWORKERTRANSLATOR_TEST(GIVEN_no_mapping_WHEN_receiving_empty_mapping_THEN_ // 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_receiving_incomplete_mapping_THEN_ignore_it) -{ - ULBStrategyStub* LBStrategyStub = NewObject(); - TUniquePtr Translator = MakeUnique(LBStrategyStub, SpatialConstants::TRANSLATOR_UNSET_PHYSICAL_NAME); - - // 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. 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); - - 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); + 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); + TestEqual("LBStrategy stub reports an invalid virtual worker ID.", LBStrategyStub->GetVirtualWorkerId(), + SpatialConstants::INVALID_VIRTUAL_WORKER_ID); return true; } @@ -100,72 +94,31 @@ VIRTUALWORKERTRANSLATOR_TEST(GIVEN_no_mapping_WHEN_receiving_incomplete_mapping_ 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"); + TUniquePtr Translator = + MakeUnique(LBStrategyStub, nullptr, 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"); + TestingSchemaHelpers::AddTranslationComponentDataMapping(DataObject, 1, ValidWorkerOne, WorkerOneId); + TestingSchemaHelpers::AddTranslationComponentDataMapping(DataObject, 2, ValidWorkerTwo, WorkerTwoId); // Now apply the mapping to the translator and test the result. 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"); - - 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_an_invalid_mapping_is_received_THEN_ignore_it) -{ - ULBStrategyStub* LBStrategyStub = NewObject(); - TUniquePtr Translator = MakeUnique(LBStrategyStub, "ValidWorkerOne"); - - // Create a base mapping. - Schema_Object* ValidDataObject = TestingSchemaHelpers::CreateTranslationComponentDataFields(); - - // The mapping only has the following entries: - TestingSchemaHelpers::AddTranslationComponentDataMapping(ValidDataObject, 1, "ValidWorkerOne"); - TestingSchemaHelpers::AddTranslationComponentDataMapping(ValidDataObject, 2, "ValidWorkerTwo"); - - // Apply valid mapping to the translator. - Translator->ApplyVirtualWorkerManagerData(ValidDataObject); - - // 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"); + 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"); + 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); + TestEqual("Local claimed partition ID is known.", Translator->GetClaimedPartitionId(), WorkerOneId); TestTrue("Translator with local virtual worker ID is ready.", Translator->IsReady()); TestEqual("LBStrategy stub reports the correct virtual worker ID.", LBStrategyStub->GetVirtualWorkerId(), 1); @@ -175,14 +128,15 @@ VIRTUALWORKERTRANSLATOR_TEST(GIVEN_have_a_valid_mapping_WHEN_an_invalid_mapping_ VIRTUALWORKERTRANSLATOR_TEST(GIVEN_have_a_valid_mapping_WHEN_another_valid_mapping_is_received_THEN_update_accordingly) { ULBStrategyStub* LBStrategyStub = NewObject(); - TUniquePtr Translator = MakeUnique(LBStrategyStub, "ValidWorkerOne"); + TUniquePtr Translator = + MakeUnique(LBStrategyStub, nullptr, 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"); + TestingSchemaHelpers::AddTranslationComponentDataMapping(FirstValidDataObject, 1, ValidWorkerOne, WorkerOneId); + TestingSchemaHelpers::AddTranslationComponentDataMapping(FirstValidDataObject, 2, ValidWorkerTwo, WorkerTwoId); // Apply valid mapping to the translator. Translator->ApplyVirtualWorkerManagerData(FirstValidDataObject); @@ -191,8 +145,8 @@ VIRTUALWORKERTRANSLATOR_TEST(GIVEN_have_a_valid_mapping_WHEN_another_valid_mappi Schema_Object* SecondValidDataObject = TestingSchemaHelpers::CreateTranslationComponentDataFields(); // The mapping only has the following entries: - TestingSchemaHelpers::AddTranslationComponentDataMapping(SecondValidDataObject, 1, "ValidWorkerOne"); - TestingSchemaHelpers::AddTranslationComponentDataMapping(SecondValidDataObject, 2, "ValidWorkerThree"); + TestingSchemaHelpers::AddTranslationComponentDataMapping(SecondValidDataObject, 1, ValidWorkerOne, WorkerOneId); + TestingSchemaHelpers::AddTranslationComponentDataMapping(SecondValidDataObject, 2, ValidWorkerThree, WorkerThreeId); // Apply valid mapping to the translator. Translator->ApplyVirtualWorkerManagerData(SecondValidDataObject); @@ -200,53 +154,16 @@ VIRTUALWORKERTRANSLATOR_TEST(GIVEN_have_a_valid_mapping_WHEN_another_valid_mappi // 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"); + TestEqual("Virtual worker 1 is ValidWorkerOne", *VirtualWorker1PhysicalName, ValidWorkerOne); + TestEqual("Virtual worker 1 partition is 101", Translator->GetPartitionEntityForVirtualWorker(1), WorkerOneId); 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); - - // 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"); - - TestNull("There is no mapping for virtual worker 2", Translator->GetPhysicalWorkerForVirtualWorker(2)); + TestEqual("VirtualWorker 2 is ValidWorkerThree", *VirtualWorker2PhysicalName, ValidWorkerThree); + TestEqual("Virtual worker 2 partition is 103", Translator->GetPartitionEntityForVirtualWorker(2), WorkerThreeId); TestEqual("Local virtual worker ID is still known.", Translator->GetLocalVirtualWorkerId(), 1); + TestEqual("Local claimed partition ID is known.", Translator->GetClaimedPartitionId(), WorkerOneId); TestTrue("Translator with local virtual worker ID is still ready.", Translator->IsReady()); TestEqual("LBStrategy stub reports the correct virtual worker ID.", LBStrategyStub->GetVirtualWorkerId(), 1); diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialConnectionManagerTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialConnectionManagerTest.cpp index caee69fbbf..635712e4a9 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialConnectionManagerTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialConnectionManagerTest.cpp @@ -1,11 +1,10 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved -#include "Tests/TestDefinitions.h" -#include "Interop/Connection/SpatialConnectionManager.h" #include "CoreMinimal.h" +#include "Interop/Connection/SpatialConnectionManager.h" +#include "SpatialGDKTests/Public/GDKAutomationTestBase.h" -#define CONNECTIONMANAGER_TEST(TestName) \ - GDK_TEST(Core, SpatialConnectionManager, TestName) +#define CONNECTIONMANAGER_TEST(TestName) GDK_AUTOMATION_TEST(Core, SpatialConnectionManager, TestName) class FTemporaryCommandLine { @@ -55,6 +54,46 @@ CONNECTIONMANAGER_TEST(SetupFromURL_Locator_CustomLocator) return true; } +CONNECTIONMANAGER_TEST(SetupFromURL_Locator_CustomLocator_LocatorPort) +{ + // GIVEN + FTemporaryCommandLine TemporaryCommandLine(""); + const FURL URL(nullptr, TEXT("10.20.30.40?locator?customLocator?locatorPort=7000?playeridentity=foo?login=bar"), TRAVEL_Absolute); + USpatialConnectionManager* Manager = NewObject(); + + // WHEN + Manager->SetupConnectionConfigFromURL(URL, "SomeWorkerType"); + + // THEN + TestEqual("LocatorHost", Manager->LocatorConfig.LocatorHost, "10.20.30.40"); + TestEqual("LocatorPort", Manager->LocatorConfig.LocatorPort, 7000); + TestEqual("PlayerIdentityToken", Manager->LocatorConfig.PlayerIdentityToken, "foo"); + TestEqual("LoginToken", Manager->LocatorConfig.LoginToken, "bar"); + TestEqual("WorkerType", Manager->LocatorConfig.WorkerType, "SomeWorkerType"); + + return true; +} + +CONNECTIONMANAGER_TEST(SetupFromURL_Locator_IgnoreURLPort) +{ + // GIVEN + FTemporaryCommandLine TemporaryCommandLine("-locatorHost 99.88.77.66"); + const FURL URL(nullptr, TEXT("10.20.30.40:7000?locator?playeridentity=foo?login=bar"), TRAVEL_Absolute); + USpatialConnectionManager* Manager = NewObject(); + + // WHEN + Manager->SetupConnectionConfigFromURL(URL, "SomeWorkerType"); + + // THEN + TestEqual("LocatorHost", Manager->LocatorConfig.LocatorHost, "99.88.77.66"); + TestEqual("LocatorPort", Manager->LocatorConfig.LocatorPort, SpatialConstants::LOCATOR_PORT); + TestEqual("PlayerIdentityToken", Manager->LocatorConfig.PlayerIdentityToken, "foo"); + TestEqual("LoginToken", Manager->LocatorConfig.LoginToken, "bar"); + TestEqual("WorkerType", Manager->LocatorConfig.WorkerType, "SomeWorkerType"); + + return true; +} + CONNECTIONMANAGER_TEST(SetupFromURL_Locator_LocatorHost) { // GIVEN @@ -78,7 +117,8 @@ CONNECTIONMANAGER_TEST(SetupFromURL_DevAuth) { // GIVEN FTemporaryCommandLine TemporaryCommandLine("-locatorHost 99.88.77.66"); - const FURL URL(nullptr, + const FURL URL( + nullptr, TEXT("10.20.30.40?devauth?customLocator?devauthtoken=foo?deployment=bar?playerid=666?displayname=n00bkilla?metadata=important"), TRAVEL_Absolute); USpatialConnectionManager* Manager = NewObject(); @@ -212,7 +252,8 @@ CONNECTIONMANAGER_TEST(SetupFromCommandLine_Locator) CONNECTIONMANAGER_TEST(SetupFromCommandLine_DevAuth) { // GIVEN - FTemporaryCommandLine TemporaryCommandLine("-locatorHost 10.20.30.40 -devAuthToken foo -deployment bar -playerId 666 -displayName n00bkilla -metadata important"); + FTemporaryCommandLine TemporaryCommandLine( + "-locatorHost 10.20.30.40 -devAuthToken foo -deployment bar -playerId 666 -displayName n00bkilla -metadata important"); USpatialConnectionManager* Manager = NewObject(); // WHEN @@ -324,3 +365,17 @@ CONNECTIONMANAGER_TEST(SetupFromCommandLine_Receptionist_URLAndExternalBridge) return true; } + +CONNECTIONMANAGER_TEST(SetupFromCommandLine_Empty) +{ + // GIVEN + FTemporaryCommandLine TemporaryCommandLine(""); + USpatialConnectionManager* Manager = NewObject(); + + // WHEN + const bool bSuccess = Manager->TrySetupConnectionConfigFromCommandLine(""); + + // THEN + TestEqual("Success", bSuccess, false); + return true; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialOSWorkerInterface/SpatialOSWorkerConnectionSpy.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialOSWorkerInterface/SpatialOSWorkerConnectionSpy.cpp index 7c3f122446..875041e190 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialOSWorkerInterface/SpatialOSWorkerConnectionSpy.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialOSWorkerInterface/SpatialOSWorkerConnectionSpy.cpp @@ -4,70 +4,89 @@ #include "Interop/Connection/OutgoingMessages.h" #include "SpatialCommonTypes.h" +#include "SpatialView/ViewDelta.h" #include "Utils/SpatialLatencyTracer.h" #include #include +#include "SpatialView/CommandRetryHandler.h" + SpatialOSWorkerConnectionSpy::SpatialOSWorkerConnectionSpy() : NextRequestId(0) , LastEntityQuery(nullptr) -{} +{ +} -TArray SpatialOSWorkerConnectionSpy::GetOpList() +const TArray& SpatialOSWorkerConnectionSpy::GetEntityDeltas() { - return TArray(); + return PlaceholderEntityDeltas; } -Worker_RequestId SpatialOSWorkerConnectionSpy::SendReserveEntityIdsRequest(uint32_t NumOfEntities) +const TArray& SpatialOSWorkerConnectionSpy::GetWorkerMessages() +{ + return PlaceholderWorkerMessages; +} + +Worker_RequestId SpatialOSWorkerConnectionSpy::SendReserveEntityIdsRequest(uint32_t NumOfEntities, const SpatialGDK::FRetryData& RetryData) { return NextRequestId++; } -Worker_RequestId SpatialOSWorkerConnectionSpy::SendCreateEntityRequest(TArray Components, const Worker_EntityId* EntityId) +Worker_RequestId SpatialOSWorkerConnectionSpy::SendCreateEntityRequest(TArray Components, + const Worker_EntityId* EntityId, + const SpatialGDK::FRetryData& RetryData, + const FSpatialGDKSpanId& SpanId) { return NextRequestId++; } -Worker_RequestId SpatialOSWorkerConnectionSpy::SendDeleteEntityRequest(Worker_EntityId EntityId) +Worker_RequestId SpatialOSWorkerConnectionSpy::SendDeleteEntityRequest(Worker_EntityId EntityId, const SpatialGDK::FRetryData& RetryData, + const FSpatialGDKSpanId& SpanId) { return NextRequestId++; } -void SpatialOSWorkerConnectionSpy::SendAddComponent(Worker_EntityId EntityId, FWorkerComponentData* ComponentData) -{} +void SpatialOSWorkerConnectionSpy::SendAddComponent(Worker_EntityId EntityId, FWorkerComponentData* ComponentData, + const FSpatialGDKSpanId& SpanId) +{ +} -void SpatialOSWorkerConnectionSpy::SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) -{} +void SpatialOSWorkerConnectionSpy::SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, + const FSpatialGDKSpanId& SpanId) +{ +} -void SpatialOSWorkerConnectionSpy::SendComponentUpdate(Worker_EntityId EntityId, FWorkerComponentUpdate* ComponentUpdate) -{} +void SpatialOSWorkerConnectionSpy::SendComponentUpdate(Worker_EntityId EntityId, FWorkerComponentUpdate* ComponentUpdate, + const FSpatialGDKSpanId& SpanId) +{ +} -Worker_RequestId SpatialOSWorkerConnectionSpy::SendCommandRequest(Worker_EntityId EntityId, Worker_CommandRequest* Request, uint32_t CommandId) +Worker_RequestId SpatialOSWorkerConnectionSpy::SendCommandRequest(Worker_EntityId EntityId, Worker_CommandRequest* Request, + const SpatialGDK::FRetryData& RetryData, const FSpatialGDKSpanId& SpanId) { return NextRequestId++; } -void SpatialOSWorkerConnectionSpy::SendCommandResponse(Worker_RequestId RequestId, Worker_CommandResponse* Response) -{} - -void SpatialOSWorkerConnectionSpy::SendCommandFailure(Worker_RequestId RequestId, const FString& Message) -{} +void SpatialOSWorkerConnectionSpy::SendCommandResponse(Worker_RequestId RequestId, Worker_CommandResponse* Response, + const FSpatialGDKSpanId& SpanId) +{ +} -void SpatialOSWorkerConnectionSpy::SendLogMessage(uint8_t Level, const FName& LoggerName, const TCHAR* Message) -{} +void SpatialOSWorkerConnectionSpy::SendCommandFailure(Worker_RequestId RequestId, const FString& Message, const FSpatialGDKSpanId& SpanId) +{ +} -void SpatialOSWorkerConnectionSpy::SendComponentInterest(Worker_EntityId EntityId, TArray&& ComponentInterest) -{} +void SpatialOSWorkerConnectionSpy::SendLogMessage(uint8_t Level, const FName& LoggerName, const TCHAR* Message) {} -Worker_RequestId SpatialOSWorkerConnectionSpy::SendEntityQueryRequest(const Worker_EntityQuery* EntityQuery) +Worker_RequestId SpatialOSWorkerConnectionSpy::SendEntityQueryRequest(const Worker_EntityQuery* EntityQuery, + const SpatialGDK::FRetryData& RetryData) { LastEntityQuery = EntityQuery; return NextRequestId++; } -void SpatialOSWorkerConnectionSpy::SendMetrics(SpatialGDK::SpatialMetrics Metrics) -{} +void SpatialOSWorkerConnectionSpy::SendMetrics(SpatialGDK::SpatialMetrics Metrics) {} const Worker_EntityQuery* SpatialOSWorkerConnectionSpy::GetLastEntityQuery() { diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialOSWorkerInterface/SpatialOSWorkerConnectionSpy.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialOSWorkerInterface/SpatialOSWorkerConnectionSpy.h index a3cd9e4453..ea23ec46b4 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialOSWorkerInterface/SpatialOSWorkerConnectionSpy.h +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialOSWorkerInterface/SpatialOSWorkerConnectionSpy.h @@ -6,6 +6,7 @@ #include "Interop/Connection/OutgoingMessages.h" #include "SpatialCommonTypes.h" +#include "SpatialView/ViewDelta.h" #include "Utils/SpatialLatencyTracer.h" #include @@ -22,19 +23,25 @@ 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, FWorkerComponentUpdate* ComponentUpdate) override; - virtual Worker_RequestId SendCommandRequest(Worker_EntityId EntityId, Worker_CommandRequest* Request, uint32_t CommandId) override; - virtual void SendCommandResponse(Worker_RequestId RequestId, Worker_CommandResponse* Response) override; - virtual void SendCommandFailure(Worker_RequestId RequestId, const FString& Message) override; + virtual const TArray& GetEntityDeltas() override; + virtual const TArray& GetWorkerMessages() override; + virtual Worker_RequestId SendReserveEntityIdsRequest(uint32_t NumOfEntities, const SpatialGDK::FRetryData& RetryData) override; + virtual Worker_RequestId SendCreateEntityRequest(TArray Components, const Worker_EntityId* EntityId, + const SpatialGDK::FRetryData& RetryData, const FSpatialGDKSpanId& SpanId) override; + virtual Worker_RequestId SendDeleteEntityRequest(Worker_EntityId EntityId, const SpatialGDK::FRetryData& RetryData, + const FSpatialGDKSpanId& SpanId) override; + virtual void SendAddComponent(Worker_EntityId EntityId, FWorkerComponentData* ComponentData, const FSpatialGDKSpanId& SpanId) override; + virtual void SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, const FSpatialGDKSpanId& SpanId) override; + virtual void SendComponentUpdate(Worker_EntityId EntityId, FWorkerComponentUpdate* ComponentUpdate, + const FSpatialGDKSpanId& SpanId) override; + virtual Worker_RequestId SendCommandRequest(Worker_EntityId EntityId, Worker_CommandRequest* Request, + const SpatialGDK::FRetryData& RetryData, const FSpatialGDKSpanId& SpanId) override; + virtual void SendCommandResponse(Worker_RequestId RequestId, Worker_CommandResponse* Response, + const FSpatialGDKSpanId& SpanId) override; + virtual void SendCommandFailure(Worker_RequestId RequestId, const FString& Message, const FSpatialGDKSpanId& SpanId) 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 Worker_RequestId SendEntityQueryRequest(const Worker_EntityQuery* EntityQuery, + const SpatialGDK::FRetryData& RetryData) override; virtual void SendMetrics(SpatialGDK::SpatialMetrics Metrics) override; // The following methods are used to query for state in tests. @@ -47,4 +54,7 @@ class SpatialOSWorkerConnectionSpy : public SpatialOSWorkerInterface Worker_RequestId NextRequestId; const Worker_EntityQuery* LastEntityQuery; + + TArray PlaceholderEntityDeltas; + TArray PlaceholderWorkerMessages; }; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialWorkerConnectionTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialWorkerConnectionTest.cpp index 1f71f10e20..c5ac2a3a23 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialWorkerConnectionTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialWorkerConnectionTest.cpp @@ -1,16 +1,16 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved +#include "SpatialGDKTests/Public/GDKAutomationTestBase.h" #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" +#include "Engine/Engine.h" -#define WORKERCONNECTION_TEST(TestName) \ - GDK_TEST(Core, SpatialWorkerConnection, TestName) +#define WORKERCONNECTION_TEST(TestName) GDK_AUTOMATION_TEST(Core, SpatialWorkerConnection, TestName) using namespace SpatialGDK; @@ -47,7 +47,8 @@ void StartSetupConnectionConfigFromURL(USpatialConnectionManager* ConnectionMana } } -void FinishSetupConnectionConfig(USpatialConnectionManager* ConnectionManager, const FString& WorkerType, const FURL& URL, bool bUseReceptionist) +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) @@ -84,20 +85,20 @@ bool FWaitForSeconds::Update() } } -DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER(FSetupWorkerConnection, USpatialConnectionManager*, ConnectionManager, bool, bConnectAsClient); +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) - { + 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); @@ -124,7 +125,8 @@ bool FResetConnectionProcessed::Update() return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCheckConnectionStatus, FAutomationTestBase*, Test, USpatialConnectionManager*, ConnectionManager, bool, bExpectedIsConnected); +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); @@ -136,7 +138,13 @@ bool FSendReserveEntityIdsRequest::Update() { uint32_t NumOfEntities = 1; USpatialWorkerConnection* Connection = ConnectionManager->GetWorkerConnection(); - Connection->SendReserveEntityIdsRequest(NumOfEntities); + if (Connection == nullptr) + { + return false; + } + + Connection->SendReserveEntityIdsRequest(NumOfEntities, RETRY_UNTIL_COMPLETE); + Connection->Flush(); return true; } @@ -147,7 +155,13 @@ bool FSendCreateEntityRequest::Update() TArray Components; const Worker_EntityId* EntityId = nullptr; USpatialWorkerConnection* Connection = ConnectionManager->GetWorkerConnection(); - Connection->SendCreateEntityRequest(MoveTemp(Components), EntityId); + if (Connection == nullptr) + { + return false; + } + + Connection->SendCreateEntityRequest(MoveTemp(Components), EntityId, RETRY_UNTIL_COMPLETE); + Connection->Flush(); return true; } @@ -157,25 +171,35 @@ bool FSendDeleteEntityRequest::Update() { const Worker_EntityId EntityId = 0; USpatialWorkerConnection* Connection = ConnectionManager->GetWorkerConnection(); - Connection->SendDeleteEntityRequest(EntityId); + if (Connection == nullptr) + { + return false; + } + + Connection->SendDeleteEntityRequest(EntityId, RETRY_UNTIL_COMPLETE); + Connection->Flush(); return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FFindWorkerResponseOfType, FAutomationTestBase*, Test, USpatialConnectionManager*, ConnectionManager, uint8_t, ExpectedOpType); +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& Ops : Connection->GetOpList()) + if (Connection == nullptr) + { + return false; + } + + Connection->Advance(0); + for (const auto& Op : Connection->GetWorkerMessages()) { - for (uint32_t i = 0; i < Ops.Count; i++) + if (Op.op_type == ExpectedOpType) { - if (Ops.Ops[i].op_type == ExpectedOpType) - { - bFoundOpOfExpectedType = true; - break; - } + bFoundOpOfExpectedType = true; + break; } } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialOSDispatcherInterface/SpatialOSDispatcherSpy.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialOSDispatcherInterface/SpatialOSDispatcherSpy.cpp index ae6243939e..5f5fc1870d 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialOSDispatcherInterface/SpatialOSDispatcherSpy.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialOSDispatcherInterface/SpatialOSDispatcherSpy.cpp @@ -2,76 +2,66 @@ #include "SpatialOSDispatcherSpy.h" -SpatialOSDispatcherSpy::SpatialOSDispatcherSpy() -{} +SpatialOSDispatcherSpy::SpatialOSDispatcherSpy() {} // Dispatcher Calls -void SpatialOSDispatcherSpy::OnCriticalSection(bool InCriticalSection) -{} +void SpatialOSDispatcherSpy::OnCriticalSection(bool InCriticalSection) {} -void SpatialOSDispatcherSpy::OnAddEntity(const Worker_AddEntityOp& Op) -{} +void SpatialOSDispatcherSpy::OnAddEntity(const Worker_AddEntityOp& Op) {} -void SpatialOSDispatcherSpy::OnAddComponent(const Worker_AddComponentOp& Op) -{} +void SpatialOSDispatcherSpy::OnAddComponent(const Worker_AddComponentOp& Op) {} -void SpatialOSDispatcherSpy::OnRemoveEntity(const Worker_RemoveEntityOp& Op) -{} +void SpatialOSDispatcherSpy::OnRemoveEntity(const Worker_RemoveEntityOp& Op) {} -void SpatialOSDispatcherSpy::OnRemoveComponent(const Worker_RemoveComponentOp& Op) -{} +void SpatialOSDispatcherSpy::OnRemoveComponent(const Worker_RemoveComponentOp& Op) {} -void SpatialOSDispatcherSpy::FlushRemoveComponentOps() -{} +void SpatialOSDispatcherSpy::FlushRemoveComponentOps() {} -void SpatialOSDispatcherSpy::DropQueuedRemoveComponentOpsForEntity(Worker_EntityId EntityId) -{} +void SpatialOSDispatcherSpy::DropQueuedRemoveComponentOpsForEntity(Worker_EntityId EntityId) {} -void SpatialOSDispatcherSpy::OnAuthorityChange(const Worker_AuthorityChangeOp& Op) -{} +void SpatialOSDispatcherSpy::OnAuthorityChange(const Worker_ComponentSetAuthorityChangeOp& Op) {} -void SpatialOSDispatcherSpy::OnComponentUpdate(const Worker_ComponentUpdateOp& 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. +// 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::OnCommandRequest(const Worker_Op& Op) {} -void SpatialOSDispatcherSpy::OnCommandResponse(const Worker_CommandResponseOp& Op) -{} +void SpatialOSDispatcherSpy::OnCommandResponse(const Worker_Op& Op) {} -void SpatialOSDispatcherSpy::OnReserveEntityIdsResponse(const Worker_ReserveEntityIdsResponseOp& Op) -{} +void SpatialOSDispatcherSpy::OnReserveEntityIdsResponse(const Worker_ReserveEntityIdsResponseOp& Op) {} -void SpatialOSDispatcherSpy::OnCreateEntityResponse(const Worker_CreateEntityResponseOp& Op) -{} +void SpatialOSDispatcherSpy::OnCreateEntityResponse(const Worker_Op& Op) {} -void SpatialOSDispatcherSpy::AddPendingActorRequest(Worker_RequestId RequestId, USpatialActorChannel* Channel) -{} +void SpatialOSDispatcherSpy::AddPendingActorRequest(Worker_RequestId RequestId, USpatialActorChannel* Channel) {} -void SpatialOSDispatcherSpy::AddPendingReliableRPC(Worker_RequestId RequestId, TSharedRef ReliableRPC) -{} +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::AddReserveEntityIdsDelegate(Worker_RequestId RequestId, ReserveEntityIDsDelegate Delegate) {} void SpatialOSDispatcherSpy::AddCreateEntityDelegate(Worker_RequestId RequestId, CreateEntityDelegate Delegate) -{} +{ + CreateEntityDelegates.Add(RequestId, Delegate); +} -void SpatialOSDispatcherSpy::OnEntityQueryResponse(const Worker_EntityQueryResponseOp& Op) -{} +void SpatialOSDispatcherSpy::OnEntityQueryResponse(const Worker_EntityQueryResponseOp& Op) {} EntityQueryDelegate* SpatialOSDispatcherSpy::GetEntityQueryDelegate(Worker_RequestId RequestId) { return EntityQueryDelegates.Find(RequestId); } + +CreateEntityDelegate* SpatialOSDispatcherSpy::GetCreateEntityDelegate(Worker_RequestId RequestId) +{ + return CreateEntityDelegates.Find(RequestId); +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialOSDispatcherInterface/SpatialOSDispatcherSpy.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialOSDispatcherInterface/SpatialOSDispatcherSpy.h index 45546d62ff..1193b292ec 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialOSDispatcherInterface/SpatialOSDispatcherSpy.h +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialOSDispatcherInterface/SpatialOSDispatcherSpy.h @@ -25,18 +25,19 @@ class SpatialOSDispatcherSpy : public SpatialOSDispatcherInterface 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 OnAuthorityChange(const Worker_ComponentSetAuthorityChangeOp& 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. + // 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 OnCommandRequest(const Worker_Op& Op) override; + virtual void OnCommandResponse(const Worker_Op& Op) override; virtual void OnReserveEntityIdsResponse(const Worker_ReserveEntityIdsResponseOp& Op) override; - virtual void OnCreateEntityResponse(const Worker_CreateEntityResponseOp& Op) override; + virtual void OnCreateEntityResponse(const Worker_Op& Op) override; virtual void AddPendingActorRequest(Worker_RequestId RequestId, USpatialActorChannel* Channel) override; virtual void AddPendingReliableRPC(Worker_RequestId RequestId, TSharedRef ReliableRPC) override; @@ -49,7 +50,9 @@ class SpatialOSDispatcherSpy : public SpatialOSDispatcherInterface // Methods to extract information about calls made. EntityQueryDelegate* GetEntityQueryDelegate(Worker_RequestId RequestId); + CreateEntityDelegate* GetCreateEntityDelegate(Worker_RequestId RequestId); private: TMap EntityQueryDelegates; + TMap CreateEntityDelegates; }; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialWorkerFlags/SpatialWorkerFlagsTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialWorkerFlags/SpatialWorkerFlagsTest.cpp index 2dfd93acae..0837d536ad 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialWorkerFlags/SpatialWorkerFlagsTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialWorkerFlags/SpatialWorkerFlagsTest.cpp @@ -2,95 +2,276 @@ #include "Interop/SpatialWorkerFlags.h" +#include "SpatialGDKTests/Public/GDKAutomationTestBase.h" #include "Tests/TestDefinitions.h" #include "WorkerFlagsTestSpyObject.h" -#define SPATIALWORKERFLAGS_TEST(TestName) \ - GDK_TEST(Core, SpatialWorkerFlags, TestName) +#define SPATIALWORKERFLAGS_TEST(TestName) GDK_AUTOMATION_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 +const FString TestWorkerFlagKey = TEXT("test"); +const FString TestWorkerFlagKey2 = TEXT("test2"); +const FString TestWorkerFlagValue = TEXT("10"); +const FString TestWorkerFlagValue2 = TEXT("20"); +} // anonymous namespace SPATIALWORKERFLAGS_TEST(GIVEN_a_flagUpdate_op_WHEN_adding_a_worker_flag_THEN_flag_added) { - USpatialWorkerFlags* SpatialWorkerFlags = NewObject(); + // GIVEN // Add test flag - Worker_FlagUpdateOp OpAddFlag = CreateWorkerFlagUpdateOp("test", "10"); - SpatialWorkerFlags->ApplyWorkerFlagUpdate(OpAddFlag); - + USpatialWorkerFlags* SpatialWorkerFlags = NewObject(); + // WHEN + SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey, TestWorkerFlagValue); + // THEN FString OutFlagValue; - TestTrue("Flag added in the WorkerFlags map: ", SpatialWorkerFlags->GetWorkerFlag("test", OutFlagValue)); + TestTrue("Flag added in the WorkerFlags map: ", SpatialWorkerFlags->GetWorkerFlag(TestWorkerFlagKey, OutFlagValue)); + TestEqual("Correct value stored in the WorkerFlags map: ", OutFlagValue, TestWorkerFlagValue); + return true; +} +SPATIALWORKERFLAGS_TEST(GIVEN_a_flagUpdate_op_WHEN_removing_a_worker_flag_THEN_flag_removed) +{ + // GIVEN + // Add test flag + USpatialWorkerFlags* SpatialWorkerFlags = NewObject(); + SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey, TestWorkerFlagValue); + FString OutFlagValue; + TestTrue("Flag added in the WorkerFlags map: ", SpatialWorkerFlags->GetWorkerFlag(TestWorkerFlagKey, OutFlagValue)); + TestEqual("Correct value stored in the WorkerFlags map: ", OutFlagValue, TestWorkerFlagValue); + // WHEN + // Remove test flag + SpatialWorkerFlags->RemoveWorkerFlag(TestWorkerFlagKey); + // THEN + TestFalse("Flag removed from the WorkerFlags map: ", SpatialWorkerFlags->GetWorkerFlag(TestWorkerFlagKey, OutFlagValue)); return true; } +// Delegates +// Any flag updates +SPATIALWORKERFLAGS_TEST(GIVEN_a_registered_any_flag_update_delegate_WHEN_any_worker_flag_updates_THEN_delegate_invoked) +{ + // GIVEN + // Register callback + UWorkerFlagsTestSpyObject* SpyObj = NewObject(); + FOnAnyWorkerFlagUpdatedBP WorkerFlagDelegate; + WorkerFlagDelegate.BindDynamic(SpyObj, &UWorkerFlagsTestSpyObject::SetAnyFlagUpdated); + USpatialWorkerFlags* SpatialWorkerFlags = NewObject(); + SpatialWorkerFlags->RegisterAnyFlagUpdatedCallback(WorkerFlagDelegate); + // WHEN + // Update flag + SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey, TestWorkerFlagValue); + // Update another flag + SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey2, TestWorkerFlagValue2); + // THEN + TestEqual("Delegate Function was called twice", SpyObj->GetTimesFlagUpdated(), 2); + return true; +} -SPATIALWORKERFLAGS_TEST(GIVEN_a_flagUpdate_op_WHEN_removing_a_worker_flag_THEN_flag_removed) +SPATIALWORKERFLAGS_TEST(GIVEN_a_registered_any_flag_update_delegate_WHEN_unregistering_the_delegate_THEN_delegate_is_not_invoked) { + // GIVEN + // Register callback + UWorkerFlagsTestSpyObject* SpyObj = NewObject(); + FOnAnyWorkerFlagUpdatedBP WorkerFlagDelegate; + WorkerFlagDelegate.BindDynamic(SpyObj, &UWorkerFlagsTestSpyObject::SetAnyFlagUpdated); USpatialWorkerFlags* SpatialWorkerFlags = NewObject(); + SpatialWorkerFlags->RegisterAnyFlagUpdatedCallback(WorkerFlagDelegate); // 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)); + SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey, TestWorkerFlagValue); + TestEqual("Delegate Function was called", SpyObj->GetTimesFlagUpdated(), 1); + // WHEN + // Unregister callback + SpatialWorkerFlags->UnregisterAnyFlagUpdatedCallback(WorkerFlagDelegate); + // Update test flag again + SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey, TestWorkerFlagValue); + // THEN + TestEqual("Delegate Function was called only once", SpyObj->GetTimesFlagUpdated(), 1); + return true; +} - // Remove test flag - Worker_FlagUpdateOp OpRemoveFlag = CreateWorkerFlagUpdateOp("test", nullptr); - SpatialWorkerFlags->ApplyWorkerFlagUpdate(OpRemoveFlag); +SPATIALWORKERFLAGS_TEST(GIVEN_an_updated_flag_WHEN_registering_and_invoking_an_any_flag_update_delegate_THEN_delegate_is_invoked) +{ + // GIVEN + // Update flag + USpatialWorkerFlags* SpatialWorkerFlags = NewObject(); + SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey, TestWorkerFlagValue); + // WHEN + // Register callback + UWorkerFlagsTestSpyObject* SpyObj = NewObject(); + FOnAnyWorkerFlagUpdatedBP WorkerFlagDelegate; + WorkerFlagDelegate.BindDynamic(SpyObj, &UWorkerFlagsTestSpyObject::SetAnyFlagUpdated); + SpatialWorkerFlags->RegisterAndInvokeAnyFlagUpdatedCallback(WorkerFlagDelegate); + // THEN + TestEqual("Delegate Function was called once", SpyObj->GetTimesFlagUpdated(), 1); + // Add test flag 2 + SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey2, TestWorkerFlagValue2); + TestEqual("Delegate Function was called twice", SpyObj->GetTimesFlagUpdated(), 2); + return true; +} - TestFalse("Flag removed from the WorkerFlags map: ", SpatialWorkerFlags->GetWorkerFlag("test", OutFlagValue)); +SPATIALWORKERFLAGS_TEST(GIVEN_no_flags_WHEN_registering_and_invoking_an_any_flag_update_delegate_THEN_delegate_is_not_invoked) +{ + // GIVEN + // No flags + // WHEN + // Register callback + UWorkerFlagsTestSpyObject* SpyObj = NewObject(); + FOnAnyWorkerFlagUpdatedBP WorkerFlagDelegate; + WorkerFlagDelegate.BindDynamic(SpyObj, &UWorkerFlagsTestSpyObject::SetAnyFlagUpdated); + USpatialWorkerFlags* SpatialWorkerFlags = NewObject(); + SpatialWorkerFlags->RegisterAndInvokeAnyFlagUpdatedCallback(WorkerFlagDelegate); + // THEN + TestEqual("Delegate Function was not called", SpyObj->GetTimesFlagUpdated(), 0); + return true; +} +// a flag update +SPATIALWORKERFLAGS_TEST(GIVEN_a_registered_flag_update_delegate_WHEN_the_worker_flag_updates_THEN_delegate_is_invoked) +{ + // GIVEN + // Register callback + UWorkerFlagsTestSpyObject* SpyObj = NewObject(); + FOnWorkerFlagUpdatedBP WorkerFlagDelegate; + WorkerFlagDelegate.BindDynamic(SpyObj, &UWorkerFlagsTestSpyObject::SetFlagUpdated); + USpatialWorkerFlags* SpatialWorkerFlags = NewObject(); + SpatialWorkerFlags->RegisterFlagUpdatedCallback(TestWorkerFlagKey, WorkerFlagDelegate); + // WHEN + // Update test flag + SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey, TestWorkerFlagValue); + // Update test flag again + SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey, TestWorkerFlagValue); + // THEN + TestEqual("Delegate Function was called twice", SpyObj->GetTimesFlagUpdated(), 2); return true; } -SPATIALWORKERFLAGS_TEST(GIVEN_a_bound_delegate_WHEN_a_worker_flag_updates_THEN_bound_function_invoked) +SPATIALWORKERFLAGS_TEST(GIVEN_a_registered_flag_update_delegate_WHEN_a_different_worker_flag_updates_THEN_delegate_is_not_invoked) { + // GIVEN + // Register callback UWorkerFlagsTestSpyObject* SpyObj = NewObject(); - FOnWorkerFlagsUpdatedBP WorkerFlagDelegate; + FOnWorkerFlagUpdatedBP WorkerFlagDelegate; WorkerFlagDelegate.BindDynamic(SpyObj, &UWorkerFlagsTestSpyObject::SetFlagUpdated); USpatialWorkerFlags* SpatialWorkerFlags = NewObject(); - SpatialWorkerFlags->BindToOnWorkerFlagsUpdated(WorkerFlagDelegate); + SpatialWorkerFlags->RegisterFlagUpdatedCallback(TestWorkerFlagKey, WorkerFlagDelegate); + + // WHEN + // Add a different test flag + SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey2, TestWorkerFlagValue2); + + // Update different test flag again + SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey2, TestWorkerFlagValue2); + + // THEN + TestEqual("Delegate Function was not called", SpyObj->GetTimesFlagUpdated(), 0); + + return true; +} + +SPATIALWORKERFLAGS_TEST(GIVEN_a_registered_flag_update_delegate_WHEN_unregistered_the_delegate_THEN_delegate_is_not_invoked) +{ + // GIVEN + // Register callback + UWorkerFlagsTestSpyObject* SpyObj = NewObject(); + FOnWorkerFlagUpdatedBP WorkerFlagDelegate; + WorkerFlagDelegate.BindDynamic(SpyObj, &UWorkerFlagsTestSpyObject::SetFlagUpdated); + USpatialWorkerFlags* SpatialWorkerFlags = NewObject(); + SpatialWorkerFlags->RegisterFlagUpdatedCallback(TestWorkerFlagKey, WorkerFlagDelegate); // Add test flag - Worker_FlagUpdateOp OpAddFlag = CreateWorkerFlagUpdateOp("test", "10"); - SpatialWorkerFlags->ApplyWorkerFlagUpdate(OpAddFlag); + SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey, TestWorkerFlagValue); + + TestEqual("Delegate Function was called", SpyObj->GetTimesFlagUpdated(), 1); + + // WHEN + // Unregister callback + SpatialWorkerFlags->UnregisterFlagUpdatedCallback(TestWorkerFlagKey, WorkerFlagDelegate); + + // Update test flag + SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey, TestWorkerFlagValue); - TestTrue("Delegate Function was called", SpyObj->GetTimesFlagUpdated() == 1); + // THEN + TestEqual("Delegate Function was called only once", SpyObj->GetTimesFlagUpdated(), 1); return true; } -SPATIALWORKERFLAGS_TEST(GIVEN_a_bound_delegate_WHEN_unbind_the_delegate_THEN_bound_function_is_not_invoked) +SPATIALWORKERFLAGS_TEST(GIVEN_an_updated_flag_WHEN_registering_and_invoking_flag_update_delegate_THEN_delegate_is_invoked) { + // GIVEN + // Add test flag + USpatialWorkerFlags* SpatialWorkerFlags = NewObject(); + SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey, TestWorkerFlagValue); + + // WHEN + // Register callback UWorkerFlagsTestSpyObject* SpyObj = NewObject(); - FOnWorkerFlagsUpdatedBP WorkerFlagDelegate; + FOnWorkerFlagUpdatedBP WorkerFlagDelegate; + WorkerFlagDelegate.BindDynamic(SpyObj, &UWorkerFlagsTestSpyObject::SetFlagUpdated); + SpatialWorkerFlags->RegisterAndInvokeFlagUpdatedCallback(TestWorkerFlagKey, WorkerFlagDelegate); + // THEN + TestEqual("Delegate Function was called", SpyObj->GetTimesFlagUpdated(), 1); + // Update test flag + SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey, TestWorkerFlagValue); + TestEqual("Delegate Function was called twice", SpyObj->GetTimesFlagUpdated(), 2); + return true; +} + +SPATIALWORKERFLAGS_TEST(GIVEN_no_flags_WHEN_registering_and_invoking_flag_update_delegate_THEN_delegate_is_not_invoked) +{ + // GIVEN + // No flags + + // WHEN + // Register callback + UWorkerFlagsTestSpyObject* SpyObj = NewObject(); + FOnWorkerFlagUpdatedBP WorkerFlagDelegate; WorkerFlagDelegate.BindDynamic(SpyObj, &UWorkerFlagsTestSpyObject::SetFlagUpdated); USpatialWorkerFlags* SpatialWorkerFlags = NewObject(); - SpatialWorkerFlags->BindToOnWorkerFlagsUpdated(WorkerFlagDelegate); - // Add test flag - Worker_FlagUpdateOp OpAddFlag = CreateWorkerFlagUpdateOp("test", "10"); - SpatialWorkerFlags->ApplyWorkerFlagUpdate(OpAddFlag); + SpatialWorkerFlags->RegisterAndInvokeFlagUpdatedCallback(TestWorkerFlagKey, WorkerFlagDelegate); + + // THEN + TestEqual("Delegate Function was not called", SpyObj->GetTimesFlagUpdated(), 0); - TestTrue("Delegate Function was called", SpyObj->GetTimesFlagUpdated() == 1); + // Add a different test flag + SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey2, TestWorkerFlagValue2); - SpatialWorkerFlags->UnbindFromOnWorkerFlagsUpdated(WorkerFlagDelegate); + TestEqual("Delegate Function was not called", SpyObj->GetTimesFlagUpdated(), 0); + return true; +} + +// Callback that unregister itself +// Defined this function here so the relevant code can be read together with the test +void UWorkerFlagsTestSpyObject::SetFlagUpdatedAndUnregisterCallback(const FString& FlagName, const FString& FlagValue) +{ + SetFlagUpdated(FlagName, FlagValue); + SpatialWorkerFlags->UnregisterFlagUpdatedCallback(FlagName, WorkerFlagDelegate); +} + +SPATIALWORKERFLAGS_TEST( + GIVEN_a_registered_flag_update_delegate_that_unregisters_delegate_WHEN_the_worker_flag_updates_THEN_delegate_is_invoked_and_the_delegate_is_unregistered) +{ + // GIVEN + // Register callback + UWorkerFlagsTestSpyObject* SpyObj = NewObject(); + SpyObj->SpatialWorkerFlags = NewObject(); + SpyObj->WorkerFlagDelegate.BindDynamic(SpyObj, &UWorkerFlagsTestSpyObject::SetFlagUpdatedAndUnregisterCallback); + + SpyObj->SpatialWorkerFlags->RegisterFlagUpdatedCallback(TestWorkerFlagKey, SpyObj->WorkerFlagDelegate); + + // WHEN // Update test flag - SpatialWorkerFlags->ApplyWorkerFlagUpdate(OpAddFlag); + SpyObj->SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey, TestWorkerFlagValue); + + // Update test flag again + SpyObj->SpatialWorkerFlags->SetWorkerFlag(TestWorkerFlagKey, TestWorkerFlagValue); - TestTrue("Delegate Function was called only once", SpyObj->GetTimesFlagUpdated() == 1); + // THEN + TestEqual("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 index e2388a6061..a74f7c240f 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialWorkerFlags/WorkerFlagsTestSpyObject.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialWorkerFlags/WorkerFlagsTestSpyObject.cpp @@ -2,15 +2,17 @@ #include "WorkerFlagsTestSpyObject.h" -void UWorkerFlagsTestSpyObject::SetFlagUpdated(const FString& FlagName, const FString& FlagValue) +void UWorkerFlagsTestSpyObject::SetAnyFlagUpdated(const FString& FlagName, const FString& FlagValue) { - TimesUpdated++; + SetFlagUpdated(FlagValue, FlagValue); +} - return; +void UWorkerFlagsTestSpyObject::SetFlagUpdated(const FString& FlagName, const FString& FlagValue) +{ + ++TimesUpdated; } 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 index 193e744bc6..09739286b8 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialWorkerFlags/WorkerFlagsTestSpyObject.h +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/SpatialWorkerFlags/WorkerFlagsTestSpyObject.h @@ -5,19 +5,29 @@ #include "WorkerFlagsTestSpyObject.generated.h" - UCLASS() class UWorkerFlagsTestSpyObject : public UObject { GENERATED_BODY() + public: + UFUNCTION() + void SetAnyFlagUpdated(const FString& FlagName, const FString& FlagValue); UFUNCTION() void SetFlagUpdated(const FString& FlagName, const FString& FlagValue); + UFUNCTION() // Defined in SpatialWorkerFlagsTest.cpp so the relevant code can be read together with the test + void SetFlagUpdatedAndUnregisterCallback(const FString& FlagName, const FString& FlagValue); + int GetTimesFlagUpdated() const; -private: + UPROPERTY() + USpatialWorkerFlags* SpatialWorkerFlags; + + UPROPERTY() + FOnWorkerFlagUpdatedBP WorkerFlagDelegate; - int TimesUpdated = 0; -}; +private: + int TimesUpdated = 0; +}; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalanceEnforcer/SpatialLoadBalanceEnforcerTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalanceEnforcer/SpatialLoadBalanceEnforcerTest.cpp deleted file mode 100644 index f175ce919b..0000000000 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalanceEnforcer/SpatialLoadBalanceEnforcerTest.cpp +++ /dev/null @@ -1,485 +0,0 @@ -// 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 index 052d8398b3..687e25e969 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/AbstractLBStrategy/LBStrategyStub.h +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/AbstractLBStrategy/LBStrategyStub.h @@ -16,8 +16,5 @@ class SPATIALGDKTESTS_API ULBStrategyStub : public UAbstractLBStrategy GENERATED_BODY() public: - VirtualWorkerId GetVirtualWorkerId() const - { - return LocalVirtualWorkerId; - } + 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 02e6ba581b..e06e6f313f 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/GridBasedLBStrategyTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/GridBasedLBStrategyTest.cpp @@ -2,6 +2,7 @@ #include "LoadBalancing/GridBasedLBStrategy.h" #include "Schema/StandardLibrary.h" +#include "SpatialConstants.h" #include "TestGridBasedLBStrategy.h" #include "CoreMinimal.h" @@ -9,17 +10,16 @@ #include "Engine/World.h" #include "GameFramework/DefaultPawn.h" #include "GameFramework/GameStateBase.h" +#include "SpatialGDKTests/Public/GDKAutomationTestBase.h" #include "Tests/AutomationCommon.h" #include "Tests/AutomationEditorCommon.h" #include "Tests/TestDefinitions.h" -#define GRIDBASEDLBSTRATEGY_TEST(TestName) \ - GDK_TEST(Core, UGridBasedLBStrategy, TestName) +#define GRIDBASEDLBSTRATEGY_TEST(TestName) GDK_AUTOMATION_TEST(Core, UGridBasedLBStrategy, TestName) // Test Globals namespace { - UWorld* TestWorld; TMap TestActors; UGridBasedLBStrategy* Strat; @@ -31,8 +31,7 @@ UWorld* GetAnyGameWorld() const TIndirectArray& WorldContexts = GEngine->GetWorldContexts(); for (const FWorldContext& Context : WorldContexts) { - if ((Context.WorldType == EWorldType::PIE || Context.WorldType == EWorldType::Game) - && (Context.World() != nullptr)) + if ((Context.WorldType == EWorldType::PIE || Context.WorldType == EWorldType::Game) && (Context.World() != nullptr)) { World = Context.World(); break; @@ -61,10 +60,13 @@ bool FCleanup::Update() TestActors.Empty(); Strat = nullptr; + GEditor->RequestEndPlayMap(); + return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(FCreateStrategy, uint32, Rows, uint32, Cols, float, WorldWidth, float, WorldHeight, uint32, LocalWorkerId); +DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(FCreateStrategy, uint32, Rows, uint32, Cols, float, WorldWidth, float, WorldHeight, uint32, + LocalWorkerId); bool FCreateStrategy::Update() { CreateStrategy(Rows, Cols, WorldWidth, WorldHeight, LocalWorkerId); @@ -115,7 +117,8 @@ bool FWaitForActor::Update() return (IsValid(TestActor) && TestActor->IsActorInitialized() && TestActor->HasActorBegunPlay()); } -DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCheckShouldRelinquishAuthority, FAutomationTestBase*, Test, FName, Handle, bool, bExpected); +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCheckShouldRelinquishAuthority, FAutomationTestBase*, Test, FName, Handle, bool, + bExpected); bool FCheckShouldRelinquishAuthority::Update() { bool bActual = !Strat->ShouldHaveAuthority(*TestActors[Handle]); @@ -125,12 +128,14 @@ bool FCheckShouldRelinquishAuthority::Update() return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCheckWhoShouldHaveAuthority, FAutomationTestBase*, Test, FName, Handle, uint32, ExpectedVirtualWorker); +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCheckWhoShouldHaveAuthority, FAutomationTestBase*, Test, FName, Handle, uint32, + ExpectedVirtualWorker); bool FCheckWhoShouldHaveAuthority::Update() { uint32 Actual = Strat->WhoShouldHaveAuthority(*TestActors[Handle]); - Test->TestEqual(FString::Printf(TEXT("Who Should Have Authority. Actual: %d, Expected: %d"), Actual, ExpectedVirtualWorker), Actual, ExpectedVirtualWorker); + Test->TestEqual(FString::Printf(TEXT("Who Should Have Authority. Actual: %d, Expected: %d"), Actual, ExpectedVirtualWorker), Actual, + ExpectedVirtualWorker); return true; } @@ -144,8 +149,8 @@ bool FCheckVirtualWorkersDiffer::Update() const uint32 WorkerId = Strat->WhoShouldHaveAuthority(*TestActors[Handles[i]]); if (WorkerIdToHandle.Contains(WorkerId)) { - Test->AddError(FString::Printf(TEXT("%s and %s both belong to virtual worker %d"), - *WorkerIdToHandle[WorkerId].ToString(), *Handles[i].ToString(), WorkerId)); + Test->AddError(FString::Printf(TEXT("%s and %s both belong to virtual worker %d"), *WorkerIdToHandle[WorkerId].ToString(), + *Handles[i].ToString(), WorkerId)); } else { @@ -164,14 +169,20 @@ bool FCheckVirtualWorkersMatch::Update() for (int i = 1; i < Handles.Num(); i++) { uint32 NextVirtualWorkerId = Strat->WhoShouldHaveAuthority(*TestActors[Handles[i]]); - Test->TestEqual(FString::Printf(TEXT("Should Have Authority %s(%d) and %s(%d)"), - *Handles[0].ToString(), VirtualWorkerId, *Handles[i].ToString(), NextVirtualWorkerId), - VirtualWorkerId, NextVirtualWorkerId); + Test->TestEqual(FString::Printf(TEXT("Should Have Authority %s(%d) and %s(%d)"), *Handles[0].ToString(), VirtualWorkerId, + *Handles[i].ToString(), NextVirtualWorkerId), + VirtualWorkerId, NextVirtualWorkerId); } return true; } +void TearDown(FAutomationTestBase* Test) +{ + ADD_LATENT_AUTOMATION_COMMAND(FCleanup()); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForDeployment(Test, EDeploymentState::IsNotRunning)); +} + GRIDBASEDLBSTRATEGY_TEST(GIVEN_2_rows_3_cols_WHEN_get_minimum_required_workers_is_called_THEN_it_returns_6) { CreateStrategy(2, 3, 10000.f, 10000.f, 1); @@ -206,13 +217,14 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_four_cells_WHEN_get_worker_interest_for_virtual_w Strat->SetVirtualWorkerIds(1, Strat->GetMinimumRequiredWorkers()); Strat->SetLocalVirtualWorkerId(4); - SpatialGDK::QueryConstraint StratConstraint = Strat->GetWorkerInterestQueryConstraint(); + SpatialGDK::QueryConstraint StratConstraint = Strat->GetWorkerInterestQueryConstraint(4); 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. + // 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); @@ -242,43 +254,44 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_four_cells_WHEN_get_worker_entity_position_for_vi GRIDBASEDLBSTRATEGY_TEST(GIVEN_one_cell_WHEN_requires_handover_data_called_THEN_returns_false) { CreateStrategy(1, 1, 10000.f, 10000.f, 1); - TestFalse("Strategy doesn't require handover data",Strat->RequiresHandoverData()); + TestFalse("Strategy doesn't require handover data", Strat->RequiresHandoverData()); return true; } GRIDBASEDLBSTRATEGY_TEST(GIVEN_more_than_one_row_WHEN_requires_handover_data_called_THEN_returns_true) { CreateStrategy(2, 1, 10000.f, 10000.f, 1); - TestTrue("Strategy doesn't require handover data",Strat->RequiresHandoverData()); + TestTrue("Strategy doesn't require handover data", Strat->RequiresHandoverData()); return true; } GRIDBASEDLBSTRATEGY_TEST(GIVEN_more_than_one_column_WHEN_requires_handover_data_called_THEN_returns_true) { CreateStrategy(1, 2, 10000.f, 10000.f, 1); - TestTrue("Strategy doesn't require handover data",Strat->RequiresHandoverData()); + TestTrue("Strategy doesn't require handover data", Strat->RequiresHandoverData()); return true; } -} // anonymous namespace +} // anonymous namespace GRIDBASEDLBSTRATEGY_TEST(GIVEN_a_single_cell_and_valid_local_id_WHEN_should_relinquish_called_THEN_returns_false) { - AutomationOpenMap("/Engine/Maps/Entry"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(1, 1, 10000.f, 10000.f, 1)); ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld()); ADD_LATENT_AUTOMATION_COMMAND(FSpawnActorAtLocation("Actor", FVector::ZeroVector)); ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor("Actor")); ADD_LATENT_AUTOMATION_COMMAND(FCheckShouldRelinquishAuthority(this, "Actor", false)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup()); + + TearDown(this); return true; } GRIDBASEDLBSTRATEGY_TEST(GIVEN_four_cells_WHEN_actors_in_each_cell_THEN_should_return_different_virtual_workers) { - AutomationOpenMap("/Engine/Maps/Entry"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(2, 2, 10000.f, 10000.f, 1)); ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld()); @@ -290,15 +303,16 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_four_cells_WHEN_actors_in_each_cell_THEN_should_r ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor("Actor2")); ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor("Actor3")); ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor("Actor4")); - ADD_LATENT_AUTOMATION_COMMAND(FCheckVirtualWorkersDiffer(this, {"Actor1", "Actor2", "Actor3", "Actor4"})); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup()); + ADD_LATENT_AUTOMATION_COMMAND(FCheckVirtualWorkersDiffer(this, { "Actor1", "Actor2", "Actor3", "Actor4" })); + + TearDown(this); return true; } GRIDBASEDLBSTRATEGY_TEST(GIVEN_moving_actor_WHEN_actor_crosses_boundary_THEN_should_relinquish_authority) { - AutomationOpenMap("/Engine/Maps/Entry"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(2, 1, 10000.f, 10000.f, 1)); ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld()); @@ -309,14 +323,15 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_moving_actor_WHEN_actor_crosses_boundary_THEN_sho ADD_LATENT_AUTOMATION_COMMAND(FCheckShouldRelinquishAuthority(this, "Actor1", true)); ADD_LATENT_AUTOMATION_COMMAND(FMoveActor("Actor1", FVector(2.f, 0.f, 0.f))); ADD_LATENT_AUTOMATION_COMMAND(FCheckShouldRelinquishAuthority(this, "Actor1", true)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup()); + + TearDown(this); return true; } GRIDBASEDLBSTRATEGY_TEST(GIVEN_two_actors_WHEN_actors_are_in_same_cell_THEN_should_belong_to_same_worker_id) { - AutomationOpenMap("/Engine/Maps/Entry"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(1, 2, 10000.f, 10000.f, 1)); ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld()); @@ -325,14 +340,15 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_two_actors_WHEN_actors_are_in_same_cell_THEN_shou ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor("Actor1")); ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor("Actor2")); ADD_LATENT_AUTOMATION_COMMAND(FCheckVirtualWorkersMatch(this, { "Actor1", "Actor2" })); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup()); + + TearDown(this); return true; } GRIDBASEDLBSTRATEGY_TEST(GIVEN_two_cells_WHEN_actor_in_one_cell_THEN_strategy_relinquishes_based_on_local_id) { - AutomationOpenMap("/Engine/Maps/Entry"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld()); ADD_LATENT_AUTOMATION_COMMAND(FSpawnActorAtLocation("Actor1", FVector(0.f, -2500.f, 0.f))); @@ -341,8 +357,8 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_two_cells_WHEN_actor_in_one_cell_THEN_strategy_re ADD_LATENT_AUTOMATION_COMMAND(FCheckShouldRelinquishAuthority(this, "Actor1", false)); ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(1, 2, 10000.f, 10000.f, 2)); ADD_LATENT_AUTOMATION_COMMAND(FCheckShouldRelinquishAuthority(this, "Actor1", true)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup()); + + TearDown(this); return true; } - diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.cpp index 295edaf281..04a3c2cc83 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.cpp @@ -2,7 +2,8 @@ #include "TestGridBasedLBStrategy.h" -UGridBasedLBStrategy* UTestGridBasedLBStrategy::Create(uint32 InRows, uint32 InCols, float WorldWidth, float WorldHeight, float InterestBorder) +UGridBasedLBStrategy* UTestGridBasedLBStrategy::Create(uint32 InRows, uint32 InCols, float WorldWidth, float WorldHeight, + float InterestBorder) { UTestGridBasedLBStrategy* Strat = NewObject(); diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.h index 0b8095345c..88e86bc960 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.h +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.h @@ -15,6 +15,5 @@ class SPATIALGDKTESTS_API UTestGridBasedLBStrategy : public UGridBasedLBStrategy GENERATED_BODY() public: - static UGridBasedLBStrategy* Create(uint32 Rows, uint32 Cols, float WorldWidth, float WorldHeight, float InterestBorder = 0.0f); }; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/LayeredLBStrategy/LayeredLBStrategyTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/LayeredLBStrategy/LayeredLBStrategyTest.cpp index 85eecc7ed0..5af566846e 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/LayeredLBStrategy/LayeredLBStrategyTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/LayeredLBStrategy/LayeredLBStrategyTest.cpp @@ -4,6 +4,8 @@ #include "LoadBalancing/GridBasedLBStrategy.h" #include "LoadBalancing/LayeredLBStrategy.h" #include "LoadBalancing/SpatialMultiWorkerSettings.h" +#include "SpatialConstants.h" +#include "SpatialGDKEditorSettings.h" #include "TestLayeredLBStrategy.h" #include "Utils/LayerInfo.h" @@ -11,16 +13,15 @@ #include "GameFramework/DefaultPawn.h" #include "GameFramework/GameStateBase.h" #include "Misc/Optional.h" +#include "SpatialGDKTests/Public/GDKAutomationTestBase.h" #include "Tests/AutomationCommon.h" #include "Tests/AutomationEditorCommon.h" #include "Tests/TestDefinitions.h" -#define LAYEREDLBSTRATEGY_TEST(TestName) \ - GDK_TEST(Core, ULayeredLBStrategy, TestName) +#define LAYEREDLBSTRATEGY_TEST(TestName) GDK_AUTOMATION_TEST(Core, ULayeredLBStrategy, TestName) namespace { - struct TestData { ULayeredLBStrategy* Strat{ nullptr }; @@ -35,8 +36,7 @@ UWorld* GetAnyGameWorld() const TIndirectArray& WorldContexts = GEngine->GetWorldContexts(); for (const FWorldContext& Context : WorldContexts) { - if ((Context.WorldType == EWorldType::PIE || Context.WorldType == EWorldType::Game) - && (Context.World() != nullptr)) + if ((Context.WorldType == EWorldType::PIE || Context.WorldType == EWorldType::Game) && (Context.World() != nullptr)) { World = Context.World(); break; @@ -48,6 +48,18 @@ UWorld* GetAnyGameWorld() } // anonymous namespace +// Disable local deployments from starting and open map +void DisableLocalDeploymentsAndOpenMap() +{ + const auto Settings = GetMutableDefault(); + const auto OldBAutoStartLocalDeployment = Settings->bAutoStartLocalDeployment; + Settings->bAutoStartLocalDeployment = false; + + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); + + Settings->bAutoStartLocalDeployment = OldBAutoStartLocalDeployment; +} + DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FWaitForWorld, TSharedPtr, TestData); bool FWaitForWorld::Update() { @@ -66,7 +78,8 @@ bool FWaitForWorld::Update() return false; } -DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCreateStrategy, TSharedPtr, TestData, UAbstractSpatialMultiWorkerSettings*, MultiWorkerSettings, TOptional, NumVirtualWorkers); +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCreateStrategy, TSharedPtr, TestData, UAbstractSpatialMultiWorkerSettings*, + MultiWorkerSettings, TOptional, NumVirtualWorkers); bool FCreateStrategy::Update() { TestData->Strat = NewObject(TestData->TestWorld); @@ -90,38 +103,37 @@ bool FSetLocalVirtualWorker::Update() return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_FOUR_PARAMETER(FCheckWhoShouldHaveAuthority, TSharedPtr, TestData, FAutomationTestBase*, Test, FName, ActorName, VirtualWorkerId, Expected); +DEFINE_LATENT_AUTOMATION_COMMAND_FOUR_PARAMETER(FCheckWhoShouldHaveAuthority, TSharedPtr, TestData, FAutomationTestBase*, Test, + FName, ActorName, VirtualWorkerId, Expected); bool FCheckWhoShouldHaveAuthority::Update() { const VirtualWorkerId Actual = TestData->Strat->WhoShouldHaveAuthority(*TestData->TestActors[ActorName]); - Test->TestEqual( - FString::Printf(TEXT("Who Should Have Authority. Actual: %d, Expected: %d"), Actual, Expected), - Actual, Expected); + Test->TestEqual(FString::Printf(TEXT("Who Should Have Authority. Actual: %d, Expected: %d"), Actual, Expected), Actual, Expected); return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCheckMinimumWorkers, TSharedPtr, TestData, FAutomationTestBase*, Test, uint32, Expected); +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCheckMinimumWorkers, TSharedPtr, TestData, FAutomationTestBase*, Test, uint32, + Expected); bool FCheckMinimumWorkers::Update() { const uint32 Actual = TestData->Strat->GetMinimumRequiredWorkers(); - Test->TestEqual( - FString::Printf(TEXT("Strategy for minimum required workers. Actual: %d, Expected: %d"), Actual, Expected), - Actual, Expected); + Test->TestEqual(FString::Printf(TEXT("Strategy for minimum required workers. Actual: %d, Expected: %d"), Actual, Expected), Actual, + Expected); return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCheckStratIsReady, TSharedPtr, TestData, FAutomationTestBase*, Test, bool, Expected); +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCheckStratIsReady, TSharedPtr, TestData, FAutomationTestBase*, Test, bool, + Expected); bool FCheckStratIsReady::Update() { const UAbstractLBStrategy* Strat = TestData->Strat; - Test->TestEqual( - FString::Printf(TEXT("Strategy is ready. Actual: %d, Expected: %d"), Strat->IsReady(), Expected), - Strat->IsReady(), Expected); + Test->TestEqual(FString::Printf(TEXT("Strategy is ready. Actual: %d, Expected: %d"), Strat->IsReady(), Expected), Strat->IsReady(), + Expected); return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FSpawnLayer1PawnAtLocation, TSharedPtr, TestData, FName, Handle, - FVector, Location); +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FSpawnLayer1PawnAtLocation, TSharedPtr, TestData, FName, Handle, FVector, + Location); bool FSpawnLayer1PawnAtLocation::Update() { FActorSpawnParameters SpawnParams; @@ -134,8 +146,8 @@ bool FSpawnLayer1PawnAtLocation::Update() return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FSpawnLayer2PawnAtLocation, TSharedPtr, TestData, - FName, Handle, FVector, Location); +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FSpawnLayer2PawnAtLocation, TSharedPtr, TestData, FName, Handle, FVector, + Location); bool FSpawnLayer2PawnAtLocation::Update() { FActorSpawnParameters SpawnParams; @@ -148,7 +160,8 @@ bool FSpawnLayer2PawnAtLocation::Update() return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(FCheckActorsAuth, TSharedPtr, TestData, FAutomationTestBase*, Test, FName, FirstActorName, FName, SecondActorName, bool, ExpectEqual); +DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(FCheckActorsAuth, TSharedPtr, TestData, FAutomationTestBase*, Test, FName, + FirstActorName, FName, SecondActorName, bool, ExpectEqual); bool FCheckActorsAuth::Update() { const auto& Strat = TestData->Strat; @@ -159,11 +172,11 @@ bool FCheckActorsAuth::Update() if (ExpectEqual) { - Test->TestEqual( - FString::Printf(TEXT("Actors should have the same auth. Actor1: %d, Actor2: %d"), FirstActorAuth, SecondActorAuth), - FirstActorAuth, SecondActorAuth); + Test->TestEqual(FString::Printf(TEXT("Actors should have the same auth. Actor1: %d, Actor2: %d"), FirstActorAuth, SecondActorAuth), + FirstActorAuth, SecondActorAuth); } - else { + else + { Test->TestNotEqual( FString::Printf(TEXT("Actors should have different auth. Actor1: %d, Actor2: %d"), FirstActorAuth, SecondActorAuth), FirstActorAuth, SecondActorAuth); @@ -172,29 +185,28 @@ bool FCheckActorsAuth::Update() return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCheckRequiresHandover, TSharedPtr, TestData, FAutomationTestBase*, Test, bool, Expected); +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCheckRequiresHandover, TSharedPtr, TestData, FAutomationTestBase*, Test, bool, + Expected); bool FCheckRequiresHandover::Update() { const bool Actual = TestData->Strat->RequiresHandoverData(); - Test->TestEqual( - FString::Printf(TEXT("Strategy requires handover data. Expected: %c Actual: %c"), Expected, Actual), - Expected, Actual); + Test->TestEqual(FString::Printf(TEXT("Strategy requires handover data. Expected: %c Actual: %c"), Expected, Actual), Expected, Actual); return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCheckShouldHaveAuthMatchesWhoShouldHaveAuth, TSharedPtr, TestData, FAutomationTestBase*, Test, FName, ActorName); +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCheckShouldHaveAuthMatchesWhoShouldHaveAuth, TSharedPtr, TestData, + FAutomationTestBase*, Test, FName, ActorName); bool FCheckShouldHaveAuthMatchesWhoShouldHaveAuth::Update() { const auto Strat = TestData->Strat; const auto& TestActors = TestData->TestActors; - const bool WeShouldHaveAuthority - = Strat->WhoShouldHaveAuthority(*TestActors[ActorName]) == Strat->GetLocalVirtualWorkerId(); + const bool WeShouldHaveAuthority = Strat->WhoShouldHaveAuthority(*TestActors[ActorName]) == Strat->GetLocalVirtualWorkerId(); const bool DoWeActuallyHaveAuthority = Strat->ShouldHaveAuthority(*TestActors[ActorName]); - Test->TestEqual( - FString::Printf(TEXT("WhoShouldHaveAuthority should match ShouldHaveAuthority. Expected: %b Actual: %b"), WeShouldHaveAuthority, DoWeActuallyHaveAuthority), - WeShouldHaveAuthority, DoWeActuallyHaveAuthority); + Test->TestEqual(FString::Printf(TEXT("WhoShouldHaveAuthority should match ShouldHaveAuthority. Expected: %b Actual: %b"), + WeShouldHaveAuthority, DoWeActuallyHaveAuthority), + WeShouldHaveAuthority, DoWeActuallyHaveAuthority); return true; } @@ -206,12 +218,14 @@ bool FCleanup::Update() Pair.Value->Destroy(/*bNetForce*/ true); } + GEditor->RequestEndPlayMap(); + return true; } LAYEREDLBSTRATEGY_TEST(GIVEN_strat_is_not_ready_WHEN_local_virtual_worker_id_is_set_THEN_is_ready) { - AutomationOpenMap("/Engine/Maps/Entry"); + DisableLocalDeploymentsAndOpenMap(); TSharedPtr Data = TSharedPtr(new TestData); @@ -222,100 +236,135 @@ LAYEREDLBSTRATEGY_TEST(GIVEN_strat_is_not_ready_WHEN_local_virtual_worker_id_is_ ADD_LATENT_AUTOMATION_COMMAND(FCheckStratIsReady(Data, this, false)); ADD_LATENT_AUTOMATION_COMMAND(FSetLocalVirtualWorker(Data, 1)); ADD_LATENT_AUTOMATION_COMMAND(FCheckStratIsReady(Data, this, true)); + ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); return true; } -LAYEREDLBSTRATEGY_TEST(GIVEN_layered_strat_of_two_by_four_grid_strat_singleton_strat_and_default_strat_WHEN_get_minimum_required_workers_called_THEN_ten_returned) +LAYEREDLBSTRATEGY_TEST( + GIVEN_layered_strat_of_two_by_four_grid_strat_singleton_strat_and_default_strat_WHEN_get_minimum_required_workers_called_THEN_ten_returned) { - AutomationOpenMap("/Engine/Maps/Entry"); + DisableLocalDeploymentsAndOpenMap(); TSharedPtr Data = TSharedPtr(new TestData); USpatialMultiWorkerSettings* MultiWorkerSettings = NewObject(); - MultiWorkerSettings->WorkerLayers.Add(FLayerInfo{TEXT("LayerOne"), {}, UTwoByFourLBGridStrategy::StaticClass()}); - MultiWorkerSettings->WorkerLayers.Add(FLayerInfo{TEXT("LayerTwo"), {}, USingleWorkerStrategy::StaticClass()}); + MultiWorkerSettings->WorkerLayers.Add(FLayerInfo{ TEXT("LayerOne"), {}, UTwoByFourLBGridStrategy::StaticClass() }); + MultiWorkerSettings->WorkerLayers.Add(FLayerInfo{ TEXT("LayerTwo"), {}, USingleWorkerStrategy::StaticClass() }); ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data, MultiWorkerSettings, {})); ADD_LATENT_AUTOMATION_COMMAND(FCheckMinimumWorkers(Data, this, 10)); + ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); return true; } -LAYEREDLBSTRATEGY_TEST(Given_layered_strat_of_2_single_cell_strats_and_default_strat_WHEN_set_virtual_worker_ids_called_with_2_ids_THEN_error_is_logged) +LAYEREDLBSTRATEGY_TEST( + Given_layered_strat_of_2_single_cell_strats_and_default_strat_WHEN_set_virtual_worker_ids_called_with_2_ids_THEN_error_is_logged) { - AutomationOpenMap("/Engine/Maps/Entry"); + DisableLocalDeploymentsAndOpenMap(); TSharedPtr Data = TSharedPtr(new TestData); USpatialMultiWorkerSettings* MultiWorkerSettings = NewObject(); - MultiWorkerSettings->WorkerLayers.Add(FLayerInfo{TEXT("LayerOne"), {ALayer1Pawn::StaticClass()}, USingleWorkerStrategy::StaticClass()}); - MultiWorkerSettings->WorkerLayers.Add(FLayerInfo{TEXT("LayerTwo"), {ALayer2Pawn::StaticClass()}, USingleWorkerStrategy::StaticClass()}); + MultiWorkerSettings->WorkerLayers.Add( + FLayerInfo{ TEXT("LayerOne"), { ALayer1Pawn::StaticClass() }, USingleWorkerStrategy::StaticClass() }); + MultiWorkerSettings->WorkerLayers.Add( + FLayerInfo{ TEXT("LayerTwo"), { ALayer2Pawn::StaticClass() }, USingleWorkerStrategy::StaticClass() }); this->AddExpectedError("LayeredLBStrategy was not given enough VirtualWorkerIds to meet the demands of the layer strategies.", - EAutomationExpectedErrorFlags::MatchType::Contains, 1); + EAutomationExpectedErrorFlags::MatchType::Contains, 1); // The two single strategies plus the default strategy require 3 virtual workers, but we only explicitly provide 2. ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data, MultiWorkerSettings, 2)); + ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); return true; } -LAYEREDLBSTRATEGY_TEST(Given_layered_strat_of_2_single_cell_grid_strats_and_default_strat_WHEN_set_virtual_worker_ids_called_with_3_ids_THEN_no_error_is_logged) +LAYEREDLBSTRATEGY_TEST( + Given_layered_strat_of_2_single_cell_grid_strats_and_default_strat_WHEN_set_virtual_worker_ids_called_with_3_ids_THEN_no_error_is_logged) { - AutomationOpenMap("/Engine/Maps/Entry"); + DisableLocalDeploymentsAndOpenMap(); TSharedPtr Data = TSharedPtr(new TestData); USpatialMultiWorkerSettings* MultiWorkerSettings = NewObject(); - MultiWorkerSettings->WorkerLayers.Add(FLayerInfo{TEXT("LayerOne"), {ALayer1Pawn::StaticClass()}, USingleWorkerStrategy::StaticClass()}); - MultiWorkerSettings->WorkerLayers.Add(FLayerInfo{TEXT("LayerTwo"), {ALayer2Pawn::StaticClass()}, USingleWorkerStrategy::StaticClass()}); + MultiWorkerSettings->WorkerLayers.Add( + FLayerInfo{ TEXT("LayerOne"), { ALayer1Pawn::StaticClass() }, USingleWorkerStrategy::StaticClass() }); + MultiWorkerSettings->WorkerLayers.Add( + FLayerInfo{ TEXT("LayerTwo"), { ALayer2Pawn::StaticClass() }, USingleWorkerStrategy::StaticClass() }); // The two single strategies plus the default strategy require 3 virtual workers. ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data, MultiWorkerSettings, 3)); + ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); return true; } LAYEREDLBSTRATEGY_TEST(Given_layered_strat_of_default_strat_WHEN_requires_handover_called_THEN_returns_false) { - AutomationOpenMap("/Engine/Maps/Entry"); + DisableLocalDeploymentsAndOpenMap(); + + TSharedPtr Data = TSharedPtr(new TestData); + + USpatialMultiWorkerSettings* MultiWorkerSettings = NewObject(); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data, MultiWorkerSettings, {})); + ADD_LATENT_AUTOMATION_COMMAND(FSetLocalVirtualWorker(Data, 1)); + ADD_LATENT_AUTOMATION_COMMAND(FCheckRequiresHandover(Data, this, false)); + ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + + return true; +} + +LAYEREDLBSTRATEGY_TEST(Given_layered_strat_of_single_cell_grid_strat_and_default_strat_WHEN_requires_handover_called_THEN_returns_false) +{ + DisableLocalDeploymentsAndOpenMap(); TSharedPtr Data = TSharedPtr(new TestData); USpatialMultiWorkerSettings* MultiWorkerSettings = NewObject(); + MultiWorkerSettings->WorkerLayers.Add( + FLayerInfo{ TEXT("LayerOne"), { ADefaultPawn::StaticClass() }, USingleWorkerStrategy::StaticClass() }); ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data, MultiWorkerSettings, {})); ADD_LATENT_AUTOMATION_COMMAND(FSetLocalVirtualWorker(Data, 1)); ADD_LATENT_AUTOMATION_COMMAND(FCheckRequiresHandover(Data, this, false)); + ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); return true; } -LAYEREDLBSTRATEGY_TEST(Given_layered_strat_of_single_cell_grid_strat_and_default_strat_WHEN_requires_handover_called_THEN_returns_true) +LAYEREDLBSTRATEGY_TEST(Given_layered_strat_of_multiple_single_cell_grid_strategies_WHEN_requires_handover_called_THEN_returns_false) { - AutomationOpenMap("/Engine/Maps/Entry"); + DisableLocalDeploymentsAndOpenMap(); TSharedPtr Data = TSharedPtr(new TestData); USpatialMultiWorkerSettings* MultiWorkerSettings = NewObject(); - MultiWorkerSettings->WorkerLayers.Add(FLayerInfo{TEXT("LayerOne"), {ADefaultPawn::StaticClass()}, USingleWorkerStrategy::StaticClass()}); + MultiWorkerSettings->WorkerLayers.Add( + FLayerInfo{ TEXT("LayerOne"), { ALayer1Pawn::StaticClass() }, USingleWorkerStrategy::StaticClass() }); + MultiWorkerSettings->WorkerLayers.Add( + FLayerInfo{ TEXT("LayerTwo"), { ALayer2Pawn::StaticClass() }, USingleWorkerStrategy::StaticClass() }); ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data, MultiWorkerSettings, {})); ADD_LATENT_AUTOMATION_COMMAND(FSetLocalVirtualWorker(Data, 1)); - ADD_LATENT_AUTOMATION_COMMAND(FCheckRequiresHandover(Data, this, true)); + ADD_LATENT_AUTOMATION_COMMAND(FCheckRequiresHandover(Data, this, false)); + ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); return true; } LAYEREDLBSTRATEGY_TEST(Given_layered_strat_of_default_strat_WHEN_who_should_have_auth_called_THEN_return_1) { - AutomationOpenMap("/Engine/Maps/Entry"); + DisableLocalDeploymentsAndOpenMap(); TSharedPtr Data = TSharedPtr(new TestData); @@ -333,7 +382,7 @@ LAYEREDLBSTRATEGY_TEST(Given_layered_strat_of_default_strat_WHEN_who_should_have LAYEREDLBSTRATEGY_TEST(Given_layered_strat_WHEN_set_local_worker_called_twice_THEN_an_error_is_logged) { - AutomationOpenMap("/Engine/Maps/Entry"); + DisableLocalDeploymentsAndOpenMap(); TSharedPtr Data = TSharedPtr(new TestData); @@ -343,22 +392,25 @@ LAYEREDLBSTRATEGY_TEST(Given_layered_strat_WHEN_set_local_worker_called_twice_TH ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data, MultiWorkerSettings, {})); ADD_LATENT_AUTOMATION_COMMAND(FSetLocalVirtualWorker(Data, 1)); ADD_LATENT_AUTOMATION_COMMAND(FSetLocalVirtualWorker(Data, 2)); + ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); - this->AddExpectedError("The Local Virtual Worker Id cannot be set twice. Current value:", - EAutomationExpectedErrorFlags::MatchType::Contains, 1); + this->AddExpectedError( + "The Local Virtual Worker Id cannot be set twice. Current value:", EAutomationExpectedErrorFlags::MatchType::Contains, 1); return true; } LAYEREDLBSTRATEGY_TEST(Given_two_actors_of_same_type_at_same_position_WHEN_who_should_have_auth_called_THEN_return_same_for_both) { - AutomationOpenMap("/Engine/Maps/Entry"); + DisableLocalDeploymentsAndOpenMap(); TSharedPtr Data = TSharedPtr(new TestData); USpatialMultiWorkerSettings* MultiWorkerSettings = NewObject(); - MultiWorkerSettings->WorkerLayers.Add(FLayerInfo{TEXT("LayerOne"), {ALayer1Pawn::StaticClass()}, UTwoByFourLBGridStrategy::StaticClass()}); - MultiWorkerSettings->WorkerLayers.Add(FLayerInfo{TEXT("LayerTwo"), {ALayer2Pawn::StaticClass()}, UTwoByFourLBGridStrategy::StaticClass()}); + MultiWorkerSettings->WorkerLayers.Add( + FLayerInfo{ TEXT("LayerOne"), { ALayer1Pawn::StaticClass() }, UTwoByFourLBGridStrategy::StaticClass() }); + MultiWorkerSettings->WorkerLayers.Add( + FLayerInfo{ TEXT("LayerTwo"), { ALayer2Pawn::StaticClass() }, UTwoByFourLBGridStrategy::StaticClass() }); ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data, MultiWorkerSettings, {})); @@ -375,15 +427,18 @@ LAYEREDLBSTRATEGY_TEST(Given_two_actors_of_same_type_at_same_position_WHEN_who_s return true; } -LAYEREDLBSTRATEGY_TEST(GIVEN_two_actors_of_different_types_and_same_positions_managed_by_different_layers_WHEN_who_has_auth_called_THEN_return_different_values) +LAYEREDLBSTRATEGY_TEST( + GIVEN_two_actors_of_different_types_and_same_positions_managed_by_different_layers_WHEN_who_has_auth_called_THEN_return_different_values) { - AutomationOpenMap("/Engine/Maps/Entry"); + DisableLocalDeploymentsAndOpenMap(); TSharedPtr Data = TSharedPtr(new TestData); USpatialMultiWorkerSettings* MultiWorkerSettings = NewObject(); - MultiWorkerSettings->WorkerLayers.Add(FLayerInfo{TEXT("LayerOne"), {ALayer1Pawn::StaticClass()}, UTwoByFourLBGridStrategy::StaticClass()}); - MultiWorkerSettings->WorkerLayers.Add(FLayerInfo{TEXT("LayerTwo"), {ALayer2Pawn::StaticClass()}, UTwoByFourLBGridStrategy::StaticClass()}); + MultiWorkerSettings->WorkerLayers.Add( + FLayerInfo{ TEXT("LayerOne"), { ALayer1Pawn::StaticClass() }, UTwoByFourLBGridStrategy::StaticClass() }); + MultiWorkerSettings->WorkerLayers.Add( + FLayerInfo{ TEXT("LayerTwo"), { ALayer2Pawn::StaticClass() }, UTwoByFourLBGridStrategy::StaticClass() }); ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data, MultiWorkerSettings, {})); diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/LayeredLBStrategy/TestLayeredLBStrategy.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/LayeredLBStrategy/TestLayeredLBStrategy.h index 571731990a..5da2e3445f 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/LayeredLBStrategy/TestLayeredLBStrategy.h +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/LayeredLBStrategy/TestLayeredLBStrategy.h @@ -13,12 +13,13 @@ * This class is for testing purposes only. */ UCLASS(HideDropdown, NotBlueprintable) -class SPATIALGDKTESTS_API UTwoByFourLBGridStrategy : public UGridBasedLBStrategy +class SPATIALGDKTESTS_API UTwoByFourLBGridStrategy : public UGridBasedLBStrategy { GENERATED_BODY() public: - UTwoByFourLBGridStrategy(): Super() + UTwoByFourLBGridStrategy() + : Super() { Rows = 2; Cols = 4; @@ -34,7 +35,6 @@ class SPATIALGDKTESTS_API ALayer1Pawn : public ADefaultPawn GENERATED_BODY() }; - /** * Same as a Default pawn but for testing */ diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/OwnershipLockingPolicy/OwnershipLockingPolicyTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/OwnershipLockingPolicy/OwnershipLockingPolicyTest.cpp index 60843040a7..5ef284d66f 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/OwnershipLockingPolicy/OwnershipLockingPolicyTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/OwnershipLockingPolicy/OwnershipLockingPolicyTest.cpp @@ -9,19 +9,18 @@ #include "Containers/UnrealString.h" #include "Engine/Engine.h" #include "Engine/EngineTypes.h" -#include "GameFramework/GameStateBase.h" #include "GameFramework/DefaultPawn.h" +#include "GameFramework/GameStateBase.h" #include "Improbable/SpatialEngineDelegates.h" -#include "Tests/AutomationCommon.h" +#include "SpatialGDKTests/Public/GDKAutomationTestBase.h" #include "Templates/SharedPointer.h" +#include "Tests/AutomationCommon.h" #include "UObject/UObjectGlobals.h" -#define OWNERSHIPLOCKINGPOLICY_TEST(TestName) \ - GDK_TEST(Core, UOwnershipLockingPolicy, TestName) +#define OWNERSHIPLOCKINGPOLICY_TEST(TestName) GDK_AUTOMATION_TEST(Core, UOwnershipLockingPolicy, TestName) namespace { - using LockingTokenAndDebugString = TPair; struct TestData @@ -61,8 +60,7 @@ UWorld* GetAnyGameWorld() const TIndirectArray& WorldContexts = GEngine->GetWorldContexts(); for (const FWorldContext& Context : WorldContexts) { - if ((Context.WorldType == EWorldType::PIE || Context.WorldType == EWorldType::Game) - && (Context.World() != nullptr)) + if ((Context.WorldType == EWorldType::PIE || Context.WorldType == EWorldType::Game) && (Context.World() != nullptr)) { World = Context.World(); break; @@ -128,7 +126,8 @@ bool FDestroyActor::Update() return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FSetOwnership, TSharedPtr, Data, FName, ActorBeingOwnedHandle, FName, ActorToOwnHandle); +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FSetOwnership, TSharedPtr, Data, FName, ActorBeingOwnedHandle, FName, + ActorToOwnHandle); bool FSetOwnership::Update() { AActor* ActorBeingOwned = Data->TestActors[ActorBeingOwnedHandle]; @@ -139,7 +138,8 @@ bool FSetOwnership::Update() return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(FAcquireLock, FAutomationTestBase*, Test, TSharedPtr, Data, FName, ActorHandle, FString, DebugString, bool, bExpectedSuccess); +DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(FAcquireLock, FAutomationTestBase*, Test, TSharedPtr, Data, FName, ActorHandle, + FString, DebugString, bool, bExpectedSuccess); bool FAcquireLock::Update() { if (!bExpectedSuccess) @@ -154,16 +154,18 @@ bool FAcquireLock::Update() // If the token returned is valid, it MUST be unique. if (bAcquireLockSucceeded) { - for (const TPair>& ActorLockingTokenAndDebugStrings : Data->TestActorToLockingTokenAndDebugStrings) + for (const TPair>& ActorLockingTokenAndDebugStrings : + Data->TestActorToLockingTokenAndDebugStrings) { const TArray& LockingTokensAndDebugStrings = ActorLockingTokenAndDebugStrings.Value; - const bool TokenAlreadyExists = LockingTokensAndDebugStrings.ContainsByPredicate([Token](const LockingTokenAndDebugString& InnerData) - { - return Token == InnerData.Key; - }); + const bool TokenAlreadyExists = + LockingTokensAndDebugStrings.ContainsByPredicate([Token](const LockingTokenAndDebugString& InnerData) { + return Token == InnerData.Key; + }); if (TokenAlreadyExists) { - Test->AddError(FString::Printf(TEXT("AcquireLock returned a valid ActorLockToken that had already been assigned. Token: %d"), Token)); + Test->AddError( + FString::Printf(TEXT("AcquireLock returned a valid ActorLockToken that had already been assigned. Token: %d"), Token)); } } } @@ -177,7 +179,8 @@ bool FAcquireLock::Update() return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(FReleaseLock, FAutomationTestBase*, Test, TSharedPtr, Data, FName, ActorHandle, FString, LockDebugString, bool, bExpectedSuccess); +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]; @@ -195,8 +198,7 @@ bool FReleaseLock::Update() return true; } - const int32 TokenIndex = LockTokenAndDebugStrings->IndexOfByPredicate([this](const LockingTokenAndDebugString& InnerData) - { + const int32 TokenIndex = LockTokenAndDebugStrings->IndexOfByPredicate([this](const LockingTokenAndDebugString& InnerData) { return InnerData.Value == LockDebugString; }); Test->TestTrue("Found valid lock token", TokenIndex != INDEX_NONE); @@ -218,25 +220,30 @@ bool FReleaseLock::Update() return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FReleaseAllLocks, FAutomationTestBase*, Test, TSharedPtr, Data, int32, ExpectedFailures); +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); + 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 (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: %lld"), Token), !bExpectedSuccess && bReleaseLockSucceeded); - Test->TestFalse(FString::Printf(TEXT("Expected ReleaseAllLocks to succeed but it failed. Token: %lld"), Token), bExpectedSuccess && !bReleaseLockSucceeded); + Test->TestFalse(FString::Printf(TEXT("Expected ReleaseAllLocks to fail but it succeeded. Token: %lld"), Token), + !bExpectedSuccess && bReleaseLockSucceeded); + Test->TestFalse(FString::Printf(TEXT("Expected ReleaseAllLocks to succeed but it failed. Token: %lld"), Token), + bExpectedSuccess && !bReleaseLockSucceeded); } } @@ -246,12 +253,22 @@ bool FReleaseAllLocks::Update() return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(FAcquireLockViaDelegate, FAutomationTestBase*, Test, TSharedPtr, Data, FName, ActorHandle, FString, DelegateLockIdentifier, bool, bExpectedSuccess); +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()); + + if (!bExpectedSuccess) + { + Test->AddExpectedError( + FString::Printf(TEXT("AcquireLockFromDelegate: A lock with identifier \"%s\" already exists for actor \"%s\"."), + *DelegateLockIdentifier, *GetNameSafe(Actor)), + EAutomationExpectedErrorFlags::Contains, 1); + } + const bool bAcquireLockSucceeded = Data->AcquireLockDelegate.Execute(Actor, DelegateLockIdentifier); Test->TestFalse(TEXT("Expected AcquireLockDelegate to succeed but it failed"), bExpectedSuccess && !bAcquireLockSucceeded); @@ -260,7 +277,8 @@ bool FAcquireLockViaDelegate::Update() return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(FReleaseLockViaDelegate, FAutomationTestBase*, Test, TSharedPtr, Data, FName, ActorHandle, FString, DelegateLockIdentifier, bool, bExpectedSuccess); +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]; @@ -269,7 +287,10 @@ bool FReleaseLockViaDelegate::Update() if (!bExpectedSuccess) { - Test->AddExpectedError(TEXT("Executed ReleaseLockDelegate for unidentified delegate lock identifier."), EAutomationExpectedErrorFlags::Contains, 1); + Test->AddExpectedError( + FString::Printf(TEXT("ReleaseLockFromDelegate: Lock identifier \"%s\" has no lock associated with it for actor \"%s\"."), + *DelegateLockIdentifier, *GetNameSafe(Actor)), + EAutomationExpectedErrorFlags::Contains, 1); } const bool bReleaseLockSucceeded = Data->ReleaseLockDelegate.Execute(Actor, DelegateLockIdentifier); @@ -280,12 +301,14 @@ bool FReleaseLockViaDelegate::Update() return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_FOUR_PARAMETER(FTestIsLocked, FAutomationTestBase*, Test, TSharedPtr, Data, FName, Handle, bool, bIsLockedExpected); +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); + Test->TestEqual(FString::Printf(TEXT("%s. Is locked. Actual: %d. Expected: %d"), *Handle.ToString(), bIsLocked, bIsLockedExpected), + bIsLocked, bIsLockedExpected); return true; } @@ -297,9 +320,18 @@ bool FCleanup::Update() Pair.Value->Destroy(/*bNetForce*/ true); } Data->TestActors.Empty(); + + GEditor->RequestEndPlayMap(); + return true; } +void TearDown(FAutomationTestBase* Test, TSharedPtr Data) +{ + ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FWaitForDeployment(Test, EDeploymentState::IsNotRunning)); +} + void SpawnABCDHierarchy(FAutomationTestBase* Test, TSharedPtr Data) { // A @@ -337,11 +369,11 @@ void SpawnABCDEHierarchy(FAutomationTestBase* Test, TSharedPtr Data) ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor(Data, "E")); } -} // anonymous namespace +} // 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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -349,7 +381,7 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_an_actor_has_not_been_locked_WHEN_IsLocked_is_ 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(FCleanup(Data)); + TearDown(this, Data); return true; } @@ -358,7 +390,7 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_an_actor_has_not_been_locked_WHEN_IsLocked_is_ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_Actor_is_not_locked_WHEN_ReleaseLock_is_called_THEN_it_errors_and_returns_false) { - AutomationOpenMap("/Engine/Maps/Entry"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -367,14 +399,14 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_Actor_is_not_locked_WHEN_ReleaseLock_is_called 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); return true; } OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_WHEN_the_locked_Actor_is_not_authoritative_THEN_AcquireLock_returns_false) { - AutomationOpenMap("/Engine/Maps/Entry"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -384,14 +416,14 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_WHEN_the_locked_Actor_is ADD_LATENT_AUTOMATION_COMMAND(FSetActorRole(Data, "Actor", ROLE_SimulatedProxy)); ADD_LATENT_AUTOMATION_COMMAND(FAcquireLock(this, Data, "Actor", "First lock", false)); ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", false)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); return true; } OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_WHEN_the_locked_Actor_is_deleted_THEN_ReleaseLock_returns_false) { - AutomationOpenMap("/Engine/Maps/Entry"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -404,14 +436,14 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_WHEN_the_locked_Actor_is // 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); return true; } OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_twice_WHEN_the_locked_Actor_is_deleted_THEN_ReleaseLock_returns_false) { - AutomationOpenMap("/Engine/Maps/Entry"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -425,14 +457,14 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_twice_WHEN_the_locked_Ac // 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); return true; } OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_WHEN_IsLocked_is_called_THEN_returns_correctly_between_calls) { - AutomationOpenMap("/Engine/Maps/Entry"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -444,14 +476,14 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_WHEN_Is 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(FCleanup(Data)); + TearDown(this, Data); return true; } OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_twice_WHEN_IsLocked_is_called_THEN_returns_correctly_between_calls) { - AutomationOpenMap("/Engine/Maps/Entry"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -467,14 +499,14 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_twice_W 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); 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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -487,14 +519,14 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_WHEN_Re 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); return true; } OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_WHEN_AcquireLock_is_called_again_THEN_it_succeeds) { - AutomationOpenMap("/Engine/Maps/Entry"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -508,16 +540,17 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_WHEN_Ac 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); 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) +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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -538,14 +571,15 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_on_hier 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); 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) +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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -566,14 +600,15 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_on_hier 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); 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) +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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -594,14 +629,15 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_on_hier 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); 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) +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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -630,16 +666,17 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_are_called_on_mult 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); 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) +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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -657,14 +694,15 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_leaf_Actor_ 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); return true; } -OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_leaf_Actor_WHEN_hierarchy_path_Actor_is_destroyed_THEN_IsLocked_returns_correctly_between_calls) +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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -682,14 +720,15 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_leaf_Actor_ 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); return true; } -OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_leaf_Actor_WHEN_hierarchy_root_is_destroyed_THEN_IsLocked_returns_correctly_between_calls) +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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -707,14 +746,15 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_leaf_Actor_ 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(FCleanup(Data)); + TearDown(this, Data); return true; } -OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_path_Actor_WHEN_hierarchy_root_is_destroyed_THEN_IsLocked_returns_correctly_between_calls) +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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -732,14 +772,15 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_path_Actor_ 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(FCleanup(Data)); + TearDown(this, Data); return true; } -OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_root_Actor_WHEN_hierarchy_root_is_destroyed_THEN_IsLocked_returns_correctly_between_calls) +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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -757,14 +798,15 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_root_Actor_ 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); return true; } -OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_root_Actor_WHEN_hierarchy_path_Actor_is_destroyed_THEN_IsLocked_returns_correctly_between_calls) +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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -782,16 +824,17 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_root_Actor_ 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); 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) +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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -815,14 +858,15 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_leaf_hierarchy_Actor_ 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); 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) +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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -844,14 +888,15 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_leaf_hierarchy_Actor_ 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); 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) +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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -871,14 +916,15 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_leaf_hierarchy_Actor_ 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); 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) +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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -900,14 +946,15 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_path_Actor_ 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); return true; } -OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_path_Actor_WHEN_hierarchy_root_switches_owner_THEN_IsLocked_returns_correctly_for_all_Actors) +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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -931,14 +978,15 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_path_Actor_ 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); return true; } -OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_path_Actor_WHEN_hierarchy_leaf_switches_owner_THEN_IsLocked_returns_correctly_for_all_Actors) +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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -958,14 +1006,15 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_path_Actor_ 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); return true; } -OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_root_WHEN_hierarchy_root_switches_owner_THEN_IsLocked_returns_correctly_for_all_Actors) +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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -989,14 +1038,15 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_root_WHEN_h 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); return true; } -OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_root_WHEN_hierarchy_path_Actor_switches_owner_THEN_IsLocked_returns_correctly_for_all_Actors) +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"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -1018,16 +1068,36 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_is_called_on_hierarchy_root_WHEN_h 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); return true; } // AcquireLockDelegate and ReleaseLockDelegate +OWNERSHIPLOCKINGPOLICY_TEST( + GIVEN_AcquireLockDelegate_is_executed_WHEN_AcquireLockDelegate_is_executed_again_THEN_it_errors_and_returns_false) +{ + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); + + 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", "First lock", false)); + ADD_LATENT_AUTOMATION_COMMAND(FTestIsLocked(this, Data, "Actor", true)); + TearDown(this, Data); + + return true; +} + OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_Actor_is_not_locked_WHEN_ReleaseLock_delegate_is_executed_THEN_it_errors_and_returns_false) { - AutomationOpenMap("/Engine/Maps/Entry"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -1036,14 +1106,15 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_Actor_is_not_locked_WHEN_ReleaseLock_delegate_ 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); return true; } -OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_delegates_are_executed_WHEN_IsLocked_is_called_THEN_returns_correctly_between_calls) +OWNERSHIPLOCKINGPOLICY_TEST( + GIVEN_AcquireLock_and_ReleaseLock_delegates_are_executed_WHEN_IsLocked_is_called_THEN_returns_correctly_between_calls) { - AutomationOpenMap("/Engine/Maps/Entry"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -1055,14 +1126,15 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_delegates_are_exec 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(FCleanup(Data)); + TearDown(this, Data); return true; } -OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_delegates_are_executed_twice_WHEN_IsLocked_is_called_THEN_returns_correctly_between_calls) +OWNERSHIPLOCKINGPOLICY_TEST( + GIVEN_AcquireLock_and_ReleaseLock_delegates_are_executed_twice_WHEN_IsLocked_is_called_THEN_returns_correctly_between_calls) { - AutomationOpenMap("/Engine/Maps/Entry"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -1078,14 +1150,15 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLock_and_ReleaseLock_delegates_are_exec 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); return true; } -OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLockDelegate_and_ReleaseLockDelegate_are_executed_WHEN_ReleaseLockDelegate_is_executed_again_THEN_it_errors_and_returns_false) +OWNERSHIPLOCKINGPOLICY_TEST( + GIVEN_AcquireLockDelegate_and_ReleaseLockDelegate_are_executed_WHEN_ReleaseLockDelegate_is_executed_again_THEN_it_errors_and_returns_false) { - AutomationOpenMap("/Engine/Maps/Entry"); + AutomationOpenMap(SpatialConstants::EMPTY_TEST_MAP_PATH); TSharedPtr Data = MakeNewTestData(); @@ -1098,7 +1171,7 @@ OWNERSHIPLOCKINGPOLICY_TEST(GIVEN_AcquireLockDelegate_and_ReleaseLockDelegate_ar 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)); - ADD_LATENT_AUTOMATION_COMMAND(FCleanup(Data)); + TearDown(this, Data); return true; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Schema/UnrealObjectRef/UnrealObjectRefTests.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Schema/UnrealObjectRef/UnrealObjectRefTests.cpp index 775da63133..27dca16c92 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Schema/UnrealObjectRef/UnrealObjectRefTests.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Schema/UnrealObjectRef/UnrealObjectRefTests.cpp @@ -2,13 +2,13 @@ #include "CoreMinimal.h" -#include "Tests/TestDefinitions.h" -#include "Tests/AutomationCommon.h" #include "Schema/UnrealObjectRef.h" +#include "SpatialGDKTests/Public/GDKAutomationTestBase.h" +#include "Tests/AutomationCommon.h" +#include "Tests/TestDefinitions.h" #include "UObject/SoftObjectPtr.h" -#define UNREALOBJECTREF_TEST(TestName) \ - GDK_TEST(Core, FUnrealObjectRef, TestName) +#define UNREALOBJECTREF_TEST(TestName) GDK_AUTOMATION_TEST(Core, FUnrealObjectRef, TestName) UNREALOBJECTREF_TEST(GIVEN_a_softpointer_WHEN_making_an_object_ref_from_it_THEN_we_can_recover_it) { @@ -29,4 +29,5 @@ UNREALOBJECTREF_TEST(GIVEN_a_softpointer_WHEN_making_an_object_ref_from_it_THEN_ 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). +// 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 index 67ad4b405b..ab191b88ea 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/CheckoutRadiusConstraintUtils/NetCullDistanceInterestTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/CheckoutRadiusConstraintUtils/NetCullDistanceInterestTest.cpp @@ -2,15 +2,10 @@ #include "Tests/TestDefinitions.h" -#include "HAL/IPlatformFileProfilerWrapper.h" -#include "HAL/PlatformFilemanager.h" -#include "Misc/ScopeTryLock.h" -#include "Misc/Paths.h" - +#include "SpatialGDKTests/Public/GDKAutomationTestBase.h" #include "Utils/Interest/NetCullDistanceInterest.h" -#define CHECKOUT_RADIUS_CONSTRAINT_TEST(TestName) \ - GDK_TEST(Core, NetCullDistanceInterest, TestName) +#define CHECKOUT_RADIUS_CONSTRAINT_TEST(TestName) GDK_AUTOMATION_TEST(Core, NetCullDistanceInterest, TestName) DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKCheckoutRadiusTest, Log, All); DEFINE_LOG_CATEGORY(LogSpatialGDKCheckoutRadiusTest); @@ -18,58 +13,57 @@ 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; - } +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; +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/Misc/SpatialActivationFlagsTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/Misc/SpatialActivationFlagsTest.cpp index af4e94bb44..f588a61dbf 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/Misc/SpatialActivationFlagsTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/Misc/SpatialActivationFlagsTest.cpp @@ -2,19 +2,19 @@ #include "CoreMinimal.h" -#include "Tests/TestDefinitions.h" -#include "Tests/AutomationCommon.h" #include "Runtime/EngineSettings/Classes/GeneralProjectSettings.h" +#include "Tests/AutomationCommon.h" +#include "Tests/TestDefinitions.h" #include "Utils/GDKPropertyMacros.h" namespace { - bool bEarliestFlag; +bool bEarliestFlag; - const FString EarliestFlagReport = TEXT("Spatial activation Flag [Earliest]:"); - const FString CurrentFlagReport = TEXT("Spatial activation Flag [Current]:"); -} +const FString EarliestFlagReport = TEXT("Spatial activation Flag [Earliest]:"); +const FString CurrentFlagReport = TEXT("Spatial activation Flag [Current]:"); +} // namespace void InitializeSpatialFlagEarlyValues() { @@ -33,37 +33,36 @@ GDK_SLOW_TEST(Core, UGeneralProjectSettings, SpatialActivationReport) namespace { - struct ReportedFlags - { - bool bEarliestFlag; - bool bCurrentFlag; - }; +struct ReportedFlags +{ + bool bEarliestFlag; + bool bCurrentFlag; +}; - ReportedFlags RunSubProcessAndExtractFlags(FAutomationTestBase& Test, const FString& CommandLineArgs) - { - ReportedFlags Flags; +ReportedFlags RunSubProcessAndExtractFlags(FAutomationTestBase& Test, const FString& CommandLineArgs) +{ + ReportedFlags Flags; - int32 ReturnCode = 1; - FString StdOut; - FString StdErr; + int32 ReturnCode = 1; + FString StdOut; + FString StdErr; - FPlatformProcess::ExecProcess(TEXT("UE4Editor"), *CommandLineArgs, &ReturnCode, &StdOut, &StdErr); + FPlatformProcess::ExecProcess(TEXT("UE4Editor"), *CommandLineArgs, &ReturnCode, &StdOut, &StdErr); - Test.TestTrue("Successful run", ReturnCode == 0); + 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; - }; + 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); + ExtractFlag(EarliestFlagReport, Flags.bEarliestFlag); + ExtractFlag(CurrentFlagReport, Flags.bCurrentFlag); - return Flags; - } + return Flags; } +} // namespace struct SpatialActivationFlagTestFixture { @@ -71,7 +70,8 @@ struct SpatialActivationFlagTestFixture { ProjectPath = FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath()); CommandLineArgs = ProjectPath; - CommandLineArgs.Append(TEXT(" -ExecCmds=\"Automation RunTests SpatialGDKSlow.Core.UGeneralProjectSettings.SpatialActivationReport; Quit\"")); + CommandLineArgs.Append( + TEXT(" -ExecCmds=\"Automation RunTests SpatialGDKSlow.Core.UGeneralProjectSettings.SpatialActivationReport; Quit\"")); CommandLineArgs.Append(TEXT(" -TestExit=\"Automation Test Queue Empty\"")); CommandLineArgs.Append(TEXT(" -nopause")); CommandLineArgs.Append(TEXT(" -nosplash")); @@ -79,7 +79,8 @@ struct SpatialActivationFlagTestFixture CommandLineArgs.Append(TEXT(" -nullRHI")); CommandLineArgs.Append(TEXT(" -stdout")); - SpatialFlagProperty = GDK_CASTFIELD(UGeneralProjectSettings::StaticClass()->FindPropertyByName("bSpatialNetworking")); + SpatialFlagProperty = + GDK_CASTFIELD(UGeneralProjectSettings::StaticClass()->FindPropertyByName("bSpatialNetworking")); Test.TestNotNull("Property existence", SpatialFlagProperty); ProjectSettings = GetMutableDefault(); @@ -106,18 +107,21 @@ struct SpatialActivationFlagTestFixture private: FString ProjectPath; - GDK_PROPERTY(BoolProperty)* SpatialFlagProperty; + GDK_PROPERTY(BoolProperty) * SpatialFlagProperty; UGeneralProjectSettings* ProjectSettings; void* SpatialFlagPtr; bool bSavedFlagValue; }; -DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FRunSubProcessCommand, FAutomationTestBase*, Test, TSharedPtr, Fixture, bool, ExpectedValue); +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); })); + Fixture->CheckResult = Async(EAsyncExecution::Thread, TFunction([&] { + return RunSubProcessAndExtractFlags(*Test, Fixture->CommandLineArgs); + })); } if (!Fixture->CheckResult.IsReady()) @@ -132,7 +136,6 @@ bool FRunSubProcessCommand::Update() return true; } - GDK_SLOW_TEST(Core, UGeneralProjectSettings, SpatialActivationSetting_False) { auto TestFixture = MakeShared(*this); diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/ObjectSpy.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/ObjectSpy.h index 1dedb94b16..69834acf76 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 RPCTypeToByteArray(ERPCType Type); - ERPCType ByteArrayToRPCType(const TArray& Array); +TArray RPCTypeToByteArray(ERPCType Type); +ERPCType ByteArrayToRPCType(const TArray& Array); } // namespace SpyUtils UCLASS() diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/RPCContainerTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/RPCContainerTest.cpp index bb4f78ae3c..b1912d20b7 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/RPCContainerTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/RPCContainer/RPCContainerTest.cpp @@ -6,43 +6,43 @@ #include "ObjectSpy.h" #include "ObjectStub.h" -#include "Utils/RPCContainer.h" #include "Schema/RPCPayload.h" #include "SpatialGDKSettings.h" +#include "SpatialGDKTests/Public/GDKAutomationTestBase.h" +#include "Utils/RPCContainer.h" #include "CoreMinimal.h" -#define RPCCONTAINER_TEST(TestName) \ - GDK_TEST(Core, FRPCContainer, TestName) +#define RPCCONTAINER_TEST(TestName) GDK_AUTOMATION_TEST(Core, FRPCContainer, TestName) using namespace SpatialGDK; namespace { - ERPCType AnySchemaComponentType = ERPCType::ClientReliable; - ERPCType AnyOtherSchemaComponentType = ERPCType::ClientUnreliable; +ERPCType AnySchemaComponentType = ERPCType::ClientReliable; +ERPCType AnyOtherSchemaComponentType = ERPCType::ClientUnreliable; - FUnrealObjectRef GenerateObjectRef(UObject* TargetObject) - { - return FUnrealObjectRef{ Worker_EntityId(TargetObject), 0 }; - } +FUnrealObjectRef GenerateObjectRef(UObject* TargetObject) +{ + return FUnrealObjectRef{ Worker_EntityId(TargetObject), 0 }; +} - uint32 GeneratePayloadFunctionIndex() - { - static uint32 FreeIndex = 0; - return FreeIndex++; - } +uint32 GeneratePayloadFunctionIndex() +{ + static uint32 FreeIndex = 0; + return FreeIndex++; +} - FPendingRPCParams CreateMockParameters(UObject* TargetObject, ERPCType Type) - { - // Use PayloadData as a place to store RPC type - RPCPayload Payload(0, GeneratePayloadFunctionIndex(), SpyUtils::RPCTypeToByteArray(Type)); - int ReliableRPCIndex = 0; +FPendingRPCParams CreateMockParameters(UObject* TargetObject, ERPCType Type) +{ + // Use PayloadData as a place to store RPC type + RPCPayload Payload(0, GeneratePayloadFunctionIndex(), SpyUtils::RPCTypeToByteArray(Type)); + int ReliableRPCIndex = 0; - FUnrealObjectRef ObjectRef = GenerateObjectRef(TargetObject); + FUnrealObjectRef ObjectRef = GenerateObjectRef(TargetObject); - return FPendingRPCParams{ ObjectRef, Type, MoveTemp(Payload) }; - } + return FPendingRPCParams{ ObjectRef, Type, MoveTemp(Payload), 0 }; +} } // anonymous namespace RPCCONTAINER_TEST(GIVEN_a_container_WHEN_nothing_has_been_added_THEN_nothing_is_queued) @@ -63,11 +63,11 @@ RPCCONTAINER_TEST(GIVEN_a_container_WHEN_one_value_has_been_added_THEN_it_is_que FRPCContainer RPCs(ERPCQueueType::Send); RPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(TargetObject, &UObjectStub::ProcessRPC)); - RPCs.ProcessOrQueueRPC(Params.ObjectRef, Params.Type, MoveTemp(Params.Payload)); + RPCs.ProcessOrQueueRPC(Params.ObjectRef, Params.Type, MoveTemp(Params.Payload), 0); TestTrue("Has queued RPCs", RPCs.ObjectHasRPCsQueuedOfType(Params.ObjectRef.Entity, AnySchemaComponentType)); - return true; + return true; } RPCCONTAINER_TEST(GIVEN_a_container_WHEN_multiple_values_of_same_type_have_been_added_THEN_they_are_queued) @@ -78,12 +78,12 @@ RPCCONTAINER_TEST(GIVEN_a_container_WHEN_multiple_values_of_same_type_have_been_ FRPCContainer RPCs(ERPCQueueType::Send); RPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(TargetObject, &UObjectStub::ProcessRPC)); - RPCs.ProcessOrQueueRPC(Params1.ObjectRef, Params1.Type, MoveTemp(Params1.Payload)); - RPCs.ProcessOrQueueRPC(Params2.ObjectRef, Params2.Type, MoveTemp(Params2.Payload)); + RPCs.ProcessOrQueueRPC(Params1.ObjectRef, Params1.Type, MoveTemp(Params1.Payload), 0); + RPCs.ProcessOrQueueRPC(Params2.ObjectRef, Params2.Type, MoveTemp(Params2.Payload), 0); TestTrue("Has queued RPCs", RPCs.ObjectHasRPCsQueuedOfType(Params1.ObjectRef.Entity, AnyOtherSchemaComponentType)); - return true; + return true; } RPCCONTAINER_TEST(GIVEN_a_container_storing_one_value_WHEN_processed_once_THEN_nothing_is_queued) @@ -93,11 +93,11 @@ RPCCONTAINER_TEST(GIVEN_a_container_storing_one_value_WHEN_processed_once_THEN_n FRPCContainer RPCs(ERPCQueueType::Send); RPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(TargetObject, &UObjectDummy::ProcessRPC)); - RPCs.ProcessOrQueueRPC(Params.ObjectRef, Params.Type, MoveTemp(Params.Payload)); + RPCs.ProcessOrQueueRPC(Params.ObjectRef, Params.Type, MoveTemp(Params.Payload), 0); TestFalse("Has queued RPCs", RPCs.ObjectHasRPCsQueuedOfType(Params.ObjectRef.Entity, AnySchemaComponentType)); - return true; + return true; } RPCCONTAINER_TEST(GIVEN_a_container_storing_multiple_values_of_same_type_WHEN_processed_once_THEN_nothing_is_queued) @@ -108,12 +108,12 @@ RPCCONTAINER_TEST(GIVEN_a_container_storing_multiple_values_of_same_type_WHEN_pr FRPCContainer RPCs(ERPCQueueType::Send); RPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(TargetObject, &UObjectDummy::ProcessRPC)); - RPCs.ProcessOrQueueRPC(Params1.ObjectRef, Params1.Type, MoveTemp(Params1.Payload)); - RPCs.ProcessOrQueueRPC(Params2.ObjectRef, Params2.Type, MoveTemp(Params2.Payload)); + RPCs.ProcessOrQueueRPC(Params1.ObjectRef, Params1.Type, MoveTemp(Params1.Payload), 0); + RPCs.ProcessOrQueueRPC(Params2.ObjectRef, Params2.Type, MoveTemp(Params2.Payload), 0); TestFalse("Has queued RPCs", RPCs.ObjectHasRPCsQueuedOfType(Params1.ObjectRef.Entity, AnyOtherSchemaComponentType)); - return true; + return true; } RPCCONTAINER_TEST(GIVEN_a_container_WHEN_multiple_values_of_different_type_have_been_added_THEN_they_are_queued) @@ -126,13 +126,13 @@ RPCCONTAINER_TEST(GIVEN_a_container_WHEN_multiple_values_of_different_type_have_ FRPCContainer RPCs(ERPCQueueType::Send); RPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(TargetObject, &UObjectStub::ProcessRPC)); - RPCs.ProcessOrQueueRPC(ParamsUnreliable.ObjectRef, ParamsUnreliable.Type, MoveTemp(ParamsUnreliable.Payload)); - RPCs.ProcessOrQueueRPC(ParamsReliable.ObjectRef, ParamsReliable.Type, MoveTemp(ParamsReliable.Payload)); + RPCs.ProcessOrQueueRPC(ParamsUnreliable.ObjectRef, ParamsUnreliable.Type, MoveTemp(ParamsUnreliable.Payload), 0); + RPCs.ProcessOrQueueRPC(ParamsReliable.ObjectRef, ParamsReliable.Type, MoveTemp(ParamsReliable.Payload), 0); TestTrue("Has queued RPCs", RPCs.ObjectHasRPCsQueuedOfType(ParamsUnreliable.ObjectRef.Entity, AnyOtherSchemaComponentType)); TestTrue("Has queued RPCs", RPCs.ObjectHasRPCsQueuedOfType(ParamsReliable.ObjectRef.Entity, AnySchemaComponentType)); - return true; + return true; } RPCCONTAINER_TEST(GIVEN_a_container_storing_multiple_values_of_different_type_WHEN_processed_once_THEN_nothing_is_queued) @@ -144,13 +144,13 @@ RPCCONTAINER_TEST(GIVEN_a_container_storing_multiple_values_of_different_type_WH FRPCContainer RPCs(ERPCQueueType::Send); RPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(TargetObject, &UObjectDummy::ProcessRPC)); - RPCs.ProcessOrQueueRPC(ParamsUnreliable.ObjectRef, ParamsUnreliable.Type, MoveTemp(ParamsUnreliable.Payload)); - RPCs.ProcessOrQueueRPC(ParamsReliable.ObjectRef, ParamsReliable.Type, MoveTemp(ParamsReliable.Payload)); + RPCs.ProcessOrQueueRPC(ParamsUnreliable.ObjectRef, ParamsUnreliable.Type, MoveTemp(ParamsUnreliable.Payload), 0); + RPCs.ProcessOrQueueRPC(ParamsReliable.ObjectRef, ParamsReliable.Type, MoveTemp(ParamsReliable.Payload), 0); TestFalse("Has queued RPCs", RPCs.ObjectHasRPCsQueuedOfType(ParamsUnreliable.ObjectRef.Entity, AnyOtherSchemaComponentType)); TestFalse("Has queued RPCs", RPCs.ObjectHasRPCsQueuedOfType(ParamsReliable.ObjectRef.Entity, AnySchemaComponentType)); - return true; + return true; } RPCCONTAINER_TEST(GIVEN_a_container_storing_multiple_values_of_different_type_WHEN_processed_once_THEN_values_have_been_processed_in_order) @@ -170,8 +170,8 @@ RPCCONTAINER_TEST(GIVEN_a_container_storing_multiple_values_of_different_type_WH RPCIndices.FindOrAdd(AnyOtherSchemaComponentType).Push(ParamsUnreliable.Payload.Index); RPCIndices.FindOrAdd(AnySchemaComponentType).Push(ParamsReliable.Payload.Index); - RPCs.ProcessOrQueueRPC(ObjectRef, ParamsUnreliable.Type, MoveTemp(ParamsUnreliable.Payload)); - RPCs.ProcessOrQueueRPC(ObjectRef, ParamsReliable.Type, MoveTemp(ParamsReliable.Payload)); + RPCs.ProcessOrQueueRPC(ObjectRef, ParamsUnreliable.Type, MoveTemp(ParamsUnreliable.Payload), 0); + RPCs.ProcessOrQueueRPC(ObjectRef, ParamsReliable.Type, MoveTemp(ParamsReliable.Payload), 0); } bool bProcessedInOrder = true; @@ -194,7 +194,7 @@ RPCCONTAINER_TEST(GIVEN_a_container_storing_multiple_values_of_different_type_WH TestTrue("Queued RPCs have been processed in order", bProcessedInOrder); - return true; + return true; } RPCCONTAINER_TEST(GIVEN_a_container_with_one_value_WHEN_processing_after_RPCQueueWarningDefaultTimeout_seconds_THEN_warning_is_logged) @@ -203,7 +203,7 @@ RPCCONTAINER_TEST(GIVEN_a_container_with_one_value_WHEN_processing_after_RPCQueu FPendingRPCParams Params = CreateMockParameters(TargetObject, AnySchemaComponentType); FRPCContainer RPCs(ERPCQueueType::Send); RPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(TargetObject, &UObjectStub::ProcessRPC)); - RPCs.ProcessOrQueueRPC(Params.ObjectRef, Params.Type, MoveTemp(Params.Payload)); + RPCs.ProcessOrQueueRPC(Params.ObjectRef, Params.Type, MoveTemp(Params.Payload), 0); AddExpectedError(TEXT("Unresolved Parameters"), EAutomationExpectedErrorFlags::Contains, 1); @@ -212,6 +212,5 @@ RPCCONTAINER_TEST(GIVEN_a_container_with_one_value_WHEN_processing_after_RPCQueu FPlatformProcess::Sleep(SpatialGDKSettings->RPCQueueWarningDefaultTimeout); RPCs.ProcessRPCs(); - return true; + return true; } - diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SchemaGenObjectStub.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SchemaGenObjectStub.h index 00f0056c83..fcba63f5da 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SchemaGenObjectStub.h +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SchemaGenObjectStub.h @@ -10,7 +10,6 @@ class USchemaGenObjectStub : public UObject { GENERATED_BODY() public: - UPROPERTY(Replicated) int IntValue; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SpatialGDKEditorSchemaGeneratorTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SpatialGDKEditorSchemaGeneratorTest.cpp index 5e255c2503..f4070f6a49 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SpatialGDKEditorSchemaGeneratorTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SpatialGDKEditorSchemaGeneratorTest.cpp @@ -17,8 +17,7 @@ #define LOCTEXT_NAMESPACE "SpatialGDKEDitorSchemaGeneratorTest" -#define SCHEMA_GENERATOR_TEST(TestName) \ - GDK_TEST(SpatialGDKEditor, SchemaGenerator, TestName) +#define SCHEMA_GENERATOR_TEST(TestName) GDK_TEST(SpatialGDKEditor, SchemaGenerator, TestName) namespace { @@ -36,7 +35,9 @@ TArray LoadSchemaFileForClassToStringArray(const FString& InSchemaOutpu } TArray FileContent; - FFileHelper::LoadFileToStringArray(FileContent, *FPaths::SetExtension(FPaths::Combine(InSchemaOutputFolder, SchemaFileFolder, CurrentClass->GetName()), TEXT(".schema"))); + FFileHelper::LoadFileToStringArray( + FileContent, + *FPaths::SetExtension(FPaths::Combine(InSchemaOutputFolder, SchemaFileFolder, CurrentClass->GetName()), TEXT(".schema"))); return FileContent; } @@ -69,7 +70,12 @@ ComponentNamesAndIds ParseAvailableNamesAndIdsFromSchemaFile(const TArray LoadedSchema = LoadSchemaFileForClassToStringArray(InSchemaOutputFolder, CurrentClass); ComponentNamesAndIds ParsedNamesAndIds = ParseAvailableNamesAndIdsFromSchemaFile(LoadedSchema); @@ -127,7 +132,7 @@ bool TestEqualDatabaseEntryAndSchemaFile(const UClass* CurrentClass, const FStri } // TODO: UNR-2298 - Uncomment and fix it - //for (int i = 0; i < ParsedNamesAndIds.SubobjectNames.Num(); ++i) + // for (int i = 0; i < ParsedNamesAndIds.SubobjectNames.Num(); ++i) //{ // const auto& Predicate = [&SchemaName = ParsedNamesAndIds.SubobjectNames[i]](const FActorSpecificSubobjectSchemaData& Data) // { @@ -194,57 +199,57 @@ FString LoadSchemaFileForClass(const FString& InSchemaOutputFolder, const UClass } FString FileContent; - FFileHelper::LoadFileToString(FileContent, *FPaths::SetExtension(FPaths::Combine(InSchemaOutputFolder, SchemaFileFolder, CurrentClass->GetName()), TEXT(".schema"))); + FFileHelper::LoadFileToString( + FileContent, + *FPaths::SetExtension(FPaths::Combine(InSchemaOutputFolder, SchemaFileFolder, CurrentClass->GetName()), TEXT(".schema"))); return FileContent; } const TArray& AllTestClassesArray() { - static TArray TestClassesArray = { - USchemaGenObjectStub::StaticClass(), - USpatialTypeObjectStub::StaticClass(), - UChildOfSpatialTypeObjectStub::StaticClass(), - UNotSpatialTypeObjectStub::StaticClass(), - UChildOfNotSpatialTypeObjectStub::StaticClass(), - UNoSpatialFlagsObjectStub::StaticClass(), - UChildOfNoSpatialFlagsObjectStub::StaticClass(), - ASpatialTypeActor::StaticClass(), - ANonSpatialTypeActor::StaticClass(), - USpatialTypeActorComponent::StaticClass(), - ASpatialTypeActorWithActorComponent::StaticClass(), - ASpatialTypeActorWithMultipleActorComponents::StaticClass(), - ASpatialTypeActorWithMultipleObjectComponents::StaticClass(), - ASpatialTypeActorWithSubobject::StaticClass() - }; + static TArray TestClassesArray = { USchemaGenObjectStub::StaticClass(), + USpatialTypeObjectStub::StaticClass(), + UChildOfSpatialTypeObjectStub::StaticClass(), + UNotSpatialTypeObjectStub::StaticClass(), + UChildOfNotSpatialTypeObjectStub::StaticClass(), + UNoSpatialFlagsObjectStub::StaticClass(), + UChildOfNoSpatialFlagsObjectStub::StaticClass(), + ASpatialTypeActor::StaticClass(), + ANonSpatialTypeActor::StaticClass(), + USpatialTypeActorComponent::StaticClass(), + ASpatialTypeActorWithActorComponent::StaticClass(), + ASpatialTypeActorWithMultipleActorComponents::StaticClass(), + ASpatialTypeActorWithMultipleObjectComponents::StaticClass(), + ASpatialTypeActorWithSubobject::StaticClass() }; return TestClassesArray; }; const TSet& AllTestClassesSet() { - static TSet TestClassesSet = { - USchemaGenObjectStub::StaticClass(), - USpatialTypeObjectStub::StaticClass(), - UChildOfSpatialTypeObjectStub::StaticClass(), - UNotSpatialTypeObjectStub::StaticClass(), - UChildOfNotSpatialTypeObjectStub::StaticClass(), - UNoSpatialFlagsObjectStub::StaticClass(), - UChildOfNoSpatialFlagsObjectStub::StaticClass(), - ASpatialTypeActor::StaticClass(), - ANonSpatialTypeActor::StaticClass(), - USpatialTypeActorComponent::StaticClass(), - ASpatialTypeActorWithActorComponent::StaticClass(), - ASpatialTypeActorWithMultipleActorComponents::StaticClass(), - ASpatialTypeActorWithMultipleObjectComponents::StaticClass(), - ASpatialTypeActorWithSubobject::StaticClass() - }; + static TSet TestClassesSet = { USchemaGenObjectStub::StaticClass(), + USpatialTypeObjectStub::StaticClass(), + UChildOfSpatialTypeObjectStub::StaticClass(), + UNotSpatialTypeObjectStub::StaticClass(), + UChildOfNotSpatialTypeObjectStub::StaticClass(), + UNoSpatialFlagsObjectStub::StaticClass(), + UChildOfNoSpatialFlagsObjectStub::StaticClass(), + ASpatialTypeActor::StaticClass(), + ANonSpatialTypeActor::StaticClass(), + USpatialTypeActorComponent::StaticClass(), + ASpatialTypeActorWithActorComponent::StaticClass(), + ASpatialTypeActorWithMultipleActorComponents::StaticClass(), + ASpatialTypeActorWithMultipleObjectComponents::StaticClass(), + ASpatialTypeActorWithSubobject::StaticClass() }; return TestClassesSet; }; #if ENGINE_MINOR_VERSION < 25 -FString ExpectedContentsDirectory = TEXT("SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema"); +FString ExpectedContentsDirectory = + TEXT("SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema"); #else -FString ExpectedContentsDirectory = TEXT("SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425"); +FString ExpectedContentsDirectory = + TEXT("SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_425"); #endif TMap ExpectedContentsFilenames = { { "SpatialTypeActor", "SpatialTypeActor.schema" }, @@ -262,7 +267,8 @@ class SchemaValidator public: bool ValidateGeneratedSchemaAgainstExpectedSchema(const FString& GeneratedSchemaContent, const FString& ExpectedSchemaFilename) { - FString ExpectedContentFullPath = FPaths::Combine(FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(ExpectedContentsDirectory), ExpectedSchemaFilename); + FString ExpectedContentFullPath = + FPaths::Combine(FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(ExpectedContentsDirectory), ExpectedSchemaFilename); FString ExpectedContent; FFileHelper::LoadFileToString(ExpectedContent, *ExpectedContentFullPath); @@ -283,10 +289,7 @@ class SchemaValidator } private: - int GetNextFreeId() - { - return FreeId++; - } + int GetNextFreeId() { return FreeId++; } int FreeId = 10000; @@ -316,7 +319,6 @@ class SchemaTestFixture } private: - void DeleteTestFolders() { IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); @@ -344,14 +346,8 @@ class SchemaTestFixture class SchemaRPCEndpointTestFixture : public SchemaTestFixture { public: - SchemaRPCEndpointTestFixture() - { - SetMaxRPCRingBufferSize(); - } - ~SchemaRPCEndpointTestFixture() - { - ResetMaxRPCRingBufferSize(); - } + SchemaRPCEndpointTestFixture() { SetMaxRPCRingBufferSize(); } + ~SchemaRPCEndpointTestFixture() { ResetMaxRPCRingBufferSize(); } private: void SetMaxRPCRingBufferSize() @@ -481,11 +477,7 @@ SCHEMA_GENERATOR_TEST(GIVEN_multiple_classes_WHEN_generated_schema_for_these_cla SchemaTestFixture Fixture; // GIVEN - TSet Classes = - { - USpatialTypeObjectStub::StaticClass(), - ASpatialTypeActor::StaticClass() - }; + TSet Classes = { USpatialTypeObjectStub::StaticClass(), ASpatialTypeActor::StaticClass() }; // WHEN SpatialGDKEditor::Schema::SpatialGDKGenerateSchemaForClasses(Classes, SchemaOutputFolder); @@ -531,19 +523,13 @@ SCHEMA_GENERATOR_TEST(GIVEN_multiple_Actor_classes_WHEN_generated_schema_for_the // GIVEN SchemaValidator Validator; - TSet Classes = - { - ASpatialTypeActor::StaticClass(), - ANonSpatialTypeActor::StaticClass() - }; + TSet Classes = { ASpatialTypeActor::StaticClass(), ANonSpatialTypeActor::StaticClass() }; // Classes need to be sorted to have proper ids - Classes.Sort([](const UClass& A, const UClass& B) - { + Classes.Sort([](const UClass& A, const UClass& B) { return A.GetPathName() < B.GetPathName(); }); - // WHEN SpatialGDKEditor::Schema::SpatialGDKGenerateSchemaForClasses(Classes, SchemaOutputFolder); @@ -573,7 +559,6 @@ SCHEMA_GENERATOR_TEST(GIVEN_an_Actor_component_class_WHEN_generated_schema_for_t UClass* CurrentClass = USpatialTypeActorComponent::StaticClass(); TSet Classes = { CurrentClass }; - // WHEN SpatialGDKEditor::Schema::SpatialGDKGenerateSchemaForClasses(Classes, SchemaOutputFolder); @@ -584,7 +569,8 @@ SCHEMA_GENERATOR_TEST(GIVEN_an_Actor_component_class_WHEN_generated_schema_for_t return true; } -SCHEMA_GENERATOR_TEST(GIVEN_an_Actor_class_with_an_actor_component_WHEN_generated_schema_for_this_class_THEN_a_file_with_expected_schema_exists) +SCHEMA_GENERATOR_TEST( + GIVEN_an_Actor_class_with_an_actor_component_WHEN_generated_schema_for_this_class_THEN_a_file_with_expected_schema_exists) { SchemaTestFixture Fixture; @@ -603,7 +589,8 @@ SCHEMA_GENERATOR_TEST(GIVEN_an_Actor_class_with_an_actor_component_WHEN_generate return true; } -SCHEMA_GENERATOR_TEST(GIVEN_an_Actor_class_with_multiple_actor_components_WHEN_generated_schema_for_this_class_THEN_files_with_expected_schema_exist) +SCHEMA_GENERATOR_TEST( + GIVEN_an_Actor_class_with_multiple_actor_components_WHEN_generated_schema_for_this_class_THEN_files_with_expected_schema_exist) { SchemaTestFixture Fixture; @@ -622,7 +609,8 @@ SCHEMA_GENERATOR_TEST(GIVEN_an_Actor_class_with_multiple_actor_components_WHEN_g return true; } -SCHEMA_GENERATOR_TEST(GIVEN_an_Actor_class_with_multiple_object_components_WHEN_generated_schema_for_this_class_THEN_files_with_expected_schema_exist) +SCHEMA_GENERATOR_TEST( + GIVEN_an_Actor_class_with_multiple_object_components_WHEN_generated_schema_for_this_class_THEN_files_with_expected_schema_exist) { SchemaTestFixture Fixture; @@ -646,11 +634,7 @@ SCHEMA_GENERATOR_TEST(GIVEN_multiple_schema_files_exist_WHEN_refresh_generated_f SchemaTestFixture Fixture; // GIVEN - TSet Classes = - { - USpatialTypeObjectStub::StaticClass(), - ASpatialTypeActor::StaticClass() - }; + TSet Classes = { USpatialTypeObjectStub::StaticClass(), ASpatialTypeActor::StaticClass() }; SpatialGDKEditor::Schema::SpatialGDKGenerateSchemaForClasses(Classes, SchemaOutputFolder); @@ -685,20 +669,18 @@ SCHEMA_GENERATOR_TEST(GIVEN_multiple_classes_with_schema_generated_WHEN_schema_d SchemaTestFixture Fixture; // GIVEN - TSet Classes = - { - USpatialTypeObjectStub::StaticClass(), - ASpatialTypeActor::StaticClass() - }; + TSet Classes = { USpatialTypeObjectStub::StaticClass(), ASpatialTypeActor::StaticClass() }; SpatialGDKEditor::Schema::SpatialGDKGenerateSchemaForClasses(Classes, SchemaOutputFolder); // WHEN - SpatialGDKEditor::Schema::SaveSchemaDatabase(DatabaseOutputFile); + USchemaDatabase* SchemaDatabase = SpatialGDKEditor::Schema::InitialiseSchemaDatabase(DatabaseOutputFile); + SpatialGDKEditor::Schema::SaveSchemaDatabase(SchemaDatabase); // THEN const FString SchemaDatabasePackagePath = FPaths::Combine(FPaths::ProjectContentDir(), SchemaDatabaseFileName); - const FString ExpectedSchemaDatabaseFileName = FPaths::SetExtension(SchemaDatabasePackagePath, FPackageName::GetAssetPackageExtension()); + const FString ExpectedSchemaDatabaseFileName = + FPaths::SetExtension(SchemaDatabasePackagePath, FPackageName::GetAssetPackageExtension()); IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); TestTrue("Generated schema database exists", PlatformFile.FileExists(*ExpectedSchemaDatabaseFileName)); @@ -716,12 +698,13 @@ SCHEMA_GENERATOR_TEST(GIVEN_a_class_with_schema_generated_WHEN_schema_database_s SpatialGDKEditor::Schema::SpatialGDKGenerateSchemaForClasses(Classes, SchemaOutputFolder); // WHEN - SpatialGDKEditor::Schema::SaveSchemaDatabase(DatabaseOutputFile); + USchemaDatabase* SchemaDatabase = SpatialGDKEditor::Schema::InitialiseSchemaDatabase(DatabaseOutputFile); + SpatialGDKEditor::Schema::SaveSchemaDatabase(SchemaDatabase); // THEN bool bDatabaseMatchesExpected = true; FSoftObjectPath SchemaDatabasePath = FSoftObjectPath(FPaths::SetExtension(DatabaseOutputFile, TEXT(".SchemaDatabase"))); - USchemaDatabase* SchemaDatabase = Cast(SchemaDatabasePath.TryLoad()); + USchemaDatabase* LoadedSchemaDatabase = Cast(SchemaDatabasePath.TryLoad()); if (SchemaDatabase == nullptr) { bDatabaseMatchesExpected = false; @@ -749,12 +732,13 @@ SCHEMA_GENERATOR_TEST(GIVEN_multiple_classes_with_schema_generated_WHEN_schema_d SpatialGDKEditor::Schema::SpatialGDKGenerateSchemaForClasses(Classes, SchemaOutputFolder); // WHEN - SpatialGDKEditor::Schema::SaveSchemaDatabase(DatabaseOutputFile); + USchemaDatabase* SchemaDatabase = SpatialGDKEditor::Schema::InitialiseSchemaDatabase(DatabaseOutputFile); + SpatialGDKEditor::Schema::SaveSchemaDatabase(SchemaDatabase); // THEN bool bDatabaseMatchesExpected = true; FSoftObjectPath SchemaDatabasePath = FSoftObjectPath(FPaths::SetExtension(DatabaseOutputFile, TEXT(".SchemaDatabase"))); - USchemaDatabase* SchemaDatabase = Cast(SchemaDatabasePath.TryLoad()); + USchemaDatabase* LoadedSchemaDatabase = Cast(SchemaDatabasePath.TryLoad()); if (SchemaDatabase == nullptr) { bDatabaseMatchesExpected = false; @@ -785,15 +769,17 @@ SCHEMA_GENERATOR_TEST(GIVEN_schema_database_exists_WHEN_schema_database_deleted_ TSet Classes = { CurrentClass }; SpatialGDKEditor::Schema::SpatialGDKGenerateSchemaForClasses(Classes, SchemaOutputFolder); - SpatialGDKEditor::Schema::SaveSchemaDatabase(DatabaseOutputFile); + USchemaDatabase* SchemaDatabase = SpatialGDKEditor::Schema::InitialiseSchemaDatabase(DatabaseOutputFile); + SpatialGDKEditor::Schema::SaveSchemaDatabase(SchemaDatabase); IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); const FString SchemaDatabasePackagePath = FPaths::Combine(FPaths::ProjectContentDir(), SchemaDatabaseFileName); - const FString ExpectedSchemaDatabaseFileName = FPaths::SetExtension(SchemaDatabasePackagePath, FPackageName::GetAssetPackageExtension()); + const FString ExpectedSchemaDatabaseFileName = + FPaths::SetExtension(SchemaDatabasePackagePath, FPackageName::GetAssetPackageExtension()); bool bFileCreated = PlatformFile.FileExists(*ExpectedSchemaDatabaseFileName); // WHEN - SpatialGDKEditor::Schema::DeleteSchemaDatabase(SchemaDatabaseFileName ); + SpatialGDKEditor::Schema::DeleteSchemaDatabase(SchemaDatabaseFileName); // THEN bool bResult = bFileCreated && !PlatformFile.FileExists(*ExpectedSchemaDatabaseFileName); @@ -811,7 +797,8 @@ SCHEMA_GENERATOR_TEST(GIVEN_schema_database_exists_WHEN_tried_to_load_THEN_loade TSet Classes = { CurrentClass }; SpatialGDKEditor::Schema::SpatialGDKGenerateSchemaForClasses(Classes, SchemaOutputFolder); - SpatialGDKEditor::Schema::SaveSchemaDatabase(DatabaseOutputFile); + USchemaDatabase* SchemaDatabase = SpatialGDKEditor::Schema::InitialiseSchemaDatabase(DatabaseOutputFile); + SpatialGDKEditor::Schema::SaveSchemaDatabase(SchemaDatabase); // WHEN bool bSuccess = SpatialGDKEditor::Schema::LoadGeneratorStateFromSchemaDatabase(SchemaDatabaseFileName); @@ -827,11 +814,12 @@ SCHEMA_GENERATOR_TEST(GIVEN_schema_database_does_not_exist_WHEN_tried_to_load_TH SchemaTestFixture Fixture; // GIVEN - SpatialGDKEditor::Schema::DeleteSchemaDatabase(SchemaDatabaseFileName ); + SpatialGDKEditor::Schema::DeleteSchemaDatabase(SchemaDatabaseFileName); // WHEN const FString SchemaDatabasePackagePath = FPaths::Combine(FPaths::ProjectContentDir(), SchemaDatabaseFileName); - const FString ExpectedSchemaDatabaseFileName = FPaths::SetExtension(SchemaDatabasePackagePath, FPackageName::GetAssetPackageExtension()); + const FString ExpectedSchemaDatabaseFileName = + FPaths::SetExtension(SchemaDatabasePackagePath, FPackageName::GetAssetPackageExtension()); bool bSuccess = SpatialGDKEditor::Schema::LoadGeneratorStateFromSchemaDatabase(SchemaDatabaseFileName); // THEN @@ -846,33 +834,31 @@ SCHEMA_GENERATOR_TEST(GIVEN_source_and_destination_of_well_known_schema_files_WH // GIVEN 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", - "spawndata.schema", - "spawner.schema", - "spatial_debugging.schema", - "tombstone.schema", - "unreal_metadata.schema", - "virtual_worker_translation.schema" - }; - TArray CoreSDKFilePaths = - { - "improbable\\restricted\\system_components.schema", - "improbable\\standard_library.schema" - }; + FString CoreSDKSchemaCopyDir = + FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("/Tests/build/dependencies/schema/standard_library")); + TArray GDKSchemaFilePaths = { "authority_intent.schema", + "core_types.schema", + "debug_component.schema", + "debug_metrics.schema", + "global_state_manager.schema", + "heartbeat.schema", + "known_entity_auth_component_set.schema", + "migration_diagnostic.schema", + "net_owning_client_worker.schema", + "not_streamed.schema", + "partition_shadow.schema", + "query_tags.schema", + "relevant.schema", + "rpc_components.schema", + "rpc_payload.schema", + "server_worker.schema", + "spatial_debugging.schema", + "spawndata.schema", + "spawner.schema", + "tombstone.schema", + "unreal_metadata.schema", + "virtual_worker_translation.schema" }; + TArray CoreSDKFilePaths = { "improbable\\restricted\\system_components.schema", "improbable\\standard_library.schema" }; // WHEN SpatialGDKEditor::Schema::CopyWellKnownSchemaFiles(GDKSchemaCopyDir, CoreSDKSchemaCopyDir); @@ -928,18 +914,15 @@ SCHEMA_GENERATOR_TEST(GIVEN_multiple_classes_WHEN_getting_all_supported_classes_ TSet FilteredClasses = SpatialGDKEditor::Schema::GetAllSupportedClasses(Classes); // THEN - TSet ExpectedClasses = - { - USpatialTypeObjectStub::StaticClass(), - UChildOfSpatialTypeObjectStub::StaticClass(), - ASpatialTypeActor::StaticClass(), - ANonSpatialTypeActor::StaticClass(), - USpatialTypeActorComponent::StaticClass(), - ASpatialTypeActorWithActorComponent::StaticClass(), - ASpatialTypeActorWithMultipleActorComponents::StaticClass(), - ASpatialTypeActorWithMultipleObjectComponents::StaticClass(), - ASpatialTypeActorWithSubobject::StaticClass() - }; + TSet ExpectedClasses = { USpatialTypeObjectStub::StaticClass(), + UChildOfSpatialTypeObjectStub::StaticClass(), + ASpatialTypeActor::StaticClass(), + ANonSpatialTypeActor::StaticClass(), + USpatialTypeActorComponent::StaticClass(), + ASpatialTypeActorWithActorComponent::StaticClass(), + ASpatialTypeActorWithMultipleActorComponents::StaticClass(), + ASpatialTypeActorWithMultipleObjectComponents::StaticClass(), + ASpatialTypeActorWithSubobject::StaticClass() }; bool bClassesFilteredCorrectly = true; if (FilteredClasses.Num() == ExpectedClasses.Num()) @@ -963,7 +946,8 @@ SCHEMA_GENERATOR_TEST(GIVEN_multiple_classes_WHEN_getting_all_supported_classes_ return true; } -SCHEMA_GENERATOR_TEST(GIVEN_3_level_names_WHEN_generating_schema_for_sublevels_THEN_generated_schema_contains_3_components_with_unique_names) +SCHEMA_GENERATOR_TEST( + GIVEN_3_level_names_WHEN_generating_schema_for_sublevels_THEN_generated_schema_contains_3_components_with_unique_names) { SchemaTestFixture Fixture; @@ -1009,7 +993,8 @@ SCHEMA_GENERATOR_TEST(GIVEN_no_schema_exists_WHEN_generating_schema_for_rpc_endp FString FileContent; FFileHelper::LoadFileToString(FileContent, *FPaths::Combine(SchemaOutputFolder, ExpectedRPCEndpointsSchemaFilename)); - TestTrue("Generated RPC endpoints schema matches the expected schema", Validator.ValidateGeneratedSchemaAgainstExpectedSchema(FileContent, 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 24c9adc615..ceaeb0cec2 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerTest.cpp @@ -6,8 +6,7 @@ #include "CoreMinimal.h" -#define LOCALDEPLOYMENT_TEST(TestName) \ - GDK_TEST(Services, LocalDeployment, TestName) +#define LOCALDEPLOYMENT_TEST(TestName) GDK_TEST(Services, LocalDeployment, TestName) LOCALDEPLOYMENT_TEST(GIVEN_no_deployment_running_WHEN_deployment_started_THEN_deployment_running) { @@ -25,7 +24,7 @@ LOCALDEPLOYMENT_TEST(GIVEN_no_deployment_running_WHEN_deployment_started_THEN_de // Cleanup ADD_LATENT_AUTOMATION_COMMAND(FStopDeployment()); ADD_LATENT_AUTOMATION_COMMAND(FWaitForDeployment(this, EDeploymentState::IsNotRunning)); - return true; + return true; } LOCALDEPLOYMENT_TEST(GIVEN_deployment_running_WHEN_deployment_stopped_THEN_deployment_not_running) @@ -42,5 +41,5 @@ LOCALDEPLOYMENT_TEST(GIVEN_deployment_running_WHEN_deployment_stopped_THEN_deplo // THEN ADD_LATENT_AUTOMATION_COMMAND(FCheckDeploymentState(this, EDeploymentState::IsNotRunning)); - return true; + return true; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.cpp index 12246ef460..f7c4a76b4e 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.cpp @@ -10,60 +10,63 @@ #include "CoreMinimal.h" +namespace SpatialGDK +{ +FLocalDeploymentManager* GetLocalDeploymentManager() +{ + FSpatialGDKServicesModule& GDKServices = FModuleManager::GetModuleChecked("SpatialGDKServices"); + FLocalDeploymentManager* LocalDeploymentManager = GDKServices.GetLocalDeploymentManager(); + return LocalDeploymentManager; +} +} // namespace SpatialGDK + namespace { - // TODO: UNR-1969 - Prepare LocalDeployment in CI pipeline - const double MAX_WAIT_TIME_FOR_LOCAL_DEPLOYMENT_OPERATION = 30.0; +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")); +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); - bool GenerateWorkerAssemblies() - { - FString BuildConfigArgs = TEXT("worker build build-config"); - FString WorkerBuildConfigResult; - int32 ExitCode; - FSpatialGDKServicesModule::ExecuteAndReadOutput(SpatialGDKServicesConstants::SpatialExe, BuildConfigArgs, SpatialGDKServicesConstants::SpatialOSDirectory, WorkerBuildConfigResult, ExitCode); + return ExitCode == SpatialGDKServicesConstants::ExitCodeSuccess; +} - const int32 ExitCodeSuccess = 0; - return (ExitCode == ExitCodeSuccess); - } +bool GenerateWorkerJson() +{ + const FString WorkerJsonDir = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("workers/unreal")); - bool GenerateWorkerJson() + FString Filename = FString(TEXT("spatialos.")) + *AutomationWorkerType.ToString() + FString(TEXT(".worker.json")); + FString JsonPath = FPaths::Combine(WorkerJsonDir, Filename); + if (!FPaths::FileExists(JsonPath)) { - 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 bRedeployRequired = false; + return GenerateDefaultWorkerJson(JsonPath, bRedeployRequired); } + + return true; } +} // namespace bool FStartDeployment::Update() { if (const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault()) { - FLocalDeploymentManager* LocalDeploymentManager = GetLocalDeploymentManager(); - const FString LaunchConfig = FPaths::Combine(FPaths::ConvertRelativePathToFull(FPaths::ProjectIntermediateDir()), AutomationLaunchConfig); + FLocalDeploymentManager* LocalDeploymentManager = SpatialGDK::GetLocalDeploymentManager(); + const FString LaunchConfig = + FPaths::Combine(FPaths::ConvertRelativePathToFull(FPaths::ProjectIntermediateDir()), AutomationLaunchConfig); const FString LaunchFlags = SpatialGDKSettings->GetSpatialOSCommandLineLaunchFlags(); - const FString SnapshotName = SpatialGDKSettings->GetSpatialOSSnapshotToLoad(); + const FString SnapshotName = SpatialGDKSettings->GetSpatialOSSnapshotToLoadPath(); const FString RuntimeVersion = SpatialGDKSettings->GetSelectedRuntimeVariantVersion().GetVersionForLocal(); - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [LocalDeploymentManager, LaunchConfig, LaunchFlags, SnapshotName, RuntimeVersion] - { + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [LocalDeploymentManager, LaunchConfig, LaunchFlags, SnapshotName, + RuntimeVersion] { if (!GenerateWorkerJson()) { return; @@ -76,9 +79,11 @@ bool FStartDeployment::Update() FSpatialLaunchConfigDescription LaunchConfigDescription; - FWorkerTypeLaunchSection Conf; + FWorkerTypeLaunchSection AutomationWorkerConfig; + AutomationWorkerConfig.WorkerTypeName = TEXT("AutomationWorker"); + LaunchConfigDescription.AdditionalWorkerConfigs.Add(AutomationWorkerConfig); - if (!GenerateLaunchConfig(LaunchConfig, &LaunchConfigDescription, Conf)) + if (!GenerateLaunchConfig(LaunchConfig, &LaunchConfigDescription, /*bGenerateCloudConfig*/ false)) { return; } @@ -97,7 +102,13 @@ bool FStartDeployment::Update() bool FStopDeployment::Update() { - FLocalDeploymentManager* LocalDeploymentManager = GetLocalDeploymentManager(); + FLocalDeploymentManager* LocalDeploymentManager = SpatialGDK::GetLocalDeploymentManager(); + + if (LocalDeploymentManager->IsDeploymentStarting()) + { + // Wait for deployment to finish starting before stopping it + return false; + } if (!LocalDeploymentManager->IsLocalDeploymentRunning() && !LocalDeploymentManager->IsDeploymentStopping()) { @@ -106,8 +117,7 @@ bool FStopDeployment::Update() if (!LocalDeploymentManager->IsDeploymentStopping()) { - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [LocalDeploymentManager] - { + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [LocalDeploymentManager] { LocalDeploymentManager->TryStopLocalDeployment(); }); } @@ -117,7 +127,13 @@ bool FStopDeployment::Update() bool FWaitForDeployment::Update() { - FLocalDeploymentManager* const LocalDeploymentManager = GetLocalDeploymentManager(); + FLocalDeploymentManager* const LocalDeploymentManager = SpatialGDK::GetLocalDeploymentManager(); + + if (LocalDeploymentManager->IsDeploymentStarting()) + { + // Wait for deployment to finish starting before stopping it + return false; + } const double NewTime = FPlatformTime::Seconds(); @@ -126,11 +142,13 @@ bool FWaitForDeployment::Update() // 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()); + Test->TestTrue(TEXT("Deployment is running"), + LocalDeploymentManager->IsLocalDeploymentRunning() && !LocalDeploymentManager->IsDeploymentStopping()); } else { - Test->TestFalse(TEXT("Deployment is not running"), LocalDeploymentManager->IsLocalDeploymentRunning() || LocalDeploymentManager->IsDeploymentStopping()); + Test->TestFalse(TEXT("Deployment is not running"), + LocalDeploymentManager->IsLocalDeploymentRunning() || LocalDeploymentManager->IsDeploymentStopping()); } return true; } @@ -141,21 +159,24 @@ bool FWaitForDeployment::Update() } else { - return (ExpectedDeploymentState == EDeploymentState::IsRunning) ? LocalDeploymentManager->IsLocalDeploymentRunning() : !LocalDeploymentManager->IsLocalDeploymentRunning(); + return (ExpectedDeploymentState == EDeploymentState::IsRunning) ? LocalDeploymentManager->IsLocalDeploymentRunning() + : !LocalDeploymentManager->IsLocalDeploymentRunning(); } } bool FCheckDeploymentState::Update() { - FLocalDeploymentManager* LocalDeploymentManager = GetLocalDeploymentManager(); + FLocalDeploymentManager* LocalDeploymentManager = SpatialGDK::GetLocalDeploymentManager(); if (ExpectedDeploymentState == EDeploymentState::IsRunning) { - Test->TestTrue(TEXT("Deployment is running"), LocalDeploymentManager->IsLocalDeploymentRunning() && !LocalDeploymentManager->IsDeploymentStopping()); + Test->TestTrue(TEXT("Deployment is running"), + LocalDeploymentManager->IsLocalDeploymentRunning() && !LocalDeploymentManager->IsDeploymentStopping()); } else { - Test->TestFalse(TEXT("Deployment is not running"), LocalDeploymentManager->IsLocalDeploymentRunning() || LocalDeploymentManager->IsDeploymentStopping()); + 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 index 2706242aec..247288ad53 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.h +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.h @@ -2,14 +2,26 @@ #pragma once -#include "Tests/TestDefinitions.h" +#include "Misc/AutomationTest.h" #include "CoreMinimal.h" +#include "LocalDeploymentManager.h" + // TODO: UNR-1964 - Move EDeploymentState enum to LocalDeploymentManager -enum class EDeploymentState { IsRunning, IsNotRunning }; +enum class EDeploymentState +{ + IsRunning, + IsNotRunning +}; + +namespace SpatialGDK +{ +FLocalDeploymentManager* GetLocalDeploymentManager(); +} // namespace SpatialGDK 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); +DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER(FCheckDeploymentState, FAutomationTestBase*, Test, EDeploymentState, + ExpectedDeploymentState); diff --git a/SpatialGDK/SpatialGDK.uplugin b/SpatialGDK/SpatialGDK.uplugin index acb9faf4b5..e264891e24 100644 --- a/SpatialGDK/SpatialGDK.uplugin +++ b/SpatialGDK/SpatialGDK.uplugin @@ -1,7 +1,7 @@ { "FileVersion": 3, - "Version": 8, - "VersionName": "0.11.0", + "Version": 9, + "VersionName": "0.12.0", "FriendlyName": "SpatialOS GDK for Unreal", "Description": "The SpatialOS Game Development Kit (GDK) for Unreal Engine allows you to host your game and combine multiple dedicated server instances across one seamless game world whilst using the Unreal Engine networking API.", "Category": "SpatialOS", diff --git a/UnrealGDKTestGymsVersion.txt b/UnrealGDKTestGymsVersion.txt new file mode 100644 index 0000000000..21c322074a --- /dev/null +++ b/UnrealGDKTestGymsVersion.txt @@ -0,0 +1 @@ +0.12.0-rc \ No newline at end of file diff --git a/ci/ReleaseTool/Common.cs b/ci/ReleaseTool/Common.cs index 79dd15d5af..2bb6be6b78 100644 --- a/ci/ReleaseTool/Common.cs +++ b/ci/ReleaseTool/Common.cs @@ -1,6 +1,8 @@ +using NLog; using System; using System.IO; using System.Linq; +using System.Text.RegularExpressions; namespace ReleaseTool { @@ -55,5 +57,110 @@ private static string AppendDirectorySeparator(string originalPath) return originalPath + directorySeparator; } + + public static bool IsMarkdownHeading(string markdownLine, int level, string startTitle = null) + { + var heading = $"{new string('#', level)} {startTitle ?? string.Empty}"; + + return markdownLine.StartsWith(heading); + } + + public static bool UpdateChangeLog(string changeLogFilePath, string version, GitClient gitClient, string changeLogReleaseHeadingTemplate) + { + using (new WorkingDirectoryScope(gitClient.RepositoryPath)) + { + if (File.Exists(changeLogFilePath)) + { + var originalContents = File.ReadAllText(changeLogFilePath); + var changelog = File.ReadAllLines(changeLogFilePath).ToList(); + var releaseHeading = string.Format(changeLogReleaseHeadingTemplate, version, + DateTime.Now); + var releaseIndex = changelog.FindIndex(line => IsMarkdownHeading(line, 2, $"[`{version}`] - ")); + // If we already have a changelog entry for this release, replace it. + if (releaseIndex != -1) + { + changelog[releaseIndex] = releaseHeading; + } + else + { + // Add the new release heading under the "## Unreleased" one. + // Assuming that this is the first heading. + var unreleasedIndex = changelog.FindIndex(line => IsMarkdownHeading(line, 2)); + changelog.InsertRange(unreleasedIndex + 1, new[] + { + string.Empty, + releaseHeading + }); + } + File.WriteAllLines(changeLogFilePath, changelog); + + // If nothing has changed, return false, so we can react to it from the caller. + if (File.ReadAllText(changeLogFilePath) == originalContents) + { + return false; + } + + gitClient.StageFile(changeLogFilePath); + return true; + } + } + + throw new Exception($"Failed to update the changelog. Arguments: " + + $"ChangeLogFilePath: {changeLogFilePath}, Version: {version}, Heading template: {changeLogReleaseHeadingTemplate}."); + } + + public static bool UpdateVersionFile(GitClient gitClient, string fileContents, string versionFileRelativePath, NLog.Logger logger) + { + using (new WorkingDirectoryScope(gitClient.RepositoryPath)) + { + logger.Info("Updating contents of version file '{0}' to '{1}'...", versionFileRelativePath, fileContents); + + if (!File.Exists(versionFileRelativePath)) + { + throw new InvalidOperationException("Could not update the version file as the file " + + $"'{versionFileRelativePath}' does not exist."); + } + + if (File.ReadAllText(versionFileRelativePath) == fileContents) + { + logger.Info("Contents of '{0}' are already up-to-date ('{1}').", versionFileRelativePath, fileContents); + return false; + } + + File.WriteAllText(versionFileRelativePath, $"{fileContents}"); + + gitClient.StageFile(versionFileRelativePath); + } + + return true; + } + + public static (string, int) ExtractPullRequestInfo(string pullRequestUrl) + { + const string regexString = "github\\.com\\/.*\\/(.*)\\/pull\\/([0-9]*)"; + + var match = Regex.Match(pullRequestUrl, regexString); + + if (!match.Success) + { + throw new ArgumentException($"Malformed pull request url: {pullRequestUrl}"); + } + + if (match.Groups.Count < 3) + { + throw new ArgumentException($"Malformed pull request url: {pullRequestUrl}"); + } + + var repoName = match.Groups[1].Value; + var pullRequestIdStr = match.Groups[2].Value; + + if (!int.TryParse(pullRequestIdStr, out int pullRequestId)) + { + throw new Exception( + $"Parsing pull request URL failed. Expected number for pull request id, received: {pullRequestIdStr}"); + } + + return (repoName, pullRequestId); + } } } diff --git a/ci/ReleaseTool/EntryPoint.cs b/ci/ReleaseTool/EntryPoint.cs index 2ae7308679..9efc3e03e3 100644 --- a/ci/ReleaseTool/EntryPoint.cs +++ b/ci/ReleaseTool/EntryPoint.cs @@ -9,10 +9,11 @@ private static int Main(string[] args) { ConfigureLogger(); - return Parser.Default.ParseArguments(args) + return Parser.Default.ParseArguments(args) .MapResult( (PrepCommand.Options options) => new PrepCommand(options).Run(), (ReleaseCommand.Options options) => new ReleaseCommand(options).Run(), + (PrepFullReleaseCommand.Options options) => new PrepFullReleaseCommand(options).Run(), errors => 1); } diff --git a/ci/ReleaseTool/GitHubClient.cs b/ci/ReleaseTool/GitHubClient.cs index 6bf95d44cf..2b379e590e 100644 --- a/ci/ReleaseTool/GitHubClient.cs +++ b/ci/ReleaseTool/GitHubClient.cs @@ -116,10 +116,11 @@ public MergeState GetMergeState(Repository repository, int pullRequestId) } } - public PullRequestMerge MergePullRequest(Repository repository, int pullRequestId, PullRequestMergeMethod mergeMethod) + public PullRequestMerge MergePullRequest(Repository repository, int pullRequestId, PullRequestMergeMethod mergeMethod, string commitTitle) { var mergePullRequest = new MergePullRequest { + CommitTitle = commitTitle, MergeMethod = mergeMethod }; diff --git a/ci/ReleaseTool/PrepCommand.cs b/ci/ReleaseTool/PrepCommand.cs index f4f563a011..c73c831fbf 100644 --- a/ci/ReleaseTool/PrepCommand.cs +++ b/ci/ReleaseTool/PrepCommand.cs @@ -25,11 +25,12 @@ internal class PrepCommand private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private const string CandidateCommitMessageTemplate = "Release candidate for version {0}."; - private const string ReleaseBranchCreationCommitMessageTemplate = "Created a release branch based on {0} release candidate."; private const string PullRequestTemplate = "Release {0}"; private const string prAnnotationTemplate = "* Successfully created a [pull request]({0}) " + "in the repo `{1}` from `{2}` into `{3}`. " + "Your human labour is now required to complete the tasks listed in the PR descriptions and unblock the pipeline and resume the release.\n"; + private const string branchAnnotationTemplate = "* Successfully created a [release candidate branch]({0}) " + + "in the repo `{1}`, and it will evantually become `{2}` (no pull request as the specified release branch did not exist for this repository).\n"; // Names of the version files that live in the UnrealEngine repository. private const string UnrealGDKVersionFile = "UnrealGDKVersion.txt"; @@ -111,11 +112,11 @@ public int Run() // 1. Clones the source repo. using (var gitClient = GitClient.FromRemote(remoteUrl)) { - // 2. Checks out the source branch, which defaults to 4.xx-SpatialOSUnrealGDK in UnrealEngine and master in all other repos. - gitClient.CheckoutRemoteBranch(options.SourceBranch); - if (!gitClient.LocalBranchExists($"origin/{options.CandidateBranch}")) { + // 2. Checks out the source branch, which defaults to 4.xx-SpatialOSUnrealGDK in UnrealEngine and master in all other repos. + gitClient.CheckoutRemoteBranch(options.SourceBranch); + // 3. Makes repo-specific changes for prepping the release (e.g. updating version files, formatting the CHANGELOG). switch (options.GitRepoName) { @@ -154,10 +155,12 @@ public int Run() // 5. IF the release branch does not exist, creates it from the source branch and pushes it to the remote. if (!gitClient.LocalBranchExists($"origin/{options.ReleaseBranch}")) { - gitClient.Fetch(); - gitClient.CheckoutRemoteBranch(options.CandidateBranch); - gitClient.Commit(string.Format(ReleaseBranchCreationCommitMessageTemplate, options.Version)); - gitClient.ForcePush(options.ReleaseBranch); + Logger.Info("The release branch {0} does not exist! Going ahead with the PR-less release process.", options.ReleaseBranch); + Logger.Info("Release candidate head hash: {0}", gitClient.GetHeadCommit().Sha); + var branchAnnotation = string.Format(branchAnnotationTemplate, + $"https://github.com/{options.GithubOrgName}/{options.GitRepoName}/tree/{options.CandidateBranch}", options.GitRepoName, options.ReleaseBranch); + BuildkiteAgent.Annotate(AnnotationLevel.Info, "candidate-into-release-prs", branchAnnotation, true); + return 0; } // 6. Opens a PR for merging the RC branch into the release branch. @@ -184,8 +187,8 @@ public int Run() BuildkiteAgent.Annotate(AnnotationLevel.Info, "candidate-into-release-prs", prAnnotation, true); Logger.Info("Pull request available: {0}", pullRequest.HtmlUrl); - Logger.Info("Successfully created release!"); - Logger.Info("Release hash: {0}", gitClient.GetHeadCommit().Sha); + Logger.Info("Successfully created pull request for the release!"); + Logger.Info("PR head hash: {0}", gitClient.GetHeadCommit().Sha); } } catch (Exception e) diff --git a/ci/ReleaseTool/PrepFullReleaseCommand.cs b/ci/ReleaseTool/PrepFullReleaseCommand.cs new file mode 100644 index 0000000000..433fff4afb --- /dev/null +++ b/ci/ReleaseTool/PrepFullReleaseCommand.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using CommandLine; +using Octokit; + +namespace ReleaseTool +{ + /// + /// Runs the commands required for releasing a candidate. + /// * Merges the candidate branch into the release branch. + /// * Pushes the release branch. + /// * Creates a GitHub release draft. + /// * Creates a PR from the release-branch (defaults to release) branch into the source-branch (defaults to master). + /// + internal class PrepFullReleaseCommand + { + private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + + // Changelog file configuration + private const string ChangeLogFilename = "CHANGELOG.md"; + private const string CandidateCommitMessageTemplate = "Prepare GDK for Unreal release {0}."; + private const string ChangeLogReleaseHeadingTemplate = "## [`{0}`] - {1:yyyy-MM-dd}"; + + // Names of the version files that live in the UnrealEngine repository. + private const string UnrealGDKVersionFile = "UnrealGDKVersion.txt"; + private const string UnrealGDKExampleProjectVersionFile = "UnrealGDKExampleProjectVersion.txt"; + + [Verb("prepfullrelease", HelpText = "Prepare a release candidate branch for the full release.")] + public class Options : GitHubClient.IGitHubOptions + { + [Value(0, MetaName = "version", HelpText = "The version that is being released.")] + public string Version { get; set; } + + [Option("candidate-branch", HelpText = "The candidate branch name.", Required = true)] + public string CandidateBranch { get; set; } + + [Option("git-repository-name", HelpText = "The Git repository that we are targeting.", Required = true)] + public string GitRepoName { get; set; } + + [Option("github-organization", HelpText = "The Github Organization that contains the targeted repository.", Required = true)] + public string GithubOrgName { get; set; } + + public string GitHubTokenFile { get; set; } + + public string GitHubToken { get; set; } + + public string MetadataFilePath { get; set; } + } + + private readonly Options options; + + public PrepFullReleaseCommand(Options options) + { + this.options = options; + } + + /* + * This command does the necessary preparations for releasing an rc-branch: + * 1. Re-pointing all Version.txt files + */ + public int Run() + { + Common.VerifySemanticVersioningFormat(options.Version); + var gitRepoName = options.GitRepoName; + var remoteUrl = string.Format(Common.RepoUrlTemplate, options.GithubOrgName, gitRepoName); + try + { + // 1. Clones the source repo. + using (var gitClient = GitClient.FromRemote(remoteUrl)) + { + // 2. Checks out the candidate branch, which defaults to 4.xx-SpatialOSUnrealGDK-x.y.z-rc in UnrealEngine and x.y.z-rc in all other repos. + gitClient.CheckoutRemoteBranch(options.CandidateBranch); + + bool madeChanges = false; + + // 3. Makes repo-specific changes for prepping the release (e.g. updating version files, formatting the CHANGELOG). + switch (gitRepoName) + { + case "UnrealEngine": + madeChanges |= Common.UpdateVersionFile(gitClient, options.Version, UnrealGDKVersionFile, Logger); + madeChanges |= Common.UpdateVersionFile(gitClient, options.Version, UnrealGDKExampleProjectVersionFile, Logger); + break; + case "UnrealGDK": + Logger.Info("Updating {0}...", ChangeLogFilename); + madeChanges |= Common.UpdateChangeLog(ChangeLogFilename, options.Version, gitClient, ChangeLogReleaseHeadingTemplate); + if (!madeChanges) Logger.Info("{0} was already up-to-date.", ChangeLogFilename); + break; + case "UnrealGDKExampleProject": + madeChanges |= Common.UpdateVersionFile(gitClient, options.Version, UnrealGDKVersionFile, Logger); + break; + case "UnrealGDKTestGyms": + madeChanges |= Common.UpdateVersionFile(gitClient, options.Version, UnrealGDKVersionFile, Logger); + break; + case "UnrealGDKEngineNetTest": + madeChanges |= Common.UpdateVersionFile(gitClient, options.Version, UnrealGDKVersionFile, Logger); + break; + case "TestGymBuildKite": + madeChanges |= Common.UpdateVersionFile(gitClient, options.Version, UnrealGDKVersionFile, Logger); + break; + } + + if (madeChanges) + { + // 4. Commit changes and push them to a remote candidate branch. + gitClient.Commit(string.Format(CandidateCommitMessageTemplate, options.Version)); + gitClient.ForcePush(options.CandidateBranch); + Logger.Info($"Updated branch '{options.CandidateBranch}' in preparation for the full release."); + } + else + { + Logger.Info($"Tried to update branch '{options.CandidateBranch}' in preparation for the full release, but it was already up-to-date."); + } + } + } + catch (Exception e) + { + Logger.Error(e, $"ERROR: Unable to update '{options.CandidateBranch}'. Error: {0}", e.Message); + return 1; + } + + return 0; + } + } +} diff --git a/ci/ReleaseTool/ReleaseCommand.cs b/ci/ReleaseTool/ReleaseCommand.cs index 66f1d4eb80..847b7c28f8 100644 --- a/ci/ReleaseTool/ReleaseCommand.cs +++ b/ci/ReleaseTool/ReleaseCommand.cs @@ -32,21 +32,17 @@ internal class ReleaseCommand // Changelog file configuration private const string ChangeLogFilename = "CHANGELOG.md"; - private const string CandidateCommitMessageTemplate = "{0}."; + private const string CandidateCommitMessageTemplate = "Update branch for GDK for Unreal {0}."; private const string ChangeLogReleaseHeadingTemplate = "## [`{0}`] - {1:yyyy-MM-dd}"; - // Names of the version files that live in the UnrealEngine repository. - private const string UnrealGDKVersionFile = "UnrealGDKVersion.txt"; - private const string UnrealGDKExampleProjectVersionFile = "UnrealGDKExampleProjectVersion.txt"; - [Verb("release", HelpText = "Merge a release branch and create a github release draft.")] public class Options : GitHubClient.IGitHubOptions { [Value(0, MetaName = "version", HelpText = "The version that is being released.")] public string Version { get; set; } - [Option('u', "pull-request-url", HelpText = "The link to the release candidate branch to merge.", - Required = true)] + [Option('u', "pull-request-url", Default = "", HelpText = "The link to the release candidate branch to merge.", + Required = false)] public string PullRequestUrl { get; set; } [Option("source-branch", HelpText = "The source branch name from which we are cutting the candidate.", Required = true)] @@ -58,6 +54,9 @@ public class Options : GitHubClient.IGitHubOptions [Option("release-branch", HelpText = "The name of the branch into which we are merging the candidate.", Required = true)] public string ReleaseBranch { get; set; } + [Option("git-repository-name", HelpText = "The Git repository that we are targeting.", Required = true)] + public string GitRepoName { get; set; } + [Option("github-organization", HelpText = "The Github Organization that contains the targeted repository.", Required = true)] public string GithubOrgName { get; set; } @@ -87,66 +86,54 @@ public ReleaseCommand(Options options) public int Run() { Common.VerifySemanticVersioningFormat(options.Version); - var (repoName, pullRequestId) = ExtractPullRequestInfo(options.PullRequestUrl); + var gitRepoName = options.GitRepoName; var gitHubClient = new GitHubClient(options); - var repoUrl = string.Format(Common.RepoUrlTemplate, options.GithubOrgName, repoName); + var repoUrl = string.Format(Common.RepoUrlTemplate, options.GithubOrgName, gitRepoName); var gitHubRepo = gitHubClient.GetRepositoryFromUrl(repoUrl); - // Check if the PR has been merged already. - // If it has, log the PR URL and move on. - // This ensures the idempotence of the pipeline. - if (gitHubClient.GetMergeState(gitHubRepo, pullRequestId) == GitHubClient.MergeState.AlreadyMerged) + if (string.IsNullOrWhiteSpace(options.PullRequestUrl.Trim().Replace("\"",""))) { - Logger.Info("Candidate branch has already merged into release branch. No merge operation will be attempted."); - - // Check if a PR has already been opened from release branch into source branch. - // If it has, log the PR URL and move on. - // This ensures the idempotence of the pipeline. - var githubOrg = options.GithubOrgName; - var branchFrom = $"{options.CandidateBranch}-cleanup"; - var branchTo = options.SourceBranch; + Logger.Info("The passed PullRequestUrl was empty or missing. Trying to release without merging a PR."); - if (!gitHubClient.TryGetPullRequest(gitHubRepo, githubOrg, branchFrom, branchTo, out var pullRequest)) + using (var gitClient = GitClient.FromRemote(repoUrl)) { - try + // Create the release branch, since if there is no PR, the release branch did not exist previously + gitClient.Fetch(); + if (gitClient.LocalBranchExists($"origin/{options.ReleaseBranch}")) { - using (var gitClient = GitClient.FromRemote(repoUrl)) - { - gitClient.CheckoutRemoteBranch(options.ReleaseBranch); - gitClient.ForcePush(branchFrom); - } - pullRequest = gitHubClient.CreatePullRequest(gitHubRepo, - branchFrom, - branchTo, - string.Format(PullRequestNameTemplate, options.Version, options.ReleaseBranch, options.SourceBranch), - string.Format(pullRequestBody, options.ReleaseBranch, options.SourceBranch)); + Logger.Error("The PullRequestUrl was empty or missing, but the release branch already exists, so presuming this step already ran."); } - catch (Octokit.ApiValidationException e) + else { - // Handles the case where source-branch (default master) and release-branch (default release) are identical, so there is no need to merge source-branch back into release-branch. - if (e.ApiError.Errors.Count>0 && e.ApiError.Errors[0].Message.Contains("No commits between")) - { - Logger.Info(e.ApiError.Errors[0].Message); - Logger.Info("No PR will be created."); - return 0; - } - - throw; + gitClient.CheckoutRemoteBranch(options.CandidateBranch); + gitClient.ForcePush(options.ReleaseBranch); } - } - else - { - Logger.Info("A PR has already been opened from release branch into source branch: {0}", pullRequest.HtmlUrl); + gitClient.Fetch(); + gitClient.CheckoutRemoteBranch(options.ReleaseBranch); + + FinalizeRelease(gitHubClient, gitClient, gitHubRepo, gitRepoName, repoUrl); } - var prAnnotation = string.Format(prAnnotationTemplate, - pullRequest.HtmlUrl, repoName, options.ReleaseBranch, options.SourceBranch); - BuildkiteAgent.Annotate(AnnotationLevel.Info, "release-into-source-prs", prAnnotation, true); + return 0; + } + + var (repoName, pullRequestId) = Common.ExtractPullRequestInfo(options.PullRequestUrl); + if (gitRepoName != repoName) + { + Logger.Error($"Repository names given do not match. Repository name given: {gitRepoName}, PR URL repository name: {repoName}."); + return 1; + } + + // Check if the PR has been merged already. + // If it has, log the PR URL and move on. + // This ensures the idempotence of the pipeline. + if (gitHubClient.GetMergeState(gitHubRepo, pullRequestId) == GitHubClient.MergeState.AlreadyMerged) + { + Logger.Info("Candidate branch has already merged into release branch. No merge operation will be attempted."); - Logger.Info("Pull request available: {0}", pullRequest.HtmlUrl); - Logger.Info("Successfully created PR from release branch into source branch."); - Logger.Info("Merge hash: {0}", pullRequest.MergeCommitSha); + // null for GitClient will let it create one if necessary + CreatePRFromReleaseToSource(gitHubClient, gitHubRepo, repoUrl, repoName, null); return 0; } @@ -154,50 +141,33 @@ public int Run() var remoteUrl = string.Format(Common.RepoUrlTemplate, options.GithubOrgName, repoName); try { - // 1. Clones the source repo. - using (var gitClient = GitClient.FromRemote(remoteUrl)) + // Only do something for the UnrealGDK, since the other repos should have been prepped by the PrepFullReleaseCommand. + if (repoName == "UnrealGDK") { - // 2. Checks out the candidate branch, which defaults to 4.xx-SpatialOSUnrealGDK-x.y.z-rc in UnrealEngine and x.y.z-rc in all other repos. - gitClient.CheckoutRemoteBranch(options.CandidateBranch); - - // 3. Makes repo-specific changes for prepping the release (e.g. updating version files, formatting the CHANGELOG). - switch (repoName) + // 1. Clones the source repo. + using (var gitClient = GitClient.FromRemote(remoteUrl)) { - case "UnrealEngine": - UpdateVersionFile(gitClient, options.Version, UnrealGDKVersionFile); - UpdateVersionFile(gitClient, options.Version, UnrealGDKExampleProjectVersionFile); - break; - case "UnrealGDK": - UpdateChangeLog(ChangeLogFilename, options, gitClient); + // 2. Checks out the candidate branch, which defaults to 4.xx-SpatialOSUnrealGDK-x.y.z-rc in UnrealEngine and x.y.z-rc in all other repos. + gitClient.CheckoutRemoteBranch(options.CandidateBranch); - var releaseHashes = options.EngineVersions.Split(" ") - .Select(version => $"{version.Trim()}-release") - .Select(BuildkiteAgent.GetMetadata) - .Select(hash => $"UnrealEngine-{hash}") - .ToList(); + // 3. Makes repo-specific changes for prepping the release (e.g. updating version files, formatting the CHANGELOG). + Common.UpdateChangeLog(ChangeLogFilename, options.Version, gitClient, ChangeLogReleaseHeadingTemplate); - UpdateUnrealEngineVersionFile(releaseHashes, gitClient); - break; - case "UnrealGDKExampleProject": - UpdateVersionFile(gitClient, options.Version, UnrealGDKVersionFile); - break; - case "UnrealGDKTestGyms": - UpdateVersionFile(gitClient, options.Version, UnrealGDKVersionFile); - break; - case "UnrealGDKEngineNetTest": - UpdateVersionFile(gitClient, options.Version, UnrealGDKVersionFile); - break; - case "TestGymBuildKite": - UpdateVersionFile(gitClient, options.Version, UnrealGDKVersionFile); - break; - } + var releaseHashes = options.EngineVersions.Replace("\"", "").Split(" ") + .Select(version => $"{version.Trim()}") + .Select(BuildkiteAgent.GetMetadata) + .Select(hash => $"{hash}") + .ToList(); + + UpdateUnrealEngineVersionFile(releaseHashes, gitClient); - // 4. Commit changes and push them to a remote candidate branch. - gitClient.Commit(string.Format(CandidateCommitMessageTemplate, options.Version)); - gitClient.ForcePush(options.CandidateBranch); + // 4. Commit changes and push them to a remote candidate branch. + gitClient.Commit(string.Format(CandidateCommitMessageTemplate, options.Version)); + gitClient.ForcePush(options.CandidateBranch); + } } - // Since we've pushed changes, we need to wait for all checks to pass before attempting to merge it. + // Since we've (maybe) pushed changes, we need to wait for all checks to pass before attempting to merge it. var startTime = DateTime.Now; while (true) { @@ -222,16 +192,17 @@ public int Run() // Merge into release try { - mergeResult = gitHubClient.MergePullRequest(gitHubRepo, pullRequestId, PullRequestMergeMethod.Merge); + mergeResult = gitHubClient.MergePullRequest(gitHubRepo, pullRequestId, PullRequestMergeMethod.Merge, $"Merging final GDK for Unreal {options.Version} release"); + } + catch (Octokit.PullRequestNotMergeableException e) { + Logger.Info($"Was unable to merge pull request at: {options.PullRequestUrl}. Received error: {e.Message}"); } - catch (Octokit.PullRequestNotMergeableException e) {} // Will be covered by log below if (DateTime.Now.Subtract(startTime) > TimeSpan.FromHours(12)) { throw new Exception($"Exceeded timeout waiting for PR to be mergeable: {options.PullRequestUrl}"); } if (!mergeResult.Merged) { - Logger.Info($"Was unable to merge pull request at: {options.PullRequestUrl}. Received error: {mergeResult.Message}"); Logger.Info($"{options.PullRequestUrl} is not in a mergeable state, will query mergeability again in one minute."); Thread.Sleep(TimeSpan.FromMinutes(1)); } @@ -243,19 +214,12 @@ public int Run() Logger.Info($"{options.PullRequestUrl} had been merged."); - // This uploads the commit hashes of the merge into release. - // When run against UnrealGDK, the UnrealEngine hashes are used to update the unreal-engine.version file to include the UnrealEngine release commits. - BuildkiteAgent.SetMetaData(options.ReleaseBranch, mergeResult.Sha); - - //TODO: UNR-3615 - Fix this so it does not throw Octokit.ApiValidationException: Reference does not exist. - // Delete candidate branch. - //gitHubClient.DeleteBranch(gitHubClient.GetRepositoryFromUrl(repoUrl), options.CandidateBranch); - using (var gitClient = GitClient.FromRemote(repoUrl)) { // Create GitHub release in the repo gitClient.Fetch(); gitClient.CheckoutRemoteBranch(options.ReleaseBranch); + FinalizeRelease(gitHubClient, gitClient, gitHubRepo, gitRepoName, repoUrl); var release = CreateRelease(gitHubClient, gitHubRepo, gitClient, repoName); BuildkiteAgent.Annotate(AnnotationLevel.Info, "draft-releases", @@ -264,95 +228,39 @@ public int Run() Logger.Info("Release Successful!"); Logger.Info("Release hash: {0}", gitClient.GetHeadCommit().Sha); Logger.Info("Draft release: {0}", release.HtmlUrl); - } - - // Check if a PR has already been opened from release branch into source branch. - // If it has, log the PR URL and move on. - // This ensures the idempotence of the pipeline. - var githubOrg = options.GithubOrgName; - var branchFrom = $"{options.CandidateBranch}-cleanup"; - var branchTo = options.SourceBranch; - - if (!gitHubClient.TryGetPullRequest(gitHubRepo, githubOrg, branchFrom, branchTo, out var pullRequest)) - { - try - { - using (var gitClient = GitClient.FromRemote(repoUrl)) - { - gitClient.CheckoutRemoteBranch(options.ReleaseBranch); - gitClient.ForcePush(branchFrom); - } - pullRequest = gitHubClient.CreatePullRequest(gitHubRepo, - branchFrom, - branchTo, - string.Format(PullRequestNameTemplate, options.Version, options.ReleaseBranch, options.SourceBranch), - string.Format(pullRequestBody, options.ReleaseBranch, options.SourceBranch)); - } - catch (Octokit.ApiValidationException e) - { - // Handles the case where source-branch (default master) and release-branch (default release) are identical, so there is no need to merge source-branch back into release-branch. - if (e.ApiError.Errors.Count > 0 && e.ApiError.Errors[0].Message.Contains("No commits between")) - { - Logger.Info(e.ApiError.Errors[0].Message); - Logger.Info("No PR will be created."); - return 0; - } - - throw; - } - } - else - { - Logger.Info("A PR has already been opened from release branch into source branch: {0}", pullRequest.HtmlUrl); + CreatePRFromReleaseToSource(gitHubClient, gitHubRepo, repoUrl, repoName, gitClient); } - - var prAnnotation = string.Format(prAnnotationTemplate, - pullRequest.HtmlUrl, repoName, options.ReleaseBranch, options.SourceBranch); - BuildkiteAgent.Annotate(AnnotationLevel.Info, "release-into-source-prs", prAnnotation, true); - - Logger.Info("Pull request available: {0}", pullRequest.HtmlUrl); - Logger.Info($"Successfully created PR for merging {options.ReleaseBranch} into {options.SourceBranch}."); } catch (Exception e) { - Logger.Error(e, $"ERROR: Unable to merge {options.CandidateBranch} into {options.ReleaseBranch} and/or clean up by merging {options.ReleaseBranch} into {options.SourceBranch}. Error: {0}", e); + Logger.Error(e, $"ERROR: Unable to merge {options.CandidateBranch} into {options.ReleaseBranch} and/or clean up by merging {options.ReleaseBranch} into {options.SourceBranch}. Error: {e.Message}"); return 1; } return 0; } - internal static void UpdateChangeLog(string ChangeLogFilePath, Options options, GitClient gitClient) + + private void FinalizeRelease(GitHubClient gitHubClient, GitClient gitClient, Repository gitHubRepo, string gitRepoName, string repoUrl) { - using (new WorkingDirectoryScope(gitClient.RepositoryPath)) - { - if (File.Exists(ChangeLogFilePath)) - { - Logger.Info("Updating {0}...", ChangeLogFilePath); - var changelog = File.ReadAllLines(ChangeLogFilePath).ToList(); - var releaseHeading = string.Format(ChangeLogReleaseHeadingTemplate, options.Version, - DateTime.Now); - var releaseIndex = changelog.FindIndex(line => IsMarkdownHeading(line, 2, $"[`{options.Version}`] - ")); - // If we already have a changelog entry for this release, replace it. - if (releaseIndex != -1) - { - changelog[releaseIndex] = releaseHeading; - } - else - { - // Add the new release heading under the "## Unreleased" one. - // Assuming that this is the first heading. - var unreleasedIndex = changelog.FindIndex(line => IsMarkdownHeading(line, 2)); - changelog.InsertRange(unreleasedIndex + 1, new[] - { - string.Empty, - releaseHeading - }); - } - File.WriteAllLines(ChangeLogFilePath, changelog); - gitClient.StageFile(ChangeLogFilePath); - } - } + // This uploads the commit hashes of the merge into release. + // When run against UnrealGDK, the UnrealEngine hashes are used to update the unreal-engine.version file to include the UnrealEngine release commits. + BuildkiteAgent.SetMetaData(options.ReleaseBranch, gitClient.GetHeadCommit().Sha); + + // TODO: UNR-3615 - Fix this so it does not throw Octokit.ApiValidationException: Reference does not exist. + // Delete candidate branch. + //gitHubClient.DeleteBranch(gitHubClient.GetRepositoryFromUrl(repoUrl), options.CandidateBranch); + + var release = CreateRelease(gitHubClient, gitHubRepo, gitClient, gitRepoName); + + BuildkiteAgent.Annotate(AnnotationLevel.Info, "draft-releases", + string.Format(releaseAnnotationTemplate, release.HtmlUrl, gitRepoName), true); + + Logger.Info("Release Successful!"); + Logger.Info("Release hash: {0}", gitClient.GetHeadCommit().Sha); + Logger.Info("Draft release: {0}", release.HtmlUrl); + + CreatePRFromReleaseToSource(gitHubClient, gitHubRepo, repoUrl, gitRepoName, gitClient); } private Release CreateRelease(GitHubClient gitHubClient, Repository gitHubRepo, GitClient gitClient, string repoName) @@ -361,6 +269,7 @@ private Release CreateRelease(GitHubClient gitHubClient, Repository gitHubRepo, var engineVersion = options.SourceBranch.Trim(); + string tag = options.Version; // Default tag, only changed for Engine versions currently string name; string releaseBody; @@ -380,7 +289,7 @@ Release notes 将同时提供中英文。要浏览中文版本,向下滚动页 # English version -**Unreal GDK version {options.Version} is go!** +**Unreal GDK version {options.Version} has been released!** ## Release Notes @@ -389,7 +298,7 @@ Release notes 将同时提供中英文。要浏览中文版本,向下滚动页 ## Upgrading * You can find the corresponding UnrealEngine version(s) [here](https://github.com/improbableio/UnrealEngine/releases). -* You can find the corresponding UnrealGDKExampleProject version [here](https://github.com/spatialos/UnrealGDKExampleProject/releases). +* You can find the corresponding UnrealGDKExampleProject version [here](https://github.com/spatialos/UnrealGDKExampleProject/releases/tag/{options.Version}). Follow **[these](https://documentation.improbable.io/gdk-for-unreal/docs/keep-your-gdk-up-to-date)** steps to upgrade your GDK, Engine fork and Example Project to the latest release. @@ -416,12 +325,13 @@ Join the community on our [forums](https://forums.improbable.io/), or on [Discor "; break; case "UnrealEngine": + tag = $"{engineVersion}-{options.Version}"; name = $"{engineVersion}-{options.Version}"; releaseBody = -$@"Unreal GDK version {options.Version} is go! +$@"Unreal GDK version {options.Version} has been released! -* This Engine version corresponds to GDK version: [{options.Version}](https://github.com/spatialos/UnrealGDK/releases). -* You can find the corresponding UnrealGDKExampleProject version [here](https://github.com/spatialos/UnrealGDKExampleProject/releases). +* This Engine version corresponds to GDK version: [{options.Version}](https://github.com/spatialos/UnrealGDK/releases/tag/{options.Version}). +* You can find the corresponding UnrealGDKExampleProject version [here](https://github.com/spatialos/UnrealGDKExampleProject/releases/tag/{options.Version}). Follow [these steps](https://documentation.improbable.io/gdk-for-unreal/docs/keep-your-gdk-up-to-date) to upgrade your GDK, Unreal Engine fork and your Project to the latest release. @@ -433,12 +343,12 @@ Happy developing!
GDK team"; break; case "UnrealGDKTestGyms": - name = $"{options.Version}"; + name = $"Unreal GDK Test Gyms {options.Version}"; releaseBody = -$@"Unreal GDK version {options.Version} is go! +$@"Unreal GDK version {options.Version} has been released! -* This UnrealGDKTestGyms version corresponds to GDK version: [{options.Version}](https://github.com/spatialos/UnrealGDK/releases). -* You can find the corresponding UnrealGDKExampleProject version [here](https://github.com/spatialos/UnrealGDKExampleProject/releases). +* This UnrealGDKTestGyms version corresponds to GDK version: [{options.Version}](https://github.com/spatialos/UnrealGDK/releases/tag/{options.Version}). +* You can find the corresponding UnrealGDKExampleProject version [here](https://github.com/spatialos/UnrealGDKExampleProject/releases/tag/{options.Version}). * You can find the corresponding UnrealEngine version(s) [here](https://github.com/improbableio/UnrealEngine/releases). Follow [these steps](https://documentation.improbable.io/gdk-for-unreal/docs/keep-your-gdk-up-to-date) to upgrade your GDK, Unreal Engine fork and your Project to the latest release. @@ -451,13 +361,13 @@ Happy developing!
GDK team"; break; case "UnrealGDKEngineNetTest": - name = $"{options.Version}"; + name = $"Unreal GDK EngineNetTest {options.Version}"; releaseBody = -$@"Unreal GDK version {options.Version} is go! +$@"Unreal GDK version {options.Version} has been released! -* This UnrealGDKEngineNetTest version corresponds to GDK version: [{options.Version}](https://github.com/spatialos/UnrealGDK/releases). -* You can find the corresponding UnrealGDKTestGyms version [here](https://github.com/improbable/UnrealGDKTestGyms/releases). -* You can find the corresponding UnrealGDKExampleProject version [here](https://github.com/spatialos/UnrealGDKExampleProject/releases). +* This UnrealGDKEngineNetTest version corresponds to GDK version: [{options.Version}](https://github.com/spatialos/UnrealGDK/releases/tag/{options.Version}). +* You can find the corresponding UnrealGDKTestGyms version [here](https://github.com/improbable/UnrealGDKTestGyms/releases/tag/{options.Version}). +* You can find the corresponding UnrealGDKExampleProject version [here](https://github.com/spatialos/UnrealGDKExampleProject/releases/tag/{options.Version}). * You can find the corresponding UnrealEngine version(s) [here](https://github.com/improbableio/UnrealEngine/releases). Follow [these steps](https://documentation.improbable.io/gdk-for-unreal/docs/keep-your-gdk-up-to-date) to upgrade your GDK, Unreal Engine fork and your Project to the latest release. @@ -470,13 +380,13 @@ Happy developing!
GDK team"; break; case "TestGymBuildKite": - name = $"{options.Version}"; + name = $"Unreal GDK TestGymBuildKite {options.Version}"; releaseBody = -$@"Unreal GDK version {options.Version} is go! +$@"Unreal GDK version {options.Version} has been released! -* This TestGymBuildKite version corresponds to GDK version: [{options.Version}](https://github.com/spatialos/UnrealGDK/releases). -* You can find the corresponding UnrealGDKTestGyms version [here](https://github.com/improbable/UnrealGDKTestGyms/releases). -* You can find the corresponding UnrealGDKExampleProject version [here](https://github.com/spatialos/UnrealGDKExampleProject/releases). +* This TestGymBuildKite version corresponds to GDK version: [{options.Version}](https://github.com/spatialos/UnrealGDK/releases/tag/{options.Version}). +* You can find the corresponding UnrealGDKTestGyms version [here](https://github.com/improbable/UnrealGDKTestGyms/releases/tag/{options.Version}). +* You can find the corresponding UnrealGDKExampleProject version [here](https://github.com/spatialos/UnrealGDKExampleProject/releases/tag/{options.Version}). * You can find the corresponding UnrealEngine version(s) [here](https://github.com/improbableio/UnrealEngine/releases). Follow [these steps](https://documentation.improbable.io/gdk-for-unreal/docs/keep-your-gdk-up-to-date) to upgrade your GDK, Unreal Engine fork and your Project to the latest release. @@ -489,11 +399,11 @@ Happy developing!
GDK team"; break; case "UnrealGDKExampleProject": - name = $"{options.Version}"; + name = $"Unreal GDK Example Project {options.Version}"; releaseBody = -$@"Unreal GDK version {options.Version} is go! +$@"Unreal GDK version {options.Version} has been released! -* This UnrealGDKExampleProject version corresponds to GDK version: [{options.Version}](https://github.com/spatialos/UnrealGDK/releases). +* This UnrealGDKExampleProject version corresponds to GDK version: [{options.Version}](https://github.com/spatialos/UnrealGDK/releases/tag/{options.Version}). * You can find the corresponding UnrealEngine version(s) [here](https://github.com/improbableio/UnrealEngine/releases). Follow [these steps](https://documentation.improbable.io/gdk-for-unreal/docs/keep-your-gdk-up-to-date) to upgrade your GDK, Unreal Engine fork and your Project to the latest release. @@ -509,60 +419,65 @@ Happy developing!
throw new ArgumentException("Unsupported repository.", nameof(repoName)); } - return gitHubClient.CreateDraftRelease(gitHubRepo, options.Version, releaseBody, name, headCommit); + return gitHubClient.CreateDraftRelease(gitHubRepo, tag, releaseBody, name, headCommit); } - private static void UpdateVersionFile(GitClient gitClient, string fileContents, string relativeFilePath) + private void CreatePRFromReleaseToSource(GitHubClient gitHubClient, Repository gitHubRepo, string repoUrl, string repoName, GitClient gitClient) { - using (new WorkingDirectoryScope(gitClient.RepositoryPath)) - { - Logger.Info("Updating contents of version file '{0}' to '{1}'...", relativeFilePath, fileContents); + // Check if a PR has already been opened from release branch into source branch. + // If it has, log the PR URL and move on. + // This ensures the idempotence of the pipeline. + var githubOrg = options.GithubOrgName; + var branchFrom = $"{options.CandidateBranch}-cleanup"; + var branchTo = options.SourceBranch; - if (!File.Exists(relativeFilePath)) + if (!gitHubClient.TryGetPullRequest(gitHubRepo, githubOrg, branchFrom, branchTo, out var pullRequest)) + { + try { - throw new InvalidOperationException("Could not update the version file as the file " + - $"'{relativeFilePath}' does not exist."); + if (gitClient == null) + { + using (gitClient = GitClient.FromRemote(repoUrl)) + { + gitClient.CheckoutRemoteBranch(options.ReleaseBranch); + gitClient.ForcePush(branchFrom); + } + } + else + { + gitClient.CheckoutRemoteBranch(options.ReleaseBranch); + gitClient.ForcePush(branchFrom); + } + pullRequest = gitHubClient.CreatePullRequest(gitHubRepo, + branchFrom, + branchTo, + string.Format(PullRequestNameTemplate, options.Version, options.ReleaseBranch, options.SourceBranch), + string.Format(pullRequestBody, options.ReleaseBranch, options.SourceBranch)); } + catch (Octokit.ApiValidationException e) + { + // Handles the case where source-branch (default master) and release-branch (default release) are identical, so there is no need to merge source-branch back into release-branch. + if (e.ApiError.Errors.Count > 0 && e.ApiError.Errors[0].Message.Contains("No commits between")) + { + Logger.Info(e.ApiError.Errors[0].Message); + Logger.Info("No PR will be created."); + return; + } - File.WriteAllText(relativeFilePath, $"{fileContents}"); - - gitClient.StageFile(relativeFilePath); - } - } - - private static bool IsMarkdownHeading(string markdownLine, int level, string startTitle = null) - { - var heading = $"{new string('#', level)} {startTitle ?? string.Empty}"; - - return markdownLine.StartsWith(heading); - } - - private static (string, int) ExtractPullRequestInfo(string pullRequestUrl) - { - const string regexString = "github\\.com\\/.*\\/(.*)\\/pull\\/([0-9]*)"; - - var match = Regex.Match(pullRequestUrl, regexString); - - if (!match.Success) - { - throw new ArgumentException($"Malformed pull request url: {pullRequestUrl}"); + throw; + } } - - if (match.Groups.Count < 3) + else { - throw new ArgumentException($"Malformed pull request url: {pullRequestUrl}"); + Logger.Info("A PR has already been opened from release branch into source branch: {0}", pullRequest.HtmlUrl); } - var repoName = match.Groups[1].Value; - var pullRequestIdStr = match.Groups[2].Value; - - if (!int.TryParse(pullRequestIdStr, out int pullRequestId)) - { - throw new Exception( - $"Parsing pull request URL failed. Expected number for pull request id, received: {pullRequestIdStr}"); - } + var prAnnotation = string.Format(prAnnotationTemplate, + pullRequest.HtmlUrl, repoName, options.ReleaseBranch, options.SourceBranch); + BuildkiteAgent.Annotate(AnnotationLevel.Info, "release-into-source-prs", prAnnotation, true); - return (repoName, pullRequestId); + Logger.Info("Pull request available: {0}", pullRequest.HtmlUrl); + Logger.Info($"Successfully created PR for merging {options.ReleaseBranch} into {options.SourceBranch}."); } private static string GetReleaseNotesFromChangeLog() diff --git a/ci/build-project.ps1 b/ci/build-project.ps1 index 62d8d7e831..8c05e5aeb0 100644 --- a/ci/build-project.ps1 +++ b/ci/build-project.ps1 @@ -22,6 +22,10 @@ if (-Not $?) { # 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" +# Unreal likes to save some settings outside of the project folders. Let's clean this up to make sure it doesn't cause issues when running the tests. +$project_name = $(Get-ChildItem $test_repo_uproject_path).BaseName +Remove-Item $env:LOCALAPPDATA\$project_name\Saved\Config -ErrorAction ignore -Recurse -Force + # 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) diff --git a/ci/build-project.sh b/ci/build-project.sh index 6e3f7376d4..4f382177a8 100755 --- a/ci/build-project.sh +++ b/ci/build-project.sh @@ -30,6 +30,9 @@ pushd "$(dirname "$0")" mkdir -p "${TEST_REPO_PATH}/Game/Plugins" cp -R "${GDK_HOME}" "${TEST_REPO_PATH}/Game/Plugins/UnrealGDK" + # Unreal likes to save some settings outside of the project folders. Let's clean this up to make sure it doesn't cause issues when running the tests. + rm -rf ~/Library/Preferences/Unreal\ Engine/ + # 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}" diff --git a/ci/common-release.sh b/ci/common-release.sh index bb6bcdcb61..b1fdc63c91 100644 --- a/ci/common-release.sh +++ b/ci/common-release.sh @@ -29,3 +29,12 @@ function setupReleaseTool() { --file ./ci/docker/release-tool.Dockerfile \ . } + +function getDryrunBranchPrefix() { + DRY_RUN_METADATA="$(buildkite-agent meta-data get dry-run)" + if [[ "${DRY_RUN_METADATA}" == "false" ]]; then + echo "" + else + echo "dry-run/" + fi +} diff --git a/ci/gdk_build.template.steps.yaml b/ci/gdk_build.template.steps.yaml index e1d41badf3..8d3f5ab356 100644 --- a/ci/gdk_build.template.steps.yaml +++ b/ci/gdk_build.template.steps.yaml @@ -23,7 +23,7 @@ windows: &windows - "platform=windows" - "permission_set=builder" - "scaler_version=2" - - "queue=${CI_WINDOWS_BUILDER_QUEUE:-v4-20-03-26-102432-bk9951-8afe0ffb}" + - "queue=${CI_WINDOWS_BUILDER_QUEUE:-v4-20-11-18-224740-bk17641-0c4125be-d}" - "boot_disk_size_gb=500" retry: automatic: @@ -32,7 +32,7 @@ windows: &windows - <<: *bk_interrupted_by_signal timeout_in_minutes: 120 plugins: - - ca-johnson/taskkill#v4.1: ~ + - improbable-eng/taskkill#v4.4.1: ~ macos: &macos agents: @@ -59,6 +59,7 @@ steps: command: "${BUILD_COMMAND}" artifact_paths: - "../UnrealEngine/Engine/Programs/AutomationTool/Saved/Logs/*" + - "GDKTestGyms/spatial/logs/**/*" env: BUILD_ALL_CONFIGURATIONS: "${BUILD_ALL_CONFIGURATIONS}" ENGINE_COMMIT_HASH: "${ENGINE_COMMIT_HASH}" diff --git a/ci/generate-and-upload-build-steps.sh b/ci/generate-and-upload-build-steps.sh index 4d3c3da649..188e27a180 100755 --- a/ci/generate-and-upload-build-steps.sh +++ b/ci/generate-and-upload-build-steps.sh @@ -23,10 +23,11 @@ generate_build_configuration_steps () { # See https://docs.unrealengine.com/en-US/Programming/Development/BuildConfigurations/index.html for possible configurations ENGINE_COMMIT_HASH="${1}" + # This matches SpatialOS node sizes (https://github.com/improbable/platform/blob/master/go/src/improbable.io/lib/nodesizes/nodesizes.go) if [[ -z "${NIGHTLY_BUILD+x}" ]]; then - export BK_MACHINE_TYPE="quad-high-cpu" + export BK_MACHINE_TYPE="quad-high-perf" else - export BK_MACHINE_TYPE="single-high-cpu" # nightly builds run on smaller nodes + export BK_MACHINE_TYPE="single" # nightly builds run on smaller nodes fi if [[ -z "${MAC_BUILD:-}" ]]; then @@ -56,7 +57,7 @@ generate_build_configuration_steps () { 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 + export BK_MACHINE_TYPE="single" # 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 diff --git a/ci/get-engine.ps1 b/ci/get-engine.ps1 index a728e82e12..b9ae5781e6 100644 --- a/ci/get-engine.ps1 +++ b/ci/get-engine.ps1 @@ -38,7 +38,7 @@ Push-Location "$($gdk_home)" $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 = $version_description + $unreal_version = "UnrealEngine-$version_description" } Pop-Location diff --git a/ci/prepare-full-release.sh b/ci/prepare-full-release.sh new file mode 100644 index 0000000000..27687d6674 --- /dev/null +++ b/ci/prepare-full-release.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash + +### This script should only be run on Improbable's internal build machines. +### If you don't work at Improbable, this may be interesting as a guide to what software versions we use for our +### automation, but not much more than that. + +prepfullrelease () { + local REPO_NAME="${1}" + local CANDIDATE_BRANCH="${2}" + local GITHUB_ORG="${3}" + + echo "--- Preparing ${REPO_NAME} for the full release!" + + docker run \ + -v "${BUILDKITE_ARGS[@]}" \ + -v "${SECRETS_DIR}":/var/ssh \ + -v "${SECRETS_DIR}":/var/github \ + -v "$(pwd)"/logs:/var/logs \ + local:gdk-release-tool \ + prepfullrelease "${GDK_VERSION}" \ + --candidate-branch="${CANDIDATE_BRANCH}" \ + --github-key-file="/var/github/github_token" \ + --git-repository-name="${REPO_NAME}" \ + --github-organization="${GITHUB_ORG}" +} + +set -e -u -o pipefail + +if [[ -n "${DEBUG-}" ]]; then + set -x +fi + +if [[ -z "$BUILDKITE" ]]; then + echo "This script is only intended to be run on Improbable CI." + exit 1 +fi + +cd "$(dirname "$0")/../" + +source ci/common-release.sh + +# This BUILDKITE ARGS section is sourced from: improbable/nfr-benchmark-pipeline/blob/feature/nfr-framework/run.sh +declare -a BUILDKITE_ARGS=() + +if [[ -n "${BUILDKITE:-}" ]]; then + declare -a BUILDKITE_ARGS=( + "-e=BUILDKITE=${BUILDKITE}" + "-e=BUILD_EVENT_CACHE_ROOT_PATH=/build-event-data" + "-e=BUILDKITE_AGENT_ACCESS_TOKEN=${BUILDKITE_AGENT_ACCESS_TOKEN}" + "-e=BUILDKITE_AGENT_ENDPOINT=${BUILDKITE_AGENT_ENDPOINT}" + "-e=BUILDKITE_AGENT_META_DATA_CAPABLE_OF_BUILDING=${BUILDKITE_AGENT_META_DATA_CAPABLE_OF_BUILDING}" + "-e=BUILDKITE_AGENT_META_DATA_ENVIRONMENT=${BUILDKITE_AGENT_META_DATA_ENVIRONMENT}" + "-e=BUILDKITE_AGENT_META_DATA_PERMISSION_SET=${BUILDKITE_AGENT_META_DATA_PERMISSION_SET}" + "-e=BUILDKITE_AGENT_META_DATA_PLATFORM=${BUILDKITE_AGENT_META_DATA_PLATFORM}" + "-e=BUILDKITE_AGENT_META_DATA_SCALER_VERSION=${BUILDKITE_AGENT_META_DATA_SCALER_VERSION}" + "-e=BUILDKITE_AGENT_META_DATA_AGENT_COUNT=${BUILDKITE_AGENT_META_DATA_AGENT_COUNT}" + "-e=BUILDKITE_AGENT_META_DATA_WORKING_HOURS_TIME_ZONE=${BUILDKITE_AGENT_META_DATA_WORKING_HOURS_TIME_ZONE}" + "-e=BUILDKITE_AGENT_META_DATA_MACHINE_TYPE=${BUILDKITE_AGENT_META_DATA_MACHINE_TYPE}" + "-e=BUILDKITE_AGENT_META_DATA_QUEUE=${BUILDKITE_AGENT_META_DATA_QUEUE}" + "-e=BUILDKITE_TIMEOUT=${BUILDKITE_TIMEOUT}" + "-e=BUILDKITE_ARTIFACT_UPLOAD_DESTINATION=${BUILDKITE_ARTIFACT_UPLOAD_DESTINATION}" + "-e=BUILDKITE_BRANCH=${BUILDKITE_BRANCH}" + "-e=BUILDKITE_BUILD_CREATOR_EMAIL=${BUILDKITE_BUILD_CREATOR_EMAIL}" + "-e=BUILDKITE_BUILD_CREATOR=${BUILDKITE_BUILD_CREATOR}" + "-e=BUILDKITE_BUILD_ID=${BUILDKITE_BUILD_ID}" + "-e=BUILDKITE_BUILD_URL=${BUILDKITE_BUILD_URL}" + "-e=BUILDKITE_COMMIT=${BUILDKITE_COMMIT}" + "-e=BUILDKITE_JOB_ID=${BUILDKITE_JOB_ID}" + "-e=BUILDKITE_LABEL=${BUILDKITE_LABEL}" + "-e=BUILDKITE_MESSAGE=${BUILDKITE_MESSAGE}" + "-e=BUILDKITE_ORGANIZATION_SLUG=${BUILDKITE_ORGANIZATION_SLUG}" + "-e=BUILDKITE_PIPELINE_SLUG=${BUILDKITE_PIPELINE_SLUG}" + "--volume=/usr/bin/buildkite-agent:/usr/bin/buildkite-agent" + "--volume=/usr/local/bin/imp-tool-bootstrap:/usr/local/bin/imp-tool-bootstrap" + ) +fi + +# This assigns the gdk-version key that was set in .buildkite\release.steps.yaml to the variable GDK-VERSION +GDK_VERSION="$(buildkite-agent meta-data get gdk-version)" + +# This assigns the (potential) dry-run prefix to this variable if we are doing a dry-run +DRY_RUN_PREFIX=$(getDryrunBranchPrefix) + +# This assigns the engine-version key that was set in .buildkite\release.steps.yaml to the variable ENGINE-VERSION +ENGINE_VERSIONS=($(buildkite-agent meta-data get engine-source-branches)) + +setupReleaseTool + +mkdir -p ./logs +USER_ID=$(id -u) + +# Run the C Sharp Release Tool for each candidate we want to release. + +# The format is: +# 1. REPO_NAME +# 2. CANDIDATE_BRANCH +# 3. PR_URL +# 4. GITHUB_ORG + +# Run the C Sharp Release Tool for each candidate we want to prepare for the full release. +prepfullrelease "UnrealGDK" "${GDK_VERSION}-rc" "spatialos" +prepfullrelease "UnrealGDKExampleProject" "${GDK_VERSION}-rc" "spatialos" +prepfullrelease "UnrealGDKTestGyms" "${GDK_VERSION}-rc" "spatialos" +prepfullrelease "UnrealGDKEngineNetTest" "${GDK_VERSION}-rc" "improbable" +prepfullrelease "TestGymBuildKite" "${GDK_VERSION}-rc" "improbable" + +for ENGINE_VERSION in "${ENGINE_VERSIONS[@]}" +do + : + # Once per ENGINE_VERSION do: + prepfullrelease "UnrealEngine" \ + "${ENGINE_VERSION}-${GDK_VERSION}-rc" \ + "improbableio" +done diff --git a/ci/prepare-release.sh b/ci/prepare-release.sh index dfbab75d9b..02ea471333 100644 --- a/ci/prepare-release.sh +++ b/ci/prepare-release.sh @@ -89,15 +89,18 @@ USER_ID=$(id -u) # This assigns the gdk-version key that was set in .buildkite\release.steps.yaml to the variable GDK-VERSION GDK_VERSION="$(buildkite-agent meta-data get gdk-version)" +# This assigns the (potential) dry-run prefix to this variable if we are doing a dry-run +DRY_RUN_PREFIX=$(getDryrunBranchPrefix) + # This assigns the engine-version key that was set in .buildkite\release.steps.yaml to the variable ENGINE-VERSION ENGINE_VERSIONS=($(buildkite-agent meta-data get engine-source-branches)) # Run the C Sharp Release Tool for each candidate we want to cut. -prepareRelease "UnrealGDK" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "release" "spatialos" -prepareRelease "UnrealGDKExampleProject" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "release" "spatialos" -prepareRelease "UnrealGDKTestGyms" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "release" "spatialos" -prepareRelease "UnrealGDKEngineNetTest" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "release" "improbable" -prepareRelease "TestGymBuildKite" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "release" "improbable" +prepareRelease "UnrealGDK" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "${DRY_RUN_PREFIX}release" "spatialos" +prepareRelease "UnrealGDKExampleProject" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "${DRY_RUN_PREFIX}release" "spatialos" +prepareRelease "UnrealGDKTestGyms" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "${DRY_RUN_PREFIX}release" "spatialos" +prepareRelease "UnrealGDKEngineNetTest" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "${DRY_RUN_PREFIX}release" "improbable" +prepareRelease "TestGymBuildKite" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "${GDK_VERSION}" "improbable" for ENGINE_VERSION in "${ENGINE_VERSIONS[@]}" do @@ -106,6 +109,6 @@ do prepareRelease "UnrealEngine" \ "${ENGINE_VERSION}" \ "${ENGINE_VERSION}-${GDK_VERSION}-rc" \ - "${ENGINE_VERSION}-release" \ + "${DRY_RUN_PREFIX}${ENGINE_VERSION}-release" \ "improbableio" done \ No newline at end of file diff --git a/ci/release.sh b/ci/release.sh index fbaf6784f8..3393ee2e3d 100644 --- a/ci/release.sh +++ b/ci/release.sh @@ -11,7 +11,7 @@ release () { local RELEASE_BRANCH="${4}" local PR_URL="${5}" local GITHUB_ORG="${6}" - local ENGINE_VERSIONS_LOCAL_VAR=$(echo ${ENGINE_VERSIONS[*]// }) + local RELEASE_ENGINE_BRANCHES_LOCAL_VAR="$(echo ${RELEASE_ENGINE_BRANCHES[*]// })" echo "--- Releasing ${REPO_NAME}: Merging ${CANDIDATE_BRANCH} into ${RELEASE_BRANCH} :package:" @@ -26,9 +26,10 @@ release () { --candidate-branch="${CANDIDATE_BRANCH}" \ --release-branch="${RELEASE_BRANCH}" \ --github-key-file="/var/github/github_token" \ - --pull-request-url="${PR_URL}" \ + --pull-request-url="\"${PR_URL}\"" \ + --git-repository-name="${REPO_NAME}" \ --github-organization="${GITHUB_ORG}" \ - --engine-versions="${ENGINE_VERSIONS_LOCAL_VAR}" + --engine-versions="\"${RELEASE_ENGINE_BRANCHES_LOCAL_VAR}\"" } set -e -u -o pipefail @@ -85,6 +86,9 @@ fi # This assigns the gdk-version key that was set in .buildkite\release.steps.yaml to the variable GDK-VERSION GDK_VERSION="$(buildkite-agent meta-data get gdk-version)" +# This assigns the (potential) dry-run prefix to this variable if we are doing a dry-run +DRY_RUN_PREFIX=$(getDryrunBranchPrefix) + # This assigns the engine-version key that was set in .buildkite\release.steps.yaml to the variable ENGINE-VERSION ENGINE_VERSIONS=($(buildkite-agent meta-data get engine-source-branches)) @@ -103,22 +107,24 @@ USER_ID=$(id -u) # 5. PR_URL # 6. GITHUB_ORG -# Release UnrealEngine must run before UnrealGDK so that the resulting commits can be included in that repo's unreal-engine.version -for ENGINE_VERSION in "${ENGINE_VERSIONS[@]}" -do - : - # Once per ENGINE_VERSION do: - release "UnrealEngine" \ +# Release UnrealEngine must run before UnrealGDK so that the resulting commits can be included in that repo's unreal-engine.version. +# We go over the array in reverse order here, just to release the least relevant engine version first, so the most relevant one will +# end up on top of the releases page. +RELEASE_ENGINE_BRANCHES=() +for (( idx=${#ENGINE_VERSIONS[@]}-1 ; idx>=0 ; idx-- )) ; do + ENGINE_VERSION=${ENGINE_VERSIONS[idx]} + RELEASE_BRANCH_NAME="${DRY_RUN_PREFIX}${ENGINE_VERSION}-release" + release "UnrealEngine" \ "${ENGINE_VERSION}" \ "${ENGINE_VERSION}-${GDK_VERSION}-rc" \ - "${ENGINE_VERSION}-release" \ + "${RELEASE_BRANCH_NAME}" \ "$(buildkite-agent meta-data get UnrealEngine-${ENGINE_VERSION}-pr-url)" \ "improbableio" + RELEASE_ENGINE_BRANCHES+=("${RELEASE_BRANCH_NAME}") done - -release "UnrealGDK" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "release" "$(buildkite-agent meta-data get UnrealGDK-$(buildkite-agent meta-data get gdk-source-branch)-pr-url)" "spatialos" -release "UnrealGDKExampleProject" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "release" "$(buildkite-agent meta-data get UnrealGDKExampleProject-$(buildkite-agent meta-data get gdk-source-branch)-pr-url)" "spatialos" -release "UnrealGDKTestGyms" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "release" "$(buildkite-agent meta-data get UnrealGDKTestGyms-$(buildkite-agent meta-data get gdk-source-branch)-pr-url)" "spatialos" -release "UnrealGDKEngineNetTest" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "release" "$(buildkite-agent meta-data get UnrealGDKEngineNetTest-$(buildkite-agent meta-data get gdk-source-branch)-pr-url)" "improbable" -release "TestGymBuildKite" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "release" "$(buildkite-agent meta-data get TestGymBuildKite-$(buildkite-agent meta-data get gdk-source-branch)-pr-url)" "improbable" +release "UnrealGDK" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "${DRY_RUN_PREFIX}release" "$(buildkite-agent meta-data get UnrealGDK-$(buildkite-agent meta-data get gdk-source-branch)-pr-url)" "spatialos" +release "UnrealGDKExampleProject" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "${DRY_RUN_PREFIX}release" "$(buildkite-agent meta-data get UnrealGDKExampleProject-$(buildkite-agent meta-data get gdk-source-branch)-pr-url)" "spatialos" +release "UnrealGDKTestGyms" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "${DRY_RUN_PREFIX}release" "$(buildkite-agent meta-data get UnrealGDKTestGyms-$(buildkite-agent meta-data get gdk-source-branch)-pr-url)" "spatialos" +release "UnrealGDKEngineNetTest" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "${DRY_RUN_PREFIX}release" "$(buildkite-agent meta-data get UnrealGDKEngineNetTest-$(buildkite-agent meta-data get gdk-source-branch)-pr-url)" "improbable" +release "TestGymBuildKite" "$(buildkite-agent meta-data get gdk-source-branch)" "${GDK_VERSION}-rc" "${GDK_VERSION}" "$(buildkite-agent meta-data get TestGymBuildKite-$(buildkite-agent meta-data get gdk-source-branch)-pr-url)" "improbable" diff --git a/ci/report-tests.ps1 b/ci/report-tests.ps1 index 23a295f237..10d54ade9b 100644 --- a/ci/report-tests.ps1 +++ b/ci/report-tests.ps1 @@ -45,6 +45,7 @@ if (Test-Path "$test_result_dir\index.html" -PathType Leaf) { else { $error_msg = "The Unreal Editor crashed while running tests, see the test-gdk annotation for logs (or the tests.log buildkite artifact)." Write-Error $error_msg + buildkite-agent artifact upload "$test_result_dir\*" Throw $error_msg } diff --git a/ci/report-tests.sh b/ci/report-tests.sh new file mode 100755 index 0000000000..21d390beb4 --- /dev/null +++ b/ci/report-tests.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash + +set -e -u -o pipefail +if [[ -n "${DEBUG-}" ]]; then + set -x +fi + +pushd "$(dirname "$0")" + TEST_RESULTS_DIRECTORY="${1?Please enter the directory to the test results.}" + TARGET_PLATFORM="${2?Please enter the target platform.}" + + pushd "${TEST_RESULTS_DIRECTORY}" + TEST_RESULTS_DIRECTORY="$(pwd)" + popd + + UPLOAD_ARTIFACT_PATH="https://buildkite.com/organizations/${BUILDKITE_ORGANIZATION_SLUG}/pipelines/${BUILDKITE_PIPELINE_SLUG}/builds/${BUILDKITE_BUILD_ID}/jobs/${BUILDKITE_JOB_ID}/artifacts" + pushd "${TEST_RESULTS_DIRECTORY}" + HTML_RESULTS_FILE=index.html + JSON_RESULTS_FILE="index.json" + + if [[ -f "${HTML_RESULTS_FILE}" ]]; then + # 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 + # (replacing local dependencies in the html by CDNs or correcting paths) + sed -i -e ' + s?/bower_components/font-awesome/css/font-awesome.min.css?https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css?g + s?/bower_components/twentytwenty/css/twentytwenty.css?https://cdnjs.cloudflare.com/ajax/libs/mhayes-twentytwenty/1.0.0/css/twentytwenty.min.css?g + s?/bower_components/featherlight/release/featherlight.min.css?https://cdnjs.cloudflare.com/ajax/libs/featherlight/1.7.13/featherlight.min.css?g + s?/bower_components/bootstrap/dist/css/bootstrap.min.css?https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css?g + s?/bower_components/jquery/dist/jquery.min.js?https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js?g + s?/bower_components/jquery.event.move/js/jquery.event.move.js?https://cdnjs.cloudflare.com/ajax/libs/mhayes-twentytwenty/1.0.0/js/jquery.event.move.min.js?g + s?/bower_components/jquery_lazyload/jquery.lazyload.js?https://cdnjs.cloudflare.com/ajax/libs/jquery_lazyload/1.9.7/jquery.lazyload.min.js?g + s?/bower_components/twentytwenty/js/jquery.twentytwenty.js?https://cdnjs.cloudflare.com/ajax/libs/mhayes-twentytwenty/1.0.0/js/jquery.twentytwenty.min.js?g + s?/bower_components/clipboard/dist/clipboard.min.js?https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/1.5.16/clipboard.min.js?g + s?/bower_components/anchor-js/anchor.min.js?https://cdnjs.cloudflare.com/ajax/libs/anchor-js/3.2.2/anchor.min.js?g + s?/bower_components/featherlight/release/featherlight.min.js?https://cdnjs.cloudflare.com/ajax/libs/featherlight/1.7.13/featherlight.min.js?g + s?/bower_components/bootstrap/dist/js/bootstrap.min.js?https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js?g + s?/bower_components/dustjs-linkedin/dist/dust-full.min.js?https://cdnjs.cloudflare.com/ajax/libs/dustjs-linkedin/2.7.5/dust-full.min.js?g + s?/bower_components/numeral/min/numeral.min.js?https://cdnjs.cloudflare.com/ajax/libs/numeral.js/2.0.4/numeral.min.js?g + ' "${HTML_RESULTS_FILE}" + + else + echo "The Unreal Editor crashed while running tests, see the test-gdk annotation for logs (or the tests.log buildkite artifact)." + exit 1 + fi + + # Upload artifacts to Buildkite, capture output to extract artifact ID in the Slack message generation. + buildkite-agent artifact upload "${JSON_RESULTS_FILE}" + if [[ $? -ne 0 ]]; then + echo "Failed to upload artifact ${JSON_RESULTS_FILE}." + exit 1 + fi + UPLOAD_OUTPUT=$(buildkite-agent artifact upload "${HTML_RESULTS_FILE}" 2>&1 > /dev/null) + if [[ $? -ne 0 ]]; then + echo "Failed to upload artifact ${HTML_RESULTS_FILE}." + exit 1 + fi + + # Artifacts are assigned an ID upon upload, so grab IDs from upload process output to build the artifact URLs + # The output log is: "Uploading artifact ". We are interested in the artifact id + REGEX='Uploading artifact ([a-z0-9-]*) .*index\.html' + if [[ ${UPLOAD_OUTPUT} =~ ${REGEX} ]]; then + TEST_RESULTS_URL="${UPLOAD_ARTIFACT_PATH}/${BASH_REMATCH[1]}" + else + echo "Failed to extract artifact ids" + exit 1 + fi + + echo "Test results in a nicer format can be found here."| buildkite-agent annotate \ + --context "unreal-gdk-test-artifact-location" \ + --style info + + # Read the test results + + SLACK_ATTACHMENT_FILE="slack_attachment_${BUILDKITE_STEP_ID}.json" + TESTS_SUMMARY_FILE="test_summary_${BUILDKITE_STEP_ID}.json" + + TESTS_PASSED="danger" + if [[ $(cat ${JSON_RESULTS_FILE} | jq '.failed') == 0 ]]; then + TESTS_PASSED="good" + fi + + TOTAL_TESTS_SUCCEEDED=$(($(cat ${JSON_RESULTS_FILE} | jq '.succeeded') + $(cat ${JSON_RESULTS_FILE} | jq '.succeededWithWarnings'))) + TOTAL_TESTS_RUN=$(($(cat ${JSON_RESULTS_FILE} | jq '.failed') + ${TOTAL_TESTS_SUCCEEDED=})) + jq -n \ + --arg value0 "Find the test results at ${TEST_RESULTS_URL}" \ + --arg value1 "${TESTS_PASSED}" \ + --arg value2 "*${ENGINE_COMMIT_HASH}* $(basename ${TEST_RESULTS_DIRECTORY})" \ + --arg value3 "Passed ${TOTAL_TESTS_SUCCEEDED} / ${TOTAL_TESTS_RUN} tests." \ + --arg value4 "${TEST_RESULTS_URL}" \ + '{ + fallback: $value0, + color: $value1, + fields: [ + { + value: $value2, + short: true + }, + { + value: $value3, + short: true + } + ], + actions: [ + { + "url": $value4, + "style": "primary", + "type": "button", + "text": ":bar_chart: Test results" + } + ] + }' >> "${SLACK_ATTACHMENT_FILE}" + + buildkite-agent artifact upload "${SLACK_ATTACHMENT_FILE}" + + # Count the number of SpatialGDK & functional tests in order to report this + NUM_GDK_TESTS=$(cat "${JSON_RESULTS_FILE}"| jq '.tests | map(select(.fullTestPath | contains("SpatialGDK."))) | map(select(.errors!=0)) | length') + NUM_PROJECT_TESTS=$(cat "${JSON_RESULTS_FILE}" | jq '.tests | map(select(.fullTestPath | contains("Project."))) | map(select(.errors!=0)) | length') + jq -n \ + --arg value0 "$(date +%s)" \ + --arg value1 "${BUILDKITE_BUILD_URL}" \ + --arg value2 "${TARGET_PLATFORM}" \ + --arg value3 "${ENGINE_COMMIT_HASH}" \ + --arg value4 "${TESTS_PASSED}" \ + --arg value5 "$(cat ${JSON_RESULTS_FILE} | jq '.totalDuration')" \ + --arg value6 "${TOTAL_TESTS_RUN}" \ + --arg value7 "${NUM_GDK_TESTS}" \ + --arg value8 "${NUM_PROJECT_TESTS}" \ + --arg value9 "$(basename ${TEST_RESULTS_DIRECTORY})" \ + '{ + time: $value0, + build_url: $value1, + platform: $value2, + unreal_engine_commit: $value3, + passed_all_tests: $value4, + tests_duration_seconds: $value5, + num_tests: $value6, + num_gdk_tests: $value7, + num_project_tests: $value8, + test_result_directory_name: $value9, + }' >> "${TESTS_SUMMARY_FILE}" + buildkite-agent artifact upload "${TESTS_SUMMARY_FILE}" + popd + + # Fail this build if any tests failed + if [[ "${TESTS_PASSED}" == "danger" ]]; then + echo "Tests failed. Logs for these tests are contained in the tests.log artifact." + exit 1 + fi + echo "All tests passed!" +popd diff --git a/ci/run-tests.sh b/ci/run-tests.sh index ab6a6ebbf8..bce2280315 100755 --- a/ci/run-tests.sh +++ b/ci/run-tests.sh @@ -44,7 +44,8 @@ pushd "$(dirname "$0")" -unattended \ -nullRHI \ -run=GenerateSnapshot \ - -MapPaths="${TEST_REPO_MAP}" + -MapPaths="${TEST_REPO_MAP}" \ + || true cp "${TEST_REPO_PATH}/spatial/snapshots/${TEST_REPO_MAP}.snapshot" "${TEST_REPO_PATH}/spatial/snapshots/default.snapshot" fi @@ -64,5 +65,5 @@ pushd "$(dirname "$0")" -OverrideSpatialNetworking="${RUN_WITH_SPATIAL}" popd - # TODO: UNR-3167 - report tests + ./report-tests.sh "${REPORT_OUTPUT_PATH}" MacNoEditor popd diff --git a/ci/setup-build-test-gdk.ps1 b/ci/setup-build-test-gdk.ps1 index ce2073ff23..7b6d5c26fb 100644 --- a/ci/setup-build-test-gdk.ps1 +++ b/ci/setup-build-test-gdk.ps1 @@ -3,7 +3,8 @@ param( [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" + [string] $unreal_engine_symlink_dir = "$build_home\UnrealEngine", + [string] $gyms_version_path = "$gdk_home\UnrealGDKTestGymsVersion.txt" ) class TestProjectTarget { @@ -11,17 +12,29 @@ class TestProjectTarget { [ValidateNotNullOrEmpty()][string]$test_repo_branch [ValidateNotNullOrEmpty()][string]$test_repo_relative_uproject_path [ValidateNotNullOrEmpty()][string]$test_project_name + [ValidateNotNullOrEmpty()][string]$test_gyms_version_path + [ValidateNotNull()][string]$test_env_override - TestProjectTarget([string]$test_repo_url, [string]$gdk_branch, [string]$test_repo_relative_uproject_path, [string]$test_project_name) { + TestProjectTarget([string]$test_repo_url, [string]$gdk_branch, [string]$test_repo_relative_uproject_path, [string]$test_project_name, [string]$test_gyms_version_path, [string]$test_env_override) { $this.test_repo_url = $test_repo_url $this.test_repo_relative_uproject_path = $test_repo_relative_uproject_path $this.test_project_name = $test_project_name + $this.test_gyms_version_path = $test_gyms_version_path + $this.test_env_override = $test_env_override - # If the testing repo has a branch with the same name as the current branch, use that + # Resolve the branch to run against. The order of priority is: + # envvar > same-name branch as the branch we are currently on > UnrealGDKTestGymVersion.txt > "master". $testing_repo_heads = git ls-remote --heads $test_repo_url $gdk_branch - if($testing_repo_heads -Match [Regex]::Escape("refs/heads/$gdk_branch")) { + $test_gym_version = if (Test-Path -Path $test_gyms_version_path) {[System.IO.File]::ReadAllText($test_gyms_version_path)} else {[string]::Empty} + if (Test-Path $test_env_override) { + $this.test_repo_branch = $test_env_override + } + elseif($testing_repo_heads -Match [Regex]::Escape("refs/heads/$gdk_branch")) { $this.test_repo_branch = $gdk_branch } + elseif(Test-Path $test_gym_version) { + $this.test_repo_branch = $test_gym_version + } else { $this.test_repo_branch = "master" } @@ -54,24 +67,17 @@ class TestSuite { [string] $user_cmd_line_args = "$env:TEST_ARGS" [string] $gdk_branch = "$env:BUILDKITE_BRANCH" -[TestProjectTarget] $gdk_test_project = [TestProjectTarget]::new("git@github.com:spatialos/UnrealGDKTestGyms.git", $gdk_branch, "Game\GDKTestGyms.uproject", "GDKTestGyms") -[TestProjectTarget] $native_test_project = [TestProjectTarget]::new("git@github.com:improbable/UnrealGDKEngineNetTest.git", $gdk_branch, "Game\EngineNetTest.uproject", "NativeNetworkTestProject") - -# Allow overriding testing branch via environment variable -if (Test-Path env:TEST_REPO_BRANCH) { - $gdk_test_project.test_repo_branch = $env:TEST_REPO_BRANCH -} -if (Test-Path env:NATIVE_TEST_REPO_BRANCH) { - $native_test_project.test_repo_branch = $env:NATIVE_TEST_REPO_BRANCH -} +[TestProjectTarget] $gdk_test_project = [TestProjectTarget]::new("git@github.com:spatialos/UnrealGDKTestGyms.git", $gdk_branch, "Game\GDKTestGyms.uproject", "GDKTestGyms", $gyms_version_path, $env:TEST_REPO_BRANCH) +[TestProjectTarget] $native_test_project = [TestProjectTarget]::new("git@github.com:improbable/UnrealGDKEngineNetTest.git", $gdk_branch, "Game\EngineNetTest.uproject", "NativeNetworkTestProject", $gyms_version_path, $env:NATIVE_TEST_REPO_BRANCH) $tests = @() if ((Test-Path env:TEST_CONFIG) -And ($env:TEST_CONFIG -eq "Native")) { # We run spatial tests against Vanilla UE4 - $tests += [TestSuite]::new($gdk_test_project, "NetworkingMap", "VanillaTestResults", "/Game/Maps/FunctionalTests/SpatialNetworkingMap", "$user_gdk_settings", $False, "$user_cmd_line_args") + $tests += [TestSuite]::new($gdk_test_project, "NetworkingMap", "VanillaTestResults", "/Game/Maps/FunctionalTests/CI_Fast/", "$user_gdk_settings", $False, "$user_cmd_line_args") if ($env:SLOW_NETWORKING_TESTS -like "true") { + $tests[0].tests_path += "+/Game/Maps/FunctionalTests/CI_Slow/" $tests[0].test_results_dir = "Slow" + $tests[0].test_results_dir # And if slow, we run NetTest functional maps against Vanilla UE4 as well @@ -80,11 +86,11 @@ if ((Test-Path env:TEST_CONFIG) -And ($env:TEST_CONFIG -eq "Native")) { } else { # We run all tests and networked functional maps - $tests += [TestSuite]::new($gdk_test_project, "SpatialNetworkingMap", "TestResults", "SpatialGDK.+/Game/Maps/FunctionalTests/SpatialNetworkingMap+/Game/Maps/FunctionalTests/SpatialZoningMap", "$user_gdk_settings", $True, "$user_cmd_line_args") - + $tests += [TestSuite]::new($gdk_test_project, "SpatialNetworkingMap", "TestResults", "SpatialGDK.+/Game/Maps/FunctionalTests/CI_Fast/+/Game/Maps/FunctionalTests/CI_Fast_Spatial_Only/", "$user_gdk_settings", $True, "$user_cmd_line_args") + if ($env:SLOW_NETWORKING_TESTS -like "true") { # And if slow, we run GDK slow tests - $tests[0].tests_path += "+SpatialGDKSlow." + $tests[0].tests_path += "+SpatialGDKSlow.+/Game/Maps/FunctionalTests/CI_Slow/+/Game/Maps/FunctionalTests/CI_Slow_Spatial_Only/" $tests[0].test_results_dir = "Slow" + $tests[0].test_results_dir # And NetTests functional maps against GDK as well diff --git a/ci/unreal-engine.version b/ci/unreal-engine.version index 37de1a4553..2d415939df 100644 --- a/ci/unreal-engine.version +++ b/ci/unreal-engine.version @@ -1,2 +1,3 @@ -UnrealEngine-12dfe67f9f1bee648e3515b8577892bc0c29f95b -UnrealEngine-0d01cd0d96afb53e7e46bee883e5995e71941555 +ad2f4b8ed2d7c7b72c0a9ca81d74f03bc1fb0b1e +f43c01538b8d7808527327fbcbc70c08dc63fd8c +8c158a3bf4c96d5a867e4886a9afa9656d81da0a