diff --git a/.buildkite/premerge.steps.yaml b/.buildkite/premerge.steps.yaml index e34cc50887..6bb2a0a29d 100755 --- a/.buildkite/premerge.steps.yaml +++ b/.buildkite/premerge.steps.yaml @@ -57,7 +57,7 @@ steps: # Trigger an Example Project build for any merges into master, preview or release branches of UnrealGDK - trigger: "unrealgdkexampleproject-nightly" label: "post-merge-example-project-build" - branches: "master preview release" + if: build.env("ENGINE_NET_TEST") != "true" && (build.branch == "master" || build.branch == "preview" || build.branch == "release") async: true build: env: @@ -78,6 +78,6 @@ steps: <<: *script_runner - label: "slack-notify" - if: (build.env("SLACK_NOTIFY") == "true" || build.branch == "master") && build.env("SLACK_NOTIFY") != "false" + if: (build.env("SLACK_NOTIFY") == "true" || build.branch == "master") && build.env("SLACK_NOTIFY") != "false" && build.env("ENGINE_NET_TEST") != "true" commands: "powershell ./ci/build-and-send-slack-notification.ps1" <<: *windows diff --git a/.buildkite/release.definition.yaml b/.buildkite/release.definition.yaml new file mode 100644 index 0000000000..59478f7bc2 --- /dev/null +++ b/.buildkite/release.definition.yaml @@ -0,0 +1,11 @@ +agent_queue_id: trigger-pipelines +description: Releases all UnrealGDK associated repos to `release` branches. +github: + branch_configuration: [] + default_branch: master + pull_request_branch_filter_configuration: [] +teams: +- name: Everyone + permission: READ_ONLY +- name: gbu/develop/unreal + permission: MANAGE_BUILD_AND_READ diff --git a/.buildkite/release.steps.yaml b/.buildkite/release.steps.yaml new file mode 100644 index 0000000000..9bff8f1779 --- /dev/null +++ b/.buildkite/release.steps.yaml @@ -0,0 +1,90 @@ +--- +common: &common + agents: + - "capable_of_building=gdk-for-unreal" + - "environment=production" + - "permission_set=builder" + - "platform=linux" # if you need a different platform, configure this: macos|linux|windows. + - "queue=${CI_LINUX_BUILDER_QUEUE:-v4-20-04-27-095849-bk10828-316979f1}" + - "scaler_version=2" + - "working_hours_time_zone=london" + + retry: + automatic: + # This is designed to trap and retry failures because agent lost connection. Agent exits with -1 in this case. + - exit_status: -1 + limit: 3 + +steps: + # Stage 0: Tell Buildkite what you want to release. + - block: "Configure your release." + prompt: "Fill out these details for your release." + fields: + - text: "UnrealGDK component release version" + key: "gdk-version" + required: true + hint: "The name of component release version you want to create release candidates for. For example: `0.12.2` or `1.0.1`." + + - text: "UnrealGDK source branch" + key: "gdk-source-branch" + required: true + hint: "The branch you want to create UnrealGDK, UnrealGDKExampleProject, UnrealGDKTestGyms, TestGymBuildKite & UnrealGDKEngineNetTest, release candidates from." + default: "master" + + - text: "UnrealEngine source branches" + key: "engine-source-branches" + required: true + hint: "The Unreal Engine branch (or branches) that you want to create release candidates from. Put each branch on a separate line with the primary Engine version at the top." + default: "4.24-SpatialOSUnrealGDK\n4.23-SpatialOSUnrealGDK" + + # Stage 1: Prepare the release candidates. Prepare steps create a PR and upload metadata but do not release anything. + - label: "Prepare the release" + command: ci/prepare-release.sh + <<: *common # This folds the YAML named anchor into this step. Overrides, if any, should follow, not precede. + retry: + manual: + permit_on_passed: true + concurrency: 1 + concurrency_group: "unrealgdk-release" + + - wait + +# Stage 2: Builds all UnrealEngine release candidates, compresses and uploads Engine artifacts to Google Cloud Storage for use by test pipelines. + - label: "Build & upload all UnrealEngine release candidates" + command: ci/generate-unrealengine-premerge-trigger.sh | tee /dev/fd/2 | buildkite-agent pipeline upload + <<: *common # This folds the YAML named anchor into this step. Overrides, if any, should follow, not precede. + retry: + manual: + permit_on_passed: true + concurrency: 1 + concurrency_group: "unrealgdk-release" + #TODO: This step is actually not strictly necessary. It will be removed as part of: UNR-3662 + skip: true + + # Stage 3: Run all tests against the release candidates. + # Block steps require a human to click a button. This + - block: "Run all tests" + prompt: "This action triggers all tests. Tests depend on the presence of unrealengine-premerge artifacts in Google Cloud Storage. Only click OK if the above unrealengine-premerge build(s) have passed." + + - label: "Trigger all automated tests" + command: ci/generate-release-qa-trigger.sh | tee /dev/fd/2 | buildkite-agent pipeline upload + <<: *common # This folds the YAML named anchor into this step. Overrides, if any, should follow, not precede. + retry: + manual: + permit_on_passed: true + concurrency: 1 + concurrency_group: "unrealgdk-release" + + - wait + + # Stage 4: Promote the release candiates to their respective release branches. + + # Block steps require a human to click a button, this is a safety precaution. + - block: "Unblock the release" + prompt: "This action will merge all release candidates into their respective release branches." + + - label: Release + command: ci/release.sh + concurrency: 1 + concurrency_group: "unrealgdk-release" + <<: *common # This folds the YAML named anchor into this step. Overrides, if any, should follow, not precede. diff --git a/.github/pull-request-template.md b/.github/pull-request-template.md index 556a100010..a86aa8cbb8 100644 --- a/.github/pull-request-template.md +++ b/.github/pull-request-template.md @@ -13,5 +13,12 @@ STRONGLY SUGGESTED: How can this be verified by QA? #### Documentation How is this documented (for example: release note, upgrade guide, feature page, in-code documentation)? +#### Reminders (IMPORTANT) +If your change relies on a breaking engine change: +* Increment `SPATIAL_ENGINE_VERSION` in `Engine\Source\Runtime\Launch\Resources\SpatialVersion.h` (in the engine fork) as well as `SPATIAL_GDK_VERSION` in `SpatialGDK\Source\SpatialGDK\Public\Utils\EngineVersionCheck.h`. This helps others by providing a more helpful message during compilation to make sure the GDK and the Engine are up to date. + +If your change updates `Setup.bat`, `Setup.sh`, core SDK version, any C# tools in `SpatialGDK\Build\Programs\Improbable.Unreal.Scripts`, or hand-written schema in `SpatialGDK\Extras\schema`: +* Increment the number in `RequireSetup`. This will automatically run `Setup.bat` or `Setup.sh` when the GDK is next pulled. + #### Primary reviewers If your change will take a long time to review, you can name at most two primary reviewers who are ultimately responsible for reviewing this request. @ mention them. diff --git a/.gitignore b/.gitignore index d5ae672f0b..ccd739a27d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Visual Studio Code user specific files .vscode/ +# IDEA user files +.idea/ + # Visual Studio 2015 user specific files .vs/ @@ -81,6 +84,9 @@ logs/ Scripts/spatialos.*.build.json +# Docker needs this sln to build the Release Tool +!ci/Tools.sln + !SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Improbable.Unreal.Scripts.sln !SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Mac/Improbable.Unreal.Scripts.sln !SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/packages/Newtonsoft.Json.12.0.2/lib/net45/Newtonsoft.Json.dll @@ -93,3 +99,6 @@ SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.csproj.user _ReSharper.caches **/.DS_Store + +# File to indicate services region, created by Setup script +UseChinaServicesRegion diff --git a/CHANGELOG.md b/CHANGELOG.md index cb5c6a04b5..6c467df762 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,121 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 **Note**: Since GDK for Unreal v0.8.0, the changelog is published in both English and Chinese. The Chinese version of each changelog is shown after its English version.
**注意**:自虚幻引擎开发套件 v0.8.0 版本起,其日志提供中英文两个版本。每个日志的中文版本都置于英文版本之后。 +## [`x.y.z`] - Unreleased + +## [`0.10.0`] - 2020-07-08 + +### New Known Issues: +- Replicated properties that use the `COND_SkipOwner` replication condition can still replicate in the first few frames of an Actor becoming owned. +- Microsoft have fixed a defect in MSVC that previously caused errors when building Unreal Engine. We documented a workaround for this issue in GDK version [`0.7.0-preview`](#070-preview---2019-10-11). If you set up the GDK on your computer between the release of `0.7.0` and `0.10.0`, you have performed this workaround, which is no longer necessary. To undo this workaround, follow these steps: +1. Open Visual Studio Installer. +1. Select **Modify** on your Visual Studio 2019 installation. +1. In the **Installation details** section, clear all the checkboxes for workloads and components except **Visual Studio Code editor**. +1. In the **Workloads** tab, select the following items: + - **Universal Windows Platform development** + - **.NET desktop development** (You must also select the **.NET Framework 4.6.2 development tools**.) + - **Desktop development with C++** +5. Select **Modify** to confirm your changes. + +### Breaking Changes: +- We've deprecated the `preview` branches. We now only release the GDK to the `release` branch, which is fully tested, stable, and documented. If you have the `preview` branches checked out, you must check out `release` to receive the latest changes. +- The SpatialOS Runtime Standard variant requires the latest version of the SpatialOS CLI. Run `spatial update` to get the latest version. +- Old snapshots are incompatible with version 0.10 of the GDK. You must generate new snapshots after upgrading to 0.10. +- The old Inspector is incompatible with the SpatialOS Runtime Standard variant. The Standard variant uses the new Inspector by default. +- We’ve removed `Singleton` as a class specifier, and you need to remove your uses of it. You can achieve the behavior of former Singleton Actors by ensuring that your Actor is spawned once by a single server-worker instance in your deployment. +- We’ve renamed `OnConnected` and `OnConnectionFailed` (on `SpatialGameInstance`) to `OnSpatialConnected` and `OnSpatialConnectionFailed`. They are now also Blueprint-assignable. +- The `GenerateSchema` and `GenerateSchemaAndSnapshots` commandlets do not generate schema any more. We’ve deprecated them in favor of `CookAndGenerateSchemaCommandlet`. (`GenerateSchemaAndSnapshots` still works if you use the `-SkipSchema` option.) +- We’ve combined the settings for offloading and load balancing and moved them from the Editor and Runtime Settings to be per map in the World Settings. For more information, see the [offloading tutorial](https://documentation.improbable.io/gdk-for-unreal/docs/multiserver-offloading-1-set-up). +- We’ve removed the command-line arguments `OverrideSpatialOffloading` and `OverrideLoadBalancer`, and GDK load balancing is always enabled. To override a map's `Enable Multi Worker` setting, use the command-line flag `OverrideMultiWorker`. +- It is now mandatory to run a deployment with result types (previously result types were enabled by default). We’ve removed the Runtime setting `bEnableResultTypes` to reflect this. +- Whether an Actor is offloaded depends on whether the root owner of that Actor is offloaded. This might affect you if you're using functions such as `IsActorGroupOwnerForActor`. +- We’ve removed `QueuedOutgoingRPCWaitTime`. All RPC failure cases are now correctly queued or dropped. +- We’ve removed `Max connection capacity limit` and `Login rate limit` from the generated worker configuration file, because we no longer support them. +- We no longer support secure worker connections when you run your game within the Unreal Editor. We still support secure worker connections for packaged builds. + +### Features: +- The GDK now uses the SpatialOS Worker SDK version [`14.6.1`](https://documentation.improbable.io/sdks-and-data/docs/release-notes#section-14-6-1). +- Added support for the SpatialOS Runtime [Standard variant](https://documentation.improbable.io/gdk-for-unreal/docs/the-spatialos-runtime#section-runtime-variants), version 0.4.3. +- Added support for the SpatialOS Runtime [Compatibility Mode variant](https://documentation.improbable.io/gdk-for-unreal/docs/the-spatialos-runtime#section-runtime-variants), version [`14.5.4`](https://forums.improbable.io/t/spatialos-13-runtime-release-notes-14-5-4/7333). +- Added a new drop-down menu in Editor Settings so that you can select which SpatialOS Runtime variant to use. The two variants are Standard and Compatibility Mode. For Windows users, Standard is the default, but you can use Compatibility Mode if you experience networking issues when you upgrade to the latest GDK version. For macOS users, Compatibility Mode is the default, and you can’t use Standard. For more information, see [Runtime variants](https://documentation.improbable.io/gdk-for-unreal/docs/the-spatialos-runtime#section-runtime-variants). +- Added new default game templates. Your default game template depends on the SpatialOS Runtime variant that you have selected, and on your primary deployment region. +- The SpatialOS Runtime Standard variant uses the new Inspector by default, and is incompatible with the old Inspector. (The Compatibility Mode variant uses the old Inspector by default, and is incompatible with the new Inspector.) +- The Example Project has a new default game mode: Control. This game mode replaces Deathmatch. In Control, two teams compete to capture control points on the map. NPCs guard the control points, and if you capture an NPC’s control point, then the NPC joins your team. +- You can now generate valid schema for classes that start with a leading digit. The generated schema classes are prefixed with `ZZ` internally. +- Handover properties are now automatically replicated when this is required for load balancing. `bEnableHandover` is off by default. +- Added `OnSpatialPlayerSpawnFailed` delegate to `SpatialGameInstance`. This is useful if you have established a successful connection from the client-worker instance to the SpatialOS Runtime, but the server-worker instance crashed. +- Added `bWorkerFlushAfterOutgoingNetworkOp` (default false) which sends RPCs and property replication changes over the network immediately, to allow for lower latencies. You can use this with `bRunSpatialWorkerConnectionOnGameThread` to achieve the lowest available latency at a trade-off with bandwidth. +- You can now edit the project name field in the `Cloud Deployment Configuration` dialog box. Changes that you make here are reflected in your project's `spatialos.json` file. +- You now define worker types in Runtime Settings. +- Local deployments now use the map's load balancing strategy to get the launch configuration settings. The launch configuration file is saved per map in the `Intermediate/Improbable` folder. +- Added a `Launch Configuration Editor` under the Cloud toolbar button. +- In the `Cloud Deployment Configuration` dialog box you can now generate a launch configuration file from the current map, or you can click through to the `Launch Configuration Editor`. +- You can now specify worker load in game logic by using `SpatialMetrics::SetWorkerLoadDelegate`. +- You can now specify deployment tags in the `Cloud Deployment Configuration` dialog box. +- You can now execute RPCs that were declared in a `UInterface`. Previously, this caused a runtime assertion. +- Full Scan schema generation now uses the `CookAndGenerateSchema` commandlet, which results in faster and more stable schema generation for big projects. +- Added an `Open Deployment Page` button to the `Cloud Deployment Configuration` dialog box. +- The `Start Deployment` button in the `Cloud Deployment Configuration` dialog box now generates schema and a snapshot, builds all selected workers, and uploads the assembly before starting the deployment. There are checkboxes so that you can choose whether to generate schema and a snapshot, and whether to build the game client and add simulated players. +- When you start a cloud deployment from the Unreal Editor, the cloud deployment now automatically has the dev_login deployment tag. +- Several command-line parameter changes: + - Renamed the `enableProtocolLogging` command-line parameter to `enableWorkerSDKProtocolLogging`. + - Added a parameter named enableWorkerSDKOpLogging so that you can log user-level ops. + - Renamed the `protocolLoggingPrefix` parameter to workerSDKLogPrefix. This prefix is used for both protocol logging and op logging. + - Added a parameter named `workerSDKLogLevel` that takes the arguments `debug`, `info`, `warning`, and `error`. + - Added a parameter named `workerSDKLogFileSize` to control the maximum file size of the Worker SDK log file. +- The icon on the `Start Deployment` toolbar button now changes depending on the connection flow that you select. +- Created a new drop-down menu in the GDK toolbar. You can use it to configure how to connect your PIE client or your Launch on Device client: + - Choose between `Connect to a local deployment` and `Connect to a cloud deployment` to specify the flow that the client should automatically use when you select `Play` or `Launch`. + - Added the `Local Deployment IP` field to specify which local deployment the client should connect to. By default, the IP is `127.0.0.1`. + - Added the `Cloud deployment name` field to specify which cloud deployment the client should connect to. If you select Connect to cloud deployment but you don’t specify a cloud deployment, the client tries to connect to the first running deployment that has the `dev_login` deployment tag. + - Added the `Editor Settings` field so that you can quickly access the SpatialOS Editor Settings. +- Added the `Build Client Worker` and `Build Simulated Player` checkboxes to the `Connection` drop-down menu, so that you can quickly choose whether to build and include the client-worker instance and simulated player worker instance in the assembly. +- Updated the GDK toolbar icons. +- When you specify a URL to connect a client to a deployment using the Receptionist, the URL port option is now respected. - --- However, in certain circumstances, the initial connection attempt uses the `-receptionistPort` command-line argument. +- When you run `BuildWorker.bat` with `client`, this now builds the client target of your project. +- When you change the project name in the `Cloud Deployment Configuration` dialog box, this automatically regenerates the development authentication token. +- Changed the names of the following toolbar buttons: + - `Start` is now called `Start Deployment` + - `Deploy` is now called `Cloud` +- Marked all the required fields in the `Cloud Deployment Configuration` dialog box with asterisks. +- You can now change the project name in Editor Settings. +- Replaced the `Generate from current map` button in the `Cloud Deployment Configuration` dialog box with a checkbox labelled `Automatically Generate Launch Configuration`. If you select this checkbox, the GDK generates an up-to-date launch configuration file from the current map when you select `Start Deployment`. +- Android and iOS are now in preview. We support workflows for developing and doing in-studio playtests on Android and iOS devices, and have documentation for these workflows. We also support macOS (also in preview) for developing and testing iOS game clients. + +## Bug fixes: +- Fixed a problem that caused load balanced cloud deployments to fail to start while under heavy load. +- Fix to avoid using packages that are still being processed in the asynchronous loading thread. +- Fixed a bug that sometimes caused GDK setup scripts to fail to unzip dependencies. +- Fixed a bug where RPCs that were called before calling the `CreateEntityRequest` were not processed as early as possible in the RPC ring buffer system, resulting in startup delays on the client. +- Fixed a crash that occurred when running a game with `nullrhi` and using `SpatialDebugger`. +- When you use a URL with options in the command line, we now parse the Receptionist parameters correctly, using the URL if necessary. +- Fixed a bug that occurred when creating multiple dynamic subobjects at the same time, and caused them to fail to be created on clients. +- `OwnerOnly` components are now properly replicated when a worker instance gains authority over an Actor. Previously, they were sometimes only replicated when a value on them changed (after the worker instance had already gained authority). +- Fixed a rare server crash that could occur when closing an Actor channel immediately after attaching a dynamic subobject to that Actor. +- Fixed a defect in `InstallGDK.bat` that sometimes caused it to incorrectly report `Error: Could not clone…` when repositories were cloned correctly. +- Actors from the same ownership hierarchy are now handled together when they are load balanced. + +## SpatialOS tooling compatibility: +If you are using the Standard Runtime variant, note the following compatibility issues: +- The [old Inspector](https://documentation.improbable.io/spatialos-tools/docs/the-inspector) won’t work. You must use the [new Inspector](https://documentation.improbable.io/spatialos-tools/docs/the-new-inspector) instead. +- In the [Platform SDK in C#](https://documentation.improbable.io/sdks-and-data/docs/platform-csharp-introduction), you can’t set [capacity limits](https://documentation.improbable.io/sdks-and-data/docs/platform-csharp-capacity-limiting) or use the [remote interaction service](https://documentation.improbable.io/sdks-and-data/docs/platform-csharp-remote-interactions). You also can’t use the Platform SDK to take snapshots of cloud deployments, but we’ll fix this snapshot issue in a future release. +- You can't generate a snapshot of a cloud deployment. We'll fix this in a future release. +- In the [CLI](https://documentation.improbable.io/spatialos-tools/docs/cli-introduction), the following commands don’t work: + - `spatial local worker replace` + - `spatial project deployment worker replace` + - `spatial local worker-flag set` + - `spatial project deployment worker-flag delete` + - `spatial project deployment worker-flag set` + - `spatial cloud runtime flags set` (We’ll improve debug tooling and add functionality to [dynamically change worker flag values](https://documentation.improbable.io/gdk-for-unreal/docs/worker-flags#section-change-worker-flag-values-while-the-deployment-is-running) in future releases.) + +If you need any of the functionality mentioned above, [change your Runtime variant to Compatibility Mode](https://documentation.improbable.io/gdk-for-unreal/docs/the-spatialos-runtime#section-change-your-runtime-variant). + +### Internal: +Features listed in this section are not ready to use. However, in the spirit of open development, we record every change that we make to the GDK. + +- The SpatialOS GDK for Unreal is now released automatically using Buildkite CI. This should result in more frequent releases. +- Improbable now measures the non-functional characteristics of the GDK in Buildkite CI. This enables us to reason about and improve these characteristics. We track them as non-functional requirements (NFRs). + ## [`0.9.0`] - 2020-05-05 ### New Known Issues: @@ -64,6 +179,7 @@ Usage: `DeploymentLauncher createsim **Editor Settings** > **Region Settings** has been moved to **SpatialOS GDK for Unreal** > **Runtime Settings** > **Region Settings**. - You can now choose which SpatialOS service region you want to use by adjusting the **Region where services are located** setting. You must use the service region that you're geographically located in. diff --git a/RequireSetup b/RequireSetup index 77d0f13ca4..51ca8c42ac 100644 --- a/RequireSetup +++ b/RequireSetup @@ -1,4 +1,4 @@ Increment the below number whenever it is required to run Setup.bat as part of a new commit. Our git hooks will detect this file has been updated and automatically run Setup.bat on pull. -53 +60 diff --git a/Setup.bat b/Setup.bat index 35a9fc4200..869d3dbfd7 100644 --- a/Setup.bat +++ b/Setup.bat @@ -23,7 +23,7 @@ call :MarkStartOfBlock "Setup the git hooks" echo check_run() {>>.git\hooks\post-merge echo echo "$changed_files" ^| grep --quiet "$1" ^&^& exec $2>>.git\hooks\post-merge echo }>>.git\hooks\post-merge - echo check_run RequireSetup "cmd.exe /c Setup.bat">>.git\hooks\post-merge + echo check_run RequireSetup "cmd.exe /c Setup.bat %*">>.git\hooks\post-merge :SkipGitHooks call :MarkEndOfBlock "Setup the git hooks" @@ -67,6 +67,20 @@ call :MarkStartOfBlock "Setup variables" ) call :MarkEndOfBlock "Setup variables" +call :MarkStartOfBlock "Setup services region" + set USE_CHINA_SERVICES_REGION= + for %%A in (%*) do ( + if "%%A"=="--china" set USE_CHINA_SERVICES_REGION=True + ) + + rem Create or remove an empty file in the plugin directory indicating whether to use China services region. + if defined USE_CHINA_SERVICES_REGION ( + echo. 2> UseChinaServicesRegion + ) else ( + if exist UseChinaServicesRegion del UseChinaServicesRegion + ) +call :MarkEndOfBlock "Setup services region" + call :MarkStartOfBlock "Clean folders" rd /s /q "%CORE_SDK_DIR%" 2>nul rd /s /q "%WORKER_SDK_DIR%" 2>nul @@ -103,14 +117,17 @@ call :MarkStartOfBlock "Retrieve dependencies" spatial package retrieve worker_sdk c-dynamic-x86_64-gcc510-linux %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-gcc510-linux.zip" if defined DOWNLOAD_MOBILE ( spatial package retrieve worker_sdk c-static-fullylinked-arm-clang-ios %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-static-fullylinked-arm-clang-ios.zip" - spatial package retrieve worker_sdk c-dynamic-arm64v8a-clang_ndk16b-android %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-arm64v8a-clang_ndk16b-android.zip" - spatial package retrieve worker_sdk c-dynamic-armv7a-clang_ndk16b-android %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-armv7a-clang_ndk16b-android.zip" - spatial package retrieve worker_sdk c-dynamic-x86_64-clang_ndk16b-android %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-clang_ndk16b-android.zip" + spatial package retrieve worker_sdk c-dynamic-arm64v8a-clang_ndk21-android %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-arm64v8a-clang_clang_ndk21-android.zip" + spatial package retrieve worker_sdk c-dynamic-armv7a-clang_ndk21-android %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-armv7a-clang_clang_ndk21-android.zip" + spatial package retrieve worker_sdk c-dynamic-x86_64-clang_ndk21-android %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-clang_clang_ndk21-android.zip" ) spatial package retrieve worker_sdk csharp %PINNED_CORE_SDK_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%CORE_SDK_DIR%\worker_sdk\csharp.zip" spatial package retrieve spot spot-win64 %PINNED_SPOT_VERSION% %DOMAIN_ENVIRONMENT_VAR% "%BINARIES_DIR%\Programs\spot.exe" call :MarkEndOfBlock "Retrieve dependencies" +REM There is a race condition between retrieve and unzip, add version call to stall briefly +call spatial version + call :MarkStartOfBlock "Unpack dependencies" powershell -Command "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c_headers.zip\" -DestinationPath \"%BINARIES_DIR%\Headers\" -Force; "^ "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86-vc141_md-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Win32\" -Force; "^ @@ -122,9 +139,9 @@ call :MarkStartOfBlock "Unpack dependencies" if defined DOWNLOAD_MOBILE ( powershell -Command "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-static-fullylinked-arm-clang-ios.zip\" -DestinationPath \"%BINARIES_DIR%\IOS\" -Force;"^ - "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-arm64v8a-clang_ndk16b-android.zip\" -DestinationPath \"%BINARIES_DIR%\Android\arm64-v8a\" -Force; "^ - "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-armv7a-clang_ndk16b-android.zip\" -DestinationPath \"%BINARIES_DIR%\Android\armeabi-v7a\" -Force; "^ - "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-clang_ndk16b-android.zip\" -DestinationPath \"%BINARIES_DIR%\Android\x86_64\" -Force; "^ + "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-arm64v8a-clang_clang_ndk21-android.zip\" -DestinationPath \"%BINARIES_DIR%\Android\arm64-v8a\" -Force; "^ + "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-armv7a-clang_clang_ndk21-android.zip\" -DestinationPath \"%BINARIES_DIR%\Android\armeabi-v7a\" -Force; "^ + "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-clang_clang_ndk21-android.zip\" -DestinationPath \"%BINARIES_DIR%\Android\x86_64\" -Force; "^ ) xcopy /s /i /q "%BINARIES_DIR%\Headers\include" "%WORKER_SDK_DIR%" call :MarkEndOfBlock "Unpack dependencies" diff --git a/Setup.sh b/Setup.sh index 633b3a427e..f893ad66a4 100755 --- a/Setup.sh +++ b/Setup.sh @@ -20,11 +20,14 @@ SCHEMA_COPY_DIR="$(pwd)/../../../spatial/schema/unreal/gdk" SCHEMA_STD_COPY_DIR="$(pwd)/../../../spatial/build/dependencies/schema/standard_library" SPATIAL_DIR="$(pwd)/../../../spatial" DOWNLOAD_MOBILE= +USE_CHINA_SERVICES_REGION= while test $# -gt 0 do case "$1" in - --china) DOMAIN_ENVIRONMENT_VAR="--environment cn-production" + --china) + DOMAIN_ENVIRONMENT_VAR="--environment cn-production" + USE_CHINA_SERVICES_REGION=true ;; --mobile) DOWNLOAD_MOBILE=true ;; @@ -48,6 +51,13 @@ if [[ -e .git/hooks ]]; then cp "$(pwd)/SpatialGDK/Extras/git/post-merge" "$(pwd)/.git/hooks" fi +# Create or remove an empty file in the plugin directory indicating whether to use China services region. +if [[ -n "${USE_CHINA_SERVICES_REGION}" ]]; then + touch UseChinaServicesRegion +else + rm -f UseChinaServicesRegion +fi + echo "Clean folders" rm -rf "${CORE_SDK_DIR}" rm -rf "${WORKER_SDK_DIR}" @@ -80,9 +90,9 @@ spatial package retrieve worker_sdk c-dynamic-x86_64-clang-macos "${ if [[ -n "${DOWNLOAD_MOBILE}" ]]; then spatial package retrieve worker_sdk c-static-fullylinked-arm-clang-ios "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-static-fullylinked-arm-clang-ios.zip - spatial package retrieve worker_sdk c-dynamic-arm64v8a-clang_ndk16b-android "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-arm64v8a-clang_ndk16b-android.zip - spatial package retrieve worker_sdk c-dynamic-armv7a-clang_ndk16b-android "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-armv7a-clang_ndk16b-android.zip - spatial package retrieve worker_sdk c-dynamic-x86_64-clang_ndk16b-android "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang_ndk16b-android.zip + spatial package retrieve worker_sdk c-dynamic-arm64v8a-clang_ndk21-android "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-arm64v8a-clang_ndk21-android.zip + spatial package retrieve worker_sdk c-dynamic-armv7a-clang_ndk21-android "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-armv7a-clang_ndk21-android.zip + spatial package retrieve worker_sdk c-dynamic-x86_64-clang_ndk21-android "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang_ndk21-android.zip fi spatial package retrieve worker_sdk csharp "${PINNED_CORE_SDK_VERSION}" ${DOMAIN_ENVIRONMENT_VAR:-} "${CORE_SDK_DIR}"/worker_sdk/csharp.zip @@ -98,9 +108,9 @@ unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang-macos.zip if [[ -n "${DOWNLOAD_MOBILE}" ]]; then unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-static-fullylinked-arm-clang-ios.zip -d "${BINARIES_DIR}"/IOS/ - unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-arm64v8a-clang_ndk16b-android.zip -d "${BINARIES_DIR}"/Android/arm64-v8a/ - unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-armv7a-clang_ndk16b-android.zip -d "${BINARIES_DIR}"/Android/armeabi-v7a/ - unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang_ndk16b-android.zip -d "${BINARIES_DIR}"/Android/x86_64/ + unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-arm64v8a-clang_ndk21-android.zip -d "${BINARIES_DIR}"/Android/arm64-v8a/ + unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-armv7a-clang_ndk21-android.zip -d "${BINARIES_DIR}"/Android/armeabi-v7a/ + unzip -oq "${CORE_SDK_DIR}"/worker_sdk/c-dynamic-x86_64-clang_ndk21-android.zip -d "${BINARIES_DIR}"/Android/x86_64/ fi unzip -oq "${CORE_SDK_DIR}"/worker_sdk/csharp.zip -d "${BINARIES_DIR}"/Programs/worker_sdk/csharp/ diff --git a/SetupIncTraceLibs.bat b/SetupIncTraceLibs.bat index b13e731b88..b7d1eb65b4 100644 --- a/SetupIncTraceLibs.bat +++ b/SetupIncTraceLibs.bat @@ -22,6 +22,9 @@ call :MarkStartOfBlock "Retrieve dependencies" spatial package retrieve internal trace-dynamic-x86_64-gcc510-linux 14.3.0-b2647-85717ee-WORKER-SNAPSHOT "%CORE_SDK_DIR%\trace_lib\trace-linux.zip" call :MarkEndOfBlock "Retrieve dependencies" +REM There is a race condition between retrieve and unzip, add version call to stall briefly +call spatial version + call :MarkStartOfBlock "Unpack dependencies" powershell -Command "Expand-Archive -Path \"%CORE_SDK_DIR%\trace_lib\trace-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Win64\" -Force;"^ "Expand-Archive -Path \"%CORE_SDK_DIR%\trace_lib\trace-linux.zip\" -DestinationPath \"%BINARIES_DIR%\Linux\" -Force;" diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs index 69f0e1b51e..b5c6492620 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs @@ -178,31 +178,9 @@ public static void Main(string[] args) var windowsNoEditorPath = Path.Combine(stagingDir, "WindowsNoEditor"); - if (additionalUATArgs.Contains("-pak")) - { - Console.WriteLine("Cannot force bSpatialNetworking with -pak argument."); - } - else - { - ForceSpatialNetworkingInConfig(windowsNoEditorPath, baseGameName); - } + ForceSpatialNetworkingUnlessPakSpecified(additionalUATArgs, windowsNoEditorPath, baseGameName); - // Add a _ to the start of the exe name, to ensure it is the exe selected by the launcher. - // TO-DO: Remove this once LAUNCH-341 has been completed, and the _ is no longer necessary. - var oldExe = Path.Combine(windowsNoEditorPath, $"{gameName}.exe"); - var renamedExe = Path.Combine(windowsNoEditorPath, $"_{gameName}.exe"); - if (File.Exists(renamedExe)) - { - File.Delete(renamedExe); - } - if (File.Exists(oldExe)) - { - File.Move(oldExe, renamedExe); - } - else - { - Console.WriteLine("Could not find the executable to rename."); - } + RenameExeForLauncher(windowsNoEditorPath, baseGameName); Common.RunRedirected(runUATBat, new[] { @@ -244,14 +222,7 @@ public static void Main(string[] args) var linuxSimulatedPlayerPath = Path.Combine(stagingDir, "LinuxNoEditor"); - if (additionalUATArgs.Contains("-pak")) - { - Console.WriteLine("Cannot force bSpatialNetworking with -pak argument."); - } - else - { - ForceSpatialNetworkingInConfig(linuxSimulatedPlayerPath, baseGameName); - } + ForceSpatialNetworkingUnlessPakSpecified(additionalUATArgs, linuxSimulatedPlayerPath, baseGameName); LinuxScripts.WriteWithLinuxLineEndings(LinuxScripts.GetSimulatedPlayerWorkerShellScript(baseGameName), Path.Combine(linuxSimulatedPlayerPath, "StartSimulatedClient.sh")); LinuxScripts.WriteWithLinuxLineEndings(LinuxScripts.GetSimulatedPlayerCoordinatorShellScript(baseGameName), Path.Combine(linuxSimulatedPlayerPath, "StartCoordinator.sh")); @@ -318,14 +289,7 @@ public static void Main(string[] args) var assemblyPlatform = isLinux ? "Linux" : "Windows"; var serverPath = Path.Combine(stagingDir, assemblyPlatform + "Server"); - if (additionalUATArgs.Contains("-pak")) - { - Console.WriteLine("Cannot force bSpatialNetworking with -pak argument."); - } - else - { - ForceSpatialNetworkingInConfig(serverPath, baseGameName); - } + ForceSpatialNetworkingUnlessPakSpecified(additionalUATArgs, serverPath, baseGameName); if (isLinux) { @@ -341,6 +305,51 @@ public static void Main(string[] args) "-archive=" + Quote(Path.Combine(outputDir, $"UnrealWorker@{assemblyPlatform}.zip")) }); } + else if (gameName == baseGameName + "Client") + { + Common.WriteHeading(" > Building client."); + Common.RunRedirected(runUATBat, new[] + { + "BuildCookRun", + noCompile ? "-nobuild" : "-build", + noCompile ? "-nocompile" : "-compile", + "-project=" + Quote(projectFile), + "-noP4", + "-clientconfig=" + configuration, + "-serverconfig=" + configuration, + "-utf8output", + "-cook", + "-stage", + "-package", + "-unversioned", + "-compressed", + "-stagingdirectory=" + Quote(stagingDir), + "-stdout", + "-FORCELOGFLUSH", + "-CrashForUAT", + "-unattended", + "-fileopenlog", + "-SkipCookingEditorContent", + "-client", + "-noserver", + "-platform=" + platform, + "-targetplatform=" + platform, + additionalUATArgs + }); + + var windowsClientPath = Path.Combine(stagingDir, "WindowsClient"); + + ForceSpatialNetworkingUnlessPakSpecified(additionalUATArgs, windowsClientPath, baseGameName); + + RenameExeForLauncher(windowsClientPath, baseGameName + "Client"); + + Common.RunRedirected(runUATBat, new[] + { + "ZipUtils", + "-add=" + Quote(windowsClientPath), + "-archive=" + Quote(Path.Combine(outputDir, "UnrealClient@Windows.zip")), + }); + } else { // Pass-through to Unreal's Build.bat. @@ -360,6 +369,38 @@ private static string Quote(string toQuote) return $"\"{toQuote}\""; } + private static void RenameExeForLauncher(string workerPath, string gameName) + { + // Add a _ to the start of the exe name, to ensure it is the exe selected by the launcher. + // TO-DO: Remove this once LAUNCH-341 has been completed, and the _ is no longer necessary. + var oldExe = Path.Combine(workerPath, $"{gameName}.exe"); + var renamedExe = Path.Combine(workerPath, $"_{gameName}.exe"); + if (File.Exists(renamedExe)) + { + File.Delete(renamedExe); + } + if (File.Exists(oldExe)) + { + File.Move(oldExe, renamedExe); + } + else + { + Console.WriteLine("Could not find the executable to rename."); + } + } + + private static void ForceSpatialNetworkingUnlessPakSpecified(string additionalUATArgs, string workerPath, string gameName) + { + if (additionalUATArgs.Contains("-pak")) + { + Console.WriteLine("Cannot force bSpatialNetworking with -pak argument."); + } + else + { + ForceSpatialNetworkingInConfig(workerPath, gameName); + } + } + private static void ForceSpatialNetworkingInConfig(string workerPath, string gameName) { var defaultGameIniPath = Path.Combine(workerPath, gameName, "Config", "DefaultGame.ini"); diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/LinuxScripts.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/LinuxScripts.cs index f5c6f7a12d..c64ef77ac8 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/LinuxScripts.cs +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/LinuxScripts.cs @@ -43,14 +43,9 @@ exit 1 WORKER_ID=$1 shift 1 -# 2>/dev/null silences errors by redirecting stderr to the null device. This is done to prevent errors when a machine attempts to add the same user more than once. -useradd $NEW_USER -m -d /improbable/logs/ >> ""/improbable/logs/${{WORKER_ID}}.log"" 2>&1 -chown -R $NEW_USER:$NEW_USER $(pwd) >> ""/improbable/logs/${{WORKER_ID}}.log"" 2>&1 -chmod -R o+rw /improbable/logs >> ""/improbable/logs/${{WORKER_ID}}.log"" 2>&1 SCRIPT=""$(pwd)/{0}.sh"" -chmod +x $SCRIPT >> ""/improbable/logs/${{WORKER_ID}}.log"" 2>&1 -echo ""Trying to launch worker {0} with id ${{WORKER_ID}}"" > ""/improbable/logs/${{WORKER_ID}}.log"" +echo ""Trying to launch worker {0} with id ${{WORKER_ID}}"" >> ""/improbable/logs/${{WORKER_ID}}.log"" gosu $NEW_USER ""${{SCRIPT}}"" ""$@"" >> ""/improbable/logs/${{WORKER_ID}}.log"" 2>&1"; public const string SimulatedPlayerCoordinatorShellScript = @@ -68,6 +63,11 @@ sleep 5 chmod +x StartSimulatedClient.sh chmod +x {0}.sh +NEW_USER=unrealworker +useradd $NEW_USER -m -d /improbable/logs/ 2> /improbable/logs/CoordinatorErrors.log +chown -R $NEW_USER:$NEW_USER $(pwd) 2> /improbable/logs/CoordinatorErrors.log +chmod -R o+rw /improbable/logs 2> /improbable/logs/CoordinatorErrors.log + mono WorkerCoordinator.exe $@ 2> /improbable/logs/CoordinatorErrors.log"; // Returns a version of UnrealWorkerShellScript with baseGameName templated into the right places. diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs index d1fb157a51..dafdb7530f 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs @@ -33,7 +33,7 @@ internal class DeploymentLauncher "https://auth.spatialoschina.com/auth/v1/token"); private static string UploadSnapshot(SnapshotServiceClient client, string snapshotPath, string projectName, - string deploymentName, string region) + string deploymentName, bool useChinaPlatform) { Console.WriteLine($"Uploading {snapshotPath} to project {projectName}"); @@ -69,7 +69,7 @@ private static string UploadSnapshot(SnapshotServiceClient client, string snapsh httpRequest.ContentLength = snapshotToUpload.Size; httpRequest.Headers.Add("Content-MD5", snapshotToUpload.Checksum); - if (region == "CN") + if (useChinaPlatform) { httpRequest.Headers.Add("x-amz-server-side-encryption", "AES256"); } @@ -92,24 +92,24 @@ private static string UploadSnapshot(SnapshotServiceClient client, string snapsh return confirmUploadResponse.Snapshot.Id; } - - private static PlatformApiEndpoint GetApiEndpoint(string region) + + private static PlatformApiEndpoint GetApiEndpoint(bool useChinaPlatform) { - if (region == "CN") + if (useChinaPlatform) { return new PlatformApiEndpoint(CHINA_ENDPOINT_URL, CHINA_ENDPOINT_PORT); } return null; // Use default } - private static PlatformRefreshTokenCredential GetPlatformRefreshTokenCredential(string region) + private static PlatformRefreshTokenCredential GetPlatformRefreshTokenCredential(bool useChinaPlatform) { - return region == "CN" ? ChinaCredentials : null; + return useChinaPlatform ? ChinaCredentials : null; } - private static int CreateDeployment(string[] args) + private static int CreateDeployment(string[] args, bool useChinaPlatform) { - bool launchSimPlayerDeployment = args.Length == 12; + bool launchSimPlayerDeployment = args.Length == 15; var projectName = args[1]; var assemblyName = args[2]; @@ -118,19 +118,23 @@ private static int CreateDeployment(string[] args) var mainDeploymentJsonPath = args[5]; var mainDeploymentSnapshotPath = args[6]; var mainDeploymentRegion = args[7]; + var mainDeploymentCluster = args[8]; + var mainDeploymentTags = args[9]; var simDeploymentName = string.Empty; var simDeploymentJson = string.Empty; var simDeploymentRegion = string.Empty; + var simDeploymentCluster = string.Empty; var simNumPlayers = 0; if (launchSimPlayerDeployment) { - simDeploymentName = args[8]; - simDeploymentJson = args[9]; - simDeploymentRegion = args[10]; + simDeploymentName = args[10]; + simDeploymentJson = args[11]; + simDeploymentRegion = args[12]; + simDeploymentCluster = args[13]; - if (!Int32.TryParse(args[11], out simNumPlayers)) + if (!Int32.TryParse(args[14], out simNumPlayers)) { Console.WriteLine("Cannot parse the number of simulated players to connect."); return 1; @@ -139,83 +143,71 @@ private static int CreateDeployment(string[] args) try { - var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(mainDeploymentRegion), GetPlatformRefreshTokenCredential(mainDeploymentRegion)); + var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(useChinaPlatform), GetPlatformRefreshTokenCredential(useChinaPlatform)); if (DeploymentExists(deploymentServiceClient, projectName, mainDeploymentName)) { StopDeploymentByName(deploymentServiceClient, projectName, mainDeploymentName); } - var createMainDeploymentOp = CreateMainDeploymentAsync(deploymentServiceClient, launchSimPlayerDeployment, projectName, assemblyName, runtimeVersion, mainDeploymentName, mainDeploymentJsonPath, mainDeploymentSnapshotPath, mainDeploymentRegion); + var createMainDeploymentOp = CreateMainDeploymentAsync(deploymentServiceClient, + launchSimPlayerDeployment, projectName, assemblyName, runtimeVersion, + mainDeploymentName, mainDeploymentJsonPath, mainDeploymentSnapshotPath, + mainDeploymentRegion, mainDeploymentCluster, mainDeploymentTags, useChinaPlatform); - if (!launchSimPlayerDeployment) - { - // Don't launch a simulated player deployment. Wait for main deployment to be created and then return. - Console.WriteLine("Waiting for deployment to be ready..."); - var result = createMainDeploymentOp.PollUntilCompleted().GetResultOrNull(); - if (result == null) - { - Console.WriteLine("Failed to create the main deployment"); - return 1; - } - - Console.WriteLine("Successfully created the main deployment"); - return 0; - } - - if (DeploymentExists(deploymentServiceClient, projectName, simDeploymentName)) + if (launchSimPlayerDeployment && DeploymentExists(deploymentServiceClient, projectName, simDeploymentName)) { StopDeploymentByName(deploymentServiceClient, projectName, simDeploymentName); } - var createSimDeploymentOp = CreateSimPlayerDeploymentAsync(deploymentServiceClient, projectName, assemblyName, runtimeVersion, mainDeploymentName, simDeploymentName, simDeploymentJson, simDeploymentRegion, simNumPlayers); - - // Wait for both deployments to be created. - Console.WriteLine("Waiting for deployments to be ready..."); - var mainDeploymentResult = createMainDeploymentOp.PollUntilCompleted().GetResultOrNull(); - if (mainDeploymentResult == null) + // TODO: UNR-3550 - Re-add dynamic worker flags when supported with new runtime. + // We need to wait for the main deployment to be finished starting before we can launch the sim player deployment. + Console.WriteLine("Waiting for deployment to be ready..."); + var result = createMainDeploymentOp.PollUntilCompleted().GetResultOrNull(); + if (result == null) { Console.WriteLine("Failed to create the main deployment"); return 1; } Console.WriteLine("Successfully created the main deployment"); - var simPlayerDeployment = createSimDeploymentOp.PollUntilCompleted().GetResultOrNull(); - if (simPlayerDeployment == null) + + if (launchSimPlayerDeployment) { - Console.WriteLine("Failed to create the simulated player deployment"); - return 1; - } + // we are using the main deployment snapshot also for the sim player deployment, because we only need to specify a snapshot + // to be able to start the deployment. The sim players don't care about the actual snapshot. + var createSimDeploymentOp = CreateSimPlayerDeploymentAsync(deploymentServiceClient, + projectName, assemblyName, runtimeVersion, mainDeploymentName, simDeploymentName, + simDeploymentJson, mainDeploymentSnapshotPath, simDeploymentRegion, simDeploymentCluster, + simNumPlayers, useChinaPlatform); - Console.WriteLine("Successfully created the simulated player deployment"); + // Wait for both deployments to be created. + Console.WriteLine("Waiting for simulated player deployment to be ready..."); - // Update coordinator worker flag for simulated player deployment to notify target deployment is ready. - simPlayerDeployment.WorkerFlags.Add(new WorkerFlag - { - Key = "target_deployment_ready", - Value = "true", - WorkerType = CoordinatorWorkerName - }); - deploymentServiceClient.UpdateDeployment(new UpdateDeploymentRequest { Deployment = simPlayerDeployment }); + var simPlayerDeployment = createSimDeploymentOp.PollUntilCompleted().GetResultOrNull(); + if (simPlayerDeployment == null) + { + Console.WriteLine("Failed to create the simulated player deployment"); + return 1; + } - Console.WriteLine("Done! Simulated players will start to connect to your deployment"); + Console.WriteLine("Done! Simulated players will start to connect to your deployment"); + } } catch (Grpc.Core.RpcException e) { if (e.Status.StatusCode == Grpc.Core.StatusCode.NotFound) { - Console.WriteLine( - $"Unable to launch the deployment(s). This is likely because the project '{projectName}' or assembly '{assemblyName}' doesn't exist."); + Console.WriteLine($"Unable to launch the deployment(s). This is likely because the project '{projectName}' or assembly '{assemblyName}' doesn't exist."); + Console.WriteLine($"Detail: '{e.Status.Detail}'"); } else if (e.Status.StatusCode == Grpc.Core.StatusCode.ResourceExhausted) { - Console.WriteLine( - $"Unable to launch the deployment(s). Cloud cluster resources exhausted, Detail: '{e.Status.Detail}'" ); + Console.WriteLine($"Unable to launch the deployment(s). Cloud cluster resources exhausted, Detail: '{e.Status.Detail}'" ); } else { - Console.WriteLine( - $"Unable to launch the deployment(s). Detail: '{e.Status.Detail}'"); + Console.WriteLine($"Unable to launch the deployment(s). Detail: '{e.Status.Detail}'"); } return 1; @@ -224,7 +216,7 @@ private static int CreateDeployment(string[] args) return 0; } - private static int CreateSimDeployments(string[] args) + private static int CreateSimDeployments(string[] args, bool useChinaPlatform) { var projectName = args[1]; var assemblyName = args[2]; @@ -233,31 +225,28 @@ private static int CreateSimDeployments(string[] args) var simDeploymentName = args[5]; var simDeploymentJson = args[6]; var simDeploymentRegion = args[7]; + var simDeploymentCluster = args[8]; + var simDeploymentSnapshotPath = args[9]; var simNumPlayers = 0; - if (!Int32.TryParse(args[8], out simNumPlayers)) + if (!Int32.TryParse(args[10], out simNumPlayers)) { Console.WriteLine("Cannot parse the number of simulated players to connect."); return 1; } - var autoConnect = false; - if (!Boolean.TryParse(args[9], out autoConnect)) - { - Console.WriteLine("Cannot parse the auto-connect flag."); - return 1; - } - try { - var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(simDeploymentRegion)); + var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(useChinaPlatform)); if (DeploymentExists(deploymentServiceClient, projectName, simDeploymentName)) { StopDeploymentByName(deploymentServiceClient, projectName, simDeploymentName); } - var createSimDeploymentOp = CreateSimPlayerDeploymentAsync(deploymentServiceClient, projectName, assemblyName, runtimeVersion, targetDeploymentName, simDeploymentName, simDeploymentJson, simDeploymentRegion, simNumPlayers); + var createSimDeploymentOp = CreateSimPlayerDeploymentAsync(deploymentServiceClient, + projectName, assemblyName, runtimeVersion, targetDeploymentName, simDeploymentName, + simDeploymentJson, simDeploymentSnapshotPath, simDeploymentRegion, simDeploymentCluster, simNumPlayers, useChinaPlatform); // Wait for both deployments to be created. Console.WriteLine("Waiting for the simulated player deployment to be ready..."); @@ -268,17 +257,6 @@ private static int CreateSimDeployments(string[] args) return 1; } - Console.WriteLine("Successfully created the simulated player deployment"); - - // Update coordinator worker flag for simulated player deployment to notify target deployment is ready. - simPlayerDeployment.WorkerFlags.Add(new WorkerFlag - { - Key = "target_deployment_ready", - Value = autoConnect.ToString(), - WorkerType = CoordinatorWorkerName - }); - deploymentServiceClient.UpdateDeployment(new UpdateDeploymentRequest { Deployment = simPlayerDeployment }); - Console.WriteLine("Done! Simulated players will start to connect to your deployment"); } catch (Grpc.Core.RpcException e) @@ -329,13 +307,15 @@ private static void StopDeploymentByName(DeploymentServiceClient deploymentServi } private static Operation CreateMainDeploymentAsync(DeploymentServiceClient deploymentServiceClient, - bool launchSimPlayerDeployment, string projectName, string assemblyName, string runtimeVersion, string mainDeploymentName, string mainDeploymentJsonPath, string mainDeploymentSnapshotPath, string regionCode) + bool launchSimPlayerDeployment, string projectName, string assemblyName, string runtimeVersion, + string mainDeploymentName, string mainDeploymentJsonPath, string mainDeploymentSnapshotPath, + string regionCode, string clusterCode, string deploymentTags, bool useChinaPlatform) { - var snapshotServiceClient = SnapshotServiceClient.Create(GetApiEndpoint(regionCode), GetPlatformRefreshTokenCredential(regionCode)); + var snapshotServiceClient = SnapshotServiceClient.Create(GetApiEndpoint(useChinaPlatform), GetPlatformRefreshTokenCredential(useChinaPlatform)); // Upload snapshots. var mainSnapshotId = UploadSnapshot(snapshotServiceClient, mainDeploymentSnapshotPath, projectName, - mainDeploymentName, regionCode); + mainDeploymentName, useChinaPlatform); if (mainSnapshotId.Length == 0) { @@ -353,11 +333,26 @@ private static Operation CreateMainDeploym Name = mainDeploymentName, ProjectName = projectName, StartingSnapshotId = mainSnapshotId, - RegionCode = regionCode, RuntimeVersion = runtimeVersion }; + if (!String.IsNullOrEmpty(clusterCode)) + { + mainDeploymentConfig.ClusterCode = clusterCode; + } + else + { + mainDeploymentConfig.RegionCode = regionCode; + } + mainDeploymentConfig.Tag.Add(DEPLOYMENT_LAUNCHED_BY_LAUNCHER_TAG); + foreach (String tag in deploymentTags.Split(' ')) + { + if (tag.Length > 0) + { + mainDeploymentConfig.Tag.Add(tag); + } + } if (launchSimPlayerDeployment) { @@ -378,9 +373,21 @@ private static Operation CreateMainDeploym } private static Operation CreateSimPlayerDeploymentAsync(DeploymentServiceClient deploymentServiceClient, - string projectName, string assemblyName, string runtimeVersion, string mainDeploymentName, string simDeploymentName, string simDeploymentJsonPath, string regionCode, int simNumPlayers) + string projectName, string assemblyName, string runtimeVersion, string mainDeploymentName, string simDeploymentName, + string simDeploymentJsonPath, string simDeploymentSnapshotPath, string regionCode, string clusterCode, int simNumPlayers, bool useChinaPlatform) { - var playerAuthServiceClient = PlayerAuthServiceClient.Create(GetApiEndpoint(regionCode), GetPlatformRefreshTokenCredential(regionCode)); + var snapshotServiceClient = SnapshotServiceClient.Create(GetApiEndpoint(useChinaPlatform), GetPlatformRefreshTokenCredential(useChinaPlatform)); + + // Upload snapshots. + var simDeploymentSnapshotId = UploadSnapshot(snapshotServiceClient, simDeploymentSnapshotPath, projectName, + simDeploymentName, useChinaPlatform); + + if (simDeploymentSnapshotId.Length == 0) + { + throw new Exception("Error while uploading sim player snapshot."); + } + + var playerAuthServiceClient = PlayerAuthServiceClient.Create(GetApiEndpoint(useChinaPlatform), GetPlatformRefreshTokenCredential(useChinaPlatform)); // Create development authentication token used by the simulated players. var dat = playerAuthServiceClient.CreateDevelopmentAuthenticationToken( @@ -488,11 +495,19 @@ private static Operation CreateSimPlayerDe }, Name = simDeploymentName, ProjectName = projectName, - RegionCode = regionCode, - RuntimeVersion = runtimeVersion - // No snapshot included for the simulated player deployment + RuntimeVersion = runtimeVersion, + StartingSnapshotId = simDeploymentSnapshotId, }; + if (!String.IsNullOrEmpty(clusterCode)) + { + simDeploymentConfig.ClusterCode = clusterCode; + } + else + { + simDeploymentConfig.RegionCode = regionCode; + } + simDeploymentConfig.Tag.Add(DEPLOYMENT_LAUNCHED_BY_LAUNCHER_TAG); simDeploymentConfig.Tag.Add(SIM_PLAYER_DEPLOYMENT_TAG); @@ -508,17 +523,16 @@ private static Operation CreateSimPlayerDe } - private static int StopDeployments(string[] args) + private static int StopDeployments(string[] args, bool useChinaPlatform) { var projectName = args[1]; - var regionCode = args[2]; - var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(regionCode), GetPlatformRefreshTokenCredential(regionCode)); + var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(useChinaPlatform), GetPlatformRefreshTokenCredential(useChinaPlatform)); - if (args.Length == 4) + if (args.Length == 3) { // Stop only the specified deployment. - var deploymentId = args[3]; + var deploymentId = args[2]; StopDeploymentById(deploymentServiceClient, projectName, deploymentId); return 0; @@ -560,12 +574,11 @@ private static void StopDeploymentById(DeploymentServiceClient client, string pr } } - private static int ListDeployments(string[] args) + private static int ListDeployments(string[] args, bool useChinaPlatform) { var projectName = args[1]; - var regionCode = args[2]; - var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(regionCode), GetPlatformRefreshTokenCredential(regionCode)); + var deploymentServiceClient = DeploymentServiceClient.Create(GetApiEndpoint(useChinaPlatform), GetPlatformRefreshTokenCredential(useChinaPlatform)); var activeDeployments = ListLaunchedActiveDeployments(deploymentServiceClient, projectName); foreach (var deployment in activeDeployments) @@ -618,24 +631,33 @@ private static IEnumerable ListLaunchedActiveDeployments(DeploymentS private static void ShowUsage() { Console.WriteLine("Usage:"); - Console.WriteLine("DeploymentLauncher create [ ]"); + Console.WriteLine("DeploymentLauncher create [ ]"); Console.WriteLine($" Starts a cloud deployment, with optionally a simulated player deployment. The deployments can be started in different regions ('EU', 'US', 'AP' and 'CN')."); - Console.WriteLine("DeploymentLauncher createsim "); + Console.WriteLine("DeploymentLauncher createsim "); Console.WriteLine($" Starts a simulated player deployment. Can be started in a different region from the target deployment ('EU', 'US', 'AP' and 'CN')."); - Console.WriteLine("DeploymentLauncher stop [deployment-id]"); + Console.WriteLine("DeploymentLauncher stop [deployment-id]"); Console.WriteLine(" Stops the specified deployment within the project."); Console.WriteLine(" If no deployment id argument is specified, all active deployments started by the deployment launcher in the project will be stopped."); - Console.WriteLine("DeploymentLauncher list "); + Console.WriteLine("DeploymentLauncher list "); Console.WriteLine(" Lists all active deployments within the specified project that are started by the deployment launcher."); + Console.WriteLine(); + Console.WriteLine("Flags:"); + Console.WriteLine(" --china Use China platform endpoints."); } private static int Main(string[] args) { + // Filter flags from the rest of the arguments. + string[] flags = args.Where(arg => arg.StartsWith("--")).Select(arg => arg.ToLowerInvariant()).ToArray(); + args = args.Where(arg => !arg.StartsWith("--")).ToArray(); + + bool useChinaPlatform = flags.Contains("--china"); + if (args.Length == 0 || - (args[0] == "create" && (args.Length != 12 && args.Length != 8)) || - (args[0] == "createsim" && args.Length != 10) || - (args[0] == "stop" && (args.Length != 3 && args.Length != 4)) || - (args[0] == "list" && args.Length != 3)) + (args[0] == "create" && (args.Length != 15 && args.Length != 10)) || + (args[0] == "createsim" && args.Length != 11) || + (args[0] == "stop" && (args.Length != 2 && args.Length != 3)) || + (args[0] == "list" && args.Length != 2)) { ShowUsage(); return 1; @@ -645,22 +667,22 @@ private static int Main(string[] args) { if (args[0] == "create") { - return CreateDeployment(args); + return CreateDeployment(args, useChinaPlatform); } if (args[0] == "createsim") { - return CreateSimDeployments(args); + return CreateSimDeployments(args, useChinaPlatform); } if (args[0] == "stop") { - return StopDeployments(args); + return StopDeployments(args, useChinaPlatform); } if (args[0] == "list") { - return ListDeployments(args); + return ListDeployments(args, useChinaPlatform); } ShowUsage(); diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs index 3e69e42ac9..edbfcd7d2e 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs @@ -23,7 +23,6 @@ internal class ManagedWorkerCoordinator : AbstractWorkerCoordinator private const string DevAuthTokenWorkerFlag = "simulated_players_dev_auth_token"; private const string TargetDeploymentWorkerFlag = "simulated_players_target_deployment"; private const string DeploymentTotalNumSimulatedPlayersWorkerFlag = "total_num_simulated_players"; - private const string TargetDeploymentReadyWorkerFlag = "target_deployment_ready"; private const int AverageDelayMillisBetweenConnections = 1500; private const int PollTargetDeploymentReadyIntervalMillis = 5000; @@ -122,10 +121,7 @@ public override void Run() Option targetDeploymentOpt = connection.GetWorkerFlag(TargetDeploymentWorkerFlag); int deploymentTotalNumSimulatedPlayers = int.Parse(GetWorkerFlagOrDefault(connection, DeploymentTotalNumSimulatedPlayersWorkerFlag, "100")); - Logger.WriteLog("Waiting for target deployment to become ready."); - WaitForTargetDeploymentReady(connection); - - Logger.WriteLog($"Target deployment is ready. Starting {NumSimulatedPlayersToStart} simulated players."); + Logger.WriteLog($"Starting {NumSimulatedPlayersToStart} simulated players."); Thread.Sleep(InitialStartDelayMillis); var maxDelayMillis = deploymentTotalNumSimulatedPlayers * AverageDelayMillisBetweenConnections; @@ -201,20 +197,5 @@ private static string GetWorkerFlagOrDefault(Connection connection, string flagN return defaultValue; } - - private void WaitForTargetDeploymentReady(Connection connection) - { - while (true) - { - var readyFlagOpt = connection.GetWorkerFlag(TargetDeploymentReadyWorkerFlag); - if (readyFlagOpt == "true") - { - // Ready. - break; - } - - Thread.Sleep(PollTargetDeploymentReadyIntervalMillis); - } - } } } diff --git a/SpatialGDK/Content/SpatialDebugger/BP_SpatialDebugger.uasset b/SpatialGDK/Content/SpatialDebugger/BP_SpatialDebugger.uasset index aad9ed3100..daf8c9b2b8 100644 Binary files a/SpatialGDK/Content/SpatialDebugger/BP_SpatialDebugger.uasset and b/SpatialGDK/Content/SpatialDebugger/BP_SpatialDebugger.uasset differ diff --git a/SpatialGDK/Extras/core-sdk.version b/SpatialGDK/Extras/core-sdk.version index d18453764c..e4ce12fcfb 100644 --- a/SpatialGDK/Extras/core-sdk.version +++ b/SpatialGDK/Extras/core-sdk.version @@ -1 +1 @@ -14.5.0 +14.6.1 diff --git a/SpatialGDK/Extras/internal-documentation/release-process.md b/SpatialGDK/Extras/internal-documentation/release-process.md index 113e226af3..8d2cf928df 100644 --- a/SpatialGDK/Extras/internal-documentation/release-process.md +++ b/SpatialGDK/Extras/internal-documentation/release-process.md @@ -2,151 +2,33 @@ This document outlines the process for releasing a version of the GDK for Unreal and all associated projects. -> This process will need to be reviewed once we hit beta. - ## Terminology -* **Release version** is the version of the SpatialOS GDK for Unreal that you are releasing by performing the steps in this document. -* **Previous version** is the latest version of the SpatialOS GDK for Unreal that is currently released to customers. You can find out what this version is [here](https://github.com/spatialos/UnrealGDK/releases). -* `` - The directory that contains your project’s .uproject file and Source folder. -* `` - The directory that contains your `` directory. -* `` - The name of your project and .uproject file (for example, `\\YourProject.uproject`). - -## Validation pre-requisites - -The following entry criteria must be met before you start the validation steps: - -### Ensure Xbox DLLs exist - -To check that Xbox-compatible Worker SDK DLLs are available. - -1. Identify the Worker SDK version pinned by the GDK. To do this, check [core-sdk.version](https://github.com/spatialos/UnrealGDK/blob/master/SpatialGDK/Extras/core-sdk.version) on `master`. -1. Identify the XDK version(s) officially supported in all UE4 versions that the GDK version you're about to release supports. To do this: - 1. Google search for the release notes for all UE4 versions that the GDK version you're about to release supports (for example [4.22 Release Notes](https://docs.unrealengine.com/en-US/Support/Builds/ReleaseNotes/4_22/index.html)) - 1. Search in page (ctrl-f) for `XDK` and note down the supported version (for example, the [4.22 Release Notes](https://docs.unrealengine.com/en-US/Support/Builds/ReleaseNotes/4_22/index.html) reveals that it supports `XDK: July 2018 QFE-4`). - 1. Convert `XDK: July 2018 QFE-4` to the format that `spatial package get` expects. This is **year** **month** **QFE Version**. For example `XDK: July 2018 QFE-4` converts to `180704`. - 1. Using the information you just ascertained, fill in the `<...>` blanks in the below command and run it via command line:
-`spatial package get worker_sdk c-dynamic-x86_64--xbone c-sdk-.zip`
-A correct command looks something like this:
-`spatial package get worker_sdk c-dynamic-x86_64-xdk180401-xbone 13.7.1 c-sdk-13.7.1-180401.zip`
-If it succeeds it will download a DLL.
-If it fails because the DLL is not available, file a WRK ticket for the Worker team to generate the required DLL(s). See [WRK-1676](https://improbableio.atlassian.net/browse/WRK-1676) for an example. - -### Create the `UnrealGDK` release candidate -1. Notify `#dev-unreal-internal` that you intend to commence a release. Ask if anyone `@here` knows of any blocking defects in code or docs that should be resolved prior to commencement of the release process. -1. `git clone` the [UnrealGDK](https://github.com/spatialos/UnrealGDK). -1. `git checkout master` -1. `git pull` -1. Using `git log`, take note of the latest commit hash. -1. `git checkout -b x.y.z-rc` in order to create release candidate branch. -1. Open `CHANGELOG.md`, which is in the root of the repository. -1. Read **every** release note in the `Unreleased` section. Ensure that they make sense, they conform to [how-to-write-good-release-notes.md](https://github.com/spatialos/UnrealGDK/blob/master/SpatialGDK/Extras/internal-documentation/how-to-write-good-release-notes.md) structure. -1. Compare `master` to `release` using the GitHub UI and ensure that every change that requires a release note has one. -1. Enter the release version and planned date of release in a `##` block. Move the `Unreleased` section above this. - - Look at the previous release versions in the changelog to see how this should be done. -1. Commit your changes to `CHANGELOG.md`. -1. Open `SpatialGDK/SpatialGDK.uplugin`. -1. Increment the `VersionName` and `Version`. -1. Commit your changes to `SpatialGDK/SpatialGDK.uplugin`. -1. `git push --set-upstream origin x.y.z-rc` to push the branch. -1. Announce the branch and the commit hash it uses in the `#unreal-gdk-release` channel. - -### Create the `improbableio/UnrealEngine` release candidate -1. `git clone` the [improbableio/UnrealEngine](https://github.com/improbableio/UnrealEngine). -1. `git checkout 4.xx-SpatialOSUnrealGDK` -1. `git pull` -1. Using `git log`, take note of the latest commit hash. -1. `git checkout -b 4.xx-SpatialOSUnrealGDK-x.y.z-rc` in order to create release candidate branch. -1. `git push --set-upstream origin 4.xx-SpatialOSUnrealGDK-x.y.z-rc` to push the branch. -1. Repeat the above steps for all supported `4.xx` engine versions. -1. Announce the branch and the commit hash it uses in the `#unreal-gdk-release` channel. -1. Make sure to update UnrealGDKExampleProjectVersion.txt and UnrealGDKVersion.txt so that they contain the relevant release tag for the UnrealGDK and UnrealGDKExampleProject. - -### Create the `UnrealGDKExampleProject` release candidate -1. `git clone` the [UnrealGDKExampleProject](https://github.com/spatialos/UnrealGDKExampleProject). -1. `git checkout master` -1. `git pull` -1. Using `git log`, take note of the latest commit hash. -1. `git checkout -b x.y.z-rc` in order to create release candidate branch. -1. `git push --set-upstream origin x.y.z-rc` to push the branch. -1. Announce the branch and the commit hash it uses in the #unreal-gdk-release channel. - -## Build your release candidate engine -1. Open https://documentation.improbable.io/gdk-for-unreal/docs/get-started-1-get-the-dependencies. -1. Uninstall all dependencies listed on this page so that you can accurately validate our installation steps. -1. If you have one, delete your local clone of `UnrealEngine`. -1. Follow the installation steps on https://documentation.improbable.io/gdk-for-unreal/docs/get-started-1-get-the-dependencies. -1. When you clone the `UnrealEngine`, be sure to checkout `x.y.z-rc-x` so you're building the release version. - -## Implementing fixes - -If at any point in the below validation steps you encounter a blocker, you must fix that defect prior to releasing. - -The workflow for this is: - -1. Raise a bug ticket in JIRA detailing the blocker. -1. `git checkout x.y.z-rc` -1. `git pull` -1. `git checkout -b bugfix/UNR-xxx` -1. Fix the defect. -1. `git commit`, `git push -u origin HEAD`, target your PR at `x.y.z-rc`. -1. When the PR is merged, `git checkout x.y.z-rc`, `git pull` and re-test the defect to ensure you fixed it. -1. Notify #unreal-gdk-release that the release candidate has been updated. -1. **Judgment call**: If the fix was isolated, continue the validation steps from where you left off. If the fix was significant, restart testing from scratch. Consult the rest of the team if you are unsure which to choose. - -## Validation (GDK, Starter Template and Example Project) -You must perform these steps twice, once in the EU region and once in CN. - -1. Open the [Component Release](https://improbabletest.testrail.io/index.php?/suites/view/72) test suite and click run test. -1. Name your test run in this format: Component release: GDK [UnrealGDK version], UE (Unreal Engine version), [region]. -1. Execute the test runs. - -## Validation (Docs) -1. @techwriters in [#docs](https://improbable.slack.com/archives/C0TBQAB5X) and ask them what's changes in the docs since the last release. -1. Proof read the pages that have changed. -1. Spend an additional 20 minutes reading the docs and ensuring that nothing is incorrect. +* **GDK release version** is the version of the SpatialOS GDK for Unreal that you are releasing by performing the steps in this document. It's [semantically versioned](https://semver.org/) and looks like `x.y.z`. +* **Previous GDK version** is the version of the SpatialOS GDK for Unreal that is currently at HEAD of the `release` branch. You can find out what this version is [here](https://github.com/spatialos/UnrealGDK/releases). ## Release - -All of the above tests **must** have passed and there must be no outstanding blocking issues before you start this, the release phase. - -The order of `git merge` operations in all UnrealGDK related repositories is:
-`release candidate` > `preview` > `release` > `master` - -If you want to soak test this release on the `preview` branch before promoting it to the `release` branch, only execute the steps that merge into `preview` and `master`. - -1. When merging the following PRs, you need to enable `Allow merge commits` option on the repos and choose `Create a merge commit` from the dropdown in the pull request UI to merge the branch, then disable `Allow merge commits` option on the repos once the release process is complete. You need to be an admin to perform this. - -**UnrealGDK** -1. In `UnrealGDK`, merge `x.y.z-rc` into `preview`. -1. If you want to soak test this release on the `preview`, use the [GitHub Release UI](https://github.com/spatialos/UnrealGDK/releases) to tag the commit you just made to `preview` as `x.y.z-preview`.
-Copy the latest release notes from `CHANGELOG.md` and paste them into the release description field. -1. In `UnrealGDK`, merge `preview` into `release`. -1. Use the [GitHub Release UI](https://github.com/spatialos/UnrealGDK/releases) to tag the commit you just made to `release` as `x.y.z`.
-Copy the latest release notes from `CHANGELOG.md` and paste them into the release description field. -1. In `UnrealGDK`, merge `release` into `master`. This merge could have conflicts. Don't hesitate to ask for help resolving these if you are unsure. - -**improbableio/UnrealEngine** -1. In `improbableio/UnrealEngine`, merge `4.xx-SpatialOSUnrealGDK-x.y.z-rc` into `4.xx-SpatialOSUnrealGDK-preview`. -1. If you want to soak test this release on the `preview`, use the [GitHub Release UI](https://github.com/improbableio/UnrealEngine/releases) to tag the commit you just made to `4.xx-SpatialOSUnrealGDK-preview` as `4.xx-SpatialOSUnrealGDK-x.y.z-preview`.
-Copy the latest release notes from `CHANGELOG.md` and paste them into the release description field. -1. In `improbableio/UnrealEngine`, merge `4.xx-SpatialOSUnrealGDK-preview` into `4.xx-SpatialOSUnrealGDK-release`. -1. Use the [GitHub Release UI](https://github.com/improbableio/UnrealEngine/releases) to tag the commit you just made to `release` as `4.xx-SpatialOSUnrealGDK-x.y`.
-Copy the latest release notes from `CHANGELOG.md` and paste them into the release description field. -1. In `improbableio/UnrealEngine`, merge `4.xx-SpatialOSUnrealGDK-release` into `master`. This merge could have conflicts. Don't hesitate to ask for help resolving these if you are unsure. - -**UnrealGDKExampleProject** -1. In `UnrealGDKExampleProject`, merge `x.y.z-rc` into `preview`. -1. If you want to soak test this release on the `preview`, use the [GitHub Release UI](https://github.com/spatialos/UnrealGDKExampleProject/releases) to tag the commit you just made to `preview` as `x.y.z-preview`.
-Copy the latest release notes from `CHANGELOG.md` and paste them into the release description field. -1. In `UnrealGDKExampleProject`, merge `preview` into `release`. -1. Use the [GitHub Release UI](https://github.com/spatialos/UnrealGDKExampleProject/releases) to tag the commit you just made to `release` as `x.y.z`.
-Copy the latest release notes from `CHANGELOG.md` and paste them into the release description field. -1. In `UnrealGDK`, merge `release` into `master`. - -**Documentation** +1. Notify `#dev-unreal-internal` that you intend to commence a release. Ask if anyone `@here` knows of any blocking defects in code or documentation that should be resolved prior to commencement of the release process. +1. If nobody objects to the release, navigate to [unrealgdk-release](https://buildkite.com/improbable/unrealgdk-release/) and select the New Build button. +1. In the Message field type "Releasing [GDK release version]". +1. The "Commit" field is prepopulated with `HEAD`, leave it as is. +1. The "Branch" field is prepopulated with `master`. Leave it as is. This determines which version of the unrealgdk-release pipeline is run. +1. Select "Create Build". +1. Wait about 20 seconds for `imp-ci steps run --from .buildkite/release.steps.yaml` to pass and then select "Configure your release." +1. In the "UnrealGDK component release version" field enter the GDK release version. +1. The "UnrealGDK source branch" field is prepopulated with `master`. Leave it as is if you're executing a major or minor release, change it to `release` if you're executing a patch release. +1. The "UnrealEngine source branches" field should be prepopulated with the source branches of the latest fully supported and legacy supported Unreal Engine versions. If you're executing a patch release you'll need to suffix each branch with `-release`.
Wrong prepopulated branches? If the prepopulated branches are wrong, select the button with an X at the upper-right corner of the form, and then select "Cancel" to stop this build of unrealgdk-release. Then, on the UnrealGDK's `master` branch at [`.buildkite/release.steps.yaml#L32`](https://github.com/spatialos/UnrealGDK/blob/master/.buildkite/release.steps.yaml#L32), update the default branches to the latest, merge that change and restart this release process
+1. Select "Continue" and move onto the next step. +1. Wait for the "Prepare the release" step to run, it takes about 20 minutes, maybe grab a coffee? +1. Once the "Prepare the release" step has passed the "Build & upload all UnrealEngine release candidates" step will commence.
While those builds run, take a look at the top of the build page, where you'll notice a new [annotation](https://buildkite.com/docs/agent/v3/cli-annotate): "your human labour is now required to complete the tasks listed in the PR descriptions and unblock the pipeline to resume the release."
Click through to the PRs using the links in the annotations and follow the steps. Come back when you're done. +1. As soon as the "Build & upload all UnrealEngine release candidates" step has passed, select "Run all tests". +1. Once all test have passed, all PRs are approved and all tasks listed in the PR descriptions are complete, select "Unblock the release". This will trigger "Release `ci/release.sh`". +1. When "Release `ci/release.sh`" is complete, the unrealgdk-release pipeline will pass.
+Take a look at the top of the build page, where you'll notice a new [annotation](https://buildkite.com/docs/agent/v3/cli-annotate):
+"Release Successful!"
+"Release hash: [hash]"
+"Draft release: [url]" +1. Open every draft release link and click publish on each one. 1. Notify @techwriters in [#docs](https://improbable.slack.com/archives/C0TBQAB5X) that they may publish the new version of the docs. - -**Announce** 1. Announce the release in: * Forums @@ -158,19 +40,23 @@ Congratulations, you've completed the release process! ## Clean up -1. Delete all `rc` branches. +1. Delete all `-rc` branches. ## Appendix
Forum Post Template - We are happy to announce the release of version x.y.z of the SpatialOS GDK for Unreal. + We are happy to announce the release of version [GDK release version] of the SpatialOS GDK for Unreal. Please see the full release notes on GitHub: Unreal GDK - https://github.com/spatialos/UnrealGDK/releases/tag/x.y.z
-Corresponding fork of Unreal Engine - https://github.com/improbableio/UnrealEngine/releases/tag/4.xx-SpatialOSUnrealGDK-x.y.z
+ +Corresponding Unreal Engine versions: +- https://github.com/improbableio/UnrealEngine/releases/tag/4.xx-SpatialOSUnrealGDK-x.y.z
+- https://github.com/improbableio/UnrealEngine/releases/tag/4.xx-SpatialOSUnrealGDK-x.y.z
+ Corresponding version of the Example Project - https://github.com/spatialos/UnrealGDKExampleProject/releases/tag/x.y.z
diff --git a/SpatialGDK/Extras/schema/core_types.schema b/SpatialGDK/Extras/schema/core_types.schema index d1629f67fa..eda04e2659 100644 --- a/SpatialGDK/Extras/schema/core_types.schema +++ b/SpatialGDK/Extras/schema/core_types.schema @@ -4,6 +4,9 @@ package unreal; type Void { } +// Below, option property types are used for boolean behaviour where an empty +// boolean indicates false. This is cheaper for having that property set to false +// because empty options types involve no idea being sent across the network. type UnrealObjectRef { EntityId entity = 1; uint32 offset = 2; @@ -12,8 +15,10 @@ type UnrealObjectRef { // a reference, e.g. anything inside streaming levels should not be loaded. option no_load_on_client = 4; option outer = 5; - // Singleton objects can be referred to by their class path in the case of an + // Actors such as the game state, game mode and level script Actors (formerly + // known as Singletons) can be referred to by their class path in the case of an // authoritative server that hasn't checked the singleton entity yet. This bool - // will differentiate that from their class pointer. - option use_singleton_class_path = 6; + // will indicate that non-auth servers should try to load such Actors from + // their package map. + option use_class_path_to_load_object = 6; } diff --git a/SpatialGDK/Extras/schema/global_state_manager.schema b/SpatialGDK/Extras/schema/global_state_manager.schema index 5a0e2230ca..be49cf0e8d 100644 --- a/SpatialGDK/Extras/schema/global_state_manager.schema +++ b/SpatialGDK/Extras/schema/global_state_manager.schema @@ -10,11 +10,6 @@ type ShutdownMultiProcessResponse { type ShutdownAdditionalServersEvent { } -component SingletonManager { - id = 9995; - map singleton_name_to_entity_id = 1; -} - component DeploymentMap { id = 9994; string map_url = 1; diff --git a/SpatialGDK/Extras/schema/singleton.schema b/SpatialGDK/Extras/schema/singleton.schema deleted file mode 100644 index 30df1c47ea..0000000000 --- a/SpatialGDK/Extras/schema/singleton.schema +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved -package unreal; - -component Singleton { - id = 9997; -} diff --git a/SpatialGDK/Resources/Cloud.png b/SpatialGDK/Resources/Cloud.png index 7e69932be7..0b779f2055 100644 Binary files a/SpatialGDK/Resources/Cloud.png and b/SpatialGDK/Resources/Cloud.png differ diff --git a/SpatialGDK/Resources/Cloud@0.5x.png b/SpatialGDK/Resources/Cloud@0.5x.png index 8913079aa8..e9d7babfec 100644 Binary files a/SpatialGDK/Resources/Cloud@0.5x.png and b/SpatialGDK/Resources/Cloud@0.5x.png differ diff --git a/SpatialGDK/Resources/Inspector.png b/SpatialGDK/Resources/Inspector.png index 91abd3067b..85208f68be 100644 Binary files a/SpatialGDK/Resources/Inspector.png and b/SpatialGDK/Resources/Inspector.png differ diff --git a/SpatialGDK/Resources/Inspector@0.5x.png b/SpatialGDK/Resources/Inspector@0.5x.png index 4cd0722bcb..2d28f93bba 100644 Binary files a/SpatialGDK/Resources/Inspector@0.5x.png and b/SpatialGDK/Resources/Inspector@0.5x.png differ diff --git a/SpatialGDK/Resources/Launch.png b/SpatialGDK/Resources/Launch.png deleted file mode 100644 index b367ea269f..0000000000 Binary files a/SpatialGDK/Resources/Launch.png and /dev/null differ diff --git a/SpatialGDK/Resources/Launch@0.5x.png b/SpatialGDK/Resources/Launch@0.5x.png deleted file mode 100644 index 46ecd5831e..0000000000 Binary files a/SpatialGDK/Resources/Launch@0.5x.png and /dev/null differ diff --git a/SpatialGDK/Resources/None.png b/SpatialGDK/Resources/None.png new file mode 100644 index 0000000000..a433b029c8 Binary files /dev/null and b/SpatialGDK/Resources/None.png differ diff --git a/SpatialGDK/Resources/None@0.5x.png b/SpatialGDK/Resources/None@0.5x.png new file mode 100644 index 0000000000..fb3c907df5 Binary files /dev/null and b/SpatialGDK/Resources/None@0.5x.png differ diff --git a/SpatialGDK/Resources/Schema.png b/SpatialGDK/Resources/Schema.png index 9ed71d0901..96fbfad8c6 100644 Binary files a/SpatialGDK/Resources/Schema.png and b/SpatialGDK/Resources/Schema.png differ diff --git a/SpatialGDK/Resources/Schema@0.5x.png b/SpatialGDK/Resources/Schema@0.5x.png index e83b8fd399..ff6903d5f5 100644 Binary files a/SpatialGDK/Resources/Schema@0.5x.png and b/SpatialGDK/Resources/Schema@0.5x.png differ diff --git a/SpatialGDK/Resources/Snapshot.png b/SpatialGDK/Resources/Snapshot.png index 9541498f0b..df581e920e 100644 Binary files a/SpatialGDK/Resources/Snapshot.png and b/SpatialGDK/Resources/Snapshot.png differ diff --git a/SpatialGDK/Resources/Snapshot@0.5x.png b/SpatialGDK/Resources/Snapshot@0.5x.png index ecd3b87047..019a0d4d79 100644 Binary files a/SpatialGDK/Resources/Snapshot@0.5x.png and b/SpatialGDK/Resources/Snapshot@0.5x.png differ diff --git a/SpatialGDK/Resources/StartCloud.png b/SpatialGDK/Resources/StartCloud.png new file mode 100644 index 0000000000..4dd8b28aba Binary files /dev/null and b/SpatialGDK/Resources/StartCloud.png differ diff --git a/SpatialGDK/Resources/StartCloud@0.5x.png b/SpatialGDK/Resources/StartCloud@0.5x.png new file mode 100644 index 0000000000..e82c43b380 Binary files /dev/null and b/SpatialGDK/Resources/StartCloud@0.5x.png differ diff --git a/SpatialGDK/Resources/StartLocal.png b/SpatialGDK/Resources/StartLocal.png new file mode 100644 index 0000000000..4701d32284 Binary files /dev/null and b/SpatialGDK/Resources/StartLocal.png differ diff --git a/SpatialGDK/Resources/StartLocal@0.5x.png b/SpatialGDK/Resources/StartLocal@0.5x.png new file mode 100644 index 0000000000..475327f243 Binary files /dev/null and b/SpatialGDK/Resources/StartLocal@0.5x.png differ diff --git a/SpatialGDK/Resources/Stop.png b/SpatialGDK/Resources/Stop.png deleted file mode 100644 index dfdf5fb6ef..0000000000 Binary files a/SpatialGDK/Resources/Stop.png and /dev/null differ diff --git a/SpatialGDK/Resources/Stop@0.5x.png b/SpatialGDK/Resources/Stop@0.5x.png deleted file mode 100644 index 06f0327559..0000000000 Binary files a/SpatialGDK/Resources/Stop@0.5x.png and /dev/null differ diff --git a/SpatialGDK/Resources/StopCloud.png b/SpatialGDK/Resources/StopCloud.png new file mode 100644 index 0000000000..de4978ea79 Binary files /dev/null and b/SpatialGDK/Resources/StopCloud.png differ diff --git a/SpatialGDK/Resources/StopCloud@0.5x.png b/SpatialGDK/Resources/StopCloud@0.5x.png new file mode 100644 index 0000000000..bd40858ef0 Binary files /dev/null and b/SpatialGDK/Resources/StopCloud@0.5x.png differ diff --git a/SpatialGDK/Resources/StopLocal.png b/SpatialGDK/Resources/StopLocal.png new file mode 100644 index 0000000000..900fd7225b Binary files /dev/null and b/SpatialGDK/Resources/StopLocal.png differ diff --git a/SpatialGDK/Resources/StopLocal@0.5x.png b/SpatialGDK/Resources/StopLocal@0.5x.png new file mode 100644 index 0000000000..84f28e69d9 Binary files /dev/null and b/SpatialGDK/Resources/StopLocal@0.5x.png differ diff --git a/SpatialGDK/Resources/Upload.png b/SpatialGDK/Resources/Upload.png deleted file mode 100644 index 7e69932be7..0000000000 Binary files a/SpatialGDK/Resources/Upload.png and /dev/null differ diff --git a/SpatialGDK/Resources/Upload@0.5x .png b/SpatialGDK/Resources/Upload@0.5x .png deleted file mode 100644 index 9a0391f2a1..0000000000 Binary files a/SpatialGDK/Resources/Upload@0.5x .png and /dev/null differ diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/SpatialPingComponent.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/SpatialPingComponent.cpp index 399fece084..6c56cd8408 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/SpatialPingComponent.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/SpatialPingComponent.cpp @@ -196,6 +196,11 @@ FSpatialPingAverageData USpatialPingComponent::GetAverageData() const Data.LastMeasurementsWindowAvg = Total / LastPingMeasurements.Num(); Data.LastMeasurementsWindowMin = FMath::Min(LastPingMeasurements); Data.LastMeasurementsWindowMax = FMath::Max(LastPingMeasurements); + + TArray Sorted = LastPingMeasurements; + Sorted.Sort(); + Data.LastMeasurementsWindow50thPercentile = Sorted[static_cast(Sorted.Num() * 0.5f)]; + Data.LastMeasurementsWindow90thPercentile = Sorted[static_cast(Sorted.Num() * 0.9f)]; } Data.WindowSize = LastPingMeasurements.Num(); diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp index 74f05b7b9a..a33fa96aa1 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp @@ -44,22 +44,14 @@ DECLARE_CYCLE_STAT(TEXT("IsAuthoritativeServer"), STAT_IsAuthoritativeServer, ST namespace { -#if ENGINE_MINOR_VERSION <= 22 - const int32 MaxSendingChangeHistory = FRepState::MAX_CHANGE_HISTORY; -#else - const int32 MaxSendingChangeHistory = FSendingRepState::MAX_CHANGE_HISTORY; -#endif +const int32 MaxSendingChangeHistory = FSendingRepState::MAX_CHANGE_HISTORY; // This is a bookkeeping function that is similar to the one in RepLayout.cpp, modified for our needs (e.g. no NaKs) // We can't use the one in RepLayout.cpp because it's private and it cannot account for our approach. // In this function, we poll for any changes in Unreal properties compared to the last time we replicated this actor. void UpdateChangelistHistory(TUniquePtr& RepState) { -#if ENGINE_MINOR_VERSION <= 22 - FRepState* SendingRepState = RepState.Get(); -#else FSendingRepState* SendingRepState = RepState->GetSendingRepState(); -#endif check(SendingRepState->HistoryEnd >= SendingRepState->HistoryStart); @@ -88,6 +80,23 @@ void UpdateChangelistHistory(TUniquePtr& RepState) SendingRepState->HistoryStart = SendingRepState->HistoryStart % MaxSendingChangeHistory; SendingRepState->HistoryEnd = SendingRepState->HistoryStart + NewHistoryCount; } + +void ForceReplicateOnActorHierarchy(USpatialNetDriver* NetDriver, const AActor* HierarchyActor, const AActor* OriginalActorBeingReplicated) +{ + if (HierarchyActor->GetIsReplicated() && HierarchyActor != OriginalActorBeingReplicated) + { + if (USpatialActorChannel* Channel = NetDriver->GetOrCreateSpatialActorChannel(const_cast(HierarchyActor))) + { + Channel->ReplicateActor(); + } + } + + for (const AActor* Child : HierarchyActor->Children) + { + ForceReplicateOnActorHierarchy(NetDriver, Child, OriginalActorBeingReplicated); + } +} + } // end anonymous namespace bool FSpatialObjectRepState::MoveMappedObjectToUnmapped_r(const FUnrealObjectRef& ObjRef, FObjectReferencesMap& ObjectReferencesMap) @@ -218,6 +227,7 @@ void USpatialActorChannel::Init(UNetConnection* InConnection, int32 ChannelIndex bIsAuthServer = false; LastPositionSinceUpdate = FVector::ZeroVector; TimeWhenPositionLastUpdated = 0.0f; + AuthorityReceivedTimestamp = 0; PendingDynamicSubobjects.Empty(); SavedConnectionOwningWorkerId.Empty(); @@ -234,34 +244,49 @@ void USpatialActorChannel::Init(UNetConnection* InConnection, int32 ChannelIndex Receiver = NetDriver->Receiver; } -void USpatialActorChannel::DeleteEntityIfAuthoritative() +void USpatialActorChannel::RetireEntityIfAuthoritative() { if (NetDriver->Connection == nullptr) { return; } - bool bHasAuthority = NetDriver->IsAuthoritativeDestructionAllowed() && NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialGDK::Position::ComponentId); - - UE_LOG(LogSpatialActorChannel, Log, TEXT("Delete entity request on %lld. Has authority: %d"), EntityId, (int)bHasAuthority); + if (!NetDriver->IsAuthoritativeDestructionAllowed()) + { + return; + } - if (bHasAuthority) + const bool bHasAuthority = NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialGDK::Position::ComponentId); + if (Actor != nullptr) { - // Workaround to delay the delete entity request if tearing off. - // Task to improve this: https://improbableio.atlassian.net/browse/UNR-841 - if (Actor != nullptr && Actor->GetTearOff()) + if (bHasAuthority) { - NetDriver->DelayedSendDeleteEntityRequest(EntityId, 1.0f); - // Since the entity deletion is delayed, this creates a situation, - // when the Actor is torn off, but still replicates. - // Disabling replication makes RPC calls impossible for this Actor. - Actor->SetReplicates(false); + // Workaround to delay the delete entity request if tearing off. + // Task to improve this: UNR-841 + if (Actor->GetTearOff()) + { + NetDriver->DelayedRetireEntity(EntityId, 1.0f, Actor->IsNetStartupActor()); + // Since the entity deletion is delayed, this creates a situation, + // when the Actor is torn off, but still replicates. + // Disabling replication makes RPC calls impossible for this Actor. + Actor->SetReplicates(false); + } + else + { + Sender->RetireEntity(EntityId, Actor->IsNetStartupActor()); + } } - else + else if (bCreatedEntity) // We have not gained authority yet { - Sender->RetireEntity(EntityId); + Actor->SetReplicates(false); + Receiver->RetireWhenAuthoritive(EntityId, NetDriver->ClassInfoManager->GetComponentIdForClass(*Actor->GetClass()), Actor->IsNetStartupActor(), Actor->GetTearOff()); // Ensure we don't recreate the actor } } + else + { + // This is unsupported, and shouldn't happen, don't attempt to cleanup entity to better indicate something has gone wrong + UE_LOG(LogSpatialActorChannel, Error, TEXT("RetireEntityIfAuthoritative called on actor channel with null actor - entity id (%lld)"), EntityId); + } } bool USpatialActorChannel::CleanUp(const bool bForDestroy, EChannelCloseReason CloseReason) @@ -277,7 +302,7 @@ bool USpatialActorChannel::CleanUp(const bool bForDestroy, EChannelCloseReason C CloseReason != EChannelCloseReason::Dormancy) { // If we're a server worker, and the entity hasn't already been cleaned up, delete it on shutdown. - DeleteEntityIfAuthoritative(); + RetireEntityIfAuthoritative(); } #endif // WITH_EDITOR @@ -319,7 +344,7 @@ int64 USpatialActorChannel::Close(EChannelCloseReason Reason) } else { - DeleteEntityIfAuthoritative(); + RetireEntityIfAuthoritative(); NetDriver->PackageMap->RemoveEntityActor(EntityId); } @@ -392,8 +417,8 @@ FRepChangeState USpatialActorChannel::CreateInitialRepChangeState(TWeakObjectPtr DynamicArrayDepth++; // For the first layer of each dynamic array encountered at the root level - // add the number of array properties to conform to Unreal's RepLayout design and - // allow FRepHandleIterator to jump over arrays. Cmd.EndCmd is an index into + // add the number of array properties to conform to Unreal's RepLayout design and + // allow FRepHandleIterator to jump over arrays. Cmd.EndCmd is an index into // RepLayout->Cmds[] that points to the value after the termination NULL of this array. if (DynamicArrayDepth == 1) { @@ -421,6 +446,25 @@ FHandoverChangeState USpatialActorChannel::CreateInitialHandoverChangeState(cons return HandoverChanged; } +void USpatialActorChannel::GetLatestAuthorityChangeFromHierarchy(const AActor* HierarchyActor, uint64& OutTimestamp) +{ + if (HierarchyActor->GetIsReplicated()) + { + if (USpatialActorChannel* Channel = NetDriver->GetOrCreateSpatialActorChannel(const_cast(HierarchyActor))) + { + if (Channel->AuthorityReceivedTimestamp > OutTimestamp) + { + OutTimestamp = Channel->AuthorityReceivedTimestamp; + } + } + } + + for (const AActor* Child : HierarchyActor->Children) + { + GetLatestAuthorityChangeFromHierarchy(Child, OutTimestamp); + } +} + int64 USpatialActorChannel::ReplicateActor() { SCOPE_CYCLE_COUNTER(STAT_SpatialActorChannelReplicateActor); @@ -474,9 +518,7 @@ int64 USpatialActorChannel::ReplicateActor() else if (Actor->IsPendingKillOrUnreachable()) { bActorIsPendingKill = true; -#if ENGINE_MINOR_VERSION > 22 ActorReplicator.Reset(); -#endif FString Error(FString::Printf(TEXT("ReplicateActor called with PendingKill Actor! %s"), *Describe())); UE_LOG(LogNet, Log, TEXT("%s"), *Error); ensureMsgf(false, TEXT("%s"), *Error); @@ -532,7 +574,7 @@ int64 USpatialActorChannel::ReplicateActor() // Replicate Actor and Component properties and RPCs // ---------------------------------------------------------- -#if USE_NETWORK_PROFILER +#if USE_NETWORK_PROFILER const uint32 ActorReplicateStartTime = GNetworkProfiler.IsTrackingEnabled() ? FPlatformTime::Cycles() : 0; #endif @@ -554,13 +596,8 @@ int64 USpatialActorChannel::ReplicateActor() // Update the replicated property change list. FRepChangelistState* ChangelistState = ActorReplicator->ChangelistMgr->GetRepChangelistState(); -#if ENGINE_MINOR_VERSION <= 22 - ActorReplicator->ChangelistMgr->Update(ActorReplicator->RepState.Get(), Actor, Connection->Driver->ReplicationFrame, RepFlags, bForceCompareProperties); - FRepState* SendingRepState = ActorReplicator->RepState.Get(); -#else ActorReplicator->RepLayout->UpdateChangelistMgr(ActorReplicator->RepState->GetSendingRepState(), *ActorReplicator->ChangelistMgr, Actor, Connection->Driver->ReplicationFrame, RepFlags, bForceCompareProperties); FSendingRepState* SendingRepState = ActorReplicator->RepState->GetSendingRepState(); -#endif const int32 PossibleNewHistoryIndex = SendingRepState->HistoryEnd % MaxSendingChangeHistory; FRepChangedHistory& PossibleNewHistoryItem = SendingRepState->ChangeHistory[PossibleNewHistoryIndex]; @@ -609,17 +646,12 @@ int64 USpatialActorChannel::ReplicateActor() bCreatedEntity = true; - // If we're not offloading AND either load balancing isn't enabled or it is and we're spawning an Actor that we know - // will be load-balanced to another worker then preemptively set the role to SimulatedProxy. - if (!USpatialStatics::IsSpatialOffloadingEnabled() && (!SpatialGDKSettings->bEnableUnrealLoadBalancer || !NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*Actor))) + // We preemptively set the Actor role to SimulatedProxy if load balancing is disabled + // (since the legacy behaviour is to wait until Spatial tells us we have authority) + if (NetDriver->LoadBalanceStrategy == nullptr) { Actor->Role = ROLE_SimulatedProxy; Actor->RemoteRole = ROLE_Authority; - - if (SpatialGDKSettings->bEnableUnrealLoadBalancer) - { - UE_LOG(LogSpatialActorChannel, Verbose, TEXT("Spawning Actor that will immediately become authoritative on a different worker. Actor: %s. Target virtual worker: %d"), *Actor->GetName(), NetDriver->LoadBalanceStrategy->WhoShouldHaveAuthority(*Actor)); - } } } else @@ -653,11 +685,7 @@ int64 USpatialActorChannel::ReplicateActor() } SendingRepState->LastChangelistIndex = ChangelistState->HistoryEnd; -#if ENGINE_MINOR_VERSION <= 22 - SendingRepState->OpenAckedCalled = true; -#else SendingRepState->bOpenAckedCalled = true; -#endif ActorReplicator->bLastUpdateEmpty = 1; if (bCreatingNewEntity) @@ -717,25 +745,48 @@ int64 USpatialActorChannel::ReplicateActor() // TODO: the 'bWroteSomethingImportant' check causes problems for actors that need to transition in groups (ex. Character, PlayerController, PlayerState), // so disabling it for now. Figure out a way to deal with this to recover the perf lost by calling ShouldChangeAuthority() frequently. [UNR-2387] - if (SpatialGDKSettings->bEnableUnrealLoadBalancer && + if (NetDriver->LoadBalanceStrategy != nullptr && NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID)) { if (!NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*Actor) && !NetDriver->LockingPolicy->IsLocked(Actor)) - { - const VirtualWorkerId NewAuthVirtualWorkerId = NetDriver->LoadBalanceStrategy->WhoShouldHaveAuthority(*Actor); - if (NewAuthVirtualWorkerId != SpatialConstants::INVALID_VIRTUAL_WORKER_ID) + { + const AActor* NetOwner = Actor->GetNetOwner(); + + uint64 HierarchyAuthorityReceivedTimestamp = AuthorityReceivedTimestamp; + if (NetOwner != nullptr) { - Sender->SendAuthorityIntentUpdate(*Actor, NewAuthVirtualWorkerId); + GetLatestAuthorityChangeFromHierarchy(NetOwner, HierarchyAuthorityReceivedTimestamp); + } - // If we're setting a different authority intent, preemptively changed to ROLE_SimulatedProxy - Actor->Role = ROLE_SimulatedProxy; - Actor->RemoteRole = ROLE_Authority; + const float TimeSinceReceivingAuthInSeconds = double(FPlatformTime::Cycles64() - HierarchyAuthorityReceivedTimestamp) * FPlatformTime::GetSecondsPerCycle64(); + const float MigrationBackoffTimeInSeconds = 1.0f; - Actor->OnAuthorityLost(); + if (TimeSinceReceivingAuthInSeconds < MigrationBackoffTimeInSeconds) + { + UE_LOG(LogSpatialActorChannel, Verbose, TEXT("Tried to change auth too early for actor %s"), *Actor->GetName()); } else { - UE_LOG(LogSpatialActorChannel, Error, TEXT("Load Balancing Strategy returned invalid virtual worker for actor %s"), *Actor->GetName()); + const VirtualWorkerId NewAuthVirtualWorkerId = NetDriver->LoadBalanceStrategy->WhoShouldHaveAuthority(*Actor); + if (NewAuthVirtualWorkerId == SpatialConstants::INVALID_VIRTUAL_WORKER_ID) + { + UE_LOG(LogSpatialActorChannel, Error, TEXT("Load Balancing Strategy returned invalid virtual worker for actor %s"), *Actor->GetName()); + } + else + { + Sender->SendAuthorityIntentUpdate(*Actor, NewAuthVirtualWorkerId); + + // If we're setting a different authority intent, preemptively changed to ROLE_SimulatedProxy + Actor->Role = ROLE_SimulatedProxy; + Actor->RemoteRole = ROLE_Authority; + + Actor->OnAuthorityLost(); + + if (NetOwner != nullptr) + { + ForceReplicateOnActorHierarchy(NetDriver, NetOwner, Actor); + } + } } } @@ -750,9 +801,9 @@ int64 USpatialActorChannel::ReplicateActor() } } } -#if USE_NETWORK_PROFILER +#if USE_NETWORK_PROFILER NETWORK_PROFILER(GNetworkProfiler.TrackReplicateActor(Actor, RepFlags, FPlatformTime::Cycles() - ActorReplicateStartTime, Connection)); -#endif +#endif // If we evaluated everything, mark LastUpdateTime, even if nothing changed. LastUpdateTime = Connection->Driver->Time; @@ -847,7 +898,7 @@ bool USpatialActorChannel::ReplicateSubobject(UObject* Object, const FReplicatio FObjectReplicator& Replicator = FindOrCreateReplicator(Object, &bCreatedReplicator).Get(); - // If we're creating an entity, don't try replicating + // If we're creating an entity, don't try replicating if (bCreatingNewEntity) { return false; @@ -869,13 +920,8 @@ bool USpatialActorChannel::ReplicateSubobject(UObject* Object, const FReplicatio FRepChangelistState* ChangelistState = Replicator.ChangelistMgr->GetRepChangelistState(); -#if ENGINE_MINOR_VERSION <= 22 - Replicator.ChangelistMgr->Update(Replicator.RepState.Get(), Object, Replicator.Connection->Driver->ReplicationFrame, RepFlags, bForceCompareProperties); - FRepState* SendingRepState = Replicator.RepState.Get(); -#else Replicator.RepLayout->UpdateChangelistMgr(Replicator.RepState->GetSendingRepState(), *Replicator.ChangelistMgr, Object, Replicator.Connection->Driver->ReplicationFrame, RepFlags, bForceCompareProperties); FSendingRepState* SendingRepState = Replicator.RepState->GetSendingRepState(); -#endif const int32 PossibleNewHistoryIndex = SendingRepState->HistoryEnd % MaxSendingChangeHistory; FRepChangedHistory& PossibleNewHistoryItem = SendingRepState->ChangeHistory[PossibleNewHistoryIndex]; @@ -920,11 +966,7 @@ bool USpatialActorChannel::ReplicateSubobject(UObject* Object, const FReplicatio UpdateChangelistHistory(Replicator.RepState); SendingRepState->LastChangelistIndex = ChangelistState->HistoryEnd; -#if ENGINE_MINOR_VERSION <= 22 - SendingRepState->OpenAckedCalled = true; -#else SendingRepState->bOpenAckedCalled = true; -#endif Replicator.bLastUpdateEmpty = 1; return RepChanged.Num() > 0; @@ -1043,15 +1085,9 @@ FHandoverChangeState USpatialActorChannel::GetHandoverChangeList(TArray& return HandoverChanged; } -#if ENGINE_MINOR_VERSION <= 22 -void USpatialActorChannel::SetChannelActor(AActor* InActor) -{ - Super::SetChannelActor(InActor); -#else void USpatialActorChannel::SetChannelActor(AActor* InActor, ESetChannelActorFlags Flags) { Super::SetChannelActor(InActor, Flags); -#endif USpatialPackageMapClient* PackageMap = NetDriver->PackageMap; EntityId = PackageMap->GetEntityIdFromObject(InActor); @@ -1105,12 +1141,6 @@ bool USpatialActorChannel::TryResolveActor() return false; } - // If a Singleton was created, update the GSM with the proper Id. - if (Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)) - { - NetDriver->GlobalStateManager->UpdateSingletonEntityId(Actor->GetClass()->GetPathName(), EntityId); - } - // Inform USpatialNetDriver of this new actor channel/entity pairing NetDriver->AddActorChannel(EntityId, this); @@ -1140,11 +1170,7 @@ void USpatialActorChannel::PostReceiveSpatialUpdate(UObject* TargetObject, const FObjectReplicator& Replicator = FindOrCreateReplicator(TargetObject).Get(); TargetObject->PostNetReceive(); -#if ENGINE_MINOR_VERSION <= 22 - Replicator.RepState->RepNotifies = RepNotifies; -#else Replicator.RepState->GetReceivingRepState()->RepNotifies = RepNotifies; -#endif Replicator.CallRepNotifies(false); } @@ -1155,7 +1181,7 @@ void USpatialActorChannel::OnCreateEntityResponse(const Worker_CreateEntityRespo if (Actor == nullptr || Actor->IsPendingKill()) { - UE_LOG(LogSpatialActorChannel, Warning, TEXT("Actor is invalid after trying to create entity")); + UE_LOG(LogSpatialActorChannel, Log, TEXT("Actor is invalid after trying to create entity")); return; } @@ -1224,7 +1250,7 @@ void USpatialActorChannel::UpdateSpatialPosition() if ((ActorOwner != nullptr || Actor->GetNetConnection() != nullptr) && !Actor->IsA()) { // If this Actor's owner is not replicated (e.g. parent = AI Controller), the actor will not have it's spatial - // position updated as this code will never be run for the parent. + // position updated as this code will never be run for the parent. if (!(Actor->GetNetConnection() == nullptr && ActorOwner != nullptr && !ActorOwner->GetIsReplicated())) { return; @@ -1334,7 +1360,7 @@ void USpatialActorChannel::ServerProcessOwnershipChange() bUpdatedThisActor = true; } - + // Changing owner can affect which interest bucket the Actor should be in so we need to update it. Worker_ComponentId NewInterestBucketComponentId = NetDriver->ClassInfoManager->ComputeActorInterestComponentId(Actor); if (SavedInterestBucketComponentID != NewInterestBucketComponentId) @@ -1371,11 +1397,6 @@ void USpatialActorChannel::ClientProcessOwnershipChange(bool bNewNetOwned) if (bNewNetOwned != bNetOwned) { bNetOwned = bNewNetOwned; - // Don't send dynamic interest for this ownership change if it is otherwise handled by result types. - if (!GetDefault()->bEnableResultTypes) - { - Sender->SendComponentInterestForActor(this, GetEntityId(), bNetOwned); - } Actor->SetIsOwnedByClient(bNetOwned); @@ -1406,11 +1427,7 @@ void USpatialActorChannel::ResetShadowData(FRepLayout& RepLayout, FRepStateStati { if (StaticBuffer.Num() == 0) { -#if ENGINE_MINOR_VERSION <= 22 - RepLayout.InitShadowData(StaticBuffer, TargetObject->GetClass(), reinterpret_cast(TargetObject)); -#else RepLayout.InitRepStateStaticBuffer(StaticBuffer, reinterpret_cast(TargetObject)); -#endif } else { diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp index 1f9710a6fe..599c644377 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp @@ -25,6 +25,11 @@ DEFINE_LOG_CATEGORY(LogSpatialGameInstance); +USpatialGameInstance::USpatialGameInstance() + : Super() + , bIsSpatialNetDriverReady(false) +{} + bool USpatialGameInstance::HasSpatialNetDriver() const { bool bHasSpatialNetDriver = false; @@ -105,32 +110,38 @@ void USpatialGameInstance::DestroySpatialConnectionManager() #if WITH_EDITOR FGameInstancePIEResult USpatialGameInstance::StartPlayInEditorGameInstance(ULocalPlayer* LocalPlayer, const FGameInstancePIEParameters& Params) +{ + SpatialWorkerType = Params.SpatialWorkerType; + bIsSimulatedPlayer = Params.bIsSimulatedPlayer; + + StartSpatialConnection(); + return Super::StartPlayInEditorGameInstance(LocalPlayer, Params); +} +#endif + +void USpatialGameInstance::StartSpatialConnection() { if (HasSpatialNetDriver()) { // If we are using spatial networking then prepare a spatial connection. + TryInjectSpatialLocatorIntoCommandLine(); CreateNewSpatialConnectionManager(); } #if TRACE_LIB_ACTIVE else { // In native, setup worker name here as we don't get a HandleOnConnected() callback - FString WorkerName = FString::Printf(TEXT("%s:%s"), *Params.SpatialWorkerType.ToString(), *FGuid::NewGuid().ToString(EGuidFormats::Digits)); + FString WorkerName = FString::Printf(TEXT("%s:%s"), *SpatialWorkerType.ToString(), *FGuid::NewGuid().ToString(EGuidFormats::Digits)); SpatialLatencyTracer->SetWorkerId(WorkerName); } #endif - - return Super::StartPlayInEditorGameInstance(LocalPlayer, Params); } -#endif -void USpatialGameInstance::TryConnectToSpatial() +void USpatialGameInstance::TryInjectSpatialLocatorIntoCommandLine() { - if (HasSpatialNetDriver()) + if (!HasPreviouslyConnectedToSpatial()) { - // If we are using spatial networking then prepare a spatial connection. - CreateNewSpatialConnectionManager(); - + SetHasPreviouslyConnectedToSpatial(); // Native Unreal creates a NetDriver and attempts to automatically connect if a Host is specified as the first commandline argument. // Since the SpatialOS Launcher does not specify this, we need to check for a locator loginToken to allow automatic connection to provide parity with native. @@ -147,19 +158,18 @@ void USpatialGameInstance::TryConnectToSpatial() FCommandLine::Set(*NewCommandLineArgs); } } -#if TRACE_LIB_ACTIVE - else - { - // In native, setup worker name here as we don't get a HandleOnConnected() callback - FString WorkerName = FString::Printf(TEXT("%s:%s"), *SpatialWorkerType.ToString(), *FGuid::NewGuid().ToString(EGuidFormats::Digits)); - SpatialLatencyTracer->SetWorkerId(WorkerName); - } -#endif } void USpatialGameInstance::StartGameInstance() { - TryConnectToSpatial(); + if (GetDefault()->GetPreventClientCloudDeploymentAutoConnect()) + { + DisableShouldConnectUsingCommandLineArgs(); + } + else + { + StartSpatialConnection(); + } Super::StartGameInstance(); } @@ -200,12 +210,11 @@ void USpatialGameInstance::Init() Super::Init(); SpatialLatencyTracer = NewObject(this); - FWorldDelegates::LevelInitializedNetworkActors.AddUObject(this, &USpatialGameInstance::OnLevelInitializedNetworkActors); - ActorGroupManager = MakeUnique(); - ActorGroupManager->Init(); - - checkf(!(GetDefault()->bEnableUnrealLoadBalancer && USpatialStatics::IsSpatialOffloadingEnabled()), TEXT("Offloading and the Unreal Load Balancer are enabled at the same time, this is currently not supported. Please change your project settings.")); + if (HasSpatialNetDriver()) + { + FWorldDelegates::LevelInitializedNetworkActors.AddUObject(this, &USpatialGameInstance::OnLevelInitializedNetworkActors); + } } void USpatialGameInstance::HandleOnConnected() @@ -219,7 +228,23 @@ void USpatialGameInstance::HandleOnConnected() WorkerConnection->OnEnqueueMessage.AddUObject(SpatialLatencyTracer, &USpatialLatencyTracer::OnEnqueueMessage); WorkerConnection->OnDequeueMessage.AddUObject(SpatialLatencyTracer, &USpatialLatencyTracer::OnDequeueMessage); #endif - OnConnected.Broadcast(); + + OnSpatialConnected.Broadcast(); +} + +void USpatialGameInstance::CleanupCachedLevelsAfterConnection() +{ + // Cleanup any actors which were created during level load. + UWorld* World = GetWorld(); + check(World != nullptr); + for (ULevel* Level : CachedLevelsForNetworkIntialize) + { + if (World->ContainsLevel(Level)) + { + CleanupLevelInitializedNetworkActors(Level); + } + } + CachedLevelsForNetworkIntialize.Empty(); } void USpatialGameInstance::HandleOnConnectionFailed(const FString& Reason) @@ -228,13 +253,17 @@ void USpatialGameInstance::HandleOnConnectionFailed(const FString& Reason) #if TRACE_LIB_ACTIVE SpatialLatencyTracer->ResetWorkerId(); #endif - OnConnectionFailed.Broadcast(Reason); + OnSpatialConnectionFailed.Broadcast(Reason); } -void USpatialGameInstance::OnLevelInitializedNetworkActors(ULevel* LoadedLevel, UWorld* OwningWorld) +void USpatialGameInstance::HandleOnPlayerSpawnFailed(const FString& Reason) { - const FString WorkerType = GetSpatialWorkerType().ToString(); + UE_LOG(LogSpatialGameInstance, Error, TEXT("Could not spawn the local player on SpatialOS. Reason: %s"), *Reason); + OnSpatialPlayerSpawnFailed.Broadcast(Reason); +} +void USpatialGameInstance::OnLevelInitializedNetworkActors(ULevel* LoadedLevel, UWorld* OwningWorld) +{ if (OwningWorld != GetWorld() || !OwningWorld->IsServer() || !GetDefault()->UsesSpatialNetworking() @@ -246,6 +275,19 @@ void USpatialGameInstance::OnLevelInitializedNetworkActors(ULevel* LoadedLevel, return; } + if (bIsSpatialNetDriverReady) + { + CleanupLevelInitializedNetworkActors(LoadedLevel); + } + else + { + CachedLevelsForNetworkIntialize.Add(LoadedLevel); + } +} + +void USpatialGameInstance::CleanupLevelInitializedNetworkActors(ULevel* LoadedLevel) +{ + bIsSpatialNetDriverReady = true; for (int32 ActorIndex = 0; ActorIndex < LoadedLevel->Actors.Num(); ActorIndex++) { AActor* Actor = LoadedLevel->Actors[ActorIndex]; @@ -254,7 +296,7 @@ void USpatialGameInstance::OnLevelInitializedNetworkActors(ULevel* LoadedLevel, continue; } - if (USpatialStatics::IsSpatialOffloadingEnabled()) + if (USpatialStatics::IsSpatialOffloadingEnabled(GetWorld())) { if (!USpatialStatics::IsActorGroupOwnerForActor(Actor)) { @@ -264,7 +306,7 @@ void USpatialGameInstance::OnLevelInitializedNetworkActors(ULevel* LoadedLevel, } else { - UE_LOG(LogSpatialGameInstance, Verbose, TEXT("WorkerType %s is not the actor group owner of startup actor %s, exchanging Roles"), *WorkerType, *GetPathNameSafe(Actor)); + UE_LOG(LogSpatialGameInstance, Verbose, TEXT("This worker %s is not the owner of startup actor %s, exchanging Roles"), *GetPathNameSafe(Actor)); ENetRole Temp = Actor->Role; Actor->Role = Actor->RemoteRole; Actor->RemoteRole = Temp; diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitReader.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitReader.cpp index d27974e684..0b2f520097 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitReader.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitReader.cpp @@ -40,7 +40,7 @@ void FSpatialNetBitReader::DeserializeObjectRef(FUnrealObjectRef& ObjectRef) } SerializeBits(&ObjectRef.bNoLoadOnClient, 1); - SerializeBits(&ObjectRef.bUseSingletonClassPath, 1); + SerializeBits(&ObjectRef.bUseClassPathToLoadObject, 1); } UObject* FSpatialNetBitReader::ReadObject(bool& bUnresolved) diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitWriter.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitWriter.cpp index fd32bb3995..ea7a6b062d 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitWriter.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitWriter.cpp @@ -37,7 +37,7 @@ void FSpatialNetBitWriter::SerializeObjectRef(FUnrealObjectRef& ObjectRef) } SerializeBits(&ObjectRef.bNoLoadOnClient, 1); - SerializeBits(&ObjectRef.bUseSingletonClassPath, 1); + SerializeBits(&ObjectRef.bUseClassPathToLoadObject, 1); } FArchive& FSpatialNetBitWriter::operator<<(UObject*& Value) diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp index 105a2168b3..6e7083644e 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp @@ -14,6 +14,7 @@ #include "Net/DataReplication.h" #include "Net/RepLayout.h" #include "SocketSubsystem.h" +#include "UObject/WeakObjectPtrTemplates.h" #include "UObject/UObjectIterator.h" #include "EngineClasses/SpatialActorChannel.h" @@ -32,6 +33,7 @@ #include "Interop/SpatialWorkerFlags.h" #include "LoadBalancing/AbstractLBStrategy.h" #include "LoadBalancing/GridBasedLBStrategy.h" +#include "LoadBalancing/LayeredLBStrategy.h" #include "LoadBalancing/OwnershipLockingPolicy.h" #include "SpatialConstants.h" #include "SpatialGDKSettings.h" @@ -40,8 +42,8 @@ #include "Utils/ErrorCodeRemapping.h" #include "Utils/InterestFactory.h" #include "Utils/OpUtils.h" -#include "Utils/SpatialActorGroupManager.h" #include "Utils/SpatialDebugger.h" +#include "Utils/SpatialLatencyTracer.h" #include "Utils/SpatialMetrics.h" #include "Utils/SpatialMetricsDisplay.h" #include "Utils/SpatialStatics.h" @@ -53,6 +55,7 @@ using SpatialGDK::ComponentFactory; using SpatialGDK::FindFirstOpOfType; +using SpatialGDK::AppendAllOpsOfType; using SpatialGDK::FindFirstOpOfTypeForComponent; using SpatialGDK::InterestFactory; using SpatialGDK::RPCPayload; @@ -82,13 +85,11 @@ USpatialNetDriver::USpatialNetDriver(const FObjectInitializer& ObjectInitializer , NextRPCIndex(0) , TimeWhenPositionLastUpdated(0.f) { -#if ENGINE_MINOR_VERSION >= 23 // Due to changes in 4.23, we now use an outdated flow in ComponentReader::ApplySchemaObject // Native Unreal now iterates over all commands on clients, and no longer has access to a BaseHandleToCmdIndex // in the RepLayout, the below change forces its creation on clients, but this is a workaround // TODO: UNR-2375 bMaySendProperties = true; -#endif } bool USpatialNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, const FURL& URL, bool bReuseAddressAndPort, FString& Error) @@ -135,8 +136,6 @@ bool USpatialNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, c bPersistSpatialConnection = true; } - ActorGroupManager = GetGameInstance()->ActorGroupManager.Get(); - // Initialize ClassInfoManager here because it needs to load SchemaDatabase. // We shouldn't do that in CreateAndInitializeCoreClasses because it is called // from OnConnectionToSpatialOSSucceeded callback which could be executed with the async @@ -145,7 +144,7 @@ bool USpatialNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, c ClassInfoManager = NewObject(); // If it fails to load, don't attempt to connect to spatial. - if (!ClassInfoManager->TryInit(this, ActorGroupManager)) + if (!ClassInfoManager->TryInit(this)) { Error = TEXT("Failed to load Spatial SchemaDatabase! Make sure that schema has been generated for your project"); return false; @@ -224,7 +223,11 @@ void USpatialNetDriver::InitiateConnectionToSpatialOS(const FURL& URL) bPersistSpatialConnection = URL.HasOption(*SpatialConstants::ClientsStayConnectedURLOption); } - if (!bPersistSpatialConnection) + if (GameInstance->GetSpatialConnectionManager() == nullptr) + { + GameInstance->CreateNewSpatialConnectionManager(); + } + else if (!bPersistSpatialConnection) { GameInstance->DestroySpatialConnectionManager(); GameInstance->CreateNewSpatialConnectionManager(); @@ -241,20 +244,21 @@ void USpatialNetDriver::InitiateConnectionToSpatialOS(const FURL& URL) // If this is the first connection try using the command line arguments to setup the config objects. // If arguments can not be found we will use the regular flow of loading from the input URL. - FString SpatialWorkerType = GetGameInstance()->GetSpatialWorkerType().ToString(); + FString SpatialWorkerType = GameInstance->GetSpatialWorkerType().ToString(); - if (!GameInstance->GetFirstConnectionToSpatialOSAttempted()) + // Ensures that any connections attempting to using command line arguments have a valid locater host in the command line. + GameInstance->TryInjectSpatialLocatorIntoCommandLine(); + + UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Attempting connection to SpatialOS")); + + if (GameInstance->GetShouldConnectUsingCommandLineArgs()) { - GameInstance->SetFirstConnectionToSpatialOSAttempted(); - if (GetDefault()->GetPreventClientCloudDeploymentAutoConnect(bConnectAsClient)) - { - // If first time connecting but the bGetPreventClientCloudDeploymentAutoConnect flag is set then use input URL to setup connection config. - ConnectionManager->SetupConnectionConfigFromURL(URL, SpatialWorkerType); - } - // Otherwise, try using command line arguments to setup connection config. - else if (!ConnectionManager->TrySetupConnectionConfigFromCommandLine(SpatialWorkerType)) + GameInstance->DisableShouldConnectUsingCommandLineArgs(); + + // Try using command line arguments to setup connection config. + if (!ConnectionManager->TrySetupConnectionConfigFromCommandLine(SpatialWorkerType)) { - // If the command line arguments can not be used, use the input URL to setup connection config. + // If the command line arguments can not be used, use the input URL to setup connection config instead. ConnectionManager->SetupConnectionConfigFromURL(URL, SpatialWorkerType); } } @@ -298,10 +302,6 @@ void USpatialNetDriver::OnConnectionToSpatialOSSucceeded() { QueryGSMToLoadMap(); } - else - { - Sender->CreateServerWorkerEntity(); - } USpatialGameInstance* GameInstance = GetGameInstance(); check(GameInstance != nullptr); @@ -373,7 +373,7 @@ void USpatialNetDriver::CreateAndInitializeCoreClasses() const USpatialGDKSettings* SpatialSettings = GetDefault(); #if !UE_BUILD_SHIPPING - // If metrics display is enabled, spawn a singleton actor to replicate the information to each client + // If metrics display is enabled, spawn an Actor to replicate the information to each client if (IsServer()) { if (SpatialSettings->bEnableMetricsDisplay) @@ -388,14 +388,11 @@ void USpatialNetDriver::CreateAndInitializeCoreClasses() } #endif - if (SpatialSettings->bEnableUnrealLoadBalancer) - { - CreateAndInitializeLoadBalancingClasses(); - } + CreateAndInitializeLoadBalancingClasses(); if (SpatialSettings->UseRPCRingBuffer()) { - RPCService = MakeUnique(ExtractRPCDelegate::CreateUObject(Receiver, &USpatialReceiver::OnExtractIncomingRPC), StaticComponentView); + RPCService = MakeUnique(ExtractRPCDelegate::CreateUObject(Receiver, &USpatialReceiver::OnExtractIncomingRPC), StaticComponentView, USpatialLatencyTracer::GetTracer(GetWorld())); } Dispatcher->Init(Receiver, StaticComponentView, SpatialMetrics, SpatialWorkerFlags); @@ -404,6 +401,7 @@ void USpatialNetDriver::CreateAndInitializeCoreClasses() GlobalStateManager->Init(this); SnapshotManager->Init(Connection, GlobalStateManager, Receiver); PlayerSpawner->Init(this, &TimerManager); + PlayerSpawner->OnPlayerSpawnFailed.BindUObject(GameInstance, &USpatialGameInstance::HandleOnPlayerSpawnFailed); SpatialMetrics->Init(Connection, NetServerMaxTickRate, IsServer()); SpatialMetrics->ControllerRefProvider.BindUObject(this, &USpatialNetDriver::GetCurrentPlayerControllerRef); @@ -413,6 +411,10 @@ void USpatialNetDriver::CreateAndInitializeCoreClasses() check(NewPackageMap == PackageMap); PackageMap->Init(this, &TimerManager); + if (IsServer()) + { + PackageMap->GetEntityPoolReadyDelegate().AddDynamic(Sender, &USpatialSender::CreateServerWorkerEntity); + } // The interest factory depends on the package map, so is created last. InterestFactory = MakeUnique(ClassInfoManager, PackageMap); @@ -423,23 +425,9 @@ void USpatialNetDriver::CreateAndInitializeLoadBalancingClasses() const ASpatialWorldSettings* WorldSettings = GetWorld() ? Cast(GetWorld()->GetWorldSettings()) : nullptr; if (IsServer()) { - if (WorldSettings == nullptr || WorldSettings->LoadBalanceStrategy == nullptr) - { - if (WorldSettings == nullptr) - { - UE_LOG(LogSpatialOSNetDriver, Error, TEXT("If EnableUnrealLoadBalancer is set, WorldSettings should inherit from SpatialWorldSettings to get the load balancing strategy. Using a 1x1 grid.")); - } - else - { - UE_LOG(LogSpatialOSNetDriver, Error, TEXT("If EnableUnrealLoadBalancer is set, there must be a LoadBalancing strategy set. Using a 1x1 grid.")); - } - LoadBalanceStrategy = NewObject(this); - } - else - { - LoadBalanceStrategy = NewObject(this, WorldSettings->LoadBalanceStrategy); - } + LoadBalanceStrategy = NewObject(this); LoadBalanceStrategy->Init(); + LoadBalanceStrategy->SetVirtualWorkerIds(1, LoadBalanceStrategy->GetMinimumRequiredWorkers()); } VirtualWorkerTranslator = MakeUnique(LoadBalanceStrategy, Connection->GetWorkerId()); @@ -448,20 +436,24 @@ void USpatialNetDriver::CreateAndInitializeLoadBalancingClasses() { LoadBalanceEnforcer = MakeUnique(Connection->GetWorkerId(), StaticComponentView, VirtualWorkerTranslator.Get()); - if (WorldSettings == nullptr || WorldSettings->LockingPolicy == nullptr) + const bool bIsMultiWorkerEnabled = WorldSettings != nullptr && WorldSettings->IsMultiWorkerEnabled(); + if (!bIsMultiWorkerEnabled) + { + LockingPolicy = NewObject(this); + } + else if (WorldSettings->DefaultLayerLockingPolicy == nullptr) { - UE_LOG(LogSpatialOSNetDriver, Error, TEXT("If EnableUnrealLoadBalancer is set, there must be a Locking Policy set. Using default policy.")); + UE_LOG(LogSpatialOSNetDriver, Error, TEXT("If Load balancing is enabled, there must be a Locking Policy set. Using default policy.")); LockingPolicy = NewObject(this); } else { - LockingPolicy = NewObject(this, WorldSettings->LockingPolicy); + LockingPolicy = NewObject(this, WorldSettings->DefaultLayerLockingPolicy); } LockingPolicy->Init(AcquireLockDelegate, ReleaseLockDelegate); } } - void USpatialNetDriver::CreateServerSpatialOSNetConnection() { check(!bConnectAsClient); @@ -539,9 +531,6 @@ void USpatialNetDriver::OnGSMQuerySuccess() WorldContext.PendingNetGame->bSuccessfullyConnected = true; WorldContext.PendingNetGame->bSentJoinRequest = false; WorldContext.PendingNetGame->URL = RedirectURL; - - // Ensure the singleton map is reset as it will contain bad data from the old map - GlobalStateManager->RemoveAllSingletons(); } else { @@ -583,14 +572,19 @@ void USpatialNetDriver::GSMQueryDelegateFunction(const Worker_EntityQueryRespons if (!bQueryResponseSuccess) { - UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Failed to extract AcceptingPlayers and SessionId from GSM query response. Will retry query for GSM.")); + UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Failed to extract AcceptingPlayers and SessionId from GSM query response.")); RetryQueryGSM(); return; } - else if (bNewAcceptingPlayers != true || - QuerySessionId != SessionId) + else if (!bNewAcceptingPlayers) { - UE_LOG(LogSpatialOSNetDriver, Log, TEXT("GlobalStateManager did not match expected state. Will retry query for GSM.")); + UE_LOG(LogSpatialOSNetDriver, Log, TEXT("GlobalStateManager not accepting players. Usually caused by servers not registering themselves with the deployment yet. Did you launch the correct number of servers?")); + RetryQueryGSM(); + return; + } + else if (QuerySessionId != SessionId) + { + UE_LOG(LogSpatialOSNetDriver, Log, TEXT("GlobalStateManager session id mismatch - got (%d) expected (%d)."), QuerySessionId, SessionId); RetryQueryGSM(); return; } @@ -614,19 +608,40 @@ void USpatialNetDriver::QueryGSMToLoadMap() void USpatialNetDriver::OnActorSpawned(AActor* Actor) { + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + if (!SpatialGDKSettings->bEnableMultiWorkerDebuggingWarnings) + { + return; + } + if (!Actor->GetIsReplicated() || Actor->GetLocalRole() != ROLE_Authority || - Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton) || !Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_SpatialType) || - USpatialStatics::IsActorGroupOwnerForActor(Actor)) + (IsReady() && USpatialStatics::IsActorGroupOwnerForActor(Actor))) { - // We only want to delete actors which are replicated and we somehow gain local authority over, while not the actor group owner. + // We only want to delete actors which are replicated and we somehow gain local authority over, + // when they should be in a different Layer. return; } - const FString WorkerType = GetGameInstance()->GetSpatialWorkerType().ToString(); - UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Worker %s spawned replicated actor %s (owner: %s) but is not actor group owner for actor group %s. The actor will be destroyed in 0.01s"), - *WorkerType, *GetNameSafe(Actor), *GetNameSafe(Actor->GetOwner()), *USpatialStatics::GetActorGroupForActor(Actor).ToString()); + if (!IsReady()) + { + UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Spawned replicated actor %s (owner: %s) before the NetDriver was ready. This is not supported. Actors should only be spawned after BeginPlay is called."), + *GetNameSafe(Actor), *GetNameSafe(Actor->GetOwner())); + return; + } + + if (LoadBalanceStrategy != nullptr) + { + UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Worker ID %d spawned replicated actor %s (owner: %s) but should not have authority. It should be owned by %d. The actor will be destroyed in 0.01s"), + LoadBalanceStrategy->GetLocalVirtualWorkerId(), *GetNameSafe(Actor), *GetNameSafe(Actor->GetOwner()), LoadBalanceStrategy->WhoShouldHaveAuthority(*Actor)); + } + else + { + UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Worker spawned replicated actor %s (owner: %s) but should not have authority. The actor will be destroyed in 0.01s"), + *GetNameSafe(Actor), *GetNameSafe(Actor->GetOwner())); + } + // We tear off, because otherwise SetLifeSpan fails, we SetLifeSpan because we are just about to spawn the Actor and Unreal would complain if we destroyed it. Actor->TearOff(); Actor->SetLifeSpan(0.01f); @@ -693,24 +708,16 @@ void USpatialNetDriver::OnLevelAddedToWorld(ULevel* LoadedLevel, UWorld* OwningW if (OwningWorld != World || !IsServer() - || GlobalStateManager == nullptr - || USpatialStatics::IsSpatialOffloadingEnabled()) + || GlobalStateManager == nullptr) { // If the world isn't our owning world, we are a client, or we loaded the levels - // before connecting to Spatial, or we are running with offloading, we return early. + // before connecting to Spatial, we return early. return; } - const bool bLoadBalancingEnabled = GetDefault()->bEnableUnrealLoadBalancer; const bool bHaveGSMAuthority = StaticComponentView->HasAuthority(SpatialConstants::INITIAL_GLOBAL_STATE_MANAGER_ENTITY_ID, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID); - if (!bLoadBalancingEnabled && !bHaveGSMAuthority) - { - // If load balancing is disabled and this worker is not GSM authoritative then exit early. - return; - } - - if (bLoadBalancingEnabled && !LoadBalanceStrategy->IsReady()) + if (!LoadBalanceStrategy->IsReady()) { // Load balancer isn't ready, this should only occur when servers are loading composition levels on startup, before connecting to spatial. return; @@ -720,11 +727,11 @@ void USpatialNetDriver::OnLevelAddedToWorld(ULevel* LoadedLevel, UWorld* OwningW { // If load balancing is disabled, we must be the GSM-authoritative worker, so set Role_Authority // otherwise, load balancing is enabled, so check the lb strategy. - if (Actor->GetIsReplicated() && - (!bLoadBalancingEnabled || LoadBalanceStrategy->ShouldHaveAuthority(*Actor))) + if (Actor->GetIsReplicated()) { - Actor->Role = ROLE_Authority; - Actor->RemoteRole = ROLE_SimulatedProxy; + const bool bRoleAuthoritative = LoadBalanceStrategy->ShouldHaveAuthority(*Actor); + Actor->Role = bRoleAuthoritative ? ROLE_Authority : ROLE_SimulatedProxy; + Actor->RemoteRole = bRoleAuthoritative ? ROLE_SimulatedProxy : ROLE_Authority; } } } @@ -745,6 +752,12 @@ void USpatialNetDriver::SpatialProcessServerTravel(const FString& URL, bool bAbs return; } + if (NetDriver->LoadBalanceStrategy->GetMinimumRequiredWorkers() > 1) + { + UE_LOG(LogGameMode, Error, TEXT("Server travel is not supported on a deployment with multiple workers.")); + return; + } + NetDriver->GlobalStateManager->ResetGSM(); GameMode->StartToLeaveMap(); @@ -827,6 +840,11 @@ void USpatialNetDriver::BeginDestroy() if (WorkerEntityId != SpatialConstants::INVALID_ENTITY_ID) { Connection->SendDeleteEntityRequest(WorkerEntityId); + + // Flush the connection and wait a moment to allow the message to propagate. + // TODO: UNR-3697 - This needs to be handled more correctly + Connection->Flush(); + FPlatformProcess::Sleep(0.1f); } // Destroy the connection to disconnect from SpatialOS if we aren't meant to persist it. @@ -847,8 +865,6 @@ void USpatialNetDriver::BeginDestroy() GDKServices->GetLocalDeploymentManager()->OnDeploymentStart.Remove(SpatialDeploymentStartHandle); } #endif - - ActorGroupManager = nullptr; } void USpatialNetDriver::PostInitProperties() @@ -882,14 +898,6 @@ void USpatialNetDriver::NotifyActorDestroyed(AActor* ThisActor, bool IsSeamlessT RepChangedPropertyTrackerMap.Remove(ThisActor); const bool bIsServer = ServerConnection == nullptr; - - // Remove the record of destroyed singletons. - if (ThisActor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)) - { - // We check for this not being a server below to make sure we don't call this incorrectly in single process PIE sessions. - GlobalStateManager->RemoveSingletonInstance(ThisActor); - } - if (bIsServer) { // Check if this is a dormant entity, and if so retire the entity @@ -897,13 +905,6 @@ void USpatialNetDriver::NotifyActorDestroyed(AActor* ThisActor, bool IsSeamlessT { const Worker_EntityId EntityId = PackageMap->GetEntityIdFromObject(ThisActor); - // It is safe to check that we aren't destroying a singleton actor on a server if there is a valid entity ID and this is not a client. - if (ThisActor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton) && EntityId != SpatialConstants::INVALID_ENTITY_ID) - { - UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Removed a singleton actor on a server. This should never happen. " - "Actor: %s."), *ThisActor->GetName()); - } - // If the actor is an initially dormant startup actor that has not been replicated. if (EntityId == SpatialConstants::INVALID_ENTITY_ID && ThisActor->IsNetStartupActor() && ThisActor->GetIsReplicated() && ThisActor->HasAuthority()) { @@ -918,7 +919,7 @@ void USpatialNetDriver::NotifyActorDestroyed(AActor* ThisActor, bool IsSeamlessT { UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Retiring dormant entity that we don't have spatial authority over [%lld][%s]"), EntityId, *ThisActor->GetName()); } - Sender->RetireEntity(EntityId); + Sender->RetireEntity(EntityId, ThisActor->IsNetStartupActor()); } } @@ -1357,12 +1358,7 @@ void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConne continue; } - if (!Actor->HasAuthority()) - { - // Trying to replicate Actor which we don't have authority over. - // Remove after UNR-961 - continue; - } + check(Actor->HasAuthority()); Channel = GetOrCreateSpatialActorChannel(Actor); if ((Channel == nullptr) && (Actor->NetUpdateFrequency < 1.0f)) @@ -1414,18 +1410,13 @@ void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConne } } - // UNR-865 - Handle closing actor channels for non-relevant actors without deleting the entity. - // If the actor wasn't recently relevant, or if it was torn off, close the actor channel if it exists for this connection + // If the actor has been torn off, close the channel + // Native also checks here for !bIsRecentlyRelevant and if so closes due to relevancy, we're not doing because it's less likely + // in a SpatialOS game. Might be worth an investigation in future as a performance win - UNR-3063 if (Actor->GetTearOff() && Channel != NULL) { - // Non startup (map) actors have their channels closed immediately, which destroys them. - // Startup actors get to keep their channels open. - if (!Actor->IsNetStartupActor()) - { - UE_LOG(LogNetTraffic, Log, TEXT("- Closing channel for no longer relevant actor %s"), *Actor->GetName()); - // TODO: UNR-952 - Add code here for cleaning up actor channels from our maps. - Channel->Close(Actor->GetTearOff() ? EChannelCloseReason::TearOff : EChannelCloseReason::Relevancy); - } + UE_LOG(LogNetTraffic, Log, TEXT("- Closing channel for no longer relevant actor %s"), *Actor->GetName()); + Channel->Close(Actor->GetTearOff() ? EChannelCloseReason::TearOff : EChannelCloseReason::Relevancy); } } } @@ -1447,7 +1438,11 @@ void USpatialNetDriver::ProcessRPC(AActor* Actor, UObject* SubObject, UFunction* if (IsServer()) { // Creating channel to ensure that object will be resolvable - GetOrCreateSpatialActorChannel(CallingObject); + if (GetOrCreateSpatialActorChannel(CallingObject) == nullptr) + { + // No point processing any further since there is no channel, possibly because the actor is being destroyed. + return; + } } // If this object's class isn't present in the schema database, we will log an error and tell the @@ -1768,7 +1763,7 @@ void USpatialNetDriver::TickFlush(float DeltaTime) PollPendingLoads(); - if (IsServer() && GetSpatialOSNetConnection() != nullptr && PackageMap->IsEntityPoolReady() && bIsReadyToStart) + if (IsServer() && GetSpatialOSNetConnection() != nullptr && bIsReadyToStart) { // Update all clients. #if WITH_SERVER_CODE @@ -1793,6 +1788,10 @@ void USpatialNetDriver::TickFlush(float DeltaTime) } } + if (Connection != nullptr) + { + Connection->MaybeFlush(); + } #endif // WITH_SERVER_CODE } @@ -2207,6 +2206,13 @@ USpatialActorChannel* USpatialNetDriver::GetOrCreateSpatialActorChannel(UObject* UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("GetOrCreateSpatialActorChannel: No channel for target object but channel already present for actor. Target object: %s, actor: %s"), *TargetObject->GetPathName(), *TargetActor->GetPathName()); return ActorChannel; } + + if (TargetActor->IsPendingKillPending()) + { + UE_LOG(LogSpatialOSNetDriver, Log, TEXT("A SpatialActorChannel will not be created for %s because the Actor is being destroyed."), *GetNameSafe(TargetActor)); + return nullptr; + } + Channel = CreateSpatialActorChannel(TargetActor); } #if !UE_BUILD_SHIPPING @@ -2312,32 +2318,16 @@ USpatialActorChannel* USpatialNetDriver::CreateSpatialActorChannel(AActor* Actor USpatialNetConnection* NetConnection = GetSpatialOSNetConnection(); check(NetConnection != nullptr); - USpatialActorChannel* Channel = nullptr; - if (Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)) + USpatialActorChannel* Channel = static_cast(NetConnection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::OpenedLocally)); + if (Channel == nullptr) { - Channel = GlobalStateManager->AddSingleton(Actor); - } - else - { - Channel = static_cast(NetConnection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::OpenedLocally)); - if (Channel == nullptr) - { - UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Failed to create a channel for actor %s."), *GetNameSafe(Actor)); - } - else - { -#if ENGINE_MINOR_VERSION <= 22 - Channel->SetChannelActor(Actor); -#else - Channel->SetChannelActor(Actor, ESetChannelActorFlags::None); -#endif - } + UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Failed to create a channel for Actor %s."), *GetNameSafe(Actor)); + return Channel; } - if (Channel != nullptr) - { - Channel->RefreshAuthority(); - } + Channel->SetChannelActor(Actor, ESetChannelActorFlags::None); + + Channel->RefreshAuthority(); return Channel; } @@ -2347,12 +2337,12 @@ void USpatialNetDriver::WipeWorld(const PostWorldWipeDelegate& LoadSnapshotAfter SnapshotManager->WorldWipe(LoadSnapshotAfterWorldWipe); } -void USpatialNetDriver::DelayedSendDeleteEntityRequest(Worker_EntityId EntityId, float Delay) +void USpatialNetDriver::DelayedRetireEntity(Worker_EntityId EntityId, float Delay, bool bIsNetStartupActor) { FTimerHandle RetryTimer; - TimerManager.SetTimer(RetryTimer, [this, EntityId]() + TimerManager.SetTimer(RetryTimer, [this, EntityId, bIsNetStartupActor]() { - Sender->RetireEntity(EntityId); + Sender->RetireEntity(EntityId, bIsNetStartupActor); }, Delay, false); } @@ -2370,11 +2360,11 @@ void USpatialNetDriver::HandleStartupOpQueueing(const TArray& In if (bIsReadyToStart) { - if (GetDefault()->bEnableUnrealLoadBalancer) - { - // We know at this point that we have all the information to set the worker's interest query. - Sender->UpdateServerWorkerEntityInterestAndPosition(); - } + // Process levels which were loaded before the connection to Spatial was ready. + GetGameInstance()->CleanupCachedLevelsAfterConnection(); + + // We know at this point that we have all the information to set the worker's interest query. + Sender->UpdateServerWorkerEntityInterestAndPosition(); // We've found and dispatched all ops we need for startup, // trigger BeginPlay() on the GSM and process the queued ops. @@ -2410,22 +2400,34 @@ bool USpatialNetDriver::FindAndDispatchStartupOpsServer(const TArray FoundOps; - Worker_Op* EntityQueryResponseOp = nullptr; - FindFirstOpOfType(InOpLists, WORKER_OP_TYPE_ENTITY_QUERY_RESPONSE, &EntityQueryResponseOp); + AppendAllOpsOfType(InOpLists, WORKER_OP_TYPE_ENTITY_QUERY_RESPONSE, FoundOps); - if (EntityQueryResponseOp != nullptr) + // To correctly initialize the ServerWorkerEntity on each server during op queueing, we need to catch several ops here. + // Note that this will break if any other CreateEntity requests are issued during the startup flow. { - FoundOps.Add(EntityQueryResponseOp); - } + Worker_Op* CreateEntityResponseOp = nullptr; + FindFirstOpOfType(InOpLists, WORKER_OP_TYPE_CREATE_ENTITY_RESPONSE, &CreateEntityResponseOp); - // CreateEntityResponseOps are needed for non-GSM-authoritative server workers sending an update - // to the Runtime indicating that the worker is ready to begin play. - Worker_Op* CreateEntityResponseOp = nullptr; - FindFirstOpOfType(InOpLists, WORKER_OP_TYPE_CREATE_ENTITY_RESPONSE, &CreateEntityResponseOp); + Worker_Op* AddComponentOp = nullptr; + FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_ADD_COMPONENT, SpatialConstants::SERVER_WORKER_COMPONENT_ID, &AddComponentOp); - if (CreateEntityResponseOp != nullptr) - { - FoundOps.Add(CreateEntityResponseOp); + Worker_Op* AuthorityChangedOp = nullptr; + FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_AUTHORITY_CHANGE, SpatialConstants::SERVER_WORKER_COMPONENT_ID, &AuthorityChangedOp); + + if (CreateEntityResponseOp != nullptr) + { + FoundOps.Add(CreateEntityResponseOp); + } + + if (AddComponentOp != nullptr) + { + FoundOps.Add(AddComponentOp); + } + + if (AuthorityChangedOp != nullptr) + { + FoundOps.Add(AuthorityChangedOp); + } } // Search for entity id reservation response and process it. The entity id reservation @@ -2502,17 +2504,24 @@ bool USpatialNetDriver::FindAndDispatchStartupOpsServer(const TArrayIsEntityPoolReady() && - GlobalStateManager->IsReady() && - (!VirtualWorkerTranslator.IsValid() || VirtualWorkerTranslator->IsReady())) + if (!PackageMap->IsEntityPoolReady()) { - // Return whether or not we are ready to start - UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Ready to begin processing.")); - return true; + UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Waiting for the EntityPool to be ready.")); + return false; } - - UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Not yet ready to begin processing, still processing startup ops.")); - return false; + else if (!GlobalStateManager->IsReady()) + { + UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Waiting for the GSM to be ready.")); + return false; + } + else if (VirtualWorkerTranslator.IsValid() && !VirtualWorkerTranslator->IsReady()) + { + GlobalStateManager->QueryTranslation(); + UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Waiting for the Load balancing system to be ready.")); + return false; + } + UE_LOG(LogSpatialOSNetDriver, Log, TEXT("Ready to begin processing.")); + return true; } bool USpatialNetDriver::FindAndDispatchStartupOpsClient(const TArray& InOpLists) @@ -2557,7 +2566,6 @@ void USpatialNetDriver::SelectiveProcessOps(TArray FoundOps) } // This should only be called once on each client, in the SpatialMetricsDisplay constructor after the class is replicated to each client. -// This is enforced by the fact that the class is a Singleton spawned on servers by the SpatialNetDriver. void USpatialNetDriver::SetSpatialMetricsDisplay(ASpatialMetricsDisplay* InSpatialMetricsDisplay) { check(SpatialMetricsDisplay == nullptr); @@ -2571,8 +2579,12 @@ void USpatialNetDriver::TrackTombstone(const Worker_EntityId EntityId) } #endif +bool USpatialNetDriver::IsReady() const +{ + return bIsReadyToStart; +} + // This should only be called once on each client, in the SpatialDebugger constructor after the class is replicated to each client. -// This is enforced by the fact that the class is a Singleton spawned on servers by the SpatialNetDriver. void USpatialNetDriver::SetSpatialDebugger(ASpatialDebugger* InSpatialDebugger) { check(!IsServer()); @@ -2605,5 +2617,5 @@ FUnrealObjectRef USpatialNetDriver::GetCurrentPlayerControllerRef() void USpatialNetDriver::InitializeVirtualWorkerTranslationManager() { VirtualWorkerTranslationManager = MakeUnique(Receiver, Connection, VirtualWorkerTranslator.Get()); - VirtualWorkerTranslationManager->AddVirtualWorkerIds(LoadBalanceStrategy->GetVirtualWorkerIds()); + VirtualWorkerTranslationManager->SetNumberOfVirtualWorkers(LoadBalanceStrategy->GetMinimumRequiredWorkers()); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp index a96e8fa76c..0a4d3069db 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp @@ -2,11 +2,6 @@ #include "EngineClasses/SpatialPackageMapClient.h" -#include "EngineUtils.h" -#include "Engine/Engine.h" -#include "GameFramework/Actor.h" -#include "Kismet/GameplayStatics.h" - #include "EngineClasses/SpatialActorChannel.h" #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialNetBitReader.h" @@ -15,8 +10,12 @@ #include "Interop/SpatialSender.h" #include "Schema/UnrealObjectRef.h" #include "SpatialConstants.h" -#include "Utils/EntityPool.h" #include "Utils/SchemaOption.h" + +#include "EngineUtils.h" +#include "Engine/Engine.h" +#include "GameFramework/Actor.h" +#include "Kismet/GameplayStatics.h" #include "UObject/UObjectGlobals.h" DEFINE_LOG_CATEGORY(LogSpatialPackageMap); @@ -42,7 +41,7 @@ void GetSubobjects(UObject* ParentObject, TArray& InSubobjects) { // Walk up the outer chain and ensure that no object is PendingKill. This is required because although // EInternalObjectFlags::PendingKill prevents objects that are PendingKill themselves from getting added - // to the list, it'll still add children of PendingKill objects. This then causes an assertion within + // to the list, it'll still add children of PendingKill objects. This then causes an assertion within // FNetGUIDCache::RegisterNetGUID_Server where it again iterates up the object's owner chain, assigning // ids and ensuring that no object is set to PendingKill in the process. UObject* Outer = Object->GetOuter(); @@ -75,7 +74,7 @@ Worker_EntityId USpatialPackageMapClient::AllocateEntityIdAndResolveActor(AActor return SpatialConstants::INVALID_ENTITY_ID; } - Worker_EntityId EntityId = EntityPool->GetNextEntityId(); + Worker_EntityId EntityId = AllocateEntityId(); if (EntityId == SpatialConstants::INVALID_ENTITY_ID) { UE_LOG(LogSpatialPackageMap, Error, TEXT("Unable to retrieve an Entity ID for Actor: %s"), *Actor->GetName()); @@ -112,12 +111,6 @@ FNetworkGUID USpatialPackageMapClient::TryResolveObjectAsEntity(UObject* Value) return NetGUID; } - if (Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)) - { - // Singletons will always go through GlobalStateManager first. - return NetGUID; - } - // Resolve as an entity if it is an unregistered actor if (Actor->Role == ROLE_Authority && GetEntityIdFromObject(Actor) == SpatialConstants::INVALID_ENTITY_ID) { @@ -265,39 +258,49 @@ Worker_EntityId USpatialPackageMapClient::GetEntityIdFromObject(const UObject* O return GetUnrealObjectRefFromNetGUID(NetGUID).Entity; } -AActor* USpatialPackageMapClient::GetSingletonByClassRef(const FUnrealObjectRef& SingletonClassRef) +bool USpatialPackageMapClient::CanClientLoadObject(UObject* Object) { - if (UClass* SingletonClass = Cast(GetObjectFromUnrealObjectRef(SingletonClassRef))) - { - TArray FoundActors; - // USpatialPackageMapClient is an inner object of UNetConnection, - // which in turn contains a NetDriver and gets the UWorld it references - UGameplayStatics::GetAllActorsOfClass(this, SingletonClass, FoundActors); - - // There should be only one singleton actor per class - if (FoundActors.Num() == 1) - { - return FoundActors[0]; - } + FNetworkGUID NetGUID = GetNetGUIDFromObject(Object); + return GuidCache->CanClientLoadObject(Object, NetGUID); +} - FString FullPath; - SpatialGDK::GetFullPathFromUnrealObjectReference(SingletonClassRef, FullPath); - UE_LOG(LogSpatialPackageMap, Verbose, TEXT("GetSingletonByClassRef: Found %d actors for singleton class: %s"), FoundActors.Num(), *FullPath); - return nullptr; +AActor* USpatialPackageMapClient::GetUniqueActorInstanceByClassRef(const FUnrealObjectRef& UniqueObjectClassRef) +{ + if (UClass* UniqueObjectClass = Cast(GetObjectFromUnrealObjectRef(UniqueObjectClassRef))) + { + return GetUniqueActorInstanceByClass(UniqueObjectClass); } else { FString FullPath; - SpatialGDK::GetFullPathFromUnrealObjectReference(SingletonClassRef, FullPath); - UE_LOG(LogSpatialPackageMap, Warning, TEXT("GetSingletonByClassRef: Can't resolve singleton class: %s"), *FullPath); + SpatialGDK::GetFullPathFromUnrealObjectReference(UniqueObjectClassRef, FullPath); + UE_LOG(LogSpatialPackageMap, Warning, TEXT("Can't resolve unique object class: %s"), *FullPath); return nullptr; } } -bool USpatialPackageMapClient::CanClientLoadObject(UObject* Object) +AActor* USpatialPackageMapClient::GetUniqueActorInstanceByClass(UClass* UniqueObjectClass) const { - FNetworkGUID NetGUID = GetNetGUIDFromObject(Object); - return GuidCache->CanClientLoadObject(Object, NetGUID); + check(UniqueObjectClass != nullptr); + + TArray FoundActors; + // USpatialPackageMapClient is an inner object of UNetConnection, + // which in turn contains a NetDriver and gets the UWorld it references. + UGameplayStatics::GetAllActorsOfClass(this, UniqueObjectClass, FoundActors); + + // There should be only one Actor per class. + if (FoundActors.Num() == 1) + { + return FoundActors[0]; + } + + UE_LOG(LogSpatialPackageMap, Warning, TEXT("Found %d Actors for class: %s. There should only be one."), FoundActors.Num(), *UniqueObjectClass->GetName()); + return nullptr; +} + +Worker_EntityId USpatialPackageMapClient::AllocateEntityId() +{ + return EntityPool->GetNextEntityId(); } bool USpatialPackageMapClient::IsEntityPoolReady() const @@ -305,6 +308,12 @@ bool USpatialPackageMapClient::IsEntityPoolReady() const return (EntityPool != nullptr) && (EntityPool->IsReady()); } +FEntityPoolReadyEvent& USpatialPackageMapClient::GetEntityPoolReadyDelegate() +{ + check(bIsServer); + return EntityPool->GetEntityPoolReadyDelegate(); +} + bool USpatialPackageMapClient::SerializeObject(FArchive& Ar, UClass* InClass, UObject*& Obj, FNetworkGUID *OutNetGUID) { // Super::SerializeObject is not called here on purpose diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp index d376bf6ff2..a1539d9d4b 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialVirtualWorkerTranslationManager.cpp @@ -19,14 +19,15 @@ SpatialVirtualWorkerTranslationManager::SpatialVirtualWorkerTranslationManager( , bWorkerEntityQueryInFlight(false) {} -void SpatialVirtualWorkerTranslationManager::AddVirtualWorkerIds(const TSet& InVirtualWorkerIds) +void SpatialVirtualWorkerTranslationManager::SetNumberOfVirtualWorkers(const uint32 NumVirtualWorkers) { + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT("TranslationManager is configured to look for %d workers"), NumVirtualWorkers); + // Currently, this should only be called once on startup. In the future we may allow for more // flexibility. - check(UnassignedVirtualWorkers.IsEmpty()); - for (VirtualWorkerId VirtualWorkerId : InVirtualWorkerIds) + for (uint32 i = 1; i <= NumVirtualWorkers; i++) { - UnassignedVirtualWorkers.Enqueue(VirtualWorkerId); + UnassignedVirtualWorkers.Enqueue(i); } } @@ -172,7 +173,7 @@ void SpatialVirtualWorkerTranslationManager::ServerWorkerEntityQueryDelegate(con } else { - UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT(" Processing ServerWorker Entity query response")); + UE_LOG(LogSpatialVirtualWorkerTranslationManager, Log, TEXT("Processing ServerWorker Entity query response")); ConstructVirtualWorkerMappingFromQueryResponse(Op); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp index 935c573e45..4f8df69bcc 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialConnectionManager.cpp @@ -7,7 +7,9 @@ #include "Async/Async.h" #include "Improbable/SpatialEngineConstants.h" +#include "Improbable/SpatialGDKSettingsBridge.h" #include "Misc/Paths.h" +#include "Modules/ModuleManager.h" #include "Interop/Connection/SpatialWorkerConnection.h" #include "SpatialGDKSettings.h" @@ -23,14 +25,33 @@ struct ConfigureConnection : Config(InConfig) , Params() , WorkerType(*Config.WorkerType) - , ProtocolLogPrefix(*FormatProtocolPrefix()) + , WorkerSDKLogFilePrefix(*FormatWorkerSDKLogFilePrefix()) { Params = Worker_DefaultConnectionParameters(); Params.worker_type = WorkerType.Get(); - Params.enable_protocol_logging_at_startup = Config.EnableProtocolLoggingAtStartup; - Params.protocol_logging.log_prefix = ProtocolLogPrefix.Get(); + Logsink.logsink_type = WORKER_LOGSINK_TYPE_ROTATING_FILE; + Logsink.rotating_logfile_parameters.log_prefix = WorkerSDKLogFilePrefix.Get(); + Logsink.rotating_logfile_parameters.max_log_files = 1; + // TODO: When upgrading to Worker SDK 14.6.2, remove the WorkerSDKLogFileSize parameter and set this to 0 for infinite file size + Logsink.rotating_logfile_parameters.max_log_file_size_bytes = Config.WorkerSDKLogFileSize; + + uint32_t Categories = 0; + if (Config.EnableWorkerSDKOpLogging) + { + Categories |= WORKER_LOG_CATEGORY_API; + } + if (Config.EnableWorkerSDKProtocolLogging) + { + Categories |= WORKER_LOG_CATEGORY_NETWORK_STATUS | WORKER_LOG_CATEGORY_NETWORK_TRAFFIC; + } + Logsink.filter_parameters.categories = Categories; + Logsink.filter_parameters.level = Config.WorkerSDKLogLevel; + + Params.logsinks = &Logsink; + Params.logsink_count = 1; + Params.enable_logging_at_startup = Categories != 0; Params.component_vtable_count = 0; Params.default_component_vtable = &DefaultVtable; @@ -58,45 +79,42 @@ struct ConfigureConnection Params.network.modular_kcp.upstream_heartbeat = &HeartbeatParams; #endif - if (!bConnectAsClient && GetDefault()->bUseSecureServerConnection) - { - Params.network.modular_kcp.security_type = WORKER_NETWORK_SECURITY_TYPE_TLS; - Params.network.modular_tcp.security_type = WORKER_NETWORK_SECURITY_TYPE_TLS; - } - else if (bConnectAsClient && GetDefault()->bUseSecureClientConnection) + // Use insecure connections default. + Params.network.modular_kcp.security_type = WORKER_NETWORK_SECURITY_TYPE_INSECURE; + Params.network.modular_tcp.security_type = WORKER_NETWORK_SECURITY_TYPE_INSECURE; + + // Override the security type to be secure only if the user has requested it and we are not using an editor build. + if ((!bConnectAsClient && GetDefault()->bUseSecureServerConnection) || (bConnectAsClient && GetDefault()->bUseSecureClientConnection)) { +#if WITH_EDITOR + UE_LOG(LogSpatialWorkerConnection, Warning, TEXT("Secure connection requested but this is not supported in Editor builds. Connection will be insecure.")); +#else Params.network.modular_kcp.security_type = WORKER_NETWORK_SECURITY_TYPE_TLS; Params.network.modular_tcp.security_type = WORKER_NETWORK_SECURITY_TYPE_TLS; - } - else - { - Params.network.modular_kcp.security_type = WORKER_NETWORK_SECURITY_TYPE_INSECURE; - Params.network.modular_tcp.security_type = WORKER_NETWORK_SECURITY_TYPE_INSECURE; +#endif } Params.enable_dynamic_components = true; } - FString FormatProtocolPrefix() const + FString FormatWorkerSDKLogFilePrefix() const { - FString FinalProtocolLoggingPrefix = FPaths::ConvertRelativePathToFull(FPaths::ProjectLogDir()); - if (!Config.ProtocolLoggingPrefix.IsEmpty()) + FString FinalLogFilePrefix = FPaths::ConvertRelativePathToFull(FPaths::ProjectLogDir()); + if (!Config.WorkerSDKLogPrefix.IsEmpty()) { - FinalProtocolLoggingPrefix += Config.ProtocolLoggingPrefix; + FinalLogFilePrefix += Config.WorkerSDKLogPrefix; } - else - { - FinalProtocolLoggingPrefix += Config.WorkerId; - } - return FinalProtocolLoggingPrefix; + FinalLogFilePrefix += Config.WorkerId + TEXT("-"); + return FinalLogFilePrefix; } const FConnectionConfig& Config; Worker_ConnectionParameters Params; FTCHARToUTF8 WorkerType; - FTCHARToUTF8 ProtocolLogPrefix; + FTCHARToUTF8 WorkerSDKLogFilePrefix; Worker_ComponentVtable DefaultVtable{}; Worker_CompressionParameters EnableCompressionParams{}; + Worker_LogsinkParameters Logsink{}; #if WITH_EDITOR Worker_HeartbeatParameters HeartbeatParams{ WORKER_DEFAULTS_HEARTBEAT_INTERVAL_MILLIS, MAX_int64 }; @@ -154,13 +172,13 @@ void USpatialConnectionManager::Connect(bool bInitAsClient, uint32 PlayInEditorI bConnectAsClient = bInitAsClient; - const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); - if (SpatialGDKSettings->bUseDevelopmentAuthenticationFlow && bInitAsClient) + const ISpatialGDKEditorModule* SpatialGDKEditorModule = FModuleManager::GetModulePtr("SpatialGDKEditor"); + if (SpatialGDKEditorModule != nullptr && SpatialGDKEditorModule->ShouldConnectToCloudDeployment() && bInitAsClient) { - DevAuthConfig.Deployment = SpatialGDKSettings->DevelopmentDeploymentToConnect; + DevAuthConfig.Deployment = SpatialGDKEditorModule->GetSpatialOSCloudDeploymentName(); DevAuthConfig.WorkerType = SpatialConstants::DefaultClientWorkerType.ToString(); DevAuthConfig.UseExternalIp = true; - StartDevelopmentAuth(SpatialGDKSettings->DevelopmentAuthenticationToken); + StartDevelopmentAuth(SpatialGDKEditorModule->GetDevAuthToken()); return; } @@ -205,24 +223,36 @@ void USpatialConnectionManager::ProcessLoginTokensResponse(const Worker_Alpha_Lo return; } - const FString& DeploymentToConnect = DevAuthConfig.Deployment; + FString DeploymentToConnect = DevAuthConfig.Deployment; // If not set, use the first deployment. It can change every query if you have multiple items available, because the order is not guaranteed. if (DeploymentToConnect.IsEmpty()) { DevAuthConfig.LoginToken = FString(LoginTokens->login_tokens[0].login_token); + DeploymentToConnect = UTF8_TO_TCHAR(LoginTokens->login_tokens[0].deployment_name); } else { + bool bFoundDeployment = false; + for (uint32 i = 0; i < LoginTokens->login_token_count; i++) { - FString DeploymentName = FString(LoginTokens->login_tokens[i].deployment_name); + FString DeploymentName = UTF8_TO_TCHAR(LoginTokens->login_tokens[i].deployment_name); if (DeploymentToConnect.Compare(DeploymentName) == 0) { DevAuthConfig.LoginToken = FString(LoginTokens->login_tokens[i].login_token); + bFoundDeployment = true; break; } } + + if (!bFoundDeployment) + { + OnConnectionFailure(WORKER_CONNECTION_STATUS_CODE_NETWORK_ERROR, FString::Printf(TEXT("Deployment not found! Make sure that the deployment with name '%s' is running and has the 'dev_login' deployment tag."), *DeploymentToConnect)); + return; + } } + + UE_LOG(LogSpatialConnectionManager, Log, TEXT("Dev auth flow: connecting to deployment \"%s\""), *DeploymentToConnect); ConnectToLocator(&DevAuthConfig); } @@ -287,7 +317,7 @@ void USpatialConnectionManager::ConnectToReceptionist(uint32 PlayInEditorID) ConfigureConnection ConnectionConfig(ReceptionistConfig, bConnectAsClient); Worker_ConnectionFuture* ConnectionFuture = Worker_ConnectAsync( - TCHAR_TO_UTF8(*ReceptionistConfig.GetReceptionistHost()), ReceptionistConfig.ReceptionistPort, + TCHAR_TO_UTF8(*ReceptionistConfig.GetReceptionistHost()), ReceptionistConfig.GetReceptionistPort(), TCHAR_TO_UTF8(*ReceptionistConfig.WorkerId), &ConnectionConfig.Params); FinishConnecting(ConnectionFuture); @@ -380,6 +410,7 @@ bool USpatialConnectionManager::TrySetupConnectionConfigFromCommandLine(const FS bool bSuccessfullyLoaded = LocatorConfig.TryLoadCommandLineArgs(); if (bSuccessfullyLoaded) { + UE_LOG(LogSpatialWorkerConnection, Log, TEXT("Successfully set up locator config from command line arguments")); SetConnectionType(ESpatialConnectionType::Locator); LocatorConfig.WorkerType = SpatialWorkerType; } @@ -388,11 +419,13 @@ bool USpatialConnectionManager::TrySetupConnectionConfigFromCommandLine(const FS bSuccessfullyLoaded = DevAuthConfig.TryLoadCommandLineArgs(); if (bSuccessfullyLoaded) { + UE_LOG(LogSpatialWorkerConnection, Log, TEXT("Successfully set up dev auth config from command line arguments")); SetConnectionType(ESpatialConnectionType::DevAuthFlow); DevAuthConfig.WorkerType = SpatialWorkerType; } else { + UE_LOG(LogSpatialWorkerConnection, Log, TEXT("Setting up receptionist config from command line arguments")); bSuccessfullyLoaded = ReceptionistConfig.TryLoadCommandLineArgs(); SetConnectionType(ESpatialConnectionType::Receptionist); ReceptionistConfig.WorkerType = SpatialWorkerType; @@ -404,6 +437,8 @@ bool USpatialConnectionManager::TrySetupConnectionConfigFromCommandLine(const FS void USpatialConnectionManager::SetupConnectionConfigFromURL(const FURL& URL, const FString& SpatialWorkerType) { + UE_LOG(LogSpatialWorkerConnection, Log, TEXT("Setting up connection config from URL")); + if (URL.HasOption(TEXT("locator")) || URL.HasOption(TEXT("devauth"))) { FString LocatorHostOverride; @@ -448,20 +483,8 @@ void USpatialConnectionManager::SetupConnectionConfigFromURL(const FURL& URL, co { SetConnectionType(ESpatialConnectionType::Receptionist); - // If we have a non-empty host then use this to connect. If not - use the default configured in FReceptionistConfig initialisation. - if (!URL.Host.IsEmpty()) - { - ReceptionistConfig.SetReceptionistHost(URL.Host); - } - + ReceptionistConfig.SetupFromURL(URL); ReceptionistConfig.WorkerType = SpatialWorkerType; - - const TCHAR* UseExternalIpForBridge = TEXT("useExternalIpForBridge"); - if (URL.HasOption(UseExternalIpForBridge)) - { - FString UseExternalIpOption = URL.GetOption(UseExternalIpForBridge, TEXT("")); - ReceptionistConfig.UseExternalIp = !UseExternalIpOption.Equals(TEXT("false"), ESearchCase::IgnoreCase); - } } } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp index d49a443b21..717df64bad 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp @@ -20,6 +20,16 @@ void USpatialWorkerConnection::SetConnection(Worker_Connection* WorkerConnection { if (OpsProcessingThread == nullptr) { + bool bCanWake = SpatialGDKSettings->bWorkerFlushAfterOutgoingNetworkOp; + float WaitTimeS = 1.0f / (GetDefault()->OpsUpdateRate); + int32 WaitTimeMs = static_cast(FTimespan::FromSeconds(WaitTimeS).GetTotalMilliseconds()); + if (WaitTimeMs <= 0) + { + UE_LOG(LogSpatialWorkerConnection, Warning, TEXT("Clamping wait time for worker ops thread to the minimum rate of 1ms.")); + WaitTimeMs = 1; + } + ThreadWaitCondition.Emplace(bCanWake, WaitTimeMs); + InitializeOpsProcessingThread(); } } @@ -43,6 +53,8 @@ void USpatialWorkerConnection::DestroyConnection() OpsProcessingThread = nullptr; } + ThreadWaitCondition.Reset(); // Set TOptional value to null + if (WorkerConnection) { AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [WorkerConnection = WorkerConnection] @@ -167,13 +179,6 @@ void USpatialWorkerConnection::CacheWorkerAttributes() } } -bool USpatialWorkerConnection::Init() -{ - OpsUpdateInterval = 1.0f / GetDefault()->OpsUpdateRate; - - return true; -} - uint32 USpatialWorkerConnection::Run() { const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); @@ -181,7 +186,7 @@ uint32 USpatialWorkerConnection::Run() while (KeepRunning) { - FPlatformProcess::Sleep(OpsUpdateInterval); + ThreadWaitCondition->Wait(); QueueLatestOpList(); ProcessOutgoingMessages(); } @@ -217,8 +222,11 @@ void USpatialWorkerConnection::QueueLatestOpList() void USpatialWorkerConnection::ProcessOutgoingMessages() { + bool bSentData = false; while (!OutgoingMessagesQueue.IsEmpty()) { + bSentData = true; + TUniquePtr OutgoingMessage; OutgoingMessagesQueue.Dequeue(OutgoingMessage); @@ -388,6 +396,7 @@ void USpatialWorkerConnection::ProcessOutgoingMessages() TArray WorkerHistogramMetrics; TArray> WorkerHistogramMetricBuckets; WorkerHistogramMetrics.SetNum(Message->Metrics.HistogramMetrics.Num()); + WorkerHistogramMetricBuckets.SetNum(Message->Metrics.HistogramMetrics.Num()); for (int i = 0; i < Message->Metrics.HistogramMetrics.Num(); i++) { WorkerHistogramMetrics[i].key = Message->Metrics.HistogramMetrics[i].Key.c_str(); @@ -417,6 +426,34 @@ void USpatialWorkerConnection::ProcessOutgoingMessages() } } } + + // Flush worker API calls + if (bSentData) + { + Worker_Connection_Alpha_Flush(WorkerConnection); + } +} + +void USpatialWorkerConnection::MaybeFlush() +{ + const USpatialGDKSettings* Settings = GetDefault(); + if (Settings->bWorkerFlushAfterOutgoingNetworkOp) + { + Flush(); + } +} + +void USpatialWorkerConnection::Flush() +{ + const USpatialGDKSettings* Settings = GetDefault(); + if (Settings->bRunSpatialWorkerConnectionOnGameThread) + { + ProcessOutgoingMessages(); + } + else if (ensure(ThreadWaitCondition.IsSet())) + { + ThreadWaitCondition->Wake(); // No-op if wake is not enabled. + } } template diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp index 51a792d0ec..b9f6da832a 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp @@ -54,23 +54,18 @@ void UGlobalStateManager::Init(USpatialNetDriver* InNetDriver) } } #endif // WITH_EDITOR - + bAcceptingPlayers = false; bHasSentReadyForVirtualWorkerAssignment = false; bCanBeginPlay = false; bCanSpawnWithAuthority = false; -} - -void UGlobalStateManager::ApplySingletonManagerData(const Worker_ComponentData& Data) -{ - Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); - SingletonNameToEntityId = GetStringToEntityMapFromSchema(ComponentObject, SpatialConstants::SINGLETON_MANAGER_SINGLETON_NAME_TO_ENTITY_ID); + bTranslationQueryInFlight = false; } void UGlobalStateManager::ApplyDeploymentMapData(const Worker_ComponentData& Data) { Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); - + SetDeploymentMapURL(GetStringFromSchema(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_MAP_URL_ID)); bAcceptingPlayers = GetBoolFromSchema(ComponentObject, SpatialConstants::DEPLOYMENT_MAP_ACCEPTING_PLAYERS_ID); @@ -98,8 +93,10 @@ void UGlobalStateManager::TrySendWorkerReadyToBeginPlay() // AddComponent. This is important for handling startup Actors correctly in a zoned // environment. const bool bHasReceivedStartupActorData = StaticComponentView->HasComponent(GlobalStateManagerEntityId, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID); - const bool bWorkerEntityCreated = NetDriver->WorkerEntityId != SpatialConstants::INVALID_ENTITY_ID; - if (bHasSentReadyForVirtualWorkerAssignment || !bHasReceivedStartupActorData || !bWorkerEntityCreated) + const bool bWorkerEntityReady = NetDriver->WorkerEntityId != SpatialConstants::INVALID_ENTITY_ID && + StaticComponentView->HasAuthority(NetDriver->WorkerEntityId, SpatialConstants::SERVER_WORKER_COMPONENT_ID); + + if (bHasSentReadyForVirtualWorkerAssignment || !bHasReceivedStartupActorData || !bWorkerEntityReady) { return; } @@ -114,16 +111,6 @@ void UGlobalStateManager::TrySendWorkerReadyToBeginPlay() NetDriver->Connection->SendComponentUpdate(NetDriver->WorkerEntityId, &Update); } -void UGlobalStateManager::ApplySingletonManagerUpdate(const Worker_ComponentUpdate& Update) -{ - Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); - - if (Schema_GetObjectCount(ComponentObject, SpatialConstants::SINGLETON_MANAGER_SINGLETON_NAME_TO_ENTITY_ID) > 0) - { - SingletonNameToEntityId = GetStringToEntityMapFromSchema(ComponentObject, SpatialConstants::SINGLETON_MANAGER_SINGLETON_NAME_TO_ENTITY_ID); - } -} - void UGlobalStateManager::ApplyDeploymentMapUpdate(const Worker_ComponentUpdate& Update) { Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); @@ -162,7 +149,7 @@ void UGlobalStateManager::SendShutdownMultiProcessRequest() * Standard UnrealEngine behavior is to call TerminateProc on external processes and there is no method to send any messaging * to those external process. * The GDK requires shutdown code to be ran for workers to disconnect cleanly so instead of abruptly shutting down the server worker, - * just send a command to the worker to begin it's shutdown phase. + * just send a command to the worker to begin it's shutdown phase. */ Worker_CommandRequest CommandRequest = {}; CommandRequest.component_id = SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID; @@ -177,8 +164,8 @@ void UGlobalStateManager::ReceiveShutdownMultiProcessRequest() if (NetDriver && NetDriver->GetNetMode() == NM_DedicatedServer) { UE_LOG(LogGlobalStateManager, Log, TEXT("Received shutdown multi-process request.")); - - // Since the server works are shutting down, set reset the accepting_players flag to false to prevent race conditions where the client connects quicker than the server. + + // Since the server works are shutting down, set reset the accepting_players flag to false to prevent race conditions where the client connects quicker than the server. SetAcceptingPlayers(false); DeploymentSessionId = 0; SendSessionIdUpdate(); @@ -237,220 +224,14 @@ void UGlobalStateManager::ApplyStartupActorManagerUpdate(const Worker_ComponentU bCanSpawnWithAuthority = true; } -void UGlobalStateManager::LinkExistingSingletonActor(const UClass* SingletonActorClass) -{ - const Worker_EntityId* SingletonEntityIdPtr = SingletonNameToEntityId.Find(SingletonActorClass->GetPathName()); - if (SingletonEntityIdPtr == nullptr) - { - // No entry in SingletonNameToEntityId for this singleton class type - UE_LOG(LogGlobalStateManager, Verbose, TEXT("LinkExistingSingletonActor %s failed to find entry"), *SingletonActorClass->GetName()); - return; - } - - const Worker_EntityId SingletonEntityId = *SingletonEntityIdPtr; - if (SingletonEntityId == SpatialConstants::INVALID_ENTITY_ID) - { - // Singleton Entity hasn't been created yet - UE_LOG(LogGlobalStateManager, Log, TEXT("LinkExistingSingletonActor %s entity id is invalid"), *SingletonActorClass->GetName()); - return; - } - - TPair* ActorChannelPair = SingletonClassPathToActorChannels.Find(SingletonActorClass->GetPathName()); - if (ActorChannelPair == nullptr) - { - // Dynamically spawn singleton actor if we have queued up data - ala USpatialReceiver::ReceiveActor - JIRA: 735 - - // No local actor has registered itself as replicatible on this worker - UE_LOG(LogGlobalStateManager, Log, TEXT("LinkExistingSingletonActor no actor registered for class %s"), *SingletonActorClass->GetName()); - return; - } - - AActor* SingletonActor = ActorChannelPair->Key; - USpatialActorChannel*& Channel = ActorChannelPair->Value; - - if (Channel != nullptr) - { - // Channel has already been setup - UE_LOG(LogGlobalStateManager, Verbose, TEXT("UGlobalStateManager::LinkExistingSingletonActor channel already setup for %s"), *SingletonActorClass->GetName()); - return; - } - - // If we have previously queued up data for this entity, apply it - UNR-734 - - // We're now ready to start replicating this actor, create a channel - USpatialNetConnection* Connection = Cast(NetDriver->ClientConnections[0]); - - Channel = Cast(Connection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::OpenedLocally)); - - if (StaticComponentView->HasAuthority(SingletonEntityId, SpatialConstants::POSITION_COMPONENT_ID)) - { - SingletonActor->Role = ROLE_Authority; - SingletonActor->RemoteRole = ROLE_SimulatedProxy; - } - else - { - SingletonActor->Role = ROLE_SimulatedProxy; - SingletonActor->RemoteRole = ROLE_Authority; - } - - // Since the entity already exists, we have to handle setting up the PackageMap properly for this Actor - NetDriver->PackageMap->ResolveEntityActor(SingletonActor, SingletonEntityId); - -#if ENGINE_MINOR_VERSION <= 22 - Channel->SetChannelActor(SingletonActor); -#else - Channel->SetChannelActor(SingletonActor, ESetChannelActorFlags::None); -#endif - - UE_LOG(LogGlobalStateManager, Log, TEXT("Linked Singleton Actor %s with id %d"), *SingletonActor->GetClass()->GetName(), SingletonEntityId); -} - -void UGlobalStateManager::LinkAllExistingSingletonActors() -{ - // Early out for clients as they receive Singleton Actors via the normal Unreal replicated actor flow - if (!NetDriver->IsServer()) - { - return; - } - - for (const auto& Pair : SingletonNameToEntityId) - { - UClass* SingletonActorClass = LoadObject(nullptr, *Pair.Key); - if (SingletonActorClass == nullptr) - { - UE_LOG(LogGlobalStateManager, Error, TEXT("Failed to find Singleton Actor Class: %s"), *Pair.Key); - continue; - } - - LinkExistingSingletonActor(SingletonActorClass); - } -} - -USpatialActorChannel* UGlobalStateManager::AddSingleton(AActor* SingletonActor) -{ - check(SingletonActor->GetIsReplicated()); - - UClass* SingletonActorClass = SingletonActor->GetClass(); - - TPair& ActorChannelPair = SingletonClassPathToActorChannels.FindOrAdd(SingletonActorClass->GetPathName()); - USpatialActorChannel*& Channel = ActorChannelPair.Value; - check(ActorChannelPair.Key == nullptr || ActorChannelPair.Key == SingletonActor); - ActorChannelPair.Key = SingletonActor; - - // Just return the channel if it's already been setup - if (Channel != nullptr) - { - UE_LOG(LogGlobalStateManager, Log, TEXT("AddSingleton called when channel already setup: %s"), *SingletonActor->GetName()); - return Channel; - } - - bool bHasGSMAuthority = NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID); - if (bHasGSMAuthority) - { - // We have control over the GSM, so can safely setup a new channel and let it allocate an entity id - USpatialNetConnection* Connection = Cast(NetDriver->ClientConnections[0]); - Channel = Cast(Connection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::OpenedLocally)); - - // If entity id already exists for this singleton, set the actor to it - // Otherwise SetChannelActor will issue a new entity id request - if (const Worker_EntityId* SingletonEntityId = SingletonNameToEntityId.Find(SingletonActorClass->GetPathName())) - { - check(NetDriver->PackageMap->GetObjectFromEntityId(*SingletonEntityId) == nullptr); - NetDriver->PackageMap->ResolveEntityActor(SingletonActor, *SingletonEntityId); - if (!StaticComponentView->HasAuthority(*SingletonEntityId, SpatialConstants::POSITION_COMPONENT_ID)) - { - SingletonActor->Role = ROLE_SimulatedProxy; - SingletonActor->RemoteRole = ROLE_Authority; - } - } - -#if ENGINE_MINOR_VERSION <= 22 - Channel->SetChannelActor(SingletonActor); -#else - Channel->SetChannelActor(SingletonActor, ESetChannelActorFlags::None); -#endif - UE_LOG(LogGlobalStateManager, Log, TEXT("Started replication of Singleton Actor %s"), *SingletonActorClass->GetName()); - } - else - { - // We don't have control over the GSM, but we may have received the entity id for this singleton already - LinkExistingSingletonActor(SingletonActorClass); - } - - return Channel; -} - -void UGlobalStateManager::RemoveSingletonInstance(const AActor* SingletonActor) -{ - check(SingletonActor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)); - - SingletonClassPathToActorChannels.Remove(SingletonActor->GetClass()->GetPathName()); -} - -void UGlobalStateManager::RemoveAllSingletons() -{ - SingletonClassPathToActorChannels.Reset(); -} - -void UGlobalStateManager::RegisterSingletonChannel(AActor* SingletonActor, USpatialActorChannel* SingletonChannel) -{ - TPair& ActorChannelPair = SingletonClassPathToActorChannels.FindOrAdd(SingletonActor->GetClass()->GetPathName()); - - check(ActorChannelPair.Key == nullptr || ActorChannelPair.Key == SingletonActor); - check(ActorChannelPair.Value == nullptr || ActorChannelPair.Value == SingletonChannel); - - ActorChannelPair.Key = SingletonActor; - ActorChannelPair.Value = SingletonChannel; -} - -void UGlobalStateManager::ExecuteInitialSingletonActorReplication() -{ - for (auto& ClassToActorChannel : SingletonClassPathToActorChannels) - { - auto& ActorChannelPair = ClassToActorChannel.Value; - AddSingleton(ActorChannelPair.Key); - } -} - -void UGlobalStateManager::UpdateSingletonEntityId(const FString& ClassName, const Worker_EntityId SingletonEntityId) -{ - Worker_EntityId& EntityId = SingletonNameToEntityId.FindOrAdd(ClassName); - EntityId = SingletonEntityId; - - if (!NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID)) - { - UE_LOG(LogGlobalStateManager, Warning, TEXT("UpdateSingletonEntityId: no authority over the GSM! Update will not be sent. Singleton class: %s, entity: %lld"), *ClassName, SingletonEntityId); - return; - } - - FWorkerComponentUpdate Update = {}; - Update.component_id = SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID; - Update.schema_type = Schema_CreateComponentUpdate(); - Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update.schema_type); - - AddStringToEntityMapToSchema(UpdateObject, 1, SingletonNameToEntityId); - - NetDriver->Connection->SendComponentUpdate(GlobalStateManagerEntityId, &Update); -} - -bool UGlobalStateManager::IsSingletonEntity(Worker_EntityId EntityId) const -{ - for (const auto& Pair : SingletonNameToEntityId) - { - if (Pair.Value == EntityId) - { - return true; - } - } - return false; -} - void UGlobalStateManager::SetDeploymentState() { check(NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID)); + UWorld* CurrentWorld = NetDriver->GetWorld(); + // Send the component update that we can now accept players. - UE_LOG(LogGlobalStateManager, Log, TEXT("Setting deployment URL to '%s'"), *NetDriver->GetWorld()->URL.Map); + UE_LOG(LogGlobalStateManager, Log, TEXT("Setting deployment URL to '%s'"), *CurrentWorld->URL.Map); UE_LOG(LogGlobalStateManager, Log, TEXT("Setting schema hash to '%u'"), NetDriver->ClassInfoManager->SchemaDatabase->SchemaDescriptorHash); FWorkerComponentUpdate Update = {}; @@ -459,7 +240,7 @@ void UGlobalStateManager::SetDeploymentState() Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update.schema_type); // Set the map URL on the GSM. - AddStringToSchema(UpdateObject, SpatialConstants::DEPLOYMENT_MAP_MAP_URL_ID, NetDriver->GetWorld()->URL.Map); + AddStringToSchema(UpdateObject, SpatialConstants::DEPLOYMENT_MAP_MAP_URL_ID, CurrentWorld->RemovePIEPrefix(CurrentWorld->URL.Map)); // Set the schema hash for connecting workers to check against Schema_AddUint32(UpdateObject, SpatialConstants::DEPLOYMENT_MAP_SCHEMA_HASH, NetDriver->ClassInfoManager->SchemaDatabase->SchemaDescriptorHash); @@ -516,11 +297,6 @@ void UGlobalStateManager::AuthorityChanged(const Worker_AuthorityChangeOp& AuthO SetAcceptingPlayers(true); break; } - case SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID: - { - ExecuteInitialSingletonActorReplication(); - break; - } case SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID: { // The bCanSpawnWithAuthority member determines whether a server-side worker @@ -548,7 +324,6 @@ bool UGlobalStateManager::HandlesComponent(const Worker_ComponentId ComponentId) { switch (ComponentId) { - case SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID: case SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID: case SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID: case SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID: @@ -560,21 +335,12 @@ bool UGlobalStateManager::HandlesComponent(const Worker_ComponentId ComponentId) void UGlobalStateManager::ResetGSM() { - UE_LOG(LogGlobalStateManager, Display, TEXT("GlobalStateManager singletons are being reset. Session restarting.")); + UE_LOG(LogGlobalStateManager, Display, TEXT("GlobalStateManager not accepting players and resetting BeginPlay lifecycle properties. Session restarting.")); - SingletonNameToEntityId.Empty(); SetAcceptingPlayers(false); // Reset the BeginPlay flag so Startup Actors are properly managed. SendCanBeginPlayUpdate(false); - - // Reset the Singleton map so Singletons are recreated. - FWorkerComponentUpdate Update = {}; - Update.component_id = SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID; - Update.schema_type = Schema_CreateComponentUpdate(); - Schema_AddComponentUpdateClearedField(Update.schema_type, SpatialConstants::SINGLETON_MANAGER_SINGLETON_NAME_TO_ENTITY_ID); - - NetDriver->Connection->SendComponentUpdate(GlobalStateManagerEntityId, &Update); } void UGlobalStateManager::BeginDestroy() @@ -590,26 +356,17 @@ void UGlobalStateManager::BeginDestroy() // Reset the BeginPlay flag so Startup Actors are properly managed. SendCanBeginPlayUpdate(false); - // Reset the Singleton map so Singletons are recreated. - FWorkerComponentUpdate Update = {}; - Update.component_id = SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID; - Update.schema_type = Schema_CreateComponentUpdate(); - Schema_AddComponentUpdateClearedField(Update.schema_type, SpatialConstants::SINGLETON_MANAGER_SINGLETON_NAME_TO_ENTITY_ID); - - NetDriver->Connection->SendComponentUpdate(GlobalStateManagerEntityId, &Update); + // Flush the connection and wait a moment to allow the message to propagate. + // TODO: UNR-3697 - This needs to be handled more correctly + NetDriver->Connection->Flush(); + FPlatformProcess::Sleep(0.1f); } } #endif } -void UGlobalStateManager::BecomeAuthoritativeOverAllActors() +void UGlobalStateManager::SetAllActorRolesBasedOnLBStrategy() { - // This logic is not used in offloading. - if (USpatialStatics::IsSpatialOffloadingEnabled()) - { - return; - } - for (TActorIterator It(NetDriver->World); It; ++It) { AActor* Actor = *It; @@ -617,24 +374,9 @@ void UGlobalStateManager::BecomeAuthoritativeOverAllActors() { if (Actor->GetIsReplicated()) { - Actor->Role = ROLE_Authority; - Actor->RemoteRole = ROLE_SimulatedProxy; - } - } - } -} - -void UGlobalStateManager::BecomeAuthoritativeOverActorsBasedOnLBStrategy() -{ - for (TActorIterator It(NetDriver->World); It; ++It) - { - AActor* Actor = *It; - if (Actor != nullptr && !Actor->IsPendingKill()) - { - if (Actor->GetIsReplicated() && NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*Actor)) - { - Actor->Role = ROLE_Authority; - Actor->RemoteRole = ROLE_SimulatedProxy; + const bool bAuthoritative = NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*Actor); + Actor->Role = bAuthoritative ? ROLE_Authority : ROLE_SimulatedProxy; + Actor->RemoteRole = bAuthoritative ? ROLE_SimulatedProxy : ROLE_Authority; } } } @@ -654,14 +396,7 @@ void UGlobalStateManager::TriggerBeginPlay() // If we're loading from a snapshot, we shouldn't try and call BeginPlay with authority. if (bCanSpawnWithAuthority) { - if (GetDefault()->bEnableUnrealLoadBalancer) - { - BecomeAuthoritativeOverActorsBasedOnLBStrategy(); - } - else - { - BecomeAuthoritativeOverAllActors(); - } + SetAllActorRolesBasedOnLBStrategy(); } NetDriver->World->GetWorldSettings()->SetGSMReadyForPlay(); @@ -699,6 +434,7 @@ void UGlobalStateManager::SendCanBeginPlayUpdate(const bool bInCanBeginPlay) // This is so clients know when to connect to the deployment. void UGlobalStateManager::QueryGSM(const QueryDelegate& Callback) { + // Build a constraint for the GSM. Worker_ComponentConstraint GSMComponentConstraint{}; GSMComponentConstraint.component_id = SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID; @@ -726,10 +462,6 @@ void UGlobalStateManager::QueryGSM(const QueryDelegate& Callback) } else { - if (NetDriver->VirtualWorkerTranslator.IsValid()) - { - ApplyVirtualWorkerMappingFromQueryResponse(Op); - } ApplyDeploymentMapDataFromQueryResponse(Op); Callback.ExecuteIfBound(Op); } @@ -738,7 +470,53 @@ void UGlobalStateManager::QueryGSM(const QueryDelegate& Callback) Receiver->AddEntityQueryDelegate(RequestID, GSMQueryDelegate); } -void UGlobalStateManager::ApplyVirtualWorkerMappingFromQueryResponse(const Worker_EntityQueryResponseOp& Op) +void UGlobalStateManager::QueryTranslation() +{ + if (bTranslationQueryInFlight) + { + // Only allow one in flight query. Retries will be handled by the SpatialNetDriver. + return; + } + + // Build a constraint for the Virtual Worker Translation. + Worker_ComponentConstraint TranslationComponentConstraint{}; + TranslationComponentConstraint.component_id = SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID; + + Worker_Constraint TranslationConstraint{}; + TranslationConstraint.constraint_type = WORKER_CONSTRAINT_TYPE_COMPONENT; + TranslationConstraint.constraint.component_constraint = TranslationComponentConstraint; + + Worker_EntityQuery TranslationQuery{}; + TranslationQuery.constraint = TranslationConstraint; + TranslationQuery.result_type = WORKER_RESULT_TYPE_SNAPSHOT; + + Worker_RequestId RequestID = NetDriver->Connection->SendEntityQueryRequest(&TranslationQuery); + bTranslationQueryInFlight = true; + + TWeakObjectPtr WeakGlobalStateManager(this); + EntityQueryDelegate TranslationQueryDelegate; + TranslationQueryDelegate.BindLambda([WeakGlobalStateManager](const Worker_EntityQueryResponseOp& Op) + { + if (!WeakGlobalStateManager.IsValid()) + { + // The GSM was destroyed before receiving the response. + return; + } + + UGlobalStateManager* GlobalStateManager = WeakGlobalStateManager.Get(); + if (Op.status_code == WORKER_STATUS_CODE_SUCCESS) + { + if (GlobalStateManager->NetDriver->VirtualWorkerTranslator.IsValid()) + { + GlobalStateManager->ApplyVirtualWorkerMappingFromQueryResponse(Op); + } + } + GlobalStateManager->bTranslationQueryInFlight = false; + }); + Receiver->AddEntityQueryDelegate(RequestID, TranslationQueryDelegate); +} + +void UGlobalStateManager::ApplyVirtualWorkerMappingFromQueryResponse(const Worker_EntityQueryResponseOp& Op) const { check(NetDriver->VirtualWorkerTranslator.IsValid()); for (uint32_t i = 0; i < Op.results[0].component_count; i++) diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp index 34fa9ee48a..a29d1c0ade 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp @@ -17,19 +17,17 @@ #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" -#include "Utils/SpatialActorGroupManager.h" +#include "EngineClasses/SpatialWorldSettings.h" +#include "LoadBalancing/AbstractLBStrategy.h" #include "Utils/RepLayoutUtils.h" DEFINE_LOG_CATEGORY(LogSpatialClassInfoManager); -bool USpatialClassInfoManager::TryInit(USpatialNetDriver* InNetDriver, SpatialActorGroupManager* InActorGroupManager) +bool USpatialClassInfoManager::TryInit(USpatialNetDriver* InNetDriver) { check(InNetDriver != nullptr); NetDriver = InNetDriver; - check(InActorGroupManager != nullptr); - ActorGroupManager = InActorGroupManager; - FSoftObjectPath SchemaDatabasePath = FSoftObjectPath(FPaths::SetExtension(SpatialConstants::SCHEMA_DATABASE_ASSET_PATH, TEXT(".SchemaDatabase"))); SchemaDatabase = Cast(SchemaDatabasePath.TryLoad()); @@ -138,13 +136,12 @@ void USpatialClassInfoManager::CreateClassInfoForClass(UClass* Class) Info->RPCInfoMap.Add(RemoteFunction, RPCInfo); } - const bool bEnableHandover = GetDefault()->bEnableHandover; - + const bool bTrackHandoverProperties = ShouldTrackHandoverProperties(); for (TFieldIterator PropertyIt(Class); PropertyIt; ++PropertyIt) { UProperty* Property = *PropertyIt; - if (bEnableHandover && (Property->PropertyFlags & CPF_Handover)) + if (bTrackHandoverProperties && (Property->PropertyFlags & CPF_Handover)) { for (int32 ArrayIdx = 0; ArrayIdx < PropertyIt->ArrayDim; ++ArrayIdx) { @@ -187,7 +184,7 @@ void USpatialClassInfoManager::FinishConstructingActorClassInfo(const FString& C { Worker_ComponentId ComponentId = SchemaDatabase->ActorClassPathToSchema[ClassPath].SchemaComponents[Type]; - if (!GetDefault()->bEnableHandover && Type == SCHEMA_Handover) + if (!ShouldTrackHandoverProperties() && Type == SCHEMA_Handover) { return; } @@ -221,7 +218,7 @@ void USpatialClassInfoManager::FinishConstructingActorClassInfo(const FString& C ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { - if (!GetDefault()->bEnableHandover && Type == SCHEMA_Handover) + if (!ShouldTrackHandoverProperties() && Type == SCHEMA_Handover) { return; } @@ -238,18 +235,6 @@ void USpatialClassInfoManager::FinishConstructingActorClassInfo(const FString& C Info->SubobjectInfo.Add(Offset, ActorSubobjectInfo); } - - if (UClass* ActorClass = Info->Class.Get()) - { - if (ActorClass->IsChildOf()) - { - Info->ActorGroup = ActorGroupManager->GetActorGroupForClass(TSubclassOf(ActorClass)); - Info->WorkerType = ActorGroupManager->GetWorkerTypeForClass(TSubclassOf(ActorClass)); - - UE_LOG(LogSpatialClassInfoManager, VeryVerbose, TEXT("[%s] is in ActorGroup [%s], on WorkerType [%s]"), - *ActorClass->GetPathName(), *Info->ActorGroup.ToString(), *Info->WorkerType.ToString()) - } - } } void USpatialClassInfoManager::FinishConstructingSubobjectClassInfo(const FString& ClassPath, TSharedRef& Info) @@ -279,6 +264,23 @@ void USpatialClassInfoManager::FinishConstructingSubobjectClassInfo(const FStrin } } +bool USpatialClassInfoManager::ShouldTrackHandoverProperties() const +{ + // There's currently a bug that lets handover data get sent to clients in the initial + // burst of data for an entity, which leads to log spam in the SpatialReceiver. By tracking handover + // properties on clients, we can prevent that spam. + if (!NetDriver->IsServer()) + { + return true; + } + + const USpatialGDKSettings* Settings = GetDefault(); + + const UAbstractLBStrategy* Strategy = NetDriver->LoadBalanceStrategy; + check(Strategy != nullptr); + return Strategy->RequiresHandoverData() || Settings->bEnableHandover; +} + void USpatialClassInfoManager::TryCreateClassInfoForComponentId(Worker_ComponentId ComponentId) { if (FString* ClassPath = SchemaDatabase->ComponentIdToClassPath.Find(ComponentId)) @@ -301,7 +303,7 @@ const FClassInfo& USpatialClassInfoManager::GetOrCreateClassInfoByClass(UClass* { CreateClassInfoForClass(Class); } - + return ClassInfoMap[Class].Get(); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp index b0d5d3dea2..0d4a7ba1d3 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp @@ -51,19 +51,20 @@ void USpatialPlayerSpawner::SendPlayerSpawnRequest() SpatialSpawnerQuery.constraint = SpatialSpawnerConstraint; SpatialSpawnerQuery.result_type = WORKER_RESULT_TYPE_SNAPSHOT; - Worker_RequestId RequestID; - RequestID = NetDriver->Connection->SendEntityQueryRequest(&SpatialSpawnerQuery); + const Worker_RequestId RequestID = NetDriver->Connection->SendEntityQueryRequest(&SpatialSpawnerQuery); EntityQueryDelegate SpatialSpawnerQueryDelegate; SpatialSpawnerQueryDelegate.BindLambda([this, RequestID](const Worker_EntityQueryResponseOp& Op) { + FString Reason; + if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) { - UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Entity query for SpatialSpawner failed: %s"), UTF8_TO_TCHAR(Op.message)); + Reason = FString::Printf(TEXT("Entity query for SpatialSpawner failed: %s"), UTF8_TO_TCHAR(Op.message)); } else if (Op.result_count == 0) { - UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Could not find SpatialSpawner via entity query: %s"), UTF8_TO_TCHAR(Op.message)); + Reason = FString::Printf(TEXT("Could not find SpatialSpawner via entity query: %s"), UTF8_TO_TCHAR(Op.message)); } else { @@ -73,6 +74,12 @@ void USpatialPlayerSpawner::SendPlayerSpawnRequest() Worker_CommandRequest SpawnPlayerCommandRequest = PlayerSpawner::CreatePlayerSpawnRequest(SpawnRequest); NetDriver->Connection->SendCommandRequest(Op.results[0].entity_id, &SpawnPlayerCommandRequest, SpatialConstants::PLAYER_SPAWNER_SPAWN_PLAYER_COMMAND_ID); } + + if (!Reason.IsEmpty()) + { + UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("%s"), *Reason); + OnPlayerSpawnFailed.ExecuteIfBound(Reason); + } }); UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Sending player spawn request")); @@ -85,7 +92,7 @@ SpatialGDK::SpawnPlayerRequest USpatialPlayerSpawner::ObtainPlayerParams() const { FURL LoginURL; FUniqueNetIdRepl UniqueId; - + const FWorldContext* const WorldContext = GEngine->GetWorldContextFromWorld(NetDriver->GetWorld()); check(WorldContext->OwningGameInstance); @@ -96,7 +103,7 @@ SpatialGDK::SpawnPlayerRequest USpatialPlayerSpawner::ObtainPlayerParams() const if (const ULocalPlayer* LocalPlayer = WorldContext->OwningGameInstance->GetFirstGamePlayer()) { // Send the player nickname if available - FString OverrideName = LocalPlayer->GetNickname(); + const FString OverrideName = LocalPlayer->GetNickname(); if (OverrideName.Len() > 0) { LoginURL.AddOption(*FString::Printf(TEXT("Name=%s"), *OverrideName)); @@ -131,7 +138,7 @@ SpatialGDK::SpawnPlayerRequest USpatialPlayerSpawner::ObtainPlayerParams() const UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Couldn't get LocalPlayer data from game instance when trying to spawn player.")); } - FName OnlinePlatformName = WorldContext->OwningGameInstance->GetOnlinePlatformName(); + const FName OnlinePlatformName = WorldContext->OwningGameInstance->GetOnlinePlatformName(); return { LoginURL, UniqueId, OnlinePlatformName, bIsSimulatedPlayer }; } @@ -158,8 +165,10 @@ void USpatialPlayerSpawner::ReceivePlayerSpawnResponseOnClient(const Worker_Comm } else { - UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Player spawn request failed too many times. (%u attempts)"), + FString Reason = FString::Printf(TEXT("Player spawn request failed too many times. (%u attempts)"), SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS); + UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("%s"), *Reason); + OnPlayerSpawnFailed.ExecuteIfBound(Reason); } } @@ -167,7 +176,7 @@ void USpatialPlayerSpawner::ReceivePlayerSpawnRequestOnServer(const Worker_Comma { UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Received PlayerSpawn request on server")); - FUTF8ToTCHAR FStringConversion(reinterpret_cast(Op.caller_worker_id), strlen(Op.caller_worker_id)); + const FUTF8ToTCHAR FStringConversion(reinterpret_cast(Op.caller_worker_id), strlen(Op.caller_worker_id)); FString ClientWorkerId(FStringConversion.Length(), FStringConversion.Get()); // Accept the player if we have not already accepted a player from this worker. @@ -188,43 +197,68 @@ void USpatialPlayerSpawner::ReceivePlayerSpawnRequestOnServer(const Worker_Comma void USpatialPlayerSpawner::FindPlayerStartAndProcessPlayerSpawn(Schema_Object* SpawnPlayerRequest, const PhysicalWorkerName& ClientWorkerId) { - // If load-balancing is enabled AND the strategy dictates that another worker should have authority over - // the chosen PlayerStart THEN the spawn request is forwarded to that worker to prevent an initial player - // migration. Immediate player migrations can still happen if - // 1) the load-balancing strategy has different rules for PlayerStart Actors and Characters / Controllers / - // Player States or, - // 2) the load-balancing strategy can change the authoritative virtual worker ID for a PlayerStart Actor - // during the lifetime of a deployment. - if (GetDefault()->bEnableUnrealLoadBalancer) + // If the load balancing strategy dictates that this worker should have authority over the chosen PlayerStart THEN the spawn is handled locally, + // Else if the the PlayerStart is handled by another worker THEN forward the request to that worker to prevent an initial player migration, + // Else if a PlayerStart can't be found THEN we could be on the wrong worker type, so forward to the GameMode authoritative server. + // + // This implementation depends on: + // 1) the load-balancing strategy having the same rules for PlayerStart Actors and Characters / Controllers / Player States or, + // 2) the authoritative virtual worker ID for a PlayerStart Actor not changing during the lifetime of a deployment. + check (NetDriver->LoadBalanceStrategy != nullptr) + + // We need to specifically extract the URL from the PlayerSpawn request for finding a PlayerStart. + const FURL Url = PlayerSpawner::ExtractUrlFromPlayerSpawnParams(SpawnPlayerRequest); + + // Find a PlayerStart Actor on this server. + AActor* PlayerStartActor = NetDriver->GetWorld()->GetAuthGameMode()->FindPlayerStart(nullptr, Url.Portal); + + // If the PlayerStart is authoritative locally, spawn the player locally. + if (PlayerStartActor != nullptr && NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*PlayerStartActor)) { - // We need to specifically extract the URL from the PlayerSpawn request for finding a PlayerStart. - const FURL Url = PlayerSpawner::ExtractUrlFromPlayerSpawnParams(SpawnPlayerRequest); - AActor* PlayerStartActor = NetDriver->GetWorld()->GetAuthGameMode()->FindPlayerStart(nullptr, Url.Portal); + UE_LOG(LogSpatialPlayerSpawner, Verbose, TEXT("Handling SpawnPlayerRequest request locally. Client worker ID: %s."), *ClientWorkerId); + PassSpawnRequestToNetDriver(SpawnPlayerRequest, PlayerStartActor); + return; + } + + VirtualWorkerId VirtualWorkerToForwardTo = SpatialConstants::INVALID_VIRTUAL_WORKER_ID; - check(NetDriver->LoadBalanceStrategy != nullptr); - if (!NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*PlayerStartActor)) + // If we can't find a PlayerStart Actor, the PlayerSpawner authoritative worker may be part of a layer + // which has a limited view of the world and/or shouldn't be processing player spawning. In this case, + // we attempt to forward to the worker authoritative over the GameMode, as we assume the FindPlayerStart + // implementation may depend on authoritative game mode logic. We pass a null object ref so that the + // forwarded worker knows to search for a PlayerStart. + if (PlayerStartActor == nullptr) + { + VirtualWorkerToForwardTo = NetDriver->LoadBalanceStrategy->WhoShouldHaveAuthority(*UGameplayStatics::GetGameMode(GetWorld())); + if (VirtualWorkerToForwardTo == SpatialConstants::INVALID_VIRTUAL_WORKER_ID) { - // If we fail to forward the spawn request, we default to the normal player spawning flow. - const bool bSuccessfullyForwardedRequest = ForwardSpawnRequestToStrategizedServer(SpawnPlayerRequest, PlayerStartActor, ClientWorkerId); - if (bSuccessfullyForwardedRequest) - { - return; - } + UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("The server authoritative over the GameMode could not locate any PlayerStart, this is unsupported.")); } - else + } + else if (!NetDriver->LoadBalanceStrategy->ShouldHaveAuthority(*PlayerStartActor)) + { + VirtualWorkerToForwardTo = NetDriver->LoadBalanceStrategy->WhoShouldHaveAuthority(*PlayerStartActor); + if (VirtualWorkerToForwardTo == SpatialConstants::INVALID_VIRTUAL_WORKER_ID) { - UE_LOG(LogSpatialPlayerSpawner, Verbose, TEXT("Handling SpawnPlayerRequest request locally. Client worker ID: %s."), *ClientWorkerId); - PassSpawnRequestToNetDriver(SpawnPlayerRequest, PlayerStartActor); - return; + UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Load-balance strategy returned invalid virtual worker ID for selected PlayerStart Actor: %s"), + *GetNameSafe(PlayerStartActor)); } } - PassSpawnRequestToNetDriver(SpawnPlayerRequest, nullptr); + // If the load balancing strategy returns invalid virtual worker IDs for the PlayerStart, we should error. + if (VirtualWorkerToForwardTo == SpatialConstants::INVALID_VIRTUAL_WORKER_ID) + { + UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Defaulting to normal player spawning flow.")); + PassSpawnRequestToNetDriver(SpawnPlayerRequest, nullptr); + return; + } + + ForwardSpawnRequestToStrategizedServer(SpawnPlayerRequest, PlayerStartActor, ClientWorkerId, VirtualWorkerToForwardTo); } -void USpatialPlayerSpawner::PassSpawnRequestToNetDriver(Schema_Object* PlayerSpawnData, AActor* PlayerStart) +void USpatialPlayerSpawner::PassSpawnRequestToNetDriver(const Schema_Object* PlayerSpawnData, AActor* PlayerStart) { - SpatialGDK::SpawnPlayerRequest SpawnRequest = PlayerSpawner::ExtractPlayerSpawnParams(PlayerSpawnData); + const SpatialGDK::SpawnPlayerRequest SpawnRequest = PlayerSpawner::ExtractPlayerSpawnParams(PlayerSpawnData); AGameModeBase* GameMode = NetDriver->GetWorld()->GetAuthGameMode(); @@ -234,31 +268,29 @@ void USpatialPlayerSpawner::PassSpawnRequestToNetDriver(Schema_Object* PlayerSpa GameMode->SetPrioritizedPlayerStart(nullptr); } -// Copies the fields from the SpawnPlayerRequest argument into a ForwardSpawnPlayerRequest (along with the PlayerStart UnrealObjectRef). -bool USpatialPlayerSpawner::ForwardSpawnRequestToStrategizedServer(const Schema_Object* OriginalPlayerSpawnRequest, AActor* PlayerStart, const PhysicalWorkerName& ClientWorkerId) +void USpatialPlayerSpawner::ForwardSpawnRequestToStrategizedServer(const Schema_Object* OriginalPlayerSpawnRequest, AActor* PlayerStart, const PhysicalWorkerName& ClientWorkerId, const VirtualWorkerId SpawningVirtualWorker) { - // Find which virtual worker should have authority of the PlayerStart. - const VirtualWorkerId SpawningVirtualWorker = NetDriver->LoadBalanceStrategy->WhoShouldHaveAuthority(*PlayerStart); - if (SpawningVirtualWorker == SpatialConstants::INVALID_VIRTUAL_WORKER_ID) - { - UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Load-balance strategy returned invalid virtual worker ID for selected PlayerStart Actor: %s. Defaulting to normal player spawning flow."), *GetNameSafe(PlayerStart)); - return false; - } + UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Forwarding player spawn request to strategized worker. Client ID: %s. PlayerStart: %s. Strategeized virtual worker %d"), + *ClientWorkerId, *GetNameSafe(PlayerStart), SpawningVirtualWorker); // Find the server worker entity corresponding to the PlayerStart strategized virtual worker. const Worker_EntityId ServerWorkerEntity = NetDriver->VirtualWorkerTranslator->GetServerWorkerEntityForVirtualWorker(SpawningVirtualWorker); if (ServerWorkerEntity == SpatialConstants::INVALID_ENTITY_ID) { - UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Virtual worker translator returned invalid server worker entity ID. Virtual worker: %d. Defaulting to normal player spawning flow."), SpawningVirtualWorker); - return false; + UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Player spawning failed. Virtual worker translator returned invalid server worker entity ID. Virtual worker: %d. " + "Defaulting to normal player spawning flow."), SpawningVirtualWorker); + PassSpawnRequestToNetDriver(OriginalPlayerSpawnRequest, nullptr); + return; } - UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Forwarding player spawn request to strategized worker. Client ID: %s. PlayerStart: %s. Strategeized virtual worker %d. Forward server worker entity: %lld"), - *ClientWorkerId, *GetNameSafe(PlayerStart), SpawningVirtualWorker, ServerWorkerEntity); - // To pass the PlayerStart Actor to another worker we use a FUnrealObjectRef. - FNetworkGUID PlayerStartGuid = NetDriver->PackageMap->ResolveStablyNamedObject(PlayerStart); - FUnrealObjectRef PlayerStartObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromNetGUID(PlayerStartGuid); + // The PlayerStartObjectRef can be null if we are trying to just forward the spawn request to the correct worker layer, rather than some specific PlayerStart authoritative worker. + FUnrealObjectRef PlayerStartObjectRef = FUnrealObjectRef::NULL_OBJECT_REF; + if (PlayerStart != nullptr) + { + const FNetworkGUID PlayerStartGuid = NetDriver->PackageMap->ResolveStablyNamedObject(PlayerStart); + PlayerStartObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromNetGUID(PlayerStartGuid); + } // Create a request using the PlayerStart reference and by copying the data from the PlayerSpawn request from the client. // The Schema_CommandRequest is constructed separately from the Worker_CommandRequest so we can store it in the outgoing @@ -267,11 +299,9 @@ bool USpatialPlayerSpawner::ForwardSpawnRequestToStrategizedServer(const Schema_ ServerWorker::CreateForwardPlayerSpawnSchemaRequest(ForwardSpawnPlayerSchemaRequest, PlayerStartObjectRef, OriginalPlayerSpawnRequest, ClientWorkerId); Worker_CommandRequest ForwardSpawnPlayerRequest = ServerWorker::CreateForwardPlayerSpawnRequest(Schema_CopyCommandRequest(ForwardSpawnPlayerSchemaRequest)); - Worker_RequestId RequestId = NetDriver->Connection->SendCommandRequest(ServerWorkerEntity, &ForwardSpawnPlayerRequest, SpatialConstants::SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND_ID); + const Worker_RequestId RequestId = NetDriver->Connection->SendCommandRequest(ServerWorkerEntity, &ForwardSpawnPlayerRequest, SpatialConstants::SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND_ID); OutgoingForwardPlayerSpawnRequests.Add(RequestId, TUniquePtr(ForwardSpawnPlayerSchemaRequest)); - - return true; } void USpatialPlayerSpawner::ReceiveForwardedPlayerSpawnRequest(const Worker_CommandRequestOp& Op) @@ -289,20 +319,34 @@ void USpatialPlayerSpawner::ReceiveForwardedPlayerSpawnRequest(const Worker_Comm return; } - FUnrealObjectRef PlayerStartRef = GetObjectRefFromSchema(Payload, SpatialConstants::FORWARD_SPAWN_PLAYER_START_ACTOR_ID); + bool bRequestHandledSuccessfully = true; - bool bUnresolvedRef = false; - if (AActor* PlayerStart = Cast(FUnrealObjectRef::ToObjectPtr(PlayerStartRef, NetDriver->PackageMap, bUnresolvedRef))) + const FUnrealObjectRef PlayerStartRef = GetObjectRefFromSchema(Payload, SpatialConstants::FORWARD_SPAWN_PLAYER_START_ACTOR_ID); + if (PlayerStartRef != FUnrealObjectRef::NULL_OBJECT_REF) { - UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Received ForwardPlayerSpawn request. Client worker ID: %s. PlayerStart: %s"), *ClientWorkerId, *PlayerStart->GetName()); + bool bUnresolvedRef = false; + AActor* PlayerStart = Cast(FUnrealObjectRef::ToObjectPtr(PlayerStartRef, NetDriver->PackageMap, bUnresolvedRef)); + bRequestHandledSuccessfully = !bUnresolvedRef; + + if (bRequestHandledSuccessfully) + { + UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("Received ForwardPlayerSpawn request. Client worker ID: %s. PlayerStart: %s"), *ClientWorkerId, *PlayerStart->GetName()); + } + else + { + UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("PlayerStart Actor UnrealObjectRef was invalid on forwarded player spawn request worker: %s"), *ClientWorkerId); + } + PassSpawnRequestToNetDriver(PlayerSpawnData, PlayerStart); } else { - UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("PlayerStart Actor UnrealObjectRef was invalid on forwarded player spawn request worker: %s. Defaulting to normal player spawning flow."), *ClientWorkerId); + UE_LOG(LogSpatialPlayerSpawner, Log, TEXT("PlayerStart Actor was null object ref in forward spawn request. This is intentional when handing request to the correct " + "load balancing layer. Attempting to find a player start again.")); + FindPlayerStartAndProcessPlayerSpawn(PlayerSpawnData, ClientWorkerId); } - Worker_CommandResponse Response = ServerWorker::CreateForwardPlayerSpawnResponse(!bUnresolvedRef); + Worker_CommandResponse Response = ServerWorker::CreateForwardPlayerSpawnResponse(bRequestHandledSuccessfully); NetDriver->Connection->SendCommandResponse(Op.request_id, &Response); } @@ -363,7 +407,7 @@ void USpatialPlayerSpawner::RetryForwardSpawnPlayerRequest(const Worker_EntityId // Resend the ForwardSpawnPlayer request. Worker_CommandRequest ForwardSpawnPlayerRequest = ServerWorker::CreateForwardPlayerSpawnRequest(Schema_CopyCommandRequest(OldRequest)); - Worker_RequestId NewRequestId = NetDriver->Connection->SendCommandRequest(EntityId, &ForwardSpawnPlayerRequest, SpatialConstants::SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND_ID); + const Worker_RequestId NewRequestId = NetDriver->Connection->SendCommandRequest(EntityId, &ForwardSpawnPlayerRequest, SpatialConstants::SERVER_WORKER_FORWARD_SPAWN_REQUEST_COMMAND_ID); // Move the request data from the old request ID map entry across to the new ID entry. OutgoingForwardPlayerSpawnRequests.Add(NewRequestId, TUniquePtr(OldRequest)); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialRPCService.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialRPCService.cpp index 63c85219da..eaeaa34b23 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialRPCService.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialRPCService.cpp @@ -6,40 +6,50 @@ #include "Schema/ClientEndpoint.h" #include "Schema/MulticastRPCs.h" #include "Schema/ServerEndpoint.h" +#include "Utils/SpatialLatencyTracer.h" DEFINE_LOG_CATEGORY(LogSpatialRPCService); namespace SpatialGDK { -SpatialRPCService::SpatialRPCService(ExtractRPCDelegate ExtractRPCCallback, const USpatialStaticComponentView* View) +SpatialRPCService::SpatialRPCService(ExtractRPCDelegate ExtractRPCCallback, const USpatialStaticComponentView* View, USpatialLatencyTracer* SpatialLatencyTracer) : ExtractRPCCallback(ExtractRPCCallback) , View(View) + , SpatialLatencyTracer(SpatialLatencyTracer) { } -EPushRPCResult SpatialRPCService::PushRPC(Worker_EntityId EntityId, ERPCType Type, RPCPayload Payload) +EPushRPCResult SpatialRPCService::PushRPC(Worker_EntityId EntityId, ERPCType Type, RPCPayload Payload, bool bCreatedEntity) { EntityRPCType EntityType = EntityRPCType(EntityId, Type); + EPushRPCResult Result = EPushRPCResult::Success; + if (RPCRingBufferUtils::ShouldQueueOverflowed(Type) && OverflowedRPCs.Contains(EntityType)) { // Already has queued RPCs of this type, queue until those are pushed. AddOverflowedRPC(EntityType, MoveTemp(Payload)); - return EPushRPCResult::QueueOverflowed; + Result = EPushRPCResult::QueueOverflowed; } - - EPushRPCResult Result = PushRPCInternal(EntityId, Type, MoveTemp(Payload)); - - if (Result == EPushRPCResult::QueueOverflowed) + else { - AddOverflowedRPC(EntityType, MoveTemp(Payload)); + Result = PushRPCInternal(EntityId, Type, MoveTemp(Payload), bCreatedEntity); + + if (Result == EPushRPCResult::QueueOverflowed) + { + AddOverflowedRPC(EntityType, MoveTemp(Payload)); + } } +#if TRACE_LIB_ACTIVE + ProcessResultToLatencyTrace(Result, Payload.Trace); +#endif + return Result; } -EPushRPCResult SpatialRPCService::PushRPCInternal(Worker_EntityId EntityId, ERPCType Type, RPCPayload&& Payload) +EPushRPCResult SpatialRPCService::PushRPCInternal(Worker_EntityId EntityId, ERPCType Type, RPCPayload&& Payload, bool bCreatedEntity) { const Worker_ComponentId RingBufferComponentId = RPCRingBufferUtils::GetRingBufferComponentId(Type); @@ -52,6 +62,10 @@ EPushRPCResult SpatialRPCService::PushRPCInternal(Worker_EntityId EntityId, ERPC { if (!View->HasAuthority(EntityId, RingBufferComponentId)) { + if (bCreatedEntity) + { + return EPushRPCResult::EntityBeingCreated; + } return EPushRPCResult::NoRingBufferAuthority; } @@ -75,6 +89,10 @@ EPushRPCResult SpatialRPCService::PushRPCInternal(Worker_EntityId EntityId, ERPC } else { + if (bCreatedEntity) + { + return EPushRPCResult::EntityBeingCreated; + } // If the entity isn't in the view, we assume this RPC was called before // CreateEntityRequest, so we put it into a component data object. EndpointObject = Schema_GetComponentDataFields(GetOrCreateComponentData(EntityComponent)); @@ -89,6 +107,20 @@ EPushRPCResult SpatialRPCService::PushRPCInternal(Worker_EntityId EntityId, ERPC { RPCRingBufferUtils::WriteRPCToSchema(EndpointObject, Type, NewRPCId, Payload); +#if TRACE_LIB_ACTIVE + if (SpatialLatencyTracer != nullptr && Payload.Trace != InvalidTraceKey) + { + if (PendingTraces.Find(EntityComponent) == nullptr) + { + PendingTraces.Add(EntityComponent, Payload.Trace); + } + else + { + SpatialLatencyTracer->WriteAndEndTrace(Payload.Trace, TEXT("Multiple rpc updates in single update, ending further stack tracing"), true); + } + } +#endif + LastSentRPCIds.Add(EntityType, NewRPCId); } else @@ -119,7 +151,7 @@ void SpatialRPCService::PushOverflowedRPCs() bool bShouldDrop = false; for (RPCPayload& Payload : OverflowedRPCArray) { - EPushRPCResult Result = PushRPCInternal(EntityId, Type, MoveTemp(Payload)); + const EPushRPCResult Result = PushRPCInternal(EntityId, Type, MoveTemp(Payload), false); switch (Result) { @@ -137,8 +169,14 @@ void SpatialRPCService::PushOverflowedRPCs() UE_LOG(LogSpatialRPCService, Warning, TEXT("SpatialRPCService::PushOverflowedRPCs: Lost authority over ring buffer component for RPC type that was overflowed. Entity: %lld, RPC type: %s"), EntityId, *SpatialConstants::RPCTypeToString(Type)); bShouldDrop = true; break; + default: + checkNoEntry(); } +#if TRACE_LIB_ACTIVE + ProcessResultToLatencyTrace(Result, Payload.Trace); +#endif + // This includes the valid case of RPCs still overflowing (EPushRPCResult::QueueOverflowed), as well as the error cases. if (Result != EPushRPCResult::Success) { @@ -175,6 +213,11 @@ TArray SpatialRPCService::GetRPCsAndAcksToSend( UpdateToSend.EntityId = It.Key.EntityId; UpdateToSend.Update.component_id = It.Key.ComponentId; UpdateToSend.Update.schema_type = It.Value; +#if TRACE_LIB_ACTIVE + TraceKey Trace = InvalidTraceKey; + PendingTraces.RemoveAndCopyValue(It.Key, Trace); + UpdateToSend.Update.Trace = Trace; +#endif } PendingComponentUpdatesToSend.Empty(); @@ -182,7 +225,7 @@ TArray SpatialRPCService::GetRPCsAndAcksToSend( return UpdatesToSend; } -TArray SpatialRPCService::GetRPCComponentsOnEntityCreation(Worker_EntityId EntityId) +TArray SpatialRPCService::GetRPCComponentsOnEntityCreation(Worker_EntityId EntityId) { static Worker_ComponentId EndpointComponentIds[] = { SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID, @@ -190,13 +233,13 @@ TArray SpatialRPCService::GetRPCComponentsOnEntityCreation SpatialConstants::MULTICAST_RPCS_COMPONENT_ID }; - TArray Components; + TArray Components; for (Worker_ComponentId EndpointComponentId : EndpointComponentIds) { const EntityComponentId EntityComponent = { EntityId, EndpointComponentId }; - Worker_ComponentData& Component = Components.AddZeroed_GetRef(); + FWorkerComponentData& Component = Components.Emplace_GetRef(FWorkerComponentData{}); Component.component_id = EndpointComponentId; if (Schema_ComponentData** ComponentData = PendingRPCsOnEntityCreation.Find(EntityComponent)) { @@ -214,6 +257,11 @@ TArray SpatialRPCService::GetRPCComponentsOnEntityCreation } Component.schema_type = *ComponentData; +#if TRACE_LIB_ACTIVE + TraceKey Trace = InvalidTraceKey; + PendingTraces.RemoveAndCopyValue(EntityComponent, Trace); + Component.Trace = Trace; +#endif PendingRPCsOnEntityCreation.Remove(EntityComponent); } else @@ -494,4 +542,50 @@ Schema_ComponentData* SpatialRPCService::GetOrCreateComponentData(EntityComponen return *ComponentDataPtr; } +#if TRACE_LIB_ACTIVE +void SpatialRPCService::ProcessResultToLatencyTrace(const EPushRPCResult Result, const TraceKey Trace) +{ + if (SpatialLatencyTracer != nullptr && Trace != InvalidTraceKey) + { + bool bEndTrace = false; + FString TraceMsg; + switch (Result) + { + case SpatialGDK::EPushRPCResult::Success: + // No further action + break; + case SpatialGDK::EPushRPCResult::QueueOverflowed: + TraceMsg = TEXT("Overflowed"); + break; + case SpatialGDK::EPushRPCResult::DropOverflowed: + TraceMsg = TEXT("OverflowedAndDropped"); + bEndTrace = true; + break; + case SpatialGDK::EPushRPCResult::HasAckAuthority: + TraceMsg = TEXT("NoAckAuth"); + bEndTrace = true; + break; + case SpatialGDK::EPushRPCResult::NoRingBufferAuthority: + TraceMsg = TEXT("NoRingBufferAuth"); + bEndTrace = true; + break; + default: + TraceMsg = TEXT("UnrecognisedResult"); + break; + } + + if (bEndTrace) + { + // This RPC has been dropped, end the trace + SpatialLatencyTracer->WriteAndEndTrace(Trace, TraceMsg, false); + } + else if (!TraceMsg.IsEmpty()) + { + // This RPC will be sent later + SpatialLatencyTracer->WriteToLatencyTrace(Trace, TraceMsg); + } + } +} +#endif // TRACE_LIB_ACTIVE + } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp index 0291887392..ea8fa7baf3 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp @@ -105,11 +105,54 @@ void USpatialReceiver::LeaveCriticalSection() { OnEntityAddedDelegate.Broadcast(PendingAddEntity); } + PendingAddComponents.RemoveAll([PendingAddEntity](const PendingAddComponentWrapper& Component) {return Component.EntityId == PendingAddEntity;}); } + // The reason the AuthorityChange processing is split according to authority is to avoid cases + // where we receive data while being authoritative, as that could be unintuitive to the game devs. + // We process Lose Auth -> Add Components -> Gain Auth. A common thing that happens is that on handover we get + // ComponentData -> Gain Auth, and with this split you receive data as if you were a client to get the most up-to-date state, + // and then gain authority. Similarly, you first lose authority, and then receive data, in the opposite situation. for (Worker_AuthorityChangeOp& PendingAuthorityChange : PendingAuthorityChanges) { - HandleActorAuthority(PendingAuthorityChange); + if (PendingAuthorityChange.authority != WORKER_AUTHORITY_AUTHORITATIVE) + { + HandleActorAuthority(PendingAuthorityChange); + } + } + + for (PendingAddComponentWrapper& PendingAddComponent : PendingAddComponents) + { + if (ClassInfoManager->IsGeneratedQBIMarkerComponent(PendingAddComponent.ComponentId)) + { + continue; + } + USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(PendingAddComponent.EntityId); + if (Channel == nullptr) + { + UE_LOG(LogSpatialReceiver, Error, TEXT("Got an add component for an entity that doesn't have an associated actor channel." + " Entity id: %lld, component id: %d."), PendingAddComponent.EntityId, PendingAddComponent.ComponentId); + continue; + } + if (Channel->bCreatedEntity) + { + // Allows servers to change state if they are going to be authoritative, without us overwriting it with old data. + // TODO: UNR-3457 to remove this workaround. + continue; + } + + UE_LOG(LogSpatialReceiver, Verbose, + TEXT("Add component inside of a critical section, outside of an add entity, being handled: entity id %lld, component id %d."), + PendingAddComponent.EntityId, PendingAddComponent.ComponentId); + HandleIndividualAddComponent(PendingAddComponent.EntityId, PendingAddComponent.ComponentId, MoveTemp(PendingAddComponent.Data)); + } + + for (Worker_AuthorityChangeOp& PendingAuthorityChange : PendingAuthorityChanges) + { + if (PendingAuthorityChange.authority == WORKER_AUTHORITY_AUTHORITATIVE) + { + HandleActorAuthority(PendingAuthorityChange); + } } // Mark that we've left the critical section. @@ -136,6 +179,11 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op) return; } + if (HasEntityBeenRequestedForDelete(Op.entity_id)) + { + return; + } + // Remove all RemoveComponentOps that have already been received and have the same entityId and componentId as the AddComponentOp. // TODO: This can probably be removed when spatial view is added. QueuedRemoveComponentOps.RemoveAll([&Op](const Worker_RemoveComponentOp& RemoveComponentOp) { @@ -150,7 +198,6 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op) case SpatialConstants::PERSISTENCE_COMPONENT_ID: case SpatialConstants::SPAWN_DATA_COMPONENT_ID: case SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID: - case SpatialConstants::SINGLETON_COMPONENT_ID: case SpatialConstants::INTEREST_COMPONENT_ID: case SpatialConstants::NOT_STREAMED_COMPONENT_ID: case SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID: @@ -165,13 +212,16 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op) case SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID: case SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID: case SpatialConstants::SPATIAL_DEBUGGING_COMPONENT_ID: - // Ignore static spatial components as they are managed by the SpatialStaticComponentView. + case SpatialConstants::SERVER_WORKER_COMPONENT_ID: + // We either don't care about processing these components or we only need to store + // the data (which is handled by the SpatialStaticComponentView). return; case SpatialConstants::UNREAL_METADATA_COMPONENT_ID: - // The unreal metadata component is used to indicate when an actor needs to be created from the entity. - // This means we need to be inside a critical section, otherwise we may not have all the requisite information at the point of creating the actor. + // The UnrealMetadata component is used to indicate when an Actor needs to be created from the entity. + // This means we need to be inside a critical section, otherwise we may not have all the requisite + // information at the point of creating the Actor. check(bInCriticalSection); - PendingAddActors.Emplace(Op.entity_id); + PendingAddActors.AddUnique(Op.entity_id); return; case SpatialConstants::ENTITY_ACL_COMPONENT_ID: case SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID: @@ -183,7 +233,7 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op) } return; case SpatialConstants::WORKER_COMPONENT_ID: - if(NetDriver->IsServer()) + if (NetDriver->IsServer() && !WorkerConnectionEntities.Contains(Op.entity_id)) { // Register system identity for a worker connection, to know when a player has disconnected. Worker* WorkerData = StaticComponentView->GetComponentData(Op.entity_id); @@ -198,10 +248,6 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op) RPCService->OnCheckoutMulticastRPCComponentOnEntity(Op.entity_id); } return; - case SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID: - GlobalStateManager->ApplySingletonManagerData(Op.data); - GlobalStateManager->LinkAllExistingSingletonActors(); - return; case SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID: GlobalStateManager->ApplyDeploymentMapData(Op.data); return; @@ -243,11 +289,11 @@ void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op) if (bInCriticalSection) { - PendingAddComponents.Emplace(Op.entity_id, Op.data.component_id, MakeUnique(Op.data)); + PendingAddComponents.AddUnique(PendingAddComponentWrapper(Op.entity_id, Op.data.component_id, MakeUnique(Op.data))); } else { - HandleIndividualAddComponent(Op); + HandleIndividualAddComponent(Op.entity_id, Op.data.component_id, MakeUnique(Op.data)); } } @@ -255,6 +301,14 @@ void USpatialReceiver::OnRemoveEntity(const Worker_RemoveEntityOp& Op) { SCOPE_CYCLE_COUNTER(STAT_ReceiverRemoveEntity); + // Stop tracking if the entity was deleted as a result of deleting the actor during creation. + // This assumes that authority will be gained before interest is gained and lost. + const int32 RetiredActorIndex = EntitiesToRetireOnAuthorityGain.IndexOfByPredicate([Op](const DeferredRetire& Retire) { return Op.entity_id == Retire.EntityId; }); + if (RetiredActorIndex != INDEX_NONE) + { + EntitiesToRetireOnAuthorityGain.RemoveAtSwap(RetiredActorIndex); + } + if (LoadBalanceEnforcer != nullptr) { LoadBalanceEnforcer->OnEntityRemoved(Op); @@ -287,6 +341,17 @@ void USpatialReceiver::OnRemoveEntity(const Worker_RemoveEntityOp& Op) void USpatialReceiver::OnRemoveComponent(const Worker_RemoveComponentOp& Op) { + // We should exit early if we're receiving a duplicate RemoveComponent op. This can happen with dynamic + // components enabled. We detect if the op is a duplicate via the queue of ops to be processed (duplicate + // op receive in the same op list). + if (QueuedRemoveComponentOps.ContainsByPredicate([&Op](const Worker_RemoveComponentOp& QueuedOp) + { + return QueuedOp.entity_id == Op.entity_id && QueuedOp.component_id == Op.component_id; + })) + { + return; + } + if (Op.component_id == SpatialConstants::UNREAL_METADATA_COMPONENT_ID) { if (IsEntityWaitingForAsyncLoad(Op.entity_id)) @@ -349,6 +414,10 @@ USpatialActorChannel* USpatialReceiver::GetOrRecreateChannelForDomantActor(AActo { // Receive would normally create channel in ReceiveActor - this function is used to recreate the channel after waking up a dormant actor USpatialActorChannel* Channel = NetDriver->GetOrCreateSpatialActorChannel(Actor); + if (Channel == nullptr) + { + return nullptr; + } check(!Channel->bCreatingNewEntity); check(Channel->GetEntityId() == EntityID); @@ -366,6 +435,12 @@ void USpatialReceiver::ProcessRemoveComponent(const Worker_RemoveComponentOp& Op return; } + // We want to do nothing for RemoveComponent ops for which we never received a corresponding + // AddComponent op. This can happen because of the worker SDK generating a RemoveComponent op + // when a worker receives authority over a component without having already received the + // AddComponent op. The generation is a known part of the worker SDK we need to tolerate for + // enabling dynamic components, and having authority ACL entries without having the component + // data present on an entity is permitted as part of our Unreal dynamic component implementation. if (!StaticComponentView->HasComponent(Op.entity_id, Op.component_id)) { return; @@ -405,10 +480,25 @@ void USpatialReceiver::UpdateShadowData(Worker_EntityId EntityId) void USpatialReceiver::OnAuthorityChange(const Worker_AuthorityChangeOp& Op) { + if (HasEntityBeenRequestedForDelete(Op.entity_id)) + { + if (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE && Op.component_id == SpatialConstants::POSITION_COMPONENT_ID) + { + HandleEntityDeletedAuthority(Op.entity_id); + } + return; + } + // Update this worker's view of authority. We do this here as this is when the worker is first notified of the authority change. // This way systems that depend on having non-stale state can function correctly. StaticComponentView->OnAuthorityChange(Op); + if (Op.component_id == SpatialConstants::SERVER_WORKER_COMPONENT_ID && Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) + { + GlobalStateManager->TrySendWorkerReadyToBeginPlay(); + return; + } + if (Op.component_id == SpatialConstants::ENTITY_ACL_COMPONENT_ID && LoadBalanceEnforcer != nullptr) { LoadBalanceEnforcer->OnAclAuthorityChanged(Op); @@ -421,6 +511,18 @@ void USpatialReceiver::OnAuthorityChange(const Worker_AuthorityChangeOp& Op) return; } + // Process authority gained event immediately, so if we're in a critical section, the RPCService will + // be correctly configured to process RPCs sent during Actor creation + if (GetDefault()->UseRPCRingBuffer() && RPCService != nullptr && Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) + { + if (Op.component_id == SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID || + Op.component_id == SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID || + Op.component_id == SpatialConstants::MULTICAST_RPCS_COMPONENT_ID) + { + RPCService->OnEndpointAuthorityGained(Op.entity_id, Op.component_id); + } + } + if (bInCriticalSection) { // The actor receiving flow requires authority to be handled after all components have been received, so buffer those if we @@ -482,7 +584,9 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) NetDriver->VirtualWorkerTranslationManager->AuthorityChanged(Op); } - if (NetDriver->SpatialDebugger != nullptr) + if (NetDriver->SpatialDebugger != nullptr + && Op.authority == WORKER_AUTHORITY_AUTHORITATIVE + && Op.component_id == SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID) { NetDriver->SpatialDebugger->ActorAuthorityChanged(Op); } @@ -493,7 +597,9 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) return; } - if (USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(Op.entity_id)) + USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(Op.entity_id); + + if (Channel != nullptr) { if (Op.component_id == SpatialConstants::POSITION_COMPONENT_ID) { @@ -546,7 +652,7 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) { const bool bDormantActor = (Actor->NetDormancy >= DORM_DormantAll); - if (IsValid(NetDriver->GetActorChannelByEntityId(Op.entity_id)) || bDormantActor) + if (IsValid(Channel) || bDormantActor) { Actor->Role = ROLE_Authority; Actor->RemoteRole = ROLE_SimulatedProxy; @@ -585,7 +691,7 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) else { UE_LOG(LogSpatialReceiver, Verbose, TEXT("Received authority over actor %s, with entity id %lld, which has no channel. This means it attempted to delete it earlier, when it had no authority. Retrying to delete now."), *Actor->GetName(), Op.entity_id); - Sender->RetireEntity(Op.entity_id); + Sender->RetireEntity(Op.entity_id, Actor->IsNetStartupActor()); } } else if (Op.authority == WORKER_AUTHORITY_AUTHORITY_LOSS_IMMINENT) @@ -594,9 +700,9 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) } else if (Op.authority == WORKER_AUTHORITY_NOT_AUTHORITATIVE) { - if (USpatialActorChannel* ActorChannel = NetDriver->GetActorChannelByEntityId(Op.entity_id)) + if (Channel != nullptr) { - ActorChannel->bCreatedEntity = false; + Channel->bCreatedEntity = false; } // With load-balancing enabled, we already set ROLE_SimulatedProxy and trigger OnAuthorityLost when we @@ -624,9 +730,12 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) { if (UObject* Object = PendingSubobjectAttachment.Subobject.Get()) { - // TODO: UNR-664 - We should track the bytes sent here and factor them into channel saturation. - uint32 BytesWritten = 0; - Sender->SendAddComponentForSubobject(PendingSubobjectAttachment.Channel, Object, *PendingSubobjectAttachment.Info, BytesWritten); + if (IsValid(Channel)) + { + // TODO: UNR-664 - We should track the bytes sent here and factor them into channel saturation. + uint32 BytesWritten = 0; + Sender->SendAddComponentForSubobject(Channel, Object, *PendingSubobjectAttachment.Info, BytesWritten); + } } } @@ -635,12 +744,12 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) } else if (Op.component_id == SpatialConstants::GetClientAuthorityComponent(GetDefault()->UseRPCRingBuffer())) { - if (USpatialActorChannel* ActorChannel = NetDriver->GetActorChannelByEntityId(Op.entity_id)) + if (Channel != nullptr) { // Soft handover isn't supported currently. if (Op.authority != WORKER_AUTHORITY_AUTHORITY_LOSS_IMMINENT) { - ActorChannel->ClientProcessOwnershipChange(Op.authority == WORKER_AUTHORITY_AUTHORITATIVE); + Channel->ClientProcessOwnershipChange(Op.authority == WORKER_AUTHORITY_AUTHORITATIVE); } } @@ -659,10 +768,12 @@ void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) { if (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) { - RPCService->OnEndpointAuthorityGained(Op.entity_id, Op.component_id); if (Op.component_id != SpatialConstants::MULTICAST_RPCS_COMPONENT_ID) { - RPCService->ExtractRPCsForEntity(Op.entity_id, Op.component_id); + // If we have just received authority over the client endpoint, then we are a client. In that case, + // we want to scrape the server endpoint for any server -> client RPCs that are waiting to be called. + const Worker_ComponentId ComponentToExtractFrom = Op.component_id == SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID ? SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID : SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID; + RPCService->ExtractRPCsForEntity(Op.entity_id, ComponentToExtractFrom); } } else if (Op.authority == WORKER_AUTHORITY_NOT_AUTHORITATIVE) @@ -742,22 +853,8 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) AActor* EntityActor = Cast(PackageMap->GetObjectFromEntityId(EntityId)); if (EntityActor != nullptr) { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("%s: Entity for actor %s has been checked out on the worker which spawned it or is a singleton linked on this worker. " - "Entity ID: %lld"), *NetDriver->Connection->GetWorkerId(), *EntityActor->GetName(), EntityId); - - // Assume SimulatedProxy until we've been delegated Authority - bool bAuthority = StaticComponentView->HasAuthority(EntityId, Position::ComponentId); - EntityActor->Role = bAuthority ? ROLE_Authority : ROLE_SimulatedProxy; - EntityActor->RemoteRole = bAuthority ? ROLE_SimulatedProxy : ROLE_Authority; - if (bAuthority) - { - if (EntityActor->GetNetConnection() != nullptr || EntityActor->IsA()) - { - EntityActor->RemoteRole = ROLE_AutonomousProxy; - } - } - - // If we're a singleton, apply the data, regardless of authority - JIRA: 736 + UE_LOG(LogSpatialReceiver, Verbose, TEXT("%s: Entity %lld for Actor %s has been checked out on the worker which spawned it."), + *NetDriver->Connection->GetWorkerId(), EntityId, *EntityActor->GetName()); return; } @@ -789,7 +886,6 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) { // This could be nullptr if: // a stably named actor could not be found - // the Actor is a singleton that has arrived over the wire before it has been created on this worker // the class couldn't be loaded return; } @@ -844,11 +940,7 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) if (Channel->Actor == nullptr) { -#if ENGINE_MINOR_VERSION <= 22 - Channel->SetChannelActor(EntityActor); -#else Channel->SetChannelActor(EntityActor, ESetChannelActorFlags::None); -#endif } TArray ObjectsToResolvePendingOpsFor; @@ -878,11 +970,6 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) if (!NetDriver->IsServer()) { // Update interest on the entity's components after receiving initial component data (so Role and RemoteRole are properly set). - // Don't send dynamic interest for this actor if it is otherwise handled by result types. - if (!SpatialGDKSettings->bEnableResultTypes) - { - Sender->SendComponentInterestForActor(Channel, EntityId, Channel->IsAuthoritativeClient()); - } // This is a bit of a hack unfortunately, among the core classes only PlayerController implements this function and it requires // a player index. For now we don't support split screen, so the number is always 0. @@ -909,17 +996,13 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) // flow) take care of setting roles correctly. if (EntityActor->HasAuthority()) { + UE_LOG(LogSpatialReceiver, Error, TEXT("Trying to unexpectedly spawn received network Actor with authority. Actor %s. Entity: %lld"), *EntityActor->GetName(), EntityId); EntityActor->Role = ROLE_SimulatedProxy; EntityActor->RemoteRole = ROLE_Authority; } EntityActor->DispatchBeginPlay(); } - if (EntityActor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)) - { - GlobalStateManager->RegisterSingletonChannel(EntityActor, Channel); - } - EntityActor->UpdateOverlaps(); if (StaticComponentView->HasComponent(EntityId, SpatialConstants::DORMANT_COMPONENT_ID)) @@ -1037,11 +1120,6 @@ void USpatialReceiver::RemoveActor(Worker_EntityId EntityId) } } - if (Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)) - { - return; - } - DestroyActor(Actor, EntityId); } @@ -1106,6 +1184,13 @@ AActor* USpatialReceiver::TryGetOrCreateActor(UnrealMetadata* UnrealMetadataComp } } + // Handle linking received unique Actors (e.g. game state, game mode) to instances already spawned on this worker. + UClass* ActorClass = UnrealMetadataComp->GetNativeEntityClass(); + if (FUnrealObjectRef::IsUniqueActorClass(ActorClass) && NetDriver->IsServer()) + { + return PackageMap->GetUniqueActorInstanceByClass(ActorClass); + } + return CreateActor(UnrealMetadataComp, SpawnDataComp, NetOwningClientWorkerComp); } @@ -1120,14 +1205,6 @@ AActor* USpatialReceiver::CreateActor(UnrealMetadata* UnrealMetadataComp, SpawnD return nullptr; } - const bool bIsServer = NetDriver->IsServer(); - - // Initial Singleton Actor replication is handled with GlobalStateManager::LinkExistingSingletonActors - if (bIsServer && ActorClass->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)) - { - return FindSingletonActor(ActorClass); - } - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Spawning a %s whilst checking out an entity."), *ActorClass->GetFullName()); const bool bCreatingPlayerController = ActorClass->IsChildOf(APlayerController::StaticClass()); @@ -1142,7 +1219,7 @@ AActor* USpatialReceiver::CreateActor(UnrealMetadata* UnrealMetadataComp, SpawnD AActor* NewActor = NetDriver->GetWorld()->SpawnActorAbsolute(ActorClass, FTransform(SpawnDataComp->Rotation, SpawnLocation), SpawnInfo); check(NewActor); - if (bIsServer && bCreatingPlayerController) + if (NetDriver->IsServer() && bCreatingPlayerController) { // If we're spawning a PlayerController, it should definitely have a net-owning client worker ID. check(NetOwningClientWorkerComp->WorkerId.IsSet()); @@ -1190,7 +1267,7 @@ void USpatialReceiver::ApplyComponentDataOnActorCreation(Worker_EntityId EntityI bool bFoundOffset = ClassInfoManager->GetOffsetByComponentId(Data.component_id, Offset); if (!bFoundOffset) { - UE_LOG(LogSpatialReceiver, Warning, TEXT("EntityId %lld, ComponentId %d - Could not find offset for component id when applying component data to Actor %s!"), EntityId, Data.component_id, *Actor->GetPathName()); + UE_LOG(LogSpatialReceiver, Warning, TEXT("Worker: %s EntityId: %lld, ComponentId: %d - Could not find offset for component id when applying component data to Actor %s!"), *NetDriver->Connection->GetWorkerId(), EntityId, Data.component_id, *Actor->GetPathName()); return; } @@ -1220,32 +1297,32 @@ void USpatialReceiver::ApplyComponentDataOnActorCreation(Worker_EntityId EntityI OutObjectsToResolve.Add(ObjectPtrRefPair(TargetObject.Get(), TargetObjectRef)); } -void USpatialReceiver::HandleIndividualAddComponent(const Worker_AddComponentOp& Op) +void USpatialReceiver::HandleIndividualAddComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId, TUniquePtr Data) { uint32 Offset = 0; - bool bFoundOffset = ClassInfoManager->GetOffsetByComponentId(Op.data.component_id, Offset); + bool bFoundOffset = ClassInfoManager->GetOffsetByComponentId(ComponentId, Offset); if (!bFoundOffset) { - UE_LOG(LogSpatialReceiver, Warning, TEXT("EntityId %lld, ComponentId %d - Could not find offset for component id " - "when receiving dynamic AddComponent."), Op.entity_id, Op.data.component_id); + UE_LOG(LogSpatialReceiver, Warning, TEXT("Could not find offset for component id when receiving dynamic AddComponent." + " (EntityId %lld, ComponentId %d)"), EntityId, ComponentId); return; } // Object already exists, we can apply data directly. - if (UObject* Object = PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(Op.entity_id, Offset)).Get()) + if (UObject* Object = PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(EntityId, Offset)).Get()) { - if (USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(Op.entity_id)) + if (USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(EntityId)) { - ApplyComponentData(*Channel, *Object, Op.data); + ApplyComponentData(*Channel, *Object, *Data->ComponentData); } return; } - const FClassInfo& Info = ClassInfoManager->GetClassInfoByComponentId(Op.data.component_id); - AActor* Actor = Cast(PackageMap->GetObjectFromEntityId(Op.entity_id).Get()); + const FClassInfo& Info = ClassInfoManager->GetClassInfoByComponentId(ComponentId); + AActor* Actor = Cast(PackageMap->GetObjectFromEntityId(EntityId).Get()); if (Actor == nullptr) { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Received an add component op for subobject of type %s on entity %lld but couldn't find Actor!"), *Info.Class->GetName(), Op.entity_id); + UE_LOG(LogSpatialReceiver, Warning, TEXT("Received an add component op for subobject of type %s on entity %lld but couldn't find Actor!"), *Info.Class->GetName(), EntityId); return; } @@ -1254,25 +1331,25 @@ void USpatialReceiver::HandleIndividualAddComponent(const Worker_AddComponentOp& bool bIsDynamicSubobject = !ActorClassInfo.SubobjectInfo.Contains(Offset); if (!bIsDynamicSubobject) { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Tried to apply component data on add component for a static subobject that's been deleted, will skip. Entity: %lld, Component: %d, Actor: %s"), Op.entity_id, Op.data.component_id, *Actor->GetPathName()); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Tried to apply component data on add component for a static subobject that's been deleted, will skip. Entity: %lld, Component: %d, Actor: %s"), EntityId, ComponentId, *Actor->GetPathName()); return; } // Otherwise this is a dynamically attached component. We need to make sure we have all related components before creation. - PendingDynamicSubobjectComponents.Add(MakeTuple(static_cast(Op.entity_id), Op.data.component_id), - PendingAddComponentWrapper(Op.entity_id, Op.data.component_id, MakeUnique(Op.data))); + PendingDynamicSubobjectComponents.Add(MakeTuple(static_cast(EntityId), ComponentId), + PendingAddComponentWrapper(EntityId, ComponentId, MoveTemp(Data))); bool bReadyToCreate = true; ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { - Worker_ComponentId ComponentId = Info.SchemaComponents[Type]; + Worker_ComponentId SchemaComponentId = Info.SchemaComponents[Type]; - if (ComponentId == SpatialConstants::INVALID_COMPONENT_ID) + if (SchemaComponentId == SpatialConstants::INVALID_COMPONENT_ID) { return; } - if (!PendingDynamicSubobjectComponents.Contains(MakeTuple(static_cast(Op.entity_id), ComponentId))) + if (!PendingDynamicSubobjectComponents.Contains(MakeTuple(static_cast(EntityId), SchemaComponentId))) { bReadyToCreate = false; } @@ -1280,7 +1357,7 @@ void USpatialReceiver::HandleIndividualAddComponent(const Worker_AddComponentOp& if (bReadyToCreate) { - AttachDynamicSubobject(Actor, Op.entity_id, Info); + AttachDynamicSubobject(Actor, EntityId, Info); } } @@ -1320,18 +1397,6 @@ void USpatialReceiver::AttachDynamicSubobject(AActor* Actor, Worker_EntityId Ent // Resolve things like RepNotify or RPCs after applying component data. ResolvePendingOperations(Subobject, SubobjectRef); - - // Don't send dynamic interest for this subobject if it is otherwise handled by result types. - if (GetDefault()->bEnableResultTypes) - { - return; - } - - // If on a client, we need to set up the proper component interest for the new subobject. - if (!NetDriver->IsServer()) - { - Sender->SendComponentInterestForSubobject(Info, EntityId, Channel->IsAuthoritativeClient()); - } } struct USpatialReceiver::RepStateUpdateHelper @@ -1453,7 +1518,6 @@ void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) case SpatialConstants::INTEREST_COMPONENT_ID: case SpatialConstants::SPAWN_DATA_COMPONENT_ID: case SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID: - case SpatialConstants::SINGLETON_COMPONENT_ID: case SpatialConstants::UNREAL_METADATA_COMPONENT_ID: case SpatialConstants::NOT_STREAMED_COMPONENT_ID: case SpatialConstants::RPCS_ON_ENTITY_CREATION_ID: @@ -1470,10 +1534,6 @@ void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) case SpatialConstants::HEARTBEAT_COMPONENT_ID: OnHeartbeatComponentUpdate(Op); return; - case SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID: - GlobalStateManager->ApplySingletonManagerUpdate(Op.update); - GlobalStateManager->LinkAllExistingSingletonActors(); - return; case SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID: NetDriver->GlobalStateManager->ApplyDeploymentMapUpdate(Op.update); return; @@ -1553,13 +1613,11 @@ void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) } } - const FClassInfo& Info = ClassInfoManager->GetClassInfoByComponentId(Op.update.component_id); - uint32 Offset; bool bFoundOffset = ClassInfoManager->GetOffsetByComponentId(Op.update.component_id, Offset); if (!bFoundOffset) { - UE_LOG(LogSpatialReceiver, Warning, TEXT("Entity: %d Component: %d - Couldn't find Offset for component id"), Op.entity_id, Op.update.component_id); + UE_LOG(LogSpatialReceiver, Warning, TEXT("Worker: %s EntityId %d ComponentId %d - Could not find offset for component id when receiving a component update."), *NetDriver->Connection->GetWorkerId(), Op.entity_id, Op.update.component_id); return; } @@ -2062,20 +2120,6 @@ TWeakObjectPtr USpatialReceiver::PopPendingActorRequest(Wo return Channel; } -AActor* USpatialReceiver::FindSingletonActor(UClass* SingletonClass) -{ - TArray FoundActors; - UGameplayStatics::GetAllActorsOfClass(NetDriver->World, SingletonClass, FoundActors); - - // There should be only one singleton actor per class - if (FoundActors.Num() == 1) - { - return FoundActors[0]; - } - - return nullptr; -} - void USpatialReceiver::ProcessQueuedActorRPCsOnEntityCreation(Worker_EntityId EntityId, RPCsOnEntityCreation& QueuedRPCs) { for (auto& RPC : QueuedRPCs.RPCs) @@ -2134,7 +2178,20 @@ void USpatialReceiver::ProcessOrQueueIncomingRPC(const FUnrealObjectRef& InTarge UObject* TargetObject = TargetObjectWeakPtr.Get(); const FClassInfo& ClassInfo = ClassInfoManager->GetOrCreateClassInfoByObject(TargetObject); + + if (InPayload.Index >= static_cast(ClassInfo.RPCs.Num())) + { + // This should only happen if there's a class layout disagreement between workers, which would indicate incompatible binaries. + UE_LOG(LogSpatialReceiver, Error, TEXT("Invalid RPC index (%d) received on %s, dropping the RPC"), InPayload.Index, *TargetObject->GetPathName()); + return; + } UFunction* Function = ClassInfo.RPCs[InPayload.Index]; + if (Function == nullptr) + { + UE_LOG(LogSpatialReceiver, Error, TEXT("Missing function info received on %s, dropping the RPC"), *TargetObject->GetPathName()); + return; + } + const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); ERPCType Type = RPCInfo.Type; @@ -2153,16 +2210,18 @@ void USpatialReceiver::ResolvePendingOperations(UObject* Object, const FUnrealOb UE_LOG(LogSpatialReceiver, Verbose, TEXT("Resolving pending object refs and RPCs which depend on object: %s %s."), *Object->GetName(), *ObjectRef.ToString()); ResolveIncomingOperations(Object, ObjectRef); - if (Object->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton) && !Object->IsFullNameStableForNetworking()) + + // When resolving an Actor that should uniquely exist in a deployment, e.g. GameMode, GameState, LevelScriptActors, we also + // resolve using class path (in case any properties were set from a server that hasn't resolved the Actor yet). + if (FUnrealObjectRef::ShouldLoadObjectFromClassPath(Object)) { - // When resolving a singleton, also resolve using class path (in case any properties - // were set from a server that hasn't resolved the singleton yet) - FUnrealObjectRef ClassObjectRef = FUnrealObjectRef::GetSingletonClassRef(Object, PackageMap); + FUnrealObjectRef ClassObjectRef = FUnrealObjectRef::GetRefFromObjectClassPath(Object, PackageMap); if (ClassObjectRef.IsValid()) { ResolveIncomingOperations(Object, ClassObjectRef); } } + // TODO: UNR-1650 We're trying to resolve all queues, which introduces more overhead. IncomingRPCs.ProcessRPCs(); } @@ -2259,7 +2318,7 @@ void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* R if (AbsOffset >= MaxAbsOffset) { - UE_LOG(LogSpatialReceiver, Log, TEXT("ResolveObjectReferences: Removed unresolved reference: AbsOffset >= MaxAbsOffset: %d"), AbsOffset); + UE_LOG(LogSpatialReceiver, Error, TEXT("ResolveObjectReferences: Removed unresolved reference: AbsOffset >= MaxAbsOffset: %d"), AbsOffset); It.RemoveCurrent(); continue; } @@ -2445,7 +2504,32 @@ void USpatialReceiver::PeriodicallyProcessIncomingRPCs() bool USpatialReceiver::NeedToLoadClass(const FString& ClassPath) { - return FindObject(nullptr, *ClassPath, false) == nullptr; + UObject* ClassObject = FindObject(nullptr, *ClassPath, false); + if (ClassObject == nullptr) + { + return true; + } + + FString PackagePath = GetPackagePath(ClassPath); + FName PackagePathName = *PackagePath; + + // UNR-3320 The following test checks if the package is currently being processed in the async loading thread. + // Without it, we could be using an object loaded in memory, but not completely ready to be used. + // Looking through PackageMapClient's code, which handles asset async loading in Native unreal, checking + // UPackage::IsFullyLoaded, or UObject::HasAnyInternalFlag(EInternalObjectFlag::AsyncLoading) should tell us if it is the case. + // In practice, these tests are not enough to prevent using objects too early (symptom is RF_NeedPostLoad being set, and crash when using them later). + // GetAsyncLoadPercentage will actually look through the async loading thread's UAsyncPackage maps to see if there are any entries. + // TODO : UNR-3374 This looks like an expensive check, but it does the job. We should investigate further + // what is the issue with the other flags and why they do not give us reliable information. + + float Percentage = GetAsyncLoadPercentage(PackagePathName); + if (Percentage != -1.0f) + { + UE_LOG(LogSpatialReceiver, Warning, TEXT("Class %s package is registered in async loading thread."), *ClassPath) + return true; + } + + return false; } FString USpatialReceiver::GetPackagePath(const FString& ClassPath) @@ -2491,6 +2575,12 @@ void USpatialReceiver::OnAsyncPackageLoaded(const FName& PackageName, UPackage* return; } + if (Result != EAsyncLoadingResult::Succeeded) + { + UE_LOG(LogSpatialReceiver, Error, TEXT("USpatialReceiver::OnAsyncPackageLoaded: Package was not loaded successfully. Package: %s"), *PackageName.ToString()); + return; + } + for (Worker_EntityId Entity : Entities) { if (IsEntityWaitingForAsyncLoad(Entity)) @@ -2530,6 +2620,12 @@ void USpatialReceiver::MoveMappedObjectToUnmapped(const FUnrealObjectRef& Ref) } } +void USpatialReceiver::RetireWhenAuthoritive(Worker_EntityId EntityId, Worker_ComponentId ActorClassId, bool bIsNetStartup, bool bNeedsTearOff) +{ + DeferredRetire DeferredObj = { EntityId, ActorClassId, bIsNetStartup, bNeedsTearOff }; + EntitiesToRetireOnAuthorityGain.Add(DeferredObj); +} + bool USpatialReceiver::IsEntityWaitingForAsyncLoad(Worker_EntityId Entity) { return EntitiesWaitingForAsyncLoad.Contains(Entity); @@ -2539,6 +2635,17 @@ void USpatialReceiver::QueueAddComponentOpForAsyncLoad(const Worker_AddComponent { EntityWaitingForAsyncLoad& AsyncLoadEntity = EntitiesWaitingForAsyncLoad.FindChecked(Op.entity_id); + // Skip queuing a duplicate AddComponent op. + if (AsyncLoadEntity.PendingOps.ContainsByPredicate([&Op](const QueuedOpForAsyncLoad& QueuedOp) + { + return QueuedOp.Op.op_type == WORKER_OP_TYPE_ADD_COMPONENT + && QueuedOp.Op.op.add_component.entity_id == Op.entity_id + && QueuedOp.Op.op.add_component.data.component_id && Op.data.component_id; + })) + { + return; + } + QueuedOpForAsyncLoad NewOp = {}; NewOp.AcquiredData = Worker_AcquireComponentData(&Op.data); NewOp.Op.op_type = WORKER_OP_TYPE_ADD_COMPONENT; @@ -2700,3 +2807,30 @@ void USpatialReceiver::CleanupRepStateMap(FSpatialObjectRepState& RepState) } } } + +bool USpatialReceiver::HasEntityBeenRequestedForDelete(Worker_EntityId EntityId) +{ + return EntitiesToRetireOnAuthorityGain.ContainsByPredicate([EntityId](const DeferredRetire& Retire) { return EntityId == Retire.EntityId; }); +} + +void USpatialReceiver::HandleDeferredEntityDeletion(const DeferredRetire& Retire) +{ + if (Retire.bNeedsTearOff) + { + Sender->SendActorTornOffUpdate(Retire.EntityId, Retire.ActorClassId); + NetDriver->DelayedRetireEntity(Retire.EntityId, 1.0f, Retire.bIsNetStartupActor); + } + else + { + Sender->RetireEntity(Retire.EntityId, Retire.bIsNetStartupActor); + } +} + +void USpatialReceiver::HandleEntityDeletedAuthority(Worker_EntityId EntityId) +{ + int32 Index = EntitiesToRetireOnAuthorityGain.IndexOfByPredicate([EntityId](const DeferredRetire& Retire) { return Retire.EntityId == EntityId; }); + if (Index != INDEX_NONE) + { + HandleDeferredEntityDeletion(EntitiesToRetireOnAuthorityGain[Index]); + } +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp index 12acb545dc..c00693c626 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp @@ -14,6 +14,7 @@ #include "Interop/Connection/SpatialWorkerConnection.h" #include "Interop/GlobalStateManager.h" #include "Interop/SpatialReceiver.h" +#include "LoadBalancing/AbstractLBStrategy.h" #include "Net/NetworkProfiler.h" #include "Schema/AuthorityIntent.h" #include "Schema/ClientRPCEndpointLegacy.h" @@ -29,7 +30,6 @@ #include "Utils/EntityFactory.h" #include "Utils/InterestFactory.h" #include "Utils/RepLayoutUtils.h" -#include "Utils/SpatialActorGroupManager.h" #include "Utils/SpatialActorUtils.h" #include "Utils/SpatialDebugger.h" #include "Utils/SpatialLatencyTracer.h" @@ -66,17 +66,21 @@ void USpatialSender::Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimer Receiver = InNetDriver->Receiver; PackageMap = InNetDriver->PackageMap; ClassInfoManager = InNetDriver->ClassInfoManager; - check(InNetDriver->ActorGroupManager != nullptr); - ActorGroupManager = InNetDriver->ActorGroupManager; TimerManager = InTimerManager; RPCService = InRPCService; OutgoingRPCs.BindProcessingFunction(FProcessRPCDelegate::CreateUObject(this, &USpatialSender::SendRPC)); + + // Attempt to send RPCs that might have been queued while waiting for authority over entities this worker created. + if (GetDefault()->QueuedOutgoingRPCRetryTime > 0.0f) + { + PeriodicallyProcessOutgoingRPCs(); + } } Worker_RequestId USpatialSender::CreateEntity(USpatialActorChannel* Channel, uint32& OutBytesWritten) { - EntityFactory DataFactory(NetDriver, PackageMap, ClassInfoManager, ActorGroupManager, RPCService); + EntityFactory DataFactory(NetDriver, PackageMap, ClassInfoManager, RPCService); TArray ComponentDatas = DataFactory.CreateEntityComponents(Channel, OutgoingOnCreateEntityRPCs, OutBytesWritten); // If the Actor was loaded rather than dynamically spawned, associate it with its owning sublevel. @@ -110,6 +114,18 @@ Worker_ComponentData USpatialSender::CreateLevelComponentData(AActor* Actor) return ComponentFactory::CreateEmptyComponentData(SpatialConstants::NOT_STREAMED_COMPONENT_ID); } +void USpatialSender::PeriodicallyProcessOutgoingRPCs() +{ + FTimerHandle Timer; + TimerManager->SetTimer(Timer, [WeakThis = TWeakObjectPtr(this)]() + { + if (USpatialSender* SpatialSender = WeakThis.Get()) + { + SpatialSender->OutgoingRPCs.ProcessRPCs(); + } + }, GetDefault()->QueuedOutgoingRPCRetryTime, true); +} + void USpatialSender::SendAddComponentForSubobject(USpatialActorChannel* Channel, UObject* Subobject, const FClassInfo& SubobjectInfo, uint32& OutBytesWritten) { FRepChangeState SubobjectRepChanges = Channel->CreateInitialRepChangeState(Subobject); @@ -149,7 +165,6 @@ void USpatialSender::GainAuthorityThenAddComponent(USpatialActorChannel* Channel TSharedRef PendingSubobjectAttachment = MakeShared(); PendingSubobjectAttachment->Subobject = Object; - PendingSubobjectAttachment->Channel = Channel; PendingSubobjectAttachment->Info = Info; // We collect component IDs related to the dynamic subobject being added to gain authority over. @@ -173,9 +188,7 @@ void USpatialSender::GainAuthorityThenAddComponent(USpatialActorChannel* Channel // If this worker is EntityACL authoritative, we can directly update the component IDs to gain authority over. if (StaticComponentView->HasAuthority(EntityId, SpatialConstants::ENTITY_ACL_COMPONENT_ID)) { - const FClassInfo& ActorInfo = ClassInfoManager->GetOrCreateClassInfoByClass(Channel->Actor->GetClass()); - const WorkerAttributeSet WorkerAttribute = { ActorInfo.WorkerType.ToString() }; - const WorkerRequirementSet AuthoritativeWorkerRequirementSet = { WorkerAttribute }; + const WorkerRequirementSet AuthoritativeWorkerRequirementSet = { SpatialConstants::UnrealServerAttributeSet }; EntityAcl* EntityACL = StaticComponentView->GetComponentData(Channel->GetEntityId()); for (auto& ComponentId : NewComponentIds) @@ -227,8 +240,13 @@ void USpatialSender::SendRemoveComponents(Worker_EntityId EntityId, TArrayAllocateEntityId(), 1); +} + // Creates an entity authoritative on this server worker, ensuring it will be able to receive updates for the GSM. -void USpatialSender::CreateServerWorkerEntity(int AttemptCounter) +void USpatialSender::RetryServerWorkerEntityCreation(Worker_EntityId EntityId, int AttemptCounter) { const WorkerRequirementSet WorkerIdPermission{ { FString::Format(TEXT("workerId:{0}"), { Connection->GetWorkerId() }) } }; @@ -251,10 +269,10 @@ void USpatialSender::CreateServerWorkerEntity(int AttemptCounter) Components.Add(NetDriver->InterestFactory->CreateServerWorkerInterest(NetDriver->LoadBalanceStrategy).CreateInterestData()); Components.Add(ComponentPresence(EntityFactory::GetComponentPresenceList(Components)).CreateComponentPresenceData()); - const Worker_RequestId RequestId = Connection->SendCreateEntityRequest(MoveTemp(Components), nullptr); + const Worker_RequestId RequestId = Connection->SendCreateEntityRequest(MoveTemp(Components), &EntityId); CreateEntityDelegate OnCreateWorkerEntityResponse; - OnCreateWorkerEntityResponse.BindLambda([WeakSender = TWeakObjectPtr(this), AttemptCounter](const Worker_CreateEntityResponseOp& Op) + OnCreateWorkerEntityResponse.BindLambda([WeakSender = TWeakObjectPtr(this), EntityId, AttemptCounter](const Worker_CreateEntityResponseOp& Op) { if (!WeakSender.IsValid()) { @@ -265,7 +283,13 @@ void USpatialSender::CreateServerWorkerEntity(int AttemptCounter) if (Op.status_code == WORKER_STATUS_CODE_SUCCESS) { Sender->NetDriver->WorkerEntityId = Op.entity_id; - Sender->NetDriver->GlobalStateManager->TrySendWorkerReadyToBeginPlay(); + return; + } + + // Given the nature of commands, it's possible we have multiple create commands in flight at once. If a command fails where + // we've already set the worker entity ID locally, this means we already successfully create the entity, so nothing needs doing. + if (Op.status_code != WORKER_STATUS_CODE_SUCCESS && Sender->NetDriver->WorkerEntityId != SpatialConstants::INVALID_ENTITY_ID) + { return; } @@ -285,11 +309,11 @@ void USpatialSender::CreateServerWorkerEntity(int AttemptCounter) UE_LOG(LogSpatialSender, Warning, TEXT("Worker entity creation request timed out and will retry.")); FTimerHandle RetryTimer; - Sender->TimerManager->SetTimer(RetryTimer, [WeakSender, AttemptCounter]() + Sender->TimerManager->SetTimer(RetryTimer, [WeakSender, EntityId, AttemptCounter]() { if (USpatialSender* SpatialSender = WeakSender.Get()) { - SpatialSender->CreateServerWorkerEntity(AttemptCounter + 1); + SpatialSender->RetryServerWorkerEntityCreation(EntityId, AttemptCounter + 1); } }, SpatialConstants::GetCommandRetryWaitTimeSeconds(AttemptCounter), false); }); @@ -448,61 +472,17 @@ void USpatialSender::FlushRPCService() { RPCService->PushOverflowedRPCs(); - for (const SpatialRPCService::UpdateToSend& Update : RPCService->GetRPCsAndAcksToSend()) + const TArray RPCs = RPCService->GetRPCsAndAcksToSend(); + for (const SpatialRPCService::UpdateToSend& Update : RPCs) { Connection->SendComponentUpdate(Update.EntityId, &Update.Update); } - } -} - -void FillComponentInterests(const FClassInfo& Info, bool bNetOwned, TArray& ComponentInterest) -{ - if (Info.SchemaComponents[SCHEMA_OwnerOnly] != SpatialConstants::INVALID_COMPONENT_ID) - { - Worker_InterestOverride SingleClientInterest = { Info.SchemaComponents[SCHEMA_OwnerOnly], bNetOwned }; - ComponentInterest.Add(SingleClientInterest); - } - if (Info.SchemaComponents[SCHEMA_Handover] != SpatialConstants::INVALID_COMPONENT_ID) - { - Worker_InterestOverride HandoverInterest = { Info.SchemaComponents[SCHEMA_Handover], false }; - ComponentInterest.Add(HandoverInterest); - } -} - -TArray USpatialSender::CreateComponentInterestForActor(USpatialActorChannel* Channel, bool bIsNetOwned) -{ - TArray ComponentInterest; - - const FClassInfo& ActorInfo = ClassInfoManager->GetOrCreateClassInfoByClass(Channel->Actor->GetClass()); - FillComponentInterests(ActorInfo, bIsNetOwned, ComponentInterest); - - // Statically attached subobjects - for (auto& SubobjectInfoPair : ActorInfo.SubobjectInfo) - { - const FClassInfo& SubobjectInfo = SubobjectInfoPair.Value.Get(); - FillComponentInterests(SubobjectInfo, bIsNetOwned, ComponentInterest); - } - - // Subobjects dynamically created through replication - for (const auto& Subobject : Channel->CreateSubObjects) - { - const FClassInfo& SubobjectInfo = ClassInfoManager->GetOrCreateClassInfoByObject(Subobject); - FillComponentInterests(SubobjectInfo, bIsNetOwned, ComponentInterest); - } - - if (GetDefault()->UseRPCRingBuffer()) - { - ComponentInterest.Add({ SpatialConstants::CLIENT_ENDPOINT_COMPONENT_ID, bIsNetOwned }); - ComponentInterest.Add({ SpatialConstants::SERVER_ENDPOINT_COMPONENT_ID, bIsNetOwned }); - } - else - { - ComponentInterest.Add({ SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY, bIsNetOwned }); - ComponentInterest.Add({ SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID_LEGACY, bIsNetOwned }); + if (RPCs.Num()) + { + Connection->MaybeFlush(); + } } - - return ComponentInterest; } RPCPayload USpatialSender::CreateRPCPayloadFromParams(UObject* TargetObject, const FUnrealObjectRef& TargetObjectRef, UFunction* Function, void* Params) @@ -518,13 +498,6 @@ RPCPayload USpatialSender::CreateRPCPayloadFromParams(UObject* TargetObject, con #endif } -void USpatialSender::SendComponentInterestForActor(USpatialActorChannel* Channel, Worker_EntityId EntityId, bool bNetOwned) -{ - checkf(!NetDriver->IsServer(), TEXT("Tried to set ComponentInterest on a server-worker. This should never happen!")); - - NetDriver->Connection->SendComponentInterest(EntityId, CreateComponentInterestForActor(Channel, bNetOwned)); -} - void USpatialSender::SendInterestBucketComponentChange(const Worker_EntityId EntityId, const Worker_ComponentId OldComponent, const Worker_ComponentId NewComponent) { if (OldComponent != SpatialConstants::INVALID_COMPONENT_ID) @@ -552,13 +525,17 @@ void USpatialSender::SendInterestBucketComponentChange(const Worker_EntityId Ent } } -void USpatialSender::SendComponentInterestForSubobject(const FClassInfo& Info, Worker_EntityId EntityId, bool bNetOwned) +void USpatialSender::SendActorTornOffUpdate(Worker_EntityId EntityId, Worker_ComponentId ComponentId) { - checkf(!NetDriver->IsServer(), TEXT("Tried to set ComponentInterest on a server-worker. This should never happen!")); + FWorkerComponentUpdate ComponentUpdate = {}; - TArray ComponentInterest; - FillComponentInterests(Info, bNetOwned, ComponentInterest); - NetDriver->Connection->SendComponentInterest(EntityId, MoveTemp(ComponentInterest)); + ComponentUpdate.component_id = ComponentId; + ComponentUpdate.schema_type = Schema_CreateComponentUpdate(); + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(ComponentUpdate.schema_type); + + Schema_AddBool(ComponentObject, SpatialConstants::ACTOR_TEAROFF_ID, 1); + + Connection->SendComponentUpdate(EntityId, &ComponentUpdate); } void USpatialSender::SendPositionUpdate(Worker_EntityId EntityId, const FVector& Location) @@ -631,7 +608,7 @@ void USpatialSender::SetAclWriteAuthority(const SpatialLoadBalanceEnforcer::AclW if (ComponentId == SpatialConstants::ENTITY_ACL_COMPONENT_ID) { - NewAcl->ComponentWriteAcl.Add(ComponentId, { SpatialConstants::GetLoadBalancerAttributeSet(GetDefault()->LoadBalancingWorkerType.WorkerTypeName) }); + NewAcl->ComponentWriteAcl.Add(ComponentId, { SpatialConstants::UnrealServerAttributeSet } ); continue; } @@ -663,206 +640,194 @@ FRPCErrorInfo USpatialSender::SendRPC(const FPendingRPCParams& Params) return FRPCErrorInfo{ TargetObject, nullptr, ERPCResult::MissingFunctionInfo, true }; } - const float TimeDiff = (FDateTime::Now() - Params.Timestamp).GetTotalSeconds(); - if (GetDefault()->QueuedOutgoingRPCWaitTime < TimeDiff) + USpatialActorChannel* Channel = NetDriver->GetOrCreateSpatialActorChannel(TargetObject); + if (Channel == nullptr) { - return FRPCErrorInfo{ TargetObject, Function, ERPCResult::TimedOut, true }; + return FRPCErrorInfo{ TargetObject, Function, ERPCResult::NoActorChannel, true }; } - if (AActor* TargetActor = Cast(TargetObject)) + const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); + bool bUseRPCRingBuffer = GetDefault()->UseRPCRingBuffer(); + + if (RPCInfo.Type == ERPCType::CrossServer) { - if (TargetActor->IsPendingKillPending()) - { - return FRPCErrorInfo{ TargetObject, Function, ERPCResult::ActorPendingKill, true }; - } + SendCrossServerRPC(TargetObject, Function, Params.Payload, Channel, Params.ObjectRef); + return FRPCErrorInfo{ TargetObject, Function, ERPCResult::Success }; } - ERPCResult Result = SendRPCInternal(TargetObject, Function, Params.Payload); - - if (Result == ERPCResult::NoAuthority) + if (bUseRPCRingBuffer && RPCService != nullptr) { - if (AActor* TargetActor = Cast(TargetObject)) + if (SendRingBufferedRPC(TargetObject, Function, Params.Payload, Channel, Params.ObjectRef)) { - bool bShouldDrop = !WillHaveAuthorityOverActor(TargetActor, Params.ObjectRef.Entity); - return FRPCErrorInfo{ TargetObject, Function, Result, bShouldDrop }; + return FRPCErrorInfo{ TargetObject, Function, ERPCResult::Success }; } - } - - return FRPCErrorInfo{ TargetObject, Function, Result, false }; -} - -#if !UE_BUILD_SHIPPING -void USpatialSender::TrackRPC(AActor* Actor, UFunction* Function, const RPCPayload& Payload, const ERPCType RPCType) -{ - NETWORK_PROFILER(GNetworkProfiler.TrackSendRPC(Actor, Function, 0, Payload.CountDataBits(), 0, NetDriver->GetSpatialOSNetConnection())); - NetDriver->SpatialMetrics->TrackSentRPC(Function, RPCType, Payload.PayloadData.Num()); -} -#endif - -bool USpatialSender::WillHaveAuthorityOverActor(AActor* TargetActor, Worker_EntityId TargetEntity) -{ - bool WillHaveAuthorityOverActor = true; - - if (GetDefault()->bEnableOffloading) - { - if (!USpatialStatics::IsActorGroupOwnerForActor(TargetActor)) + else { - WillHaveAuthorityOverActor = false; + return FRPCErrorInfo{ TargetObject, Function, ERPCResult::RPCServiceFailure }; } } - if (GetDefault()->bEnableUnrealLoadBalancer) + if (Channel->bCreatingNewEntity && Function->HasAnyFunctionFlags(FUNC_NetClient)) { - if (NetDriver->VirtualWorkerTranslator != nullptr) - { - if (const SpatialGDK::AuthorityIntent* AuthorityIntentComponent = StaticComponentView->GetComponentData(TargetEntity)) - { - if (AuthorityIntentComponent->VirtualWorkerId != NetDriver->VirtualWorkerTranslator->GetLocalVirtualWorkerId()) - { - WillHaveAuthorityOverActor = false; - } - } - } + SendOnEntityCreationRPC(TargetObject, Function, Params.Payload, Channel, Params.ObjectRef); + return FRPCErrorInfo{ TargetObject, Function, ERPCResult::Success }; } - return WillHaveAuthorityOverActor; + return SendLegacyRPC(TargetObject, Function, Params.Payload, Channel, Params.ObjectRef); } -ERPCResult USpatialSender::SendRPCInternal(UObject* TargetObject, UFunction* Function, const RPCPayload& Payload) +void USpatialSender::SendOnEntityCreationRPC(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef) { - USpatialActorChannel* Channel = NetDriver->GetOrCreateSpatialActorChannel(TargetObject); + check(NetDriver->IsServer()); - if (!Channel) - { - UE_LOG(LogSpatialSender, Warning, TEXT("Failed to create an Actor Channel for %s."), *TargetObject->GetName()); - return ERPCResult::NoActorChannel; - } const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); - const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); - if (Channel->bCreatingNewEntity && !SpatialGDKSettings->UseRPCRingBuffer()) - { - if (Function->HasAnyFunctionFlags(FUNC_NetClient)) - { - check(NetDriver->IsServer()); - - OutgoingOnCreateEntityRPCs.FindOrAdd(Channel->Actor).RPCs.Add(Payload); + OutgoingOnCreateEntityRPCs.FindOrAdd(Channel->Actor).RPCs.Add(Payload); #if !UE_BUILD_SHIPPING - TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type); + TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type); #endif // !UE_BUILD_SHIPPING - return ERPCResult::Success; - } - } +} - Worker_EntityId EntityId = SpatialConstants::INVALID_ENTITY_ID; +void USpatialSender::SendCrossServerRPC(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef) +{ + const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); - switch (RPCInfo.Type) - { - case ERPCType::CrossServer: - { - Worker_ComponentId ComponentId = SpatialConstants::SERVER_TO_SERVER_COMMAND_ENDPOINT_COMPONENT_ID; + Worker_ComponentId ComponentId = SpatialConstants::SERVER_TO_SERVER_COMMAND_ENDPOINT_COMPONENT_ID; - Worker_CommandRequest CommandRequest = CreateRPCCommandRequest(TargetObject, Payload, ComponentId, RPCInfo.Index, EntityId); + Worker_EntityId EntityId = SpatialConstants::INVALID_ENTITY_ID; + Worker_CommandRequest CommandRequest = CreateRPCCommandRequest(TargetObject, Payload, ComponentId, RPCInfo.Index, EntityId); - check(EntityId != SpatialConstants::INVALID_ENTITY_ID); - Worker_RequestId RequestId = Connection->SendCommandRequest(EntityId, &CommandRequest, SpatialConstants::UNREAL_RPC_ENDPOINT_COMMAND_ID); + check(EntityId != SpatialConstants::INVALID_ENTITY_ID); + Worker_RequestId RequestId = Connection->SendCommandRequest(EntityId, &CommandRequest, SpatialConstants::UNREAL_RPC_ENDPOINT_COMMAND_ID); + if (Function->HasAnyFunctionFlags(FUNC_NetReliable)) + { + UE_LOG(LogSpatialSender, Verbose, TEXT("Sending reliable command request (entity: %lld, component: %d, function: %s, attempt: 1)"), + EntityId, CommandRequest.component_id, *Function->GetName()); + Receiver->AddPendingReliableRPC(RequestId, MakeShared(TargetObject, Function, ComponentId, RPCInfo.Index, Payload.PayloadData, 0)); + } + else + { + UE_LOG(LogSpatialSender, Verbose, TEXT("Sending unreliable command request (entity: %lld, component: %d, function: %s)"), + EntityId, CommandRequest.component_id, *Function->GetName()); + } #if !UE_BUILD_SHIPPING - TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type); + TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type); #endif // !UE_BUILD_SHIPPING +} - if (Function->HasAnyFunctionFlags(FUNC_NetReliable)) - { - UE_LOG(LogSpatialSender, Verbose, TEXT("Sending reliable command request (entity: %lld, component: %d, function: %s, attempt: 1)"), - EntityId, CommandRequest.component_id, *Function->GetName()); - Receiver->AddPendingReliableRPC(RequestId, MakeShared(TargetObject, Function, ComponentId, RPCInfo.Index, Payload.PayloadData, 0)); - } - else - { - UE_LOG(LogSpatialSender, Verbose, TEXT("Sending unreliable command request (entity: %lld, component: %d, function: %s)"), - EntityId, CommandRequest.component_id, *Function->GetName()); - } +FRPCErrorInfo USpatialSender::SendLegacyRPC(UObject* TargetObject, UFunction* Function, const RPCPayload& Payload, USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef) +{ + const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); - return ERPCResult::Success; + // Check if the Channel is listening + if ((RPCInfo.Type != ERPCType::NetMulticast) && !Channel->IsListening()) + { + // If the Entity endpoint is not yet ready to receive RPCs - + // treat the corresponding object as unresolved and queue RPC + // However, it doesn't matter in case of Multicast + return FRPCErrorInfo{ TargetObject, Function, ERPCResult::SpatialActorChannelNotListening }; } - case ERPCType::NetMulticast: - case ERPCType::ClientReliable: - case ERPCType::ServerReliable: - case ERPCType::ClientUnreliable: - case ERPCType::ServerUnreliable: + + // Check for Authority + Worker_EntityId EntityId = TargetObjectRef.Entity; + check(EntityId != SpatialConstants::INVALID_ENTITY_ID); + + Worker_ComponentId ComponentId = SpatialConstants::RPCTypeToWorkerComponentIdLegacy(RPCInfo.Type); + if (!NetDriver->StaticComponentView->HasAuthority(EntityId, ComponentId)) { - FUnrealObjectRef TargetObjectRef = PackageMap->GetUnrealObjectRefFromObject(TargetObject); - if (TargetObjectRef == FUnrealObjectRef::UNRESOLVED_OBJECT_REF) + bool bShouldDrop = true; + if (AActor* TargetActor = Cast(TargetObject)) { - return ERPCResult::UnresolvedTargetObject; + bShouldDrop = !WillHaveAuthorityOverActor(TargetActor, TargetObjectRef.Entity); } - if (SpatialGDKSettings->UseRPCRingBuffer() && RPCService != nullptr) - { - EPushRPCResult Result = RPCService->PushRPC(TargetObjectRef.Entity, RPCInfo.Type, Payload); + return FRPCErrorInfo{ TargetObject, Function, ERPCResult::NoAuthority, bShouldDrop }; + } - if (Result == EPushRPCResult::Success) - { - FlushRPCService(); - } + FWorkerComponentUpdate ComponentUpdate = CreateRPCEventUpdate(TargetObject, Payload, ComponentId, RPCInfo.Index); + Connection->SendComponentUpdate(EntityId, &ComponentUpdate); + Connection->MaybeFlush(); #if !UE_BUILD_SHIPPING - if (Result == EPushRPCResult::Success || Result == EPushRPCResult::QueueOverflowed) - { - TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type); - } + TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type); #endif // !UE_BUILD_SHIPPING - switch (Result) - { - case EPushRPCResult::QueueOverflowed: - UE_LOG(LogSpatialSender, Log, TEXT("USpatialSender::SendRPCInternal: Ring buffer queue overflowed, queuing RPC locally. Actor: %s, entity: %lld, function: %s"), *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); - break; - case EPushRPCResult::DropOverflowed: - UE_LOG(LogSpatialSender, Log, TEXT("USpatialSender::SendRPCInternal: Ring buffer queue overflowed, dropping RPC. Actor: %s, entity: %lld, function: %s"), *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); - break; - case EPushRPCResult::HasAckAuthority: - UE_LOG(LogSpatialSender, Warning, TEXT("USpatialSender::SendRPCInternal: Worker has authority over ack component for RPC it is sending. RPC will not be sent. Actor: %s, entity: %lld, function: %s"), *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); - break; - case EPushRPCResult::NoRingBufferAuthority: - // TODO: Change engine logic that calls Client RPCs from non-auth servers and change this to error. UNR-2517 - UE_LOG(LogSpatialSender, Log, TEXT("USpatialSender::SendRPCInternal: Failed to send RPC because the worker does not have authority over ring buffer component. Actor: %s, entity: %lld, function: %s"), *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); - break; - } - - return ERPCResult::Success; - } - - if (RPCInfo.Type != ERPCType::NetMulticast && !Channel->IsListening()) - { - // If the Entity endpoint is not yet ready to receive RPCs - - // treat the corresponding object as unresolved and queue RPC - // However, it doesn't matter in case of Multicast - return ERPCResult::SpatialActorChannelNotListening; - } - - EntityId = TargetObjectRef.Entity; - check(EntityId != SpatialConstants::INVALID_ENTITY_ID); - - Worker_ComponentId ComponentId = SpatialConstants::RPCTypeToWorkerComponentIdLegacy(RPCInfo.Type); + return FRPCErrorInfo{ TargetObject, Function, ERPCResult::Success }; +} - if (!NetDriver->StaticComponentView->HasAuthority(EntityId, ComponentId)) - { - return ERPCResult::NoAuthority; - } +bool USpatialSender::SendRingBufferedRPC(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, USpatialActorChannel* Channel, const FUnrealObjectRef& TargetObjectRef) +{ + const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); + const EPushRPCResult Result = RPCService->PushRPC(TargetObjectRef.Entity, RPCInfo.Type, Payload, Channel->bCreatedEntity); - FWorkerComponentUpdate ComponentUpdate = CreateRPCEventUpdate(TargetObject, Payload, ComponentId, RPCInfo.Index); + if (Result == EPushRPCResult::Success) + { + FlushRPCService(); + } - Connection->SendComponentUpdate(EntityId, &ComponentUpdate); #if !UE_BUILD_SHIPPING + if (Result == EPushRPCResult::Success || Result == EPushRPCResult::QueueOverflowed) + { TrackRPC(Channel->Actor, Function, Payload, RPCInfo.Type); -#endif // !UE_BUILD_SHIPPING - return ERPCResult::Success; } +#endif // !UE_BUILD_SHIPPING + + switch (Result) + { + case EPushRPCResult::QueueOverflowed: + UE_LOG(LogSpatialSender, Log, TEXT("USpatialSender::SendRingBufferedRPC: Ring buffer queue overflowed, queuing RPC locally. Actor: %s, entity: %lld, function: %s"), + *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); + return true; + case EPushRPCResult::DropOverflowed: + UE_LOG(LogSpatialSender, Log, TEXT("USpatialSender::SendRingBufferedRPC: Ring buffer queue overflowed, dropping RPC. Actor: %s, entity: %lld, function: %s"), + *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); + return true; + case EPushRPCResult::HasAckAuthority: + UE_LOG(LogSpatialSender, Warning, + TEXT("USpatialSender::SendRingBufferedRPC: Worker has authority over ack component for RPC it is sending. RPC will not be sent. Actor: %s, entity: %lld, function: %s"), + *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); + return true; + case EPushRPCResult::NoRingBufferAuthority: + // TODO: Change engine logic that calls Client RPCs from non-auth servers and change this to error. UNR-2517 + UE_LOG(LogSpatialSender, Log, + TEXT("USpatialSender::SendRingBufferedRPC: Failed to send RPC because the worker does not have authority over ring buffer component. Actor: %s, entity: %lld, function: %s"), + *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); + return true; + case EPushRPCResult::EntityBeingCreated: + UE_LOG(LogSpatialSender, Log, + TEXT("USpatialSender::SendRingBufferedRPC: RPC was called between entity creation and initial authority gain, so it will be queued. Actor: %s, entity: %lld, function: %s"), + *TargetObject->GetPathName(), TargetObjectRef.Entity, *Function->GetName()); + return false; default: - checkNoEntry(); - return ERPCResult::InvalidRPCType; + return true; + } +} + +#if !UE_BUILD_SHIPPING +void USpatialSender::TrackRPC(AActor* Actor, UFunction* Function, const RPCPayload& Payload, const ERPCType RPCType) +{ + NETWORK_PROFILER(GNetworkProfiler.TrackSendRPC(Actor, Function, 0, Payload.CountDataBits(), 0, NetDriver->GetSpatialOSNetConnection())); + NetDriver->SpatialMetrics->TrackSentRPC(Function, RPCType, Payload.PayloadData.Num()); +} +#endif + +bool USpatialSender::WillHaveAuthorityOverActor(AActor* TargetActor, Worker_EntityId TargetEntity) +{ + bool WillHaveAuthorityOverActor = true; + + if (NetDriver->VirtualWorkerTranslator != nullptr) + { + if (const SpatialGDK::AuthorityIntent* AuthorityIntentComponent = StaticComponentView->GetComponentData(TargetEntity)) + { + if (AuthorityIntentComponent->VirtualWorkerId != NetDriver->VirtualWorkerTranslator->GetLocalVirtualWorkerId()) + { + WillHaveAuthorityOverActor = false; + } + } } + + return WillHaveAuthorityOverActor; } void USpatialSender::EnqueueRetryRPC(TSharedRef RetryRPC) @@ -1100,31 +1065,29 @@ void USpatialSender::UpdateInterestComponent(AActor* Actor) Connection->SendComponentUpdate(EntityId, &Update); } -void USpatialSender::RetireEntity(const Worker_EntityId EntityId) +void USpatialSender::RetireEntity(const Worker_EntityId EntityId, bool bIsNetStartupActor) { - if (AActor* Actor = Cast(PackageMap->GetObjectFromEntityId(EntityId).Get())) + if (bIsNetStartupActor) { - if (Actor->IsNetStartupActor()) + Receiver->RemoveActor(EntityId); + // In the case that this is a startup actor, we won't actually delete the entity in SpatialOS. Instead we'll Tombstone it. + if (!StaticComponentView->HasComponent(EntityId, SpatialConstants::TOMBSTONE_COMPONENT_ID)) { - Receiver->RemoveActor(EntityId); - // In the case that this is a startup actor, we won't actually delete the entity in SpatialOS. Instead we'll Tombstone it. - if (!StaticComponentView->HasComponent(EntityId, SpatialConstants::TOMBSTONE_COMPONENT_ID)) - { - AddTombstoneToEntity(EntityId); - } - else - { - UE_LOG(LogSpatialSender, Verbose, TEXT("RetireEntity called on already retired entity: %lld (actor: %s)"), EntityId, *Actor->GetName()); - } + UE_LOG(LogSpatialSender, Log, TEXT("Adding tombstone to entity: %lld"), EntityId); + AddTombstoneToEntity(EntityId); } else { - Connection->SendDeleteEntityRequest(EntityId); + UE_LOG(LogSpatialSender, Verbose, TEXT("RetireEntity called on already retired entity: %lld"), EntityId); } } else { - UE_LOG(LogSpatialSender, Warning, TEXT("RetireEntity: Couldn't get Actor from PackageMap for EntityId: %lld"), EntityId); + // Actor no longer guaranteed to be in package map, but still useful for additional logging info + AActor* Actor = Cast(PackageMap->GetObjectFromEntityId(EntityId)); + + UE_LOG(LogSpatialSender, Log, TEXT("Sending delete entity request for %s with EntityId %lld, HasAuthority: %d"), *GetPathNameSafe(Actor), EntityId, Actor != nullptr ? Actor->HasAuthority() : false); + Connection->SendDeleteEntityRequest(EntityId); } } @@ -1134,7 +1097,7 @@ void USpatialSender::CreateTombstoneEntity(AActor* Actor) const Worker_EntityId EntityId = NetDriver->PackageMap->AllocateEntityIdAndResolveActor(Actor); - EntityFactory DataFactory(NetDriver, PackageMap, ClassInfoManager, ActorGroupManager, RPCService); + EntityFactory DataFactory(NetDriver, PackageMap, ClassInfoManager, RPCService); TArray Components = DataFactory.CreateTombstoneEntityComponents(Actor); Components.Add(CreateLevelComponentData(Actor)); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSnapshotManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSnapshotManager.cpp index 08d194b61c..97541dfdf6 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSnapshotManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSnapshotManager.cpp @@ -183,7 +183,7 @@ void SpatialSnapshotManager::LoadSnapshot(const FString& SnapshotName) // Check if this is the GSM for (auto& ComponentData : EntityToSpawn) { - if (ComponentData.component_id == SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID) + if (ComponentData.component_id == SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID) { // Save the new GSM Entity ID. GlobalStateManager->GlobalStateManagerEntityId = ReservedEntityID; diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp index c0e3a96716..2481a7d3a2 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp @@ -14,7 +14,6 @@ #include "Schema/RPCPayload.h" #include "Schema/ServerEndpoint.h" #include "Schema/ServerRPCEndpointLegacy.h" -#include "Schema/Singleton.h" #include "Schema/SpatialDebugging.h" #include "Schema/SpawnData.h" #include "Schema/UnrealMetadata.h" @@ -49,6 +48,13 @@ bool USpatialStaticComponentView::HasComponent(Worker_EntityId EntityId, Worker_ void USpatialStaticComponentView::OnAddComponent(const Worker_AddComponentOp& Op) { + // With dynamic components enabled, it's possible to get duplicate AddComponent ops which need handling idempotently. + // For the sake of efficiency we just exit early here. + if (HasComponent(Op.entity_id, Op.data.component_id)) + { + return; + } + TUniquePtr Data; switch (Op.data.component_id) { @@ -70,9 +76,6 @@ void USpatialStaticComponentView::OnAddComponent(const Worker_AddComponentOp& Op case SpatialConstants::SPAWN_DATA_COMPONENT_ID: Data = MakeUnique(Op.data); break; - case SpatialConstants::SINGLETON_COMPONENT_ID: - Data = MakeUnique(Op.data); - break; case SpatialConstants::UNREAL_METADATA_COMPONENT_ID: Data = MakeUnique(Op.data); break; diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp index 3833d0e994..ec00fc501a 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/GridBasedLBStrategy.cpp @@ -16,6 +16,8 @@ UGridBasedLBStrategy::UGridBasedLBStrategy() , WorldWidth(1000000.f) , WorldHeight(1000000.f) , InterestBorder(0.f) + , LocalCellId(0) + , bIsStrategyUsedOnLocalWorker(false) { } @@ -25,11 +27,6 @@ void UGridBasedLBStrategy::Init() UE_LOG(LogGridBasedLBStrategy, Log, TEXT("GridBasedLBStrategy initialized with Rows = %d and Cols = %d."), Rows, Cols); - for (uint32 i = 1; i <= Rows * Cols; i++) - { - VirtualWorkerIds.Add(i); - } - const float WorldWidthMin = -(WorldWidth / 2.f); const float WorldHeightMin = -(WorldHeight / 2.f); @@ -63,6 +60,22 @@ void UGridBasedLBStrategy::Init() } } +void UGridBasedLBStrategy::SetLocalVirtualWorkerId(VirtualWorkerId InLocalVirtualWorkerId) +{ + if (!VirtualWorkerIds.Contains(InLocalVirtualWorkerId)) + { + // This worker is simulating a layer which is not part of the grid. + LocalCellId = WorkerCells.Num(); + bIsStrategyUsedOnLocalWorker = false; + } + else + { + LocalCellId = VirtualWorkerIds.IndexOfByKey(InLocalVirtualWorkerId); + bIsStrategyUsedOnLocalWorker = true; + } + LocalVirtualWorkerId = InLocalVirtualWorkerId; +} + TSet UGridBasedLBStrategy::GetVirtualWorkerIds() const { return TSet(VirtualWorkerIds); @@ -76,8 +89,13 @@ bool UGridBasedLBStrategy::ShouldHaveAuthority(const AActor& Actor) const return false; } + if (!bIsStrategyUsedOnLocalWorker) + { + return false; + } + const FVector2D Actor2DLocation = FVector2D(SpatialGDK::GetActorSpatialPosition(&Actor)); - return IsInside(WorkerCells[LocalVirtualWorkerId - 1], Actor2DLocation); + return IsInside(WorkerCells[LocalCellId], Actor2DLocation); } VirtualWorkerId UGridBasedLBStrategy::WhoShouldHaveAuthority(const AActor& Actor) const @@ -90,10 +108,12 @@ VirtualWorkerId UGridBasedLBStrategy::WhoShouldHaveAuthority(const AActor& Actor const FVector2D Actor2DLocation = FVector2D(SpatialGDK::GetActorSpatialPosition(&Actor)); + check(VirtualWorkerIds.Num() == WorkerCells.Num()); for (int i = 0; i < WorkerCells.Num(); i++) { if (IsInside(WorkerCells[i], Actor2DLocation)) { + UE_LOG(LogGridBasedLBStrategy, Log, TEXT("Actor: %s, grid %d, worker %d for position %f, %f"), *AActor::GetDebugName(&Actor), i, VirtualWorkerIds[i], Actor2DLocation.X, Actor2DLocation.Y); return VirtualWorkerIds[i]; } } @@ -105,8 +125,9 @@ SpatialGDK::QueryConstraint UGridBasedLBStrategy::GetWorkerInterestQueryConstrai { // For a grid-based strategy, the interest area is the cell that the worker is authoritative over plus some border region. check(IsReady()); + check(bIsStrategyUsedOnLocalWorker); - const FBox2D Interest2D = WorkerCells[LocalVirtualWorkerId - 1].ExpandBy(InterestBorder); + const FBox2D Interest2D = WorkerCells[LocalCellId].ExpandBy(InterestBorder); const FVector2D Center2D = Interest2D.GetCenter(); const FVector Center3D{ Center2D.X, Center2D.Y, 0.0f}; @@ -123,10 +144,25 @@ SpatialGDK::QueryConstraint UGridBasedLBStrategy::GetWorkerInterestQueryConstrai FVector UGridBasedLBStrategy::GetWorkerEntityPosition() const { check(IsReady()); - const FVector2D Centre = WorkerCells[LocalVirtualWorkerId - 1].GetCenter(); + check(bIsStrategyUsedOnLocalWorker); + const FVector2D Centre = WorkerCells[LocalCellId].GetCenter(); return FVector{ Centre.X, Centre.Y, 0.f }; } +uint32 UGridBasedLBStrategy::GetMinimumRequiredWorkers() const +{ + return Rows * Cols; +} + +void UGridBasedLBStrategy::SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtualWorkerId, const VirtualWorkerId& LastVirtualWorkerId) +{ + UE_LOG(LogGridBasedLBStrategy, Log, TEXT("Setting VirtualWorkerIds %d to %d"), FirstVirtualWorkerId, LastVirtualWorkerId); + for (VirtualWorkerId CurrentVirtualWorkerId = FirstVirtualWorkerId; CurrentVirtualWorkerId <= LastVirtualWorkerId; CurrentVirtualWorkerId++) + { + VirtualWorkerIds.Add(CurrentVirtualWorkerId); + } +} + bool UGridBasedLBStrategy::IsInside(const FBox2D& Box, const FVector2D& Location) { return Location.X >= Box.Min.X && Location.Y >= Box.Min.Y diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/LayeredLBStrategy.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/LayeredLBStrategy.cpp new file mode 100644 index 0000000000..b5e36da985 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/LayeredLBStrategy.cpp @@ -0,0 +1,315 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "LoadBalancing/LayeredLBStrategy.h" + +#include "EngineClasses/SpatialNetDriver.h" +#include "EngineClasses/SpatialWorldSettings.h" +#include "LoadBalancing/GridBasedLBStrategy.h" +#include "Utils/LayerInfo.h" +#include "Utils/SpatialActorUtils.h" + +#include "Templates/Tuple.h" + +DEFINE_LOG_CATEGORY(LogLayeredLBStrategy); + +ULayeredLBStrategy::ULayeredLBStrategy() + : Super() +{ +} + +void ULayeredLBStrategy::Init() +{ + Super::Init(); + + VirtualWorkerId CurrentVirtualWorkerId = SpatialConstants::INVALID_VIRTUAL_WORKER_ID + 1; + + const ASpatialWorldSettings* WorldSettings = GetWorld() ? Cast(GetWorld()->GetWorldSettings()) : nullptr; + const bool bIsMultiWorkerEnabled = WorldSettings != nullptr && WorldSettings->IsMultiWorkerEnabled(); + + if (!bIsMultiWorkerEnabled) + { + UE_LOG(LogLayeredLBStrategy, Log, TEXT("Multi-Worker has been disabled. Creating LBStrategy for the Default Layer")); + UAbstractLBStrategy* DefaultLBStrategy = NewObject(this); + AddStrategyForLayer(SpatialConstants::DefaultLayer, DefaultLBStrategy); + return; + } + + // For each Layer, add a LB Strategy for that layer. + for (const TPair& Layer : WorldSettings->WorkerLayers) + { + const FName& LayerName = Layer.Key; + const FLayerInfo& LayerInfo = Layer.Value; + + UAbstractLBStrategy* LBStrategy; + if (LayerInfo.LoadBalanceStrategy == nullptr) + { + UE_LOG(LogLayeredLBStrategy, Error, TEXT("WorkerLayer %s does not specify a loadbalancing strategy (or it cannot be resolved). Using a 1x1 grid."), *LayerName.ToString()); + LBStrategy = NewObject(this); + } + else + { + LBStrategy = NewObject(this, LayerInfo.LoadBalanceStrategy); + } + + AddStrategyForLayer(LayerName, LBStrategy); + + UE_LOG(LogLayeredLBStrategy, Log, TEXT("Creating LBStrategy for Layer %s."), *LayerName.ToString()); + for (const TSoftClassPtr& ClassPtr : LayerInfo.ActorClasses) + { + UE_LOG(LogLayeredLBStrategy, Log, TEXT(" - Adding class %s."), *ClassPtr.GetAssetName()); + ClassPathToLayer.Add(ClassPtr, LayerName); + + } + } + + // Finally, add the default layer. + UE_LOG(LogLayeredLBStrategy, Log, TEXT("Creating LBStrategy for the Default Layer.")); + if (WorldSettings->DefaultLayerLoadBalanceStrategy == nullptr) + { + UE_LOG(LogLayeredLBStrategy, Error, TEXT("If EnableMultiWorker is set, there must be a LoadBalancing strategy set. Using a 1x1 grid.")); + UAbstractLBStrategy* DefaultLBStrategy = NewObject(this); + AddStrategyForLayer(SpatialConstants::DefaultLayer, DefaultLBStrategy); + } + else + { + UAbstractLBStrategy* DefaultLBStrategy = NewObject(this, WorldSettings->DefaultLayerLoadBalanceStrategy); + AddStrategyForLayer(SpatialConstants::DefaultLayer, DefaultLBStrategy); + + // Any class not specified on one of the other layers will be on the default layer. However, some games may have a class hierarchy with + // some parts of the hierarchy on different layers. This provides a way to specify that. + for (const TSoftClassPtr& ClassPtr : WorldSettings->ExplicitDefaultActorClasses) + { + UE_LOG(LogLayeredLBStrategy, Log, TEXT(" - Adding class to default layer %s."), *ClassPtr.GetAssetName()); + ClassPathToLayer.Add(ClassPtr, SpatialConstants::DefaultLayer); + } + } +} + +void ULayeredLBStrategy::SetLocalVirtualWorkerId(VirtualWorkerId InLocalVirtualWorkerId) +{ + if (LocalVirtualWorkerId != SpatialConstants::INVALID_VIRTUAL_WORKER_ID) + { + UE_LOG(LogLayeredLBStrategy, Error, + TEXT("The Local Virtual Worker Id cannot be set twice. Current value: %d Requested new value: %d"), + LocalVirtualWorkerId, InLocalVirtualWorkerId); + return; + } + + LocalVirtualWorkerId = InLocalVirtualWorkerId; + for (const auto& Elem : LayerNameToLBStrategy) + { + Elem.Value->SetLocalVirtualWorkerId(InLocalVirtualWorkerId); + } +} + +TSet ULayeredLBStrategy::GetVirtualWorkerIds() const +{ + return TSet(VirtualWorkerIds); +} + +bool ULayeredLBStrategy::ShouldHaveAuthority(const AActor& Actor) const +{ + if (!IsReady()) + { + UE_LOG(LogLayeredLBStrategy, Warning, TEXT("LayeredLBStrategy not ready to relinquish authority for Actor %s."), *AActor::GetDebugName(&Actor)); + return false; + } + + const AActor* RootOwner = &Actor; + while (RootOwner->GetOwner() != nullptr && RootOwner->GetOwner()->GetIsReplicated()) + { + RootOwner = RootOwner->GetOwner(); + } + + const FName& LayerName = GetLayerNameForActor(*RootOwner); + if (!LayerNameToLBStrategy.Contains(LayerName)) + { + UE_LOG(LogLayeredLBStrategy, Error, TEXT("LayeredLBStrategy doesn't have a LBStrategy for Actor %s which is in Layer %s."), *AActor::GetDebugName(RootOwner), *LayerName.ToString()); + return false; + } + + // If this worker is not responsible for the Actor's layer, just return false. + if (VirtualWorkerIdToLayerName.Contains(LocalVirtualWorkerId) && VirtualWorkerIdToLayerName[LocalVirtualWorkerId] != LayerName) + { + return false; + } + + return LayerNameToLBStrategy[LayerName]->ShouldHaveAuthority(Actor); +} + +VirtualWorkerId ULayeredLBStrategy::WhoShouldHaveAuthority(const AActor& Actor) const +{ + if (!IsReady()) + { + UE_LOG(LogLayeredLBStrategy, Warning, TEXT("LayeredLBStrategy not ready to decide on authority for Actor %s."), *AActor::GetDebugName(&Actor)); + return SpatialConstants::INVALID_VIRTUAL_WORKER_ID; + } + + const AActor* RootOwner = &Actor; + while (RootOwner->GetOwner() != nullptr && RootOwner->GetOwner()->GetIsReplicated()) + { + RootOwner = RootOwner->GetOwner(); + } + + const FName& LayerName = GetLayerNameForActor(*RootOwner); + if (!LayerNameToLBStrategy.Contains(LayerName)) + { + UE_LOG(LogLayeredLBStrategy, Error, TEXT("LayeredLBStrategy doesn't have a LBStrategy for Actor %s which is in Layer %s."), *AActor::GetDebugName(RootOwner), *LayerName.ToString()); + return SpatialConstants::INVALID_VIRTUAL_WORKER_ID; + } + + const VirtualWorkerId ReturnedWorkerId = LayerNameToLBStrategy[LayerName]->WhoShouldHaveAuthority(*RootOwner); + + UE_LOG(LogLayeredLBStrategy, Log, TEXT("LayeredLBStrategy returning virtual worker id %d for Actor %s."), ReturnedWorkerId, *AActor::GetDebugName(RootOwner)); + return ReturnedWorkerId; +} + +SpatialGDK::QueryConstraint ULayeredLBStrategy::GetWorkerInterestQueryConstraint() const +{ + check(IsReady()); + if (!VirtualWorkerIdToLayerName.Contains(LocalVirtualWorkerId)) + { + UE_LOG(LogLayeredLBStrategy, Error, TEXT("LayeredLBStrategy doesn't have a LBStrategy for worker %d."), LocalVirtualWorkerId); + SpatialGDK::QueryConstraint Constraint; + Constraint.ComponentConstraint = 0; + return Constraint; + } + else + { + const FName& LayerName = VirtualWorkerIdToLayerName[LocalVirtualWorkerId]; + check(LayerNameToLBStrategy.Contains(LayerName)); + return LayerNameToLBStrategy[LayerName]->GetWorkerInterestQueryConstraint(); + } +} + +FVector ULayeredLBStrategy::GetWorkerEntityPosition() const +{ + check(IsReady()); + if (!VirtualWorkerIdToLayerName.Contains(LocalVirtualWorkerId)) + { + UE_LOG(LogLayeredLBStrategy, Error, TEXT("LayeredLBStrategy doesn't have a LBStrategy for worker %d."), LocalVirtualWorkerId); + return FVector{ 0.f, 0.f, 0.f }; + } + else + { + const FName& LayerName = VirtualWorkerIdToLayerName[LocalVirtualWorkerId]; + check(LayerNameToLBStrategy.Contains(LayerName)); + return LayerNameToLBStrategy[LayerName]->GetWorkerEntityPosition(); + } +} + +uint32 ULayeredLBStrategy::GetMinimumRequiredWorkers() const +{ + // The MinimumRequiredWorkers for this strategy is a sum of the required workers for each of the wrapped strategies. + uint32 MinimumRequiredWorkers = 0; + for (const auto& Elem : LayerNameToLBStrategy) + { + MinimumRequiredWorkers += Elem.Value->GetMinimumRequiredWorkers(); + } + + UE_LOG(LogLayeredLBStrategy, Verbose, TEXT("LayeredLBStrategy needs %d workers to support all layer strategies."), MinimumRequiredWorkers); + return MinimumRequiredWorkers; +} + +void ULayeredLBStrategy::SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtualWorkerId, const VirtualWorkerId& LastVirtualWorkerId) +{ + // If the LayeredLBStrategy wraps { SingletonStrategy, 2x2 grid, Singleton } and is given IDs 1 through 6 it will assign: + // Singleton : 1 + // Grid : 2 - 5 + // Singleton: 6 + VirtualWorkerId NextWorkerIdToAssign = FirstVirtualWorkerId; + for (const auto& Elem : LayerNameToLBStrategy) + { + UAbstractLBStrategy* LBStrategy = Elem.Value; + VirtualWorkerId MinimumRequiredWorkers = LBStrategy->GetMinimumRequiredWorkers(); + + VirtualWorkerId LastVirtualWorkerIdToAssign = NextWorkerIdToAssign + MinimumRequiredWorkers - 1; + if (LastVirtualWorkerIdToAssign > LastVirtualWorkerId) + { + UE_LOG(LogLayeredLBStrategy, Error, TEXT("LayeredLBStrategy was not given enough VirtualWorkerIds to meet the demands of the layer strategies.")); + return; + } + UE_LOG(LogLayeredLBStrategy, Log, TEXT("LayeredLBStrategy assigning VirtualWorkerIds %d to %d to Layer %s"), NextWorkerIdToAssign, LastVirtualWorkerIdToAssign, *Elem.Key.ToString()); + LBStrategy->SetVirtualWorkerIds(NextWorkerIdToAssign, LastVirtualWorkerIdToAssign); + + for (VirtualWorkerId id = NextWorkerIdToAssign; id <= LastVirtualWorkerIdToAssign; id++) + { + VirtualWorkerIdToLayerName.Add(id, Elem.Key); + } + + NextWorkerIdToAssign += MinimumRequiredWorkers; + } + + // Keep a copy of the VirtualWorkerIds. This is temporary and will be removed in the next PR. + for (VirtualWorkerId CurrentVirtualWorkerId = FirstVirtualWorkerId; CurrentVirtualWorkerId <= LastVirtualWorkerId; CurrentVirtualWorkerId++) + { + VirtualWorkerIds.Add(CurrentVirtualWorkerId); + } +} + +// DEPRECATED +// This is only included because Scavengers uses the function in SpatialStatics that calls this. +// Once they are pick up this code, they should be able to switch to another method and we can remove this. +bool ULayeredLBStrategy::CouldHaveAuthority(const TSubclassOf Class) const +{ + check(IsReady()); + return *VirtualWorkerIdToLayerName.Find(LocalVirtualWorkerId) == GetLayerNameForClass(Class); +} + +UAbstractLBStrategy* ULayeredLBStrategy::GetLBStrategyForVisualRendering() const +{ + // The default strategy is guaranteed to exist as long as the strategy is ready. + check(IsReady()); + return LayerNameToLBStrategy[SpatialConstants::DefaultLayer]; +} + +FName ULayeredLBStrategy::GetLayerNameForClass(const TSubclassOf Class) const +{ + if (Class == nullptr) + { + return NAME_None; + } + + UClass* FoundClass = Class; + TSoftClassPtr ClassPtr = TSoftClassPtr(FoundClass); + + while (FoundClass != nullptr && FoundClass->IsChildOf(AActor::StaticClass())) + { + if (const FName* Layer = ClassPathToLayer.Find(ClassPtr)) + { + FName LayerHolder = *Layer; + if (FoundClass != Class) + { + ClassPathToLayer.Add(TSoftClassPtr(Class), LayerHolder); + } + return LayerHolder; + } + + FoundClass = FoundClass->GetSuperClass(); + ClassPtr = TSoftClassPtr(FoundClass); + } + + // No mapping found so set and return default actor group. + ClassPathToLayer.Add(TSoftClassPtr(Class), SpatialConstants::DefaultLayer); + return SpatialConstants::DefaultLayer; +} + +bool ULayeredLBStrategy::IsSameWorkerType(const AActor* ActorA, const AActor* ActorB) const +{ + if (ActorA == nullptr || ActorB == nullptr) + { + return false; + } + return GetLayerNameForClass(ActorA->GetClass()) == GetLayerNameForClass(ActorB->GetClass()); +} + +FName ULayeredLBStrategy::GetLayerNameForActor(const AActor& Actor) const +{ + return GetLayerNameForClass(Actor.GetClass()); +} + +void ULayeredLBStrategy::AddStrategyForLayer(const FName& LayerName, UAbstractLBStrategy* LBStrategy) +{ + LayerNameToLBStrategy.Add(LayerName, LBStrategy); + LayerNameToLBStrategy[LayerName]->Init(); +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/OwnershipLockingPolicy.cpp b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/OwnershipLockingPolicy.cpp index a2c0fcde99..91a0993c12 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/OwnershipLockingPolicy.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/LoadBalancing/OwnershipLockingPolicy.cpp @@ -49,7 +49,7 @@ ActorLockToken UOwnershipLockingPolicy::AcquireLock(AActor* Actor, FString Debug ActorToLockingState.Add(Actor, MigrationLockElement{ 1, OwnershipHierarchyRoot }); } - UE_LOG(LogOwnershipLockingPolicy, Log, TEXT("Acquiring migration lock. " + UE_LOG(LogOwnershipLockingPolicy, Verbose, TEXT("Acquiring migration lock. " "Actor: %s. Lock name: %s. Token %d: Locks held: %d."), *GetNameSafe(Actor), *DebugString, NextToken, ActorToLockingState.Find(Actor)->LockCount); TokenToNameAndActor.Emplace(NextToken, LockNameAndActor{ MoveTemp(DebugString), Actor }); return NextToken++; @@ -66,7 +66,7 @@ bool UOwnershipLockingPolicy::ReleaseLock(const ActorLockToken Token) AActor* Actor = NameAndActor->Actor; const FString& Name = NameAndActor->LockName; - UE_LOG(LogOwnershipLockingPolicy, Log, TEXT("Releasing Actor migration lock. Actor: %s. Token: %d. Lock name: %s"), *Actor->GetName(), Token, *Name); + UE_LOG(LogOwnershipLockingPolicy, Verbose, TEXT("Releasing Actor migration lock. Actor: %s. Token: %d. Lock name: %s"), *Actor->GetName(), Token, *Name); check(ActorToLockingState.Contains(Actor)); @@ -76,7 +76,7 @@ bool UOwnershipLockingPolicy::ReleaseLock(const ActorLockToken Token) MigrationLockElement& ActorLockingState = CountIt.Value(); if (ActorLockingState.LockCount == 1) { - UE_LOG(LogOwnershipLockingPolicy, Log, TEXT("Actor migration no longer locked. Actor: %s"), *Actor->GetName()); + UE_LOG(LogOwnershipLockingPolicy, Verbose, TEXT("Actor migration no longer locked. Actor: %s"), *Actor->GetName()); Actor->OnDestroyed.RemoveDynamic(this, &UOwnershipLockingPolicy::OnExplicitlyLockedActorDeleted); RemoveOwnershipHierarchyRootInformation(ActorLockingState.HierarchyRoot, Actor); CountIt.RemoveCurrent(); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Schema/UnrealObjectRef.cpp b/SpatialGDK/Source/SpatialGDK/Private/Schema/UnrealObjectRef.cpp index 6a4a486ad5..fb2375fe72 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Schema/UnrealObjectRef.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Schema/UnrealObjectRef.cpp @@ -5,6 +5,11 @@ #include "EngineClasses/SpatialPackageMapClient.h" #include "SpatialConstants.h" #include "Utils/SchemaUtils.h" +#include "Utils/SpatialDebugger.h" +#include "Utils/SpatialMetricsDisplay.h" + +#include "GameFramework/GameModeBase.h" +#include "GameFramework/GameStateBase.h" DEFINE_LOG_CATEGORY_STATIC(LogUnrealObjectRef, Log, All); @@ -19,16 +24,16 @@ UObject* FUnrealObjectRef::ToObjectPtr(const FUnrealObjectRef& ObjectRef, USpati } else { - if (ObjectRef.bUseSingletonClassPath) + if (ObjectRef.bUseClassPathToLoadObject) { - // This is a singleton ref, which means it's just the UnrealObjectRef of the singleton class, with this boolean set. - // Unset it to get the original UnrealObjectRef of its singleton class, and look it up in the PackageMap. - FUnrealObjectRef SingletonClassRef = ObjectRef; - SingletonClassRef.bUseSingletonClassPath = false; + FUnrealObjectRef ClassRef = ObjectRef; + ClassRef.bUseClassPathToLoadObject = false; - UObject* Value = PackageMap->GetSingletonByClassRef(SingletonClassRef); + UObject* Value = PackageMap->GetUniqueActorInstanceByClassRef(ClassRef); if (Value == nullptr) { + // This could happen if we no longer spawn all of these Actors before starting replication. + UE_LOG(LogUnrealObjectRef, Warning, TEXT("Could not load object reference by class path: %s"), **ClassRef.Path); bOutUnresolved = true; } return Value; @@ -121,10 +126,11 @@ FUnrealObjectRef FUnrealObjectRef::FromObjectPtr(UObject* ObjectValue, USpatialP } } - // If this is a singleton that hasn't been resolved yet, send its class path instead. - if (ObjectValue->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)) + // If this is an Actor that should exist once per Worker (e.g. GameMode, GameState) and hasn't been resolved yet, + // send its class path instead. + if (ShouldLoadObjectFromClassPath(ObjectValue)) { - ObjectRef = GetSingletonClassRef(ObjectValue, PackageMap); + ObjectRef = GetRefFromObjectClassPath(ObjectValue, PackageMap); if (ObjectRef.IsValid()) { return ObjectRef; @@ -194,12 +200,30 @@ FSoftObjectPath FUnrealObjectRef::ToSoftObjectPath(const FUnrealObjectRef& Objec return FSoftObjectPath(MoveTemp(FullPackagePath)); } -FUnrealObjectRef FUnrealObjectRef::GetSingletonClassRef(UObject* SingletonObject, USpatialPackageMapClient* PackageMap) +bool FUnrealObjectRef::ShouldLoadObjectFromClassPath(UObject* Object) +{ + // We don't want to add objects to this list which are stably-named. This is because: + // - stably-named Actors are already handled correctly by the GDK and don't need additional special casing, + // - stably-named Actors then follow two different logic paths at various points in the GDK which results in + // inconsistent package map entries. + // The ensure statement below is a sanity check that we don't inadvertently add a stably-name Actor to this list. + return IsUniqueActorClass(Object->GetClass()) && ensure(!Object->IsNameStableForNetworking()); +} + +bool FUnrealObjectRef::IsUniqueActorClass(UClass* Class) +{ + return Class->IsChildOf() + || Class->IsChildOf() + || Class->IsChildOf() + || Class->IsChildOf(); +} + +FUnrealObjectRef FUnrealObjectRef::GetRefFromObjectClassPath(UObject* Object, USpatialPackageMapClient* PackageMap) { - FUnrealObjectRef ClassObjectRef = FromObjectPtr(SingletonObject->GetClass(), PackageMap); + FUnrealObjectRef ClassObjectRef = FromObjectPtr(Object->GetClass(), PackageMap); if (ClassObjectRef.IsValid()) { - ClassObjectRef.bUseSingletonClassPath = true; + ClassObjectRef.bUseClassPathToLoadObject = true; } return ClassObjectRef; } diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp index 2da7e83a1b..c7818b0aa9 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp @@ -1,13 +1,19 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "SpatialGDKSettings.h" + #include "Improbable/SpatialEngineConstants.h" #include "Misc/MessageDialog.h" #include "Misc/CommandLine.h" #include "SpatialConstants.h" #if WITH_EDITOR +#include "HAL/PlatformFilemanager.h" +#include "Misc/FileHelper.h" #include "Settings/LevelEditorPlaySettings.h" + +#include "SpatialGDKServicesConstants.h" +#include "SpatialGDKServicesModule.h" #endif DEFINE_LOG_CATEGORY(LogSpatialGDKSettings); @@ -30,6 +36,23 @@ namespace } 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) + { + if (FParse::Param(CommandLine, Parameter)) + { + bOutValue = true; + } + else + { + TCHAR TempStr[16]; + if (FParse::Value(CommandLine, Parameter, TempStr, 16) && TempStr[0] == '=') + { + bOutValue = FCString::ToBool(TempStr + 1); // + 1 to skip = + } + } + UE_LOG(LogSpatialGDKSettings, Log, TEXT("%s is %s."), PrettyName, bOutValue.IsSet() ? bOutValue ? TEXT("enabled") : TEXT("disabled") : TEXT("not set")); + } } USpatialGDKSettings::USpatialGDKSettings(const FObjectInitializer& ObjectInitializer) @@ -44,10 +67,10 @@ USpatialGDKSettings::USpatialGDKSettings(const FObjectInitializer& ObjectInitial , EntityCreationRateLimit(0) , bUseIsActorRelevantForConnection(false) , OpsUpdateRate(1000.0f) - , bEnableHandover(true) + , bEnableHandover(false) , MaxNetCullDistanceSquared(0.0f) // Default disabled , QueuedIncomingRPCWaitTime(1.0f) - , QueuedOutgoingRPCWaitTime(30.0f) + , QueuedOutgoingRPCRetryTime(1.0f) , PositionUpdateFrequency(1.0f) , PositionDistanceThreshold(100.0f) // 1m (100cm) , bEnableMetrics(true) @@ -56,23 +79,17 @@ USpatialGDKSettings::USpatialGDKSettings(const FObjectInitializer& ObjectInitial , bUseFrameTimeAsLoad(false) , bBatchSpatialPositionUpdates(false) , MaxDynamicallyAttachedSubobjectsPerClass(3) - , bEnableResultTypes(true) , ServicesRegion(EServicesRegion::Default) - , DefaultWorkerType(FWorkerType(SpatialConstants::DefaultServerWorkerType)) - , bEnableOffloading(false) - , ServerWorkerTypes({ SpatialConstants::DefaultServerWorkerType }) , WorkerLogLevel(ESettingsWorkerLogVerbosity::Warning) - , bEnableUnrealLoadBalancer(false) , bRunSpatialWorkerConnectionOnGameThread(false) , bUseRPCRingBuffers(true) , DefaultRPCRingBufferSize(32) , MaxRPCRingBufferSize(32) // TODO - UNR 2514 - These defaults are not necessarily optimal - readdress when we have better data , bTcpNoDelay(false) - , UdpServerUpstreamUpdateIntervalMS(1) , UdpServerDownstreamUpdateIntervalMS(1) - , UdpClientUpstreamUpdateIntervalMS(1) , UdpClientDownstreamUpdateIntervalMS(1) + , bWorkerFlushAfterOutgoingNetworkOp(false) // TODO - end , bAsyncLoadNewClassesOnEntityCheckout(false) , RPCQueueWarningDefaultTimeout(2.0f) @@ -83,7 +100,7 @@ USpatialGDKSettings::USpatialGDKSettings(const FObjectInitializer& ObjectInitial , bUseSecureServerConnection(false) , bEnableClientQueriesOnServer(false) , bUseSpatialView(false) - , bUseDevelopmentAuthenticationFlow(false) + , bEnableMultiWorkerDebuggingWarnings(false) { DefaultReceptionistHost = SpatialConstants::LOCAL_HOST; } @@ -94,29 +111,21 @@ void USpatialGDKSettings::PostInitProperties() // Check any command line overrides for using QBI, Offloading (after reading the config value): const TCHAR* CommandLine = FCommandLine::Get(); - CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideSpatialOffloading"), TEXT("Offloading"), bEnableOffloading); CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideHandover"), TEXT("Handover"), bEnableHandover); - CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideLoadBalancer"), TEXT("Load balancer"), bEnableUnrealLoadBalancer); + 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("OverrideResultTypes"), TEXT("Result types"), bEnableResultTypes); CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideNetCullDistanceInterest"), TEXT("Net cull distance interest"), bEnableNetCullDistanceInterest); CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideNetCullDistanceInterestFrequency"), TEXT("Net cull distance interest frequency"), bEnableNetCullDistanceFrequency); CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideActorRelevantForConnection"), TEXT("Actor relevant for connection"), bUseIsActorRelevantForConnection); CheckCmdLineOverrideBool(CommandLine, TEXT("OverrideBatchSpatialPositionUpdates"), TEXT("Batch spatial position updates"), bBatchSpatialPositionUpdates); - - if (bEnableUnrealLoadBalancer) - { - if (bEnableHandover == false) - { - UE_LOG(LogSpatialGDKSettings, Warning, TEXT("Unreal load balancing is enabled, but handover is disabled.")); - } - } + 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); #if WITH_EDITOR ULevelEditorPlaySettings* PlayInSettings = GetMutableDefault(); - PlayInSettings->bEnableOffloading = bEnableOffloading; - PlayInSettings->DefaultWorkerType = DefaultWorkerType.WorkerTypeName; + PlayInSettings->DefaultWorkerType = SpatialConstants::DefaultServerWorkerType; #endif } @@ -128,20 +137,18 @@ void USpatialGDKSettings::PostEditChangeProperty(struct FPropertyChangedEvent& P // 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(USpatialGDKSettings, bEnableOffloading)) - { - GetMutableDefault()->bEnableOffloading = bEnableOffloading; - } - else if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, DefaultWorkerType)) - { - GetMutableDefault()->DefaultWorkerType = DefaultWorkerType.WorkerTypeName; - } - else if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, MaxDynamicallyAttachedSubobjectsPerClass)) + // TODO(UNR-3569): Engine PR to remove bEnableOffloading from ULevelEditorPlaySettings. + + if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, MaxDynamicallyAttachedSubobjectsPerClass)) { FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(TEXT("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)) + { + UpdateServicesRegionFile(); + } } bool USpatialGDKSettings::CanEditChange(const UProperty* InProperty) const @@ -153,11 +160,6 @@ bool USpatialGDKSettings::CanEditChange(const UProperty* InProperty) const const FName Name = InProperty->GetFName(); - if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, bUseRPCRingBuffers)) - { - return !bEnableUnrealLoadBalancer; - } - if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, DefaultRPCRingBufferSize) || Name == GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, RPCRingBufferSizeMap) || Name == GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, MaxRPCRingBufferSize)) @@ -168,7 +170,27 @@ bool USpatialGDKSettings::CanEditChange(const UProperty* InProperty) const return true; } -#endif +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); + if (IsRunningInChina()) + { + if (!FPaths::FileExists(UseChinaServicesRegionFilepath)) + { + FFileHelper::SaveStringToFile(TEXT(""), *UseChinaServicesRegionFilepath); + } + } + else + { + if (FPaths::FileExists(UseChinaServicesRegionFilepath)) + { + FPlatformFileManager::Get().GetPlatformFile().DeleteFile(*UseChinaServicesRegionFilepath); + } + } +} + +#endif // WITH_EDITOR uint32 USpatialGDKSettings::GetRPCRingBufferSize(ERPCType RPCType) const { @@ -184,7 +206,7 @@ uint32 USpatialGDKSettings::GetRPCRingBufferSize(ERPCType RPCType) const bool USpatialGDKSettings::UseRPCRingBuffer() const { // RPC Ring buffer are necessary in order to do RPC handover, something legacy RPC does not handle. - return bUseRPCRingBuffers || bEnableUnrealLoadBalancer; + return bUseRPCRingBuffers; } float USpatialGDKSettings::GetSecondsBeforeWarning(const ERPCResult Result) const @@ -197,11 +219,16 @@ float USpatialGDKSettings::GetSecondsBeforeWarning(const ERPCResult Result) cons return RPCQueueWarningDefaultTimeout; } -bool USpatialGDKSettings::GetPreventClientCloudDeploymentAutoConnect(bool bIsClient) const +void USpatialGDKSettings::SetServicesRegion(EServicesRegion::Type NewRegion) { -#if WITH_EDITOR - return false; -#else - return bIsClient && bPreventClientCloudDeploymentAutoConnect; -#endif + ServicesRegion = NewRegion; + + // Save in default config so this applies for other platforms e.g. Linux, Android. + UProperty* ServicesRegionProperty = USpatialGDKSettings::StaticClass()->FindPropertyByName(FName("ServicesRegion")); + UpdateSinglePropertyInConfigFile(ServicesRegionProperty, GetDefaultConfigFilename()); +} + +bool USpatialGDKSettings::GetPreventClientCloudDeploymentAutoConnect() const +{ + return (IsRunningGame() || IsRunningClientOnly()) && bPreventClientCloudDeploymentAutoConnect; }; diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ComponentData.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ComponentData.cpp new file mode 100644 index 0000000000..db184f1b9d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ComponentData.cpp @@ -0,0 +1,71 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialView/ComponentData.h" +#include "SpatialView/ComponentUpdate.h" + +namespace SpatialGDK +{ + +void ComponentDataDeleter::operator()(Schema_ComponentData* ComponentData) const noexcept +{ + if (ComponentData == nullptr) + { + return; + } + + Schema_DestroyComponentData(ComponentData); +} + +ComponentData::ComponentData(Worker_ComponentId Id) + : ComponentId(Id) + , Data(Schema_CreateComponentData()) +{ +} + +ComponentData::ComponentData(OwningComponentDataPtr Data, Worker_ComponentId Id) + : ComponentId(Id) + , Data(MoveTemp(Data)) +{ +} + +ComponentData ComponentData::CreateCopy(const Schema_ComponentData* Data, Worker_ComponentId Id) +{ + return ComponentData(OwningComponentDataPtr(Schema_CopyComponentData(Data)), Id); +} + +ComponentData ComponentData::DeepCopy() const +{ + return CreateCopy(Data.Get(), ComponentId); +} + +Schema_ComponentData* ComponentData::Release() && +{ + return Data.Release(); +} + +bool ComponentData::ApplyUpdate(const ComponentUpdate& Update) +{ + check(Update.GetComponentId() == GetComponentId()); + check(Update.GetUnderlying() != nullptr); + + return Schema_ApplyComponentUpdateToData(Update.GetUnderlying(), Data.Get()) != 0; +} + +Schema_Object* ComponentData::GetFields() const +{ + check(Data.IsValid()); + return Schema_GetComponentDataFields(Data.Get()); +} + +Schema_ComponentData* ComponentData::GetUnderlying() const +{ + check(Data.IsValid()); + return Data.Get(); +} + +Worker_ComponentId ComponentData::GetComponentId() const +{ + return ComponentId; +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ComponentUpdate.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ComponentUpdate.cpp new file mode 100644 index 0000000000..65f73fc7ce --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ComponentUpdate.cpp @@ -0,0 +1,75 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialView/ComponentUpdate.h" + +namespace SpatialGDK +{ + +void ComponentUpdateDeleter::operator()(Schema_ComponentUpdate* ComponentUpdate) const noexcept +{ + if (ComponentUpdate == nullptr) + { + return; + } + Schema_DestroyComponentUpdate(ComponentUpdate); +} + +ComponentUpdate::ComponentUpdate(Worker_ComponentId Id) + : ComponentId(Id) + , Update(Schema_CreateComponentUpdate()) +{ +} + +ComponentUpdate::ComponentUpdate(OwningComponentUpdatePtr Update, Worker_ComponentId Id) + : ComponentId(Id) + , Update(MoveTemp(Update)) +{ +} + +ComponentUpdate ComponentUpdate::CreateCopy(const Schema_ComponentUpdate* Update, Worker_ComponentId Id) +{ + return ComponentUpdate(OwningComponentUpdatePtr(Schema_CopyComponentUpdate(Update)), Id); +} + +ComponentUpdate ComponentUpdate::DeepCopy() const +{ + return CreateCopy(Update.Get(), ComponentId); +} + +Schema_ComponentUpdate* ComponentUpdate::Release() && +{ + return Update.Release(); +} + +bool ComponentUpdate::Merge(ComponentUpdate Other) +{ + check(Other.GetComponentId() == GetComponentId()); + check(Other.Update.IsValid()); + // Calling GetUnderlying instead of Release + // as we still need to manually destroy Other. + return Schema_MergeComponentUpdateIntoUpdate(Other.GetUnderlying(), Update.Get()) != 0; +} + +Schema_Object* ComponentUpdate::GetFields() const +{ + check(Update.IsValid()); + return Schema_GetComponentUpdateFields(Update.Get()); +} + +Schema_Object* ComponentUpdate::GetEvents() const +{ + check(Update.IsValid()); + return Schema_GetComponentUpdateEvents(Update.Get()); +} + +Schema_ComponentUpdate* ComponentUpdate::GetUnderlying() const +{ + return Update.Get(); +} + +Worker_ComponentId ComponentUpdate::GetComponentId() const +{ + return ComponentId; +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityComponentRecord.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityComponentRecord.cpp new file mode 100644 index 0000000000..4063d64d96 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityComponentRecord.cpp @@ -0,0 +1,103 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialView/EntityComponentRecord.h" + +namespace SpatialGDK +{ + +void EntityComponentRecord::AddComponent(Worker_EntityId EntityId, ComponentData Data) +{ + const EntityComponentId Id = { EntityId, Data.GetComponentId() }; + + // If the component is recorded as removed then transition to complete-updated. + // otherwise record it as added. + if (ComponentsRemoved.RemoveSingleSwap(Id)) + { + UpdateRecord.AddComponentDataAsUpdate(EntityId, MoveTemp(Data)); + } + else + { + ComponentsAdded.Push(EntityComponentData{ EntityId, MoveTemp(Data) }); + } +} + +void EntityComponentRecord::RemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) +{ + const EntityComponentId Id = { EntityId, ComponentId }; + const EntityComponentData* FoundComponentAdded = ComponentsAdded.FindByPredicate(EntityComponentIdEquality{ Id }); + + // If the component is recorded as added then erase the record. + // Otherwise record it as removed (additionally making sure it isn't recorded as updated). + if (FoundComponentAdded) + { + ComponentsAdded.RemoveAtSwap(FoundComponentAdded - ComponentsAdded.GetData()); + } + else + { + UpdateRecord.RemoveComponent(EntityId, ComponentId); + ComponentsRemoved.Push(Id); + } +} + +void EntityComponentRecord::AddComponentAsUpdate(Worker_EntityId EntityId, ComponentData Data) +{ + const EntityComponentId Id = { EntityId, Data.GetComponentId() }; + EntityComponentData* FoundComponentAdded = ComponentsAdded.FindByPredicate(EntityComponentIdEquality{ Id }); + + // If the entity-component is recorded is added, then merge the update to the added component. + // Otherwise handle it as an update. + if (FoundComponentAdded) + { + FoundComponentAdded->Data = MoveTemp(Data); + } + else + { + UpdateRecord.AddComponentDataAsUpdate(EntityId, MoveTemp(Data)); + } +} + +void EntityComponentRecord::AddUpdate(Worker_EntityId EntityId, ComponentUpdate Update) +{ + const EntityComponentId Id = { EntityId, Update.GetComponentId() }; + EntityComponentData* FoundComponentAdded = ComponentsAdded.FindByPredicate(EntityComponentIdEquality{ Id }); + + // If the entity-component is recorded is added, then merge the update to the added component. + // Otherwise handle it as an update. + if (FoundComponentAdded) + { + FoundComponentAdded->Data.ApplyUpdate(Update); + } + else + { + UpdateRecord.AddComponentUpdate(EntityId, MoveTemp(Update)); + } +} + +void EntityComponentRecord::Clear() +{ + ComponentsAdded.Empty(); + ComponentsRemoved.Empty(); + UpdateRecord.Clear(); +} + +const TArray& EntityComponentRecord::GetComponentsAdded() const +{ + return ComponentsAdded; +} + +const TArray& EntityComponentRecord::GetComponentsRemoved() const +{ + return ComponentsRemoved; +} + +const TArray& EntityComponentRecord::GetUpdates() const +{ + return UpdateRecord.GetUpdates(); +} + +const TArray& EntityComponentRecord::GetCompleteUpdates() const +{ + return UpdateRecord.GetCompleteUpdates(); +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityComponentUpdateRecord.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityComponentUpdateRecord.cpp new file mode 100644 index 0000000000..adee8f37ec --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/EntityComponentUpdateRecord.cpp @@ -0,0 +1,106 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialView/EntityComponentUpdateRecord.h" + +namespace SpatialGDK +{ + +void EntityComponentUpdateRecord::AddComponentDataAsUpdate(Worker_EntityId EntityId, ComponentData CompleteUpdate) +{ + const EntityComponentId Id = {EntityId, CompleteUpdate.GetComponentId()}; + EntityComponentUpdate* FoundUpdate = Updates.FindByPredicate(EntityComponentIdEquality{Id}); + + if (FoundUpdate) + { + CompleteUpdates.Emplace(EntityComponentCompleteUpdate{ EntityId, MoveTemp(CompleteUpdate), MoveTemp(FoundUpdate->Update) }); + Updates.RemoveAtSwap(FoundUpdate - Updates.GetData()); + } + else + { + InsertOrSetCompleteUpdate(EntityId, MoveTemp(CompleteUpdate)); + } +} + +void EntityComponentUpdateRecord::AddComponentUpdate(Worker_EntityId EntityId, ComponentUpdate Update) +{ + const EntityComponentId Id = {EntityId, Update.GetComponentId()}; + EntityComponentCompleteUpdate* FoundCompleteUpdate = CompleteUpdates.FindByPredicate(EntityComponentIdEquality{Id}); + + if (FoundCompleteUpdate != nullptr) + { + FoundCompleteUpdate->CompleteUpdate.ApplyUpdate(Update); + FoundCompleteUpdate->Events.Merge(MoveTemp(Update)); + } + else + { + InsertOrMergeUpdate(EntityId, MoveTemp(Update)); + } +} + +void EntityComponentUpdateRecord::RemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) +{ + const EntityComponentId Id = {EntityId, ComponentId}; + + const EntityComponentUpdate* FoundUpdate = Updates.FindByPredicate(EntityComponentIdEquality{Id}); + if (FoundUpdate) + { + Updates.RemoveAtSwap(FoundUpdate - Updates.GetData()); + } + // 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}); + if (FoundCompleteUpdate) + { + CompleteUpdates.RemoveAtSwap(FoundCompleteUpdate - CompleteUpdates.GetData()); + } + } +} + +void EntityComponentUpdateRecord::Clear() +{ + Updates.Empty(); + CompleteUpdates.Empty(); +} + +const TArray& EntityComponentUpdateRecord::GetUpdates() const +{ + return Updates; +} + +const TArray& EntityComponentUpdateRecord::GetCompleteUpdates() const +{ + return CompleteUpdates; +} + +void EntityComponentUpdateRecord::InsertOrMergeUpdate(Worker_EntityId EntityId, ComponentUpdate Update) +{ + const EntityComponentId Id = {EntityId, Update.GetComponentId()}; + EntityComponentUpdate* FoundUpdate = Updates.FindByPredicate(EntityComponentIdEquality{Id}); + + if (FoundUpdate != nullptr) + { + FoundUpdate->Update.Merge(MoveTemp(Update)); + } + else + { + Updates.Emplace(EntityComponentUpdate{ EntityId, MoveTemp(Update) }); + } +} + +void EntityComponentUpdateRecord::InsertOrSetCompleteUpdate(Worker_EntityId EntityId, ComponentData CompleteUpdate) +{ + const EntityComponentId Id = {EntityId, CompleteUpdate.GetComponentId()}; + EntityComponentCompleteUpdate* FoundCompleteUpdate = CompleteUpdates.FindByPredicate(EntityComponentIdEquality{Id}); + + if (FoundCompleteUpdate != nullptr) + { + FoundCompleteUpdate->CompleteUpdate = MoveTemp(CompleteUpdate); + } + else + { + CompleteUpdates.Emplace(EntityComponentCompleteUpdate{ EntityId, MoveTemp(CompleteUpdate), ComponentUpdate(Id.ComponentId) }); + } +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewCoordinator.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewCoordinator.cpp index 3ef57076c2..d02bb8a90f 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewCoordinator.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewCoordinator.cpp @@ -27,6 +27,21 @@ void ViewCoordinator::FlushMessagesToSend() ConnectionHandler->SendMessages(View.FlushLocalChanges()); } +void ViewCoordinator::SendAddComponent(Worker_EntityId EntityId, ComponentData Data) +{ + View.SendAddComponent(EntityId, MoveTemp(Data)); +} + +void ViewCoordinator::SendComponentUpdate(Worker_EntityId EntityId, ComponentUpdate Update) +{ + View.SendComponentUpdate(EntityId, MoveTemp(Update)); +} + +void ViewCoordinator::SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) +{ + View.SendRemoveComponent(EntityId, ComponentId); +} + const TArray& ViewCoordinator::GetCreateEntityResponses() const { return Delta->GetCreateEntityResponses(); @@ -47,9 +62,29 @@ const TArray& ViewCoordinator::GetAuthorityLostTemporarily() return Delta->GetAuthorityLostTemporarily(); } +const TArray& ViewCoordinator::GetComponentsAdded() const +{ + return Delta->GetComponentsAdded(); +} + +const TArray& ViewCoordinator::GetComponentsRemoved() const +{ + return Delta->GetComponentsRemoved(); +} + +const TArray& ViewCoordinator::GetUpdates() const +{ + return Delta->GetUpdates(); +} + +const TArray& ViewCoordinator::GetCompleteUpdates() const +{ + return Delta->GetCompleteUpdates(); +} + TUniquePtr ViewCoordinator::GenerateLegacyOpList() const { return Delta->GenerateLegacyOpList(); } -} // SpatialView +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewDelta.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewDelta.cpp index aef4e1d6bb..a883f91317 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewDelta.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/ViewDelta.cpp @@ -17,6 +17,26 @@ void ViewDelta::SetAuthority(Worker_EntityId EntityId, Worker_ComponentId Compon AuthorityChanges.SetAuthority(EntityId, ComponentId, Authority); } +void ViewDelta::AddComponent(Worker_EntityId EntityId, ComponentData Data) +{ + EntityComponentChanges.AddComponent(EntityId, MoveTemp(Data)); +} + +void ViewDelta::RemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) +{ + EntityComponentChanges.RemoveComponent(EntityId, ComponentId); +} + +void ViewDelta::AddComponentAsUpdate(Worker_EntityId EntityId, ComponentData Data) +{ + EntityComponentChanges.AddComponentAsUpdate(EntityId, MoveTemp(Data)); +} + +void ViewDelta::AddUpdate(Worker_EntityId EntityId, ComponentUpdate Update) +{ + EntityComponentChanges.AddUpdate(EntityId, MoveTemp(Update)); +} + const TArray& ViewDelta::GetCreateEntityResponses() const { return CreateEntityResponses; @@ -37,6 +57,26 @@ const TArray& ViewDelta::GetAuthorityLostTemporarily() const return AuthorityChanges.GetAuthorityLostTemporarily(); } +const TArray& ViewDelta::GetComponentsAdded() const +{ + return EntityComponentChanges.GetComponentsAdded(); +} + +const TArray& ViewDelta::GetComponentsRemoved() const +{ + return EntityComponentChanges.GetComponentsRemoved(); +} + +const TArray& ViewDelta::GetUpdates() const +{ + return EntityComponentChanges.GetUpdates(); +} + +const TArray& ViewDelta::GetCompleteUpdates() const +{ + return EntityComponentChanges.GetCompleteUpdates(); +} + TUniquePtr ViewDelta::GenerateLegacyOpList() const { // Todo - refactor individual op creation to an oplist type. diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/WorkerView.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/WorkerView.cpp index caa3acb2d3..95bba3bb2f 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialView/WorkerView.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialView/WorkerView.cpp @@ -38,6 +38,23 @@ TUniquePtr WorkerView::FlushLocalChanges() return OutgoingMessages; } +void WorkerView::SendAddComponent(Worker_EntityId EntityId, ComponentData Data) +{ + AddedComponents.Add(EntityComponentId{ EntityId, Data.GetComponentId() }); + LocalChanges->ComponentMessages.Emplace(EntityId, MoveTemp(Data)); +} + +void WorkerView::SendComponentUpdate(Worker_EntityId EntityId, ComponentUpdate Update) +{ + LocalChanges->ComponentMessages.Emplace(EntityId, MoveTemp(Update)); +} + +void WorkerView::SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) +{ + AddedComponents.Remove(EntityComponentId{ EntityId, ComponentId }); + LocalChanges->ComponentMessages.Emplace(EntityId, ComponentId); +} + void WorkerView::SendCreateEntityRequest(CreateEntityRequest Request) { LocalChanges->CreateEntityRequests.Push(MoveTemp(Request)); @@ -71,13 +88,16 @@ void WorkerView::ProcessOp(const Worker_Op& Op) case WORKER_OP_TYPE_ENTITY_QUERY_RESPONSE: break; case WORKER_OP_TYPE_ADD_COMPONENT: + HandleAddComponent(Op.op.add_component); break; case WORKER_OP_TYPE_REMOVE_COMPONENT: + HandleRemoveComponent(Op.op.remove_component); 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: break; @@ -101,4 +121,32 @@ void WorkerView::HandleCreateEntityResponse(const Worker_CreateEntityResponseOp& }); } +void WorkerView::HandleAddComponent(const Worker_AddComponentOp& Component) +{ + const EntityComponentId Id = { Component.entity_id, Component.data.component_id }; + if (AddedComponents.Contains(Id)) + { + Delta.AddComponentAsUpdate(Id.EntityId, ComponentData::CreateCopy(Component.data.schema_type, Id.ComponentId)); + } + else + { + AddedComponents.Add(Id); + Delta.AddComponent(Id.EntityId, ComponentData::CreateCopy(Component.data.schema_type, Id.ComponentId)); + } +} + +void WorkerView::HandleComponentUpdate(const Worker_ComponentUpdateOp& Update) +{ + Delta.AddUpdate(Update.entity_id, ComponentUpdate::CreateCopy(Update.update.schema_type, Update.update.component_id)); +} + +void WorkerView::HandleRemoveComponent(const Worker_RemoveComponentOp& Component) +{ + const EntityComponentId Id = { Component.entity_id, Component.component_id }; + // If the component has been added, remove it. Otherwise drop the op. + if (AddedComponents.Remove(Id)) + { + Delta.RemoveComponent(Id.EntityId, Id.ComponentId); + } +} } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/RPCServiceTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/RPCServiceTest.cpp index 7536941964..803f93f497 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Tests/RPCServiceTest.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/RPCServiceTest.cpp @@ -116,7 +116,7 @@ SpatialGDK::SpatialRPCService CreateRPCService(const TArray& En StaticComponentView = CreateStaticComponentView(EntityIdArray, RPCEndpointType); } - SpatialGDK::SpatialRPCService RPCService = SpatialGDK::SpatialRPCService(RPCDelegate, StaticComponentView); + SpatialGDK::SpatialRPCService RPCService = SpatialGDK::SpatialRPCService(RPCDelegate, StaticComponentView, nullptr); for (Worker_EntityId EntityId : EntityIdArray) { @@ -154,17 +154,17 @@ bool CompareUpdateToSendAndEntityPayload(SpatialGDK::SpatialRPCService::UpdateTo Update.EntityId == EntityPayloadItem.EntityId; } -bool CompareComponentDataAndEntityPayload(const Worker_ComponentData& ComponentData, const EntityPayload& EntityPayloadItem, ERPCType RPCType, uint64 RPCId) +bool CompareComponentDataAndEntityPayload(const FWorkerComponentData& ComponentData, const EntityPayload& EntityPayloadItem, ERPCType RPCType, uint64 RPCId) { return CompareSchemaObjectToSendAndPayload(Schema_GetComponentDataFields(ComponentData.schema_type), EntityPayloadItem.Payload, RPCType, RPCId); } -Worker_ComponentData GetComponentDataOnEntityCreationFromRPCService(SpatialGDK::SpatialRPCService& RPCService, Worker_EntityId EntityID, ERPCType RPCType) +FWorkerComponentData GetComponentDataOnEntityCreationFromRPCService(SpatialGDK::SpatialRPCService& RPCService, Worker_EntityId EntityID, ERPCType RPCType) { Worker_ComponentId ExpectedUpdateComponentId = SpatialGDK::RPCRingBufferUtils::GetRingBufferComponentId(RPCType); - TArray ComponentDataArray = RPCService.GetRPCComponentsOnEntityCreation(EntityID); + TArray ComponentDataArray = RPCService.GetRPCComponentsOnEntityCreation(EntityID); - const Worker_ComponentData* ComponentData = ComponentDataArray.FindByPredicate([ExpectedUpdateComponentId](const Worker_ComponentData& CompData) { + const FWorkerComponentData* ComponentData = ComponentDataArray.FindByPredicate([ExpectedUpdateComponentId](const FWorkerComponentData& CompData) { return CompData.component_id == ExpectedUpdateComponentId; }); @@ -180,7 +180,7 @@ Worker_ComponentData 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); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload, false); TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::Success)); return true; } @@ -188,7 +188,7 @@ RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_client_reliable_ 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); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientUnreliable, SimplePayload, false); TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::Success)); return true; } @@ -196,7 +196,7 @@ RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_client_unreliabl 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); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerReliable, SimplePayload, false); TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority)); return true; } @@ -204,7 +204,7 @@ RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_server_reliable_ 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); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerUnreliable, SimplePayload, false); TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority)); return true; } @@ -212,7 +212,7 @@ RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_server_unreliabl 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); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload, false); TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority)); return true; } @@ -220,7 +220,7 @@ RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_client_reliable_ 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); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientUnreliable, SimplePayload, false); TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority)); return true; } @@ -228,7 +228,7 @@ RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_client_unreliabl 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); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerReliable, SimplePayload, false); TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::Success)); return true; } @@ -236,7 +236,7 @@ RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_server_reliable_ 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); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerUnreliable, SimplePayload, false); TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::Success)); return true; } @@ -244,7 +244,7 @@ RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_server_unreliabl 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); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload, false); TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::NoRingBufferAuthority)); return true; } @@ -252,7 +252,7 @@ RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_multicast_rpcs_t 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); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload, false); TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::Success)); return true; } @@ -260,7 +260,7 @@ RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_multicast_rpcs_t 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); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload, false); TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::HasAckAuthority)); return true; } @@ -273,10 +273,10 @@ RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_overflow_client_ uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ClientReliable); for (uint32 i = 0; i < RPCsToSend; ++i) { - RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload); + RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload, false); } - SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload, false); TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::QueueOverflowed)); return true; } @@ -289,10 +289,10 @@ RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_overflow_client_ uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ClientUnreliable); for (uint32 i = 0; i < RPCsToSend; ++i) { - RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientUnreliable, SimplePayload); + RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientUnreliable, SimplePayload, false); } - SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientUnreliable, SimplePayload); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientUnreliable, SimplePayload, false); TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::DropOverflowed)); return true; } @@ -305,10 +305,10 @@ RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_overflow_client_ uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ServerReliable); for (uint32 i = 0; i < RPCsToSend; ++i) { - RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerReliable, SimplePayload); + RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerReliable, SimplePayload, false); } - SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerReliable, SimplePayload); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerReliable, SimplePayload, false); TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::QueueOverflowed)); return true; } @@ -321,10 +321,10 @@ RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_overflow_client_ uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::ServerUnreliable); for (uint32 i = 0; i < RPCsToSend; ++i) { - RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerUnreliable, SimplePayload); + RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerUnreliable, SimplePayload, false); } - SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerUnreliable, SimplePayload); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ServerUnreliable, SimplePayload, false); TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::DropOverflowed)); return true; } @@ -337,10 +337,10 @@ RPC_SERVICE_TEST(GIVEN_authority_over_server_endpoint_WHEN_push_overflow_multica uint32 RPCsToSend = GetDefault()->GetRPCRingBufferSize(ERPCType::NetMulticast); for (uint32 i = 0; i < RPCsToSend; ++i) { - RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload); + RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload, false); } - SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload); + SpatialGDK::EPushRPCResult Result = RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload, false); TestTrue("Push RPC returned expected results", (Result == SpatialGDK::EPushRPCResult::Success)); return true; } @@ -358,7 +358,7 @@ RPC_SERVICE_TEST(GIVEN_authority_over_client_endpoint_WHEN_push_server_unreliabl SpatialGDK::SpatialRPCService RPCService = CreateRPCService(EntityIdArray, CLIENT_AUTH); for (const EntityPayload& EntityPayloadItem : EntityPayloads) { - RPCService.PushRPC(EntityPayloadItem.EntityId, ERPCType::ServerUnreliable, EntityPayloadItem.Payload); + RPCService.PushRPC(EntityPayloadItem.EntityId, ERPCType::ServerUnreliable, EntityPayloadItem.Payload, false); } TArray UpdateToSendArray = RPCService.GetRPCsAndAcksToSend(); @@ -389,9 +389,9 @@ RPC_SERVICE_TEST(GIVEN_no_authority_over_rpc_endpoint_WHEN_push_client_reliable_ // Create RPCService with empty component view SpatialGDK::SpatialRPCService RPCService = CreateRPCService({}, NO_AUTH); - RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload); + RPCService.PushRPC(RPCTestEntityId_1, ERPCType::ClientReliable, SimplePayload, false); - Worker_ComponentData ComponentData = GetComponentDataOnEntityCreationFromRPCService(RPCService, RPCTestEntityId_1, ERPCType::ClientReliable); + FWorkerComponentData ComponentData = GetComponentDataOnEntityCreationFromRPCService(RPCService, RPCTestEntityId_1, ERPCType::ClientReliable); bool bTestPassed = CompareComponentDataAndEntityPayload(ComponentData, EntityPayload(RPCTestEntityId_1, SimplePayload), ERPCType::ClientReliable, 1); TestTrue("Entity creation test returned expected results", bTestPassed); return true; @@ -402,10 +402,10 @@ RPC_SERVICE_TEST(GIVEN_no_authority_over_rpc_endpoint_WHEN_push_multicast_rpcs_t // Create RPCService with empty component view SpatialGDK::SpatialRPCService RPCService = CreateRPCService({}, NO_AUTH); - RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload); - RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload); + RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload, false); + RPCService.PushRPC(RPCTestEntityId_1, ERPCType::NetMulticast, SimplePayload, false); - Worker_ComponentData ComponentData = GetComponentDataOnEntityCreationFromRPCService(RPCService, RPCTestEntityId_1, ERPCType::NetMulticast); + 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()); TestTrue("Entity creation multicast test returned expected results", (InitiallyPresent == 2)); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentRecordTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentRecordTest.cpp new file mode 100644 index 0000000000..ce200264ca --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentRecordTest.cpp @@ -0,0 +1,187 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/TestDefinitions.h" + +#include "SpatialView/EntityComponentRecord.h" + +#include "EntityComponentTestUtils.h" + +#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 + +ENTITYCOMPONENTRECORD_TEST(GIVEN_empty_component_record_WHEN_component_added_THEN_has_component_data) +{ + // GIVEN + ComponentData TestData = CreateTestComponentData(TEST_COMPONENT_ID, TEST_VALUE); + + const TArray ExpectedComponentsRemoved = {}; + TArray ExpectedComponentsAdded; + ExpectedComponentsAdded.Push(EntityComponentData{ TEST_ENTITY_ID, TestData.DeepCopy() }); + + EntityComponentRecord Storage; + + // WHEN + Storage.AddComponent(TEST_ENTITY_ID, MoveTemp(TestData)); + + // THEN + TestTrue(TEXT("ComponentsAdded are equal to expected"), AreEquivalent(Storage.GetComponentsAdded(), ExpectedComponentsAdded)); + TestTrue(TEXT("ComponentsRemoved are equal to expected"), AreEquivalent(Storage.GetComponentsRemoved(), ExpectedComponentsRemoved)); + return true; +} + +ENTITYCOMPONENTRECORD_TEST(GIVEN_empty_component_record_WHEN_component_removed_THEN_has_removed_component_id) +{ + // GIVEN + const EntityComponentId kEntityComponentId = { TEST_ENTITY_ID, TEST_COMPONENT_ID }; + const TArray ExpectedComponentsAdded = {}; + const TArray ExpectedComponentsRemoved = { kEntityComponentId }; + + EntityComponentRecord Storage; + + // WHEN + Storage.RemoveComponent(kEntityComponentId.EntityId, kEntityComponentId.ComponentId); + + // THEN + TestTrue(TEXT("ComponentsAdded are equal to expected"), AreEquivalent(Storage.GetComponentsAdded(), ExpectedComponentsAdded)); + TestTrue(TEXT("ComponentsRemoved are equal to expected"), AreEquivalent(Storage.GetComponentsRemoved(), ExpectedComponentsRemoved)); + + return true; +} + +ENTITYCOMPONENTRECORD_TEST(GIVEN_component_record_with_component_WHEN_that_component_removed_THEN_component_record_is_empty) +{ + // GIVEN + ComponentData TestData = CreateTestComponentData(TEST_COMPONENT_ID, TEST_VALUE); + + const TArray ExpectedComponentsAdded = {}; + const TArray ExpectedComponentsRemoved = {}; + + EntityComponentRecord Storage; + Storage.AddComponent(TEST_ENTITY_ID, MoveTemp(TestData)); + + // WHEN + Storage.RemoveComponent(TEST_ENTITY_ID, TEST_COMPONENT_ID); + + // THEN + TestTrue(TEXT("ComponentsAdded are equal to expected"), AreEquivalent(Storage.GetComponentsAdded(), ExpectedComponentsAdded)); + TestTrue(TEXT("ComponentsRemoved are equal to expected"), AreEquivalent(Storage.GetComponentsRemoved(), ExpectedComponentsRemoved)); + return true; +} + +// This should not produce the component added - just cancel removing it +ENTITYCOMPONENTRECORD_TEST(GIVEN_component_record_with_removed_component_WHEN_component_added_again_THEN_component_record_is_empty) +{ + // GIVEN + ComponentData TestData = CreateTestComponentData(TEST_COMPONENT_ID, TEST_VALUE); + + const TArray ExpectedComponentsAdded = {}; + const TArray ExpectedComponentsRemoved = {}; + + EntityComponentRecord Storage; + Storage.RemoveComponent(TEST_ENTITY_ID, TEST_COMPONENT_ID); + + // WHEN + Storage.AddComponent(TEST_ENTITY_ID, MoveTemp(TestData)); + + // THEN + TestTrue(TEXT("ComponentsAdded are equal to expected"), AreEquivalent(Storage.GetComponentsAdded(), ExpectedComponentsAdded)); + TestTrue(TEXT("ComponentsRemoved are equal to expected"), AreEquivalent(Storage.GetComponentsRemoved(), ExpectedComponentsRemoved)); + return true; +} + +ENTITYCOMPONENTRECORD_TEST(GIVEN_component_record_with_component_WHEN_update_added_THEN_component_record_has_updated_component_data) +{ + // GIVEN + ComponentData TestData = CreateTestComponentData(TEST_COMPONENT_ID, TEST_VALUE); + ComponentUpdate TestUpdate = CreateTestComponentUpdate(TEST_COMPONENT_ID, TEST_UPDATE_VALUE); + ComponentData ExpectedData = CreateTestComponentData(TEST_COMPONENT_ID, TEST_UPDATE_VALUE); + + TArray ExpectedComponentsAdded; + ExpectedComponentsAdded.Push(EntityComponentData{ TEST_ENTITY_ID, MoveTemp(ExpectedData) }); + const TArray ExpectedComponentsRemoved = {}; + + EntityComponentRecord Storage; + Storage.AddComponent(TEST_ENTITY_ID, MoveTemp(TestData)); + + // WHEN + Storage.AddUpdate(TEST_ENTITY_ID, MoveTemp(TestUpdate)); + + // THEN + TestTrue(TEXT("ComponentsAdded are equal to expected"), AreEquivalent(Storage.GetComponentsAdded(), ExpectedComponentsAdded)); + TestTrue(TEXT("ComponentsRemoved are equal to expected"), AreEquivalent(Storage.GetComponentsRemoved(), ExpectedComponentsRemoved)); + return true; +} + +ENTITYCOMPONENTRECORD_TEST(GIVEN_empty_component_record_WHEN_updated_added_THEN_component_record_is_empty) +{ + // GIVEN + ComponentUpdate TestUpdate = CreateTestComponentUpdate(TEST_COMPONENT_ID, TEST_UPDATE_VALUE); + + const TArray ExpectedComponentsAdded = {}; + const TArray ExpectedComponentsRemoved = {}; + + EntityComponentRecord Storage; + + // WHEN + Storage.AddUpdate(TEST_ENTITY_ID, MoveTemp(TestUpdate)); + + // THEN + TestTrue(TEXT("ComponentsAdded are equal to expected"), AreEquivalent(Storage.GetComponentsAdded(), ExpectedComponentsAdded)); + TestTrue(TEXT("ComponentsRemoved are equal to expected"), AreEquivalent(Storage.GetComponentsRemoved(), ExpectedComponentsRemoved)); + return true; +} + +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); + ComponentData TestUpdate = CreateTestComponentData(TEST_COMPONENT_ID, TEST_UPDATE_VALUE); + ComponentData ExpectedData = CreateTestComponentData(TEST_COMPONENT_ID, TEST_UPDATE_VALUE); + + TArray ExpectedComponentsAdded; + ExpectedComponentsAdded.Push(EntityComponentData{ TEST_ENTITY_ID, MoveTemp(ExpectedData) }); + const TArray ExpectedComponentsRemoved = {}; + + EntityComponentRecord Storage; + Storage.AddComponent(TEST_ENTITY_ID, MoveTemp(TestData)); + + // WHEN + Storage.AddComponentAsUpdate(TEST_ENTITY_ID, MoveTemp(TestUpdate)); + + // THEN + TestTrue(TEXT("ComponentsAdded are equal to expected"), AreEquivalent(Storage.GetComponentsAdded(), ExpectedComponentsAdded)); + TestTrue(TEXT("ComponentsRemoved are equal to expected"), AreEquivalent(Storage.GetComponentsRemoved(), ExpectedComponentsRemoved)); + return true; +} + +ENTITYCOMPONENTRECORD_TEST(GIVEN_empty_component_record_WHEN_complete_update_added_THEN_component_record_is_empty) +{ + // GIVEN + ComponentData TestUpdate = CreateTestComponentData(TEST_COMPONENT_ID, TEST_UPDATE_VALUE); + + const TArray ExpectedComponentsAdded = {}; + const TArray ExpectedComponentsRemoved = {}; + + EntityComponentRecord Storage; + + // WHEN + Storage.AddComponentAsUpdate(TEST_ENTITY_ID, MoveTemp(TestUpdate)); + + // THEN + TestTrue(TEXT("ComponentsAdded are equal to expected"), AreEquivalent(Storage.GetComponentsAdded(), ExpectedComponentsAdded)); + TestTrue(TEXT("ComponentsRemoved are equal to expected"), AreEquivalent(Storage.GetComponentsRemoved(), ExpectedComponentsRemoved)); + return true; +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentTestUtils.h b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentTestUtils.h new file mode 100644 index 0000000000..13a8f92152 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentTestUtils.h @@ -0,0 +1,166 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialView/EntityComponentTypes.h" + +#include + +namespace SpatialGDK +{ + +namespace EntityComponentTestUtils +{ +const Schema_FieldId EVENT_ID = 1; +const Schema_FieldId EVENT_INT_FIELD_ID = 2; +const Schema_FieldId TEST_DOUBLE_FIELD_ID = 1; +} // namespace EntityComponentTestUtils + +inline ComponentData CreateTestComponentData(const Worker_ComponentId Id, const double Value) +{ + ComponentData Data{ Id }; + Schema_Object* Fields = Data.GetFields(); + Schema_AddDouble(Fields, EntityComponentTestUtils::TEST_DOUBLE_FIELD_ID, Value); + return Data; +} + +inline ComponentUpdate CreateTestComponentUpdate(const Worker_ComponentId Id, const double Value) +{ + ComponentUpdate Update{ Id }; + Schema_Object* Fields = Update.GetFields(); + Schema_AddDouble(Fields, EntityComponentTestUtils::TEST_DOUBLE_FIELD_ID, Value); + return Update; +} + +inline void AddTestEvent(ComponentUpdate* Update, 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) +{ + ComponentUpdate Update{ Id }; + AddTestEvent(&Update, Value); + return Update; +} + +/** Returns true if Lhs and Rhs have the same serialized form. */ +inline bool CompareSchemaObjects(const Schema_Object* Lhs, const Schema_Object* Rhs) +{ + const auto Length = Schema_GetWriteBufferLength(Lhs); + if (Schema_GetWriteBufferLength(Rhs) != Length) + { + return false; + } + const TUniquePtr LhsBuffer = MakeUnique(Length); + const TUniquePtr RhsBuffer = MakeUnique(Length); + Schema_SerializeToBuffer(Lhs, LhsBuffer.Get(), Length); + Schema_SerializeToBuffer(Rhs, RhsBuffer.Get(), Length); + return FMemory::Memcmp(LhsBuffer.Get(), RhsBuffer.Get(), Length) == 0; +} + +/** Returns true if Lhs and Rhs have the same component ID and state. */ +inline bool CompareComponentData(const ComponentData& Lhs, const ComponentData& Rhs) +{ + if (Lhs.GetComponentId() != Rhs.GetComponentId()) + { + return false; + } + return CompareSchemaObjects(Lhs.GetFields(), Rhs.GetFields()); +} + +/** Returns true if Lhs and Rhs have the same component ID and events. */ +inline bool CompareComponentUpdateEvents(const ComponentUpdate& Lhs, const ComponentUpdate& Rhs) +{ + if (Lhs.GetComponentId() != Rhs.GetComponentId()) + { + return false; + } + return CompareSchemaObjects(Lhs.GetEvents(), Rhs.GetEvents()); +} + +/** Returns true if Lhs and Rhs have the same component ID and state. */ +inline bool CompareComponentUpdates(const ComponentUpdate& Lhs, const ComponentUpdate& Rhs) +{ + if (Lhs.GetComponentId() != Rhs.GetComponentId()) + { + return false; + } + 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. */ +inline bool CompareEntityComponentData(const EntityComponentData& Lhs, const EntityComponentData& Rhs) +{ + if (Lhs.EntityId != Rhs.EntityId) + { + return false; + } + return CompareComponentData(Lhs.Data, Rhs.Data); +} + +/** Returns true if Lhs and Rhs have the same entity ID, component ID, and events. */ +inline bool CompareEntityComponentUpdateEvents(const EntityComponentUpdate& Lhs, const EntityComponentUpdate& Rhs) +{ + if (Lhs.EntityId != Rhs.EntityId) + { + return false; + } + return CompareComponentUpdateEvents(Lhs.Update, Rhs.Update); +} + +/** Returns true if Lhs and Rhs have the same entity ID, component ID, state, and events. */ +inline bool CompareEntityComponentUpdates(const EntityComponentUpdate& Lhs, const EntityComponentUpdate& Rhs) +{ + if (Lhs.EntityId != Rhs.EntityId) + { + return false; + } + return CompareComponentUpdates(Lhs.Update, Rhs.Update); +} + +/** Returns true if Lhs and Rhs have the same ID, component ID, data, state and events. */ +inline bool CompareEntityComponentCompleteUpdates(const EntityComponentCompleteUpdate& Lhs, const EntityComponentCompleteUpdate& Rhs) +{ + if (Lhs.EntityId != Rhs.EntityId) + { + return false; + } + return CompareComponentData(Lhs.CompleteUpdate, Rhs.CompleteUpdate) && CompareComponentUpdateEvents(Lhs.Events, Rhs.Events); +} + +inline bool CompareEntityComponentId(const EntityComponentId& Lhs, const EntityComponentId& 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)); +} + +inline bool AreEquivalent(const TArray& Lhs, const TArray& Rhs) +{ + return AreEquivalent(Lhs, Rhs, CompareEntityComponentUpdates); +} + +inline bool AreEquivalent(const TArray& Lhs, const TArray& Rhs) +{ + return AreEquivalent(Lhs, Rhs, CompareEntityComponentCompleteUpdates); +} + +inline bool AreEquivalent(const TArray& Lhs, const TArray& Rhs) +{ + return AreEquivalent(Lhs, Rhs, CompareEntityComponentData); +} + +inline bool AreEquivalent(const TArray& Lhs, const TArray& Rhs) +{ + return AreEquivalent(Lhs, Rhs, CompareEntityComponentId); +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentUpdateRecordTest.cpp b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentUpdateRecordTest.cpp new file mode 100644 index 0000000000..2c2f0ef96e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Tests/SpatialView/EntityComponentUpdateRecordTest.cpp @@ -0,0 +1,197 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/TestDefinitions.h" + +#include "SpatialView/EntityComponentUpdateRecord.h" + +#include "EntityComponentTestUtils.h" + +#define ENTITYCOMPONENTUPDATERECORD_TEST(TestName) \ + GDK_TEST(Core, EntityComponentUpdateRecord, TestName) + +namespace SpatialGDK +{ + +namespace +{ + 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 int EVENT_VALUE = 7332; + + 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) +{ + // GIVEN + ComponentUpdate TestUpdate = CreateTestComponentUpdate(TEST_COMPONENT_ID, TEST_VALUE); + + TArray ExpectedUpdates; + ExpectedUpdates.Push(EntityComponentUpdate{ TEST_ENTITY_ID, TestUpdate.DeepCopy() }); + const TArray ExpectedCompleteUpdates = {}; + + EntityComponentUpdateRecord Storage; + + // WHEN + Storage.AddComponentUpdate(TEST_ENTITY_ID, MoveTemp(TestUpdate)); + + // THEN + TestTrue(TEXT("Updates are equal to expected"), AreEquivalent(Storage.GetUpdates(), ExpectedUpdates)); + TestTrue(TEXT("Complete updates are equal to expected"), AreEquivalent(Storage.GetCompleteUpdates(), ExpectedCompleteUpdates)); + + return true; +} + +ENTITYCOMPONENTUPDATERECORD_TEST(GIVEN_update_record_with_update_WHEN_new_update_added_THEN_new_update_merged) +{ + // GIVEN + ComponentUpdate FirstUpdate = CreateTestComponentUpdate(TEST_COMPONENT_ID, TEST_VALUE); + ComponentUpdate SecondUpdate = CreateTestComponentUpdate(TEST_COMPONENT_ID, TEST_UPDATE_VALUE); + + TArray ExpectedUpdates; + ExpectedUpdates.Push(EntityComponentUpdate{ TEST_ENTITY_ID, SecondUpdate.DeepCopy() }); + const TArray ExpectedCompleteUpdates = {}; + + EntityComponentUpdateRecord Storage; + Storage.AddComponentUpdate(TEST_ENTITY_ID, MoveTemp(FirstUpdate)); + + // WHEN + Storage.AddComponentUpdate(TEST_ENTITY_ID, MoveTemp(SecondUpdate)); + + // THEN + TestTrue(TEXT("Updates are equal to expected"), AreEquivalent(Storage.GetUpdates(), ExpectedUpdates)); + TestTrue(TEXT("Complete updates are equal to expected"), AreEquivalent(Storage.GetCompleteUpdates(), ExpectedCompleteUpdates)); + + return true; +} + +ENTITYCOMPONENTUPDATERECORD_TEST(GIVEN_empty_update_record_WHEN_complete_update_added_THEN_update_record_has_complete_update) +{ + // GIVEN + ComponentData Data = CreateTestComponentData(TEST_COMPONENT_ID, TEST_VALUE); + + TArray ExpectedCompleteUpdates; + ExpectedCompleteUpdates.Push(EntityComponentCompleteUpdate{ TEST_ENTITY_ID, Data.DeepCopy(), ComponentUpdate(TEST_COMPONENT_ID) }); + const TArray ExpectedUpdates = {}; + + EntityComponentUpdateRecord Storage; + + // WHEN + Storage.AddComponentDataAsUpdate(TEST_ENTITY_ID, MoveTemp(Data)); + + // THEN + TestTrue(TEXT("Updates are equal to expected"), AreEquivalent(Storage.GetUpdates(), ExpectedUpdates)); + TestTrue(TEXT("Complete updates are equal to expected"), AreEquivalent(Storage.GetCompleteUpdates(), ExpectedCompleteUpdates)); + + return true; +} + +ENTITYCOMPONENTUPDATERECORD_TEST(GIVEN_update_record_with_update_WHEN_complete_update_added_THEN_complete_update_merged) +{ + // GIVEN + ComponentUpdate Update = CreateTestComponentUpdate(TEST_COMPONENT_ID, TEST_VALUE); + AddTestEvent(&Update, EVENT_VALUE); + ComponentData CompleteUpdate = CreateTestComponentData(TEST_COMPONENT_ID, UPDATE_VALUE); + + const TArray ExpectedUpdates = {}; + TArray ExpectedCompleteUpdates; + ExpectedCompleteUpdates.Push({ TEST_ENTITY_ID, CompleteUpdate.DeepCopy(), Update.DeepCopy() }); + + EntityComponentUpdateRecord Storage; + Storage.AddComponentUpdate(TEST_ENTITY_ID, MoveTemp(Update)); + + // WHEN + Storage.AddComponentDataAsUpdate(TEST_ENTITY_ID, MoveTemp(CompleteUpdate)); + + // THEN + TestTrue(TEXT("Updates are equal to expected"), AreEquivalent(Storage.GetUpdates(), ExpectedUpdates)); + TestTrue(TEXT("Complete updates are equal to expected"), AreEquivalent(Storage.GetCompleteUpdates(), ExpectedCompleteUpdates)); + + return true; +} + +ENTITYCOMPONENTUPDATERECORD_TEST(GIVEN_update_record_with_a_complete_update_WHEN_new_update_added_THEN_new_update_merged) +{ + // GIVEN + ComponentData CompleteUpdate = CreateTestComponentData(TEST_COMPONENT_ID, TEST_VALUE); + ComponentUpdate Update = CreateTestComponentUpdate(TEST_COMPONENT_ID, UPDATE_VALUE); + AddTestEvent(&Update, EVENT_VALUE); + ComponentUpdate AdditionalEvent = CreateTestComponentEvent(TEST_COMPONENT_ID, EVENT_VALUE); + + ComponentData ExpectedCompleteUpdate = CreateTestComponentData(TEST_COMPONENT_ID, UPDATE_VALUE); + ComponentUpdate ExpectedEvent = CreateTestComponentEvent(TEST_COMPONENT_ID, EVENT_VALUE); + AddTestEvent(&ExpectedEvent, EVENT_VALUE); + + const TArray ExpectedUpdates{}; + TArray ExpectedCompleteUpdates; + ExpectedCompleteUpdates.Push(EntityComponentCompleteUpdate{ TEST_ENTITY_ID, MoveTemp(ExpectedCompleteUpdate), MoveTemp(ExpectedEvent) }); + + EntityComponentUpdateRecord Storage; + Storage.AddComponentDataAsUpdate(TEST_ENTITY_ID, MoveTemp(CompleteUpdate)); + + // WHEN + Storage.AddComponentUpdate(TEST_ENTITY_ID, MoveTemp(Update)); + Storage.AddComponentUpdate(TEST_ENTITY_ID, MoveTemp(AdditionalEvent)); + + // THEN + TestTrue(TEXT("Updates are equal to expected"), AreEquivalent(Storage.GetUpdates(), ExpectedUpdates)); + TestTrue(TEXT("Complete updates are equal to expected"), AreEquivalent(Storage.GetCompleteUpdates(), ExpectedCompleteUpdates)); + + return true; +} + +ENTITYCOMPONENTUPDATERECORD_TEST(GIVEN_update_record_with_multiple_updates_WHEN_component_removed_THEN_its_updates_removed) +{ + // GIVEN + ComponentData CompleteUpdateToRemove = CreateTestComponentData(COMPONENT_ID_TO_REMOVE, TEST_VALUE); + ComponentUpdate EventToRemove = CreateTestComponentEvent(COMPONENT_ID_TO_REMOVE, EVENT_VALUE); + + ComponentUpdate UpdateToKeep = CreateTestComponentUpdate(COMPONENT_ID_TO_KEEP, UPDATE_VALUE); + + TArray ExpectedUpdates; + ExpectedUpdates.Push(EntityComponentUpdate{ TEST_ENTITY_ID, UpdateToKeep.DeepCopy() }); + const TArray ExpectedCompleteUpdates = {}; + + EntityComponentUpdateRecord Storage; + Storage.AddComponentDataAsUpdate(TEST_ENTITY_ID, MoveTemp(CompleteUpdateToRemove)); + Storage.AddComponentUpdate(TEST_ENTITY_ID, MoveTemp(EventToRemove)); + Storage.AddComponentUpdate(TEST_ENTITY_ID, MoveTemp(UpdateToKeep)); + + // WHEN + Storage.RemoveComponent(TEST_ENTITY_ID, COMPONENT_ID_TO_REMOVE); + + // THEN + TestTrue(TEXT("Updates are equal to expected"), AreEquivalent(Storage.GetUpdates(), ExpectedUpdates)); + TestTrue(TEXT("Complete updates are equal to expected"), AreEquivalent(Storage.GetCompleteUpdates(), ExpectedCompleteUpdates)); + + return true; +} + +ENTITYCOMPONENTUPDATERECORD_TEST(GIVEN_update_record_with_update_WHEN_component_removed_THEN_its_update_removed) +{ + // GIVEN + ComponentUpdate Update = CreateTestComponentUpdate(COMPONENT_ID_TO_REMOVE, UPDATE_VALUE); + + const TArray ExpectedUpdates = {}; + const TArray ExpectedCompleteUpdates = {}; + + EntityComponentUpdateRecord Storage; + Storage.AddComponentUpdate(TEST_ENTITY_ID, MoveTemp(Update)); + + // WHEN + Storage.RemoveComponent(TEST_ENTITY_ID, COMPONENT_ID_TO_REMOVE); + + // THEN + TestTrue(TEXT("Updates are equal to expected"), AreEquivalent(Storage.GetUpdates(), ExpectedUpdates)); + TestTrue(TEXT("Complete updates are equal to expected"), AreEquivalent(Storage.GetCompleteUpdates(), ExpectedCompleteUpdates)); + + return true; +} + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp index db2f18ce86..996186699f 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp @@ -56,11 +56,7 @@ uint32 ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObjec if (Changes.RepChanged.Num() > 0) { FChangelistIterator ChangelistIterator(Changes.RepChanged, 0); -#if ENGINE_MINOR_VERSION <= 22 - FRepHandleIterator HandleIterator(ChangelistIterator, Changes.RepLayout.Cmds, Changes.RepLayout.BaseHandleToCmdIndex, 0, 1, 0, Changes.RepLayout.Cmds.Num() - 1); -#else FRepHandleIterator HandleIterator(static_cast(Changes.RepLayout.GetOwner()), ChangelistIterator, Changes.RepLayout.Cmds, Changes.RepLayout.BaseHandleToCmdIndex, 0, 1, 0, Changes.RepLayout.Cmds.Num() - 1); -#endif while (HandleIterator.NextHandle()) { const FRepLayoutCmd& Cmd = Changes.RepLayout.Cmds[HandleIterator.CmdIndex]; @@ -82,7 +78,7 @@ uint32 ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObjec if (*OutLatencyTraceId != InvalidTraceKey) { UE_LOG(LogComponentFactory, Warning, TEXT("%s property trace being dropped because too many active on this actor (%s)"), *Cmd.Property->GetName(), *Object->GetName()); - LatencyTracer->WriteAndEndTraceIfRemote(*OutLatencyTraceId, TEXT("Multiple actor component traces not supported")); + LatencyTracer->WriteAndEndTrace(*OutLatencyTraceId, TEXT("Multiple actor component traces not supported"), true); } *OutLatencyTraceId = PropertyKey; } @@ -133,7 +129,7 @@ uint32 ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObjec */ const uint32 ProfilerBytesEnd = Schema_GetWriteBufferLength(ComponentObject); NETWORK_PROFILER(GNetworkProfiler.TrackReplicateProperty(Cmd.Property, (ProfilerBytesEnd - ProfilerBytesStart) * CHAR_BIT, nullptr)); -#endif +#endif } if (Cmd.Type == ERepLayoutCmdType::DynamicArray) @@ -169,7 +165,7 @@ uint32 ComponentFactory::FillHandoverSchemaObject(Schema_Object* ComponentObject if (*OutLatencyTraceId != InvalidTraceKey) { UE_LOG(LogComponentFactory, Warning, TEXT("%s handover trace being dropped because too many active on this actor (%s)"), *PropertyInfo.Property->GetName(), *Object->GetName()); - LatencyTracer->WriteAndEndTraceIfRemote(*OutLatencyTraceId, TEXT("Multiple actor component traces not supported")); + LatencyTracer->WriteAndEndTrace(*OutLatencyTraceId, TEXT("Multiple actor component traces not supported"), true); } *OutLatencyTraceId = LatencyTracer->RetrievePendingTrace(Object, PropertyInfo.Property); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp index dbf690d7cc..78e3f81538 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp @@ -200,11 +200,7 @@ void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject& // If the property has RepNotifies, update with local data and possibly initialize the shadow data if (Parent.Property->HasAnyPropertyFlags(CPF_RepNotify)) { -#if ENGINE_MINOR_VERSION <= 22 - FRepStateStaticBuffer& ShadowData = RepState->StaticBuffer; -#else FRepStateStaticBuffer& ShadowData = RepState->GetReceivingRepState()->StaticBuffer; -#endif if (ShadowData.Num() == 0) { Channel.ResetShadowData(*Replicator->RepLayout.Get(), ShadowData, &Object); @@ -280,11 +276,7 @@ void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject& // Parent.Property is the "root" replicated property, e.g. if a struct property was flattened if (Parent.Property->HasAnyPropertyFlags(CPF_RepNotify)) { - #if ENGINE_MINOR_VERSION <= 22 - bool bIsIdentical = Cmd.Property->Identical(RepState->StaticBuffer.GetData() + SwappedCmd.ShadowOffset, Data); - #else bool bIsIdentical = Cmd.Property->Identical(RepState->GetReceivingRepState()->StaticBuffer.GetData() + SwappedCmd.ShadowOffset, Data); - #endif // Only call RepNotify for REPNOTIFY_Always if we are not applying initial data. if (bIsInitialData) @@ -376,7 +368,7 @@ void ComponentReader::ApplyProperty(Schema_Object* Object, Schema_FieldId FieldI { InObjectReferencesMap.Remove(Offset); } - + bOutReferencesChanged = true; } } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityFactory.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityFactory.cpp index 63f9b890c7..36df307402 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityFactory.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityFactory.cpp @@ -15,7 +15,6 @@ #include "Schema/ServerRPCEndpointLegacy.h" #include "Schema/NetOwningClientWorker.h" #include "Schema/RPCPayload.h" -#include "Schema/Singleton.h" #include "Schema/SpatialDebugging.h" #include "Schema/SpawnData.h" #include "Schema/Tombstone.h" @@ -24,22 +23,23 @@ #include "Utils/ComponentFactory.h" #include "Utils/InspectionColors.h" #include "Utils/InterestFactory.h" -#include "Utils/SpatialActorGroupManager.h" #include "Utils/SpatialActorUtils.h" #include "Utils/SpatialDebugger.h" -#include "Engine/Engine.h" +#include "Engine.h" +#include "Engine/LevelScriptActor.h" +#include "GameFramework/GameModeBase.h" +#include "GameFramework/GameStateBase.h" DEFINE_LOG_CATEGORY(LogEntityFactory); namespace SpatialGDK { -EntityFactory::EntityFactory(USpatialNetDriver* InNetDriver, USpatialPackageMapClient* InPackageMap, USpatialClassInfoManager* InClassInfoManager, SpatialActorGroupManager* InActorGroupManager, SpatialRPCService* InRPCService) +EntityFactory::EntityFactory(USpatialNetDriver* InNetDriver, USpatialPackageMapClient* InPackageMap, USpatialClassInfoManager* InClassInfoManager, SpatialRPCService* InRPCService) : NetDriver(InNetDriver) , PackageMap(InPackageMap) , ClassInfoManager(InClassInfoManager) - , ActorGroupManager(InActorGroupManager) , RPCService(InRPCService) { } @@ -51,53 +51,27 @@ TArray EntityFactory::CreateEntityComponents(USpatialActor FString ClientWorkerAttribute = GetConnectionOwningWorkerId(Actor); - WorkerRequirementSet AnyServerRequirementSet; - WorkerRequirementSet AnyServerOrClientRequirementSet = { SpatialConstants::UnrealClientAttributeSet }; + WorkerRequirementSet AnyServerRequirementSet = { SpatialConstants::UnrealServerAttributeSet }; + WorkerRequirementSet AnyServerOrClientRequirementSet = { SpatialConstants::UnrealServerAttributeSet, SpatialConstants::UnrealClientAttributeSet }; WorkerAttributeSet OwningClientAttributeSet = { ClientWorkerAttribute }; - WorkerRequirementSet AnyServerOrOwningClientRequirementSet = { OwningClientAttributeSet }; + WorkerRequirementSet AnyServerOrOwningClientRequirementSet = { SpatialConstants::UnrealServerAttributeSet, OwningClientAttributeSet }; WorkerRequirementSet OwningClientOnlyRequirementSet = { OwningClientAttributeSet }; - for (const FName& WorkerType : GetDefault()->ServerWorkerTypes) - { - WorkerAttributeSet ServerWorkerAttributeSet = { WorkerType.ToString() }; - - AnyServerRequirementSet.Add(ServerWorkerAttributeSet); - AnyServerOrClientRequirementSet.Add(ServerWorkerAttributeSet); - AnyServerOrOwningClientRequirementSet.Add(ServerWorkerAttributeSet); - } - const FClassInfo& Info = ClassInfoManager->GetOrCreateClassInfoByClass(Class); - const USpatialGDKSettings* SpatialSettings = GetDefault(); - - const FName AclAuthoritativeWorkerType = SpatialSettings->bEnableOffloading ? - ActorGroupManager->GetWorkerTypeForActorGroup(USpatialStatics::GetActorGroupForActor(Actor)) : - Info.WorkerType; - - WorkerAttributeSet WorkerAttributeOrSpecificWorker{ AclAuthoritativeWorkerType.ToString() }; - VirtualWorkerId IntendedVirtualWorkerId = SpatialConstants::INVALID_VIRTUAL_WORKER_ID; - - // Add Load Balancer Attribute if we are using the load balancer. - if (SpatialSettings->bEnableUnrealLoadBalancer) + // 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) { - AnyServerRequirementSet.Add(SpatialConstants::GetLoadBalancerAttributeSet(SpatialSettings->LoadBalancingWorkerType.WorkerTypeName)); - AnyServerOrClientRequirementSet.Add(SpatialConstants::GetLoadBalancerAttributeSet(SpatialSettings->LoadBalancingWorkerType.WorkerTypeName)); - AnyServerOrOwningClientRequirementSet.Add(SpatialConstants::GetLoadBalancerAttributeSet(SpatialSettings->LoadBalancingWorkerType.WorkerTypeName)); - - const UAbstractLBStrategy* LBStrategy = NetDriver->LoadBalanceStrategy; - check(LBStrategy != nullptr); - IntendedVirtualWorkerId = LBStrategy->WhoShouldHaveAuthority(*Actor); - if (IntendedVirtualWorkerId == SpatialConstants::INVALID_VIRTUAL_WORKER_ID) - { - UE_LOG(LogEntityFactory, Error, TEXT("Load balancing strategy provided invalid virtual worker ID to spawn actor with. Actor: %s. Strategy: %s"), *Actor->GetName(), *LBStrategy->GetName()); - } - else - { - const PhysicalWorkerName* IntendedAuthoritativePhysicalWorkerName = NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(IntendedVirtualWorkerId); - WorkerAttributeOrSpecificWorker = { FString::Format(TEXT("workerId:{0}"), { *IntendedAuthoritativePhysicalWorkerName }) }; - } + const 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 }; @@ -124,7 +98,10 @@ TArray EntityFactory::CreateEntityComponents(USpatialActor ComponentWriteAcl.Add(SpatialConstants::SERVER_TO_SERVER_COMMAND_ENDPOINT_COMPONENT_ID, AuthoritativeWorkerRequirementSet); ComponentWriteAcl.Add(SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID, AuthoritativeWorkerRequirementSet); ComponentWriteAcl.Add(SpatialConstants::NET_OWNING_CLIENT_WORKER_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + 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); @@ -144,19 +121,6 @@ TArray EntityFactory::CreateEntityComponents(USpatialActor } } - if (SpatialSettings->bEnableUnrealLoadBalancer) - { - const WorkerRequirementSet ACLRequirementSet = { SpatialConstants::GetLoadBalancerAttributeSet(SpatialSettings->LoadBalancingWorkerType.WorkerTypeName) }; - ComponentWriteAcl.Add(SpatialConstants::ENTITY_ACL_COMPONENT_ID, ACLRequirementSet); - ComponentWriteAcl.Add(SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID, AuthoritativeWorkerRequirementSet); - } - else - { - const WorkerAttributeSet ACLAttributeSet = { AclAuthoritativeWorkerType.ToString() }; - const WorkerRequirementSet ACLRequirementSet = { ACLAttributeSet }; - ComponentWriteAcl.Add(SpatialConstants::ENTITY_ACL_COMPONENT_ID, ACLRequirementSet); - } - if (Actor->IsNetStartupActor()) { ComponentWriteAcl.Add(SpatialConstants::TOMBSTONE_COMPONENT_ID, AuthoritativeWorkerRequirementSet); @@ -245,40 +209,29 @@ TArray EntityFactory::CreateEntityComponents(USpatialActor ComponentDatas.Add(SpawnData(Actor).CreateSpawnDataData()); ComponentDatas.Add(UnrealMetadata(StablyNamedObjectRef, Class->GetPathName(), bNetStartup).CreateUnrealMetadataData()); ComponentDatas.Add(NetOwningClientWorker(GetConnectionOwningWorkerId(Channel->Actor)).CreateNetOwningClientWorkerData()); + ComponentDatas.Add(AuthorityIntent::CreateAuthorityIntentData(IntendedVirtualWorkerId)); if (!Class->HasAnySpatialClassFlags(SPATIALCLASS_NotPersistent)) { ComponentDatas.Add(Persistence().CreatePersistenceData()); } - if (SpatialSettings->bEnableUnrealLoadBalancer) - { - ComponentDatas.Add(AuthorityIntent::CreateAuthorityIntentData(IntendedVirtualWorkerId)); - } - +#if !UE_BUILD_SHIPPING if (NetDriver->SpatialDebugger != nullptr) { - if (SpatialSettings->bEnableUnrealLoadBalancer) - { - check(NetDriver->VirtualWorkerTranslator != nullptr); + check(NetDriver->VirtualWorkerTranslator != nullptr); - VirtualWorkerId IntentVirtualWorkerId = NetDriver->VirtualWorkerTranslator->GetLocalVirtualWorkerId(); + const PhysicalWorkerName* PhysicalWorkerName = NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(IntendedVirtualWorkerId); + FColor InvalidServerTintColor = NetDriver->SpatialDebugger->InvalidServerTintColor; + FColor IntentColor = PhysicalWorkerName != nullptr ? SpatialGDK::GetColorForWorkerName(*PhysicalWorkerName) : InvalidServerTintColor; - const PhysicalWorkerName* PhysicalWorkerName = NetDriver->VirtualWorkerTranslator->GetPhysicalWorkerForVirtualWorker(IntentVirtualWorkerId); - FColor InvalidServerTintColor = NetDriver->SpatialDebugger->InvalidServerTintColor; - FColor IntentColor = PhysicalWorkerName == nullptr ? InvalidServerTintColor : SpatialGDK::GetColorForWorkerName(*PhysicalWorkerName); - - SpatialDebugging DebuggingInfo(SpatialConstants::INVALID_VIRTUAL_WORKER_ID, InvalidServerTintColor, IntentVirtualWorkerId, IntentColor, false); - ComponentDatas.Add(DebuggingInfo.CreateSpatialDebuggingData()); - } + const bool bIsLocked = NetDriver->LockingPolicy->IsLocked(Actor); + SpatialDebugging DebuggingInfo(SpatialConstants::INVALID_VIRTUAL_WORKER_ID, InvalidServerTintColor, IntendedVirtualWorkerId, IntentColor, bIsLocked); + ComponentDatas.Add(DebuggingInfo.CreateSpatialDebuggingData()); ComponentWriteAcl.Add(SpatialConstants::SPATIAL_DEBUGGING_COMPONENT_ID, AuthoritativeWorkerRequirementSet); } - - if (Class->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)) - { - ComponentDatas.Add(Singleton().CreateSingletonData()); - } +#endif if (ActorInterestComponentId != SpatialConstants::INVALID_COMPONENT_ID) { @@ -437,24 +390,8 @@ TArray EntityFactory::CreateTombstoneEntityComponents(AAct const UClass* Class = Actor->GetClass(); // Construct an ACL for a read-only entity. - WorkerRequirementSet AnyServerRequirementSet; - WorkerRequirementSet AnyServerOrClientRequirementSet = { SpatialConstants::UnrealClientAttributeSet }; - - for (const FName& WorkerType : GetDefault()->ServerWorkerTypes) - { - WorkerAttributeSet ServerWorkerAttributeSet = { WorkerType.ToString() }; - - AnyServerRequirementSet.Add(ServerWorkerAttributeSet); - AnyServerOrClientRequirementSet.Add(ServerWorkerAttributeSet); - } - - // Add Zoning Attribute if we are using the load balancer. - const USpatialGDKSettings* SpatialSettings = GetDefault(); - if (SpatialSettings->bEnableUnrealLoadBalancer) - { - AnyServerRequirementSet.Add(SpatialConstants::GetLoadBalancerAttributeSet(SpatialSettings->LoadBalancingWorkerType.WorkerTypeName)); - AnyServerOrClientRequirementSet.Add(SpatialConstants::GetLoadBalancerAttributeSet(SpatialSettings->LoadBalancingWorkerType.WorkerTypeName)); - } + WorkerRequirementSet AnyServerRequirementSet = { SpatialConstants::UnrealServerAttributeSet }; + WorkerRequirementSet AnyServerOrClientRequirementSet = { SpatialConstants::UnrealServerAttributeSet, SpatialConstants::UnrealClientAttributeSet }; WorkerRequirementSet ReadAcl; if (Class->HasAnySpatialClassFlags(SPATIALCLASS_ServerOnly)) diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityPool.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityPool.cpp index 38eceb4dfb..d18c10a807 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityPool.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityPool.cpp @@ -2,11 +2,12 @@ #include "Utils/EntityPool.h" -#include "TimerManager.h" - #include "Interop/SpatialReceiver.h" +#include "Interop/SpatialSender.h" #include "SpatialGDKSettings.h" +#include "TimerManager.h" + DEFINE_LOG_CATEGORY(LogSpatialEntityPool); using namespace SpatialGDK; @@ -78,6 +79,7 @@ void UEntityPool::ReserveEntityIDs(int32 EntitiesToReserve) if (!bIsReady) { bIsReady = true; + EntityPoolReadyDelegate.Broadcast(); } }); @@ -157,3 +159,8 @@ Worker_EntityId UEntityPool::GetNextEntityId() return NextId; } + +FEntityPoolReadyEvent& UEntityPool::GetEntityPoolReadyDelegate() +{ + return EntityPoolReadyDelegate; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp index f0ecc7659c..7f5b48f3d8 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp @@ -6,9 +6,10 @@ #include "EngineClasses/SpatialNetConnection.h" #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" +#include "EngineClasses/SpatialWorldSettings.h" #include "LoadBalancing/AbstractLBStrategy.h" -#include "SpatialGDKSettings.h" #include "SpatialConstants.h" +#include "SpatialGDKSettings.h" #include "Utils/Interest/NetCullDistanceInterest.h" #include "Engine/World.h" @@ -34,33 +35,28 @@ InterestFactory::InterestFactory(USpatialClassInfoManager* InClassInfoManager, U void InterestFactory::CreateAndCacheInterestState() { ClientCheckoutRadiusConstraint = NetCullDistanceInterest::CreateCheckoutRadiusConstraints(ClassInfoManager); - ClientNonAuthInterestResultType = CreateClientNonAuthInterestResultType(ClassInfoManager); - ClientAuthInterestResultType = CreateClientAuthInterestResultType(ClassInfoManager); - ServerNonAuthInterestResultType = CreateServerNonAuthInterestResultType(ClassInfoManager); + ClientNonAuthInterestResultType = CreateClientNonAuthInterestResultType(); + ClientAuthInterestResultType = CreateClientAuthInterestResultType(); + ServerNonAuthInterestResultType = CreateServerNonAuthInterestResultType(); ServerAuthInterestResultType = CreateServerAuthInterestResultType(); } -ResultType InterestFactory::CreateClientNonAuthInterestResultType(USpatialClassInfoManager* InClassInfoManager) +SchemaResultType InterestFactory::CreateClientNonAuthInterestResultType() { - ResultType ClientNonAuthResultType; + SchemaResultType ClientNonAuthResultType; // Add the required unreal components ClientNonAuthResultType.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(InClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_Data)); - - // In direct disagreement with the above comment, we add the owner only components as well. - // This is because GDK workers currently make assumptions about information being available at the point of possession. - // TODO(jacques): fix (unr-2865) - ClientNonAuthResultType.Append(InClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_OwnerOnly)); + ClientNonAuthResultType.Append(ClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_Data)); return ClientNonAuthResultType; } -ResultType InterestFactory::CreateClientAuthInterestResultType(USpatialClassInfoManager* InClassInfoManager) +SchemaResultType InterestFactory::CreateClientAuthInterestResultType() { - ResultType ClientAuthResultType; + SchemaResultType ClientAuthResultType; // Add the required known components ClientAuthResultType.Append(SpatialConstants::REQUIRED_COMPONENTS_FOR_AUTH_CLIENT_INTEREST); @@ -73,22 +69,22 @@ ResultType InterestFactory::CreateClientAuthInterestResultType(USpatialClassInfo return ClientAuthResultType; } -ResultType InterestFactory::CreateServerNonAuthInterestResultType(USpatialClassInfoManager* InClassInfoManager) +SchemaResultType InterestFactory::CreateServerNonAuthInterestResultType() { - ResultType ServerNonAuthResultType; + SchemaResultType ServerNonAuthResultType; // Add the required unreal components ServerNonAuthResultType.Append(SpatialConstants::REQUIRED_COMPONENTS_FOR_NON_AUTH_SERVER_INTEREST); // Add all data, owner only, and handover components - ServerNonAuthResultType.Append(InClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_Data)); - ServerNonAuthResultType.Append(InClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_OwnerOnly)); - ServerNonAuthResultType.Append(InClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_Handover)); + ServerNonAuthResultType.Append(ClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_Data)); + ServerNonAuthResultType.Append(ClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_OwnerOnly)); + ServerNonAuthResultType.Append(ClassInfoManager->GetComponentIdsForComponentType(ESchemaComponentType::SCHEMA_Handover)); return ServerNonAuthResultType; } -ResultType InterestFactory::CreateServerAuthInterestResultType() +SchemaResultType InterestFactory::CreateServerAuthInterestResultType() { // Just the components that we won't have already checked out through authority return SpatialConstants::REQUIRED_COMPONENTS_FOR_AUTH_SERVER_INTEREST; @@ -115,38 +111,18 @@ Interest InterestFactory::CreateServerWorkerInterest(const UAbstractLBStrategy* QueryConstraint Constraint; // Set the result type of the query - if (SpatialGDKSettings->bEnableResultTypes) - { - ServerQuery.ResultComponentIds = ServerNonAuthInterestResultType; - } - else - { - ServerQuery.FullSnapshotResult = true; - } - - if (SpatialGDKSettings->bEnableOffloading) - { - // In offloading scenarios, hijack the server worker entity to ensure each server has interest in all entities - Constraint.ComponentConstraint = SpatialConstants::POSITION_COMPONENT_ID; - ServerQuery.Constraint = Constraint; - - // No need to add any further interest as we are already interested in everything - AddComponentQueryPairToInterestComponent(ServerInterest, SpatialConstants::POSITION_COMPONENT_ID, ServerQuery); - return ServerInterest; - } - - // If we aren't offloading, the server gets more granular interest. + ServerQuery.ResultComponentIds = ServerNonAuthInterestResultType; // Ensure server worker receives always relevant entities QueryConstraint AlwaysRelevantConstraint = CreateAlwaysRelevantConstraint(); Constraint = AlwaysRelevantConstraint; - // If we are using the unreal load balancer, we also add the server worker interest defined by the load balancing strategy. - if (SpatialGDKSettings->bEnableUnrealLoadBalancer) + // 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); - + // 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. @@ -171,7 +147,7 @@ Interest InterestFactory::CreateServerWorkerInterest(const UAbstractLBStrategy* // TODO UNR-3042 : Migrate the VirtualWorkerTranslationManager to use the checked-out worker components instead of making a query. ServerQuery = Query(); - SetResultType(ServerQuery, ResultType{ SpatialConstants::WORKER_COMPONENT_ID }); + ServerQuery.ResultComponentIds = SchemaResultType{ SpatialConstants::WORKER_COMPONENT_ID }; ServerQuery.Constraint.ComponentConstraint = SpatialConstants::WORKER_COMPONENT_ID; AddComponentQueryPairToInterestComponent(ServerInterest, SpatialConstants::POSITION_COMPONENT_ID, ServerQuery); @@ -191,17 +167,11 @@ Interest InterestFactory::CreateInterest(AActor* InActor, const FClassInfo& InIn AddPlayerControllerActorInterest(ResultInterest, InActor, InInfo); } - if (Settings->bEnableResultTypes) - { - if (InActor->GetNetConnection() != nullptr) - { - // Clients need to see owner only and server RPC components on entities they have authority over - AddClientSelfInterest(ResultInterest, InEntityId); - } + // Clients need to see owner only and server RPC components on entities they have authority over + AddClientSelfInterest(ResultInterest, InEntityId); - // Every actor needs a self query for the server to the client RPC endpoint - AddServerSelfInterest(ResultInterest, InEntityId); - } + // Every actor needs a self query for the server to the client RPC endpoint + AddServerSelfInterest(ResultInterest, InEntityId); return ResultInterest; } @@ -226,7 +196,6 @@ void InterestFactory::AddClientSelfInterest(Interest& OutInterest, const Worker_ 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; AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::GetClientAuthorityComponent(GetDefault()->UseRPCRingBuffer()), NewQuery); @@ -237,13 +206,15 @@ void InterestFactory::AddServerSelfInterest(Interest& OutInterest, const Worker_ // Add a query for components all servers need to read client data Query ClientQuery; ClientQuery.Constraint.EntityIdConstraint = EntityId; - ClientQuery.ResultComponentIds = ServerAuthInterestResultType; + // 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); // Add a query for the load balancing worker (whoever is delegated the ACL) to read the authority intent Query LoadBalanceQuery; LoadBalanceQuery.Constraint.EntityIdConstraint = EntityId; - LoadBalanceQuery.ResultComponentIds = ResultType{ SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID, SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID }; + 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); } @@ -273,8 +244,7 @@ void InterestFactory::AddAlwaysRelevantAndInterestedQuery(Interest& OutInterest, Query ClientSystemQuery; ClientSystemQuery.Constraint = SystemAndLevelConstraint; - - SetResultType(ClientSystemQuery, ClientNonAuthInterestResultType); + ClientSystemQuery.ResultComponentIds = ClientNonAuthInterestResultType; AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::GetClientAuthorityComponent(Settings->UseRPCRingBuffer()), ClientSystemQuery); @@ -287,8 +257,7 @@ void InterestFactory::AddAlwaysRelevantAndInterestedQuery(Interest& OutInterest, QueryConstraint ServerSystemConstraint; ServerSystemConstraint.OrConstraint.Add(AlwaysInterestedConstraint); ServerSystemQuery.Constraint = ServerSystemConstraint; - - SetResultType(ServerSystemQuery, ServerNonAuthInterestResultType); + ServerSystemQuery.ResultComponentIds = ServerNonAuthInterestResultType; AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::POSITION_COMPONENT_ID, ServerSystemQuery); } @@ -330,7 +299,7 @@ void InterestFactory::AddUserDefinedQueries(Interest& OutInterest, const AActor* // We enforce result type even for user defined queries. Here we are assuming what a user wants from their defined // queries are for their players to check out more actors than they normally would, so use the client non auth result type, // which includes all components required for a client to see non-authoritative actors. - SetResultType(UserQuery, ClientNonAuthInterestResultType); + UserQuery.ResultComponentIds = ClientNonAuthInterestResultType; AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::GetClientAuthorityComponent(Settings->UseRPCRingBuffer()), UserQuery); @@ -342,8 +311,7 @@ void InterestFactory::AddUserDefinedQueries(Interest& OutInterest, const AActor* Query ServerUserQuery; ServerUserQuery.Constraint = UserConstraint; ServerUserQuery.Frequency = FrequencyToConstraints.Key; - - SetResultType(ServerUserQuery, ServerNonAuthInterestResultType); + ServerUserQuery.ResultComponentIds = ServerNonAuthInterestResultType; AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::POSITION_COMPONENT_ID, ServerUserQuery); } @@ -427,8 +395,7 @@ void InterestFactory::AddNetCullDistanceQueries(Interest& OutInterest, const Que } NewQuery.Frequency = CheckoutRadiusConstraintFrequencyPair.Frequency; - - SetResultType(NewQuery, ClientNonAuthInterestResultType); + NewQuery.ResultComponentIds = ClientNonAuthInterestResultType; AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::GetClientAuthorityComponent(Settings->UseRPCRingBuffer()), NewQuery); @@ -438,8 +405,7 @@ void InterestFactory::AddNetCullDistanceQueries(Interest& OutInterest, const Que Query ServerQuery; ServerQuery.Constraint = CheckoutRadiusConstraintFrequencyPair.Constraint; ServerQuery.Frequency = CheckoutRadiusConstraintFrequencyPair.Frequency; - - SetResultType(ServerQuery, ServerNonAuthInterestResultType); + ServerQuery.ResultComponentIds = ServerNonAuthInterestResultType; AddComponentQueryPairToInterestComponent(OutInterest, SpatialConstants::POSITION_COMPONENT_ID, ServerQuery); } @@ -509,8 +475,6 @@ QueryConstraint InterestFactory::CreateAlwaysRelevantConstraint() const QueryConstraint AlwaysRelevantConstraint; Worker_ComponentId ComponentIds[] = { - SpatialConstants::SINGLETON_COMPONENT_ID, - SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID, SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID, SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID @@ -582,16 +546,4 @@ void InterestFactory::AddObjectToConstraint(UObjectPropertyBase* Property, uint8 OutConstraint.OrConstraint.Add(EntityIdConstraint); } -void InterestFactory::SetResultType(Query& OutQuery, const ResultType& InResultType) const -{ - if (GetDefault()->bEnableResultTypes) - { - OutQuery.ResultComponentIds = InResultType; - } - else - { - OutQuery.FullSnapshotResult = true; - } -} - } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/OpUtils.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/OpUtils.cpp index b0c0272403..d827e8b9d6 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/OpUtils.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/OpUtils.cpp @@ -22,6 +22,22 @@ void FindFirstOpOfType(const TArray& InOpLists, const Worker_OpT } } +void AppendAllOpsOfType(const TArray& InOpLists, const Worker_OpType InOpType, TArray& FoundOps) +{ + for (const Worker_OpList* OpList : InOpLists) + { + for (size_t i = 0; i < OpList->op_count; ++i) + { + Worker_Op* Op = &OpList->ops[i]; + + if (Op->op_type == InOpType) + { + FoundOps.Add(Op); + } + } + } +} + void FindFirstOpOfTypeForComponent(const TArray& InOpLists, const Worker_OpType InOpType, const Worker_ComponentId InComponentId, Worker_Op** OutOp) { for (const Worker_OpList* OpList : InOpLists) diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp index b3fe925f10..dafce20e8e 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp @@ -27,12 +27,6 @@ namespace case ERPCResult::UnresolvedParameters: return TEXT("Unresolved Parameters"); - case ERPCResult::ActorPendingKill: - return TEXT("Actor Pending Kill"); - - case ERPCResult::TimedOut: - return TEXT("Timed Out"); - case ERPCResult::NoActorChannel: return TEXT("No Actor Channel"); @@ -57,6 +51,9 @@ namespace case ERPCResult::ControllerChannelNotListening: return TEXT("Controller Channel Not Listening"); + case ERPCResult::RPCServiceFailure: + return TEXT("SpatialRPCService couldn't handle the RPC"); + default: return TEXT("Unknown"); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialActorGroupManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialActorGroupManager.cpp deleted file mode 100644 index 2fa19baa31..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialActorGroupManager.cpp +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#include "Utils/SpatialActorGroupManager.h" -#include "SpatialGDKSettings.h" - -void SpatialActorGroupManager::Init() -{ - if (const USpatialGDKSettings* Settings = GetDefault()) - { - DefaultWorkerType = Settings->DefaultWorkerType.WorkerTypeName; - if (Settings->bEnableOffloading) - { - for (const TPair& ActorGroup : Settings->ActorGroups) - { - ActorGroupToWorkerType.Add(ActorGroup.Key, ActorGroup.Value.OwningWorkerType.WorkerTypeName); - - for (const TSoftClassPtr& ClassPtr : ActorGroup.Value.ActorClasses) - { - ClassPathToActorGroup.Add(ClassPtr, ActorGroup.Key); - } - } - } - } -} - -FName SpatialActorGroupManager::GetActorGroupForClass(const TSubclassOf Class) -{ - if (Class == nullptr) - { - return NAME_None; - } - - UClass* FoundClass = Class; - TSoftClassPtr ClassPtr = TSoftClassPtr(FoundClass); - - while (FoundClass != nullptr && FoundClass->IsChildOf(AActor::StaticClass())) - { - if (const FName* ActorGroup = ClassPathToActorGroup.Find(ClassPtr)) - { - FName ActorGroupHolder = *ActorGroup; - if (FoundClass != Class) - { - ClassPathToActorGroup.Add(TSoftClassPtr(Class), ActorGroupHolder); - } - return ActorGroupHolder; - } - - FoundClass = FoundClass->GetSuperClass(); - ClassPtr = TSoftClassPtr(FoundClass); - } - - // No mapping found so set and return default actor group. - ClassPathToActorGroup.Add(TSoftClassPtr(Class), SpatialConstants::DefaultActorGroup); - return SpatialConstants::DefaultActorGroup; -} - -FName SpatialActorGroupManager::GetWorkerTypeForClass(const TSubclassOf Class) -{ - const FName ActorGroup = GetActorGroupForClass(Class); - - if (const FName* WorkerType = ActorGroupToWorkerType.Find(ActorGroup)) - { - return *WorkerType; - } - - return DefaultWorkerType; -} - -FName SpatialActorGroupManager::GetWorkerTypeForActorGroup(const FName& ActorGroup) const -{ - if (const FName* WorkerType = ActorGroupToWorkerType.Find(ActorGroup)) - { - return *WorkerType; - } - - return DefaultWorkerType; -} - -bool SpatialActorGroupManager::IsSameWorkerType(const AActor* ActorA, const AActor* ActorB) -{ - if (ActorA == nullptr || ActorB == nullptr) - { - return false; - } - - const FName& WorkerTypeA = GetWorkerTypeForClass(ActorA->GetClass()); - const FName& WorkerTypeB = GetWorkerTypeForClass(ActorB->GetClass()); - - return (WorkerTypeA == WorkerTypeB); -} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebugger.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebugger.cpp index b5a45663dc..8196c292c2 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebugger.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialDebugger.cpp @@ -6,6 +6,8 @@ #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" @@ -143,7 +145,14 @@ void ASpatialDebugger::OnAuthorityGained() { if (NetDriver->LoadBalanceStrategy) { - if (const UGridBasedLBStrategy* GridBasedLBStrategy = 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())) { const UGridBasedLBStrategy::LBStrategyRegions LBStrategyRegions = GridBasedLBStrategy->GetLBStrategyRegions(); WorkerRegions.SetNum(LBStrategyRegions.Num()); @@ -229,7 +238,7 @@ void ASpatialDebugger::LoadIcons() { check(NetDriver != nullptr && !NetDriver->IsServer()); - UTexture2D* DefaultTexture = DefaultTexture = LoadObject(nullptr, TEXT("/Engine/EngineResources/DefaultTexture.DefaultTexture")); + UTexture2D* DefaultTexture = LoadObject(nullptr, TEXT("/Engine/EngineResources/DefaultTexture.DefaultTexture")); const float IconWidth = 16.0f; const float IconHeight = 16.0f; @@ -273,35 +282,30 @@ void ASpatialDebugger::OnEntityRemoved(const Worker_EntityId EntityId) void ASpatialDebugger::ActorAuthorityChanged(const Worker_AuthorityChangeOp& AuthOp) const { - const bool bAuthoritative = AuthOp.authority == WORKER_AUTHORITY_AUTHORITATIVE; + check(AuthOp.authority == WORKER_AUTHORITY_AUTHORITATIVE && AuthOp.component_id == SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID); - if (bAuthoritative && AuthOp.component_id == SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID) + if (NetDriver->VirtualWorkerTranslator == nullptr) { - if (NetDriver->VirtualWorkerTranslator == nullptr) - { - // Currently, there's nothing to display in the debugger other than load balancing information. - return; - } + // Currently, there's nothing to display in the debugger other than load balancing information. + return; + } - VirtualWorkerId LocalVirtualWorkerId = NetDriver->VirtualWorkerTranslator->GetLocalVirtualWorkerId(); - FColor LocalVirtualWorkerColor = SpatialGDK::GetColorForWorkerName(NetDriver->VirtualWorkerTranslator->GetLocalPhysicalWorkerName()); + VirtualWorkerId LocalVirtualWorkerId = NetDriver->VirtualWorkerTranslator->GetLocalVirtualWorkerId(); + FColor LocalVirtualWorkerColor = SpatialGDK::GetColorForWorkerName(NetDriver->VirtualWorkerTranslator->GetLocalPhysicalWorkerName()); - SpatialDebugging* DebuggingInfo = NetDriver->StaticComponentView->GetComponentData(AuthOp.entity_id); - if (DebuggingInfo == nullptr) - { - // Some entities won't have debug info, so create it now. - SpatialDebugging NewDebuggingInfo(LocalVirtualWorkerId, LocalVirtualWorkerColor, SpatialConstants::INVALID_VIRTUAL_WORKER_ID, InvalidServerTintColor, false); - NetDriver->Sender->SendAddComponents(AuthOp.entity_id, { NewDebuggingInfo.CreateSpatialDebuggingData() }); - return; - } - else - { - DebuggingInfo->AuthoritativeVirtualWorkerId = LocalVirtualWorkerId; - DebuggingInfo->AuthoritativeColor = LocalVirtualWorkerColor; - FWorkerComponentUpdate DebuggingUpdate = DebuggingInfo->CreateSpatialDebuggingUpdate(); - NetDriver->Connection->SendComponentUpdate(AuthOp.entity_id, &DebuggingUpdate); - } + SpatialDebugging* DebuggingInfo = NetDriver->StaticComponentView->GetComponentData(AuthOp.entity_id); + if (DebuggingInfo == nullptr) + { + // Some entities won't have debug info, so create it now. + SpatialDebugging NewDebuggingInfo(LocalVirtualWorkerId, LocalVirtualWorkerColor, SpatialConstants::INVALID_VIRTUAL_WORKER_ID, InvalidServerTintColor, false); + NetDriver->Sender->SendAddComponents(AuthOp.entity_id, { NewDebuggingInfo.CreateSpatialDebuggingData() }); + return; } + + DebuggingInfo->AuthoritativeVirtualWorkerId = LocalVirtualWorkerId; + DebuggingInfo->AuthoritativeColor = LocalVirtualWorkerColor; + FWorkerComponentUpdate DebuggingUpdate = DebuggingInfo->CreateSpatialDebuggingUpdate(); + NetDriver->Connection->SendComponentUpdate(AuthOp.entity_id, &DebuggingUpdate); } void ASpatialDebugger::ActorAuthorityIntentChanged(Worker_EntityId EntityId, VirtualWorkerId NewIntentVirtualWorkerId) const @@ -335,6 +339,11 @@ void ASpatialDebugger::DrawTag(UCanvas* Canvas, const FVector2D& ScreenLocation, static const float BaseHorizontalOffset(16.0f); + if (!FApp::CanEverRender()) // DrawIcon can attempt to use the underlying texture resource even when using nullrhi + { + return; + } + if (bShowLock) { SCOPE_CYCLE_COUNTER(STAT_DrawIcons); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLatencyTracer.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLatencyTracer.cpp index 8b3fe20d9f..4e3daf5d72 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLatencyTracer.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialLatencyTracer.cpp @@ -35,7 +35,7 @@ namespace } }; - UEStream Stream; + UEStream UStream; #if TRACE_LIB_ACTIVE improbable::trace::SpanContext ReadSpanContext(const void* TraceBytes, const void* SpanBytes) @@ -66,10 +66,13 @@ void USpatialLatencyTracer::RegisterProject(UObject* WorldContextObject, const F StackdriverExporter::Register({ TCHAR_TO_UTF8(*ProjectId) }); - std::cout.rdbuf(&Stream); - std::cerr.rdbuf(&Stream); + if (UE_GET_LOG_VERBOSITY(LogSpatialLatencyTracing) >= ELogVerbosity::Verbose) + { + std::cout.rdbuf(&UStream); + std::cerr.rdbuf(&UStream); - StdoutExporter::Register(); + StdoutExporter::Register(); + } #endif // TRACE_LIB_ACTIVE } @@ -215,7 +218,7 @@ void USpatialLatencyTracer::WriteToLatencyTrace(const TraceKey Key, const FStrin } } -void USpatialLatencyTracer::WriteAndEndTraceIfRemote(const TraceKey Key, const FString& TraceDesc) +void USpatialLatencyTracer::WriteAndEndTrace(const TraceKey Key, const FString& TraceDesc, bool bOnlyEndIfTraceRootIsRemote) { FScopeLock Lock(&Mutex); @@ -225,7 +228,7 @@ void USpatialLatencyTracer::WriteAndEndTraceIfRemote(const TraceKey Key, const F // Check RootTraces to verify if this trace was started locally. If it was, we don't End the trace yet, but // wait for an explicit call to EndLatencyTrace. - if (RootTraces.Find(Key) == nullptr) + if (!bOnlyEndIfTraceRootIsRemote || RootTraces.Find(Key) == nullptr) { Trace->End(); TraceMap.Remove(Key); @@ -349,19 +352,19 @@ void USpatialLatencyTracer::OnDequeueMessage(const SpatialGDK::FOutgoingMessage* if (Message->Type == SpatialGDK::EOutgoingMessageType::ComponentUpdate) { const SpatialGDK::FComponentUpdate* ComponentUpdate = static_cast(Message); - WriteAndEndTraceIfRemote(ComponentUpdate->Update.Trace, TEXT("Sent componentUpdate to Worker SDK")); + WriteAndEndTrace(ComponentUpdate->Update.Trace, TEXT("Sent componentUpdate to Worker SDK"), true); } else if (Message->Type == SpatialGDK::EOutgoingMessageType::AddComponent) { const SpatialGDK::FAddComponent* ComponentAdd = static_cast(Message); - WriteAndEndTraceIfRemote(ComponentAdd->Data.Trace, TEXT("Sent componentAdd to Worker SDK")); + WriteAndEndTrace(ComponentAdd->Data.Trace, TEXT("Sent componentAdd to Worker SDK"), true); } else if (Message->Type == SpatialGDK::EOutgoingMessageType::CreateEntityRequest) { const SpatialGDK::FCreateEntityRequest* CreateEntityRequest = static_cast(Message); for (auto& Component : CreateEntityRequest->Components) { - WriteAndEndTraceIfRemote(Component.Trace, TEXT("Sent createEntityRequest to Worker SDK")); + WriteAndEndTrace(Component.Trace, TEXT("Sent createEntityRequest to Worker SDK"), true); } } } @@ -437,7 +440,7 @@ bool USpatialLatencyTracer::ContinueLatencyTrace_Internal(const AActor* Actor, c // If we're not doing any further tracking, end the trace if (!bInternalTracking) { - WriteAndEndTraceIfRemote(Key, TEXT("Native - End of Tracking")); + WriteAndEndTrace(Key, TEXT("Native - End of Tracking"), true); } return true; diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp index 6d587f1346..238a1b0396 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp @@ -23,8 +23,14 @@ void USpatialMetrics::Init(USpatialWorkerConnection* InConnection, float InNetSe FramesSinceLastReport = 0; TimeOfLastReport = 0.0f; + WorkerLoad = 0.0; + bRPCTrackingEnabled = false; RPCTrackingStartTime = 0.0f; + + UserSuppliedMetric Delegate; + Delegate.BindUObject(this, &USpatialMetrics::GetAverageFPS); + SetCustomMetric(SpatialConstants::SPATIALOS_METRICS_DYNAMIC_FPS, Delegate); } void USpatialMetrics::TickMetrics(float NetDriverTime) @@ -40,20 +46,44 @@ void USpatialMetrics::TickMetrics(float NetDriverTime) } AverageFPS = FramesSinceLastReport / TimeSinceLastReport; - WorkerLoad = CalculateLoad(); + if (WorkerLoadDelegate.IsBound()) + { + WorkerLoad = WorkerLoadDelegate.Execute(); + } + else + { + WorkerLoad = CalculateLoad(); + } - SpatialGDK::GaugeMetric DynamicFPSGauge; - DynamicFPSGauge.Key = TCHAR_TO_UTF8(*SpatialConstants::SPATIALOS_METRICS_DYNAMIC_FPS); - DynamicFPSGauge.Value = AverageFPS; + SpatialGDK::SpatialMetrics Metrics; + Metrics.Load = WorkerLoad; + + // User supplied metrics + TArray UnboundMetrics; + for (const TPair& Gauge : UserSuppliedMetrics) + { + if (Gauge.Value.IsBound()) + { + SpatialGDK::GaugeMetric Metric; - SpatialGDK::SpatialMetrics DynamicFPSMetrics; - DynamicFPSMetrics.GaugeMetrics.Add(DynamicFPSGauge); - DynamicFPSMetrics.Load = WorkerLoad; + Metric.Key = TCHAR_TO_UTF8(*Gauge.Key); + Metric.Value = Gauge.Value.Execute(); + Metrics.GaugeMetrics.Add(Metric); + } + else + { + UnboundMetrics.Add(Gauge.Key); + } + } + for (const FString& KeyToRemove : UnboundMetrics) + { + UserSuppliedMetrics.Remove(KeyToRemove); + } TimeOfLastReport = NetDriverTime; FramesSinceLastReport = 0; - Connection->SendMetrics(DynamicFPSMetrics); + Connection->SendMetrics(Metrics); } // Load defined as performance relative to target frame time or just frame time based on config value. @@ -315,3 +345,25 @@ 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")); + if (UserSuppliedMetric* ExistingMetric = UserSuppliedMetrics.Find(Metric)) + { + *ExistingMetric = Delegate; + } + else + { + UserSuppliedMetrics.Add(Metric, Delegate); + } +} + +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")); + UserSuppliedMetrics.Remove(Metric); + } +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp index 7df399b35d..c98f9c44cd 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp @@ -5,14 +5,15 @@ #include "Engine/World.h" #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" +#include "EngineClasses/SpatialWorldSettings.h" #include "GeneralProjectSettings.h" #include "Interop/SpatialWorkerFlags.h" #include "Kismet/KismetSystemLibrary.h" #include "SpatialConstants.h" #include "EngineClasses/SpatialGameInstance.h" +#include "LoadBalancing/LayeredLBStrategy.h" #include "SpatialGDKSettings.h" #include "Utils/InspectionColors.h" -#include "Utils/SpatialActorGroupManager.h" DEFINE_LOG_CATEGORY(LogSpatial); @@ -21,19 +22,6 @@ bool USpatialStatics::IsSpatialNetworkingEnabled() return GetDefault()->UsesSpatialNetworking(); } -SpatialActorGroupManager* USpatialStatics::GetActorGroupManager(const UObject* WorldContext) -{ - if (const UWorld* World = WorldContext->GetWorld()) - { - if (const USpatialGameInstance* SpatialGameInstance = Cast(World->GetGameInstance())) - { - check(SpatialGameInstance->ActorGroupManager.IsValid()); - return SpatialGameInstance->ActorGroupManager.Get(); - } - } - return nullptr; -} - FName USpatialStatics::GetCurrentWorkerType(const UObject* WorldContext) { if (const UWorld* World = WorldContext->GetWorld()) @@ -78,9 +66,17 @@ FColor USpatialStatics::GetInspectorColorForWorkerName(const FString& WorkerName return SpatialGDK::GetColorForWorkerName(WorkerName); } -bool USpatialStatics::IsSpatialOffloadingEnabled() +bool USpatialStatics::IsSpatialOffloadingEnabled(const UWorld* World) { - return IsSpatialNetworkingEnabled() && GetDefault()->bEnableOffloading; + if (World != nullptr) + { + if (const ASpatialWorldSettings* WorldSettings = Cast(World->GetWorldSettings())) + { + return IsSpatialNetworkingEnabled() && WorldSettings->WorkerLayers.Num() > 0 && WorldSettings->IsMultiWorkerEnabled(); + } + } + + return false; } bool USpatialStatics::IsActorGroupOwnerForActor(const AActor* Actor) @@ -90,73 +86,44 @@ bool USpatialStatics::IsActorGroupOwnerForActor(const AActor* Actor) return false; } - const AActor* EffectiveActor = Actor; - while (EffectiveActor->bUseNetOwnerActorGroup && EffectiveActor->GetOwner() != nullptr) + // Offloading using the Unreal Load Balancing always load balances based on the owning actor. + const AActor* RootOwner = Actor; + while (RootOwner->GetOwner() != nullptr && RootOwner->GetOwner()->GetIsReplicated()) { - EffectiveActor = EffectiveActor->GetOwner(); + RootOwner = RootOwner->GetOwner(); } - return IsActorGroupOwnerForClass(EffectiveActor, EffectiveActor->GetClass()); + return IsActorGroupOwnerForClass(RootOwner, RootOwner->GetClass()); } bool USpatialStatics::IsActorGroupOwnerForClass(const UObject* WorldContextObject, const TSubclassOf ActorClass) { - if (SpatialActorGroupManager* ActorGroupManager = GetActorGroupManager(WorldContextObject)) - { - const FName ClassWorkerType = ActorGroupManager->GetWorkerTypeForClass(ActorClass); - const FName CurrentWorkerType = GetCurrentWorkerType(WorldContextObject); - return ClassWorkerType == CurrentWorkerType; - } - - if (const UWorld* World = WorldContextObject->GetWorld()) + const UWorld* World = WorldContextObject->GetWorld(); + if (World == nullptr) { - return World->GetNetMode() != NM_Client; - } - - return false; -} - -bool USpatialStatics::IsActorGroupOwner(const UObject* WorldContextObject, const FName ActorGroup) -{ - if (SpatialActorGroupManager* ActorGroupManager = GetActorGroupManager(WorldContextObject)) - { - const FName ActorGroupWorkerType = ActorGroupManager->GetWorkerTypeForActorGroup(ActorGroup); - const FName CurrentWorkerType = GetCurrentWorkerType(WorldContextObject); - return ActorGroupWorkerType == CurrentWorkerType; + return false; } - if (const UWorld* World = WorldContextObject->GetWorld()) + if (World->IsNetMode(NM_Client)) { - return World->GetNetMode() != NM_Client; + return false; } - return false; -} - -FName USpatialStatics::GetActorGroupForActor(const AActor* Actor) -{ - if (SpatialActorGroupManager* ActorGroupManager = GetActorGroupManager(Actor)) + if (const USpatialNetDriver* SpatialNetDriver = Cast(World->GetNetDriver())) { - const AActor* EffectiveActor = Actor; - while (EffectiveActor->bUseNetOwnerActorGroup && EffectiveActor->GetOwner() != nullptr) + // Calling IsActorGroupOwnerForClass before NotifyBeginPlay has been called (when NetDriver is ready) is invalid. + if (!SpatialNetDriver->IsReady()) { - EffectiveActor = EffectiveActor->GetOwner(); + UE_LOG(LogSpatial, Error, TEXT("Called IsActorGroupOwnerForClass before NotifyBeginPlay has been called is invalid. Actor class: %s"), *GetNameSafe(ActorClass)); + return true; } - return ActorGroupManager->GetActorGroupForClass(EffectiveActor->GetClass()); - } - - return SpatialConstants::DefaultActorGroup; -} - -FName USpatialStatics::GetActorGroupForClass(const UObject* WorldContextObject, const TSubclassOf ActorClass) -{ - if (SpatialActorGroupManager* ActorGroupManager = GetActorGroupManager(WorldContextObject)) - { - return ActorGroupManager->GetActorGroupForClass(ActorClass); + if (const ULayeredLBStrategy* LBStrategy = Cast(SpatialNetDriver->LoadBalanceStrategy)) + { + return LBStrategy->CouldHaveAuthority(ActorClass); + } } - - return SpatialConstants::DefaultActorGroup; + 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*/) diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/SpatialPingComponent.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/SpatialPingComponent.h index b0b66c258a..85f5bac7a5 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/SpatialPingComponent.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/SpatialPingComponent.h @@ -23,6 +23,10 @@ struct FSpatialPingAverageData UPROPERTY(BlueprintReadWrite, Category = SpatialPing) float LastMeasurementsWindowMax; UPROPERTY(BlueprintReadWrite, Category = SpatialPing) + float LastMeasurementsWindow50thPercentile; + UPROPERTY(BlueprintReadWrite, Category = SpatialPing) + float LastMeasurementsWindow90thPercentile; + UPROPERTY(BlueprintReadWrite, Category = SpatialPing) int WindowSize; UPROPERTY(BlueprintReadWrite, Category = SpatialPing) diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h index 27a568c797..d3b0b68766 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h @@ -75,7 +75,6 @@ struct FObjectReferences struct FPendingSubobjectAttachment { - USpatialActorChannel* Channel; const FClassInfo* Info; TWeakObjectPtr Subobject; @@ -137,8 +136,8 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel if (EntityId != SpatialConstants::INVALID_ENTITY_ID) { - // If the entity already exists, make sure we have spatial authority before we replicate with Offloading, because we pretend to have local authority - if (USpatialStatics::IsSpatialOffloadingEnabled() && !bCreatingNewEntity && !NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::POSITION_COMPONENT_ID)) + // If the entity already exists, make sure we have spatial authority before we replicate. + if (!bCreatingNewEntity && !NetDriver->StaticComponentView->HasAuthority(EntityId, SpatialConstants::POSITION_COMPONENT_ID)) { return false; } @@ -171,32 +170,7 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel // Indicates whether this client worker has "ownership" (authority over Client endpoint) over the entity corresponding to this channel. inline bool IsAuthoritativeClient() const { - if (GetDefault()->bEnableResultTypes) - { - return bIsAuthClient; - } - - // If we aren't using result types, we have to actually look at the ACL to see if we should be authoritative or not to guess if we are going to receive authority - // in order to send dynamic interest overrides correctly for this client. If we don't do this there's a good chance we will see that there is no server RPC endpoint - // on this entity when we try to send any RPCs immediately after checking out the entity, which can lead to inconsistent state. - const TArray& WorkerAttributes = NetDriver->Connection->GetWorkerAttributes(); - if (const SpatialGDK::EntityAcl* EntityACL = NetDriver->StaticComponentView->GetComponentData(EntityId)) - { - if (const WorkerRequirementSet* WorkerRequirementsSet = EntityACL->ComponentWriteAcl.Find(SpatialConstants::GetClientAuthorityComponent(GetDefault()->UseRPCRingBuffer()))) { - for (const WorkerAttributeSet& AttributeSet : *WorkerRequirementsSet) - { - for (const FString& Attribute : AttributeSet) - { - if (WorkerAttributes.Contains(Attribute)) - { - return true; - } - } - } - } - } - - return false; + return bIsAuthClient; } // Sets the server and client authorities for this SpatialActorChannel based on the StaticComponentView @@ -212,8 +186,12 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel } } - inline void SetServerAuthority(const bool IsAuth) + void SetServerAuthority(const bool IsAuth) { + if (IsAuth && !bIsAuthServer) + { + AuthorityReceivedTimestamp = FPlatformTime::Cycles64(); + } bIsAuthServer = IsAuth; } @@ -231,11 +209,7 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel FORCEINLINE FRepStateStaticBuffer& GetObjectStaticBuffer(UObject* Object) { check(ObjectHasReplicator(Object)); -#if ENGINE_MINOR_VERSION <= 22 - return FindOrCreateReplicator(Object)->RepState->StaticBuffer; -#else return FindOrCreateReplicator(Object)->RepState->GetReceivingRepState()->StaticBuffer; -#endif } // Begin UChannel interface @@ -245,11 +219,7 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel // Begin UActorChannel interface virtual int64 ReplicateActor() override; -#if ENGINE_MINOR_VERSION <= 22 - virtual void SetChannelActor(AActor* InActor) override; -#else virtual void SetChannelActor(AActor* InActor, ESetChannelActorFlags Flags) override; -#endif virtual bool ReplicateSubobject(UObject* Obj, FOutBunch& Bunch, const FReplicationFlags& RepFlags) override; virtual bool ReadyForDormancy(bool suppressLogs = false) override; // End UActorChannel interface @@ -272,7 +242,7 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel void OnCreateEntityResponse(const Worker_CreateEntityResponseOp& Op); void RemoveRepNotifiesWithUnresolvedObjs(TArray& RepNotifies, const FRepLayout& RepLayout, const FObjectReferencesMap& RefMap, UObject* Object); - + void UpdateShadowData(); void UpdateSpatialPositionWithFrequencyCheck(); void UpdateSpatialPosition(); @@ -298,13 +268,15 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel private: void DynamicallyAttachSubobject(UObject* Object); - void DeleteEntityIfAuthoritative(); + void RetireEntityIfAuthoritative(); void SendPositionUpdate(AActor* InActor, Worker_EntityId InEntityId, const FVector& NewPosition); void InitializeHandoverShadowData(TArray& ShadowData, UObject* Object); FHandoverChangeState GetHandoverChangeList(TArray& ShadowData, UObject* Object); - + + void GetLatestAuthorityChangeFromHierarchy(const AActor* HierarchyActor, uint64& OutTimestamp); + 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; @@ -359,4 +331,15 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel // when those properties change. TArray* ActorHandoverShadowData; TMap, TSharedRef>> HandoverShadowDataMap; + + // Band-aid until we get Actor Sets. + // Used on server-side workers only. + // Record when this worker receives SpatialOS Position component authority over the Actor. + // Tracking this helps prevent authority thrashing which can happen due a replication race + // between hierarchy Actors. This happens because hierarchy Actor migration using the + // default load-balancing strategy depends on the position of the hierarchy root Actor, + // or its controlled pawn. If the hierarchy Actor data is replicated to the new worker + // 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; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialFastArrayNetSerialize.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialFastArrayNetSerialize.h index b3d2513096..d55b6b583f 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialFastArrayNetSerialize.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialFastArrayNetSerialize.h @@ -21,7 +21,6 @@ class SpatialFastArrayNetSerializeCB : public INetSerializeCB : NetDriver(InNetDriver) { } virtual void NetSerializeStruct(UScriptStruct* Struct, FBitArchive& Ar, UPackageMap* PackageMap, void* Data, bool& bHasUnmapped) override; -#if ENGINE_MINOR_VERSION >= 23 //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.")); }; @@ -29,7 +28,6 @@ class SpatialFastArrayNetSerializeCB : public INetSerializeCB 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; }; -#endif private: USpatialNetDriver* NetDriver; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialGameInstance.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialGameInstance.h index 2ccf09d741..844b1beca7 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialGameInstance.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialGameInstance.h @@ -4,7 +4,6 @@ #include "CoreMinimal.h" #include "Engine/GameInstance.h" -#include "Utils/SpatialActorGroupManager.h" #include "SpatialGameInstance.generated.h" @@ -15,8 +14,9 @@ class USpatialStaticComponentView; DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGameInstance, Log, All); -DECLARE_EVENT(USpatialGameInstance, FOnConnectedEvent); -DECLARE_EVENT_OneParam(USpatialGameInstance, FOnConnectionFailedEvent, const FString&); +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnConnectedEvent); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnConnectionFailedEvent, const FString&, Reason); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlayerSpawnFailedEvent, const FString&, Reason); UCLASS(config = Engine) class SPATIALGDK_API USpatialGameInstance : public UGameInstance @@ -24,11 +24,11 @@ class SPATIALGDK_API USpatialGameInstance : public UGameInstance GENERATED_BODY() public: + USpatialGameInstance(); + #if WITH_EDITOR virtual FGameInstancePIEResult StartPlayInEditorGameInstance(ULocalPlayer* LocalPlayer, const FGameInstancePIEParameters& Params) override; #endif - // Initializes the Spatial connection if Spatial networking is enabled, otherwise does nothing. - void TryConnectToSpatial(); virtual void StartGameInstance() override; @@ -53,16 +53,26 @@ class SPATIALGDK_API USpatialGameInstance : public UGameInstance void HandleOnConnected(); void HandleOnConnectionFailed(const FString& Reason); + void HandleOnPlayerSpawnFailed(const FString& Reason); + + void CleanupCachedLevelsAfterConnection(); // Invoked when this worker has successfully connected to SpatialOS - FOnConnectedEvent OnConnected; + UPROPERTY(BlueprintAssignable) + FOnConnectedEvent OnSpatialConnected; // Invoked when this worker fails to initiate a connection to SpatialOS - FOnConnectionFailedEvent OnConnectionFailed; + UPROPERTY(BlueprintAssignable) + FOnConnectionFailedEvent OnSpatialConnectionFailed; + // Invoked when the player could not be spawned + UPROPERTY(BlueprintAssignable) + FOnPlayerSpawnFailedEvent OnSpatialPlayerSpawnFailed; - void SetFirstConnectionToSpatialOSAttempted() { bFirstConnectionToSpatialOSAttempted = true; }; - bool GetFirstConnectionToSpatialOSAttempted() const { return bFirstConnectionToSpatialOSAttempted; }; + void DisableShouldConnectUsingCommandLineArgs() { bShouldConnectUsingCommandLineArgs = false; } + bool GetShouldConnectUsingCommandLineArgs() const { return bShouldConnectUsingCommandLineArgs; } - TUniquePtr ActorGroupManager; + void TryInjectSpatialLocatorIntoCommandLine(); + + void CleanupLevelInitializedNetworkActors(ULevel* LoadedLevel); protected: // Checks whether the current net driver is a USpatialNetDriver. @@ -74,7 +84,8 @@ class SPATIALGDK_API USpatialGameInstance : public UGameInstance UPROPERTY() USpatialConnectionManager* SpatialConnectionManager; - bool bFirstConnectionToSpatialOSAttempted = false; + bool bShouldConnectUsingCommandLineArgs = true; + bool bHasPreviouslyConnectedToSpatial = false; UPROPERTY() USpatialLatencyTracer* SpatialLatencyTracer = nullptr; @@ -87,6 +98,19 @@ class SPATIALGDK_API USpatialGameInstance : public UGameInstance UPROPERTY() USpatialStaticComponentView* StaticComponentView; + // A set of the levels which were loaded before the SpatialOS connection. + UPROPERTY() + TSet CachedLevelsForNetworkIntialize; + + // Initializes the Spatial connection manager if Spatial networking is enabled, otherwise does nothing. + void StartSpatialConnection(); + + void SetHasPreviouslyConnectedToSpatial() { bHasPreviouslyConnectedToSpatial = true; } + bool HasPreviouslyConnectedToSpatial() const { return bHasPreviouslyConnectedToSpatial; } + UFUNCTION() - void OnLevelInitializedNetworkActors(ULevel* Level, UWorld* OwningWorld); + void OnLevelInitializedNetworkActors(ULevel* LoadedLevel, UWorld* OwningWorld); + + // Boolean for whether or not the Spatial connection is ready for normal operations. + bool bIsSpatialNetDriverReady; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h index b73acd97d5..7123e3c70c 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h @@ -10,7 +10,6 @@ #include "Interop/SpatialOutputDevice.h" #include "Interop/SpatialRPCService.h" #include "Interop/SpatialSnapshotManager.h" -#include "Utils/SpatialActorGroupManager.h" #include "Utils/InterestFactory.h" #include "LoadBalancing/AbstractLockingPolicy.h" @@ -26,7 +25,6 @@ class ASpatialDebugger; class ASpatialMetricsDisplay; -class SpatialActorGroupManager; class UAbstractLBStrategy; class UEntityPool; class UGlobalStateManager; @@ -157,7 +155,6 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver UPROPERTY() USpatialWorkerFlags* SpatialWorkerFlags; - SpatialActorGroupManager* ActorGroupManager; TUniquePtr InterestFactory; TUniquePtr LoadBalanceEnforcer; TUniquePtr VirtualWorkerTranslator; @@ -176,7 +173,7 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver int32 GetConsiderListSize() const { return ConsiderListSize; } #endif - void DelayedSendDeleteEntityRequest(Worker_EntityId EntityId, float Delay); + void DelayedRetireEntity(Worker_EntityId EntityId, float Delay, bool bIsNetStartupActor); #if WITH_EDITOR // We store the PlayInEditorID associated with this NetDriver to handle replace a worker initialization when in the editor. @@ -185,6 +182,10 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver void TrackTombstone(const Worker_EntityId EntityId); #endif + // IsReady evaluates the GSM, Load Balancing system, and others to get a holistic + // view of whether the SpatialNetDriver is ready to assume normal operations. + bool IsReady() const; + private: TUniquePtr Dispatcher; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h index 48de674d43..d0d7379f83 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h @@ -2,11 +2,12 @@ #pragma once -#include "CoreMinimal.h" #include "Engine/PackageMapClient.h" - #include "Schema/UnrealMetadata.h" #include "Schema/UnrealObjectRef.h" +#include "Utils/EntityPool.h" + +#include "CoreMinimal.h" #include @@ -52,12 +53,15 @@ class SPATIALGDK_API USpatialPackageMapClient : public UPackageMapClient FUnrealObjectRef GetUnrealObjectRefFromObject(const UObject* Object); Worker_EntityId GetEntityIdFromObject(const UObject* Object); - AActor* GetSingletonByClassRef(const FUnrealObjectRef& SingletonClassRef); + AActor* GetUniqueActorInstanceByClassRef(const FUnrealObjectRef& ClassRef); + AActor* GetUniqueActorInstanceByClass(UClass* Class) const; // Expose FNetGUIDCache::CanClientLoadObject so we can include this info with UnrealObjectRef. bool CanClientLoadObject(UObject* Object); + Worker_EntityId AllocateEntityId(); bool IsEntityPoolReady() const; + FEntityPoolReadyEvent& GetEntityPoolReadyDelegate(); virtual bool SerializeObject(FArchive& Ar, UClass* InClass, UObject*& Obj, FNetworkGUID *OutNetGUID = NULL) override; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslationManager.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslationManager.h index 8bc70fa516..544cc82226 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslationManager.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialVirtualWorkerTranslationManager.h @@ -38,7 +38,7 @@ class SPATIALGDK_API SpatialVirtualWorkerTranslationManager SpatialOSWorkerInterface* InConnection, SpatialVirtualWorkerTranslator* InTranslator); - void AddVirtualWorkerIds(const TSet& InVirtualWorkerIds); + 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); diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialWorldSettings.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialWorldSettings.h index 58340fed2c..d1025b2971 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialWorldSettings.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialWorldSettings.h @@ -2,8 +2,12 @@ #pragma once +#include "LoadBalancing/LayeredLBStrategy.h" + #include "CoreMinimal.h" #include "GameFramework/WorldSettings.h" +#include "SpatialGDKSettings.h" +#include "Utils/LayerInfo.h" #include "SpatialWorldSettings.generated.h" @@ -16,10 +20,34 @@ class SPATIALGDK_API ASpatialWorldSettings : public AWorldSettings GENERATED_BODY() public: - UPROPERTY(EditAnywhere, Config, Category = "Load Balancing") - TSubclassOf LoadBalanceStrategy; - - UPROPERTY(EditAnywhere, Config, Category = "Load Balancing") - TSubclassOf LockingPolicy; - + /** Enable running different server worker types to split the simulation. */ + UPROPERTY(EditAnywhere, Config, Category = "Multi-Worker") + bool bEnableMultiWorker; + + UPROPERTY(EditAnywhere, Config, Category = "Multi-Worker", meta = (EditCondition = "bEnableMultiWorker")) + TSubclassOf DefaultLayerLoadBalanceStrategy; + + UPROPERTY(EditAnywhere, Config, Category = "Multi-Worker", meta = (EditCondition = "bEnableMultiWorker")) + TSubclassOf DefaultLayerLockingPolicy; + + /** + * Any classes not specified on another layer will be handled by the default layer, but this also gives a way + * to force classes to be on the default layer. + */ + UPROPERTY(EditAnywhere, Category = "Multi-Worker", meta = (EditCondition = "bEnableMultiWorker")) + TSet> ExplicitDefaultActorClasses; + + /** Layer configuration. */ + UPROPERTY(EditAnywhere, Config, Category = "Multi-Worker", meta = (EditCondition = "bEnableMultiWorker")) + TMap WorkerLayers; + + bool IsMultiWorkerEnabled() const + { + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + if (SpatialGDKSettings->bOverrideMultiWorker.IsSet()) + { + return SpatialGDKSettings->bOverrideMultiWorker.GetValue(); + } + return bEnableMultiWorker; + } }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/ConnectionConfig.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/ConnectionConfig.h index 8ce0da4e6b..4f68281f18 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/ConnectionConfig.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/ConnectionConfig.h @@ -8,14 +8,16 @@ #include "Misc/Parse.h" #include "SpatialConstants.h" #include "SpatialGDKSettings.h" - #include struct FConnectionConfig { FConnectionConfig() : UseExternalIp(false) - , EnableProtocolLoggingAtStartup(false) + , EnableWorkerSDKProtocolLogging(false) + , EnableWorkerSDKOpLogging(false) + , WorkerSDKLogFileSize(10 * 1024 * 1024) + , WorkerSDKLogLevel(WORKER_LOG_LEVEL_INFO) , LinkProtocol(WORKER_NETWORK_CONNECTION_TYPE_MODULAR_KCP) , TcpMultiplexLevel(2) // This is a "finger-in-the-air" number. // These settings will be overridden by Spatial GDK settings before connection applied (see PreConnectInit) @@ -26,24 +28,14 @@ struct FConnectionConfig const TCHAR* CommandLine = FCommandLine::Get(); FParse::Value(CommandLine, TEXT("workerId"), WorkerId); - FParse::Bool(CommandLine, TEXT("useExternalIpForBridge"), UseExternalIp); - FParse::Bool(CommandLine, TEXT("enableProtocolLogging"), EnableProtocolLoggingAtStartup); - FParse::Value(CommandLine, TEXT("protocolLoggingPrefix"), ProtocolLoggingPrefix); - - FString LinkProtocolString; - FParse::Value(CommandLine, TEXT("linkProtocol"), LinkProtocolString); - if (LinkProtocolString == TEXT("Tcp")) - { - LinkProtocol = WORKER_NETWORK_CONNECTION_TYPE_MODULAR_TCP; - } - else if (LinkProtocolString == TEXT("Kcp")) - { - LinkProtocol = WORKER_NETWORK_CONNECTION_TYPE_MODULAR_KCP; - } - else if (!LinkProtocolString.IsEmpty()) - { - UE_LOG(LogTemp, Warning, TEXT("Unknown network protocol %s specified for connecting to SpatialOS. Defaulting to KCP."), *LinkProtocolString); - } + FParse::Bool(CommandLine, TEXT("enableWorkerSDKProtocolLogging"), EnableWorkerSDKProtocolLogging); + FParse::Bool(CommandLine, TEXT("enableWorkerSDKOpLogging"), EnableWorkerSDKOpLogging); + FParse::Value(CommandLine, TEXT("workerSDKLogPrefix"), WorkerSDKLogPrefix); + // TODO: When upgrading to Worker SDK 14.6.2, remove this parameter and set it to 0 for infinite file size + FParse::Value(CommandLine, TEXT("workerSDKLogFileSize"), WorkerSDKLogFileSize); + + GetWorkerSDKLogLevel(CommandLine); + GetLinkProtocol(CommandLine); } void PreConnectInit(const bool bConnectAsClient) @@ -63,17 +55,66 @@ struct FConnectionConfig TcpNoDelay = (SpatialGDKSettings->bTcpNoDelay ? 1 : 0); - UdpUpstreamIntervalMS = (bConnectAsClient ? SpatialGDKSettings->UdpClientUpstreamUpdateIntervalMS : SpatialGDKSettings->UdpServerUpstreamUpdateIntervalMS); + 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); } +private: + void GetWorkerSDKLogLevel(const TCHAR* CommandLine) + { + FString LogLevelString; + FParse::Value(CommandLine, TEXT("workerSDKLogLevel"), LogLevelString); + if (LogLevelString.Compare(TEXT("debug"), ESearchCase::IgnoreCase) == 0) + { + WorkerSDKLogLevel = WORKER_LOG_LEVEL_DEBUG; + } + else if (LogLevelString.Compare(TEXT("info"), ESearchCase::IgnoreCase) == 0) + { + WorkerSDKLogLevel = WORKER_LOG_LEVEL_INFO; + } + else if (LogLevelString.Compare(TEXT("warning"), ESearchCase::IgnoreCase) == 0) + { + WorkerSDKLogLevel = WORKER_LOG_LEVEL_WARN; + } + else if (LogLevelString.Compare(TEXT("error"), ESearchCase::IgnoreCase) == 0) + { + WorkerSDKLogLevel = WORKER_LOG_LEVEL_ERROR; + } + else if (!LogLevelString.IsEmpty()) + { + UE_LOG(LogTemp, Warning, TEXT("Unknown worker SDK log verbosity %s specified. Defaulting to Info."), *LogLevelString); + } + } + + void GetLinkProtocol(const TCHAR* CommandLine) + { + FString LinkProtocolString; + FParse::Value(CommandLine, TEXT("linkProtocol"), LinkProtocolString); + if (LinkProtocolString.Compare(TEXT("Tcp"), ESearchCase::IgnoreCase) == 0) + { + LinkProtocol = WORKER_NETWORK_CONNECTION_TYPE_MODULAR_TCP; + } + else if (LinkProtocolString.Compare(TEXT("Kcp"), ESearchCase::IgnoreCase) == 0) + { + LinkProtocol = WORKER_NETWORK_CONNECTION_TYPE_MODULAR_KCP; + } + else if (!LinkProtocolString.IsEmpty()) + { + UE_LOG(LogTemp, Warning, TEXT("Unknown network protocol %s specified for connecting to SpatialOS. Defaulting to KCP."), *LinkProtocolString); + } + } + +public: FString WorkerId; FString WorkerType; bool UseExternalIp; - bool EnableProtocolLoggingAtStartup; - FString ProtocolLoggingPrefix; + bool EnableWorkerSDKProtocolLogging; + bool EnableWorkerSDKOpLogging; + FString WorkerSDKLogPrefix; + uint32 WorkerSDKLogFileSize; + Worker_LogLevel WorkerSDKLogLevel; Worker_NetworkConnectionType LinkProtocol; - Worker_ConnectionParameters ConnectionParams; + Worker_ConnectionParameters ConnectionParams = {}; uint8 TcpMultiplexLevel; uint8 TcpNoDelay; uint8 UdpUpstreamIntervalMS; @@ -142,14 +183,13 @@ class FDevAuthConfig : public FLocatorConfig bool TryLoadCommandLineArgs() { - bool bSuccess = true; const TCHAR* CommandLine = FCommandLine::Get(); FParse::Value(CommandLine, TEXT("locatorHost"), LocatorHost); FParse::Value(CommandLine, TEXT("deployment"), Deployment); FParse::Value(CommandLine, TEXT("playerId"), PlayerId); FParse::Value(CommandLine, TEXT("displayName"), DisplayName); FParse::Value(CommandLine, TEXT("metaData"), MetaData); - bSuccess = FParse::Value(CommandLine, TEXT("devAuthToken"), DevelopmentAuthToken); + const bool bSuccess = FParse::Value(CommandLine, TEXT("devAuthToken"), DevelopmentAuthToken); return bSuccess; } @@ -170,39 +210,56 @@ class FReceptionistConfig : public FConnectionConfig void LoadDefaults() { + UseExternalIp = false; ReceptionistPort = SpatialConstants::DEFAULT_PORT; SetReceptionistHost(GetDefault()->DefaultReceptionistHost); } bool TryLoadCommandLineArgs() { - bool bSuccess = true; const TCHAR* CommandLine = FCommandLine::Get(); + // Get command line options first since the URL handling will modify the CommandLine string + uint16 Port; + bool bReceptionistPortParsed = FParse::Value(CommandLine, TEXT("receptionistPort"), Port); + FParse::Bool(CommandLine, *SpatialConstants::URL_USE_EXTERNAL_IP_FOR_BRIDGE_OPTION, UseExternalIp); + // Parse the command line for receptionistHost, if it exists then use this as the host IP. - if (!FParse::Value(CommandLine, TEXT("receptionistHost"), ReceptionistHost)) + FString Host; + 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. FString URLAddress; - FParse::Token(CommandLine, URLAddress, 0); - FRegexPattern Ipv4RegexPattern(TEXT("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$")); - FRegexMatcher IpV4RegexMatcher(Ipv4RegexPattern, *URLAddress); - bSuccess = IpV4RegexMatcher.FindNext(); - if (bSuccess) + FParse::Token(CommandLine, URLAddress, false /* UseEscape */); + const FURL URL(nullptr /* Base */, *URLAddress, TRAVEL_Absolute); + if (URL.Valid) { - SetReceptionistHost(URLAddress); + SetupFromURL(URL); } } + else + { + SetReceptionistHost(Host); + } + // If the ReceptionistPort was parsed in the command-line arguments, it would be overwritten by the URL setup above. + // So we restore/set it here. + if (bReceptionistPortParsed) + { + SetReceptionistPort(Port); + } - FParse::Value(CommandLine, TEXT("receptionistPort"), ReceptionistPort); - return bSuccess; + return true; } - void SetReceptionistHost(const FString& host) + void SetupFromURL(const FURL& URL) { - ReceptionistHost = host; - if (ReceptionistHost.Compare(SpatialConstants::LOCAL_HOST) != 0) + if (!URL.Host.IsEmpty()) + { + SetReceptionistHost(URL.Host); + SetReceptionistPort(URL.Port); + } + if (URL.HasOption(*SpatialConstants::URL_USE_EXTERNAL_IP_FOR_BRIDGE_OPTION)) { UseExternalIp = true; } @@ -210,8 +267,20 @@ class FReceptionistConfig : public FConnectionConfig FString GetReceptionistHost() const { return ReceptionistHost; } - uint16 ReceptionistPort; + uint16 GetReceptionistPort() const { return ReceptionistPort; } private: + void SetReceptionistHost(const FString& Host) + { + if (!Host.IsEmpty()) + { + ReceptionistHost = Host; + } + } + + void SetReceptionistPort(const uint16 Port) { ReceptionistPort = Port; } + FString ReceptionistHost; + + uint16 ReceptionistPort; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h index 9fdef3e471..45a5eed613 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h @@ -3,11 +3,12 @@ #pragma once #include "Containers/Queue.h" +#include "HAL/Event.h" #include "HAL/Runnable.h" #include "HAL/ThreadSafeBool.h" - -#include "Interop/Connection/SpatialOSWorkerInterface.h" #include "Interop/Connection/OutgoingMessages.h" +#include "Interop/Connection/SpatialOSWorkerInterface.h" +#include "Interop/Connection/WorkerConnectionCoordinator.h" #include "SpatialCommonTypes.h" #include "UObject/WeakObjectPtr.h" @@ -55,12 +56,13 @@ class SPATIALGDK_API USpatialWorkerConnection : public UObject, public FRunnable void QueueLatestOpList(); void ProcessOutgoingMessages(); + void MaybeFlush(); + void Flush(); private: void CacheWorkerAttributes(); // Begin FRunnable Interface - virtual bool Init() override; virtual uint32 Run() override; virtual void Stop() override; // End FRunnable Interface @@ -70,18 +72,19 @@ class SPATIALGDK_API USpatialWorkerConnection : public UObject, public FRunnable template void QueueOutgoingMessage(ArgsType&&... Args); -private: Worker_Connection* WorkerConnection; TArray CachedWorkerAttributes; FRunnableThread* OpsProcessingThread; FThreadSafeBool KeepRunning = true; - float OpsUpdateInterval; 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/WorkerConnectionCoordinator.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/WorkerConnectionCoordinator.h new file mode 100644 index 0000000000..1a645bf728 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/WorkerConnectionCoordinator.h @@ -0,0 +1,51 @@ +// 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/GlobalStateManager.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/GlobalStateManager.h index 994524eadf..8bf2e12d0d 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/GlobalStateManager.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/GlobalStateManager.h @@ -28,25 +28,20 @@ class SPATIALGDK_API UGlobalStateManager : public UObject public: void Init(USpatialNetDriver* InNetDriver); - void ApplySingletonManagerData(const Worker_ComponentData& Data); void ApplyDeploymentMapData(const Worker_ComponentData& Data); void ApplyStartupActorManagerData(const Worker_ComponentData& Data); - void ApplySingletonManagerUpdate(const Worker_ComponentUpdate& Update); void ApplyDeploymentMapUpdate(const Worker_ComponentUpdate& Update); void ApplyStartupActorManagerUpdate(const Worker_ComponentUpdate& Update); - bool IsSingletonEntity(Worker_EntityId EntityId) const; - void LinkAllExistingSingletonActors(); - void ExecuteInitialSingletonActorReplication(); - void UpdateSingletonEntityId(const FString& ClassName, const Worker_EntityId SingletonEntityId); - DECLARE_DELEGATE_OneParam(QueryDelegate, const Worker_EntityQueryResponseOp&); void QueryGSM(const QueryDelegate& Callback); bool GetAcceptingPlayersAndSessionIdFromQueryResponse(const Worker_EntityQueryResponseOp& Op, bool& OutAcceptingPlayers, int32& OutSessionId); - void ApplyVirtualWorkerMappingFromQueryResponse(const Worker_EntityQueryResponseOp& Op); + void ApplyVirtualWorkerMappingFromQueryResponse(const Worker_EntityQueryResponseOp& Op) const; void ApplyDeploymentMapDataFromQueryResponse(const Worker_EntityQueryResponseOp& Op); + void QueryTranslation(); + void SetDeploymentState(); void SetAcceptingPlayers(bool bAcceptingPlayers); void IncrementSessionID(); @@ -69,16 +64,8 @@ class SPATIALGDK_API UGlobalStateManager : public UObject bool IsReady() const; - USpatialActorChannel* AddSingleton(AActor* SingletonActor); - void RegisterSingletonChannel(AActor* SingletonActor, USpatialActorChannel* SingletonChannel); - void RemoveSingletonInstance(const AActor* SingletonActor); - void RemoveAllSingletons(); - Worker_EntityId GlobalStateManagerEntityId; - // Singleton Manager Component - StringToEntityMap SingletonNameToEntityId; - private: // Deployment Map Component FString DeploymentMapURL; @@ -102,10 +89,9 @@ class SPATIALGDK_API UGlobalStateManager : public UObject private: void SetDeploymentMapURL(const FString& MapURL); void SendSessionIdUpdate(); - void LinkExistingSingletonActor(const UClass* SingletonClass); void BecomeAuthoritativeOverAllActors(); - void BecomeAuthoritativeOverActorsBasedOnLBStrategy(); + void SetAllActorRolesBasedOnLBStrategy(); void SendCanBeginPlayUpdate(const bool bInCanBeginPlay); #if WITH_EDITOR @@ -128,5 +114,5 @@ class SPATIALGDK_API UGlobalStateManager : public UObject FDelegateHandle PrePIEEndedHandle; - TMap> SingletonClassPathToActorChannels; + bool bTranslationQueryInFlight; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h index 6a0bc64eaf..8f1006aeff 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h @@ -22,6 +22,7 @@ FORCEINLINE ESchemaComponentType GetGroupFromCondition(ELifetimeCondition Condit switch (Condition) { case COND_AutonomousOnly: + case COND_ReplayOrOwner: case COND_OwnerOnly: return SCHEMA_OwnerOnly; default: @@ -73,12 +74,8 @@ struct FClassInfo // Only for Subobject classes TArray> DynamicSubobjectInfo; - - FName ActorGroup; - FName WorkerType; }; -class SpatialActorGroupManager; class USpatialNetDriver; DECLARE_LOG_CATEGORY_EXTERN(LogSpatialClassInfoManager, Log, All) @@ -90,7 +87,7 @@ class SPATIALGDK_API USpatialClassInfoManager : public UObject public: - bool TryInit(USpatialNetDriver* InNetDriver, SpatialActorGroupManager* InActorGroupManager); + bool TryInit(USpatialNetDriver* InNetDriver); // Checks whether a class is supported and quits the game if not. This is to avoid crashing // when running with an out-of-date schema database. @@ -143,12 +140,12 @@ class SPATIALGDK_API USpatialClassInfoManager : public UObject void FinishConstructingActorClassInfo(const FString& ClassPath, TSharedRef& Info); void FinishConstructingSubobjectClassInfo(const FString& ClassPath, TSharedRef& Info); + bool ShouldTrackHandoverProperties() const; + private: UPROPERTY() USpatialNetDriver* NetDriver; - SpatialActorGroupManager* ActorGroupManager; - TMap, TSharedRef> ClassInfoMap; TMap> ComponentToClassInfoMap; TMap ComponentToOffsetMap; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialConditionMapFilter.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialConditionMapFilter.h index 4b715a5a94..a1fa82b204 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialConditionMapFilter.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialConditionMapFilter.h @@ -18,7 +18,7 @@ class FSpatialConditionMapFilter RepFlags.bReplay = 0; RepFlags.bNetInitial = 1; // The server will only ever send one update for bNetInitial, so just let them through here. RepFlags.bNetSimulated = ActorChannel->Actor->Role == ROLE_SimulatedProxy; - RepFlags.bNetOwner = bIsClient && ActorChannel->IsAuthoritativeClient(); + RepFlags.bNetOwner = bIsClient; #if ENGINE_MINOR_VERSION <= 23 RepFlags.bRepPhysics = ActorChannel->Actor->ReplicatedMovement.bRepPhysics; #else @@ -34,12 +34,8 @@ class FSpatialConditionMapFilter RepFlags.bRepPhysics); #endif - // Build a ConditionMap. This code is taken directly from FRepLayout::RebuildConditionalProperties -#if ENGINE_MINOR_VERSION <= 22 - static_assert(COND_Max == 14, "We are expecting 14 rep conditions"); // Guard in case more are added. -#else + // Build a ConditionMap. This code is taken directly from FRepLayout::BuildConditionMapFromRepFlags static_assert(COND_Max == 16, "We are expecting 16 rep conditions"); // Guard in case more are added. -#endif const bool bIsInitial = RepFlags.bNetInitial ? true : false; const bool bIsOwner = RepFlags.bNetOwner ? true : false; const bool bIsSimulated = RepFlags.bNetSimulated ? true : false; @@ -48,21 +44,24 @@ class FSpatialConditionMapFilter ConditionMap[COND_None] = true; ConditionMap[COND_InitialOnly] = bIsInitial; + ConditionMap[COND_OwnerOnly] = bIsOwner; - ConditionMap[COND_SkipOwner] = !bIsOwner; + 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; ConditionMap[COND_AutonomousOnly] = !bIsSimulated; + ConditionMap[COND_SimulatedOrPhysics] = bIsSimulated || bIsPhysics; ConditionMap[COND_SimulatedOrPhysicsNoReplay] = (bIsSimulated || bIsPhysics) && !bIsReplay; + ConditionMap[COND_InitialOrOwner] = bIsInitial || bIsOwner; ConditionMap[COND_ReplayOrOwner] = bIsReplay || bIsOwner; ConditionMap[COND_ReplayOnly] = bIsReplay; ConditionMap[COND_SkipReplay] = !bIsReplay; -#if ENGINE_MINOR_VERSION >= 23 - ConditionMap[COND_Never] = false; -#endif + ConditionMap[COND_Custom] = true; + ConditionMap[COND_Never] = false; } bool IsRelevant(ELifetimeCondition Condition) const diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialPlayerSpawner.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialPlayerSpawner.h index 233622db27..88630ef9c0 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialPlayerSpawner.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialPlayerSpawner.h @@ -19,6 +19,8 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialPlayerSpawner, Log, All); class FTimerManager; class USpatialNetDriver; +DECLARE_DELEGATE_OneParam(FOnPlayerSpawnFailed, const FString&); + UCLASS() class SPATIALGDK_API USpatialPlayerSpawner : public UObject { @@ -32,6 +34,8 @@ class SPATIALGDK_API USpatialPlayerSpawner : public UObject void SendPlayerSpawnRequest(); void ReceivePlayerSpawnResponseOnClient(const Worker_CommandResponseOp& Op); + FOnPlayerSpawnFailed OnPlayerSpawnFailed; + // Authoritative server worker void ReceivePlayerSpawnRequestOnServer(const Worker_CommandRequestOp& Op); void ReceiveForwardPlayerSpawnResponse(const Worker_CommandResponseOp& Op); @@ -57,11 +61,11 @@ class SPATIALGDK_API USpatialPlayerSpawner : public UObject // Authoritative server worker void FindPlayerStartAndProcessPlayerSpawn(Schema_Object* Request, const PhysicalWorkerName& ClientWorkerId); - bool ForwardSpawnRequestToStrategizedServer(const Schema_Object* OriginalPlayerSpawnRequest, AActor* PlayerStart, 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); // Any server - void PassSpawnRequestToNetDriver(Schema_Object* PlayerSpawnData, AActor* PlayerStart); + void PassSpawnRequestToNetDriver(const Schema_Object* PlayerSpawnData, AActor* PlayerStart); UPROPERTY() USpatialNetDriver* NetDriver; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialRPCService.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialRPCService.h index 7888ab6da5..24cdcbcf3e 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialRPCService.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialRPCService.h @@ -13,6 +13,7 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialRPCService, Log, All); +class USpatialLatencyTracer; class USpatialStaticComponentView; struct RPCRingBuffer; @@ -49,15 +50,16 @@ enum class EPushRPCResult : uint8 QueueOverflowed, DropOverflowed, HasAckAuthority, - NoRingBufferAuthority + NoRingBufferAuthority, + EntityBeingCreated }; class SPATIALGDK_API SpatialRPCService { public: - SpatialRPCService(ExtractRPCDelegate ExtractRPCCallback, const USpatialStaticComponentView* View); + SpatialRPCService(ExtractRPCDelegate ExtractRPCCallback, const USpatialStaticComponentView* View, USpatialLatencyTracer* SpatialLatencyTracer); - EPushRPCResult PushRPC(Worker_EntityId EntityId, ERPCType Type, RPCPayload Payload); + EPushRPCResult PushRPC(Worker_EntityId EntityId, ERPCType Type, RPCPayload Payload, bool bCreatedEntity); void PushOverflowedRPCs(); struct UpdateToSend @@ -66,7 +68,7 @@ class SPATIALGDK_API SpatialRPCService FWorkerComponentUpdate Update; }; TArray GetRPCsAndAcksToSend(); - TArray GetRPCComponentsOnEntityCreation(Worker_EntityId EntityId); + TArray GetRPCComponentsOnEntityCreation(Worker_EntityId EntityId); // Will also store acked IDs locally. // Calls ExtractRPCCallback for each RPC it extracts from a given component. If the callback returns false, @@ -84,7 +86,7 @@ class SPATIALGDK_API SpatialRPCService // 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); + EPushRPCResult PushRPCInternal(Worker_EntityId EntityId, ERPCType Type, RPCPayload&& Payload, bool bCreatedEntity); void ExtractRPCsForType(Worker_EntityId EntityId, ERPCType Type); @@ -99,6 +101,7 @@ class SPATIALGDK_API SpatialRPCService private: ExtractRPCDelegate ExtractRPCCallback; const USpatialStaticComponentView* View; + USpatialLatencyTracer* SpatialLatencyTracer; // This is local, not written into schema. TMap LastSeenMulticastRPCIds; @@ -111,6 +114,11 @@ class SPATIALGDK_API SpatialRPCService 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 8a23a34336..c71619f445 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialReceiver.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialReceiver.h @@ -37,6 +37,13 @@ struct PendingAddComponentWrapper 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. + bool operator==(const PendingAddComponentWrapper& Other) const + { + return EntityId == Other.EntityId && ComponentId == Other.ComponentId; + } + Worker_EntityId EntityId; Worker_ComponentId ComponentId; TUniquePtr Data; @@ -93,6 +100,7 @@ class USpatialReceiver : public UObject, public SpatialOSDispatcherInterface void CleanupRepStateMap(FSpatialObjectRepState& Replicator); void MoveMappedObjectToUnmapped(const FUnrealObjectRef&); + void RetireWhenAuthoritive(Worker_EntityId EntityId, Worker_ComponentId ActorClassId, bool bIsNetStartup, bool bNeedsTearOff); private: void EnterCriticalSection(); void LeaveCriticalSection(); @@ -117,9 +125,8 @@ class USpatialReceiver : public UObject, public SpatialOSDispatcherInterface void ApplyComponentDataOnActorCreation(Worker_EntityId EntityId, const Worker_ComponentData& Data, USpatialActorChannel& Channel, const FClassInfo& ActorClassInfo, TArray& OutObjectsToResolve); void ApplyComponentData(USpatialActorChannel& Channel, UObject& TargetObject, const Worker_ComponentData& Data); - - // This is called for AddComponentOps not in a critical section, which means they are not a part of the initial entity creation. - void HandleIndividualAddComponent(const Worker_AddComponentOp& Op); + + void 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); @@ -141,8 +148,6 @@ class USpatialReceiver : public UObject, public SpatialOSDispatcherInterface void UpdateShadowData(Worker_EntityId EntityId); TWeakObjectPtr PopPendingActorRequest(Worker_RequestId RequestId); - AActor* FindSingletonActor(UClass* SingletonClass); - void OnHeartbeatComponentUpdate(const Worker_ComponentUpdateOp& Op); void CloseClientConnection(USpatialNetConnection* ClientConnection, Worker_EntityId PlayerControllerEntityId); @@ -258,4 +263,16 @@ class USpatialReceiver : public UObject, public SpatialOSDispatcherInterface TMap EntitiesWaitingForAsyncLoad; TMap> AsyncLoadingPackages; // END TODO + + struct DeferredRetire + { + Worker_EntityId EntityId; + Worker_ComponentId ActorClassId; + bool bIsNetStartupActor; + bool bNeedsTearOff; + }; + TArray EntitiesToRetireOnAuthorityGain; + bool HasEntityBeenRequestedForDelete(Worker_EntityId EntityId); + void HandleDeferredEntityDeletion(const DeferredRetire& Retire); + void HandleEntityDeletedAuthority(Worker_EntityId EntityId); }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSender.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSender.h index 42b92ab5da..f443d7300b 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSender.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSender.h @@ -27,7 +27,6 @@ class USpatialPackageMapClient; class USpatialReceiver; class USpatialStaticComponentView; class USpatialClassInfoManager; -class SpatialActorGroupManager; class USpatialWorkerConnection; struct FReliableRPCForRetry @@ -72,13 +71,14 @@ class SPATIALGDK_API USpatialSender : public UObject // Actor Updates void SendComponentUpdates(UObject* Object, const FClassInfo& Info, USpatialActorChannel* Channel, const FRepChangeState* RepChanges, const FHandoverChangeState* HandoverChanges, uint32& OutBytesWritten); - void SendComponentInterestForActor(USpatialActorChannel* Channel, Worker_EntityId EntityId, bool bNetOwned); - void SendComponentInterestForSubobject(const FClassInfo& Info, Worker_EntityId EntityId, bool bNetOwned); void SendPositionUpdate(Worker_EntityId EntityId, const FVector& Location); void SendAuthorityIntentUpdate(const AActor& Actor, VirtualWorkerId NewAuthoritativeVirtualWorkerId); void SetAclWriteAuthority(const SpatialLoadBalanceEnforcer::AclWriteAuthorityRequest& Request); FRPCErrorInfo SendRPC(const FPendingRPCParams& Params); - ERPCResult SendRPCInternal(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload); + 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); @@ -87,9 +87,10 @@ class SPATIALGDK_API USpatialSender : public UObject 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 SendActorTornOffUpdate(Worker_EntityId EntityId, Worker_ComponentId ComponentId); void SendCreateEntityRequest(USpatialActorChannel* Channel, uint32& OutBytesWritten); - void RetireEntity(const Worker_EntityId EntityId); + void RetireEntity(const Worker_EntityId EntityId, bool bIsNetStartupActor); // Creates an entity containing just a tombstone component and the minimal data to resolve an actor. void CreateTombstoneEntity(AActor* Actor); @@ -119,7 +120,9 @@ class SPATIALGDK_API USpatialSender : public UObject void GainAuthorityThenAddComponent(USpatialActorChannel* Channel, UObject* Object, const FClassInfo* Info); // Creates an entity authoritative on this server worker, ensuring it will be able to receive updates for the GSM. - void CreateServerWorkerEntity(int AttemptCounter = 1); + UFUNCTION() + void CreateServerWorkerEntity(); + void RetryServerWorkerEntityCreation(Worker_EntityId EntityId, int AttemptCounte); void UpdateServerWorkerEntityInterestAndPosition(); void ClearPendingRPCs(const Worker_EntityId EntityId); @@ -141,6 +144,8 @@ class SPATIALGDK_API USpatialSender : public UObject void AddTombstoneToEntity(const Worker_EntityId EntityId); + void PeriodicallyProcessOutgoingRPCs(); + // RPC Construction FSpatialNetBitWriter PackRPCDataToSpatialNetBitWriter(UFunction* Function, void* Parameters) const; @@ -148,8 +153,6 @@ class SPATIALGDK_API USpatialSender : public UObject Worker_CommandRequest CreateRetryRPCCommandRequest(const FReliableRPCForRetry& RPC, uint32 TargetObjectOffset); FWorkerComponentUpdate CreateRPCEventUpdate(UObject* TargetObject, const SpatialGDK::RPCPayload& Payload, Worker_ComponentId ComponentId, Schema_FieldId EventIndext); - TArray CreateComponentInterestForActor(USpatialActorChannel* Channel, bool bIsNetOwned); - // RPC Tracking #if !UE_BUILD_SHIPPING void TrackRPC(AActor* Actor, UFunction* Function, const SpatialGDK::RPCPayload& Payload, const ERPCType RPCType); @@ -176,8 +179,6 @@ class SPATIALGDK_API USpatialSender : public UObject UPROPERTY() USpatialClassInfoManager* ClassInfoManager; - SpatialActorGroupManager* ActorGroupManager; - FTimerManager* TimerManager; SpatialGDK::SpatialRPCService* RPCService; diff --git a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLBStrategy.h b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLBStrategy.h index 5d3e2ee537..b311abb343 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLBStrategy.h +++ b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/AbstractLBStrategy.h @@ -36,8 +36,10 @@ class SPATIALGDK_API UAbstractLBStrategy : public UObject bool IsReady() const { return LocalVirtualWorkerId != SpatialConstants::INVALID_VIRTUAL_WORKER_ID; } - void SetLocalVirtualWorkerId(VirtualWorkerId LocalVirtualWorkerId); + VirtualWorkerId GetLocalVirtualWorkerId() const { return LocalVirtualWorkerId; }; + virtual void SetLocalVirtualWorkerId(VirtualWorkerId LocalVirtualWorkerId); + // Deprecated: will be removed ASAP. virtual TSet GetVirtualWorkerIds() const PURE_VIRTUAL(UAbstractLBStrategy::GetVirtualWorkerIds, return {};) virtual bool ShouldHaveAuthority(const AActor& Actor) const { return false; } @@ -48,11 +50,21 @@ class SPATIALGDK_API UAbstractLBStrategy : public UObject */ 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;) + /** * 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. + */ + virtual uint32 GetMinimumRequiredWorkers() const PURE_VIRTUAL(UAbstractLBStrategy::GetMinimumRequiredWorkers, return 0;) + virtual void SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtualWorkerId, const VirtualWorkerId& LastVirtualWorkerId) PURE_VIRTUAL(UAbstractLBStrategy::SetVirtualWorkerIds, return;) + protected: VirtualWorkerId LocalVirtualWorkerId; diff --git a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GridBasedLBStrategy.h b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GridBasedLBStrategy.h index f15974764c..49e62ec0f5 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GridBasedLBStrategy.h +++ b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/GridBasedLBStrategy.h @@ -38,6 +38,7 @@ class SPATIALGDK_API UGridBasedLBStrategy : public UAbstractLBStrategy /* 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; @@ -45,7 +46,12 @@ class SPATIALGDK_API UGridBasedLBStrategy : public UAbstractLBStrategy virtual SpatialGDK::QueryConstraint GetWorkerInterestQueryConstraint() const override; + virtual bool RequiresHandoverData() const override { return Rows * Cols > 1; } + virtual FVector GetWorkerEntityPosition() const override; + + virtual uint32 GetMinimumRequiredWorkers() const override; + virtual void SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtualWorkerId, const VirtualWorkerId& LastVirtualWorkerId) override; /* End UAbstractLBStrategy Interface */ LBStrategyRegions GetLBStrategyRegions() const; @@ -71,6 +77,8 @@ class SPATIALGDK_API UGridBasedLBStrategy : public UAbstractLBStrategy TArray VirtualWorkerIds; TArray WorkerCells; + uint32 LocalCellId; + bool bIsStrategyUsedOnLocalWorker; static bool IsInside(const FBox2D& Box, const FVector2D& Location); }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/LayeredLBStrategy.h b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/LayeredLBStrategy.h new file mode 100644 index 0000000000..6c1528fb1c --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/LoadBalancing/LayeredLBStrategy.h @@ -0,0 +1,102 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "LoadBalancing/AbstractLBStrategy.h" + +#include "CoreMinimal.h" +#include "Math/Box2D.h" +#include "Math/Vector2D.h" + +#include "LayeredLBStrategy.generated.h" + +class SpatialVirtualWorkerTranslator; +class UAbstractLockingPolicy; + +DECLARE_LOG_CATEGORY_EXTERN(LogLayeredLBStrategy, Log, All) + +USTRUCT() +struct FLBLayerInfo +{ + GENERATED_BODY() + + FLBLayerInfo() : Name(NAME_None) + { + } + + UPROPERTY() + FName Name; + + UPROPERTY(EditAnywhere, Category = "Load Balancing") + TSubclassOf LoadBalanceStrategy; + + UPROPERTY(EditAnywhere, Category = "Load Balancing") + TSubclassOf LockingPolicy; +}; + +/** + * A load balancing strategy that wraps multiple LBStrategies. The user can define "Layers" of work, which are + * specified by sets of classes, and a LBStrategy for each Layer. This class will then allocate virtual workers + * to each Layer/Strategy and keep track of which Actors belong in which layer and should be load balanced + * by the corresponding Strategy. + */ +UCLASS() +class SPATIALGDK_API ULayeredLBStrategy : public UAbstractLBStrategy +{ + GENERATED_BODY() + +public: + ULayeredLBStrategy(); + + /* 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 override; + + virtual bool RequiresHandoverData() const override { return GetMinimumRequiredWorkers() > 1; } + + virtual FVector GetWorkerEntityPosition() const override; + + virtual uint32 GetMinimumRequiredWorkers() const override; + virtual void SetVirtualWorkerIds(const VirtualWorkerId& FirstVirtualWorkerId, const VirtualWorkerId& LastVirtualWorkerId) 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; + +private: + TArray VirtualWorkerIds; + + mutable TMap, FName> ClassPathToLayer; + + TMap VirtualWorkerIdToLayerName; + + UPROPERTY() + TMap LayerNameToLBStrategy; + + // Returns the name of the first Layer that contains this, or a parent of this class, + // or the default actor group, if no mapping is found. + FName GetLayerNameForClass(TSubclassOf Class) const; + + // Returns true if ActorA and ActorB are contained in Layers that are + // on the same Server worker type. + bool IsSameWorkerType(const AActor* ActorA, const AActor* ActorB) const; + + // Returns the name of the Layer this Actor belongs to. + FName GetLayerNameForActor(const AActor& Actor) const; + + // Add a LBStrategy to our map and do bookkeeping around it. + void AddStrategyForLayer(const FName& LayerName, UAbstractLBStrategy* LBStrategy); +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/ComponentPresence.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/ComponentPresence.h index 0413518353..e5c180d9d1 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/ComponentPresence.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/ComponentPresence.h @@ -8,6 +8,7 @@ #include "Utils/SchemaUtils.h" #include "Containers/Array.h" +#include "HAL/UnrealMemory.h" #include "Templates/UnrealTemplate.h" #include @@ -43,10 +44,11 @@ struct ComponentPresence : Component Data.schema_type = Schema_CreateComponentData(); Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); - for (const Worker_ComponentId& InComponentId : ComponentList) - { - Schema_AddUint32(ComponentObject, SpatialConstants::COMPONENT_PRESENCE_COMPONENT_LIST_ID, InComponentId); - } + 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; } @@ -63,8 +65,11 @@ struct ComponentPresence : Component Update.schema_type = Schema_CreateComponentUpdate(); Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); - Schema_AddUint32List(ComponentObject, SpatialConstants::COMPONENT_PRESENCE_COMPONENT_LIST_ID, - ComponentList.GetData(), ComponentList.Num()); + 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; } diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/Interest.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/Interest.h index 121e02eb13..813127ac78 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/Interest.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/Interest.h @@ -7,7 +7,7 @@ namespace SpatialGDK { using EdgeLength = Coordinates; -using ResultType = TArray; +using SchemaResultType = TArray; struct SphereConstraint { @@ -112,7 +112,7 @@ struct Query // Either full_snapshot_result or a list of result_component_id should be provided. Providing both is invalid. TSchemaOption FullSnapshotResult; // Whether all components should be included or none. - ResultType ResultComponentIds; // Which components should be included. + SchemaResultType ResultComponentIds; // Which components should be included. // Used for frequency-based rate limiting. Represents the maximum frequency of updates for this // particular query. An empty option represents no rate-limiting (ie. updates are received diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/Singleton.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/Singleton.h deleted file mode 100644 index 4e42583550..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/Singleton.h +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#pragma once - -#include "Schema/Component.h" -#include "SpatialConstants.h" - -#include -#include - -namespace SpatialGDK -{ - -struct Singleton : Component -{ - static const Worker_ComponentId ComponentId = SpatialConstants::SINGLETON_COMPONENT_ID; - - Singleton() = default; - Singleton(const Worker_ComponentData& Data) - { - } - - FORCEINLINE Worker_ComponentData CreateSingletonData() - { - Worker_ComponentData Data = {}; - Data.component_id = ComponentId; - Data.schema_type = Schema_CreateComponentData(); - - return Data; - } -}; - -} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h index a1ac848edd..66a33e9e52 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h @@ -58,7 +58,7 @@ struct SPATIALGDK_API FUnrealObjectRef ((!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. - bUseSingletonClassPath == Other.bUseSingletonClassPath; + bUseClassPathToLoadObject == Other.bUseClassPathToLoadObject; } FORCEINLINE bool operator!=(const FUnrealObjectRef& Other) const @@ -75,7 +75,9 @@ struct SPATIALGDK_API FUnrealObjectRef static FSoftObjectPath ToSoftObjectPath(const FUnrealObjectRef& ObjectRef); static FUnrealObjectRef FromObjectPtr(UObject* ObjectValue, USpatialPackageMapClient* PackageMap); static FUnrealObjectRef FromSoftObjectPath(const FSoftObjectPath& ObjectPath); - static FUnrealObjectRef GetSingletonClassRef(UObject* SingletonObject, USpatialPackageMapClient* PackageMap); + static FUnrealObjectRef GetRefFromObjectClassPath(UObject* Object, USpatialPackageMapClient* PackageMap); + static bool ShouldLoadObjectFromClassPath(UObject* Object); + static bool IsUniqueActorClass(UClass* Class); static const FUnrealObjectRef NULL_OBJECT_REF; static const FUnrealObjectRef UNRESOLVED_OBJECT_REF; @@ -85,7 +87,12 @@ struct SPATIALGDK_API FUnrealObjectRef SpatialGDK::TSchemaOption Path; SpatialGDK::TSchemaOption Outer; bool bNoLoadOnClient = false; - bool bUseSingletonClassPath = false; + // If this field is set to true, we are saying that the Actor will exist at most once on the given worker. + // In addition, if we receive information for an Actor of this class over the network, then this data + // should be applied to the Actor we've already spawned (where another worker created the entity). This + // information is important for the object ref, since it means we can identify the correct Actor to apply + // the replicated data to on each worker via the class path (since only 1 Actor should exist for this class). + bool bUseClassPathToLoadObject = false; }; inline uint32 GetTypeHash(const FUnrealObjectRef& ObjectRef) @@ -96,7 +103,7 @@ inline uint32 GetTypeHash(const FUnrealObjectRef& ObjectRef) Result = (Result * 977u) + GetTypeHash(ObjectRef.Path); Result = (Result * 977u) + GetTypeHash(ObjectRef.Outer); // Intentionally don't hash bNoLoadOnClient. - Result = (Result * 977u) + GetTypeHash(ObjectRef.bUseSingletonClassPath ? 1 : 0); + Result = (Result * 977u) + GetTypeHash(ObjectRef.bUseClassPathToLoadObject ? 1 : 0); return Result; } diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h index 8a675b9f72..d378a46682 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h @@ -88,9 +88,7 @@ const Worker_ComponentId MAX_RESERVED_SPATIAL_SYSTEM_COMPONENT_ID = 100; const Worker_ComponentId SPAWN_DATA_COMPONENT_ID = 9999; const Worker_ComponentId PLAYER_SPAWNER_COMPONENT_ID = 9998; -const Worker_ComponentId SINGLETON_COMPONENT_ID = 9997; const Worker_ComponentId UNREAL_METADATA_COMPONENT_ID = 9996; -const Worker_ComponentId SINGLETON_MANAGER_COMPONENT_ID = 9995; const Worker_ComponentId DEPLOYMENT_MAP_COMPONENT_ID = 9994; const Worker_ComponentId STARTUP_ACTOR_MANAGER_COMPONENT_ID = 9993; const Worker_ComponentId GSM_SHUTDOWN_COMPONENT_ID = 9992; @@ -121,8 +119,6 @@ const Worker_ComponentId NET_OWNING_CLIENT_WORKER_COMPONENT_ID = 9971; const Worker_ComponentId STARTING_GENERATED_COMPONENT_ID = 10000; -const Schema_FieldId SINGLETON_MANAGER_SINGLETON_NAME_TO_ENTITY_ID = 1; - const Schema_FieldId DEPLOYMENT_MAP_MAP_URL_ID = 1; const Schema_FieldId DEPLOYMENT_MAP_ACCEPTING_PLAYERS_ID = 2; const Schema_FieldId DEPLOYMENT_MAP_SESSION_ID = 3; @@ -156,7 +152,7 @@ const Schema_FieldId UNREAL_OBJECT_REF_OFFSET_ID = 2; const Schema_FieldId UNREAL_OBJECT_REF_PATH_ID = 3; const Schema_FieldId UNREAL_OBJECT_REF_NO_LOAD_ON_CLIENT_ID = 4; const Schema_FieldId UNREAL_OBJECT_REF_OUTER_ID = 5; -const Schema_FieldId UNREAL_OBJECT_REF_USE_SINGLETON_CLASS_PATH_ID = 6; +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; @@ -230,12 +226,12 @@ const float FIRST_COMMAND_RETRY_WAIT_SECONDS = 0.2f; const uint32 MAX_NUMBER_COMMAND_ATTEMPTS = 5u; const float FORWARD_PLAYER_SPAWN_COMMAND_WAIT_SECONDS = 0.2f; -const FName DefaultActorGroup = FName(TEXT("Default")); - const VirtualWorkerId INVALID_VIRTUAL_WORKER_ID = 0; const ActorLockToken INVALID_ACTOR_LOCK_TOKEN = 0; const FString INVALID_WORKER_NAME = TEXT(""); +static const FName DefaultLayer = FName(TEXT("UnrealWorker")); + const WorkerAttributeSet UnrealServerAttributeSet = TArray{DefaultServerWorkerType.ToString()}; const WorkerAttributeSet UnrealClientAttributeSet = TArray{DefaultClientWorkerType.ToString()}; @@ -244,15 +240,22 @@ 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_CN = TEXT("locator.spatialoschina.com"); const uint16 LOCATOR_PORT = 443; -const FString AssemblyPattern = TEXT("^[a-zA-Z0-9_.-]{5,64}$"); -const FString ProjectPattern = TEXT("^[a-z0-9_]{3,32}$"); -const FString DeploymentPattern = TEXT("^[a-z0-9_]{2,32}$"); +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 FString AssemblyPatternHint = TEXT("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 FString ProjectPatternHint = TEXT("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 FString DeploymentPatternHint = TEXT("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) { @@ -280,12 +283,15 @@ const FString URL_TARGET_DEPLOYMENT_OPTION = TEXT("deployment="); const FString URL_PLAYER_ID_OPTION = TEXT("playerid="); const FString URL_DISPLAY_NAME_OPTION = TEXT("displayname="); const FString URL_METADATA_OPTION = TEXT("metadata="); +const FString URL_USE_EXTERNAL_IP_FOR_BRIDGE_OPTION = TEXT("useExternalIpForBridge"); const FString DEVELOPMENT_AUTH_PLAYER_ID = TEXT("Player Id"); const FString SCHEMA_DATABASE_FILE_PATH = TEXT("Spatial/SchemaDatabase"); const FString SCHEMA_DATABASE_ASSET_PATH = TEXT("/Game/Spatial/SchemaDatabase"); +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 { @@ -301,7 +307,6 @@ const TArray REQUIRED_COMPONENTS_FOR_NON_AUTH_CLIENT_INTERES NETMULTICAST_RPCS_COMPONENT_ID_LEGACY, // Global state components - SINGLETON_MANAGER_COMPONENT_ID, DEPLOYMENT_MAP_COMPONENT_ID, STARTUP_ACTOR_MANAGER_COMPONENT_ID, GSM_SHUTDOWN_COMPONENT_ID, @@ -335,7 +340,6 @@ const TArray REQUIRED_COMPONENTS_FOR_NON_AUTH_SERVER_INTERES NETMULTICAST_RPCS_COMPONENT_ID_LEGACY, // Global state components - SINGLETON_MANAGER_COMPONENT_ID, DEPLOYMENT_MAP_COMPONENT_ID, STARTUP_ACTOR_MANAGER_COMPONENT_ID, GSM_SHUTDOWN_COMPONENT_ID, @@ -388,15 +392,6 @@ inline Worker_ComponentId GetClientAuthorityComponent(bool bUsingRingBuffers) return bUsingRingBuffers ? CLIENT_ENDPOINT_COMPONENT_ID : CLIENT_RPC_ENDPOINT_COMPONENT_ID_LEGACY; } -inline WorkerAttributeSet GetLoadBalancerAttributeSet(FName LoadBalancingWorkerType) -{ - if (LoadBalancingWorkerType == "") - { - return { DefaultServerWorkerType.ToString() }; - } - return { LoadBalancingWorkerType.ToString() }; -} - } // ::SpatialConstants DECLARE_STATS_GROUP(TEXT("SpatialNet"), STATGROUP_SpatialNet, STATCAT_Advanced); diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h index e6229be567..1eab4bddbe 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h @@ -2,8 +2,6 @@ #pragma once -#include "Utils/SpatialActorGroupManager.h" - #include "CoreMinimal.h" #include "Engine/EngineTypes.h" #include "Misc/Paths.h" @@ -66,24 +64,24 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject #if WITH_EDITOR virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override; #endif - + virtual void PostInitProperties() override; - /** + /** * 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 + * 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 + /** + * 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. */ UPROPERTY(EditAnywhere, config, Category = "Entity Pool", meta = (DisplayName = "Refresh Count")) @@ -93,9 +91,9 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject 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; @@ -115,7 +113,7 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject 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. @@ -138,7 +136,7 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (DisplayName = "SpatialOS Network Update Rate")) float OpsUpdateRate; - /** Replicate handover properties between servers, required for zoned worker deployments.*/ + /** 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; @@ -153,9 +151,9 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (DisplayName = "Wait Time Before Processing Received RPC With Unresolved Refs")) float QueuedIncomingRPCWaitTime; - /** Seconds to wait before dropping an outgoing RPC.*/ - UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (DisplayName = "Wait Time Before Dropping Outgoing RPC")) - float QueuedOutgoingRPCWaitTime; + /** 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.*/ UPROPERTY(EditAnywhere, config, Category = "SpatialOS Position Updates") @@ -177,9 +175,9 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject UPROPERTY(EditAnywhere, config, Category = "Metrics", meta = (DisplayName = "Metrics Report Rate (seconds)")) 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. + /** + * 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") @@ -193,13 +191,6 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject UPROPERTY(EditAnywhere, config, Category = "Schema Generation", meta = (DisplayName = "Maximum Dynamically Attached Subobjects Per Class")) uint32 MaxDynamicallyAttachedSubobjectsPerClass; - /** - * Adds granular result types for queries. - * Granular here means specifically the required Unreal components for spawning other actors and all data type components. - */ - UPROPERTY(config) - bool bEnableResultTypes; - /** The receptionist host to use if no 'receptionistHost' argument is passed to the command line. */ UPROPERTY(EditAnywhere, config, Category = "Local Connection") FString DefaultReceptionistHost; @@ -211,27 +202,11 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject public: - bool GetPreventClientCloudDeploymentAutoConnect(bool bIsClient) const; + bool GetPreventClientCloudDeploymentAutoConnect() const; UPROPERTY(EditAnywhere, Config, Category = "Region settings", meta = (ConfigRestartRequired = true, DisplayName = "Region where services are located")) TEnumAsByte ServicesRegion; - /** Single server worker type to launch when offloading is disabled, fallback server worker type when offloading is enabled (owns all actor classes by default). */ - UPROPERTY(EditAnywhere, Config, Category = "Offloading") - FWorkerType DefaultWorkerType; - - /** Enable running different server worker types to split the simulation by Actor Groups. Can be overridden with command line argument OverrideSpatialOffloading. */ - UPROPERTY(EditAnywhere, Config, Category = "Offloading") - bool bEnableOffloading; - - /** Actor Group configuration. */ - UPROPERTY(EditAnywhere, Config, Category = "Offloading", meta = (EditCondition = "bEnableOffloading")) - TMap ActorGroups; - - /** Available server worker types. */ - UPROPERTY(Config) - TSet ServerWorkerTypes; - /** Controls the verbosity of worker logs which are sent to SpatialOS. These logs will appear in the Spatial Output and launch.log */ UPROPERTY(EditAnywhere, config, Category = "Logging", meta = (DisplayName = "Worker Log Level")) TEnumAsByte WorkerLogLevel; @@ -239,14 +214,6 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject UPROPERTY(EditAnywhere, config, Category = "Debug", meta = (MetaClass = "SpatialDebugger")) TSubclassOf SpatialDebugger; - /** EXPERIMENTAL: Disable runtime load balancing and use a worker to do it instead. */ - UPROPERTY(EditAnywhere, Config, Category = "Load Balancing") - bool bEnableUnrealLoadBalancer; - - /** EXPERIMENTAL: Worker type to assign for load balancing. */ - UPROPERTY(EditAnywhere, Config, Category = "Load Balancing", meta = (EditCondition = "bEnableUnrealLoadBalancer")) - FWorkerType LoadBalancingWorkerType; - /** EXPERIMENTAL: Run SpatialWorkerConnection on Game Thread. */ UPROPERTY(Config) bool bRunSpatialWorkerConnectionOnGameThread; @@ -257,6 +224,8 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject private: #if WITH_EDITOR bool CanEditChange(const UProperty* InProperty) const override; + + void UpdateServicesRegionFile(); #endif UPROPERTY(EditAnywhere, Config, Category = "Replication", meta = (DisplayName = "Use RPC Ring Buffers")) @@ -282,22 +251,18 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject UPROPERTY(Config) bool bTcpNoDelay; - /** Only valid on Udp connections - specifies server upstream flush interval - see c_worker.h */ - UPROPERTY(Config) - uint32 UdpServerUpstreamUpdateIntervalMS; - /** Only valid on Udp connections - specifies server downstream flush interval - see c_worker.h */ UPROPERTY(Config) uint32 UdpServerDownstreamUpdateIntervalMS; - /** Only valid on Udp connections - specifies client upstream flush interval - see c_worker.h */ - UPROPERTY(Config) - uint32 UdpClientUpstreamUpdateIntervalMS; - /** Only valid on Udp connections - specifies client downstream flush interval - see c_worker.h */ UPROPERTY(Config) uint32 UdpClientDownstreamUpdateIntervalMS; + /** Will flush worker messages immediately after every RPC. Higher bandwidth but lower latency on RPC calls. */ + UPROPERTY(Config) + bool bWorkerFlushAfterOutgoingNetworkOp; + /** Do async loading for new classes when checking out entities. */ UPROPERTY(Config) bool bAsyncLoadNewClassesOnEntityCheckout; @@ -310,6 +275,8 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject FORCEINLINE bool IsRunningInChina() const { return ServicesRegion == EServicesRegion::CN; } + void SetServicesRegion(EServicesRegion::Type NewRegion); + /** Enable to use the new net cull distance component tagging form of interest */ UPROPERTY(EditAnywhere, Config, Category = "Interest") bool bEnableNetCullDistanceInterest; @@ -326,12 +293,12 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject UPROPERTY(EditAnywhere, Config, Category = "Interest", meta = (EditCondition = "bEnableNetCullDistanceFrequency")) TArray InterestRangeFrequencyPairs; - /** Use TLS encryption for UnrealClient workers connection. May impact performance. */ - UPROPERTY(EditAnywhere, Config, Category = "Connection") + /** Use TLS encryption for UnrealClient workers connection. May impact performance. Only works in non-editor builds. */ + UPROPERTY(EditAnywhere, Config, Category = "Connection", meta = (DisplayName = "Use Secure Client Connection In Packaged Builds")) bool bUseSecureClientConnection; - /** Use TLS encryption for UnrealWorker (server) workers connection. May impact performance. */ - UPROPERTY(EditAnywhere, Config, Category = "Connection") + /** Use TLS encryption for UnrealWorker (server) workers connection. May impact performance. Only works in non-editor builds. */ + UPROPERTY(EditAnywhere, Config, Category = "Connection", meta = (DisplayName = "Use Secure Server Connection In Packaged Builds")) bool bUseSecureServerConnection; /** @@ -346,9 +313,15 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject UPROPERTY(Config) bool bUseSpatialView; -public: - // UI Hidden settings passed through from SpatialGDKEditorSettings - bool bUseDevelopmentAuthenticationFlow; - FString DevelopmentAuthenticationToken; - FString DevelopmentDeploymentToConnect; + /** + * 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; + + /** + * This will enable warning messages for ActorSpawning that could be legitimate but is likely to be an error. + */ + UPROPERTY(Config) + bool bEnableMultiWorkerDebuggingWarnings; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandMessages.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandMessages.h index 0efa2cf885..3078fc1aad 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandMessages.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/CommandMessages.h @@ -1,6 +1,7 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #pragma once + #include "Misc/Optional.h" #include "Containers/UnrealString.h" #include diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ComponentData.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ComponentData.h new file mode 100644 index 0000000000..366feeabd7 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ComponentData.h @@ -0,0 +1,61 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Templates/UniquePtr.h" +#include +#include + +namespace SpatialGDK +{ + +class ComponentUpdate; + +struct ComponentDataDeleter +{ + void operator()(Schema_ComponentData* ComponentData) const noexcept; +}; + +using OwningComponentDataPtr = TUniquePtr; + +// An RAII wrapper for component data. +class ComponentData +{ +public: + // Creates a new component data. + explicit ComponentData(Worker_ComponentId Id); + // Takes ownership of component data. + explicit ComponentData(OwningComponentDataPtr Data, Worker_ComponentId Id); + + ~ComponentData() = default; + + // Moveable, not copyable. + ComponentData(const ComponentData&) = delete; + ComponentData(ComponentData&&) = default; + ComponentData& operator=(const ComponentData&) = delete; + ComponentData& operator=(ComponentData&&) = default; + + static ComponentData CreateCopy(const Schema_ComponentData* Data, Worker_ComponentId Id); + + // Creates a copy of the component data. + ComponentData DeepCopy() const; + // Releases ownership of the component data. + Schema_ComponentData* Release() &&; + + // Appends the fields from the provided update. + // Returns true if the update was successfully applied and false otherwise. + // This will cause the size of the component data to increase. + // To resize use DeepCopy to create a new data object with the serialized size of the data. + bool ApplyUpdate(const ComponentUpdate& Update); + + Schema_Object* GetFields() const; + + Schema_ComponentData* GetUnderlying() const; + + Worker_ComponentId GetComponentId() const; + +private: + Worker_ComponentId ComponentId; + OwningComponentDataPtr Data; +}; +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ComponentUpdate.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ComponentUpdate.h new file mode 100644 index 0000000000..5fc887b0ed --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ComponentUpdate.h @@ -0,0 +1,57 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Templates/UniquePtr.h" +#include +#include + +namespace SpatialGDK +{ + +struct ComponentUpdateDeleter +{ + void operator()(Schema_ComponentUpdate* ComponentUpdate) const noexcept; +}; + +using OwningComponentUpdatePtr = TUniquePtr; + +// An RAII wrapper for component updates. +class ComponentUpdate +{ +public: + // Creates a new component update. + explicit ComponentUpdate(Worker_ComponentId Id); + // Takes ownership of a component update. + explicit ComponentUpdate(OwningComponentUpdatePtr Update, Worker_ComponentId Id); + + ~ComponentUpdate() = default; + + // Moveable, not copyable. + ComponentUpdate(const ComponentUpdate&) = delete; + ComponentUpdate(ComponentUpdate&&) = default; + ComponentUpdate& operator=(const ComponentUpdate&) = delete; + ComponentUpdate& operator=(ComponentUpdate&&) = default; + + static ComponentUpdate CreateCopy(const Schema_ComponentUpdate* Update, Worker_ComponentId Id); + + // Creates a copy of the component update. + ComponentUpdate DeepCopy() const; + // Releases ownership of the component update. + Schema_ComponentUpdate* Release() &&; + + // Appends the fields and events from other to the update. + bool Merge(ComponentUpdate Update); + + Schema_Object* GetFields() const; + Schema_Object* GetEvents() const; + + Schema_ComponentUpdate* GetUnderlying() const; + + Worker_ComponentId GetComponentId() const; + +private: + Worker_ComponentId ComponentId; + OwningComponentUpdatePtr Update; +}; +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandlers/AbstractConnectionHandler.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandlers/AbstractConnectionHandler.h index bb15635816..02fa3dd49e 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandlers/AbstractConnectionHandler.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandlers/AbstractConnectionHandler.h @@ -1,6 +1,7 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #pragma once + #include "SpatialView/MessagesToSend.h" #include "SpatialView/OpList/AbstractOpList.h" #include "Templates/UniquePtr.h" diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandlers/QueuedOpListConnectionHandler.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandlers/QueuedOpListConnectionHandler.h index e3fc664eed..ba01aef072 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandlers/QueuedOpListConnectionHandler.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ConnectionHandlers/QueuedOpListConnectionHandler.h @@ -1,6 +1,7 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #pragma once + #include "SpatialView/ConnectionHandlers/AbstractConnectionHandler.h" #include "SpatialView/OpList/AbstractOpList.h" #include "Containers/Array.h" diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentRecord.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentRecord.h new file mode 100644 index 0000000000..72cb2371e0 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentRecord.h @@ -0,0 +1,39 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialView/EntityComponentId.h" +#include "SpatialView/EntityComponentUpdateRecord.h" +#include "Containers/Array.h" + +namespace SpatialGDK +{ + +// Can be recorded as at most one of +// added +// removed +// updated +// Corollary of this is that if the component is recorded as added, that events received in the same tick will be dropped. +class EntityComponentRecord +{ +public: + void AddComponent(Worker_EntityId EntityId, ComponentData Data); + void RemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + void AddComponentAsUpdate(Worker_EntityId EntityId, ComponentData Data); + void AddUpdate(Worker_EntityId EntityId, ComponentUpdate Update); + + void Clear(); + + const TArray& GetComponentsAdded() const; + const TArray& GetComponentsRemoved() const; + + const TArray& GetUpdates() const; + const TArray& GetCompleteUpdates() const; + +private: + TArray ComponentsAdded; + TArray ComponentsRemoved; + EntityComponentUpdateRecord UpdateRecord; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentTypes.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentTypes.h new file mode 100644 index 0000000000..24417ba13e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentTypes.h @@ -0,0 +1,66 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialView/EntityComponentId.h" +#include "SpatialView/ComponentData.h" +#include "SpatialView/ComponentUpdate.h" + +namespace SpatialGDK +{ + +struct EntityComponentUpdate +{ + Worker_EntityId EntityId; + ComponentUpdate Update; + + EntityComponentId GetEntityComponentId() const + { + return { EntityId, Update.GetComponentId() }; + } +}; + +struct EntityComponentData +{ + Worker_EntityId EntityId; + ComponentData Data; + + EntityComponentId GetEntityComponentId() const + { + return { EntityId, Data.GetComponentId() }; + } +}; + +struct EntityComponentCompleteUpdate +{ + Worker_EntityId EntityId; + ComponentData CompleteUpdate; + ComponentUpdate Events; + + EntityComponentId GetEntityComponentId() const + { + return { EntityId, CompleteUpdate.GetComponentId() }; + } +}; + +struct EntityComponentIdEquality +{ + EntityComponentId 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; + } +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentUpdateRecord.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentUpdateRecord.h new file mode 100644 index 0000000000..8c4459aca9 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/EntityComponentUpdateRecord.h @@ -0,0 +1,51 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialView/EntityComponentId.h" +#include "SpatialView/EntityComponentTypes.h" +#include "Containers/Array.h" + +namespace SpatialGDK +{ + +class EntityComponentUpdateRecord +{ +public: + EntityComponentUpdateRecord() = default; + ~EntityComponentUpdateRecord() = default; + + // Moveable, not copyable. + EntityComponentUpdateRecord(const EntityComponentUpdateRecord&) = delete; + EntityComponentUpdateRecord(EntityComponentUpdateRecord&&) = default; + EntityComponentUpdateRecord& operator=(const EntityComponentUpdateRecord&) = delete; + EntityComponentUpdateRecord& operator=(EntityComponentUpdateRecord&&) = default; + + // Record a complete update for an entity-component. + // The entity-component will be recorded as completely-updated. + void AddComponentDataAsUpdate(Worker_EntityId EntityId, ComponentData CompleteUpdate); + + // Record a update for an entity-component. + // If there is no record for the entity-component it will be recorded as updated. + void AddComponentUpdate(Worker_EntityId EntityId, ComponentUpdate Update); + + // Clear all records for an entity-component. + void RemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + + // Clear all records. + void Clear(); + + // Get all entity-components recorded as updated and their associated data. + const TArray& GetUpdates() const; + // Get all entity-components recorded as completely-updated and their associated data. + const TArray& GetCompleteUpdates() const; + +private: + void InsertOrMergeUpdate(Worker_EntityId EntityId, ComponentUpdate Update); + void InsertOrSetCompleteUpdate(Worker_EntityId EntityId, ComponentData CompleteUpdate); + + TArray Updates; + TArray CompleteUpdates; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/MessagesToSend.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/MessagesToSend.h index c3613e9be4..7998621b46 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/MessagesToSend.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/MessagesToSend.h @@ -1,16 +1,18 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #pragma once + +#include "SpatialView/OutgoingComponentMessage.h" #include "SpatialView/CommandMessages.h" #include "Containers/Array.h" namespace SpatialGDK { -// todo Placeholder for vertical slice. This should be revisited when we have more complicated messages. struct MessagesToSend { TArray CreateEntityRequests; + TArray ComponentMessages; }; -} // SpatialView +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/AbstractOpList.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/AbstractOpList.h index c1edc33018..decc00296c 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/AbstractOpList.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/AbstractOpList.h @@ -1,6 +1,7 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #pragma once + #include namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/ViewDeltaLegacyOpList.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/ViewDeltaLegacyOpList.h index 806583d8fa..1970bbe7f7 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/ViewDeltaLegacyOpList.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/ViewDeltaLegacyOpList.h @@ -1,6 +1,7 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #pragma once + #include "SpatialView/OpList/AbstractOpList.h" #include "Containers/Array.h" #include diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/WorkerConnectionOpList.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/WorkerConnectionOpList.h index 15fc17f2d3..a8f0135ed4 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/WorkerConnectionOpList.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OpList/WorkerConnectionOpList.h @@ -1,8 +1,9 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #pragma once + #include "SpatialView/OpList/AbstractOpList.h" -#include "UniquePtr.h" +#include "Templates/UniquePtr.h" #include namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OutgoingComponentMessage.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OutgoingComponentMessage.h new file mode 100644 index 0000000000..ea3c59abc8 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/OutgoingComponentMessage.h @@ -0,0 +1,149 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "SpatialView/ComponentData.h" +#include "SpatialView/ComponentUpdate.h" +#include "SpatialView/EntityComponentId.h" + +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. +class OutgoingComponentMessage +{ +public: + enum MessageType {NONE, ADD, UPDATE, REMOVE}; + + explicit OutgoingComponentMessage() + : EntityId(0), ComponentId(0), 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, ComponentUpdate ComponentUpdated) + : EntityId(EntityId), ComponentId(ComponentUpdated.GetComponentId()), ComponentUpdated(MoveTemp(ComponentUpdated).Release()), Type(UPDATE) + { + } + + explicit OutgoingComponentMessage(Worker_EntityId EntityId, Worker_ComponentId RemovedComponentId) + : EntityId(EntityId), ComponentId(RemovedComponentId), Type(REMOVE) + { + } + + ~OutgoingComponentMessage() + { + // As data is stored in owning raw pointers we need to make sure resources are released. + DeleteSchemaObjects(); + } + + // Moveable, not copyable. + OutgoingComponentMessage(const OutgoingComponentMessage&) = delete; + OutgoingComponentMessage& operator=(const OutgoingComponentMessage& Other) = delete; + + OutgoingComponentMessage(OutgoingComponentMessage&& Other) noexcept + : EntityId(Other.EntityId), ComponentId(Other.ComponentId), Type(Other.Type) + { + switch (Other.Type) + { + case NONE: + break; + case ADD: + ComponentAdded = Other.ComponentAdded; + Other.ComponentAdded = nullptr; + break; + case UPDATE: + ComponentUpdated = Other.ComponentUpdated; + Other.ComponentUpdated = nullptr; + break; + case REMOVE: + break; + } + Other.Type = NONE; + } + + OutgoingComponentMessage& operator=(OutgoingComponentMessage&& Other) noexcept { + + EntityId = Other.EntityId; + ComponentId = Other.ComponentId; + + // As data is stored in owning raw pointers we need to make sure resources are released. + DeleteSchemaObjects(); + switch (Other.Type) + { + case NONE: + break; + case ADD: + ComponentAdded = Other.ComponentAdded; + Other.ComponentAdded = nullptr; + break; + case UPDATE: + ComponentUpdated = Other.ComponentUpdated; + Other.ComponentUpdated = nullptr; + break; + case REMOVE: + break; + } + + Other.Type = NONE; + + return *this; + } + + MessageType GetType() const + { + return Type; + } + + ComponentData ReleaseComponentAdded() && + { + check(Type == ADD); + ComponentData Data(OwningComponentDataPtr(ComponentAdded), ComponentId); + ComponentAdded = nullptr; + return Data; + } + + ComponentUpdate ReleaseComponentUpdate() && + { + check(Type == UPDATE); + ComponentUpdate Update(OwningComponentUpdatePtr(ComponentUpdated), ComponentId); + ComponentUpdated = nullptr; + return Update; + } + + Worker_EntityId EntityId; + Worker_ComponentId ComponentId; + +private: + void DeleteSchemaObjects() + { + switch (Type) + { + case NONE: + break; + case ADD: + Schema_DestroyComponentData(ComponentAdded); + break; + case UPDATE: + Schema_DestroyComponentUpdate(ComponentUpdated); + break; + case REMOVE: + break; + } + } + + union + { + Schema_ComponentData* ComponentAdded; + Schema_ComponentUpdate* ComponentUpdated; + }; + + MessageType Type; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewCoordinator.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewCoordinator.h index 6084e1ff91..175886c9a6 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewCoordinator.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewCoordinator.h @@ -1,6 +1,7 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #pragma once + #include "SpatialView/WorkerView.h" #include "SpatialView/ConnectionHandlers/AbstractConnectionHandler.h" #include "Templates/UniquePtr.h" @@ -16,10 +17,18 @@ class ViewCoordinator void Advance(); void FlushMessagesToSend(); + void SendAddComponent(Worker_EntityId EntityId, ComponentData Data); + void SendComponentUpdate(Worker_EntityId EntityId, ComponentUpdate Update); + void SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + const TArray& GetCreateEntityResponses() 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; TUniquePtr GenerateLegacyOpList() const; @@ -29,4 +38,4 @@ class ViewCoordinator TUniquePtr ConnectionHandler; }; -} // namespace SpatialGDK +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewDelta.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewDelta.h index 3dc71d2c4b..36033e0696 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewDelta.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/ViewDelta.h @@ -1,9 +1,12 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #pragma once + +#include +#include "SpatialView/AuthorityRecord.h" #include "SpatialView/CommandMessages.h" +#include "SpatialView/EntityComponentRecord.h" #include "SpatialView/OpList/AbstractOpList.h" -#include "SpatialView/AuthorityRecord.h" #include "Containers/Array.h" #include "Templates/UniquePtr.h" #include @@ -17,11 +20,19 @@ class ViewDelta void AddCreateEntityResponse(CreateEntityResponse Response); void SetAuthority(Worker_EntityId EntityId, Worker_ComponentId ComponentId, Worker_Authority Authority); + void AddComponent(Worker_EntityId EntityId, ComponentData Data); + void RemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId); + void AddComponentAsUpdate(Worker_EntityId EntityId, ComponentData Data); + void AddUpdate(Worker_EntityId EntityId, ComponentUpdate Update); const TArray& GetCreateEntityResponses() 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; // Returns an array of ops equivalent to the current state of the view delta. // It is expected that Clear should be called between calls to GenerateLegacyOpList. @@ -35,6 +46,7 @@ class ViewDelta TArray CreateEntityResponses; AuthorityRecord AuthorityChanges; + EntityComponentRecord EntityComponentChanges; }; } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/WorkerView.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/WorkerView.h index 52f7085448..f4e789f9b4 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialView/WorkerView.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialView/WorkerView.h @@ -1,10 +1,11 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #pragma once -#include "MessagesToSend.h" -#include "ViewDelta.h" + +#include "SpatialView/MessagesToSend.h" +#include "SpatialView/ViewDelta.h" +#include "Containers/Set.h" #include "Templates/UniquePtr.h" -#include namespace SpatialGDK { @@ -24,6 +25,9 @@ class WorkerView // 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 SendCreateEntityRequest(CreateEntityRequest Request); private: @@ -31,11 +35,15 @@ class WorkerView void HandleAuthorityChange(const Worker_AuthorityChangeOp& AuthorityChange); void HandleCreateEntityResponse(const Worker_CreateEntityResponseOp& Response); + void HandleAddComponent(const Worker_AddComponentOp& Component); + void HandleComponentUpdate(const Worker_ComponentUpdateOp& Update); + void HandleRemoveComponent(const Worker_RemoveComponentOp& Component); TArray> QueuedOps; ViewDelta Delta; TUniquePtr LocalChanges; + TSet AddedComponents; }; } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Tests/TestDefinitions.h b/SpatialGDK/Source/SpatialGDK/Public/Tests/TestDefinitions.h index 31a48dfe66..c417d86b1f 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Tests/TestDefinitions.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Tests/TestDefinitions.h @@ -10,3 +10,10 @@ #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) \ + 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) diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/EngineVersionCheck.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/EngineVersionCheck.h index a0839d3849..a5553def8c 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/EngineVersionCheck.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/EngineVersionCheck.h @@ -7,7 +7,7 @@ // GDK Version to be updated with SPATIAL_ENGINE_VERSION // when breaking changes are made to the engine that requires // changes to the GDK to remain compatible -#define SPATIAL_GDK_VERSION 19 +#define SPATIAL_GDK_VERSION 22 // Check if GDK is compatible with the current version of Unreal Engine // SPATIAL_ENGINE_VERSION is incremented in engine when breaking changes diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityFactory.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityFactory.h index 1fb285c7f1..8dcfeb1ec3 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityFactory.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityFactory.h @@ -16,7 +16,6 @@ class USpatialNetDriver; class USpatialPackageMap; class USpatialClassInfoManager; class USpatialPackageMapClient; -class SpatialActorGroupManager; namespace SpatialGDK { @@ -28,7 +27,7 @@ using FRPCsOnEntityCreationMap = TMap, RPCsOnEntit class SPATIALGDK_API EntityFactory { public: - EntityFactory(USpatialNetDriver* InNetDriver, USpatialPackageMapClient* InPackageMap, USpatialClassInfoManager* InClassInfoManager, SpatialActorGroupManager* InActorGroupManager, SpatialRPCService* InRPCService); + EntityFactory(USpatialNetDriver* InNetDriver, USpatialPackageMapClient* InPackageMap, USpatialClassInfoManager* InClassInfoManager, SpatialRPCService* InRPCService); TArray CreateEntityComponents(USpatialActorChannel* Channel, FRPCsOnEntityCreationMap& OutgoingOnCreateEntityRPCs, uint32& OutBytesWritten); TArray CreateTombstoneEntityComponents(AActor* Actor); @@ -39,7 +38,6 @@ class SPATIALGDK_API EntityFactory USpatialNetDriver* NetDriver; USpatialPackageMapClient* PackageMap; USpatialClassInfoManager* ClassInfoManager; - SpatialActorGroupManager* ActorGroupManager; SpatialRPCService* RPCService; }; } // namepsace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityPool.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityPool.h index 3ece75d09e..39e4a83f81 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityPool.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/EntityPool.h @@ -25,6 +25,8 @@ class FTimerManager; DECLARE_LOG_CATEGORY_EXTERN(LogSpatialEntityPool, Log, All) +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FEntityPoolReadyEvent); + UCLASS() class SPATIALGDK_API UEntityPool : public UObject { @@ -34,6 +36,7 @@ class SPATIALGDK_API UEntityPool : public UObject void Init(USpatialNetDriver* InNetDriver, FTimerManager* TimerManager); void ReserveEntityIDs(int32 EntitiesToReserve); Worker_EntityId GetNextEntityId(); + FEntityPoolReadyEvent& GetEntityPoolReadyDelegate(); FORCEINLINE bool IsReady() const { @@ -56,4 +59,6 @@ class SPATIALGDK_API UEntityPool : public UObject bool bIsAwaitingResponse; uint32 NextEntityRangeId; + + FEntityPoolReadyEvent EntityPoolReadyDelegate; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/InterestFactory.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/InterestFactory.h index c5ca3bc390..6b01aaa14e 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/InterestFactory.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/InterestFactory.h @@ -51,15 +51,12 @@ class SPATIALGDK_API InterestFactory // Shared constraints and result types are created at initialization and reused throughout the lifetime of the factory. void CreateAndCacheInterestState(); - // Build the checkout radius constraints for client workers - FrequencyConstraints CreateClientCheckoutRadiusConstraint(USpatialClassInfoManager* ClassInfoManager); - // Builds the result types of necessary components for clients // TODO: create and pull out into result types class - ResultType CreateClientNonAuthInterestResultType(USpatialClassInfoManager* ClassInfoManager); - ResultType CreateClientAuthInterestResultType(USpatialClassInfoManager* ClassInfoManager); - ResultType CreateServerNonAuthInterestResultType(USpatialClassInfoManager* ClassInfoManager); - ResultType CreateServerAuthInterestResultType(); + SchemaResultType CreateClientNonAuthInterestResultType(); + SchemaResultType CreateClientAuthInterestResultType(); + SchemaResultType CreateServerNonAuthInterestResultType(); + SchemaResultType CreateServerAuthInterestResultType(); Interest CreateInterest(AActor* InActor, const FClassInfo& InInfo, const Worker_EntityId InEntityId) const; @@ -92,9 +89,6 @@ class SPATIALGDK_API InterestFactory void AddObjectToConstraint(UObjectPropertyBase* Property, uint8* Data, QueryConstraint& OutConstraint) const; - // If the result types flag is flipped, set the specified result type. - void SetResultType(Query& OutQuery, const ResultType& InResultType) const; - USpatialClassInfoManager* ClassInfoManager; USpatialPackageMapClient* PackageMap; @@ -103,10 +97,10 @@ class SPATIALGDK_API InterestFactory FrequencyConstraints ClientCheckoutRadiusConstraint; // Cache the result types of queries. - ResultType ClientNonAuthInterestResultType; - ResultType ClientAuthInterestResultType; - ResultType ServerNonAuthInterestResultType; - ResultType ServerAuthInterestResultType; + SchemaResultType ClientNonAuthInterestResultType; + SchemaResultType ClientAuthInterestResultType; + SchemaResultType ServerNonAuthInterestResultType; + SchemaResultType ServerAuthInterestResultType; }; } // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/LayerInfo.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/LayerInfo.h new file mode 100644 index 0000000000..4b1471e747 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/LayerInfo.h @@ -0,0 +1,31 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" + +#include "LayerInfo.generated.h" + +class UAbstractLBStrategy; +class UAbstractLockingPolicy; + +USTRUCT() +struct FLayerInfo +{ + GENERATED_BODY() + + FLayerInfo() : Name(NAME_None) + { + } + + UPROPERTY() + FName Name; + + // Using TSoftClassPtr here to prevent eagerly loading all classes. + /** The Actor classes contained within this group. Children of these classes will also be included. */ + UPROPERTY(EditAnywhere, Category = "SpatialGDK") + TSet> ActorClasses; + + UPROPERTY(EditAnywhere, Category = "Load Balancing") + TSubclassOf LoadBalanceStrategy; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/OpUtils.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/OpUtils.h index bbdcc3b53f..35e1a67894 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/OpUtils.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/OpUtils.h @@ -9,6 +9,7 @@ namespace SpatialGDK { void FindFirstOpOfType(const TArray& InOpLists, const Worker_OpType OpType, Worker_Op** OutOp); +void AppendAllOpsOfType(const TArray& InOpLists, const Worker_OpType OpType, TArray& FoundOps); void FindFirstOpOfTypeForComponent(const TArray& InOpLists, const Worker_OpType OpType, const Worker_ComponentId ComponentId, Worker_Op** OutOp); 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 3f8d385a84..5358d0bf76 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/RPCContainer.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/RPCContainer.h @@ -27,8 +27,6 @@ enum class ERPCResult : uint8 UnresolvedTargetObject, MissingFunctionInfo, UnresolvedParameters, - ActorPendingKill, - TimedOut, // Sender specific NoActorChannel, @@ -42,6 +40,8 @@ enum class ERPCResult : uint8 NoControllerChannel, ControllerChannelNotListening, + RPCServiceFailure, + Unknown }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/RepLayoutUtils.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/RepLayoutUtils.h index be4bfeb1fa..66a3212fc3 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/RepLayoutUtils.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/RepLayoutUtils.h @@ -87,7 +87,7 @@ inline void RepLayout_SendPropertiesForRPC(FRepLayout& RepLayout, FNetBitWriter& if (!Cast(Parent.Property)) { // check for a complete match, including arrays - // (we're comparing against zero data here, since + // (we're comparing against zero data here, since // that's the default.) bSend = !Parent.Property->Identical_InContainer(Data, NULL, Parent.ArrayIndex); @@ -162,7 +162,9 @@ inline TArray GetClassRPCFunctions(const UClass* Class) // Get all remote functions from the class. This includes parents super functions and child override functions. TArray AllClassFunctions; - for (TFieldIterator RemoteFunction(Class); RemoteFunction; ++RemoteFunction) + TFieldIterator RemoteFunction(Class, EFieldIteratorFlags::IncludeSuper, EFieldIteratorFlags::IncludeDeprecated, + EFieldIteratorFlags::IncludeInterfaces); + for (; RemoteFunction; ++RemoteFunction) { if (RemoteFunction->FunctionFlags & FUNC_NetClient || RemoteFunction->FunctionFlags & FUNC_NetServer || @@ -194,11 +196,7 @@ 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) { -#if ENGINE_MINOR_VERSION <= 22 - return A.GetFName() < B.GetFName(); -#else return FNameLexicalLess()(A.GetFName(), B.GetFName()); -#endif }); return RelevantClassFunctions; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaUtils.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaUtils.h index ba94c9ec7e..1d6556d250 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaUtils.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaUtils.h @@ -129,9 +129,9 @@ inline void AddObjectRefToSchema(Schema_Object* Object, Schema_FieldId Id, const { AddObjectRefToSchema(ObjectRefObject, UNREAL_OBJECT_REF_OUTER_ID, *ObjectRef.Outer); } - if (ObjectRef.bUseSingletonClassPath) + if (ObjectRef.bUseClassPathToLoadObject) { - Schema_AddBool(ObjectRefObject, UNREAL_OBJECT_REF_USE_SINGLETON_CLASS_PATH_ID, ObjectRef.bUseSingletonClassPath); + Schema_AddBool(ObjectRefObject, UNREAL_OBJECT_REF_USE_CLASS_PATH_TO_LOAD_ID, ObjectRef.bUseClassPathToLoadObject); } } @@ -159,9 +159,9 @@ inline FUnrealObjectRef IndexObjectRefFromSchema(Schema_Object* Object, Schema_F { ObjectRef.Outer = GetObjectRefFromSchema(ObjectRefObject, UNREAL_OBJECT_REF_OUTER_ID); } - if (Schema_GetBoolCount(ObjectRefObject, UNREAL_OBJECT_REF_USE_SINGLETON_CLASS_PATH_ID) > 0) + if (Schema_GetBoolCount(ObjectRefObject, UNREAL_OBJECT_REF_USE_CLASS_PATH_TO_LOAD_ID) > 0) { - ObjectRef.bUseSingletonClassPath = GetBoolFromSchema(ObjectRefObject, UNREAL_OBJECT_REF_USE_SINGLETON_CLASS_PATH_ID); + ObjectRef.bUseClassPathToLoadObject = GetBoolFromSchema(ObjectRefObject, UNREAL_OBJECT_REF_USE_CLASS_PATH_TO_LOAD_ID); } return ObjectRef; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialActorGroupManager.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialActorGroupManager.h deleted file mode 100644 index 05bddd1f59..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialActorGroupManager.h +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#pragma once - -#include "CoreMinimal.h" -#include "GameFramework/Actor.h" -#include "SpatialConstants.h" - -#include "SpatialActorGroupManager.generated.h" - -USTRUCT() -struct FWorkerType -{ - GENERATED_BODY() - - UPROPERTY(EditAnywhere, Category = "SpatialGDK") - FName WorkerTypeName; - - FWorkerType() : WorkerTypeName(NAME_None) - { - } - - FWorkerType(FName InWorkerTypeName) : WorkerTypeName(InWorkerTypeName) - { - } -}; - -USTRUCT() -struct FActorGroupInfo -{ - GENERATED_BODY() - - UPROPERTY() - FName Name; - - /** The server worker type that has authority of all classes in this actor group. */ - UPROPERTY(EditAnywhere, Category = "SpatialGDK") - FWorkerType OwningWorkerType; - - // Using TSoftClassPtr here to prevent eagerly loading all classes. - /** The Actor classes contained within this group. Children of these classes will also be included. */ - UPROPERTY(EditAnywhere, Category = "SpatialGDK") - TSet> ActorClasses; - - FActorGroupInfo() : Name(NAME_None), OwningWorkerType() - { - } -}; - -class SPATIALGDK_API SpatialActorGroupManager -{ -private: - TMap, FName> ClassPathToActorGroup; - - TMap ActorGroupToWorkerType; - - FName DefaultWorkerType; - -public: - void Init(); - - // Returns the first ActorGroup that contains this, or a parent of this class, - // or the default actor group, if no mapping is found. - FName GetActorGroupForClass(TSubclassOf Class); - - // Returns the Server worker type that is authoritative over the ActorGroup - // that contains this class (or parent class). Returns DefaultWorkerType - // if no mapping is found. - FName GetWorkerTypeForClass(TSubclassOf Class); - - // Returns the Server worker type that is authoritative over this ActorGroup. - FName GetWorkerTypeForActorGroup(const FName& ActorGroup) const; - - // Returns true if ActorA and ActorB are contained in ActorGroups that are - // on the same Server worker type. - bool IsSameWorkerType(const AActor* ActorA, const AActor* ActorB); -}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialDebugger.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialDebugger.h index 095dca0b6b..1f0030bd30 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialDebugger.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialDebugger.h @@ -2,7 +2,6 @@ #pragma once -#include "LoadBalancing/GridBasedLBStrategy.h" #include "LoadBalancing/WorkerRegion.h" #include "SpatialCommonTypes.h" @@ -49,7 +48,7 @@ struct FWorkerRegionInfo FBox2D Extents; }; -UCLASS(SpatialType=(Singleton, NotPersistent), Blueprintable, NotPlaceable) +UCLASS(SpatialType=(NotPersistent), Blueprintable, NotPlaceable) class SPATIALGDK_API ASpatialDebugger : public AInfo { @@ -132,7 +131,6 @@ class SPATIALGDK_API ASpatialDebugger : void ActorAuthorityIntentChanged(Worker_EntityId EntityId, VirtualWorkerId NewIntentVirtualWorkerId) const; private: - void LoadIcons(); // FOnEntityAdded/FOnEntityRemoved Delegates diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyTracer.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyTracer.h index d5c8ad9d49..68ad547a7d 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyTracer.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialLatencyTracer.h @@ -131,7 +131,7 @@ class SPATIALGDK_API USpatialLatencyTracer : public UObject TraceKey RetrievePendingTrace(const UObject* Obj, const FString& Tag); void WriteToLatencyTrace(const TraceKey Key, const FString& TraceDesc); - void WriteAndEndTraceIfRemote(const TraceKey Key, const FString& TraceDesc); + void WriteAndEndTrace(const TraceKey Key, const FString& TraceDesc, bool bOnlyEndIfTraceRootIsRemote); void WriteTraceToSchemaObject(const TraceKey Key, Schema_Object* Obj, const Schema_FieldId FieldId); TraceKey ReadTraceFromSchemaObject(Schema_Object* Obj, const Schema_FieldId FieldId); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h index aa3e0920b8..bed822db93 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h @@ -15,6 +15,8 @@ class USpatialWorkerConnection; DECLARE_LOG_CATEGORY_EXTERN(LogSpatialMetrics, Log, All); +DECLARE_DELEGATE_RetVal(double, UserSuppliedMetric); + UCLASS() class SPATIALGDK_API USpatialMetrics : public UObject { @@ -55,6 +57,9 @@ class SPATIALGDK_API USpatialMetrics : public UObject DECLARE_DELEGATE_RetVal(FUnrealObjectRef, FControllerRefProviderDelegate); FControllerRefProviderDelegate ControllerRefProvider; + void SetWorkerLoadDelegate(const UserSuppliedMetric& Delegate) { WorkerLoadDelegate = Delegate; } + void SetCustomMetric(const FString& Metric, const UserSuppliedMetric& Delegate); + void RemoveCustomMetric(const FString& Metric); private: UPROPERTY() @@ -70,6 +75,9 @@ class SPATIALGDK_API USpatialMetrics : public UObject double AverageFPS; double WorkerLoad; + UserSuppliedMetric WorkerLoadDelegate; + + TMap UserSuppliedMetrics; // RPC tracking is activated with "SpatialStartRPCMetrics" and stopped with "SpatialStopRPCMetrics" // console command. It will record every sent RPC as well as the size of its payload, and then display diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetricsDisplay.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetricsDisplay.h index c5bdba01d6..3ea64a26c2 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetricsDisplay.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetricsDisplay.h @@ -30,7 +30,7 @@ struct FWorkerStats } }; -UCLASS(SpatialType=Singleton) +UCLASS(SpatialType) class SPATIALGDK_API ASpatialMetricsDisplay : public AInfo { diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialStatics.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialStatics.h index 3506523682..3c6f16efaf 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialStatics.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialStatics.h @@ -13,7 +13,6 @@ #include "SpatialStatics.generated.h" class AActor; -class SpatialActorGroupManager; // This log category will always log to the spatial runtime and thus also be printed in the SpatialOutput. DECLARE_LOG_CATEGORY_EXTERN(LogSpatial, Log, All); @@ -32,10 +31,10 @@ class SPATIALGDK_API USpatialStatics : public UBlueprintFunctionLibrary static bool IsSpatialNetworkingEnabled(); /** - * Returns true if SpatialOS Offloading is enabled. + * Returns true if there is more than one worker layer in the SpatialWorldSettings and IsMultiWorkerEnabled. */ UFUNCTION(BlueprintPure, Category = "SpatialOS|Offloading") - static bool IsSpatialOffloadingEnabled(); + static bool IsSpatialOffloadingEnabled(const UWorld* World); /** * Returns true if the current Worker Type owns the Actor Group this Actor belongs to. @@ -51,25 +50,6 @@ class SPATIALGDK_API USpatialStatics : public UBlueprintFunctionLibrary UFUNCTION(BlueprintPure, Category = "SpatialOS|Offloading", meta = (WorldContext = "WorldContextObject")) static bool IsActorGroupOwnerForClass(const UObject* WorldContextObject, const TSubclassOf ActorClass); - /** - * Returns true if the current Worker Type owns this Actor Group. - * Equivalent to World->GetNetMode() != NM_Client when Spatial Networking is disabled. - */ - UFUNCTION(BlueprintPure, Category = "SpatialOS|Offloading", meta = (WorldContext = "WorldContextObject")) - static bool IsActorGroupOwner(const UObject* WorldContextObject, const FName ActorGroup); - - /** - * Returns the ActorGroup this Actor belongs to. - */ - UFUNCTION(BlueprintPure, Category = "SpatialOS|Offloading") - static FName GetActorGroupForActor(const AActor* Actor); - - /** - * Returns the ActorGroup this Actor Class belongs to. - */ - UFUNCTION(BlueprintPure, Category = "SpatialOS|Offloading", meta = (WorldContext = "WorldContextObject")) - static FName GetActorGroupForClass(const UObject* WorldContextObject, const TSubclassOf ActorClass); - /** * Functionally the same as the native Unreal PrintString but also logs to the spatial runtime. */ @@ -129,6 +109,5 @@ class SPATIALGDK_API USpatialStatics : public UBlueprintFunctionLibrary private: - static SpatialActorGroupManager* GetActorGroupManager(const UObject* WorldContext); static FName GetCurrentWorkerType(const UObject* WorldContext); }; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/CloudDeploymentConfiguration.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/CloudDeploymentConfiguration.cpp new file mode 100644 index 0000000000..5ae9ce5fc4 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/CloudDeploymentConfiguration.cpp @@ -0,0 +1,45 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "CloudDeploymentConfiguration.h" + +#include "SpatialGDKSettings.h" +#include "SpatialGDKEditorSettings.h" + +void FCloudDeploymentConfiguration::InitFromSettings() +{ + const USpatialGDKEditorSettings* Settings = GetDefault(); + + AssemblyName = Settings->GetAssemblyName(); + RuntimeVersion = Settings->GetSelectedRuntimeVariantVersion().GetVersionForCloud(); + PrimaryDeploymentName = Settings->GetPrimaryDeploymentName(); + PrimaryLaunchConfigPath = Settings->GetPrimaryLaunchConfigPath(); + SnapshotPath = Settings->GetSnapshotPath(); + PrimaryRegionCode = Settings->GetPrimaryRegionCodeText().ToString(); + MainDeploymentCluster = Settings->GetMainDeploymentCluster(); + DeploymentTags = Settings->GetDeploymentTags(); + + bSimulatedPlayersEnabled = Settings->IsSimulatedPlayersEnabled(); + SimulatedPlayerDeploymentName = Settings->GetSimulatedPlayerDeploymentName(); + SimulatedPlayerLaunchConfigPath = Settings->GetSimulatedPlayerLaunchConfigPath(); + SimulatedPlayerRegionCode = Settings->GetSimulatedPlayerRegionCode().ToString(); + SimulatedPlayerCluster = Settings->GetSimulatedPlayerCluster(); + NumberOfSimulatedPlayers = Settings->GetNumberOfSimulatedPlayers(); + + bBuildAndUploadAssembly = Settings->ShouldBuildAndUploadAssembly(); + bGenerateSchema = Settings->IsGenerateSchemaEnabled(); + bGenerateSnapshot = Settings->IsGenerateSnapshotEnabled(); + BuildConfiguration = Settings->GetAssemblyBuildConfiguration().ToString(); + bBuildClientWorker = Settings->IsBuildClientWorkerEnabled(); + bForceAssemblyOverwrite = Settings->IsForceAssemblyOverwriteEnabled(); + + BuildServerExtraArgs = Settings->BuildServerExtraArgs; + BuildClientExtraArgs = Settings->BuildClientExtraArgs; + BuildSimulatedPlayerExtraArgs = Settings->BuildSimulatedPlayerExtraArgs; + + bUseChinaPlatform = GetDefault()->IsRunningInChina(); + if (bUseChinaPlatform) + { + PrimaryRegionCode = TEXT("CN"); + SimulatedPlayerRegionCode = TEXT("CN"); + } +} diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/GridLBStrategyEditorExtension.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/GridLBStrategyEditorExtension.cpp index 25af3a89c3..e7aec8ddf3 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/GridLBStrategyEditorExtension.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/GridLBStrategyEditorExtension.cpp @@ -2,6 +2,7 @@ #include "GridLBStrategyEditorExtension.h" #include "SpatialGDKEditorSettings.h" +#include "SpatialRuntimeLoadBalancingStrategies.h" class UGridBasedLBStrategy_Spy : public UGridBasedLBStrategy { @@ -12,13 +13,14 @@ class UGridBasedLBStrategy_Spy : public UGridBasedLBStrategy using UGridBasedLBStrategy::Cols; }; -bool FGridLBStrategyEditorExtension::GetDefaultLaunchConfiguration(const UGridBasedLBStrategy* Strategy, FWorkerTypeLaunchSection& OutConfiguration, FIntPoint& OutWorldDimensions) const +bool FGridLBStrategyEditorExtension::GetDefaultLaunchConfiguration(const UGridBasedLBStrategy* Strategy, UAbstractRuntimeLoadBalancingStrategy*& OutConfiguration, FIntPoint& OutWorldDimensions) const { const UGridBasedLBStrategy_Spy* StrategySpy = static_cast(Strategy); - OutConfiguration.Rows = StrategySpy->Rows; - OutConfiguration.Columns = StrategySpy->Cols; - OutConfiguration.NumEditorInstances = StrategySpy->Rows * StrategySpy->Cols; + UGridRuntimeLoadBalancingStrategy* GridStrategy = NewObject(); + GridStrategy->Rows = StrategySpy->Rows; + GridStrategy->Columns = StrategySpy->Cols; + OutConfiguration = GridStrategy; // Convert from cm to m. OutWorldDimensions = FIntPoint(StrategySpy->WorldWidth / 100, StrategySpy->WorldHeight / 100); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/GridLBStrategyEditorExtension.h b/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/GridLBStrategyEditorExtension.h index ab3e53cec2..984cd98888 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/GridLBStrategyEditorExtension.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/GridLBStrategyEditorExtension.h @@ -8,5 +8,5 @@ class FGridLBStrategyEditorExtension : public FLBStrategyEditorExtensionTemplate { public: - bool GetDefaultLaunchConfiguration(const UGridBasedLBStrategy* Strategy, FWorkerTypeLaunchSection& OutConfiguration, FIntPoint& OutWorldDimensions) const; + bool GetDefaultLaunchConfiguration(const UGridBasedLBStrategy* Strategy, UAbstractRuntimeLoadBalancingStrategy*& OutConfiguration, FIntPoint& OutWorldDimensions) const; }; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/LBStrategyEditorExtension.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/LBStrategyEditorExtension.cpp index c1d383ce0e..867da38a74 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/LBStrategyEditorExtension.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/EditorExtension/LBStrategyEditorExtension.cpp @@ -3,17 +3,20 @@ #include "EditorExtension/LBStrategyEditorExtension.h" #include "LoadBalancing/AbstractLBStrategy.h" +DEFINE_LOG_CATEGORY(LogSpatialGDKEditorLBExtension); + namespace { bool InheritFromClosest(UClass* Derived, UClass* PotentialBase, uint32& InOutPreviousDistance) { uint32 InheritanceDistance = 0; - for (const UStruct* TempStruct = Derived; TempStruct; TempStruct = TempStruct->GetSuperStruct()) + for (const UStruct* TempStruct = Derived; TempStruct != nullptr; TempStruct = TempStruct->GetSuperStruct()) { if (TempStruct == PotentialBase) { - break; + InOutPreviousDistance = InheritanceDistance; + return true; } ++InheritanceDistance; if (InheritanceDistance > InOutPreviousDistance) @@ -21,14 +24,12 @@ bool InheritFromClosest(UClass* Derived, UClass* PotentialBase, uint32& InOutPre return false; } } - - InOutPreviousDistance = InheritanceDistance; - return true; + return false; } } // anonymous namespace -bool FLBStrategyEditorExtensionManager::GetDefaultLaunchConfiguration(const UAbstractLBStrategy* Strategy, FWorkerTypeLaunchSection& OutConfiguration, FIntPoint& OutWorldDimensions) const +bool FLBStrategyEditorExtensionManager::GetDefaultLaunchConfiguration(const UAbstractLBStrategy* Strategy, UAbstractRuntimeLoadBalancingStrategy*& OutConfiguration, FIntPoint& OutWorldDimensions) const { if (!Strategy) { @@ -53,12 +54,18 @@ bool FLBStrategyEditorExtensionManager::GetDefaultLaunchConfiguration(const UAbs return StrategyInterface->GetDefaultLaunchConfiguration_Virtual(Strategy, OutConfiguration, OutWorldDimensions); } + UE_LOG(LogSpatialGDKEditorLBExtension, Error, TEXT("Could not find editor extension for load balancing strategy %s"), *StrategyClass->GetName()); return false; } void FLBStrategyEditorExtensionManager::RegisterExtension(UClass* StrategyClass, TUniquePtr StrategyExtension) { - Extensions.Push(ExtensionArray::ElementType(StrategyClass, MoveTemp(StrategyExtension))); + Extensions.Add(StrategyClass, MoveTemp(StrategyExtension)); +} + +void FLBStrategyEditorExtensionManager::UnregisterExtension(UClass* StrategyClass) +{ + Extensions.Remove(StrategyClass); } void FLBStrategyEditorExtensionManager::Cleanup() diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp index d1acefaa97..045cfbf582 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp @@ -70,15 +70,14 @@ void AddPotentialNameCollision(const FString& DesiredSchemaName, const FString& PotentialSchemaNameCollisions.FindOrAdd(DesiredSchemaName).Add(FString::Printf(TEXT("%s(%s)"), *ClassPath, *GeneratedSchemaName)); } -void OnStatusOutput(FString Message) +void OnStatusOutput(const FString& Message) { UE_LOG(LogSpatialGDKSchemaGenerator, Log, TEXT("%s"), *Message); } -void GenerateCompleteSchemaFromClass(FString SchemaPath, FComponentIdGenerator& IdGenerator, TSharedPtr TypeInfo) +void GenerateCompleteSchemaFromClass(const FString& SchemaPath, FComponentIdGenerator& IdGenerator, TSharedPtr TypeInfo) { UClass* Class = Cast(TypeInfo->Type); - FString SchemaFilename = UnrealNameToSchemaName(Class->GetName()); if (Class->IsChildOf()) { @@ -90,7 +89,7 @@ void GenerateCompleteSchemaFromClass(FString SchemaPath, FComponentIdGenerator& } } -bool CheckSchemaNameValidity(FString Name, FString Identifier, FString Category) +bool CheckSchemaNameValidity(const FString& Name, const FString& Identifier, const FString& Category) { if (Name.IsEmpty()) { @@ -197,7 +196,7 @@ bool ValidateIdentifierNames(TArray>& TypeInfos) check(Class); const FString& ClassName = Class->GetName(); const FString& ClassPath = Class->GetPathName(); - FString SchemaName = UnrealNameToSchemaName(ClassName); + FString SchemaName = UnrealNameToSchemaName(ClassName, true); if (!CheckSchemaNameValidity(SchemaName, ClassPath, TEXT("Class"))) { @@ -256,7 +255,7 @@ void GenerateSchemaFromClasses(const TArray>& TypeInfos, } } -void WriteLevelComponent(FCodeWriter& Writer, FString LevelName, Worker_ComponentId ComponentId, FString ClassPath) +void WriteLevelComponent(FCodeWriter& Writer, const FString& LevelName, Worker_ComponentId ComponentId, const FString& ClassPath) { Writer.PrintNewLine(); Writer.Printf("// {0}", *ClassPath); @@ -326,7 +325,7 @@ 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 @@ -609,7 +608,7 @@ void CopyWellKnownSchemaFiles(const FString& GDKSchemaCopyDir, const FString& Co FString GDKSchemaDir = FPaths::Combine(PluginDir, TEXT("SpatialGDK/Extras/schema")); FString CoreSDKSchemaDir = FPaths::Combine(PluginDir, TEXT("SpatialGDK/Binaries/ThirdParty/Improbable/Programs/schema")); - + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); RefreshSchemaFiles(*GDKSchemaCopyDir); @@ -716,7 +715,7 @@ bool LoadGeneratorStateFromSchemaDatabase(const FString& FileName) return true; } -bool IsAssetReadOnly(FString FileName) +bool IsAssetReadOnly(const FString& FileName) { FString RelativeFileName = FPaths::Combine(FPaths::ProjectContentDir(), FileName); RelativeFileName = FPaths::SetExtension(RelativeFileName, FPackageName::GetAssetPackageExtension()); @@ -768,7 +767,7 @@ bool DeleteSchemaDatabase(const FString& PackagePath) bool GeneratedSchemaDatabaseExists() { IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); - + return PlatformFile.FileExists(*RelativeSchemaDatabaseFilePath); } @@ -905,7 +904,7 @@ bool SpatialGDKGenerateSchema() return false; } - if (!SaveSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_ASSET_PATH)) // This requires RunSchemaCompiler to run first + if (!SaveSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_ASSET_PATH)) // This requires RunSchemaCompiler to run first { return false; } @@ -920,7 +919,7 @@ bool SpatialGDKGenerateSchemaForClasses(TSet Classes, FString SchemaOut { return A.GetPathName() < B.GetPathName(); }); - + // Generate Type Info structs for all classes TArray> TypeInfos; @@ -968,10 +967,6 @@ bool SpatialGDKGenerateSchemaForClasses(TSet Classes, FString SchemaOut return false; } -#if ENGINE_MINOR_VERSION <= 22 - check(GetDefault()->UsesSpatialNetworking()); -#endif - FComponentIdGenerator IdGenerator = FComponentIdGenerator(NextAvailableComponentId); GenerateSchemaFromClasses(TypeInfos, SchemaOutputPath, IdGenerator); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.cpp index 69d76bb172..84c87a109f 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.cpp @@ -255,13 +255,8 @@ 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. -#if ENGINE_MINOR_VERSION <= 22 - FRepLayout RepLayout; - RepLayout.InitFromObjectClass(Class); -#else TSharedPtr RepLayoutPtr = FRepLayout::CreateFromClass(Class, nullptr/*ServerConnection*/, ECreateRepLayoutFlags::None); FRepLayout& RepLayout = *RepLayoutPtr.Get(); -#endif for (int CmdIndex = 0; CmdIndex < RepLayout.Cmds.Num(); ++CmdIndex) { FRepLayoutCmd& Cmd = RepLayout.Cmds[CmdIndex]; @@ -399,6 +394,7 @@ FUnrealFlatRepData GetFlatRepData(TSharedPtr TypeInfo) switch (PropertyInfo->ReplicationData->Condition) { case COND_AutonomousOnly: + case COND_ReplayOrOwner: case COND_OwnerOnly: Group = REP_SingleClient; break; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.cpp index 0e948b7ad4..5dcc79f1d0 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.cpp @@ -4,6 +4,7 @@ #include "Algo/Transform.h" #include "Internationalization/Regex.h" +#include "SpatialGDKEditorSchemaGenerator.h" // Regex pattern matcher to match alphanumeric characters. const FRegexPattern AlphanumericPattern(TEXT("[A-Za-z0-9]")); @@ -25,9 +26,19 @@ FString GetEnumDataType(const UEnumProperty* EnumProperty) return DataType; } -FString UnrealNameToSchemaName(const FString& UnrealName) +FString UnrealNameToSchemaName(const FString& UnrealName, bool bWarnAboutRename /* = false */) { - return AlphanumericSanitization(UnrealName); + FString Sanitized = AlphanumericSanitization(UnrealName); + if (Sanitized.IsValidIndex(0) && FChar::IsDigit(Sanitized[0])) + { + 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); + } + return Result; + } + return Sanitized; } FString AlphanumericSanitization(const FString& InString) diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.h b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.h index 786ce3e038..b8b899a244 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/Utils/DataTypeUtilities.h @@ -12,7 +12,7 @@ extern TMap ClassPathToSchemaName; FString GetEnumDataType(const UEnumProperty* 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); +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. FString UnrealNameToSchemaComponentName(const FString& UnrealName); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp index 9e8351705f..66a2c4f6ad 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp @@ -10,7 +10,6 @@ #include "Schema/StandardLibrary.h" #include "Schema/UnrealMetadata.h" #include "SpatialConstants.h" -#include "SpatialGDKEditorSettings.h" #include "SpatialGDKSettings.h" #include "Utils/EntityFactory.h" #include "Utils/ComponentFactory.h" @@ -68,7 +67,6 @@ bool CreateSpawnerEntity(Worker_SnapshotOutputStream* OutputStream) ComponentWriteAcl.Add(SpatialConstants::PERSISTENCE_COMPONENT_ID, SpatialConstants::UnrealServerPermission); ComponentWriteAcl.Add(SpatialConstants::ENTITY_ACL_COMPONENT_ID, SpatialConstants::UnrealServerPermission); ComponentWriteAcl.Add(SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID, SpatialConstants::UnrealServerPermission); - ComponentWriteAcl.Add(SpatialConstants::AUTHORITY_INTENT_COMPONENT_ID, SpatialConstants::UnrealServerPermission); ComponentWriteAcl.Add(SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID, SpatialConstants::UnrealServerPermission); Components.Add(Position(DeploymentOrigin).CreatePositionData()); @@ -84,20 +82,6 @@ bool CreateSpawnerEntity(Worker_SnapshotOutputStream* OutputStream) return Worker_SnapshotOutputStream_GetState(OutputStream).stream_state == WORKER_STREAM_STATE_GOOD; } -Worker_ComponentData CreateSingletonManagerData() -{ - StringToEntityMap SingletonNameToEntityId; - - Worker_ComponentData Data{}; - Data.component_id = SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID; - Data.schema_type = Schema_CreateComponentData(); - Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); - - AddStringToEntityMapToSchema(ComponentObject, 1, SingletonNameToEntityId); - - return Data; -} - Worker_ComponentData CreateDeploymentData() { Worker_ComponentData DeploymentData{}; @@ -133,20 +117,6 @@ Worker_ComponentData CreateStartupActorManagerData() return StartupActorManagerData; } -WorkerRequirementSet CreateReadACLForAlwaysRelevantEntities() -{ - const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); - - WorkerRequirementSet ReadACL; - for (const FName& WorkerType : SpatialGDKSettings->ServerWorkerTypes) - { - const WorkerAttributeSet WorkerTypeAttributeSet{ { WorkerType.ToString() } }; - ReadACL.Add(WorkerTypeAttributeSet); - } - - return ReadACL; -} - bool CreateGlobalStateManager(Worker_SnapshotOutputStream* OutputStream) { Worker_Entity GSM; @@ -159,21 +129,20 @@ bool CreateGlobalStateManager(Worker_SnapshotOutputStream* OutputStream) 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::SINGLETON_MANAGER_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::AUTHORITY_INTENT_COMPONENT_ID, SpatialConstants::UnrealServerPermission); ComponentWriteAcl.Add(SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID, SpatialConstants::UnrealServerPermission); + WorkerRequirementSet ReadACL = { SpatialConstants::UnrealServerAttributeSet }; + Components.Add(Position(DeploymentOrigin).CreatePositionData()); Components.Add(Metadata(TEXT("GlobalStateManager")).CreateMetadataData()); Components.Add(Persistence().CreatePersistenceData()); - Components.Add(CreateSingletonManagerData()); Components.Add(CreateDeploymentData()); Components.Add(CreateGSMShutdownData()); Components.Add(CreateStartupActorManagerData()); - Components.Add(EntityAcl(CreateReadACLForAlwaysRelevantEntities(), ComponentWriteAcl).CreateEntityAclData()); + Components.Add(EntityAcl(ReadACL, ComponentWriteAcl).CreateEntityAclData()); Components.Add(ComponentPresence(EntityFactory::GetComponentPresenceList(Components)).CreateComponentPresenceData()); SetEntityData(GSM, Components); @@ -205,11 +174,13 @@ bool CreateVirtualWorkerTranslator(Worker_SnapshotOutputStream* OutputStream) ComponentWriteAcl.Add(SpatialConstants::VIRTUAL_WORKER_TRANSLATION_COMPONENT_ID, SpatialConstants::UnrealServerPermission); ComponentWriteAcl.Add(SpatialConstants::COMPONENT_PRESENCE_COMPONENT_ID, SpatialConstants::UnrealServerPermission); + WorkerRequirementSet ReadACL = { SpatialConstants::UnrealServerAttributeSet }; + Components.Add(Position(DeploymentOrigin).CreatePositionData()); Components.Add(Metadata(TEXT("VirtualWorkerTranslator")).CreateMetadataData()); Components.Add(Persistence().CreatePersistenceData()); Components.Add(CreateVirtualWorkerTranslatorData()); - Components.Add(EntityAcl(CreateReadACLForAlwaysRelevantEntities(), ComponentWriteAcl).CreateEntityAclData()); + Components.Add(EntityAcl(ReadACL, ComponentWriteAcl).CreateEntityAclData()); Components.Add(ComponentPresence(EntityFactory::GetComponentPresenceList(Components)).CreateComponentPresenceData()); SetEntityData(VirtualWorkerTranslator, Components); @@ -287,23 +258,21 @@ bool FillSnapshot(Worker_SnapshotOutputStream* OutputStream, UWorld* World) return true; } -bool SpatialGDKGenerateSnapshot(UWorld* World, FString SnapshotFilename) +bool SpatialGDKGenerateSnapshot(UWorld* World, FString SnapshotPath) { - const USpatialGDKEditorSettings* Settings = GetDefault(); - FString SavePath = FPaths::Combine(Settings->GetSpatialOSSnapshotFolderPath(), SnapshotFilename); - if (!ValidateAndCreateSnapshotGenerationPath(SavePath)) + if (!ValidateAndCreateSnapshotGenerationPath(SnapshotPath)) { return false; } - UE_LOG(LogSpatialGDKSnapshot, Display, TEXT("Saving snapshot to: %s"), *SavePath); + UE_LOG(LogSpatialGDKSnapshot, Display, TEXT("Saving snapshot to: %s"), *SnapshotPath); Worker_ComponentVtable DefaultVtable{}; Worker_SnapshotParameters Parameters{}; Parameters.default_component_vtable = &DefaultVtable; bool bSuccess = true; - Worker_SnapshotOutputStream* OutputStream = Worker_SnapshotOutputStream_Create(TCHAR_TO_UTF8(*SavePath), &Parameters); + Worker_SnapshotOutputStream* OutputStream = Worker_SnapshotOutputStream_Create(TCHAR_TO_UTF8(*SnapshotPath), &Parameters); if (const char* SchemaError = Worker_SnapshotOutputStream_GetState(OutputStream).error_message) { UE_LOG(LogSpatialGDKSnapshot, Error, TEXT("Error creating SnapshotOutputStream: %s"), UTF8_TO_TCHAR(SchemaError)); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultLaunchConfigGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultLaunchConfigGenerator.cpp index 80a197c5fe..516341355d 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultLaunchConfigGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultLaunchConfigGenerator.cpp @@ -2,15 +2,20 @@ #include "SpatialGDKDefaultLaunchConfigGenerator.h" +#include "EditorExtension/LBStrategyEditorExtension.h" +#include "EngineClasses/SpatialWorldSettings.h" +#include "LoadBalancing/AbstractLBStrategy.h" +#include "SpatialGDKEditorModule.h" +#include "SpatialGDKSettings.h" #include "SpatialGDKEditorSettings.h" +#include "SpatialRuntimeLoadBalancingStrategies.h" -#include "Serialization/JsonWriter.h" +#include "Editor.h" +#include "ISettingsModule.h" #include "Misc/FileHelper.h" - #include "Misc/MessageDialog.h" - -#include "ISettingsModule.h" -#include "SpatialGDKSettings.h" +#include "Serialization/JsonWriter.h" +#include "Settings/LevelEditorPlaySettings.h" DEFINE_LOG_CATEGORY(LogSpatialGDKDefaultLaunchConfigGenerator); @@ -30,68 +35,55 @@ bool WriteFlagSection(TSharedRef> Writer, const FString& Key, cons return true; } -bool WriteWorkerSection(TSharedRef> Writer, const FWorkerTypeLaunchSection& Worker) +bool WriteWorkerSection(TSharedRef> Writer, const FName& WorkerTypeName, const FWorkerTypeLaunchSection& WorkerConfig) { Writer->WriteObjectStart(); - Writer->WriteValue(TEXT("worker_type"), *Worker.WorkerTypeName.ToString()); + Writer->WriteValue(TEXT("worker_type"), *WorkerTypeName.ToString()); Writer->WriteArrayStart(TEXT("flags")); - for (const auto& Flag : Worker.Flags) + for (const auto& Flag : WorkerConfig.Flags) { WriteFlagSection(Writer, Flag.Key, Flag.Value); } Writer->WriteArrayEnd(); Writer->WriteArrayStart(TEXT("permissions")); Writer->WriteObjectStart(); - if (Worker.WorkerPermissions.bAllPermissions) - { - Writer->WriteObjectStart(TEXT("all")); - Writer->WriteObjectEnd(); - } - else - { - Writer->WriteObjectStart(TEXT("entity_creation")); - Writer->WriteValue(TEXT("allow"), Worker.WorkerPermissions.bAllowEntityCreation); - Writer->WriteObjectEnd(); - Writer->WriteObjectStart(TEXT("entity_deletion")); - Writer->WriteValue(TEXT("allow"), Worker.WorkerPermissions.bAllowEntityDeletion); - Writer->WriteObjectEnd(); - Writer->WriteObjectStart(TEXT("entity_query")); - Writer->WriteValue(TEXT("allow"), Worker.WorkerPermissions.bAllowEntityQuery); - Writer->WriteArrayStart("components"); - for (const FString& Component : Worker.WorkerPermissions.Components) - { - Writer->WriteValue(Component); - } - Writer->WriteArrayEnd(); - Writer->WriteObjectEnd(); - } + 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(); - if (Worker.MaxConnectionCapacityLimit > 0) - { - Writer->WriteObjectStart(TEXT("connection_capacity_limit")); - Writer->WriteValue(TEXT("max_capacity"), Worker.MaxConnectionCapacityLimit); - Writer->WriteObjectEnd(); - } - if (Worker.bLoginRateLimitEnabled) - { - Writer->WriteObjectStart(TEXT("login_rate_limit")); - Writer->WriteValue(TEXT("duration"), Worker.LoginRateLimit.Duration); - Writer->WriteValue(TEXT("requests_per_duration"), Worker.LoginRateLimit.RequestsPerDuration); - Writer->WriteObjectEnd(); - } Writer->WriteObjectEnd(); return true; } -bool WriteLoadbalancingSection(TSharedRef> Writer, const FName& WorkerType, const int32 Columns, const int32 Rows, const bool ManualWorkerConnectionOnly) +bool WriteLoadbalancingSection(TSharedRef> Writer, const FName& WorkerType, uint32 NumEditorInstances, const bool ManualWorkerConnectionOnly) { Writer->WriteObjectStart(); Writer->WriteValue(TEXT("layer"), *WorkerType.ToString()); Writer->WriteObjectStart("rectangle_grid"); - Writer->WriteValue(TEXT("cols"), Columns); - Writer->WriteValue(TEXT("rows"), Rows); + 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); @@ -101,9 +93,128 @@ bool WriteLoadbalancingSection(TSharedRef> Writer, const FName& Wo return true; } +} // anonymous namespace + +void SetLevelEditorPlaySettingsWorkerType(const FWorkerTypeLaunchSection& InWorker) +{ + ULevelEditorPlaySettings* PlayInSettings = GetMutableDefault(); + + PlayInSettings->WorkerTypesToLaunch.Empty(1); + + // TODO: Engine PR to remove PlayInSettings WorkerType map. + PlayInSettings->WorkerTypesToLaunch.Add(SpatialConstants::DefaultServerWorkerType, InWorker.NumEditorInstances); +} + +uint32 GetWorkerCountFromWorldSettings(const UWorld& World) +{ + const ASpatialWorldSettings* WorldSettings = Cast(World.GetWorldSettings()); + + if (WorldSettings == nullptr) + { + UE_LOG(LogSpatialGDKDefaultLaunchConfigGenerator, Error, TEXT("Missing SpatialWorldSettings on map %s"), *World.GetMapName()); + return 1; + } + + if (WorldSettings->bEnableMultiWorker == false) + { + return 1; + } + + FSpatialGDKEditorModule& EditorModule = FModuleManager::GetModuleChecked("SpatialGDKEditor"); + uint32 NumWorkers = 0; + if (WorldSettings->DefaultLayerLoadBalanceStrategy == nullptr) + { + UE_LOG(LogSpatialGDKDefaultLaunchConfigGenerator, Error, TEXT("Missing Load balancing strategy on map %s"), *World.GetMapName()); + return 1; + } + else + { + UAbstractRuntimeLoadBalancingStrategy* LoadBalancingStrat = nullptr; + FIntPoint Dimension; + if (!EditorModule.GetLBStrategyExtensionManager().GetDefaultLaunchConfiguration(WorldSettings->DefaultLayerLoadBalanceStrategy->GetDefaultObject(), LoadBalancingStrat, Dimension)) + { + UE_LOG(LogSpatialGDKDefaultLaunchConfigGenerator, Error, TEXT("Could not get the default SpatialOS Load balancing strategy from %s"), *WorldSettings->DefaultLayerLoadBalanceStrategy->GetName()); + NumWorkers += 1; + } + else + { + NumWorkers += LoadBalancingStrat->GetNumberOfWorkersForPIE(); + } + } + + for (const auto& Layer : WorldSettings->WorkerLayers) + { + const FName& LayerKey = Layer.Key; + const FLayerInfo& LayerInfo = Layer.Value; + + UAbstractRuntimeLoadBalancingStrategy* LoadBalancingStrat = nullptr; + FIntPoint Dimension; + if (LayerInfo.LoadBalanceStrategy == nullptr) + { + UE_LOG(LogSpatialGDKDefaultLaunchConfigGenerator, Error, TEXT("Missing Load balancing strategy on layer %s"), *LayerKey.ToString()); + NumWorkers += 1; + } + else if (!EditorModule.GetLBStrategyExtensionManager().GetDefaultLaunchConfiguration(LayerInfo.LoadBalanceStrategy->GetDefaultObject(), LoadBalancingStrat, Dimension)) + { + UE_LOG(LogSpatialGDKDefaultLaunchConfigGenerator, Error, TEXT("Could not get the SpatialOS Load balancing strategy for layer %s"), *LayerKey.ToString()); + NumWorkers += 1; + } + else + { + NumWorkers += LoadBalancingStrat->GetNumberOfWorkersForPIE(); + } + } + + return NumWorkers; } -bool GenerateDefaultLaunchConfig(const FString& LaunchConfigPath, const FSpatialLaunchConfigDescription* InLaunchConfigDescription) +bool TryGetLoadBalancingStrategyFromWorldSettings(const UWorld& World, UAbstractRuntimeLoadBalancingStrategy*& OutStrategy, FIntPoint& OutWorldDimension) +{ + const ASpatialWorldSettings* WorldSettings = Cast(World.GetWorldSettings()); + + if (WorldSettings == nullptr || !WorldSettings->bEnableMultiWorker) + { + UE_LOG(LogSpatialGDKDefaultLaunchConfigGenerator, Log, TEXT("No SpatialWorldSettings on map %s"), *World.GetMapName()); + return false; + } + + if (WorldSettings->DefaultLayerLoadBalanceStrategy == nullptr) + { + UE_LOG(LogSpatialGDKDefaultLaunchConfigGenerator, Error, TEXT("Missing Load balancing strategy on map %s"), *World.GetMapName()); + return false; + } + + FSpatialGDKEditorModule& EditorModule = FModuleManager::GetModuleChecked("SpatialGDKEditor"); + + if (!EditorModule.GetLBStrategyExtensionManager().GetDefaultLaunchConfiguration(WorldSettings->DefaultLayerLoadBalanceStrategy->GetDefaultObject(), OutStrategy, OutWorldDimension)) + { + UE_LOG(LogSpatialGDKDefaultLaunchConfigGenerator, Error, TEXT("Could not get the SpatialOS Load balancing strategy from %s"), *WorldSettings->DefaultLayerLoadBalanceStrategy->GetName()); + return false; + } + + return true; +} + +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) { if (InLaunchConfigDescription != nullptr) { @@ -114,48 +225,46 @@ bool GenerateDefaultLaunchConfig(const FString& LaunchConfigPath, const FSpatial // Populate json file for launch config Writer->WriteObjectStart(); // Start of json - Writer->WriteValue(TEXT("template"), LaunchConfigDescription.Template); // Template section + 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->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"); - for (const FWorkerTypeLaunchSection& Worker : LaunchConfigDescription.ServerWorkers) - { - WriteLoadbalancingSection(Writer, Worker.WorkerTypeName, Worker.Columns, Worker.Rows, Worker.bManualWorkerConnectionOnly); - } - Writer->WriteArrayEnd(); - Writer->WriteObjectEnd(); // Load balancing section end - Writer->WriteArrayStart(TEXT("workers")); // Workers section begin - for (const FWorkerTypeLaunchSection& Worker : LaunchConfigDescription.ServerWorkers) - { - WriteWorkerSection(Writer, Worker); - } - // Write the client worker section - FWorkerTypeLaunchSection ClientWorker; - ClientWorker.WorkerTypeName = SpatialConstants::DefaultClientWorkerType; - ClientWorker.WorkerPermissions.bAllPermissions = true; - ClientWorker.bLoginRateLimitEnabled = false; - WriteWorkerSection(Writer, ClientWorker); + 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->WriteObjectEnd(); // End of json @@ -173,7 +282,7 @@ bool GenerateDefaultLaunchConfig(const FString& LaunchConfigPath, const FSpatial return false; } -bool ValidateGeneratedLaunchConfig(const FSpatialLaunchConfigDescription& LaunchConfigDesc) +bool ValidateGeneratedLaunchConfig(const FSpatialLaunchConfigDescription& LaunchConfigDesc, const FWorkerTypeLaunchSection& InWorker) { const USpatialGDKSettings* SpatialGDKRuntimeSettings = GetDefault(); @@ -191,67 +300,6 @@ bool ValidateGeneratedLaunchConfig(const FSpatialLaunchConfigDescription& Launch return false; } } - - if (!SpatialGDKRuntimeSettings->bEnableHandover && LaunchConfigDesc.ServerWorkers.ContainsByPredicate([](const FWorkerTypeLaunchSection& Section) - { - return (Section.Rows * Section.Columns) > 1; - })) - { - const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(TEXT("Property handover is disabled and a zoned deployment is specified.\nThis is not supported.\n\nDo you want to configure your project settings now?"))); - - if (Result == EAppReturnType::Yes) - { - FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Runtime Settings"); - } - - return false; - } - - if (LaunchConfigDesc.ServerWorkers.ContainsByPredicate([](const FWorkerTypeLaunchSection& Section) - { - return (Section.Rows * Section.Columns) < Section.NumEditorInstances; - })) - { - const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(TEXT("Attempting to launch too many servers for load balance configuration.\nThis is not supported.\n\nDo you want to configure your project settings now?"))); - - if (Result == EAppReturnType::Yes) - { - FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Editor Settings"); - } - - return false; - } - - if (!SpatialGDKRuntimeSettings->ServerWorkerTypes.Contains(SpatialGDKRuntimeSettings->DefaultWorkerType.WorkerTypeName)) - { - const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(TEXT("Default Worker Type is invalid, please choose a valid worker type as the default.\n\nDo you want to configure your project settings now?"))); - - if (Result == EAppReturnType::Yes) - { - FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Runtime Settings"); - } - - return false; - } - - if (SpatialGDKRuntimeSettings->bEnableOffloading) - { - for (const TPair& ActorGroup : SpatialGDKRuntimeSettings->ActorGroups) - { - if (!SpatialGDKRuntimeSettings->ServerWorkerTypes.Contains(ActorGroup.Value.OwningWorkerType.WorkerTypeName)) - { - const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(FString::Printf(TEXT("Actor Group '%s' has an invalid Owning Worker Type, please choose a valid worker type.\n\nDo you want to configure your project settings now?"), *ActorGroup.Key.ToString()))); - - if (Result == EAppReturnType::Yes) - { - FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Runtime Settings"); - } - - return false; - } - } - } - return true; } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultWorkerJsonGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultWorkerJsonGenerator.cpp index 59409e5bee..b656a8c5b6 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultWorkerJsonGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDefaultWorkerJsonGenerator.cpp @@ -2,7 +2,7 @@ #include "SpatialGDKDefaultWorkerJsonGenerator.h" -#include "SpatialGDKEditorSettings.h" +#include "SpatialGDKSettings.h" #include "SpatialGDKServicesConstants.h" #include "Misc/FileHelper.h" @@ -43,20 +43,17 @@ bool GenerateAllDefaultWorkerJsons(bool& bOutRedeployRequired) const FString WorkerJsonDir = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("workers/unreal")); bool bAllJsonsGeneratedSuccessfully = true; - if (const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault()) + if (const USpatialGDKSettings* SpatialGDKSettings = GetDefault()) { - const FSpatialLaunchConfigDescription& LaunchConfigDescription = SpatialGDKEditorSettings->LaunchConfigDesc; - for (const FWorkerTypeLaunchSection& Worker : LaunchConfigDescription.ServerWorkers) + const FName& Worker = SpatialConstants::DefaultServerWorkerType; + FString JsonPath = FPaths::Combine(WorkerJsonDir, FString::Printf(TEXT("spatialos.%s.worker.json"), *Worker.ToString())); + if (!FPaths::FileExists(JsonPath)) { - FString JsonPath = FPaths::Combine(WorkerJsonDir, FString::Printf(TEXT("spatialos.%s.worker.json"), *Worker.WorkerTypeName.ToString())); - if (!FPaths::FileExists(JsonPath)) - { - UE_LOG(LogSpatialGDKDefaultWorkerJsonGenerator, Verbose, TEXT("Could not find worker json at %s"), *JsonPath); + UE_LOG(LogSpatialGDKDefaultWorkerJsonGenerator, Verbose, TEXT("Could not find worker json at %s"), *JsonPath); - if (!GenerateDefaultWorkerJson(JsonPath, Worker.WorkerTypeName.ToString(), bOutRedeployRequired)) - { - bAllJsonsGeneratedSuccessfully = false; - } + if (!GenerateDefaultWorkerJson(JsonPath, Worker.ToString(), bOutRedeployRequired)) + { + bAllJsonsGeneratedSuccessfully = false; } } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDevAuthTokenGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDevAuthTokenGenerator.cpp new file mode 100644 index 0000000000..4a00d33320 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKDevAuthTokenGenerator.cpp @@ -0,0 +1,103 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialGDKDevAuthTokenGenerator.h" + +#include "Async/Async.h" +#include "Framework/Notifications/NotificationManager.h" + +#include "SpatialCommandUtils.h" +#include "SpatialGDKEditorSettings.h" +#include "SpatialGDKSettings.h" + +DEFINE_LOG_CATEGORY(LogSpatialGDKDevAuthTokenGenerator); + +FSpatialGDKDevAuthTokenGenerator::FSpatialGDKDevAuthTokenGenerator() + : bIsGenerating(false) +{ +} + +void FSpatialGDKDevAuthTokenGenerator::DoGenerateDevAuthTokenTasks() +{ + bool bIsRunningInChina = GetDefault()->IsRunningInChina(); + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, bIsRunningInChina] + { + AsyncTask(ENamedThreads::GameThread, [this]() + { + ShowTaskStartedNotification(TEXT("Generating Development Authentication Token")); + }); + + FString DevAuthToken; + FString ErrorMessage; + if (SpatialCommandUtils::GenerateDevAuthToken(bIsRunningInChina, DevAuthToken, ErrorMessage)) + { + 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); + AsyncTask(ENamedThreads::GameThread, [this]() + { + EndTask(/* bSuccess */ false); + }); + } + }); +} + +void FSpatialGDKDevAuthTokenGenerator::AsyncGenerateDevAuthToken() +{ + bool bExpected = false; + if (bIsGenerating.CompareExchange(bExpected, true)) + { + DoGenerateDevAuthTokenTasks(); + } + else + { + UE_LOG(LogSpatialGDKDevAuthTokenGenerator, Display, TEXT("A previous Development Authentication Token request is still pending. New request for generation ignored.")); + } +} + +void FSpatialGDKDevAuthTokenGenerator::ShowTaskStartedNotification(const FString& NotificationText) +{ + FNotificationInfo Info(FText::AsCultureInvariant(NotificationText)); + Info.ExpireDuration = 5.0f; + Info.bFireAndForget = false; + + TaskNotificationPtr = FSlateNotificationManager::Get().AddNotification(Info); + + if (TaskNotificationPtr.IsValid()) + { + TaskNotificationPtr.Pin()->SetCompletionState(SNotificationItem::CS_Pending); + } +} + +void FSpatialGDKDevAuthTokenGenerator::EndTask(bool bSuccess) +{ + if (bSuccess) + { + ShowTaskEndedNotification(TEXT("Development Authentication Token Updated"), SNotificationItem::CS_Success); + } + else + { + ShowTaskEndedNotification(TEXT("Failed to generate Development Authentication Token"), SNotificationItem::CS_Fail); + } + + bIsGenerating = false; +} + +void FSpatialGDKDevAuthTokenGenerator::ShowTaskEndedNotification(const FString& NotificationText, SNotificationItem::ECompletionState CompletionState) +{ + TSharedPtr Notification = TaskNotificationPtr.Pin(); + if (Notification.IsValid()) + { + Notification->SetFadeInDuration(0.1f); + Notification->SetFadeOutDuration(0.5f); + Notification->SetExpireDuration(5.0f); + Notification->SetText(FText::AsCultureInvariant(NotificationText)); + Notification->SetCompletionState(CompletionState); + Notification->ExpireAndFadeout(); + } +} diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp index 2b2ac37705..792cda5fd7 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp @@ -2,23 +2,28 @@ #include "SpatialGDKEditor.h" +#include "AssetDataTagMap.h" +#include "AssetRegistryModule.h" #include "Async/Async.h" -#include "SpatialGDKEditorCloudLauncher.h" -#include "SpatialGDKEditorSchemaGenerator.h" -#include "SpatialGDKEditorSnapshotGenerator.h" - #include "Editor.h" #include "FileHelpers.h" - -#include "AssetDataTagMap.h" -#include "AssetRegistryModule.h" #include "GeneralProjectSettings.h" #include "Internationalization/Regex.h" +#include "IUATHelperModule.h" +#include "Misc/MessageDialog.h" #include "Misc/ScopedSlowTask.h" +#include "PackageTools.h" #include "Settings/ProjectPackagingSettings.h" +#include "UnrealEdMisc.h" +#include "UObject/StrongObjectPtr.h" + +#include "SpatialGDKDevAuthTokenGenerator.h" +#include "SpatialGDKEditorCloudLauncher.h" +#include "SpatialGDKEditorPackageAssembly.h" +#include "SpatialGDKEditorSchemaGenerator.h" #include "SpatialGDKEditorSettings.h" +#include "SpatialGDKEditorSnapshotGenerator.h" #include "SpatialGDKServicesConstants.h" -#include "UObject/StrongObjectPtr.h" using namespace SpatialGDKEditor; @@ -26,7 +31,71 @@ DEFINE_LOG_CATEGORY(LogSpatialGDKEditor); #define LOCTEXT_NAMESPACE "FSpatialGDKEditor" -bool FSpatialGDKEditor::GenerateSchema(bool bFullScan) +namespace +{ + +bool CheckAutomationToolsUpToDate() +{ +#if PLATFORM_WINDOWS + FString RunUATScriptName = TEXT("RunUAT.bat"); + FString CmdExe = TEXT("cmd.exe"); +#elif PLATFORM_LINUX + FString RunUATScriptName = TEXT("RunUAT.sh"); + FString CmdExe = TEXT("/bin/bash"); +#else + FString RunUATScriptName = TEXT("RunUAT.command"); + FString CmdExe = TEXT("/bin/sh"); +#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)); + + return false; + } + +#if PLATFORM_WINDOWS + FString FullCommandLine = FString::Printf(TEXT("/c \"\"%s\" -list\""), *UatPath); +#else + FString FullCommandLine = FString::Printf(TEXT("\"%s\" -list"), *UatPath); +#endif + + FString ListCommandResult; + int32 ResultCode = -1; + FSpatialGDKServicesModule::ExecuteAndReadOutput(CmdExe, FullCommandLine, FPaths::EngineDir(), ListCommandResult, ResultCode); + + if (ResultCode != 0) + { + UE_LOG(LogSpatialGDKEditor, Error, TEXT("Automation tool execution error : %i"), ResultCode); + return false; + } + + if (ListCommandResult.Find(TEXT("CookAndGenerateSchema")) >= 0) + { + 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")); + + return false; +} + +} + +FSpatialGDKEditor::FSpatialGDKEditor() + : bSchemaGeneratorRunning(false) + , SpatialGDKDevAuthTokenGeneratorInstance(MakeShared()) + , SpatialGDKPackageAssemblyInstance(MakeShared()) +{ +} + +bool FSpatialGDKEditor::GenerateSchema(ESchemaGenerationMethod Method) { if (bSchemaGeneratorRunning) { @@ -34,6 +103,12 @@ bool FSpatialGDKEditor::GenerateSchema(bool bFullScan) return false; } + if (!FPaths::IsProjectFilePathSet()) + { + UE_LOG(LogSpatialGDKEditor, Error, TEXT("Schema generation called when no project was opened")); + return false; + } + // If this has been run from an open editor then prompt the user to save dirty packages and maps. if (!IsRunningCommandlet()) { @@ -50,97 +125,112 @@ bool FSpatialGDKEditor::GenerateSchema(bool bFullScan) } } - bSchemaGeneratorRunning = true; - - // 80/10/10 load assets / gen schema / garbage collection. - FScopedSlowTask Progress(100.f, LOCTEXT("GeneratingSchema", "Generating Schema...")); - Progress.MakeDialog(true); - -#if ENGINE_MINOR_VERSION <= 22 - // Force spatial networking so schema layouts are correct - UGeneralProjectSettings* GeneralProjectSettings = GetMutableDefault(); - bool bCachedSpatialNetworking = GeneralProjectSettings->UsesSpatialNetworking(); - GeneralProjectSettings->SetUsesSpatialNetworking(true); -#endif - - RemoveEditorAssetLoadedCallback(); - if (Schema::IsAssetReadOnly(SpatialConstants::SCHEMA_DATABASE_FILE_PATH)) { - bSchemaGeneratorRunning = false; return false; } - if (!Schema::LoadGeneratorStateFromSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_FILE_PATH)) - { - Schema::ResetSchemaGeneratorStateAndCleanupFolders(); - } - - TArray> LoadedAssets; - if (bFullScan) + if (Method == FullAssetScan) { - Progress.EnterProgressFrame(80.f); - if (!LoadPotentialAssets(LoadedAssets)) + if (!CheckAutomationToolsUpToDate()) { - bSchemaGeneratorRunning = false; - LoadedAssets.Empty(); - CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS, true); return false; } - } - // If running from an open editor then compile all dirty blueprints - TArray ErroredBlueprints; - if (!IsRunningCommandlet()) - { - const bool bPromptForCompilation = false; - UEditorEngine::ResolveDirtyBlueprints(bPromptForCompilation, ErroredBlueprints); - } + // Make sure SchemaDatabase is not loaded. + if (UPackage* LoadedDatabase = FindPackage(nullptr, *FPaths::Combine(TEXT("/Game/"), *SpatialConstants::SCHEMA_DATABASE_FILE_PATH))) + { + TArray ToUnload; + ToUnload.Add(LoadedDatabase); + UPackageTools::UnloadPackages(ToUnload); + } - if (bFullScan) - { - // UNR-1610 - This copy is a workaround to enable schema_compiler usage until FPL is ready. Without this prepare_for_run checks crash local launch and cloud upload. - FString GDKSchemaCopyDir = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("schema/unreal/gdk")); - FString CoreSDKSchemaCopyDir = FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("build/dependencies/schema/standard_library")); - Schema::CopyWellKnownSchemaFiles(GDKSchemaCopyDir, CoreSDKSchemaCopyDir); - Schema::RefreshSchemaFiles(GetDefault()->GetGeneratedSchemaOutputFolder()); - } + const USpatialGDKEditorSettings* EditorSettings = GetDefault(); - Progress.EnterProgressFrame(bFullScan ? 10.f : 100.f); - bool bResult = Schema::SpatialGDKGenerateSchema(); - - // 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:")); - for (const auto& Blueprint : ErroredBlueprints) + const FString& PlatformName = EditorSettings->GetCookAndGenerateSchemaTargetPlatform(); + + if (PlatformName.IsEmpty()) { - UE_LOG(LogSpatialGDKEditor, Error, TEXT("%s"), *GetPathNameSafe(Blueprint)); + UE_LOG(LogSpatialGDKEditor, Error, TEXT("Empty platform passed to CookAndGenerateSchema")); + return false; } - } - if (bFullScan) - { - Progress.EnterProgressFrame(10.f); - LoadedAssets.Empty(); - CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS, true); - } + FString OptionalParams = EditorSettings->GetCookAndGenerateSchemaAdditionalArgs(); + OptionalParams += FString::Printf(TEXT(" -targetplatform=%s"), *PlatformName); -#if ENGINE_MINOR_VERSION <= 22 - GetMutableDefault()->SetUsesSpatialNetworking(bCachedSpatialNetworking); -#endif - bSchemaGeneratorRunning = false; + 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 + ); - if (bResult) - { - UE_LOG(LogSpatialGDKEditor, Display, TEXT("Schema Generation succeeded!")); + IUATHelperModule::Get().CreateUatTask(UATCommandLine, + FText::FromString(PlatformName), + LOCTEXT("CookAndGenerateSchemaTaskName", "Cook and generate project schema"), + LOCTEXT("CookAndGenerateSchemaTaskName", "Generating Schema"), + FEditorStyle::GetBrush(TEXT("MainFrame.PackageProject"))); + + return true; } else { - UE_LOG(LogSpatialGDKEditor, Error, TEXT("Schema Generation failed. View earlier log messages for errors.")); + bSchemaGeneratorRunning = true; + + FScopedSlowTask Progress(100.f, LOCTEXT("GeneratingSchema", "Generating Schema...")); + Progress.MakeDialog(true); + + RemoveEditorAssetLoadedCallback(); + + if (!Schema::LoadGeneratorStateFromSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_FILE_PATH)) + { + Schema::ResetSchemaGeneratorStateAndCleanupFolders(); + } + + // If running from an open editor then compile all dirty blueprints + TArray ErroredBlueprints; + if (!IsRunningCommandlet()) + { + const bool bPromptForCompilation = false; + UEditorEngine::ResolveDirtyBlueprints(bPromptForCompilation, ErroredBlueprints); + } + + Progress.EnterProgressFrame(100.f); + + bool bResult = Schema::SpatialGDKGenerateSchema(); + + // 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:")); + for (const auto& Blueprint : ErroredBlueprints) + { + UE_LOG(LogSpatialGDKEditor, Error, TEXT("%s"), *GetPathNameSafe(Blueprint)); + } + } + + bSchemaGeneratorRunning = false; + + if (bResult) + { + UE_LOG(LogSpatialGDKEditor, Display, TEXT("Schema Generation succeeded!")); + } + else + { + UE_LOG(LogSpatialGDKEditor, Error, TEXT("Schema Generation failed. View earlier log messages for errors.")); + } + + return bResult; } +} - return bResult; +bool FSpatialGDKEditor::IsSchemaGenerated() +{ + 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(); } bool FSpatialGDKEditor::LoadPotentialAssets(TArray>& OutAssets) @@ -197,7 +287,7 @@ bool FSpatialGDKEditor::LoadPotentialAssets(TArray>& O } if (GeneratedClassPathPtr != nullptr) - { + { const FString ClassObjectPath = FPackageName::ExportTextPathToObjectPath(*GeneratedClassPathPtr); const FString ClassName = FPackageName::ObjectPathToObjectName(ClassObjectPath); FSoftObjectPath SoftPath = FSoftObjectPath(ClassObjectPath); @@ -210,7 +300,9 @@ bool FSpatialGDKEditor::LoadPotentialAssets(TArray>& O void FSpatialGDKEditor::GenerateSnapshot(UWorld* World, FString SnapshotFilename, FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback, FSpatialGDKEditorErrorHandler ErrorCallback) { - const bool bSuccess = SpatialGDKGenerateSnapshot(World, SnapshotFilename); + const USpatialGDKEditorSettings* Settings = GetDefault(); + FString SavePath = FPaths::Combine(Settings->GetSpatialOSSnapshotFolderPath(), SnapshotFilename); + const bool bSuccess = SpatialGDKGenerateSnapshot(World, SavePath); if (bSuccess) { @@ -222,13 +314,9 @@ void FSpatialGDKEditor::GenerateSnapshot(UWorld* World, FString SnapshotFilename } } -void FSpatialGDKEditor::LaunchCloudDeployment(FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback) +void FSpatialGDKEditor::StartCloudDeployment(const FCloudDeploymentConfiguration& Configuration, FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback) { -#if ENGINE_MINOR_VERSION <= 22 - LaunchCloudResult = Async(EAsyncExecution::Thread, SpatialGDKCloudLaunch, -#else - LaunchCloudResult = Async(EAsyncExecution::Thread, SpatialGDKCloudLaunch, -#endif + LaunchCloudResult = Async(EAsyncExecution::Thread, [&Configuration]() { return SpatialGDKCloudLaunch(Configuration); }, [this, SuccessCallback, FailureCallback] { if (!LaunchCloudResult.IsReady() || LaunchCloudResult.Get() != true) @@ -244,11 +332,7 @@ void FSpatialGDKEditor::LaunchCloudDeployment(FSimpleDelegate SuccessCallback, F void FSpatialGDKEditor::StopCloudDeployment(FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback) { -#if ENGINE_MINOR_VERSION <= 22 - StopCloudResult = Async(EAsyncExecution::Thread, SpatialGDKCloudStop, -#else StopCloudResult = Async(EAsyncExecution::Thread, SpatialGDKCloudStop, -#endif [this, SuccessCallback, FailureCallback] { if (!StopCloudResult.IsReady() || StopCloudResult.Get() != true) @@ -267,6 +351,15 @@ bool FSpatialGDKEditor::FullScanRequired() return !Schema::GeneratedSchemaFolderExists() || !Schema::GeneratedSchemaDatabaseExists(); } +void FSpatialGDKEditor::SetProjectName(const FString& InProjectName) +{ + if (!FSpatialGDKServicesModule::GetProjectName().Equals(InProjectName)) + { + FSpatialGDKServicesModule::SetProjectName(InProjectName); + SpatialGDKDevAuthTokenGeneratorInstance->AsyncGenerateDevAuthToken(); + } +} + void FSpatialGDKEditor::RemoveEditorAssetLoadedCallback() { if (OnAssetLoadedHandle.IsValid()) @@ -315,4 +408,14 @@ void FSpatialGDKEditor::OnAssetLoaded(UObject* Asset) } } +TSharedRef FSpatialGDKEditor::GetDevAuthTokenGeneratorRef() +{ + return SpatialGDKDevAuthTokenGeneratorInstance; +} + +TSharedRef FSpatialGDKEditor::GetPackageAssemblyRef() +{ + return SpatialGDKPackageAssemblyInstance; +} + #undef LOCTEXT_NAMESPACE diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCloudLauncher.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCloudLauncher.cpp index b1995678a6..f87dad6f63 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCloudLauncher.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCloudLauncher.cpp @@ -2,8 +2,9 @@ #include "SpatialGDKEditorCloudLauncher.h" -#include "Interfaces/IPluginManager.h" -#include "SpatialGDKEditorSettings.h" +#include "GenericPlatform/GenericPlatformProcess.h" + +#include "CloudDeploymentConfiguration.h" #include "SpatialGDKServicesModule.h" DEFINE_LOG_CATEGORY(LogSpatialGDKEditorCloudLauncher); @@ -13,33 +14,39 @@ namespace const FString LauncherExe = FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(TEXT("SpatialGDK/Binaries/ThirdParty/Improbable/Programs/DeploymentLauncher/DeploymentLauncher.exe")); } -bool SpatialGDKCloudLaunch() +bool SpatialGDKCloudLaunch(const FCloudDeploymentConfiguration& Configuration) { - const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); - FString LauncherCreateArguments = FString::Printf( - TEXT("create %s %s %s %s \"%s\" \"%s\" %s"), + TEXT("create %s %s %s %s \"%s\" \"%s\" %s \"%s\" \"%s\""), *FSpatialGDKServicesModule::GetProjectName(), - *SpatialGDKSettings->GetAssemblyName(), - *SpatialGDKSettings->GetSpatialOSRuntimeVersionForCloud(), - *SpatialGDKSettings->GetPrimaryDeploymentName(), - *SpatialGDKSettings->GetPrimaryLaunchConfigPath(), - *SpatialGDKSettings->GetSnapshotPath(), - *SpatialGDKSettings->GetPrimaryRegionCode().ToString() + *Configuration.AssemblyName, + *Configuration.RuntimeVersion, + *Configuration.PrimaryDeploymentName, + *Configuration.PrimaryLaunchConfigPath, + *Configuration.SnapshotPath, + *Configuration.PrimaryRegionCode, + *Configuration.MainDeploymentCluster, + *Configuration.DeploymentTags ); - if (SpatialGDKSettings->IsSimulatedPlayersEnabled()) + if (Configuration.bSimulatedPlayersEnabled) { LauncherCreateArguments = FString::Printf( - TEXT("%s %s \"%s\" %s %s"), + TEXT("%s %s \"%s\" %s \"%s\" %u"), *LauncherCreateArguments, - *SpatialGDKSettings->GetSimulatedPlayerDeploymentName(), - *SpatialGDKSettings->GetSimulatedPlayerLaunchConfigPath(), - *SpatialGDKSettings->GetSimulatedPlayerRegionCode().ToString(), - *FString::FromInt(SpatialGDKSettings->GetNumberOfSimulatedPlayer()) + *Configuration.SimulatedPlayerDeploymentName, + *Configuration.SimulatedPlayerLaunchConfigPath, + *Configuration.SimulatedPlayerRegionCode, + *Configuration.SimulatedPlayerCluster, + Configuration.NumberOfSimulatedPlayers ); } + if (Configuration.bUseChinaPlatform) + { + LauncherCreateArguments += TEXT(" --china"); + } + int32 OutCode = 0; FString OutString; FString OutErr; @@ -63,20 +70,13 @@ bool SpatialGDKCloudStop() UE_LOG(LogSpatialGDKEditorCloudLauncher, Error, TEXT("Function not available")); return false; - const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); - // TODO: UNR-2435 - Add a Stop Deployment button and fix the code below: // get and provide deployment-id to stop the deployment as one of the LauncherStopArguments - const FString LauncherStopArguments = FString::Printf( - TEXT("stop %s"), - *SpatialGDKSettings->GetPrimaryRegionCode().ToString() - ); - int32 OutCode = 0; FString OutString; FString OutErr; - bool bSuccess = FPlatformProcess::ExecProcess(*LauncherExe, *LauncherStopArguments, &OutCode, &OutString, &OutErr); + bool bSuccess = FPlatformProcess::ExecProcess(*LauncherExe, TEXT("stop"), &OutCode, &OutString, &OutErr); if (OutCode != 0) { UE_LOG(LogSpatialGDKEditorCloudLauncher, Error, TEXT("Cloud Launch failed with code %d: %s"), OutCode, *OutString); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCommandLineArgsManager.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCommandLineArgsManager.cpp new file mode 100644 index 0000000000..ed7bd88a92 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCommandLineArgsManager.cpp @@ -0,0 +1,293 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialGDKEditorCommandLineArgsManager.h" + +#include "HAL/PlatformFilemanager.h" +#include "IOSRuntimeSettings.h" +#include "Misc/App.h" +#include "Misc/FileHelper.h" +#include "Misc/MessageDialog.h" +#include "Serialization/JsonSerializer.h" + +#ifdef ENABLE_LAUNCHER_DELEGATE +#include "ILauncher.h" +#include "ILauncherServicesModule.h" +#include "ILauncherWorker.h" +#endif // ENABLE_LAUNCHER_DELEGATE + +#include "SpatialCommandUtils.h" +#include "SpatialGDKEditorSettings.h" +#include "SpatialGDKSettings.h" + +DEFINE_LOG_CATEGORY(LogSpatialGDKEditorCommandLineArgsManager); + +FSpatialGDKEditorCommandLineArgsManager::FSpatialGDKEditorCommandLineArgsManager() +#ifdef ENABLE_LAUNCHER_DELEGATE + : bAndroidDevice(false) +#endif // ENABLE_LAUNCHER_DELEGATE +{ +} + +void FSpatialGDKEditorCommandLineArgsManager::Init() +{ +#ifdef ENABLE_LAUNCHER_DELEGATE + ILauncherServicesModule& LauncherServicesModule = FModuleManager::LoadModuleChecked(TEXT("LauncherServices")); + LauncherServicesModule.OnCreateLauncherDelegate.AddRaw(this, &FSpatialGDKEditorCommandLineArgsManager::OnCreateLauncher); +#endif // ENABLE_LAUNCHER_DELEGATE +} + +#ifdef ENABLE_LAUNCHER_DELEGATE +void FSpatialGDKEditorCommandLineArgsManager::OnLauncherCanceled(double ExecutionTime) +{ + RemoveCommandLineFromDevice(); +} + +void FSpatialGDKEditorCommandLineArgsManager::OnLauncherFinished(bool bSuccess, double ExecutionTime, int32 ReturnCode) +{ + RemoveCommandLineFromDevice(); +} + +void FSpatialGDKEditorCommandLineArgsManager::RemoveCommandLineFromDevice() +{ + if (bAndroidDevice) + { + RemoveCommandLineFromAndroidDevice(); + } +} + +void FSpatialGDKEditorCommandLineArgsManager::OnLaunch(ILauncherWorkerPtr LauncherWorkerPtr, ILauncherProfileRef LauncherProfileRef) +{ + LauncherWorkerPtr->OnCanceled().AddRaw(this, &FSpatialGDKEditorCommandLineArgsManager::OnLauncherCanceled); + LauncherWorkerPtr->OnCompleted().AddRaw(this, &FSpatialGDKEditorCommandLineArgsManager::OnLauncherFinished); + + bAndroidDevice = false; + TArray TaskList; + LauncherWorkerPtr->GetTasks(TaskList); + for (const ILauncherTaskPtr& Task : TaskList) + { + if (Task->GetDesc().Contains(TEXT("android"))) + { + bAndroidDevice = true; + UE_LOG(LogSpatialGDKEditorCommandLineArgsManager, Log, TEXT("Launched game on Android device")); + break; + } + } +} + +void FSpatialGDKEditorCommandLineArgsManager::OnCreateLauncher(ILauncherRef LauncherRef) +{ + LauncherRef->FLauncherWorkerStartedDelegate.AddRaw(this, &FSpatialGDKEditorCommandLineArgsManager::OnLaunch); +} +#endif // ENABLE_LAUNCHER_DELEGATE + +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, FText::FromString(TEXT("Environment variable ANDROID_HOME is not set. Please make sure to configure this."))); + return TEXT(""); + } + +#if PLATFORM_WINDOWS + const FString AdbExe = FPaths::ConvertRelativePathToFull(FPaths::Combine(AndroidHome, TEXT("platform-tools/adb.exe"))); +#else + const FString AdbExe = FPaths::ConvertRelativePathToFull(FPaths::Combine(AndroidHome, TEXT("platform-tools/adb"))); +#endif + + return AdbExe; +} + +} // anonymous namespace + +FReply FSpatialGDKEditorCommandLineArgsManager::PushCommandLineToIOSDevice() +{ + const UIOSRuntimeSettings* IOSRuntimeSettings = GetDefault(); + FString OutCommandLineArgsFile; + + if (!TryConstructMobileCommandLineArgumentsFile(OutCommandLineArgsFile)) + { + return FReply::Unhandled(); + } + + FString Executable = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::EngineDir(), TEXT("Binaries/DotNET/IOS/deploymentserver.exe"))); + FString DeploymentServerArguments = FString::Printf(TEXT("copyfile -bundle \"%s\" -file \"%s\" -file \"/Documents/ue4commandline.txt\""), *(IOSRuntimeSettings->BundleIdentifier.Replace(TEXT("[PROJECT_NAME]"), FApp::GetProjectName())), *OutCommandLineArgsFile); + +#if PLATFORM_MAC + DeploymentServerArguments = FString::Printf(TEXT("%s %s"), *Executable, *DeploymentServerArguments); + Executable = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::EngineDir(), TEXT("Binaries/ThirdParty/Mono/Mac/bin/mono"))); +#endif + + if (!TryPushCommandLineArgsToDevice(Executable, DeploymentServerArguments, OutCommandLineArgsFile)) + { + return FReply::Unhandled(); + } + + return FReply::Handled(); +} + +FReply FSpatialGDKEditorCommandLineArgsManager::PushCommandLineToAndroidDevice() +{ + const FString AdbExe = GetAdbExePath(); + if (AdbExe.IsEmpty()) + { + return FReply::Unhandled(); + } + + FString OutCommandLineArgsFile; + + if (!TryConstructMobileCommandLineArgumentsFile(OutCommandLineArgsFile)) + { + return FReply::Unhandled(); + } + + const FString AndroidCommandLineFile = FString::Printf(TEXT("/mnt/sdcard/UE4Game/%s/UE4CommandLine.txt"), *FString(FApp::GetProjectName())); + const FString AdbArguments = FString::Printf(TEXT("push \"%s\" \"%s\""), *OutCommandLineArgsFile, *AndroidCommandLineFile); + + if (!TryPushCommandLineArgsToDevice(AdbExe, AdbArguments, OutCommandLineArgsFile)) + { + return FReply::Unhandled(); + } + + return FReply::Handled(); +} + +FReply FSpatialGDKEditorCommandLineArgsManager::RemoveCommandLineFromAndroidDevice() +{ + const FString AdbExe = GetAdbExePath(); + if (AdbExe.IsEmpty()) + { + return FReply::Unhandled(); + } + + FString ExeOutput; + FString StdErr; + int32 ExitCode; + + FString ExeArguments = FString::Printf(TEXT("shell rm -f /mnt/sdcard/UE4Game/%s/UE4CommandLine.txt"), FApp::GetProjectName()); + + 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, FText::FromString(TEXT("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); + + return FReply::Handled(); +} + +bool FSpatialGDKEditorCommandLineArgsManager::TryConstructMobileCommandLineArgumentsFile(FString& OutCommandLineArgsFile) +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + const FString ProjectName = FApp::GetProjectName(); + + // The project path is based on this: https://github.com/improbableio/UnrealEngine/blob/4.22-SpatialOSUnrealGDK-release/Engine/Source/Programs/AutomationTool/AutomationUtils/DeploymentContext.cs#L408 + const FString MobileProjectPath = FString::Printf(TEXT("../../../%s/%s.uproject"), *ProjectName, *ProjectName); + FString TravelUrl; + FString SpatialOSOptions = FString::Printf(TEXT("-workerType %s"), *(SpatialGDKSettings->MobileWorkerType)); + + ESpatialOSNetFlow::Type ConnectionFlow = SpatialGDKSettings->SpatialOSNetFlowType; + if (SpatialGDKSettings->bMobileOverrideConnectionFlow) + { + ConnectionFlow = SpatialGDKSettings->MobileConnectionFlow; + } + + if (ConnectionFlow == ESpatialOSNetFlow::LocalDeployment) + { + FString RuntimeIP = SpatialGDKSettings->ExposedRuntimeIP; + if (!SpatialGDKSettings->MobileRuntimeIPOverride.IsEmpty()) + { + RuntimeIP = SpatialGDKSettings->MobileRuntimeIPOverride; + } + + if (RuntimeIP.IsEmpty()) + { + const FString ErrorMessage = TEXT("The Runtime IP is currently not set. Please make sure to specify a Runtime IP."); + UE_LOG(LogSpatialGDKEditorCommandLineArgsManager, Error, TEXT("%s"), *ErrorMessage); + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(ErrorMessage)); + return false; + } + + TravelUrl = RuntimeIP; + + SpatialOSOptions += TEXT(" -useExternalIpForBridge true"); + } + else if (ConnectionFlow == ESpatialOSNetFlow::CloudDeployment) + { + TravelUrl = TEXT("connect.to.spatialos"); + + if (SpatialGDKSettings->DevelopmentAuthenticationToken.IsEmpty()) + { + FReply GeneratedTokenReply = GenerateDevAuthToken(); + if (!GeneratedTokenReply.IsEventHandled()) + { + return false; + } + } + + SpatialOSOptions += FString::Printf(TEXT(" -devauthToken %s"), *(SpatialGDKSettings->DevelopmentAuthenticationToken)); + if (!SpatialGDKSettings->DevelopmentDeploymentToConnect.IsEmpty()) + { + SpatialOSOptions += FString::Printf(TEXT(" -deployment %s"), *(SpatialGDKSettings->DevelopmentDeploymentToConnect)); + } + } + + const FString SpatialOSCommandLineArgs = FString::Printf(TEXT("%s %s %s %s"), *MobileProjectPath, *TravelUrl, *SpatialOSOptions, *(SpatialGDKSettings->MobileExtraCommandLineArgs)); + OutCommandLineArgsFile = FPaths::ConvertRelativePathToFull(FPaths::Combine(*FPaths::ProjectLogDir(), TEXT("ue4commandline.txt"))); + + 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::FromString(FString::Printf(TEXT("Failed to write command line args to file: %s"), *OutCommandLineArgsFile))); + return false; + } + + return true; +} + +FReply FSpatialGDKEditorCommandLineArgsManager::GenerateDevAuthToken() +{ + FString DevAuthToken; + FString ErrorMessage; + if (!SpatialCommandUtils::GenerateDevAuthToken(GetMutableDefault()->IsRunningInChina(), DevAuthToken, ErrorMessage)) + { + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(ErrorMessage)); + return FReply::Unhandled(); + } + + USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetMutableDefault(); + SpatialGDKEditorSettings->SetDevelopmentAuthenticationToken(DevAuthToken); + return FReply::Handled(); +} + +bool FSpatialGDKEditorCommandLineArgsManager::TryPushCommandLineArgsToDevice(const FString& Executable, const FString& ExeArguments, const FString& CommandLineArgsFile) +{ + FString ExeOutput; + FString StdErr; + int32 ExitCode; + + FPlatformProcess::ExecProcess(*Executable, *ExeArguments, &ExitCode, &ExeOutput, &StdErr); + if (ExitCode != 0) + { + UE_LOG(LogSpatialGDKEditorCommandLineArgsManager, Error, TEXT("Failed to update the mobile client. %s %s"), *ExeOutput, *StdErr); + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(TEXT("Failed to update the mobile client. See the Output log for more information."))); + return false; + } + + UE_LOG(LogSpatialGDKEditorCommandLineArgsManager, Log, TEXT("Successfully stored command line args on device: %s"), *ExeOutput); + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + if (!PlatformFile.DeleteFile(*CommandLineArgsFile)) + { + UE_LOG(LogSpatialGDKEditorCommandLineArgsManager, Error, TEXT("Failed to delete file %s"), *CommandLineArgsFile); + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(TEXT("Failed to delete file %s"), *CommandLineArgsFile))); + return false; + } + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorLayoutDetails.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorLayoutDetails.cpp index 140f7fc202..3aa427150e 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorLayoutDetails.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorLayoutDetails.cpp @@ -2,62 +2,79 @@ #include "SpatialGDKEditorLayoutDetails.h" +#include "DetailCategoryBuilder.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" -#include "DetailCategoryBuilder.h" -#include "HAL/PlatformFilemanager.h" -#include "IOSRuntimeSettings.h" -#include "Misc/App.h" -#include "Misc/FileHelper.h" -#include "Misc/MessageDialog.h" -#include "Serialization/JsonSerializer.h" -#include "SpatialGDKSettings.h" -#include "SpatialGDKEditorSettings.h" -#include "SpatialGDKServicesConstants.h" -#include "SpatialGDKServicesModule.h" -#include "Widgets/Text/STextBlock.h" #include "Widgets/Input/SButton.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/Notifications/SPopupErrorText.h" +#include "Widgets/Text/STextBlock.h" -DEFINE_LOG_CATEGORY(LogSpatialGDKEditorLayoutDetails); +#include "SpatialCommandUtils.h" +#include "SpatialGDKEditor.h" +#include "SpatialGDKEditorCommandLineArgsManager.h" +#include "SpatialGDKEditorModule.h" +#include "SpatialGDKEditorSettings.h" +#include "SpatialGDKServicesConstants.h" +#include "SpatialGDKSettings.h" TSharedRef FSpatialGDKEditorLayoutDetails::MakeInstance() { return MakeShareable(new FSpatialGDKEditorLayoutDetails); } +void FSpatialGDKEditorLayoutDetails::ForceRefreshLayout() +{ + if (CurrentLayout != nullptr) + { + TArray> Objects; + CurrentLayout->GetObjectsBeingCustomized(Objects); + USpatialGDKEditorSettings* Settings = Objects.Num() > 0 ? Cast(Objects[0].Get()) : nullptr; + CurrentLayout->ForceRefreshDetails(); + } +} + void FSpatialGDKEditorLayoutDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) { - TSharedPtr UsePinnedVersionProperty = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, bUseGDKPinnedRuntimeVersion)); + CurrentLayout = &DetailBuilder; + const USpatialGDKEditorSettings* GDKEditorSettings = GetDefault(); + + GDKEditorSettings->OnDefaultTemplateNameRequireUpdate.AddSP(this, &FSpatialGDKEditorLayoutDetails::ForceRefreshLayout); - IDetailPropertyRow* CustomRow = DetailBuilder.EditDefaultProperty(UsePinnedVersionProperty); + FString ProjectName = FSpatialGDKServicesModule::GetProjectName(); - FString PinnedVersionDisplay = FString::Printf(TEXT("GDK Pinned Version : %s"), *SpatialGDKServicesConstants::SpatialOSRuntimePinnedVersion); + ProjectNameInputErrorReporting = SNew(SPopupErrorText); + ProjectNameInputErrorReporting->SetError(TEXT("")); - CustomRow->CustomWidget() + IDetailCategoryBuilder& CloudConnectionCategory = DetailBuilder.EditCategory("Cloud Connection"); + CloudConnectionCategory.AddCustomRow(FText::FromString("Project Name")) .NameContent() [ - UsePinnedVersionProperty->CreatePropertyNameWidget() + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(FString(TEXT("Project Name")))) + .ToolTipText(FText::FromString(FString(TEXT("The name of the SpatialOS project.")))) + ] ] .ValueContent() + .VAlign(VAlign_Center) + .MinDesiredWidth(250) [ SNew(SHorizontalBox) - +SHorizontalBox::Slot() - .HAlign(HAlign_Left) - .AutoWidth() - [ - UsePinnedVersionProperty->CreatePropertyValueWidget() - ] - +SHorizontalBox::Slot() - .Padding(5) - .HAlign(HAlign_Center) - .AutoWidth() + + SHorizontalBox::Slot() + .FillWidth(1.0f) [ - SNew(STextBlock) - .Text(FText::FromString(PinnedVersionDisplay)) + SNew(SEditableTextBox) + .Text(FText::FromString(ProjectName)) + .ToolTipText(FText::FromString(FString(TEXT("The name of the SpatialOS project.")))) + .OnTextCommitted(this, &FSpatialGDKEditorLayoutDetails::OnProjectNameCommitted) + .ErrorReporting(ProjectNameInputErrorReporting) ] ]; - IDetailCategoryBuilder& CloudConnectionCategory = DetailBuilder.EditCategory("Cloud Connection"); CloudConnectionCategory.AddCustomRow(FText::FromString("Generate Development Authentication Token")) .ValueContent() .VAlign(VAlign_Center) @@ -65,7 +82,7 @@ void FSpatialGDKEditorLayoutDetails::CustomizeDetails(IDetailLayoutBuilder& Deta [ SNew(SButton) .VAlign(VAlign_Center) - .OnClicked(this, &FSpatialGDKEditorLayoutDetails::GenerateDevAuthToken) + .OnClicked_Static(FSpatialGDKEditorCommandLineArgsManager::GenerateDevAuthToken) .Content() [ SNew(STextBlock).Text(FText::FromString("Generate Dev Auth Token")) @@ -76,223 +93,58 @@ void FSpatialGDKEditorLayoutDetails::CustomizeDetails(IDetailLayoutBuilder& Deta MobileCategory.AddCustomRow(FText::FromString("Push SpatialOS settings to Android device")) .ValueContent() .VAlign(VAlign_Center) - .MinDesiredWidth(250) - [ - SNew(SButton) - .VAlign(VAlign_Center) - .OnClicked(this, &FSpatialGDKEditorLayoutDetails::PushCommandLineArgsToAndroidDevice) - .Content() + .MinDesiredWidth(550) [ - SNew(STextBlock).Text(FText::FromString("Push SpatialOS settings to Android device")) - ] + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .OnClicked_Static(FSpatialGDKEditorCommandLineArgsManager::PushCommandLineToAndroidDevice) + .Content() + [ + SNew(STextBlock).Text(FText::FromString("Push SpatialOS settings to Android device")) + ] + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .OnClicked_Static(FSpatialGDKEditorCommandLineArgsManager::RemoveCommandLineFromAndroidDevice) + .Content() + [ + SNew(STextBlock).Text(FText::FromString("Remove SpatialOS settings from Android device")) + ] + ] ]; MobileCategory.AddCustomRow(FText::FromString("Push SpatialOS settings to iOS device")) .ValueContent() .VAlign(VAlign_Center) - .MinDesiredWidth(250) + .MinDesiredWidth(275) [ SNew(SButton) .VAlign(VAlign_Center) - .OnClicked(this, &FSpatialGDKEditorLayoutDetails::PushCommandLineArgsToIOSDevice) - .Content() - [ - SNew(STextBlock).Text(FText::FromString("Push SpatialOS settings to iOS device")) - ] + .OnClicked_Static(FSpatialGDKEditorCommandLineArgsManager::PushCommandLineToIOSDevice) + .Content() + [ + SNew(STextBlock).Text(FText::FromString("Push SpatialOS settings to iOS device")) + ] ]; } -FReply FSpatialGDKEditorLayoutDetails::GenerateDevAuthToken() -{ - FString Arguments = TEXT("project auth dev-auth-token create --description=\"Unreal GDK Token\" --json_output"); - if (GetDefault()->IsRunningInChina()) - { - Arguments += TEXT(" --environment cn-production"); - } - - FString CreateDevAuthTokenResult; - int32 ExitCode; - FSpatialGDKServicesModule::ExecuteAndReadOutput(SpatialGDKServicesConstants::SpatialExe, Arguments, SpatialGDKServicesConstants::SpatialOSDirectory, CreateDevAuthTokenResult, ExitCode); - - if (ExitCode != 0) - { - UE_LOG(LogSpatialGDKEditorLayoutDetails, Error, TEXT("Unable to generate a development authentication token. Result: %s"), *CreateDevAuthTokenResult); - FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(TEXT("Unable to generate a development authentication token. Result: %s"), *CreateDevAuthTokenResult))); - return FReply::Unhandled(); - }; - - FString AuthResult; - FString DevAuthTokenResult; - bool bFoundNewline = CreateDevAuthTokenResult.TrimEnd().Split(TEXT("\n"), &AuthResult, &DevAuthTokenResult, ESearchCase::IgnoreCase, ESearchDir::FromEnd); - if (!bFoundNewline || DevAuthTokenResult.IsEmpty()) - { - // This is necessary because depending on whether you are already authenticated against spatial, it will either return two json structs or one. - DevAuthTokenResult = CreateDevAuthTokenResult; - } - - TSharedRef> JsonReader = TJsonReaderFactory::Create(DevAuthTokenResult); - TSharedPtr JsonRootObject; - if (!(FJsonSerializer::Deserialize(JsonReader, JsonRootObject) && JsonRootObject.IsValid())) - { - UE_LOG(LogSpatialGDKEditorLayoutDetails, Error, TEXT("Unable to parse the received development authentication token. Result: %s"), *DevAuthTokenResult); - FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(TEXT("Unable to parse the received development authentication token. Result: %s"), *DevAuthTokenResult))); - return FReply::Unhandled(); - } - - // We need a pointer to a shared pointer due to how the JSON API works. - const TSharedPtr* JsonDataObject; - if (!(JsonRootObject->TryGetObjectField("json_data", JsonDataObject))) - { - UE_LOG(LogSpatialGDKEditorLayoutDetails, Error, TEXT("Unable to parse the received json data. Result: %s"), *DevAuthTokenResult); - FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(TEXT("Unable to parse the received json data. Result: %s"), *DevAuthTokenResult))); - return FReply::Unhandled(); - } - - FString TokenSecret; - if (!(*JsonDataObject)->TryGetStringField("token_secret", TokenSecret)) - { - UE_LOG(LogSpatialGDKEditorLayoutDetails, Error, TEXT("Unable to parse the token_secret field inside the received json data. Result: %s"), *DevAuthTokenResult); - FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(TEXT("Unable to parse the token_secret field inside the received json data. Result: %s"), *DevAuthTokenResult))); - return FReply::Unhandled(); - } - - if (USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetMutableDefault()) - { - SpatialGDKEditorSettings->DevelopmentAuthenticationToken = TokenSecret; - SpatialGDKEditorSettings->SaveConfig(); - SpatialGDKEditorSettings->SetRuntimeDevelopmentAuthenticationToken(); - } - - return FReply::Handled(); -} - -bool FSpatialGDKEditorLayoutDetails::TryConstructMobileCommandLineArgumentsFile(FString& CommandLineArgsFile) -{ - const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); - const FString ProjectName = FApp::GetProjectName(); - - // The project path is based on this: https://github.com/improbableio/UnrealEngine/blob/4.22-SpatialOSUnrealGDK-release/Engine/Source/Programs/AutomationTool/AutomationUtils/DeploymentContext.cs#L408 - const FString MobileProjectPath = FString::Printf(TEXT("../../../%s/%s.uproject"), *ProjectName, *ProjectName); - FString TravelUrl; - FString SpatialOSOptions = FString::Printf(TEXT("-workerType %s"), *(SpatialGDKSettings->MobileWorkerType)); - if (SpatialGDKSettings->bMobileConnectToLocalDeployment) - { - if (SpatialGDKSettings->MobileRuntimeIP.IsEmpty()) - { - UE_LOG(LogSpatialGDKEditorLayoutDetails, Error, TEXT("The Runtime IP is currently not set. Please make sure to specify a Runtime IP.")); - FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(TEXT("The Runtime IP is currently not set. Please make sure to specify a Runtime IP.")))); - return false; - } - - TravelUrl = SpatialGDKSettings->MobileRuntimeIP; - } - else - { - TravelUrl = TEXT("connect.to.spatialos"); - - if (SpatialGDKSettings->DevelopmentAuthenticationToken.IsEmpty()) - { - FReply GeneratedTokenReply = GenerateDevAuthToken(); - if (!GeneratedTokenReply.IsEventHandled()) - { - return false; - } - } - - SpatialOSOptions += FString::Printf(TEXT(" +devauthToken %s"), *(SpatialGDKSettings->DevelopmentAuthenticationToken)); - if (!SpatialGDKSettings->DevelopmentDeploymentToConnect.IsEmpty()) - { - SpatialOSOptions += FString::Printf(TEXT(" +deployment %s"), *(SpatialGDKSettings->DevelopmentDeploymentToConnect)); - } - } - - const FString SpatialOSCommandLineArgs = FString::Printf(TEXT("%s %s %s %s"), *MobileProjectPath, *TravelUrl, *SpatialOSOptions, *(SpatialGDKSettings->MobileExtraCommandLineArgs)); - CommandLineArgsFile = FPaths::ConvertRelativePathToFull(FPaths::Combine(*FPaths::ProjectLogDir(), TEXT("ue4commandline.txt"))); - - if (!FFileHelper::SaveStringToFile(SpatialOSCommandLineArgs, *CommandLineArgsFile, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) - { - UE_LOG(LogSpatialGDKEditorLayoutDetails, Error, TEXT("Failed to write command line args to file: %s"), *CommandLineArgsFile); - FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(TEXT("Failed to write command line args to file: %s"), *CommandLineArgsFile))); - return false; - } - - return true; -} - -bool FSpatialGDKEditorLayoutDetails::TryPushCommandLineArgsToDevice(const FString& Executable, const FString& ExeArguments, const FString& CommandLineArgsFile) -{ - FString ExeOutput; - FString StdErr; - int32 ExitCode; - - FPlatformProcess::ExecProcess(*Executable, *ExeArguments, &ExitCode, &ExeOutput, &StdErr); - if (ExitCode != 0) - { - UE_LOG(LogSpatialGDKEditorLayoutDetails, Error, TEXT("Failed to update the mobile client. %s %s"), *ExeOutput, *StdErr); - FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(TEXT("Failed to update the mobile client. See the Output log for more information."))); - return false; - } - - UE_LOG(LogSpatialGDKEditorLayoutDetails, Log, TEXT("Successfully stored command line args on device: %s"), *ExeOutput); - IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); - if (!PlatformFile.DeleteFile(*CommandLineArgsFile)) - { - UE_LOG(LogSpatialGDKEditorLayoutDetails, Error, TEXT("Failed to delete file %s"), *CommandLineArgsFile); - FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(TEXT("Failed to delete file %s"), *CommandLineArgsFile))); - return false; - } - - return true; -} - -FReply FSpatialGDKEditorLayoutDetails::PushCommandLineArgsToAndroidDevice() +void FSpatialGDKEditorLayoutDetails::OnProjectNameCommitted(const FText& InText, ETextCommit::Type InCommitType) { - FString AndroidHome = FPlatformMisc::GetEnvironmentVariable(TEXT("ANDROID_HOME")); - if (AndroidHome.IsEmpty()) + FString NewProjectName = InText.ToString(); + if (!USpatialGDKEditorSettings::IsProjectNameValid(NewProjectName)) { - UE_LOG(LogSpatialGDKEditorLayoutDetails, Error, TEXT("Environment variable ANDROID_HOME is not set. Please make sure to configure this.")); - FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(TEXT("Environment variable ANDROID_HOME is not set. Please make sure to configure this."))); - return FReply::Unhandled(); + ProjectNameInputErrorReporting->SetError(SpatialConstants::ProjectPatternHint); + return; } + ProjectNameInputErrorReporting->SetError(TEXT("")); - FString OutCommandLineArgsFile; - - if (!TryConstructMobileCommandLineArgumentsFile(OutCommandLineArgsFile)) - { - return FReply::Unhandled(); - } - - const FString AndroidCommandLineFile = FString::Printf(TEXT("/mnt/sdcard/UE4Game/%s/UE4CommandLine.txt"), *FString(FApp::GetProjectName())); - const FString AdbArguments = FString::Printf(TEXT("push \"%s\" \"%s\""), *OutCommandLineArgsFile, *AndroidCommandLineFile); - -#if PLATFORM_WINDOWS - const FString AdbExe = FPaths::ConvertRelativePathToFull(FPaths::Combine(AndroidHome, TEXT("platform-tools/adb.exe"))); -#else - const FString AdbExe = FPaths::ConvertRelativePathToFull(FPaths::Combine(AndroidHome, TEXT("platform-tools/adb"))); -#endif - - TryPushCommandLineArgsToDevice(AdbExe, AdbArguments, OutCommandLineArgsFile); - return FReply::Handled(); -} - -FReply FSpatialGDKEditorLayoutDetails::PushCommandLineArgsToIOSDevice() -{ - const UIOSRuntimeSettings* IOSRuntimeSettings = GetDefault(); - FString OutCommandLineArgsFile; - - if (!TryConstructMobileCommandLineArgumentsFile(OutCommandLineArgsFile)) - { - return FReply::Unhandled(); - } - - FString Executable = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::EngineDir(), TEXT("Binaries/DotNET/IOS/deploymentserver.exe"))); - FString DeploymentServerArguments = FString::Printf(TEXT("copyfile -bundle \"%s\" -file \"%s\" -file \"/Documents/ue4commandline.txt\""), *(IOSRuntimeSettings->BundleIdentifier.Replace(TEXT("[PROJECT_NAME]"), FApp::GetProjectName())), *OutCommandLineArgsFile); - -#if PLATFORM_MAC - DeploymentServerArguments = FString::Printf(TEXT("%s %s"), *Executable, *DeploymentServerArguments); - Executable = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::EngineDir(), TEXT("Binaries/ThirdParty/Mono/Mac/bin/mono"))); -#endif - - TryPushCommandLineArgsToDevice(Executable, DeploymentServerArguments, OutCommandLineArgsFile); - return FReply::Handled(); + 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 46dbe03e5f..8b47453cf9 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorModule.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorModule.cpp @@ -2,22 +2,31 @@ #include "SpatialGDKEditorModule.h" -#include "SpatialGDKSettings.h" -#include "SpatialGDKEditorSettings.h" -#include "SpatialGDKEditorLayoutDetails.h" - -#include "ISettingsModule.h" +#include "EditorExtension/GridLBStrategyEditorExtension.h" +#include "GeneralProjectSettings.h" #include "ISettingsContainer.h" +#include "ISettingsModule.h" #include "ISettingsSection.h" +#include "Misc/MessageDialog.h" #include "PropertyEditor/Public/PropertyEditorModule.h" +#include "SpatialCommandUtils.h" +#include "SpatialGDKEditor.h" +#include "SpatialGDKEditorCommandLineArgsManager.h" +#include "SpatialGDKEditorLayoutDetails.h" +#include "SpatialGDKEditorPackageAssembly.h" +#include "SpatialGDKEditorSchemaGenerator.h" +#include "SpatialGDKEditorSettings.h" +#include "SpatialGDKSettings.h" +#include "SpatialLaunchConfigCustomization.h" +#include "Utils/LaunchConfigurationEditor.h" +#include "SpatialRuntimeVersionCustomization.h" #include "WorkerTypeCustomization.h" -#include "EditorExtension/GridLBStrategyEditorExtension.h" - #define LOCTEXT_NAMESPACE "FSpatialGDKEditorModule" FSpatialGDKEditorModule::FSpatialGDKEditorModule() : ExtensionManager(MakeUnique()) + , CommandLineArgsManager(MakeUnique()) { } @@ -27,6 +36,8 @@ void FSpatialGDKEditorModule::StartupModule() RegisterSettings(); ExtensionManager->RegisterExtension(); + SpatialGDKEditorInstance = MakeShareable(new FSpatialGDKEditor()); + CommandLineArgsManager->Init(); } void FSpatialGDKEditorModule::ShutdownModule() @@ -39,6 +50,122 @@ void FSpatialGDKEditorModule::ShutdownModule() } } +bool FSpatialGDKEditorModule::ShouldConnectToLocalDeployment() const +{ + return GetDefault()->UsesSpatialNetworking() && GetDefault()->SpatialOSNetFlowType == ESpatialOSNetFlow::LocalDeployment; +} + +FString FSpatialGDKEditorModule::GetSpatialOSLocalDeploymentIP() const +{ + return GetDefault()->ExposedRuntimeIP; +} + +bool FSpatialGDKEditorModule::ShouldStartPIEClientsWithLocalLaunchOnDevice() const +{ + return GetDefault()->bStartPIEClientsWithLocalLaunchOnDevice; +} + +bool FSpatialGDKEditorModule::ShouldConnectToCloudDeployment() const +{ + return GetDefault()->UsesSpatialNetworking() && GetDefault()->SpatialOSNetFlowType == ESpatialOSNetFlow::CloudDeployment; +} + +FString FSpatialGDKEditorModule::GetDevAuthToken() const +{ + return GetDefault()->DevelopmentAuthenticationToken; +} + +FString FSpatialGDKEditorModule::GetSpatialOSCloudDeploymentName() const +{ + return GetDefault()->DevelopmentDeploymentToConnect; +} + +bool FSpatialGDKEditorModule::CanExecuteLaunch() const +{ + return SpatialGDKEditorInstance->GetPackageAssemblyRef()->CanBuild(); +} + +bool FSpatialGDKEditorModule::CanStartSession(FText& OutErrorMessage) const +{ + if (!SpatialGDKEditorInstance->IsSchemaGenerated()) + { + 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; + } + + if (ShouldConnectToCloudDeployment()) + { + 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."); + return false; + } + + const USpatialGDKEditorSettings* Settings = GetDefault(); + bool bIsRunningInChina = GetDefault()->IsRunningInChina(); + if (!Settings->DevelopmentDeploymentToConnect.IsEmpty() && !SpatialCommandUtils::HasDevLoginTag(Settings->DevelopmentDeploymentToConnect, bIsRunningInChina, OutErrorMessage)) + { + return false; + } + } + + return true; +} + +bool FSpatialGDKEditorModule::CanStartPlaySession(FText& OutErrorMessage) const +{ + if (!GetDefault()->UsesSpatialNetworking()) + { + return true; + } + + return CanStartSession(OutErrorMessage); +} + +bool FSpatialGDKEditorModule::CanStartLaunchSession(FText& OutErrorMessage) const +{ + if (!GetDefault()->UsesSpatialNetworking()) + { + return true; + } + + 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."); + return false; + } + + return CanStartSession(OutErrorMessage); +} + +FString FSpatialGDKEditorModule::GetMobileClientCommandLineArgs() const +{ + FString CommandLine; + if (ShouldConnectToLocalDeployment()) + { + CommandLine = FString::Printf(TEXT("%s -useExternalIpForBridge true"), *GetSpatialOSLocalDeploymentIP()); + } + else if (ShouldConnectToCloudDeployment()) + { + CommandLine = TEXT("connect.to.spatialos -devAuthToken ") + GetDevAuthToken(); + FString CloudDeploymentName = GetSpatialOSCloudDeploymentName(); + if (!CloudDeploymentName.IsEmpty()) + { + CommandLine += TEXT(" -deployment ") + CloudDeploymentName; + } + else + { + UE_LOG(LogTemp, 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; +} + +bool FSpatialGDKEditorModule::ShouldPackageMobileCommandLineArgs() const +{ + return GetDefault()->bPackageMobileCommandLineArgs; +} + void FSpatialGDKEditorModule::RegisterSettings() { if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr("Settings")) @@ -71,6 +198,8 @@ 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)); } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorPackageAssembly.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorPackageAssembly.cpp new file mode 100644 index 0000000000..82e546d0de --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorPackageAssembly.cpp @@ -0,0 +1,241 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialGDKEditorPackageAssembly.h" + +#include "Async/Async.h" +#include "Framework/Notifications/NotificationManager.h" +#include "Misc/App.h" +#include "Misc/FileHelper.h" +#include "Misc/MessageDialog.h" +#include "Misc/MonitoredProcess.h" +#include "UnrealEdMisc.h" + +#include "SpatialGDKEditorModule.h" +#include "SpatialGDKServicesConstants.h" +#include "SpatialGDKServicesModule.h" +#include "SpatialGDKSettings.h" + +DEFINE_LOG_CATEGORY(LogSpatialGDKEditorPackageAssembly); + +#define LOCTEXT_NAMESPACE "SpatialGDKEditorPackageAssembly" + +namespace +{ + 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) +{ + PackageAssemblyTask = MakeShareable(new FMonitoredProcess(Exe, Args, WorkingDir, /* Hidden */ true)); + PackageAssemblyTask->OnCompleted().BindSP(this, &FSpatialGDKPackageAssembly::OnTaskCompleted); + PackageAssemblyTask->OnOutput().BindSP(this, &FSpatialGDKPackageAssembly::OnTaskOutput); + PackageAssemblyTask->OnCanceled().BindSP(this, &FSpatialGDKPackageAssembly::OnTaskCanceled); + PackageAssemblyTask->Launch(); +} + +void FSpatialGDKPackageAssembly::BuildAssembly(const FString& ProjectName, 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); + LaunchTask(SpatialBuildExe, Args, WorkingDir); +} + +void FSpatialGDKPackageAssembly::UploadAssembly(const FString& AssemblyName, bool bForceAssemblyOverwrite) +{ + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + const FString& WorkingDir = SpatialGDKServicesConstants::SpatialOSDirectory; + FString Flags = TEXT("--no_animation"); + if (bForceAssemblyOverwrite) + { + Flags += TEXT(" --force"); + } + if (SpatialGDKSettings->IsRunningInChina()) + { + Flags += SpatialGDKServicesConstants::ChinaEnvironmentArgument; + } + FString Args = FString::Printf(TEXT("cloud upload %s %s"), *AssemblyName, *Flags); + LaunchTask(SpatialGDKServicesConstants::SpatialExe, Args, WorkingDir); +} + +void FSpatialGDKPackageAssembly::BuildAndUploadAssembly(const FCloudDeploymentConfiguration& InCloudDeploymentConfiguration) +{ + if (CanBuild()) + { + Status = EPackageAssemblyStatus::NONE; + CloudDeploymentConfiguration = InCloudDeploymentConfiguration; + + Steps.Enqueue(EPackageAssemblyStep::BUILD_SERVER); + if (CloudDeploymentConfiguration.bBuildClientWorker) + { + Steps.Enqueue(EPackageAssemblyStep::BUILD_CLIENT); + } + if (CloudDeploymentConfiguration.bSimulatedPlayersEnabled) + { + Steps.Enqueue(EPackageAssemblyStep::BUILD_SIMULATED_PLAYERS); + } + Steps.Enqueue(EPackageAssemblyStep::UPLOAD_ASSEMBLY); + + AsyncTask(ENamedThreads::GameThread, [this]() + { + ShowTaskStartedNotification(TEXT("Building Assembly")); + NextStep(); + }); + } +} + +bool FSpatialGDKPackageAssembly::CanBuild() const +{ + return Steps.IsEmpty(); +} + +bool FSpatialGDKPackageAssembly::NextStep() +{ + bool bHasStepsRemaining = false; + EPackageAssemblyStep Target = EPackageAssemblyStep::NONE; + if (Steps.Dequeue(Target)) + { + bHasStepsRemaining = true; + switch (Target) + { + case EPackageAssemblyStep::BUILD_SERVER: + 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); + }); + break; + case EPackageAssemblyStep::BUILD_SIMULATED_PLAYERS: + AsyncTask(ENamedThreads::GameThread, [this]() + { + BuildAssembly(FString::Printf(TEXT("%sSimulatedPlayer"), FApp::GetProjectName()), LinuxPlatform, CloudDeploymentConfiguration.BuildConfiguration, CloudDeploymentConfiguration.BuildSimulatedPlayerExtraArgs); + }); + break; + case EPackageAssemblyStep::UPLOAD_ASSEMBLY: + AsyncTask(ENamedThreads::GameThread, [this]() + { + UploadAssembly(CloudDeploymentConfiguration.AssemblyName, CloudDeploymentConfiguration.bForceAssemblyOverwrite); + }); + break; + default: + checkNoEntry(); + } + } + return bHasStepsRemaining; +} + +void FSpatialGDKPackageAssembly::OnTaskCompleted(int32 TaskResult) +{ + if (TaskResult == 0) + { + if (!NextStep()) + { + AsyncTask(ENamedThreads::GameThread, [this]() + { + FString NotificationMessage = FString::Printf(TEXT("Assembly successfully uploaded to project: %s"), *FSpatialGDKServicesModule::GetProjectName()); + ShowTaskEndedNotification(NotificationMessage, SNotificationItem::CS_Success); + OnSuccess.ExecuteIfBound(); + }); + } + } + else + { + 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("Assembly_Exists", "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("Bad_Project_Name", "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) + { + Status = EPackageAssemblyStatus::UNKNOWN_ERROR; + } + }); + Steps.Empty(); + } +} + +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 + if (Message.Find(TEXT("Either change the name or use the '--force' flag")) >= 0) + { + Status = EPackageAssemblyStatus::ASSEMBLY_EXISTS; + } + else if (Message.Find(TEXT("Make sure the project name is correct and you have permission to upload new assemblies")) >= 0) + { + Status = EPackageAssemblyStatus::BAD_PROJECT_NAME; + } + UE_LOG(LogSpatialGDKEditorPackageAssembly, Display, TEXT("%s"), *Message); +} + +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]() + { + ShowTaskEndedNotification(NotificationMessage, SNotificationItem::CS_Fail); + }); +} + +void FSpatialGDKPackageAssembly::HandleCancelButtonClicked() +{ + if (PackageAssemblyTask.IsValid()) + { + PackageAssemblyTask->Cancel(true); + } +} + +void FSpatialGDKPackageAssembly::ShowTaskStartedNotification(const FString& NotificationText) +{ + FNotificationInfo Info(FText::AsCultureInvariant(NotificationText)); + Info.ButtonDetails.Add( + FNotificationButtonInfo( + LOCTEXT("PackageAssemblyTaskCancel", "Cancel"), + LOCTEXT("PackageAssemblyTaskCancelToolTip", "Cancels execution of this task."), + FSimpleDelegate::CreateRaw(this, &FSpatialGDKPackageAssembly::HandleCancelButtonClicked), + SNotificationItem::CS_Pending + ) + ); + Info.ExpireDuration = 5.0f; + Info.bFireAndForget = false; + + TaskNotificationPtr = FSlateNotificationManager::Get().AddNotification(Info); + + if (TaskNotificationPtr.IsValid()) + { + TaskNotificationPtr.Pin()->SetCompletionState(SNotificationItem::CS_Pending); + } +} + +void FSpatialGDKPackageAssembly::ShowTaskEndedNotification(const FString& NotificationText, SNotificationItem::ECompletionState CompletionState) +{ + TSharedPtr Notification = TaskNotificationPtr.Pin(); + if (Notification.IsValid()) + { + Notification->SetFadeInDuration(0.1f); + Notification->SetFadeOutDuration(0.5f); + Notification->SetExpireDuration(5.0); + Notification->SetText(FText::AsCultureInvariant(NotificationText)); + Notification->SetCompletionState(CompletionState); + Notification->ExpireAndFadeout(); + } +} + +#undef LOCTEXT_NAMESPACE diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorSettings.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorSettings.cpp index 9b177a734f..3bc63dcd50 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorSettings.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorSettings.cpp @@ -2,31 +2,39 @@ #include "SpatialGDKEditorSettings.h" +#include "Interfaces/ITargetPlatformManagerModule.h" #include "Internationalization/Regex.h" #include "ISettingsModule.h" #include "Misc/FileHelper.h" #include "Misc/MessageDialog.h" #include "Modules/ModuleManager.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonSerializer.h" #include "Settings/LevelEditorPlaySettings.h" #include "Templates/SharedPointer.h" + #include "SpatialConstants.h" #include "SpatialGDKSettings.h" -#include "Serialization/JsonReader.h" -#include "Serialization/JsonSerializer.h" - DEFINE_LOG_CATEGORY(LogSpatialEditorSettings); #define LOCTEXT_NAMESPACE "USpatialGDKEditorSettings" -void FSpatialLaunchConfigDescription::SetLevelEditorPlaySettingsWorkerTypes() +const FString& FRuntimeVariantVersion::GetVersionForLocal() const { - ULevelEditorPlaySettings* PlayInSettings = GetMutableDefault(); + if (bUseGDKPinnedRuntimeVersionForLocal || LocalRuntimeVersion.IsEmpty()) + { + return PinnedVersion; + } + return LocalRuntimeVersion; +} - PlayInSettings->WorkerTypesToLaunch.Empty(ServerWorkers.Num()); - for (const FWorkerTypeLaunchSection& WorkerLaunch : ServerWorkers) +const FString& FRuntimeVariantVersion::GetVersionForCloud() const +{ + if (bUseGDKPinnedRuntimeVersionForCloud || CloudRuntimeVersion.IsEmpty()) { - PlayInSettings->WorkerTypesToLaunch.Add(WorkerLaunch.WorkerTypeName, WorkerLaunch.NumEditorInstances); + return PinnedVersion; } + return CloudRuntimeVersion; } USpatialGDKEditorSettings::USpatialGDKEditorSettings(const FObjectInitializer& ObjectInitializer) @@ -34,37 +42,43 @@ USpatialGDKEditorSettings::USpatialGDKEditorSettings(const FObjectInitializer& O , bShowSpatialServiceButton(false) , bDeleteDynamicEntities(true) , bGenerateDefaultLaunchConfig(true) - , bUseGDKPinnedRuntimeVersion(true) - , bExposeRuntimeIP(false) + , RuntimeVariant(ESpatialOSRuntimeVariant::Standard) + , StandardRuntimeVersion(SpatialGDKServicesConstants::SpatialOSRuntimePinnedStandardVersion) + , CompatibilityModeRuntimeVersion(SpatialGDKServicesConstants::SpatialOSRuntimePinnedCompatbilityModeVersion) , ExposedRuntimeIP(TEXT("")) , bStopSpatialOnExit(false) , bAutoStartLocalDeployment(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"))) - , bUseDevelopmentAuthenticationFlow(false) + , bBuildAndUploadAssembly(true) + , AssemblyBuildConfiguration(TEXT("Development")) , SimulatedPlayerDeploymentRegionCode(ERegionCode::US) + , bPackageMobileCommandLineArgs(false) + , bStartPIEClientsWithLocalLaunchOnDevice(false) + , SpatialOSNetFlowType(ESpatialOSNetFlow::LocalDeployment) { SpatialOSLaunchConfig.FilePath = GetSpatialOSLaunchConfig(); SpatialOSSnapshotToSave = GetSpatialOSSnapshotToSave(); SpatialOSSnapshotToLoad = GetSpatialOSSnapshotToLoad(); + SnapshotPath.FilePath = GetSpatialOSSnapshotToSavePath(); } -const FString& USpatialGDKEditorSettings::GetSpatialOSRuntimeVersionForLocal() const +FRuntimeVariantVersion& USpatialGDKEditorSettings::GetRuntimeVariantVersion(ESpatialOSRuntimeVariant::Type Variant) { - if (bUseGDKPinnedRuntimeVersion || LocalRuntimeVersion.IsEmpty()) - { - return SpatialGDKServicesConstants::SpatialOSRuntimePinnedVersion; - } - return LocalRuntimeVersion; -} +#if PLATFORM_MAC + return CompatibilityModeRuntimeVersion; +#endif -const FString& USpatialGDKEditorSettings::GetSpatialOSRuntimeVersionForCloud() const -{ - if (bUseGDKPinnedRuntimeVersion || CloudRuntimeVersion.IsEmpty()) + switch (Variant) { - return SpatialGDKServicesConstants::SpatialOSRuntimePinnedVersion; + case ESpatialOSRuntimeVariant::CompatibilityMode: + return CompatibilityModeRuntimeVersion; + default: + return StandardRuntimeVersion; } - return CloudRuntimeVersion; } void USpatialGDKEditorSettings::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) @@ -82,21 +96,18 @@ void USpatialGDKEditorSettings::PostEditChangeProperty(struct FPropertyChangedEv PlayInSettings->PostEditChange(); PlayInSettings->SaveConfig(); } - else if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, LaunchConfigDesc)) - { - SetRuntimeWorkerTypes(); - } - else if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, bUseDevelopmentAuthenticationFlow)) - { - SetRuntimeUseDevelopmentAuthenticationFlow(); - } - else if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, DevelopmentAuthenticationToken)) + + if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, RuntimeVariant)) { - SetRuntimeDevelopmentAuthenticationToken(); + FSpatialGDKServicesModule& GDKServices = FModuleManager::GetModuleChecked("SpatialGDKServices"); + GDKServices.GetLocalDeploymentManager()->SetRedeployRequired(); + + OnDefaultTemplateNameRequireUpdate.Broadcast(); } - else if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, DevelopmentDeploymentToConnect)) + + if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, PrimaryDeploymentRegionCode)) { - SetRuntimeDevelopmentDeploymentToConnect(); + OnDefaultTemplateNameRequireUpdate.Broadcast(); } } @@ -109,50 +120,7 @@ void USpatialGDKEditorSettings::PostInitProperties() PlayInSettings->PostEditChange(); PlayInSettings->SaveConfig(); - SetRuntimeWorkerTypes(); - SetRuntimeUseDevelopmentAuthenticationFlow(); - SetRuntimeDevelopmentAuthenticationToken(); - SetRuntimeDevelopmentDeploymentToConnect(); -} - -void USpatialGDKEditorSettings::SetRuntimeWorkerTypes() -{ - TSet WorkerTypes; - - for (const FWorkerTypeLaunchSection& WorkerLaunch : LaunchConfigDesc.ServerWorkers) - { - if (WorkerLaunch.WorkerTypeName != NAME_None) - { - WorkerTypes.Add(WorkerLaunch.WorkerTypeName); - } - } - - USpatialGDKSettings* RuntimeSettings = GetMutableDefault(); - if (RuntimeSettings != nullptr) - { - RuntimeSettings->ServerWorkerTypes.Empty(WorkerTypes.Num()); - RuntimeSettings->ServerWorkerTypes.Append(WorkerTypes); - RuntimeSettings->PostEditChange(); - RuntimeSettings->UpdateSinglePropertyInConfigFile(RuntimeSettings->GetClass()->FindPropertyByName(GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, ServerWorkerTypes)), RuntimeSettings->GetDefaultConfigFilename()); - } -} - -void USpatialGDKEditorSettings::SetRuntimeUseDevelopmentAuthenticationFlow() -{ - USpatialGDKSettings* RuntimeSettings = GetMutableDefault(); - RuntimeSettings->bUseDevelopmentAuthenticationFlow = bUseDevelopmentAuthenticationFlow; -} - -void USpatialGDKEditorSettings::SetRuntimeDevelopmentAuthenticationToken() -{ - USpatialGDKSettings* RuntimeSettings = GetMutableDefault(); - RuntimeSettings->DevelopmentAuthenticationToken = DevelopmentAuthenticationToken; -} - -void USpatialGDKEditorSettings::SetRuntimeDevelopmentDeploymentToConnect() -{ - USpatialGDKSettings* RuntimeSettings = GetMutableDefault(); - RuntimeSettings->DevelopmentDeploymentToConnect = DevelopmentDeploymentToConnect; + const USpatialGDKSettings* GDKSettings = GetDefault(); } bool USpatialGDKEditorSettings::IsAssemblyNameValid(const FString& Name) @@ -181,6 +149,13 @@ bool USpatialGDKEditorSettings::IsDeploymentNameValid(const FString& Name) bool USpatialGDKEditorSettings::IsRegionCodeValid(const ERegionCode::Type RegionCode) { + // Selecting CN region code in the Cloud Deployment Configuration window has been deprecated. + // It will now be automatically determined based on the services region. + if (RegionCode == ERegionCode::CN) + { + return false; + } + UEnum* pEnum = FindObject(ANY_PACKAGE, TEXT("ERegionCode"), true); return pEnum != nullptr && pEnum->IsValidEnumValue(RegionCode); @@ -214,18 +189,45 @@ void USpatialGDKEditorSettings::SetPrimaryLaunchConfigPath(const FString& Path) void USpatialGDKEditorSettings::SetSnapshotPath(const FString& Path) { - SnapshotPath.FilePath = FPaths::ConvertRelativePathToFull(Path); + // If a non-empty path is specified, convert it to full, otherwise just empty the field. + SnapshotPath.FilePath = Path.IsEmpty() ? TEXT("") : FPaths::ConvertRelativePathToFull(Path); SaveConfig(); } void USpatialGDKEditorSettings::SetPrimaryRegionCode(const ERegionCode::Type RegionCode) { PrimaryDeploymentRegionCode = RegionCode; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetMainDeploymentCluster(const FString& NewCluster) +{ + MainDeploymentCluster = NewCluster; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetDeploymentTags(const FString& Tags) +{ + DeploymentTags = Tags; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetAssemblyBuildConfiguration(const FString& Configuration) +{ + AssemblyBuildConfiguration = Configuration; + SaveConfig(); } void USpatialGDKEditorSettings::SetSimulatedPlayerRegionCode(const ERegionCode::Type RegionCode) { SimulatedPlayerDeploymentRegionCode = RegionCode; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetSimulatedPlayerCluster(const FString& NewCluster) +{ + SimulatedPlayerCluster = NewCluster; + SaveConfig(); } void USpatialGDKEditorSettings::SetSimulatedPlayersEnabledState(bool IsEnabled) @@ -234,15 +236,59 @@ void USpatialGDKEditorSettings::SetSimulatedPlayersEnabledState(bool IsEnabled) SaveConfig(); } -void USpatialGDKEditorSettings::SetUseGDKPinnedRuntimeVersion(bool Use) +void USpatialGDKEditorSettings::SetAutoGenerateCloudLaunchConfigEnabledState(bool IsEnabled) { - bUseGDKPinnedRuntimeVersion = Use; + bIsAutoGenerateCloudConfigEnabled = IsEnabled; SaveConfig(); } -void USpatialGDKEditorSettings::SetCustomCloudSpatialOSRuntimeVersion(const FString& Version) +void USpatialGDKEditorSettings::SetBuildAndUploadAssembly(bool bBuildAndUpload) { - CloudRuntimeVersion = Version; + bBuildAndUploadAssembly = bBuildAndUpload; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetForceAssemblyOverwrite(bool bForce) +{ + bForceAssemblyOverwrite = bForce; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetBuildClientWorker(bool bBuild) +{ + bBuildClientWorker = bBuild; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetGenerateSchema(bool bGenerate) +{ + bGenerateSchema = bGenerate; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetGenerateSnapshot(bool bGenerate) +{ + bGenerateSnapshot = bGenerate; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetUseGDKPinnedRuntimeVersionForLocal(ESpatialOSRuntimeVariant::Type Variant, bool bUse) +{ + GetRuntimeVariantVersion(Variant).bUseGDKPinnedRuntimeVersionForLocal = bUse; + SaveConfig(); + FSpatialGDKServicesModule& GDKServices = FModuleManager::GetModuleChecked("SpatialGDKServices"); + GDKServices.GetLocalDeploymentManager()->SetRedeployRequired(); +} + +void USpatialGDKEditorSettings::SetUseGDKPinnedRuntimeVersionForCloud(ESpatialOSRuntimeVariant::Type Variant, bool bUse) +{ + GetRuntimeVariantVersion(Variant).bUseGDKPinnedRuntimeVersionForCloud = bUse; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetCustomCloudSpatialOSRuntimeVersion(ESpatialOSRuntimeVariant::Type Variant, const FString& Version) +{ + GetRuntimeVariantVersion(Variant).CloudRuntimeVersion = Version; SaveConfig(); } @@ -326,14 +372,19 @@ bool USpatialGDKEditorSettings::IsManualWorkerConnectionSet(const FString& Launc bool USpatialGDKEditorSettings::IsDeploymentConfigurationValid() const { bool bValid = true; + if (!IsProjectNameValid(FSpatialGDKServicesModule::GetProjectName())) + { + UE_LOG(LogSpatialEditorSettings, Error, TEXT("Project name is invalid. %s"), *SpatialConstants::ProjectPatternHint); + bValid = false; + } if (!IsAssemblyNameValid(AssemblyName)) { - UE_LOG(LogSpatialEditorSettings, Error, TEXT("Assembly name is invalid. It should match the regex: %s"), *SpatialConstants::AssemblyPattern); + UE_LOG(LogSpatialEditorSettings, Error, TEXT("Assembly name is invalid. %s"), *SpatialConstants::AssemblyPatternHint); bValid = false; } if (!IsDeploymentNameValid(PrimaryDeploymentName)) { - UE_LOG(LogSpatialEditorSettings, Error, TEXT("Deployment name is invalid. It should match the regex: %s"), *SpatialConstants::DeploymentPattern); + UE_LOG(LogSpatialEditorSettings, Error, TEXT("Deployment name is invalid. %s"), *SpatialConstants::DeploymentPatternHint); bValid = false; } if (!IsRegionCodeValid(PrimaryDeploymentRegionCode)) @@ -346,7 +397,7 @@ bool USpatialGDKEditorSettings::IsDeploymentConfigurationValid() const UE_LOG(LogSpatialEditorSettings, Error, TEXT("Snapshot path cannot be empty.")); bValid = false; } - if (GetPrimaryLaunchConfigPath().IsEmpty()) + if (GetPrimaryLaunchConfigPath().IsEmpty() && !bIsAutoGenerateCloudConfigEnabled) { UE_LOG(LogSpatialEditorSettings, Error, TEXT("Launch config path cannot be empty.")); bValid = false; @@ -356,7 +407,7 @@ bool USpatialGDKEditorSettings::IsDeploymentConfigurationValid() const { if (!IsDeploymentNameValid(SimulatedPlayerDeploymentName)) { - UE_LOG(LogSpatialEditorSettings, Error, TEXT("Simulated player deployment name is invalid. It should match the regex: %s"), *SpatialConstants::DeploymentPattern); + UE_LOG(LogSpatialEditorSettings, Error, TEXT("Simulated player deployment name is invalid. %s"), *SpatialConstants::DeploymentPatternHint); bValid = false; } if (!IsRegionCodeValid(SimulatedPlayerDeploymentRegionCode)) @@ -389,3 +440,77 @@ bool USpatialGDKEditorSettings::IsDeploymentConfigurationValid() const return bValid; } + +void USpatialGDKEditorSettings::SetDevelopmentAuthenticationToken(const FString& Token) +{ + DevelopmentAuthenticationToken = Token; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetDevelopmentDeploymentToConnect(const FString& Deployment) +{ + DevelopmentDeploymentToConnect = Deployment; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetExposedRuntimeIP(const FString& RuntimeIP) +{ + ExposedRuntimeIP = RuntimeIP; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetSpatialOSNetFlowType(ESpatialOSNetFlow::Type NetFlowType) +{ + SpatialOSNetFlowType = NetFlowType; + SaveConfig(); +} + +FString USpatialGDKEditorSettings::GetCookAndGenerateSchemaTargetPlatform() const +{ + if (!CookAndGeneratePlatform.IsEmpty()) + { + return CookAndGeneratePlatform; + } + + // Return current Editor's Build variant as default. + return FPlatformProcess::GetBinariesSubdirectory(); +} + +const FString& FSpatialLaunchConfigDescription::GetTemplate() const +{ + if (bUseDefaultTemplateForRuntimeVariant) + { + return GetDefaultTemplateForRuntimeVariant(); + } + + return Template; +} + +const FString& FSpatialLaunchConfigDescription::GetDefaultTemplateForRuntimeVariant() const +{ +#if PLATFORM_MAC + switch (ESpatialOSRuntimeVariant::CompatibilityMode) +#else + switch (GetDefault()->GetSpatialOSRuntimeVariant()) +#endif + { + case ESpatialOSRuntimeVariant::CompatibilityMode: + if (GetDefault()->IsRunningInChina()) + { + return SpatialGDKServicesConstants::PinnedChinaCompatibilityModeRuntimeTemplate; + } + else + { + return SpatialGDKServicesConstants::PinnedCompatibilityModeRuntimeTemplate; + } + default: + if (GetDefault()->IsRunningInChina()) + { + return SpatialGDKServicesConstants::PinnedChinaStandardRuntimeTemplate; + } + else + { + return SpatialGDKServicesConstants::PinnedStandardRuntimeTemplate; + } + } +} diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialLaunchConfigCustomization.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialLaunchConfigCustomization.cpp new file mode 100644 index 0000000000..54ec7a2a0d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialLaunchConfigCustomization.cpp @@ -0,0 +1,89 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialLaunchConfigCustomization.h" + +#include "SpatialGDKSettings.h" +#include "SpatialGDKEditorSettings.h" + +#include "IDetailChildrenBuilder.h" +#include "IDetailGroup.h" +#include "PropertyCustomizationHelpers.h" +#include "PropertyHandle.h" +#include "Widgets/SToolTip.h" +#include "Widgets/Text/STextBlock.h" + +TSharedRef FSpatialLaunchConfigCustomization::MakeInstance() +{ + return MakeShared(); +} + +void FSpatialLaunchConfigCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, class FDetailWidgetRow& HeaderRow, 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); + + if (EditedObject.Num() == 0) + { + return; + } + + const bool bIsInSettings = Cast(EditedObject[0]) != nullptr; + + uint32 NumChildren; + StructPropertyHandle->GetNumChildren(NumChildren); + for (uint32 ChildIdx = 0; ChildIdx < NumChildren; ++ChildIdx) + { + TSharedPtr ChildProperty = StructPropertyHandle->GetChildHandle(ChildIdx); + + if (ChildProperty->GetProperty()->GetFName() == PinnedGDKRuntimeLocalPropertyName) + { + // Place the pinned template name for this runtime variant in the pinned template field. + + void* StructPtr; + check(StructPropertyHandle->GetValueData(StructPtr) == FPropertyAccess::Success); + + const FSpatialLaunchConfigDescription* LaunchConfigDesc = reinterpret_cast(StructPtr); + + FString PinnedTemplateDisplay = FString::Printf(TEXT("Default: %s"), *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(FText::FromString(PinnedTemplateDisplay)) + ] + ]; + } + else + { + // Layout regular properties as usual. + StructBuilder.AddProperty(ChildProperty.ToSharedRef()); + continue; + } + } +} diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialRuntimeLoadBalancingStrategies.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialRuntimeLoadBalancingStrategies.cpp new file mode 100644 index 0000000000..cde7872c55 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialRuntimeLoadBalancingStrategies.cpp @@ -0,0 +1,33 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialRuntimeLoadBalancingStrategies.h" + +USingleWorkerRuntimeStrategy::USingleWorkerRuntimeStrategy() = default; + +int32 USingleWorkerRuntimeStrategy::GetNumberOfWorkersForPIE() const +{ + return 1; +} + +UGridRuntimeLoadBalancingStrategy::UGridRuntimeLoadBalancingStrategy() + : Columns(1) + , Rows(1) +{ + +} + +int32 UGridRuntimeLoadBalancingStrategy::GetNumberOfWorkersForPIE() const +{ + return Rows * Columns; +} + +UEntityShardingRuntimeLoadBalancingStrategy::UEntityShardingRuntimeLoadBalancingStrategy() + : NumWorkers(1) +{ + +} + +int32 UEntityShardingRuntimeLoadBalancingStrategy::GetNumberOfWorkersForPIE() const +{ + return NumWorkers; +} diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialRuntimeVersionCustomization.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialRuntimeVersionCustomization.cpp new file mode 100644 index 0000000000..159ecd3391 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialRuntimeVersionCustomization.cpp @@ -0,0 +1,80 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialRuntimeVersionCustomization.h" + +#include "SpatialGDKEditorSettings.h" + +#include "DetailWidgetRow.h" +#include "IDetailChildrenBuilder.h" +#include "IDetailPropertyRow.h" +#include "PropertyHandle.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/Text/STextBlock.h" + +TSharedRef FSpatialRuntimeVersionCustomization::MakeInstance() +{ + return MakeShareable(new FSpatialRuntimeVersionCustomization); +} + +void FSpatialRuntimeVersionCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + HeaderRow + .NameContent() + [ + StructPropertyHandle->CreatePropertyNameWidget() + ]; +} + +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); + + uint32 NumChildren; + StructPropertyHandle->GetNumChildren(NumChildren); + + for (uint32 ChildIdx = 0; ChildIdx < NumChildren; ++ChildIdx) + { + TSharedPtr ChildProperty = StructPropertyHandle->GetChildHandle(ChildIdx); + + // Layout other properties as usual. + if (ChildProperty->GetProperty()->GetFName() != PinnedGDKRuntimeLocalPropertyName && ChildProperty->GetProperty()->GetFName() != PinnedGDKRuntimeCloudPropertyName) + { + StructBuilder.AddProperty(ChildProperty.ToSharedRef()); + continue; + } + + void* StructPtr; + check(StructPropertyHandle->GetValueData(StructPtr) == FPropertyAccess::Success); + + const FRuntimeVariantVersion* VariantVersion = reinterpret_cast(StructPtr); + + IDetailPropertyRow& CustomRow = StructBuilder.AddProperty(ChildProperty.ToSharedRef()); + + FString PinnedVersionDisplay = FString::Printf(TEXT("GDK Pinned Version : %s"), *VariantVersion->GetPinnedVersion()); + + 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)) + ] + ]; + } +} diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/LaunchConfigEditor.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/LaunchConfigEditor.cpp deleted file mode 100644 index f172539eef..0000000000 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/LaunchConfigEditor.cpp +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#include "Utils/LaunchConfigEditor.h" - -#include "DesktopPlatformModule.h" -#include "Framework/Application/SlateApplication.h" -#include "IDesktopPlatform.h" -#include "SpatialGDKDefaultLaunchConfigGenerator.h" - -void ULaunchConfigurationEditor::SaveConfiguration() -{ - IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get(); - - FString DefaultOutPath = SpatialGDKServicesConstants::SpatialOSDirectory; - TArray Filenames; - - bool bSaved = DesktopPlatform->SaveFileDialog( - FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr), - TEXT("Save launch configuration"), - DefaultOutPath, - TEXT(""), - TEXT("JSON Configuration|*.json"), - EFileDialogFlags::None, - Filenames); - - if (bSaved && Filenames.Num() > 0) - { - GenerateDefaultLaunchConfig(Filenames[0], &LaunchConfiguration); - } -} diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/LaunchConfigurationEditor.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/LaunchConfigurationEditor.cpp new file mode 100644 index 0000000000..f1951ab55c --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/LaunchConfigurationEditor.cpp @@ -0,0 +1,184 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Utils/LaunchConfigurationEditor.h" + +#include "DesktopPlatformModule.h" +#include "Framework/Application/SlateApplication.h" +#include "IDesktopPlatform.h" +#include "MainFrame/Public/Interfaces/IMainFrameModule.h" +#include "PropertyEditor/Public/PropertyEditorModule.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Layout/SBorder.h" + +#include "SpatialGDKDefaultLaunchConfigGenerator.h" +#include "SpatialGDKSettings.h" +#include "SpatialRuntimeLoadBalancingStrategies.h" + +void ULaunchConfigurationEditor::PostInitProperties() +{ + Super::PostInitProperties(); + + const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); + + LaunchConfiguration = SpatialGDKEditorSettings->LaunchConfigDesc; + FillWorkerConfigurationFromCurrentMap(LaunchConfiguration.ServerWorkerConfig, LaunchConfiguration.World.Dimensions); +} + +void ULaunchConfigurationEditor::SaveConfiguration() +{ + if (!ValidateGeneratedLaunchConfig(LaunchConfiguration, LaunchConfiguration.ServerWorkerConfig)) + { + return; + } + + IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get(); + + FString DefaultOutPath = SpatialGDKServicesConstants::SpatialOSDirectory; + TArray Filenames; + + bool bSaved = DesktopPlatform->SaveFileDialog( + FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr), + TEXT("Save launch configuration"), + DefaultOutPath, + TEXT(""), + TEXT("JSON Configuration|*.json"), + EFileDialogFlags::None, + Filenames); + + if (bSaved && Filenames.Num() > 0) + { + if (GenerateLaunchConfig(Filenames[0], &LaunchConfiguration, LaunchConfiguration.ServerWorkerConfig)) + { + OnConfigurationSaved.ExecuteIfBound(Filenames[0]); + } + } +} + +namespace +{ + // Copied from FPropertyEditorModule::CreateFloatingDetailsView. + bool ShouldShowProperty(const FPropertyAndParent& PropertyAndParent, bool bHaveTemplate) + { + const UProperty& Property = PropertyAndParent.Property; + + if (bHaveTemplate) + { + const UClass* PropertyOwnerClass = Cast(Property.GetOuter()); + const bool bDisableEditOnTemplate = PropertyOwnerClass + && PropertyOwnerClass->IsNative() + && Property.HasAnyPropertyFlags(CPF_DisableEditOnTemplate); + + if (bDisableEditOnTemplate) + { + return false; + } + } + return true; + } + + FReply ExecuteEditorCommand(ULaunchConfigurationEditor* Instance, UFunction* MethodToExecute) + { + Instance->CallFunctionByNameWithArguments(*MethodToExecute->GetName(), *GLog, nullptr, true); + + return FReply::Handled(); + } +} + +void ULaunchConfigurationEditor::OpenModalWindow(TSharedPtr InParentWindow, OnLaunchConfigurationSaved InSaved) +{ + ULaunchConfigurationEditor* ObjectInstance = NewObject(GetTransientPackage(), ULaunchConfigurationEditor::StaticClass()); + ObjectInstance->AddToRoot(); + + FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked("PropertyEditor"); + + TArray ObjectsToView; + ObjectsToView.Add(ObjectInstance); + + FDetailsViewArgs Args; + Args.bHideSelectionTip = true; + Args.bLockable = false; + Args.bAllowSearch = false; + Args.bShowPropertyMatrixButton = false; + + TSharedRef DetailView = PropertyEditorModule.CreateDetailView(Args); + + bool bHaveTemplate = false; + for (int32 i = 0; i < ObjectsToView.Num(); i++) + { + if (ObjectsToView[i] != NULL && ObjectsToView[i]->IsTemplate()) + { + bHaveTemplate = true; + break; + } + } + + DetailView->SetIsPropertyVisibleDelegate(FIsPropertyVisible::CreateStatic(&ShouldShowProperty, bHaveTemplate)); + + DetailView->SetObjects(ObjectsToView); + + TSharedRef VBoxBuilder = SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + .FillHeight(1.0) + [ + DetailView + ]; + + // Add UFunction marked Exec as buttons in the editor's window + for (TFieldIterator FuncIt(ULaunchConfigurationEditor::StaticClass()); FuncIt; ++FuncIt) + { + UFunction* Function = *FuncIt; + if (Function->HasAnyFunctionFlags(FUNC_Exec) && (Function->NumParms == 0)) + { + const FText ButtonCaption = Function->GetDisplayNameText(); + + VBoxBuilder->AddSlot() + .AutoHeight() + .VAlign(VAlign_Bottom) + .HAlign(HAlign_Right) + .Padding(2.0) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0) + [ + SNew(SButton) + .Text(ButtonCaption) + .OnClicked(FOnClicked::CreateStatic(&ExecuteEditorCommand, ObjectInstance, Function)) + ] + ]; + } + } + + TSharedRef NewSlateWindow = SNew(SWindow) + .Title(FText::FromString(TEXT("Launch Configuration Editor"))) + .ClientSize(FVector2D(600, 400)) + [ + SNew(SBorder) + .BorderImage(FEditorStyle::GetBrush(TEXT("PropertyWindow.WindowBorder"))) + [ + VBoxBuilder + ] + ]; + + if (!InParentWindow.IsValid() && FModuleManager::Get().IsModuleLoaded("MainFrame")) + { + // If the main frame exists parent the window to it + IMainFrameModule& MainFrame = FModuleManager::GetModuleChecked("MainFrame"); + InParentWindow = MainFrame.GetParentWindow(); + } + if (InSaved != nullptr) + { + ObjectInstance->OnConfigurationSaved.BindLambda(InSaved); + } + + if (InParentWindow.IsValid()) + { + FSlateApplication::Get().AddModalWindow(NewSlateWindow, InParentWindow.ToSharedRef()); + } + else + { + FSlateApplication::Get().AddModalWindow(NewSlateWindow, nullptr); + } +} diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/TransientUObjectEditor.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/TransientUObjectEditor.cpp index 14a097072a..bd2c308542 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/TransientUObjectEditor.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/Utils/TransientUObjectEditor.cpp @@ -47,16 +47,16 @@ namespace } // Rewrite of FPropertyEditorModule::CreateFloatingDetailsView to use the detail property view in a new window. -void UTransientUObjectEditor::LaunchTransientUObjectEditor(const FString& EditorName, UClass* ObjectClass) +UTransientUObjectEditor* UTransientUObjectEditor::LaunchTransientUObjectEditor(const FString& EditorName, UClass* ObjectClass, TSharedPtr ParentWindow) { if (!ObjectClass) { - return; + return nullptr; } if (!ObjectClass->IsChildOf()) { - return; + return nullptr; } UTransientUObjectEditor* ObjectInstance = NewObject(GetTransientPackage(), ObjectClass); @@ -133,18 +133,16 @@ void UTransientUObjectEditor::LaunchTransientUObjectEditor(const FString& Editor VBoxBuilder ] ]; - - // If the main frame exists parent the window to it - TSharedPtr ParentWindow; - if (FModuleManager::Get().IsModuleLoaded("MainFrame")) + + if (!ParentWindow.IsValid() && FModuleManager::Get().IsModuleLoaded("MainFrame")) { + // If the main frame exists parent the window to it IMainFrameModule& MainFrame = FModuleManager::GetModuleChecked("MainFrame"); ParentWindow = MainFrame.GetParentWindow(); } if (ParentWindow.IsValid()) { - // Parent the window to the main frame FSlateApplication::Get().AddWindowAsNativeChild(NewSlateWindow, ParentWindow.ToSharedRef()); } else @@ -157,4 +155,6 @@ void UTransientUObjectEditor::LaunchTransientUObjectEditor(const FString& Editor 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 da61005167..bc39b3053f 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/WorkerTypeCustomization.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/WorkerTypeCustomization.cpp @@ -41,12 +41,9 @@ void FWorkerTypeCustomization::OnGetStrings(TArray>& OutComb { if (const USpatialGDKSettings* Settings = GetDefault()) { - for (const FName& WorkerType : Settings->ServerWorkerTypes) - { - OutComboBoxStrings.Add(MakeShared(WorkerType.ToString())); - OutToolTips.Add(SNew(SToolTip).Text(FText::FromName(WorkerType))); - OutRestrictedItems.Add(false); - } + OutComboBoxStrings.Add(MakeShared(SpatialConstants::DefaultServerWorkerType.ToString())); + OutToolTips.Add(SNew(SToolTip).Text(FText::FromName(SpatialConstants::DefaultServerWorkerType))); + OutRestrictedItems.Add(false); } } @@ -64,7 +61,7 @@ FString FWorkerTypeCustomization::OnGetValue(TSharedPtr WorkerT WorkerTypeNameHandle->GetValue(WorkerTypeValue); const FName WorkerTypeName = FName(*WorkerTypeValue); - return Settings->ServerWorkerTypes.Contains(WorkerTypeName) ? WorkerTypeValue : TEXT("INVALID"); + return WorkerTypeName == SpatialConstants::DefaultServerWorkerType ? WorkerTypeValue : TEXT("INVALID"); } return WorkerTypeValue; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/CloudDeploymentConfiguration.h b/SpatialGDK/Source/SpatialGDKEditor/Public/CloudDeploymentConfiguration.h new file mode 100644 index 0000000000..b9d72153ed --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/CloudDeploymentConfiguration.h @@ -0,0 +1,43 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Containers/UnrealString.h" + +/** + * This struct is used to save all the fields needed to build, upload, and start a cloud deployment. + * This lets the user continue to modify the settings without affecting the deployment that is being prepared. + */ +struct SPATIALGDKEDITOR_API FCloudDeploymentConfiguration +{ + void InitFromSettings(); + + FString AssemblyName; + FString RuntimeVersion; + FString PrimaryDeploymentName; + FString PrimaryLaunchConfigPath; + FString SnapshotPath; + FString PrimaryRegionCode; + FString MainDeploymentCluster; + FString DeploymentTags; + + bool bSimulatedPlayersEnabled = false; + FString SimulatedPlayerDeploymentName; + FString SimulatedPlayerLaunchConfigPath; + FString SimulatedPlayerRegionCode; + FString SimulatedPlayerCluster; + uint32 NumberOfSimulatedPlayers = 0; + + bool bBuildAndUploadAssembly = false; + bool bGenerateSchema = false; + bool bGenerateSnapshot = false; + FString BuildConfiguration; + bool bBuildClientWorker = false; + bool bForceAssemblyOverwrite = false; + + FString BuildServerExtraArgs; + FString BuildClientExtraArgs; + FString BuildSimulatedPlayerExtraArgs; + + bool bUseChinaPlatform = false; +}; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/EditorExtension/LBStrategyEditorExtension.h b/SpatialGDK/Source/SpatialGDKEditor/Public/EditorExtension/LBStrategyEditorExtension.h index 1006fb0b4b..232c4a8e79 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/EditorExtension/LBStrategyEditorExtension.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/EditorExtension/LBStrategyEditorExtension.h @@ -4,8 +4,11 @@ #include "CoreMinimal.h" +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKEditorLBExtension, Log, All); + class UAbstractLBStrategy; class FLBStrategyEditorExtensionManager; +class UAbstractRuntimeLoadBalancingStrategy; struct FWorkerTypeLaunchSection; class FLBStrategyEditorExtensionInterface @@ -14,7 +17,7 @@ class FLBStrategyEditorExtensionInterface virtual ~FLBStrategyEditorExtensionInterface() {} private: friend FLBStrategyEditorExtensionManager; - virtual bool GetDefaultLaunchConfiguration_Virtual(const UAbstractLBStrategy* Strategy, FWorkerTypeLaunchSection& OutConfiguration, FIntPoint& OutWorldDimensions) const = 0; + virtual bool GetDefaultLaunchConfiguration_Virtual(const UAbstractLBStrategy* Strategy, UAbstractRuntimeLoadBalancingStrategy*& OutConfiguration, FIntPoint& OutWorldDimensions) const = 0; }; template @@ -24,7 +27,7 @@ class FLBStrategyEditorExtensionTemplate : public FLBStrategyEditorExtensionInte using ExtendedStrategy = StrategyImpl; private: - bool GetDefaultLaunchConfiguration_Virtual(const UAbstractLBStrategy* Strategy, FWorkerTypeLaunchSection& OutConfiguration, FIntPoint& OutWorldDimensions) const override + bool GetDefaultLaunchConfiguration_Virtual(const UAbstractLBStrategy* Strategy, UAbstractRuntimeLoadBalancingStrategy*& OutConfiguration, FIntPoint& OutWorldDimensions) const override { return static_cast(this)->GetDefaultLaunchConfiguration(static_cast(Strategy), OutConfiguration, OutWorldDimensions); } @@ -33,7 +36,7 @@ class FLBStrategyEditorExtensionTemplate : public FLBStrategyEditorExtensionInte class FLBStrategyEditorExtensionManager { public: - SPATIALGDKEDITOR_API bool GetDefaultLaunchConfiguration(const UAbstractLBStrategy* Strategy, FWorkerTypeLaunchSection& OutConfiguration, FIntPoint& OutWorldDimensions) const; + SPATIALGDKEDITOR_API bool GetDefaultLaunchConfiguration(const UAbstractLBStrategy* Strategy, UAbstractRuntimeLoadBalancingStrategy*& OutConfiguration, FIntPoint& OutWorldDimensions) const; template void RegisterExtension() @@ -41,12 +44,20 @@ class FLBStrategyEditorExtensionManager RegisterExtension(Extension::ExtendedStrategy::StaticClass(), MakeUnique()); } + template + void UnregisterExtension() + { + UnregisterExtension(Extension::ExtendedStrategy::StaticClass()); + } + void Cleanup(); private: - void RegisterExtension(UClass* StrategyClass, TUniquePtr StrategyExtension); + SPATIALGDKEDITOR_API void RegisterExtension(UClass* StrategyClass, TUniquePtr StrategyExtension); + + SPATIALGDKEDITOR_API void UnregisterExtension(UClass* StrategyClass); - using ExtensionArray = TArray>>; + using ExtensionArray = TMap>; ExtensionArray Extensions; }; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDefaultLaunchConfigGenerator.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDefaultLaunchConfigGenerator.h index ac093de32b..73b8dd0fcb 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDefaultLaunchConfigGenerator.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDefaultLaunchConfigGenerator.h @@ -4,10 +4,23 @@ #include "Logging/LogMacros.h" +#include "SpatialGDKSettings.h" +#include "SpatialGDKEditorSettings.h" + DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKDefaultLaunchConfigGenerator, Log, All); +class UAbstractRuntimeLoadBalancingStrategy; struct FSpatialLaunchConfigDescription; -bool SPATIALGDKEDITOR_API GenerateDefaultLaunchConfig(const FString& LaunchConfigPath, const FSpatialLaunchConfigDescription* InLaunchConfigDescription); +/** Set WorkerTypesToLaunch in level editor play settings. */ +void SPATIALGDKEDITOR_API SetLevelEditorPlaySettingsWorkerType(const FWorkerTypeLaunchSection& InWorker); + +uint32 SPATIALGDKEDITOR_API GetWorkerCountFromWorldSettings(const UWorld& World); + +bool SPATIALGDKEDITOR_API TryGetLoadBalancingStrategyFromWorldSettings(const UWorld& World, UAbstractRuntimeLoadBalancingStrategy*& OutStrategy, FIntPoint& OutWorldDimension); + +bool SPATIALGDKEDITOR_API FillWorkerConfigurationFromCurrentMap(FWorkerTypeLaunchSection& OutWorker, FIntPoint& OutWorldDimensions); + +bool SPATIALGDKEDITOR_API GenerateLaunchConfig(const FString& LaunchConfigPath, const FSpatialLaunchConfigDescription* InLaunchConfigDescription, const FWorkerTypeLaunchSection& InWorker); -bool SPATIALGDKEDITOR_API ValidateGeneratedLaunchConfig(const FSpatialLaunchConfigDescription& LaunchConfigDesc); +bool SPATIALGDKEDITOR_API ValidateGeneratedLaunchConfig(const FSpatialLaunchConfigDescription& LaunchConfigDesc, const FWorkerTypeLaunchSection& InWorker); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDevAuthTokenGenerator.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDevAuthTokenGenerator.h new file mode 100644 index 0000000000..4667e6b21c --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKDevAuthTokenGenerator.h @@ -0,0 +1,28 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "Templates/Atomic.h" +#include "Widgets/Notifications/SNotificationList.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKDevAuthTokenGenerator, Log, All); + +class SPATIALGDKEDITOR_API FSpatialGDKDevAuthTokenGenerator : public TSharedFromThis +{ +public: + FSpatialGDKDevAuthTokenGenerator(); + + void AsyncGenerateDevAuthToken(); + +private: + void ShowTaskStartedNotification(const FString& NotificationText); + void ShowTaskEndedNotification(const FString& NotificationText, SNotificationItem::ECompletionState CompletionState); + + void EndTask(bool bSuccess); + void DoGenerateDevAuthTokenTasks(); + +private: + TAtomic bIsGenerating; + TWeakPtr TaskNotificationPtr; +}; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditor.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditor.h index 778ce24d32..e5da0b0627 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditor.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditor.h @@ -10,20 +10,34 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKEditor, Log, All); DECLARE_DELEGATE_OneParam(FSpatialGDKEditorErrorHandler, FString); +class FSpatialGDKDevAuthTokenGenerator; +class FSpatialGDKPackageAssembly; +struct FCloudDeploymentConfiguration; + class SPATIALGDKEDITOR_API FSpatialGDKEditor { public: - FSpatialGDKEditor() : bSchemaGeneratorRunning(false) + FSpatialGDKEditor(); + + enum ESchemaGenerationMethod { - } + InMemoryAsset, + FullAssetScan + }; - bool GenerateSchema(bool bFullScan); + bool GenerateSchema(ESchemaGenerationMethod Method); void GenerateSnapshot(UWorld* World, FString SnapshotFilename, FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback, FSpatialGDKEditorErrorHandler ErrorCallback); - void LaunchCloudDeployment(FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback); + void StartCloudDeployment(const FCloudDeploymentConfiguration& Configuration, FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback); void StopCloudDeployment(FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback); bool IsSchemaGeneratorRunning() { return bSchemaGeneratorRunning; } bool FullScanRequired(); + bool IsSchemaGenerated(); + + void SetProjectName(const FString& InProjectName); + + TSharedRef GetDevAuthTokenGeneratorRef(); + TSharedRef GetPackageAssemblyRef(); private: bool bSchemaGeneratorRunning; @@ -36,4 +50,7 @@ class SPATIALGDKEDITOR_API FSpatialGDKEditor FDelegateHandle OnAssetLoadedHandle; void OnAssetLoaded(UObject* Asset); void RemoveEditorAssetLoadedCallback(); + + TSharedRef SpatialGDKDevAuthTokenGeneratorInstance; + TSharedRef SpatialGDKPackageAssemblyInstance; }; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorCloudLauncher.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorCloudLauncher.h index fc8815bd96..da913b28df 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorCloudLauncher.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorCloudLauncher.h @@ -6,6 +6,8 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKEditorCloudLauncher, Log, All); -SPATIALGDKEDITOR_API bool SpatialGDKCloudLaunch(); +struct FCloudDeploymentConfiguration; + +SPATIALGDKEDITOR_API bool SpatialGDKCloudLaunch(const FCloudDeploymentConfiguration& Configuration); SPATIALGDKEDITOR_API bool SpatialGDKCloudStop(); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorCommandLineArgsManager.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorCommandLineArgsManager.h new file mode 100644 index 0000000000..04d7f98f2f --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorCommandLineArgsManager.h @@ -0,0 +1,48 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "Input/Reply.h" +#include "Templates/SharedPointer.h" + +using ILauncherRef = TSharedRef; +using ILauncherWorkerPtr = TSharedPtr; +using ILauncherProfileRef = TSharedRef; + +#if ENGINE_MINOR_VERSION >= 24 +#define ENABLE_LAUNCHER_DELEGATE +#endif + +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKEditorCommandLineArgsManager, Log, All); + +class FSpatialGDKEditorCommandLineArgsManager +{ +public: + FSpatialGDKEditorCommandLineArgsManager(); + + void Init(); + + static FReply GenerateDevAuthToken(); + static FReply PushCommandLineToIOSDevice(); + static FReply PushCommandLineToAndroidDevice(); + static FReply RemoveCommandLineFromAndroidDevice(); + +private: +#ifdef ENABLE_LAUNCHER_DELEGATE + void OnCreateLauncher(ILauncherRef LauncherRef); + void OnLaunch(ILauncherWorkerPtr LauncherWorkerPtr, ILauncherProfileRef LauncherProfileRef); + void OnLauncherCanceled(double ExecutionTime); + void OnLauncherFinished(bool bSuccess, double ExecutionTime, int32 ReturnCode); + + void RemoveCommandLineFromDevice(); +#endif // ENABLE_LAUNCHER_DELEGATE + + static bool TryConstructMobileCommandLineArgumentsFile(FString& OutCommandLineArgsFile); + static bool TryPushCommandLineArgsToDevice(const FString& Executable, const FString& ExeArguments, const FString& CommandLineArgsFile); + +private: +#ifdef ENABLE_LAUNCHER_DELEGATE + bool bAndroidDevice; +#endif // ENABLE_LAUNCHER_DELEGATE +}; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorLayoutDetails.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorLayoutDetails.h index 0dea03656b..4d7bf070a4 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorLayoutDetails.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorLayoutDetails.h @@ -4,21 +4,22 @@ #include "CoreMinimal.h" #include "IDetailCustomization.h" -#include "Input/Reply.h" -DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKEditorLayoutDetails, Log, All); +class IErrorReportingWidget; class FSpatialGDKEditorLayoutDetails : public IDetailCustomization { -private: - bool TryConstructMobileCommandLineArgumentsFile(FString& CommandLineArgsFile); - bool TryPushCommandLineArgsToDevice(const FString& Executable, const FString& ExeArguments, const FString& CommandLineArgsFile); - - FReply GenerateDevAuthToken(); - FReply PushCommandLineArgsToIOSDevice(); - FReply PushCommandLineArgsToAndroidDevice(); - public: static TSharedRef MakeInstance(); virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; + +private: + void ForceRefreshLayout(); + +private: + IDetailLayoutBuilder* CurrentLayout = nullptr; + TSharedPtr ProjectNameInputErrorReporting; + + /** Delegate to commit project name */ + void OnProjectNameCommitted(const FText& InText, ETextCommit::Type InCommitType); }; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorModule.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorModule.h index b5a7ae3936..50c9b2d563 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorModule.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorModule.h @@ -1,17 +1,21 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved -#include "Modules/ModuleInterface.h" +#pragma once + +#include "Improbable/SpatialGDKSettingsBridge.h" #include "Modules/ModuleManager.h" class FLBStrategyEditorExtensionManager; +class FSpatialGDKEditor; +class FSpatialGDKEditorCommandLineArgsManager; -class FSpatialGDKEditorModule : public IModuleInterface +class FSpatialGDKEditorModule : public ISpatialGDKEditorModule { public: FSpatialGDKEditorModule(); - SPATIALGDKEDITOR_API const FLBStrategyEditorExtensionManager& GetLBStrategyExtensionManager() { return *ExtensionManager; } + SPATIALGDKEDITOR_API FLBStrategyEditorExtensionManager& GetLBStrategyExtensionManager() { return *ExtensionManager; } virtual void StartupModule() override; virtual void ShutdownModule() override; @@ -21,12 +25,39 @@ class FSpatialGDKEditorModule : public IModuleInterface return true; } + TSharedPtr GetSpatialGDKEditorInstance() const + { + return SpatialGDKEditorInstance; + } + +private: + // Local deployment connection flow + virtual bool ShouldConnectToLocalDeployment() const override; + virtual FString GetSpatialOSLocalDeploymentIP() const override; + virtual bool ShouldStartPIEClientsWithLocalLaunchOnDevice() const override; + + // Cloud deployment connection flow + virtual bool ShouldConnectToCloudDeployment() const override; + virtual FString GetDevAuthToken() const override; + virtual FString GetSpatialOSCloudDeploymentName() const override; + + virtual bool CanExecuteLaunch() const override; + virtual bool CanStartPlaySession(FText& OutErrorMessage) const override; + virtual bool CanStartLaunchSession(FText& OutErrorMessage) const override; + + virtual FString GetMobileClientCommandLineArgs() const override; + virtual bool ShouldPackageMobileCommandLineArgs() const override; + private: void RegisterSettings(); void UnregisterSettings(); bool HandleEditorSettingsSaved(); bool HandleRuntimeSettingsSaved(); bool HandleCloudLauncherSettingsSaved(); + bool CanStartSession(FText& OutErrorMessage) const; +private: TUniquePtr ExtensionManager; + TSharedPtr SpatialGDKEditorInstance; + TUniquePtr CommandLineArgsManager; }; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorPackageAssembly.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorPackageAssembly.h new file mode 100644 index 0000000000..1ec5b2cac5 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorPackageAssembly.h @@ -0,0 +1,65 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "Widgets/Notifications/SNotificationList.h" + +#include "CloudDeploymentConfiguration.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKEditorPackageAssembly, Log, All); + +class FMonitoredProcess; + +class SPATIALGDKEDITOR_API FSpatialGDKPackageAssembly : public TSharedFromThis +{ +public: + bool CanBuild() const; + + void BuildAndUploadAssembly(const FCloudDeploymentConfiguration& InCloudDeploymentConfiguration); + + FSimpleDelegate OnSuccess; + +private: + enum class EPackageAssemblyStep + { + NONE = 0, + BUILD_SERVER, + BUILD_CLIENT, + BUILD_SIMULATED_PLAYERS, + UPLOAD_ASSEMBLY, + }; + + enum class EPackageAssemblyStatus + { + NONE = 0, + SUCCESS, + CANCELED, + UNKNOWN_ERROR, + BAD_PROJECT_NAME, + ASSEMBLY_EXISTS, + }; + + EPackageAssemblyStatus Status = EPackageAssemblyStatus::NONE; + + TQueue Steps; + + TSharedPtr PackageAssemblyTask; + TWeakPtr TaskNotificationPtr; + + FCloudDeploymentConfiguration CloudDeploymentConfiguration; + + void LaunchTask(const FString& Exe, const FString& Args, const FString& WorkingDir); + + void BuildAssembly(const FString& ProjectName, const FString& Platform, const FString& Configuration, const FString& AdditionalArgs); + void UploadAssembly(const FString& AssemblyName, bool bForceAssemblyOverwrite); + + bool NextStep(); + + void ShowTaskStartedNotification(const FString& NotificationText); + void ShowTaskEndedNotification(const FString& NotificationText, SNotificationItem::ECompletionState CompletionState); + void HandleCancelButtonClicked(); + void OnTaskCompleted(int32 TaskResult); + void OnTaskOutput(FString Message); + void OnTaskCanceled(); +}; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSchemaGenerator.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSchemaGenerator.h index e8b044faac..bd80403e2c 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSchemaGenerator.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSchemaGenerator.h @@ -32,7 +32,7 @@ namespace SpatialGDKEditor SPATIALGDKEDITOR_API bool LoadGeneratorStateFromSchemaDatabase(const FString& FileName); - SPATIALGDKEDITOR_API bool IsAssetReadOnly(FString FileName); + SPATIALGDKEDITOR_API bool IsAssetReadOnly(const FString& FileName); SPATIALGDKEDITOR_API bool GeneratedSchemaDatabaseExists(); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h index 524ddc04e3..21382f5e40 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h @@ -5,8 +5,9 @@ #include "CoreMinimal.h" #include "Engine/EngineTypes.h" #include "Misc/Paths.h" -#include "SpatialConstants.h" #include "UObject/Package.h" + +#include "SpatialConstants.h" #include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesModule.h" @@ -14,6 +15,12 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialEditorSettings, Log, All); +DECLARE_MULTICAST_DELEGATE(FOnDefaultTemplateNameRequireUpdate) + +class FSpatialRuntimeVersionCustomization; +class UAbstractRuntimeLoadBalancingStrategy; +class USpatialGDKEditorSettings; + USTRUCT() struct FWorldLaunchSection { @@ -111,48 +118,27 @@ struct FWorkerTypeLaunchSection GENERATED_BODY() FWorkerTypeLaunchSection() - : WorkerTypeName() - , WorkerPermissions() - , MaxConnectionCapacityLimit(0) - , bLoginRateLimitEnabled(false) - , LoginRateLimit() - , Columns(1) - , Rows(1) + : WorkerPermissions() + , bAutoNumEditorInstances(true) , NumEditorInstances(1) - , bManualWorkerConnectionOnly(true) + , bManualWorkerConnectionOnly(false) { } - /** The name of the worker type, defined in the filename of its spatialos..worker.json file. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config) - FName WorkerTypeName; + /** Worker type name, deprecated in favor of defining them in the runtime settings.*/ + UPROPERTY(config) + FName WorkerTypeName_DEPRECATED; /** Defines the worker instance's permissions. */ UPROPERTY(Category = "SpatialGDK", EditAnywhere, config) FWorkerPermissionsSection WorkerPermissions; - /** Defines the maximum number of worker instances that can connect. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Max connection capacity limit (0 = unlimited capacity)", ClampMin = "0", UIMin = "0")) - int32 MaxConnectionCapacityLimit; - - /** Enable connection rate limiting. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Login rate limit enabled")) - bool bLoginRateLimitEnabled; - - /** Login rate limiting configuration. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "bLoginRateLimitEnabled")) - FLoginRateLimitSection LoginRateLimit; - - /** Number of columns in the rectangle grid load balancing config. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Rectangle grid column count", ClampMin = "1", UIMin = "1")) - int32 Columns; - - /** Number of rows in the rectangle grid load balancing config. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Rectangle grid row count", ClampMin = "1", UIMin = "1")) - int32 Rows; + /** 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")) + 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")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (DisplayName = "Instances to launch in editor", ClampMin = "0", UIMin = "0", EditCondition = "!bAutoNumEditorInstances")) int32 NumEditorInstances; /** Flags defined for a worker instance. */ @@ -167,40 +153,26 @@ struct FWorkerTypeLaunchSection USTRUCT() struct FSpatialLaunchConfigDescription { + friend class USpatialGDKEditorSettings; + GENERATED_BODY() FSpatialLaunchConfigDescription() - : Template(TEXT("w2_r0500_e5")) - , World() - { - FWorkerTypeLaunchSection UnrealWorkerDefaultSetting; - UnrealWorkerDefaultSetting.WorkerTypeName = SpatialConstants::DefaultServerWorkerType; - UnrealWorkerDefaultSetting.Rows = 1; - UnrealWorkerDefaultSetting.Columns = 1; - UnrealWorkerDefaultSetting.bManualWorkerConnectionOnly = true; - - ServerWorkers.Add(UnrealWorkerDefaultSetting); - } - - FSpatialLaunchConfigDescription(const FName& WorkerTypeName) - : Template(TEXT("w2_r0500_e5")) + : bUseDefaultTemplateForRuntimeVariant(true) + , Template() , World() - { - FWorkerTypeLaunchSection UnrealWorkerDefaultSetting; - UnrealWorkerDefaultSetting.WorkerTypeName = WorkerTypeName; - UnrealWorkerDefaultSetting.Rows = 1; - UnrealWorkerDefaultSetting.Columns = 1; - UnrealWorkerDefaultSetting.bManualWorkerConnectionOnly = true; + {} - ServerWorkers.Add(UnrealWorkerDefaultSetting); - } + const FString& GetTemplate() const; + const FString& GetDefaultTemplateForRuntimeVariant() const; - /** Set WorkerTypesToLaunch in level editor play settings. */ - SPATIALGDKEDITOR_API void SetLevelEditorPlaySettingsWorkerTypes(); + /** Use default template for deployments. */ + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config) + bool bUseDefaultTemplateForRuntimeVariant; /** Deployment template. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "!bUseDefaultTemplateForRuntimeVariant")) FString Template; /** Configuration for the simulated world. */ @@ -208,8 +180,11 @@ struct FSpatialLaunchConfigDescription FWorldLaunchSection World; /** Worker-specific configuration parameters. */ - UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (TitleProperty = "WorkerTypeName")) - TArray ServerWorkers; + UPROPERTY(config) + TArray ServerWorkers_DEPRECATED; + + UPROPERTY(Category = "SpatialGDK", EditAnywhere, EditFixedSize, config) + FWorkerTypeLaunchSection ServerWorkerConfig; }; /** @@ -223,32 +198,91 @@ namespace ERegionCode US = 1, EU, AP, - CN + CN UMETA(Hidden) }; } -UCLASS(config = SpatialGDKEditorSettings, defaultconfig) -class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject +UENUM() +namespace ESpatialOSNetFlow { + enum Type + { + LocalDeployment, + CloudDeployment + }; +} + +UENUM() +namespace ESpatialOSRuntimeVariant +{ + enum Type + { + Standard, + CompatibilityMode + }; +} + +USTRUCT() +struct SPATIALGDKEDITOR_API FRuntimeVariantVersion +{ + friend class USpatialGDKEditorSettings; + friend class FSpatialRuntimeVersionCustomization; + GENERATED_BODY() -public: - USpatialGDKEditorSettings(const FObjectInitializer& ObjectInitializer); + FRuntimeVariantVersion() : PinnedVersion(SpatialGDKServicesConstants::SpatialOSRuntimePinnedStandardVersion) + {} - virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override; - virtual void PostInitProperties() override; + 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. */ + 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. */ + const FString& GetVersionForLocal() const; + + bool GetUseGDKPinnedRuntimeVersionForLocal() const { return bUseGDKPinnedRuntimeVersionForLocal; } + + bool GetUseGDKPinnedRuntimeVersionForCloud() const { return bUseGDKPinnedRuntimeVersionForCloud; } + + const FString& GetPinnedVersion() const { return PinnedVersion; } private: - /** Set WorkerTypes in runtime settings. */ - void SetRuntimeWorkerTypes(); + /** 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; - /** Set DAT in runtime settings. */ - void SetRuntimeUseDevelopmentAuthenticationFlow(); - void SetRuntimeDevelopmentDeploymentToConnect(); + /** 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; + + /** Runtime version to use for local deployments, if not using the GDK pinned version. */ + UPROPERTY(EditAnywhere, config, Category = "Runtime", meta = (EditCondition = "!bUseGDKPinnedRuntimeVersionForLocal")) + FString LocalRuntimeVersion; + + /** Runtime version to use for cloud deployments, if not using the GDK pinned version. */ + UPROPERTY(EditAnywhere, config, Category = "Runtime", meta = (EditCondition = "!bUseGDKPinnedRuntimeVersionForCloud")) + FString CloudRuntimeVersion; + +private: + /** Pinned version for this variant. */ + FString PinnedVersion; +}; + +UCLASS(config = SpatialGDKEditorSettings, defaultconfig, HideCategories = LoadBalancing) +class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject +{ + GENERATED_BODY() public: + USpatialGDKEditorSettings(const FObjectInitializer& ObjectInitializer); + + virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override; + 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; @@ -261,37 +295,37 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (DisplayName = "Auto-generate launch configuration file")) bool bGenerateDefaultLaunchConfig; - /** Returns the Runtime version to use for cloud deployments, either the pinned one, or the user-specified one depending of the settings. */ - const FString& GetSpatialOSRuntimeVersionForCloud() const; + /** Returns which runtime variant we should use. */ + TEnumAsByte GetSpatialOSRuntimeVariant() const { return RuntimeVariant; } + + /** Returns the version information for the currently set variant*/ + const FRuntimeVariantVersion& GetSelectedRuntimeVariantVersion() const + { + return const_cast(this)->GetRuntimeVariantVersion(RuntimeVariant); + } - /** Returns the Runtime version to use for local deployments, either the pinned one, or the user-specified one depending of the settings. */ - const FString& GetSpatialOSRuntimeVersionForLocal() const; + UPROPERTY(EditAnywhere, config, Category = "Runtime") + TEnumAsByte RuntimeVariant; - /** Whether to use the GDK-associated SpatialOS runtime version, or to use the one specified in the RuntimeVersion field. */ - UPROPERTY(EditAnywhere, config, Category = "Runtime", meta = (DisplayName = "Use GDK pinned runtime version")) - bool bUseGDKPinnedRuntimeVersion; + UPROPERTY(EditAnywhere, config, Category = "Runtime", AdvancedDisplay) + FRuntimeVariantVersion StandardRuntimeVersion; - /** Runtime version to use for local deployments, if not using the GDK pinned version. */ - UPROPERTY(EditAnywhere, config, Category = "Runtime", meta = (EditCondition = "!bUseGDKPinnedRuntimeVersion")) - FString LocalRuntimeVersion; + UPROPERTY(EditAnywhere, config, Category = "Runtime", AdvancedDisplay) + FRuntimeVariantVersion CompatibilityModeRuntimeVersion; - /** Runtime version to use for cloud deployments, if not using the GDK pinned version. */ - UPROPERTY(EditAnywhere, config, Category = "Runtime", meta = (EditCondition = "!bUseGDKPinnedRuntimeVersion")) - FString CloudRuntimeVersion; + 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")) FFilePath SpatialOSLaunchConfig; public: - /** Expose the runtime on a particular IP address when it is running on this machine. Changes are applied on next local deployment startup. */ - UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (DisplayName = "Expose local runtime")) - bool bExposeRuntimeIP; - - /** If the runtime is set to be exposed, specify on which IP address it should be reachable. Changes are applied on next local deployment startup. */ - UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (EditCondition = "bExposeRuntimeIP", DisplayName = "Exposed local runtime IP address")) + /** 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; /** Select the check box to stop your game’s local deployment when you shut down Unreal Editor. */ @@ -311,66 +345,120 @@ 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)")) + FString CookAndGeneratePlatform; + + 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.*/ UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (DisplayName = "Command line flags for local launch")) TArray SpatialOSCommandLineLaunchFlags; private: - UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (DisplayName = "Assembly name")) - FString AssemblyName; + UPROPERTY(config) + FString AssemblyName; - UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (DisplayName = "Deployment name")) - FString PrimaryDeploymentName; + UPROPERTY(config) + FString PrimaryDeploymentName; - UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (DisplayName = "Cloud launch configuration path")) - FFilePath PrimaryLaunchConfigPath; + UPROPERTY(config) + FFilePath PrimaryLaunchConfigPath; - UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (DisplayName = "Snapshot path")) - FFilePath SnapshotPath; + UPROPERTY(config) + FFilePath SnapshotPath; - UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (DisplayName = "Region")) - TEnumAsByte PrimaryDeploymentRegionCode; + UPROPERTY(config) + TEnumAsByte PrimaryDeploymentRegionCode; + + UPROPERTY(config) + FString MainDeploymentCluster; + + /** Tags used when launching a deployment */ + UPROPERTY(config) + FString DeploymentTags; + + UPROPERTY(config) + bool bIsAutoGenerateCloudConfigEnabled; const FString SimulatedPlayerLaunchConfigPath; public: - /** If the Development Authentication Flow is used, the client will try to connect to the cloud rather than local deployment. */ - UPROPERTY(EditAnywhere, config, Category = "Cloud Connection") - bool bUseDevelopmentAuthenticationFlow; + /** Whether to build and upload the assembly when starting the cloud deployment. */ + UPROPERTY(EditAnywhere, config, Category = "Assembly", meta = (DisplayName = "Build and Upload Assembly")) + bool bBuildAndUploadAssembly; + + /** The build configuration to use when creating workers for the assembly, e.g. Development */ + UPROPERTY(EditAnywhere, config, Category = "Assembly", meta = (DisplayName = "Build Configuration")) + FString AssemblyBuildConfiguration; + + /** Allow overwriting an assembly of the same name */ + UPROPERTY(EditAnywhere, config, Category = "Assembly", meta = (DisplayName = "Force Assembly Overwrite")) + bool bForceAssemblyOverwrite; + + /** Whether to build client worker as part of the assembly */ + UPROPERTY(EditAnywhere, config, Category = "Assembly", meta = (DisplayName = "Build Client Worker")) + bool bBuildClientWorker; + + /** Whether to generate schema automatically before building an assembly */ + UPROPERTY(EditAnywhere, config, Category = "Assembly", meta = (DisplayName = "Generate Schema")) + bool bGenerateSchema; + + /** Whether to generate a snapshot automatically before building an assembly */ + UPROPERTY(EditAnywhere, config, Category = "Assembly", meta = (DisplayName = "Generate Snapshot")) + bool bGenerateSnapshot; + + /** Extra arguments to pass when building the server worker. */ + UPROPERTY(EditAnywhere, config, Category = "Assembly") + FString BuildServerExtraArgs; + + /** Extra arguments to pass when building the client worker. */ + UPROPERTY(EditAnywhere, config, Category = "Assembly") + FString BuildClientExtraArgs; + + /** Extra arguments to pass when building the simulated player worker. */ + UPROPERTY(EditAnywhere, config, Category = "Assembly") + FString BuildSimulatedPlayerExtraArgs; /** The token created using 'spatial project auth dev-auth-token' */ UPROPERTY(EditAnywhere, config, Category = "Cloud Connection") - FString DevelopmentAuthenticationToken; + FString DevelopmentAuthenticationToken; /** The deployment to connect to when using the Development Authentication Flow. If left empty, it uses the first available one (order not guaranteed when there are multiple items). The deployment needs to be tagged with 'dev_login'. */ UPROPERTY(EditAnywhere, config, Category = "Cloud Connection") - FString DevelopmentDeploymentToConnect; + FString DevelopmentDeploymentToConnect; private: - UPROPERTY(EditAnywhere, config, Category = "Simulated Players", meta = (EditCondition = "bSimulatedPlayersIsEnabled", DisplayName = "Region")) - TEnumAsByte SimulatedPlayerDeploymentRegionCode; + UPROPERTY(config) + TEnumAsByte SimulatedPlayerDeploymentRegionCode; - UPROPERTY(EditAnywhere, config, Category = "Simulated Players", meta = (DisplayName = "Include simulated players")) - bool bSimulatedPlayersIsEnabled; + UPROPERTY(config) + FString SimulatedPlayerCluster; - UPROPERTY(EditAnywhere, config, Category = "Simulated Players", meta = (EditCondition = "bSimulatedPlayersIsEnabled", DisplayName = "Deployment name")) - FString SimulatedPlayerDeploymentName; + UPROPERTY(config) + bool bSimulatedPlayersIsEnabled; - UPROPERTY(EditAnywhere, config, Category = "Simulated Players", meta = (EditCondition = "bSimulatedPlayersIsEnabled", DisplayName = "Number of simulated players")) - uint32 NumberOfSimulatedPlayers; + UPROPERTY(config) + FString SimulatedPlayerDeploymentName; + + UPROPERTY(config) + uint32 NumberOfSimulatedPlayers; - static bool IsAssemblyNameValid(const FString& Name); - static bool IsProjectNameValid(const FString& Name); - static bool IsDeploymentNameValid(const FString& Name); static bool IsRegionCodeValid(const ERegionCode::Type RegionCode); static bool IsManualWorkerConnectionSet(const FString& LaunchConfigPath, TArray& OutWorkersManuallyLaunched); public: - UPROPERTY(EditAnywhere, config, Category = "Mobile", meta = (DisplayName = "Connect to a local deployment")) - bool bMobileConnectToLocalDeployment; + /** 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")) + TEnumAsByte MobileConnectionFlow; - UPROPERTY(EditAnywhere, config, Category = "Mobile", meta = (EditCondition = "bMobileConnectToLocalDeployment", DisplayName = "Runtime IP to local deployment")) - FString MobileRuntimeIP; + /** 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; UPROPERTY(EditAnywhere, config, Category = "Mobile", meta = (DisplayName = "Mobile Client Worker Type")) FString MobileWorkerType = SpatialConstants::DefaultClientWorkerType.ToString(); @@ -378,16 +466,25 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject UPROPERTY(EditAnywhere, config, Category = "Mobile", meta = (DisplayName = "Extra Command Line Arguments")) FString MobileExtraCommandLineArgs; + UPROPERTY(EditAnywhere, config, Category = "Mobile", meta = (DisplayName = "Include Command Line Arguments when Packaging")) + 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")) + 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")) 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.IsEmpty() - ? FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, TEXT("default_launch.json")) - : SpatialOSLaunchConfig.FilePath; + return SpatialOSLaunchConfig.FilePath; } FORCEINLINE FString GetSpatialOSSnapshotToSave() const @@ -409,6 +506,13 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject : SpatialOSSnapshotToLoad; } + FString GetCookAndGenerateSchemaTargetPlatform() const; + + FORCEINLINE FString GetCookAndGenerateSchemaAdditionalArgs() const + { + return CookAndGenerateAdditionalArguments; + } + FORCEINLINE FString GetSpatialOSSnapshotToLoadPath() const { return FPaths::Combine(GetSpatialOSSnapshotFolderPath(), GetSpatialOSSnapshotToLoad()); @@ -464,14 +568,11 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject void SetSnapshotPath(const FString& Path); FORCEINLINE FString GetSnapshotPath() const { - const USpatialGDKEditorSettings* SpatialEditorSettings = GetDefault(); - return SnapshotPath.FilePath.IsEmpty() - ? SpatialEditorSettings->GetSpatialOSSnapshotToSavePath() - : SnapshotPath.FilePath; + return SnapshotPath.FilePath; } void SetPrimaryRegionCode(const ERegionCode::Type RegionCode); - FORCEINLINE FText GetPrimaryRegionCode() const + FORCEINLINE FText GetPrimaryRegionCodeText() const { if (!IsRegionCodeValid(PrimaryDeploymentRegionCode)) { @@ -483,6 +584,29 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject return Region->GetDisplayNameTextByValue(static_cast(PrimaryDeploymentRegionCode.GetValue())); } + const ERegionCode::Type GetPrimaryRegionCode() const + { + return PrimaryDeploymentRegionCode; + } + + void SetMainDeploymentCluster(const FString& NewCluster); + FORCEINLINE FString GetMainDeploymentCluster() const + { + return MainDeploymentCluster; + } + + void SetDeploymentTags(const FString& Tags); + FORCEINLINE FString GetDeploymentTags() const + { + return DeploymentTags; + } + + void SetAssemblyBuildConfiguration(const FString& Configuration); + FORCEINLINE FText GetAssemblyBuildConfiguration() const + { + return FText::FromString(AssemblyBuildConfiguration); + } + void SetSimulatedPlayerRegionCode(const ERegionCode::Type RegionCode); FORCEINLINE FText GetSimulatedPlayerRegionCode() const { @@ -502,31 +626,65 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject return bSimulatedPlayersIsEnabled; } - void SetUseGDKPinnedRuntimeVersion(bool IsEnabled); - FORCEINLINE bool GetUseGDKPinnedRuntimeVersion() const + void SetAutoGenerateCloudLaunchConfigEnabledState(bool IsEnabled); + FORCEINLINE bool ShouldAutoGenerateCloudLaunchConfig() const + { + return bIsAutoGenerateCloudConfigEnabled; + } + + void SetBuildAndUploadAssembly(bool bBuildAndUpload); + FORCEINLINE bool ShouldBuildAndUploadAssembly() const + { + return bBuildAndUploadAssembly; + } + + void SetForceAssemblyOverwrite(bool bForce); + FORCEINLINE bool IsForceAssemblyOverwriteEnabled() const { - return bUseGDKPinnedRuntimeVersion; + return bForceAssemblyOverwrite; } - void SetCustomCloudSpatialOSRuntimeVersion(const FString& Version); - FORCEINLINE const FString& GetCustomCloudSpatialOSRuntimeVersion() const + void SetBuildClientWorker(bool bBuild); + FORCEINLINE bool IsBuildClientWorkerEnabled() const { - return CloudRuntimeVersion; + return bBuildClientWorker; } + void SetGenerateSchema(bool bGenerate); + FORCEINLINE bool IsGenerateSchemaEnabled() const + { + return bGenerateSchema; + } + + void SetGenerateSnapshot(bool bGenerate); + 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; } + void SetSimulatedPlayerCluster(const FString& NewCluster); + FORCEINLINE FString GetSimulatedPlayerCluster() const + { + return SimulatedPlayerCluster; + } + FORCEINLINE FString GetSimulatedPlayerLaunchConfigPath() const { return SimulatedPlayerLaunchConfigPath; } void SetNumberOfSimulatedPlayers(uint32 Number); - FORCEINLINE uint32 GetNumberOfSimulatedPlayer() const + FORCEINLINE uint32 GetNumberOfSimulatedPlayers() const { return NumberOfSimulatedPlayers; } @@ -538,5 +696,14 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject bool IsDeploymentConfigurationValid() const; - void SetRuntimeDevelopmentAuthenticationToken(); + void SetDevelopmentAuthenticationToken(const FString& Token); + void SetDevelopmentDeploymentToConnect(const FString& Deployment); + + void SetExposedRuntimeIP(const FString& RuntimeIP); + + void SetSpatialOSNetFlowType(ESpatialOSNetFlow::Type NetFlowType); + + static bool IsProjectNameValid(const FString& Name); + static bool IsAssemblyNameValid(const FString& Name); + static bool IsDeploymentNameValid(const FString& Name); }; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSnapshotGenerator.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSnapshotGenerator.h index 6ee06dcd54..add89ec65c 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSnapshotGenerator.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSnapshotGenerator.h @@ -6,4 +6,4 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKSnapshot, Log, All); -SPATIALGDKEDITOR_API bool SpatialGDKGenerateSnapshot(class UWorld* World, FString SnapshotFilename); +SPATIALGDKEDITOR_API bool SpatialGDKGenerateSnapshot(class UWorld* World, FString SnapshotPath); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialLaunchConfigCustomization.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialLaunchConfigCustomization.h new file mode 100644 index 0000000000..72b1724117 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialLaunchConfigCustomization.h @@ -0,0 +1,17 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "IPropertyTypeCustomization.h" + +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; +}; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialRuntimeLoadBalancingStrategies.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialRuntimeLoadBalancingStrategies.h new file mode 100644 index 0000000000..e165605f7a --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialRuntimeLoadBalancingStrategies.h @@ -0,0 +1,62 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Serialization/JsonWriter.h" +#include "UObject/Object.h" + +#include "SpatialRuntimeLoadBalancingStrategies.generated.h" + +UCLASS(Abstract) +class SPATIALGDKEDITOR_API UAbstractRuntimeLoadBalancingStrategy : public UObject +{ + GENERATED_BODY() + +public: + virtual int32 GetNumberOfWorkersForPIE() const PURE_VIRTUAL(UAbstractRuntimeLoadBalancingStrategy::GetNumberOfWorkersForPIE, return 0;); +}; + +UCLASS() +class SPATIALGDKEDITOR_API USingleWorkerRuntimeStrategy : public UAbstractRuntimeLoadBalancingStrategy +{ + GENERATED_BODY() + +public: + USingleWorkerRuntimeStrategy(); + + int32 GetNumberOfWorkersForPIE() const override; +}; + +UCLASS(EditInlineNew) +class SPATIALGDKEDITOR_API UGridRuntimeLoadBalancingStrategy : public UAbstractRuntimeLoadBalancingStrategy +{ + GENERATED_BODY() + +public: + UGridRuntimeLoadBalancingStrategy(); + + /** Number of columns in the rectangle grid load balancing config. */ + UPROPERTY(Category = "LoadBalancing", EditAnywhere, meta = (DisplayName = "Rectangle grid column count", ClampMin = "1", UIMin = "1")) + int32 Columns; + + /** Number of rows in the rectangle grid load balancing config. */ + UPROPERTY(Category = "LoadBalancing", EditAnywhere, meta = (DisplayName = "Rectangle grid row count", ClampMin = "1", UIMin = "1")) + int32 Rows; + + int32 GetNumberOfWorkersForPIE() const override; +}; + +UCLASS(EditInlineNew) +class SPATIALGDKEDITOR_API UEntityShardingRuntimeLoadBalancingStrategy : public UAbstractRuntimeLoadBalancingStrategy +{ + GENERATED_BODY() + +public: + UEntityShardingRuntimeLoadBalancingStrategy(); + + /** Number of columns in the rectangle grid load balancing config. */ + UPROPERTY(Category = "LoadBalancing", EditAnywhere, meta = (DisplayName = "Number of workers", ClampMin = "1", UIMin = "1")) + int32 NumWorkers; + + int32 GetNumberOfWorkersForPIE() const override; +}; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialRuntimeVersionCustomization.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialRuntimeVersionCustomization.h new file mode 100644 index 0000000000..cf7f052e3d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialRuntimeVersionCustomization.h @@ -0,0 +1,16 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "IPropertyTypeCustomization.h" + +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; +}; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/LaunchConfigEditor.h b/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/LaunchConfigEditor.h deleted file mode 100644 index 71e5e88f8a..0000000000 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/LaunchConfigEditor.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#pragma once - -#include "SpatialGDKEditorSettings.h" -#include "Utils/TransientUObjectEditor.h" - -#include "LaunchConfigEditor.generated.h" - -UCLASS() -class SPATIALGDKEDITOR_API ULaunchConfigurationEditor : public UTransientUObjectEditor -{ - GENERATED_BODY() - - UPROPERTY(EditAnywhere, Category = "Launch Configuration") - FSpatialLaunchConfigDescription LaunchConfiguration; - - UFUNCTION(Exec) - void SaveConfiguration(); -}; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/LaunchConfigurationEditor.h b/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/LaunchConfigurationEditor.h new file mode 100644 index 0000000000..ada387020c --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/LaunchConfigurationEditor.h @@ -0,0 +1,32 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "SpatialGDKEditorSettings.h" + +#include "LaunchConfigurationEditor.generated.h" + +class ULaunchConfigurationEditor; + +DECLARE_DELEGATE_OneParam(FOnSpatialOSLaunchConfigurationSaved, const FString&) + +UCLASS(Transient, CollapseCategories) +class SPATIALGDKEDITOR_API ULaunchConfigurationEditor : public UObject +{ + GENERATED_BODY() +public: + FOnSpatialOSLaunchConfigurationSaved OnConfigurationSaved; + + UPROPERTY(EditAnywhere, Category = "Launch Configuration") + FSpatialLaunchConfigDescription LaunchConfiguration; + + typedef void(*OnLaunchConfigurationSaved)(const FString&); + + static void OpenModalWindow(TSharedPtr InParentWindow, OnLaunchConfigurationSaved InSaved = nullptr); +protected: + void PostInitProperties() override; + + UFUNCTION(Exec) + void SaveConfiguration(); +}; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/TransientUObjectEditor.h b/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/TransientUObjectEditor.h index 1b8a1055f8..75c25ff491 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/TransientUObjectEditor.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/Utils/TransientUObjectEditor.h @@ -7,6 +7,10 @@ #include "TransientUObjectEditor.generated.h" +DECLARE_DELEGATE(FOnTransientUObjectEditorClosed) + +class SWindow; + // Utility class to create Editor tools exposing a UObject Field and automatically adding Exec UFUNCTION as buttons. UCLASS(Blueprintable, Abstract) class SPATIALGDKEDITOR_API UTransientUObjectEditor : public UObject @@ -15,11 +19,11 @@ class SPATIALGDKEDITOR_API UTransientUObjectEditor : public UObject public: template - static void LaunchTransientUObjectEditor(const FString& EditorName) + static T* LaunchTransientUObjectEditor(const FString& EditorName, TSharedPtr ParentWindow) { - LaunchTransientUObjectEditor(EditorName, T::StaticClass()); + return Cast(LaunchTransientUObjectEditor(EditorName, T::StaticClass(), ParentWindow)); } private: - static void LaunchTransientUObjectEditor(const FString& EditorName, UClass* ObjectClass); + static UTransientUObjectEditor* LaunchTransientUObjectEditor(const FString& EditorName, UClass* ObjectClass, TSharedPtr ParentWindow); }; diff --git a/SpatialGDK/Source/SpatialGDKEditor/SpatialGDKEditor.Build.cs b/SpatialGDK/Source/SpatialGDKEditor/SpatialGDKEditor.Build.cs index d908fd08d0..5d6d5c3ff5 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/SpatialGDKEditor.Build.cs +++ b/SpatialGDK/Source/SpatialGDKEditor/SpatialGDKEditor.Build.cs @@ -24,12 +24,14 @@ public SpatialGDKEditor(ReadOnlyTargetRules Target) : base(Target) "Engine", "EngineSettings", "IOSRuntimeSettings", + "LauncherServices", "Json", "PropertyEditor", "Slate", "SlateCore", "SpatialGDK", "SpatialGDKServices", + "UATHelper", "UnrealEd", "DesktopPlatform" }); diff --git a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/CookAndGenerateSchemaCommandlet.cpp b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/CookAndGenerateSchemaCommandlet.cpp index d5cf86737e..841202b24d 100644 --- a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/CookAndGenerateSchemaCommandlet.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/CookAndGenerateSchemaCommandlet.cpp @@ -4,6 +4,8 @@ #include "SpatialConstants.h" #include "SpatialGDKEditorCommandletPrivate.h" #include "SpatialGDKEditorSchemaGenerator.h" +#include "SpatialGDKEditorSettings.h" +#include "SpatialGDKServicesConstants.h" using namespace SpatialGDKEditor::Schema; @@ -45,12 +47,10 @@ struct FObjectListener : public FUObjectArray::FUObjectCreateListener } } -#if ENGINE_MINOR_VERSION >= 23 virtual void OnUObjectArrayShutdown() override { GUObjectArray.RemoveUObjectCreateListener(this); } -#endif private: TSet* VisitedClasses; @@ -71,15 +71,10 @@ int32 UCookAndGenerateSchemaCommandlet::Main(const FString& CmdLineParams) TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, GIsRunningUnattendedScript || IsRunningCommandlet()); -#if ENGINE_MINOR_VERSION <= 22 - // Force spatial networking - GetMutableDefault()->SetUsesSpatialNetworking(true); -#endif - FObjectListener ObjectListener; TSet ReferencedClasses; ObjectListener.StartListening(&ReferencedClasses); - + UE_LOG(LogCookAndGenerateSchemaCommandlet, Display, TEXT("Try Load Schema Database.")); if (IsAssetReadOnly(SpatialConstants::SCHEMA_DATABASE_FILE_PATH)) { @@ -87,6 +82,12 @@ int32 UCookAndGenerateSchemaCommandlet::Main(const FString& CmdLineParams) 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(); @@ -112,11 +113,7 @@ int32 UCookAndGenerateSchemaCommandlet::Main(const FString& CmdLineParams) // Sort classes here so that batching does not have an effect on ordering. ReferencedClasses.Sort([](const FSoftClassPath& A, const FSoftClassPath& B) { -#if ENGINE_MINOR_VERSION <= 22 - return A.GetAssetPathName() < B.GetAssetPathName(); -#else return FNameLexicalLess()(A.GetAssetPathName(), B.GetAssetPathName()); -#endif }); UE_LOG(LogCookAndGenerateSchemaCommandlet, Display, TEXT("Start Schema Generation for discovered assets.")); @@ -154,7 +151,7 @@ int32 UCookAndGenerateSchemaCommandlet::Main(const FString& CmdLineParams) { UE_LOG(LogCookAndGenerateSchemaCommandlet, Error, TEXT("Failed to run schema compiler.")); return 0; - } + } if (!SaveSchemaDatabase(SpatialConstants::SCHEMA_DATABASE_ASSET_PATH)) { diff --git a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaAndSnapshotsCommandlet.cpp b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaAndSnapshotsCommandlet.cpp index a46cd819a9..7565292ac4 100644 --- a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaAndSnapshotsCommandlet.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaAndSnapshotsCommandlet.cpp @@ -220,18 +220,9 @@ bool UGenerateSchemaAndSnapshotsCommandlet::GenerateSnapshotForMap(FSpatialGDKEd bool UGenerateSchemaAndSnapshotsCommandlet::GenerateSchema(FSpatialGDKEditor& InSpatialGDKEditor) { - bool bSchemaGenSuccess; - if (InSpatialGDKEditor.GenerateSchema(true)) - { - UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Schema Generation Completed!")); - bSchemaGenSuccess = true; - } - else - { - UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Schema Generation Failed")); - bSchemaGenSuccess = false; - } - return bSchemaGenSuccess; + UE_LOG(LogSpatialGDKEditorCommandlet, Error, TEXT("Commandlet GenerateSchemaAndSnapshots without -SkipSchema has been deprecated in favor of CookAndGenerateSchemaCommandlet.")); + + return false; } bool UGenerateSchemaAndSnapshotsCommandlet::GenerateSnapshotForLoadedMap(FSpatialGDKEditor& InSpatialGDKEditor, const FString& MapName) diff --git a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaCommandlet.cpp b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaCommandlet.cpp index 64e80777c6..051d186e81 100644 --- a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaCommandlet.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Private/Commandlets/GenerateSchemaCommandlet.cpp @@ -50,21 +50,9 @@ int32 UGenerateSchemaCommandlet::Main(const FString& Args) return 1; } - //Generate Schema! - bool bSchemaGenSuccess; - FSpatialGDKEditor SpatialGDKEditor; - if (SpatialGDKEditor.GenerateSchema(true)) - { - UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Schema Generation Completed!")); - bSchemaGenSuccess = true; - } - else - { - UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Schema Generation Failed")); - bSchemaGenSuccess = false; - } - + UE_LOG(LogSpatialGDKEditorCommandlet, Error, TEXT("Commandlet GenerateSchema has been deprecated in favor of CookAndGenerateSchemaCommandlet.")); + UE_LOG(LogSpatialGDKEditorCommandlet, Display, TEXT("Schema Generation Commandlet Complete")); - return bSchemaGenSuccess ? 0 : 1; + return false; } diff --git a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Public/SpatialGDKEditorCommandletModule.h b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Public/SpatialGDKEditorCommandletModule.h index 3169d3e970..e90a876293 100644 --- a/SpatialGDK/Source/SpatialGDKEditorCommandlet/Public/SpatialGDKEditorCommandletModule.h +++ b/SpatialGDK/Source/SpatialGDKEditorCommandlet/Public/SpatialGDKEditorCommandletModule.h @@ -1,5 +1,7 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved +#pragma once + #include "SpatialGDKEditorCommandletPrivate.h" #include "Modules/ModuleInterface.h" diff --git a/SpatialGDK/Source/SpatialGDKEditorCommandlet/SpatialGDKEditorCommandlet.Build.cs b/SpatialGDK/Source/SpatialGDKEditorCommandlet/SpatialGDKEditorCommandlet.Build.cs index e64708a4a4..9f5dafc348 100644 --- a/SpatialGDK/Source/SpatialGDKEditorCommandlet/SpatialGDKEditorCommandlet.Build.cs +++ b/SpatialGDK/Source/SpatialGDKEditorCommandlet/SpatialGDKEditorCommandlet.Build.cs @@ -24,6 +24,7 @@ public SpatialGDKEditorCommandlet(ReadOnlyTargetRules Target) : base(Target) "EngineSettings", "SpatialGDK", "SpatialGDKEditor", + "SpatialGDKServices", "UnrealEd", }); diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKCloudDeploymentConfiguration.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKCloudDeploymentConfiguration.cpp new file mode 100644 index 0000000000..5d6b15b463 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKCloudDeploymentConfiguration.cpp @@ -0,0 +1,1159 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialGDKCloudDeploymentConfiguration.h" + +#include "Async/Async.h" +#include "DesktopPlatformModule.h" +#include "Editor.h" +#include "EditorDirectories.h" +#include "EditorStyleSet.h" +#include "Framework/Application/SlateApplication.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "Framework/Notifications/NotificationManager.h" +#include "HAL/PlatformFilemanager.h" +#include "InstalledPlatformInfo.h" +#include "Internationalization/Regex.h" +#include "Misc/MessageDialog.h" +#include "Runtime/Launch/Resources/Version.h" +#include "Templates/SharedPointer.h" +#include "Textures/SlateIcon.h" +#include "UnrealEd/Classes/Settings/ProjectPackagingSettings.h" +#include "Utils/LaunchConfigurationEditor.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SComboButton.h" +#include "Widgets/Input/SFilePathPicker.h" +#include "Widgets/Input/SHyperlink.h" +#include "Widgets/Input/SSpinBox.h" +#include "Widgets/Layout/SBox.h" +#include "Widgets/Layout/SExpandableArea.h" +#include "Widgets/Layout/SSeparator.h" +#include "Widgets/Layout/SUniformGridPanel.h" +#include "Widgets/Layout/SWrapBox.h" +#include "Widgets/Notifications/SNotificationList.h" +#include "Widgets/Notifications/SPopupErrorText.h" +#include "Widgets/Text/STextBlock.h" + +#include "SpatialCommandUtils.h" +#include "SpatialConstants.h" +#include "SpatialGDKDefaultLaunchConfigGenerator.h" +#include "SpatialGDKDevAuthTokenGenerator.h" +#include "SpatialGDKEditorSettings.h" +#include "SpatialGDKEditorToolbar.h" +#include "SpatialGDKEditorPackageAssembly.h" +#include "SpatialGDKEditorSnapshotGenerator.h" +#include "SpatialGDKServicesConstants.h" +#include "SpatialGDKServicesModule.h" +#include "SpatialGDKSettings.h" + +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")); +} // anonymous namespace + +void SSpatialGDKCloudDeploymentConfiguration::Construct(const FArguments& InArgs) +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + FString ProjectName = FSpatialGDKServicesModule::GetProjectName(); + FSpatialGDKEditorToolbarModule* ToolbarPtr = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar"); + + 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(FText::FromString(FString(TEXT("*")))) + .ToolTipText(FText::FromString(FString(TEXT("Required field")))) + .ColorAndOpacity(FLinearColor(1.0f, 0.0f, 0.0f)) + ]; + }; + + ProjectNameInputErrorReporting = SNew(SPopupErrorText); + ProjectNameInputErrorReporting->SetError(TEXT("")); + AssemblyNameInputErrorReporting = SNew(SPopupErrorText); + AssemblyNameInputErrorReporting->SetError(TEXT("")); + DeploymentNameInputErrorReporting = SNew(SPopupErrorText); + DeploymentNameInputErrorReporting->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(FText::FromString(FString(TEXT("Project Name")))) + .ToolTipText(FText::FromString(FString(TEXT("The name of the SpatialOS project.")))) + ) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SEditableTextBox) + .Text(FText::FromString(ProjectName)) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Assembly Name")))) + .ToolTipText(FText::FromString(FString(TEXT("The name of the assembly.")))) + ) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SEditableTextBox) + .Text(FText::FromString(SpatialGDKSettings->GetAssemblyName())) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Use GDK Pinned Version For Cloud")))) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Runtime Version")))) + .ToolTipText(FText::FromString(FString(TEXT("User supplied version of the SpatialOS runtime to use")))) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SEditableTextBox) + .Text(this, &SSpatialGDKCloudDeploymentConfiguration::GetSpatialOSRuntimeVersionToUseText) + .OnTextCommitted(this, &SSpatialGDKCloudDeploymentConfiguration::OnRuntimeCustomVersionCommited) + .OnTextChanged(this, &SSpatialGDKCloudDeploymentConfiguration::OnRuntimeCustomVersionCommited, ETextCommit::Default) + .IsEnabled(this, &SSpatialGDKCloudDeploymentConfiguration::IsUsingCustomRuntimeVersion) + ] + ] + // Pirmary Deployment Name + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + AddRequiredFieldAsterisk( + SNew(STextBlock) + .Text(FText::FromString(FString(TEXT("Deployment Name")))) + .ToolTipText(FText::FromString(FString(TEXT("The name of the cloud deployment. Must be unique.")))) + ) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SEditableTextBox) + .Text(FText::FromString(SpatialGDKSettings->GetPrimaryDeploymentName())) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Snapshot File")))) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Path to the snapshot file.")))) + .BrowseDirectory(SpatialGDKSettings->GetSpatialOSSnapshotFolderPath()) + .BrowseTitle(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Automatically Generate Launch Configuration")))) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Launch Config File")))) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Path to the launch configuration file.")))) + .BrowseDirectory(SpatialGDKServicesConstants::SpatialOSDirectory) + .BrowseTitle(FText::FromString(FString(TEXT("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) + [ + SNew(STextBlock) + .Text(FText::FromString(FString(TEXT("")))) + .ToolTipText(FText::FromString(FString(TEXT("")))) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SButton) + .Text(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Region")))) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Deployment Cluster")))) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Deployment Tags")))) + .ToolTipText(FText::FromString(FString(TEXT("Tags for the deployment (separated by spaces).")))) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SEditableTextBox) + .Text(FText::FromString(SpatialGDKSettings->GetDeploymentTags())) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Simulated Players")))) + ] + // Toggle Simulated Players + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Deployment Name")))) + .ToolTipText(FText::FromString(FString(TEXT("The name of the simulated player deployment.")))) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SEditableTextBox) + .Text(FText::FromString(SpatialGDKSettings->GetSimulatedPlayerDeploymentName())) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Number of Simulated Players")))) + .ToolTipText(FText::FromString(FString(TEXT("The number of Simulated Players to be launch and connect to the game.")))) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SSpinBox) + .ToolTipText(FText::FromString(FString(TEXT("Number of Simulated Players.")))) + .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(FText::FromString(FString(TEXT("Region")))) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Deployment Cluster")))) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Assembly Configuration")))) + ] + // Build and Upload Assembly + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(FString(TEXT("Build and Upload Assembly")))) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Generate Schema")))) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Generate Snapshot")))) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Build Configuration")))) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Build Client Worker")))) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Force Overwrite on Upload")))) + .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Start Deployment")))) + .OnClicked_Raw(ToolbarPtr, &FSpatialGDKEditorToolbarModule::OnStartCloudDeployment) + .IsEnabled_Raw(ToolbarPtr, &FSpatialGDKEditorToolbarModule::CanStartCloudDeployment) + ] + ] + ] + ] + ] + ] + ] + ]; +} + +void SSpatialGDKCloudDeploymentConfiguration::OnDeploymentAssemblyCommited(const FText& InText, ETextCommit::Type InCommitType) +{ + const FString& InputAssemblyName = InText.ToString(); + if (!USpatialGDKEditorSettings::IsAssemblyNameValid(InputAssemblyName)) + { + AssemblyNameInputErrorReporting->SetError(SpatialConstants::AssemblyPatternHint); + return; + } + AssemblyNameInputErrorReporting->SetError(TEXT("")); + + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetAssemblyName(InputAssemblyName); +} + +void SSpatialGDKCloudDeploymentConfiguration::OnProjectNameCommitted(const FText& InText, ETextCommit::Type InCommitType) +{ + FString NewProjectName = InText.ToString(); + if (!USpatialGDKEditorSettings::IsProjectNameValid(NewProjectName)) + { + ProjectNameInputErrorReporting->SetError(SpatialConstants::ProjectPatternHint); + return; + } + ProjectNameInputErrorReporting->SetError(TEXT("")); + + if (SpatialGDKEditorPtr.IsValid()) + { + SpatialGDKEditorPtr.Pin()->SetProjectName(NewProjectName); + } +} + +void SSpatialGDKCloudDeploymentConfiguration::OnPrimaryDeploymentNameCommited(const FText& InText, ETextCommit::Type InCommitType) +{ + const FString& InputDeploymentName = InText.ToString(); + if (!USpatialGDKEditorSettings::IsDeploymentNameValid(InputDeploymentName)) + { + DeploymentNameInputErrorReporting->SetError(SpatialConstants::DeploymentPatternHint); + return; + } + DeploymentNameInputErrorReporting->SetError(TEXT("")); + + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetPrimaryDeploymentName(InputDeploymentName); +} + +void SSpatialGDKCloudDeploymentConfiguration::OnCheckedUsePinnedVersion(ECheckBoxState NewCheckedState) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetUseGDKPinnedRuntimeVersionForCloud(SpatialGDKSettings->RuntimeVariant, NewCheckedState == ECheckBoxState::Checked); +} + +void SSpatialGDKCloudDeploymentConfiguration::OnRuntimeCustomVersionCommited(const FText& InText, ETextCommit::Type InCommitType) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetCustomCloudSpatialOSRuntimeVersion(SpatialGDKSettings->RuntimeVariant, InText.ToString()); +} + +void SSpatialGDKCloudDeploymentConfiguration::OnSnapshotPathPicked(const FString& PickedPath) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetSnapshotPath(PickedPath); +} + +void SSpatialGDKCloudDeploymentConfiguration::OnPrimaryLaunchConfigPathPicked(const FString& PickedPath) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetPrimaryLaunchConfigPath(PickedPath); +} + +void SSpatialGDKCloudDeploymentConfiguration::OnDeploymentTagsCommitted(const FText& InText, ETextCommit::Type InCommitType) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetDeploymentTags(InText.ToString()); +} + +TSharedRef SSpatialGDKCloudDeploymentConfiguration::OnGetPrimaryDeploymentRegionCode() +{ + FMenuBuilder MenuBuilder(true, NULL); + UEnum* pEnum = FindObject(ANY_PACKAGE, TEXT("ERegionCode"), true); + + if (pEnum != nullptr) + { + for (int32 EnumIdx = 0; EnumIdx < pEnum->NumEnums() - 1; EnumIdx++) + { + if (!pEnum->HasMetaData(TEXT("Hidden"), EnumIdx)) + { + int64 CurrentEnumValue = pEnum->GetValueByIndex(EnumIdx); + FUIAction ItemAction(FExecuteAction::CreateSP(this, &SSpatialGDKCloudDeploymentConfiguration::OnPrimaryDeploymentRegionCodePicked, CurrentEnumValue)); + MenuBuilder.AddMenuEntry(pEnum->GetDisplayNameTextByValue(CurrentEnumValue), TAttribute(), FSlateIcon(), ItemAction); + } + } + } + + return MenuBuilder.MakeWidget(); +} + +EVisibility SSpatialGDKCloudDeploymentConfiguration::GetRegionPickerVisibility() const +{ + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + return SpatialGDKSettings->IsRunningInChina() ? EVisibility::Collapsed : EVisibility::SelfHitTestInvisible; +} + +bool SSpatialGDKCloudDeploymentConfiguration::IsPrimaryRegionPickerEnabled() const +{ + const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); + return SpatialGDKEditorSettings->GetMainDeploymentCluster().IsEmpty(); +} + +bool SSpatialGDKCloudDeploymentConfiguration::IsSimulatedPlayerRegionPickerEnabled() const +{ + const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); + return SpatialGDKEditorSettings->IsSimulatedPlayersEnabled() && SpatialGDKEditorSettings->GetSimulatedPlayerCluster().IsEmpty(); +} + +void SSpatialGDKCloudDeploymentConfiguration::OnDeploymentClusterCommited(const FText& InText, ETextCommit::Type InCommitType) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetMainDeploymentCluster(InText.ToString()); +} + +TSharedRef SSpatialGDKCloudDeploymentConfiguration::OnGetSimulatedPlayerDeploymentRegionCode() +{ + FMenuBuilder MenuBuilder(true, NULL); + UEnum* pEnum = FindObject(ANY_PACKAGE, TEXT("ERegionCode"), true); + + if (pEnum != nullptr) + { + for (int32 EnumIdx = 0; EnumIdx < pEnum->NumEnums() - 1; EnumIdx++) + { + if (!pEnum->HasMetaData(TEXT("Hidden"), EnumIdx)) + { + int64 CurrentEnumValue = pEnum->GetValueByIndex(EnumIdx); + FUIAction ItemAction(FExecuteAction::CreateSP(this, &SSpatialGDKCloudDeploymentConfiguration::OnSimulatedPlayerDeploymentRegionCodePicked, CurrentEnumValue)); + MenuBuilder.AddMenuEntry(pEnum->GetDisplayNameTextByValue(CurrentEnumValue), TAttribute(), FSlateIcon(), ItemAction); + } + } + } + + return MenuBuilder.MakeWidget(); +} + +void SSpatialGDKCloudDeploymentConfiguration::OnSimulatedPlayerClusterCommited(const FText& InText, ETextCommit::Type InCommitType) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetSimulatedPlayerCluster(InText.ToString()); +} + +void SSpatialGDKCloudDeploymentConfiguration::OnPrimaryDeploymentRegionCodePicked(const int64 RegionCodeEnumValue) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetPrimaryRegionCode((ERegionCode::Type) RegionCodeEnumValue); + +} + +void SSpatialGDKCloudDeploymentConfiguration::OnSimulatedPlayerDeploymentRegionCodePicked(const int64 RegionCodeEnumValue) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetSimulatedPlayerRegionCode((ERegionCode::Type) RegionCodeEnumValue); +} + +void SSpatialGDKCloudDeploymentConfiguration::OnSimulatedPlayerDeploymentNameCommited(const FText& InText, ETextCommit::Type InCommitType) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetSimulatedPlayerDeploymentName(InText.ToString()); +} + +void SSpatialGDKCloudDeploymentConfiguration::OnNumberOfSimulatedPlayersCommited(uint32 NewValue) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetNumberOfSimulatedPlayers(NewValue); +} + +FReply SSpatialGDKCloudDeploymentConfiguration::OnRefreshClicked() +{ + // TODO (UNR-1193): Invoke the Deployment Launcher script to list the deployments + return FReply::Handled(); +} + +FReply SSpatialGDKCloudDeploymentConfiguration::OnStopClicked() +{ + 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")) + { + ToolbarPtr->OnShowSuccessNotification("Successfully stopped cloud deployment."); + } + }), + + FSimpleDelegate::CreateLambda([]() + { + if (FSpatialGDKEditorToolbarModule* ToolbarPtr = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) + { + ToolbarPtr->OnShowFailedNotification("Failed to stop cloud deployment."); + } + })); + } + return FReply::Handled(); +} + +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); + if (!WebError.IsEmpty()) + { + FNotificationInfo Info(FText::FromString(WebError)); + Info.ExpireDuration = 3.0f; + Info.bUseSuccessFailIcons = true; + TSharedPtr NotificationItem = FSlateNotificationManager::Get().AddNotification(Info); + NotificationItem->SetCompletionState(SNotificationItem::CS_Fail); + NotificationItem->ExpireAndFadeout(); + } +} + +void SSpatialGDKCloudDeploymentConfiguration::OnCheckedSimulatedPlayers(ECheckBoxState NewCheckedState) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetSimulatedPlayersEnabledState(NewCheckedState == ECheckBoxState::Checked); +} + +ECheckBoxState SSpatialGDKCloudDeploymentConfiguration::IsBuildAndUploadAssemblyEnabled() const +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + return SpatialGDKSettings->ShouldBuildAndUploadAssembly() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; +} + +void SSpatialGDKCloudDeploymentConfiguration::OnCheckedBuildAndUploadAssembly(ECheckBoxState NewCheckedState) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetBuildAndUploadAssembly(NewCheckedState == ECheckBoxState::Checked); +} + +ECheckBoxState SSpatialGDKCloudDeploymentConfiguration::IsSimulatedPlayersEnabled() const +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + return SpatialGDKSettings->IsSimulatedPlayersEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; +} + +ECheckBoxState SSpatialGDKCloudDeploymentConfiguration::IsUsingGDKPinnedRuntimeVersion() const +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + const FRuntimeVariantVersion& RuntimeVersion = SpatialGDKSettings->GetSelectedRuntimeVariantVersion(); + return RuntimeVersion.GetUseGDKPinnedRuntimeVersionForCloud() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; +} + +bool SSpatialGDKCloudDeploymentConfiguration::IsUsingCustomRuntimeVersion() const +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + const FRuntimeVariantVersion& RuntimeVersion = SpatialGDKSettings->GetSelectedRuntimeVariantVersion(); + return !RuntimeVersion.GetUseGDKPinnedRuntimeVersionForCloud(); +} + +FText SSpatialGDKCloudDeploymentConfiguration::GetSpatialOSRuntimeVersionToUseText() const +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + const FRuntimeVariantVersion& RuntimeVersion = SpatialGDKSettings->GetSelectedRuntimeVariantVersion(); + const FString& RuntimeVersionString = RuntimeVersion.GetVersionForCloud(); + return FText::FromString(RuntimeVersionString); +} + +bool SSpatialGDKCloudDeploymentConfiguration::CanPickOrEditCloudLaunchConfig() const +{ + const USpatialGDKEditorSettings* SpatialGKDSettings = GetDefault(); + return !SpatialGKDSettings->ShouldAutoGenerateCloudLaunchConfig(); +} + +ECheckBoxState SSpatialGDKCloudDeploymentConfiguration::IsAutoGenerateCloudLaunchConfigEnabled() const +{ + const USpatialGDKEditorSettings* SpatialGKDSettings = GetDefault(); + return SpatialGKDSettings->ShouldAutoGenerateCloudLaunchConfig() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; +} + +void SSpatialGDKCloudDeploymentConfiguration::OnCheckedAutoGenerateCloudLaunchConfig(ECheckBoxState NewCheckedState) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetAutoGenerateCloudLaunchConfigEnabledState(NewCheckedState == ECheckBoxState::Checked); +} + +FReply SSpatialGDKCloudDeploymentConfiguration::OnOpenLaunchConfigEditor() +{ + ULaunchConfigurationEditor::OpenModalWindow(ParentWindowPtr.Pin(), [](const FString& FilePath) { + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetPrimaryLaunchConfigPath(FilePath); + }); + + return FReply::Handled(); +} + +TSharedRef SSpatialGDKCloudDeploymentConfiguration::OnGetBuildConfiguration() +{ + FMenuBuilder MenuBuilder(true, nullptr); + + MenuBuilder.AddMenuEntry(FText::FromString(DebugConfiguration), TAttribute(), FSlateIcon(), + FUIAction(FExecuteAction::CreateSP(this, &SSpatialGDKCloudDeploymentConfiguration::OnBuildConfigurationPicked, DebugConfiguration)) + ); + + MenuBuilder.AddMenuEntry(FText::FromString(DebugGameConfiguration), TAttribute(), FSlateIcon(), + FUIAction(FExecuteAction::CreateSP(this, &SSpatialGDKCloudDeploymentConfiguration::OnBuildConfigurationPicked, DebugGameConfiguration)) + ); + + MenuBuilder.AddMenuEntry(FText::FromString(DevelopmentConfiguration), TAttribute(), FSlateIcon(), + 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(ShippingConfiguration), TAttribute(), FSlateIcon(), + FUIAction(FExecuteAction::CreateSP(this, &SSpatialGDKCloudDeploymentConfiguration::OnBuildConfigurationPicked, ShippingConfiguration)) + ); + + return MenuBuilder.MakeWidget(); +} + +void SSpatialGDKCloudDeploymentConfiguration::OnBuildConfigurationPicked(FString Configuration) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetAssemblyBuildConfiguration(Configuration); +} + +ECheckBoxState SSpatialGDKCloudDeploymentConfiguration::ForceAssemblyOverwrite() const +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + return SpatialGDKSettings->IsForceAssemblyOverwriteEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; +} + +void SSpatialGDKCloudDeploymentConfiguration::OnCheckedForceAssemblyOverwrite(ECheckBoxState NewCheckedState) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetForceAssemblyOverwrite(NewCheckedState == ECheckBoxState::Checked); +} + +ECheckBoxState SSpatialGDKCloudDeploymentConfiguration::IsBuildClientWorkerEnabled() const +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + return SpatialGDKSettings->IsBuildClientWorkerEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; +} + +void SSpatialGDKCloudDeploymentConfiguration::OnCheckedBuildClientWorker(ECheckBoxState NewCheckedState) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetBuildClientWorker(NewCheckedState == ECheckBoxState::Checked); +} + +ECheckBoxState SSpatialGDKCloudDeploymentConfiguration::IsGenerateSchemaEnabled() const +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + return SpatialGDKSettings->IsGenerateSchemaEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; +} + +void SSpatialGDKCloudDeploymentConfiguration::OnCheckedGenerateSchema(ECheckBoxState NewCheckedState) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetGenerateSchema(NewCheckedState == ECheckBoxState::Checked); +} + +ECheckBoxState SSpatialGDKCloudDeploymentConfiguration::IsGenerateSnapshotEnabled() const +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + return SpatialGDKSettings->IsGenerateSnapshotEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; +} + +void SSpatialGDKCloudDeploymentConfiguration::OnCheckedGenerateSnapshot(ECheckBoxState NewCheckedState) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetGenerateSnapshot(NewCheckedState == ECheckBoxState::Checked); +} + +FReply SSpatialGDKCloudDeploymentConfiguration::OnOpenCloudDeploymentPageClicked() +{ + FString ProjectName = FSpatialGDKServicesModule::GetProjectName(); + FString ConsoleHost = GetDefault()->IsRunningInChina() ? SpatialConstants::CONSOLE_HOST_CN : SpatialConstants::CONSOLE_HOST; + FString Url = FString::Printf(TEXT("https://%s/projects/%s"), *ConsoleHost, *ProjectName); + + FString WebError; + FPlatformProcess::LaunchURL(*Url, TEXT(""), &WebError); + if (!WebError.IsEmpty()) + { + FNotificationInfo Info(FText::FromString(WebError)); + Info.ExpireDuration = 3.0f; + Info.bUseSuccessFailIcons = true; + TSharedPtr NotificationItem = FSlateNotificationManager::Get().AddNotification(Info); + NotificationItem->SetCompletionState(SNotificationItem::CS_Fail); + NotificationItem->ExpireAndFadeout(); + return FReply::Unhandled(); + } + + return FReply::Handled(); +} + +bool SSpatialGDKCloudDeploymentConfiguration::CanOpenCloudDeploymentPage() const +{ + return !FSpatialGDKServicesModule::GetProjectName().IsEmpty(); +} diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp index 7a1de185be..2f1fd107e6 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp @@ -2,51 +2,52 @@ #include "SpatialGDKEditorToolbar.h" +#include "AssetRegistryModule.h" #include "Async/Async.h" #include "Editor.h" #include "Editor/EditorEngine.h" #include "EditorStyleSet.h" +#include "EngineClasses/SpatialWorldSettings.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 "LevelEditor.h" +#include "Misc/FileHelper.h" #include "Misc/MessageDialog.h" -#include "SpatialGDKEditorToolbarCommands.h" -#include "SpatialGDKEditorToolbarStyle.h" +#include "Sound/SoundBase.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Notifications/SNotificationList.h" +#include "CloudDeploymentConfiguration.h" +#include "SpatialCommandUtils.h" #include "SpatialConstants.h" #include "SpatialGDKDefaultLaunchConfigGenerator.h" #include "SpatialGDKDefaultWorkerJsonGenerator.h" +#include "SpatialGDKDevAuthTokenGenerator.h" #include "SpatialGDKEditor.h" +#include "SpatialGDKEditorModule.h" #include "SpatialGDKEditorSchemaGenerator.h" #include "SpatialGDKEditorSettings.h" #include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesModule.h" #include "SpatialGDKSettings.h" -#include "SpatialGDKSimulatedPlayerDeployment.h" -#include "Utils/LaunchConfigEditor.h" - -#include "Editor/EditorEngine.h" -#include "HAL/FileManager.h" -#include "Sound/SoundBase.h" - -#include "AssetRegistryModule.h" -#include "GeneralProjectSettings.h" -#include "LevelEditor.h" -#include "Misc/FileHelper.h" -#include "EngineClasses/SpatialWorldSettings.h" -#include "EditorExtension/LBStrategyEditorExtension.h" -#include "LoadBalancing/AbstractLBStrategy.h" -#include "SpatialGDKEditorModule.h" +#include "SpatialGDKEditorPackageAssembly.h" +#include "SpatialGDKEditorSnapshotGenerator.h" +#include "SpatialGDKEditorToolbarCommands.h" +#include "SpatialGDKEditorToolbarStyle.h" +#include "SpatialGDKCloudDeploymentConfiguration.h" +#include "SpatialRuntimeLoadBalancingStrategies.h" +#include "Utils/LaunchConfigurationEditor.h" DEFINE_LOG_CATEGORY(LogSpatialGDKEditorToolbar); @@ -55,6 +56,7 @@ DEFINE_LOG_CATEGORY(LogSpatialGDKEditorToolbar); FSpatialGDKEditorToolbarModule::FSpatialGDKEditorToolbarModule() : bStopSpatialOnExit(false) , bSchemaBuildError(false) +, bStartingCloudDeployment(false) { } @@ -76,27 +78,21 @@ void FSpatialGDKEditorToolbarModule::StartupModule() ExecutionSuccessSound->AddToRoot(); ExecutionFailSound = LoadObject(nullptr, TEXT("/Engine/EditorSounds/Notifications/CompileFailed_Cue.CompileFailed_Cue")); ExecutionFailSound->AddToRoot(); - SpatialGDKEditorInstance = MakeShareable(new FSpatialGDKEditor()); const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); OnPropertyChangedDelegateHandle = FCoreUObjectDelegates::OnObjectPropertyChanged.AddRaw(this, &FSpatialGDKEditorToolbarModule::OnPropertyChanged); bStopSpatialOnExit = SpatialGDKEditorSettings->bStopSpatialOnExit; + // Check for UseChinaServicesRegion file in the plugin directory to determine the services region. + bool bUseChinaServicesRegion = FPaths::FileExists(FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(SpatialGDKServicesConstants::UseChinaServicesRegionFilename)); + GetMutableDefault()->SetServicesRegion(bUseChinaServicesRegion ? EServicesRegion::CN : EServicesRegion::Default); + FSpatialGDKServicesModule& GDKServices = FModuleManager::GetModuleChecked("SpatialGDKServices"); LocalDeploymentManager = GDKServices.GetLocalDeploymentManager(); LocalDeploymentManager->PreInit(GetDefault()->IsRunningInChina()); - LocalDeploymentManager->SetAutoDeploy(SpatialGDKEditorSettings->bAutoStartLocalDeployment); - - // Bind the play button delegate to starting a local spatial deployment. - if (!UEditorEngine::TryStartSpatialDeployment.IsBound() && SpatialGDKEditorSettings->bAutoStartLocalDeployment) - { - UEditorEngine::TryStartSpatialDeployment.BindLambda([this] - { - VerifyAndStartDeployment(); - }); - } + OnAutoStartLocalDeploymentChanged(); FEditorDelegates::PreBeginPIE.AddLambda([this](bool bIsSimulatingInEditor) { @@ -118,6 +114,7 @@ void FSpatialGDKEditorToolbarModule::StartupModule() }); LocalDeploymentManager->Init(GetOptionalExposedRuntimeIP()); + SpatialGDKEditorInstance = FModuleManager::GetModuleChecked("SpatialGDKEditor").GetSpatialGDKEditorInstance(); } void FSpatialGDKEditorToolbarModule::ShutdownModule() @@ -199,11 +196,25 @@ void FSpatialGDKEditorToolbarModule::MapActions(TSharedPtr FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CanExecuteSnapshotGenerator)); InPluginCommands->MapAction( - FSpatialGDKEditorToolbarCommands::Get().StartSpatialDeployment, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartSpatialDeploymentButtonClicked), - FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartSpatialDeploymentCanExecute), + 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(), + FIsActionButtonVisible::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartLocalSpatialDeploymentIsVisible)); + + InPluginCommands->MapAction( + FSpatialGDKEditorToolbarCommands::Get().StartCloudSpatialDeployment, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::LaunchOrShowCloudDeployment), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartCloudSpatialDeploymentCanExecute), FIsActionChecked(), - FIsActionButtonVisible::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartSpatialDeploymentIsVisible)); + FIsActionButtonVisible::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartCloudSpatialDeploymentIsVisible)); InPluginCommands->MapAction( FSpatialGDKEditorToolbarCommands::Get().StopSpatialDeployment, @@ -218,15 +229,27 @@ void FSpatialGDKEditorToolbarModule::MapActions(TSharedPtr FCanExecuteAction()); InPluginCommands->MapAction( - FSpatialGDKEditorToolbarCommands::Get().OpenSimulatedPlayerConfigurationWindowAction, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::ShowSimulatedPlayerDeploymentDialog), + FSpatialGDKEditorToolbarCommands::Get().EnableBuildClientWorker, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::OnCheckedBuildClientWorker), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::AreCloudDeploymentPropertiesEditable), + FIsActionChecked::CreateRaw(this, &FSpatialGDKEditorToolbarModule::IsBuildClientWorkerEnabled)); + + InPluginCommands->MapAction( + FSpatialGDKEditorToolbarCommands::Get().EnableBuildSimulatedPlayer, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::OnCheckedSimulatedPlayers), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::AreCloudDeploymentPropertiesEditable), + FIsActionChecked::CreateRaw(this, &FSpatialGDKEditorToolbarModule::IsSimulatedPlayersEnabled)); + + InPluginCommands->MapAction( + FSpatialGDKEditorToolbarCommands::Get().OpenCloudDeploymentWindowAction, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::ShowCloudDeploymentDialog), FCanExecuteAction()); InPluginCommands->MapAction( FSpatialGDKEditorToolbarCommands::Get().OpenLaunchConfigurationEditorAction, FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::OpenLaunchConfigurationEditor), FCanExecuteAction()); - + InPluginCommands->MapAction( FSpatialGDKEditorToolbarCommands::Get().StartSpatialService, FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartSpatialServiceButtonClicked), @@ -240,6 +263,32 @@ void FSpatialGDKEditorToolbarModule::MapActions(TSharedPtr FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialServiceCanExecute), FIsActionChecked(), FIsActionButtonVisible::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialServiceIsVisible)); + + InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().EnableSpatialNetworking, + 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) + ); + + InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().CloudDeployment, + 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) + ); + + InPluginCommands->MapAction(FSpatialGDKEditorToolbarCommands::Get().GDKRuntimeSettings, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::GDKRuntimeSettingsClicked) + ); } void FSpatialGDKEditorToolbarModule::SetupToolbar(TSharedPtr InPluginCommands) @@ -270,14 +319,16 @@ void FSpatialGDKEditorToolbarModule::AddMenuExtension(FMenuBuilder& Builder) { Builder.BeginSection("SpatialOS Unreal GDK", LOCTEXT("SpatialOS Unreal GDK", "SpatialOS Unreal GDK")); { - Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSchema); - Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSnapshot); - Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StartSpatialDeployment); + Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StartNative); + Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StartLocalSpatialDeployment); + Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StartCloudSpatialDeployment); Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StopSpatialDeployment); Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().LaunchInspectorWebPageAction); #if PLATFORM_WINDOWS - Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().OpenSimulatedPlayerConfigurationWindowAction); + Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().OpenCloudDeploymentWindowAction); #endif + Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSchema); + Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSnapshot); Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StartSpatialService); Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StopSpatialService); } @@ -287,21 +338,21 @@ void FSpatialGDKEditorToolbarModule::AddMenuExtension(FMenuBuilder& Builder) void FSpatialGDKEditorToolbarModule::AddToolbarExtension(FToolBarBuilder& Builder) { Builder.AddSeparator(NAME_None); - Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSchema); + Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StartNative); + Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StartLocalSpatialDeployment); + Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StartCloudSpatialDeployment); + Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StopSpatialDeployment); Builder.AddComboButton( FUIAction(), - FOnGetContent::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CreateGenerateSchemaMenuContent), - LOCTEXT("GDKSchemaCombo_Label", "Schema Generation Options"), + FOnGetContent::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CreateStartDropDownMenuContent), + LOCTEXT("StartDropDownMenu_Label", "SpatialOS Network Options"), TAttribute(), - FSlateIcon(FEditorStyle::GetStyleSetName(), "GDK.Schema"), + FSlateIcon(FEditorStyle::GetStyleSetName(), "GDK.Start"), true ); - Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSnapshot); - Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StartSpatialDeployment); - Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StopSpatialDeployment); Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().LaunchInspectorWebPageAction); #if PLATFORM_WINDOWS - Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().OpenSimulatedPlayerConfigurationWindowAction); + Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().OpenCloudDeploymentWindowAction); Builder.AddComboButton( FUIAction(), FOnGetContent::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CreateLaunchDeploymentMenuContent), @@ -311,6 +362,16 @@ void FSpatialGDKEditorToolbarModule::AddToolbarExtension(FToolBarBuilder& Builde 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.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSnapshot); Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StartSpatialService); Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StopSpatialService); } @@ -340,6 +401,105 @@ TSharedRef FSpatialGDKEditorToolbarModule::CreateLaunchDeploymentMenuCo return MenuBuilder.MakeWidget(); } +void OnLocalDeploymentIPChanged(const FText& InText, ETextCommit::Type InCommitType) +{ + if (InCommitType != ETextCommit::OnEnter && InCommitType != ETextCommit::OnUserMovedFocus) + { + return; + } + + const FString& InputIpAddress = InText.ToString(); + const FRegexPattern IpV4PatternRegex(SpatialConstants::Ipv4Pattern); + FRegexMatcher IpV4RegexMatcher(IpV4PatternRegex, InputIpAddress); + if (!InputIpAddress.IsEmpty() && !IpV4RegexMatcher.FindNext()) + { + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(TEXT("Please input a valid IP address."))); + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Invalid IP address: %s"), *InputIpAddress); + return; + } + + USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetMutableDefault(); + SpatialGDKEditorSettings->SetExposedRuntimeIP(InputIpAddress); + UE_LOG(LogSpatialGDKEditorToolbar, Display, TEXT("Setting local deployment IP address to %s"), *InputIpAddress); +} + +void OnCloudDeploymentNameChanged(const FText& InText, ETextCommit::Type InCommitType) +{ + if (InCommitType != ETextCommit::OnEnter && InCommitType != ETextCommit::OnUserMovedFocus) + { + return; + } + + const FString& InputDeploymentName = InText.ToString(); + const FRegexPattern DeploymentNamePatternRegex(SpatialConstants::DeploymentPattern); + FRegexMatcher DeploymentNameRegexMatcher(DeploymentNamePatternRegex, InputDeploymentName); + if (!InputDeploymentName.IsEmpty() && !DeploymentNameRegexMatcher.FindNext()) + { + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(TEXT("Please input a valid deployment name. %s"), *SpatialConstants::DeploymentPatternHint))); + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Invalid deployment name: %s"), *InputDeploymentName); + return; + } + + USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetMutableDefault(); + SpatialGDKEditorSettings->SetDevelopmentDeploymentToConnect(InputDeploymentName); + + UE_LOG(LogSpatialGDKEditorToolbar, Display, TEXT("Setting cloud deployment name to %s"), *InputDeploymentName); +} + +TSharedRef FSpatialGDKEditorToolbarModule::CreateStartDropDownMenuContent() +{ + FMenuBuilder MenuBuilder(false /*bInShouldCloseWindowAfterMenuSelection*/, PluginCommands); + UGeneralProjectSettings* GeneralProjectSettings = GetMutableDefault(); + USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetMutableDefault(); + MenuBuilder.BeginSection("SpatialOSSettings", LOCTEXT("SpatialOSSettingsLabel", "SpatialOS Settings")); + { + MenuBuilder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().EnableSpatialNetworking); + } + MenuBuilder.EndSection(); + + MenuBuilder.BeginSection("ConnectionFlow", LOCTEXT("ConnectionFlowLabel", "Connection Flow")); + { + MenuBuilder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().LocalDeployment); + MenuBuilder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().CloudDeployment); + } + MenuBuilder.EndSection(); + + MenuBuilder.BeginSection("AdditionalProperties"); + { + MenuBuilder.AddWidget(SNew(SEditableText) + .OnTextCommitted_Static(OnLocalDeploymentIPChanged) + .Text(FText::FromString(GetDefault()->ExposedRuntimeIP)) + .SelectAllTextWhenFocused(true) + .ColorAndOpacity(FLinearColor::White * 0.8f) + .IsEnabled_Raw(this, &FSpatialGDKEditorToolbarModule::IsLocalDeploymentIPEditable) + .Font(FEditorStyle::GetFontStyle(TEXT("SourceControl.LoginWindow.Font"))), + LOCTEXT("LocalDeploymentIPLabel", "Local Deployment IP:") + ); + + MenuBuilder.AddWidget(SNew(SEditableText) + .OnTextCommitted_Static(OnCloudDeploymentNameChanged) + .Text(FText::FromString(SpatialGDKEditorSettings->DevelopmentDeploymentToConnect)) + .SelectAllTextWhenFocused(true) + .ColorAndOpacity(FLinearColor::White * 0.8f) + .IsEnabled_Raw(this, &FSpatialGDKEditorToolbarModule::AreCloudDeploymentPropertiesEditable) + .Font(FEditorStyle::GetFontStyle(TEXT("SourceControl.LoginWindow.Font"))), + LOCTEXT("CloudDeploymentNameLabel", "Cloud Deployment Name:") + ); + MenuBuilder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().EnableBuildClientWorker); + MenuBuilder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().EnableBuildSimulatedPlayer); + } + MenuBuilder.EndSection(); + + MenuBuilder.BeginSection("SettingsShortcuts"); + { + MenuBuilder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().GDKEditorSettings); + MenuBuilder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().GDKRuntimeSettings); + } + MenuBuilder.EndSection(); + + return MenuBuilder.MakeWidget(); +} + void FSpatialGDKEditorToolbarModule::CreateSnapshotButtonClicked() { OnShowTaskStartNotification("Started snapshot generation"); @@ -377,7 +537,35 @@ void FSpatialGDKEditorToolbarModule::SchemaGenerateButtonClicked() void FSpatialGDKEditorToolbarModule::SchemaGenerateFullButtonClicked() { GenerateSchema(true); -} +} + +void FSpatialGDKEditorToolbarModule::OnShowSingleFailureNotification(const FString& NotificationText) +{ + AsyncTask(ENamedThreads::GameThread, [NotificationText] + { + if (FSpatialGDKEditorToolbarModule* Module = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) + { + Module->ShowSingleFailureNotification(NotificationText); + } + }); +} + +void FSpatialGDKEditorToolbarModule::ShowSingleFailureNotification(const FString& NotificationText) +{ + // If a task notification already exists then expire it. + if (TaskNotificationPtr.IsValid()) + { + TaskNotificationPtr.Pin()->ExpireAndFadeout(); + } + + FNotificationInfo Info(FText::AsCultureInvariant(NotificationText)); + Info.Image = FSpatialGDKEditorToolbarStyle::Get().GetBrush(TEXT("SpatialGDKEditorToolbar.SpatialOSLogo")); + Info.ExpireDuration = 5.0f; + Info.bFireAndForget = false; + + TaskNotificationPtr = FSlateNotificationManager::Get().AddNotification(Info); + ShowFailedNotification(NotificationText); +} void FSpatialGDKEditorToolbarModule::OnShowTaskStartNotification(const FString& NotificationText) { @@ -521,33 +709,6 @@ void FSpatialGDKEditorToolbarModule::StopSpatialServiceButtonClicked() }); } -bool FSpatialGDKEditorToolbarModule::FillWorkerLaunchConfigFromWorldSettings(UWorld& World, FWorkerTypeLaunchSection& OutLaunchConfig, FIntPoint& OutWorldDimension) -{ - const ASpatialWorldSettings* WorldSettings = Cast(World.GetWorldSettings()); - - if (!WorldSettings) - { - UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Missing SpatialWorldSettings on map %s"), *World.GetMapName()); - return false; - } - - if (!WorldSettings->LoadBalanceStrategy) - { - UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Missing Load balancing strategy on map %s"), *World.GetMapName()); - return false; - } - - FSpatialGDKEditorModule& EditorModule = FModuleManager::GetModuleChecked("SpatialGDKEditor"); - - if (!EditorModule.GetLBStrategyExtensionManager().GetDefaultLaunchConfiguration(WorldSettings->LoadBalanceStrategy->GetDefaultObject(), OutLaunchConfig, OutWorldDimension)) - { - UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Could not get the number of worker to launch for load balancing strategy %s"), *WorldSettings->LoadBalanceStrategy->GetName()); - return false; - } - - return true; -} - void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment() { // Don't try and start a local deployment if spatial networking is disabled. @@ -559,23 +720,10 @@ void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment() if (!IsSnapshotGenerated()) { - UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Attempted to start a local deployment but snapshot is not generated.")); - return; - } - - if (!IsSchemaGenerated()) - { - UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Attempted to start a local deployment but schema is not generated.")); - return; - } - - if (bSchemaBuildError) - { - UE_LOG(LogSpatialGDKEditorToolbar, Warning, TEXT("Schema did not previously compile correctly, you may be running a stale build.")); - - EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString("Last schema generation failed or failed to run the schema compiler. Schema will most likely be out of date, which may lead to undefined behavior. Are you sure you want to continue?")); - if (Result == EAppReturnType::No) + const USpatialGDKEditorSettings* Settings = GetDefault(); + if (!SpatialGDKGenerateSnapshot(GEditor->GetEditorWorldContext().World(), Settings->GetSpatialOSSnapshotToLoadPath())) { + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Attempted to start a local deployment but failed to generate a snapshot.")); return; } } @@ -603,56 +751,53 @@ void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment() LaunchConfig = FPaths::Combine(FPaths::ConvertRelativePathToFull(FPaths::ProjectIntermediateDir()), FString::Printf(TEXT("Improbable/%s_LocalLaunchConfig.json"), *EditorWorld->GetMapName())); FSpatialLaunchConfigDescription LaunchConfigDescription = SpatialGDKEditorSettings->LaunchConfigDesc; - if (SpatialGDKSettings->bEnableUnrealLoadBalancer) - { - FIntPoint WorldDimensions; - FWorkerTypeLaunchSection WorkerLaunch; + USingleWorkerRuntimeStrategy* DefaultStrategy = USingleWorkerRuntimeStrategy::StaticClass()->GetDefaultObject(); + UAbstractRuntimeLoadBalancingStrategy* LoadBalancingStrat = DefaultStrategy; - if (FillWorkerLaunchConfigFromWorldSettings(*EditorWorld, WorkerLaunch, WorldDimensions)) - { - LaunchConfigDescription.World.Dimensions = WorldDimensions; - LaunchConfigDescription.ServerWorkers.Empty(SpatialGDKSettings->ServerWorkerTypes.Num()); - - for (auto WorkerType : SpatialGDKSettings->ServerWorkerTypes) - { - LaunchConfigDescription.ServerWorkers.Add(WorkerLaunch); - LaunchConfigDescription.ServerWorkers.Last().WorkerTypeName = WorkerType; - } - } + if (TryGetLoadBalancingStrategyFromWorldSettings(*EditorWorld, LoadBalancingStrat, LaunchConfigDescription.World.Dimensions)) + { + LoadBalancingStrat->AddToRoot(); } - for (auto& WorkerLaunchSection : LaunchConfigDescription.ServerWorkers) + FWorkerTypeLaunchSection Conf = SpatialGDKEditorSettings->LaunchConfigDesc.ServerWorkerConfig; + // Force manual connection to true as this is the config for PIE. + Conf.bManualWorkerConnectionOnly = true; + if (Conf.bAutoNumEditorInstances) { - WorkerLaunchSection.bManualWorkerConnectionOnly = true; + Conf.NumEditorInstances = GetWorkerCountFromWorldSettings(*EditorWorld); } - if (!ValidateGeneratedLaunchConfig(LaunchConfigDescription)) + if (!ValidateGeneratedLaunchConfig(LaunchConfigDescription, Conf)) { return; } - GenerateDefaultLaunchConfig(LaunchConfig, &LaunchConfigDescription); - LaunchConfigDescription.SetLevelEditorPlaySettingsWorkerTypes(); + GenerateLaunchConfig(LaunchConfig, &LaunchConfigDescription, Conf); + SetLevelEditorPlaySettingsWorkerType(Conf); // Also create default launch config for cloud deployments. { - for (auto& WorkerLaunchSection : LaunchConfigDescription.ServerWorkers) - { - WorkerLaunchSection.bManualWorkerConnectionOnly = false; - } - + // 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())); - GenerateDefaultLaunchConfig(CloudLaunchConfig, &LaunchConfigDescription); + GenerateLaunchConfig(CloudLaunchConfig, &LaunchConfigDescription, Conf); + } + + if (LoadBalancingStrat != DefaultStrategy) + { + LoadBalancingStrat->RemoveFromRoot(); } } else { LaunchConfig = SpatialGDKEditorSettings->GetSpatialOSLaunchConfig(); + + SetLevelEditorPlaySettingsWorkerType(SpatialGDKEditorSettings->LaunchConfigDesc.ServerWorkerConfig); } const FString LaunchFlags = SpatialGDKEditorSettings->GetSpatialOSCommandLineLaunchFlags(); const FString SnapshotName = SpatialGDKEditorSettings->GetSpatialOSSnapshotToLoad(); - const FString RuntimeVersion = SpatialGDKEditorSettings->GetSpatialOSRuntimeVersionForLocal(); + const FString RuntimeVersion = SpatialGDKEditorSettings->GetSelectedRuntimeVariantVersion().GetVersionForLocal(); AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, LaunchConfig, LaunchFlags, SnapshotName, RuntimeVersion] { @@ -666,7 +811,7 @@ void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment() if (LocalDeploymentManager->IsRedeployRequired() && LocalDeploymentManager->IsLocalDeploymentRunning()) { UE_LOG(LogSpatialGDKEditorToolbar, Display, TEXT("Local deployment must restart.")); - OnShowTaskStartNotification(TEXT("Local deployment restarting.")); + OnShowTaskStartNotification(TEXT("Local deployment restarting.")); LocalDeploymentManager->TryStopLocalDeployment(); } else if (LocalDeploymentManager->IsLocalDeploymentRunning()) @@ -692,7 +837,7 @@ void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment() }); } -void FSpatialGDKEditorToolbarModule::StartSpatialDeploymentButtonClicked() +void FSpatialGDKEditorToolbarModule::StartLocalSpatialDeploymentButtonClicked() { VerifyAndStartDeployment(); } @@ -710,13 +855,24 @@ void FSpatialGDKEditorToolbarModule::StopSpatialDeploymentButtonClicked() { OnShowFailedNotification(TEXT("Failed to stop local deployment!")); } - }); + }); } void FSpatialGDKEditorToolbarModule::LaunchInspectorWebpageButtonClicked() { + // 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(TEXT("http://localhost:31000/inspector"), TEXT(""), &WebError); + FPlatformProcess::LaunchURL(*InspectorURL, TEXT(""), &WebError); if (!WebError.IsEmpty()) { FNotificationInfo Info(FText::FromString(WebError)); @@ -728,21 +884,40 @@ void FSpatialGDKEditorToolbarModule::LaunchInspectorWebpageButtonClicked() } } -bool FSpatialGDKEditorToolbarModule::StartSpatialDeploymentIsVisible() const +bool FSpatialGDKEditorToolbarModule::StartNativeIsVisible() const { - if (LocalDeploymentManager->IsSpatialServiceRunning()) - { - return !LocalDeploymentManager->IsLocalDeploymentRunning(); - } - else - { - return true; - } + return !GetDefault()->UsesSpatialNetworking(); } -bool FSpatialGDKEditorToolbarModule::StartSpatialDeploymentCanExecute() const +bool FSpatialGDKEditorToolbarModule::StartNativeCanExecute() const { - return !LocalDeploymentManager->IsDeploymentStarting() && GetDefault()->UsesSpatialNetworking(); + return false; +} + +bool FSpatialGDKEditorToolbarModule::StartLocalSpatialDeploymentIsVisible() const +{ + return !LocalDeploymentManager->IsLocalDeploymentRunning() && GetDefault()->UsesSpatialNetworking() && GetDefault()->SpatialOSNetFlowType == ESpatialOSNetFlow::LocalDeployment; +} + +bool FSpatialGDKEditorToolbarModule::StartLocalSpatialDeploymentCanExecute() const +{ + return !LocalDeploymentManager->IsServiceStarting() && !LocalDeploymentManager->IsDeploymentStarting(); +} + +bool FSpatialGDKEditorToolbarModule::StartCloudSpatialDeploymentIsVisible() const +{ + return GetDefault()->UsesSpatialNetworking() && GetDefault()->SpatialOSNetFlowType == ESpatialOSNetFlow::CloudDeployment; +} + +bool FSpatialGDKEditorToolbarModule::StartCloudSpatialDeploymentCanExecute() const +{ +#if PLATFORM_MAC + // Launching cloud deployments is not supported on Mac + // TODO: UNR-3396 - allow launching cloud deployments from mac + return false; +#else + return CanBuildAndUpload() && !bStartingCloudDeployment; +#endif } bool FSpatialGDKEditorToolbarModule::StopSpatialDeploymentIsVisible() const @@ -774,6 +949,78 @@ bool FSpatialGDKEditorToolbarModule::StopSpatialServiceIsVisible() const return SpatialGDKSettings->bShowSpatialServiceButton && LocalDeploymentManager->IsSpatialServiceRunning(); } +void FSpatialGDKEditorToolbarModule::OnToggleSpatialNetworking() +{ + UGeneralProjectSettings* GeneralProjectSettings = GetMutableDefault(); + UProperty* SpatialNetworkingProperty = UGeneralProjectSettings::StaticClass()->FindPropertyByName(FName("bSpatialNetworking")); + + GeneralProjectSettings->SetUsesSpatialNetworking(!GeneralProjectSettings->UsesSpatialNetworking()); + GeneralProjectSettings->UpdateSinglePropertyInConfigFile(SpatialNetworkingProperty, GeneralProjectSettings->GetDefaultConfigFilename()); +} + +bool FSpatialGDKEditorToolbarModule::OnIsSpatialNetworkingEnabled() const +{ + return GetDefault()->UsesSpatialNetworking(); +} + +void FSpatialGDKEditorToolbarModule::GDKEditorSettingsClicked() const +{ + FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Editor Settings"); +} + +void FSpatialGDKEditorToolbarModule::GDKRuntimeSettingsClicked() const +{ + FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Runtime Settings"); +} + +bool FSpatialGDKEditorToolbarModule::IsLocalDeploymentSelected() const +{ + const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); + return SpatialGDKEditorSettings->SpatialOSNetFlowType == ESpatialOSNetFlow::LocalDeployment; +} + +bool FSpatialGDKEditorToolbarModule::IsCloudDeploymentSelected() const +{ + const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); + return SpatialGDKEditorSettings->SpatialOSNetFlowType == ESpatialOSNetFlow::CloudDeployment; +} + +bool FSpatialGDKEditorToolbarModule::IsSpatialOSNetFlowConfigurable() const +{ + return OnIsSpatialNetworkingEnabled() && !(LocalDeploymentManager->IsLocalDeploymentRunning()); +} + +void FSpatialGDKEditorToolbarModule::LocalDeploymentClicked() +{ + USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetMutableDefault(); + SpatialGDKEditorSettings->SetSpatialOSNetFlowType(ESpatialOSNetFlow::LocalDeployment); + + OnAutoStartLocalDeploymentChanged(); +} + +void FSpatialGDKEditorToolbarModule::CloudDeploymentClicked() +{ + USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetMutableDefault(); + SpatialGDKEditorSettings->SetSpatialOSNetFlowType(ESpatialOSNetFlow::CloudDeployment); + + TSharedRef DevAuthTokenGenerator = SpatialGDKEditorInstance->GetDevAuthTokenGeneratorRef(); + DevAuthTokenGenerator->AsyncGenerateDevAuthToken(); + + OnAutoStartLocalDeploymentChanged(); +} + +bool FSpatialGDKEditorToolbarModule::IsLocalDeploymentIPEditable() const +{ + const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); + return GetDefault()->UsesSpatialNetworking() && (SpatialGDKEditorSettings->SpatialOSNetFlowType == ESpatialOSNetFlow::LocalDeployment); +} + +bool FSpatialGDKEditorToolbarModule::AreCloudDeploymentPropertiesEditable() const +{ + const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); + return GetDefault()->UsesSpatialNetworking() && (SpatialGDKEditorSettings->SpatialOSNetFlowType == ESpatialOSNetFlow::CloudDeployment); +} + bool FSpatialGDKEditorToolbarModule::StopSpatialServiceCanExecute() const { return !LocalDeploymentManager->IsServiceStopping(); @@ -798,52 +1045,59 @@ void FSpatialGDKEditorToolbarModule::OnPropertyChanged(UObject* ObjectBeingModif } else if (PropertyName.ToString() == TEXT("bAutoStartLocalDeployment")) { - // TODO: UNR-1776 Workaround for SpatialNetDriver requiring editor settings. - LocalDeploymentManager->SetAutoDeploy(Settings->bAutoStartLocalDeployment); - - if (Settings->bAutoStartLocalDeployment) - { - // Bind the TryStartSpatialDeployment delegate if autostart is enabled. - UEditorEngine::TryStartSpatialDeployment.BindLambda([this] - { - VerifyAndStartDeployment(); - }); - } - else - { - // Unbind the TryStartSpatialDeployment if autostart is disabled. - UEditorEngine::TryStartSpatialDeployment.Unbind(); - } + OnAutoStartLocalDeploymentChanged(); } } } -void FSpatialGDKEditorToolbarModule::ShowSimulatedPlayerDeploymentDialog() +void FSpatialGDKEditorToolbarModule::ShowCloudDeploymentDialog() { // Create and open the cloud configuration dialog - SimulatedPlayerDeploymentWindowPtr = SNew(SWindow) - .Title(LOCTEXT("SimulatedPlayerConfigurationTitle", "Cloud Deployment")) - .HasCloseButton(true) - .SupportsMaximize(false) - .SupportsMinimize(false) - .SizingRule(ESizingRule::Autosized); - - SimulatedPlayerDeploymentWindowPtr->SetContent( - SNew(SBox) - .WidthOverride(700.0f) - [ - SAssignNew(SimulatedPlayerDeploymentConfigPtr, SSpatialGDKSimulatedPlayerDeployment) - .SpatialGDKEditor(SpatialGDKEditorInstance) - .ParentWindow(SimulatedPlayerDeploymentWindowPtr) - ] - ); - - FSlateApplication::Get().AddWindow(SimulatedPlayerDeploymentWindowPtr.ToSharedRef()); + if (CloudDeploymentSettingsWindowPtr.IsValid()) + { + CloudDeploymentSettingsWindowPtr->BringToFront(); + } + else + { + CloudDeploymentSettingsWindowPtr = SNew(SWindow) + .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) + { + CloudDeploymentSettingsWindowPtr = nullptr; + })); + FSlateApplication::Get().AddWindow(CloudDeploymentSettingsWindowPtr.ToSharedRef()); + } } void FSpatialGDKEditorToolbarModule::OpenLaunchConfigurationEditor() { - ULaunchConfigurationEditor::LaunchTransientUObjectEditor(TEXT("Launch Configuration Editor")); + ULaunchConfigurationEditor::OpenModalWindow(nullptr); +} + +void FSpatialGDKEditorToolbarModule::LaunchOrShowCloudDeployment() +{ + if (CanStartCloudDeployment()) + { + OnStartCloudDeployment(); + } + else + { + ShowCloudDeploymentDialog(); + } } void FSpatialGDKEditorToolbarModule::GenerateSchema(bool bFullScan) @@ -856,7 +1110,7 @@ void FSpatialGDKEditorToolbarModule::GenerateSchema(bool bFullScan) { OnShowTaskStartNotification("Initial Schema Generation"); - if (SpatialGDKEditorInstance->GenerateSchema(true)) + if (SpatialGDKEditorInstance->GenerateSchema(FSpatialGDKEditor::FullAssetScan)) { OnShowSuccessNotification("Initial Schema Generation completed!"); } @@ -870,7 +1124,7 @@ void FSpatialGDKEditorToolbarModule::GenerateSchema(bool bFullScan) { OnShowTaskStartNotification("Generating Schema (Full)"); - if (SpatialGDKEditorInstance->GenerateSchema(true)) + if (SpatialGDKEditorInstance->GenerateSchema(FSpatialGDKEditor::FullAssetScan)) { OnShowSuccessNotification("Full Schema Generation completed!"); } @@ -884,7 +1138,7 @@ void FSpatialGDKEditorToolbarModule::GenerateSchema(bool bFullScan) { OnShowTaskStartNotification("Generating Schema (Incremental)"); - if (SpatialGDKEditorInstance->GenerateSchema(false)) + if (SpatialGDKEditorInstance->GenerateSchema(FSpatialGDKEditor::InMemoryAsset)) { OnShowSuccessNotification("Incremental Schema Generation completed!"); } @@ -902,35 +1156,240 @@ bool FSpatialGDKEditorToolbarModule::IsSnapshotGenerated() const return FPaths::FileExists(SpatialGDKSettings->GetSpatialOSSnapshotToLoadPath()); } -bool FSpatialGDKEditorToolbarModule::IsSchemaGenerated() const +FString FSpatialGDKEditorToolbarModule::GetOptionalExposedRuntimeIP() const { - 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(); + const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); + if (SpatialGDKEditorSettings->SpatialOSNetFlowType == ESpatialOSNetFlow::LocalDeployment) + { + return SpatialGDKEditorSettings->ExposedRuntimeIP; + } + else + { + return TEXT(""); + } } -FString FSpatialGDKEditorToolbarModule::GetOptionalExposedRuntimeIP() const +void FSpatialGDKEditorToolbarModule::OnAutoStartLocalDeploymentChanged() { - const UGeneralProjectSettings* GeneralProjectSettings = GetDefault(); - const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault(); - if (GeneralProjectSettings->bEnableSpatialLocalLauncher) + 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); + + // TODO: UNR-1776 Workaround for SpatialNetDriver requiring editor settings. + LocalDeploymentManager->SetAutoDeploy(bShouldAutoStartLocalDeployment); + + if (bShouldAutoStartLocalDeployment) { - if (SpatialGDKEditorSettings->bExposeRuntimeIP && GeneralProjectSettings->SpatialLocalDeploymentRuntimeIP != SpatialGDKEditorSettings->ExposedRuntimeIP) + if (!UEditorEngine::TryStartSpatialDeployment.IsBound()) { - UE_LOG(LogSpatialGDKEditorToolbar, Warning, TEXT("Local runtime IP specified from both general settings and Spatial settings! " - "Using IP specified in general settings: %s (Spatial settings has \"%s\")"), - *GeneralProjectSettings->SpatialLocalDeploymentRuntimeIP, *SpatialGDKEditorSettings->ExposedRuntimeIP); + // Bind the TryStartSpatialDeployment delegate if autostart is enabled. + UEditorEngine::TryStartSpatialDeployment.BindLambda([this] + { + VerifyAndStartDeployment(); + }); } - return GeneralProjectSettings->SpatialLocalDeploymentRuntimeIP; } + else + { + if (UEditorEngine::TryStartSpatialDeployment.IsBound()) + { + // Unbind the TryStartSpatialDeployment if autostart is disabled. + UEditorEngine::TryStartSpatialDeployment.Unbind(); + } + } +} - if (SpatialGDKEditorSettings->bExposeRuntimeIP) + +void FSpatialGDKEditorToolbarModule::GenerateConfigFromCurrentMap() +{ + 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())); + + FSpatialLaunchConfigDescription LaunchConfiguration = SpatialGDKEditorSettings->LaunchConfigDesc; + FWorkerTypeLaunchSection& ServerWorkerConfig = LaunchConfiguration.ServerWorkerConfig; + + FillWorkerConfigurationFromCurrentMap(ServerWorkerConfig, LaunchConfiguration.World.Dimensions); + GenerateLaunchConfig(LaunchConfig, &LaunchConfiguration, ServerWorkerConfig); + + SpatialGDKEditorSettings->SetPrimaryLaunchConfigPath(LaunchConfig); +} + +FReply FSpatialGDKEditorToolbarModule::OnStartCloudDeployment() +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + + if (!SpatialGDKSettings->IsDeploymentConfigurationValid()) { - return SpatialGDKEditorSettings->ExposedRuntimeIP; + OnShowFailedNotification(TEXT("Deployment configuration is not valid.")); + + return FReply::Unhandled(); + } + + if (SpatialGDKSettings->ShouldAutoGenerateCloudLaunchConfig()) + { + GenerateConfigFromCurrentMap(); + } + + AddDeploymentTagIfMissing(SpatialConstants::DEV_LOGIN_TAG); + + CloudDeploymentConfiguration.InitFromSettings(); + + const FString& DeploymentName = CloudDeploymentConfiguration.PrimaryDeploymentName; + GetMutableDefault()->SetDevelopmentDeploymentToConnect(DeploymentName); + UE_LOG(LogSpatialGDKEditorToolbar, Display, TEXT("Setting deployment to connect to %s"), *DeploymentName); + + if (CloudDeploymentConfiguration.bBuildAndUploadAssembly) + { + if (CloudDeploymentConfiguration.bGenerateSchema) + { + if (SpatialGDKEditorInstance->FullScanRequired()) + { + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(TEXT("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)) + { + OnShowSingleFailureNotification(TEXT("Generate schema failed.")); + return FReply::Unhandled(); + } + } + + if (CloudDeploymentConfiguration.bGenerateSnapshot) + { + if (!SpatialGDKGenerateSnapshot(GEditor->GetEditorWorldContext().World(), CloudDeploymentConfiguration.SnapshotPath)) + { + OnShowSingleFailureNotification(TEXT("Generate snapshot failed.")); + return FReply::Unhandled(); + } + } + + TSharedRef PackageAssembly = SpatialGDKEditorInstance->GetPackageAssemblyRef(); + PackageAssembly->OnSuccess.BindRaw(this, &FSpatialGDKEditorToolbarModule::OnBuildSuccess); + PackageAssembly->BuildAndUploadAssembly(CloudDeploymentConfiguration); } else { - return TEXT(""); + UE_LOG(LogSpatialGDKEditorToolbar, Display, TEXT("Skipping building and uploading assembly.")); + OnBuildSuccess(); + } + + return FReply::Handled(); +} + +void FSpatialGDKEditorToolbarModule::OnBuildSuccess() +{ + bStartingCloudDeployment = true; + + auto StartCloudDeployment = [this]() + { + OnShowTaskStartNotification(FString::Printf(TEXT("Starting cloud deployment: %s"), *CloudDeploymentConfiguration.PrimaryDeploymentName)); + SpatialGDKEditorInstance->StartCloudDeployment( + CloudDeploymentConfiguration, + FSimpleDelegate::CreateLambda([this]() + { + OnStartCloudDeploymentFinished(); + OnShowSuccessNotification("Successfully started cloud deployment."); + }), + 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.")); + } + }); +} + +void FSpatialGDKEditorToolbarModule::OnStartCloudDeploymentFinished() +{ + AsyncTask(ENamedThreads::GameThread, [this] + { + bStartingCloudDeployment = false; + }); +} + +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()); +} + +bool FSpatialGDKEditorToolbarModule::CanBuildAndUpload() const +{ + return SpatialGDKEditorInstance->GetPackageAssemblyRef()->CanBuild(); +} + +bool FSpatialGDKEditorToolbarModule::CanStartCloudDeployment() const +{ + return IsDeploymentConfigurationValid() && CanBuildAndUpload() && !bStartingCloudDeployment; +} + +bool FSpatialGDKEditorToolbarModule::IsSimulatedPlayersEnabled() const +{ + return GetDefault()->IsSimulatedPlayersEnabled(); +} + +void FSpatialGDKEditorToolbarModule::OnCheckedSimulatedPlayers() +{ + GetMutableDefault()->SetSimulatedPlayersEnabledState(!IsSimulatedPlayersEnabled()); +} + +bool FSpatialGDKEditorToolbarModule::IsBuildClientWorkerEnabled() const +{ + return GetDefault()->IsBuildClientWorkerEnabled(); +} + +void FSpatialGDKEditorToolbarModule::OnCheckedBuildClientWorker() +{ + GetMutableDefault()->SetBuildClientWorker(!IsBuildClientWorkerEnabled()); +} + +void FSpatialGDKEditorToolbarModule::AddDeploymentTagIfMissing(const FString& TagToAdd) +{ + if (TagToAdd.IsEmpty()) + { + return; + } + + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + + FString Tags = SpatialGDKSettings->GetDeploymentTags(); + TArray ExistingTags; + Tags.ParseIntoArray(ExistingTags, TEXT(" ")); + + if (!ExistingTags.Contains(TagToAdd)) + { + if (ExistingTags.Num() > 0) + { + Tags += TEXT(" "); + } + + Tags += TagToAdd; + SpatialGDKSettings->SetDeploymentTags(Tags); } } diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp index cb1a3b0605..6c03b4f9e0 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp @@ -8,15 +8,24 @@ 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 scheme database file", 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(StartSpatialDeployment, "Start", "Starts a local instance of SpatialOS.", EUserInterfaceActionType::Button, FInputGesture()); - UI_COMMAND(StopSpatialDeployment, "Stop", "Stops SpatialOS.", 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(StopSpatialDeployment, "Stop Deployment", "Stops SpatialOS.", EUserInterfaceActionType::Button, FInputGesture()); UI_COMMAND(LaunchInspectorWebPageAction, "Inspector", "Launches default web browser to SpatialOS Inspector.", EUserInterfaceActionType::Button, FInputGesture()); - UI_COMMAND(OpenSimulatedPlayerConfigurationWindowAction, "Deploy", "Opens a configuration menu for cloud deployments.", EUserInterfaceActionType::Button, FInputGesture()); + UI_COMMAND(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()); } #undef LOCTEXT_NAMESPACE diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarStyle.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarStyle.cpp index 253823939b..dce343adb6 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarStyle.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarStyle.cpp @@ -61,17 +61,29 @@ TSharedRef FSpatialGDKEditorToolbarStyle::Create() Style->Set("SpatialGDKEditorToolbar.CreateSpatialGDKSchema.Small", new IMAGE_BRUSH(TEXT("Schema@0.5x"), Icon20x20)); - Style->Set("SpatialGDKEditorToolbar.StartSpatialDeployment", - new IMAGE_BRUSH(TEXT("Launch"), Icon40x40)); + Style->Set("SpatialGDKEditorToolbar.StartNative", + new IMAGE_BRUSH(TEXT("None"), Icon40x40)); - Style->Set("SpatialGDKEditorToolbar.StartSpatialDeployment.Small", - new IMAGE_BRUSH(TEXT("Launch@0.5x"), Icon20x20)); + Style->Set("SpatialGDKEditorToolbar.StartNative.Small", + new IMAGE_BRUSH(TEXT("None@0.5x"), Icon20x20)); + + Style->Set("SpatialGDKEditorToolbar.StartLocalSpatialDeployment", + new IMAGE_BRUSH(TEXT("StartLocal"), Icon40x40)); + + Style->Set("SpatialGDKEditorToolbar.StartLocalSpatialDeployment.Small", + new IMAGE_BRUSH(TEXT("StartLocal@0.5x"), Icon20x20)); + + Style->Set("SpatialGDKEditorToolbar.StartCloudSpatialDeployment", + new IMAGE_BRUSH(TEXT("StartCloud"), Icon40x40)); + + Style->Set("SpatialGDKEditorToolbar.StartCloudSpatialDeployment.Small", + new IMAGE_BRUSH(TEXT("StartCloud@0.5x"), Icon20x20)); Style->Set("SpatialGDKEditorToolbar.StopSpatialDeployment", - new IMAGE_BRUSH(TEXT("Stop"), Icon40x40)); + new IMAGE_BRUSH(TEXT("StopLocal"), Icon40x40)); Style->Set("SpatialGDKEditorToolbar.StopSpatialDeployment.Small", - new IMAGE_BRUSH(TEXT("Stop@0.5x"), Icon20x20)); + new IMAGE_BRUSH(TEXT("StopLocal@0.5x"), Icon20x20)); Style->Set("SpatialGDKEditorToolbar.LaunchInspectorWebPageAction", new IMAGE_BRUSH(TEXT("Inspector"), Icon40x40)); @@ -79,23 +91,23 @@ TSharedRef FSpatialGDKEditorToolbarStyle::Create() Style->Set("SpatialGDKEditorToolbar.LaunchInspectorWebPageAction.Small", new IMAGE_BRUSH(TEXT("Inspector@0.5x"), Icon20x20)); - Style->Set("SpatialGDKEditorToolbar.OpenSimulatedPlayerConfigurationWindowAction", + Style->Set("SpatialGDKEditorToolbar.OpenCloudDeploymentWindowAction", new IMAGE_BRUSH(TEXT("Cloud"), Icon40x40)); - Style->Set("SpatialGDKEditorToolbar.OpenSimulatedPlayerConfigurationWindowAction.Small", + Style->Set("SpatialGDKEditorToolbar.OpenCloudDeploymentWindowAction.Small", new IMAGE_BRUSH(TEXT("Cloud@0.5x"), Icon20x20)); Style->Set("SpatialGDKEditorToolbar.StartSpatialService", - new IMAGE_BRUSH(TEXT("Launch"), Icon40x40)); + new IMAGE_BRUSH(TEXT("StartLocal"), Icon40x40)); Style->Set("SpatialGDKEditorToolbar.StartSpatialService.Small", - new IMAGE_BRUSH(TEXT("Launch@0.5x"), Icon20x20)); + new IMAGE_BRUSH(TEXT("StartLocal@0.5x"), Icon20x20)); Style->Set("SpatialGDKEditorToolbar.StopSpatialService", - new IMAGE_BRUSH(TEXT("Stop"), Icon40x40)); + new IMAGE_BRUSH(TEXT("StopLocal"), Icon40x40)); Style->Set("SpatialGDKEditorToolbar.StopSpatialService.Small", - new IMAGE_BRUSH(TEXT("Stop@0.5x"), Icon20x20)); + new IMAGE_BRUSH(TEXT("StopLocal@0.5x"), Icon20x20)); Style->Set("SpatialGDKEditorToolbar.SpatialOSLogo", new IMAGE_BRUSH(TEXT("SPATIALOS_LOGO_WHITE"), Icon100x22)); diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKSimulatedPlayerDeployment.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKSimulatedPlayerDeployment.cpp deleted file mode 100644 index 41b67946a9..0000000000 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKSimulatedPlayerDeployment.cpp +++ /dev/null @@ -1,725 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved - -#include "SpatialGDKSimulatedPlayerDeployment.h" - -#include "Async/Async.h" -#include "DesktopPlatformModule.h" -#include "EditorDirectories.h" -#include "EditorStyleSet.h" -#include "Framework/Application/SlateApplication.h" -#include "Framework/MultiBox/MultiBoxBuilder.h" -#include "Framework/Notifications/NotificationManager.h" -#include "HAL/PlatformFilemanager.h" -#include "Misc/MessageDialog.h" -#include "Runtime/Launch/Resources/Version.h" -#include "SpatialCommandUtils.h" -#include "SpatialGDKSettings.h" -#include "SpatialGDKEditorSettings.h" -#include "SpatialGDKEditorToolbar.h" -#include "SpatialGDKServicesConstants.h" -#include "SpatialGDKServicesModule.h" -#include "Templates/SharedPointer.h" -#include "Textures/SlateIcon.h" -#include "Widgets/Input/SButton.h" -#include "Widgets/Input/SComboButton.h" -#include "Widgets/Input/SFilePathPicker.h" -#include "Widgets/Input/SHyperlink.h" -#include "Widgets/Input/SSpinBox.h" -#include "Widgets/Layout/SBox.h" -#include "Widgets/Layout/SExpandableArea.h" -#include "Widgets/Layout/SSeparator.h" -#include "Widgets/Layout/SUniformGridPanel.h" -#include "Widgets/Layout/SWrapBox.h" -#include "Widgets/Notifications/SNotificationList.h" -#include "Widgets/Text/STextBlock.h" - -#include "Internationalization/Regex.h" - -DEFINE_LOG_CATEGORY(LogSpatialGDKSimulatedPlayerDeployment); - -void SSpatialGDKSimulatedPlayerDeployment::Construct(const FArguments& InArgs) -{ - const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); - FString ProjectName = FSpatialGDKServicesModule::GetProjectName(); - - ParentWindowPtr = InArgs._ParentWindow; - SpatialGDKEditorPtr = InArgs._SpatialGDKEditor; - - 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) - // Build explanation set - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - .VAlign(VAlign_Center) - [ - SNew(SWrapBox) - .UseAllottedWidth(true) - + SWrapBox::Slot() - .VAlign(VAlign_Bottom) - [ - SNew(STextBlock) - .AutoWrapText(true) - .Text(FText::FromString(FString(TEXT("NOTE: You can set default values in the SpatialOS settings under \"Cloud\".")))) - ] - + SWrapBox::Slot() - .VAlign(VAlign_Bottom) - [ - SNew(STextBlock) - .AutoWrapText(true) - .Text(FText::FromString(FString(TEXT("The assembly has to be built and uploaded manually. Follow the docs ")))) - ] - + SWrapBox::Slot() - [ - SNew(SHyperlink) - .Text(FText::FromString(FString(TEXT("here.")))) - .OnNavigate(this, &SSpatialGDKSimulatedPlayerDeployment::OnCloudDocumentationClicked) - ] - ] - // Separator - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - .VAlign(VAlign_Center) - [ - SNew(SSeparator) - ] - // Project - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(FText::FromString(FString(TEXT("Project Name")))) - .ToolTipText(FText::FromString(FString(TEXT("The name of the SpatialOS project.")))) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SEditableTextBox) - .Text(FText::FromString(ProjectName)) - .ToolTipText(FText::FromString(FString(TEXT("The name of the SpatialOS project.")))) - .IsEnabled(false) - ] - ] - // Assembly Name - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(FText::FromString(FString(TEXT("Assembly Name")))) - .ToolTipText(FText::FromString(FString(TEXT("The name of the assembly.")))) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SEditableTextBox) - .Text(FText::FromString(SpatialGDKSettings->GetAssemblyName())) - .ToolTipText(FText::FromString(FString(TEXT("The name of the assembly.")))) - .OnTextCommitted(this, &SSpatialGDKSimulatedPlayerDeployment::OnDeploymentAssemblyCommited) - .OnTextChanged(this, &SSpatialGDKSimulatedPlayerDeployment::OnDeploymentAssemblyCommited, ETextCommit::Default) - ] - ] - // RuntimeVersion - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(FText::FromString(FString(TEXT("Use GDK Pinned Version")))) - .ToolTipText(FText::FromString(FString(TEXT("Whether to use the SpatialOS Runtime version associated to the current GDK version")))) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SCheckBox) - .IsChecked(this, &SSpatialGDKSimulatedPlayerDeployment::IsUsingGDKPinnedRuntimeVersion) - .OnCheckStateChanged(this, &SSpatialGDKSimulatedPlayerDeployment::OnCheckedUsePinnedVersion) - ] - ] - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(FText::FromString(FString(TEXT("Runtime Version")))) - .ToolTipText(FText::FromString(FString(TEXT("User supplied version of the SpatialOS runtime to use")))) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SEditableTextBox) - .Text(this, &SSpatialGDKSimulatedPlayerDeployment::GetSpatialOSRuntimeVersionToUseText) - .OnTextCommitted(this, &SSpatialGDKSimulatedPlayerDeployment::OnRuntimeCustomVersionCommited) - .OnTextChanged(this, &SSpatialGDKSimulatedPlayerDeployment::OnRuntimeCustomVersionCommited, ETextCommit::Default) - .IsEnabled(this, &SSpatialGDKSimulatedPlayerDeployment::IsUsingCustomRuntimeVersion) - ] - ] - // Pirmary Deployment Name - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(FText::FromString(FString(TEXT("Deployment Name")))) - .ToolTipText(FText::FromString(FString(TEXT("The name of the cloud deployment. Must be unique.")))) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SEditableTextBox) - .Text(FText::FromString(SpatialGDKSettings->GetPrimaryDeploymentName())) - .ToolTipText(FText::FromString(FString(TEXT("The name of the cloud deployment. Must be unique.")))) - .OnTextCommitted(this, &SSpatialGDKSimulatedPlayerDeployment::OnPrimaryDeploymentNameCommited) - .OnTextChanged(this, &SSpatialGDKSimulatedPlayerDeployment::OnPrimaryDeploymentNameCommited, ETextCommit::Default) - ] - ] - // Snapshot File + File Picker - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(FText::FromString(FString(TEXT("Snapshot File")))) - .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Path to the snapshot file.")))) - .BrowseDirectory(SpatialGDKSettings->GetSpatialOSSnapshotFolderPath()) - .BrowseTitle(FText::FromString(FString(TEXT("File picker...")))) - .FilePath_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::GetSnapshotPath) - .FileTypeFilter(TEXT("Snapshot files (*.snapshot)|*.snapshot")) - .OnPathPicked(this, &SSpatialGDKSimulatedPlayerDeployment::OnSnapshotPathPicked) - ] - ] - // Primary Launch Config + File Picker - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(FText::FromString(FString(TEXT("Launch Config File")))) - .ToolTipText(FText::FromString(FString(TEXT("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(FText::FromString(FString(TEXT("Path to the launch configuration file.")))) - .BrowseDirectory(SpatialGDKServicesConstants::SpatialOSDirectory) - .BrowseTitle(FText::FromString(FString(TEXT("File picker...")))) - .FilePath_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::GetPrimaryLaunchConfigPath) - .FileTypeFilter(TEXT("Launch configuration files (*.json)|*.json")) - .OnPathPicked(this, &SSpatialGDKSimulatedPlayerDeployment::OnPrimaryLaunchConfigPathPicked) - ] - ] - // Primary Deployment Region Picker - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(FText::FromString(FString(TEXT("Region")))) - .ToolTipText(FText::FromString(FString(TEXT("The region in which the deployment will be deployed.")))) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SComboButton) - .OnGetMenuContent(this, &SSpatialGDKSimulatedPlayerDeployment::OnGetPrimaryDeploymentRegionCode) - .ContentPadding(FMargin(2.0f, 2.0f)) - .ButtonContent() - [ - SNew(STextBlock) - .Text_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::GetPrimaryRegionCode) - ] - ] - ] - // 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(FText::FromString(FString(TEXT("Simulated Players")))) - ] - // Toggle - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - .VAlign(VAlign_Center) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - .VAlign(VAlign_Center) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .AutoWidth() - [ - SNew(SCheckBox) - .IsChecked(this, &SSpatialGDKSimulatedPlayerDeployment::IsSimulatedPlayersEnabled) - .OnCheckStateChanged(this, &SSpatialGDKSimulatedPlayerDeployment::OnCheckedSimulatedPlayers) - ] - + SHorizontalBox::Slot() - .AutoWidth() - .HAlign(HAlign_Center) - [ - SNew(STextBlock) - .Text(FText::FromString(FString(TEXT("Add simulated players")))) - ] - ] - ] - ] - // Simulated Players Deployment Name - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(FText::FromString(FString(TEXT("Deployment Name")))) - .ToolTipText(FText::FromString(FString(TEXT("The name of the simulated player deployment.")))) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SEditableTextBox) - .Text(FText::FromString(SpatialGDKSettings->GetSimulatedPlayerDeploymentName())) - .ToolTipText(FText::FromString(FString(TEXT("The name of the simulated player deployment.")))) - .OnTextCommitted(this, &SSpatialGDKSimulatedPlayerDeployment::OnSimulatedPlayerDeploymentNameCommited) - .OnTextChanged(this, &SSpatialGDKSimulatedPlayerDeployment::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(FText::FromString(FString(TEXT("Number of Simulated Players")))) - .ToolTipText(FText::FromString(FString(TEXT("The number of Simulated Players to be launch and connect to the game.")))) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SSpinBox) - .ToolTipText(FText::FromString(FString(TEXT("Number of Simulated Players.")))) - .MinValue(1) - .MaxValue(8192) - .Value(SpatialGDKSettings->GetNumberOfSimulatedPlayer()) - .OnValueChanged(this, &SSpatialGDKSimulatedPlayerDeployment::OnNumberOfSimulatedPlayersCommited) - .IsEnabled_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::IsSimulatedPlayersEnabled) - ] - ] - // Simulated Players Deployment Region Picker - + SVerticalBox::Slot() - .AutoHeight() - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(STextBlock) - .Text(FText::FromString(FString(TEXT("Region")))) - .ToolTipText(FText::FromString(FString(TEXT("The region in which the simulated player deployment will be deployed.")))) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNew(SComboButton) - .OnGetMenuContent(this, &SSpatialGDKSimulatedPlayerDeployment::OnGetSimulatedPlayerDeploymentRegionCode) - .ContentPadding(FMargin(2.0f, 2.0f)) - .IsEnabled_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::IsSimulatedPlayersEnabled) - .ButtonContent() - [ - SNew(STextBlock) - .Text_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::GetSimulatedPlayerRegionCode) - ] - ] - ] - // Buttons - + SVerticalBox::Slot() - .FillHeight(1.0f) - .Padding(2.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .FillWidth(1.0f) - .HAlign(HAlign_Right) - [ - // Launch Simulated Players Deployment Button - SNew(SUniformGridPanel) - .SlotPadding(FMargin(2.0f, 20.0f, 0.0f, 0.0f)) - + SUniformGridPanel::Slot(0, 0) - [ - SNew(SButton) - .HAlign(HAlign_Center) - .Text(FText::FromString(FString(TEXT("Launch Deployment")))) - .OnClicked(this, &SSpatialGDKSimulatedPlayerDeployment::OnLaunchClicked) - .IsEnabled(this, &SSpatialGDKSimulatedPlayerDeployment::IsDeploymentConfigurationValid) - ] - ] - ] - ] - ] - ] - ] - ]; -} - -void SSpatialGDKSimulatedPlayerDeployment::OnDeploymentAssemblyCommited(const FText& InText, ETextCommit::Type InCommitType) -{ - USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); - SpatialGDKSettings->SetAssemblyName(InText.ToString()); -} - -void SSpatialGDKSimulatedPlayerDeployment::OnPrimaryDeploymentNameCommited(const FText& InText, ETextCommit::Type InCommitType) -{ - USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); - SpatialGDKSettings->SetPrimaryDeploymentName(InText.ToString()); -} - -void SSpatialGDKSimulatedPlayerDeployment::OnCheckedUsePinnedVersion(ECheckBoxState NewCheckedState) -{ - USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); - SpatialGDKSettings->SetUseGDKPinnedRuntimeVersion(NewCheckedState == ECheckBoxState::Checked); -} - -void SSpatialGDKSimulatedPlayerDeployment::OnRuntimeCustomVersionCommited(const FText& InText, ETextCommit::Type InCommitType) -{ - USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); - SpatialGDKSettings->SetCustomCloudSpatialOSRuntimeVersion(InText.ToString()); -} - -void SSpatialGDKSimulatedPlayerDeployment::OnSnapshotPathPicked(const FString& PickedPath) -{ - USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); - SpatialGDKSettings->SetSnapshotPath(PickedPath); -} - -void SSpatialGDKSimulatedPlayerDeployment::OnPrimaryLaunchConfigPathPicked(const FString& PickedPath) -{ - USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); - SpatialGDKSettings->SetPrimaryLaunchConfigPath(PickedPath); -} - -TSharedRef SSpatialGDKSimulatedPlayerDeployment::OnGetPrimaryDeploymentRegionCode() -{ - FMenuBuilder MenuBuilder(true, NULL); - UEnum* pEnum = FindObject(ANY_PACKAGE, TEXT("ERegionCode"), true); - - if (pEnum != nullptr) - { - for (int32 i = 0; i < pEnum->NumEnums() - 1; i++) - { - int64 CurrentEnumValue = pEnum->GetValueByIndex(i); - FUIAction ItemAction(FExecuteAction::CreateSP(this, &SSpatialGDKSimulatedPlayerDeployment::OnPrimaryDeploymentRegionCodePicked, CurrentEnumValue)); - MenuBuilder.AddMenuEntry(pEnum->GetDisplayNameTextByValue(CurrentEnumValue), TAttribute(), FSlateIcon(), ItemAction); - } - } - - return MenuBuilder.MakeWidget(); -} - -TSharedRef SSpatialGDKSimulatedPlayerDeployment::OnGetSimulatedPlayerDeploymentRegionCode() -{ - FMenuBuilder MenuBuilder(true, NULL); - UEnum* pEnum = FindObject(ANY_PACKAGE, TEXT("ERegionCode"), true); - - if (pEnum != nullptr) - { - for (int32 i = 0; i < pEnum->NumEnums() - 1; i++) - { - int64 CurrentEnumValue = pEnum->GetValueByIndex(i); - FUIAction ItemAction(FExecuteAction::CreateSP(this, &SSpatialGDKSimulatedPlayerDeployment::OnSimulatedPlayerDeploymentRegionCodePicked, CurrentEnumValue)); - MenuBuilder.AddMenuEntry(pEnum->GetDisplayNameTextByValue(CurrentEnumValue), TAttribute(), FSlateIcon(), ItemAction); - } - } - - return MenuBuilder.MakeWidget(); -} - -void SSpatialGDKSimulatedPlayerDeployment::OnPrimaryDeploymentRegionCodePicked(const int64 RegionCodeEnumValue) -{ - USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); - SpatialGDKSettings->SetPrimaryRegionCode((ERegionCode::Type) RegionCodeEnumValue); - -} - -void SSpatialGDKSimulatedPlayerDeployment::OnSimulatedPlayerDeploymentRegionCodePicked(const int64 RegionCodeEnumValue) -{ - USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); - SpatialGDKSettings->SetSimulatedPlayerRegionCode((ERegionCode::Type) RegionCodeEnumValue); -} - -void SSpatialGDKSimulatedPlayerDeployment::OnSimulatedPlayerDeploymentNameCommited(const FText& InText, ETextCommit::Type InCommitType) -{ - USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); - SpatialGDKSettings->SetSimulatedPlayerDeploymentName(InText.ToString()); -} - -void SSpatialGDKSimulatedPlayerDeployment::OnNumberOfSimulatedPlayersCommited(uint32 NewValue) -{ - USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); - SpatialGDKSettings->SetNumberOfSimulatedPlayers(NewValue); -} - -FReply SSpatialGDKSimulatedPlayerDeployment::OnLaunchClicked() -{ - const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); - - FSpatialGDKEditorToolbarModule* ToolbarPtr = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar"); - - if (!SpatialGDKSettings->IsDeploymentConfigurationValid()) - { - if (ToolbarPtr) - { - ToolbarPtr->OnShowFailedNotification(TEXT("Deployment configuration is not valid.")); - } - - return FReply::Handled(); - } - - if (SpatialGDKSettings->IsSimulatedPlayersEnabled()) - { - IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); - FString BuiltWorkerFolder = GetDefault()->GetBuiltWorkerFolder(); - FString BuiltSimPlayersName = TEXT("UnrealSimulatedPlayer@Linux.zip"); - FString BuiltSimPlayerPath = FPaths::Combine(BuiltWorkerFolder, BuiltSimPlayersName); - - if (!PlatformFile.FileExists(*BuiltSimPlayerPath)) - { - FString MissingSimPlayerBuildText = FString::Printf(TEXT("Warning: Detected that %s is missing. To launch a successful SimPlayer deployment ensure that SimPlayers is built and uploaded."), *BuiltSimPlayersName); - FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(MissingSimPlayerBuildText)); - } - } - - if (ToolbarPtr) - { - ToolbarPtr->OnShowTaskStartNotification(TEXT("Starting cloud deployment...")); - } - - auto LaunchCloudDeployment = [this, ToolbarPtr]() - { - if (TSharedPtr SpatialGDKEditorSharedPtr = SpatialGDKEditorPtr.Pin()) - { - SpatialGDKEditorSharedPtr->LaunchCloudDeployment( - FSimpleDelegate::CreateLambda([]() - { - if (FSpatialGDKEditorToolbarModule* ToolbarPtr = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) - { - ToolbarPtr->OnShowSuccessNotification("Successfully launched cloud deployment."); - } - }), - - FSimpleDelegate::CreateLambda([]() - { - if (FSpatialGDKEditorToolbarModule* ToolbarPtr = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) - { - ToolbarPtr->OnShowFailedNotification("Failed to launch cloud deployment. See output logs for details."); - } - }) - ); - - return; - } - - FNotificationInfo Info(FText::FromString(TEXT("Couldn't launch the deployment."))); - Info.bUseSuccessFailIcons = true; - Info.ExpireDuration = 3.0f; - - TSharedPtr NotificationItem = FSlateNotificationManager::Get().AddNotification(Info); - NotificationItem->SetCompletionState(SNotificationItem::CS_Fail); - }; - -#if ENGINE_MINOR_VERSION <= 22 - AttemptSpatialAuthResult = Async(EAsyncExecution::Thread, []() { return SpatialCommandUtils::AttemptSpatialAuth(GetDefault()->IsRunningInChina()); }, -#else - AttemptSpatialAuthResult = Async(EAsyncExecution::Thread, []() { return SpatialCommandUtils::AttemptSpatialAuth(GetDefault()->IsRunningInChina()); }, -#endif - [this, LaunchCloudDeployment, ToolbarPtr]() - { - if (AttemptSpatialAuthResult.IsReady() && AttemptSpatialAuthResult.Get() == true) - { - LaunchCloudDeployment(); - } - else - { - ToolbarPtr->OnShowTaskStartNotification(TEXT("Spatial auth failed attempting to launch cloud deployment.")); - } - }); - - return FReply::Handled(); -} - -FReply SSpatialGDKSimulatedPlayerDeployment::OnRefreshClicked() -{ - // TODO (UNR-1193): Invoke the Deployment Launcher script to list the deployments - return FReply::Handled(); -} - -FReply SSpatialGDKSimulatedPlayerDeployment::OnStopClicked() -{ - 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")) - { - ToolbarPtr->OnShowSuccessNotification("Successfully stopped cloud deployment."); - } - }), - - FSimpleDelegate::CreateLambda([]() - { - if (FSpatialGDKEditorToolbarModule* ToolbarPtr = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) - { - ToolbarPtr->OnShowFailedNotification("Failed to stop cloud deployment."); - } - })); - } - return FReply::Handled(); -} - -void SSpatialGDKSimulatedPlayerDeployment::OnCloudDocumentationClicked() -{ - FString 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)); - Info.ExpireDuration = 3.0f; - Info.bUseSuccessFailIcons = true; - TSharedPtr NotificationItem = FSlateNotificationManager::Get().AddNotification(Info); - NotificationItem->SetCompletionState(SNotificationItem::CS_Fail); - NotificationItem->ExpireAndFadeout(); - } -} - -void SSpatialGDKSimulatedPlayerDeployment::OnCheckedSimulatedPlayers(ECheckBoxState NewCheckedState) -{ - USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); - SpatialGDKSettings->SetSimulatedPlayersEnabledState(NewCheckedState == ECheckBoxState::Checked); -} - -ECheckBoxState SSpatialGDKSimulatedPlayerDeployment::IsSimulatedPlayersEnabled() const -{ - const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); - return SpatialGDKSettings->IsSimulatedPlayersEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; -} - -bool SSpatialGDKSimulatedPlayerDeployment::IsDeploymentConfigurationValid() const -{ - return true; -} - -ECheckBoxState SSpatialGDKSimulatedPlayerDeployment::IsUsingGDKPinnedRuntimeVersion() const -{ - const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); - return SpatialGDKSettings->GetUseGDKPinnedRuntimeVersion() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; -} - -bool SSpatialGDKSimulatedPlayerDeployment::IsUsingCustomRuntimeVersion() const -{ - const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); - return !SpatialGDKSettings->GetUseGDKPinnedRuntimeVersion(); -} - -FText SSpatialGDKSimulatedPlayerDeployment::GetSpatialOSRuntimeVersionToUseText() const -{ - const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); - const FString& RuntimeVersion = SpatialGDKSettings->bUseGDKPinnedRuntimeVersion ? SpatialGDKServicesConstants::SpatialOSRuntimePinnedVersion : SpatialGDKSettings->CloudRuntimeVersion; - return FText::FromString(RuntimeVersion); -} diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKSimulatedPlayerDeployment.h b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKCloudDeploymentConfiguration.h similarity index 58% rename from SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKSimulatedPlayerDeployment.h rename to SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKCloudDeploymentConfiguration.h index 4ee349272b..ea2c3c5f6b 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKSimulatedPlayerDeployment.h +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKCloudDeploymentConfiguration.h @@ -13,17 +13,17 @@ #include "Widgets/Layout/SBorder.h" #include "Widgets/SCompoundWidget.h" -DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKSimulatedPlayerDeployment, Log, All); +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKCloudDeploymentConfiguration, Log, All); class SWindow; enum class ECheckBoxState : uint8; -class SSpatialGDKSimulatedPlayerDeployment : public SCompoundWidget +class SSpatialGDKCloudDeploymentConfiguration : public SCompoundWidget { public: - SLATE_BEGIN_ARGS(SSpatialGDKSimulatedPlayerDeployment) {} + SLATE_BEGIN_ARGS(SSpatialGDKCloudDeploymentConfiguration) {} /** A reference to the parent window */ SLATE_ARGUMENT(TSharedPtr, ParentWindow) @@ -43,7 +43,13 @@ class SSpatialGDKSimulatedPlayerDeployment : public SCompoundWidget /** Pointer to the SpatialGDK editor */ TWeakPtr SpatialGDKEditorPtr; - TFuture AttemptSpatialAuthResult; + // Error reporting + TSharedPtr ProjectNameInputErrorReporting; + TSharedPtr AssemblyNameInputErrorReporting; + TSharedPtr DeploymentNameInputErrorReporting; + + /** Delegate to commit project name */ + void OnProjectNameCommitted(const FText& InText, ETextCommit::Type InCommitType); /** Delegate to commit assembly name */ void OnDeploymentAssemblyCommited(const FText& InText, ETextCommit::Type InCommitType); @@ -63,6 +69,9 @@ class SSpatialGDKSimulatedPlayerDeployment : public SCompoundWidget /** Delegate called when the user has picked a path for the primary launch configuration file */ void OnPrimaryLaunchConfigPathPicked(const FString& PickedPath); + /** Delegate to commit deployment tags */ + void OnDeploymentTagsCommitted(const FText& InText, ETextCommit::Type InCommitType); + /** Delegate called to populate the region codes for the primary deployment */ TSharedRef OnGetPrimaryDeploymentRegionCode(); @@ -72,6 +81,21 @@ class SSpatialGDKSimulatedPlayerDeployment : public SCompoundWidget /** Delegate called when the user selects a region code from the dropdown for the primary deployment */ void OnPrimaryDeploymentRegionCodePicked(const int64 RegionCodeEnumValue); + /** Delegate to determine whether the region picker is visible. */ + EVisibility GetRegionPickerVisibility() const; + + /** Delegate to determine whether the primary region picker is enabled. */ + bool IsPrimaryRegionPickerEnabled() const; + + /** Delegate to determine whether the simulated player region picker is enabled. */ + bool IsSimulatedPlayerRegionPickerEnabled() const; + + /** Delegate to commit main deployment cluster */ + void OnDeploymentClusterCommited(const FText& InText, ETextCommit::Type InCommitType); + + /** Delegate to commit simulated player cluster */ + void OnSimulatedPlayerClusterCommited(const FText& InText, ETextCommit::Type InCommitType); + /** Delegate called when the user selects a region code from the dropdown for the simulated player deployment */ void OnSimulatedPlayerDeploymentRegionCodePicked(const int64 RegionCodeEnumValue); @@ -81,9 +105,6 @@ class SSpatialGDKSimulatedPlayerDeployment : public SCompoundWidget /** Delegate to commit the number of Simulated Players */ void OnNumberOfSimulatedPlayersCommited(uint32 NewValue); - /** Delegate called when the user clicks the 'Launch Simulated Player Deployment' button */ - FReply OnLaunchClicked(); - /** Delegate called when the user clicks the 'Refresh' button */ FReply OnRefreshClicked(); @@ -96,11 +117,35 @@ class SSpatialGDKSimulatedPlayerDeployment : public SCompoundWidget /** Delegate called when the user either clicks the simulated players checkbox */ void OnCheckedSimulatedPlayers(ECheckBoxState NewCheckedState); + ECheckBoxState IsBuildAndUploadAssemblyEnabled() const; + void OnCheckedBuildAndUploadAssembly(ECheckBoxState NewCheckedState); + + TSharedRef OnGetBuildConfiguration(); + void OnBuildConfigurationPicked(FString Configuration); + + ECheckBoxState ForceAssemblyOverwrite() const; + void OnCheckedForceAssemblyOverwrite(ECheckBoxState NewCheckedState); + ECheckBoxState IsSimulatedPlayersEnabled() const; ECheckBoxState IsUsingGDKPinnedRuntimeVersion() const; bool IsUsingCustomRuntimeVersion() const; FText GetSpatialOSRuntimeVersionToUseText() const; - /** Delegate to determine the 'Launch Deployment' button enabled state */ - bool IsDeploymentConfigurationValid() const; + ECheckBoxState IsAutoGenerateCloudLaunchConfigEnabled() const; + bool CanPickOrEditCloudLaunchConfig() const; + void OnCheckedAutoGenerateCloudLaunchConfig(ECheckBoxState NewCheckedState); + + FReply OnOpenLaunchConfigEditor(); + + ECheckBoxState IsBuildClientWorkerEnabled() const; + void OnCheckedBuildClientWorker(ECheckBoxState NewCheckedState); + + ECheckBoxState IsGenerateSchemaEnabled() const; + void OnCheckedGenerateSchema(ECheckBoxState NewCheckedState); + + ECheckBoxState IsGenerateSnapshotEnabled() const; + void OnCheckedGenerateSnapshot(ECheckBoxState NewCheckedState); + + FReply OnOpenCloudDeploymentPageClicked(); + bool CanOpenCloudDeploymentPage() const; }; diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h index 44e7461fae..3a153ffbd6 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h @@ -4,7 +4,6 @@ #include "Async/Future.h" #include "CoreMinimal.h" -#include "LocalDeploymentManager.h" #include "Modules/ModuleManager.h" #include "Serialization/JsonWriter.h" #include "Templates/SharedPointer.h" @@ -12,15 +11,19 @@ #include "UObject/UnrealType.h" #include "Widgets/Notifications/SNotificationList.h" +#include "CloudDeploymentConfiguration.h" +#include "LocalDeploymentManager.h" + class FMenuBuilder; class FSpatialGDKEditor; class FToolBarBuilder; class FUICommandList; -class SSpatialGDKSimulatedPlayerDeployment; +class SSpatialGDKCloudDeploymentConfiguration; class SWindow; class USoundBase; struct FWorkerTypeLaunchSection; +class UAbstractRuntimeLoadBalancingStrategy; DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKEditorToolbar, Log, All); @@ -45,10 +48,21 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable RETURN_QUICK_DECLARE_CYCLE_STAT(FSpatialGDKEditorToolbarModule, STATGROUP_Tickables); } + void OnShowSingleFailureNotification(const FString& NotificationText); void OnShowSuccessNotification(const FString& NotificationText); void OnShowFailedNotification(const FString& NotificationText); void OnShowTaskStartNotification(const FString& NotificationText); + FReply OnStartCloudDeployment(); + bool CanStartCloudDeployment() const; + + bool IsSimulatedPlayersEnabled() const; + /** Delegate called when the user either clicks the simulated players checkbox */ + void OnCheckedSimulatedPlayers(); + + bool IsBuildClientWorkerEnabled() const; + void OnCheckedBuildClientWorker(); + private: void MapActions(TSharedPtr PluginCommands); void SetupToolbar(TSharedPtr PluginCommands); @@ -57,14 +71,20 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable void VerifyAndStartDeployment(); - void StartSpatialDeploymentButtonClicked(); + void StartLocalSpatialDeploymentButtonClicked(); void StopSpatialDeploymentButtonClicked(); void StartSpatialServiceButtonClicked(); void StopSpatialServiceButtonClicked(); - bool StartSpatialDeploymentIsVisible() const; - bool StartSpatialDeploymentCanExecute() const; + bool StartNativeIsVisible() const; + bool StartNativeCanExecute() const; + + bool StartLocalSpatialDeploymentIsVisible() const; + bool StartLocalSpatialDeploymentCanExecute() const; + + bool StartCloudSpatialDeploymentIsVisible() const; + bool StartCloudSpatialDeploymentCanExecute() const; bool StopSpatialDeploymentIsVisible() const; bool StopSpatialDeploymentCanExecute() const; @@ -75,6 +95,23 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable bool StopSpatialServiceIsVisible() const; bool StopSpatialServiceCanExecute() const; + void OnToggleSpatialNetworking(); + bool OnIsSpatialNetworkingEnabled() const; + + void GDKEditorSettingsClicked() const; + void GDKRuntimeSettingsClicked() const; + + bool IsLocalDeploymentSelected() const; + bool IsCloudDeploymentSelected() const; + + bool IsSpatialOSNetFlowConfigurable() const; + + void LocalDeploymentClicked(); + void CloudDeploymentClicked(); + + bool IsLocalDeploymentIPEditable() const; + bool AreCloudDeploymentPropertiesEditable() const; + void LaunchInspectorWebpageButtonClicked(); void CreateSnapshotButtonClicked(); void SchemaGenerateButtonClicked(); @@ -82,8 +119,18 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable void DeleteSchemaDatabaseButtonClicked(); void OnPropertyChanged(UObject* ObjectBeingModified, FPropertyChangedEvent& PropertyChangedEvent); - void ShowSimulatedPlayerDeploymentDialog(); + void ShowCloudDeploymentDialog(); void OpenLaunchConfigurationEditor(); + void LaunchOrShowCloudDeployment(); + + /** Delegate to determine the 'Start Deployment' button enabled state */ + bool IsDeploymentConfigurationValid() const; + bool CanBuildAndUpload() const; + + void OnBuildSuccess(); + void OnStartCloudDeploymentFinished(); + + void AddDeploymentTagIfMissing(const FString& TagToAdd); private: bool CanExecuteSchemaGenerator() const; @@ -91,23 +138,23 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable TSharedRef CreateGenerateSchemaMenuContent(); TSharedRef CreateLaunchDeploymentMenuContent(); + TSharedRef CreateStartDropDownMenuContent(); + void ShowSingleFailureNotification(const FString& NotificationText); void ShowTaskStartNotification(const FString& NotificationText); void ShowSuccessNotification(const FString& NotificationText); void ShowFailedNotification(const FString& NotificationText); - bool FillWorkerLaunchConfigFromWorldSettings(UWorld& World, FWorkerTypeLaunchSection& OutLaunchConfig, FIntPoint& OutWorldDimension); - void GenerateSchema(bool bFullScan); bool IsSnapshotGenerated() const; - bool IsSchemaGenerated() const; FString GetOptionalExposedRuntimeIP() const; - static void ShowCompileLog(); + // This should be called whenever the settings determining whether a local deployment should be automatically started have changed. + void OnAutoStartLocalDeploymentChanged(); TSharedPtr PluginCommands; FDelegateHandle OnPropertyChangedDelegateHandle; @@ -125,8 +172,16 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable TFuture SchemaGeneratorResult; TSharedPtr SpatialGDKEditorInstance; - TSharedPtr SimulatedPlayerDeploymentWindowPtr; - TSharedPtr SimulatedPlayerDeploymentConfigPtr; + TSharedPtr CloudDeploymentSettingsWindowPtr; + TSharedPtr CloudDeploymentConfigPtr; FLocalDeploymentManager* LocalDeploymentManager; + + TFuture AttemptSpatialAuthResult; + + FCloudDeploymentConfiguration CloudDeploymentConfiguration; + + bool bStartingCloudDeployment; + + void GenerateConfigFromCurrentMap(); }; diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h index 7291a55343..85559b4d95 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h @@ -24,13 +24,22 @@ class FSpatialGDKEditorToolbarCommands : public TCommands CreateSpatialGDKSchemaFull; TSharedPtr DeleteSchemaDatabase; TSharedPtr CreateSpatialGDKSnapshot; - TSharedPtr StartSpatialDeployment; + TSharedPtr StartNative; + TSharedPtr StartLocalSpatialDeployment; + TSharedPtr StartCloudSpatialDeployment; TSharedPtr StopSpatialDeployment; TSharedPtr LaunchInspectorWebPageAction; - TSharedPtr OpenSimulatedPlayerConfigurationWindowAction; + TSharedPtr OpenCloudDeploymentWindowAction; TSharedPtr OpenLaunchConfigurationEditorAction; + TSharedPtr EnableBuildClientWorker; + TSharedPtr EnableBuildSimulatedPlayer; TSharedPtr StartSpatialService; TSharedPtr StopSpatialService; + TSharedPtr EnableSpatialNetworking; + TSharedPtr GDKEditorSettings; + TSharedPtr GDKRuntimeSettings; + TSharedPtr LocalDeployment; + TSharedPtr CloudDeployment; }; diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/SpatialGDKEditorToolbar.Build.cs b/SpatialGDK/Source/SpatialGDKEditorToolbar/SpatialGDKEditorToolbar.Build.cs index cdc2d90d08..0b2f770e4d 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/SpatialGDKEditorToolbar.Build.cs +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/SpatialGDKEditorToolbar.Build.cs @@ -6,7 +6,7 @@ public class SpatialGDKEditorToolbar : ModuleRules { public SpatialGDKEditorToolbar(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 @@ -38,7 +38,8 @@ public SpatialGDKEditorToolbar(ReadOnlyTargetRules Target) : base(Target) "SpatialGDK", "SpatialGDKEditor", "SpatialGDKServices", - "UnrealEd" + "UnrealEd", + "UATHelper" } ); } diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp index b0243c32ff..9af772ed6f 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp +++ b/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp @@ -25,7 +25,7 @@ DEFINE_LOG_CATEGORY(LogSpatialDeploymentManager); #define LOCTEXT_NAMESPACE "FLocalDeploymentManager" -static const FString SpatialServiceVersion(TEXT("20200311.145308.ef0fc31004")); +static const FString SpatialServiceVersion(TEXT("20200603.093801.6c37c65988")); FLocalDeploymentManager::FLocalDeploymentManager() : bLocalDeploymentRunning(false) @@ -76,7 +76,7 @@ void FLocalDeploymentManager::Init(FString RuntimeIPToExpose) // Stop existing spatial service to guarantee that any new existing spatial service would be running in the current project. TryStopSpatialService(); // Start spatial service in the current project if spatial networking is enabled - + if (GetDefault()->UsesSpatialNetworking()) { TryStartSpatialService(RuntimeIPToExpose); @@ -175,7 +175,7 @@ bool FLocalDeploymentManager::CheckIfPortIsBound(int32 Port) TSharedRef BroadcastAddr = SocketSubsystem->CreateInternetAddr(); BroadcastAddr->SetBroadcastAddress(); BroadcastAddr->SetPort(Port); - + // Now the listen address. TSharedRef ListenAddr = SocketSubsystem->GetLocalBindAddr(*GLog); ListenAddr->SetPort(Port); @@ -188,7 +188,7 @@ bool FLocalDeploymentManager::CheckIfPortIsBound(int32 Port) ListenSocket->SetReuseAddr(); ListenSocket->SetNonBlocking(); ListenSocket->SetRecvErr(); - + // Bind to our listen port. if (ListenSocket->Bind(*ListenAddr)) { @@ -419,11 +419,7 @@ void FLocalDeploymentManager::TryStartLocalDeployment(FString LaunchConfig, FStr SnapshotName.RemoveFromEnd(TEXT(".snapshot")); -#if ENGINE_MINOR_VERSION <= 22 - AttemptSpatialAuthResult = Async(EAsyncExecution::Thread, [this]() { return SpatialCommandUtils::AttemptSpatialAuth(bIsInChina); }, -#else AttemptSpatialAuthResult = Async(EAsyncExecution::Thread, [this]() { return SpatialCommandUtils::AttemptSpatialAuth(bIsInChina); }, -#endif [this, LaunchConfig, RuntimeVersion, LaunchArgs, SnapshotName, RuntimeIPToExpose, CallBack]() { bool bSuccess = AttemptSpatialAuthResult.IsReady() && AttemptSpatialAuthResult.Get() == true; @@ -433,7 +429,7 @@ void FLocalDeploymentManager::TryStartLocalDeployment(FString LaunchConfig, FStr } else { - UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Spatial auth failed attempting to launch local deployment.")); + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Failed to authenticate against SpatialOS while attempting to start a local deployment.")); } bStartingDeployment = false; @@ -718,7 +714,7 @@ bool FLocalDeploymentManager::IsServiceRunningAndInCorrectDirectory() else { UE_LOG(LogSpatialDeploymentManager, Error, - TEXT("Spatial service running in a different project! Please run 'spatial service stop' if you wish to launch deployments in the current project. Service at: %s"), *SpatialServiceProjectPath); + 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; diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/SSpatialOutputLog.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/SSpatialOutputLog.cpp index e78cb15853..72ae7a9486 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Private/SSpatialOutputLog.cpp +++ b/SpatialGDK/Source/SpatialGDKServices/Private/SSpatialOutputLog.cpp @@ -289,7 +289,7 @@ void SSpatialOutputLog::FormatAndPrintRawErrorLine(const FString& LogLine) 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=(.*\\.)?(.*)")); + const FRegexPattern LogPattern = FRegexPattern(TEXT("level=(.*) msg=\"(.*)\" loggerName=(.*\\.)?(.*)")); FRegexMatcher LogMatcher(LogPattern, LogLine); if (!LogMatcher.FindNext()) @@ -305,7 +305,7 @@ void SSpatialOutputLog::FormatAndPrintRawLogLine(const FString& LogLine) // 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")) + if (LogCategory == TEXT("WorkerLogMessageHandler") || LogCategory == TEXT("Runtime")) { const FRegexPattern WorkerLogPattern = FRegexPattern(TEXT("\\[([^:]*):([^\\]]*)\\] (.*)")); FRegexMatcher WorkerLogMatcher(WorkerLogPattern, LogMessage); diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/SpatialCommandUtils.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/SpatialCommandUtils.cpp index ab769d5274..dd5c810970 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Private/SpatialCommandUtils.cpp +++ b/SpatialGDK/Source/SpatialGDKServices/Private/SpatialCommandUtils.cpp @@ -1,15 +1,14 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "SpatialCommandUtils.h" + +#include "Serialization/JsonSerializer.h" #include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesModule.h" DEFINE_LOG_CATEGORY(LogSpatialCommandUtils); -namespace -{ - FString ChinaEnvironmentArgument = TEXT(" --environment=cn-production"); -} // anonymous namespace +#define LOCTEXT_NAMESPACE "SpatialCommandUtils" bool SpatialCommandUtils::SpatialVersion(bool bIsRunningInChina, const FString& DirectoryToRun, FString& OutResult, int32& OutExitCode) { @@ -17,7 +16,7 @@ bool SpatialCommandUtils::SpatialVersion(bool bIsRunningInChina, const FString& if (bIsRunningInChina) { - Command += ChinaEnvironmentArgument; + Command += SpatialGDKServicesConstants::ChinaEnvironmentArgument; } FSpatialGDKServicesModule::ExecuteAndReadOutput(*SpatialGDKServicesConstants::SpatialExe, Command, DirectoryToRun, OutResult, OutExitCode); @@ -37,7 +36,7 @@ bool SpatialCommandUtils::AttemptSpatialAuth(bool bIsRunningInChina) if (bIsRunningInChina) { - Command += ChinaEnvironmentArgument; + Command += SpatialGDKServicesConstants::ChinaEnvironmentArgument; } int32 OutExitCode; @@ -61,7 +60,7 @@ bool SpatialCommandUtils::StartSpatialService(const FString& Version, const FStr if (bIsRunningInChina) { - Command += ChinaEnvironmentArgument; + Command += SpatialGDKServicesConstants::ChinaEnvironmentArgument; } if (!Version.IsEmpty()) @@ -92,7 +91,7 @@ bool SpatialCommandUtils::StopSpatialService(bool bIsRunningInChina, const FStri if (bIsRunningInChina) { - Command += ChinaEnvironmentArgument; + Command += SpatialGDKServicesConstants::ChinaEnvironmentArgument; } FSpatialGDKServicesModule::ExecuteAndReadOutput(*SpatialGDKServicesConstants::SpatialExe, Command, DirectoryToRun, OutResult, OutExitCode); @@ -112,7 +111,7 @@ bool SpatialCommandUtils::BuildWorkerConfig(bool bIsRunningInChina, const FStrin if (bIsRunningInChina) { - Command += ChinaEnvironmentArgument; + Command += SpatialGDKServicesConstants::ChinaEnvironmentArgument; } FSpatialGDKServicesModule::ExecuteAndReadOutput(*SpatialGDKServicesConstants::SpatialExe, Command, DirectoryToRun, OutResult, OutExitCode); @@ -140,3 +139,145 @@ FProcHandle SpatialCommandUtils::LocalWorkerReplace(const FString& ServicePort, return FPlatformProcess::CreateProc(*SpatialGDKServicesConstants::SpatialExe, *Command, false, true, true, OutProcessID, 2 /*PriorityModifier*/, nullptr, nullptr, nullptr); } + +bool SpatialCommandUtils::GenerateDevAuthToken(bool bIsRunningInChina, FString& OutTokenSecret, FString& OutErrorMessage) +{ + FString Arguments = TEXT("project auth dev-auth-token create --description=\"Unreal GDK Token\" --json_output"); + if (bIsRunningInChina) + { + Arguments += SpatialGDKServicesConstants::ChinaEnvironmentArgument; + } + + FString CreateDevAuthTokenResult; + int32 ExitCode; + FSpatialGDKServicesModule::ExecuteAndReadOutput(SpatialGDKServicesConstants::SpatialExe, Arguments, SpatialGDKServicesConstants::SpatialOSDirectory, CreateDevAuthTokenResult, ExitCode); + + if (ExitCode != 0) + { + FString ErrorMessage = CreateDevAuthTokenResult; + TSharedRef> JsonReader = TJsonReaderFactory::Create(CreateDevAuthTokenResult); + TSharedPtr JsonRootObject; + if (FJsonSerializer::Deserialize(JsonReader, JsonRootObject) && JsonRootObject.IsValid()) + { + JsonRootObject->TryGetStringField("error", ErrorMessage); + } + OutErrorMessage = FString::Printf(TEXT("Unable to generate a development authentication token. Result: %s"), *ErrorMessage); + return false; + }; + + FString AuthResult; + FString DevAuthTokenResult; + bool bFoundNewline = CreateDevAuthTokenResult.TrimEnd().Split(TEXT("\n"), &AuthResult, &DevAuthTokenResult, ESearchCase::IgnoreCase, ESearchDir::FromEnd); + if (!bFoundNewline || DevAuthTokenResult.IsEmpty()) + { + // This is necessary because 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; + } + + TSharedRef> JsonReader = TJsonReaderFactory::Create(DevAuthTokenResult); + TSharedPtr JsonRootObject; + if (!(FJsonSerializer::Deserialize(JsonReader, JsonRootObject) && JsonRootObject.IsValid())) + { + OutErrorMessage = FString::Printf(TEXT("Unable to parse the received development authentication token. Result: %s"), *DevAuthTokenResult); + return false; + } + + // We need a pointer to a shared pointer due to how the JSON API works. + const TSharedPtr* JsonDataObject; + if (!(JsonRootObject->TryGetObjectField("json_data", JsonDataObject))) + { + OutErrorMessage = FString::Printf(TEXT("Unable to parse the received json data. Result: %s"), *DevAuthTokenResult); + return false; + } + + FString TokenSecret; + if (!(*JsonDataObject)->TryGetStringField("token_secret", TokenSecret)) + { + OutErrorMessage = FString::Printf(TEXT("Unable to parse the token_secret field inside the received json data. Result: %s"), *DevAuthTokenResult); + return false; + } + + OutTokenSecret = TokenSecret; + return true; +} + +bool SpatialCommandUtils::HasDevLoginTag(const FString& DeploymentName, bool bIsRunningInChina, FText& OutErrorMessage) +{ + if (DeploymentName.IsEmpty()) + { + OutErrorMessage = LOCTEXT("NoDeploymentName", "No deployment name has been specified."); + return false; + } + + FString TagsCommand = FString::Printf(TEXT("project deployment tags list %s --json_output"), *DeploymentName); + if (bIsRunningInChina) + { + TagsCommand += SpatialGDKServicesConstants::ChinaEnvironmentArgument; + } + + FString DeploymentCheckResult; + int32 ExitCode; + FSpatialGDKServicesModule::ExecuteAndReadOutput(*SpatialGDKServicesConstants::SpatialExe, TagsCommand, SpatialGDKServicesConstants::SpatialOSDirectory, DeploymentCheckResult, ExitCode); + if (ExitCode != 0) + { + FString ErrorMessage = DeploymentCheckResult; + TSharedRef> JsonReader = TJsonReaderFactory::Create(DeploymentCheckResult); + TSharedPtr JsonRootObject; + if (FJsonSerializer::Deserialize(JsonReader, JsonRootObject) && JsonRootObject.IsValid()) + { + 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)); + return false; + }; + + FString AuthResult; + FString RetrieveTagsResult; + 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. + RetrieveTagsResult = DeploymentCheckResult; + } + + TSharedRef> JsonReader = TJsonReaderFactory::Create(RetrieveTagsResult); + 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)); + 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)); + return false; + } + + /* + Output looks like this: + Tags: [unreal_deployment_launcher,dev_login] + We need to parse it a bit to be able to iterate through the tags + */ + if (JsonMessage[6] != '[' || JsonMessage[JsonMessage.Len() - 1] != ']') + { + OutErrorMessage = FText::Format(LOCTEXT("DeploymentTagsInvalid", "Could not parse the tags.\nMessage: {0}"), FText::FromString(JsonMessage)); + return false; + } + + FString TagsString = JsonMessage.Mid(7, JsonMessage.Len() - 8); + TArray Tags; + TagsString.ParseIntoArray(Tags, TEXT(","), true); + + if (Tags.Contains(SpatialGDKServicesConstants::DevLoginDeploymentTag)) + { + 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)); + return false; +} + +#undef LOCTEXT_NAMESPACE diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/SpatialGDKServicesModule.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/SpatialGDKServicesModule.cpp index d782defe8b..c4792b4ccd 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Private/SpatialGDKServicesModule.cpp +++ b/SpatialGDK/Source/SpatialGDKServices/Private/SpatialGDKServicesModule.cpp @@ -12,6 +12,7 @@ #include "SSpatialOutputLog.h" #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" +#include "Serialization/JsonWriter.h" #include "SpatialGDKServicesConstants.h" #include "SpatialGDKServicesPrivate.h" #include "Widgets/Docking/SDockTab.h" @@ -137,26 +138,61 @@ void FSpatialGDKServicesModule::ExecuteAndReadOutput(const FString& Executable, FPlatformProcess::ClosePipe(0, WritePipe); } +void FSpatialGDKServicesModule::SetProjectName(const FString& InProjectName) +{ + FString SpatialFileResult; + + 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); + return; + } + JsonParsedSpatialFile->SetStringField("name", 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.")); + return; + } + if (!FFileHelper::SaveStringToFile(SpatialFileResult, *FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, SpatialGDKServicesConstants::SpatialOSConfigFileName))) + { + UE_LOG(LogSpatialGDKServices, Error, TEXT("Failed to write file content to %s"), *SpatialGDKServicesConstants::SpatialOSConfigFileName); + } + ProjectName = InProjectName; +} + FString FSpatialGDKServicesModule::ParseProjectName() { FString ProjectNameParsed; - FString SpatialFileName = TEXT("spatialos.json"); + if (TSharedPtr JsonParsedSpatialFile = ParseProjectFile()) + { + if (JsonParsedSpatialFile->TryGetStringField(TEXT("name"), ProjectNameParsed)) + { + return ProjectNameParsed; + } + else + { + UE_LOG(LogSpatialGDKServices, Error, TEXT("'name' does not exist in spatialos.json. Can't read project name.")); + } + } + + ProjectNameParsed.Empty(); + return ProjectNameParsed; +} + +TSharedPtr FSpatialGDKServicesModule::ParseProjectFile() +{ FString SpatialFileResult; + TSharedPtr JsonParsedSpatialFile; - if (FFileHelper::LoadFileToString(SpatialFileResult, *FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, SpatialFileName))) + if (FFileHelper::LoadFileToString(SpatialFileResult, *FPaths::Combine(SpatialGDKServicesConstants::SpatialOSDirectory, SpatialGDKServicesConstants::SpatialOSConfigFileName))) { - TSharedPtr JsonParsedSpatialFile; if (ParseJson(SpatialFileResult, JsonParsedSpatialFile)) { - if (JsonParsedSpatialFile->TryGetStringField(TEXT("name"), ProjectNameParsed)) - { - return ProjectNameParsed; - } - else - { - UE_LOG(LogSpatialGDKServices, Error, TEXT("'name' does not exist in spatialos.json. Can't read project name.")); - } + return JsonParsedSpatialFile; } else { @@ -168,8 +204,7 @@ FString FSpatialGDKServicesModule::ParseProjectName() UE_LOG(LogSpatialGDKServices, Error, TEXT("Loading spatialos.json failed. Can't get project name.")); } - ProjectNameParsed.Empty(); - return ProjectNameParsed; + return nullptr; } #undef LOCTEXT_NAMESPACE diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialCommandUtils.h b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialCommandUtils.h index 9cc61bc676..65c77ff780 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialCommandUtils.h +++ b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialCommandUtils.h @@ -9,12 +9,12 @@ 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 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 GenerateDevAuthToken(bool bIsRunningInChina, FString& OutTokenSecret, FString& OutErrorMessage); + SPATIALGDKSERVICES_API static bool HasDevLoginTag(const FString& DeploymentName, bool bIsRunningInChinat, FText& OutErrorMessage); }; diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesConstants.h b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesConstants.h index 51a193057d..ca8250a32b 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesConstants.h +++ b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesConstants.h @@ -29,5 +29,21 @@ namespace SpatialGDKServicesConstants const FString SpotExe = CreateExePath(GDKProgramPath, TEXT("spot")); const FString SchemaCompilerExe = CreateExePath(GDKProgramPath, TEXT("schema_compiler")); const FString SpatialOSDirectory = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectDir(), TEXT("/../spatial/"))); - const FString SpatialOSRuntimePinnedVersion("14.5.1"); + const FString SpatialOSConfigFileName = TEXT("spatialos.json"); + const FString ChinaEnvironmentArgument = TEXT(" --environment=cn-production"); + + const FString SpatialOSRuntimePinnedStandardVersion = TEXT("0.4.3"); + const FString SpatialOSRuntimePinnedCompatbilityModeVersion = TEXT("14.5.4"); + + const FString InspectorURL = TEXT("http://localhost:31000/inspector"); + const FString InspectorV2URL = TEXT("http://localhost:31000/inspector-v2"); + + 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"); } diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesModule.h b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesModule.h index dfe958d351..53ee5cadf1 100644 --- a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesModule.h +++ b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesModule.h @@ -30,10 +30,13 @@ class SPATIALGDKSERVICES_API FSpatialGDKServicesModule : public IModuleInterface 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); private: FLocalDeploymentManager LocalDeploymentManager; static FString ParseProjectName(); + static TSharedPtr ParseProjectFile(); }; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/EngineClasses/SpatialVirtualWorkerTranslationManager/SpatialVirtualWorkerTranslationManagerTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/EngineClasses/SpatialVirtualWorkerTranslationManager/SpatialVirtualWorkerTranslationManagerTest.cpp index 5d59fecf79..6f038c3a48 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/EngineClasses/SpatialVirtualWorkerTranslationManager/SpatialVirtualWorkerTranslationManagerTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/EngineClasses/SpatialVirtualWorkerTranslationManager/SpatialVirtualWorkerTranslationManagerTest.cpp @@ -81,9 +81,7 @@ VIRTUALWORKERTRANSLATIONMANAGER_TEST(Given_a_failed_query_response_THEN_query_ag ResponseOp.result_count = 0; ResponseOp.message = "Failed call"; - TSet VirtualWorkerIds; - VirtualWorkerIds.Add(1); - Manager->AddVirtualWorkerIds(VirtualWorkerIds); + Manager->SetNumberOfVirtualWorkers(1); Delegate->ExecuteIfBound(ResponseOp); TestTrue("After a failed query response, the TranslationManager queried again for server worker entities.", Connection->GetLastEntityQuery() != nullptr); @@ -106,9 +104,7 @@ VIRTUALWORKERTRANSLATIONMANAGER_TEST(Given_a_successful_query_without_enough_wor ResponseOp.message = "Successfully returned 0 entities"; // Make sure the TranslationManager is expecting more workers than are returned. - TSet VirtualWorkerIds; - VirtualWorkerIds.Add(1); - Manager->AddVirtualWorkerIds(VirtualWorkerIds); + Manager->SetNumberOfVirtualWorkers(1); Delegate->ExecuteIfBound(ResponseOp); TestTrue("When not enough workers available, the TranslationManager queried again for server worker entities.", Connection->GetLastEntityQuery() != nullptr); @@ -137,9 +133,7 @@ VIRTUALWORKERTRANSLATIONMANAGER_TEST(Given_a_successful_query_with_invalid_worke ResponseOp.results = &worker; // Make sure the TranslationManager is only expecting a single worker. - TSet VirtualWorkerIds; - VirtualWorkerIds.Add(1); - Manager->AddVirtualWorkerIds(VirtualWorkerIds); + 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); diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialConnectionManagerTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialConnectionManagerTest.cpp new file mode 100644 index 0000000000..caee69fbbf --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialConnectionManagerTest.cpp @@ -0,0 +1,326 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/TestDefinitions.h" +#include "Interop/Connection/SpatialConnectionManager.h" +#include "CoreMinimal.h" + +#define CONNECTIONMANAGER_TEST(TestName) \ + GDK_TEST(Core, SpatialConnectionManager, TestName) + +class FTemporaryCommandLine +{ +public: + explicit FTemporaryCommandLine(const FString& NewCommandLine) + { + if (OldCommandLine.IsEmpty()) + { + OldCommandLine = FCommandLine::GetOriginal(); + FCommandLine::Set(*NewCommandLine); + bDidSetCommandLine = true; + } + } + + ~FTemporaryCommandLine() + { + if (bDidSetCommandLine) + { + FCommandLine::Set(*OldCommandLine); + OldCommandLine.Empty(); + } + } + +private: + static FString OldCommandLine; + bool bDidSetCommandLine = false; +}; + +FString FTemporaryCommandLine::OldCommandLine; + +CONNECTIONMANAGER_TEST(SetupFromURL_Locator_CustomLocator) +{ + // GIVEN + FTemporaryCommandLine TemporaryCommandLine("-locatorHost 99.88.77.66"); + const FURL URL(nullptr, TEXT("10.20.30.40?locator?customLocator?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("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 + FTemporaryCommandLine TemporaryCommandLine("-locatorHost 99.88.77.66"); + const FURL URL(nullptr, TEXT("10.20.30.40?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("PlayerIdentityToken", Manager->LocatorConfig.PlayerIdentityToken, "foo"); + TestEqual("LoginToken", Manager->LocatorConfig.LoginToken, "bar"); + TestEqual("WorkerType", Manager->LocatorConfig.WorkerType, "SomeWorkerType"); + + return true; +} + +CONNECTIONMANAGER_TEST(SetupFromURL_DevAuth) +{ + // GIVEN + FTemporaryCommandLine TemporaryCommandLine("-locatorHost 99.88.77.66"); + 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(); + + // WHEN + Manager->SetupConnectionConfigFromURL(URL, "SomeWorkerType"); + + // THEN + TestEqual("LocatorHost", Manager->DevAuthConfig.LocatorHost, "10.20.30.40"); + TestEqual("DevAuthToken", Manager->DevAuthConfig.DevelopmentAuthToken, "foo"); + TestEqual("Deployment", Manager->DevAuthConfig.Deployment, "bar"); + TestEqual("PlayerId", Manager->DevAuthConfig.PlayerId, "666"); + TestEqual("DisplayName", Manager->DevAuthConfig.DisplayName, "n00bkilla"); + TestEqual("Metadata", Manager->DevAuthConfig.MetaData, "important"); + TestEqual("WorkerType", Manager->DevAuthConfig.WorkerType, "SomeWorkerType"); + + return true; +} + +CONNECTIONMANAGER_TEST(SetupFromURL_DevAuth_LocatorHost) +{ + // GIVEN + FTemporaryCommandLine TemporaryCommandLine("-locatorHost 99.88.77.66"); + const FURL URL(nullptr, TEXT("10.20.30.40?devauth"), TRAVEL_Absolute); + USpatialConnectionManager* Manager = NewObject(); + + // WHEN + Manager->SetupConnectionConfigFromURL(URL, "SomeWorkerType"); + + // THEN + TestEqual("LocatorHost", Manager->DevAuthConfig.LocatorHost, "99.88.77.66"); + + return true; +} + +CONNECTIONMANAGER_TEST(SetupFromURL_Receptionist_Localhost) +{ + // GIVEN + FTemporaryCommandLine TemporaryCommandLine(""); + const FURL URL(nullptr, TEXT("127.0.0.1:777"), TRAVEL_Absolute); + USpatialConnectionManager* Manager = NewObject(); + + // WHEN + Manager->SetupConnectionConfigFromURL(URL, "SomeWorkerType"); + + // THEN + TestEqual("UseExternalIp", Manager->ReceptionistConfig.UseExternalIp, false); + TestEqual("ReceptionistHost", Manager->ReceptionistConfig.GetReceptionistHost(), "127.0.0.1"); + TestEqual("ReceptionistPort", Manager->ReceptionistConfig.GetReceptionistPort(), 777); + TestEqual("WorkerType", Manager->ReceptionistConfig.WorkerType, "SomeWorkerType"); + + return true; +} + +CONNECTIONMANAGER_TEST(SetupFromURL_Receptionist_ExternalHost) +{ + // GIVEN + FTemporaryCommandLine TemporaryCommandLine(""); + const FURL URL(nullptr, TEXT("10.20.30.40:777"), TRAVEL_Absolute); + USpatialConnectionManager* Manager = NewObject(); + + // WHEN + Manager->SetupConnectionConfigFromURL(URL, "SomeWorkerType"); + + // THEN + TestEqual("UseExternalIp", Manager->ReceptionistConfig.UseExternalIp, false); + TestEqual("ReceptionistHost", Manager->ReceptionistConfig.GetReceptionistHost(), "10.20.30.40"); + TestEqual("ReceptionistPort", Manager->ReceptionistConfig.GetReceptionistPort(), 777); + TestEqual("WorkerType", Manager->ReceptionistConfig.WorkerType, "SomeWorkerType"); + + return true; +} + +CONNECTIONMANAGER_TEST(SetupFromURL_Receptionist_ExternalBridge) +{ + // GIVEN + FTemporaryCommandLine TemporaryCommandLine(""); + const FURL URL(nullptr, TEXT("127.0.0.1:777?useExternalIpForBridge"), TRAVEL_Absolute); + USpatialConnectionManager* Manager = NewObject(); + + // WHEN + Manager->SetupConnectionConfigFromURL(URL, "SomeWorkerType"); + + // THEN + TestEqual("UseExternalIp", Manager->ReceptionistConfig.UseExternalIp, true); + TestEqual("ReceptionistHost", Manager->ReceptionistConfig.GetReceptionistHost(), "127.0.0.1"); + TestEqual("ReceptionistPort", Manager->ReceptionistConfig.GetReceptionistPort(), 777); + TestEqual("WorkerType", Manager->ReceptionistConfig.WorkerType, "SomeWorkerType"); + + return true; +} + +CONNECTIONMANAGER_TEST(SetupFromURL_Receptionist_ExternalBridgeNoHost) +{ + // GIVEN + FTemporaryCommandLine TemporaryCommandLine(""); + const FURL URL(nullptr, TEXT("?useExternalIpForBridge"), TRAVEL_Absolute); + USpatialConnectionManager* Manager = NewObject(); + + // WHEN + Manager->SetupConnectionConfigFromURL(URL, "SomeWorkerType"); + + // THEN + TestEqual("UseExternalIp", Manager->ReceptionistConfig.UseExternalIp, true); + TestEqual("ReceptionistHost", Manager->ReceptionistConfig.GetReceptionistHost(), "127.0.0.1"); + TestEqual("ReceptionistPort", Manager->ReceptionistConfig.GetReceptionistPort(), SpatialConstants::DEFAULT_PORT); + TestEqual("WorkerType", Manager->ReceptionistConfig.WorkerType, "SomeWorkerType"); + + return true; +} + +CONNECTIONMANAGER_TEST(SetupFromCommandLine_Locator) +{ + // GIVEN + FTemporaryCommandLine TemporaryCommandLine("-locatorHost 10.20.30.40 -playerIdentityToken foo -loginToken bar"); + USpatialConnectionManager* Manager = NewObject(); + + // WHEN + const bool bSuccess = Manager->TrySetupConnectionConfigFromCommandLine("SomeWorkerType"); + + // THEN + TestEqual("Success", bSuccess, true); + TestEqual("LocatorHost", Manager->LocatorConfig.LocatorHost, "10.20.30.40"); + TestEqual("PlayerIdentityToken", Manager->LocatorConfig.PlayerIdentityToken, "foo"); + TestEqual("LoginToken", Manager->LocatorConfig.LoginToken, "bar"); + TestEqual("WorkerType", Manager->LocatorConfig.WorkerType, "SomeWorkerType"); + + return true; +} + +CONNECTIONMANAGER_TEST(SetupFromCommandLine_DevAuth) +{ + // GIVEN + FTemporaryCommandLine TemporaryCommandLine("-locatorHost 10.20.30.40 -devAuthToken foo -deployment bar -playerId 666 -displayName n00bkilla -metadata important"); + USpatialConnectionManager* Manager = NewObject(); + + // WHEN + const bool bSuccess = Manager->TrySetupConnectionConfigFromCommandLine("SomeWorkerType"); + + // THEN + TestEqual("Success", bSuccess, true); + TestEqual("LocatorHost", Manager->DevAuthConfig.LocatorHost, "10.20.30.40"); + TestEqual("DevAuthToken", Manager->DevAuthConfig.DevelopmentAuthToken, "foo"); + TestEqual("Deployment", Manager->DevAuthConfig.Deployment, "bar"); + TestEqual("PlayerId", Manager->DevAuthConfig.PlayerId, "666"); + TestEqual("DisplayName", Manager->DevAuthConfig.DisplayName, "n00bkilla"); + TestEqual("Metadata", Manager->DevAuthConfig.MetaData, "important"); + TestEqual("WorkerType", Manager->DevAuthConfig.WorkerType, "SomeWorkerType"); + + return true; +} + +CONNECTIONMANAGER_TEST(SetupFromCommandLine_Receptionist_ReceptionistHost) +{ + // GIVEN + FTemporaryCommandLine TemporaryCommandLine("-receptionistHost 10.20.30.40 -receptionistPort 666"); + USpatialConnectionManager* Manager = NewObject(); + + // WHEN + const bool bSuccess = Manager->TrySetupConnectionConfigFromCommandLine("SomeWorkerType"); + + // THEN + TestEqual("Success", bSuccess, true); + TestEqual("UseExternalIp", Manager->ReceptionistConfig.UseExternalIp, false); + TestEqual("ReceptionistHost", Manager->ReceptionistConfig.GetReceptionistHost(), "10.20.30.40"); + TestEqual("ReceptionistPort", Manager->ReceptionistConfig.GetReceptionistPort(), 666); + TestEqual("WorkerType", Manager->ReceptionistConfig.WorkerType, "SomeWorkerType"); + + return true; +} + +CONNECTIONMANAGER_TEST(SetupFromCommandLine_Receptionist_ReceptionistHostLocal) +{ + // GIVEN + FTemporaryCommandLine TemporaryCommandLine("-receptionistPort 666"); + USpatialConnectionManager* Manager = NewObject(); + + // WHEN + const bool bSuccess = Manager->TrySetupConnectionConfigFromCommandLine("SomeWorkerType"); + + // THEN + TestEqual("Success", bSuccess, true); + TestEqual("UseExternalIp", Manager->ReceptionistConfig.UseExternalIp, false); + TestEqual("ReceptionistHost", Manager->ReceptionistConfig.GetReceptionistHost(), "127.0.0.1"); + TestEqual("ReceptionistPort", Manager->ReceptionistConfig.GetReceptionistPort(), 666); + TestEqual("WorkerType", Manager->ReceptionistConfig.WorkerType, "SomeWorkerType"); + + return true; +} + +CONNECTIONMANAGER_TEST(SetupFromCommandLine_Receptionist_ReceptionistHostLocalExternalBridge) +{ + // GIVEN + FTemporaryCommandLine TemporaryCommandLine("-receptionistPort 666 -useExternalIpForBridge true"); + USpatialConnectionManager* Manager = NewObject(); + + // WHEN + const bool bSuccess = Manager->TrySetupConnectionConfigFromCommandLine("SomeWorkerType"); + + // THEN + TestEqual("Success", bSuccess, true); + TestEqual("UseExternalIp", Manager->ReceptionistConfig.UseExternalIp, true); + TestEqual("ReceptionistHost", Manager->ReceptionistConfig.GetReceptionistHost(), "127.0.0.1"); + TestEqual("ReceptionistPort", Manager->ReceptionistConfig.GetReceptionistPort(), 666); + TestEqual("WorkerType", Manager->ReceptionistConfig.WorkerType, "SomeWorkerType"); + + return true; +} + +CONNECTIONMANAGER_TEST(SetupFromCommandLine_Receptionist_URL) +{ + // GIVEN + FTemporaryCommandLine TemporaryCommandLine("10.20.30.40?someUnknownFlag?otherFlag -receptionistPort 666"); + USpatialConnectionManager* Manager = NewObject(); + + // WHEN + const bool bSuccess = Manager->TrySetupConnectionConfigFromCommandLine("SomeWorkerType"); + + // THEN + TestEqual("Success", bSuccess, true); + TestEqual("UseExternalIp", Manager->ReceptionistConfig.UseExternalIp, false); + TestEqual("ReceptionistHost", Manager->ReceptionistConfig.GetReceptionistHost(), "10.20.30.40"); + TestEqual("ReceptionistPort", Manager->ReceptionistConfig.GetReceptionistPort(), 666); + TestEqual("WorkerType", Manager->ReceptionistConfig.WorkerType, "SomeWorkerType"); + + return true; +} + +CONNECTIONMANAGER_TEST(SetupFromCommandLine_Receptionist_URLAndExternalBridge) +{ + // GIVEN + FTemporaryCommandLine TemporaryCommandLine("127.0.0.1?useExternalIpForBridge -receptionistPort 666"); + USpatialConnectionManager* Manager = NewObject(); + + // WHEN + Manager->TrySetupConnectionConfigFromCommandLine("SomeWorkerType"); + + // THEN + TestEqual("UseExternalIp", Manager->ReceptionistConfig.UseExternalIp, true); + TestEqual("ReceptionistHost", Manager->ReceptionistConfig.GetReceptionistHost(), "127.0.0.1"); + TestEqual("ReceptionistPort", Manager->ReceptionistConfig.GetReceptionistPort(), 666); + TestEqual("WorkerType", Manager->ReceptionistConfig.WorkerType, "SomeWorkerType"); + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialWorkerConnectionTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialWorkerConnectionTest.cpp index 68ba9d2385..56c0dcc9ce 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialWorkerConnectionTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Interop/Connection/SpatialWorkerConnectionTest.cpp @@ -37,7 +37,7 @@ void StartSetupConnectionConfigFromURL(USpatialConnectionManager* ConnectionMana bOutUseReceptionist = (URL.Host != SpatialConstants::LOCATOR_HOST) && !URL.HasOption(TEXT("locator")); if (bOutUseReceptionist) { - ConnectionManager->ReceptionistConfig.SetReceptionistHost(URL.Host); + ConnectionManager->ReceptionistConfig.SetupFromURL(URL); } else { @@ -57,13 +57,6 @@ void FinishSetupConnectionConfig(USpatialConnectionManager* ConnectionManager, c FReceptionistConfig& ReceptionistConfig = ConnectionManager->ReceptionistConfig; ReceptionistConfig.WorkerType = WorkerType; - - const TCHAR* UseExternalIpForBridge = TEXT("useExternalIpForBridge"); - if (URL.HasOption(UseExternalIpForBridge)) - { - FString UseExternalIpOption = URL.GetOption(UseExternalIpForBridge, TEXT("")); - ReceptionistConfig.UseExternalIp = !UseExternalIpOption.Equals(TEXT("false"), ESearchCase::IgnoreCase); - } } else { @@ -75,7 +68,7 @@ void FinishSetupConnectionConfig(USpatialConnectionManager* ConnectionManager, c } } } // anonymous namespace - + DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FWaitForSeconds, double, Seconds); bool FWaitForSeconds::Update() { diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/GridBasedLBStrategyTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/GridBasedLBStrategyTest.cpp index 8b97a73d4a..46a77311ce 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/GridBasedLBStrategyTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/GridBasedLBStrategyTest.cpp @@ -43,6 +43,14 @@ UWorld* GetAnyGameWorld() return World; } +void CreateStrategy(uint32 Rows, uint32 Cols, float WorldWidth, float WorldHeight, uint32 LocalWorkerId) +{ + Strat = UTestGridBasedLBStrategy::Create(Rows, Cols, WorldWidth, WorldHeight); + Strat->Init(); + Strat->SetVirtualWorkerIds(1, Strat->GetMinimumRequiredWorkers()); + Strat->SetLocalVirtualWorkerId(LocalWorkerId); +} + DEFINE_LATENT_AUTOMATION_COMMAND(FCleanup); bool FCleanup::Update() { @@ -53,15 +61,10 @@ bool FCleanup::Update() return true; } -DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(FCreateStrategy, uint32, Rows, uint32, Cols, float, WorldWidth, float, WorldHeight, uint32, LocalWorkerIdIndex); +DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(FCreateStrategy, uint32, Rows, uint32, Cols, float, WorldWidth, float, WorldHeight, uint32, LocalWorkerId); bool FCreateStrategy::Update() { - Strat = UTestGridBasedLBStrategy::Create(Rows, Cols, WorldWidth, WorldHeight); - Strat->Init(); - - TSet VirtualWorkerIds = Strat->GetVirtualWorkerIds(); - Strat->SetLocalVirtualWorkerId(VirtualWorkerIds.Array()[LocalWorkerIdIndex]); - + CreateStrategy(Rows, Cols, WorldWidth, WorldHeight, LocalWorkerId); return true; } @@ -166,27 +169,12 @@ bool FCheckVirtualWorkersMatch::Update() return true; } -GRIDBASEDLBSTRATEGY_TEST(GIVEN_2_rows_3_cols_WHEN_get_virtual_worker_ids_is_called_THEN_it_returns_6_ids) +GRIDBASEDLBSTRATEGY_TEST(GIVEN_2_rows_3_cols_WHEN_get_minimum_required_workers_is_called_THEN_it_returns_6) { - Strat = UTestGridBasedLBStrategy::Create(2, 3, 10000.f, 10000.f); - Strat->Init(); + CreateStrategy(2, 3, 10000.f, 10000.f, 1); - TSet VirtualWorkerIds = Strat->GetVirtualWorkerIds(); - TestEqual("Number of Virtual Workers", VirtualWorkerIds.Num(), 6); - - return true; -} - -GRIDBASEDLBSTRATEGY_TEST(GIVEN_a_grid_WHEN_get_virtual_worker_ids_THEN_all_worker_ids_are_valid) -{ - Strat = UTestGridBasedLBStrategy::Create(5, 10, 10000.f, 10000.f); - Strat->Init(); - - TSet VirtualWorkerIds = Strat->GetVirtualWorkerIds(); - for (uint32 VirtualWorkerId : VirtualWorkerIds) - { - TestNotEqual("Virtual Worker Id", VirtualWorkerId, SpatialConstants::INVALID_VIRTUAL_WORKER_ID); - } + uint32 NumVirtualWorkers = Strat->GetMinimumRequiredWorkers(); + TestEqual("Number of Virtual Workers", NumVirtualWorkers, 6); return true; } @@ -195,6 +183,7 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_grid_is_not_ready_WHEN_local_virtual_worker_id_is { Strat = UTestGridBasedLBStrategy::Create(1, 1, 10000.f, 10000.f); Strat->Init(); + Strat->SetVirtualWorkerIds(1, Strat->GetMinimumRequiredWorkers()); TestFalse("IsReady Before LocalVirtualWorkerId Set", Strat->IsReady()); @@ -207,10 +196,11 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_grid_is_not_ready_WHEN_local_virtual_worker_id_is GRIDBASEDLBSTRATEGY_TEST(GIVEN_four_cells_WHEN_get_worker_interest_for_virtual_worker_THEN_returns_correct_constraint) { - Strat = UTestGridBasedLBStrategy::Create(2, 2, 10000.f, 10000.f, 1000.0f); - Strat->Init(); - // Take the top right corner, as then all our testing numbers can be positive. + // Create the Strategy manually so we can set an interest border. + Strat = UTestGridBasedLBStrategy::Create(2, 2, 10000.f, 10000.f, 1000.f); + Strat->Init(); + Strat->SetVirtualWorkerIds(1, Strat->GetMinimumRequiredWorkers()); Strat->SetLocalVirtualWorkerId(4); SpatialGDK::QueryConstraint StratConstraint = Strat->GetWorkerInterestQueryConstraint(); @@ -234,11 +224,8 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_four_cells_WHEN_get_worker_interest_for_virtual_w GRIDBASEDLBSTRATEGY_TEST(GIVEN_four_cells_WHEN_get_worker_entity_position_for_virtual_worker_THEN_returns_correct_position) { - Strat = UTestGridBasedLBStrategy::Create(2, 2, 10000.f, 10000.f, 1000.0f); - Strat->Init(); - // Take the top right corner, as then all our testing numbers can be positive. - Strat->SetLocalVirtualWorkerId(4); + CreateStrategy(2, 2, 10000.f, 10000.f, 4); FVector WorkerPosition = Strat->GetWorkerEntityPosition(); @@ -249,13 +236,34 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_four_cells_WHEN_get_worker_entity_position_for_vi return true; } +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()); + 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()); + 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()); + return true; +} + } // anonymous namespace GRIDBASEDLBSTRATEGY_TEST(GIVEN_a_single_cell_and_valid_local_id_WHEN_should_relinquish_called_THEN_returns_false) { AutomationOpenMap("/Engine/Maps/Entry"); - ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(1, 1, 10000.f, 10000.f, 0)); + 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")); @@ -269,7 +277,7 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_four_cells_WHEN_actors_in_each_cell_THEN_should_r { AutomationOpenMap("/Engine/Maps/Entry"); - ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(2, 2, 10000.f, 10000.f, 0)); + ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(2, 2, 10000.f, 10000.f, 1)); ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld()); ADD_LATENT_AUTOMATION_COMMAND(FSpawnActorAtLocation("Actor1", FVector(-2500.f, -2500.f, 0.f))); ADD_LATENT_AUTOMATION_COMMAND(FSpawnActorAtLocation("Actor2", FVector(2500.f, -2500.f, 0.f))); @@ -289,7 +297,7 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_moving_actor_WHEN_actor_crosses_boundary_THEN_sho { AutomationOpenMap("/Engine/Maps/Entry"); - ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(2, 1, 10000.f, 10000.f, 0)); + ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(2, 1, 10000.f, 10000.f, 1)); ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld()); ADD_LATENT_AUTOMATION_COMMAND(FSpawnActorAtLocation("Actor1", FVector(-2.f, 0.f, 0.f))); ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor("Actor1")); @@ -307,7 +315,7 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_two_actors_WHEN_actors_are_in_same_cell_THEN_shou { AutomationOpenMap("/Engine/Maps/Entry"); - ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(1, 2, 10000.f, 10000.f, 0)); + ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(1, 2, 10000.f, 10000.f, 1)); ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld()); ADD_LATENT_AUTOMATION_COMMAND(FSpawnActorAtLocation("Actor1", FVector(-2.f, 100.f, 0.f))); ADD_LATENT_AUTOMATION_COMMAND(FSpawnActorAtLocation("Actor2", FVector(-500.f, 0.f, 0.f))); @@ -326,11 +334,12 @@ GRIDBASEDLBSTRATEGY_TEST(GIVEN_two_cells_WHEN_actor_in_one_cell_THEN_strategy_re ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld()); ADD_LATENT_AUTOMATION_COMMAND(FSpawnActorAtLocation("Actor1", FVector(0.f, -2500.f, 0.f))); ADD_LATENT_AUTOMATION_COMMAND(FWaitForActor("Actor1")); - ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(1, 2, 10000.f, 10000.f, 0)); - ADD_LATENT_AUTOMATION_COMMAND(FCheckShouldRelinquishAuthority(this, "Actor1", false)); ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(1, 2, 10000.f, 10000.f, 1)); + 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()); return true; } + diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.h index e7edc1fd89..0b8095345c 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.h +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.h @@ -9,7 +9,7 @@ /** * This class is for testing purposes only. */ -UCLASS(HideDropdown) +UCLASS(HideDropdown, NotBlueprintable) class SPATIALGDKTESTS_API UTestGridBasedLBStrategy : public UGridBasedLBStrategy { GENERATED_BODY() diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/LayeredLBStrategy/LayeredLBStrategyTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/LayeredLBStrategy/LayeredLBStrategyTest.cpp new file mode 100644 index 0000000000..dfaedc27ce --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/LayeredLBStrategy/LayeredLBStrategyTest.cpp @@ -0,0 +1,432 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "EngineClasses/SpatialWorldSettings.h" +#include "LoadBalancing/GridBasedLBStrategy.h" +#include "LoadBalancing/LayeredLBStrategy.h" +#include "SpatialGDKTests/SpatialGDK/LoadBalancing/GridBasedLBStrategy/TestGridBasedLBStrategy.h" +#include "SpatialGDKSettings.h" +#include "TestLayeredLBStrategy.h" + +#include "Engine/Engine.h" +#include "GameFramework/DefaultPawn.h" +#include "GameFramework/GameStateBase.h" +#include "Misc/Optional.h" +#include "Tests/AutomationCommon.h" +#include "Tests/AutomationEditorCommon.h" +#include "Tests/TestDefinitions.h" + +#define LAYEREDLBSTRATEGY_TEST(TestName) \ + GDK_TEST(Core, ULayeredLBStrategy, TestName) + +namespace +{ + +struct TestData { + ULayeredLBStrategy* Strat{ nullptr }; + UWorld* TestWorld{ nullptr }; + TMap TestActors{}; + + TestData() + {} + + ~TestData() + { + ASpatialWorldSettings* WorldSettings = Cast(TestWorld->GetWorldSettings()); + WorldSettings->WorkerLayers.Empty(); + } +}; + +// Copied from AutomationCommon::GetAnyGameWorld(). +UWorld* GetAnyGameWorld() +{ + UWorld* World = nullptr; + const TIndirectArray& WorldContexts = GEngine->GetWorldContexts(); + for (const FWorldContext& Context : WorldContexts) + { + if ((Context.WorldType == EWorldType::PIE || Context.WorldType == EWorldType::Game) + && (Context.World() != nullptr)) + { + World = Context.World(); + break; + } + } + + return World; +} + +} // anonymous namespace + +DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FWaitForWorld, TSharedPtr, TestData); +bool FWaitForWorld::Update() +{ + auto& TestWorld = TestData->TestWorld; + TestWorld = GetAnyGameWorld(); + + if (TestWorld && TestWorld->AreActorsInitialized()) + { + AGameStateBase* GameState = TestWorld->GetGameState(); + if (GameState && GameState->HasMatchStarted()) + { + return true; + } + } + + return false; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FCreateStrategy, TSharedPtr, TestData); +bool FCreateStrategy::Update() +{ + TestData->Strat = NewObject(TestData->TestWorld); + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER(FSetDefaultLayer, TSharedPtr, TestData, TSubclassOf, DefaultLayer); +bool FSetDefaultLayer::Update() +{ + ASpatialWorldSettings* WorldSettings = Cast(TestData->TestWorld->GetWorldSettings()); + WorldSettings->DefaultLayerLoadBalanceStrategy = DefaultLayer; + + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FAddLayer, TSharedPtr, TestData, TSubclassOf, StrategyClass, TSet>, TargetActorTypes); +bool FAddLayer::Update() +{ + ASpatialWorldSettings* WorldSettings = Cast(TestData->TestWorld->GetWorldSettings()); + auto StratName = FName{ *FString::FromInt((WorldSettings->WorkerLayers.Num())) }; + FLayerInfo LayerInfo; + LayerInfo.Name = StratName; + LayerInfo.LoadBalanceStrategy = StrategyClass; + for (const auto& TargetActors : TargetActorTypes) + { + LayerInfo.ActorClasses.Add(TargetActors); + } + WorldSettings->WorkerLayers.Add(StratName, LayerInfo); + + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER(FSetupStrategy, TSharedPtr, TestData, TOptional, NumVirtualWorkers); +bool FSetupStrategy::Update() +{ + ASpatialWorldSettings* WorldSettings = Cast(TestData->TestWorld->GetWorldSettings()); + WorldSettings->DefaultLayerLoadBalanceStrategy = UGridBasedLBStrategy::StaticClass(); + WorldSettings->bEnableMultiWorker = true; + + auto& Strat = TestData->Strat; + Strat->Init(); + + if (!NumVirtualWorkers.IsSet()) + { + NumVirtualWorkers = Strat->GetMinimumRequiredWorkers(); + } + + Strat->SetVirtualWorkerIds(1, NumVirtualWorkers.GetValue()); + + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER(FSetupStrategyLocalWorker, TSharedPtr, TestData, VirtualWorkerId, WorkerId); +bool FSetupStrategyLocalWorker::Update() +{ + TestData->Strat->SetLocalVirtualWorkerId(WorkerId); + return true; +} + +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); + return true; +} + +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); + return true; +} + +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); + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FSpawnLayer1PawnAtLocation, TSharedPtr, TestData, FName, Handle, + FVector, Location); +bool FSpawnLayer1PawnAtLocation::Update() +{ + FActorSpawnParameters SpawnParams; + SpawnParams.bNoFail = true; + SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + + AActor* NewActor = TestData->TestWorld->SpawnActor(Location, FRotator::ZeroRotator, SpawnParams); + TestData->TestActors.Add(Handle, NewActor); + + return true; +} + +DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FSpawnLayer2PawnAtLocation, TSharedPtr, TestData, + FName, Handle, FVector, Location); +bool FSpawnLayer2PawnAtLocation::Update() +{ + FActorSpawnParameters SpawnParams; + SpawnParams.bNoFail = true; + SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + + AActor* NewActor = TestData->TestWorld->SpawnActor(Location, FRotator::ZeroRotator, SpawnParams); + TestData->TestActors.Add(Handle, NewActor); + + return true; +} + +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; + const auto& TestActors = TestData->TestActors; + + const VirtualWorkerId FirstActorAuth = Strat->WhoShouldHaveAuthority(*TestActors[FirstActorName]); + const VirtualWorkerId SecondActorAuth = Strat->WhoShouldHaveAuthority(*TestActors[SecondActorName]); + + if (ExpectEqual) + { + Test->TestEqual( + FString::Printf(TEXT("Actors should have the same auth. Actor1: %d, Actor2: %d"), FirstActorAuth, SecondActorAuth), + FirstActorAuth, SecondActorAuth); + } + else { + Test->TestNotEqual( + FString::Printf(TEXT("Actors should have different auth. Actor1: %d, Actor2: %d"), FirstActorAuth, SecondActorAuth), + FirstActorAuth, SecondActorAuth); + } + + return true; +} + +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); + return true; +} + +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 DoWeActuallyHaveAuthority = Strat->ShouldHaveAuthority(*TestActors[ActorName]); + + Test->TestEqual( + FString::Printf(TEXT("WhoShouldHaveAuthority should match ShouldHaveAuthority. Expected: %b Actual: %b"), WeShouldHaveAuthority, DoWeActuallyHaveAuthority), + WeShouldHaveAuthority, DoWeActuallyHaveAuthority); + return true; +} + +LAYEREDLBSTRATEGY_TEST(GIVEN_strat_is_not_ready_WHEN_local_virtual_worker_id_is_set_THEN_is_ready) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = TSharedPtr(new TestData); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSetDefaultLayer(Data, UGridBasedLBStrategy::StaticClass())); + ADD_LATENT_AUTOMATION_COMMAND(FSetupStrategy(Data, {})); + ADD_LATENT_AUTOMATION_COMMAND(FCheckStratIsReady(Data, this, false)); + ADD_LATENT_AUTOMATION_COMMAND(FSetupStrategyLocalWorker(Data, 1)); + ADD_LATENT_AUTOMATION_COMMAND(FCheckStratIsReady(Data, this, true)); + + 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) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = TSharedPtr(new TestData); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSetDefaultLayer(Data, UGridBasedLBStrategy::StaticClass())); + ADD_LATENT_AUTOMATION_COMMAND(FAddLayer(Data, UTwoByFourLBGridStrategy::StaticClass(), {} )); + ADD_LATENT_AUTOMATION_COMMAND(FAddLayer(Data, UGridBasedLBStrategy::StaticClass(), {})); + ADD_LATENT_AUTOMATION_COMMAND(FSetupStrategy(Data, {})); + ADD_LATENT_AUTOMATION_COMMAND(FCheckMinimumWorkers(Data, this, 10)); + + 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) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = TSharedPtr(new TestData); + + this->AddExpectedError("LayeredLBStrategy was not given enough VirtualWorkerIds to meet the demands of the layer strategies.", + EAutomationExpectedErrorFlags::MatchType::Contains, 1); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSetDefaultLayer(Data, UGridBasedLBStrategy::StaticClass())); + ADD_LATENT_AUTOMATION_COMMAND(FAddLayer(Data, UGridBasedLBStrategy::StaticClass(), {ALayer1Pawn::StaticClass()})); + ADD_LATENT_AUTOMATION_COMMAND(FAddLayer(Data, UGridBasedLBStrategy::StaticClass(), {ALayer2Pawn::StaticClass()})); + // The two single strategies plus the default strat require 3 vitual workers, but we only have 2. + ADD_LATENT_AUTOMATION_COMMAND(FSetupStrategy(Data, 2)); + + 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) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = TSharedPtr(new TestData); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSetDefaultLayer(Data, UGridBasedLBStrategy::StaticClass())); + ADD_LATENT_AUTOMATION_COMMAND(FAddLayer(Data, UGridBasedLBStrategy::StaticClass(), {ALayer1Pawn::StaticClass()} )); + ADD_LATENT_AUTOMATION_COMMAND(FAddLayer(Data, UGridBasedLBStrategy::StaticClass(), {ALayer2Pawn::StaticClass()} )); + + // The two single strategies plus the default strat require 3 vitual workers. + ADD_LATENT_AUTOMATION_COMMAND(FSetupStrategy(Data, 3)); + + return true; +} + +LAYEREDLBSTRATEGY_TEST(Given_layered_strat_of_default_strat_WHEN_requires_handover_called_THEN_returns_false) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = TSharedPtr(new TestData); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSetDefaultLayer(Data, UGridBasedLBStrategy::StaticClass())); + ADD_LATENT_AUTOMATION_COMMAND(FSetupStrategy(Data, {})); + ADD_LATENT_AUTOMATION_COMMAND(FSetupStrategyLocalWorker(Data, 1)); + ADD_LATENT_AUTOMATION_COMMAND(FCheckRequiresHandover(Data, this, false)); + + return true; +} + +LAYEREDLBSTRATEGY_TEST(Given_layered_strat_of_single_cell_grid_strat_and_default_strat_WHEN_requires_handover_called_THEN_returns_true) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = TSharedPtr(new TestData); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSetDefaultLayer(Data, UGridBasedLBStrategy::StaticClass())); + ADD_LATENT_AUTOMATION_COMMAND(FAddLayer(Data, UGridBasedLBStrategy::StaticClass(), {ADefaultPawn::StaticClass()})); + ADD_LATENT_AUTOMATION_COMMAND(FSetupStrategy(Data, {})); + ADD_LATENT_AUTOMATION_COMMAND(FSetupStrategyLocalWorker(Data, 1)); + ADD_LATENT_AUTOMATION_COMMAND(FCheckRequiresHandover(Data, this, true)); + + return true; +} + +LAYEREDLBSTRATEGY_TEST(Given_layered_strat_of_default_strat_WHEN_who_should_have_auth_called_THEN_return_1) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = TSharedPtr(new TestData); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSetupStrategy(Data, {})); + ADD_LATENT_AUTOMATION_COMMAND(FSetupStrategyLocalWorker(Data, 1)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnLayer1PawnAtLocation(Data, TEXT("DefaultLayerActor"), FVector::ZeroVector)); + ADD_LATENT_AUTOMATION_COMMAND(FCheckWhoShouldHaveAuthority(Data, this, "DefaultLayerActor", 1)); + + return true; +} + +LAYEREDLBSTRATEGY_TEST(Given_layered_strat_WHEN_set_local_worker_called_twice_THEN_an_error_is_logged) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = TSharedPtr(new TestData); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSetDefaultLayer(Data, UGridBasedLBStrategy::StaticClass())); + ADD_LATENT_AUTOMATION_COMMAND(FSetupStrategy(Data, {})); + ADD_LATENT_AUTOMATION_COMMAND(FSetupStrategyLocalWorker(Data, 1)); + ADD_LATENT_AUTOMATION_COMMAND(FSetupStrategyLocalWorker(Data, 2)); + + 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"); + + TSharedPtr Data = TSharedPtr(new TestData); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSetDefaultLayer(Data, UGridBasedLBStrategy::StaticClass())); + ADD_LATENT_AUTOMATION_COMMAND(FAddLayer(Data, UTwoByFourLBGridStrategy::StaticClass(), {ALayer1Pawn::StaticClass()})); + ADD_LATENT_AUTOMATION_COMMAND(FAddLayer(Data, UTwoByFourLBGridStrategy::StaticClass(), {ALayer2Pawn::StaticClass()})); + ADD_LATENT_AUTOMATION_COMMAND(FSetupStrategy(Data, {})); + ADD_LATENT_AUTOMATION_COMMAND(FSetupStrategyLocalWorker(Data, 1)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnLayer1PawnAtLocation(Data, TEXT("Layer1Actor1"), FVector::ZeroVector)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnLayer1PawnAtLocation(Data, TEXT("Layer1Actor2"), FVector::ZeroVector)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnLayer2PawnAtLocation(Data, TEXT("Layer2Actor1"), FVector::ZeroVector)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnLayer2PawnAtLocation(Data, TEXT("Later2Actor2"), FVector::ZeroVector)); + + ADD_LATENT_AUTOMATION_COMMAND(FCheckActorsAuth(Data, this, TEXT("Layer1Actor1"), TEXT("Layer1Actor2"), true)); + ADD_LATENT_AUTOMATION_COMMAND(FCheckActorsAuth(Data, this, TEXT("Layer2Actor1"), TEXT("Later2Actor2"), true)); + + 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) +{ + AutomationOpenMap("/Engine/Maps/Entry"); + + TSharedPtr Data = TSharedPtr(new TestData); + + ADD_LATENT_AUTOMATION_COMMAND(FWaitForWorld(Data)); + + ADD_LATENT_AUTOMATION_COMMAND(FCreateStrategy(Data)); + ADD_LATENT_AUTOMATION_COMMAND(FSetDefaultLayer(Data, UGridBasedLBStrategy::StaticClass())); + ADD_LATENT_AUTOMATION_COMMAND(FAddLayer(Data, UTwoByFourLBGridStrategy::StaticClass(), {ALayer1Pawn::StaticClass()})); + ADD_LATENT_AUTOMATION_COMMAND(FAddLayer(Data, UTwoByFourLBGridStrategy::StaticClass(), {ALayer2Pawn::StaticClass()})); + + ADD_LATENT_AUTOMATION_COMMAND(FSetupStrategy(Data, {})); + ADD_LATENT_AUTOMATION_COMMAND(FSetupStrategyLocalWorker(Data, 1)); + + ADD_LATENT_AUTOMATION_COMMAND(FSpawnLayer1PawnAtLocation(Data, TEXT("Layer1Actor"), FVector::ZeroVector)); + ADD_LATENT_AUTOMATION_COMMAND(FSpawnLayer2PawnAtLocation(Data, TEXT("Layer2Actor"), FVector::ZeroVector)); + + ADD_LATENT_AUTOMATION_COMMAND(FCheckActorsAuth(Data, this, TEXT("Layer1Actor"), TEXT("Layer2Actor"), false)); + + return true; +} + diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/LayeredLBStrategy/TestLayeredLBStrategy.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/LayeredLBStrategy/TestLayeredLBStrategy.h new file mode 100644 index 0000000000..571731990a --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/LoadBalancing/LayeredLBStrategy/TestLayeredLBStrategy.h @@ -0,0 +1,45 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "LoadBalancing/GridBasedLBStrategy.h" + +#include "CoreMinimal.h" +#include "GameFramework/DefaultPawn.h" + +#include "TestLayeredLBStrategy.generated.h" + +/** + * This class is for testing purposes only. + */ +UCLASS(HideDropdown, NotBlueprintable) +class SPATIALGDKTESTS_API UTwoByFourLBGridStrategy : public UGridBasedLBStrategy +{ + GENERATED_BODY() + +public: + UTwoByFourLBGridStrategy(): Super() + { + Rows = 2; + Cols = 4; + } +}; + +/** + * Same as a Default pawn but for testing + */ +UCLASS(NotPlaceable) +class SPATIALGDKTESTS_API ALayer1Pawn : public ADefaultPawn +{ + GENERATED_BODY() +}; + + +/** + * Same as a Default pawn but for testing + */ +UCLASS(NotPlaceable) +class SPATIALGDKTESTS_API ALayer2Pawn : public ADefaultPawn +{ + GENERATED_BODY() +}; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/Misc/SpatialActivationFlagsTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/Misc/SpatialActivationFlagsTest.cpp index 055d59bda6..86b17aec15 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/Misc/SpatialActivationFlagsTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDK/Utils/Misc/SpatialActivationFlagsTest.cpp @@ -19,7 +19,7 @@ void InitializeSpatialFlagEarlyValues() bEarliestFlag = GetDefault()->UsesSpatialNetworking(); } -GDK_TEST(Core, UGeneralProjectSettings, SpatialActivationReport) +GDK_SLOW_TEST(Core, UGeneralProjectSettings, SpatialActivationReport) { const UGeneralProjectSettings* ProjectSettings = GetDefault(); @@ -69,7 +69,7 @@ struct SpatialActivationFlagTestFixture { ProjectPath = FPaths::GetProjectFilePath(); CommandLineArgs = ProjectPath; - CommandLineArgs.Append(TEXT(" -ExecCmds=\"Automation RunTests SpatialGDK.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")); @@ -131,7 +131,7 @@ bool FRunSubProcessCommand::Update() } -GDK_TEST(Core, UGeneralProjectSettings, SpatialActivationSetting_False) +GDK_SLOW_TEST(Core, UGeneralProjectSettings, SpatialActivationSetting_False) { auto TestFixture = MakeShared(*this); TestFixture->ChangeSetting(false); @@ -141,7 +141,7 @@ GDK_TEST(Core, UGeneralProjectSettings, SpatialActivationSetting_False) return true; } -GDK_TEST(Core, UGeneralProjectSettings, SpatialActivationSetting_True) +GDK_SLOW_TEST(Core, UGeneralProjectSettings, SpatialActivationSetting_True) { auto TestFixture = MakeShared(*this); TestFixture->ChangeSetting(true); @@ -151,7 +151,7 @@ GDK_TEST(Core, UGeneralProjectSettings, SpatialActivationSetting_True) return true; } -GDK_TEST(Core, UGeneralProjectSettings, SpatialActivationOverride_True) +GDK_SLOW_TEST(Core, UGeneralProjectSettings, SpatialActivationOverride_True) { auto TestFixture = MakeShared(*this); TestFixture->ChangeSetting(false); @@ -165,7 +165,7 @@ GDK_TEST(Core, UGeneralProjectSettings, SpatialActivationOverride_True) return true; } -GDK_TEST(Core, UGeneralProjectSettings, SpatialActivationOverride_False) +GDK_SLOW_TEST(Core, UGeneralProjectSettings, SpatialActivationOverride_False) { auto TestFixture = MakeShared(*this); TestFixture->ChangeSetting(false); diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/LoadBalancingEditorExtension/SpatialGDKEditorLBExtensionTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/LoadBalancingEditorExtension/SpatialGDKEditorLBExtensionTest.cpp new file mode 100644 index 0000000000..329ee3e52f --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/LoadBalancingEditorExtension/SpatialGDKEditorLBExtensionTest.cpp @@ -0,0 +1,144 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Tests/TestDefinitions.h" + +#include "SpatialGDKEditorModule.h" +#include "SpatialGDKEditorSettings.h" + +#include "TestLoadBalancingStrategyEditorExtension.h" + +#define LB_EXTENSION_TEST(TestName) \ + GDK_TEST(SpatialGDKEditor, LoadBalancingEditorExtension, TestName) + +namespace +{ + +struct TestFixture +{ + TestFixture() + : ExtensionManager(FModuleManager::GetModuleChecked("SpatialGDKEditor").GetLBStrategyExtensionManager()) + { } + + ~TestFixture() + { + CleanupRuntimeStrategy(); + + // Cleanup registered strategies for tests. + ExtensionManager.UnregisterExtension(); + ExtensionManager.UnregisterExtension(); + } + + bool GetDefaultLaunchConfiguration(const UAbstractLBStrategy* Strategy, UAbstractRuntimeLoadBalancingStrategy*& OutConfiguration, FIntPoint& OutWorldDimensions) + { + CleanupRuntimeStrategy(); + const bool bResult = ExtensionManager.GetDefaultLaunchConfiguration(Strategy, OutConfiguration, OutWorldDimensions); + + if (OutConfiguration) + { + OutConfiguration->AddToRoot(); + RuntimeStrategy = OutConfiguration; + } + + return bResult; + } + + void CleanupRuntimeStrategy() + { + if (RuntimeStrategy) + { + RuntimeStrategy->RemoveFromRoot(); + RuntimeStrategy = nullptr; + } + } + + FLBStrategyEditorExtensionManager& ExtensionManager; + UAbstractRuntimeLoadBalancingStrategy* RuntimeStrategy = nullptr; +}; + +} +LB_EXTENSION_TEST(GIVEN_not_registered_strategy_WHEN_looking_for_extension_THEN_extension_is_not_found) +{ + TestFixture Fixture; + UAbstractLBStrategy* DummyStrategy = UDummyLoadBalancingStrategy::StaticClass()->GetDefaultObject(); + + UAbstractRuntimeLoadBalancingStrategy* RuntimeStrategy = nullptr; + FIntPoint WorldSize; + + AddExpectedError(TEXT("Could not find editor extension for load balancing strategy")); + + bool bResult = Fixture.GetDefaultLaunchConfiguration(DummyStrategy, RuntimeStrategy, WorldSize); + + TestTrue("Non registered strategy is properly handled", !bResult); + + return true; +} + +LB_EXTENSION_TEST(GIVEN_registered_strategy_WHEN_looking_for_extension_THEN_extension_is_found) +{ + TestFixture Fixture; + Fixture.ExtensionManager.RegisterExtension(); + + UAbstractLBStrategy* DummyStrategy = UDummyLoadBalancingStrategy::StaticClass()->GetDefaultObject(); + + UAbstractRuntimeLoadBalancingStrategy* RuntimeStrategy = nullptr; + FIntPoint WorldSize; + bool bResult = Fixture.GetDefaultLaunchConfiguration(DummyStrategy, RuntimeStrategy, WorldSize); + + TestTrue("Registered strategy is properly handled", bResult); + + return true; +} + +LB_EXTENSION_TEST(GIVEN_registered_strategy_WHEN_getting_launch_settings_THEN_launch_settings_are_filled) +{ + TestFixture Fixture; + Fixture.ExtensionManager.RegisterExtension(); + + UDummyLoadBalancingStrategy* DummyStrategy = NewObject(); + + DummyStrategy->AddToRoot(); + DummyStrategy->NumberOfWorkers = 10; + + UAbstractRuntimeLoadBalancingStrategy* RuntimeStrategy = nullptr; + FIntPoint WorldSize; + bool bResult = Fixture.GetDefaultLaunchConfiguration(DummyStrategy, RuntimeStrategy, WorldSize); + + TestTrue("Registered strategy is properly handled", bResult); + + UEntityShardingRuntimeLoadBalancingStrategy* EntityShardingStrategy = Cast(RuntimeStrategy); + + TestTrue("Launch settings are extracted", EntityShardingStrategy && EntityShardingStrategy->NumWorkers == 10); + + DummyStrategy->RemoveFromRoot(); + + return true; +} + + +LB_EXTENSION_TEST(GIVEN_registered_derived_strategy_WHEN_looking_for_extension_THEN_most_derived_extension_is_found) +{ + TestFixture Fixture; + Fixture.ExtensionManager.RegisterExtension(); + + UAbstractLBStrategy* DummyStrategy = UDummyLoadBalancingStrategy::StaticClass()->GetDefaultObject(); + UAbstractLBStrategy* DerivedDummyStrategy = UDerivedDummyLoadBalancingStrategy::StaticClass()->GetDefaultObject(); + + UAbstractRuntimeLoadBalancingStrategy* RuntimeStrategy = nullptr; + FIntPoint WorldSize; + FIntPoint WorldSizeDerived; + bool bResult = Fixture.GetDefaultLaunchConfiguration(DummyStrategy, RuntimeStrategy, WorldSize); + bResult &= Fixture.GetDefaultLaunchConfiguration(DerivedDummyStrategy, RuntimeStrategy, WorldSizeDerived); + + TestTrue("Registered strategies are properly handled", bResult); + TestTrue("Common extension used", WorldSize == WorldSizeDerived && WorldSize.X == 0); + + Fixture.ExtensionManager.RegisterExtension(); + + bResult = Fixture.GetDefaultLaunchConfiguration(DummyStrategy, RuntimeStrategy, WorldSize); + bResult &= Fixture.GetDefaultLaunchConfiguration(DerivedDummyStrategy, RuntimeStrategy, WorldSizeDerived); + + TestTrue("Registered strategies are properly handled", bResult); + TestTrue("Most derived extension used", WorldSize != WorldSizeDerived && WorldSize.X == 0 && WorldSizeDerived.X == 4242); + + return true; +} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/LoadBalancingEditorExtension/TestLoadBalancingStrategy.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/LoadBalancingEditorExtension/TestLoadBalancingStrategy.h new file mode 100644 index 0000000000..08e67428aa --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/LoadBalancingEditorExtension/TestLoadBalancingStrategy.h @@ -0,0 +1,64 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "LoadBalancing/AbstractLBStrategy.h" + +#include "TestLoadBalancingStrategy.generated.h" + +class SpatialVirtualWorkerTranslator; + +UCLASS() +class UDummyLoadBalancingStrategy : public UAbstractLBStrategy +{ + GENERATED_BODY() + +public: + UDummyLoadBalancingStrategy() = default; + + + /* UAbstractLBStrategy Interface */ + void Init() override + { + } + + TSet GetVirtualWorkerIds() const override + { + return TSet(); + } + + bool ShouldHaveAuthority(const AActor& Actor) const override + { + return false; + } + + VirtualWorkerId WhoShouldHaveAuthority(const AActor& Actor) const override + { + return 0; + } + + SpatialGDK::QueryConstraint GetWorkerInterestQueryConstraint() const override + { + return SpatialGDK::QueryConstraint(); + } + + bool RequiresHandoverData() const override + { + return false; + } + + FVector GetWorkerEntityPosition() const override + { + return FVector(ForceInitToZero); + } + /* End UAbstractLBStrategy Interface */ + + uint32 NumberOfWorkers = 1; + +}; + +UCLASS() +class UDerivedDummyLoadBalancingStrategy : public UDummyLoadBalancingStrategy +{ + GENERATED_BODY() +}; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/LoadBalancingEditorExtension/TestLoadBalancingStrategyEditorExtension.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/LoadBalancingEditorExtension/TestLoadBalancingStrategyEditorExtension.h new file mode 100644 index 0000000000..0a0dbeb24e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/LoadBalancingEditorExtension/TestLoadBalancingStrategyEditorExtension.h @@ -0,0 +1,48 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "EditorExtension/LBStrategyEditorExtension.h" +#include "SpatialRuntimeLoadBalancingStrategies.h" +#include "TestLoadBalancingStrategy.h" + +class FTestLBStrategyEditorExtension : public FLBStrategyEditorExtensionTemplate +{ +public: + bool GetDefaultLaunchConfiguration(const UDummyLoadBalancingStrategy* Strategy, UAbstractRuntimeLoadBalancingStrategy*& OutConfiguration, FIntPoint& OutWorldDimensions) const + { + if (Strategy == nullptr) + { + return false; + } + + UEntityShardingRuntimeLoadBalancingStrategy* Conf = NewObject(); + Conf->NumWorkers = Strategy->NumberOfWorkers; + OutConfiguration = Conf; + + OutWorldDimensions.X = OutWorldDimensions.Y = 0; + + return true; + } +}; + +class FTestDerivedLBStrategyEditorExtension : public FLBStrategyEditorExtensionTemplate +{ +public: + + bool GetDefaultLaunchConfiguration(const UDerivedDummyLoadBalancingStrategy* Strategy, UAbstractRuntimeLoadBalancingStrategy*& OutConfiguration, FIntPoint& OutWorldDimensions) const + { + if (Strategy == nullptr) + { + return false; + } + + UEntityShardingRuntimeLoadBalancingStrategy* Conf = NewObject(); + Conf->NumWorkers = Strategy->NumberOfWorkers; + OutConfiguration = Conf; + + OutWorldDimensions.X = OutWorldDimensions.Y = 4242; + + return true; + } +}; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/NonSpatialTypeActor.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/NonSpatialTypeActor.schema index 6b452b8ff9..7e72938313 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/NonSpatialTypeActor.schema +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/NonSpatialTypeActor.schema @@ -10,15 +10,15 @@ component NonSpatialTypeActor { bool breplicatemovement = 2; bool btearoff = 3; bool bcanbedamaged = 4; - bytes replicatedmovement = 5; - UnrealObjectRef attachmentreplication_attachparent = 6; - bytes attachmentreplication_locationoffset = 7; - bytes attachmentreplication_relativescale3d = 8; - bytes attachmentreplication_rotationoffset = 9; - string attachmentreplication_attachsocket = 10; - UnrealObjectRef attachmentreplication_attachcomponent = 11; - UnrealObjectRef owner = 12; - uint32 role = 13; - uint32 remoterole = 14; + uint32 remoterole = 5; + bytes replicatedmovement = 6; + UnrealObjectRef attachmentreplication_attachparent = 7; + bytes attachmentreplication_locationoffset = 8; + bytes attachmentreplication_relativescale3d = 9; + bytes attachmentreplication_rotationoffset = 10; + string attachmentreplication_attachsocket = 11; + UnrealObjectRef attachmentreplication_attachcomponent = 12; + UnrealObjectRef owner = 13; + uint32 role = 14; UnrealObjectRef instigator = 15; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActor.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActor.schema index 3af04e359d..7a2abbd41f 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActor.schema +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActor.schema @@ -10,15 +10,15 @@ component SpatialTypeActor { bool breplicatemovement = 2; bool btearoff = 3; bool bcanbedamaged = 4; - bytes replicatedmovement = 5; - UnrealObjectRef attachmentreplication_attachparent = 6; - bytes attachmentreplication_locationoffset = 7; - bytes attachmentreplication_relativescale3d = 8; - bytes attachmentreplication_rotationoffset = 9; - string attachmentreplication_attachsocket = 10; - UnrealObjectRef attachmentreplication_attachcomponent = 11; - UnrealObjectRef owner = 12; - uint32 role = 13; - uint32 remoterole = 14; + uint32 remoterole = 5; + bytes replicatedmovement = 6; + UnrealObjectRef attachmentreplication_attachparent = 7; + bytes attachmentreplication_locationoffset = 8; + bytes attachmentreplication_relativescale3d = 9; + bytes attachmentreplication_rotationoffset = 10; + string attachmentreplication_attachsocket = 11; + UnrealObjectRef attachmentreplication_attachcomponent = 12; + UnrealObjectRef owner = 13; + uint32 role = 14; UnrealObjectRef instigator = 15; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithActorComponent.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithActorComponent.schema index 29e2d85d1b..05762b2692 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithActorComponent.schema +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithActorComponent.schema @@ -10,16 +10,16 @@ component SpatialTypeActorWithActorComponent { bool breplicatemovement = 2; bool btearoff = 3; bool bcanbedamaged = 4; - bytes replicatedmovement = 5; - UnrealObjectRef attachmentreplication_attachparent = 6; - bytes attachmentreplication_locationoffset = 7; - bytes attachmentreplication_relativescale3d = 8; - bytes attachmentreplication_rotationoffset = 9; - string attachmentreplication_attachsocket = 10; - UnrealObjectRef attachmentreplication_attachcomponent = 11; - UnrealObjectRef owner = 12; - uint32 role = 13; - uint32 remoterole = 14; + uint32 remoterole = 5; + bytes replicatedmovement = 6; + UnrealObjectRef attachmentreplication_attachparent = 7; + bytes attachmentreplication_locationoffset = 8; + bytes attachmentreplication_relativescale3d = 9; + bytes attachmentreplication_rotationoffset = 10; + string attachmentreplication_attachsocket = 11; + UnrealObjectRef attachmentreplication_attachcomponent = 12; + UnrealObjectRef owner = 13; + uint32 role = 14; UnrealObjectRef instigator = 15; UnrealObjectRef spatialactorcomponent = 16; } diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleActorComponents.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleActorComponents.schema index 73839c1087..1c91db5e19 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleActorComponents.schema +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleActorComponents.schema @@ -10,16 +10,16 @@ component SpatialTypeActorWithMultipleActorComponents { bool breplicatemovement = 2; bool btearoff = 3; bool bcanbedamaged = 4; - bytes replicatedmovement = 5; - UnrealObjectRef attachmentreplication_attachparent = 6; - bytes attachmentreplication_locationoffset = 7; - bytes attachmentreplication_relativescale3d = 8; - bytes attachmentreplication_rotationoffset = 9; - string attachmentreplication_attachsocket = 10; - UnrealObjectRef attachmentreplication_attachcomponent = 11; - UnrealObjectRef owner = 12; - uint32 role = 13; - uint32 remoterole = 14; + uint32 remoterole = 5; + bytes replicatedmovement = 6; + UnrealObjectRef attachmentreplication_attachparent = 7; + bytes attachmentreplication_locationoffset = 8; + bytes attachmentreplication_relativescale3d = 9; + bytes attachmentreplication_rotationoffset = 10; + string attachmentreplication_attachsocket = 11; + UnrealObjectRef attachmentreplication_attachcomponent = 12; + UnrealObjectRef owner = 13; + uint32 role = 14; UnrealObjectRef instigator = 15; UnrealObjectRef firstspatialactorcomponent = 16; UnrealObjectRef secondspatialactorcomponent = 17; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleObjectComponents.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleObjectComponents.schema index 1155a5e43e..021ec5f13e 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleObjectComponents.schema +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema/SpatialTypeActorWithMultipleObjectComponents.schema @@ -10,16 +10,16 @@ component SpatialTypeActorWithMultipleObjectComponents { bool breplicatemovement = 2; bool btearoff = 3; bool bcanbedamaged = 4; - bytes replicatedmovement = 5; - UnrealObjectRef attachmentreplication_attachparent = 6; - bytes attachmentreplication_locationoffset = 7; - bytes attachmentreplication_relativescale3d = 8; - bytes attachmentreplication_rotationoffset = 9; - string attachmentreplication_attachsocket = 10; - UnrealObjectRef attachmentreplication_attachcomponent = 11; - UnrealObjectRef owner = 12; - uint32 role = 13; - uint32 remoterole = 14; + uint32 remoterole = 5; + bytes replicatedmovement = 6; + UnrealObjectRef attachmentreplication_attachparent = 7; + bytes attachmentreplication_locationoffset = 8; + bytes attachmentreplication_relativescale3d = 9; + bytes attachmentreplication_rotationoffset = 10; + string attachmentreplication_attachsocket = 11; + UnrealObjectRef attachmentreplication_attachcomponent = 12; + UnrealObjectRef owner = 13; + uint32 role = 14; UnrealObjectRef instigator = 15; UnrealObjectRef firstspatialobjectcomponent = 16; UnrealObjectRef secondspatialobjectcomponent = 17; diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/NonSpatialTypeActor.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/NonSpatialTypeActor.schema deleted file mode 100644 index 3e455e6bbf..0000000000 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/NonSpatialTypeActor.schema +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved -// Note that this file has been generated automatically -package unreal.generated.nonspatialtypeactor; - -import "unreal/gdk/core_types.schema"; - -component NonSpatialTypeActor { - id = {{id}}; - bool bhidden = 1; - bool breplicatemovement = 2; - bool btearoff = 3; - bool bcanbedamaged = 4; - bytes replicatedmovement = 5; - UnrealObjectRef attachmentreplication_attachparent = 6; - bytes attachmentreplication_locationoffset = 7; - bytes attachmentreplication_relativescale3d = 8; - bytes attachmentreplication_rotationoffset = 9; - string attachmentreplication_attachsocket = 10; - UnrealObjectRef attachmentreplication_attachcomponent = 11; - UnrealObjectRef owner = 12; - uint32 remoterole = 13; - uint32 role = 14; - UnrealObjectRef instigator = 15; -} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActor.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActor.schema deleted file mode 100644 index 110deee31a..0000000000 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActor.schema +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved -// Note that this file has been generated automatically -package unreal.generated.spatialtypeactor; - -import "unreal/gdk/core_types.schema"; - -component SpatialTypeActor { - id = {{id}}; - bool bhidden = 1; - bool breplicatemovement = 2; - bool btearoff = 3; - bool bcanbedamaged = 4; - bytes replicatedmovement = 5; - UnrealObjectRef attachmentreplication_attachparent = 6; - bytes attachmentreplication_locationoffset = 7; - bytes attachmentreplication_relativescale3d = 8; - bytes attachmentreplication_rotationoffset = 9; - string attachmentreplication_attachsocket = 10; - UnrealObjectRef attachmentreplication_attachcomponent = 11; - UnrealObjectRef owner = 12; - uint32 remoterole = 13; - uint32 role = 14; - UnrealObjectRef instigator = 15; -} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorComponent.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorComponent.schema deleted file mode 100644 index e857c08706..0000000000 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorComponent.schema +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved -// Note that this file has been generated automatically -package unreal.generated; - -type SpatialTypeActorComponent { - bool breplicates = 1; - bool bisactive = 2; -} - -component SpatialTypeActorComponentDynamic1 { - id = 10000; - data SpatialTypeActorComponent; -} - -component SpatialTypeActorComponentDynamic2 { - id = 10001; - data SpatialTypeActorComponent; -} - -component SpatialTypeActorComponentDynamic3 { - id = 10002; - data SpatialTypeActorComponent; -} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorWithActorComponent.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorWithActorComponent.schema deleted file mode 100644 index 3df8a46770..0000000000 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorWithActorComponent.schema +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved -// Note that this file has been generated automatically -package unreal.generated.spatialtypeactorwithactorcomponent; - -import "unreal/gdk/core_types.schema"; - -component SpatialTypeActorWithActorComponent { - id = {{id}}; - bool bhidden = 1; - bool breplicatemovement = 2; - bool btearoff = 3; - bool bcanbedamaged = 4; - bytes replicatedmovement = 5; - UnrealObjectRef attachmentreplication_attachparent = 6; - bytes attachmentreplication_locationoffset = 7; - bytes attachmentreplication_relativescale3d = 8; - bytes attachmentreplication_rotationoffset = 9; - string attachmentreplication_attachsocket = 10; - UnrealObjectRef attachmentreplication_attachcomponent = 11; - UnrealObjectRef owner = 12; - uint32 remoterole = 13; - uint32 role = 14; - UnrealObjectRef instigator = 15; - UnrealObjectRef spatialactorcomponent = 16; -} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorWithMultipleActorComponents.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorWithMultipleActorComponents.schema deleted file mode 100644 index 249b8511bb..0000000000 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorWithMultipleActorComponents.schema +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved -// Note that this file has been generated automatically -package unreal.generated.spatialtypeactorwithmultipleactorcomponents; - -import "unreal/gdk/core_types.schema"; - -component SpatialTypeActorWithMultipleActorComponents { - id = {{id}}; - bool bhidden = 1; - bool breplicatemovement = 2; - bool btearoff = 3; - bool bcanbedamaged = 4; - bytes replicatedmovement = 5; - UnrealObjectRef attachmentreplication_attachparent = 6; - bytes attachmentreplication_locationoffset = 7; - bytes attachmentreplication_relativescale3d = 8; - bytes attachmentreplication_rotationoffset = 9; - string attachmentreplication_attachsocket = 10; - UnrealObjectRef attachmentreplication_attachcomponent = 11; - UnrealObjectRef owner = 12; - uint32 remoterole = 13; - uint32 role = 14; - UnrealObjectRef instigator = 15; - UnrealObjectRef firstspatialactorcomponent = 16; - UnrealObjectRef secondspatialactorcomponent = 17; -} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorWithMultipleObjectComponents.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorWithMultipleObjectComponents.schema deleted file mode 100644 index dfef85c8c5..0000000000 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/SpatialTypeActorWithMultipleObjectComponents.schema +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved -// Note that this file has been generated automatically -package unreal.generated.spatialtypeactorwithmultipleobjectcomponents; - -import "unreal/gdk/core_types.schema"; - -component SpatialTypeActorWithMultipleObjectComponents { - id = {{id}}; - bool bhidden = 1; - bool breplicatemovement = 2; - bool btearoff = 3; - bool bcanbedamaged = 4; - bytes replicatedmovement = 5; - UnrealObjectRef attachmentreplication_attachparent = 6; - bytes attachmentreplication_locationoffset = 7; - bytes attachmentreplication_relativescale3d = 8; - bytes attachmentreplication_rotationoffset = 9; - string attachmentreplication_attachsocket = 10; - UnrealObjectRef attachmentreplication_attachcomponent = 11; - UnrealObjectRef owner = 12; - uint32 remoterole = 13; - uint32 role = 14; - UnrealObjectRef instigator = 15; - UnrealObjectRef firstspatialobjectcomponent = 16; - UnrealObjectRef secondspatialobjectcomponent = 17; -} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/rpc_endpoints.schema b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/rpc_endpoints.schema deleted file mode 100644 index 81498ba52e..0000000000 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24/rpc_endpoints.schema +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved -// Note that this file has been generated automatically -package unreal.generated; - -import "unreal/gdk/core_types.schema"; -import "unreal/gdk/rpc_payload.schema"; - -component UnrealClientEndpoint { - id = 9978; - option client_to_server_reliable_rpc_0 = 1; - option client_to_server_reliable_rpc_1 = 2; - option client_to_server_reliable_rpc_2 = 3; - option client_to_server_reliable_rpc_3 = 4; - option client_to_server_reliable_rpc_4 = 5; - option client_to_server_reliable_rpc_5 = 6; - option client_to_server_reliable_rpc_6 = 7; - option client_to_server_reliable_rpc_7 = 8; - option client_to_server_reliable_rpc_8 = 9; - option client_to_server_reliable_rpc_9 = 10; - option client_to_server_reliable_rpc_10 = 11; - option client_to_server_reliable_rpc_11 = 12; - option client_to_server_reliable_rpc_12 = 13; - option client_to_server_reliable_rpc_13 = 14; - option client_to_server_reliable_rpc_14 = 15; - option client_to_server_reliable_rpc_15 = 16; - option client_to_server_reliable_rpc_16 = 17; - option client_to_server_reliable_rpc_17 = 18; - option client_to_server_reliable_rpc_18 = 19; - option client_to_server_reliable_rpc_19 = 20; - option client_to_server_reliable_rpc_20 = 21; - option client_to_server_reliable_rpc_21 = 22; - option client_to_server_reliable_rpc_22 = 23; - option client_to_server_reliable_rpc_23 = 24; - option client_to_server_reliable_rpc_24 = 25; - option client_to_server_reliable_rpc_25 = 26; - option client_to_server_reliable_rpc_26 = 27; - option client_to_server_reliable_rpc_27 = 28; - option client_to_server_reliable_rpc_28 = 29; - option client_to_server_reliable_rpc_29 = 30; - option client_to_server_reliable_rpc_30 = 31; - option client_to_server_reliable_rpc_31 = 32; - uint64 last_sent_client_to_server_reliable_rpc_id = 33; - option client_to_server_unreliable_rpc_0 = 34; - option client_to_server_unreliable_rpc_1 = 35; - option client_to_server_unreliable_rpc_2 = 36; - option client_to_server_unreliable_rpc_3 = 37; - option client_to_server_unreliable_rpc_4 = 38; - option client_to_server_unreliable_rpc_5 = 39; - option client_to_server_unreliable_rpc_6 = 40; - option client_to_server_unreliable_rpc_7 = 41; - option client_to_server_unreliable_rpc_8 = 42; - option client_to_server_unreliable_rpc_9 = 43; - option client_to_server_unreliable_rpc_10 = 44; - option client_to_server_unreliable_rpc_11 = 45; - option client_to_server_unreliable_rpc_12 = 46; - option client_to_server_unreliable_rpc_13 = 47; - option client_to_server_unreliable_rpc_14 = 48; - option client_to_server_unreliable_rpc_15 = 49; - option client_to_server_unreliable_rpc_16 = 50; - option client_to_server_unreliable_rpc_17 = 51; - option client_to_server_unreliable_rpc_18 = 52; - option client_to_server_unreliable_rpc_19 = 53; - option client_to_server_unreliable_rpc_20 = 54; - option client_to_server_unreliable_rpc_21 = 55; - option client_to_server_unreliable_rpc_22 = 56; - option client_to_server_unreliable_rpc_23 = 57; - option client_to_server_unreliable_rpc_24 = 58; - option client_to_server_unreliable_rpc_25 = 59; - option client_to_server_unreliable_rpc_26 = 60; - option client_to_server_unreliable_rpc_27 = 61; - option client_to_server_unreliable_rpc_28 = 62; - option client_to_server_unreliable_rpc_29 = 63; - option client_to_server_unreliable_rpc_30 = 64; - option client_to_server_unreliable_rpc_31 = 65; - uint64 last_sent_client_to_server_unreliable_rpc_id = 66; - uint64 last_acked_server_to_client_reliable_rpc_id = 67; - uint64 last_acked_server_to_client_unreliable_rpc_id = 68; -} - -component UnrealServerEndpoint { - id = 9977; - option server_to_client_reliable_rpc_0 = 1; - option server_to_client_reliable_rpc_1 = 2; - option server_to_client_reliable_rpc_2 = 3; - option server_to_client_reliable_rpc_3 = 4; - option server_to_client_reliable_rpc_4 = 5; - option server_to_client_reliable_rpc_5 = 6; - option server_to_client_reliable_rpc_6 = 7; - option server_to_client_reliable_rpc_7 = 8; - option server_to_client_reliable_rpc_8 = 9; - option server_to_client_reliable_rpc_9 = 10; - option server_to_client_reliable_rpc_10 = 11; - option server_to_client_reliable_rpc_11 = 12; - option server_to_client_reliable_rpc_12 = 13; - option server_to_client_reliable_rpc_13 = 14; - option server_to_client_reliable_rpc_14 = 15; - option server_to_client_reliable_rpc_15 = 16; - option server_to_client_reliable_rpc_16 = 17; - option server_to_client_reliable_rpc_17 = 18; - option server_to_client_reliable_rpc_18 = 19; - option server_to_client_reliable_rpc_19 = 20; - option server_to_client_reliable_rpc_20 = 21; - option server_to_client_reliable_rpc_21 = 22; - option server_to_client_reliable_rpc_22 = 23; - option server_to_client_reliable_rpc_23 = 24; - option server_to_client_reliable_rpc_24 = 25; - option server_to_client_reliable_rpc_25 = 26; - option server_to_client_reliable_rpc_26 = 27; - option server_to_client_reliable_rpc_27 = 28; - option server_to_client_reliable_rpc_28 = 29; - option server_to_client_reliable_rpc_29 = 30; - option server_to_client_reliable_rpc_30 = 31; - option server_to_client_reliable_rpc_31 = 32; - uint64 last_sent_server_to_client_reliable_rpc_id = 33; - option server_to_client_unreliable_rpc_0 = 34; - option server_to_client_unreliable_rpc_1 = 35; - option server_to_client_unreliable_rpc_2 = 36; - option server_to_client_unreliable_rpc_3 = 37; - option server_to_client_unreliable_rpc_4 = 38; - option server_to_client_unreliable_rpc_5 = 39; - option server_to_client_unreliable_rpc_6 = 40; - option server_to_client_unreliable_rpc_7 = 41; - option server_to_client_unreliable_rpc_8 = 42; - option server_to_client_unreliable_rpc_9 = 43; - option server_to_client_unreliable_rpc_10 = 44; - option server_to_client_unreliable_rpc_11 = 45; - option server_to_client_unreliable_rpc_12 = 46; - option server_to_client_unreliable_rpc_13 = 47; - option server_to_client_unreliable_rpc_14 = 48; - option server_to_client_unreliable_rpc_15 = 49; - option server_to_client_unreliable_rpc_16 = 50; - option server_to_client_unreliable_rpc_17 = 51; - option server_to_client_unreliable_rpc_18 = 52; - option server_to_client_unreliable_rpc_19 = 53; - option server_to_client_unreliable_rpc_20 = 54; - option server_to_client_unreliable_rpc_21 = 55; - option server_to_client_unreliable_rpc_22 = 56; - option server_to_client_unreliable_rpc_23 = 57; - option server_to_client_unreliable_rpc_24 = 58; - option server_to_client_unreliable_rpc_25 = 59; - option server_to_client_unreliable_rpc_26 = 60; - option server_to_client_unreliable_rpc_27 = 61; - option server_to_client_unreliable_rpc_28 = 62; - option server_to_client_unreliable_rpc_29 = 63; - option server_to_client_unreliable_rpc_30 = 64; - option server_to_client_unreliable_rpc_31 = 65; - uint64 last_sent_server_to_client_unreliable_rpc_id = 66; - uint64 last_acked_client_to_server_reliable_rpc_id = 67; - uint64 last_acked_client_to_server_unreliable_rpc_id = 68; -} - -component UnrealMulticastRPCs { - id = 9976; - option multicast_rpc_0 = 1; - option multicast_rpc_1 = 2; - option multicast_rpc_2 = 3; - option multicast_rpc_3 = 4; - option multicast_rpc_4 = 5; - option multicast_rpc_5 = 6; - option multicast_rpc_6 = 7; - option multicast_rpc_7 = 8; - option multicast_rpc_8 = 9; - option multicast_rpc_9 = 10; - option multicast_rpc_10 = 11; - option multicast_rpc_11 = 12; - option multicast_rpc_12 = 13; - option multicast_rpc_13 = 14; - option multicast_rpc_14 = 15; - option multicast_rpc_15 = 16; - option multicast_rpc_16 = 17; - option multicast_rpc_17 = 18; - option multicast_rpc_18 = 19; - option multicast_rpc_19 = 20; - option multicast_rpc_20 = 21; - option multicast_rpc_21 = 22; - option multicast_rpc_22 = 23; - option multicast_rpc_23 = 24; - option multicast_rpc_24 = 25; - option multicast_rpc_25 = 26; - option multicast_rpc_26 = 27; - option multicast_rpc_27 = 28; - option multicast_rpc_28 = 29; - option multicast_rpc_29 = 30; - option multicast_rpc_30 = 31; - option multicast_rpc_31 = 32; - uint64 last_sent_multicast_rpc_id = 33; - uint32 initially_present_multicast_rpc_count = 34; -} diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SpatialGDKEditorSchemaGeneratorTest.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SpatialGDKEditorSchemaGeneratorTest.cpp index f63f9e51c0..97ed2989ed 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SpatialGDKEditorSchemaGeneratorTest.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/SpatialGDKEditorSchemaGeneratorTest.cpp @@ -241,12 +241,7 @@ const TSet& AllTestClassesSet() return TestClassesSet; }; -#if ENGINE_MINOR_VERSION <= 23 FString ExpectedContentsDirectory = TEXT("SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema"); -#else -// Remove this once we fix 4.22 and 4.23: UNR-2988 -FString ExpectedContentsDirectory = TEXT("SpatialGDK/Source/SpatialGDKTests/SpatialGDKEditor/SpatialGDKEditorSchemaGenerator/ExpectedSchema_4.24"); -#endif TMap ExpectedContentsFilenames = { { "SpatialTypeActor", "SpatialTypeActor.schema" }, { "NonSpatialTypeActor", "NonSpatialTypeActor.schema" }, @@ -564,7 +559,7 @@ 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); @@ -770,7 +765,7 @@ SCHEMA_GENERATOR_TEST(GIVEN_multiple_classes_with_schema_generated_WHEN_schema_d SCHEMA_GENERATOR_TEST(GIVEN_schema_database_exists_WHEN_schema_database_deleted_THEN_no_schema_database_exists) { SchemaTestFixture Fixture; - + // GIVEN UClass* CurrentClass = ASpatialTypeActor::StaticClass(); TSet Classes = { CurrentClass }; @@ -852,7 +847,6 @@ SCHEMA_GENERATOR_TEST(GIVEN_source_and_destination_of_well_known_schema_files_WH "rpc_components.schema", "rpc_payload.schema", "server_worker.schema", - "singleton.schema", "spawndata.schema", "spawner.schema", "spatial_debugging.schema", @@ -912,7 +906,7 @@ SCHEMA_GENERATOR_TEST(GIVEN_source_and_destination_of_well_known_schema_files_WH SCHEMA_GENERATOR_TEST(GIVEN_multiple_classes_WHEN_getting_all_supported_classes_THEN_all_unsupported_classes_are_filtered) { SchemaTestFixture Fixture; - + // GIVEN const TArray& Classes = AllTestClassesArray(); @@ -958,7 +952,7 @@ SCHEMA_GENERATOR_TEST(GIVEN_multiple_classes_WHEN_getting_all_supported_classes_ SCHEMA_GENERATOR_TEST(GIVEN_3_level_names_WHEN_generating_schema_for_sublevels_THEN_generated_schema_contains_3_components_with_unique_names) { SchemaTestFixture Fixture; - + // GIVEN TMultiMap LevelNamesToPaths; LevelNamesToPaths.Add(TEXT("TestLevel0"), TEXT("/Game/Maps/FirstTestLevel0")); diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.cpp b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.cpp index d36e846d02..8d618fe388 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.cpp +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.cpp @@ -7,6 +7,7 @@ #include "SpatialGDKDefaultWorkerJsonGenerator.h" #include "SpatialGDKEditorSettings.h" #include "SpatialGDKServicesConstants.h" +#include "SpatialRuntimeLoadBalancingStrategies.h" #include "CoreMinimal.h" @@ -60,7 +61,7 @@ bool FStartDeployment::Update() const FString LaunchConfig = FPaths::Combine(FPaths::ConvertRelativePathToFull(FPaths::ProjectIntermediateDir()), AutomationLaunchConfig); const FString LaunchFlags = SpatialGDKSettings->GetSpatialOSCommandLineLaunchFlags(); const FString SnapshotName = SpatialGDKSettings->GetSpatialOSSnapshotToLoad(); - const FString RuntimeVersion = SpatialGDKSettings->GetSpatialOSRuntimeVersionForLocal(); + const FString RuntimeVersion = SpatialGDKSettings->GetSelectedRuntimeVariantVersion().GetVersionForLocal(); AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [LocalDeploymentManager, LaunchConfig, LaunchFlags, SnapshotName, RuntimeVersion] { @@ -74,13 +75,15 @@ bool FStartDeployment::Update() return; } - FSpatialLaunchConfigDescription LaunchConfigDescription(AutomationWorkerType); - LaunchConfigDescription.SetLevelEditorPlaySettingsWorkerTypes(); + FSpatialLaunchConfigDescription LaunchConfigDescription; - if (!GenerateDefaultLaunchConfig(LaunchConfig, &LaunchConfigDescription)) + FWorkerTypeLaunchSection Conf; + + if (!GenerateLaunchConfig(LaunchConfig, &LaunchConfigDescription, Conf)) { return; } + SetLevelEditorPlaySettingsWorkerType(Conf); if (LocalDeploymentManager->IsLocalDeploymentRunning()) { diff --git a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.h b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.h index bdcd5e103a..2706242aec 100644 --- a/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.h +++ b/SpatialGDK/Source/SpatialGDKTests/SpatialGDKServices/LocalDeploymentManager/LocalDeploymentManagerUtilities.h @@ -1,5 +1,7 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved +#pragma once + #include "Tests/TestDefinitions.h" #include "CoreMinimal.h" diff --git a/SpatialGDK/SpatialGDK.uplugin b/SpatialGDK/SpatialGDK.uplugin index b5c9b1c0bb..63a08fcc5b 100644 --- a/SpatialGDK/SpatialGDK.uplugin +++ b/SpatialGDK/SpatialGDK.uplugin @@ -1,7 +1,7 @@ { "FileVersion": 3, - "Version": 6, - "VersionName": "0.9.0", + "Version": 7, + "VersionName": "0.10.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", @@ -19,37 +19,59 @@ "Name": "SpatialGDK", "Type": "Runtime", "LoadingPhase": "PreDefault", - "WhitelistPlatforms": [ "Win64", "Linux", "Mac", "XboxOne", "PS4", "IOS", "Android" ] + "WhitelistPlatforms": [ + "Win64", + "Linux", + "Mac", + "XboxOne", + "PS4", + "IOS", + "Android" + ] }, { "Name": "SpatialGDKEditor", "Type": "Editor", "LoadingPhase": "Default", - "WhitelistPlatforms": [ "Win64", "Mac" ] + "WhitelistPlatforms": [ + "Win64", + "Mac" + ] }, { "Name": "SpatialGDKEditorToolbar", "Type": "Editor", "LoadingPhase": "Default", - "WhitelistPlatforms": [ "Win64", "Mac" ] + "WhitelistPlatforms": [ + "Win64", + "Mac" + ] }, { "Name": "SpatialGDKEditorCommandlet", "Type": "Editor", "LoadingPhase": "Default", - "WhitelistPlatforms": [ "Win64", "Mac" ] + "WhitelistPlatforms": [ + "Win64", + "Mac" + ] }, { "Name": "SpatialGDKServices", "Type": "Editor", "LoadingPhase": "PreDefault", - "WhitelistPlatforms": [ "Win64", "Mac" ] + "WhitelistPlatforms": [ + "Win64", + "Mac" + ] }, { "Name": "SpatialGDKTests", "Type": "Editor", "LoadingPhase": "PreLoadingScreen", - "WhitelistPlatforms": [ "Win64" ] + "WhitelistPlatforms": [ + "Win64" + ] } ], "Plugins": [ @@ -62,4 +84,4 @@ "Enabled": true } ] -} +} \ No newline at end of file diff --git a/ci/ReleaseTool.Tests/ReleaseTool.Tests.csproj b/ci/ReleaseTool.Tests/ReleaseTool.Tests.csproj new file mode 100644 index 0000000000..022c4f4a32 --- /dev/null +++ b/ci/ReleaseTool.Tests/ReleaseTool.Tests.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp2.1 + + false + + + + + + + + + + + + + + diff --git a/ci/ReleaseTool.Tests/UpdateChangelogTests.cs b/ci/ReleaseTool.Tests/UpdateChangelogTests.cs new file mode 100644 index 0000000000..9e54f8f4ce --- /dev/null +++ b/ci/ReleaseTool.Tests/UpdateChangelogTests.cs @@ -0,0 +1,169 @@ +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; + +namespace ReleaseTool.Tests +{ + public class UpdateChangelogTests + { + private static PrepCommand.Options OptionsNoPin = new PrepCommand.Options + { + Version = "test-version" + }; + + private static PrepCommand.Options OptionsWithPin = new PrepCommand.Options + { + Version = "test-version", + PinnedGdkVersion = "something" + }; + + [Test] + public void UpdateChangelog_does_nothing_if_header_is_already_there() + { + var changelog = new List + { + "## Unreleased", + "", + $"## `{OptionsNoPin.Version}` - soon :tm:", + "", + "### Breaking Changes", + "", + "- Made some breaking changes" + }; + + var previousCount = changelog.Count; + PrepCommand.UpdateChangeLog(changelog, OptionsNoPin); + Assert.AreEqual(previousCount, changelog.Count); + } + + [Test] + public void UpdateChangelog_should_insert_a_heading_after_unreleased() + { + var changelog = new List + { + "## Unreleased", + "", + "### Breaking Changes", + "", + "- Made some breaking changes" + }; + + PrepCommand.UpdateChangeLog(changelog, OptionsNoPin); + + Assert.IsTrue(changelog[2].StartsWith($"## `{OptionsNoPin.Version}` - ")); + } + + [Test] + public void UpdateChangelog_shouldnt_add_a_line_to_changed_section__if_no_gdk_pinned() + { + var changelog = new List + { + "## Unreleased", + "", + "### Breaking Changes", + "", + "- Made some breaking changes", + "", + "### Changed", + "", + "- Made some normal changes" + }; + + var expectedLineCount = changelog.Count + 2; // The release header and an extra newline. + + PrepCommand.UpdateChangeLog(changelog, OptionsNoPin); + + Assert.AreEqual(expectedLineCount, changelog.Count); + } + + [Test] + public void UpdateChangelog_should_add_a_line_to_changed_section_if_gdk_pinned() + { + var changelog = new List + { + "## Unreleased", + "", + "### Breaking Changes", + "", + "- Made some breaking changes", + "", + "### Changed", + "", + "- Made some normal changes" + }; + + var expectedLineCount = changelog.Count + 3; // The release header, an extra newline, and the changed entry. + var expectedLine = string.Format(PrepCommand.ChangeLogUpdateGdkTemplate, OptionsWithPin.Version); + + PrepCommand.UpdateChangeLog(changelog, OptionsWithPin); + + Assert.AreEqual(expectedLineCount, changelog.Count); + Assert.Contains(expectedLine, changelog); + } + + [Test] + public void UpdateChangelog_should_add_a_line_to_the_correct_changed_section_if_gdk_pinned() + { + var changelog = new List + { + "## Unreleased", + "", + "### Breaking Changes", + "", + "- Made some breaking changes", + "", + "### Changed", + "", + "- Made some normal changes", + "", + "## A Previous Release", + "", + "### Breaking Changes", + "", + "- Made some breaking changes", + "", + "### Changed", + "", + "- Made some normal changes" + }; + + var expectedLine = string.Format(PrepCommand.ChangeLogUpdateGdkTemplate, OptionsWithPin.Version); + PrepCommand.UpdateChangeLog(changelog, OptionsWithPin); + + var newReleaseSection = changelog.TakeWhile(line => line != "## A Previous Release").ToList(); + Assert.Contains(expectedLine, newReleaseSection); + } + + [Test] + public void UpdateChangelog_should_add_a_changed_section_line_if_not_there_if_gdk_pinned() + { + var changelog = new List + { + "## Unreleased", + "", + "### Breaking Changes", + "", + "- Made some breaking changes", + "", + "## A Previous Release", + "", + "### Breaking Changes", + "", + "- Made some breaking changes", + "", + "### Changed", + "", + "- Made some normal changes" + }; + + var expectedChangeHeader = "### Changed"; + var expectedChangeLine = string.Format(PrepCommand.ChangeLogUpdateGdkTemplate, OptionsWithPin.Version); + + PrepCommand.UpdateChangeLog(changelog, OptionsWithPin); + + var newReleaseSection = changelog.TakeWhile(line => line != "## A Previous Release").ToList(); + Assert.Contains(expectedChangeLine, newReleaseSection); + Assert.Contains(expectedChangeHeader, newReleaseSection); + } + } +} diff --git a/ci/ReleaseTool/AssemblyInfo.cs b/ci/ReleaseTool/AssemblyInfo.cs new file mode 100644 index 0000000000..3c49c38403 --- /dev/null +++ b/ci/ReleaseTool/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("ReleaseTool.Tests")] diff --git a/ci/ReleaseTool/BuildkiteAgent.cs b/ci/ReleaseTool/BuildkiteAgent.cs new file mode 100644 index 0000000000..16f6c363cf --- /dev/null +++ b/ci/ReleaseTool/BuildkiteAgent.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using Medallion.Shell; +using NLog; + +namespace ReleaseTool +{ + public static class BuildkiteAgent + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + public static string GetMetadata(string key) + { + var commandResult = Command + .Run("buildkite-agent", "meta-data", "get", key) + .Result; + + if (commandResult.Success) + { + return commandResult.StandardOutput; + } + + throw new MetadataNotFoundException(key, commandResult.StandardError); + } + + public static void SetMetaData(string key, string value) + { + var commandResult = Command + .Run("buildkite-agent", "meta-data", "set", key, value) + .Result; + + if (!commandResult.Success) + { + throw new MetadataNotFoundException(key, commandResult.StandardError); + } + } + + public static void Annotate(AnnotationLevel level, string context, string message, bool append = false) + { + string style; + switch (level) { + case AnnotationLevel.Info: + style = "info"; + break; + case AnnotationLevel.Warning: + style = "warning"; + break; + case AnnotationLevel.Error: + style = "error"; + break; + case AnnotationLevel.Success: + style = "success"; + break; + default: + throw new ArgumentOutOfRangeException(nameof(level)); + } + + var args = new List + { + "annotate", message, + "--style", style, + "--context", context + }; + + if (append) + { + args.Add("--append"); + } + + Logger.Debug($"Annotating build with style '{style}', context '{context}':\n{message}"); + + var commandResult = Command + .Run("buildkite-agent", args) + .Result; + + if (!commandResult.Success) + { + throw new Exception($"Failed to annotate build\nStdout: {commandResult.StandardOutput}\nStderr: {commandResult.StandardError}"); + } + } + } + + public class MetadataNotFoundException : Exception + { + public MetadataNotFoundException(string key, string stderr) + : base($"Could not find meta-data associated with {key}.\nRaw stderr: {stderr}") + { + } + } + + public class CouldNotSetMetadataException : Exception + { + public CouldNotSetMetadataException(string key, string value, string stderr) + : base($"Could not set meta-data with {key} and value {value}.\nRaw stderr: {stderr}") + { + } + } + + public enum AnnotationLevel + { + Success = 0, + Info = 1, + Warning = 2, + Error = 3, + } +} diff --git a/ci/ReleaseTool/Common.cs b/ci/ReleaseTool/Common.cs new file mode 100644 index 0000000000..79dd15d5af --- /dev/null +++ b/ci/ReleaseTool/Common.cs @@ -0,0 +1,59 @@ +using System; +using System.IO; +using System.Linq; + +namespace ReleaseTool +{ + internal static class Common + { + public const string RepoUrlTemplate = "git@github.com:{0}/{1}.git"; + + public static void VerifySemanticVersioningFormat(string version) + { + var majorMinorPatch = version.Split('.'); + var hasSemanticVersion = majorMinorPatch.Length == 3 && Enumerable.All(majorMinorPatch, s => int.TryParse(s, out _)); + + if (!hasSemanticVersion) + { + throw new ArgumentException($"The provided version '{version}' should comply " + + $"with the Semantic Versioning Specification, but does not."); + } + } + + public static string ReplaceHomePath(string originalPath) + { + if (!originalPath.StartsWith("~")) + { + return originalPath; + } + + var relativePath = new Uri(originalPath.Substring(2), UriKind.Relative); + var homeDirectory = AppendDirectorySeparator(Environment.GetFolderPath( + Environment.SpecialFolder.UserProfile)); + var homePath = new Uri(homeDirectory, UriKind.Absolute); + + return new Uri(homePath, relativePath).AbsolutePath; + } + + /** + * This is required as URIs treat paths that do not have a trailing slash as a file rather than a directory. + */ + private static string AppendDirectorySeparator(string originalPath) + { + var directorySeparator = Path.DirectorySeparatorChar.ToString(); + var altDirectorySeparator = Path.AltDirectorySeparatorChar.ToString(); + + if (originalPath.EndsWith(directorySeparator) || originalPath.EndsWith(altDirectorySeparator)) + { + return originalPath; + } + + if (originalPath.Contains(altDirectorySeparator)) + { + return originalPath + altDirectorySeparator; + } + + return originalPath + directorySeparator; + } + } +} diff --git a/ci/ReleaseTool/EntryPoint.cs b/ci/ReleaseTool/EntryPoint.cs new file mode 100644 index 0000000000..2ae7308679 --- /dev/null +++ b/ci/ReleaseTool/EntryPoint.cs @@ -0,0 +1,35 @@ +using CommandLine; +using NLog; + +namespace ReleaseTool +{ + internal static class EntryPoint + { + private static int Main(string[] args) + { + ConfigureLogger(); + + return Parser.Default.ParseArguments(args) + .MapResult( + (PrepCommand.Options options) => new PrepCommand(options).Run(), + (ReleaseCommand.Options options) => new ReleaseCommand(options).Run(), + errors => 1); + } + + private static void ConfigureLogger() + { + var config = new NLog.Config.LoggingConfiguration(); + + var logfile = new NLog.Targets.FileTarget("logfile") + { + FileName = "release-tool.log" + }; + var logconsole = new NLog.Targets.ConsoleTarget("logconsole"); + + config.AddRule(LogLevel.Info, LogLevel.Fatal, logconsole); + config.AddRule(LogLevel.Debug, LogLevel.Fatal, logfile); + + LogManager.Configuration = config; + } + } +} diff --git a/ci/ReleaseTool/GitClient.cs b/ci/ReleaseTool/GitClient.cs new file mode 100644 index 0000000000..f0397896da --- /dev/null +++ b/ci/ReleaseTool/GitClient.cs @@ -0,0 +1,154 @@ +using System; +using System.Diagnostics; +using System.IO; +using LibGit2Sharp; + +namespace ReleaseTool +{ + /// + /// This class provides helper methods for git. It uses a hybrid approach using both LibGit2Sharp for most methods, + /// but uses Processes to invoke remote methods (pull, fetch, push). This is because LibGit2Sharp does not support + /// ssh authentication yet! + /// + internal class GitClient : IDisposable + { + private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + + private const string DefaultRemote = "origin"; + + private const string GitCommand = "git"; + private const string ForcePushArgumentsTemplate = "push -f {0} HEAD:refs/heads/{1}"; + private const string FetchArguments = "fetch {0}"; + private const string CloneArgumentsTemplate = "clone {0} {1}"; + private const string AddRemoteArgumentsTemplate = "remote add {0} {1}"; + + private const string RemoteBranchRefTemplate = "{1}/{0}"; + + public string RepositoryPath { get; } + private readonly IRepository repo; + + public static GitClient FromRemote(string remoteUrl) + { + var repositoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(repositoryPath); + Clone(remoteUrl, repositoryPath); + + return new GitClient(repositoryPath); + } + + private GitClient(string repositoryPath) + { + RepositoryPath = repositoryPath; + repo = new Repository($"{repositoryPath}/.git/"); + } + + public void Dispose() + { + repo?.Dispose(); + } + + public bool LocalBranchExists(string branch) + { + return repo.Branches[branch] != null; + } + + public void CheckoutRemoteBranch(string branch, string remote = null) + { + var branchRef = string.Format(RemoteBranchRefTemplate, branch, remote ?? DefaultRemote); + Logger.Info("Checking out branch... {0}", branchRef); + Commands.Checkout(repo, branchRef); + } + + public void CheckoutLocalBranch(string branch) + { + Logger.Info("Checking out branch... {0}", branch); + Commands.Checkout(repo, branch); + } + + public void StageFile(string filePath) + { + Logger.Info("Staging... {0}", filePath); + + if (!Path.IsPathRooted(filePath)) + { + filePath = Path.Combine(RepositoryPath, filePath); + } + + Commands.Stage(repo, filePath); + } + + public void Commit(string commitMessage) + { + Logger.Info("Committing..."); + + var signature = repo.Config.BuildSignature(DateTimeOffset.Now); + repo.Commit(commitMessage, signature, signature, new CommitOptions { AllowEmptyCommit = true }); + } + + public void Fetch(string remote = null) + { + Logger.Info("Fetching from remote..."); + + RunGitCommand("fetch", string.Format(FetchArguments, remote ?? DefaultRemote), RepositoryPath); + + } + + public void ForcePush(string remoteBranchName) + { + Logger.Info("Pushing to remote..."); + + var pushArguments = string.Format(ForcePushArgumentsTemplate, DefaultRemote, remoteBranchName); + + RunGitCommand("push branch", pushArguments, RepositoryPath); + } + + public void AddRemote(string name, string remoteUrl) + { + Logger.Info($"Adding remote {remoteUrl} as {name}..."); + RunGitCommand("add remote", string.Format(AddRemoteArgumentsTemplate, name, remoteUrl), RepositoryPath); + } + + private static void Clone(string remoteUrl, string targetDirectory) + { + Logger.Info($"Cloning {remoteUrl} into {targetDirectory}..."); + RunGitCommand("clone repository", + string.Format(CloneArgumentsTemplate, remoteUrl, $"\"{targetDirectory}\"")); + } + + private static void RunGitCommand(string description, string arguments, string workingDir = null) + { + Logger.Debug("Attempting to {0}. Running command [{1} {2}]", description, + GitCommand, arguments); + + var procInfo = new ProcessStartInfo(GitCommand, arguments) + { + UseShellExecute = false + }; + + if (workingDir != null) + { + procInfo.WorkingDirectory = workingDir; + } + + using (var process = Process.Start(procInfo)) + { + if (process != null) + { + process.WaitForExit(); + + if (process.ExitCode == 0) + { + return; + } + } + } + + throw new InvalidOperationException($"Failed to {description}."); + } + + public Commit GetHeadCommit() + { + return repo.Head.Tip; + } + } +} diff --git a/ci/ReleaseTool/GitHubClient.cs b/ci/ReleaseTool/GitHubClient.cs new file mode 100644 index 0000000000..6bf95d44cf --- /dev/null +++ b/ci/ReleaseTool/GitHubClient.cs @@ -0,0 +1,173 @@ +using CommandLine; +using Octokit; +using System; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using OctoClient = Octokit.GitHubClient; + +namespace ReleaseTool +{ + /// + /// Wrapper around Octokit's GitHubClient, which provides a synchronous interface to access GitHub. + /// + internal class GitHubClient + { + public enum MergeState + { + NotReadyToMerge, + ReadyToMerge, + AlreadyMerged + } + + private const string DefaultKeyFileLocation = "~/.ssh/github.token"; + + private const string RemoteUrlRegex = @"(?:https:\/\/github\.com\/|git@github\.com\:)([^\/\.]*)\/([^\/\.]*)\.git"; + + private static readonly ProductHeaderValue ProductHeader = new ProductHeaderValue("improbable-unreal-gdk-release-tool"); + + public interface IGitHubOptions + { + [Option("github-key-file", Default = DefaultKeyFileLocation, HelpText = "The location of the github token file.")] + string GitHubTokenFile { get; set; } + + [Option("github-key", HelpText = "The github API token. If this is set, this will override the " + + "github-key-file.")] + string GitHubToken { get; set; } + } + + private readonly IGitHubOptions options; + private readonly OctoClient octoClient; + + public GitHubClient(IGitHubOptions options) + { + octoClient = new OctoClient(ProductHeader); + this.options = options; + LoadCredentials(); + + } + + public Repository GetRepositoryFromUrl(string url) + { + var matches = Regex.Match(url, RemoteUrlRegex); + + if (!matches.Success) + { + throw new ArgumentException($"Failed to parse remote {url}. Not a valid github repository."); + } + + var owner = matches.Groups[1].Value; + var repo = matches.Groups[2].Value; + + var repositoryTask = octoClient.Repository.Get(owner, repo); + + return repositoryTask.Result; + } + + public bool TryGetPullRequest(Repository repository, string githubOrg, string branchFrom, string branchTo, out PullRequest request) + { + var pullRequestRequest = new PullRequestRequest + { + State = ItemStateFilter.Open, + Base = branchTo, + Head = $"{githubOrg}:{branchFrom}" + }; + + var results = octoClient.PullRequest + .GetAllForRepository(repository.Id, pullRequestRequest) + .Result; + + if (results.Count == 0) + { + request = null; + return false; + } + + request = results[0]; + return true; + } + + public PullRequest CreatePullRequest(Repository repository, string branchFrom, string branchTo, string pullRequestTitle, string body) + { + var newPullRequest = new NewPullRequest(pullRequestTitle, branchFrom, branchTo) { Body = body }; + + var createPullRequestTask = octoClient.PullRequest.Create(repository.Id, newPullRequest); + + return createPullRequestTask.Result; + } + + public MergeState GetMergeState(Repository repository, int pullRequestId) + { + var pr = octoClient.PullRequest.Get(repository.Id, pullRequestId).Result; + + if (pr.Merged) + { + return MergeState.AlreadyMerged; + } + + if (pr.Mergeable.Equals(true) && pr.MergeableState == MergeableState.Clean) + { + return MergeState.ReadyToMerge; + } + + else + { + return MergeState.NotReadyToMerge; + } + } + + public PullRequestMerge MergePullRequest(Repository repository, int pullRequestId, PullRequestMergeMethod mergeMethod) + { + var mergePullRequest = new MergePullRequest + { + MergeMethod = mergeMethod + }; + + var mergePullRequestTask = octoClient.PullRequest.Merge(repository.Id, pullRequestId, mergePullRequest); + + return mergePullRequestTask.Result; + } + + public void DeleteBranch(Repository repository, string branch) + { + octoClient.Git.Reference.Delete(repository.Id, $"refs/heads/{branch}").Wait(); + } + + public Release CreateDraftRelease(Repository repository, string tag, string body, string name, string commitish) + { + var releaseTask = octoClient.Repository.Release.Create(repository.Id, new NewRelease(tag) + { + Body = body, + Draft = true, + Name = name, + TargetCommitish = commitish + }); + + return releaseTask.Result; + } + + public ReleaseAsset AddAssetToRelease(Release release, string fileName, string contentType, Stream data) + { + var uploadAssetTask = octoClient.Repository.Release.UploadAsset(release, new ReleaseAssetUpload(fileName, contentType, data, null)); + return uploadAssetTask.Result; + } + + private void LoadCredentials() + { + if (!string.IsNullOrEmpty(options.GitHubToken)) + { + octoClient.Credentials = new Credentials(options.GitHubToken); + } + else + { + if (!File.Exists(options.GitHubTokenFile)) + { + throw new ArgumentException($"Failed to get GitHub Token as the file specified at {options.GitHubTokenFile} does not exist."); + } + + octoClient.Credentials = new Credentials(File.ReadAllText( + Common.ReplaceHomePath(options.GitHubTokenFile))); + } + } + } +} diff --git a/ci/ReleaseTool/PrepCommand.cs b/ci/ReleaseTool/PrepCommand.cs new file mode 100644 index 0000000000..959ac55bc8 --- /dev/null +++ b/ci/ReleaseTool/PrepCommand.cs @@ -0,0 +1,360 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using CommandLine; +using Newtonsoft.Json.Linq; +using NLog; + +namespace ReleaseTool +{ + /// + /// Runs the steps required to cut release candidate branches in all repos: + /// UnrealGDK, UnrealGDKExampleProject, UnrealEngine, UnrealGDKEngineNetTest, UnrealGDKTestGyms and TestGymBuildKite. + /// + /// * Checks out the source branch, which defaults to 4.xx-SpatialOSUnrealGDK in UnrealEngine and master in all other repos. + /// * IF the release branch does not already exits, creates it from the source branch. + /// * Makes repo-specific changes for prepping the release (e.g. updating version files, formatting the CHANGELOG). + /// * Commits these changes to release candaidate branches. + /// * Pushes the release candaidate branches to origin. + /// * Opens PRs to merge the release candaidate branches into the release branches. + /// + + 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"; + + // Names of the version files that live in the UnrealEngine repository. + private const string UnrealGDKVersionFile = "UnrealGDKVersion.txt"; + private const string UnrealGDKExampleProjectVersionFile = "UnrealGDKExampleProjectVersion.txt"; + + // Plugin file configuration. + private const string pluginFileName = "SpatialGDK.uplugin"; + private const string VersionKey = "Version"; + private const string VersionNameKey = "VersionName"; + + // Changelog file configuration + private const string ChangeLogFilename = "CHANGELOG.md"; + private const string ChangeLogReleaseHeadingTemplate = "## [`{0}`] - {1:yyyy-MM-dd}"; + + [Verb("prep", HelpText = "Prep a release candidate branch.")] + public class Options : GitHubClient.IGitHubOptions + { + [Value(0, MetaName = "version", HelpText = "The release version that is being cut.", Required = true)] + public string Version { get; set; } + + [Option("source-branch", HelpText = "The source branch name from which we are cutting the candidate.", Required = true)] + public string SourceBranch { get; set; } + + [Option("candidate-branch", HelpText = "The candidate branch name.", Required = true)] + public string CandidateBranch { get; set; } + + [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; } + + [Option("engine-versions", HelpText = "An array containing every engine version source branch.", Required = false)] + public string EngineVersions {get;set;} + + #region IBuildkiteOptions implementation + + public string MetadataFilePath { get; set; } + + #endregion + + #region IGithubOptions implementation + + public string GitHubTokenFile { get; set; } + + public string GitHubToken { get; set; } + + #endregion + } + + private readonly Options options; + + public PrepCommand(Options options) + { + this.options = options; + } + + /* + * This tool is designed to be used with a robot Github account. When we prep a release: + * 1. Clones the source repo. + * 2. Checks out the source branch, which defaults to 4.xx-SpatialOSUnrealGDK in UnrealEngine and master in all other repos. + * 3. Makes repo-specific changes for prepping the release (e.g. updating version files, formatting the CHANGELOG). + * 4. Commit changes and push them to a remote candidate branch. + * 5. IF the release branch does not exist, creates it from the source branch and pushes it to the remote. + * 6. Opens a PR for merging the RC branch into the release branch. + */ + public int Run() + { + Common.VerifySemanticVersioningFormat(options.Version); + + var remoteUrl = string.Format(Common.RepoUrlTemplate, options.GithubOrgName, options.GitRepoName); + + try + { + var gitHubClient = new GitHubClient(options); + // 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); + + // 3. Makes repo-specific changes for prepping the release (e.g. updating version files, formatting the CHANGELOG). + switch (options.GitRepoName) + { + case "UnrealGDK": + UpdateChangeLog(ChangeLogFilename, options, gitClient); + UpdatePluginFile(pluginFileName, gitClient); + + var engineCandidateBranches = options.EngineVersions.Split(" ") + .Select(engineVersion => $"HEAD {engineVersion.Trim()}-{options.Version}-rc") + .ToList(); + UpdateUnrealEngineVersionFile(engineCandidateBranches, gitClient); + break; + case "UnrealEngine": + UpdateVersionFile(gitClient, $"{options.Version}-rc", UnrealGDKVersionFile); + UpdateVersionFile(gitClient, $"{options.Version}-rc", UnrealGDKExampleProjectVersionFile); + break; + case "UnrealGDKExampleProject": + UpdateVersionFile(gitClient, $"{options.Version}-rc", UnrealGDKVersionFile); + break; + case "UnrealGDKTestGyms": + UpdateVersionFile(gitClient, $"{options.Version}-rc", UnrealGDKVersionFile); + break; + case "UnrealGDKEngineNetTest": + UpdateVersionFile(gitClient, $"{options.Version}-rc", UnrealGDKVersionFile); + break; + case "TestGymBuildKite": + UpdateVersionFile(gitClient, $"{options.Version}-rc", UnrealGDKVersionFile); + break; + } + + // 4. Commit changes and push them to a remote candidate branch. + gitClient.Commit(string.Format(CandidateCommitMessageTemplate, options.Version)); + gitClient.ForcePush(options.CandidateBranch); + + // 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); + } + + // 6. Opens a PR for merging the RC branch into the release branch. + var gitHubRepo = gitHubClient.GetRepositoryFromUrl(remoteUrl); + var githubOrg = options.GithubOrgName; + var branchFrom = options.CandidateBranch; + var branchTo = options.ReleaseBranch; + + // Only open a PR if one does not exist yet. + if (!gitHubClient.TryGetPullRequest(gitHubRepo, githubOrg, branchFrom, branchTo, out var pullRequest)) + { + Logger.Info("No PR exists. Attempting to open a new PR"); + pullRequest = gitHubClient.CreatePullRequest(gitHubRepo, + branchFrom, + branchTo, + string.Format(PullRequestTemplate, options.Version), + GetPullRequestBody(options.GitRepoName, options.CandidateBranch, options.ReleaseBranch)); + } + + BuildkiteAgent.SetMetaData($"{options.GitRepoName}-{options.SourceBranch}-pr-url", pullRequest.HtmlUrl); + + var prAnnotation = string.Format(prAnnotationTemplate, + pullRequest.HtmlUrl, options.GitRepoName, options.CandidateBranch, options.ReleaseBranch); + 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); + } + } + catch (Exception e) + { + Logger.Error(e, "ERROR: Unable to prep release candidate branch. Error: {0}", e); + return 1; + } + + return 0; + } + + internal static void UpdateChangeLog(string ChangeLogFilePath, Options options, GitClient gitClient) + { + using (new WorkingDirectoryScope(gitClient.RepositoryPath)) + { + if (File.Exists(ChangeLogFilePath)) + { + Logger.Info("Updating {0}...", ChangeLogFilePath); + + var changelog = File.ReadAllLines(ChangeLogFilePath).ToList(); + + // If we already have a changelog entry for this release. Skip this step. + if (changelog.Any(line => IsMarkdownHeading(line, 2, $"[`{options.Version}`] - "))) + { + Logger.Info($"Changelog already has release version {options.Version}. Skipping..", ChangeLogFilePath); + return; + } + + // First add the new release heading under the "## Unreleased" one. + // Assuming that this is the first heading. + var unreleasedIndex = changelog.FindIndex(line => IsMarkdownHeading(line, 2)); + var releaseHeading = string.Format(ChangeLogReleaseHeadingTemplate, options.Version, + DateTime.Now); + + changelog.InsertRange(unreleasedIndex + 1, new[] + { + string.Empty, + releaseHeading + }); + + File.WriteAllLines(ChangeLogFilePath, changelog); + gitClient.StageFile(ChangeLogFilePath); + } + } + } + + private static void UpdateUnrealEngineVersionFile(List versions, GitClient client) + { + const string unrealEngineVersionFile = "ci/unreal-engine.version"; + + using (new WorkingDirectoryScope(client.RepositoryPath)) + { + File.WriteAllLines(unrealEngineVersionFile, versions); + client.StageFile(unrealEngineVersionFile); + } + } + + 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 void UpdateVersionFile(GitClient gitClient, string fileContents, string relativeFilePath) + { + using (new WorkingDirectoryScope(gitClient.RepositoryPath)) + { + Logger.Info("Updating contents of version file '{0}' to '{1}'...", relativeFilePath, fileContents); + + if (!File.Exists(relativeFilePath)) + { + throw new InvalidOperationException("Could not update the version file as the file " + + $"'{relativeFilePath}' does not exist."); + } + + File.WriteAllText(relativeFilePath, $"{fileContents}"); + + gitClient.StageFile(relativeFilePath); + } + } + + private void UpdatePluginFile(string pluginFileName, GitClient gitClient) + { + using (new WorkingDirectoryScope(gitClient.RepositoryPath)) + { + var pluginFilePath = Directory.GetFiles(".", pluginFileName, SearchOption.AllDirectories).First(); + + Logger.Info("Updating {0}...", pluginFilePath); + + JObject jsonObject; + using (var streamReader = new StreamReader(pluginFilePath)) + { + jsonObject = JObject.Parse(streamReader.ReadToEnd()); + + if (jsonObject.ContainsKey(VersionKey) && jsonObject.ContainsKey(VersionNameKey)) + { + var oldVersion = (string)jsonObject[VersionNameKey]; + if (ShouldIncrementPluginVersion(oldVersion, options.Version)) + { + jsonObject[VersionKey] = ((int)jsonObject[VersionKey] + 1); + } + + // Update the version name to the new one + jsonObject[VersionNameKey] = options.Version; + } + else + { + throw new InvalidOperationException($"Could not update the plugin file at '{pluginFilePath}', " + + $"because at least one of the two expected keys '{VersionKey}' and '{VersionNameKey}' " + + $"could not be found."); + } + } + + File.WriteAllText(pluginFilePath, jsonObject.ToString()); + + gitClient.StageFile(pluginFilePath); + } + } + + private bool ShouldIncrementPluginVersion(string oldVersionName, string newVersionName) + { + var oldMajorMinorVersions = oldVersionName.Split('.').Take(2).Select(s => int.Parse(s)); + var newMajorMinorVersions = newVersionName.Split('.').Take(2).Select(s => int.Parse(s)); + return Enumerable.Any(Enumerable.Zip(oldMajorMinorVersions, newMajorMinorVersions, (o, n) => o < n)); + } + + private string GetPullRequestBody(string repoName, string candidateBranch, string releaseBranch) + { + // If repoName is UnrealGDK do nothing, otherwise get the UnrealGDK-pr-url + var unrealGdkSourceBranch = repoName == "UnrealGDK" ? "" : BuildkiteAgent.GetMetadata("gdk-source-branch"); + var unrealGdkPrUrl = repoName == "UnrealGDK" ? "" : BuildkiteAgent.GetMetadata($"UnrealGDK-{unrealGdkSourceBranch}-pr-url"); + switch (repoName) + { + case "UnrealGDK": + return $@"#### Description +- This PR merges `{candidateBranch}` into `{releaseBranch}`. +- It was created by the [unrealgdk-release](https://buildkite.com/improbable/unrealgdk-release) Buildkite pipeline. +- Your human labour is now required to unblock the pipeline to resume the release. + +#### Next Steps +- [ ] **Release Sheriff** - Delegate the tasks below. +- [ ] **Release Sheriff** - Once the Build & upload all UnrealEngine release candidates step completes, click the [run all tests](https://buildkite.com/improbable/unrealgdk-release) button. +- [ ] **Tech writers** - Review and translate [CHANGELOG.md](https://github.com/spatialos/UnrealGDK/blob/{candidateBranch}/CHANGELOG.md). Merge the translation and any edits into `{candidateBranch}`. +- [ ] **QA** - Create and complete one [component release](https://improbabletest.testrail.io/index.php?/suites/view/72) test run per Unreal Engine version you're releasing. +- [ ] **Release Sheriff** - If any blocking defects are discovered, merge fixes into release candidate branches. +- [ ] **Release Sheriff** - Get approving reviews on *all* release candidate PRs. +- [ ] **Release Sheriff** - When the above tasks are complete, unblock the [pipeline](https://buildkite.com/improbable/unrealgdk-release). This action will merge all release candidates into their respective release branches and create draft GitHub releases that you must then publish. +"; + case "UnrealGDKExampleProject": + return $@"#### Description +- This PR merges `{candidateBranch}` into `{releaseBranch}`. +- It corresponds to {unrealGdkPrUrl}, where you can find more information about this release."; + case "UnrealGDKTestGyms": + return $@"#### Description +- This PR merges `{candidateBranch}` into `{releaseBranch}`. +- It corresponds to {unrealGdkPrUrl}, where you can find more information about this release."; + case "UnrealGDKEngineNetTest": + return $@"#### Description +- This PR merges `{candidateBranch}` into `{releaseBranch}`. +- It corresponds to {unrealGdkPrUrl}, where you can find more information about this release."; + case "TestGymBuildKite": + return $@"#### Description +- This PR merges `{candidateBranch}` into `{releaseBranch}`. +- It corresponds to {unrealGdkPrUrl}, where you can find more information about this release."; + case "UnrealEngine": + return $@"#### Description +- This PR merges `{candidateBranch}` into `{releaseBranch}`. +- It corresponds to {unrealGdkPrUrl}, where you can find more information about this release."; + default: + throw new ArgumentException($"No PR body template found for repo {repoName}"); + } + } + } +} diff --git a/ci/ReleaseTool/ReleaseCommand.cs b/ci/ReleaseTool/ReleaseCommand.cs new file mode 100644 index 0000000000..66f1d4eb80 --- /dev/null +++ b/ci/ReleaseTool/ReleaseCommand.cs @@ -0,0 +1,621 @@ +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 ReleaseCommand + { + private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + + private const string PullRequestNameTemplate = "Release {0} - Merge {1} into {2}"; + private const string pullRequestBody = "Merging {0} back into {1}. Please manually resolve merge conflicts."; + + private const string releaseAnnotationTemplate = "* Successfully created a [draft release]({0}) " + + "in the repo `{1}`. Your human labour is now required to publish it.\n"; + 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 merge these PRs.\n"; + + // Changelog file configuration + private const string ChangeLogFilename = "CHANGELOG.md"; + private const string CandidateCommitMessageTemplate = "{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)] + public string PullRequestUrl { get; set; } + + [Option("source-branch", HelpText = "The source branch name from which we are cutting the candidate.", Required = true)] + public string SourceBranch { get; set; } + + [Option("candidate-branch", HelpText = "The candidate branch name.", Required = true)] + public string CandidateBranch { get; set; } + + [Option("release-branch", HelpText = "The name of the branch into which we are merging the candidate.", Required = true)] + public string ReleaseBranch { get; set; } + + [Option("github-organization", HelpText = "The Github Organization that contains the targeted repository.", Required = true)] + public string GithubOrgName { get; set; } + + [Option("engine-versions", HelpText = "An array containing every engine version source branch.", Required = false)] + public string EngineVersions {get;set;} + + public string GitHubTokenFile { get; set; } + + public string GitHubToken { get; set; } + + public string MetadataFilePath { get; set; } + } + + private readonly Options options; + + public ReleaseCommand(Options options) + { + this.options = options; + } + + /* + * This tool is designed to execute most of the git operations required when releasing: + * 1. Merge the RC PR into the release branch. + * 2. Draft a GitHub release using the changelog notes. + * 3. Open a PR from the release-branch into source-branch. + */ + public int Run() + { + Common.VerifySemanticVersioningFormat(options.Version); + var (repoName, pullRequestId) = ExtractPullRequestInfo(options.PullRequestUrl); + var gitHubClient = new GitHubClient(options); + var repoUrl = string.Format(Common.RepoUrlTemplate, options.GithubOrgName, repoName); + 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) + { + 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; + + 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); + } + + 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 from release branch into source branch."); + Logger.Info("Merge hash: {0}", pullRequest.MergeCommitSha); + + return 0; + } + + var remoteUrl = string.Format(Common.RepoUrlTemplate, options.GithubOrgName, repoName); + 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); + + // 3. Makes repo-specific changes for prepping the release (e.g. updating version files, formatting the CHANGELOG). + switch (repoName) + { + case "UnrealEngine": + UpdateVersionFile(gitClient, options.Version, UnrealGDKVersionFile); + UpdateVersionFile(gitClient, options.Version, UnrealGDKExampleProjectVersionFile); + break; + case "UnrealGDK": + UpdateChangeLog(ChangeLogFilename, options, gitClient); + + var releaseHashes = options.EngineVersions.Split(" ") + .Select(version => $"{version.Trim()}-release") + .Select(BuildkiteAgent.GetMetadata) + .Select(hash => $"UnrealEngine-{hash}") + .ToList(); + + 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; + } + + // 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. + var startTime = DateTime.Now; + while (true) + { + if (DateTime.Now.Subtract(startTime) > TimeSpan.FromHours(12)) + { + throw new Exception($"Exceeded timeout waiting for PR to be mergeable: {options.PullRequestUrl}"); + } + + if (gitHubClient.GetMergeState(gitHubRepo, pullRequestId) == GitHubClient.MergeState.ReadyToMerge) + { + Logger.Info($"{options.PullRequestUrl} is mergeable. Attempting to merge."); + break; + } + + Logger.Info($"{options.PullRequestUrl} is not in a mergeable state, will query mergeability again in one minute."); + Thread.Sleep(TimeSpan.FromMinutes(1)); + } + + PullRequestMerge mergeResult = null; + while (true) + { + // Merge into release + try + { + mergeResult = gitHubClient.MergePullRequest(gitHubRepo, pullRequestId, PullRequestMergeMethod.Merge); + } + 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)); + } + else + { + break; + } + } + + 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); + var release = CreateRelease(gitHubClient, gitHubRepo, gitClient, repoName); + + BuildkiteAgent.Annotate(AnnotationLevel.Info, "draft-releases", + string.Format(releaseAnnotationTemplate, release.HtmlUrl, repoName), true); + + 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); + } + + 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); + return 1; + } + + return 0; + } + internal static void UpdateChangeLog(string ChangeLogFilePath, Options options, GitClient gitClient) + { + 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); + } + } + } + + private Release CreateRelease(GitHubClient gitHubClient, Repository gitHubRepo, GitClient gitClient, string repoName) + { + var headCommit = gitClient.GetHeadCommit().Sha; + + var engineVersion = options.SourceBranch.Trim(); + + string name; + string releaseBody; + + switch (repoName) + { + case "UnrealGDK": + string changelog; + using (new WorkingDirectoryScope(gitClient.RepositoryPath)) + { + changelog = GetReleaseNotesFromChangeLog(); + } + name = $"GDK for Unreal Release {options.Version}"; + releaseBody = +$@"The release notes are published in both English and Chinese. To view the Chinese version, scroll down a bit for details. Thanks! + +Release notes 将同时提供中英文。要浏览中文版本,向下滚动页面查看详情。感谢! + +# English version + +**Unreal GDK version {options.Version} is go!** + +## Release Notes + +* **Release sheriff:** Your human labour is required to populate this section with the headline new features and breaking changes from the CHANGELOG. + +## 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). + +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. + +You can read the full release notes [here](https://github.com/spatialos/UnrealGDK/blob/release/CHANGELOG.md) or below. + +Join the community on our [forums](https://forums.improbable.io/), or on [Discord](https://discordapp.com/invite/vAT7RSU). + +Happy developing, + +*The GDK team* + +--- + +{changelog} + +# 中文版本 + +**[虚幻引擎开发套件 (GDK) {options.Version} 版本已发布!** + +## Release Notes + +* **Tech writer:** Your human labour is required to translate the above and include it here. + +"; + break; + case "UnrealEngine": + name = $"{engineVersion}-{options.Version}"; + releaseBody = +$@"Unreal GDK version {options.Version} is go! + +* 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). + +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. + +You can read the full release notes [here](https://github.com/spatialos/UnrealGDK/blob/release/CHANGELOG.md). + +Join the community on our [forums](https://forums.improbable.io/), or on [Discord](https://discordapp.com/invite/vAT7RSU). + +Happy developing!
+GDK team"; + break; + case "UnrealGDKTestGyms": + name = $"{options.Version}"; + releaseBody = +$@"Unreal GDK version {options.Version} is go! + +* 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). +* 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. + +You can read the full release notes [here](https://github.com/spatialos/UnrealGDK/blob/release/CHANGELOG.md). + +Join the community on our [forums](https://forums.improbable.io/), or on [Discord](https://discordapp.com/invite/vAT7RSU). + +Happy developing!
+GDK team"; + break; + case "UnrealGDKEngineNetTest": + name = $"{options.Version}"; + releaseBody = +$@"Unreal GDK version {options.Version} is go! + +* 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). +* 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. + +You can read the full release notes [here](https://github.com/spatialos/UnrealGDK/blob/release/CHANGELOG.md). + +Join the community on our [forums](https://forums.improbable.io/), or on [Discord](https://discordapp.com/invite/vAT7RSU). + +Happy developing!
+GDK team"; + break; + case "TestGymBuildKite": + name = $"{options.Version}"; + releaseBody = +$@"Unreal GDK version {options.Version} is go! + +* 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). +* 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. + +You can read the full release notes [here](https://github.com/spatialos/UnrealGDK/blob/release/CHANGELOG.md). + +Join the community on our [forums](https://forums.improbable.io/), or on [Discord](https://discordapp.com/invite/vAT7RSU). + +Happy developing!
+GDK team"; + break; + case "UnrealGDKExampleProject": + name = $"{options.Version}"; + releaseBody = +$@"Unreal GDK version {options.Version} is go! + +* This UnrealGDKExampleProject version corresponds to GDK version: [{options.Version}](https://github.com/spatialos/UnrealGDK/releases). +* 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. + +You can read the full release notes [here](https://github.com/spatialos/UnrealGDK/blob/release/CHANGELOG.md). + +Join the community on our [forums](https://forums.improbable.io/), or on [Discord](https://discordapp.com/invite/vAT7RSU). + +Happy developing!
+GDK team"; + break; + default: + throw new ArgumentException("Unsupported repository.", nameof(repoName)); + } + + return gitHubClient.CreateDraftRelease(gitHubRepo, options.Version, releaseBody, name, headCommit); + } + + private static void UpdateVersionFile(GitClient gitClient, string fileContents, string relativeFilePath) + { + using (new WorkingDirectoryScope(gitClient.RepositoryPath)) + { + Logger.Info("Updating contents of version file '{0}' to '{1}'...", relativeFilePath, fileContents); + + if (!File.Exists(relativeFilePath)) + { + throw new InvalidOperationException("Could not update the version file as the file " + + $"'{relativeFilePath}' does not exist."); + } + + 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}"); + } + + 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); + } + + private static string GetReleaseNotesFromChangeLog() + { + if (!File.Exists(ChangeLogFilename)) + { + throw new InvalidOperationException("Could not get draft release notes, as the change log file, " + + $"{ChangeLogFilename}, does not exist."); + } + + Logger.Info("Reading {0}...", ChangeLogFilename); + + var releaseBody = new StringBuilder(); + var changedSection = 0; + + using (var reader = new StreamReader(ChangeLogFilename)) + { + while (!reader.EndOfStream) + { + // Here we target the second Heading2 ("##") section. + // The first section will be the "Unreleased" section. The second will be the correct release notes. + var line = reader.ReadLine(); + if (line.StartsWith("## ")) + { + changedSection += 1; + + if (changedSection == 3) + { + break; + } + + continue; + } + + if (changedSection == 2) + { + releaseBody.AppendLine(line); + } + } + } + + return releaseBody.ToString(); + } + + private static void UpdateUnrealEngineVersionFile(List versions, GitClient client) + { + const string unrealEngineVersionFile = "ci/unreal-engine.version"; + + using (new WorkingDirectoryScope(client.RepositoryPath)) + { + File.WriteAllLines(unrealEngineVersionFile, versions); + client.StageFile(unrealEngineVersionFile); + } + } + } +} diff --git a/ci/ReleaseTool/ReleaseTool.csproj b/ci/ReleaseTool/ReleaseTool.csproj new file mode 100644 index 0000000000..934a2c2eee --- /dev/null +++ b/ci/ReleaseTool/ReleaseTool.csproj @@ -0,0 +1,16 @@ + + + Exe + netcoreapp2.1 + + ReleaseTool.EntryPoint + + + + + + + + + + \ No newline at end of file diff --git a/ci/ReleaseTool/WorkingDirectoryScope.cs b/ci/ReleaseTool/WorkingDirectoryScope.cs new file mode 100644 index 0000000000..6ed3b4602d --- /dev/null +++ b/ci/ReleaseTool/WorkingDirectoryScope.cs @@ -0,0 +1,20 @@ +using System; + +namespace ReleaseTool +{ + public class WorkingDirectoryScope : IDisposable + { + private readonly string oldWorkingDirectory; + + public WorkingDirectoryScope(string newWorkingDirectory) + { + oldWorkingDirectory = Environment.CurrentDirectory; + Environment.CurrentDirectory = newWorkingDirectory; + } + + public void Dispose() + { + Environment.CurrentDirectory = oldWorkingDirectory; + } + } +} diff --git a/ci/Tools.sln b/ci/Tools.sln new file mode 100644 index 0000000000..4f9749a8d2 --- /dev/null +++ b/ci/Tools.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30011.22 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReleaseTool", "ReleaseTool\ReleaseTool.csproj", "{7C42D241-8F30-4C0E-A0F8-178306E9F617}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7C42D241-8F30-4C0E-A0F8-178306E9F617}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C42D241-8F30-4C0E-A0F8-178306E9F617}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C42D241-8F30-4C0E-A0F8-178306E9F617}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C42D241-8F30-4C0E-A0F8-178306E9F617}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D2487644-D63B-4B59-842F-5BAF4F2C2C06} + EndGlobalSection +EndGlobal diff --git a/ci/cleanup.ps1 b/ci/cleanup.ps1 index 4d4a432c35..00e7696f1c 100644 --- a/ci/cleanup.ps1 +++ b/ci/cleanup.ps1 @@ -5,10 +5,12 @@ param ( $project_absolute_path = "$((Get-Item `"$($PSScriptRoot)`").parent.parent.FullName)\$project_name" ## This should ultimately resolve to "C:\b\\NetworkTestProject". +. "$PSScriptRoot\common.ps1" +$ErrorActionPreference = 'Continue' + # Workaround for UNR-2156 and UNR-2076, where spatiald / runtime processes sometimes never close, or where runtimes are orphaned # Clean up any spatiald and java (i.e. runtime) processes that may not have been shut down -& spatial "service" "stop" -Stop-Process -Name "java" -Force -ErrorAction SilentlyContinue +Stop-Runtime # Clean up the symlinks if (Test-Path "$unreal_path") { diff --git a/ci/common-release.sh b/ci/common-release.sh new file mode 100644 index 0000000000..bb6bcdcb61 --- /dev/null +++ b/ci/common-release.sh @@ -0,0 +1,31 @@ +function cleanUp() { + rm -rf ${SECRETS_DIR} +} + +function setupReleaseTool() { + echo "--- Setting up release tool :gear:" + # Create temporary directory for secrets and set a trap to cleanup on exit. + export SECRETS_DIR=$(mktemp -d) + trap cleanUp EXIT + + imp-ci secrets read \ + --environment=production \ + --buildkite-org=improbable \ + --secret-type=github-personal-access-token \ + --secret-name=gdk-for-unreal-bot-github-personal-access-token \ + --field="token" \ + --write-to="${SECRETS_DIR}/github_token" + + imp-ci secrets read \ + --environment=production \ + --buildkite-org=improbable \ + --secret-type=ssh-key \ + --secret-name=gdk-for-unreal-bot-ssh-key \ + --field="privateKey" \ + --write-to="${SECRETS_DIR}/id_rsa" + + docker build \ + --tag local:gdk-release-tool \ + --file ./ci/docker/release-tool.Dockerfile \ + . +} diff --git a/ci/common.ps1 b/ci/common.ps1 index c41d731158..35e0d5e1ce 100644 --- a/ci/common.ps1 +++ b/ci/common.ps1 @@ -43,4 +43,9 @@ function Finish-Event() { ) | Out-Null } +function Stop-Runtime() { + & spatial "service" "stop" + Stop-Process -Name "java" -Force -ErrorAction SilentlyContinue +} + $ErrorActionPreference = 'Stop' diff --git a/ci/docker/entrypoint.sh b/ci/docker/entrypoint.sh new file mode 100644 index 0000000000..3a1ca1c251 --- /dev/null +++ b/ci/docker/entrypoint.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +set -e -u -o pipefail + +if [[ -n "${DEBUG-}" ]]; then + set -x +fi + +USER_ID=${LOCAL_USER_ID:-999} + +useradd --shell /bin/bash -u "${USER_ID}" -o -c "" -m user +export HOME=/home/user + +# Change ownership of directories to the "user" user. +chown -R user:user "${HOME}" +chown -R user:user "$(pwd)" +chown -R user:user "/var/ssh" +chown -R user:user "/var/github" +chown -R user:user "/var/logs" + +gosu user git config --global user.name "UnrealGDK Bot" +gosu user git config --global user.email "gdk-for-unreal-bot@improbable.io" +gosu user git config --global core.sshCommand "ssh -i /var/ssh/id_rsa" + +mkdir -p /${HOME}/.ssh + touch /${HOME}/.ssh/known_hosts + ssh-keyscan github.com >> /${HOME}/.ssh/known_hosts + +gosu user dotnet ReleaseTool.dll "$@" \ No newline at end of file diff --git a/ci/docker/release-tool.Dockerfile b/ci/docker/release-tool.Dockerfile new file mode 100644 index 0000000000..af656fbcfe --- /dev/null +++ b/ci/docker/release-tool.Dockerfile @@ -0,0 +1,31 @@ +FROM microsoft/dotnet:2.2-sdk as build + +# Copy everything and build +WORKDIR /app +COPY ./ci ./ +RUN dotnet publish -c Release -o out + +# Build runtime image +FROM mcr.microsoft.com/dotnet/core/runtime:2.2 +WORKDIR /app +COPY --from=build /app/*/out ./ + +# Setup GIT +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y git && \ + curl -LSs -o /usr/local/bin/gosu -SL "https://github.com/tianon/gosu/releases/download/1.4/gosu-$(dpkg --print-architecture)" && \ + chmod +x /usr/local/bin/gosu + +# Create a volume to mount our SSH key into and configure git to use it. +VOLUME /var/ssh +# Volume to mount our Github token into. +VOLUME /var/github +# Volume to output logs & Buildkite metadata to +VOLUME /var/logs + +COPY ./ci/docker/entrypoint.sh ./ + +RUN ["chmod", "+x", "./entrypoint.sh"] + +ENTRYPOINT ["./entrypoint.sh"] \ No newline at end of file diff --git a/ci/gdk_build.template.steps.yaml b/ci/gdk_build.template.steps.yaml index d0483327f6..e1d41badf3 100644 --- a/ci/gdk_build.template.steps.yaml +++ b/ci/gdk_build.template.steps.yaml @@ -66,3 +66,4 @@ steps: BUILD_TARGET: "${BUILD_TARGET}" BUILD_STATE: "${BUILD_STATE}" TEST_CONFIG: "${TEST_CONFIG}" + SLOW_NETWORKING_TESTS: "${SLOW_NETWORKING_TESTS}" diff --git a/ci/generate-and-upload-build-steps.sh b/ci/generate-and-upload-build-steps.sh index a191a5530e..6695045ff2 100755 --- a/ci/generate-and-upload-build-steps.sh +++ b/ci/generate-and-upload-build-steps.sh @@ -42,6 +42,7 @@ generate_build_configuration_steps () { fi fi + export SLOW_NETWORKING_TESTS="${SLOW_NETWORKING_TESTS_LOCAL}" if [[ "${SLOW_NETWORKING_TESTS_LOCAL,,}" == "true" ]]; then # Start a build with native tests as a separate step upload_build_configuration_step "${ENGINE_COMMIT_HASH}" "Win64" "Editor" "Development" "Native" diff --git a/ci/generate-release-qa-trigger.sh b/ci/generate-release-qa-trigger.sh new file mode 100755 index 0000000000..56a0f6c13d --- /dev/null +++ b/ci/generate-release-qa-trigger.sh @@ -0,0 +1,81 @@ +#!/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. + +# exit immediately on failure, or if an undefined variable is used +set -eu + +# 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 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)" + +echo "steps:" +triggerTest () { + local REPO_NAME="${1}" + local TEST_NAME="${2}" + local BRANCH_TO_TEST="${3}" + local ENVIRONMENT_VARIABLES=( "${@:4}" ) + +echo " - trigger: "${REPO_NAME}-${TEST_NAME}"" +echo " label: "Run ${REPO_NAME}-${TEST_NAME} at HEAD OF ${BRANCH_TO_TEST}"" +echo " async: true" +echo " build:" +echo " branch: "${BRANCH_TO_TEST}"" +echo " commit: "HEAD"" +echo " env:" + +for element in "${ENVIRONMENT_VARIABLES[@]}" + do + echo " ${element}" + done +} + +### unrealengine-nightly +while IFS= read -r ENGINE_VERSION; do + triggerTest "unrealengine" \ + "nightly" \ + "${ENGINE_VERSION}-${GDK_VERSION}-rc" \ + "GDK_BRANCH: "${GDK_VERSION}-rc"" \ + "EXAMPLE_PROJECT_BRANCH: "${GDK_VERSION}-rc"" +done <<< "${ENGINE_VERSIONS}" + +### unrealgdk-premerge with SLOW_NETWORKING_TESTS=true +while IFS= read -r ENGINE_VERSION; do + triggerTest "unrealgdk" \ + "premerge" \ + "${GDK_VERSION}-rc" \ + "SLOW_NETWORKING_TESTS: "true"" \ + "TEST_REPO_BRANCH: "${GDK_VERSION}-rc"" \ + "ENGINE_VERSION: ""HEAD ${ENGINE_VERSION}-${GDK_VERSION}-rc""" +done <<< "${ENGINE_VERSIONS}" + +### unrealgdk-premerge with BUILD_ALL_CONFIGURATIONS=true +while IFS= read -r ENGINE_VERSION; do + triggerTest "unrealgdk" \ + "premerge" \ + "${GDK_VERSION}-rc" \ + "BUILD_ALL_CONFIGURATIONS: "true"" \ + "TEST_REPO_BRANCH: "${GDK_VERSION}-rc"" \ + "ENGINE_VERSION: ""HEAD ${ENGINE_VERSION}-${GDK_VERSION}-rc""" +done <<< "${ENGINE_VERSIONS}" + +### unrealgdkexampleproject-nightly +### Only runs against the primary Engine version because Example Project doesn't support legacy Engine versions. +FIRST_VERSION=$(echo "${ENGINE_VERSIONS}" | sed -n '1p') + triggerTest "unrealgdkexampleproject" \ + "nightly" \ + "${GDK_VERSION}-rc" \ + "GDK_BRANCH: "${GDK_VERSION}-rc"" \ + "ENGINE_VERSION: ""HEAD ${FIRST_VERSION}-${GDK_VERSION}-rc""" + +### unrealgdk-nfr +### TODO: Uncomment the below when implementing GV-515 +###while IFS= read -r ENGINE_VERSION; do +### triggerTest "unrealgdk" \ +### "nfr" \ +### "${GDK_VERSION}-rc" +###done <<< "${ENGINE_VERSIONS}" diff --git a/ci/generate-unrealengine-premerge-trigger.sh b/ci/generate-unrealengine-premerge-trigger.sh new file mode 100755 index 0000000000..0860babd91 --- /dev/null +++ b/ci/generate-unrealengine-premerge-trigger.sh @@ -0,0 +1,36 @@ +#!/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. + +# exit immediately on failure, or if an undefined variable is used +set -eu + +# 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 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)" + +echo "steps:" +triggerTest () { + local REPO_NAME="${1}" + local TEST_NAME="${2}" + local BRANCH_TO_TEST="${3}" + local ENVIRONMENT_VARIABLES=( "${@:4}" ) + +echo " - trigger: "${REPO_NAME}-${TEST_NAME}"" +echo " label: "Run ${REPO_NAME}-${TEST_NAME} at HEAD OF ${BRANCH_TO_TEST}"" +echo " async: true" +echo " build:" +echo " branch: "${BRANCH_TO_TEST}"" +echo " commit: "HEAD"" +} + +### unrealengine-premerge +while IFS= read -r ENGINE_VERSION; do + triggerTest "unrealengine" \ + "premerge" \ + "${ENGINE_VERSION}-${GDK_VERSION}-rc" +done <<< "${ENGINE_VERSIONS}" diff --git a/ci/prepare-release.sh b/ci/prepare-release.sh new file mode 100644 index 0000000000..dfbab75d9b --- /dev/null +++ b/ci/prepare-release.sh @@ -0,0 +1,111 @@ +#!/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. + +prepareRelease () { + local REPO_NAME="${1}" + local SOURCE_BRANCH="${2}" + local CANDIDATE_BRANCH="${3}" + local RELEASE_BRANCH="${4}" + local GITHUB_ORG="${5}" + local ENGINE_VERSIONS_LOCAL_VAR=$(echo ${ENGINE_VERSIONS[*]// }) + + echo "--- Preparing ${REPO_NAME}: Cutting ${CANDIDATE_BRANCH} from ${SOURCE_BRANCH}, and creating a PR into ${RELEASE_BRANCH} :package:" + + docker run \ + -v "${BUILDKITE_ARGS[@]}" \ + -v "${SECRETS_DIR}":/var/ssh \ + -v "${SECRETS_DIR}":/var/github \ + -v "$(pwd)"/logs:/var/logs \ + local:gdk-release-tool \ + prep "${GDK_VERSION}" \ + --source-branch="${SOURCE_BRANCH}" \ + --candidate-branch="${CANDIDATE_BRANCH}" \ + --release-branch="${RELEASE_BRANCH}" \ + --git-repository-name="${REPO_NAME}" \ + --github-key-file="/var/github/github_token" \ + --github-organization="${GITHUB_ORG}" \ + --engine-versions="${ENGINE_VERSIONS_LOCAL_VAR}" +} + +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 + +setupReleaseTool + +mkdir -p ./logs +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 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" + +for ENGINE_VERSION in "${ENGINE_VERSIONS[@]}" +do + : + # Once per ENGINE_VERSION do: + prepareRelease "UnrealEngine" \ + "${ENGINE_VERSION}" \ + "${ENGINE_VERSION}-${GDK_VERSION}-rc" \ + "${ENGINE_VERSION}-release" \ + "improbableio" +done \ No newline at end of file diff --git a/ci/release.sh b/ci/release.sh new file mode 100644 index 0000000000..fbaf6784f8 --- /dev/null +++ b/ci/release.sh @@ -0,0 +1,124 @@ +#!/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. + +release () { + local REPO_NAME="${1}" + local SOURCE_BRANCH="${2}" + local CANDIDATE_BRANCH="${3}" + local RELEASE_BRANCH="${4}" + local PR_URL="${5}" + local GITHUB_ORG="${6}" + local ENGINE_VERSIONS_LOCAL_VAR=$(echo ${ENGINE_VERSIONS[*]// }) + + echo "--- Releasing ${REPO_NAME}: Merging ${CANDIDATE_BRANCH} into ${RELEASE_BRANCH} :package:" + + docker run \ + -v "${BUILDKITE_ARGS[@]}" \ + -v "${SECRETS_DIR}":/var/ssh \ + -v "${SECRETS_DIR}":/var/github \ + -v "$(pwd)"/logs:/var/logs \ + local:gdk-release-tool \ + release "${GDK_VERSION}" \ + --source-branch="${SOURCE_BRANCH}" \ + --candidate-branch="${CANDIDATE_BRANCH}" \ + --release-branch="${RELEASE_BRANCH}" \ + --github-key-file="/var/github/github_token" \ + --pull-request-url="${PR_URL}" \ + --github-organization="${GITHUB_ORG}" \ + --engine-versions="${ENGINE_VERSIONS_LOCAL_VAR}" +} + +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 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. SOURCE_BRANCH +# 3. CANDIDATE_BRANCH +# 4. RELEASE_BRANCH +# 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" \ + "${ENGINE_VERSION}" \ + "${ENGINE_VERSION}-${GDK_VERSION}-rc" \ + "${ENGINE_VERSION}-release" \ + "$(buildkite-agent meta-data get UnrealEngine-${ENGINE_VERSION}-pr-url)" \ + "improbableio" +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" diff --git a/ci/report-tests.ps1 b/ci/report-tests.ps1 index 3c3833d8bd..23a295f237 100644 --- a/ci/report-tests.ps1 +++ b/ci/report-tests.ps1 @@ -42,6 +42,11 @@ if (Test-Path "$test_result_dir\index.html" -PathType Leaf) { --context "unreal-gdk-test-artifact-location" ` --style info } +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 + Throw $error_msg +} # Upload artifacts to Buildkite, capture output to extract artifact ID in the Slack message generation # Command format is the results of Powershell weirdness, likely related to the following: diff --git a/ci/run-tests.ps1 b/ci/run-tests.ps1 index 09bd0e41d5..1819c3040d 100644 --- a/ci/run-tests.ps1 +++ b/ci/run-tests.ps1 @@ -7,7 +7,8 @@ param( [string] $report_output_path, [string] $tests_path = "SpatialGDK", [string] $additional_gdk_options = "", - [bool] $run_with_spatial = $False + [bool] $run_with_spatial = $False, + [string] $additional_cmd_line_args = "" ) # This resolves a path to be absolute, without actually reading the filesystem. @@ -19,9 +20,34 @@ function Force-ResolvePath { return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path) } +function Parse-UnrealOptions { + param ( + [string] $raw_options, + [string] $category + ) + $options_arr = $raw_options.Split(";", [System.StringSplitOptions]::RemoveEmptyEntries) + $options_arr = $options_arr | ForEach-Object { "${category}:$_" } + $options_result = $options_arr -Join "," + return $options_result +} + +. "$PSScriptRoot\common.ps1" + if ($run_with_spatial) { # Generate schema and snapshots Write-Output "Generating snapshot and schema for testing project" + Start-Process "$unreal_editor_path" -Wait -PassThru -NoNewWindow -ArgumentList @(` + "$uproject_path", ` + "-SkipShaderCompile", # Skip shader compilation + "-nopause", # Close the unreal log window automatically on exit + "-nosplash", # No splash screen + "-unattended", # Disable anything requiring user feedback + "-nullRHI", # Hard to find documentation for, but seems to indicate that we want something akin to a headless (i.e. no UI / windowing) editor + "-run=CookAndGenerateSchema", # Run the commandlet + "-cookall", # Make sure it runs for all maps (and other things) + "-targetplatform=LinuxServer" + ) + Start-Process "$unreal_editor_path" -Wait -PassThru -NoNewWindow -ArgumentList @(` "$uproject_path", ` "-NoShaderCompile", # Prevent shader compilation @@ -29,7 +55,7 @@ if ($run_with_spatial) { "-nosplash", # No splash screen "-unattended", # Disable anything requiring user feedback "-nullRHI", # Hard to find documentation for, but seems to indicate that we want something akin to a headless (i.e. no UI / windowing) editor - "-run=GenerateSchemaAndSnapshots", # Run the commandlet + "-run=GenerateSnapshot", # Run the commandlet "-MapPaths=`"$test_repo_map`"" # Which maps to run the commandlet for ) @@ -49,14 +75,7 @@ $ue_path_absolute = Force-ResolvePath $unreal_editor_path $uproject_path_absolute = Force-ResolvePath $uproject_path $output_dir_absolute = Force-ResolvePath $output_dir -$additional_gdk_options_arr = $additional_gdk_options.Split(";") -$additional_gdk_options = "" -Foreach ($additional_gdk_option in $additional_gdk_options_arr) { - if ($additional_gdk_options -ne "") { - $additional_gdk_options += "," - } - $additional_gdk_options += "[/Script/SpatialGDK.SpatialGDKSettings]:$additional_gdk_option" -} +$additional_gdk_options = Parse-UnrealOptions "$additional_gdk_options" "[/Script/SpatialGDK.SpatialGDKSettings]" $cmd_args_list = @( ` "`"$uproject_path_absolute`"", # We need some project to run tests in, but for unit tests the exact project shouldn't matter @@ -74,6 +93,10 @@ $cmd_args_list = @( ` "-OverrideSpatialNetworking=$run_with_spatial" # A parameter to switch beetween different networking implementations ) +if($additional_cmd_line_args -ne "") { + $cmd_args_list += "$additional_cmd_line_args" # Any additional command line arguments the user wants to pass in +} + Write-Output "Running $($ue_path_absolute) $($cmd_args_list)" $run_tests_proc = Start-Process $ue_path_absolute -PassThru -NoNewWindow -ArgumentList $cmd_args_list @@ -81,9 +104,13 @@ try { # Give the Unreal Editor 30 minutes to run the tests, otherwise kill it # This is so we can get some logs out of it, before we are cancelled by buildkite Wait-Process -Timeout 1800 -InputObject $run_tests_proc + # If the Editor crashes, these processes can stay lingering and prevent the job from ever timing out + Stop-Runtime } catch { Stop-Process -Force -InputObject $run_tests_proc # kill the dangling process buildkite-agent artifact upload "$log_file_path" # If the tests timed out, upload the log and throw an error + # Looks like BuildKite doesn't like this failure and a dangling runtime will prevent the job from ever timing out + Stop-Runtime throw $_ } diff --git a/ci/run-tests.sh b/ci/run-tests.sh index 05fa204796..879adb935b 100755 --- a/ci/run-tests.sh +++ b/ci/run-tests.sh @@ -24,6 +24,16 @@ pushd "$(dirname "$0")" UNREAL_EDITOR_PATH="Engine/Binaries/Mac/UE4Editor.app/Contents/MacOS/UE4Editor" if [[ -n "${RUN_WITH_SPATIAL}" ]]; then echo "Generating snapshot and schema for testing project" + "${UNREAL_EDITOR_PATH}" \ + "${UPROJECT_PATH}" \ + -SkipShaderCompile \ + -nopause \ + -nosplash \ + -unattended \ + -nullRHI \ + -run=CookAndGenerateSchema \ + -cookall + "${UNREAL_EDITOR_PATH}" \ "${UPROJECT_PATH}" \ -NoShaderCompile \ @@ -31,7 +41,7 @@ pushd "$(dirname "$0")" -nosplash \ -unattended \ -nullRHI \ - -run=GenerateSchemaAndSnapshots \ + -run=GenerateSnapshot \ -MapPaths="${TEST_REPO_MAP}" cp "${TEST_REPO_PATH}/spatial/snapshots/${TEST_REPO_MAP}.snapshot" "${TEST_REPO_PATH}/spatial/snapshots/default.snapshot" diff --git a/ci/setup-build-test-gdk.ps1 b/ci/setup-build-test-gdk.ps1 index 88e2d43bf2..edb8c6a0aa 100644 --- a/ci/setup-build-test-gdk.ps1 +++ b/ci/setup-build-test-gdk.ps1 @@ -16,8 +16,11 @@ class TestSuite { [ValidateNotNullOrEmpty()][string]$tests_path [ValidateNotNull()] [string]$additional_gdk_options [bool] $run_with_spatial + [ValidateNotNull()] [string]$additional_cmd_line_args - TestSuite([string] $test_repo_url, [string] $test_repo_branch, [string] $test_repo_relative_uproject_path, [string] $test_repo_map, [string] $test_project_name, [string] $test_results_dir, [string] $tests_path, [string] $additional_gdk_options, [bool] $run_with_spatial) { + TestSuite([string] $test_repo_url, [string] $test_repo_branch, [string] $test_repo_relative_uproject_path, [string] $test_repo_map, + [string] $test_project_name, [string] $test_results_dir, [string] $tests_path, [string] $additional_gdk_options, + [bool] $run_with_spatial, [string] $additional_cmd_line_args) { $this.test_repo_url = $test_repo_url $this.test_repo_branch = $test_repo_branch $this.test_repo_relative_uproject_path = $test_repo_relative_uproject_path @@ -27,25 +30,29 @@ class TestSuite { $this.tests_path = $tests_path $this.additional_gdk_options = $additional_gdk_options $this.run_with_spatial = $run_with_spatial + $this.additional_cmd_line_args = $additional_cmd_line_args } } [string] $test_repo_url = "git@github.com:improbable/UnrealGDKEngineNetTest.git" [string] $test_repo_relative_uproject_path = "Game\EngineNetTest.uproject" -[string] $test_repo_map = "NetworkingMap" [string] $test_project_name = "NetworkTestProject" -[string] $test_repo_branch = "0.9.0" -[string] $user_gdk_settings = "" +[string] $test_repo_branch = "master" +[string] $user_gdk_settings = "$env:GDK_SETTINGS" +[string] $user_cmd_line_args = "$env:TEST_ARGS" +[string] $gdk_branch = "$env:BUILDKITE_BRANCH" + +# If the testing repo has a branch with the same name as the current branch, use that +$testing_repo_heads = git ls-remote --heads $test_repo_url $gdk_branch +if($testing_repo_heads -Match [Regex]::Escape("refs/heads/$gdk_branch")) { + $test_repo_branch = $gdk_branch +} # Allow overriding testing branch via environment variable if (Test-Path env:TEST_REPO_BRANCH) { $test_repo_branch = $env:TEST_REPO_BRANCH } -if (Test-Path env:GDK_SETTINGS) { - $user_gdk_settings = ";" + $env:GDK_SETTINGS -} - $tests = @() # If building all configurations, use the test gyms, since the network testing project only compiles for the Editor configs @@ -54,23 +61,25 @@ $tests = @() if (Test-Path env:BUILD_ALL_CONFIGURATIONS) { $test_repo_url = "git@github.com:spatialos/UnrealGDKTestGyms.git" $test_repo_relative_uproject_path = "Game\GDKTestGyms.uproject" - $test_repo_map = "EmptyGym" $test_project_name = "GDKTestGyms" - $tests += [TestSuite]::new("$test_repo_url", "$test_repo_branch", "$test_repo_relative_uproject_path", "$test_repo_map", "$test_project_name", "TestResults", "SpatialGDK", "bEnableUnrealLoadBalancer=false$user_gdk_settings", $True) + $tests += [TestSuite]::new("$test_repo_url", "$test_repo_branch", "$test_repo_relative_uproject_path", "EmptyGym", "$test_project_name", "TestResults", "SpatialGDK.", "$user_gdk_settings", $True, "$user_cmd_line_args") } -else{ +else { if ((Test-Path env:TEST_CONFIG) -And ($env:TEST_CONFIG -eq "Native")) { - $tests += [TestSuite]::new("$test_repo_url", "$test_repo_branch", "$test_repo_relative_uproject_path", "$test_repo_map", "$test_project_name", "VanillaTestResults", "/Game/SpatialNetworkingMap", "$user_gdk_settings", $False) + $tests += [TestSuite]::new("$test_repo_url", "$test_repo_branch", "$test_repo_relative_uproject_path", "NetworkingMap", "$test_project_name", "VanillaTestResults", "/Game/SpatialNetworkingMap", "$user_gdk_settings", $False, "$user_cmd_line_args") } else { - $tests += [TestSuite]::new("$test_repo_url", "$test_repo_branch", "$test_repo_relative_uproject_path", "$test_repo_map", "$test_project_name", "TestResults", "SpatialGDK+/Game/SpatialNetworkingMap", "$user_gdk_settings", $True) - # enable load-balancing once the tests pass reliably and the testing repo is updated - # $tests += [TestSuite]::new("$test_repo_url", "$test_repo_branch", "$test_repo_relative_uproject_path", "$test_repo_map", "$test_project_name", "LoadbalancerTestResults", "/Game/Spatial_ZoningMap_1S_2C", "bEnableUnrealLoadBalancer=true;LoadBalancingWorkerType=(WorkerTypeName=`"UnrealWorker`")$user_gdk_settings", $True) + $tests += [TestSuite]::new("$test_repo_url", "$test_repo_branch", "$test_repo_relative_uproject_path", "SpatialNetworkingMap", "$test_project_name", "TestResults", "SpatialGDK.+/Game/SpatialNetworkingMap", "$user_gdk_settings", $True, "$user_cmd_line_args") + $tests += [TestSuite]::new("$test_repo_url", "$test_repo_branch", "$test_repo_relative_uproject_path", "SpatialZoningMap", "$test_project_name", "LoadbalancerTestResults", "/Game/SpatialZoningMap", + "bEnableMultiWorker=True;$user_gdk_settings", $True, "$user_cmd_line_args") } - if ($env:SLOW_NETWORKING_TESTS) { + if ($env:SLOW_NETWORKING_TESTS -like "true") { $tests[0].tests_path += "+/Game/NetworkingMap" + if($env:TEST_CONFIG -ne "Native") { + $tests[0].tests_path += "+SpatialGDKSlow." + } $tests[0].test_results_dir = "Slow" + $tests[0].test_results_dir } } @@ -116,6 +125,7 @@ Foreach ($test in $tests) { $tests_path = $test.tests_path $additional_gdk_options = $test.additional_gdk_options $run_with_spatial = $test.run_with_spatial + $additional_cmd_line_args = $test.additional_cmd_line_args $project_is_cached = $False Foreach ($cached_project in $projects_cached) { @@ -155,7 +165,8 @@ Foreach ($test in $tests) { -test_repo_map "$test_repo_map" ` -tests_path "$tests_path" ` -additional_gdk_options "$additional_gdk_options" ` - -run_with_spatial $run_with_spatial + -run_with_spatial $run_with_spatial ` + -additional_cmd_line_args "$additional_cmd_line_args" Finish-Event "test-gdk" "command" Start-Event "report-tests" "command" diff --git a/ci/unreal-engine.version b/ci/unreal-engine.version index 4592fb509b..a62b68ec71 100644 --- a/ci/unreal-engine.version +++ b/ci/unreal-engine.version @@ -1,2 +1,2 @@ -UnrealEngine-6333a45a65e6503baedd45fb684abced5bb413c3 -UnrealEngine-7e12afba8575bf84f7a39df239837af040c31c81 \ No newline at end of file +UnrealEngine-a048ec22c7e1de2196a7c3478599285d478e7bb4 +UnrealEngine-f68e59b61c5a95c9dc968508d8a30bf612fa18c2