diff --git a/.buildkite/premerge.steps.yaml b/.buildkite/premerge.steps.yaml index 725a1af423..e2e62c6ef0 100755 --- a/.buildkite/premerge.steps.yaml +++ b/.buildkite/premerge.steps.yaml @@ -7,15 +7,22 @@ agent_transients: &agent_transients common: &common agents: + - "agent_count=1" - "capable_of_building=gdk-for-unreal" - "environment=production" + - "machine_type=quad" # this name matches to SpatialOS node-size names - "platform=windows" - "permission_set=builder" - - "queue=v2-1551960839-2fc9bbe6e15deffd-------z" + - "scaler_version=2" + - "queue=${CI_WINDOWS_BUILDER_QUEUE:-v3-1562267374-ab36457a07f081fe-------z}" retry: automatic: - <<: *agent_transients timeout_in_minutes: 60 + plugins: + - git-clean#v0.0.1: + flags: "-ffdx --exclude=UnrealEngine --exclude=UnrealEngine-Cache" + - ca-johnson/taskkill#v4.1: ~ # NOTE: step labels turn into commit-status names like {org}/{repo}/{pipeline}/{step-label}, lower-case and hyphenated. # These are then relied on to have stable names by other things, so once named, please beware renaming has consequences. diff --git a/.buildkite/trigger.sh b/.buildkite/trigger.sh deleted file mode 100755 index 4d9976ee69..0000000000 --- a/.buildkite/trigger.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -# https://brevi.link/shell-style -# https://explainshell.com -set -euo pipefail -if [[ -n "${DEBUG-}" ]]; then - set -x -fi -cd "$(dirname "$0")/../" - -# The step-definitions file is uploaded dynamically to preserve ability for historical builds -# vs changes in CI pipeline configuration. -buildkite-agent pipeline upload "$1" \ No newline at end of file diff --git a/.gitignore b/.gitignore index b6392a95e2..d5ae672f0b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +# Visual Studio Code user specific files +.vscode/ # Visual Studio 2015 user specific files .vs/ @@ -81,6 +83,9 @@ Scripts/spatialos.*.build.json !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 +SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/packages/ +SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.csproj.user # Don't ignore the Build util !SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/ diff --git a/CHANGELOG.md b/CHANGELOG.md index f27439f83b..2a452f0674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,102 @@ All notable changes to the SpatialOS Game Development Kit for Unreal will be doc The format of this Changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased-`x.y.z`] - 2019-xx-xx + +## [`0.6.0`] - 2019-07-31 + +### Breaking Changes: +* You must [re-build](https://docs.improbable.io/unreal/alpha/content/get-started/example-project/exampleproject-setup#step-4-build-the-dependencies-and-launch-the-project) your [Example Project](https://github.com/spatialos/UnrealGDKExampleProject) if you're upgrading it to `0.6.0`. +* This is the last GDK version to support Unreal Engine 4.20. You will need to upgrade your project to use Unreal Engine 4.22 (`4.22-SpatialOSUnrealGDK-release`) in order to continue receiving GDK releases and support. + +### New Known Issues: +- Workers will sometimes not gain authority when quickly reconnecting to an existing deployment, resulting in a failure to spawn or simulate. When using the editor if you Play - Stop - Play in quick succession you can sometimes fail to launch correctly. + +### Features: +- The GDK now uses SpatialOS `13.8.1`. +- Dynamic components are now supported. You can now dynamically attach and remove replicated subobjects to Actors. +- Local deployment startup time has been significantly reduced. +- Local deployments now start automatically when you select `Play`. This means you no longer need to select `Start` in the GDK toolbar before you select `Play` in the Unreal toolbar. +- If your schema has changed during a local deployment, the next time you select `Play` the deployment will automatically restart. +- Local deployments no longer run in a seperate Command Prompt. Logs from these deployments are now found in the Unreal Editor's Output Log. +- SpatialOS Runtime logs can now be found at `\spatial\logs\localdeployment\\runtime.log`. +- An option to `Show spatial service button` has been added to the SpatialOS Settings menu. This button can be useful when debugging. +- Every time you open a GDK project in the Unreal Editor, 'spatial service' will be restarted. This ensures the service is always running in the correct SpatialOS project. You can disable this auto start feature via the new SpatialOS setting `Auto-start local deployment`. +- Added external schema code-generation tool for [non-Unreal server-worker types]({{urlRoot}}/content/non-unreal-server-worker-types). If you create non-Unreal server-worker types using the [SpatialOS Worker SDK](https://docs.improbable.io/reference/13.8/shared/sdks-and-data-overview) outside of the GDK and your Unreal Engine, you manually create [schema]({{urlRoot}/content/glossary#schema). Use the new [helper-script]({{urlRoot}}/content/helper-scripts) to generate Unreal code from manually-created schema; it enables your Unreal game code to interoperate with non-Unreal server-worker types. +- Added simulated player tools, which will allow you to create logic to simulate the behavior of real players. Simulated players can be used to scale test your game to hundreds of players. Simulated players can be launched locally as part of your development flow for quick iteration, as well as part of a separate "simulated player deployment" to connect a configurable amount of simulated players to your running game deployment. +- Factored out writing of Linux worker start scripts into a library, and added a standalone `WriteLinuxScript.exe` to _just_ write the launch script (for use in custom build pipelines). +- Added temporary MaxNetCullDistanceSquared to SpatialGDKSettings to prevent excessive net cull distances impacting runtime performance. Set to 0 to disable. +- Added `OnWorkerFlagsUpdated`, a delegate that can be directly bound to in C++. To bind via blueprints you can use the blueprint callable functions `BindToOnWorkerFlagsUpdated` and `UnbindToOnWorkerFlagsUpdated`. You can use `OnWorkerFlagsUpdated` to register when worker flag updates are received, which allows you to tweak values at deployment runtime. +- RPC frequency and payload size can now be tracked using console commands: `SpatialStartRPCMetrics` to start recording RPCs and `SpatialStopRPCMetrics` to stop recording and log the collected information. Using these commands will also start/stop RPC tracking on the server. +- Spatial now respects `bAlwaysRelevant` and clients will always checkout Actors that have `bAlwaysRelevant` set to true. + +### Bug fixes: +- The `improbable` namespace has been renamed to `SpatialGDK`. This prevents namespace conflicts with the C++ SDK. +- Disconnected players no longer remain on the server until they time out if the client was shut down manually. +- Fixed support for relative paths as the engine association in your games .uproject file. +- RPCs on `NotSpatial` types are no longer queued forever and are now dropped instead. +- Fixed issue where an Actor's Spatial position was not updated if it had an owner that was not replicated. +- BeginPlay is now only called with authority on startup actors once per deployment. +- Fixed null pointer dereference crash when trying to initiate a Spatial connection without an existing one. +- URL options are now properly sent through to the server when doing a ClientTravel. +- The correct error message is now printed when the SchemaDatabase is missing. +- `StartEditor.bat` is now generated correctly when you build a server worker outside of editor. +- Fixed an issue with logging errored blueprints after garbage collection which caused an invalid pointer crash. +- Removed the ability to configure snapshot save folder. Snapshots should always be saved to `/spatial/snapshots`. This prevents an issue with absolute paths being checked in which can break snapshot generation. +- Introduced a new module, `SpatialGDKServices`, on which `SpatialGDK` and `SpatilGDKEditorToolbar` now depend. This resolves a previously cyclic dependency. +- RPCs called before entity creation are now queued in case they cannot yet be executed. Previously they were simply dropped. These RPCs are also included in RPC metrics. +- RPCs are now guaranteed to arrive in the same order for a given actor and all of its subobjects on single-server deployments. This matches native Unreal behavior. + +## [`0.5.0-preview`](https://github.com/spatialos/UnrealGDK/releases/tag/0.5.0-preview) - 2019-06-25 +- Prevented `Spatial GDK Content` from appearing under Content Browser in the editor, as the GDK plugin does not contain any game content. + +### Breaking Changes: +- If you are using Unreal Engine 4.22, the AutomationTool and UnrealBuildTool now require [.NET 4.6.2](https://dotnet.microsoft.com/download/dotnet-framework/net462). + +### New Known Issues: + +### Features: +- Unreal Engine 4.22 is now supported. You can find the 4.22 verson of our engine fork [here](https://github.com/improbableio/UnrealEngine/tree/4.22-SpatialOSUnrealGDK-release). +- Setup.bat can now take a project path as an argument. This allows the UnrealGDK to be installed as an Engine Plugin, pass the project path as the first variable if you are running Setup.bat from UnrealEngine/Engine/Plugins. +- Removed the need for setting the `UNREAL_HOME` environment variable. The build and setup scripts will now use your project's engine association to find the Unreal Engine directory. If an association is not set they will search parent directories looking for the 'Engine' folder. +- Added the `ASpatialMetricsDisplay` class, which you can use to view UnrealWorker stats as an overlay on the client. +- Added the runtime option `bEnableHandover`, which you can use to toggle property handover when running in non-zoned deployments. +- Added the runtime option `bEnableMetricsDisplay`, which you can use to auto spawn `ASpatialMetricsDisplay`, which is used to remote debug server metrics. +- Added the runtime option `bBatchSpatialPositionUpdates`, which you can use to batch spatial position updates to the runtime. +- Started using the [schema_compiler tool](https://docs.improbable.io/reference/13.8/shared/schema/introduction#using-the-schema-compiler-directly) to generate [schema descriptors](https://docs.improbable.io/reference/13.8/shared/flexible-project-layout/build-process/schema-descriptor-build-process#schema-descriptor-introduction) rather than relying on 'spatial local launch' to do this. +- Changed Interest so that NetCullDistanceSquared is used to define the distance from a player that the actor type is *interesting to* the player. This replaces CheckoutRadius which defined the distance that an actor is *interested in* other types. Requires engine update to remove the CheckoutRadius property which is no longer used. +- Added ActorInterestComponent that can be used to define interest queries that are more complex than a radius around the player position. +- Enabled new Development Authentication Flow +- Added new "worker" entities which are created for each server worker in a deployment so they correctly receive interest in the global state manager. +- Added support for spawning actors with ACLs configured for offloading using actor groups. +- Removed the references to the `Number of servers` slider in the Play in editor drop-down menu. The number of each server worker type to launch in PIE is now specified within the launch configuration in the `Spatial GDK Editor Settings` settings tab. +- Added `SpatialWorkerId` which is set to the worker ID when the worker associated to the `UGameInstance` connects. +- Added `USpatialStatics` helper blueprint library exposing functions for checking if SpatialOS networking is enabled, whether offloading is enabled, and more SpatialOS related checks. + + +### Bug fixes: +- BeginPlay is not called with authority when checking out entities from Spatial. +- Launching SpatialOS would fail if there was a space in the full directory path. +- GenerateSchemaAndSnapshots commandlet no longer runs a full schema generation for each map. +- Reliable RPC checking no longer breaks compatibility between development and shipping builds. +- Fixed an issue with schema name collisions. +- Running Schema (Full Scan) now clears generated schema files first. +- [Singleton actor's](https://docs.improbable.io/unreal/latest/content/singleton-actors#singleton-actors) authority and state now resumes correctly when reconnecting servers to snapshot. +- Retrying reliable RPCs with `UObject` arguments that were destroyed before the RPC was retried no longer causes a crash. +- Fixed path naming issues in setup.sh +- Fixed an assert/crash in `SpatialMetricsDisplay` that occurred when reloading a snapshot. +- Added Singleton and SingletonManager to QBI constraints to fix issue preventing Test configuration builds from functioning correctly. +- Failing to `NetSerialize` a struct in spatial no longer causes a crash, it now prints a warning. This matches native Unreal behavior. +- Query response delegates now execute even if response status shows failure. This allows handlers to implement custom retry logic such as clients querying for the GSM. +- Fixed a crash where processing unreliable RPCs made assumption that the worker had authority over all entities in the SpatialOS op +- Ordering and reliability for single server RPCs on the same Actor are now guaranteed. + +### External contributors: + +In addition to all of the updates from Improbable, this release includes x improvements submitted by the incredible community of SpatialOS developers on GitHub! Thanks to these contributors: + +* @cyberbibby + ## [`0.4.2`](https://github.com/spatialos/UnrealGDK/releases/tag/0.4.2) - 2019-05-20 ### New Known Issues: @@ -18,8 +114,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - GenerateSchemaAndSnapshots commandlet no longer runs a full schema generation for each map. - Launching SpatialOS would fail if there was a space in the full directory path. - Fixed an issue with schema name collisions. -- Fixed an issue where schema generation was not respecting "Directories to never cook". -- Fixed an issue causing the editor to crash during schema generation if the database is readonly. +- Schema generation now respects "Directories to never cook". +- The editor no longer crashes during schema generation when the database is readonly. +- Replicating `UInterfaceProperty` no longer causes crashes. ## [`0.4.1`](https://github.com/spatialos/UnrealGDK/releases/tag/0.4.1) - 2019-05-01 @@ -51,7 +148,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [`0.3.0`](https://github.com/spatialos/UnrealGDK/releases/tag/0.3.0) - 2019-04-04 -### New Known Issues: +### New Known Issues: - Enabling Query Based Interest is needed for level streaming support, but this might affect performance in certain scenarios and is currently being investigated. - Replicated `TimelineComponents` are not supported. @@ -106,7 +203,7 @@ In addition to all of the updates from Improbable, this release includes 2 impro Startup actors revamp is merged! Snapshots are now simpler. Many bugfixes. -### New Known Issues: +### New Known Issues: - A warning about an out of date net driver is printed at startup of clients and server. For current known issues, please visit [this](https://docs.improbable.io/unreal/alpha/known-issues) docs page @@ -126,7 +223,7 @@ For current known issues, please visit [this](https://docs.improbable.io/unreal/ 3. The paths passed in via -MapPaths are flexible ### Bug fixes: -- StartPlayInEditorGameInstance() now correctly call OnStart() on PIE_Client - (@DW-Sebastien) +- StartPlayInEditorGameInstance() now correctly call OnStart() on PIE_Client - (@DW-Sebastien) - Redirect logging in the cloud to output to the correct file - Changed type of key in `TMap` so Linux build will not give errors - Disabled loopback of component updates @@ -150,7 +247,7 @@ For current known issues, please visit [this](https://docs.improbable.io/unreal/ - Fixed up default connection flows - Fixed issue will stale shadow data when crossing worker boundaries. - Removed actors from replication consider list if Unreal server-worker is not authoritative over said actor -- Remove legacy flag "qos_max_unacked_pings_rate" in generated default config - (@DW-Sebastien) +- Remove legacy flag "qos_max_unacked_pings_rate" in generated default config - (@DW-Sebastien) ### External contributors: @DW-Sebastien @@ -161,12 +258,12 @@ For current known issues, please visit [this](https://docs.improbable.io/unreal/ Support for the new Player Auth APIs has been added and general stability improvements. -### New Known Issues: +### New Known Issues: Level streaming is currently not supported. For other current known issues, please visit [this docs page](https://docs.improbable.io/unreal/alpha/known-issues). ### Features: -* Support for the new Player Auth APIs +* Support for the new Player Auth APIs * FUniqueNetId support * Support for the new network protocol KCP * Lazy loading of FClassInfo diff --git a/README.md b/README.md index 08f1477521..4bc7934acc 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,45 @@ -# The SpatialOS Game Development Kit for Unreal (alpha) -The SpatialOS Game Development Kit (GDK) for Unreal is an Unreal Engine 4 (UE4) [plugin (Unreal documentation)](https://docs.unrealengine.com/en-us/Programming/Plugins) which allows you to host your game and combine multiple dedicated server instances across one seamless game world, whilst using the Unreal Engine networking API. +# The SpatialOS GDK for Unreal Plugin -The GDK offers: -* **Multiserver support**: leveraging our cloud platform [SpatialOS](https://docs.improbable.io/reference/latest/shared/concepts/spatialos), the GDK allows you to run multiple game servers in a single game instance so your Unreal-developed games can have more players, more Actors, and better gameplay systems than previously possible. +![](SpatialGDK/Documentation/spatialos-gdkforunreal-header.png) -* **An Unreal-native experience:** keeping traditional workflows and networking APIs that Unreal Engine developers are familiar with, the GDK introduces new native-feeling concepts that turn a single-server engine into a distributed one. This enables the GDK to retain the functionality of the networking features which Unreal offers out of the box, including transform synchronization, character movement, and map travel. -* **An easy onboarding experience**: we have made sure it’s easy to get started with the GDK by including a Starter Template which you can use as a tour of SpatialOS and a base for your own game, as well as a guide to porting your current multiplayer Unreal game to run on SpatialOS. +The SpatialOS Game Development Kit (GDK) for Unreal is an Unreal Engine plugin which gives you the features of [SpatialOS](https://spatialos.improbable.io/docs/reference/latest), within the familiar workflows and APIs of Unreal Engine. For more information, please see the GDK's [documentation website](https://docs.improbable.io/unreal/latest). + +If you’re an Unreal game developer and you’re ready to try out the GDK, follow the [Get started guide](https://docs.improbable.io/unreal/latest/content/get-started/introduction). ->This is an [alpha](https://docs.improbable.io/reference/latest/shared/release-policy#maturity-stages) release of the SpatialOS GDK for Unreal, pending stability and performance improvements. The API may change as we learn from feedback - see the guidance on [Recommended use](#recommended-use), below. +> The SpatialOS GDK for Unreal is in alpha. It is ready to use for development of single-server games, but not recommended for public releases. We are committed to rapid development of the GDK to provide a performant release - for information on this, see our [development roadmap](https://github.com/spatialos/UnrealGDK/projects/1) and [Unreal features support](https://docs.improbable.io/unreal/latest/unreal-features-support) pages, and contact us via our forums, or on Discord. ----- -* [Get started](https://docs.improbable.io/unreal/latest/content/get-started/introduction) (on the SpatialOS documentation website) -* [Documentation](https://docs.improbable.io/unreal/latest) (on the SpatialOS documentation website) -* [Development roadmap](https://github.com/spatialos/UnrealGDK/projects/1) (Github project board) -* Community: [Discord](https://discordapp.com/channels/311273633307951114/339471548647866368) - [Forums](https://forums.improbable.io/) - [Mailing list](http://go.pardot.com/l/169082/2018-06-15/27ld2t) ----- +## Where to get the GDK and related projects +The GDK and its related projects are available on GitHub. +* [GDK: github.com/spatialos/UnrealGDK](https://github.com/spatialos/UnrealGDK) +* [The SpatialOS Unreal Engine fork](https://github.com/improbableio/UnrealEngine) +* [The Example Project](https://github.com/spatialos/UnrealGDKExampleProject) -## Recommended use -We are releasing the GDK in [alpha](https://docs.improbable.io/reference/latest/shared/release-policy#maturity-stages) so we can react to feedback and iterate on development quickly. To facilitate this, during our alpha stage we don't have a formal deprecation cycle for APIs and workflows. This means that everything and anything can change. In addition, documentation is limited and some aspects of the GDK are not optimized. +## Unreal Engine changes +In order to transform Unreal from a single server engine to a distributed model, we have made a number of small changes to the UE4 code. We will attempt to consolidate and remove (or submit as PR to Epic) as many of these changes as possible. You can see the changes in our forked [Unreal Engine repo](https://github.com/improbableio/UnrealEngine). + +> In order to get access to this fork, you need to link your GitHub account to a verified Epic Games account, and to have agreed to Epic's license. You will not be able to use the GDK for Unreal without doing this first. To do this, see the [Unreal documentation](https://www.unrealengine.com/en-US/ue4-on-github). -Given this, for now we recommend using the GDK in projects in the early production or prototype stage. This ensures that your project's requirements are in line with the GDK's timeline. +## Recommended use +The GDK is in [alpha](https://docs.improbable.io/reference/latest/shared/release-policy#maturity-stages) so we can react to feedback and iterate on development quickly. To facilitate this, during our alpha stage we don't have a formal deprecation cycle for APIs and workflows. This means that everything and anything can change. -Although the GDK is not fully ready in terms of performance, stability and documentation yet, this is a great time to get involved and shape it with us. We are committed to improving the GDK rapidly, aiming for a beta release in Q1 2019. +We recommend using the GDK in projects in the early production or prototype stage. This ensures that your project's requirements are in line with the GDK's timeline. -See the [full feature list](https://docs.improbable.io/unreal/latest/features) on the documentation website. +## Versioning and support +Please visit [this page](https://docs.improbable.io/unreal/latest/content/pricing-and-support/versioning-scheme) for a description of the GDK's versioning scheme, and which branches to use when developing. ## Contributions -We are not currently accepting public contributions - see our [contributions](https://docs.improbable.io/unreal/latest/contributing) policy. However, we are accepting issues and we do want your feedback. +We welcome [Github issues](https://github.com/spatialos/UnrealGDK/issues) from all users, and accept public contributions subject to the signing of our Contributors License Agreement - please see our [contributions](CONTRIBUTING.md) policy for more details. ## Run into problems? * [Troubleshooting](https://docs.improbable.io/unreal/latest/content/troubleshooting) -* [Known issues](https://docs.improbable.io/unreal/latest/known-issues) - -## Unreal Engine changes -In order to transform Unreal from a single server engine to a distributed model, we had to make a small number of changes to UE4 code. We will attempt to consolidate and remove (or submit as PR to Epic) as many of these changes as possible. You can see the changes in our forked [Unreal Engine repo](https://github.com/improbableio/UnrealEngine). -> You may get a 404 error from this link. To get access, see [these instructions](https://docs.improbable.io/unreal/latest/setup-and-installing#unreal-engine-eula)
+* [Known issues](https://github.com/spatialos/UnrealGDK/projects/2) ## Give us feedback We have released the GDK for Unreal this early in development because we want your feedback. Please come and talk to us about the software and the documentation via: [Discord](https://discordapp.com/channels/311273633307951114/339471548647866368) - [Forums](https://forums.improbable.io/) - [GitHub issues in this repository](https://github.com/spatialos/UnrealGDK/issues). -## Where to get the GDK and related projects -The GDK and its related projects are available on GitHub. -* [GDK: github.com/spatialos/UnrealGDK](https://github.com/spatialos/UnrealGDK) -* [The SpatialOS Unreal Engine fork](https://github.com/improbableio/UnrealEngine) -**NOTE:** This link may give you a 404. - -In order to get access to this fork, you need to link your GitHub account to a verified Epic Games account, and to have agreed to Epic's license. You will not be able to use the GDK for Unreal without doing this first. To do this, see the [Unreal documentation](https://www.unrealengine.com/en-US/ue4-on-github). -* [Third-Person Shooter game](https://github.com/spatialos/UnrealGDKThirdPersonShooter) (Not actively developed) -* [The Test Suite](https://github.com/spatialos/UnrealGDKTestSuite)
- - ------ * Your access to and use of the Unreal Engine is governed by the [Unreal Engine End User License Agreement](https://www.unrealengine.com/en-US/previous-versions/udk-licensing-resources?sessionInvalidated=true). Please ensure that you have agreed to those terms before you access or use the Unreal Engine. -* Version: alpha (stability and performance improvements pending) -* GDK repository: [github.com/spatialos/UnrealGDK](https://github.com/spatialos/UnrealGDK) +* Version: alpha -(c) 2018 Improbable +(c) 2019 Improbable diff --git a/RequireSetup b/RequireSetup index 989e328e30..8bb04dff44 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. -17 +30 diff --git a/Setup.bat b/Setup.bat index eb7cd51baa..d17758a503 100644 --- a/Setup.bat +++ b/Setup.bat @@ -27,14 +27,8 @@ call :MarkStartOfBlock "Setup the git hooks" call :MarkEndOfBlock "Setup the git hooks" call :MarkStartOfBlock "Check dependencies" - if not defined UNREAL_HOME ( - echo Error: Please set UNREAL_HOME environment variable to point to the Unreal Engine folder. - pause - exit /b 1 - ) - rem Use Unreal Engine's script to get the path to MSBuild. This turns off echo so turn it back on for TeamCity. - call "%UNREAL_HOME%\Engine\Build\BatchFiles\GetMSBuildPath.bat" + call "%~dp0SpatialGDK\Build\Scripts\FindMSBuild.bat" if not defined MSBUILD_EXE ( echo Error: Could not find the MSBuild executable. Please make sure you have Microsoft Visual Studio or Microsoft Build Tools installed. @@ -52,13 +46,18 @@ call :MarkEndOfBlock "Check dependencies" call :MarkStartOfBlock "Setup variables" set /p PINNED_CORE_SDK_VERSION=<.\SpatialGDK\Extras\core-sdk.version + set /p PINNED_SPOT_VERSION=<.\SpatialGDK\Extras\spot.version set BUILD_DIR=%~dp0SpatialGDK\Build set CORE_SDK_DIR=%BUILD_DIR%\core_sdk set WORKER_SDK_DIR=%~dp0SpatialGDK\Source\SpatialGDK\Public\WorkerSDK set WORKER_SDK_DIR_OLD=%~dp0SpatialGDK\Source\Public\WorkerSdk set BINARIES_DIR=%~dp0SpatialGDK\Binaries\ThirdParty\Improbable + + rem Copy schema to the projects spatial directory. set SCHEMA_COPY_DIR=%~dp0..\..\..\spatial\schema\unreal\gdk set SCHEMA_STD_COPY_DIR=%~dp0..\..\..\spatial\build\dependencies\schema\standard_library + set SPATIAL_DIR=%~dp0..\..\..\spatial + call :MarkEndOfBlock "Setup variables" call :MarkStartOfBlock "Clean folders" @@ -66,7 +65,11 @@ call :MarkStartOfBlock "Clean folders" rd /s /q "%WORKER_SDK_DIR%" 2>nul rd /s /q "%WORKER_SDK_DIR_OLD%" 2>nul rd /s /q "%BINARIES_DIR%" 2>nul - rd /s /q "%SCHEMA_STD_COPY_DIR%" 2>nul + + if exist "%SPATIAL_DIR%" ( + rd /s /q "%SCHEMA_STD_COPY_DIR%" 2>nul + rd /s /q "%SCHEMA_COPY_DIR%" 2>nul + ) call :MarkEndOfBlock "Clean folders" call :MarkStartOfBlock "Create folders" @@ -75,42 +78,51 @@ call :MarkStartOfBlock "Create folders" md "%CORE_SDK_DIR%\tools" >nul 2>nul md "%CORE_SDK_DIR%\worker_sdk" >nul 2>nul md "%BINARIES_DIR%" >nul 2>nul - md "%SCHEMA_STD_COPY_DIR%" >nul 2>nul + + if exist "%SPATIAL_DIR%" ( + md "%SCHEMA_STD_COPY_DIR%" >nul 2>nul + md "%SCHEMA_COPY_DIR%" >nul 2>nul + ) call :MarkEndOfBlock "Create folders" call :MarkStartOfBlock "Retrieve dependencies" - spatial package retrieve tools schema_compiler-x86_64-win32 %PINNED_CORE_SDK_VERSION% "%CORE_SDK_DIR%\tools\schema_compiler-x86_64-win32.zip" - spatial package retrieve schema standard_library %PINNED_CORE_SDK_VERSION% "%CORE_SDK_DIR%\schema\standard_library.zip" - spatial package retrieve worker_sdk c-dynamic-x86-msvc_md-win32 %PINNED_CORE_SDK_VERSION% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86-msvc_md-win32.zip" - spatial package retrieve worker_sdk c-dynamic-x86_64-msvc_md-win32 %PINNED_CORE_SDK_VERSION% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-msvc_md-win32.zip" - spatial package retrieve worker_sdk c-dynamic-x86_64-gcc_libstdcpp-linux %PINNED_CORE_SDK_VERSION% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-gcc_libstdcpp-linux.zip" + spatial package retrieve tools schema_compiler-x86_64-win32 %PINNED_CORE_SDK_VERSION% "%CORE_SDK_DIR%\tools\schema_compiler-x86_64-win32.zip" + spatial package retrieve schema standard_library %PINNED_CORE_SDK_VERSION% "%CORE_SDK_DIR%\schema\standard_library.zip" + spatial package retrieve worker_sdk c-dynamic-x86-msvc_md-win32 %PINNED_CORE_SDK_VERSION% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86-msvc_md-win32.zip" + spatial package retrieve worker_sdk c-dynamic-x86_64-msvc_md-win32 %PINNED_CORE_SDK_VERSION% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-msvc_md-win32.zip" + spatial package retrieve worker_sdk c-dynamic-x86_64-gcc_libstdcpp-linux %PINNED_CORE_SDK_VERSION% "%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-gcc_libstdcpp-linux.zip" + spatial package retrieve worker_sdk c-static-fullylinked-arm-clang_libcpp-ios %PINNED_CORE_SDK_VERSION% "%CORE_SDK_DIR%\worker_sdk\c-static-fullylinked-arm-clang_libcpp-ios.zip" + spatial package retrieve worker_sdk core-dynamic-x86_64-linux %PINNED_CORE_SDK_VERSION% "%CORE_SDK_DIR%\worker_sdk\core-dynamic-x86_64-linux.zip" + spatial package retrieve worker_sdk csharp %PINNED_CORE_SDK_VERSION% "%CORE_SDK_DIR%\worker_sdk\csharp.zip" + spatial package retrieve spot spot-win64 %PINNED_SPOT_VERSION% "%BINARIES_DIR%\Programs\spot.exe" call :MarkEndOfBlock "Retrieve dependencies" call :MarkStartOfBlock "Unpack dependencies" - powershell -Command "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86-msvc_md-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Win32\" -Force; "^ - "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-msvc_md-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Win64\" -Force; "^ - "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-gcc_libstdcpp-linux.zip\" -DestinationPath \"%BINARIES_DIR%\Linux\" -Force; "^ - "Expand-Archive -Path \"%CORE_SDK_DIR%\tools\schema_compiler-x86_64-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Programs\" -Force; "^ - "Expand-Archive -Path \"%CORE_SDK_DIR%\schema\standard_library.zip\" -DestinationPath \"%BINARIES_DIR%\Programs\schema\" -Force;" - + powershell -Command "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86-msvc_md-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Win32\" -Force; "^ + "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-msvc_md-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Win64\" -Force; "^ + "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-dynamic-x86_64-gcc_libstdcpp-linux.zip\" -DestinationPath \"%BINARIES_DIR%\Linux\" -Force; "^ + "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\core-dynamic-x86_64-linux.zip\" -DestinationPath \"%BINARIES_DIR%\Programs\worker_sdk\core\" -Force; "^ + "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\csharp.zip\" -DestinationPath \"%BINARIES_DIR%\Programs\worker_sdk\csharp\" -Force; "^ + "Expand-Archive -Path \"%CORE_SDK_DIR%\worker_sdk\c-static-fullylinked-arm-clang_libcpp-ios.zip\" -DestinationPath \"%BINARIES_DIR%\IOS\" -Force;"^ + "Expand-Archive -Path \"%CORE_SDK_DIR%\tools\schema_compiler-x86_64-win32.zip\" -DestinationPath \"%BINARIES_DIR%\Programs\" -Force; "^ + "Expand-Archive -Path \"%CORE_SDK_DIR%\schema\standard_library.zip\" -DestinationPath \"%BINARIES_DIR%\Programs\schema\" -Force;" xcopy /s /i /q "%BINARIES_DIR%\Win64\include" "%WORKER_SDK_DIR%" call :MarkEndOfBlock "Unpack dependencies" -call :MarkStartOfBlock "Copy standard library schema" - echo Copying standard library schemas to "%SCHEMA_STD_COPY_DIR%" - xcopy /s /i /q "%BINARIES_DIR%\Programs\schema" "%SCHEMA_STD_COPY_DIR%" -call :MarkEndOfBlock "Copy standard library schema" - -call :MarkStartOfBlock "Copy GDK schema" - rd /s /q "%SCHEMA_COPY_DIR%" 2>nul - md "%SCHEMA_COPY_DIR%" >nul 2>nul +if exist "%SPATIAL_DIR%" ( + call :MarkStartOfBlock "Copy standard library schema" + echo Copying standard library schemas to "%SCHEMA_STD_COPY_DIR%" + xcopy /s /i /q "%BINARIES_DIR%\Programs\schema" "%SCHEMA_STD_COPY_DIR%" + call :MarkEndOfBlock "Copy standard library schema" - echo Copying schemas to "%SCHEMA_COPY_DIR%". - xcopy /s /i /q "%~dp0\SpatialGDK\Extras\schema" "%SCHEMA_COPY_DIR%" -call :MarkEndOfBlock "Copy GDK schema" + call :MarkStartOfBlock "Copy GDK schema" + echo Copying schemas to "%SCHEMA_COPY_DIR%". + xcopy /s /i /q "%~dp0\SpatialGDK\Extras\schema" "%SCHEMA_COPY_DIR%" + call :MarkEndOfBlock "Copy GDK schema" +) call :MarkStartOfBlock "Build C# utilities" - %MSBUILD_EXE% /nologo /verbosity:minimal .\SpatialGDK\Build\Programs\Improbable.Unreal.Scripts\Improbable.Unreal.Scripts.sln /property:Configuration=Release + %MSBUILD_EXE% /nologo /verbosity:minimal .\SpatialGDK\Build\Programs\Improbable.Unreal.Scripts\Improbable.Unreal.Scripts.sln /property:Configuration=Release /restore call :MarkEndOfBlock "Build C# utilities" call :MarkEndOfBlock "%~0" diff --git a/Setup.sh b/Setup.sh index c494c9fec8..46b30c4e9b 100755 --- a/Setup.sh +++ b/Setup.sh @@ -4,7 +4,6 @@ # This file is experimental and is not maintained directly by Improbable. Please use at your own risk. set -e -u -o pipefail -if [ -n "${TEAMCITY_CAPTURE_ENV:-}" ]; then set -x; else set +x; fi if [ "$(uname -s)" != "Darwin" ]; then echo "This script should only be used on OS X. If you are using Windows, please run Setup.bat." @@ -12,19 +11,11 @@ if [ "$(uname -s)" != "Darwin" ]; then fi function markStartOfBlock { - if [ -n "${TEAMCITY_CAPTURE_ENV:-}" ]; then - echo -e "/x23/x23teamcity[blockOpened name='$1']" - else - echo "Starting: $1" - fi + echo "Starting: $1" } function markEndOfBlock { - if [ -n "${TEAMCITY_CAPTURE_ENV:-}" ]; then - echo -e "/x23/x23teamcity[blockClosed name='$1']" - else - echo "Finished: $1" - fi + echo "Finished: $1" } pushd "$(dirname "$0")" @@ -32,7 +23,7 @@ pushd "$(dirname "$0")" markStartOfBlock "$0" markStartOfBlock "Setup the git hooks" - if [ -z "${TEAMCITY_CAPTURE_ENV:-}" -a -e .git/hooks ]; then + if [ -e .git/hooks ]; then # Remove the old post-checkout hook. if [ -e .git/hooks/post-checkout ]; then rm -f .git/hooks/post-checkout; fi @@ -72,7 +63,7 @@ markStartOfBlock "Setup variables" PINNED_CORE_SDK_VERSION=$(cat ./SpatialGDK/Extras/core-sdk.version) BUILD_DIR="$(dirname "$0")/SpatialGDK/Build" CORE_SDK_DIR="$BUILD_DIR/core_sdk" - WORKER_SDK_DIR="$(dirname "$0")/SpatialGDK/Source/SpatialGDK/Public/WorkerSdk" + WORKER_SDK_DIR="$(dirname "$0")/SpatialGDK/Source/SpatialGDK/Public/WorkerSDK" BINARIES_DIR="$(dirname "$0")/SpatialGDK/Binaries/ThirdParty/Improbable" SCHEMA_COPY_DIR="$(dirname "$0")/../../../spatial/schema/unreal/gdk" SCHEMA_STD_COPY_DIR="$(dirname "$0")/../../../spatial/build/dependencies/schema/standard_library" @@ -102,6 +93,8 @@ markStartOfBlock "Retrieve dependencies" spatial package retrieve worker_sdk c-dynamic-x86_64-gcc_libstdcpp-linux $PINNED_CORE_SDK_VERSION $CORE_SDK_DIR/worker_sdk/c-dynamic-x86_64-gcc_libstdcpp-linux.zip spatial package retrieve worker_sdk c-dynamic-x86_64-clang_libcpp-macos $PINNED_CORE_SDK_VERSION $CORE_SDK_DIR/worker_sdk/c-dynamic-x86_64-clang_libcpp-macos.zip spatial package retrieve worker_sdk c-static-fullylinked-arm-clang_libcpp-ios $PINNED_CORE_SDK_VERSION $CORE_SDK_DIR/worker_sdk/c-static-fullylinked-arm-clang_libcpp-ios.zip + spatial package retrieve worker_sdk core-dynamic-x86_64-linux $PINNED_CORE_SDK_VERSION $CORE_SDK_DIR/worker_sdk/core-dynamic-x86_64-linux.zip + spatial package retrieve worker_sdk csharp $PINNED_CORE_SDK_VERSION $CORE_SDK_DIR/worker_sdk/csharp.zip markEndOfBlock "Retrieve dependencies" markStartOfBlock "Unpack dependencies" @@ -110,6 +103,8 @@ markStartOfBlock "Unpack dependencies" unzip -oq $CORE_SDK_DIR/worker_sdk/c-dynamic-x86_64-gcc_libstdcpp-linux.zip -d $BINARIES_DIR/Linux/ unzip -oq $CORE_SDK_DIR/worker_sdk/c-dynamic-x86_64-clang_libcpp-macos.zip -d $BINARIES_DIR/Mac/ unzip -oq $CORE_SDK_DIR/worker_sdk/c-static-fullylinked-arm-clang_libcpp-ios.zip -d $BINARIES_DIR/IOS/ + unzip -oq $CORE_SDK_DIR/worker_sdk/core-dynamic-x86_64-linux.zip -d $BINARIES_DIR/Programs/worker_sdk/core/ + unzip -oq $CORE_SDK_DIR/worker_sdk/csharp.zip -d $BINARIES_DIR/Programs/worker_sdk/csharp/ unzip -oq $CORE_SDK_DIR/tools/schema_compiler-x86_64-win32.zip -d $BINARIES_DIR/Programs/ unzip -oq $CORE_SDK_DIR/schema/standard_library.zip -d $BINARIES_DIR/Programs/schema/ @@ -118,7 +113,7 @@ markEndOfBlock "Unpack dependencies" markStartOfBlock "Copy standard library schema" echo "Copying standard library schemas to $SCHEMA_STD_COPY_DIR" - cp -R $BINARIES_DIR/Programs/schema $SCHEMA_STD_COPY_DIR + cp -R $BINARIES_DIR/Programs/schema/* $SCHEMA_STD_COPY_DIR markEndOfBlock "Copy standard library schema" markStartOfBlock "Copy GDK schema" @@ -126,11 +121,11 @@ markStartOfBlock "Copy GDK schema" mkdir -p $SCHEMA_COPY_DIR >/dev/null 2>/dev/null echo "Copying schemas to $SCHEMA_COPY_DIR." - cp -R $(dirname %0)/SpatialGDK/Extras/schema $SCHEMA_COPY_DIR + cp -R $(dirname %0)/SpatialGDK/Extras/schema/* $SCHEMA_COPY_DIR markEndOfBlock "Copy GDK schema" markStartOfBlock "Build C# utilities" - msbuild /nologo /verbosity:minimal ./SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Mac/Improbable.Unreal.Scripts.sln /property:Configuration=Release + msbuild /nologo /verbosity:minimal ./SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Mac/Improbable.Unreal.Scripts.sln /property:Configuration=Release /restore markEndOfBlock "Build C# utilities" markEndOfBlock "$0" diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs index 4e3a0ba6ca..e1ddb80cb2 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.cs @@ -1,65 +1,18 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + using System; using System.IO; using System.Linq; +using System.Reflection; using System.Text; +using Microsoft.Win32; +using Newtonsoft.Json.Linq; +using LinuxScripts = Improbable.Unreal.Build.Common.LinuxScripts; namespace Improbable { public static class Build { - private const string UnrealWorkerShellScript = -@"#!/bin/bash -NEW_USER=unrealworker -WORKER_ID=$1 -LOG_FILE=$2 -shift 2 - -# 2>/dev/null silences errors by redirecting stderr to the null device. This is done to prevent errors when a machine attempts to add the same user more than once. -useradd $NEW_USER -m -d /improbable/logs/UnrealWorker/Logs 2>/dev/null -chown -R $NEW_USER:$NEW_USER $(pwd) 2>/dev/null -chmod -R o+rw /improbable/logs 2>/dev/null - -# Create log file in case it doesn't exist and redirect stdout and stderr to the file. -touch ""${{LOG_FILE}}"" -exec 1>>""${{LOG_FILE}}"" -exec 2>&1 - -SCRIPT=""$(pwd)/{0}Server.sh"" - -if [ ! -f $SCRIPT ]; then - echo ""Expected to run ${{SCRIPT}} but file not found!"" - exit 1 -fi - -chmod +x $SCRIPT -echo ""Running ${{SCRIPT}} to start worker..."" -gosu $NEW_USER ""${{SCRIPT}}"" ""$@"""; - - - // This is for internal use only. We do not support Linux clients. - private const string SimulatedPlayerWorkerShellScript = -@"#!/bin/bash -NEW_USER=unrealworker -WORKER_ID=$1 -WORKER_NAME=$2 -shift 2 - -# 2>/dev/null silences errors by redirecting stderr to the null device. This is done to prevent errors when a machine attempts to add the same user more than once. -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)/${WORKER_NAME}.sh"" -chmod +x $SCRIPT >> ""/improbable/logs/${WORKER_ID}.log"" 2>&1 - -echo ""Trying to launch worker ${WORKER_NAME} with id ${WORKER_ID}"" > ""/improbable/logs/${WORKER_ID}.log"" -gosu $NEW_USER ""${SCRIPT}"" ""$@"" >> ""/improbable/logs/${WORKER_ID}.log"" 2>&1"; - - private const string RunEditorScript = - @"setlocal ENABLEDELAYEDEXPANSION -%UNREAL_HOME%\Engine\Binaries\Win64\UE4Editor.exe ""{0}"" %* -exit /b !ERRORLEVEL! -"; - public static void Main(string[] args) { var help = args.Count(arg => arg == "/?" || arg.ToLowerInvariant() == "--help") > 0; @@ -86,10 +39,69 @@ public static void Main(string[] args) var noCompile = args.Count(arg => arg.ToLowerInvariant() == "-nocompile") > 0; var additionalUATArgs = string.Join(" ", args.Skip(4).Where(arg => arg.ToLowerInvariant() != "-nocompile")); - var stagingDir = Path.GetFullPath(Path.Combine("../spatial", "build", "unreal")); - var outputDir = Path.GetFullPath(Path.Combine("../spatial", "build", "assembly", "worker")); + var stagingDir = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(projectFile), "../spatial", "build", "unreal")); + var outputDir = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(projectFile), "../spatial", "build", "assembly", "worker")); var baseGameName = Path.GetFileNameWithoutExtension(projectFile); + // Locate the Unreal Engine. + Console.WriteLine("Finding Unreal Engine build."); + string uproject = File.ReadAllText(projectFile, Encoding.UTF8); + + dynamic projectJson = JObject.Parse(uproject); + string engineAssociation = projectJson.EngineAssociation; + + Console.WriteLine("Engine Association: " + engineAssociation); + + string unrealEngine = ""; + + // If the engine association is empty then climb the parent directories of the project looking for the Unreal Engine root directory. + if (string.IsNullOrEmpty(engineAssociation)) + { + DirectoryInfo currentDir = new DirectoryInfo(Directory.GetCurrentDirectory()); + while (currentDir.Parent != null) + { + currentDir = currentDir.Parent; + // This is how Unreal asserts we have a valid root directory for the Unreal Engine. Must contain 'Engine/Binaries' and 'Engine/Build'. (FDesktopPlatformBase::IsValidRootDirectory) + if (Directory.Exists(Path.Combine(currentDir.FullName, "Engine", "Binaries")) && Directory.Exists(Path.Combine(currentDir.FullName, "Engine", "Build"))) + { + unrealEngine = currentDir.FullName; + break; + } + } + } + else if (Directory.Exists(Path.Combine(Path.GetDirectoryName(projectFile), engineAssociation))) // If the engine association is a path then use that. + { + unrealEngine = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(projectFile), engineAssociation)); + } + else + { + // Finally check the registry for the path using the engine association as the key. + string unrealEngineBuildKey = "HKEY_CURRENT_USER\\Software\\Epic Games\\Unreal Engine\\Builds"; + var unrealEngineValue = Registry.GetValue(unrealEngineBuildKey, engineAssociation, ""); + + if (unrealEngineValue != null) + { + unrealEngine = unrealEngineValue.ToString(); + } + else + { + Console.Error.WriteLine("Engine Association not found in the registry! Please run Setup.bat from within the UnrealEngine."); + } + } + + if (string.IsNullOrEmpty(unrealEngine)) + { + Console.Error.WriteLine("Could not find the Unreal Engine. Please associate your '.uproject' with an engine version or ensure this game project is nested within an engine build."); + Environment.Exit(1); + } + else + { + Console.WriteLine("Engine is at: " + unrealEngine); + } + + string runUATBat = Path.Combine(unrealEngine, @"Engine\Build\BatchFiles\RunUAT.bat"); + string buildBat = Path.Combine(unrealEngine, @"Engine\Build\BatchFiles\Build.bat"); + if (gameName == baseGameName + "Editor") { if (noCompile) @@ -99,8 +111,8 @@ public static void Main(string[] args) else { Common.WriteHeading(" > Building Editor for use as a managed worker."); - - Common.RunRedirected(@"%UNREAL_HOME%\Engine\Build\BatchFiles\Build.bat", new[] + + Common.RunRedirected(buildBat, new[] { gameName, platform, @@ -115,12 +127,19 @@ public static void Main(string[] args) Directory.CreateDirectory(windowsEditorPath); } + var PathToUnrealEditor = Path.Combine(unrealEngine, "Engine\\Binaries\\Win64\\UE4Editor.exe"); + + var StartEditorScript = +$@"setlocal ENABLEDELAYEDEXPANSION +{PathToUnrealEditor} {projectFile} %* +exit /b !ERRORLEVEL!"; + // Write a simple batch file to launch the Editor as a managed worker. File.WriteAllText(Path.Combine(windowsEditorPath, "StartEditor.bat"), - string.Format(RunEditorScript, projectFile), new UTF8Encoding(false)); + StartEditorScript, new UTF8Encoding(false)); // The runtime currently requires all workers to be in zip files. Zip the batch file. - Common.RunRedirected(@"%UNREAL_HOME%\Engine\Build\BatchFiles\RunUAT.bat", new[] + Common.RunRedirected(runUATBat, new[] { "ZipUtils", "-add=" + Quote(windowsEditorPath), @@ -130,7 +149,7 @@ public static void Main(string[] args) else if (gameName == baseGameName) { Common.WriteHeading(" > Building client."); - Common.RunRedirected(@"%UNREAL_HOME%\Engine\Build\BatchFiles\RunUAT.bat", new[] + Common.RunRedirected(runUATBat, new[] { "BuildCookRun", noCompile ? "-nobuild" : "-build", @@ -176,21 +195,17 @@ public static void Main(string[] args) Console.WriteLine("Could not find the executable to rename."); } - Common.RunRedirected(@"%UNREAL_HOME%\Engine\Build\BatchFiles\RunUAT.bat", new[] + Common.RunRedirected(runUATBat, new[] { "ZipUtils", "-add=" + Quote(windowsNoEditorPath), "-archive=" + Quote(Path.Combine(outputDir, "UnrealClient@Windows.zip")), }); } - else if (gameName == baseGameName + "FakeClient") - { - Common.WriteWarning("'FakeClient' has been renamed to 'SimulatedPlayer', please use this instead. It will create the same assembly under a different name: UnrealSimulatedPlayer@Linux.zip."); - } else if (gameName == baseGameName + "SimulatedPlayer") // This is for internal use only. We do not support Linux clients. { Common.WriteHeading(" > Building simulated player."); - Common.RunRedirected(@"%UNREAL_HOME%\Engine\Build\BatchFiles\RunUAT.bat", new[] + Common.RunRedirected(runUATBat, new[] { "BuildCookRun", "-build", @@ -219,9 +234,13 @@ public static void Main(string[] args) }); var linuxSimulatedPlayerPath = Path.Combine(stagingDir, "LinuxNoEditor"); - File.WriteAllText(Path.Combine(linuxSimulatedPlayerPath, "StartWorker.sh"), SimulatedPlayerWorkerShellScript.Replace("\r\n", "\n"), new UTF8Encoding(false)); + LinuxScripts.WriteWithLinuxLineEndings(LinuxScripts.GetSimulatedPlayerWorkerShellScript(baseGameName), Path.Combine(linuxSimulatedPlayerPath, "StartSimulatedClient.sh")); + LinuxScripts.WriteWithLinuxLineEndings(LinuxScripts.GetSimulatedPlayerCoordinatorShellScript(baseGameName), Path.Combine(linuxSimulatedPlayerPath, "StartCoordinator.sh")); - var workerCoordinatorPath = Path.GetFullPath(Path.Combine("../spatial", "build", "dependencies", "WorkerCoordinator")); + // Coordinator files are located in ./UnrealGDK/SpatialGDK/Binaries/ThirdParty/Improbable/Programs/WorkerCoordinator/. + // Executable of this build script is in ./UnrealGDK/SpatialGDK/Binaries/ThirdParty/Improbable/Programs/Build.exe + // Assembly.GetEntryAssembly().Location gives the location of the Build.exe executable. + var workerCoordinatorPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "./WorkerCoordinator")); if (Directory.Exists(workerCoordinatorPath)) { Common.RunRedirected("xcopy", new[] @@ -231,13 +250,14 @@ public static void Main(string[] args) workerCoordinatorPath, linuxSimulatedPlayerPath }); - } else + } + else { - Console.WriteLine("worker coordinator path did not exist"); + Common.WriteWarning($"Worker coordinator binary not found at {workerCoordinatorPath}. Please run Setup.bat to build the worker coordinator."); } var archiveFileName = "UnrealSimulatedPlayer@Linux.zip"; - Common.RunRedirected(@"%UNREAL_HOME%\Engine\Build\BatchFiles\RunUAT.bat", new[] + Common.RunRedirected(runUATBat, new[] { "ZipUtils", "-add=" + Quote(linuxSimulatedPlayerPath), @@ -247,7 +267,7 @@ public static void Main(string[] args) else if (gameName == baseGameName + "Server") { Common.WriteHeading(" > Building worker."); - Common.RunRedirected(@"%UNREAL_HOME%\Engine\Build\BatchFiles\RunUAT.bat", new[] + Common.RunRedirected(runUATBat, new[] { "BuildCookRun", noCompile ? "-nobuild" : "-build", @@ -283,10 +303,10 @@ public static void Main(string[] args) { // Write out the wrapper shell script to work around issues between UnrealEngine and our cloud Linux environments. // Also ensure script uses Linux line endings - File.WriteAllText(Path.Combine(serverPath, "StartWorker.sh"), string.Format(UnrealWorkerShellScript, baseGameName).Replace("\r\n", "\n"), new UTF8Encoding(false)); + LinuxScripts.WriteWithLinuxLineEndings(LinuxScripts.GetUnrealWorkerShellScript(baseGameName), Path.Combine(serverPath, "StartWorker.sh")); } - Common.RunRedirected(@"%UNREAL_HOME%\Engine\Build\BatchFiles\RunUAT.bat", new[] + Common.RunRedirected(runUATBat, new[] { "ZipUtils", "-add=" + Quote(serverPath), @@ -297,7 +317,7 @@ public static void Main(string[] args) { // Pass-through to Unreal's Build.bat. Common.WriteHeading($" > Building ${gameName}."); - Common.RunRedirected(@"%UNREAL_HOME%\Engine\Build\BatchFiles\Build.bat", new[] + Common.RunRedirected(buildBat, new[] { gameName, platform, diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.csproj b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.csproj index 2574aed143..e275471d8f 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.csproj +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Build/Build.csproj @@ -34,6 +34,9 @@ 4 + + ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll + @@ -56,9 +59,14 @@ + + + - copy "$(SolutionDir)\$(ProjectName)\$(OutDir)\$(ProjectName).exe" "$(SolutionDir)..\..\..\Binaries\ThirdParty\Improbable\Programs\" /Y + copy "$(SolutionDir)\$(ProjectName)\$(OutDir)\$(ProjectName).exe" "$(SolutionDir)..\..\..\Binaries\ThirdParty\Improbable\Programs\" /Y + +copy "$(SolutionDir)\$(ProjectName)\$(OutDir)\Newtonsoft.Json.dll" "$(SolutionDir)..\..\..\Binaries\ThirdParty\Improbable\Programs\" /Y + + diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Program.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Program.cs new file mode 100644 index 0000000000..4cfc026b64 --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Program.cs @@ -0,0 +1,100 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using CommandLine; +using Improbable.Codegen.Base; +using Improbable.CodeGen.Base; +using Improbable.CodeGen.Unreal; + +namespace CodeGenerator +{ + public class Options + { + [Option("input-bundle", Required = true, + HelpText = "The path to the JSON Bundle file output by the SpatialOS schema_compiler.")] + public string InputBundle { get; set; } + + [Option("output-dir", Required = true, + HelpText = "The path to write the generated code to.")] + public string OutputDir { get; set; } + } + + internal class Program + { + static readonly uint ExternalComponentIdLowerBound = 1000; + static readonly uint ExternalComponentIdUpperBound = 20000000; + + private static void Main(string[] args) + { + Parser.Default.ParseArguments(args) + .WithParsed(Run) + .WithNotParsed(errors => + { + foreach (var error in errors) + { + Console.Error.WriteLine(error); + } + + Environment.ExitCode = 1; + }); + } + + private static void Run(Options options) + { + try + { + var bundle = SchemaBundleLoader.LoadBundle(options.InputBundle); + + ValidateBundle(bundle); + + var generators = new List + { + new UnrealGenerator() + }; + + var output = new List(); + + generators.ForEach((ICodeGenerator g) => output.AddRange(g.GenerateFiles(bundle))); + + foreach (var generatedFile in output) + { + WriteFile(options.OutputDir, generatedFile); + } + } + catch (Exception exception) + { + Console.Error.WriteLine(exception); + Environment.ExitCode = 1; + } + } + + private static void ValidateBundle(Bundle bundle) + { + foreach (var component in bundle.Components) + { + if (component.Value.ComponentId < ExternalComponentIdLowerBound || component.Value.ComponentId > ExternalComponentIdUpperBound) + { + throw new Exception($@"External schema component IDs must be in the range {ExternalComponentIdLowerBound}-{ExternalComponentIdUpperBound} +Component {component.Value.QualifiedName} has ID: {component.Value.ComponentId}"); + } + } + } + + private static void WriteFile(string codegenOutputFolder, GeneratedFile file) + { + var generatedFilePath = Path.Combine(codegenOutputFolder, file.RelativeFilePath); + var generatedFileDirectory = Path.GetDirectoryName(generatedFilePath); + if (!Directory.Exists(generatedFileDirectory)) + { + Directory.CreateDirectory(generatedFileDirectory); + } + + File.WriteAllText(generatedFilePath, file.Contents, Encoding.UTF8); + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/README.md b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/README.md new file mode 100644 index 0000000000..fe17db8729 --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/README.md @@ -0,0 +1,863 @@ +# C++ Schema Codegen (with C serialization interop) + +Build with .NET Core 2.2.102 + +> Only tested on Windows. + +* Built on top of Jared's codegen framework (current exists [here](https://github.com/improbable/dotnet_core_worker/tree/feature/new-inventory-worker)). +* Visual Studio solution which can be built out to an executable or used in-editor to debug. +* Parameterized by an input schema bundle and desired output directory for generated code. +* No guaranteed resilience for different SpatialOS versions, current implementation is `13.6.0` (would suggest changing the version in `./Test.sh`, running it, and observing any compile errors). +* This is intended as both a building block for customers to build on top of, and reference for those wishing to build code generators from scratch. +* This lacks even the most basic testing. +* Few things to fix still: mutual type referencing leading to circular includes, catching for using C++ keywords as fields, fields and events whose generated names collide, components in utils namespace that collide with helper functions. + +## Building the solution + +The executable can be created by either: +* Running the `Setup.bat` script. +* Building the `Improbable.Unreal.Scripts.sln` solution in JetBrains Rider, Visual Studio, Visual Studio Code. + +## Running from Visual Studio + +For debugging purposes, the code generator can be run from Visual Studio. To do this, you'll need to add values for the `input-bundle` and `output-dir` parameters. + +This can be done through right-clicking the `CodeGenerator` project in the Solution Explorer, and inserting into `Properties -> Debug -> Application arguments`. + +## Example Output + +For each type defined in a SpatialOS schema file, a header and source are generated. For example. below are schema files generated from the TestComponent component schema: + +``` +component TestComponent { + id = 198703; + data TestComponentData; + + [EmptyType] + event TestComponentData.EmptyEvent empty; + event TestComponentData.ParameterizedEvent params; + + [EmptyType] + command Integer test_command(Integer); + command Nested.Recursive another_test_command(Integer); +} +``` + +### TestComponent.h + +```cpp +namespace improbable { +namespace test_schema { +// Generated from Testing/Schema/test.schema(57,1) +class TestComponent +{ +public: + static const Worker_ComponentId ComponentId = 198703; + + // Creates a new instance with specified arguments for each field. + TestComponent(std::int32_t TestInt, float TestFloat, bool TestBool, double TestDouble, ::improbable::List TestList, ::improbable::Map TestMap, std::vector TestBytes); + // Creates a new instance with default values for each field. + TestComponent(); + // Creates a new instance with default values for each field. This is + // equivalent to a default-constructed instance. + static TestComponent Create() { return {}; } + // Copyable and movable. + TestComponent(TestComponent&&) = default; + TestComponent(const TestComponent&) = default; + TestComponent& operator=(TestComponent&&) = default; + TestComponent& operator=(const TestComponent&) = default; + ~TestComponent() = default; + + bool operator==(const TestComponent&) const; + bool operator!=(const TestComponent&) const; + + // Serialize this object data into the C API argument + void serialize(Schema_ComponentData* component_data) const; + + // Deserialize the C API object argument into an instance of this class and return it + static TestComponent deserialize(Schema_ComponentData* component_data); + + // Field test_int = 1 + std::int32_t get_test_int() const; + std::int32_t& get_test_int(); + TestComponent& set_test_int(std::int32_t); + + // Field test_float = 2 + float get_test_float() const; + float& get_test_float(); + TestComponent& set_test_float(float); + + // Field test_bool = 3 + bool get_test_bool() const; + bool& get_test_bool(); + TestComponent& set_test_bool(bool); + + // Field test_double = 4 + double get_test_double() const; + double& get_test_double(); + TestComponent& set_test_double(double); + + // Field test_list = 5 + const ::improbable::List& get_test_list() const; + ::improbable::List& get_test_list(); + TestComponent& set_test_list(const ::improbable::List&); + + // Field test_map = 6 + const ::improbable::Map& get_test_map() const; + ::improbable::Map& get_test_map(); + TestComponent& set_test_map(const ::improbable::Map&); + + // Field test_bytes = 7 + std::vector get_test_bytes() const; + std::vector& get_test_bytes(); + TestComponent& set_test_bytes(std::vector); + +private: + std::int32_t _test_int; + float _test_float; + bool _test_bool; + double _test_double; + ::improbable::List _test_list; + ::improbable::Map _test_map; + std::vector _test_bytes; + +public: + class Update + { + // Creates a new instance with default values for each field. + Update(); + // Creates a new instance with default values for each field. This is + // equivalent to a default-constructed instance. + static Update Create() { return {}; } + // Copyable and movable. + Update(Update&&) = default; + Update(const Update&) = default; + Update& operator=(Update&&) = default; + Update& operator=(const Update&) = default; + ~Update() = default; + bool operator==(const Update&) const; + bool operator!=(const Update&) const; + + // Creates an Update from a ::improbable::test_schema::TestComponent object. + static Update FromInitialData(const ::improbable::test_schema::TestComponent& data); + + /** + * Converts to a ::improbable::test_schema::TestComponent + * object. It is an error to call this function unless *all* of the optional fields in this + * update are filled in. + */ + ::improbable::test_schema::TestComponent ToInitialData() const; + + /** + * Replaces fields in the given ::improbable::test_schema::TestComponent + * object with the corresponding fields in this update, where present. + */ + void ApplyTo(::improbable::test_schema::TestComponent&) const; + + // Serialize this update object data into the C API component update argument + void serialize(Schema_ComponentUpdate* component_update) const; + + // Deserialize the C API component update argument into an instance of this class and return it + static Update deserialize(Schema_ComponentUpdate* component_update); + + // Field test_int = 1 + const ::improbable::Option& get_test_int() const; + ::improbable::Option& get_test_int(); + TestComponent::Update& set_test_int(std::int32_t); + + // Field test_float = 2 + const ::improbable::Option& get_test_float() const; + ::improbable::Option& get_test_float(); + TestComponent::Update& set_test_float(float); + + // Field test_bool = 3 + const ::improbable::Option& get_test_bool() const; + ::improbable::Option& get_test_bool(); + TestComponent::Update& set_test_bool(bool); + + // Field test_double = 4 + const ::improbable::Option& get_test_double() const; + ::improbable::Option& get_test_double(); + TestComponent::Update& set_test_double(double); + + // Field test_list = 5 + const ::improbable::Option<::improbable::List>& get_test_list() const; + ::improbable::Option<::improbable::List>& get_test_list(); + TestComponent::Update& set_test_list(::improbable::List); + + // Field test_map = 6 + const ::improbable::Option<::improbable::Map>& get_test_map() const; + ::improbable::Option<::improbable::Map>& get_test_map(); + TestComponent::Update& set_test_map(::improbable::Map); + + // Field test_bytes = 7 + const ::improbable::Option>& get_test_bytes() const; + ::improbable::Option>& get_test_bytes(); + TestComponent::Update& set_test_bytes(std::vector); + + // Event empty = 1 + const ::improbable::List<::improbable::test_schema::TestComponentData::EmptyEvent>& get_empty_list() const; + ::improbable::List<::improbable::test_schema::TestComponentData::EmptyEvent>& get_empty_list(); + TestComponent::Update& add_empty(const ::improbable::test_schema::TestComponentData::EmptyEvent&); + + // Event params = 2 + const ::improbable::List<::improbable::test_schema::TestComponentData::ParameterizedEvent>& get_params_list() const; + ::improbable::List<::improbable::test_schema::TestComponentData::ParameterizedEvent>& get_params_list(); + TestComponent::Update& add_params(const ::improbable::test_schema::TestComponentData::ParameterizedEvent&); + + + private: + ::improbable::Option _test_int; + ::improbable::Option _test_float; + ::improbable::Option _test_bool; + ::improbable::Option _test_double; + ::improbable::Option<::improbable::List> _test_list; + ::improbable::Option<::improbable::Map> _test_map; + ::improbable::Option> _test_bytes; + ::improbable::List<::improbable::test_schema::TestComponentData::EmptyEvent> _empty_list; + ::improbable::List<::improbable::test_schema::TestComponentData::ParameterizedEvent> _params_list; + }; + + + class Commands + { + class TestCommand + { + public: + static const uint32_t CommandId = 1; + using Request = ::improbable::test_schema::Integer; + using Response = ::improbable::test_schema::Integer; + }; + class AnotherTestCommand + { + public: + static const uint32_t CommandId = 2; + using Request = ::improbable::test_schema::Integer; + using Response = ::improbable::test_schema::Nested::Recursive; + }; + }; +}; + +} // namespace test_schema +} // namespace improbable +``` + +### TestComponent.cpp + +```cpp +namespace improbable { +namespace test_schema { + +TestComponent::TestComponent( + std::int32_t test_int, + float test_float, + bool test_bool, + double test_double, + ::improbable::List test_list, + ::improbable::Map test_map, + std::vector test_bytes) +: _test_int{ test_int } +, _test_float{ test_float } +, _test_bool{ test_bool } +, _test_double{ test_double } +, _test_list{ test_list } +, _test_map{ test_map } +, _test_bytes{ test_bytes } {} + +TestComponent::TestComponent() {} + +bool TestComponent::operator==(const TestComponent& value) const +{ + return _test_int == value._test_int && + _test_float == value._test_float && + _test_bool == value._test_bool && + _test_double == value._test_double && + _test_list == value._test_list && + _test_map == value._test_map && + _test_bytes == value._test_bytes; +} + +bool TestComponent::operator!=(const TestComponent& value) const +{ + return !operator== (value); +} + +std::int32_t TestComponent::get_test_int() const +{ + return _test_int; +} + +std::int32_t& TestComponent::get_test_int() +{ + return _test_int; +} + +TestComponent& TestComponent::set_test_int(std::int32_t value) +{ + _test_int = value; + return *this; +} +float TestComponent::get_test_float() const +{ + return _test_float; +} + +float& TestComponent::get_test_float() +{ + return _test_float; +} + +TestComponent& TestComponent::set_test_float(float value) +{ + _test_float = value; + return *this; +} +bool TestComponent::get_test_bool() const +{ + return _test_bool; +} + +bool& TestComponent::get_test_bool() +{ + return _test_bool; +} + +TestComponent& TestComponent::set_test_bool(bool value) +{ + _test_bool = value; + return *this; +} +double TestComponent::get_test_double() const +{ + return _test_double; +} + +double& TestComponent::get_test_double() +{ + return _test_double; +} + +TestComponent& TestComponent::set_test_double(double value) +{ + _test_double = value; + return *this; +} +const ::improbable::List& TestComponent::get_test_list() const +{ + return _test_list; +} + +::improbable::List& TestComponent::get_test_list() +{ + return _test_list; +} + +TestComponent& TestComponent::set_test_list(const ::improbable::List& value) +{ + _test_list = value; + return *this; +} +const ::improbable::Map& TestComponent::get_test_map() const +{ + return _test_map; +} + +::improbable::Map& TestComponent::get_test_map() +{ + return _test_map; +} + +TestComponent& TestComponent::set_test_map(const ::improbable::Map& value) +{ + _test_map = value; + return *this; +} +std::vector TestComponent::get_test_bytes() const +{ + return _test_bytes; +} + +std::vector& TestComponent::get_test_bytes() +{ + return _test_bytes; +} + +TestComponent& TestComponent::set_test_bytes(std::vector value) +{ + _test_bytes = value; + return *this; +} + +void TestComponent::serialize(Schema_ComponentData* component_data) const +{ + Schema_Object* fields_objects = Schema_GetComponentDataFields(component_data); + // serializing field test_int = 1 + Schema_AddInt32(fields_objects, 1, _test_int); + + // serializing field test_float = 2 + Schema_AddFloat(fields_objects, 2, _test_float); + + // serializing field test_bool = 3 + Schema_AddBool(fields_objects, 3, static_cast(_test_bool)); + + // serializing field test_double = 4 + Schema_AddDouble(fields_objects, 4, _test_double); + + // serializing field test_list = 5 + for (std::uint32_t i = 0; i < _test_list.size(); ++i) + { + Schema_AddDouble(fields_objects, 5, _test_list[i]);; + } + + // serializing field test_map = 6 + for (auto _it=_test_map.begin(); _it!=_test_map.end(); ++_it) + { + Schema_Object * kvpair = Schema_AddObject(fields_objects, 6); + ::improbable::utils::AddString(kvpair, SCHEMA_MAP_KEY_FIELD_ID, _it->first); + Schema_AddDouble(kvpair, SCHEMA_MAP_VALUE_FIELD_ID, _it->second); + } + + // serializing field test_bytes = 7 + ::improbable::utils::AddBytes(fields_objects, 7, _test_bytes); + +} + +TestComponent TestComponent::deserialize(Schema_ComponentData* component_data) +{ + Schema_Object* fields_objects = Schema_GetComponentDataFields(component_data); + + TestComponent data; + + // deserializing field test_int = 1 + data._test_int = Schema_GetInt32(fields_objects, 1); + + // deserializing field test_float = 2 + data._test_float = Schema_GetFloat(fields_objects, 2); + + // deserializing field test_bool = 3 + data._test_bool = Schema_GetBool(fields_objects, 3); + + // deserializing field test_double = 4 + data._test_double = Schema_GetDouble(fields_objects, 4); + + // deserializing field test_list = 5 + { + uint32_t test_list_length = Schema_GetDoubleCount(fields_objects, 5); + data._test_list = ::improbable::List(test_list_length); + Schema_GetDoubleList(fields_objects, 5, data._test_list.data()); + + } + + + // deserializing field test_map = 6 + { + data._test_map = ::improbable::Map(); + auto mapEntryCount = Schema_GetObjectCount(fields_objects, 6); + for (uint32_t i = 0; i < mapEntryCount; ++i) + { + Schema_Object* kvpair = Schema_IndexObject(fields_objects, 6, i); + auto key = ::improbable::utils::GetString(kvpair, SCHEMA_MAP_KEY_FIELD_ID); + auto value = Schema_GetDouble(kvpair, SCHEMA_MAP_VALUE_FIELD_ID); + data._test_map[std::move(key)] = std::move(value); + } + + } + + + // deserializing field test_bytes = 7 + data._test_bytes = ::improbable::utils::GetBytes(fields_objects, 7); + + return data; +} + + +bool TestComponent::Update::operator==(const TestComponent::Update& value) const +{ + return _test_int == value._test_int && + _test_float == value._test_float && + _test_bool == value._test_bool && + _test_double == value._test_double && + _test_list == value._test_list && + _test_map == value._test_map && + _test_bytes == value._test_bytes; +} + +bool TestComponent::Update::operator!=(const TestComponent::Update& value) const +{ + return !operator== (value); +} + +TestComponent::Update TestComponent::Update::FromInitialData(const TestComponent& data) +{ + TestComponent::Update update; + update._test_int.emplace(data.get_test_int()); + update._test_float.emplace(data.get_test_float()); + update._test_bool.emplace(data.get_test_bool()); + update._test_double.emplace(data.get_test_double()); + update._test_list.emplace(data.get_test_list()); + update._test_map.emplace(data.get_test_map()); + update._test_bytes.emplace(data.get_test_bytes()); + return update; +} + +TestComponent TestComponent::Update::ToInitialData() const +{ + return TestComponent( + *_test_int, + *_test_float, + *_test_bool, + *_test_double, + *_test_list, + *_test_map, + *_test_bytes); +} + +void TestComponent::Update::ApplyTo(TestComponent& data) const +{ + if (_test_int) + { + data.set_test_int(*_test_int); + } + if (_test_float) + { + data.set_test_float(*_test_float); + } + if (_test_bool) + { + data.set_test_bool(*_test_bool); + } + if (_test_double) + { + data.set_test_double(*_test_double); + } + if (_test_list) + { + data.set_test_list(*_test_list); + } + if (_test_map) + { + data.set_test_map(*_test_map); + } + if (_test_bytes) + { + data.set_test_bytes(*_test_bytes); + } +} + +const ::improbable::Option& TestComponent::Update::get_test_int() const +{ + return _test_int; +} + +::improbable::Option& TestComponent::Update::get_test_int() +{ + return _test_int; +} + +TestComponent::Update& TestComponent::Update::set_test_int(std::int32_t value) +{ + _test_int.emplace(value); + return *this; +} + +const ::improbable::Option& TestComponent::Update::get_test_float() const +{ + return _test_float; +} + +::improbable::Option& TestComponent::Update::get_test_float() +{ + return _test_float; +} + +TestComponent::Update& TestComponent::Update::set_test_float(float value) +{ + _test_float.emplace(value); + return *this; +} + +const ::improbable::Option& TestComponent::Update::get_test_bool() const +{ + return _test_bool; +} + +::improbable::Option& TestComponent::Update::get_test_bool() +{ + return _test_bool; +} + +TestComponent::Update& TestComponent::Update::set_test_bool(bool value) +{ + _test_bool.emplace(value); + return *this; +} + +const ::improbable::Option& TestComponent::Update::get_test_double() const +{ + return _test_double; +} + +::improbable::Option& TestComponent::Update::get_test_double() +{ + return _test_double; +} + +TestComponent::Update& TestComponent::Update::set_test_double(double value) +{ + _test_double.emplace(value); + return *this; +} + +const ::improbable::Option<::improbable::List>& TestComponent::Update::get_test_list() const +{ + return _test_list; +} + +::improbable::Option<::improbable::List>& TestComponent::Update::get_test_list() +{ + return _test_list; +} + +TestComponent::Update& TestComponent::Update::set_test_list(::improbable::List value) +{ + _test_list.emplace(value); + return *this; +} + +const ::improbable::Option<::improbable::Map>& TestComponent::Update::get_test_map() const +{ + return _test_map; +} + +::improbable::Option<::improbable::Map>& TestComponent::Update::get_test_map() +{ + return _test_map; +} + +TestComponent::Update& TestComponent::Update::set_test_map(::improbable::Map value) +{ + _test_map.emplace(value); + return *this; +} + +const ::improbable::Option>& TestComponent::Update::get_test_bytes() const +{ + return _test_bytes; +} + +::improbable::Option>& TestComponent::Update::get_test_bytes() +{ + return _test_bytes; +} + +TestComponent::Update& TestComponent::Update::set_test_bytes(std::vector value) +{ + _test_bytes.emplace(value); + return *this; +} + +const ::improbable::List< ::improbable::test_schema::TestComponentData::EmptyEvent >& TestComponent::Update::get_empty_list() const +{ + return _empty_list; +} + +::improbable::List< ::improbable::test_schema::TestComponentData::EmptyEvent >& TestComponent::Update::get_empty_list() +{ + return _empty_list; +} + +TestComponent::Update& TestComponent::Update::add_empty(const ::improbable::test_schema::TestComponentData::EmptyEvent& value) +{ + _empty_list.emplace_back(value); + return *this; +} + +const ::improbable::List< ::improbable::test_schema::TestComponentData::ParameterizedEvent >& TestComponent::Update::get_params_list() const +{ + return _params_list; +} + +::improbable::List< ::improbable::test_schema::TestComponentData::ParameterizedEvent >& TestComponent::Update::get_params_list() +{ + return _params_list; +} + +TestComponent::Update& TestComponent::Update::add_params(const ::improbable::test_schema::TestComponentData::ParameterizedEvent& value) +{ + _params_list.emplace_back(value); + return *this; +} +void TestComponent::Update::serialize(Schema_ComponentUpdate* component_update) const +{ + Schema_Object* updates_object = Schema_GetComponentUpdateFields(component_update); + Schema_Object* events_object = Schema_GetComponentUpdateEvents(component_update); + + // serializing field test_int = 1 + if (_test_int.data() != nullptr) + { + Schema_AddInt32(updates_object, 1, (*_test_int)); + } + + // serializing field test_float = 2 + if (_test_float.data() != nullptr) + { + Schema_AddFloat(updates_object, 2, (*_test_float)); + } + + // serializing field test_bool = 3 + if (_test_bool.data() != nullptr) + { + Schema_AddBool(updates_object, 3, static_cast((*_test_bool))); + } + + // serializing field test_double = 4 + if (_test_double.data() != nullptr) + { + Schema_AddDouble(updates_object, 4, (*_test_double)); + } + + // serializing field test_list = 5 + if (_test_list.data() != nullptr) + { + if (_test_list.data()->empty()) + { + Schema_AddComponentUpdateClearedField(component_update, 5); + } + else + { + for (std::uint32_t i = 0; i < (*_test_list).size(); ++i) + { + Schema_AddDouble(updates_object, 5, (*_test_list)[i]);; + } + } + } + + // serializing field test_map = 6 + if (_test_map.data() != nullptr) + { + if (_test_map.data()->empty()) + { + Schema_AddComponentUpdateClearedField(component_update, 6); + } + else + { + for (auto _it=(*_test_map).begin(); _it!=(*_test_map).end(); ++_it) + { + Schema_Object * kvpair = Schema_AddObject(updates_object, 6); + ::improbable::utils::AddString(kvpair, SCHEMA_MAP_KEY_FIELD_ID, _it->first); + Schema_AddDouble(kvpair, SCHEMA_MAP_VALUE_FIELD_ID, _it->second); + } + } + } + + // serializing field test_bytes = 7 + if (_test_bytes.data() != nullptr) + { + ::improbable::utils::AddBytes(updates_object, 7, (*_test_bytes)); + } + + // serializing event empty = 1 + for (auto empty_it = _empty_list.begin(); empty_it != _empty_list.end(); ++ empty_it) + { + empty_it->serialize(Schema_AddObject(events_object, 1)); + } + + // serializing event params = 2 + for (auto params_it = _params_list.begin(); params_it != _params_list.end(); ++ params_it) + { + params_it->serialize(Schema_AddObject(events_object, 2)); + } + +} + +TestComponent::Update TestComponent::Update::deserialize(Schema_ComponentUpdate* component_update) +{ + Schema_Object* updates_object = Schema_GetComponentUpdateFields(component_update); + Schema_Object* events_object = Schema_GetComponentUpdateEvents(component_update); + auto fields_to_clear = new Schema_FieldId[Schema_GetComponentUpdateClearedFieldCount(component_update)]; + Schema_GetComponentUpdateClearedFieldList(component_update, fields_to_clear); + std::set fields_to_clear_set(fields_to_clear, fields_to_clear + sizeof(fields_to_clear) / sizeof(Schema_FieldId)); + + TestComponent::Update data; + + // deserializing field test_int = 1 + if (Schema_GetInt32Count(updates_object, 1) > 0) + { + data._test_int = Schema_GetInt32(updates_object, 1); + } + + // deserializing field test_float = 2 + if (Schema_GetFloatCount(updates_object, 2) > 0) + { + data._test_float = Schema_GetFloat(updates_object, 2); + } + + // deserializing field test_bool = 3 + if (Schema_GetBoolCount(updates_object, 3) > 0) + { + data._test_bool = Schema_GetBool(updates_object, 3); + } + + // deserializing field test_double = 4 + if (Schema_GetDoubleCount(updates_object, 4) > 0) + { + data._test_double = Schema_GetDouble(updates_object, 4); + } + + // deserializing field test_list = 5 + if (Schema_GetDoubleCount(updates_object, 5) > 0) + { + uint32_t test_list_length = Schema_GetDoubleCount(updates_object, 5); + data._test_list = ::improbable::List(test_list_length); + Schema_GetDoubleList(updates_object, 5, (*data._test_list).data()); + + } + else if (fields_to_clear_set.count(5)) + { + data._test_list = {}; + } + + // deserializing field test_map = 6 + if (Schema_GetObjectCount(updates_object, 6) > 0) + { + data._test_map = ::improbable::Map(); + auto mapEntryCount = Schema_GetObjectCount(updates_object, 6); + for (uint32_t i = 0; i < mapEntryCount; ++i) + { + Schema_Object* kvpair = Schema_IndexObject(updates_object, 6, i); + auto key = ::improbable::utils::GetString(kvpair, SCHEMA_MAP_KEY_FIELD_ID); + auto value = Schema_GetDouble(kvpair, SCHEMA_MAP_VALUE_FIELD_ID); + (*data._test_map)[std::move(key)] = std::move(value); + } + + } + else if (fields_to_clear_set.count(6)) + { + data._test_map = {}; + } + + // deserializing field test_bytes = 7 + if (Schema_GetBytesCount(updates_object, 7) > 0) + { + data._test_bytes = ::improbable::utils::GetBytes(updates_object, 7); + } + + // deserializing event empty = 1 + for (std::uint32_t i = 0; i < Schema_GetObjectCount(events_object, 1); ++i) + { + data.add_empty(improbable::test_schema::TestComponentData::EmptyEvent::deserialize(Schema_IndexObject(events_object, 1, i))); + } + + // deserializing event params = 2 + for (std::uint32_t i = 0; i < Schema_GetObjectCount(events_object, 2); ++i) + { + data.add_params(improbable::test_schema::TestComponentData::ParameterizedEvent::deserialize(Schema_IndexObject(events_object, 2, i))); + } + + return data; +} + + +} // namespace test_schema +} // namespace improbable +``` diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/EnumGenerator.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/EnumGenerator.cs new file mode 100644 index 0000000000..b4331d42c2 --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/EnumGenerator.cs @@ -0,0 +1,38 @@ +using Improbable.CodeGen.Base; +using System; +using System.Linq; + +namespace Improbable.CodeGen.Unreal +{ + static class EnumGenerator + { + public static string GenerateTopLevelEnum(EnumDefinition enumDefinition, Bundle bundle) + { + var enumNamespace = Text.GetNamespaceFromTypeName(enumDefinition.QualifiedName); + + return $@"// Generated by {UnrealGenerator.GeneratorTitle} + +#pragma once + +#include +#include + +{string.Join(Environment.NewLine, enumNamespace.Select(t => $"namespace {t} {{"))} + +{GenerateEnum(enumDefinition.Name, enumDefinition, bundle)} + +{string.Join(Environment.NewLine, enumNamespace.Reverse().Select(t => $"}} // namespace {t}"))} +"; + } + + public static string GenerateEnum(string name, EnumDefinition enumDefinition, Bundle bundle) + { + + return $@"// Generated from {bundle.TypeToFileName[enumDefinition.QualifiedName]}({enumDefinition.SourceReference.Line},{enumDefinition.SourceReference.Column}) +enum class {name} : uint32 +{{ +{Text.Indent(1, string.Join(Environment.NewLine, enumDefinition.Values.Select(v => $"{v.Name} = {v.Value},")))} +}};"; + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/HeaderGenerator.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/HeaderGenerator.cs new file mode 100644 index 0000000000..7c4535c85e --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/HeaderGenerator.cs @@ -0,0 +1,263 @@ +using Improbable.CodeGen.Base; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Improbable.CodeGen.Unreal +{ + public static class HeaderGenerator + { + public static string GenerateHeader(TypeDescription type, List types, Dictionary allGeneratedTypeContent, Bundle bundle) + { + var allTopLevelTypes = Types.SortTopLevelTypesTopologically(type, types, bundle); + var typeNamespaces = Text.GetNamespaceFromTypeName(type.QualifiedName); + var requiredIncludes = Types.GetRequiredTypeIncludes(type, bundle).Select(inc => $"#include \"{string.Concat(Enumerable.Repeat("../", type.QualifiedName.Count(c => c == '.')))}{inc}\""); + var allNestedEnums = Types.GetRecursivelyNestedEnums(type); + var enumDefs = allNestedEnums.Select(enumDef => EnumGenerator.GenerateEnum(Types.GetTypeClassName(enumDef.QualifiedName, bundle), enumDef, bundle).Replace($"enum {enumDef.Name}", $"class {enumDef.Name}_{enumDef.Name}")); + + var builder = new StringBuilder(); + + builder.AppendLine($@"// Generated by {UnrealGenerator.GeneratorTitle} + +#pragma once + +#include ""CoreMinimal.h"" +#include ""Utils/SchemaOption.h"" +#include +#include + +#include ""{string.Concat(Enumerable.Repeat("../", type.QualifiedName.Count(c => c == '.')))}{HelperFunctions.HeaderPath}"" +"); + if (requiredIncludes.Count() > 0) + { + builder.AppendLine(string.Join(Environment.NewLine, requiredIncludes)); + builder.AppendLine(); + } + + builder.AppendLine(string.Join(Environment.NewLine, typeNamespaces.Select(t => $"namespace {t} {{"))); + builder.AppendLine(); + + if (allTopLevelTypes.Count() > 1) + { + builder.AppendLine(string.Join(Environment.NewLine, allTopLevelTypes.Select(topLevelType => $"class {Types.GetTypeClassName(topLevelType.QualifiedName, bundle)};"))); + builder.AppendLine(); + } + + if (allNestedEnums.Count() > 0) + { + builder.AppendLine(string.Join(Environment.NewLine, enumDefs)); + builder.AppendLine(); + } + + builder.AppendLine($@"{string.Join(Environment.NewLine, allTopLevelTypes.Select(topLevelType => GenerateTypeClass(Types.GetTypeClassName(topLevelType.QualifiedName, bundle), types.Find(t => t.QualifiedName == topLevelType.QualifiedName), types, bundle)))} +{string.Join(Environment.NewLine, allTopLevelTypes.Select(nestedType => GenerateHashFunction(Types.GetTypeClassName(nestedType.QualifiedName, bundle))))} + +{string.Join(Environment.NewLine, typeNamespaces.Reverse().Select(t => $"}} // namespace {t}"))} +"); + + return builder.ToString(); + } + + private static string GenerateTypeClass(string name, TypeDescription type, List types, Bundle bundle) + { + var hasFields = type.Fields.Count > 0; + var serializedArgType = type.ComponentId.HasValue ? "Schema_ComponentData" : "Schema_Object"; + var serializedArgName = type.ComponentId.HasValue ? "ComponentData" : "SchemaObject"; + + var builder = new StringBuilder(); + + builder.AppendLine($@"// Generated from {Path.GetFullPath(bundle.TypeToFileName[type.QualifiedName])}({type.SourceReference.Line},{type.SourceReference.Column}) +class {name} : public improbable::{(type.ComponentId.HasValue ? "SpatialComponent" : "SpatialType")} +{{ +public:"); + + if (type.ComponentId.HasValue) + { + builder.AppendLine(Text.Indent(1, $"static const Worker_ComponentId ComponentId = {type.ComponentId.Value};")); + } + + if (type.NestedTypes.Count > 0) + { + builder.AppendLine(Text.Indent(1, $@"// Nested types +{string.Join(Environment.NewLine, type.NestedTypes.Select(nestedType => Text.Indent(1, $"using {nestedType.Name} = {Types.GetTypeClassName(nestedType.QualifiedName, bundle)};")))}")); + } + + if (type.NestedEnums.Count > 0) + { + builder.AppendLine(Text.Indent(1, $@"// Nested enums +{string.Join(Environment.NewLine, type.NestedEnums.Select(nestedEnum => $"using {nestedEnum.Name} = {Types.GetTypeClassName(nestedEnum.QualifiedName, bundle)};"))}")); + } + + if (type.Fields.Count > 0) + { + builder.AppendLine(Text.Indent(1, $@"// Creates a new instance with specified arguments for each field. +{name}({string.Join(", ", type.Fields.Select(f => $"{Types.GetConstAccessorTypeModification(f, bundle, type)} {Text.SnakeCaseToPascalCase(f.Name)}"))});")); + } + + builder.AppendLine(Text.Indent(1, $@"// Creates a new instance with default values for each field. +{name}(); +// Creates a new instance with default values for each field. This is +// equivalent to a default-constructed instance. +static {name} Create() {{ return {{}}; }} +// Copyable and movable. +{name}({name}&&) = default; +{name}(const {name}&) = default; +{name}& operator=({name}&&) = default; +{name}& operator=(const {name}&) = default; +~{name}() = default; + +bool operator==(const {name}&) const; +bool operator!=(const {name}&) const; + +// Serialize this object data into the C API argument +void Serialize({serializedArgType}* {serializedArgName}) const override; + +// Deserialize the C API object argument into an instance of this class and return it +static {name} Deserialize({serializedArgType}* {serializedArgName}); +")); + + if (hasFields) + { + builder.AppendLine($@"{Text.Indent(1, string.Join(Environment.NewLine, type.Fields.Select(field => $@"// Field {Text.SnakeCaseToPascalCase(field.Name)} = {field.FieldId} +{Types.GetConstAccessorTypeModification(field, bundle, type)} Get{Text.SnakeCaseToPascalCase(field.Name)}() const; +{Types.GetFieldTypeAsCpp(field, bundle, type)}& Get{Text.SnakeCaseToPascalCase(field.Name)}(); +{name}& Set{Text.SnakeCaseToPascalCase(field.Name)}({Types.GetConstAccessorTypeModification(field, bundle, type)}); +")))} +private: +{Text.Indent(1, string.Join(Environment.NewLine, type.Fields.Select(field => $"{Types.GetFieldTypeAsCpp(field, bundle, type)} _{Text.SnakeCaseToPascalCase(field.Name)};")))}"); + } + + if (type.ComponentId.HasValue) + { + if (hasFields) + { + builder.AppendLine(); + builder.AppendLine("public:"); + } + builder.AppendLine($@"{Text.Indent(1, $@"class Update : public improbable::SpatialComponentUpdate +{{ +public: +{Text.Indent(1, $@"// Creates a new instance with default values for each field. +Update() = default; +// Creates a new instance with default values for each field. This is +// equivalent to a default-constructed instance. +static Update Create() {{ return {{}}; }} +// Copyable and movable. +Update(Update&&) = default; +Update(const Update&) = default; +Update& operator=(Update&&) = default; +Update& operator=(const Update&) = default; +~Update() = default; +bool operator==(const Update&) const; +bool operator!=(const Update&) const; + +// Creates an Update from a {Types.GetNameFromQualifiedName(type.QualifiedName)} object. +static Update FromInitialData(const {Types.GetNameFromQualifiedName(type.QualifiedName)}& Data); + +/** + * Converts to a {Types.GetNameFromQualifiedName(type.QualifiedName)} + * object. It is an error to call this function unless *all* of the optional fields in this + * update are filled in. + */ +{Types.GetNameFromQualifiedName(type.QualifiedName)} ToInitialData() const; + +/** + * Replaces fields in the given {Types.GetNameFromQualifiedName(type.QualifiedName)} + * object with the corresponding fields in this update, where present. + */ +void ApplyTo({Types.GetNameFromQualifiedName(type.QualifiedName)}&) const; + +// Serialize this update object data into the C API component update argument +void Serialize(Schema_ComponentUpdate* ComponentUpdate) const override; + +// Deserialize the C API component update argument into an instance of this class and return it +static Update Deserialize(Schema_ComponentUpdate* ComponentUpdate); +")}")}"); + + if (type.Fields.Count > 0) + { + builder.AppendLine(Text.Indent(2, string.Join(Environment.NewLine, type.Fields.Select(field => $@"// Field {Text.SnakeCaseToPascalCase(field.Name)} = {field.FieldId} +const {Types.CollectionTypesToQualifiedTypes[Types.Collection.Option]}<{Types.GetFieldTypeAsCpp(field, bundle, type)}>& Get{Text.SnakeCaseToPascalCase(field.Name)}() const; +{Types.CollectionTypesToQualifiedTypes[Types.Collection.Option]}<{Types.GetFieldTypeAsCpp(field, bundle, type)}>& Get{Text.SnakeCaseToPascalCase(field.Name)}(); +{name}::Update& Set{Text.SnakeCaseToPascalCase(field.Name)}({Types.GetConstAccessorTypeModification(field, bundle, type)}); +")))); + } + + if (type.Events.Count > 0) + { + builder.AppendLine(Text.Indent(2, string.Join(Environment.NewLine, type.Events.Select(_event => $@"// Event {Text.SnakeCaseToPascalCase(_event.Name)} = {_event.EventIndex} +const {Types.CollectionTypesToQualifiedTypes[Types.Collection.List]}<{Types.GetTypeDisplayName(_event.Type)}>& Get{Text.SnakeCaseToPascalCase(_event.Name)}List() const; +{Types.CollectionTypesToQualifiedTypes[Types.Collection.List]}<{Types.GetTypeDisplayName(_event.Type)}>& Get{Text.SnakeCaseToPascalCase(_event.Name)}List(); +{name}::Update& Add{Text.SnakeCaseToPascalCase(_event.Name)}(const {Types.GetTypeDisplayName(_event.Type)}&); +")))); + } + + if (type.Fields.Count + type.Events.Count > 0) + { + builder.AppendLine(Text.Indent(1, $@"private:")); + if (type.Fields.Count > 0) + { + builder.AppendLine(Text.Indent(2, string.Join(Environment.NewLine, type.Fields.Select(field => $"{Types.CollectionTypesToQualifiedTypes[Types.Collection.Option]}<{Types.GetFieldTypeAsCpp(field, bundle, type)}> _{Text.SnakeCaseToPascalCase(field.Name)};")))); + } + + if (type.Events.Count > 0) + { + builder.AppendLine(Text.Indent(2, string.Join(Environment.NewLine, type.Events.Select(_event => $"{Types.CollectionTypesToQualifiedTypes[Types.Collection.List]}<{Types.GetTypeDisplayName(_event.Type)}> _{Text.SnakeCaseToPascalCase(_event.Name)}List;")))); + } + } + + builder.AppendLine(Text.Indent(1, "};")); + builder.AppendLine(); + + if (bundle.Components[type.QualifiedName].Commands.Count > 0) + { + builder.AppendLine(Text.Indent(1, $@"class Commands +{{ +public: +{Text.Indent(1, string.Join(Environment.NewLine, bundle.Components[type.QualifiedName].Commands.Select(command => $@"class {Text.SnakeCaseToPascalCase(command.Name)} +{{ +public: +{Text.Indent(1, $@"static const Schema_FieldId CommandIndex = {command.CommandIndex}; +struct Request +{{ +{Text.Indent(1, $@"using Type = {Types.GetTypeDisplayName(command.RequestType)}; +Request({string.Join($", ", types.Find(t => t.QualifiedName == command.RequestType).Fields.Select(f => $"{Types.GetConstAccessorTypeModification(f, bundle, type)} {Text.SnakeCaseToPascalCase(f.Name)}"))}) +: Data({string.Join($", ", types.Find(t => t.QualifiedName == command.RequestType).Fields.Select(f => $"{Text.SnakeCaseToPascalCase(f.Name)}"))}) {{}} +Request(Type Data) : Data{{ Data }} {{}} +Type Data;")} +}}; + +struct Response +{{ +{Text.Indent(1, $@"using Type = {Types.GetTypeDisplayName(command.ResponseType)}; +Response({string.Join($", ", types.Find(t => t.QualifiedName == command.ResponseType).Fields.Select(f => $"{Types.GetConstAccessorTypeModification(f, bundle, type)} {Text.SnakeCaseToPascalCase(f.Name)}"))}) +: Data({string.Join($", ", types.Find(t => t.QualifiedName == command.ResponseType).Fields.Select(f => $"{Text.SnakeCaseToPascalCase(f.Name)}"))}) {{}} +Response(Type Data) : Data{{ Data }} {{}} +{(types.Find(t => t.QualifiedName == command.ResponseType).Fields.Count() > 0 ? $@"Response() : Data() {{}} +Type Data;" : "Type Data;")}")} +}}; +using RequestOp = ::improbable::CommandRequestOp; +using ResponseOp = ::improbable::CommandResponseOp;")} +}};")))} +}};")); + } + + builder.AppendLine(Text.Indent(1, $@"using AddComponentOp = ::improbable::AddComponentOp<{name}>; +using RemoveComponentOp = ::improbable::RemoveComponentOp<{name}>; +using ComponentUpdateOp = ::improbable::ComponentUpdateOp; +using AuthorityChangeOp = ::improbable::AuthorityChangeOp<{name}>;")); + } + + builder.AppendLine("};"); + + return builder.ToString(); + } + + private static string GenerateHashFunction(string typeName) + { + return $"inline uint32 GetTypeHash(const {typeName}& Value);"; + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/HelperFunctions.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/HelperFunctions.cs new file mode 100644 index 0000000000..54910921e3 --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/HelperFunctions.cs @@ -0,0 +1,273 @@ +using Improbable.Codegen.Base; +using Improbable.CodeGen.Base; +using System.Collections.Generic; + +namespace Improbable.CodeGen.Unreal +{ + public static class HelperFunctions + { + public static string HeaderPath = "ExternalSchemaHelperFunctions.h"; + public static string SourceFile = "ExternalSchemaHelperFunctions.cpp"; + + public static List GetHelperFunctionFiles() + { + return new List + { + new GeneratedFile(HeaderPath, GetHelperFunctionHeader()), + new GeneratedFile(SourceFile, GetHelperFunctionSource()) + }; + } + + private static string GetHelperFunctionHeader() + { + return $@"// Generated by {UnrealGenerator.GeneratorTitle} + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include ""CoreMinimal.h"" + +namespace improbable {{ + +class SpatialType +{{ +public: +{Text.Indent(1, $@"virtual ~SpatialType() = 0 {{}}; +virtual void Serialize(Schema_Object* SchemaObject) const = 0;")} +}}; + +class SpatialComponent +{{ +public: +{Text.Indent(1, $@"virtual ~SpatialComponent() = 0 {{}}; +virtual void Serialize(Schema_ComponentData* ComponentData) const = 0;")} +}}; + +class SpatialComponentUpdate +{{ +public: +{Text.Indent(1, $@"virtual ~SpatialComponentUpdate() = 0 {{}}; +virtual void Serialize(Schema_ComponentUpdate* ComponentUpdate) const = 0;")} +}}; + +class ExternalSchemaOp +{{ +public: +{Text.Indent(1, $@"ExternalSchemaOp(Worker_EntityId EntityId) : EntityId{{ EntityId }} {{}} +Worker_EntityId EntityId; +virtual ~ExternalSchemaOp() = 0 {{}};")} +}}; + +template +class AddComponentOp : public ::improbable::ExternalSchemaOp +{{ +public: +{Text.Indent(1, $@"AddComponentOp( +{Text.Indent(1, $@"Worker_EntityId EntityId, +Worker_ComponentId ComponentId, +const ComponentData& Data) ")} +: ExternalSchemaOp( EntityId ) +, ComponentId(ComponentId ) +, Data( Data ) {{}} + +Worker_ComponentId ComponentId; +ComponentData Data;")} +}}; + +template +class RemoveComponentOp : public ::improbable::ExternalSchemaOp +{{ +public: +{Text.Indent(1, $@"RemoveComponentOp( +{Text.Indent(1, $@"Worker_EntityId EntityId, +Worker_ComponentId ComponentId)")} +: ExternalSchemaOp( EntityId ) +, ComponentId( ComponentId ) {{}} + +Worker_ComponentId ComponentId;")} +}}; + +template +class ComponentUpdateOp : public ::improbable::ExternalSchemaOp +{{ +public: +{Text.Indent(1, $@"ComponentUpdateOp( +{Text.Indent(1, $@"Worker_EntityId EntityId, +Worker_ComponentId ComponentId, +const ComponentUpdate& Update)")} +: ExternalSchemaOp(EntityId) +, ComponentId(ComponentId) +, Update(Update) {{}} + +Worker_ComponentId ComponentId; +ComponentUpdate Update;")} +}}; + +template // just to differentiate type aliases +class AuthorityChangeOp : public ::improbable::ExternalSchemaOp +{{ +public: +{Text.Indent(1, $@"AuthorityChangeOp( +{Text.Indent(1, $@"Worker_EntityId EntityId, +Worker_ComponentId ComponentId, Worker_Authority Authority)")} +: ExternalSchemaOp( EntityId ) +, ComponentId( ComponentId ) +, Authority( Authority ) {{}} + +Worker_ComponentId ComponentId; +Worker_Authority Authority;")} +}}; + +template +class CommandRequestOp : public ::improbable::ExternalSchemaOp +{{ +public: +{Text.Indent(1, $@"CommandRequestOp( +{Text.Indent(1, $@"Worker_EntityId EntityId, +Worker_RequestId RequestId, +uint32_t TimeoutMillis, +const char* CallerWorkerId, +Worker_WorkerAttributes CallerAttributeSet, +const RequestData& Data)")} +: ExternalSchemaOp(EntityId) +, RequestId(RequestId) +, TimeoutMillis(TimeoutMillis) +, CallerWorkerId(CallerWorkerId) +, CallerAttributeSet(CallerAttributeSet) +, Data(Data) {{}} + +Worker_RequestId RequestId; +uint32_t TimeoutMillis; +const char* CallerWorkerId; +Worker_WorkerAttributes CallerAttributeSet; +RequestData Data;")} +}}; + +template +class CommandResponseOp : public ::improbable::ExternalSchemaOp +{{ +public: +{Text.Indent(1, $@"CommandResponseOp(Worker_EntityId EntityId, +{Text.Indent(1, $@"Worker_RequestId RequestId, +uint8_t StatusCode, +const char* Message, +uint32_t CommandId, +const ResponseData& Data)")} +: ExternalSchemaOp(EntityId) +, RequestId(RequestId) +, StatusCode(StatusCode) +, Message(Message) +, CommandId(CommandId) +, Data(Data) {{}} + +Worker_RequestId RequestId; +uint8_t StatusCode; +const char* Message; +uint32_t CommandId; +ResponseData Data;")} +}}; + +namespace utils {{ +{Text.Indent(1, $@"// Utility methods for serializing and deserializing string fields +void AddBytes(Schema_Object* SchemaObject, Schema_FieldId FieldId, const TArray& Value); +void AddString(Schema_Object* SchemaObject, Schema_FieldId FieldId, FString Value); +TArray GetBytes(const Schema_Object* SchemaObject, Schema_FieldId FieldId); +FString GetString(const Schema_Object* SchemaObject, Schema_FieldId FieldId); +{Types.CollectionTypesToQualifiedTypes[Types.Collection.List]} GetStringList(const Schema_Object* SchemaObject, Schema_FieldId FieldId); +{Types.CollectionTypesToQualifiedTypes[Types.Collection.List]}> GetBytesList(const Schema_Object* SchemaObject, Schema_FieldId FieldId);")} +}} // namespace utils +}} // namespace improbable + +inline uint32 GetTypeHash(const TArray& Value); +"; + } + + private static string GetHelperFunctionSource() + { + return $@"// Generated by {UnrealGenerator.GeneratorTitle} + +#include ""{HeaderPath}"" + +namespace improbable {{ +namespace utils {{ + +void AddBytes(Schema_Object* SchemaObject, Schema_FieldId FieldId, const TArray& Value) +{{ +{Text.Indent(1, $@"uint32 BytesLength = Value.Num(); +uint8* ByteBuffer = Schema_AllocateBuffer(SchemaObject, BytesLength); +memcpy(ByteBuffer, Value.GetData(), BytesLength); +Schema_AddBytes(SchemaObject, FieldId, ByteBuffer, BytesLength);")} +}} + +void AddString(Schema_Object* SchemaObject, Schema_FieldId FieldId, FString Value) +{{ +{Text.Indent(1, $@"const char* Text = TCHAR_TO_ANSI(*Value); +uint32 TextLength = sizeof(char) * strlen(Text); // ensure to exclude null-terminator +uint8* TextBuffer = Schema_AllocateBuffer(SchemaObject, TextLength); +memcpy(TextBuffer, Text, TextLength); +Schema_AddBytes(SchemaObject, FieldId, TextBuffer, TextLength);")} +}} + +TArray GetBytes(const Schema_Object* SchemaObject, Schema_FieldId FieldId) +{{ +{Text.Indent(1, $@"uint32 BytesLength = Schema_GetBytesLength(SchemaObject, FieldId); +const uint8* Bytes = Schema_GetBytes(SchemaObject, FieldId); +return TArray(Bytes, BytesLength);")} +}} + +FString GetString(const Schema_Object* SchemaObject, Schema_FieldId FieldId) +{{ +{Text.Indent(1, $@"uint32 TextLength = Schema_GetBytesLength(SchemaObject, FieldId); +const uint8* Text = Schema_GetBytes(SchemaObject, FieldId); +return FString(TextLength, ANSI_TO_TCHAR(reinterpret_cast(Text)));")} +}} + +{Types.CollectionTypesToQualifiedTypes[Types.Collection.List]}> GetBytesList(const Schema_Object* SchemaObject, Schema_FieldId FieldId) +{{ +{Text.Indent(1, $@"{Types.CollectionTypesToQualifiedTypes[Types.Collection.List]}> BytesList{{}}; +auto ListSize = Schema_GetBytesCount(SchemaObject, FieldId); +for (uint32 i = 0; i < ListSize; ++i) +{{ +{Text.Indent(1, $@"uint32 BytesLength = Schema_IndexBytesLength(SchemaObject, FieldId, i); +const uint8* Bytes = Schema_IndexBytes(SchemaObject, FieldId, i); +BytesList.Add(TArray(Bytes, BytesLength));")} +}} +return BytesList;")} +}} + +{Types.CollectionTypesToQualifiedTypes[Types.Collection.List]} GetStringList(const Schema_Object* SchemaObject, Schema_FieldId FieldId) +{{ +{Text.Indent(2, $@"{Types.CollectionTypesToQualifiedTypes[Types.Collection.List]} StringList{{}}; +auto ListSize = Schema_GetBytesCount(SchemaObject, FieldId); +for (uint32 i = 0; i < ListSize; ++i) +{{ +{Text.Indent(1, $@"uint32 TextLength = Schema_IndexBytesLength(SchemaObject, FieldId, i); +const uint8* Text = Schema_IndexBytes(SchemaObject, FieldId, i); +StringList.Add(FString(TextLength, ANSI_TO_TCHAR(reinterpret_cast(Text))));")} +}} +return StringList;")} +}} + +}} // namespace utils +}} // namespace improbable + +uint32 GetTypeHash(const TArray& Value) +{{ +{Text.Indent(1, $@"size_t Result = 1327; +for (const auto& item : Value) +{{ +{Text.Indent(1, $"Result = (Result * 977) + GetTypeHash(item);")} +}} +return Result;")} +}} +"; + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/InterfaceGenerator.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/InterfaceGenerator.cs new file mode 100644 index 0000000000..1bf1013459 --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/InterfaceGenerator.cs @@ -0,0 +1,203 @@ +using Improbable.Codegen.Base; +using Improbable.CodeGen.Base; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Improbable.CodeGen.Unreal +{ + public static class InterfaceGenerator + { + public static string ClassName = $"ExternalSchemaInterface"; + public static string HeaderPath = $"ExternalSchemaInterface.h"; + public static string SourceFile = $"ExternalSchemaInterface.cpp"; + + public static List GenerateInterface(List componentTypes, Bundle bundle) + { + return new List + { + new GeneratedFile(HeaderPath, GenerateInterfaceHeader(componentTypes, bundle)), + new GeneratedFile(SourceFile, GenerateInterfaceSource(componentTypes, bundle)) + }; + } + + public static string GenerateInterfaceHeader(List componentTypes, Bundle bundle) + { + var builder = new StringBuilder(); + + builder.AppendLine($@"#pragma once + +#include ""CoreMinimal.h"" +#include ""SpatialConstants.h"" +#include ""SpatialDispatcher.h"" +#include ""{HelperFunctions.HeaderPath}"" + +{string.Join(Environment.NewLine, componentTypes.Select(component => $"#include \"{Types.TypeToHeaderFilename(component.QualifiedName)}\""))} + +#include + +class USpatialWorkerConnection; + +DECLARE_LOG_CATEGORY_EXTERN(LogExternalSchemaInterface, Log, All); + +class {ClassName} +{{ +public: +{Text.Indent(1, $@"{ClassName} () = delete; +{ClassName}(USpatialWorkerConnection* Connection, USpatialDispatcher* Dispatcher) +{Text.Indent(1, @": SpatialWorkerConnection(Connection), SpatialDispatcher(Dispatcher) {{ }}")} +~{ClassName}() = default; + +void RemoveCallback(USpatialDispatcher::FCallbackId Id); +")}"); + foreach (var component in componentTypes) + { + var qualifiedType = Types.GetTypeDisplayName(component.QualifiedName); + builder.AppendLine(Text.Indent(1, $@"// Component {component.QualifiedName} = {component.ComponentId} +void SendComponentUpdate(Worker_EntityId EntityId, const {qualifiedType}::Update& Update); +USpatialDispatcher::FCallbackId OnAddComponent(const TFunction& Callback); +USpatialDispatcher::FCallbackId OnRemoveComponent(const TFunction& Callback); +USpatialDispatcher::FCallbackId OnComponentUpdate(const TFunction& Callback); +USpatialDispatcher::FCallbackId OnAuthorityChange(const TFunction& Callback); +")); + if (bundle.Components[component.QualifiedName].Commands.Count() > 0) + { + builder.AppendLine(Text.Indent(1, string.Join(Environment.NewLine, bundle.Components[component.QualifiedName].Commands.Select(command => $@"// command {Text.SnakeCaseToPascalCase(command.Name)} = {component.ComponentId} +Worker_RequestId SendCommandRequest(Worker_EntityId EntityId, const {Types.GetTypeDisplayName(component.QualifiedName)}::Commands::{Text.SnakeCaseToPascalCase(command.Name)}::Request& Request); +void SendCommandResponse(Worker_RequestId RequestId, const {Types.GetTypeDisplayName(component.QualifiedName)}::Commands::{Text.SnakeCaseToPascalCase(command.Name)}::Response& Response); +USpatialDispatcher::FCallbackId OnCommandRequest(const TFunction& Callback); +USpatialDispatcher::FCallbackId OnCommandResponse(const TFunction& Callback); +")))); + } + } + + builder.AppendLine($@"private: +{Text.Indent(1, $@"void SerializeAndSendComponentUpdate(Worker_EntityId EntityId, Worker_ComponentId ComponentId, const ::improbable::SpatialComponentUpdate& Update); +Worker_RequestId SerializeAndSendCommandRequest(Worker_EntityId EntityId, Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, const ::improbable::SpatialType& Request); +void SerializeAndSendCommandResponse(Worker_RequestId RequestId, Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, const ::improbable::SpatialType& Response); + +USpatialWorkerConnection* SpatialWorkerConnection; +USpatialDispatcher* SpatialDispatcher;")} +}};"); + + return builder.ToString(); + } + + public static string GenerateInterfaceSource(List componentTypes, Bundle bundle) + { + return $@"#include ""{HeaderPath}"" +#include ""Connection/SpatialWorkerConnection.h"" + +DEFINE_LOG_CATEGORY(LogExternalSchemaInterface); + +void {ClassName}::RemoveCallback(USpatialDispatcher::FCallbackId Id) +{{ +{Text.Indent(1, $"SpatialDispatcher->RemoveOpCallback(Id);")} +}} + +void {ClassName}::SerializeAndSendComponentUpdate(Worker_EntityId EntityId, Worker_ComponentId ComponentId, const ::improbable::SpatialComponentUpdate& Update) +{{ +{Text.Indent(1, $@"Worker_ComponentUpdate SerializedUpdate = {{}}; +SerializedUpdate.component_id = ComponentId; +SerializedUpdate.schema_type = Schema_CreateComponentUpdate(ComponentId); +Update.Serialize(SerializedUpdate.schema_type); +SpatialWorkerConnection->SendComponentUpdate(EntityId, &SerializedUpdate);")} +}} + +Worker_RequestId {ClassName}::SerializeAndSendCommandRequest(Worker_EntityId EntityId, Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, const ::improbable::SpatialType& Request) +{{ +{Text.Indent(1, $@"Worker_CommandRequest SerializedRequest = {{}}; +SerializedRequest.component_id = ComponentId; +SerializedRequest.schema_type = Schema_CreateCommandRequest(ComponentId, CommandIndex); +Schema_Object* RequestObject = Schema_GetCommandRequestObject(SerializedRequest.schema_type); +Request.Serialize(RequestObject); +return SpatialWorkerConnection->SendCommandRequest(EntityId, &SerializedRequest, CommandIndex);")} +}} + +void {ClassName}::SerializeAndSendCommandResponse(Worker_RequestId RequestId, Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, const ::improbable::SpatialType& Response) +{{ +{Text.Indent(1, $@"Worker_CommandResponse SerializedResponse = {{}}; +SerializedResponse.component_id = ComponentId; +SerializedResponse.schema_type = Schema_CreateCommandResponse(ComponentId, CommandIndex); +Schema_Object* ResponseObject = Schema_GetCommandResponseObject(SerializedResponse.schema_type); +Response.Serialize(ResponseObject); +return SpatialWorkerConnection->SendCommandResponse(RequestId, &SerializedResponse);")} +}} + +{string.Join(Environment.NewLine, componentTypes.Select(component => $@"// Component {component.QualifiedName} = {component.ComponentId} +void {ClassName}::SendComponentUpdate(const Worker_EntityId EntityId, const {Types.GetTypeDisplayName(component.QualifiedName)}::Update& Update) +{{ +{Text.Indent(1, $"SerializeAndSendComponentUpdate(EntityId, {component.ComponentId}, Update);")} +}} + +USpatialDispatcher::FCallbackId {ClassName}::OnAddComponent(const TFunction& Callback) +{{ +{Text.Indent(1, $@"return SpatialDispatcher->OnAddComponent({component.ComponentId}, [Callback](const Worker_AddComponentOp& Op) +{{ +{Text.Indent(1, $@"{Types.GetTypeDisplayName(component.QualifiedName)} Data = {Types.GetTypeDisplayName(component.QualifiedName)}::Deserialize(Op.data.schema_type); +Callback({Types.GetTypeDisplayName(component.QualifiedName)}::AddComponentOp(Op.entity_id, Op.data.component_id, Data));")} +}});")} +}} + +USpatialDispatcher::FCallbackId {ClassName}::OnRemoveComponent(const TFunction& Callback) +{{ +{Text.Indent(1, $@"return SpatialDispatcher->OnRemoveComponent({component.ComponentId}, [Callback](const Worker_RemoveComponentOp& Op) +{{ +{Text.Indent(1, $"Callback({Types.GetTypeDisplayName(component.QualifiedName)}::RemoveComponentOp(Op.entity_id, Op.component_id));")} +}});")} +}} + +USpatialDispatcher::FCallbackId {ClassName}::OnComponentUpdate(const TFunction& Callback) +{{ +{Text.Indent(1, $@"return SpatialDispatcher->OnComponentUpdate({component.ComponentId}, [Callback](const Worker_ComponentUpdateOp& Op) +{{ +{Text.Indent(1, $@"{Types.GetTypeDisplayName(component.QualifiedName)}::Update Update = {Types.GetTypeDisplayName(component.QualifiedName)}::Update::Deserialize(Op.update.schema_type); +Callback({Types.GetTypeDisplayName(component.QualifiedName)}::ComponentUpdateOp(Op.entity_id, Op.update.component_id, Update));")} +}});")} +}} + +USpatialDispatcher::FCallbackId {ClassName}::OnAuthorityChange(const TFunction& Callback) +{{ +{Text.Indent(1, $@"return SpatialDispatcher->OnAuthorityChange({component.ComponentId}, [Callback](const Worker_AuthorityChangeOp& Op) +{{ +{Text.Indent(1, $"Callback({Types.GetTypeDisplayName(component.QualifiedName)}::AuthorityChangeOp(Op.entity_id, Op.component_id, static_cast(Op.authority)));")} +}});")} +}} + +{string.Join(Environment.NewLine, bundle.Components[component.QualifiedName].Commands.Select(command => $@"Worker_RequestId {ClassName}::SendCommandRequest(Worker_EntityId EntityId, const {Types.GetTypeDisplayName(component.QualifiedName)}::Commands::{ Text.SnakeCaseToPascalCase(command.Name)}::Request& Request) +{{ +{Text.Indent(1, $"return SerializeAndSendCommandRequest(EntityId, {component.ComponentId}, {command.CommandIndex}, Request.Data);")} +}} + +void {ClassName}::SendCommandResponse(Worker_RequestId RequestId, const {Types.GetTypeDisplayName(component.QualifiedName)}::Commands::{Text.SnakeCaseToPascalCase(command.Name)}::Response& Response) +{{ +{Text.Indent(1, $"SerializeAndSendCommandResponse(RequestId, {component.ComponentId}, {command.CommandIndex}, Response.Data);")} +}} + +USpatialDispatcher::FCallbackId {ClassName}::OnCommandRequest(const TFunction& Callback) +{{ +{Text.Indent(1, $@"return SpatialDispatcher->OnCommandRequest({component.ComponentId}, [Callback](const Worker_CommandRequestOp& Op) +{{ +{Text.Indent(1, $@"auto Request = {Types.GetTypeDisplayName(component.QualifiedName)}::Commands::{Text.SnakeCaseToPascalCase(command.Name)}::Request({Types.GetTypeDisplayName(component.QualifiedName)}::Commands::{ Text.SnakeCaseToPascalCase(command.Name)}::Request::Type::Deserialize(Schema_GetCommandRequestObject(Op.request.schema_type))); +Callback({Types.GetTypeDisplayName(component.QualifiedName)}::Commands::{Text.SnakeCaseToPascalCase(command.Name)}::RequestOp(Op.entity_id, Op.request_id, Op.timeout_millis, Op.caller_worker_id, Op.caller_attribute_set, Request));")} +}});")} +}} + +USpatialDispatcher::FCallbackId {ClassName}::OnCommandResponse(const TFunction& Callback) +{{ +{Text.Indent(1, $@"return SpatialDispatcher->OnCommandResponse({component.ComponentId}, [Callback](const Worker_CommandResponseOp& Op) +{{ +{Text.Indent(1, $@"if (Op.command_id == {command.CommandIndex}) +{{ +{Text.Indent(1, $@"auto Response = Op.status_code == Worker_StatusCode::WORKER_STATUS_CODE_SUCCESS ? +{Text.Indent(1, $@"{Types.GetTypeDisplayName(component.QualifiedName)}::Commands::{Text.SnakeCaseToPascalCase(command.Name)}::Response({Types.GetTypeDisplayName(component.QualifiedName)}::Commands::{ Text.SnakeCaseToPascalCase(command.Name)}::Response::Type::Deserialize(Schema_GetCommandResponseObject(Op.response.schema_type))) : +{Types.GetTypeDisplayName(component.QualifiedName)}::Commands::{Text.SnakeCaseToPascalCase(command.Name)}::Response();")} +Callback({Types.GetTypeDisplayName(component.QualifiedName)}::Commands::{Text.SnakeCaseToPascalCase(command.Name)}::ResponseOp(Op.entity_id, Op.request_id, Op.status_code, Op.message, Op.command_id, Response));")} +}}")} +}});")} +}} +"))}"))}"; + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/MapEquals.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/MapEquals.cs new file mode 100644 index 0000000000..857baceecd --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/MapEquals.cs @@ -0,0 +1,42 @@ +using Improbable.Codegen.Base; +using Improbable.CodeGen.Base; +using System.Collections.Generic; + +namespace Improbable.CodeGen.Unreal +{ + public static class MapEquals + { + public static string HeaderName = "MapEquals.h"; + + public static List GenerateMapEquals() + { + return new List + { + new GeneratedFile(HeaderName, GenerateHeader()), + }; + } + + private static string GenerateHeader() + { + return $@"#pragma once + +template +bool operator==(TMap Map1, TMap Map2) +{{ +{Text.Indent(1, $@"if (Map1.Num() != Map2.Num()) +{{ +{Text.Indent(1, "return false;")} +}} +for (const TPair& Elem : Map1) +{{ +{Text.Indent(1, $@"if (Elem.Value != Map2.FindRef(Elem.Key)) +{{ +{Text.Indent(1, "return false;")} +}}")} +}} +return true;")} +}} +"; + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/Serialization.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/Serialization.cs new file mode 100644 index 0000000000..11e30c60d6 --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/Serialization.cs @@ -0,0 +1,414 @@ +using Improbable.CodeGen.Base; +using System; +using ValueType = Improbable.CodeGen.Base.ValueType; + +namespace Improbable.CodeGen.Unreal +{ + public static class Serialization + { + public static string GetFieldSerialization(FieldDefinition field, string schemaObjectName, string targetObjectName, TypeDescription parentType, Bundle bundle) + { + switch (field.TypeSelector) + { + case FieldType.Singular: + return GetValueTypeSerialization(field.SingularType.Type, schemaObjectName, targetObjectName, field.FieldId.ToString()); + case FieldType.Option: + return GetOptionTypeSerialization(field.OptionType, schemaObjectName, targetObjectName, field.FieldId.ToString(), bundle); + case FieldType.List: + return GetListTypeSerialization(field.ListType, schemaObjectName, targetObjectName, field.FieldId.ToString(), parentType, bundle); + case FieldType.Map: + return GetMapTypeSerialization(field.MapType, schemaObjectName, targetObjectName, field.FieldId.ToString(), parentType, bundle); + default: + throw new InvalidOperationException("Trying to serialize invalid FieldDefinition"); + } + } + + public static string GetFieldDeserialization(FieldDefinition field, string schemaObjectName, string targetObjectName, TypeDescription parentType, Bundle bundle, bool wrapInBlock = false, bool targetIsOption = false) + { + var fieldName = Text.SnakeCaseToPascalCase(field.Name); + switch (field.TypeSelector) + { + case FieldType.Singular: + return $"{targetObjectName} = {GetValueTypeDeserialization(field.SingularType.Type, schemaObjectName, field.FieldId.ToString(), parentType)};"; + case FieldType.Option: + return $"{targetObjectName} = {Types.CollectionTypesToQualifiedTypes[Types.Collection.Option]}<{Types.GetTypeDisplayName(field.OptionType.InnerType, bundle, parentType)}>({GetValueTypeDeserialization(field.OptionType.InnerType, schemaObjectName, field.FieldId.ToString(), parentType)});"; + case FieldType.List: + return GetListTypeDeserialization(field.ListType, schemaObjectName, targetObjectName, fieldName, field.FieldId.ToString(), parentType, bundle, wrapInBlock, targetIsOption); + case FieldType.Map: + return GetMapTypeDeserialization(field.MapType, schemaObjectName, targetObjectName, fieldName, field.FieldId.ToString(), parentType, bundle, wrapInBlock, targetIsOption); + default: + throw new InvalidOperationException("Trying to serialize invalid FieldDefinition"); + } + } + + public static string GetEventDeserialization(ComponentDefinition.EventDefinition _event, string schemaObjectName, string targetObjectName) + { + return $@"for (uint32 i = 0; i < Schema_GetObjectCount({schemaObjectName}, {_event.EventIndex}); ++i) +{{ +{Text.Indent(1, $"{targetObjectName}.Add{Text.SnakeCaseToPascalCase(_event.Name)}({Types.GetTypeDisplayName(_event.Type)}::Deserialize(Schema_IndexObject({schemaObjectName}, {_event.EventIndex}, i)));")} +}}"; + } + + public static string GetFieldClearingCheck(FieldDefinition field) + { + var fieldName = Text.SnakeCaseToPascalCase(field.Name); + switch (field.TypeSelector) + { + case FieldType.Option: + return $"!_{Text.SnakeCaseToPascalCase(fieldName)}.GetValue().IsSet()"; + case FieldType.List: + case FieldType.Map: + return $"_{Text.SnakeCaseToPascalCase(fieldName)}.GetValue().Num() == 0"; + case FieldType.Singular: + throw new InvalidOperationException("Trying to check if singular type should be clear should never happen"); + default: + throw new InvalidOperationException("Trying to get field clearing check for invalid FieldDefinition"); + } + } + + public static string GetFieldTypeCount(FieldDefinition field, string schemaObjectName) + { + switch (field.TypeSelector) + { + case FieldType.Option: + return GetValueTypeCount(field.OptionType.InnerType, schemaObjectName, field.FieldId.ToString()); + case FieldType.List: + return GetValueTypeCount(field.ListType.InnerType, schemaObjectName, field.FieldId.ToString()); + case FieldType.Map: + return $"Schema_GetObjectCount({schemaObjectName}, {field.FieldId})"; + case FieldType.Singular: + return GetValueTypeCount(field.SingularType.Type, schemaObjectName, field.FieldId.ToString()); + default: + throw new InvalidOperationException("Trying to get schema object count for invalid FieldDefinition"); + } + } + + private static string GetValueTypeSerialization(TypeReference value, string schemaObjectName, string targetObjectName, string fieldId) + { + switch (value.ValueTypeSelector) + { + case ValueType.Primitive: + return GetPrimitiveSerialization(value.Primitive, schemaObjectName, targetObjectName, fieldId); + case ValueType.Enum: + return $"Schema_AddEnum({schemaObjectName}, {fieldId}, static_cast({targetObjectName}));"; + case ValueType.Type: + return $"{targetObjectName}.Serialize(Schema_AddObject({schemaObjectName}, {fieldId}));"; + default: + throw new InvalidOperationException("Trying to serialize invalid TypeReference"); + } + } + + private static string GetOptionTypeSerialization(FieldDefinition.OptionTypeRef optionType, string schemaObjectName, string fieldName, string fieldId, Bundle bundle) + { + return $@"if ({fieldName}) +{{ +{Text.Indent(1, GetValueTypeSerialization(optionType.InnerType, schemaObjectName, $"(*{fieldName})", fieldId))} +}}"; + } + + private static string GetMapTypeSerialization(FieldDefinition.MapTypeRef mapType, string schemaObjectName, string fieldName, string fieldId, TypeDescription parentType, Bundle bundle) + { + return $@"for (const TPair<{Types.GetTypeDisplayName(mapType.KeyType, bundle, parentType)}, {Types.GetTypeDisplayName(mapType.ValueType, bundle, parentType)}>& Pair : {fieldName}) +{{ +{Text.Indent(1, $@"Schema_Object* PairObj = Schema_AddObject({schemaObjectName}, {fieldId}); +{GetValueTypeSerialization(mapType.KeyType, "PairObj", "Pair.Key", "SCHEMA_MAP_KEY_FIELD_ID")} +{GetValueTypeSerialization(mapType.ValueType, "PairObj", "Pair.Value", "SCHEMA_MAP_VALUE_FIELD_ID")}")} +}}"; + } + + private static string GetListTypeSerialization(FieldDefinition.ListTypeRef listType, string schemaObjectName, string fieldName, string fieldId, TypeDescription parentType, Bundle bundle) + { + return $@"for (const {Types.GetTypeDisplayName(listType.InnerType, bundle, parentType)}& Element : {fieldName}) +{{ +{Text.Indent(1, GetValueTypeSerialization(listType.InnerType, schemaObjectName, "Element", fieldId))} +}}"; + } + + private static string GetPrimitiveSerialization(PrimitiveType primitive, string schemaObjectName, string fieldName, string fieldId) + { + switch (primitive) + { + case PrimitiveType.Int32: + return $"Schema_AddInt32({schemaObjectName}, {fieldId}, {fieldName});"; + case PrimitiveType.Int64: + return $"Schema_AddInt64({schemaObjectName}, {fieldId}, {fieldName});"; + case PrimitiveType.Uint32: + return $"Schema_AddUint32({schemaObjectName}, {fieldId}, {fieldName});"; + case PrimitiveType.Uint64: + return $"Schema_AddUint64({schemaObjectName}, {fieldId}, {fieldName});"; + case PrimitiveType.Sint32: + return $"Schema_AddSint32({schemaObjectName}, {fieldId}, {fieldName});"; + case PrimitiveType.Sint64: + return $"Schema_AddSint64({schemaObjectName}, {fieldId}, {fieldName});"; + case PrimitiveType.Fixed32: + return $"Schema_AddFixed32({schemaObjectName}, {fieldId}, {fieldName});"; + case PrimitiveType.Fixed64: + return $"Schema_AddFixed64({schemaObjectName}, {fieldId}, {fieldName});"; + case PrimitiveType.Sfixed32: + return $"Schema_AddSfixed32({schemaObjectName}, {fieldId}, {fieldName});"; + case PrimitiveType.Sfixed64: + return $"Schema_AddSfixed64({schemaObjectName}, {fieldId}, {fieldName});"; + case PrimitiveType.Bool: + return $"Schema_AddBool({schemaObjectName}, {fieldId}, static_cast({fieldName}));"; + case PrimitiveType.Float: + return $"Schema_AddFloat({schemaObjectName}, {fieldId}, {fieldName});"; + case PrimitiveType.Double: + return $"Schema_AddDouble({schemaObjectName}, {fieldId}, {fieldName});"; + case PrimitiveType.String: + return $@"::improbable::utils::AddString({schemaObjectName}, {fieldId}, {fieldName});"; + case PrimitiveType.EntityId: + return $"Schema_AddEntityId({schemaObjectName}, {fieldId}, {fieldName});"; + case PrimitiveType.Bytes: + return $@"::improbable::utils::AddBytes({schemaObjectName}, {fieldId}, {fieldName});"; + case PrimitiveType.Invalid: + default: + throw new InvalidOperationException("Trying to serialize invalid PrimitiveType"); + } + } + + private static string GetValueTypeDeserialization(TypeReference type, string schemaObjectName, string fieldId, TypeDescription parentType) + { + switch (type.ValueTypeSelector) + { + case ValueType.Primitive: + return GetPrimitiveDeserialization(type.Primitive, schemaObjectName, fieldId); + case ValueType.Type: + return $"{Types.GetTypeDisplayName(type.Type, Types.IsTypeBeingUsedInTheContextWhereItIsDefined(type.Type, parentType))}::Deserialize(Schema_GetObject({schemaObjectName}, {fieldId}))"; + case ValueType.Enum: + return $"static_cast<{Types.GetTypeDisplayName(type.Enum, Types.IsTypeBeingUsedInTheContextWhereItIsDefined(type.Enum, parentType))}>(Schema_GetEnum({ schemaObjectName}, { fieldId}))"; + default: + throw new InvalidOperationException("Trying to deserialize invalid TypeReference"); + } + } + + private static string GetListTypeDeserialization(FieldDefinition.ListTypeRef listType, string schemaObjectName, string targetObjectName, string fieldName, string fieldId, TypeDescription parentType, + Bundle bundle, bool wrapInBlock = false, bool targetIsOption = false) + { + var listInnerTypeName = Types.GetTypeDisplayName(listType.InnerType, bundle, parentType); + var listName = $"{Text.SnakeCaseToPascalCase(fieldName)}List"; + + var deserializationText = ""; + switch (listType.InnerType.ValueTypeSelector) + { + case ValueType.Primitive: + deserializationText = GetPrimitiveListDeserialization(listType.InnerType.Primitive, schemaObjectName, targetObjectName, fieldName, fieldId, bundle, targetIsOption); + break; + case ValueType.Type: + deserializationText = $@"auto ListLength = Schema_GetObjectCount({schemaObjectName}, {fieldId}); +{Types.CollectionTypesToQualifiedTypes[Types.Collection.List]}<{listInnerTypeName}> {listName}; +{listName}.SetNum(ListLength); +for (uint32 i = 0; i < ListLength; ++i) +{{ +{Text.Indent(1, $"{listName}[i] = {listInnerTypeName}::Deserialize(Schema_IndexObject({schemaObjectName}, {fieldId}, i));")} +}} +{targetObjectName} = {listName};"; + break; + case ValueType.Enum: + deserializationText = $@"auto ListLength = Schema_GetEnumCount({schemaObjectName}, {fieldId}); +{Types.CollectionTypesToQualifiedTypes[Types.Collection.List]}<{listInnerTypeName}> {listName}; +{listName}.SetNum(ListLength); +for (uint32 i = 0; i < ListLength; ++i) +{{ +{Text.Indent(1, $"{listName}[i] = static_cast<{listInnerTypeName}>(Schema_IndexEnum({schemaObjectName}, {fieldId}, i));")} +}} +{targetObjectName} = {listName};"; + break; + default: + throw new InvalidOperationException("Trying to deserialize invalid TypeReference"); + } + + return wrapInBlock ? $"{{{Environment.NewLine}{Text.Indent(1, deserializationText)}{Environment.NewLine}}}" : deserializationText; + } + + private static string GetMapTypeDeserialization(FieldDefinition.MapTypeRef mapType, string schemaObjectName, string targetObjectName, string fieldName, string fieldId, + TypeDescription parentType, Bundle bundle, bool wrapInBlock = false, bool targetIsOption = false) + { + var keyTypeName = Types.GetTypeDisplayName(mapType.KeyType, bundle, parentType); + var valueTypeName = Types.GetTypeDisplayName(mapType.ValueType, bundle, parentType); + var mapName = $"{fieldName}Map"; + var kvPairName = "KvPair"; + + var deserializationText = $@"{{ +{Text.Indent(1, $@"{targetObjectName} = {Types.CollectionTypesToQualifiedTypes[Types.Collection.Map]}<{keyTypeName}, {valueTypeName}>(); +auto MapEntryCount = Schema_GetObjectCount({schemaObjectName}, {fieldId}); +for (uint32 i = 0; i < MapEntryCount; ++i) +{{ +{Text.Indent(1, $@"Schema_Object* {kvPairName} = Schema_IndexObject({schemaObjectName}, {fieldId}, i); +auto Key = {GetValueTypeDeserialization(mapType.KeyType, kvPairName, "SCHEMA_MAP_KEY_FIELD_ID", parentType)}; +auto Value = {GetValueTypeDeserialization(mapType.ValueType, kvPairName, "SCHEMA_MAP_VALUE_FIELD_ID", parentType)}; +{(targetIsOption ? $"(*{targetObjectName})" : targetObjectName)}[std::move(Key)] = std::move(Value);")} +}}")} +}}"; + + return wrapInBlock ? $"{{{Environment.NewLine}{Text.Indent(1, deserializationText)}{Environment.NewLine}}}" : deserializationText; + } + + private static string GetPrimitiveListDeserialization(PrimitiveType primitive, string schemaObjectName, string targetObjectName, string fieldName, string fieldId, Bundle bundle, bool targetIsOption = false) + { + targetObjectName = targetIsOption ? $"(*{targetObjectName})" : targetObjectName; + + if (primitive == PrimitiveType.Bytes) + { + return $"{targetObjectName} = ::improbable::utils::GetBytesList({schemaObjectName}, {fieldId});"; + } + else if (primitive == PrimitiveType.String) + { + return $"{targetObjectName} = ::improbable::utils::GetStringList({schemaObjectName}, {fieldId});"; + } + + var listInnerType = Types.SchemaToCppTypes[primitive]; + var listCopyFunction = ""; + switch (primitive) + { + case PrimitiveType.Int32: + listCopyFunction = $"Schema_GetInt32List({ schemaObjectName}, {fieldId}, {targetObjectName}.GetData());"; + break; + case PrimitiveType.Int64: + listCopyFunction = $"Schema_GetInt64List({schemaObjectName}, {fieldId}, {targetObjectName}.GetData());"; + break; + case PrimitiveType.Uint32: + listCopyFunction = $"Schema_GetUint32List({schemaObjectName}, {fieldId}, {targetObjectName}.GetData());"; + break; + case PrimitiveType.Uint64: + listCopyFunction = $"Schema_GetUint64List({schemaObjectName}, {fieldId}, {targetObjectName}.GetData());"; + break; + case PrimitiveType.Sint32: + listCopyFunction = $"Schema_GetSint32List({schemaObjectName}, {fieldId}, {targetObjectName}.GetData());"; + break; + case PrimitiveType.Sint64: + listCopyFunction = $"Schema_GetSint64List({schemaObjectName}, {fieldId}, {targetObjectName}.GetData());"; + break; + case PrimitiveType.Fixed32: + listCopyFunction = $"Schema_GetFixed32List({schemaObjectName}, {fieldId}, {targetObjectName}.GetData());"; + break; + case PrimitiveType.Fixed64: + listCopyFunction = $"Schema_GetFixed64List({schemaObjectName}, {fieldId}, {targetObjectName}.GetData());"; + break; + case PrimitiveType.Sfixed32: + listCopyFunction = $"Schema_GetSfixed32List({schemaObjectName}, {fieldId}, {targetObjectName}.GetData());"; + break; + case PrimitiveType.Sfixed64: + listCopyFunction = $"Schema_GetSfixed64List({schemaObjectName}, {fieldId}, {targetObjectName}.GetData());"; + break; + case PrimitiveType.Bool: + listCopyFunction = $"Schema_GetBoolList({schemaObjectName}, {fieldId}, (uint8*) {targetObjectName}.GetData());"; + break; + case PrimitiveType.Float: + listCopyFunction = $"Schema_GetFloatList({schemaObjectName}, {fieldId}, {targetObjectName}.GetData());"; + break; + case PrimitiveType.Double: + listCopyFunction = $"Schema_GetDoubleList({schemaObjectName}, {fieldId}, {targetObjectName}.GetData());"; + break; + case PrimitiveType.EntityId: + listCopyFunction = $"Schema_GetEntityIdList({schemaObjectName}, {fieldId}, {targetObjectName}.GetData());"; + break; + case PrimitiveType.Invalid: + default: + throw new InvalidOperationException("Trying to serialize invalid PrimitiveType"); + } + + return $@"uint32 {fieldName}Length = {GetPrimitiveCount(primitive, schemaObjectName, fieldId)}; +{Types.GetListInitialisation(targetObjectName, listInnerType, $"{fieldName}Length")} +{listCopyFunction}"; + } + + private static string GetPrimitiveDeserialization(PrimitiveType primitive, string schemaObjectName, string fieldId) + { + switch (primitive) + { + case PrimitiveType.Int32: + return $"Schema_GetInt32({ schemaObjectName}, { fieldId})"; + case PrimitiveType.Int64: + return $"Schema_GetInt64({schemaObjectName}, {fieldId})"; + case PrimitiveType.Uint32: + return $"Schema_GetUint32({schemaObjectName}, {fieldId})"; + case PrimitiveType.Uint64: + return $"Schema_GetUint64({schemaObjectName}, {fieldId})"; + case PrimitiveType.Sint32: + return $"Schema_GetSint32({schemaObjectName}, {fieldId})"; + case PrimitiveType.Sint64: + return $"Schema_GetSint64({schemaObjectName}, {fieldId})"; + case PrimitiveType.Fixed32: + return $"Schema_GetFixed32({schemaObjectName}, {fieldId})"; + case PrimitiveType.Fixed64: + return $"Schema_GetFixed32({schemaObjectName}, {fieldId})"; + case PrimitiveType.Sfixed32: + return $"Schema_GetSfixed32({schemaObjectName}, {fieldId})"; + case PrimitiveType.Sfixed64: + return $"Schema_GetSfixed64({schemaObjectName}, {fieldId})"; + case PrimitiveType.Bool: + return $"Schema_GetBool({schemaObjectName}, {fieldId})"; + case PrimitiveType.Float: + return $"Schema_GetFloat({schemaObjectName}, {fieldId})"; + case PrimitiveType.Double: + return $"Schema_GetDouble({schemaObjectName}, {fieldId})"; + case PrimitiveType.EntityId: + return $"Schema_GetEntityId({schemaObjectName}, {fieldId})"; + case PrimitiveType.Bytes: + return $"::improbable::utils::GetBytes({schemaObjectName}, {fieldId})"; + case PrimitiveType.String: + return $"::improbable::utils::GetString({schemaObjectName}, {fieldId})"; + case PrimitiveType.Invalid: + default: + throw new InvalidOperationException("Trying to serialize invalid PrimitiveType"); + } + } + + private static string GetValueTypeCount(TypeReference type, string schemaObjectName, string fieldId) + { + switch (type.ValueTypeSelector) + { + case ValueType.Primitive: + return GetPrimitiveCount(type.Primitive, schemaObjectName, fieldId); + case ValueType.Type: + return $"Schema_GetObjectCount({ schemaObjectName}, { fieldId})"; + case ValueType.Enum: + return $"Schema_GetEnumCount({ schemaObjectName}, { fieldId})"; + default: + throw new InvalidOperationException("Trying to deserialize invalid TypeReference"); + } + } + + private static string GetPrimitiveCount(PrimitiveType primitive, string schemaObjectName, string fieldId) + { + switch (primitive) + { + case PrimitiveType.Int32: + return $"Schema_GetInt32Count({ schemaObjectName}, { fieldId})"; + case PrimitiveType.Int64: + return $"Schema_GetInt64Count({schemaObjectName}, {fieldId})"; + case PrimitiveType.Uint32: + return $"Schema_GetUint32Count({schemaObjectName}, {fieldId})"; + case PrimitiveType.Uint64: + return $"Schema_GetUint64Count({schemaObjectName}, {fieldId})"; + case PrimitiveType.Sint32: + return $"Schema_GetSint32Count({schemaObjectName}, {fieldId})"; + case PrimitiveType.Sint64: + return $"Schema_GetSint64Count({schemaObjectName}, {fieldId})"; + case PrimitiveType.Fixed32: + return $"Schema_GetFixed32Count({schemaObjectName}, {fieldId})"; + case PrimitiveType.Fixed64: + return $"Schema_GetFixed32Count({schemaObjectName}, {fieldId})"; + case PrimitiveType.Sfixed32: + return $"Schema_GetSfixed32Count({schemaObjectName}, {fieldId})"; + case PrimitiveType.Sfixed64: + return $"Schema_GetSfixed64Count({schemaObjectName}, {fieldId})"; + case PrimitiveType.Bool: + return $"Schema_GetBoolCount({schemaObjectName}, {fieldId})"; + case PrimitiveType.Float: + return $"Schema_GetFloatCount({schemaObjectName}, {fieldId})"; + case PrimitiveType.Double: + return $"Schema_GetDoubleCount({schemaObjectName}, {fieldId})"; + case PrimitiveType.String: + return $"Schema_GetBytesCount({schemaObjectName}, {fieldId})"; + case PrimitiveType.EntityId: + return $"Schema_GetEntityIdCount({schemaObjectName}, {fieldId})"; + case PrimitiveType.Bytes: + return $"Schema_GetBytesCount({schemaObjectName}, {fieldId})"; + case PrimitiveType.Invalid: + default: + throw new InvalidOperationException("Trying to serialize invalid PrimitiveType"); + } + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/SourceGenerator.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/SourceGenerator.cs new file mode 100644 index 0000000000..5acf5c1f0b --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/SourceGenerator.cs @@ -0,0 +1,353 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Improbable.CodeGen.Base; + +namespace Improbable.CodeGen.Unreal +{ + public static class SourceGenerator + { + public static string GenerateSource(TypeDescription type, List types, Dictionary allGeneratedTypeContent, Bundle bundle) + { + var allNestedTypes = Types.GetRecursivelyNestedTypes(type); + var allNestedEnums = Types.GetRecursivelyNestedEnums(type); + var typeNamespaces = Text.GetNamespaceFromTypeName(type.QualifiedName); + + var builder = new StringBuilder(); + + builder.AppendLine($@"// Generated by {UnrealGenerator.GeneratorTitle} + +#include ""{type.Name}.h"" +#include +#include ""{string.Concat(Enumerable.Repeat("../", type.QualifiedName.Count(c => c == '.')))}{MapEquals.HeaderName}"" + +// Generated from {Path.GetFullPath(bundle.TypeToFileName[type.QualifiedName])}({type.SourceReference.Line},{type.SourceReference.Column}) +{string.Join(Environment.NewLine, typeNamespaces.Select(t => $"namespace {t} {{"))} + +{GenerateTypeFunctions(type.Name, type, bundle)}"); + + if (type.ComponentId.HasValue) + { + builder.AppendLine(GenerateComponentUpdateFunctions(type.Name, type, bundle)); + } + + if (allNestedTypes.Count() > 0) + { + builder.AppendLine(string.Join(Environment.NewLine, allNestedTypes.Select(topLevelType => GenerateTypeFunctions(Types.GetTypeClassName(topLevelType.QualifiedName, bundle), types.Find(t => t.QualifiedName == topLevelType.QualifiedName), bundle)))); + } + + builder.AppendLine(GenerateHashFunction(type, bundle)); + + if (allNestedTypes.Count() > 0) + { + builder.AppendLine(string.Join(Environment.NewLine, allNestedTypes.Select(topLevelType => GenerateHashFunction(topLevelType, bundle)))); + } + if (allNestedEnums.Count() > 0) + { + builder.AppendLine(string.Join(Environment.NewLine, allNestedEnums.Select(nestedEnum => GenerateHashFunction(nestedEnum, bundle)))); + } + + builder.AppendLine(string.Join(Environment.NewLine, typeNamespaces.Reverse().Select(t => $"}} // namespace {t}"))); + + return builder.ToString(); + } + + private static string GenerateTypeFunctions(string name, TypeDescription type, Bundle bundle) + { + var argType = type.ComponentId.HasValue ? "Schema_ComponentData" : "Schema_Object"; + var argName = type.ComponentId.HasValue ? "ComponentData" : "SchemaObject"; + var componentFieldsName = "FieldsObject"; + var targetSchemaObject = type.ComponentId.HasValue ? componentFieldsName : argName; + + var builder = new StringBuilder(); + + if (type.Fields.Count > 0) + { + builder.AppendLine($@"{name}::{name}( +{Text.Indent(1, $"{string.Join($", {Environment.NewLine}", type.Fields.Select(f => $"{Types.GetConstAccessorTypeModification(f, bundle, type)} {Text.SnakeCaseToPascalCase(f.Name)}"))})")} +: {string.Join($"{Environment.NewLine}, ", type.Fields.Select(f => $"_{Text.SnakeCaseToPascalCase(f.Name)}{{ {Text.SnakeCaseToPascalCase(f.Name)} }}"))} {{}} +"); + } + + builder.AppendLine($@"{name}::{name}() {{}} + +bool {name}::operator==(const {name}& Value) const +{{ +{Text.Indent(1, type.Fields.Count == 0 +? "return true;" +: $"return {string.Join($@" && {Environment.NewLine}", type.Fields.Select(f => Types.GetFieldDefinitionEquals(f, $"_{Text.SnakeCaseToPascalCase(f.Name)}", "Value")))};")} +}} + +bool {name}::operator!=(const {name}& Value) const +{{ +{Text.Indent(1, $"return !operator== (Value);")} +}} +"); + if (type.Fields.Count() > 0) + { + builder.AppendLine($@"{string.Join(Environment.NewLine, type.Fields.Select(field => $@"{Types.GetConstAccessorTypeModification(field, bundle, type)} {name}::Get{Text.SnakeCaseToPascalCase(field.Name)}() const +{{ +{Text.Indent(1, $"return _{Text.SnakeCaseToPascalCase(field.Name)};")} +}} + +{Types.GetFieldTypeAsCpp(field, bundle, type)}& {name}::Get{Text.SnakeCaseToPascalCase(field.Name)}() +{{ +{Text.Indent(1, $"return _{ Text.SnakeCaseToPascalCase(field.Name)}; ")} +}} + +{name}& {name}::Set{Text.SnakeCaseToPascalCase(field.Name)}({Types.GetConstAccessorTypeModification(field, bundle, type)} Value) +{{ +{Text.Indent(1, $@"_{ Text.SnakeCaseToPascalCase(field.Name)} = Value; +return *this;")} +}} +"))}"); + } + + if (type.Fields.Count == 0 && type.Events == null) + { + builder.AppendLine($@"void {name}::Serialize({argType}* {argName}) const {{}} + +{name} {name}::Deserialize({argType}* {argName}) +{{ +{Text.Indent(1, $"return {name}::Create();")} +}}"); + } + else + { + builder.AppendLine($@"void {name}::Serialize({argType}* {argName}) const +{{"); + if (type.ComponentId.HasValue) + { + builder.AppendLine(Text.Indent(1, $"Schema_Object* {componentFieldsName} = Schema_GetComponentDataFields({argName});")); + builder.AppendLine(); + } + + builder.AppendLine($@"{Text.Indent(1, string.Join(Environment.NewLine, type.Fields.Select(field => $@"// serializing field {Text.SnakeCaseToPascalCase(field.Name)} = {field.FieldId} +{Serialization.GetFieldSerialization(field, targetSchemaObject, $"_{Text.SnakeCaseToPascalCase(field.Name)}", type, bundle)}")))} +}} + +{name} {name}::Deserialize({argType}* {argName}) +{{"); + + if (type.ComponentId.HasValue) + { + builder.AppendLine(Text.Indent(1, $"Schema_Object * {componentFieldsName} = Schema_GetComponentDataFields({argName});")); + } + + builder.AppendLine($@"{Text.Indent(1, $@"{name} Data; + +{string.Join(Environment.NewLine, type.Fields.Select(field => $@"// deserializing field {Text.SnakeCaseToPascalCase(field.Name)} = {field.FieldId} +{Serialization.GetFieldDeserialization(field, targetSchemaObject, $"Data._{Text.SnakeCaseToPascalCase(field.Name)}", type, bundle, true)} +"))} +return Data;")} +}}"); + } + + return builder.ToString(); + } + + private static string GenerateComponentUpdateFunctions(string name, TypeDescription type, Bundle bundle) + { + var updatesObjectName = "UpdatesObject"; + var eventsObjectName = "EventsObject"; + var componentUpdateObjectName = "ComponentUpdate"; + var deserializingTargetObjectName = "Data"; + + var builder = new StringBuilder(); + + builder.AppendLine($@"bool {name}::Update::operator==(const {name}::Update& Value) const +{{ +{Text.Indent(1, type.Fields.Count == 0 ? "return true;" +: $"return {string.Join($@" && {Environment.NewLine}", type.Fields.Select(f => Types.GetFieldDefinitionEquals(f, $"_{Text.SnakeCaseToPascalCase(f.Name)}", "Value")))};")} +}} + +bool {name}::Update::operator!=(const {name}::Update& Value) const +{{ +{Text.Indent(1, $"return !operator== (Value);")} +}} + +{name}::Update {name}::Update::FromInitialData(const {name}& Data) +{{ +{Text.Indent(1, $@"{name}::Update Update; +{string.Join(Environment.NewLine, type.Fields.Select(f => $"Update._{Text.SnakeCaseToPascalCase(f.Name)} = Data.Get{Text.SnakeCaseToPascalCase(f.Name)}();"))} +return Update;")} +}} + +{name} {name}::Update::ToInitialData() const +{{ +{Text.Indent(1, $@"return {name} ( +{string.Join($",{Environment.NewLine}", type.Fields.Select(f => Text.Indent(1, $"*_{Text.SnakeCaseToPascalCase(f.Name)}")))});")} +}} + +void {name}::Update::ApplyTo({name}& Data) const +{{ +{Text.Indent(1, string.Join(Environment.NewLine, type.Fields.Select(field => $@"if (_{Text.SnakeCaseToPascalCase(field.Name)}) +{{ +{Text.Indent(1, $@"Data.Set{Text.SnakeCaseToPascalCase(field.Name)}(*_{Text.SnakeCaseToPascalCase(field.Name)});")} +}}")))} +}} +"); + if (type.Fields.Count() > 0) + { + builder.AppendLine(string.Join(Environment.NewLine, type.Fields.Select(field => $@"const {Types.CollectionTypesToQualifiedTypes[Types.Collection.Option]}<{Types.GetFieldTypeAsCpp(field, bundle, type)}>& {name}::Update::Get{Text.SnakeCaseToPascalCase(field.Name)}() const +{{ +{Text.Indent(1, $"return _{Text.SnakeCaseToPascalCase(field.Name)};")} +}} + +{Types.CollectionTypesToQualifiedTypes[Types.Collection.Option]}<{Types.GetFieldTypeAsCpp(field, bundle, type)}>& {name}::Update::Get{Text.SnakeCaseToPascalCase(field.Name)}() +{{ +{Text.Indent(1, $"return _{Text.SnakeCaseToPascalCase(field.Name)};")} +}} + +{name}::Update& {name}::Update::Set{Text.SnakeCaseToPascalCase(field.Name)}({Types.GetConstAccessorTypeModification(field, bundle, type)} value) +{{ +{Text.Indent(1, $@"_{ Text.SnakeCaseToPascalCase(field.Name)} = value; +return *this;")} +}} +"))); + } + + if (type.Events.Count() > 0) + { + builder.AppendLine(string.Join(Environment.NewLine, type.Events.Select(_event => $@"const {Types.CollectionTypesToQualifiedTypes[Types.Collection.List]}< {Types.GetTypeDisplayName(_event.Type)} >& {name}::Update::Get{Text.SnakeCaseToPascalCase(_event.Name)}List() const +{{ +{Text.Indent(1, $"return _{Text.SnakeCaseToPascalCase(_event.Name)}List;")} +}} + +{Types.CollectionTypesToQualifiedTypes[Types.Collection.List]}< {Types.GetTypeDisplayName(_event.Type)} >& {name}::Update::Get{Text.SnakeCaseToPascalCase(_event.Name)}List() +{{ +{Text.Indent(1, $"return _{Text.SnakeCaseToPascalCase(_event.Name)}List;")} +}} + +{ name}::Update& {name}::Update::Add{Text.SnakeCaseToPascalCase(_event.Name)}(const {Types.GetTypeDisplayName(_event.Type)}& Value) +{{ +{Text.Indent(1, $@"_{Text.SnakeCaseToPascalCase(_event.Name)}List.Add(Value); +return *this;")} +}} +"))); + } + + if (type.Fields.Count == 0 && type.Events.Count == 0) + { + builder.AppendLine($@"void {name}::Update::Serialize(Schema_ComponentUpdate* ComponentUpdate) const {{}} + +{name}::Update {name}::Update::Deserialize(Schema_ComponentUpdate* ComponentUpdate) +{{ +{Text.Indent(1, $"return {name}::Update::Create();")} +}}"); + return builder.ToString(); + } + + builder.AppendLine($@"void {name}::Update::Serialize(Schema_ComponentUpdate* ComponentUpdate) const +{{"); + if (type.Fields.Count > 0) + { + builder.AppendLine(Text.Indent(1, $"Schema_Object* {updatesObjectName} = Schema_GetComponentUpdateFields(ComponentUpdate);")); + } + + if (type.Events.Count > 0) + { + builder.AppendLine(Text.Indent(1, $"Schema_Object* {eventsObjectName} = Schema_GetComponentUpdateEvents(ComponentUpdate);")); + } + + if (type.Fields.Count > 0) + { + builder.AppendLine(Text.Indent(1, string.Join(Environment.NewLine, type.Fields.Select(field => $@"// serializing field {Text.SnakeCaseToPascalCase(field.Name)} = {field.FieldId} +if (_{Text.SnakeCaseToPascalCase(field.Name)}.IsSet()) +{{ +{Text.Indent(1, field.TypeSelector != FieldType.Singular ? $@"if ({Serialization.GetFieldClearingCheck(field)}) +{{ +{Text.Indent(1, $"Schema_AddComponentUpdateClearedField(ComponentUpdate, {field.FieldId});")} +}} +else +{{ +{Text.Indent(1, Serialization.GetFieldSerialization(field, updatesObjectName, $"(*_{Text.SnakeCaseToPascalCase(field.Name)})", type, bundle))} +}}" +: +Serialization.GetFieldSerialization(field, updatesObjectName, $"(*_{Text.SnakeCaseToPascalCase(field.Name)})", type, bundle))} +}}")))); + } + + if (type.Events.Count > 0) + { + builder.AppendLine(Text.Indent(1, $@"{Text.Indent(1, string.Join(Environment.NewLine, type.Events.Select(_event => $@"// serializing event {Text.SnakeCaseToPascalCase(_event.Name)} = {_event.EventIndex} +for (const {Types.GetTypeDisplayName(_event.Type)}& Element : _{Text.SnakeCaseToPascalCase(_event.Name)}List) +{{ +{Text.Indent(1, $"Element.Serialize(Schema_AddObject({eventsObjectName}, {_event.EventIndex}));")} +}}")))}")); + } + + builder.AppendLine("}"); + builder.AppendLine(); + + builder.AppendLine($@"{name}::Update { name}::Update::Deserialize(Schema_ComponentUpdate * ComponentUpdate) +{{"); + + if (type.Fields.Count > 0) + { + builder.AppendLine(Text.Indent(1, $"Schema_Object* {updatesObjectName} = Schema_GetComponentUpdateFields({componentUpdateObjectName});")); + } + + if (type.Events.Count > 0) + { + builder.AppendLine(Text.Indent(1, $"Schema_Object* {eventsObjectName} = Schema_GetComponentUpdateEvents({componentUpdateObjectName});")); + } + + builder.AppendLine(Text.Indent(1, $@" +uint32_t FieldCount = Schema_GetComponentUpdateClearedFieldCount(ComponentUpdate); +Schema_FieldId* FieldsToClear = new Schema_FieldId[FieldCount]; +Schema_GetComponentUpdateClearedFieldList({componentUpdateObjectName}, FieldsToClear); +std::set FieldsToClearSet(FieldsToClear, FieldsToClear + FieldCount * sizeof(Schema_FieldId)); + +{name}::Update {deserializingTargetObjectName}; +")); + + if (type.Fields.Count > 0) + { + builder.AppendLine(Text.Indent(1, string.Join(Environment.NewLine, type.Fields.Select(field => $@"// deserializing field {Text.SnakeCaseToPascalCase(field.Name)} = {field.FieldId} +if ({Serialization.GetFieldTypeCount(field, updatesObjectName)} > 0) +{{ +{Text.Indent(1, Serialization.GetFieldDeserialization(field, updatesObjectName, $"{deserializingTargetObjectName}._{Text.SnakeCaseToPascalCase(field.Name)}", type, bundle, false, true))} +}} +{(field.TypeSelector != FieldType.Singular ? $@"else if (FieldsToClearSet.count({field.FieldId})) // only check if lists, maps, or options should be cleared +{{ +{Text.Indent(1, $"{deserializingTargetObjectName}._{Text.SnakeCaseToPascalCase(field.Name)} = {{}};")} +}} +" : string.Empty)}")))); + } + + if (type.Events.Count > 0) + { + builder.AppendLine(Text.Indent(1, string.Join(Environment.NewLine, type.Events.Select(_event => $@"// deserializing event {Text.SnakeCaseToPascalCase(_event.Name)} = {_event.EventIndex} +{Serialization.GetEventDeserialization(_event, eventsObjectName, deserializingTargetObjectName)} +")))); + } + + builder.AppendLine($@"{Text.Indent(1, $@"return {deserializingTargetObjectName};")} +}}"); + + return builder.ToString(); + } + + private static string GenerateHashFunction(TypeDescription type, Bundle bundle) + { + return $@"uint32 GetTypeHash(const {Types.GetTypeClassQualifiedPath(type.QualifiedName, bundle)}& Value) +{{ +{Text.Indent(1, $@"uint32 Result = 1327; +{string.Join(Environment.NewLine, type.Fields.Select(field => $"{Types.GetFieldDefinitionHash($"Value.Get{Text.SnakeCaseToPascalCase(field.Name)}()", field, "Result", bundle)}"))} +return Result;")} +}} +"; + } + + private static string GenerateHashFunction(EnumDefinition enumDef, Bundle bundle) + { + return $@"uint32 GetTypeHash(const {Types.GetTypeClassQualifiedPath(enumDef.QualifiedName, bundle)}& Value) +{{ +{Text.Indent(1, "return static_cast(Value);")} +}}"; + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/Types.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/Types.cs new file mode 100644 index 0000000000..8bd9c6219a --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/Types.cs @@ -0,0 +1,429 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Improbable.CodeGen.Base; +using ValueType = Improbable.CodeGen.Base.ValueType; + +namespace Improbable.CodeGen.Unreal +{ + public class TypeGeneratedCode + { + public TypeGeneratedCode(string headerText, string sourceText) + { + this.headerText = headerText; + this.sourceText = sourceText; + } + public string headerText; + public string sourceText; + } + + public static class Types + { + public enum Collection + { + List, Map, Option + } + + public static Dictionary SchemaToCppTypes = new Dictionary + { + {PrimitiveType.Double, "double"}, + {PrimitiveType.Float, "float"}, + {PrimitiveType.Int32, "int32"}, + {PrimitiveType.Int64, "int64"}, + {PrimitiveType.Uint32, "uint32"}, + {PrimitiveType.Uint64, "uint64"}, + {PrimitiveType.Sint32, "int32"}, + {PrimitiveType.Sint64, "int64"}, + {PrimitiveType.Fixed32, "uint32"}, + {PrimitiveType.Fixed64, "uint64"}, + {PrimitiveType.Sfixed32, "int32"}, + {PrimitiveType.Sfixed64, "int64"}, + {PrimitiveType.Bool, "bool"}, + {PrimitiveType.String, "FString"}, + {PrimitiveType.Bytes, "TArray"}, + {PrimitiveType.EntityId, "Worker_EntityId"} + }; + + public static Dictionary CollectionTypesToQualifiedTypes = new Dictionary + { + {Collection.Map, "TMap"}, + {Collection.List, "TArray"}, + {Collection.Option, "SpatialGDK::TSchemaOption"}, + }; + + public static string GetFieldDefinitionHash(string name, FieldDefinition field, string accumulatorName,Bundle bundle) + { + switch (field.TypeSelector) + { + case FieldType.Option: + return $"{accumulatorName} = ({accumulatorName} * 977) + ({name}.IsSet() ? 1327u * {GetFieldDefinitionHash($"*{name}", field.OptionType.InnerType, bundle)} + 977u : 977u);"; + case FieldType.List: + return $@"for (const auto& item : {name}) +{{ +{Text.Indent(1, $"{accumulatorName} = ({accumulatorName} * 977) + {GetFieldDefinitionHash("item", field.ListType.InnerType, bundle)};")} +}}"; + case FieldType.Map: + return $@"for (const auto& pair : {name}) +{{ +{Text.Indent(1, $"{accumulatorName} += 1327 * ({GetFieldDefinitionHash("pair.Key", field.MapType.KeyType, bundle)} + 977 * {GetFieldDefinitionHash("pair.Value", field.MapType.ValueType, bundle)});")} +}}"; + case FieldType.Singular: + return $"{accumulatorName} = ({accumulatorName} * 977) + {GetFieldDefinitionHash(name, field.SingularType.Type, bundle)};"; + default: + throw new InvalidOperationException("Trying to hash invalid FieldDefinition"); + } + } + + public static string GetFieldDefinitionEquals(FieldDefinition field, string fieldName, string otherObjName) + { + return $"{fieldName} == {otherObjName}.{fieldName}"; + } + + public static string GetFieldDefinitionHash(string name, TypeReference typeReference, Bundle bundle) + { + switch (typeReference.ValueTypeSelector) + { + case ValueType.Enum: + return $"::GetTypeHash({name})"; + case ValueType.Primitive: + return $"::GetTypeHash({name})"; + case ValueType.Type: + return $"{GetNamespaceFromQualifiedName(typeReference.Type, bundle)}::GetTypeHash({name})"; + default: + throw new InvalidOperationException("Trying to hash invalid TypeReference"); + } + } + + public static string GetListInitialisation(string targetObjectName, string listInnerType, string length) + { + return $@"{targetObjectName} = {CollectionTypesToQualifiedTypes[Collection.List]}<{listInnerType}>(); +{targetObjectName}.SetNum({length});"; + } + + // For a type, get all required includes based on fields, events and nested types + public static List GetRequiredTypeIncludes(TypeDescription type, Bundle bundle) + { + var includeTypeQualifiedNames = new List(); + + // Get all possible includes required by fields (and those in nested types) + foreach (var field in type.Fields.Concat(type.NestedTypes.SelectMany(t => t.Fields))) + { + AddRequiredTypeQualifiedNames(field, ref includeTypeQualifiedNames); + } + + // Get all possible includes required by events (if type is a component) + if (type.ComponentId.HasValue) + { + foreach (var _event in type.Events) + { + includeTypeQualifiedNames.Add(_event.Type); + } + foreach (var command in bundle.Components[type.QualifiedName].Commands) + { + includeTypeQualifiedNames.Add(command.RequestType); + includeTypeQualifiedNames.Add(command.ResponseType); + } + } + + // Filter out #includes for types nested in our type (they are defined in the same file so we don't need to include anything) + var nestedTypeOrEnumNames = type.NestedTypes.Select(t => t.QualifiedName).Concat(type.NestedEnums.Select(e => e.QualifiedName)); + includeTypeQualifiedNames = includeTypeQualifiedNames.Where(name => !nestedTypeOrEnumNames.Contains($"{name}.")).ToList(); + + // Map any nested type dependencies to their include file + includeTypeQualifiedNames = includeTypeQualifiedNames.Select(t => GetOutermostType(t, bundle)).ToList(); + + return includeTypeQualifiedNames.Select(name => $"{name.Replace(".", "/")}.h").Distinct().ToList(); + } + + // Nested types are created as sibling classes with the parent type embedded into the class name + // e.g. if type Bar is nested in type Foo, it will be generated as Foo_Bar + public static string GetNestedTypeQualifiedName(TypeDescription type) + { + var splitQualifiedName = type.QualifiedName.Split('.'); + if (splitQualifiedName.Count() == 1) + { + throw new InvalidOperationException("Tried to find nested type name for a top-level type"); + } + return string.Join(".", splitQualifiedName.Take(splitQualifiedName.Count() - 1)) + "_" + splitQualifiedName.Last(); + } + + public static string GetFieldTypeAsCpp(FieldDefinition field, Bundle lookups, TypeDescription typeContext) + { + switch (field.TypeSelector) + { + case FieldType.Option: + return $"{CollectionTypesToQualifiedTypes[Collection.Option]}<{GetTypeDisplayName(field.OptionType.InnerType, lookups, typeContext)}>"; + case FieldType.List: + return $"{CollectionTypesToQualifiedTypes[Collection.List]}<{GetTypeDisplayName(field.ListType.InnerType, lookups, typeContext)}>"; + case FieldType.Map: + return $"{CollectionTypesToQualifiedTypes[Collection.Map]}<{GetTypeDisplayName(field.MapType.KeyType, lookups, typeContext)}, {GetTypeDisplayName(field.MapType.ValueType, lookups, typeContext)}>"; + case FieldType.Singular: + return GetTypeDisplayName(field.SingularType.Type, lookups, typeContext); + default: + throw new ArgumentOutOfRangeException(); + } + } + + public static string GetTypeDisplayName(TypeReference typeRef, Bundle bundle, TypeDescription typeContext) + { + switch (typeRef.ValueTypeSelector) + { + case ValueType.Enum: + return IsNested(typeRef, bundle) ? GetTypeClassQualifiedPathInContext(typeRef.Enum, bundle, typeContext) + : GetTypeDisplayName(typeRef.Enum, IsTypeBeingUsedInTheContextWhereItIsDefined(typeRef.Enum, typeContext)); + case ValueType.Type: + return IsNested(typeRef, bundle) ? GetTypeClassQualifiedPathInContext(typeRef.Type, bundle, typeContext) + : GetTypeDisplayName(typeRef.Type, IsTypeBeingUsedInTheContextWhereItIsDefined(typeRef.Type, typeContext)); ; + case ValueType.Primitive: + return SchemaToCppTypes[typeRef.Primitive]; + default: + throw new ArgumentOutOfRangeException(); + } + } + + + + public static bool IsNested(TypeReference typeRef, Bundle bundle) + { + switch (typeRef.ValueTypeSelector) + { + case ValueType.Enum: + return !bundle.Enums[typeRef.Enum].OuterType.Equals(""); + case ValueType.Type: + return !bundle.Types[typeRef.Type].OuterType.Equals(""); + case ValueType.Primitive: + return false; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public static string GetOutermostType(string typeRef, Bundle bundle) + { + var outerTypeIterator = typeRef; + var outermostType = typeRef; + while (!outerTypeIterator.Equals("")) + { + outermostType = outerTypeIterator; + // Components can't be nested, so if we're a component, we know we're the outermost type + if (bundle.Components.ContainsKey(outerTypeIterator)) + { + return outermostType; + } + outerTypeIterator = bundle.Enums.ContainsKey(outermostType) ? bundle.Enums[outermostType].OuterType : bundle.Types[outermostType].OuterType; + } + return outermostType; + } + + /** If a type is being used within the same file namespace in which it is defined, the compiler complains. + * Instead, we just use the exact type name, ignoring the namespace. + */ + public static string GetTypeDisplayName(string qualifiedName, bool isTypeDefinedInCurrentContext = false) + { + return isTypeDefinedInCurrentContext ? GetNameFromQualifiedName(qualifiedName) : "::" + Text.ReplacesDotsWithDoubleColons(qualifiedName); + } + + public static string GetNameFromQualifiedName(string qualifiedName) + { + return qualifiedName.Substring(qualifiedName.LastIndexOf('.') + 1); + } + + public static string TypeToHeaderFilename(string qualifiedName) + { + return TypeToFilename(qualifiedName, ".h"); + } + + public static string TypeToSourceFilename(string qualifiedName) + { + return TypeToFilename(qualifiedName, ".cpp"); + } + + // For each const get accessor for each member field in a type, if the type is a primitive we return by value, otherwise by const ref + // e.g. double get_my_double_memeber(); + // const ::improbable::List<...>& get_my_list_memeber(); + public static string GetConstAccessorTypeModification(FieldDefinition field, Bundle bundle, TypeDescription typeContext) + { + var qualifiedFieldType = GetFieldTypeAsCpp(field, bundle, typeContext); + if (field.TypeSelector == FieldType.Singular && field.SingularType.Type.ValueTypeSelector == ValueType.Primitive && + field.SingularType.Type.Primitive != PrimitiveType.Bytes && field.SingularType.Type.Primitive != PrimitiveType.String) + { + return qualifiedFieldType; + } + return $"const {qualifiedFieldType}&"; + } + + public static bool IsTypeBeingUsedInTheContextWhereItIsDefined(string qualifiedType, TypeDescription typeContext) + { + return typeContext.NestedTypes.Select(t => t.QualifiedName).Concat(typeContext.NestedEnums.Select(e => e.QualifiedName)).Contains(qualifiedType); + } + + + /** + * When generating header files for schema types with nested types, we declare the nested types as sibling types. + * To avoid naming conflicts we prefix the sibling type names with the top-level type: + * e.g. improbable.ComponentInterest.Query becomes improbable::ComponentInterest_Query + */ + public static string GetTypeClassQualifiedPathInContext(string qualifiedName, Bundle bundle, TypeDescription typeContext) + { + if (IsTypeBeingUsedInTheContextWhereItIsDefined(qualifiedName, typeContext)) + { + return Text.ReplacesDotsWithDoubleColons(qualifiedName); + } + return GetTypeClassQualifiedPath(qualifiedName, bundle); + } + + public static string GetTypeClassQualifiedPath(string qualifiedName, Bundle bundle) + { + var outermostType = GetOutermostType(qualifiedName, bundle); + // Exit early if the type defined is top-level + if (outermostType.Equals("")) + { + return Text.ReplacesDotsWithDoubleColons(qualifiedName); + } + var typeName = string.Join("_", qualifiedName.Split('.').Skip(outermostType.Count(c => c == '.'))); + return $"{Text.ReplacesDotsWithDoubleColons(outermostType.Substring(0, outermostType.LastIndexOf(".")))}::{typeName}"; + } + + /** The following schema would generate class names for definitions like: + class NestedTypeSameName; + class NestedTypeSameName_Other; + + type NestedTypeSameName { + type Other { + ... + } + } + */ + public static string GetTypeClassName(string qualifiedName, Bundle bundle) + { + var qualifiedPath = GetTypeClassQualifiedPath(qualifiedName, bundle); + return qualifiedPath.Substring(qualifiedPath.LastIndexOf("::") + 2); + } + + public static List GetRecursivelyNestedTypes(TypeDescription type) + { + return type.NestedTypes.Concat(type.NestedTypes.SelectMany(nestedType => GetRecursivelyNestedTypes(nestedType))) + .GroupBy(t => t.QualifiedName) + .Select(t => t.First()) + .ToList(); + } + + public static List GetRecursivelyNestedEnums(TypeDescription type) + { + return type.NestedEnums.Concat(type.NestedTypes.SelectMany(nestedType => GetRecursivelyNestedEnums(nestedType))) + .GroupBy(e => e.QualifiedName) + .Select(e => e.First()) + .ToList(); + } + + // Sorts all nested type definitions in the given schema file according to singular dependencies. Returns + // an empty vector if there is a cycle (although this should be impossible if the file has passed + // validation). + public static List SortTopLevelTypesTopologically(TypeDescription type, List types, Bundle bundle) + { + var topLevelTypes = new List(GetRecursivelyNestedTypes(type)) { type }; + Dictionary allTopLevelTypesToVisitedMap = topLevelTypes.ToDictionary(t => t.QualifiedName, t => false); + + bool graphIsCyclic = false; + + var sortedTypes = new List(); + while (allTopLevelTypesToVisitedMap.Count() > 0) + { + Visit(allTopLevelTypesToVisitedMap.First().Key, topLevelTypes, ref allTopLevelTypesToVisitedMap, ref graphIsCyclic, ref sortedTypes, types, bundle); + if (graphIsCyclic) + { + throw new Exception($"Found cyclic dependency in nested types of {type.QualifiedName}"); + } + } + return sortedTypes; + } + + private static string GetNamespaceFromQualifiedName(string qualifiedName, Bundle bundle) + { + var outermostTypeWrapper = GetOutermostType(qualifiedName, bundle); + return outermostTypeWrapper.Substring(0, outermostTypeWrapper.LastIndexOf(".")).Replace(".", "::"); + } + + private static string TypeToFilename(string qualifiedName, string extension) + { + var path = qualifiedName.Split('.'); + return $"{string.Join("/", path)}{extension}"; + } + + + + // For a field definition inside a type, get all the necessary includes for the type (from collection inner types, etc) + private static void AddRequiredTypeQualifiedNames(FieldDefinition field, ref List requiredTypeQualifiedNames) + { + switch (field.TypeSelector) + { + case FieldType.Singular: + AddRequiredTypeQualifiedNames(field.SingularType.Type, ref requiredTypeQualifiedNames); + break; + case FieldType.Option: + AddRequiredTypeQualifiedNames(field.OptionType.InnerType, ref requiredTypeQualifiedNames); + break; + case FieldType.List: + AddRequiredTypeQualifiedNames(field.ListType.InnerType, ref requiredTypeQualifiedNames); + break; + case FieldType.Map: + AddRequiredTypeQualifiedNames(field.MapType.KeyType, ref requiredTypeQualifiedNames); + AddRequiredTypeQualifiedNames(field.MapType.ValueType, ref requiredTypeQualifiedNames); + break; + } + } + + private static void AddRequiredTypeQualifiedNames(TypeReference valueType, ref List requiredTypeQualifiedNames) + { + switch (valueType.ValueTypeSelector) + { + case ValueType.Enum: + requiredTypeQualifiedNames.Add(valueType.Enum); + break; + case ValueType.Type: + requiredTypeQualifiedNames.Add(valueType.Type); + break; + default: + return; + } + } + + private static void Visit(string typeName, List topLevelTypes, ref Dictionary nestedTypeNameToVisitedMap, ref bool graphIsCyclic, ref List sortedTypes, List types, Bundle bundle) + { + var type = topLevelTypes.FirstOrDefault(t => t.QualifiedName == typeName); + + // Exit early if trying to visit a nested type of a previously visited component (also is fine, this is sorting in action) + if (type.Equals(default(TypeDescription))) + { + return; + } + + // If visited this type already without removing it, this means we have a cyclic dependency + if (nestedTypeNameToVisitedMap[typeName]) + { + graphIsCyclic = true; + return; + } + + nestedTypeNameToVisitedMap[typeName] = true; + + foreach (var field in type.Fields) + { + // We're only doing this topological sort to cater for compiler errors from declaring incomplete type members + // This only occurs for singular type or map keys + if (field.TypeSelector == FieldType.Singular && field.SingularType.Type.ValueTypeSelector == ValueType.Type) + { + Visit(topLevelTypes.Find(t => t.QualifiedName == field.SingularType.Type.Type).QualifiedName, topLevelTypes, ref nestedTypeNameToVisitedMap, ref graphIsCyclic, ref sortedTypes, types, bundle); + } + else if (field.TypeSelector == FieldType.Map && field.MapType.KeyType.ValueTypeSelector == ValueType.Type) + { + Visit(topLevelTypes.Find(t => t.QualifiedName == field.MapType.KeyType.Type).QualifiedName, topLevelTypes, ref nestedTypeNameToVisitedMap, ref graphIsCyclic, ref sortedTypes, types, bundle); + } + } + sortedTypes.Add(type); + nestedTypeNameToVisitedMap.Remove(typeName); + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/UnrealGenerator.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/UnrealGenerator.cs new file mode 100644 index 0000000000..4d18f63fbd --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/CodeGenerator/Unreal/UnrealGenerator.cs @@ -0,0 +1,45 @@ +using Improbable.Codegen.Base; +using Improbable.CodeGen.Base; +using System.Collections.Generic; +using System.Linq; + +namespace Improbable.CodeGen.Unreal +{ + public class UnrealGenerator : ICodeGenerator + { + public static string GeneratorTitle = "Unreal External Schema Codegen"; + + public List GenerateFiles(Bundle bundle) + { + var generatedFiles = new List(); + var allGeneratedTypeContent = new Dictionary(); + var types = bundle.Types.Select(kv => new TypeDescription(kv.Key, bundle)) + .Union(bundle.Components.Select(kv => new TypeDescription(kv.Key, bundle))) + .ToList(); + var topLevelTypes = types.Where(type => type.OuterType.Equals("")); + var topLevelEnums = bundle.Enums.Where(_enum => _enum.Value.OuterType.Equals("")); + + // Generate utility files + generatedFiles.AddRange(HelperFunctions.GetHelperFunctionFiles()); + generatedFiles.AddRange(MapEquals.GenerateMapEquals()); + + // Generate external schema interface + generatedFiles.AddRange(InterfaceGenerator.GenerateInterface(types.Where(type => type.ComponentId.HasValue).ToList(), bundle)); + + // Generated all type file content + foreach (var toplevelType in topLevelTypes) + { + generatedFiles.Add(new GeneratedFile(Types.TypeToHeaderFilename(toplevelType.QualifiedName), HeaderGenerator.GenerateHeader(toplevelType, types, allGeneratedTypeContent, bundle))); + generatedFiles.Add(new GeneratedFile(Types.TypeToSourceFilename(toplevelType.QualifiedName), SourceGenerator.GenerateSource(toplevelType, types, allGeneratedTypeContent, bundle))); + } + + // Add enum files to generated files, ignoring nested enum which are defined in parent files + foreach (KeyValuePair topLevelEnum in topLevelEnums) + { + generatedFiles.Add(new GeneratedFile(Types.TypeToHeaderFilename(topLevelEnum.Key), EnumGenerator.GenerateTopLevelEnum(topLevelEnum.Value, bundle))); + } + + return generatedFiles; + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/Common.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/Common.cs index f2c4c96e7c..18f1fb7b7b 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/Common.cs +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/Common.cs @@ -1,3 +1,5 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + using System; using System.Collections.Generic; using System.Diagnostics; diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/Common.csproj b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/Common.csproj index b75bd03daa..f7b87a1a4d 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/Common.csproj +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/Common.csproj @@ -41,6 +41,7 @@ + diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/LinuxScripts.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/LinuxScripts.cs new file mode 100644 index 0000000000..d6380a38bf --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Common/LinuxScripts.cs @@ -0,0 +1,93 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +using System.IO; +using System.Text; + +namespace Improbable.Unreal.Build.Common +{ + public static class LinuxScripts + { + private const string UnrealWorkerShellScript = +@"#!/bin/bash +NEW_USER=unrealworker +WORKER_ID=$1 +LOG_FILE=$2 +shift 2 + +# 2>/dev/null silences errors by redirecting stderr to the null device. This is done to prevent errors when a machine attempts to add the same user more than once. +mkdir -p /improbable/logs/UnrealWorker/ +useradd $NEW_USER -m -d /improbable/logs/UnrealWorker/Logs 2>/dev/null +chown -R $NEW_USER:$NEW_USER $(pwd) 2>/dev/null +chmod -R o+rw /improbable/logs 2>/dev/null + +# Create log file in case it doesn't exist and redirect stdout and stderr to the file. +touch ""${{LOG_FILE}}"" +exec 1>>""${{LOG_FILE}}"" +exec 2>&1 + +SCRIPT=""$(pwd)/{0}Server.sh"" + +if [ ! -f $SCRIPT ]; then + echo ""Expected to run ${{SCRIPT}} but file not found!"" + exit 1 +fi + +chmod +x $SCRIPT +echo ""Running ${{SCRIPT}} to start worker..."" +gosu $NEW_USER ""${{SCRIPT}}"" ""$@"""; + + // This is for internal use only. We do not support Linux clients. + public const string SimulatedPlayerWorkerShellScript = +@"#!/bin/bash +NEW_USER=unrealworker +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"" +gosu $NEW_USER ""${{SCRIPT}}"" ""$@"" >> ""/improbable/logs/${{WORKER_ID}}.log"" 2>&1"; + + public const string SimulatedPlayerCoordinatorShellScript = +@"#!/bin/sh +sleep 5 + +chmod +x WorkerCoordinator.exe +chmod +x StartSimulatedClient.sh +chmod +x {0}.sh + +mono WorkerCoordinator.exe $@ 2> /improbable/logs/CoordinatorErrors.log"; + + // Returns a version of UnrealWorkerShellScript with baseGameName templated into the right places. + // baseGameName should be the base name of your Unreal game. + public static string GetUnrealWorkerShellScript(string baseGameName) + { + return string.Format(UnrealWorkerShellScript, baseGameName); + } + + // Returns a version of SimulatedPlayerWorkerShellScript with baseGameName templated into the right places. + // baseGameName should be the base name of your Unreal game. + public static string GetSimulatedPlayerWorkerShellScript(string baseGameName) + { + return string.Format(SimulatedPlayerWorkerShellScript, baseGameName); + } + + // Returns a version of SimulatedPlayerCoordinatorShellScript with baseGameName templated into the right places. + // baseGameName should be the base name of your Unreal game. + public static string GetSimulatedPlayerCoordinatorShellScript(string baseGameName) + { + return string.Format(SimulatedPlayerCoordinatorShellScript, baseGameName); + } + + // Writes out fileContents to fileName, ensuring that the resulting file has Linux line endings. + public static void WriteWithLinuxLineEndings(string fileContents, string fileName) + { + File.WriteAllText(fileName, fileContents.Replace("\r\n", "\n"), new UTF8Encoding(false)); + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs new file mode 100644 index 0000000000..d85ef32607 --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.cs @@ -0,0 +1,519 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Cryptography; +using Google.LongRunning; +using Google.Protobuf.WellKnownTypes; +using Improbable.SpatialOS.Deployment.V1Alpha1; +using Improbable.SpatialOS.PlayerAuth.V2Alpha1; +using Improbable.SpatialOS.Snapshot.V1Alpha1; +using Newtonsoft.Json.Linq; + +namespace Improbable +{ + internal class DeploymentLauncher + { + private const string SIM_PLAYER_DEPLOYMENT_TAG = "simulated_players"; + private const string DEPLOYMENT_LAUNCHED_BY_LAUNCHER_TAG = "unreal_deployment_launcher"; + + private const string CoordinatorWorkerName = "SimulatedPlayerCoordinator"; + + private static string UploadSnapshot(SnapshotServiceClient client, string snapshotPath, string projectName, + string deploymentName) + { + Console.WriteLine($"Uploading {snapshotPath} to project {projectName}"); + + // Read snapshot. + var bytes = File.ReadAllBytes(snapshotPath); + + if (bytes.Length == 0) + { + Console.Error.WriteLine($"Unable to load {snapshotPath}. Does the file exist?"); + return string.Empty; + } + + // Create HTTP endpoint to upload to. + var snapshotToUpload = new Snapshot + { + ProjectName = projectName, + DeploymentName = deploymentName + }; + + using (var md5 = MD5.Create()) + { + snapshotToUpload.Checksum = Convert.ToBase64String(md5.ComputeHash(bytes)); + snapshotToUpload.Size = bytes.Length; + } + + var uploadSnapshotResponse = + client.UploadSnapshot(new UploadSnapshotRequest { Snapshot = snapshotToUpload }); + snapshotToUpload = uploadSnapshotResponse.Snapshot; + + // Upload content. + var httpRequest = WebRequest.Create(uploadSnapshotResponse.UploadUrl) as HttpWebRequest; + httpRequest.Method = "PUT"; + httpRequest.ContentLength = snapshotToUpload.Size; + httpRequest.Headers.Set("Content-MD5", snapshotToUpload.Checksum); + + using (var dataStream = httpRequest.GetRequestStream()) + { + dataStream.Write(bytes, 0, bytes.Length); + } + + // Block until we have a response. + httpRequest.GetResponse(); + + // Confirm that the snapshot was uploaded successfully. + var confirmUploadResponse = client.ConfirmUpload(new ConfirmUploadRequest + { + DeploymentName = snapshotToUpload.DeploymentName, + Id = snapshotToUpload.Id, + ProjectName = snapshotToUpload.ProjectName + }); + + return confirmUploadResponse.Snapshot.Id; + } + + private static int CreateDeployment(string[] args) + { + bool launchSimPlayerDeployment = args.Length == 11; + + var projectName = args[1]; + var assemblyName = args[2]; + var mainDeploymentName = args[3]; + var mainDeploymentJsonPath = args[4]; + var mainDeploymentSnapshotPath = args[5]; + var mainDeploymentRegion = args[6]; + + var simDeploymentName = string.Empty; + var simDeploymentJson = string.Empty; + var simDeploymentRegion = string.Empty; + var simNumPlayers = 0; + + if (launchSimPlayerDeployment) + { + simDeploymentName = args[7]; + simDeploymentJson = args[8]; + simDeploymentRegion = args[9]; + + if (!Int32.TryParse(args[10], out simNumPlayers)) + { + Console.WriteLine("Cannot parse the number of simulated players to connect."); + return 1; + } + } + + try + { + var deploymentServiceClient = DeploymentServiceClient.Create(); + + if (DeploymentExists(deploymentServiceClient, projectName, mainDeploymentName)) + { + StopDeploymentByName(deploymentServiceClient, projectName, mainDeploymentName); + } + + var createMainDeploymentOp = CreateMainDeploymentAsync(deploymentServiceClient, launchSimPlayerDeployment, projectName, assemblyName, mainDeploymentName, mainDeploymentJsonPath, mainDeploymentSnapshotPath, mainDeploymentRegion); + + 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)) + { + StopDeploymentByName(deploymentServiceClient, projectName, simDeploymentName); + } + + var createSimDeploymentOp = CreateSimPlayerDeploymentAsync(deploymentServiceClient, projectName, assemblyName, 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) + { + 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) + { + Console.WriteLine("Failed to create the simulated player deployment"); + return 1; + } + + Console.WriteLine("Successfully created the simulated player deployment"); + + // Update coordinator worker flag for simulated player deployment to notify target deployment is ready. + simPlayerDeployment.WorkerFlags.Add(new WorkerFlag + { + Key = "target_deployment_ready", + Value = "true", + WorkerType = CoordinatorWorkerName + }); + deploymentServiceClient.UpdateDeployment(new UpdateDeploymentRequest { Deployment = simPlayerDeployment }); + + Console.WriteLine("Done! Simulated players will start to connect to your deployment"); + } + catch (Grpc.Core.RpcException e) + { + if (e.Status.StatusCode == Grpc.Core.StatusCode.NotFound) + { + Console.WriteLine( + $"Unable to launch the deployment(s). This is likely because the project '{projectName}' or assembly '{assemblyName}' doesn't exist."); + } + else + { + throw; + } + } + + return 0; + } + + private static bool DeploymentExists(DeploymentServiceClient deploymentServiceClient, string projectName, + string deploymentName) + { + var activeDeployments = ListLaunchedActiveDeployments(deploymentServiceClient, projectName); + + return activeDeployments.FirstOrDefault(d => d.Name == deploymentName) != null; + } + + + private static void StopDeploymentByName(DeploymentServiceClient deploymentServiceClient, string projectName, + string deploymentName) + { + var activeDeployments = ListLaunchedActiveDeployments(deploymentServiceClient, projectName); + + var deployment = activeDeployments.FirstOrDefault(d => d.Name == deploymentName); + + if (deployment == null) + { + Console.WriteLine($"Unable to stop the deployment {deployment.Name} because it can't be found or isn't running."); + return; + } + + Console.WriteLine($"Stopping active deployment by name: {deployment.Name}"); + + deploymentServiceClient.StopDeployment(new StopDeploymentRequest + { + Id = deployment.Id, + ProjectName = projectName + }); + } + + private static Operation CreateMainDeploymentAsync(DeploymentServiceClient deploymentServiceClient, + bool launchSimPlayerDeployment, string projectName, string assemblyName, string mainDeploymentName, string mainDeploymentJsonPath, string mainDeploymentSnapshotPath, string regionCode) + { + var snapshotServiceClient = SnapshotServiceClient.Create(); + + // Upload snapshots. + var mainSnapshotId = UploadSnapshot(snapshotServiceClient, mainDeploymentSnapshotPath, projectName, + mainDeploymentName); + + if (mainSnapshotId.Length == 0) + { + throw new Exception("Error while uploading snapshot."); + } + + // Create main deployment. + var mainDeploymentConfig = new Deployment + { + AssemblyId = assemblyName, + LaunchConfig = new LaunchConfig + { + ConfigJson = File.ReadAllText(mainDeploymentJsonPath) + }, + Name = mainDeploymentName, + ProjectName = projectName, + StartingSnapshotId = mainSnapshotId, + RegionCode = regionCode + }; + + mainDeploymentConfig.Tag.Add(DEPLOYMENT_LAUNCHED_BY_LAUNCHER_TAG); + + if (launchSimPlayerDeployment) + { + // This tag needs to be added to allow simulated players to connect using login + // tokens generated with anonymous auth. + mainDeploymentConfig.Tag.Add("dev_login"); + } + + Console.WriteLine( + $"Creating the main deployment {mainDeploymentName} in project {projectName} with snapshot ID {mainSnapshotId}. Link: https://console.improbable.io/projects/{projectName}/deployments/{mainDeploymentName}/overview"); + + var mainDeploymentCreateOp = deploymentServiceClient.CreateDeployment(new CreateDeploymentRequest + { + Deployment = mainDeploymentConfig + }); + + return mainDeploymentCreateOp; + } + + private static Operation CreateSimPlayerDeploymentAsync(DeploymentServiceClient deploymentServiceClient, + string projectName, string assemblyName, string mainDeploymentName, string simDeploymentName, string simDeploymentJsonPath, string regionCode, int simNumPlayers) + { + var playerAuthServiceClient = PlayerAuthServiceClient.Create(); + + // Create development authentication token used by the simulated players. + var dat = playerAuthServiceClient.CreateDevelopmentAuthenticationToken( + new CreateDevelopmentAuthenticationTokenRequest + { + Description = "DAT for simulated player deployment.", + Lifetime = Duration.FromTimeSpan(new TimeSpan(7, 0, 0, 0)), + ProjectName = projectName + }); + + // Add worker flags to sim deployment JSON. + var devAuthTokenFlag = new JObject(); + devAuthTokenFlag.Add("name", "simulated_players_dev_auth_token"); + devAuthTokenFlag.Add("value", dat.TokenSecret); + + var targetDeploymentFlag = new JObject(); + targetDeploymentFlag.Add("name", "simulated_players_target_deployment"); + targetDeploymentFlag.Add("value", mainDeploymentName); + + var numSimulatedPlayersFlag = new JObject(); + numSimulatedPlayersFlag.Add("name", "total_num_simulated_players"); + numSimulatedPlayersFlag.Add("value", $"{simNumPlayers}"); + + var simWorkerConfigJson = File.ReadAllText(simDeploymentJsonPath); + dynamic simWorkerConfig = JObject.Parse(simWorkerConfigJson); + + for (var i = 0; i < simWorkerConfig.workers.Count; ++i) + { + if (simWorkerConfig.workers[i].worker_type == CoordinatorWorkerName) + { + simWorkerConfig.workers[i].flags.Add(devAuthTokenFlag); + simWorkerConfig.workers[i].flags.Add(targetDeploymentFlag); + simWorkerConfig.workers[i].flags.Add(numSimulatedPlayersFlag); + } + } + + // Specify the number of managed coordinator workers to start by editing + // the load balancing options in the launch config. It creates a rectangular + // launch config of N cols X 1 row, N being the number of coordinators + // to create. + // This assumes the launch config contains a rectangular load balancing + // layer configuration already for the coordinator worker. + var lbLayerConfigurations = simWorkerConfig.load_balancing.layer_configurations; + for (var i = 0; i < lbLayerConfigurations.Count; ++i) + { + if (lbLayerConfigurations[i].layer == CoordinatorWorkerName) + { + var rectangleGrid = lbLayerConfigurations[i].rectangle_grid; + rectangleGrid.cols = simNumPlayers; + rectangleGrid.rows = 1; + } + } + + simWorkerConfigJson = simWorkerConfig.ToString(); + + // Create simulated player deployment. + var simDeploymentConfig = new Deployment + { + AssemblyId = assemblyName, + LaunchConfig = new LaunchConfig + { + ConfigJson = simWorkerConfigJson + }, + Name = simDeploymentName, + ProjectName = projectName, + RegionCode = regionCode + // No snapshot included for the simulated player deployment + }; + + simDeploymentConfig.Tag.Add(DEPLOYMENT_LAUNCHED_BY_LAUNCHER_TAG); + simDeploymentConfig.Tag.Add(SIM_PLAYER_DEPLOYMENT_TAG); + + Console.WriteLine( + $"Creating the simulated player deployment {simDeploymentName} in project {projectName} with {simNumPlayers} simulated players. Link: https://console.improbable.io/projects/{projectName}/deployments/{simDeploymentName}/overview"); + + var simDeploymentCreateOp = deploymentServiceClient.CreateDeployment(new CreateDeploymentRequest + { + Deployment = simDeploymentConfig + }); + + return simDeploymentCreateOp; + } + + + private static int StopDeployments(string[] args) + { + var projectName = args[1]; + + var deploymentServiceClient = DeploymentServiceClient.Create(); + + if (args.Length == 3) + { + // Stop only the specified deployment. + var deploymentId = args[2]; + StopDeploymentById(deploymentServiceClient, projectName, deploymentId); + + return 0; + } + + // Stop all active deployments launched by this launcher. + var activeDeployments = ListLaunchedActiveDeployments(deploymentServiceClient, projectName); + + foreach (var deployment in activeDeployments) + { + var deploymentId = deployment.Id; + StopDeploymentById(deploymentServiceClient, projectName, deploymentId); + } + + return 0; + } + + private static void StopDeploymentById(DeploymentServiceClient client, string projectName, string deploymentId) + { + try + { + Console.WriteLine($"Stopping deployment with id {deploymentId}"); + client.StopDeployment(new StopDeploymentRequest + { + Id = deploymentId, + ProjectName = projectName + }); + } + catch (Grpc.Core.RpcException e) + { + if (e.Status.StatusCode == Grpc.Core.StatusCode.NotFound) + { + Console.WriteLine(""); + } + else + { + throw; + } + } + } + + private static int ListDeployments(string[] args) + { + var projectName = args[1]; + + var deploymentServiceClient = DeploymentServiceClient.Create(); + var activeDeployments = ListLaunchedActiveDeployments(deploymentServiceClient, projectName); + + foreach (var deployment in activeDeployments) + { + var status = deployment.Status; + var overviewPageUrl = $"https://console.improbable.io/projects/{projectName}/deployments/{deployment.Name}/overview/{deployment.Id}"; + + if (deployment.Tag.Contains(SIM_PLAYER_DEPLOYMENT_TAG)) + { + Console.WriteLine($" {deployment.Id} {deployment.Name} {deployment.RegionCode} {overviewPageUrl} {status}"); + } + else + { + Console.WriteLine($" {deployment.Id} {deployment.Name} {deployment.RegionCode} {overviewPageUrl} {status}"); + } + } + + return 0; + } + + private static IEnumerable ListLaunchedActiveDeployments(DeploymentServiceClient client, string projectName) + { + var listDeploymentsResult = client.ListDeployments(new ListDeploymentsRequest + { + View = ViewType.Basic, + ProjectName = projectName, + DeploymentStoppedStatusFilter = ListDeploymentsRequest.Types.DeploymentStoppedStatusFilter.NotStoppedDeployments, + PageSize = 50 + }); + + return listDeploymentsResult.Where(deployment => + { + var status = deployment.Status; + if (status != Deployment.Types.Status.Starting && status != Deployment.Types.Status.Running) + { + // Deployment not active - skip + return false; + } + + if (!deployment.Tag.Contains(DEPLOYMENT_LAUNCHED_BY_LAUNCHER_TAG)) + { + // Deployment not launched by this launcher - skip + return false; + } + + return true; + }); + } + + private static void ShowUsage() + { + Console.WriteLine("Usage:"); + Console.WriteLine("DeploymentLauncher create [ ]"); + Console.WriteLine($" Starts a cloud deployment, with optionally a simulated player deployment. The deployments can be started in different regions ('EU', 'US' and 'AP')."); + 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(" Lists all active deployments within the specified project that are started by the deployment launcher."); + } + + private static int Main(string[] args) + { + if (args.Length == 0 || + args[0] == "create" && (args.Length != 11 && args.Length != 7) || + args[0] == "stop" && (args.Length != 2 && args.Length != 3) || + args[0] == "list" && args.Length != 2) + { + ShowUsage(); + return 1; + } + + try + { + if (args[0] == "create") + { + return CreateDeployment(args); + } + + if (args[0] == "stop") + { + return StopDeployments(args); + } + + if (args[0] == "list") + { + return ListDeployments(args); + } + + ShowUsage(); + } + catch (Grpc.Core.RpcException e) + { + if (e.Status.StatusCode == Grpc.Core.StatusCode.Unauthenticated) + { + Console.WriteLine("Error: unauthenticated. Please run `spatial auth login`"); + } + else + { + Console.Error.WriteLine($"Encountered an unknown gRPC error. Exception = {e.ToString()}"); + } + } + + // Unknown command or error occurred. + return 1; + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.csproj b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.csproj new file mode 100644 index 0000000000..4ba3f2bd08 --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/DeploymentLauncher/DeploymentLauncher.csproj @@ -0,0 +1,77 @@ + + + + + Debug + AnyCPU + {B879D33B-AA4B-4A13-BCD4-957178C060E7} + Exe + Properties + DeploymentLauncher + DeploymentLauncher + v4.5.1 + 512 + true + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + 13.7.1 + + + 12.0.1 + + + + + + + + {d2cc2525-ec22-4a24-bde8-d9a6bff55bad} + Common + False + + + + + + + + mkdir "$(SolutionDir)..\..\..\Binaries\ThirdParty\Improbable\Programs\DeploymentLauncher\" +xcopy "$(SolutionDir)\$(ProjectName)\$(OutDir)." "$(SolutionDir)..\..\..\Binaries\ThirdParty\Improbable\Programs\DeploymentLauncher\" /q /y + + + \ No newline at end of file diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Improbable.Unreal.Scripts.sln b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Improbable.Unreal.Scripts.sln index 614c9b37e3..8ba0745a6f 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Improbable.Unreal.Scripts.sln +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Improbable.Unreal.Scripts.sln @@ -9,26 +9,75 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Linter", "Linter\Linter.csproj", "{D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeploymentLauncher", "DeploymentLauncher\DeploymentLauncher.csproj", "{B879D33B-AA4B-4A13-BCD4-957178C060E7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkerCoordinator", "WorkerCoordinator\WorkerCoordinator.csproj", "{C41625B0-CDB7-4480-B2E4-AEB27AF3B198}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeGenerator", "CodeGenerator\CodeGenerator.csproj", "{B0165BED-C4AF-406C-A652-3DBB3D2E0C52}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WriteLinuxScript", "WriteLinuxScript\WriteLinuxScript.csproj", "{884C3696-4722-47E5-9100-ED20FE33E05D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {914F8489-3031-4A27-B403-8AC318AFD71B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {914F8489-3031-4A27-B403-8AC318AFD71B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {914F8489-3031-4A27-B403-8AC318AFD71B}.Debug|x86.ActiveCfg = Debug|Any CPU + {914F8489-3031-4A27-B403-8AC318AFD71B}.Debug|x86.Build.0 = Debug|Any CPU {914F8489-3031-4A27-B403-8AC318AFD71B}.Release|Any CPU.ActiveCfg = Release|Any CPU {914F8489-3031-4A27-B403-8AC318AFD71B}.Release|Any CPU.Build.0 = Release|Any CPU + {914F8489-3031-4A27-B403-8AC318AFD71B}.Release|x86.ActiveCfg = Release|Any CPU + {914F8489-3031-4A27-B403-8AC318AFD71B}.Release|x86.Build.0 = Release|Any CPU {D2CC2525-EC22-4A24-BDE8-D9A6BFF55BAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D2CC2525-EC22-4A24-BDE8-D9A6BFF55BAD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D2CC2525-EC22-4A24-BDE8-D9A6BFF55BAD}.Debug|x86.ActiveCfg = Debug|Any CPU + {D2CC2525-EC22-4A24-BDE8-D9A6BFF55BAD}.Debug|x86.Build.0 = Debug|Any CPU {D2CC2525-EC22-4A24-BDE8-D9A6BFF55BAD}.Release|Any CPU.ActiveCfg = Release|Any CPU {D2CC2525-EC22-4A24-BDE8-D9A6BFF55BAD}.Release|Any CPU.Build.0 = Release|Any CPU + {D2CC2525-EC22-4A24-BDE8-D9A6BFF55BAD}.Release|x86.ActiveCfg = Release|Any CPU + {D2CC2525-EC22-4A24-BDE8-D9A6BFF55BAD}.Release|x86.Build.0 = Release|Any CPU {D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Debug|x86.ActiveCfg = Debug|Any CPU + {D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Debug|x86.Build.0 = Debug|Any CPU + {B879D33B-AA4B-4A13-BCD4-957178C060E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B879D33B-AA4B-4A13-BCD4-957178C060E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B879D33B-AA4B-4A13-BCD4-957178C060E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B879D33B-AA4B-4A13-BCD4-957178C060E7}.Release|Any CPU.Build.0 = Release|Any CPU + {C41625B0-CDB7-4480-B2E4-AEB27AF3B198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C41625B0-CDB7-4480-B2E4-AEB27AF3B198}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C41625B0-CDB7-4480-B2E4-AEB27AF3B198}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C41625B0-CDB7-4480-B2E4-AEB27AF3B198}.Release|Any CPU.Build.0 = Release|Any CPU {D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Release|Any CPU.ActiveCfg = Release|Any CPU {D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Release|Any CPU.Build.0 = Release|Any CPU + {D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Release|x86.ActiveCfg = Release|Any CPU + {D1A3A29F-BEA9-492B-8F09-0FEA58DF6D36}.Release|x86.Build.0 = Release|Any CPU + {B0165BED-C4AF-406C-A652-3DBB3D2E0C52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0165BED-C4AF-406C-A652-3DBB3D2E0C52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0165BED-C4AF-406C-A652-3DBB3D2E0C52}.Debug|x86.ActiveCfg = Debug|Any CPU + {B0165BED-C4AF-406C-A652-3DBB3D2E0C52}.Debug|x86.Build.0 = Debug|Any CPU + {B0165BED-C4AF-406C-A652-3DBB3D2E0C52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0165BED-C4AF-406C-A652-3DBB3D2E0C52}.Release|Any CPU.Build.0 = Release|Any CPU + {B0165BED-C4AF-406C-A652-3DBB3D2E0C52}.Release|x86.ActiveCfg = Release|Any CPU + {B0165BED-C4AF-406C-A652-3DBB3D2E0C52}.Release|x86.Build.0 = Release|Any CPU + {884C3696-4722-47E5-9100-ED20FE33E05D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {884C3696-4722-47E5-9100-ED20FE33E05D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {884C3696-4722-47E5-9100-ED20FE33E05D}.Debug|x86.ActiveCfg = Debug|Any CPU + {884C3696-4722-47E5-9100-ED20FE33E05D}.Debug|x86.Build.0 = Debug|Any CPU + {884C3696-4722-47E5-9100-ED20FE33E05D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {884C3696-4722-47E5-9100-ED20FE33E05D}.Release|Any CPU.Build.0 = Release|Any CPU + {884C3696-4722-47E5-9100-ED20FE33E05D}.Release|x86.ActiveCfg = Release|Any CPU + {884C3696-4722-47E5-9100-ED20FE33E05D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AF1E8CEE-3239-4A07-915A-6C101E3AEABE} + EndGlobalSection EndGlobal diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Linter/Linter.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Linter/Linter.cs index da146e935a..f6d2c8d137 100644 --- a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Linter/Linter.cs +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/Linter/Linter.cs @@ -1,3 +1,5 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + using System; using System.Collections.Generic; using System.IO; diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/AbstractWorkerCoordinator.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/AbstractWorkerCoordinator.cs new file mode 100644 index 0000000000..d75761fa78 --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/AbstractWorkerCoordinator.cs @@ -0,0 +1,71 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Improbable.WorkerCoordinator +{ + /// + /// Defines the base class of a worker coordinator. + /// Keeps track of spawned processes for simulated players, + /// and provides functionality to wait for these processes to exit. + /// + internal abstract class AbstractWorkerCoordinator + { + protected Logger Logger; + private Stack ActiveProcesses = new Stack(); + + public AbstractWorkerCoordinator(Logger logger) + { + Logger = logger; + } + + /// + /// Contains the logic for running this coordinator, including starting a number of simulated players, + /// generating required tokens, and waiting for the players to exit. + /// When this method returns the coordinator process will exit. + /// + public abstract void Run(); + + /// + /// Creates a new process that runs a simulated player, providing it with the specified arguments. + /// + /// File name of the simulated player executable to start. + /// Arguments to pass to the started process. + /// The started simulated player process, or null if something went wrong. + protected Process CreateSimulatedPlayerProcess(string fileName, string args) + { + try + { + var process = Process.Start(fileName, args); + ActiveProcesses.Push(process); + return process; + } + catch (Exception e) + { + Logger.WriteError($"Error starting simulated player: {e.Message}"); + return null; + } + } + + /// + /// Blocks until all active simulated player processes have exited. + /// Will only wait for processes started through CreateSimulatedPlayerProcess(). + /// + protected void WaitForPlayersToExit() + { + while (ActiveProcesses.Count > 0) + { + try + { + ActiveProcesses.Pop().WaitForExit(); + } + catch (Exception e) + { + Logger.WriteError($"Error while waiting for simulated player to exit: {e.Message}"); + } + } + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Authentication.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Authentication.cs new file mode 100644 index 0000000000..fc30c07567 --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Authentication.cs @@ -0,0 +1,69 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +using System; +using System.Collections.Generic; +using System.Linq; +using Improbable.Worker; +using Improbable.Worker.Alpha; + +namespace Improbable.WorkerCoordinator +{ + class Authentication + { + private const string LOCATOR_HOST_NAME = "locator.improbable.io"; + private const int LOCATOR_PORT = 444; + + public static string GetDevelopmentPlayerIdentityToken(string devAuthToken, string clientName) + { + var pitResponse = DevelopmentAuthentication.CreateDevelopmentPlayerIdentityTokenAsync("locator.improbable.io", 444, + new PlayerIdentityTokenRequest + { + DevelopmentAuthenticationToken = devAuthToken, + PlayerId = clientName, + DisplayName = clientName + }).Get(); + + if (pitResponse.Status.Code != ConnectionStatusCode.Success) + { + throw new Exception($"Failed to retrieve player identity token.\n" + + $"error code: {pitResponse.Status.Code}\n" + + $"error message: {pitResponse.Status.Detail}"); + } + + return pitResponse.PlayerIdentityToken; + } + + public static List GetDevelopmentLoginTokens(string workerType, string pit) + { + var loginTokensResponse = DevelopmentAuthentication.CreateDevelopmentLoginTokensAsync(LOCATOR_HOST_NAME, LOCATOR_PORT, + new LoginTokensRequest + { + PlayerIdentityToken = pit, + WorkerType = workerType, + UseInsecureConnection = false, + DurationSeconds = 300 + }).Get(); + + if (loginTokensResponse.Status.Code != ConnectionStatusCode.Success) + { + throw new Exception($"Failed to retrieve any login tokens.\n" + + $"error code: {loginTokensResponse.Status.Code}\n" + + $"error message: {loginTokensResponse.Status.Detail}"); + } + + return loginTokensResponse.LoginTokens; + } + + public static string SelectLoginToken(List loginTokens, string targetDeployment) + { + var selectedLoginToken = loginTokens.FirstOrDefault(token => token.DeploymentName == targetDeployment).LoginToken; + + if (selectedLoginToken == null) + { + throw new Exception("Failed to launch simulated player. Login token for target deployment was not found in response. Does that deployment have the `dev_auth` tag?"); + } + + return selectedLoginToken; + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/BuildTargets.targets b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/BuildTargets.targets new file mode 100644 index 0000000000..7d969d2073 --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/BuildTargets.targets @@ -0,0 +1,35 @@ + + + false + + + bin\x64\DebugWindows\ + package\x64\DebugWindows\ + windows + + + bin\x64\DebugMacOS\ + package\x64\DebugMacOS\ + macos + + + bin\x64\DebugLinux\ + package\x64\DebugLinux\ + linux + + + bin\x64\ReleaseWindows\ + package\x64\ReleaseWindows\ + windows + + + bin\x64\ReleaseMacOS\ + package\x64\ReleaseMacOS\ + macos + + + bin\x64\ReleaseLinux\ + package\x64\ReleaseLinux\ + linux + + diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/CoordinatorConnection.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/CoordinatorConnection.cs new file mode 100644 index 0000000000..150b5742ef --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/CoordinatorConnection.cs @@ -0,0 +1,86 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +using System; +using System.Threading; +using Improbable.Worker; + +namespace Improbable.WorkerCoordinator +{ + class CoordinatorConnection + { + private const string LoggerName = "CoordinatorConnection.cs"; + private const uint GetOpListTimeoutInMilliseconds = 100; + + public static Connection ConnectAndKeepAlive(Logger logger, string receptionistHost, ushort receptionistPort, string workerId, string coordinatorWorkerType) + { + var connectionParameters = new ConnectionParameters + { + WorkerType = coordinatorWorkerType, + Network = + { + ConnectionType = NetworkConnectionType.Tcp, + UseExternalIp = false + } + }; + + Connection connection; + using (var future = Connection.ConnectAsync(receptionistHost, receptionistPort, workerId, connectionParameters)) + { + connection = future.Get(); + } + + // Start sending logs to SpatialOS. + logger.EnableSpatialOSLogging(connection); + + KeepConnectionAlive(connection, logger); + + return connection; + } + + /// + /// Starts a new background thread that will keep the connection to SpatialOS alive. + /// We do not use the connection to the simulated player deployment, + /// but we must ensure it is kept open to prevent the coordinator worker + /// from being killed. + /// + private static void KeepConnectionAlive(Connection connection, Logger logger) + { + var thread = new Thread(() => + { + using (var dispatcher = new Dispatcher()) + { + var isConnected = true; + + dispatcher.OnDisconnect(op => + { + logger.WriteError("[disconnect] " + op.Reason, logToConnectionIfExists: false); + isConnected = false; + }); + + dispatcher.OnLogMessage(op => + { + connection.SendLogMessage(op.Level, LoggerName, op.Message); + if (op.Level == LogLevel.Fatal) + { + logger.WriteError("Fatal error: " + op.Message, logToConnectionIfExists: false); + Environment.Exit(1); + } + }); + + while (isConnected) + { + using (var opList = connection.GetOpList(GetOpListTimeoutInMilliseconds)) + { + dispatcher.Process(opList); + } + } + } + }); + + // Don't keep thread / connection alive when main process stops. + thread.IsBackground = true; + + thread.Start(); + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Logger.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Logger.cs new file mode 100644 index 0000000000..f6a8be2569 --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Logger.cs @@ -0,0 +1,65 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +using System.IO; +using Improbable.Worker; + +namespace Improbable.WorkerCoordinator +{ + public class Logger + { + private readonly string LogPath; + private readonly string LoggerName; + private Connection Connection = null; + + public Logger(string logPath, string loggerName) + { + LogPath = logPath; + LoggerName = loggerName; + } + + public void EnableSpatialOSLogging(Connection connection) + { + Connection = connection; + } + + public void ClearLog() + { + File.WriteAllText(LogPath, string.Empty); + } + + public void WriteLog(string logMessage, bool logToConnectionIfExists = true) + { + WriteLogToFile(logMessage); + if (logToConnectionIfExists && Connection != null) + { + Connection.SendLogMessage(LogLevel.Info, LoggerName, logMessage); + } + } + + public void WriteWarning(string logMessage, bool logToConnectionIfExists = true) + { + WriteLogToFile("Warning: " + logMessage); + if (logToConnectionIfExists && Connection != null) + { + Connection.SendLogMessage(LogLevel.Warn, LoggerName, logMessage); + } + } + + public void WriteError(string logMessage, bool logToConnectionIfExists = true) + { + WriteLogToFile("Error: " + logMessage); + if (logToConnectionIfExists && Connection != null) + { + Connection.SendLogMessage(LogLevel.Error, LoggerName, logMessage); + } + } + + private void WriteLogToFile(string logMessage) + { + using (StreamWriter file = new StreamWriter(LogPath, true)) + { + file.WriteLine(logMessage); + } + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs new file mode 100644 index 0000000000..8cb99bf959 --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/ManagedWorkerCoordinator.cs @@ -0,0 +1,235 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Improbable.Collections; +using Improbable.Worker; + +namespace Improbable.WorkerCoordinator +{ + /// + /// Worker coordinator that connects and runs simulated players. + /// The coordinator runs as a managed worker inside a hosting deployment (i.e. the simulated player deployment). + /// + internal class ManagedWorkerCoordinator : AbstractWorkerCoordinator + { + // Arguments for the coordinator. + private const string SimulatedPlayerSpawnCountArg = "simulated_player_spawn_count"; + private const string InitialStartDelayArg = "coordinator_start_delay_millis"; + + // Worker flags. + 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; + + // Argument placeholders for simulated players - these will be replaced by the coordinator by their actual values. + private const string SimulatedPlayerWorkerNamePlaceholderArg = ""; + private const string PlayerIdentityTokenPlaceholderArg = ""; + private const string LoginTokenPlaceholderArg = ""; + + private const string CoordinatorWorkerType = "SimulatedPlayerCoordinator"; + private const string SimulatedPlayerWorkerType = "UnrealClient"; + private const string SimulatedPlayerFilename = "StartSimulatedClient.sh"; + + private static Random Random; + + // Coordinator options. + private string ReceptionistHost; + private ushort ReceptionistPort; + private string CoordinatorWorkerId; + private int NumSimulatedPlayersToStart; + private int InitialStartDelayMillis; + private string[] SimulatedPlayerArgs; + + /// + /// The arguments to start the coordinator must begin with: + /// receptionist + /// {hostname} Receptionist hostname + /// {port} Receptionist port + /// {worker_id} Worker id of the coordinator + /// + /// Next, two optional arguments can be specified: + /// simulated_player_spawn_count={value} Number of simulated player clients to start per coordinator (defaults to 1) + /// coordinator_start_delay_millis={value} Minimum delay before the coordinator starts a simulated client, to prevent clients + /// from connecting too soon to the target deployment (defaults to 10000) + /// + /// All following arguments will be passed to the simulated player instance. + /// These arguments can contain the following placeholders, which will be replaced by the coordinator: + /// `` will be replaced with the worker id of the simulated player + /// `` if a development auth token is specified as a worker flag, this will be replaced by the generated development auth login token + /// `` if both a development auth token and a target deployment are specified through the worker flags, this will be replaced by the generated development auth player identity token + /// + /// WORKER FLAGS + /// Additionally, the following worker flags are expected by the coordinator: + /// simulated_players_dev_auth_token The development auth token used to generate login tokens and player identity tokens + /// simulated_players_target_deployment Name of the target deployment which the simulated players will connect to + /// target_num_simulated_players The total number of simulated players that will connect to the target deployment. This value is used to determine the delay in connecting simulated players. + /// + public static ManagedWorkerCoordinator FromArgs(Logger logger, string[] args) + { + // Keeps track of the number of arguments used for the coordinator. + // The first 4 arguments are for connecting to the Receptionist. + // 2 optional arguments can be provided after the Receptionist args to + // modify the default options of the coordinator. + var numArgsToSkip = 4; + + // Optional args with default values. + var numSimulatedPlayersToStart = 1; + var initialStartDelayMillis = 10000; + if (Util.HasIntegerArgument(args, SimulatedPlayerSpawnCountArg)) + { + numSimulatedPlayersToStart = Util.GetIntegerArgument(args, SimulatedPlayerSpawnCountArg); + numArgsToSkip++; + } + if (Util.HasIntegerArgument(args, InitialStartDelayArg)) + { + initialStartDelayMillis = Util.GetIntegerArgument(args, InitialStartDelayArg); + numArgsToSkip++; + } + + return new ManagedWorkerCoordinator(logger) + { + // Receptionist args. + ReceptionistHost = args[1], + ReceptionistPort = Convert.ToUInt16(args[2]), + CoordinatorWorkerId = args[3], + + // Coordinator options. + NumSimulatedPlayersToStart = numSimulatedPlayersToStart, + InitialStartDelayMillis = initialStartDelayMillis, + + // Remove arguments that are only for the coordinator. + SimulatedPlayerArgs = args.Skip(numArgsToSkip).ToArray() + }; + } + + private ManagedWorkerCoordinator(Logger logger) : base(logger) + { + Random = new Random(Guid.NewGuid().GetHashCode()); + } + + public override void Run() + { + var connection = CoordinatorConnection.ConnectAndKeepAlive(Logger, ReceptionistHost, ReceptionistPort, CoordinatorWorkerId, CoordinatorWorkerType); + + // Read worker flags. + Option devAuthTokenOpt = connection.GetWorkerFlag(DevAuthTokenWorkerFlag); + Option targetDeploymentOpt = connection.GetWorkerFlag(TargetDeploymentWorkerFlag); + int deploymentTotalNumSimulatedPlayers = int.Parse(GetWorkerFlagOrDefault(connection, DeploymentTotalNumSimulatedPlayersWorkerFlag, "100")); + + Logger.WriteLog("Waiting for target deployment to become ready."); + WaitForTargetDeploymentReady(connection); + + Logger.WriteLog($"Target deployment is ready. Starting {NumSimulatedPlayersToStart} simulated players."); + Thread.Sleep(InitialStartDelayMillis); + + var maxDelayMillis = deploymentTotalNumSimulatedPlayers * AverageDelayMillisBetweenConnections; + + // Distribute player connections uniformly by generating a random time to connect between now and maxDelayMillis, + // such that, on average, a player connects every AverageDelayMillisBetweenConnections milliseconds deployment-wide. + // There can be multiple coordinator workers per deployment, to ensure that the combined connections created by all + // coordinators are spread out uniformly, generate a start delay for each player independently of other players' start delays. + var startDelaysMillis = new int[NumSimulatedPlayersToStart]; + for (int i = 0; i < NumSimulatedPlayersToStart; i++) + { + startDelaysMillis[i] = Random.Next(maxDelayMillis); + } + + Array.Sort(startDelaysMillis); + for (int i = 0; i < NumSimulatedPlayersToStart; i++) + { + string clientName = "SimulatedPlayer" + Guid.NewGuid(); + var timeToSleep = startDelaysMillis[i]; + if (i > 0) + { + timeToSleep -= startDelaysMillis[i - 1]; + } + + Thread.Sleep(timeToSleep); + StartSimulatedPlayer(clientName, devAuthTokenOpt, targetDeploymentOpt); + } + + // Wait for all clients to exit. + WaitForPlayersToExit(); + } + + private void StartSimulatedPlayer(string simulatedPlayerName, Option devAuthTokenOpt, Option targetDeploymentOpt) + { + try + { + // Create player identity token and login token + string pit = ""; + string loginToken = ""; + if (devAuthTokenOpt.HasValue) + { + pit = Authentication.GetDevelopmentPlayerIdentityToken(devAuthTokenOpt.Value, simulatedPlayerName); + + if (targetDeploymentOpt.HasValue) + { + var loginTokens = Authentication.GetDevelopmentLoginTokens(SimulatedPlayerWorkerType, pit); + loginToken = Authentication.SelectLoginToken(loginTokens, targetDeploymentOpt.Value); + } + else + { + Logger.WriteLog($"Not generating a login token for player {simulatedPlayerName}, no target deployment provided through worker flag \"{TargetDeploymentWorkerFlag}\"."); + } + } + else + { + Logger.WriteLog($"Not generating a player identity token and login token for player {simulatedPlayerName}, no development auth token provided through worker flag \"{DevAuthTokenWorkerFlag}\"."); + } + + string[] simulatedPlayerArgs = Util.ReplacePlaceholderArgs(SimulatedPlayerArgs, new Dictionary() { + { SimulatedPlayerWorkerNamePlaceholderArg, simulatedPlayerName }, + { PlayerIdentityTokenPlaceholderArg, pit }, + { LoginTokenPlaceholderArg, loginToken } + }); + + // Prepend the simulated player id as an argument to the start client script. + // This argument is consumed by the start client script and will not be passed to the client worker. + simulatedPlayerArgs = new string[] { simulatedPlayerName }.Concat(simulatedPlayerArgs).ToArray(); + + // Start the client + string flattenedArgs = string.Join(" ", simulatedPlayerArgs); + Logger.WriteLog($"Starting simulated player {simulatedPlayerName} with args: {flattenedArgs}"); + CreateSimulatedPlayerProcess(SimulatedPlayerFilename, flattenedArgs);; + } + catch (Exception e) + { + Logger.WriteError($"Failed to start simulated player: {e.Message}"); + } + } + + private static string GetWorkerFlagOrDefault(Connection connection, string flagName, string defaultValue) + { + Option flagValue = connection.GetWorkerFlag(flagName); + if (flagValue.HasValue) + { + return flagValue.Value; + } + + 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/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Program.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Program.cs new file mode 100644 index 0000000000..2b95de5a8f --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Program.cs @@ -0,0 +1,46 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +namespace Improbable.WorkerCoordinator +{ + /// + /// Executable that connects and runs simulated players. + /// + /// There are two types of coordinator workers: + /// + /// ManagedWorkerCoordinator + /// Starts as a managed worker and creates its own connection to the hosting deployment (simulated player deployment). + /// + /// + /// RegisseurWorkerCoordinator + /// Starts as a standalone process, only for internal use at Improbable for running as part of a Regisseur scenario. + /// + /// + /// To start a ManagedWorkerCoordinator, the first command line argument must be `receptionist`. + /// Otherwise, a RegisseurWorkerCoordinator is started. + /// + internal static class Program + { + private static int Main(string[] args) + { + var logger = new Logger("/improbable/logs/WorkerCoordinator.log", "WorkerCoordinator"); + logger.WriteLog("Starting coordinator with args: " + string.Join(" ", args)); + + AbstractWorkerCoordinator coordinator; + if (args[0] == "receptionist") + { + // Start coordinator inside a deployment as a managed worker. + coordinator = ManagedWorkerCoordinator.FromArgs(logger, args); + } + else + { + // Start coordinator as standalone process, not as a managed worker. + coordinator = RegisseurWorkerCoordinator.FromArgs(logger, args); + } + + // Run coordinator. Blocks until completion. + coordinator.Run(); + + return 0; + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/RegisseurWorkerCoordinator.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/RegisseurWorkerCoordinator.cs new file mode 100644 index 0000000000..8c9e5b0ef7 --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/RegisseurWorkerCoordinator.cs @@ -0,0 +1,86 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Improbable.WorkerCoordinator +{ + /// + /// For Improbable internal use only. + /// Defines a worker coordinator to be run as part of a Regisseur scenario. + /// Runs as a standalone binary without creating a connection to a deployment, by connecting a configurable + /// number of simulated players over configurable delays. + /// + internal class RegisseurWorkerCoordinator : AbstractWorkerCoordinator + { + private const string SimulatedPlayerSpawnCountArg = "simulated_player_spawn_count"; + private const string InitialStartDelayArg = "coordinator_start_delay_millis"; + private const string SpawnIntervalArg = "coordinator_spawn_interval_millis"; + private const string SimulatedPlayerWorkerNamePlaceholderArg = ""; + + private const string SimulatedPlayerFilename = "StartSimulatedClient.sh"; + + // Coordinator options. + private int NumSimulatedPlayersToStart; + private int InitialStartDelayMillis; + private int StartIntervalMillis; + private string[] SimulatedPlayerArgs; + + /// + /// The arguments to start the coordinator must begin with: + /// + /// "simulated_player_spawn_count={integer}", + /// "coordinator_spawn_interval_millis={integer}", + /// "coordinator_start_delay_millis={integer}", + /// "" + /// + /// All following arguments are passed to the simulated player process, where + /// the "" placeholder will be replaced by the + /// worker id of the simulated player. + /// + public static RegisseurWorkerCoordinator FromArgs(Logger logger, string[] args) + { + return new RegisseurWorkerCoordinator(logger) + { + NumSimulatedPlayersToStart = Util.GetIntegerArgumentOrDefault(args, SimulatedPlayerSpawnCountArg, 1), + InitialStartDelayMillis = Util.GetIntegerArgumentOrDefault(args, InitialStartDelayArg, 10000), + StartIntervalMillis = Util.GetIntegerArgumentOrDefault(args, SpawnIntervalArg, 1000), + + // First 3 arguments are for the coordinator worker only. + // The 4th argument (worker id) is consumed by the simulated player startup script, + // and not passed to the simulated player. + SimulatedPlayerArgs = args.Skip(3).ToArray() + }; + } + + private RegisseurWorkerCoordinator(Logger logger) : base(logger) + { + } + + public override void Run() + { + Thread.Sleep(InitialStartDelayMillis); + for (int i = 0; i < NumSimulatedPlayersToStart; i++) + { + StartSimulatedPlayer($"SimulatedPlayer{Guid.NewGuid()}"); + Thread.Sleep(StartIntervalMillis); + } + + WaitForPlayersToExit(); + } + + public void StartSimulatedPlayer(string name) + { + var args = Util.ReplacePlaceholderArgs(SimulatedPlayerArgs, new Dictionary() + { + { SimulatedPlayerWorkerNamePlaceholderArg, name } + }); + + string simulatedPlayerArgs = string.Join(" ", args); + Logger.WriteLog("Starting worker " + name + " with args: " + simulatedPlayerArgs); + CreateSimulatedPlayerProcess(SimulatedPlayerFilename, simulatedPlayerArgs); + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/SpatialConfig/cloud_launch_sim_player_deployment.json b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/SpatialConfig/cloud_launch_sim_player_deployment.json new file mode 100644 index 0000000000..7448dcf50d --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/SpatialConfig/cloud_launch_sim_player_deployment.json @@ -0,0 +1,40 @@ +{ + "template": "sim_players", + "world": { + "chunkEdgeLengthMeters": 50, + "snapshots": { + "snapshotWritePeriodSeconds": 0 + }, + "dimensions": { + "xMeters": 300, + "zMeters": 300 + } + }, + "load_balancing": { + "layer_configurations": [ + { + "layer": "SimulatedPlayerCoordinator", + "rectangle_grid": { + "cols": 10, + "rows": 1 + } + } + ] + }, + "workers": [ + { + "worker_type": "SimulatedPlayerCoordinator", + "flags": [ + { + "name": "coordinator_start_delay_millis", + "value": "10000" + } + ], + "permissions": [ + { + "all": {} + } + ] + } + ] +} \ No newline at end of file diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/SpatialConfig/spatialos.SimulatedPlayerCoordinator.worker.json b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/SpatialConfig/spatialos.SimulatedPlayerCoordinator.worker.json new file mode 100644 index 0000000000..d5d5a34827 --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/SpatialConfig/spatialos.SimulatedPlayerCoordinator.worker.json @@ -0,0 +1,65 @@ +{ + "build": { + "tasks": [ + { + "name": "codegen", + "description": "required by spatial worker build build-config.", + "steps": [{"name": "No-op", "command": "echo", "arguments": ["No-op."]}] + }, + { + "name": "build", + "description": "required by spatial worker build build-config.", + "steps": [{"name": "No-op", "command": "echo", "arguments": ["No-op."]}] + }, + { + "name": "clean", + "description": "required by spatial worker build build-config.", + "steps": [{"name": "No-op", "command": "echo", "arguments": ["No-op."]}] + } + ] + }, + "bridge": { + "worker_attribute_set": { + "attributes": [ + "SimulatedPlayerCoordinator" + ] + }, + "entity_interest": { + "range_entity_interest": { + "radius": 50 + } + }, + "component_delivery": { + "default": "RELIABLE_ORDERED", + "checkout_all_initially": true + } + }, + "managed": { + "linux": { + "artifact_name": "UnrealSimulatedPlayer@Linux.zip", + "command": "StartCoordinator.sh", + "arguments": [ + "receptionist", + "${IMPROBABLE_RECEPTIONIST_HOST}", + "${IMPROBABLE_RECEPTIONIST_PORT}", + "${IMPROBABLE_WORKER_ID}", + "coordinator_start_delay_millis=10000", + + "+workerType", + "UnrealClient", + "+loginToken", + "", + "+playerIdentityToken", + "", + "+workerId", + "", + "+useExternalIpForBridge", + "true", + "-abslog=${IMPROBABLE_LOG_FILE}", + "-NoVerifyGC", + "-nullRHI", + "-simulatedplayer" + ] + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Util.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Util.cs new file mode 100644 index 0000000000..80079beea4 --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/Util.cs @@ -0,0 +1,90 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Improbable.WorkerCoordinator +{ + internal static class Util + { + /// + /// Get whether the specified argument is given in the list of arguments. + /// Looks for arguments in the form {argumentName}={value}. + /// + public static bool HasIntegerArgument(IEnumerable args, string argumentName) + { + return args.Where(arg => arg.StartsWith($"{argumentName}=")).Count() > 0; + } + + /// + /// Get the value of the named argument in the given list of arguments. + /// Looks for arguments in the from {argumentName}={value}. + /// + /// Throws an exception if none or more than 1 occurrences of the argument are found, + /// or the argument value is formatted incorrectly. + /// + public static int GetIntegerArgument(IEnumerable args, string argumentName) + { + var argsWithName = args.Where(arg => arg.StartsWith($"{argumentName}=")).ToArray(); + if (argsWithName.Length != 1) + { + throw new ArgumentException($"Expected exactly one value for argument \"{argumentName}\", found {argsWithName.Length}."); + } + + var argWithName = argsWithName[0]; + var split = argWithName.Split(new[] { '=' }, 2, StringSplitOptions.None); + if (split.Length != 2) + { + throw new ArgumentException($"Cannot parse value for argument \"{argumentName}\". Expected format \"{argumentName}=\", found \"{argWithName}\"."); + } + + var valueString = split[1]; + if (int.TryParse(valueString, out int value)) + { + return value; + } + + throw new ArgumentException($"Cannot parse value,\"{valueString}\", for argument \"{argumentName}\"."); + } + + /// + /// Get the value of the named argument in the given list of arguments. + /// Looks for arguments in the from {argumentName}={value}. + /// + /// If the argument is not present in the list of arguments, the default value is returned. + /// + /// Throws an exception if more than 1 occurrences of the argument are found, + /// or the argument value is formatted incorrectly. + /// + public static int GetIntegerArgumentOrDefault(IEnumerable args, string argumentName, int defaultValue) + { + if (HasIntegerArgument(args, argumentName)) + { + return GetIntegerArgument(args, argumentName); + } + + return defaultValue; + } + + /// + /// Replaces each argument in args that is present as a key in argumentReplacements with the corresponding value in argumentReplacements. + /// Does not modify the input arguments, returns a new array with the replaced arguments. + /// + /// array of arguments to check for placeholders to replace with a value + /// mapping containing the arguments to replace as keys and values the replacement values + /// copy of input args with placeholder arguments replaced with their values + public static string[] ReplacePlaceholderArgs(string[] args, Dictionary argumentReplacements) + { + return args.ToList().ConvertAll(arg => + { + if (argumentReplacements.ContainsKey(arg)) + { + return argumentReplacements[arg]; + } + + return arg; + }).ToArray(); + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/WorkerCoordinator.csproj b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/WorkerCoordinator.csproj new file mode 100644 index 0000000000..51948df66e --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/WorkerCoordinator.csproj @@ -0,0 +1,78 @@ + + + + + Debug + AnyCPU + {C41625B0-CDB7-4480-B2E4-AEB27AF3B198} + Exe + Properties + WorkerCoordinator + WorkerCoordinator + v4.5.1 + 512 + true + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + Improbable.WorkerCoordinator.Program + + + + + + + + + + + + ..\..\..\..\Binaries\ThirdParty\Improbable\Programs\worker_sdk\csharp\Improbable.WorkerSdkCsharp.dll + + + libCoreSdkDll.so + PreserveNewest + + + + + + + + + + + + + + + mkdir "$(SolutionDir)..\..\..\Binaries\ThirdParty\Improbable\Programs\WorkerCoordinator\" +xcopy "$(SolutionDir)\$(ProjectName)\$(OutDir)." "$(SolutionDir)..\..\..\Binaries\ThirdParty\Improbable\Programs\WorkerCoordinator\" /q /y + + + \ No newline at end of file diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WriteLinuxScript/WriteLinuxScript.cs b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WriteLinuxScript/WriteLinuxScript.cs new file mode 100644 index 0000000000..ba5a9f15fb --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WriteLinuxScript/WriteLinuxScript.cs @@ -0,0 +1,39 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +using System; +using System.IO; +using System.Linq; +using Improbable.Unreal.Build.Common; + +namespace Improbable.Unreal.Build.Util +{ + public static class WriteLinuxScript + { + public static void Main(string[] args) + { + var help = args.Count(arg => arg == "/?" || arg.ToLowerInvariant() == "--help") > 0; + + var exitCode = 0; + if (args.Length < 2 && !help) + { + help = true; + exitCode = 1; + } + + if (help) + { + Console.WriteLine("Usage: "); + Environment.Exit(exitCode); + } + + var outputFile = Path.GetFullPath(args[0]); + var baseGameName = args[1]; + + Improbable.Common.WriteHeading(" > Writing Linux worker start script."); + + // Write out the wrapper shell script to work around issues between UnrealEngine and our cloud Linux environments. + // Also ensure script uses Linux line endings + LinuxScripts.WriteWithLinuxLineEndings(LinuxScripts.GetUnrealWorkerShellScript(baseGameName), outputFile); + } + } +} diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WriteLinuxScript/WriteLinuxScript.csproj b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WriteLinuxScript/WriteLinuxScript.csproj new file mode 100644 index 0000000000..854f89e6bb --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WriteLinuxScript/WriteLinuxScript.csproj @@ -0,0 +1,70 @@ + + + + + Debug + AnyCPU + {884C3696-4722-47E5-9100-ED20FE33E05D} + Exe + Properties + WriteLinuxScript + WriteLinuxScript + v4.5 + 512 + true + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + {d2cc2525-ec22-4a24-bde8-d9a6bff55bad} + Common + False + + + + + + + + copy "$(SolutionDir)\$(ProjectName)\$(OutDir)\$(ProjectName).exe" "$(SolutionDir)..\..\..\Binaries\ThirdParty\Improbable\Programs\" /Y + + + \ No newline at end of file diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/packages/Newtonsoft.Json.12.0.2/LICENSE.md b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/packages/Newtonsoft.Json.12.0.2/LICENSE.md new file mode 100644 index 0000000000..dfaadbe493 --- /dev/null +++ b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/packages/Newtonsoft.Json.12.0.2/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2007 James Newton-King + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/packages/Newtonsoft.Json.12.0.2/lib/net45/Newtonsoft.Json.dll b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/packages/Newtonsoft.Json.12.0.2/lib/net45/Newtonsoft.Json.dll new file mode 100644 index 0000000000..4395f61011 Binary files /dev/null and b/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/packages/Newtonsoft.Json.12.0.2/lib/net45/Newtonsoft.Json.dll differ diff --git a/SpatialGDK/Build/Scripts/BuildWorker.bat b/SpatialGDK/Build/Scripts/BuildWorker.bat index 0c32c1bff2..c6a7a71f0d 100644 --- a/SpatialGDK/Build/Scripts/BuildWorker.bat +++ b/SpatialGDK/Build/Scripts/BuildWorker.bat @@ -1,22 +1,73 @@ @echo off -pushd "%~dp0..\..\..\..\..\..\spatial" +if [%4] == [] goto :MissingParams -spatial worker build build-config +rem Try and build as a project plugin first, check for a project plugin structure. +set UNREAL_PROJECT_DIR="%~dp0..\..\..\..\..\" +set FOUND_UPROJECT="" -popd +for /f "delims=" %%A in (' powershell -Command "(Get-ChildItem -Path "%UNREAL_PROJECT_DIR%" *.uproject).FullName" ') do set FOUND_UPROJECT="%%A" -pushd "%~dp0..\..\..\..\..\" +if %FOUND_UPROJECT%=="" ( + goto :BuildAsEnginePlugin +) +rem If we are running as a project plugin then this will be the path to the spatial directory. +echo Building as project plugin +set SPATIAL_DIR="%~dp0..\..\..\..\..\..\spatial" set BUILD_EXE_PATH="Plugins\UnrealGDK\SpatialGDK\Binaries\ThirdParty\Improbable\Programs\Build.exe" +goto :Build + + +:BuildAsEnginePlugin +rem If we are running as an Engine plugin then we need a full path to the .uproject file! +set UPROJECT=%4 + +if not exist %UPROJECT% ( + echo To use BuildWorker.bat with an Engine plugin installation you must provide the full path to your .uproject file. + exit /b 1 +) + +rem Grab the project path from the .uproject file. +for %%i in (%UPROJECT%) do ( + rem file drive + file directory + set UNREAL_PROJECT_DIR="%%~di%%~pi" +) + +rem Path to the SpatialGDK build tool as an Engine plugin. +set BUILD_EXE_PATH="%~dp0..\..\Binaries\ThirdParty\Improbable\Programs\Build.exe" +set SPATIAL_DIR=%UNREAL_PROJECT_DIR%..\spatial\ + + +:Build + +if not exist %SPATIAL_DIR% ( + echo Could not find the project's 'spatial' directory! Please ensure your input arguments are correct and your GDK is correctly installed. + exit /b 1 +) + +echo Building using project directory: %UNREAL_PROJECT_DIR% +echo Building using spatial directory: %SPATIAL_DIR% +echo Building using uproject file: %4 + +rem First build the spatial worker configs +pushd "%SPATIAL_DIR%" +spatial worker build build-config +popd + +rem We pushd to the location of the project file to allow specifying just the .uproject name and not the full path (both are allowed for project plugins). +pushd %UNREAL_PROJECT_DIR% if not exist %BUILD_EXE_PATH% ( echo Error: Build executable not found! Please run Setup.bat in your UnrealGDK root to generate it. exit /b 1 ) +rem Build Unreal project using the SpatialGDK build tool %BUILD_EXE_PATH% %* - popd - exit /b %ERRORLEVEL% + +:MissingParams +echo Missing input arguments! Usage: " [-nocompile] " +exit /b 1 diff --git a/SpatialGDK/Build/Scripts/DeploymentLauncher.bat b/SpatialGDK/Build/Scripts/DeploymentLauncher.bat new file mode 100644 index 0000000000..a2c780dcd1 --- /dev/null +++ b/SpatialGDK/Build/Scripts/DeploymentLauncher.bat @@ -0,0 +1,15 @@ +@echo off + +set DEPLOYMENT_LAUNCHER_EXE_PATH="%~dp0..\..\Binaries\ThirdParty\Improbable\Programs\DeploymentLauncher\DeploymentLauncher.exe" + +if not exist %DEPLOYMENT_LAUNCHER_EXE_PATH% ( + echo Error: Deployment launcher executable not found! Please run Setup.bat in your UnrealGDK root to generate it. + pause + exit /b 1 +) + +%DEPLOYMENT_LAUNCHER_EXE_PATH% %* + +pause + +exit /b %ERRORLEVEL% diff --git a/SpatialGDK/Build/Scripts/ExternalSchemaCodegen.bat b/SpatialGDK/Build/Scripts/ExternalSchemaCodegen.bat new file mode 100644 index 0000000000..050bfd9e3f --- /dev/null +++ b/SpatialGDK/Build/Scripts/ExternalSchemaCodegen.bat @@ -0,0 +1,80 @@ +@echo off + +IF NOT "%~3"=="" IF "%~4"=="" GOTO START +ECHO This script requires three parameters (the first parameter is the project root and the other parameters should be defined relative to project root): +ECHO - path to your project root containing the 'spatial' folder +ECHO - path to external schema directory +ECHO - target output folder for generated code +exit /b 1 + +:START + +call :MarkStartOfBlock "Setup variables" + pushd %1 + set GAME_FOLDER=%cd% + popd + pushd %~dp0\..\..\.. + set GDK_FOLDER=%cd% + set SCHEMA_COMPILER_PATH=%GDK_FOLDER%\SpatialGDK\Binaries\ThirdParty\Improbable\Programs\schema_compiler.exe + set CODEGEN_EXE_PATH=%GDK_FOLDER%\SpatialGDK\Binaries\ThirdParty\Improbable\Programs\CodeGenerator.exe + set SCHEMA_STD_COPY_DIR=%GAME_FOLDER%\spatial\build\dependencies\schema\standard_library + set SPATIAL_SCHEMA_FOLDER=%GAME_FOLDER%\spatial\schema + set BUNDLE_CACHE_DIR=%GDK_FOLDER%\SpatialGDK\Intermediate\ExternalSchemaCodegen + set SCHEMA_BUNDLE_FILE_NAME=external_schema_bundle.json + popd +call :MarkEndOfBlock "Setup variables" + +call :MarkStartOfBlock "Clean folders" + rd /s /q "%BUNDLE_CACHE_DIR%" 2>nul +call :MarkEndOfBlock "Clean folders" + +call :MarkStartOfBlock "Create folders" + md "%BUNDLE_CACHE_DIR%" >nul 2>nul +call :MarkEndOfBlock "Create folders" + + +if not exist "%SCHEMA_COMPILER_PATH%" ( + echo Error: Schema compiler executable not found at "%SCHEMA_COMPILER_PATH%" ! Please run Setup.bat in your UnrealGDK root to generate it. + exit /b 1 +) + +if not exist "%SCHEMA_STD_COPY_DIR%" ( + echo Error: Could not locate SpatialOS standard library files at "%SCHEMA_STD_COPY_DIR%" ! Please run Setup.bat in your UnrealGDK root to generate it. + exit /b 1 +) + +call :MarkStartOfBlock "Collecting external schema files" +set EXTERNAL_SCHEMA_FILES= +setlocal enabledelayedexpansion +FOR /F %%i in ('dir /s/b "%GAME_FOLDER%\%2\*.schema"') do ( set "EXTERNAL_SCHEMA_FILES=!EXTERNAL_SCHEMA_FILES! %%i" ) +setlocal disabledelayedexpansion +call :MarkEndOfBlock "Collecting external schema files" + +call :MarkStartOfBlock "Running schema compiler" +%SCHEMA_COMPILER_PATH% --schema_path="%SPATIAL_SCHEMA_FOLDER%" --bundle_json_out="%BUNDLE_CACHE_DIR%\%SCHEMA_BUNDLE_FILE_NAME%" %EXTERNAL_SCHEMA_FILES% || exit /b 1 +call :MarkEndOfBlock "Running schema compiler" + +if not exist "%CODEGEN_EXE_PATH%" ( + echo Error: Codegen executable not found at "%CODEGEN_EXE_PATH%"! Please run Setup.bat in your UnrealGDK root to generate it. + exit /b 1 +) + +call :MarkStartOfBlock "Running code generator" +%CODEGEN_EXE_PATH% --input-bundle "%BUNDLE_CACHE_DIR%\%SCHEMA_BUNDLE_FILE_NAME%" --output-dir "%GAME_FOLDER%\%3" +if ERRORLEVEL 1 ( + echo Error: Code generation failed + pause + exit /b 1 +) +echo Code successfully generated at "%GAME_FOLDER%\%2" +call :MarkEndOfBlock "Running code generator" + +exit /b %ERRORLEVEL% + +:MarkStartOfBlock +echo Starting: %~1 +exit /b 0 + +:MarkEndOfBlock +echo Finished: %~1 +exit /b 0 diff --git a/SpatialGDK/Build/Scripts/FindMSBuild.bat b/SpatialGDK/Build/Scripts/FindMSBuild.bat new file mode 100644 index 0000000000..b767568a2d --- /dev/null +++ b/SpatialGDK/Build/Scripts/FindMSBuild.bat @@ -0,0 +1,25 @@ +rem Convenient code to find MSBuild.exe from https://github.com/microsoft/vswhere +rem We only support VS2017 so only look for MS_Build 15.0 + +@echo off + +set MSBUILD_EXE= + +if exist "%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" ( + for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere" -prerelease -latest -requires Microsoft.Component.MSBuild -property installationPath`) do ( + if exist "%%i\MSBuild\15.0\Bin\MSBuild.exe" ( + set MSBUILD_EXE="%%i\MSBuild\15.0\Bin\MSBuild.exe" + exit /b 0 + ) + ) +) + +rem As a backup, if we couldn't find MSBuild via vswhere then ask the registry. +for /f "usebackq tokens=2,*" %%A in (`reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\VisualStudio\SxS\VS7" /v 15.0`) do ( + if exist "%%BMSBuild\15.0\Bin\MSBuild.exe" ( + set MSBUILD_EXE="%%BMSBuild\15.0\Bin\MSBuild.exe" + exit /b 0 + ) +) + +exit /b 1 diff --git a/SpatialGDK/Documentation/README.md b/SpatialGDK/Documentation/README.md index e167579967..c103841554 100644 --- a/SpatialGDK/Documentation/README.md +++ b/SpatialGDK/Documentation/README.md @@ -1,5 +1,3 @@ -# The SpatialOS GDK for Unreal (alpha) documentation on GitHub +# The SpatialOS GDK for Unreal documentation -This documentation on GitHub is a copy of the [GDK documentation on the SpatialOS documentation website](https://docs.improbable.io/unreal/latest). Due to this, the internal links do not work and the image links are broken. - -We recommend you use the GDK documentation on the SpatialOS documentation website and not on GitHub. +The GDK's documentation is available at https://docs.improbable.io/unreal/latest. \ No newline at end of file diff --git a/SpatialGDK/Documentation/assets/screen-grabs/blueprint-singleton.png b/SpatialGDK/Documentation/assets/screen-grabs/blueprint-singleton.png deleted file mode 100644 index 7c19bcfd26..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/blueprint-singleton.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/cooking-maps.png b/SpatialGDK/Documentation/assets/screen-grabs/cooking-maps.png deleted file mode 100644 index 95d853ae29..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/cooking-maps.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/crossserver-blueprint.png b/SpatialGDK/Documentation/assets/screen-grabs/crossserver-blueprint.png deleted file mode 100644 index 1a63367e6c..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/crossserver-blueprint.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/game-architecture.png b/SpatialGDK/Documentation/assets/screen-grabs/game-architecture.png deleted file mode 100644 index 8689ea7572..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/game-architecture.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/handover-blueprint.png b/SpatialGDK/Documentation/assets/screen-grabs/handover-blueprint.png deleted file mode 100644 index 791ea10948..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/handover-blueprint.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/moving-across-boundaries.gif b/SpatialGDK/Documentation/assets/screen-grabs/moving-across-boundaries.gif deleted file mode 100644 index f34ef55fc0..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/moving-across-boundaries.gif and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/networking-switch.png b/SpatialGDK/Documentation/assets/screen-grabs/networking-switch.png deleted file mode 100644 index 1be6c1c9fb..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/networking-switch.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/on-authority-gained.jpg b/SpatialGDK/Documentation/assets/screen-grabs/on-authority-gained.jpg deleted file mode 100644 index 2d69b0fdd7..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/on-authority-gained.jpg and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/on-authority-lost.jpg b/SpatialGDK/Documentation/assets/screen-grabs/on-authority-lost.jpg deleted file mode 100644 index cd1193b1d2..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/on-authority-lost.jpg and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/porting-solution-config.png b/SpatialGDK/Documentation/assets/screen-grabs/porting-solution-config.png deleted file mode 100644 index 1d7645cac7..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/porting-solution-config.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/shooting-across-boundaries.png b/SpatialGDK/Documentation/assets/screen-grabs/shooting-across-boundaries.png deleted file mode 100644 index 9dabe667c4..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/shooting-across-boundaries.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/snapshot-asset-cooking.png b/SpatialGDK/Documentation/assets/screen-grabs/snapshot-asset-cooking.png deleted file mode 100644 index 461e63fc6c..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/snapshot-asset-cooking.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/snapshot.png b/SpatialGDK/Documentation/assets/screen-grabs/snapshot.png deleted file mode 100644 index 09c2f7a7bd..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/snapshot.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/spatial-game-instance-reparent.png b/SpatialGDK/Documentation/assets/screen-grabs/spatial-game-instance-reparent.png deleted file mode 100644 index d54a962647..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/spatial-game-instance-reparent.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/enable-toolbar.png b/SpatialGDK/Documentation/assets/screen-grabs/toolbar/enable-toolbar.png deleted file mode 100644 index db92d88d69..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/enable-toolbar.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/inspector-button.png b/SpatialGDK/Documentation/assets/screen-grabs/toolbar/inspector-button.png deleted file mode 100644 index 2ab7111c1f..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/inspector-button.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/multi-player-options.png b/SpatialGDK/Documentation/assets/screen-grabs/toolbar/multi-player-options.png deleted file mode 100644 index c913d5fc7b..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/multi-player-options.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/play-button.png b/SpatialGDK/Documentation/assets/screen-grabs/toolbar/play-button.png deleted file mode 100644 index 665e6e0e41..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/play-button.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/schema-button.png b/SpatialGDK/Documentation/assets/screen-grabs/toolbar/schema-button.png deleted file mode 100644 index 465c73fa30..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/schema-button.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/snapshot-button.png b/SpatialGDK/Documentation/assets/screen-grabs/toolbar/snapshot-button.png deleted file mode 100644 index 8cbab19a35..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/snapshot-button.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/start-button.png b/SpatialGDK/Documentation/assets/screen-grabs/toolbar/start-button.png deleted file mode 100644 index 78d9ebfaa5..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/start-button.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/stop-button.png b/SpatialGDK/Documentation/assets/screen-grabs/toolbar/stop-button.png deleted file mode 100644 index 8f4b78385a..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/stop-button.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/toolbar-buttons.png b/SpatialGDK/Documentation/assets/screen-grabs/toolbar/toolbar-buttons.png deleted file mode 100644 index c4723c5125..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/toolbar-buttons.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/toolbar-checkboxes.png b/SpatialGDK/Documentation/assets/screen-grabs/toolbar/toolbar-checkboxes.png deleted file mode 100644 index b5f5163447..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/toolbar-checkboxes.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/toolbar-settings.png b/SpatialGDK/Documentation/assets/screen-grabs/toolbar/toolbar-settings.png deleted file mode 100644 index b0b73bae74..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/toolbar-settings.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/toolbars-basic.png b/SpatialGDK/Documentation/assets/screen-grabs/toolbar/toolbars-basic.png deleted file mode 100644 index e154bb16e4..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/toolbars-basic.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/toolbars.png b/SpatialGDK/Documentation/assets/screen-grabs/toolbar/toolbars.png deleted file mode 100644 index 0f69c0a4d1..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/toolbars.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/window-access.png b/SpatialGDK/Documentation/assets/screen-grabs/toolbar/window-access.png deleted file mode 100644 index bddfb5990c..0000000000 Binary files a/SpatialGDK/Documentation/assets/screen-grabs/toolbar/window-access.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/set-up-template/template-multiplayer-options.png b/SpatialGDK/Documentation/assets/set-up-template/template-multiplayer-options.png deleted file mode 100644 index 3ff219940a..0000000000 Binary files a/SpatialGDK/Documentation/assets/set-up-template/template-multiplayer-options.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/set-up-template/template-project-browser.png b/SpatialGDK/Documentation/assets/set-up-template/template-project-browser.png deleted file mode 100644 index 725eee06d5..0000000000 Binary files a/SpatialGDK/Documentation/assets/set-up-template/template-project-browser.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/set-up-template/template-project-page.png b/SpatialGDK/Documentation/assets/set-up-template/template-project-page.png deleted file mode 100644 index 9d09cf8c9b..0000000000 Binary files a/SpatialGDK/Documentation/assets/set-up-template/template-project-page.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/set-up-template/template-two-client-inspector.png b/SpatialGDK/Documentation/assets/set-up-template/template-two-client-inspector.png deleted file mode 100644 index e5c954b54f..0000000000 Binary files a/SpatialGDK/Documentation/assets/set-up-template/template-two-client-inspector.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/set-up-template/template-two-clients.png b/SpatialGDK/Documentation/assets/set-up-template/template-two-clients.png deleted file mode 100644 index 116c95ffd2..0000000000 Binary files a/SpatialGDK/Documentation/assets/set-up-template/template-two-clients.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/set-up-template/template-vs-toolbar.png b/SpatialGDK/Documentation/assets/set-up-template/template-vs-toolbar.png deleted file mode 100644 index cda276917f..0000000000 Binary files a/SpatialGDK/Documentation/assets/set-up-template/template-vs-toolbar.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/shooting-workflow-simple.png b/SpatialGDK/Documentation/assets/shooting-workflow-simple.png deleted file mode 100644 index 759a23ed2c..0000000000 Binary files a/SpatialGDK/Documentation/assets/shooting-workflow-simple.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/tutorial/console.png b/SpatialGDK/Documentation/assets/tutorial/console.png deleted file mode 100644 index 325d96653e..0000000000 Binary files a/SpatialGDK/Documentation/assets/tutorial/console.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/tutorial/cross-server.gif b/SpatialGDK/Documentation/assets/tutorial/cross-server.gif deleted file mode 100644 index be562a3427..0000000000 Binary files a/SpatialGDK/Documentation/assets/tutorial/cross-server.gif and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/tutorial/inspector-two-workers.png b/SpatialGDK/Documentation/assets/tutorial/inspector-two-workers.png deleted file mode 100644 index c8fa56758b..0000000000 Binary files a/SpatialGDK/Documentation/assets/tutorial/inspector-two-workers.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/tutorial/launch.png b/SpatialGDK/Documentation/assets/tutorial/launch.png deleted file mode 100644 index 571ee38f65..0000000000 Binary files a/SpatialGDK/Documentation/assets/tutorial/launch.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/tutorial/project-name.png b/SpatialGDK/Documentation/assets/tutorial/project-name.png deleted file mode 100644 index 9d09cf8c9b..0000000000 Binary files a/SpatialGDK/Documentation/assets/tutorial/project-name.png and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/tutorial/shooting-across-boundaries.gif b/SpatialGDK/Documentation/assets/tutorial/shooting-across-boundaries.gif deleted file mode 100644 index be562a3427..0000000000 Binary files a/SpatialGDK/Documentation/assets/tutorial/shooting-across-boundaries.gif and /dev/null differ diff --git a/SpatialGDK/Documentation/assets/unrealgdk-headline-image.png b/SpatialGDK/Documentation/assets/unrealgdk-headline-image.png deleted file mode 100644 index 75c0986ea7..0000000000 Binary files a/SpatialGDK/Documentation/assets/unrealgdk-headline-image.png and /dev/null differ diff --git a/SpatialGDK/Documentation/content/ability-system.md b/SpatialGDK/Documentation/content/ability-system.md deleted file mode 100644 index 03829d6103..0000000000 --- a/SpatialGDK/Documentation/content/ability-system.md +++ /dev/null @@ -1,33 +0,0 @@ -<%(TOC)%> -# Gameplay Ability System on the GDK for Unreal - -The [Gameplay Ability System](https://docs.unrealengine.com/en-us/Gameplay/GameplayAbilitySystem) is a flexible framework for building abilities and attributes of the type you might find in an RPG, MMO, or MOBA. However, as it makes use of lots of the advanced parts of the Unreal networking stack, the GDK for Unreal doesn't *currently* support it in it's entirety. - -This page lists the workarounds that you need to use with the Gameplay Ability System in the alpha release of the GDK. We are working to remove some of these limitations in the future. - -## Gameplay Ability System workarounds -1. The GDK does not currently support replicated gameplay abilities. To work around this, you must ensure that any `UGameplayAbility` has its `ReplicationPolicy` set to `ReplicateNo`. Replicated gameplay abilities will be available once we support dynamic Actor components in the GDK. -2. You must add any `UAttributeSet` objects to the Actor that owns the `AbilitySystemComponent`, and you must add them to the Actor as `DefaultSubObjects`. For example - - - ``` - MyActor::MyActor() - { - ... - AbilitySystem = CreateDefaultSubobject(TEXT("AbilitySystem")); - AttributeSet = CreateDefaultSubobject(TEXT("AttributeSet")); - ... - } - ``` - - This ensures that the replicated data on the `UAttributeSet` object is replicated correctly. -3. If the `AbilitySystemComponent` is owned by a class that extends `UPawn`, you must override the `UPawn::OnRep_Controller()` function and call `AbilitySystemComponent::RefreshAbilityActorInfo()`. For example - - - ``` - MyActor::OnRep_Controller() - { - Super::OnRep_Controller(); - AbilitySystem->RefreshAbilityActorInfo(); - } - ``` - -This is necessary due to an edge case in the GDK for Unreal where a Pawn may be checked out from the SpatialOS Runtime before the Pawn's controller is. diff --git a/SpatialGDK/Documentation/content/authority.md b/SpatialGDK/Documentation/content/authority.md deleted file mode 100644 index 6457e2ca46..0000000000 --- a/SpatialGDK/Documentation/content/authority.md +++ /dev/null @@ -1,150 +0,0 @@ -<%(TOC)%> -# Authority -To work with authority in the GDK, it’s useful to refresh on authority in Unreal’s native networking. See: - -* [Unreal’s Network Role documentation](https://wiki.unrealengine.com/Replication#A_Guide_To_Network_Roles) -* [Unreal's Owning Connection documentation](https://docs.unrealengine.com/en-us/Gameplay/Networking/Actors/OwningConnections) - -## Unreal networking authority - -In native-Unreal networking, the single server has absolute authority over all replicated Actors, while clients have only proxies of Actors. Unreal models this using the `Role` and `RemoteRole` fields within an Actor. - -There are 3 main types of networking Roles in Unreal: - -* `ROLE_Authority` - The authoritative version of the Actor. -* `ROLE_SimulatedProxy` - Locally simulates the state of the Actor from the server. -* `ROLE_AutonomousProxy` - Locally simulates the state of the Actor but with the ability to execute RPCs on the Actor. This is usually reserved for Actors that are "owned" by a client - -For more information about network roles, see [Unreal’s Network Role documentation](https://wiki.unrealengine.com/Replication#A_Guide_To_Network_Roles). - -For the majority of use cases in native Unreal, these fields have the values: - -Actor on **server**: - -* `Role = ROLE_Authority` -* `RemoteRole = ROLE_SimulatedProxy` - -Actor on **client**: - -* `Role = ROLE_SimulatedProxy` -* `RemoteRole = ROLE_Authority` - -Actors that have an [owning connection (Unreal documentation)](https://docs.unrealengine.com/en-us/Gameplay/Networking/Actors/OwningConnections) are slightly different. An Actor can have an owning connection if: - -* It is a `PlayerController` with an associated `NetConnection`. -* It is a `Pawn` which is possessed by a `PlayerController` that has an associated `NetConnection`. -* Its `Owner` is set to an Actor that has an owning connection. - -For example, if a client has a `PlayerController` which possesses a `Character` which is holding a gun (whose `Owner` is the `Character`), all three (the gun, `Character` and `PlayerController`) have an owning connection. - -In any of these cases, Unreal authority looks like: - -Actor on **server** with an **owning connection**: - -* `Role = ROLE_Authority` -* `RemoteRole = ROLE_AutonomousProxy` - -Actor on **owning client**: - -* `Role = ROLE_AutonomousProxy` -* `RemoteRole = ROLE_Authority` - -Actor on **non-owning client**: - -* `Role = ROLE_SimulatedProxy` -* `RemoteRole = ROLE_Authority` - -## GDK authority - -As the GDK works with multiple [server-workers]({{urlRoot}}/content/glossary#workers), rather than a single server, authority needs to be dictated by SpatialOS so that authority is shared between server-workers. This means server-workers have authority over some Actors but don’t have authority over other Actors, depending on how SpatialOS assigns authority. **This is a key difference between the GDK and native Unreal networking!** - -> We use the term “authoritative” when a server-worker has authority over an Actor and “non-authoritative” when it doesn’t. - -In the GDK, a server-worker is authoritative over an Actor if it has authority over the [schema]({{urlRoot}}/content/glossary#schema) [component]({{urlRoot}}/content/glossary#spatialos-component) `Position`. - -So, in the SpatialOS GDK multiserver scenario, authority looks like this: - -Actor on **authoritative server-worker**: - -* `Role = ROLE_Authority` -* `RemoteRole = ROLE_SimulatedProxy` - -Actor on **non-authoritative server-worker**: - -* `Role = ROLE_SimulatedProxy` -* `RemoteRole = ROLE_Authority` - -Actor on **[client-worker]({{urlRoot}}/content/glossary#workers)**: - -* `Role = ROLE_SimulatedProxy` -* `RemoteRole = ROLE_Authority` - -The GDK models owning connections using a schema component which represents the `ClientRPC`s. The worker authoritative over the `ClientRPC` schema [component]({{urlRoot}}/content/glossary#spatialos-component) is the client which owns the Actor. - -In the same example as above, with the `PlayerController`, `Character` and gun, the client-worker would be authoritative over the `ClientRPC` schema component on the `PlayerController`, `Character` and gun [entities]({{urlRoot}}/content/glossary#spatialos-entity). Authority over this schema component dictates the assignment of `ROLE_AutonomousProxy`. - -Actor on **authoritative server-worker** with an **owning connection**: - -* `Role = ROLE_Authority` -* `RemoteRole = ROLE_AutonomousProxy` - -Actor on **non-authoritative server-worker** with an **owning connection**: - -* `Role = ROLE_SimulatedProxy` -* `RemoteRole = ROLE_Authority` - -Actor on **owning client-worker**: - -* `Role = ROLE_AutonomousProxy` -* `RemoteRole = ROLE_Authority` - -Actor on **non-owning client-worker**: - -* `Role = ROLE_SimulatedProxy` -* `RemoteRole = ROLE_Authority` - -A `PlayerController` possessing different `Pawn`s would change their role as expected; the newly-possessed Pawn will become an autonomous proxy on the client-worker while the older `Pawn` will become a simulated proxy. - -## Authority Callbacks - -Due to authority being dynamic in the GDK, we've added events that can trigger behavior when authority changes. These events will only ever trigger on server-workers. - -`Role` and `RemoteRole` will be properly set to their correct values within these events. - -There are two kinds of authority events: - -### OnAuthorityGained - -Triggered when authority is gained over an Actor. - -To use, override `void OnAuthorityGained()` in your Actor or use the blueprint event. - - void AMyActor::OnAuthorityGained() - { - Super::OnAuthorityGained(); // Mandatory - - // Custom behavior when authority is gained. - // ... - } - -![OnAuthorityGained]({{assetRoot}}assets/screen-grabs/on-authority-gained.jpg) - -### OnAuthorityLost - -Triggered when authority is lost over an Actor. - -To use, override `void OnAuthorityLost()` in your Actor or use the blueprint event. - - void AMyActor::OnAuthorityLost() - { - Super::OnAuthorityLost(); // Mandatory - - // Custom behavior when authority is lost. - // ... - } - -![OnAuthorityLost]({{assetRoot}}assets/screen-grabs/on-authority-lost.jpg) - -These events have the same calling order as `BeginPlay()` or `Tick()`. - -Behavior that is triggered when authority is gained over an Actor Component or Subobject should be fired through the owning Actor. diff --git a/SpatialGDK/Documentation/content/cloud-dev-workflow.md b/SpatialGDK/Documentation/content/cloud-dev-workflow.md deleted file mode 100644 index e1574e0bce..0000000000 --- a/SpatialGDK/Documentation/content/cloud-dev-workflow.md +++ /dev/null @@ -1,51 +0,0 @@ -# Cloud development workflow - -The following flowchart provides a reference of the cloud development workflow on the GDK. - -If you haven't already, please follow the [GDK Starter Template guide]({{urlRoot}}/content/get-started/gdk-template) which provides a detailed explanation of the different steps. - - - - - -You may find the following command-line snippets useful as reference: - -### Build server-worker assembly - -``` -Game\Plugins\UnrealGDK\SpatialGDK\Build\Scripts\BuildWorker.bat Server Linux Development .uproject -``` - -Replacing `` with the name of your Unreal project. - -### Build client-worker assembly - -``` -Game\Plugins\UnrealGDK\SpatialGDK\Build\Scripts\BuildWorker.bat Win64 Development .uproject -``` - -Replacing `` with the name of your Unreal project. - -### Upload assembly - -``` -spatial cloud upload -``` - -Replacing `` with the name you choose to give your assembly. - -### Launch cloud deployment - -``` -spatial cloud launch --snapshot=snapshots/default.snapshot .json -``` - -Replacing: - -* `` - identifies the worker assemblies to use (as chosen in the `spatial cloud upload` command). -* `.json` - declares the world and load balancing configuration. -* `` - labels the deployment for SpatialOS to reference in the [Console]({{urlRoot}}/content/glossary#console). - ----- - -_2019-04-15 Page added with editorial review_ \ No newline at end of file diff --git a/SpatialGDK/Documentation/content/cross-server-rpcs.md b/SpatialGDK/Documentation/content/cross-server-rpcs.md deleted file mode 100644 index 2c9cf42984..0000000000 --- a/SpatialGDK/Documentation/content/cross-server-rpcs.md +++ /dev/null @@ -1,71 +0,0 @@ -<%(TOC)%> -# Cross-server RPCs - -In native-Unreal networking, [RPCs (Unreal documentation)](https://docs.unrealengine.com/en-us/Gameplay/Networking/Actors/RPCs) are functions which either the client or the server use to send messages to each other over a network connection. - -Cross-server RPCs are a custom solution to take advantage of the SpatialOS distributed server architecture. - -In Unreal’s native single-client architecture, your game server holds the canonical state of the whole game world. As there is a single game server, it has complete authority over all server Actors and so it is able to invoke and execute functions on Actors unhindered. - -In SpatialOS games, there can be more than one server; these multiple servers are known as “server-workers”. (Find out more about server-workers as well as “client-workers” in the [glossary]({{urlRoot}}/content/glossary#workers).) As a SpatialOS game runs across many server-workers, SpatialOS server-workers have the concept of “worker authority” - where only one server-worker at a time is able to invoke and execute functions on Actors. (Find out more about authority in the [glossary]({{urlRoot}}/content/glossary#authority).) - -As Unreal expects there to be only one server, rather than several servers, the GDK has a custom solution to take advantage of the SpatialOS distributed server architecture. This involves handling the scenario where a server-worker attempts to invoke an RPC on an Actor that another server-worker has [authority]({{urlRoot}}/content/glossary#workers) over. This custom solution is the cross-server RPC. The GDK offers cross-server RPC in addition to support for the [native RPC types that Unreal provides (Unreal documentation)](https://docs.unrealengine.com/en-us/Gameplay/Networking/Actors/RPCs). - -When a cross-server RPC is invoked by a non-authoritative server-worker, SpatialOS routes the execution through the SpatialOS [Runtime]({{urlRoot}}/content/glossary#spatialos-runtime) to the authoritative server-worker - this authoritative server-worker executes the RPC. - -The example diagram below shows a player successfully shooting another player’s hat across a server-worker boundary. - -![A situation where you might need cross-server RPCs]({{assetRoot}}assets/shooting-workflow-simple.png) - -In the diagram, Server-worker 1 has authority over Player 1 and Server-worker 2 has authority over Player 2. If Player 1 shoots a bullet, Server-worker 1 knows about the bullet and can make any necessary changes to Player 1 but it can’t make changes to Player 2 when the bullet hits. SpatialOS ensures that Server-worker 2 can make changes to Player 2 (the hat gets hit by the bullet) by routing the change notification from Server-worker 1 to Server-worker 2. - -### How to send a cross-server RPC (using C++) - -To set up a cross-server RPC, follow the same instructions as you would for [marking up RPCs (Unreal documentation)](https://docs.unrealengine.com/en-us/Gameplay/Networking/Actors/RPCs) within Unreal. - -1. Add a `CrossServer` tag to the `UFUNCTION` macro of the RPC function that you want to be cross-server on your Actor (`MyActor` in this example). - - ``` - UFUNCTION(CrossServer, Reliable, WithValidation) - void MyCrossServerRPC(); - ``` - - Note: `WithValidation` is optional. - -1. Add the related function implementations: - ``` - void MyActor::MyCrossServerRPC_Implementation() - { - // Implementation goes here... - } - ``` - Note: You may need to implement the `MyCrossServerRPC_Validation()` if you used the `WithValidation` attribute. - -1. Invoke the `CrossServer` RPC function as you would with any other function. - -### How to send a cross-server RPC (using Blueprints) - -To set up a cross-server RPC in a Blueprint, follow the same instructions as you would for [marking up RPCs in Blueprints (Unreal documentation)](https://docs.unrealengine.com/en-us/Gameplay/Networking/Actors/RPCs#blueprints), but from the **Replicates** drop-down list within the **Details** panel of your event, select **Run on authoritative server (sent from server)**: - -![Setting up a cross-server RPC in blueprint]({{assetRoot}}assets/screen-grabs/crossserver-blueprint.png) - -### Execution notes - -The tables below show where cross-server RPCs are executed based on where they were invoked. (To make it easier to follow, the tables use the same format as the [Unreal documentation on RPCs](https://docs.unrealengine.com/en-us/Gameplay/Networking/Actors/RPCs#rpcinvokedfromtheserver).) - -#### Invoking a cross-server RPC from an authoritative server-worker - -| **Actor ownership** | **Cross-server RPC** -|-----------|--------- -| Client-owned Actor | Runs on the authoritative server-worker -| Server-owned Actor | Runs on the authoritative server-worker -| Unowned Actor | Runs on the authoritative server-worker - -#### Invoking a cross-server RPC from a client-worker - -| **Actor ownership** | **Cross-server RPC** -|-----------|--------- -| Owned by invoking client | Runs on invoking client-worker -| Owned by a different client | Runs on invoking client-worker -| Server-owned Actor | Runs on invoking client-worker -| Unowned Actor | Runs on invoking client-worker diff --git a/SpatialGDK/Documentation/content/directory-structure.md b/SpatialGDK/Documentation/content/directory-structure.md deleted file mode 100644 index ded3c0052d..0000000000 --- a/SpatialGDK/Documentation/content/directory-structure.md +++ /dev/null @@ -1,19 +0,0 @@ -<%(TOC)%> -# Directory structure -The table below lists the contents of the GDK for Unreal repository after running `Setup.bat` to build the Unreal engine as part of [Getting started]({{urlRoot}}/content/get-started/build-unreal-fork#step-4-build-unreal-engine). - -| Directory | Purpose -|-----------|--------- -| `SpatialGDK/Binaries/ThirdParty/Improbable/` | Not tracked in git. This directory contains the required binaries for your Unreal project to work with the SpatialOS UnrealGDK. These files are generated when running `Setup.bat`. -| `SpatialGDK/Build/core_sdk/` | Not tracked in git. Contains the [C API worker SDK](https://docs.improbable.io/reference/latest/capi/introduction) dependencies used by the GDK to serialize data to and from SpatialOS. -| `SpatialGDK/Build/Programs/` | Contains the source and project files for the executables used when building the GDK. -| `SpatialGDK/Build/Scripts/` | Contains the helper scripts that allow you to build either a server-worker or a client-worker. -| `SpatialGDK/Documentation` | Contains the documentation for the GDK. -| `SpatialGDK/Extras/fastbuild` | Contains files related to [FASTBuild](http://www.fastbuild.org/docs/home.html), an open-source build system that is currently only usable with the GDK by Improbable engineers. -| `SpatialGDK/Extras/schema` | Contains the [schema files](https://docs.improbable.io/reference/latest/shared/glossary#schema) required for the GDK to interact with SpatialOS. -| `SpatialGDK/Extras/linting/` | Contains the scripts we use to lint the GDK. -| `SpatialGDK/Source/SpatialGDK/Public` | Contains the public source code of the GDK uplugin. -| `SpatialGDK/Source/SpatialGDK/Private` | Contains the private source code of the GDK uplugin. -| `SpatialGDK/Source/SpatialGDK/Public/WorkerSdk/` | Not tracked in git. Contains the [C API worker SDK](https://docs.improbable.io/reference/latest/capi/introduction) headers which are used while building the GDK. You install these when you run `Setup.bat` -| `SpatialGDK/Source/SpatialGDKEditorToolbar/` | Contains the [SpatialOS GDK toolbar]({{urlRoot}}/content/toolbars.md) that appears within the Unreal Editor GUI, from which you can take snapshots, generate schemas, start and stop deployments, and access the SpatialOS Inspector. -| `ci/` | Contains scripts we use internally for our continuous integration. diff --git a/SpatialGDK/Documentation/content/dynamic-typebindings.md b/SpatialGDK/Documentation/content/dynamic-typebindings.md deleted file mode 100644 index e39328542c..0000000000 --- a/SpatialGDK/Documentation/content/dynamic-typebindings.md +++ /dev/null @@ -1,32 +0,0 @@ -<%(TOC)%> -# Dynamic Typebindings -To allow Unreal to replicate through the SpatialOS network stack and combine multiple dedicated server instances across one seamless game world, the GDK has to adapt Unreal networking functionality to work with the [SpatialOS Worker API](https://docs.improbable.io/reference/latest/capi/introduction). It does this seamlessly using `Dynamic Typebindings`. - -`Dynamic Typebindings` operate at runtime so that your development iteration speed is not affected, despite your network code running on a completely different representation to Unreal’s. - -At the heart of `Dynamic Typebindings` is GDK-generated [SpatialOS schema]({{urlRoot}}/content/spatialos-concepts/schema), which is the SpatialOS representation of any Unreal object, its replicated data, and RPCs. `Dynamic Typebindings` also include the binding code that is invoked when the GDK converts network-relevant data between native Unreal and SpatialOS. - -# Schema -You generate the [schema]({{urlRoot}}/content/spatialos-concepts/schema) used in `Dynamic Typebindings` via the *Schema** button in the GDK toolbar. When you select **Schema**, the GDK generates schema for all classes tagged with the [Spatial Type]({{urlRoot}}/content/spatial-type) specifier. For each Unreal object that the GDK generates schema for, there are a number of possible schema components generated, each serving a different function. - -For Unreal Actors and sub-objects: - -* Replicated property schema component (for example, `_MyActor_`): Contains all the replicated properties (including inherited) present on the object, except those tagged with the `COND_OwnerOnly` or `COND_AutonomousOnly` replication condition. -* Owner-only schema component (for example, `_MyActorOwnerOnly_`): Contains all the `COND_OwnerOnly` or `COND_AutonomousOnly` replicated properties excluded from the replicated property schema component. -* Handover schema component (for example, `_MyActorHandover_`): Contains all handover properties (including inherited) present on the object. - -In addition, Unreal Actors also generate (where relevant): - -* Client/Server/CrossServer RPC schema components (for example, `_MyActorClientRPCS_`)- Each RPC category has its own SpatialOS component containing all the Actor’s RPCs for that category converted into SpatialOS commands. -* NetMulticast RPC schema component (for example, `_MyActorNetMulticastRPCS_`)- Contains all the multicast RPCs callable on this Actor, converted into SpatialOS [events](https://docs.improbable.io/reference/latest/shared/glossary#event). -* Static sub-object schema components - For each static sub-object present on this Actor, additional schema components are generated which wrap the replicated and handover properties defined in the components above. - -> Note: The GDK doesn't currently support dynamic components. - -When comparing the two network stacks, it’s useful to keep the following mappings between Unreal terms and SpaitalOS terms in mind: - -* Unreal Actor <-> SpatialOS entity -* Unreal Replicating Property <-> SpatialOS field -* Unreal Client/Server RPC <-> SpatialOS command -* Unreal NetMulticast RPC <-> SpatialOS event -* Unreal Replication Condition <-> SpatialOS component design diff --git a/SpatialGDK/Documentation/content/get-started/build-unreal-fork.md b/SpatialGDK/Documentation/content/get-started/build-unreal-fork.md deleted file mode 100644 index 55ac4bf3f8..0000000000 --- a/SpatialGDK/Documentation/content/get-started/build-unreal-fork.md +++ /dev/null @@ -1,73 +0,0 @@ -<%(TOC)%> -# Get started: 2 - Get and build the SpatialOS Unreal Engine fork - -To use the SpatialOS GDK for Unreal, you first need to build the SpatialOS fork of Unreal Engine. - -### Step 1: Unreal Engine EULA - -To get access to our fork, you need to link your GitHub account to a verified Epic Games account, agree to the Unreal Engine End User License Agreement ([EULA](https://www.unrealengine.com/en-US/eula)) and accept the invite to join the [EpicGames organisation on Github](https://github.com/EpicGames). You cannot use the GDK without doing this first. To do this, see the [Unreal Engine documentation](https://www.unrealengine.com/en-US/ue4-on-github). - -### Step 2: Get the Unreal Engine fork source code and Unreal Linux cross-platform support - -1. Open a terminal and run either of these commands to clone the [Unreal Engine fork](https://github.com/improbableio/UnrealEngine) repository. -
(You may get a 404 from this link. See the instructions above, under _Unreal Engine EULA_, on how to get access to this repository.) - - > **TIP:** Clone the Unreal Engine fork into your root directory to avoid file path length errors. For example: `C:\GitHub\UnrealEngine`. - - | | | - | --- | --- | - | HTTPS | `git clone https://github.com/improbableio/UnrealEngine.git` | - | SSH |`git clone git@github.com:improbableio/UnrealEngine.git` - -2. To build Unreal server-workers for SpatialOS deployments you need to build the Unreal Engine fork targeting Linux. This requires cross-compilation of your SpatialOS project and the Unreal Engine fork. - - For guidance on this, see the _Getting the toolchain_ section of Unreal's [Compiling for Linux](https://wiki.unrealengine.com/Compiling_For_Linux) documentation. As you follow the guidance there, select **v11 clang 5.0.0-based** to download the `v11_clang-5.0.0-centos7.zip` archive, then unzip this file into a suitable directory. - - - -### Step 3: Add environment variables - -You need to add two [environment variables](https://docs.microsoft.com/en-us/windows/desktop/procthread/environment-variables): one to set the path to the Unreal Engine fork directory, and another one to set the path to the Linux cross-platform support directory. - -1. Go to **Control Panel > System and Security > System > Advanced system settings > Advanced > Environment variables**. -2. Create a system variable named **UNREAL_HOME**. -3. Set the variable value to the path to the directory you cloned the Unreal Engine fork into. -4. Restart your terminal and run `echo %UNREAL_HOME%` (Command Prompt) or `echo$Env:UNREAL_HOME` (PowerShell). If you have registered the environment variable correctly, this returns the path to the directory you cloned the Unreal Engine fork into. If it doesn’t, check that you’ve set the environment variable correctly. -5. Create a system variable named **LINUX_MULTIARCH_ROOT**. -6. Set the variable value to the path to the directory of your unzipped Linux cross compilation toolchain. -7. Restart your terminal and run `echo %LINUX_MULTIARCH_ROOT%` (Command Prompt) or `echo $Env:LINUX_MULTIARCH_ROOT` (PowerShell). If you have registered the environment variable correctly, this returns the path you unzipped `v11_clang-5.0.0-centos7.zip` into. If it doesn’t, check that you’ve set the environment variable correctly. - -### Step 4: Build Unreal Engine - -1. Open **File Explorer** and navigate to the directory you cloned the Unreal Engine fork into. - -1. Double-click **`Setup.bat`**. -This installs prerequisites for building Unreal Engine 4.
-This process can take a long time to complete. - - > While running the Setup file, you should see `Checking dependencies (excluding Mac, Android)...`. If it also says `excluding Linux`, make sure that you set the environment variable `LINUX_MULTIARCH_ROOT` correctly, and run the Setup file again. - -1. In the same directory, double-click **`GenerateProjectFiles.bat`**. This file automatically sets up the project files you require to build Unreal Engine 4. - - > If you encounter an `error MSB4036: The "GetReferenceNearestTargetFrameworkTask" task was not found` when building with Visual Studio 2017, check that you have NuGet Package Manager installed via the Visual Studio installer. - -1. In the same directory, open **UE4.sln** in Visual Studio. - -1. In Visual Studio, on the toolbar, go to **Build** > **Configuration Manager** and set your active solution configuration to **Development Editor** and your active solution platform to **Win64**. - -1. In the Solution Explorer window, right-click on the **UE4** project and select **Build** (you may be prompted to install some dependencies first).
- -Visual Studio then builds Unreal Engine, which can take up to a couple of hours. - -You have now built Unreal Engine 4 with cross-compilation for Linux. - -> Once you've built Unreal Engine, *don't move it into another directory*. That will break the integration. - -#### Next: [Set up the SpatialOS GDK Starter Template]({{urlRoot}}/content/get-started/gdk-template) - -
- ------- -_2019-03-27 Page updated with limited editorial review_ \ No newline at end of file diff --git a/SpatialGDK/Documentation/content/get-started/dependencies.md b/SpatialGDK/Documentation/content/get-started/dependencies.md deleted file mode 100644 index bf1f179363..0000000000 --- a/SpatialGDK/Documentation/content/get-started/dependencies.md +++ /dev/null @@ -1,56 +0,0 @@ -<%(TOC)%> -# Get started: 1 - Get the dependencies - -To start using the GDK for Unreal, you need to ensure you have the correct software installed and that your machine is capable of running Unreal Engine. - -## Sign up for a SpatialOS account, or make sure you are logged in - -If you have already signed up, make sure you are logged into [Improbable.io](https://improbable.io). If you are logged in, you should see your picture in the top right of this page. If you are not logged in, select __Sign in__ at the top of this page and follow the instructions. - -If you have not signed up before, you can sign up [here](). - -## Set up your machine - -### Step 1: Hardware - -- Ensure your machine meets the minimum hardware requirements for Unreal Engine. - -Refer to the Unreal Engine hardware recommendations for further information about the minimum hardware requirements. - -- Recommended storage: 60GB+ available space - -### Step 2: Network settings - -To configure your network to work with SpatialOS, refer to the [SpatialOS network settings](https://docs.improbable.io/reference/latest/shared/setup/requirements#network-settings). - -### Step 3: Software - -To build the GDK for Unreal you need the following software installed on your machine: - -- **Windows 10,** with Command Prompt or PowerShell. - - - The GDK for Unreal is only supported on Windows 10. - -- **Git for Windows** - - - You need Git for windows to clone the GDK and Unreal Engine GitHub repositories. - -- **SpatialOS** - - This installs the [SpatialOS CLI]({{urlRoot}}/content/glossary#spatialos-command-line-tool-cli), the [SpatialOS Launcher]({{urlRoot}}/content/glossary#launcher), and 32-bit and 64-bit Visual C++ Redistributables. - -- The [**DirectX End-User Runtimes (June 2010)**](https://www.microsoft.com/en-us/download/details.aspx?id=8109) - - - You need the DirectX End-User Runtime to run Unreal Engine 4 clients. - -- **Visual Studio** 2015 or 2017 (we recommend 2017). During the installation, select the following items in the Workloads tab: - - **Universal Windows Platform development**
- - **.NET desktop development**
- - **Desktop development with C++**
- - **Game development with C++**, including the optional **Unreal Engine installer** component. - -#### Next: [Get and build the GDK’s Unreal Engine Fork]({{urlRoot}}/content/get-started/build-unreal-fork.md) - -
- ------- -_2019-04-16 Page updated with limited editorial review_ diff --git a/SpatialGDK/Documentation/content/get-started/gdk-template.md b/SpatialGDK/Documentation/content/get-started/gdk-template.md deleted file mode 100644 index c6b2da05aa..0000000000 --- a/SpatialGDK/Documentation/content/get-started/gdk-template.md +++ /dev/null @@ -1,221 +0,0 @@ -<%(TOC)%> -# Get started: 3 - Set up the SpatialOS GDK Starter Template - -Before setting up the SpatialOS GDK Starter Template, you need to have followed: - -* [Getting started: 1 - Dependencies]({{urlRoot}}/content/get-started/dependencies) -* [Getting started: 2 - Get and build the SpatialOS Unreal Engine Fork]({{urlRoot}}/content/get-started/build-unreal-fork). - -If you are ready to start developing your own game with the GDK, follow the steps below. - -### Terms used on this page - -* `` - 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`). - -### Create a new project using the Starter Template - -After [building the Unreal Engine fork]({{urlRoot}}/content/get-started/build-unreal-fork), in **File Explorer**, navigate to `UnrealEngine\Engine\Binaries\Win64`and double-click `UE4Editor.exe` to open the Unreal Editor. - -1. In the [Project Browser](https://docs.unrealengine.com/en-us/Engine/Basics/Projects/Browser) window, select the **New Project** tab and then the **C++ tab**. -2. In this tab, select **SpatialOS GDK Starter**. -3. In the **Folder** field, choose a suitable directory for your project. -4. In the **Name** field, enter a project name of your choice. -5. Select **Create Project**. - -**Note:** When you create a project, the Unreal Engine automatically creates a directory named after the project name you entered. This page uses `` as an example project name. - -![The Unreal Engine Project Browser]({{assetRoot}}assets/set-up-template/template-project-browser.png) - -*Image: The Unreal Engine Project Browser, with the project file path and project name highlighted.* - -After you have selected **Create Project**, the Unreal Engine generates the necessary project files and directories, it then closes the Editor and automatically opens Visual Studio. - -After Visual Studio has opened, save your solution then close Visual Studio before proceeding to the Clone the GDK step. - -### Clone the GDK - -Now you need to clone the SpatialOS GDK for Unreal into your project. To do this: - -1. In **File Explorer**, navigate to the `` directory and create a `Plugins` folder in this directory. -2. In a Git Bash terminal window, navigate to `\Plugins` and clone the [GDK for Unreal](https://github.com/spatialos/UnrealGDK) repository by running either: - * (HTTPS) `git clone https://github.com/spatialos/UnrealGDK.git` - * (SSH) `git clone git@github.com:spatialos/UnrealGDK.git` - -The GDK's [default branch (GitHub documentation)](https://help.github.com/en/articles/setting-the-default-branch) is `release`. This means that, at any point during the development of your game, you can get the latest release of the GDK by running `git pull` inside the `UnrealGDK` directory. When you pull the latest changes, you must also run `git pull` inside the `UnrealEngine` directory, so that your GDK and your Unreal Engine fork remain in sync. - -**Note:** You need to ensure that the root directory of the GDK for Unreal repository is called `UnrealGDK` so the file path is: `\Plugins\UnrealGDK\...` - -### Build the dependencies - -To use the Starter Template, you must build the GDK for Unreal module dependencies and then add the GDK to your project. To do this: - -1. Open **File Explorer**, navigate to the root directory of the GDK for Unreal repository (`\Plugins\UnrealGDK\...`), and double-click **`Setup.bat`**. If you haven't already signed into your SpatialOS account, the SpatialOS developer website may prompt you to sign in. -1. In **File Explorer**, navigate to your `` directory, right-click ``.uproject and select Generate Visual Studio Project files. -1. In the same directory, double-click **``.sln** to open it with Visual Studio. -1. In the Solution Explorer window, right-click on **``** and select **Build**. -1. When Visual Studio has finished building your project, right-click on **``** and select **Set as StartUp Project**. -1. Press F5 on your keyboard or select **Local Windows Debugger** in the Visual Studio toolbar to open your project in the Unreal Editor.
-![Visual Studio toolbar]({{assetRoot}}assets/set-up-template/template-vs-toolbar.png)
-_Image: The Visual Studio toolbar_ - -Note: Ensure that your Visual Studio Solution Configuration is set to **Development Editor**. - -### Deploy your project - -To test your game, you need to launch a deployment. This means launching your game with its own instance of the [SpatialOS Runtime](https://docs.improbable.io/reference/latest/shared/glossary#spatialos-runtime), either locally using a [local deployment](https://docs.improbable.io/reference/latest/shared/glossary#local-deployment), or in the cloud using a [cloud deployment](https://docs.improbable.io/reference/latest/shared/glossary#cloud-deployment). - - - -When you launch a deployment, SpatialOS sets up the world based on a [snapshot]({{urlRoot}}/content/spatialos-concepts/generating-a-snapshot), and then starts up the worker instances needed to run the game world. - - - -You'll find out more about schema, snapshots and workers later on in this tutorial. - -#### Deploy locally with multiple clients - -Before you launch a deployment (local or cloud) you must generate [schema]({{urlRoot}}/content/spatialos-concepts/schema) and a [snapshot]({{urlRoot}}/content/spatialos-concepts/generating-a-snapshot). - -1. In the Unreal Editor, on the GDK toolbar, select **Schema** to generate schema.
-![Toolbar]({{assetRoot}}assets/screen-grabs/toolbar/schema-button.png)
-_Image: On the GDK toolbar in the Unreal Editor select **Schema**_
-1. Select **Snapshot** to generate a snapshot.
-![Toolbar]({{assetRoot}}assets/screen-grabs/toolbar/snapshot-button.png)
-_Image: On the GDK toolbar in the Unreal Editor select **Snapshot**_
- -<%(#Expandable title="What is schema?")%>Schema is a set of definitions which represent your game's objects in SpatialOS. Schema is defined in `.schema` files and written in schemalang. When you use the GDK, the schema files and their contents are generated automatically so you do not have to write or edit schema files manually. - -You can find out more about schema in the [GDK schema documentation]({{urlRoot}}/content/spatialos-concepts/schema). -<%(/Expandable)%> - - - -<%(#Expandable title="What is a snapshot?")%>A snapshot is a representation of the state of a SpatialOS world at a given point in time. A snapshot stores the current state of each entity's component data. You start each deployment with a snapshot; if it's a re-deployment of an existing game, you can use the snapshot you originally started your deployment with, or use a snapshot that contains the exact state of a deployment before you stopped it. - -You can find out more about snapshots in the [GDK snapshot documentation]({{urlRoot}}/content/spatialos-concepts/generating-a-snapshot). -<%(/Expandable)%> - -To launch a local deployment: - -1. Select **Start**. This opens a terminal window and starts a local SpatialOS deployment. Wait until you see the output `SpatialOS ready. Access the inspector at http://localhost:21000/inspector` in your terminal window.
-![Toolbar]({{assetRoot}}assets/screen-grabs/toolbar/start-button.png)
-_Image: On the GDK toolbar in the Unreal Editor select **Start**_
-1. On the Unreal Editor toolbar, open the **Play** drop-down menu. -1. Under **Modes**, select **New Editor Window (PIE)**.
-1. Under **Multiplayer Options**, set the number of players to **2** and ensure that the check box next to **Run Dedicated Server** is checked. (If it is unchecked, select the checkbox to enable it.)
-![]({{assetRoot}}assets/set-up-template/template-multiplayer-options.png)
-_Image: The Unreal Engine **Play** drop-down menu, with **Multiplayer Options** and **New Editor Window (PIE)** highlighted_
-1. On the Unreal Engine toolbar, select **Play** to run the game, and you should see two clients start.

-![]({{assetRoot}}assets/set-up-template/template-two-clients.png)
-_Image: Two clients running in the Editor, with player Actors replicated by SpatialOS and the GDK_
-1. Open the Inspector using the local URL you were given above: `http://localhost:21000/inspector`.
You should see that a local SpatialOS deployment is running with one server-worker instance and two client-worker instances connected. You can also find and follow around the two player entities.

-![]({{assetRoot}}assets/set-up-template/template-two-client-inspector.png)
-_Image: The Inspector showing the state of your local deployment_
-1. When you're done, select **Stop** in the GDK toolbar to stop your local SpatialOS deployment.
![Toolbar]({{assetRoot}}assets/screen-grabs/toolbar/stop-button.png)
-_Image: On the GDK toolbar in the Unreal Editor select **Stop**_
-<%(#Expandable title="What is the Inspector?")%> The Inspector is a browser-based tool that you use to explore the internal state of a game's SpatialOS world. It gives you a real-time view of what’s happening in a local or cloud deployment.
-The Inspector we are using here is looking at a local deployment running on your computer and not in the cloud, so we use a local URL for the Inspector as it's also running locally on your computer. When running locally, the Inspector automatically downloads and caches the latest Inspector client from the internet. When you use the Inspector in a cloud deployment, you access the Inspector through the Console via the web at https://console.improbable.io. -<%(/Expandable)%> - -If you want to run multiple server-workers in the Editor, see the [Toolbar documentation]({{urlRoot}}/content/toolbars#auto-generated-launch-config-for-pie-server-worker-types) for details on launching multiple PIE server-workers. - -#### Deploy in the cloud - -To launch a cloud deployment, you need to prepare your server-worker and client-worker [assemblies](https://docs.improbable.io/reference/latest/shared/glossary), and upload them to the cloud. - -> **TIP:** Building the assemblies can take a while - we recommend installing IncrediBuild to speed up build times. - -##### Step 1: Set up your SpatialOS project name. -When you signed up for SpatialOS, your account was automatically associated with an organisation and a project, both of which have the same generated name. - -1. Find this name by going to the [Console](https://console.improbable.io). -The name should look something like `beta_randomword_anotherword_randomnumber`. In the example below, it’s `beta_yankee_hawaii_621`.
![Toolbar]({{assetRoot}}assets/set-up-template/template-project-page.png)
_Image: The SpatialOS Console with a project name highlighted._ -2. In File Explorer, navigate to the `/spatial` directory and open the `spatialos.json` file in a text editor of your choice. -3. Replace the `name` field with the project name shown in the Console. This tells SpatialOS which SpatialOS project you intend to upload to. -<%(#Expandable title="What is the Console?")%> The Console is a web-based tool for managing cloud deployments. You gives you access to information about your games' SpatialOS project names, the SpatialOS assemblies you have uploaded, the internal state of any games you have running (via the Inspector), as well as logs and metrics.
-You can find out more about the [Console]({{urlRoot}}/content/glossary#console) in the _Glossary_. <%(/Expandable)%> -##### Step 2: Build your worker assemblies - -Workers are the programs that compute a SpatialOS world. In general, server-worker instances simulate the world, while game players connect to the world through client-worker instances. Worker assemblies are `.zip` files that contain built-out workers; that is all the files with compiled code that your game uses for running in the cloud. - - - - - -**Note:** In the following commands, you must replace **`YourProject`** with the name of your Unreal project (the one you chose when you created the project from the Unreal Editor - not the name of your SpatialOS project). - -1. In a terminal window, navigate to your `` directory. -2. Build a server-worker assembly by running the following command: -``` -Game\Plugins\UnrealGDK\SpatialGDK\Build\Scripts\BuildWorker.bat YourProject Linux Development YourProject.uproject -``` -3. Build a client-worker assembly by running the following command: -``` -Game\Plugins\UnrealGDK\SpatialGDK\Build\Scripts\BuildWorker.bat YourProject Win64 Development YourProject.uproject -``` - -Alternatively you can use the `BuildProject.bat` script found in the `` directory to run both of these commands automatically. - -### Upload your game - -1. In a terminal window, navigate to your `\spatial\` directory and run the following command: -`spatial cloud upload `
Where `` is a name of your choice (for example `myassembly`). - -A valid upload command looks like this: - -``` -spatial cloud upload myassembly -``` - -### Launch a cloud deployment - -The next step is to launch a cloud deployment using the assembly that you just uploaded. You can only do this through the SpatialOS command-line tool (also known as the [SpatialOS CLI]({{urlRoot}}/content/glossary#spatialos-command-line-tool-cli)). - -When launching a cloud deployment you must provide three parameters: - -* **the assembly name**, which identifies the worker assemblies to use. -* **a launch configuration**, which declares the world and load balancing configuration. -* **a name for your deployment**, which labels the deployment in the [Console](https://console.improbable.io). - -1. In a terminal window, navigate to `\spatial\` and run: `spatial cloud launch --snapshot=snapshots/default.snapshot one_worker_test.json ` -
where `assembly_name` is the name you gave the assembly in the previous step and `deployment_name` is a name of your choice. A valid launch command would look like this: - -``` -spatial cloud launch --snapshot=snapshots/default.snapshot myassembly one_worker_test.json mydeployment -``` - -**Note:** This command defaults to deploying to clusters located in the US. If you’re in Europe, add the `--cluster_region=eu` flag for lower latency. - -### Play your game - -![]({{assetRoot}}assets/tutorial/console.png) -_Image: The SpatialOS Console_ - -When your deployment has launched, SpatialOS automatically opens the [Console](https://console.improbable.io) in your browser. - -In the Console, Select **Launch** on the left of the page, and then select the **Launch** button that appears in the centre of the page to open the SpatialOS Launcher. The Launcher automatically downloads the game client for this deployment and runs it on your local machine.
-![]({{assetRoot}}assets/tutorial/launch.png)
-_Image: The SpatialOS console launch window_ - -**Note:** You install the SpatialOS Launcher during [Getting started: 1 - Dependencies]({{urlRoot}}/content/get-started/dependencies). - -### Congratulations! - -You've successfully set up and launched the Starter Template and the GDK! You are now ready to start developing a game with SpatialOS. - -If you have an existing Unreal multiplayer project, follow our detailed [porting guide]({{urlRoot}}/content/tutorials/tutorial-porting-guide) to get it onto the GDK. - -
-
-------------- -2019-04-02 Page updated with limited editorial review diff --git a/SpatialGDK/Documentation/content/get-started/introduction.md b/SpatialGDK/Documentation/content/get-started/introduction.md deleted file mode 100644 index 2d2ec17d04..0000000000 --- a/SpatialGDK/Documentation/content/get-started/introduction.md +++ /dev/null @@ -1,32 +0,0 @@ -<%(TOC)%> -# Get started: Introduction - -Get started with the SpatialOS GDK for Unreal by building the SpatialOS Unreal Engine Fork and setting up the GDK Starter Template which you can use as a base for your own project running on SpatialOS. - -After this you can either: - -* Follow the Multiserver Shooter Tutorial, which shows you how to deploy a project to the cloud and demonstrates a simple shooter game running across two servers. -* Or, if you have an existing Unreal multiplayer project, you can follow the porting guide to get your game running on the SpatialOS GDK. - -## Your feedback and ideas - -We'd love to hear from you - drop into our forums or discord to give us feedback on your getting started experience, our documentation, our [development roadmap](https://github.com/spatialos/UnrealGDK/projects/1), or anything else! - -**Discord**
-Find us on the [SpatialOS Discord](https://discord.gg/vAT7RSU) in the [**#unreal** channel](https://discordapp.com/channels/311273633307951114/339471548647866368). -You can get Discord [here](https://discordapp.com/). - -**The SpatialOS forums**
-Visit the **feedback** section in our [forums](https://forums.improbable.io/) and use the **unreal-gdk** tag. [This link](https://forums.improbable.io/new-topic?category=Feedback&tags=unreal-gdk) takes you there and pre-fills the category and tag. - -**GitHub issues**
-Create an issue in [this repository](https://github.com/spatialos/UnrealGDK/issues). - -#### Next: [Get the dependencies]({{urlRoot}}/content/get-started/dependencies.md) - -<%(Callout type="warn" message="This is an [alpha](https://docs.improbable.io/reference/latest/shared/release-policy#maturity-stages) version of the SpatialOS GDK for Unreal, pending stability, performance and documentation improvements. The API may change as we learn from feedback.")%> - -
- ------- -_2019-03-25 Page updated with editorial review_ diff --git a/SpatialGDK/Documentation/content/glossary.md b/SpatialGDK/Documentation/content/glossary.md deleted file mode 100644 index 256da38040..0000000000 --- a/SpatialGDK/Documentation/content/glossary.md +++ /dev/null @@ -1,448 +0,0 @@ -<%(TOC)%> - -# Glossary -This glossary covers: - -* Terms used in this documentation -* Terms relevant to the GDK for Unreal -* SpatialOS terms used in the GDK for Unreal documentation - -**SpatialOS documentation**
-Many SpatialOS term definitions link to further information in the [SpatialOS documentation](https://docs.improbable.io/reference/latest/index). This documentation covers the SpatialOS Worker SDK and Platform SDK as well some GDK-relevant tools; the [Console](#console), [schema](#schema), [snapshots](#snapshot), and [configuration files](#configuration-files) in particular. - -Note that this SpatialOS documentation assumes you are developing a SpatialOS game using the [Worker SDK and Platform SDK](https://docs.improbable.io/reference/latest/shared/get-started/working-with-spatialos), so it may reference content relevant to that workflow only. While some of the concepts underpin the GDK for Unreal, the two workflows are not always the same. - -## GDK for Unreal documentation terms -* `` - The folder containing your project's `.uproject` and source folder. -* `` - The folder containing your ``. -* `` - Name of your project's `.uproject` (for example, `\\TP_SpatialGDK.uproject`). - -## GDK for Unreal terms - -### Actor handover -Actor handover (`handover`) is a new `UPROPERTY` tag. It allows games built in Unreal (which uses single-server architecture) to take advantage of SpatialOS’ distributed, persistent server architecture. See [Actor property handover between server-workers]({{urlRoot}}/content/handover-between-server-workers.md). - -### Dynamic Typebindings -To enable the network stacks of Unreal and SpatialOS to interoperate, we've implemented [Dynamic Typebindings]({{urlRoot}}/content/dynamic-typebindings.md). `Dynamic Typebindings` operate at runtime so your that your iteration speed is not affected despite your network code running on a completely different represenetations than Unreal's. - -### Cross-server RPCs -These handle the scenario where a [server-worker](#workers) needs to execute an operation on an Actor that another server-worker has [authority](#authority) over. When a cross-server RPC is invoked by a non-authoritative server-worker, the execution is routed through SpatialOS to the authoritative server-worker - this authoritative server-worker executes the RPC. (See the documentation on [Cross-server RPCs]({{urlRoot}}/content/cross-server-rpcs)). - -### Global State Manager -The Global State Manager (GSM): - -* Makes sure that [Singleton Actors](#singleton-actor) are replicated properly, by only allowing the [server-worker](#workers) with [authority](#authority) over the GSM to execute the initial replication of these Actors. See documentation on [Singleton Actors]({{urlRoot}}/content/singleton-actors.md). -* Maintains the configuration of a [deployment’s](#deployment) currently-loaded [game world](#game-world). (Note that this is the Unreal game world not the [SpatialOS world](#spatialos-world).)
- -The GSM lists both the URL of the [Map (or Level - see Unreal documentation)](http://api.unrealengine.com/INT/Shared/Glossary/index.html#l) that the [server-workers](#workers) have loaded and the `AcceptingPlayers` flag. (This flag controls whether or not client-servers can spawn anything in the game world.) - ->Related: -> -> [Server travel]({{urlRoot}}/content/map-travel.md) - -### GSM -Short for [Global State Manager](#global-state-manager). - -### SchemaDatabase - -The SchemaDatabase is a `uasset` file (named `schemadatabase.uasset`) that contains information about UObjects and associated [schema](https://docs.improbable.io/reference/13.6/shared/concepts/schema#schema) in your project. Information is automatically added to the SchemaDatabase by the GDK whenever you generate schema. It is an auto-generated file which you cannot manually edit. - -### Schema generation -A SpatialOS GDK for Unreal toolbar command (within the Unreal Editor) which takes a set of Unreal classes and generates SpatialOS [schema](#schema) that enables automatic communication between Unreal and SpatialOS. - ->Related: ->[SpatialOS GDK for Unreal toolbar]({{urlRoot}}/content/toolbars#spatialos-gdk-for-unreal-toolbar) - -### Singleton Actor -A server-side authoritative Actor that is restricted to one instantiation on SpatialOS. See documentation on [Singleton Actors]({{urlRoot}}/content/singleton-actors.md). - -### Spatial Type -Spatial Type (`SpatialType`) is a SpatialOS-specific [class specifier (Unreal documentation)](https://docs.unrealengine.com/en-US/Programming/UnrealArchitecture/Reference/Classes/Specifiers) which is used to expose network-relevant class information to SpatialOS. There are different categories of Spatial Type, depending on the Actor’s function in your game. - -See the documentation on [Spatial Type]({{urlRoot}}/content/spatial-type). - -### SpatialType -See [Spatial Type](#spatial-type). - -## SpatialOS terms -Below is a subset of SpatialOS terms most relevant to the GDK for Unreal. See the [SpatialOS documentation glossary](https://docs.improbable.io/reference/latest/shared/glossary) for a full list of terms specific to SpatialOS. - -Note that this SpatialOS documentation glossary assumes you are developing a SpatialOS game using the [Worker SDK and Platform SDK](https://docs.improbable.io/reference/latest/shared/get-started/working-with-spatialos), so it may reference content relevant to that workflow only. While some of the concepts underpin the GDK for Unreal, the two workflows are not always the same. - -### Access control list (ACL) -In order to read from a [component](#spatialos-component), or make changes to a component, [workers](#workers) need to have [access](#authority), which they get through an access control list. -Access control lists are a component in the standard schema library: `EntityAcl`. Every [entity](#spatialos-entity) needs to have one. The ACL determines: - -* which types of workers have read access to an entity -* for each component on the entity, which types of workers can have write access - -### Assembly -An assembly is what’s created when you build your project. It contains all the files that your game uses. -This includes executable files for the [client-workers](#workers) and [server-workers](#workers), and the assets these workers use (for example, textures used by a client to visualize the game). -When you run a [cloud deployment](#deployment), you have to specify an assembly to use. - -### Authority -Also known as “write access”. - -* Write access:
-Many [workers](#workers) can connect to a [SpatialOS world](#spatialos-world). For each [component](#spatialos-component) on a [SpatialOS entity](#spatialos-entity), there can be no more than one worker with write access to it. This worker is the only one able to modify the component’s state and handle commands for it. -Workers with write access are said to “have authority” and be “authoritative”; workers without write access are said to be “non-authoritative”. -Which [types of workers](#worker-types) can have write access is governed by each entity’s [access control list (ACL)](#access-control-list-acl). A write ACL is specified per component. The write authority is managed by SpatialOS, and can change regularly due to [load balancing (SpatialOS concept documentation)](https://docs.improbable.io/reference/latest/shared/glossary#load-balancing). -
-* Read access:
-Read access allows [workers](#workers) to know the state of a [SpatialOS component](#spatialos-component). The [access control list (ACL)](#access-control-list-acl) also controls which workers have read-access to a [SpatialOS entity](#spatialos-entity). Read access does not allow a worker to change a component. Read access is at the entity level; if a worker can read from an entity, it is allowed to read from all components on that entity. - -### Check out -Each individual [worker](#workers) checks out only part of the [SpatialOS world](#spatialos-world). This happens on a [chunk](#chunk)-by-chunk basis. A worker “checking out a chunk” means that: - -* the worker has a local representation of every [entity](#spatialos-entity) in that chunk. -* the SpatialOS Runtime sends updates about those entities to the worker. - -A worker checks out all chunks that it is [interested in](#interest). - -### Chunk -A [world](#spatialos-world) is split up into chunks: the grid squares of the world. A chunk is the smallest area of space the world can be subdivided into. Every [entity](#spatialos-entity) is in exactly one chunk. -You set the size of chunks for your world in [launch configuration files](https://docs.improbable.io/reference/latest/shared/reference/file-formats/launch-config). - -### Component -See [SpatialOS component](#spatialos-component). - -### Command-line tool (CLI) -See [Spatial command-line tool (CLI)](#spatialos-command-line-tool-cli). - -### Configuration files -The configuration files contain information on how elements of your project must work. There are four configuration files: - -* The [launch configuration file - `*.json`](#launch-configuration-file) contains the information that the “launch a deployment” commands use to use to run a [deployment](#deployment). -* The [worker configuration file - `*.worker.json`](#worker-configuration-file) tells SpatialOS how to build, launch, and interact with [workers](#workers). -* The [project definition file - `spatialos.json`](#project-definition-file) -* The [worker packages file - `spatialos_worker_packages.json`](https://docs.improbable.io/reference/latest/shared/reference/file-formats/spatial-worker-packages) - -### Console - -The [Console](https://console.improbable.io/) is the main landing page for managing [cloud deployments](https://docs.improbable.io/reference/latest/shared/glossary#cloud-deployment). It shows you: - -* Your [project name](#project-name) -* Your past and present [cloud deployments](https://docs.improbable.io/reference/latest/shared/glossary#cloud-deployment) -* All of the [SpatialOS assemblies](#assembly) you’ve uploaded -* Links to the [Inspector](#inspector), [Launcher](#launcher), and the logs and metrics page for your deployments. - -> Related: -> -> * [Logs](https://docs.improbable.io/reference/latest/shared/operate/logs#cloud-deployments) -> * [Metrics](https://docs.improbable.io/reference/latest/shared/operate/metrics) - -### Deployment -When you want to try out your game, you need to deploy it. This means launching SpatialOS itself. SpatialOS sets up the [world](#spatialos-world) based on a [snapshot](#snapshot), then starts up the [server-workers](#workers) needed to run the world. - -There are two types of deployment: local and cloud. - -Local deployments allow you to start the SpatialOS [Runtime](#spatialos-runtime) locally to test changes quickly. Find out more about local deployments in the [SpatialOS documentation](https://docs.improbable.io/reference/latest/shared/deploy/deploy-local). - -As their name suggests, cloud deployments run in the cloud on [nodes](#node). They allow you to share your game with other people and run your game at a scale not possible on one local machine. Once a cloud deployment is running, you can connect clients to it using the [Launcher](#launcher). - -### Entity -See [SpatialOS entity](#spatialos-entity). - -### Game world - ->Not to be confused with [SpatialOS world](#spatialos-world). - -Everything in your Unreal game that a player can see or interact with. - -### Inspector -The Inspector is a web-based tool that you use to explore the internal state of a [SpatialOS world](#spatialos-world). It gives you a real-time view of what’s happening in a [local or cloud deployment](#deployment). Among other things, it displays: - -* which [workers](#workers) are connected to the deployment. -* how much [load](https://docs.improbable.io/reference/latest/shared/glossary#load-balancing) the workers are under. -* which [SpatialOS entities](#spatialos-entity) are in the SpatialOS world. -* what their [SpatialOS components](#spatialos-component)’ [properties](https://docs.improbable.io/reference/latest/shared/glossary#property) are. -* which workers are authoritative over each SpatialOS component. - ->Related: -> ->[The Inspector](https://docs.improbable.io/reference/latest/shared/operate/inspector) - -### Interest -There are two types of interest: entity interest and component interest. - -#### Entity interest -A [worker](#workers) is interested in all [chunks](#chunk) that contain [entities](#spatialos-entity) it has -[write access](#authority) to a [component](#spatialos-component) on. It's *also* interested in chunks within a configurable -radius of those entities: this makes sure that workers are aware of entities nearby. You can set this radius -in the [worker configuration file](#worker-configuration-file). -If a worker is interested in a chunk, it will [check out](#check-out) all the entities in that chunk. - -#### Component interest -Each [worker](#workers), in its [worker configuration file](#worker-configuration-file), specifies which [components](#spatialos-component) it is -interested in. SpatialOS only sends updates about components to a worker which is interested in that component. - -> Related: -> -> * [Entity interest settings](https://docs.improbable.io/reference/latest/shared/worker-configuration/bridge-config#entity-interest) -> * [Component delivery settings](https://docs.improbable.io/reference/latest/shared/worker-configuration/bridge-config#component-delivery) - - -### Launch - ->Not to be confused with [the Launcher](#launcher). - -In SpatialOS, “launch” means start a game [deployment](#deployment). See also [launch configuration file](#launch-configuration-file). - -### Launch configuration -The launch configuration is how you set up the start of your game’s [deployment](#deployment) (its launch). It is represented in the launch configuration file. -See [workers](#workers) and [launch configuration file](#launch-configuration-file). - -### Launch configuration file -The [launch configuration file](#launch-configuration-file) is a `.json` file containing the information that the “launch a deployment” commands use to start a [deployment](#deployment). - ->Related: -> ->[Launch configuration file](https://docs.improbable.io/reference/latest/shared/reference/file-formats/launch-config) - -### Launcher -The Launcher is a tool that can download and start clients that connect to [cloud deployments](#deployment). It's available as an application for Windows and macOS. From the [Console](#console), you can use the Launcher to connect a game client to your own cloud deployment or generate a share link so anyone with the link can download a game client and join your game. -The Launcher downloads the client executable from the [SpatialOS assembly](#assembly) you uploaded. - -> Related: -> -> [The Launcher](https://docs.improbable.io/reference/latest/shared/operate/launcher) - -### Load balancing -One of the features of SpatialOS is load balancing: dynamically adjusting how many [components](#spatialos-component) on [entities](#spatialos-entity) in the [world](#spatialos-world) each [worker](#workers) has [write access](#authority) to, so that workers don’t get overloaded. - -Load balancing only applies to [server-workers](#workers). -When an instance of a worker is struggling with a high workload, SpatialOS can start up new instances of the worker, and give them write access to some components on entities. - -This means that an [entity](#spatialos-entity) won’t necessarily stay on the same worker instance, even if that entity doesn’t move. SpatialOS may change which components on which entities a worker instance has write access to: so entities move “from” one worker instance to another. Even though the entity may be staying still, the worker instance’s [area of interest](#interest) is moving. - ->Related pages: -> -> [Configuring load balancing](https://docs.improbable.io/reference/latest/shared/worker-configuration/loadbalancer-config) - -### Network operations - -Also known as "ops". - -Network operations are network messages sent between a worker instance and the SpatialOS Runtime. They carry information about updates to worker instances, entities, entity components, commands, and more. - -For more information, see the SpatialOS documentation on [operations](https://docs.improbable.io/reference/latest/shared/design/operations). - -### Node - ->Not to be confused with [worker](#workers). - -A node refers to a single machine used by a [cloud deployment](#deployment). Its name indicates the role it plays in your deployment. You can see these on the advanced tab of your deployment details in the [Console](#console). - -### Ops - -See [Network operations](#network-operations). - -### Persistence -Most [entities](#spatialos-entity) in your [game world](#game-world) need to keep existing if you stop a game [deployment](#deployment) and start a new one. However, some entities don’t need to keep existing from one deployment to another; you may want per-deployment player abilities and a per-deployment score, for example. - -To facilitate this continuity in an entity's state between deployments, there is a `persistence` component in the standard [schema](#schema) library. It’s optional, but all entities that you want to persist in the world must have this component. Persistence means that entities are saved into [snapshots](#snapshot). - ->Related: -> ->[The persistence component in the standard schema library](https://docs.improbable.io/reference/latest/shared/schema/standard-schema-library#persistence-optional) - -### Project name -Your project name is a unique identifier for your game project as a deployment. It’s generated for you when you sign up for SpatialOS. It’s usually something like `beta_someword_anotherword_000`. -You must specify this name when you run a [cloud deployment](#deployment). -Note that your project name is (usually) not the same as the name of the directory your project is in. - -### Project definition file -This is a `spatialos.json` file which lives in your project's spatial directory. -It lists the SpatialOS [project name](#project-name) assigned to you by Improbable when you sign up as well as the version of [SpatialOS SDK](#spatialos-sdk) your project uses. - ->Related -> ->[Project defnition file - `spatialos.json`](https://docs.improbable.io/reference/latest/shared/reference/file-formats/spatialos-json) - -### Queries -Queries allow [workers](#workers) to get information about the [world](#spatialos-world) outside the region they’re [interested in](#interest). For more information, see [queries](https://docs.improbable.io/reference/latest/shared/glossary#queries). - -> Entity queries are useful if you need to get information about an entity at a particular time. -> If you need regular updates about an entity, use [streaming queries](#streaming-queries) instead. - -### Read access -See [authority](#authority). - -### Schema -The schema is where you define all the [SpatialOS components](#spatialos-component) in your [SpatialOS world](#spatialos-world). - -You define your schema in `.schema` files that are written in [schemalang](https://docs.improbable.io/reference/latest/shared/glossary#schemalang). Schema files are stored in the `schema` folder in the root directory of your SpatialOS project. - -SpatialOS uses the schema to generate code. You can use this generated code in your [workers](#workers) to interact with [SpatialOS entities](#spatialos-entity) in the SpatialOS world. - -> Related: -> -> * [Schema (GDK for Unreal documentation)]({{urlRoot}}/content/spatialos-concepts/schema) -> * [Introduction to schema](https://docs.improbable.io/reference/latest/shared/schema/introduction) -> * [Schema reference](https://docs.improbable.io/reference/latest/shared/schema/reference) - -### SpatialOS command-line tool (CLI) -The SpatialOS command-line tool (also known as the “CLI”) provides a set of commands that you use to interact with a [SpatialOS project](https://docs.improbable.io/reference/latest/shared/reference/project-structure#structure-of-a-spatialos-project). Among other things, you use it to [deploy](#deployment) your game (using [`spatial local launch`](https://docs.improbable.io/reference/latest/shared/spatial-cli/spatial-local-launch) or [`spatial cloud launch`](https://docs.improbable.io/reference/latest/shared/spatial-cli/spatial-cloud-launch)). You can run the CLI commands `spatial build` and `spatial local launch` from the [GDK toolbar]({{urlRoot}}/content/toolbars#spatialos-gdk-for-unreal-toolbar) in the Unreal Editor. - -> Related: -> -> * [An introduction to the SpatialOS command-line tool](https://docs.improbable.io/reference/latest/shared/spatial-cli-introduction). Note that the GDK does not support any `spatial worker` commands. -> * [SpatialOS CLI reference](https://docs.improbable.io/reference/latest/shared/spatialos-cli-introduction) - - -### SpatialOS component -> Not to be confused with [Unreal Actor Components (Unreal documentation](https://docs.unrealengine.com/en-us/Programming/UnrealArchitecture/Actors/Components) - -A [SpatialOS entity](#spatialos-entity) is defined by a set of components. Common components in a game might be things like `Health`, `Position`, or `PlayerControls`. They're the storage mechanism for data about the [world](#spatialos-world) that you want to be shared between [workers](#workers). -Components can contain: - -* [properties](https://docs.improbable.io/reference/latest/shared/glossary#property), which describe persistent values that change over time (for example, a property for a `Health` component could be “the current health value for this entity”.) -* [events](https://docs.improbable.io/reference/latest/shared/glossary#event), which are transient things that can happen to an entity (for example, `StartedWalking`) -* [commands](https://docs.improbable.io/reference/latest/shared/glossary#command) that another worker can call to ask the component to do something, optionally returning a value (for example, `Teleport`) - -A SpatialOS entity can have as many components as you like, but it must have at least [`Position`](https://docs.improbable.io/reference/latest/shared/glossary#position) and [`EntityAcl`](#access-control-list-acl). Most entities will have the [`Metadata`](https://docs.improbable.io/reference/latest/shared/glossary#metadata) component. - -SpatialOS components are defined as files in your [schema](#schema). -[Entity access control lists](#access-control-list-acl) govern which workers can [read from](#read-access) or [write to](#write-access) each component on an entity. - -> Related: -> -> * [Designing components](https://docs.improbable.io/reference/latest/shared/design/design-components) -> * [Component best practices](https://docs.improbable.io/reference/latest/shared/design/component-best-practices) -> * [Introduction to schema](https://docs.improbable.io/reference/latest/shared/schema/introduction) - -### SpatialOS entity -All of the objects inside a [SpatialOS world](#spatialos-world) are SpatialOS entities: they’re the basic building blocks of the world. Examples include players, NPCs, and objects in the world like trees. A SpatialOS entity approximates to an Unreal Actor. - -SpatialOS entities are made up of [SpatialOS components](#spatialos-component), which store data associated with that entity. - -[Workers](#workers) can only see the entities they're [interested in](#interest). - -> Related: -> -> * [SpatialOS concepts: Entities](https://docs.improbable.io/reference/latest/shared/concepts/world-entities-components#entities) -> * [Designing SpatialOS entities](https://docs.improbable.io/reference/latest/shared/design/design-entities) - -### SpatialOS Runtime - ->Not to be confused with the [SpatialOS world](#spatialos-world). - -Also sometimes just called “SpatialOS”. - -A SpatialOS Runtime instance manages the [SpatialOS world](#spatialos-world) of each [deployment](#deployment) by storing all [SpatialOS entities](#spatialos-entity) and the current state of their [SpatialOS components](#spatialos-component). [Workers](#workers) interact with the SpatialOS Runtime to read and modify the components of an entity as well as send messages between each other. - -### SpatialOS SDK -This is a set of low-level tools in several programming languages which you can use to integrate your game project with SpatialOS. It consists of the Worker SDK (or “Worker module”) and Platform SDK (or “Platform module”). -If you are using the GDK for Unreal, you do not need to use the Worker SDK or Platform SDK, however, you can use the Worker SDK to extend or complement the functionality of the GDK for Unreal. - -> Related: -> -> Worker SDK: [Game development tools overview](https://docs.improbable.io/reference/latest/shared/dev-tools-intro) -> [Platform SDK overview](https://docs.improbable.io/reference/latest/platform-sdk/introduction) - -### SpatialOS world - ->Not to be confused with the Unreal [game world](#game-world). - -Also known as "the world". - -The world is a central concept in SpatialOS. It’s the canonical source of truth about your game. All the world's data is stored within [entities](#entity) - specifically, within their [components](#component). -SpatialOS manages the world, keeping track of all the entities and what state they’re in. - -Changes to the world are made by [workers](#workers). Each worker has a view onto the world (the part of the world that they're [interested](#interest) in), and SpatialOS sends them updates when anything changes in that view. - -It's important to recognise this fundamental separation between the SpatialOS world and the view of that world that a worker [checks out](#check-out). Workers send updates to SpatialOS when they want to change the world: they don't control the canonical state of the world; they must use SpatialOS APIs to change it. - -### Snapshot -A snapshot is a representation of the state of a [SpatialOS world](#spatialos-world) at a given point in time. It stores each [persistent](#persistence) [SpatialOS entity](#spatialos-entity) and the values of their [SpatialOS components](#spatialos-component)' [properties](https://docs.improbable.io/reference/latest/shared/glossary#property). - -You use a snapshot as the starting point (using an an “initial snapshot”) for your [SpatialOS world](#spatialos-world) when you [deploy your game](#deployment). - -> Related: -> -> * [How to generate a snapshot (GDK for Unreal documentation)]({{urlRoot}}/content/spatialos-concepts/generating-a-snapshot) -> * [Snapshots](https://docs.improbable.io/reference/latest/shared/operate/snapshots) - -### Streaming queries -Streaming queries allow workers to get information about the [world](#spatialos-world) outside the region they’re [interested in](#interest), so that they know about entities that they don’t have checked out (for example, entities that are far away, or that don’t have a physical position). - -> Streaming queries are useful if you need to get information about an entity periodically - for example, so that a player can see and interact with it. -> -> If you just need information about an entity at one particular time, use [queries](#queries) instead. - -### Workers -The [SpatialOS Runtime](#spatialos-runtime) manages the [SpatialOS world](#spatialos-world): it keeps track of all the [SpatialOS entities](#spatialos-entity) and their -[SpatialOS components](#spatialos-component). But on its own, it doesn’t make any changes to the world. - -Workers are programs that connect to a SpatialOS world. They perform the computation associated with a world: they can read what’s happening, watch for changes, and make changes of their own. - -There are two types of workers; server-workers and client-workers. - -A server-worker approximates to a server in native Unreal networking but, unlike Unreal networking, in SpatialOS you can have have more than one server-worker. - -* **Server-worker** - A server-worker is a [worker](#workers) whose lifecycle is managed by SpatialOS. When running a [deployment](#deployment), the SpatialOS Runtime starts and stops server-workers based on your chosen [load balancing](https://docs.improbable.io/reference/latest/shared/glossary#load-balancing) configuration. - - You usually set up server-workers to implement game logic and physics simulation. You can have one server-worker connected to your [deployment](#deployment), or dozens, depending on the size and complexity of your [SpatialOS world](#spatialos-world). - If you run a [local deployment](#deployment), the server-workers run on your development computer. If you run a [cloud deployment](#deployment), the server-workers run in the cloud. -* **Client-worker** - - While the lifecycle of a server-worker is managed by the SpatialOS Runtime, the lifecycle of a client-worker is managed by the game client. - You usually set up client-workers to visualize what’s happening in the [SpatialOS world](#spatialos-world). They also deal with player input. - - > Related: - > - > * [External worker (client-worker) launch configuration](https://docs.improbable.io/reference/latest/shared/worker-configuration/launch-configuration#external-worker-launch-configuration) - -**More about workers**
-In order to achieve huge scale, SpatialOS divides up the SpatialOS entities in the world between workers, balancing the work so none of them are overloaded. For each SpatialOS entity in the world, it decides which worker should have [write access](#authority) to each SpatialOS component on the SpatialOS entity. To prevent multiple workers writing to a component at the same time, only one worker at a time can have write access to a SpatialOS component. - -As the world changes over time, the position of SpatialOS entities and the amount of updates associated with them changes. Server-workers report back to SpatialOS how much load they're under, and SpatialOS adjusts which workers have write access to components on which SpatialOS entities. SpatialOS then starts up new workers when needed. This is [load balancing](https://docs.improbable.io/reference/latest/shared/worker-configuration/loadbalancer-config). -Around the SpatialOS entities which they have write access to, every worker has an area of the world they are [interested in](#interest). - -A worker can read the current state of components of the SpatialOS entities within this area, and SpatialOS sends [updates and messages](https://docs.improbable.io/reference/latest/shared/glossary#sending-an-update) about these SpatialOS entities to the worker. - -If the worker has write access to a SpatialOS component, it can [send updates and messages](https://docs.improbable.io/reference/latest/shared/glossary#sending-an-update): -it can update [properties](https://docs.improbable.io/reference/latest/shared/glossary#property), send and handle [commands](https://docs.improbable.io/reference/latest/shared/glossary#command) and trigger [events](https://docs.improbable.io/reference/latest/shared/glossary#event). - -> Related: -> -> * [Concepts: Workers and load balancing](https://docs.improbable.io/reference/latest/shared/concepts/workers-load-balancing) - -### Worker configuration -The worker configuration is how you set up your workers. It is represented in the worker configuration file. -See [workers](#workers) and [worker configuration file](#worker-configuration-file). - -### Worker configuration file - -> First, see [workers](#workers). - -Each worker needs a worker configuration file. This file tells SpatialOS how to build, launch, and interact with the workers. -The file’s name must be `spatialos..worker.json`: for example, `spatialos.MyWorkerType.worker.json`. -Once you’ve chosen a label for the worker type (for example, myWorkerType), you use this exact label consistently throughout your project to identify this worker type. - -> Related: -> ->[Worker configuration file `worker.json`](https://docs.improbable.io/reference/latest/shared/worker-configuration/worker-configuration) - -### Worker types - -> First, see [workers](#workers). - -There are two generic types of worker that define how you would want to connect these workers and what kind of capabilities they have: - -* [server-worker](#workers) -* [client-worker](#workers) - -Within these broad types, you can define your own worker sub-types to create more specialized workers. - -### Write access -See [authority](#authority). - -
------------- -2019-03-15 Page updated with full editorial review -
-
-2019-03-15 Added layers, non-Unreal layers, network operations (ops) diff --git a/SpatialGDK/Documentation/content/handover-between-server-workers.md b/SpatialGDK/Documentation/content/handover-between-server-workers.md deleted file mode 100644 index 51edc1b661..0000000000 --- a/SpatialGDK/Documentation/content/handover-between-server-workers.md +++ /dev/null @@ -1,54 +0,0 @@ -<%(TOC)%> -# Actor handover between server-workers - -Actor handover (`handover`) is a new `UPROPERTY` tag. It allows games built in Unreal (which uses single-server architecture) to take advantage of SpatialOS’ distributed, persistent server architecture. - -In Unreal’s native single-server architecture, your game server holds the canonical state of the whole game world. As there is a single game server, there are Actor properties that the server doesn’t need to share with any other server or clients. These properties only need to exist in the game server’s local process space. - -In SpatialOS games, the work of the server is spread across several servers (known as “server-workers” in SpatialOS). (Note that in SpatialOS, game clients are “client-workers” - there’s more information on [workers](https://docs.improbable.io/reference/latest/shared/concepts/workers) in the SpatialOS documentation.) - -As Unreal expects there to be only one server, rather than several servers, the SpatialOS GDK for Unreal has a custom solution to take advantage of the SpatialOS distributed server architecture. This involves a handover of responsibility for an Actor and its properties between server-workers. (Actors approximate to “entities” in SpatialOS, so we refer to them as “entities” when we are talking about what happens to them in SpatialOS - handily, “properties” in an entity’s components in SpatialOS map to replicated Actor properties. You can find out more about [entities, components and properties](https://docs.improbable.io/reference/latest/shared/concepts/entities) in the SpatialOS documentation.) - -Server-workers have [authority]({{urlRoot}}/content/glossary#authority) over entities, meaning that they are responsible for properties of an entity. Only one server-worker has authority over the properties of an entity at a time. In order to [load balance](https://docs.improbable.io/reference/latest/shared/glossary#load-balancing) between server-workers, each server-worker has only a certain area of authority, so each server-worker has a boundary. -This means that, at the boundary between server-worker 1 and server-worker 2, server-worker 1 needs to transfer authority of entity properties to server-worker 2 so that server-worker 2 can seamlessly continue to simulate the entity exactly where server-worker 1 stopped. (See SpatialOS documentation on [`AuthorityChange`](https://docs.improbable.io/reference/latest/shared/design/operations#authoritychange).) - -Note that server-worker authority over properties is different to server-worker interest in properties. See SpatialOS documentation on [worker interest](https://docs.improbable.io/reference/latest/shared/glossary#interest). - -## How to facilitate Actor handover - -To facilitate an Actor’s property handover between server-workers, follow the instructions below: - -1. If your property is defined in a native C++ class, mark the property field with a `Handover` tag in the `UPROPERTY` macro, as shown in the example below.

- - ``` - UPROPERTY(Handover) - float MyServerSideVariable; - ``` - -1. Alternatively, if your property is defined in a Blueprint class, in the Blueprint Editor, set the **Variable**'s **Replication** setting to `Handover` .

-![Blueprint Editor]({{assetRoot}}assets/screen-grabs/handover-blueprint.png) - -1. Tag the Actor with the `SpatialType` specifier. (See documentation on [SpatialType]({{urlRoot}}/content/spatial-type) for guidance.) - -1. Generate the [schema]({{urlRoot}}/content/glossary#schema-generation) for your Actor’s class. (In the Unreal Editor, from the [GDK toolbar]({{urlRoot}}/content/toolbars), select the **Schema** icon.) - -The GDK now ensures that server-workers transfer these tagged Actor’s properties between them. - -## Native-Unreal class properties handover -To ensure native-Unreal classes work with the GDK for Unreal, we are making handover-related changes on a class-by-class basis as we identify appropriate properties for `Handover` tags. - -**Classes with properties tagged with `Handover` status (as of 2018-10-26)** - -* `UCharacterMovementComponent` -* `APlayerController` -* `MovementComponent` - -We will continue to extend our support to more built-in Actor and component types. - -## The difference between `Replicated` and `Handover` tags -It’s important to understand that the native-Unreal tag `Replicated` and GDK for Unreal `Handover` tag have different uses: - -* `Replicated` tags identify Actor properties that any client-worker or server-worker needs to have interest in. -* `Handover` tags identify Actor properties that only server-workers need to have interest in and allow server-workers to transfer authority between them. - -Note that while you could replace all `Handover` tags with `Replicated` tags and your simulation would function correctly, its network performance could suffer. This is because there are a lot of workers with interest in `Replicated`-tagged properties. `Handover`-tagged properties have limited worker interest; they only need to be serialized on demand to server-workers taking over authority. diff --git a/SpatialGDK/Documentation/content/helper-scripts.md b/SpatialGDK/Documentation/content/helper-scripts.md deleted file mode 100644 index 78da474fcc..0000000000 --- a/SpatialGDK/Documentation/content/helper-scripts.md +++ /dev/null @@ -1,8 +0,0 @@ -<%(TOC)%> -# Helper scripts - -These scripts are located under `Plugins\UnrealGDK\SpatialGDK\Build\Scripts\` directory of your game. - -| Helper script | Parameters | Description | -| --- | --- | --- | -| `BuildWorker.bat` | ` .uproject [--skip-codegen]` | Example:
`BuildWorker.bat ExampleGameEditor Win64 Development ExampleGame.uproject`

Build, cook and zip your Unreal server-workers and client-workers for use with a SpatialOS cloud deployment (uploaded using [`spatial cloud upload`](https://docs.improbable.io/reference/latest/shared/deploy/deploy-cloud)).

The following ``s generate zipped workers:
- ``
- `Server`
- `Editor`

Any other `` passes all arguments to `Engine\Build\BatchFiles\Build.bat` with no cooking or zipping performed.| diff --git a/SpatialGDK/Documentation/content/local-dev-workflow.md b/SpatialGDK/Documentation/content/local-dev-workflow.md deleted file mode 100644 index b2a4ba0d1f..0000000000 --- a/SpatialGDK/Documentation/content/local-dev-workflow.md +++ /dev/null @@ -1,13 +0,0 @@ -# Local development workflow - -The following flowchart provides a reference of the local development workflow on the GDK. - -If you haven't already, please follow the [GDK Starter Template guide]({{urlRoot}}/content/get-started/gdk-template) which provides a detailed explanation of the different steps. - - - - - ----- - -_2019-04-15 Page added with editorial review_ \ No newline at end of file diff --git a/SpatialGDK/Documentation/content/map-travel.md b/SpatialGDK/Documentation/content/map-travel.md deleted file mode 100644 index 44f22a2423..0000000000 --- a/SpatialGDK/Documentation/content/map-travel.md +++ /dev/null @@ -1,129 +0,0 @@ -<%(TOC)%> -# Map travel - -This topic is for advanced users only. Before reading this page, make sure you are familiar with the Unreal documentation on [Map travel](https://docs.unrealengine.com/en-us/Gameplay/Networking/Travelling). - -## APlayerController::ClientTravel - -### In native Unreal -`ClientTravel` is the process of changing which [map (or Level - see Unreal documentation)](http://api.unrealengine.com/INT/Shared/Glossary/index.html#l) a client currently has loaded. - -### In the GDK -In the GDK you can use `ClientTravel` to move a client-worker from an offline state to a connected state, where that connection is to a local deployment or a cloud deployment. Alternatively, you can move a connected client-worker to an offline state. You can also change which SpatialOS deployment the client-worker is connected to. - -**Note:** Always make sure your client-worker(s) have the same map loaded as the one which the server-worker(s) are running in your deployment. - -#### Using Receptionist -The Receptionist is a SpatialOS service which allows you to connect to a deployment via a host and port. You can specify these parameters in command line arguments like in native Unreal and you will connect automatically via receptionist. - -To connect to a deployment using `ClientTravel` and the receptionist flow, simply call `APlayerController::ClientTravel` with the receptionist IP and port. Make sure to also specify the map that is loaded in the deployment you are connecting to. For example: - -``` -FString TravelURL = TEXT("127.0.0.1:7777/DestinationMap"); -PlayerController->ClientTravel(TravelURL, TRAVEL_Absolute, false /*bSeamless*/); -``` -**Note:** The receptionist connection flow is intended to be used for development only. A released game should use the `Locator` flow. - -#### Using Locator -The Locator is a SpatialOS service which allows you to connect to cloud deployments. - -> Experimental: You can use `ClientTravel` to change which cloud deployment a client-worker is connected to. This functionality is highly experimental and requires you to write your own authorization code. - -Using the locator flow is very similar to using the receptionist, except with different URL options. You must add the `locator`, or `legacylocator` option, and specify the appropriate options. The `locator` workflow makes use of the new [Authentication flow](https://docs.improbable.io/reference/latest/shared/auth/integrate-authentication-platform-sdk), and the `legacylocator` makes use of the [Deprecated Authentication flow](https://docs.improbable.io/reference/latest/shared/auth/integrate-authentication) - -**Locator**: Add the options `locator`, `playeridentity` and `login`. -``` -FURL TravelURL; -TravelURL.Host = TEXT("locator.improbable.io"); -TravelURL.Map = TEXT("DesiredMap"); -TravelURL.AddOption(TEXT("locator")); -TravelURL.AddOption(TEXT("playeridentity=MY_PLAYER_IDENTITY_TOKEN")); -TravelURL.AddOption(TEXT("login=MY_LOGIN_TOKEN")); - -PlayerController->ClientTravel(TravelURL.ToString(), TRAVEL_Absolute, false /*bSeamless*/); -``` - -**Legacy Locator**: Add `legacylocator`, a project name (the SpatialOS Console project where you have started your deployment), a deployment name (the name you use to start the deployment), and a login token (requires writing authentication code, see [our authentication service docs](https://docs.improbable.io/reference/latest/shared/auth/integrate-authentication) for details). -``` -FURL TravelURL; -TravelURL.Host = TEXT("locator.improbable.io"); -TravelURL.Map = TEXT("DesiredMap"); -TravelURL.AddOption(TEXT("legacylocator")); -TravelURL.AddOption(TEXT("project=MY_PROJECT_NAME")); -TravelURL.AddOption(TEXT("deployment=MY_DEPLOYMENT_NAME")); -TravelURL.AddOption(TEXT("token=MY_LOGIN_TOKEN")); - -PlayerController->ClientTravel(TravelURL.ToString(), TRAVEL_Absolute, false /*bSeamless*/); -``` - -### ClientTravel - technical details -We have made changes to the Unreal Engine to detect if you have SpatialOS networking enabled. If you do, when you specify a ClientTravel URL containing a host to connect to, we create a `SpatialPendingNetGame` instead of a default Unreal `PendingNetGame`. This internally creates a `SpatialNetConnection` which connects you to the specified host. - -## UWorld::ServerTravel -> Warning: `ServerTravel` is in an experimental state and we currently only support it in single server-worker configurations. -> We don’t support `ServerTravel` in [PIE](https://docs.unrealengine.com/en-us/GettingStarted/HowTo/PIE#playineditor). - -### In native Unreal -`ServerTravel` in Unreal is the concept of changing the [map (or Level - see Unreal documentation)](http://api.unrealengine.com/INT/Shared/Glossary/index.html#l) for the server and all connected clients. A common use case is starting a server in a lobby level. Clients connect to this lobby level and choose loadout and character, for example. When ready, the server triggers a `ServerTravel`, which transitions the deployment and all clients into the main game level. - -When `ServerTravel` is triggered, the server tells all clients to begin to [`ClientTravel`](https://docs.unrealengine.com/en-us/Gameplay/Networking/Travelling) to the map specified. If the `ServerTravel` is seamless then the client maintains its connection to the server. If it’s not seamless then all the clients disconnect from the server and reconnect once they have loaded the map. Internally, the server does a similar process: it loads in the new level, usually a game world for all the clients to play on, and begins accepting player spawn requests once ready. - -### In the GDK -To use `ServerTravel` with the GDK there are a couple of extra steps to ensure the SpatialOS deployment is in the correct state when transitioning maps. - -#### Generate snapshot -Generate a snapshot for the map you intend to server transition to using the [snapshot generator]({{urlRoot}}/content/spatialos-concepts/generating-a-snapshot) and save it to `\Content\Spatial\Snapshots\`. You can either copy your generated snapshot manually (from `\spatial\snapshots\default.snapshot`) or set up your project settings to generate the snapshot for your level into that folder. You can find these settings via **Edit** > **Project Settings** > **SpatialOS Unreal GDK** > **Snapshot path**. - -The snapshot is read from `\Content\Spatial\Snapshots\` when you call the `UWorld::ServerTravel`. To ensure this works in a cloud deployment, add the `Spatial\Snapshots` folder to your **Additional Non-Asset Directories To Copy for dedicated server only** found at **File** > **Package Project** > **Packaging settings**. For example: - -![snapshot asset cooking]({{assetRoot}}assets/screen-grabs/snapshot-asset-cooking.png) - -Remember to package the map you intend to travel to: from the **Package Settings**, add your map(s) to **List of maps to include in a packaged build**. - -![map cooking]({{assetRoot}}assets/screen-grabs/cooking-maps.png) - -#### Specify URL parameters -Pass the snapshot to load as part of the map URL when calling `ServerTravel`. For example: - -``` -FString ServerTravelURL = TEXT("ExampleMap?snapshot=ExampleMap.snapshot"); -UWorld* World = GetWorld(); -World->ServerTravel(ServerTravelURL, true /*bAbsolute*/); -``` - -The GDK has also added the `clientsStayConnected` URL parameter. -Adding this URL parameter to your `ServerTravel` URL prevents client-workers from disconnecting from SpatialOS during the `ServerTravel` process. We recommend doing this to prevent extra load when client-workers attempt to re-connect to SpatialOS. For example: - -``` -FString ServerTravelURL = TEXT("ExampleMap?snapshot=ExampleMap.snapshot?clientsStayConnected"); -``` - -### ServerTravel - technical details -There are a few things to consider when using `ServerTravel` with SpatialOS. It’s not normal for a SpatialOS deployment to ‘load a new world’, since SpatialOS was made for very large persistent worlds. This means you need to perform extra steps to support `ServerTravel`. - -> Note that `ServerTravel` is only supported in non-PIE configurations. Launching a server-worker in PIE has a dependency on the `Use dedicated server` network configuration available in PIE. Unfortunately this setting has a dependency on `Use single process` which doesn’t support `ServerTravel`. To test and develop `ServerTravel` for your game with the SpatialOS GDK for Unreal, you need to launch workers outside of the editor, either via using the the `LaunchServer.bat` or by using a launch configuration which loads [managed workers](https://docs.improbable.io/reference/13.0/shared/concepts/workers#managed-and-external-workers). - -The first thing to consider is how to handle SpatialOS’s snapshot system. Normally, you can load a snapshot for a deployment at startup and then forget about it. But since you will be changing worlds, and you generate a single snapshot per world, you need to change the snapshot you have loaded into the deployment at runtime. You can achieve this by ‘wiping’ the deployment: essentially you delete all entities in the world, and when it’s empty you manually load a new snapshot. You specify the snapshot to load in the URL of the `ServerTravel`. - -The second thing to consider is how to hande player connections. Normally, once a map is loaded in Unreal on the server, clients can just connect, but for SpatialOS you need to make sure the deployment is in a good state (snapshot fully loaded) before client-workers can connect. - -The world wiping is handled at the start of `ServerTravel`, after client-workers have unloaded their current world and started loading a new map, but before server-workers have started unloading their current world. Only the worker which has authority over the [GSM]({{urlRoot}}/content/glossary#global-state-manager) should be able to wipe the world. It uses a large and expensive `EntityQuery` for all entities in the world. Once you have an entity query response for all entities that exist, you send deletion requests for each one. After finishing, the server-workers continue standard Unreal `ServerTravel` and load in the new world. - -The snapshot loading is again handled by the server-worker which _was_ authoritative over the GSM (since the GSM entity has now been deleted). The process of loading the snapshot starts once the server-worker with authority has loaded the world (`SpatialNetDriver::OnMapLoaded`). Using the `SnapshotManager`, this server-worker reads the snapshot specified in the map URL from the `Game\Content\Spatial\Snapshots` directory. Iterating through all entities in the snapshot, the worker sends a spawn request for all of them. Once the GSM has been spawned and the spawning process for the rest of the entities has completed, the server-worker which gains authority over the GSM sets the `AcceptingPlayers` field to true. This tells client-workers that they can now send player spawn requests. Client-workers know about the `AcceptingPlayers` state by sending entity queries on a timer for its existence and state. - -## Default connection flows -#### In PIE -Launching a [PIE](https://docs.unrealengine.com/en-us/GettingStarted/HowTo/PIE#playineditor) client-worker from the editor will automatically connect it to a local SpatialOS deployment. This is for quick editing and debugging purposes. - -#### With built clients -By default, outside of PIE, clients do not connect to a SpatialOS deployment. This is so you can implement your own connection flow, whether that be through an offline login screen, a connected lobby, etc. - -To connect a client-worker to a deployment from an offline state, you must use [`ClientTravel`](#aplayercontroller-clienttravel). - -The `LaunchClient.bat` (which we have provided) already includes the local host IP `127.0.0.1` which means client-workers launched this way will attempt to connect automatically using the [receptionist](#using-receptionist) flow. - - -#### With the Launcher -When launching a client-worker from the SpatialOS Console using the [Launcher](https://docs.improbable.io/reference/latest/shared/operate/launcher#the-launcher), the client-worker will connect to SpatialOS by default. It has the `Locator` information required to connect to said deployment included as command-line arguments. When these `Locator` arguments are present, client-workers will attempt to connect automatically. Please note the launcher login tokens are only valid for 15 minutes. - -> Connecting by default when using the launcher is subject to change. diff --git a/SpatialGDK/Documentation/content/non-unreal-server-worker-types.md b/SpatialGDK/Documentation/content/non-unreal-server-worker-types.md deleted file mode 100644 index 95ecd40436..0000000000 --- a/SpatialGDK/Documentation/content/non-unreal-server-worker-types.md +++ /dev/null @@ -1,245 +0,0 @@ -# Non-Unreal server-worker types -<%(TOC)%> - -By default, the GDK for Unreal uses a single Unreal server-worker type to handle all server-side computation. However, you can set up additional server-worker types that do not use Unreal or the GDK. - -You can use these non-Unreal server-worker types to modularize your game’s functionality so you can re-use the functionality across different games. For example, you could use a non-Unreal server-worker type written in Python that interacts with a database or other third-party service, such as [Firebase](https://firebase.google.com/) or [PlayFab](https://playfab.com/). - -## How to integrate non-Unreal server-worker types into your game -In order to interact with each other, Unreal and non-Unreal server-worker instances need to send and receive updates to and [commands](https://docs.improbable.io/reference/latest/shared/glossary#command) for the same SpatialOS components. We recommend that you define the SpatialOS components used by non-Unreal worker instances manually in [schema](https://docs.improbable.io/unreal/alpha/content/glossary#schema) files, separate from those automatically generated by the GDK. This is because: - -* SpatialOS command data in GDK-generated schema files is encoded as byte strings, making deserialization of commands in non-Unreal server-worker types more difficult. -* It aids portability; you can more easily re-use any non-Unreal server-worker types when you have an accompanying schema file. - -Default single Unreal server-worker type development doesn’t accommodate schema files that haven’t been generated by the GDK, so you need to set your game up to handle this. - -### How to set up interaction -To set up your game to interact with SpatialOS components defined outside the GDK (external SpatialOS components), you use the following: - -* To send SpatialOS component updates and commands, use methods defined in the `SpatialWorkerConnection.h` file (described in the examples section below). -* To receive [network operations](https://docs.improbable.io/reference/latest/shared/design/operations) for external SpatialOS components, you must provide custom callbacks for specific component IDs and operation types. The GDK then forwards the operations to your callbacks. (This is described in the examples section below.) - - ->**TIP:** If you’re using schema from outside the GDK, you can customise [snapshot generation](https://docs.improbable.io/unreal/alpha/content/generating-a-snapshot#generating-a-snapshot) from the GDK toolbar’s **Snapshot** button. This is to serialize additional entities with these external components which the default Unreal snapshot generation cannot currently do. See [Add to the snapshot](#add-to-the-snapshot) below for how to do this. - -#### Send data -You set up your game to send SpatialOS component updates, command requests, and command responses directly using the `SpatialWorkerConnection.h` public methods: - -* `void SendComponentUpdate(Worker_EntityId EntityId, const Worker_ComponentUpdate* ComponentUpdate);` -* `Worker_RequestId SendCommandRequest(Worker_EntityId EntityId, const Worker_CommandRequest* Request, uint32_t CommandId);` -* `void SendCommandResponse(Worker_RequestId RequestId, const Worker_CommandResponse* Response);` - -You access these methods via a reference to the net driver, as shown below: - -`SpatialWorkerConnection connection = Cast(World->GetNetDriver())->Connection;` - -There is a basic example in the _Examples_ section below. For more examples of how to construct component updates, command requests, and more, see the SpatialOS documentation on [serialization in the Worker SDK in C’s API](https://docs.improbable.io/reference/latest/capi/serialization). - -#### Receive data - ->**Note:** Your external SpatialOS components must have an ID between 1000 and 2000 to be registered by the pipeline. - -You set up your game to receive network operations using the `USpatialDispatcher::AddOpCallback` functions. These overloaded functions are parameterized with a `Worker_ComponentId` and a callback function const reference that takes one of the following network operation types as an argument: - -* `Worker_AddComponentOp` -* `Worker_RemoveComponentOp` -* `Worker_AuthorityChangeOp` -* `Worker_ComponentUpdateOp` -* `Worker_CommandRequestOp` -* `Worker_CommandResponseOp` - -`Worker_ComponentId` and each network operation type are defined in the [Worker SDK in C’s API](https://docs.improbable.io/reference/latest/capi/api-reference). - -If you want to ensure that the SpatialOS worker connection receives your callback for initial network operations, you need to register the callbacks inside your game instance's `OnConnected` event callback. - -Each `USpatialDispatcher::AddOpCallback` function returns a `CallbackId`. You can deregister your callbacks using the `USpatialDispatcher::RemoveOpCallback` function and passing the `CallbackId` parameter that was returned by the corresponding call to `USpatialDispatcher::AddOpCallback`. - -There is a basic example in the _Examples_ section below. For more examples of how to deserialize the `Worker_Op` type see the SpatialOS documentation on [serialization in the Worker SDK in C’s API](https://docs.improbable.io/reference/latest/capi/serialization). - -#### Add to the snapshot -You can customize snapshot generation by creating a class derived from the GDK `USnapshotGenerationTemplate` base class, and implementing the method below. You have the responsibility of incrementing the `NextEntityId` reference. If you don’t, snapshot generation will fail by attempting to add multiple entities to the snapshot with the same ID. -``` - * Write to the snapshot generation output stream. - * @param OutputStream the output stream for the snapshot being created. - * @param NextEntityId the next available entity ID in the snapshot, this reference should be incremented appropriately. - * @return bool the success of writing to the snapshot output stream, this is returned to the overall snapshot generation. - **/ -bool WriteToSnapshotOutput(Worker_SnapshotOutputStream* OutputStream, Worker_EntityId& NextEntityId); -``` - - -There is a basic example in the _Examples_ section below. For more examples of how to deserialize see the SpatialOS documentation on [serialization in the Worker SDK in C’s API](https://docs.improbable.io/reference/latest/capi/serialization). - -### Examples -Below is a simple example schema file which a non-Unreal server-worker type could use to track player statistics: - -``` -package improbable.testing; - -type UnrealRequest { - string some_request_string = 1; -} -type UnrealResponse { - string some_response_string = 1; -} - -component NonUnrealAuthoritative { - id = 1337; - uint32 counter = 1; -} - -component UnrealAuthoritative { - id = 1338; - uint32 other_counter = 1; - command UnrealResponse test_command(UnrealRequest); -} -``` - -#### Send data -You could serialize and send a component update in your Unreal project code in the following way: - -``` -void SendSomeUpdate(Worker_EntityId TargetEntityId, Worker_ComponentId ComponentId) -{ - Worker_ComponentUpdate Update = {}; - Update.component_id = ComponentId; - Update.schema_type = Schema_CreateComponentUpdate(ComponentId); - Schema_Object* FieldsObject = Schema_GetComponentUpdateFields(Update.schema_type); - Schema_AddInt32(FieldsObject, 1, ++UnrealCounter); - Cast(World->GetNetDriver())->Connection->SendComponentUpdate(TargetEntityId, &Update); -} -``` - - -You could serialize and send a command response in your Unreal project code in the following way: - - -``` -Worker_RequestId SendSomeCommandResponse(Worker_EntityId TargetEntityId, Worker_ComponentId ComponentId, Schema_FieldId CommandId) { - Worker_CommandResponse Response = {}; - Response.component_id = ComponentId; - Response.schema_type = Schema_CreateCommandResponse(ComponentId, CommandId); - Schema_Object* ResponseObject = Schema_GetCommandResponseObject(Response.schema_type); - const char* Text = "Hello World."; - Schema_AddBytes(ResponseObject, 1, (const uint8_t*)Text, sizeof(char) * strlen(Text)); - Cast(World->GetNetDriver())->Connection->SendCommandResponse(TargetEntityId, &Response); -} -``` - - -#### Receive data -You could receive and deserialize a component update and command request in your Unreal project code in the following way: -``` -void UTPSGameInstance::Init() -{ - // OnConnected is an event declared in USpatialGameInstance - OnConnected.AddLambda([this]() { - // On the client the world may not be completely set up, if s we can use the PendingNetGame - USpatialNetDriver* NetDriver = Cast(GetWorld()->GetNetDriver()); - if (NetDriver == nullptr) - { - NetDriver = Cast(GetWorldContext()->PendingNetGame->GetNetDriver()); - } - USpatialDispatcher* Dispatcher = NetDriver->Dispatcher; - - Dispatcher->AddOpCallback(1337, [this](const Worker_ComponentUpdateOp& Op) { - // Example deserializing network operation - uint32 PlayerCount = Schema_GetUint32(Schema_GetComponentUpdateFields(Op.update.schema_type), 1); - - // Example actor spawning - const FVector Location = FVector::ZeroVector; - const FRotator Rotation = FRotator::ZeroRotator; - GetWorld()->SpawnActor(CubeClass, &Location, &Rotation); - - // Example serializing and sending another network operation - Worker_ComponentUpdate Update = {}; - Update.component_id = 1338; - Update.schema_type = Schema_CreateComponentUpdate(1338); - Schema_Object* FieldsObject = Schema_GetComponentUpdateFields(Update.schema_type); - Schema_AddInt32(FieldsObject, 1, PlayerCount); - Cast(GetWorld()->GetNetDriver())->Connection->SendComponentUpdate(Op.entity_id, &Update); - - }); - - Dispatcher->AddOpCallback(1338, [this](const Worker_CommandRequestOp& Op) { - // Example serializing and sending command response - Worker_CommandResponse Response = {}; - Response.component_id = 1338; - Response.schema_type = Schema_CreateCommandResponse(1338, 1); - Schema_Object* response_object = Schema_GetCommandResponseObject(Response.schema_type); - FString text = "Here's my response."; - Schema_AddBytes(response_object, 1, (const uint8_t*)TCHAR_TO_ANSI(*text), sizeof(char) * strlen(TCHAR_TO_ANSI(*text))); - Cast(GetWorld()->GetNetDriver())->Connection->SendCommandResponse(Op.request_id, &Response); - }); - }); -} -``` - -#### Add to the snapshot -You could add a new entity with the given component in your Unreal project code in the following way: - -``` -UCLASS() -class UTestEntitySnapshotGeneration : public USnapshotGenerationTemplate -{ - GENERATED_BODY() - -public: - bool WriteToSnapshotOutput(Worker_SnapshotOutputStream* OutputStream, Worker_EntityId& NextEntityId) override { - Worker_Entity TestEntity; - TestEntity.entity_id = NextEntityId; - - TArray Components; - - const WorkerAttributeSet TestWorkerAttributeSet{ TArray{TEXT("test_attribute")} }; - const WorkerRequirementSet TestWorkerPermission{ TestWorkerAttributeSet }; - const WorkerRequirementSet AnyWorkerPermission{ {SpatialConstants::UnrealClientAttributeSet, SpatialConstants::UnrealServerAttributeSet, TestWorkerAttributeSet } }; - - WriteAclMap ComponentWriteAcl; - ComponentWriteAcl.Add(SpatialConstants::POSITION_COMPONENT_ID, SpatialConstants::UnrealServerPermission); - ComponentWriteAcl.Add(SpatialConstants::METADATA_COMPONENT_ID, SpatialConstants::UnrealServerPermission); - ComponentWriteAcl.Add(SpatialConstants::PERSISTENCE_COMPONENT_ID, SpatialConstants::UnrealServerPermission); - ComponentWriteAcl.Add(SpatialConstants::ENTITY_ACL_COMPONENT_ID, SpatialConstants::UnrealServerPermission); - ComponentWriteAcl.Add(1337, TestWorkerPermission); - ComponentWriteAcl.Add(1338, SpatialConstants::UnrealServerPermission); - - // Serialize NonUnrealAuthoritative component data - Worker_ComponentData NonUnrealAuthoritativeComponentData{}; - NonUnrealAuthoritativeComponentData.component_id = 1337; - NonUnrealAuthoritativeComponentData.schema_type = Schema_CreateComponentData(1337); - Schema_Object* NonUnrealAuthoritativeComponentDataObject = Schema_GetComponentDataFields(NonUnrealAuthoritativeComponentData.schema_type); - Schema_AddInt32(NonUnrealAuthoritativeComponentDataObject, 1, 1); // set counter field to 1 initially - - // Serialize FromUnreal component data - Worker_ComponentData UnrealAuthoritativeComponentData{}; - UnrealAuthoritativeComponentData.component_id = 1338; - UnrealAuthoritativeComponentData.schema_type = Schema_CreateComponentData(1338); - Schema_Object* UnrealAuthoritativeComponentDataObject = Schema_GetComponentDataFields(UnrealAuthoritativeComponentData.schema_type); - Schema_AddInt32(UnrealAuthoritativeComponentDataObject, 1, 1); // set other_counter field to 1 initially - - Components.Add(SpatialGDK::Position(SpatialGDK::Origin).CreatePositionData()); - Components.Add(SpatialGDK::Metadata(TEXT("TestEntity")).CreateMetadataData()); - Components.Add(SpatialGDK::Persistence().CreatePersistenceData()); - Components.Add(SpatialGDK::EntityAcl(AnyWorkerPermission, ComponentWriteAcl).CreateEntityAclData()); - Components.Add(NonUnrealAuthoritativeComponentData); - Components.Add(UnrealAuthoritativeComponentData); - - TestEntity.component_count = Components.Num(); - TestEntity.components = Components.GetData(); - - bool bSuccess = Worker_SnapshotOutputStream_WriteEntity(OutputStream, &TestEntity) != 0; - if (bSuccess) - { - NextEntityId++; - } - - return bSuccess; - } -}; -``` - -
- ------- -_2019-04-11 Page updated with limited editorial review_
-_2019-03-15 Page added with full editorial review_ diff --git a/SpatialGDK/Documentation/content/singleton-actors.md b/SpatialGDK/Documentation/content/singleton-actors.md deleted file mode 100644 index 3667856775..0000000000 --- a/SpatialGDK/Documentation/content/singleton-actors.md +++ /dev/null @@ -1,56 +0,0 @@ -<%(TOC)%> -# Singleton Actors - -Singleton Actors allow a single source of truth for both operations and data across a multiserver simulation. They are server-side authoritative [Unreal Actors](https://docs.unrealengine.com/en-us/Programming/UnrealArchitecture/Actors) that are restricted to one instantiation on SpatialOS. For example, if you are implementing a scoreboard, you'd most likely only want there to be one of them in your world. Ensuring this behavior in a multiserver paradigm requires a few additional steps which can be easily facilitated through the Singleton Actor. - -There are two kinds of Singleton Actors: - -* **Public Singleton Actors** - Singleton Actors which are replicated to [server-workers and client-workers]({{urlRoot}}/content/glossary#workers). [AGameState](https://docs.unrealengine.com/en-US/Gameplay/Framework/GameMode) is a Public Singleton Actor. -* **Private Singleton Actors** - Singleton Actors which are replicated to [server-workers]({{urlRoot}}/content/glossary#workers), but not accessible to [client-workers]({{urlRoot}}/content/glossary#workers). [AGameMode](https://docs.unrealengine.com/en-US/Gameplay/Framework/GameMode) is a Private Singleton Actor. - -You can define any class as a Singleton Actor. Unreal engine classes we have explicitaly tagged as Singleton Actors are - - -1. AGameModeBase -1. AGameStateBase - -* As `SpatialType` is inheritable, all classes that derive off these classes are also considered Singleton Actors. You can opt out using the `NotSpatialType` tag. - -Each server-worker should instantiate their own local version of each Singleton Actor. For `AGameMode` and `AGameState`, Unreal Engine does this automatically. Client-workers receive Public Singletons Actors from the server-workers via the normal Actor replication lifecycle. - -Due to server-workers spawning their own instances of each Singleton Actor, proper replication and authority management of Singleton Actors becomes a bit tricky. To solve this issue, we have introduced the concept of a Global State Manager (GSM) to enable proper replication of Singleton Actors. The GSM solves the problem of replicating Singleton Actors by only allowing the server-worker with [authority]({{urlRoot}}/content/glossary#authority) over the GSM to execute the initial replication of these Actors. All other server-workers will then link their local Singleton Actors to their respective SpatialOS entity. Because of this, you must update your snapshot whenever adding a new Singleton Actor to your project. - -## Setting up Singleton Actors - -To set up Singleton Actors for your project, you need to: - -1. Register Singleton Actors by tagging them with the `SpatialType=Singleton` class attribute. If you wish to make them Private Singletons, tag them with the additional `ServerOnly` class attribute. - -The code snippet below shows how to tag a native C++ class with the appropriate Public Singleton identifiers. - -``` -UCLASS(SpatialType=Singleton) -class TESTSUITE_API AExampleGameGameState : public AGameStateBase -{ - GENERATED_BODY() - ... -} -``` - -The code snippet below shows how to tag a native C++ class with the appropriate Private Singleton identifiers. - -``` -UCLASS(SpatialType=(Singleton, ServerOnly)) -class TESTSUITE_API AExampleGameGameMode : public AGameModeBase -{ - GENERATED_BODY() - ... -} -``` - -To tag a Blueprint class as a Public Singleton, open the class in the Blueprint Editor and navigate to the `Class Settings`. In the `Advanced` section inside `Class Options`, check the `Spatial Type` checkbox and add `Singleton` to the `Spatial Description` textbox. To tag a Blueprint class as a Private Singleton, follow the same steps and add `ServerOnly` to the `Spatial Description` textbox. - -This is an example of what your Blueprint `Class Options` should look like if you've tagged it as a Private Singleton: -![Singleton Blueprint]({{assetRoot}}assets/screen-grabs/blueprint-singleton.png) - -And that's it! You have successfully specified a Singleton Actor. Make sure you generate [schema]({{urlRoot}}/content/spatialos-concepts/schema) and create a new [snapshot]({{urlRoot}}/content/spatialos-concepts/generating-a-snapshot) using the [SpatialOS GDK toolbar]({{urlRoot}}/content/toolbars). - diff --git a/SpatialGDK/Documentation/content/spatial-type.md b/SpatialGDK/Documentation/content/spatial-type.md deleted file mode 100644 index dce844410c..0000000000 --- a/SpatialGDK/Documentation/content/spatial-type.md +++ /dev/null @@ -1,58 +0,0 @@ -<%(TOC)%> -# Spatial Type - - Spatial Type (`SpatialType`) is a SpatialOS-specific [class specifier (Unreal documentation)](https://docs.unrealengine.com/en-US/Programming/UnrealArchitecture/Reference/Classes/Specifiers). The GDK uses `SpatialType` to expose network-relevant class information to SpatialOS. - - `SpatialType` is similar to other Unreal class specifiers, but implemented in parallel with [EClassFlags (Unreal documentation)](https://api.unrealengine.com/INT/API/Runtime/CoreUObject/UObject/EClassFlags/index.html) to minimize the possibility of conflicting changes between the standard Unreal Engine and the GDK’s Unreal Engine fork. - -The `SpatialType` tag allows the GDK to interoperate between the network stacks of native Unreal and SpatialOS. The tag is inherited down class hierarchies. - -## Classes with automatic SpatialType tagging -By default all classes are tagged with `SpatialType`. This ensures the GDK inspects all loaded classes and generates schema for them if they have any properties tagged with `Replicated` or `Handover`, or they have RPC functions. If you don't wish your class to be considered a `SpatialType` you can opt out using the `NotSpatialType` tag. - -## Classes which need manual SpatialType tagging -You need to manually tag as `SpatialType` any classes which are [Singleton Actors]({{urlRoot}}/content/singleton-actors) or only accessible to [server-workers]({{urlRoot}}/content/glossary#workers). These classes also need `SpatialType` descriptors. - -### SpatialType descriptors -You can add descriptors to the `SpatialType` tag to define additional information SpatialOS needs to know about your Unreal class. -These are: - -* `Singleton`: this indicates this class should be treated as a Singleton. -* `ServerOnly`: this indicates this class is only relevant to [server-workers]({{urlRoot}}/content/glossary#workers). You can use this descriptor in conjunction with the `Singleton` descriptor to indicate that this class is a Private Singleton. - -## How to manually tag classes as SpatialType - -### Adding the specifier and descriptors to your Unreal C++ class - -Like other Unreal class specifiers, you specify the `SpatialType` and descriptors within your class’s `UCLASS` macro. - -For example; - -``` -UCLASS(SpatialType) -class AMyReplicatingActor : public AActor -{ - GENERATED_BODY() - ... -} -``` - -To add `SpatialType` descriptors, use the following format; - -``` -UCLASS(SpatialType=Singleton) -class AMySingleton : public AActor -{ - GENERATED_BODY() - ... -} -``` - -### Adding the specifier and descriptors to your Unreal Blueprint class -You can also tag your Blueprint classes with the `SpatialType` tag and descriptors. To do this, - -1. Open the class in the Blueprint Editor and, from the menu, select **Class Settings**. -1. Select **Class Options** and under **Advanced**, check the `Spatial Type` checkbox. -1. Add any descriptors to the `Spatial Description` textbox, like so: - -![blueprint-singleton]({{assetRoot}}assets/screen-grabs/blueprint-singleton.png) diff --git a/SpatialGDK/Documentation/content/spatialos-concepts/concepts.md b/SpatialGDK/Documentation/content/spatialos-concepts/concepts.md deleted file mode 100644 index 350dcc9f40..0000000000 --- a/SpatialGDK/Documentation/content/spatialos-concepts/concepts.md +++ /dev/null @@ -1,9 +0,0 @@ -<%(TOC)%> -# SpatialOS concepts - -See the SpatialOS documentation for guidance on the SpatialOS core concepts. - -* [What is SpatialOS](https://docs.improbable.io/reference/latest/shared/concepts/spatialos) -* [World, entities, components](https://docs.improbable.io/reference/latest/shared/concepts/world-entities-components) -* [Workers and load balancing](https://docs.improbable.io/reference/latest/shared/concepts/workers-load-balancing) -* [Interest and authority](https://docs.improbable.io/reference/latest/shared/concepts/interest-authority) \ No newline at end of file diff --git a/SpatialGDK/Documentation/content/spatialos-concepts/generating-a-snapshot.md b/SpatialGDK/Documentation/content/spatialos-concepts/generating-a-snapshot.md deleted file mode 100644 index d8d41d3fd8..0000000000 --- a/SpatialGDK/Documentation/content/spatialos-concepts/generating-a-snapshot.md +++ /dev/null @@ -1,46 +0,0 @@ -<%(TOC)%> -# Snapshots - -A snapshot is a representation of the state of a [SpatialOS world]({{urlRoot}}/content/glossary#spatialos-world) at a given point in time. It stores each [persistent]({{urlRoot}}/content/glossary##persistence) [SpatialOS entity]({{urlRoot}}/content/glossary##spatialos-entity) and the values of their [SpatialOS components]({{urlRoot}}/content/glossary#spatialos-component)' [properties](https://docs.improbable.io/reference/latest/shared/glossary#property). - -You must generate a snapshot as the starting point for your [SpatialOS world]({{urlRoot}}/content/glossary#spatialos-world) when you create a new GDK project. -
- -> **NOTE:** You can find out more about snapshots in the [SpatialOS snapshot documentation](https://docs.improbable.io/reference/latest/shared/operate/snapshot) but this documentation concentrates on working with snapshots using the [SpatialOS SDKs]({{urlRoot}}/content/glossary#spatialos-sdk) rather than the GDK for Unreal. - - - -## How to generate a snapshot - -#### How to generate a snapshot - -To generate a snapshot, on the [SpatialOS GDK toolbar]({{urlRoot}}/content/toolbars.md) in the Unreal Editor, select **Snapshot** . - -![Snapshot]({{assetRoot}}assets/screen-grabs/snapshot.png) - -_Image: The GDK Toolbar._ - -This creates a snapshot called `default.snapshot` which you can find in `spatial\snapshots`. - -If you want your snapshots to be exported to a different path you can specify the output path and file name of the snapshot using the [GDK toolbar settings]({{urlRoot}}/content/toolbars.md). - -### What’s listed in snapshots - -The GDK snapshots contain two kinds of [SpatialOS entities]({{urlRoot}}/content/glossary#spatialos-entity): -Critical entities, and placeholder entities. - -### Critical entities - -Critical entities are listed in snapshots by default. - -Critical entities are functionality critical for the GDK; do not delete them. SpatialOS needs them for launching a deployment. You save these into your initial snapshot when you generate it. - -The critical entities are: - -* `SpatialSpawner` - an entity with the `PlayerSpawner` component which contains the `spawn_player` command. [Client-workers]({{urlRoot}}/content/glossary#workers) connecting to a [deployment]({{urlRoot}}/content/glossary#deployment) use this entity to spawn their player. -* `GlobalStateManager` - an entity with the `GlobalStateManager` component which has a map of [singleton]({{urlRoot}}/content/singleton-actors.md) classes to entity IDs (see [Global State manager]({{urlRoot}}/content/glossary#global-state-manager) glossary entry). The GDK uses this entity to orchestrate the replication of [Singleton Actors]({{urlRoot}}/content/singleton-actors.md). - -### Placeholder entities -Placeholder entities are listed in snapshots by default. You can opt to exclude them when generating a snapshot via the [SpatialOS GDK toolbar]({{urlRoot}}/content/toolbars.md). - -These entities exists only to set up server-worker boundaries in a way that is easy to test with multiple server-workers. These entities do not spawn as Actors when [checked out]({{urlRoot}}/content/glossary#check-out) by a worker and serve no purpose within the GDK. In most cases you can safely ignore them. diff --git a/SpatialGDK/Documentation/content/spatialos-concepts/schema.md b/SpatialGDK/Documentation/content/spatialos-concepts/schema.md deleted file mode 100644 index ebfa92a18a..0000000000 --- a/SpatialGDK/Documentation/content/spatialos-concepts/schema.md +++ /dev/null @@ -1,41 +0,0 @@ -<%(TOC)%> -# Schema - -Schema is a set of definitions which represent your game's objects in SpatialOS. SpatialOS uses schema to generate APIs specific to the components in your project. You can then use these APIs in your game's [worker types]({{urlRoot}}//content/glossary#spatialos-component) so their instances can interact with [SpatialOS entity components]({{urlRoot}}/content/glossary#spatialos-component).
- -Schema is defined in `.schema` files and written in schemalang. When you use the GDK, the schema files and their contents are generated and deleted automatically so you do not have to write or edit schema files manually. The GDK generates and deletes schema for you, when you start schema generation. - -#### How to generate schema - -To generate schema, select the **Schema** button in the [GDK Toolbar]({{urlRoot}}/content/toolbars#buttons). The GDK automatically iterates through classes with replicated properties to generate the required schema files and then updates the [SchemaDatabase]({{urlRoot}}/content/glossary#schemadatabase). - -![Toolbar]({{assetRoot}}assets/screen-grabs/toolbar/schema-button.png) - -_Image: In the GDK toolbar in the Unreal Editor, select **Schema**_ - -As the GDK automatically generates all the schema you need, you do not have to write or edit schema manually when using the GDK. - -#### When to generate schema - -You must generate schema when you add or change any [replicated properties (Unreal documentation)](https://docs.unrealengine.com/en-US/Gameplay/Networking/Actors/Properties) that you want to deploy to SpatialOS. - -The GDK only generates schema for classes currently loaded into memory. This means if your project uses [sublevels](), you’ll need to load them in addition to your map, before generating schema. - -#### Schema deletion - -When you generate schema, the GDK verifies that any classes referenced in the [SchemaDatabase]({{urlRoot}}/content/glossary#schemadatabase) still exist. If you delete a class, the GDK removes it from the SchemaDatabase the next time you generate schema. - - -## Schema and source control - -### Checking out the `SchemaDatabase` - -**Note:** If you are using the built-in [Unreal source control system](https://docs.unrealengine.com/en-US/Engine/UI/SourceControl) Unreal locks this file on checkout, meaning other users are unable to write to it. To prevent this, mark the `SchemaDatabase` file as writable locally on each machine, and only check out the file when you are ready to commit any changes made to it. - -## Schema for unique local classs - -Whenever you generate schema, the GDK checks the [SchemaDatabase]({{urlRoot}}/content/glossary#schemadatabase) and all the in-memory classes in your project and removes any classes referenced in the `SchemaDatabase` that no longer exist. - -This means that if you have a class that only exists on one user's machine (for example, a newly created class, or a class used for local testing) then these classes are automatically removed from the SchemaDatabase file whenever another user generates schema. - -To prevent this, commit newly created or modified classes to source control alongside the SchemaDatabase. diff --git a/SpatialGDK/Documentation/content/technical-overview/gdk-concepts.md b/SpatialGDK/Documentation/content/technical-overview/gdk-concepts.md deleted file mode 100644 index 126cf0705f..0000000000 --- a/SpatialGDK/Documentation/content/technical-overview/gdk-concepts.md +++ /dev/null @@ -1,89 +0,0 @@ -<%(TOC)%> - -> This page assumes that you’re familiar with Unreal Engine, but not with SpatialOS. - -# GDK concepts - -## Unreal Engine and GDK concept mapping -Key concepts in Unreal Engine map to concepts in the GDK, as shown in the table below. - -| Unreal Engine | GDK | More information | -| --- | --- | --- | -| Actor | Entity | An entity is made up of a set of SpatialOS components. Each component stores data about the entity. Note that SpatialOS components are **not** the same thing as Unreal Components. | -| Replicated property | SpatialOS component’s property | A type of data stored in a SpatialOS component.| -| RPC | SpatialOS component’s command or event | A type of data stored in a SpatialOS component. Note that a SpatialOS component’s command is **not** the same as an Unreal command. | -| Owning connection | EntityACL component | | -| Conditional property replication | Dynamic `component_delivery` filter | | -| Server | Server-worker instance | You can have multiple server-worker instances running the cloud element of your game. | - -You can find out more about [entities]({{urlRoot}}/content/glossary#spatialos-entity), SpatialOS [components]({{urlRoot}}/content/glossary#spatialos-component) and their properties, commands, events and [EntityACLs]({{urlRoot}}/content/glossary#access-control-list-acl), as well as the [`component_delivery` filter]({{urlRoot}}/content/glossary#component-interest) and [server-workers]({{urlRoot}}/content/glossary#workers), in the [glossary]({{urlRoot}}/content/glossary). - -## GDK for Unreal concepts -We’ve introduced some new concepts to facilitate the fact that SpatialOS enables you to spread computation between multiple servers - known as “server-worker instances” in SpatialOS. - -### Zoning -Because the GDK uses SpatialOS networking, you can have multiple server-worker instances simulating your game world. This allows you to extend the size of the world. - -We call this _zoning_ - splitting up the world into zones, known as “areas of authority”, each area simulated by one server-worker instance. This means that only one server-worker instance has authority to make updates to SpatialOS components at a time. - -> Support for zoning is currently in pre-alpha. We invite you to try out the [Multiserver Shooter tutorial]({{urlRoot}}/content/get-started/tutorial) and learn about how it works, but we don’t recommend you start developing features that use zoning yet. - -### Cross-server RPCs -To facilitate zoning, we created the concept of a cross-server RPC to make updates to Actors, known as “entities” in SpatialOS. This is a type of RPC that enables a server-worker instance which does not have authority over an entity to tell the server-worker instance that does have authority over that entity to make an update to it. This is necessary if you’re using zoning because areas of authority mean that one server-worker instance can't make updates to every entity in the world; it can make updates only to the entities in its area of authority. - -Player 1 and Player 2 are player entities in different areas of authority. - -When Player 1 shoots at Player 2, the server-worker instance that has authority over Player 1 (server-worker A) invokes a cross-server RPC. - -SpatialOS sends this to the server-worker instance that has authority over Player 2 (server-worker B). Server-worker B then executes the RPC. - -![Shooting across boundaries]({{assetRoot}}assets/screen-grabs/shooting-across-boundaries.png) -_Cross-server RPC: Player 1’s action affects Player 2, even though they are in different areas of authority being updated by different server-worker instances._ - -You set up a cross-server RPC in the same way as you would set up any other RPC within Unreal. - -Here’s an example of what one might look like: - -``` -UFUNCTION(CrossServer) -void TakeDamage(int Damage); -``` - -For more information, see the documentation on [cross-server RPCs]({{urlRoot}}/content/cross-server-rpcs). - -### Actor handover -If your game uses zoning, you need to make sure that entities can move seamlessly between areas of authority and the relevant server-worker instances can simulate them. - -In Unreal’s single-server architecture, authority over an Actor stays with the single server; an Actor’s properties never leave the server’s memory. With multiple server-worker instances in SpatialOS, authority needs to pass from one server-worker instance to another as an Actor moves around the game world. Passing authority, known as “Actor handover”, allows the second server-worker instance to continue where the first one left off. You set this up by adding the `Handover` tag to the Actor’s properties. - -> * Actors equate to “entities” in SpatialOS, so we refer to them as “entities” when we’re talking about what happens to them in the GDK, and “Actors” when we’re talking about what you need to do with them in Unreal. -> -> * Replicated Actor properties map to “properties” in an entity’s components in SpatialOS. - -![Moving across boundaries]({{assetRoot}}assets/screen-grabs/moving-across-boundaries.gif) - - _An AI following a player across the boundary between two server-worker instances' areas of authority. To demonstrate Actor handover, the AI changes its material every time authority is handed over._ - -See the [Multiserver Shooter tutorial](https://docs.improbable.io/unreal/alpha/content/get-started/tutorial) for a tutorial that demonstrates this functionality. - -For more information, see the documentation on [Actor handover]({{urlRoot}}/content/handover-between-server-workers). - -### Singleton Actors -You can use a Singleton Actor to define a “single source of truth” for operations or data across a game world that uses zoning. You can only have one instance of a Singleton Actor per game world. - -You create a Singleton Actor by tagging an Actor with the `SpatialType=Singleton` class attribute. For example, if you are implementing a scoreboard, you probably want only one scoreboard in your world, so you can tag the scoreboard Actor as a Singleton Actor. - -For more information, see the documentation on [Singleton Actors]({{urlRoot}}/content/singleton-actors). - -## Non-Unreal computation -By default, the GDK uses a single Unreal server-worker type (the template for a server-worker instance) to handle all server-side computation. However, you can set up additional server-worker types that do not use Unreal or the GDK. - -You can use these non-Unreal server-worker types to modularize your game’s functionality so you can re-use the functionality across different games. For example, you could use a non-Unreal server-worker type written in Python that interacts with a database or other third-party service, such as [Firebase](https://firebase.google.com/) or [PlayFab](https://playfab.com/). - -For more information, see the documentation on [non-Unreal server-worker types]({{urlRoot}}/content/non-unreal-server-worker-types). - -
- ------------- -2019-04-25 Page added with full editorial review -
\ No newline at end of file diff --git a/SpatialGDK/Documentation/content/technical-overview/gdk-principles.md b/SpatialGDK/Documentation/content/technical-overview/gdk-principles.md deleted file mode 100644 index 995a449f11..0000000000 --- a/SpatialGDK/Documentation/content/technical-overview/gdk-principles.md +++ /dev/null @@ -1,31 +0,0 @@ -<%(TOC)%> - -> This page assumes that you’re familiar with Unreal Engine, but not with SpatialOS. - -# Principles of the GDK for Unreal - -The SpatialOS Game Development Kit (GDK) for Unreal is a plugin which allows you to use the features of SpatialOS while developing with familiar Unreal Engine workflows and APIs. - -* **Unreal-first** - - We want experienced Unreal developers to benefit from the features of Unreal and take advantage of the SpatialOS platform, with a workflow that’s as native to Unreal as possible. - - To achieve this, we’ve created a version of Unreal Engine which provides SpatialOS networking alongside Unreal’s native networking. We maintain Unreal’s networking API, which means you don’t need to rewrite your game to make it work with the GDK.

- -* **No limits** - - An Unreal dedicated server is only as powerful as the single machine running it. The single machine quickly becomes a bottleneck in games with high numbers of Actors or complex game logic. - - You don’t have to make these technical tradeoffs with the GDK. SpatialOS can spread computation across multiple servers, allowing for far more complex games and much higher player counts.

- -* **Open development** - - The GDK is a community-driven project. We do all our development in the open and under an [MIT license](https://github.com/spatialos/UnrealGDK/blob/release/LICENSE.md). - - We value your contributions (see the [contribution guidelines](https://github.com/spatialos/UnrealGDK/blob/master/CONTRIBUTING.md)) and feature requests. Get in touch on the [forums](https://forums.improbable.io/tags/unreal-gdk) or on [Discord](https://discordapp.com/invite/RFB8S8C). - -
- ------------- -2019-04-25 Page added with full editorial review -
\ No newline at end of file diff --git a/SpatialGDK/Documentation/content/technical-overview/how-the-gdk-fits-in.md b/SpatialGDK/Documentation/content/technical-overview/how-the-gdk-fits-in.md deleted file mode 100644 index 768e49dc27..0000000000 --- a/SpatialGDK/Documentation/content/technical-overview/how-the-gdk-fits-in.md +++ /dev/null @@ -1,37 +0,0 @@ -<%(TOC)%> - -> This page assumes that you’re familiar with Unreal Engine, but not with SpatialOS. - -# How the GDK fits into your game stack -The GDK provides a networking integration with SpatialOS, which enables Unreal Engine 4 clients and servers to communicate with the SpatialOS Runtime to synchronize state. - -Using the GDK, you upload your built-out UE4 server binaries to SpatialOS, which runs them in a game instance. You can also upload clients to SpatialOS and distribute them to players using the [SpatialOS Launcher]({{urlRoot}}/content/glossary#launcher) for early playtesting. - -In addition, you can integrate systems from outside the game instance, such as inventory, authentication, and matchmaking, using SpatialOS’s tools and services. (See the [Player identity APIs and other platform services](https://docs.improbable.io/reference/latest/platform-sdk/introduction). - -The diagram below shows how SpatialOS and the GDK fit into a typical multiplayer game stack: - -![Game architecture]({{assetRoot}}assets/screen-grabs/game-architecture.png) -_The GDK provides a networking integration with SpatialOS, which enables Unreal Engine 4 clients and servers to communicate with the SpatialOS Runtime to synchronize state._ - -The GDK provides a networking integration with SpatialOS, which enables Unreal Engine 4 clients and servers to communicate with the SpatialOS Runtime to synchronize state. - -You upload your built-out UE4 server binaries to SpatialOS, which runs them in a game instance. You can also upload clients to SpatialOS and distribute them to players using the [SpatialOS Launcher]({{urlRoot}}/content/glossary#launcher) for early playtesting. - -You can integrate systems sitting outside the game instance, such as inventory, authentication and matchmaking, using SpatialOS’s [identity and platform services](https://docs.improbable.io/reference/latest/platform-sdk/introduction). - -## The GDK in more detail -In Unreal, game clients communicate with the game server using Unreal’s networking code. You can think of the GDK for Unreal as a plugin inside Unreal Engine that replaces this networking code. You can switch between Unreal networking and SpatialOS networking from the toolbar in the Unreal Editor. - -When we forked Unreal Engine, we extended Unreal’s `UIpNetDriver` (which orchestrates replication) to create a `USpatialNetDriver`. This handles the connection between the GDK and SpatialOS, and translates Unreal’s native replication updates and RPCs into instructions that SpatialOS can follow. We do this by using the `UnrealHeaderTool` to generate reflection data that we then turn into the SpatialOS data format called schema. - -![Networking switch]({{assetRoot}}assets/screen-grabs/networking-switch.png) -_Use the Unreal Editor toolbar networking switch to swap out native Unreal networking and swap in SpatialOS networking._ - -The SpatialOS model differs significantly from Unreal Engine when it comes to replicating an Actor. We don't replicate Actors to each player individually, as Unreal would. Instead, we update the game instance running in the cloud, and it’s SpatialOS which handles distributing this data to connected clients, so data is not sent multiple times to each interested client. - -
- ------------- -2019-04-25 Page added with full editorial review -
\ No newline at end of file diff --git a/SpatialGDK/Documentation/content/toolbars.md b/SpatialGDK/Documentation/content/toolbars.md deleted file mode 100644 index 50111367ab..0000000000 --- a/SpatialGDK/Documentation/content/toolbars.md +++ /dev/null @@ -1,113 +0,0 @@ -<%(TOC)%> -# Toolbars - -There are two toolbars you can use in the Unreal Editor: the main Unreal toolbar, and the SpatialOS GDK toolbar. Once enabled, the GDK toolbar sits alongside the main Unreal toolbar: - - ![Toolbar]({{assetRoot}}assets/screen-grabs/toolbar/toolbars.png) - -## Definitions: -`` - The root folder of your Unreal project. -`` - The folder containing your project’s `.uproject` and source folder (for example, `\ShooterGame\`). - -## Unreal toolbar - -Alongside the standard functionality, we’ve added some extra capabilities to the Unreal toolbar under the Play drop-down menu to help with debugging your game: - - ![Toolbar]({{assetRoot}}assets/screen-grabs/toolbar/multi-player-options.png) - -1. The Unreal<->SpatialOS networking switch -1. The “Number of Servers” multiplayer option -1. The "SpatialOS Settings" menu item which opens the [SpatialOS settings](#settings) - -### Switching between native Unreal networking and SpatialOS networking - -To switch from using Unreal networking to using SpatialOS networking, open the **Play** drop-down menu and check two checkboxes: - -* **Run Dedicated Server** -* **Spatial Networking** - -These settings are valid for Editor and command-line builds. They’re stored in your project's Unreal config file, `\Config\DefaultGame.ini`, under `\Script\EngineSettings.GeneralProjectSettings`. - -You can switch back by unchecking the boxes. - -> **Warning:** As the GDK is in alpha, switching back to Unreal default networking mode can be a useful way to debug and so speed up your development iteration. However, you lose access to the multiserver features of the GDK in Unreal default networking mode which may lead to erratic behavior. - -### Auto-generated launch config for PIE server-worker types - -You can launch multiple servers at the same time from within the Unreal Editor in [PIE (Unreal documentation)](https://docs.unrealengine.com/en-us/Engine/UI/LevelEditor/InEditorTesting#playineditor) configuration. To configure the number of servers launched, open the **Play** drop-down menu and use the slider `Number of Servers` within the `Multiplayer Options` section. - -If you want to connect multiple server-worker instances to SpatialOS, you need to tell SpatialOS how many instances to connect. You do this in the load balancing section of the launch configuration file (\spatial\default_launch.json`). However, by default, when you launch SpatialOS through the editor, this launch configuration file is auto-generated for you based on the settings specified in the [SpatialOS editor settings](#settings). - - This uses the [`rectangle_grid`](https://docs.improbable.io/reference/latest/shared/worker-configuration/load-balancer-config-2#rectangular-grid-rectangle-grid) strategy with 1 column and 1 row. To connect 2 servers, change this to 1 column and 2 rows (or vice-versa). Read more about the different kinds of load balancing strategies [here](https://docs.improbable.io/reference/latest/shared/worker-configuration/load-balancing). - -## SpatialOS GDK for Unreal toolbar - -The GDK toolbar provides several functions required for building and launching your client and server-workers from inside the Unreal Editor. - -### Add the GDK toolbar to your Unreal project - -> Note: If you based your project off the [Starter Template]({{urlRoot}}/content/get-started/gdk-template), the toolbar is already enabled. - -To enable the GDK toolbar, navigate to **Edit** > **Plugins** inside the Unreal Editor and scroll down to the bottom. Select the **SpatialOS** section and enable the toolbar: - -![Toolbar]({{assetRoot}}assets/screen-grabs/toolbar/enable-toolbar.png) - -#### Buttons - -The GDK toolbar has five features mapped to individual buttons and is displayed in the main editor toolbar to the right of the `Launch` button: - - ![Toolbar]({{assetRoot}}assets/screen-grabs/toolbar/toolbar-buttons.png) - -You can also access these from the **Window** menu: - - ![Toolbar]({{assetRoot}}assets/screen-grabs/toolbar/window-access.png) - -| Button | Description | -| --- | --- | -| Snapshot | Generates a [SpatialOS snapshot]({{urlRoot}}/content/glossary#snapshot). | -| Schema | Creates [schema]({{urlRoot}}/content/glossary#schema) for your Unreal project. | -| Start | Runs [`spatial worker build build-config`](https://docs.improbable.io/reference/latest/shared/spatial-cli/spatial-worker-build-build-config) to build worker configs and runs `spatial local launch` with the launch configuration specified in the settings (see [below](#settings)). | -| Stop | Stops `spatial local launch`. | -| Inspector | Opens the [Inspector]({{urlRoot}}/content/glossary#inspector) in a browser. | - -#### Settings - -The toolbar settings are in **Edit** > **Project Settings** > **SpatialOS GDK for Unreal** > **Settings**. - - ![Toolbar]({{assetRoot}}assets/screen-grabs/toolbar/toolbar-settings.png) - -##### General - -| Setting | Description | -| --- | --- | -| SpatialOS directory | If you're using a non-standard structure, you'll need to set this yourself. This is empty by default. If you leave it empty, it defaults to `/../spatial`. | - -##### Play In Editor Settings - -| Setting | Description | -| --- | --- | -| Delete dynamically spawned entities | If checked, the GDK deletes any dynamically spawned entities from your SpatialOS deployment when you end the PIE session. | - -##### Launch - -| Setting | Description | -| --- | --- | -| Command line flags for local launch | Command line flags passed in to `spatial local launch`. | -| Launch configuration | The [launch configuration file]({{urlRoot}}/content/glossary#launch-configuration-file) to use when running `spatial local launch` using the **Start** button. | -| Stop on exit | If checked, shuts down running deployments when you close the Unreal Editor. | -| Generate default launch config | If checked, the GDK creates a [launch configuration file]({{urlRoot}}/content/glossary#launch-configuration-file) by default when you launch a local deployment through the toolbar. | -| Launch configuration description | Auto-generated launch configuration description. The settings expose the configurations in the [launch config documentation](https://docs.improbable.io/reference/latest/shared/project-layout/launch-config). | - -##### Snapshots - -| Setting | Description | -| --- | --- | -| Snapshot path | Use this to specify the filepath of your [snapshot]({{urlRoot}}/content/glossary#snapshot). If you leave this empty, it defaults to `/../spatial/snapshots`. | -| Snapshot file name | Name of your snapshot file. | -| Generate placeholder entities in snapshot | If checked, the GDK adds [placeholder entities]({{urlRoot}}/content/spatialos-concepts/generating-a-snapshot#placeholder-entities) to the snapshot when it is generated | - -##### Schema Generation - -| Setting | Description | -| --- | --- | -| Output path for the generated schemas | Use this to specify the path of the generated [schema]({{urlRoot}}/content/glossary#schema) files. If you leave this empty, it defaults to `/../spatial/schema/improbable/unreal/generated/`. | diff --git a/SpatialGDK/Documentation/content/troubleshooting.md b/SpatialGDK/Documentation/content/troubleshooting.md deleted file mode 100644 index f551f3e524..0000000000 --- a/SpatialGDK/Documentation/content/troubleshooting.md +++ /dev/null @@ -1,81 +0,0 @@ -<%(TOC)%> -# Troubleshooting - -#### Q: -I've set up my Actor for replication according to Unreal Engine’s [documentation](https://docs.unrealengine.com/en-us/Gameplay/Networking/Actors), but my Actor does not replicate. - -#### A: -There could be a few different reasons for this. The list below provides some of the most common ones, ordered by likelihood: - -1. It's easy to forget to generate the schema for your replicated Actor. Make sure you run the Schema Generator before launching your project. - -1. As per Unreal Engine’s [replication documentation](https://docs.unrealengine.com/en-us/Gameplay/Networking/Actors), your Actor needs to be created on the server-worker before it can replicate to the client-workers. - -1. Ensure that your call to `SpawnActor` is happening on your server-worker.
-Validate that the SpatialOS entity that represents your Actor appears in the Inspector. If it doesn't, then it's likely that it's not marked up for replication correctly. - -1. Mark your Actor for replication as per [Unreal Engine’s Actor replication documentation](https://docs.unrealengine.com/en-us/Gameplay/Networking/Actors). You can validate that your Actor is replicated in `USpatialNetDriver::ServerReplicateActors`. - -1. Validate that you receive an `WORKER_OP_TYPE_ADD_ENTITY` for the entity representing your Actor in the `USpatialView::ProcessOps` and that your entity is spawned in `USpatialReceiver::CreateActor`. - -
------ - -#### Q: -I've moved my game over to the SpatialOS GDK for Unreal and am getting a crash in `UEngine::TickWorldTravel` when launching a PIE instance of my game. - -#### A: -Ensure that you have set up your Game Instance Class in your project settings (from the Unreal Editor, naviagte to: **Edit** > **Project Settings** > **Project** > **Maps & Modes** > **Game Instance Class**) to point to either `SpatialGameInstance` or a game instance that inherits from `USpatialGameInstance`. -
------ - -#### Q: -Can I change between Unreal Engine’s networking stack and the SpatialOS GDK for Unreal networking stack? - -#### A: -Yes you can! In the `Unreal Editor`, select the `Play` dropdown from the toolbar, and toggle the `Spatial Networking` checkbox to switch between the two networking stacks. -
------ - -#### Q: -I’m getting the following compilation error when building the GDK: `Error C2248: FRepLayout::Cmds': cannot access private member declared in class 'FRepLayout`. - -#### A: -You're building against an unsupported version of Unreal Engine. Make sure you're targeting the fork of Unreal Engine that the GDK requires. See the [setup guide]({{urlRoot}}/content/get-started/dependencies) for more details. -
------ - -### Q: -My game uses reliable multicast RPCs - why does the SpatialOS GDK for Unreal not support these? - -#### A: -The underlying implementation of multicast RPCs uses SpatialOS [events](https://docs.improbable.io/reference/latest/shared/glossary#event). SpatialOS events can only be sent unreliably. Additionally, the cost of a multicast RPC scales with the number of client-workers present in a deployment, which means they can get very expensive. A better approach would be to send RPCs to only the workers that are close to the broadcasting worker. -
------ - -#### Q: -When I build my project, I get the following error: `Unknown class specifier 'SpatialType'`. - -#### A: -`SpatialType` is a new class specifier for tagging of classes to replicate to SpatialOS. This message likely means that you have not built our UE4 fork, or pointed your project to use it. Follow the steps [here]({{urlRoot}}/content/get-started/introduction) to ensure the GDK is set up correctly. -
------ - -#### Q: -When I launch my SpatialOS deployment, I receive error messages similar to: `uses component ID 100005 which conflicts with components defined elsewhere.` - -#### A: -This means you were using the GDK since pre-alpha. To fix the issue, delete the contents of your `spatial/schema` folder, run `Setup.bat` again in the GDK folder, and generate the schemas again. You may also need to update your streaming queries in `spatialos.json`. Refer to our [Starter Template]({{urlRoot}}/content/get-started/gdk-template) to see an example. -
------ - -#### Q: -When I run `Setup.bat`, I get the following error: `error MSB3644: The reference assemblies for framework ".NETFramework,Version=4.5" were not found`. - -(The full error message looks like: -`C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\bin\Microsoft.Common.CurrentVersion.targets(1179,5): error MSB3644: The reference assemblies for framework ".NETFramework,Version=4.5" were not found. To resolve this, install the SDK or Targeting Pack for this framework version or retarget your application to a version of the framework for which you have the SDK or Targeting Pack installed. Note that assemblies will be resolved from the Global Assembly Cache (GAC) and will be used in place of reference assemblies. Therefore your assembly may not be correctly targeted for the framework you intend. [C:\MyProject\Game\Plugins\UnrealGDK\SpatialGDK\Build\Programs\Improbable.Unreal.Scripts\Build\Build.csproj]`) - -#### A: -When you install Visual Studio as part of the [GDK dependencies]({{urlRoot}}/content/get-started/dependencies) set up step, ensure you have selected the **Universal Windows Platform development** workload. This workload includes .NET Framework 4.5. If you have already installed Visual Studio, you can add the missing workload by running the Visual Studio installer and clicking **Modify** on your existing Visual Studio Installation. -
------ diff --git a/SpatialGDK/Documentation/content/tutorials/multiserver-shooter/tutorial-multiserver-cloudtest.md b/SpatialGDK/Documentation/content/tutorials/multiserver-shooter/tutorial-multiserver-cloudtest.md deleted file mode 100644 index 163e7405c0..0000000000 --- a/SpatialGDK/Documentation/content/tutorials/multiserver-shooter/tutorial-multiserver-cloudtest.md +++ /dev/null @@ -1,75 +0,0 @@ -<%(TOC)%> -# Multiserver Shooter tutorial - -## Step 4: Test your changes in the cloud - -### Build your assemblies - -An assembly is what’s created when you run `BuildWorker.bat`. They’re .zip files that contain all the files that your game uses when running in the cloud. - -1. In a terminal window, change directory to the root directory of the Third-Person Shooter repository. -1. Build a server-worker assembly by running: `Game\Plugins\UnrealGDK\SpatialGDK\Build\Scripts\BuildWorker.bat ThirdPersonShooterServer Linux Development ThirdPersonShooter.uproject` -1. Build a client-worker assembly by running: `Game\Plugins\UnrealGDK\SpatialGDK\Build\Scripts\BuildWorker.bat ThirdPersonShooter Win64 Development ThirdPersonShooter.uproject` - -
-### Upload your game - -1. In File Explorer, navigate to `UnrealGDKThirdPersonShooter\spatial` and open `spatialos.json` in a text editor. -1. Change the `name` field to the name of your project. You can find this in the [Console](https://console.improbable.io). It’ll be something like `beta_someword_anotherword_000`. - ![]({{assetRoot}}assets/tutorial/project-name.png) -1. In a terminal window, change directory to `UnrealGDKThirdPersonShooter\spatial\` and run `spatial cloud upload `, where `` is a name of your choice (for example `myassembly`). A valid upload command looks like this: - -``` -spatial cloud upload myassembly -``` - -> **Note:** Depending on your network speed it may take a little while (1-10 minutes) to upload your assembly. - -
-### Launch a cloud deployment - -The next step is to launch a cloud deployment using the assembly that you just uploaded. This can only be done through the SpatialOS command-line interface (also known as the [SpatialOS CLI]({{urlRoot}}/content/glossary#spatialos-command-line-tool-cli). - -When launching a cloud deployment you must provide three parameters: - -* **the assembly name**, which identifies the worker assemblies to use. -* **a launch configuration**, which declares the world and load balancing configuration. -* **a name for your deployment**, which is used to label the deployment in the [Console](https://console.improbable.io). - -1. In a terminal window, in the same directory you used to upload your game, run: `spatial cloud launch --snapshot=snapshots/default.snapshot two_worker_test.json ` -
where `assembly_name` is the name you gave the assembly in the previous step and `deployment_name` is a name of your choice. A valid launch command would look like this: - -``` -spatial cloud launch --snapshot=snapshots/default.snapshot myassembly two_worker_test.json mydeployment -``` - -> **Note:** This command defaults to deploying to clusters located in the US. If you’re in Europe, add the `--cluster_region=eu` flag for lower latency. - -
-### Play your game - -![]({{assetRoot}}assets/tutorial/console.png) - -When your deployment has launched, SpatialOS automatically opens the [Console](https://console.improbable.io) in your browser. - -1. In the Console, Select the **Launch** button on the left of the page, and then click the **Launch** button that appears in the centre of the page. The SpatialOS Launcher, which was installed along with SpatialOS, downloads the game client for this deployment and runs it on your local machine. - ![]({{assetRoot}}assets/tutorial/launch.png) -1. Once the client has launched, enter the game and fire a few celebratory shots - you are now playing in your first SpatialOS cloud deployment! - -
-### Invite your friends - -1. To invite other players to this game, head back to the Deployment Overview page in your [Console](https://console.improbable.io), and select the **Share** button. -1. Share the generated link with your friends! - -When you’re done shooting your friends, you can click the **Stop** button in the [Console](https://console.improbable.io) to stop your deployment. - -
-### Next steps -We hope you've enjoyed this tutorial. If you want to build a new game using the SpatialOS GDK, you should build it on top of the [SpatialOS GDK Starter template]({{urlRoot}}/content/get-started/gdk-template). If you want to port your existing game to SpatialOS, follow the [porting guide]({{urlRoot}}/content/tutorials/tutorial-porting-guide). - -
-
- -------------- -_2019-03-20 Page updated with limited editorial review_ diff --git a/SpatialGDK/Documentation/content/tutorials/multiserver-shooter/tutorial-multiserver-healthchanges.md b/SpatialGDK/Documentation/content/tutorials/multiserver-shooter/tutorial-multiserver-healthchanges.md deleted file mode 100644 index f58ecca4d2..0000000000 --- a/SpatialGDK/Documentation/content/tutorials/multiserver-shooter/tutorial-multiserver-healthchanges.md +++ /dev/null @@ -1,72 +0,0 @@ -<%(TOC)%> -# Multiserver Shooter Tutorial - -## Step 2: Replicate health changes - -In this project each `TPSCharacter` contains a variable called `CurrentHealth`, which keeps track of that character's health. On your servers, `CurrentHealth` is reduced whenever a character is shot, but this reduction is not replicated on the clients connected to the game. This is because the `CurrentHealth` variable is not setup for replication. - -To resolve this you need to mark the `CurrentHealth` property for replication, just as you would in the native [Unreal Actor replication](https://docs.unrealengine.com/en-us/Resources/ContentExamples/Networking/1_1) workflow. To do this: - -1. In your IDE, open `UnrealGDKThirdPersonShooter\Game\Source\ThirdPersonShooter\Characters\TPSCharacter.h`. -1. Navigate to the declaration of the `CurrentHealth` variable, and add the UProperty specifiers `ReplicatedUsing = OnRep_CurrentHealth`. The UProperty should now look like this: - -``` - UPROPERTY(ReplicatedUsing = OnRep_CurrentHealth) - int32 CurrentHealth; -``` - - You have now marked this property for replication using the `OnRep_CurrentHealth` function that you’ll implement in the next section. - - Next you need to update the [GetLifetimeReplicatedProps](https://wiki.unrealengine.com/Replication#Actor_Property_Replication) implementation of the `TPSCharacter` to specify the [replication conditions](https://docs.unrealengine.com/en-US/Gameplay/Networking/Actors/Properties/Conditions) for the `CurrentHealth` variable: - -1. In your IDE, open `UnrealGDKThirdPersonShooter\Game\Source\ThirdPersonShooter\Characters\TPSCharacter.cpp`. -1. Navigate to the `GetLifetimeReplicatedProps` function (which is implementd around line 182), and insert the following snippet: - -``` - // Only replicate health to the owning client. - DOREPLIFETIME_CONDITION(ATPSCharacter, CurrentHealth, COND_OwnerOnly); -``` - - > **Note:** You only want to replicate the `CurrentHealth` variable to the client that owns this Actor, thus you specify the `COND_OwnerOnly` flag. - - Finally, you need to implement the `OnRep_CurrentHealth` function so that the player health UI gets updated when the `CurrentHealth` variable is replicated: - -1. In your IDE, open `UnrealGDKThirdPersonShooter\Game\Source\ThirdPersonShooter\Characters\TPSCharacter.h`. -1. In the public scope of the class, insert the following snippet: - -``` - UFUNCTION() - void OnRep_CurrentHealth(); -``` - -1. In your IDE, open `UnrealGDKThirdPersonShooter\Game\Source\ThirdPersonShooter\Characters\TPSCharacter.cpp` and insert the following snippet: - -``` - void ATPSCharacter::OnRep_CurrentHealth() - { - if (GetNetMode() != NM_DedicatedServer) - { - ATPSPlayerController* PC = Cast(GetController()); - if (PC) - { - PC->UpdateHealthUI(CurrentHealth, MaxHealth); - } - else - { - UE_LOG(LogTPS, Warning, TEXT("Couldn't find a player controller for character: %s"), *this->GetName()); - } - } - } -``` - -Notice that the workflow you just used mirrors that of native Unreal. - -Because you have changed code in a function, you now need to rebuild your project. Additionally, because you've modified code related to replication, you need to generate schema. To do this: - -1. Open **ThirdPersonShooter.sln** with Visual Studio. -1. In the Solution Explorer window, right-click on **ThirdPersonShooter** and select **Build**. -1. Open **ThirdPersonShooter.uproject** in the Unreal Editor and click `Schema` and then `Snapshot`. - -Now let’s test our health replication in another local deployment. - -[Step 3: Test your changes locally]({{urlRoot}}/content/tutorials/multiserver-shooter/tutorial-multiserver-localtest) diff --git a/SpatialGDK/Documentation/content/tutorials/multiserver-shooter/tutorial-multiserver-intro.md b/SpatialGDK/Documentation/content/tutorials/multiserver-shooter/tutorial-multiserver-intro.md deleted file mode 100644 index 7c8e58b073..0000000000 --- a/SpatialGDK/Documentation/content/tutorials/multiserver-shooter/tutorial-multiserver-intro.md +++ /dev/null @@ -1,13 +0,0 @@ -<%(TOC)%> - -# Multiserver Shooter tutorial - -**What the tutorial covers**
-In this tutorial you’ll implement cross-server remote procedure calls (RPCs) in a simple third person shooter. The end result will be a multiplayer, cloud-hosted Unreal game running across multiple [server-workers]({{urlRoot}}/content/glossary#inspector) that players can seamlessly move between and shoot across. It will look something like this: - -![]({{assetRoot}}assets/tutorial/cross-server.gif) - -The exercise demonstrates that the workflows and iteration speed you’re used to as an Unreal developer are almost entirely unaltered by the GDK: it’s just like regular Unreal! -
-[Step 1: Setup]({{urlRoot}}/content/tutorials/multiserver-shooter/tutorial-multiserver-setup) - diff --git a/SpatialGDK/Documentation/content/tutorials/multiserver-shooter/tutorial-multiserver-localtest.md b/SpatialGDK/Documentation/content/tutorials/multiserver-shooter/tutorial-multiserver-localtest.md deleted file mode 100644 index 5bf3038046..0000000000 --- a/SpatialGDK/Documentation/content/tutorials/multiserver-shooter/tutorial-multiserver-localtest.md +++ /dev/null @@ -1,112 +0,0 @@ -<%(TOC)%> -# Multiserver Shooter Tutorial - -## Step 3: Test your changes locally - -1. In Unreal Editor, in the SpatialOS GDK toolbar, select **Start**. It's ready when you see `SpatialOS ready. Access the inspector at [http://localhost:21000/inspector]()`. -1. From the Unreal Editor toolbar, click **Play** to run the game. - -Notice that health now decrements when you are shot. - -
-### View your SpatialOS world in the Inspector - -![]({{assetRoot}}assets/tutorial/inspector-two-workers.png) - -_Image: A local Inspector showing two server-worker instances (two Unreal servers) managing your game_
- -[The Inspector]({{urlRoot}}/content/glossary#inspector) provides a real-time view of what is happening in your [SpatialOS world]({{urlRoot}}/content/glossary#game-world). It’s a powerful tool for monitoring and debugging both during development and when your game is live in production. Let’s use the Inspector to visualise the areas that each of our server-worker instances have [authority]({{urlRoot}}/content/glossary#authority) (that is, read and write access) over. - -1. Access the Inspector at [http://localhost:21000/inspector](http://localhost:21000/inspector). -1. In the **View** tab, check the boxes next to both of the **UnrealWorkers**. -1. In the **Show me** option, select **Authority / interest**.
- This causes the Inspector to display the areas that each server-worker instance has authority over as two colored zones. -1. Back in your two Unreal game clients, run around and shoot. -1. Using the Inspector to track the location of your two players, notice that if you position them in the same area of authority then their shots damage each other, but if they are on different servers, they can’t damage each other. Let’s fix that. - -
-### Enable cross server RPCs - -To damage a player on a different server, the actor shooting the bullet must send a cross-server RPC to the actor getting hit by the bullet. You will implement this by overriding the [TakeDamage (Unreal documentation)](https://api.unrealengine.com/INT/API/Runtime/Engine/GameFramework/APawn/TakeDamage/index.html) function in the TPSCharcter class. - -1. In your IDE, open `UnrealGDKThirdPersonShooter\Game\Source\ThirdPersonShooter\Characters\TPSCharacter.h`. -1. On line 74, add this snippet: - - ``` - UFUNCTION(CrossServer, Reliable) - void TakeGunDamageCrossServer(float Damage, const struct FDamageEvent& DamageEvent, AController* EventInstigator, AActor* DamageCauser); - ``` - - This snippet creates a new `UFUNCTION` marked with the function tags [CrossServer]({{urlRoot}}/content/cross-server-rpcs) and [Reliable (Unreal documentation)](https://wiki.unrealengine.com/Replication#Reliable_vs_Unreliable_Function_Call_Replication). The CrossServer tag forces this function to be executed as a cross-server RPC. - -1. In your IDE, open `UnrealGDKThirdPersonShooter\Game\Source\ThirdPersonShooter\Characters\TPSCharacter.cpp`. -1. Replace the `TakeDamage` function (lines 514-548) with this snippet: - -``` -float ATPSCharacter::TakeDamage(float Damage, const FDamageEvent& DamageEvent, AController* EventInstigator, AActor* DamageCauser) -{ - TakeGunDamageCrossServer(Damage, DamageEvent, EventInstigator, DamageCauser); - - return Damage; -} - -void ATPSCharacter::TakeGunDamageCrossServer_Implementation(float Damage, const FDamageEvent& DamageEvent, AController* EventInstigator, AActor* DamageCauser) -{ - if (!HasAuthority()) - { - return; - } - - const ATPSCharacter* Killer = nullptr; - - // Ignore friendly fire - const AInstantWeapon* DamageSourceWeapon = Cast(DamageCauser); - if (DamageSourceWeapon != nullptr) - { - const ATPSCharacter* DamageDealer = Cast(DamageSourceWeapon->GetWeilder()); - if (DamageDealer != nullptr) - { - if (Team != ETPSTeam::Team_None // "Team_None" is not actually a team, and "teamless" should be able to damage one another - && DamageDealer->GetTeam() == Team) - { - return; - } - Killer = DamageDealer; - } - } - - int32 DamageDealt = FMath::Min(static_cast(Damage), CurrentHealth); - CurrentHealth -= DamageDealt; - - if (CurrentHealth <= 0) - { - Die(Killer); - } -} -``` - -This snippet implements the functionality that was previously contained within `TakeDamage` as a cross-server RPC called `TakeGunDamageCrossServer`. - -Because you have changed code in a function, you now need to rebuild your project. Additionally, because you've enabled replication for a variable, you need to generate schema. To do this: - -1. Open **ThirdPersonShooter.sln** with Visual Studio. -1. In the Solution Explorer window, right-click on **ThirdPersonShooter** and select **Build**. -1. Open **ThirdPersonShooter.uproject** in the Unreal Editor and click `Schema` and then `Snapshot`. - -Now let’s test our new cross-server functionality in another local deployment. - -
-### Deploy the project locally (last time) - -1. In Unreal Editor, in the SpatialOS GDK toolbar, select **Start**. It's ready when you see `SpatialOS ready. Access the inspector at [http://localhost:21000/inspector]()`. -1. From the Unreal Editor toolbar, click **Play** to run the game. -1. Using the Inspector to track the location of your two players, notice that you can now shoot between two Unreal servers and cause damage across their boundaries (provided the two players are on different teams!). - -![]({{assetRoot}}assets/tutorial/shooting-across-boundaries.gif)
-*Image: Players running and shooting between two Unreal Servers* - -Now that you're free of the single-server paradigm, have a think about the huge, seamless multiplayer worlds you can build and host using the Unreal GDK. - -Speaking of hosting, let’s upload your game. - -[Step 4: Test your changes in the cloud]({{urlRoot}}/content/tutorials/multiserver-shooter/tutorial-multiserver-cloudtest) diff --git a/SpatialGDK/Documentation/content/tutorials/multiserver-shooter/tutorial-multiserver-setup.md b/SpatialGDK/Documentation/content/tutorials/multiserver-shooter/tutorial-multiserver-setup.md deleted file mode 100644 index a1c3666bf9..0000000000 --- a/SpatialGDK/Documentation/content/tutorials/multiserver-shooter/tutorial-multiserver-setup.md +++ /dev/null @@ -1,90 +0,0 @@ -<%(TOC)%> - -# Multiserver Shooter tutorial - -## Step 1: Set up - -### Check you have the prerequistes - -Before following the Multiserver Shooter tutorial, you need to have followed: - -* [Getting started: 1 - Dependencies]({{urlRoot}}/content/get-started/dependencies) -* [Getting started: 2 - Get and build the SpatialOS Unreal Engine Fork]({{urlRoot}}/content/get-started/build-unreal-fork). - -Once you have done this, you are ready to get going with the Multiserver Shooter tutorial by following the steps below. -
-
-**Let's get started!**
-
- -### Clone the Unreal GDK Third Person Shooter repository - -Clone the Unreal GDK Third Person Shooter repository and checkout the tutorial branch using one of the following commands: - -| | | -| -------- | ---- | -| **HTTPS:** | `git clone https://github.com/spatialos/UnrealGDKThirdPersonShooter.git -b tutorial`| -| **SSH:** | `git clone git@github.com:spatialos/UnrealGDKThirdPersonShooter.git -b tutorial`| - -This repository contains a version of Unreal’s Third Person template that has been ported to the SpatialOS GDK. It includes a character model with a gun and hit detection logic. - -> **Note:** A completed version of this tutorial is available in the `tutorial-complete` branch. - -
-### Clone the GDK into the `Plugins` directory - -1. Navigate to `UnrealGDKThirdPersonShooter\Game` and create a `Plugins` directory. -1. In a terminal window, change directory to the `Plugins` directory and clone the [Unreal GDK](https://github.com/spatialos/UnrealGDK) repository using one of the following commands: - -| | | -| -------- | ---- | -| **HTTPS:** | `git clone https://github.com/spatialos/UnrealGDK.git`| -| **SSH:** | `git clone git@github.com:spatialos/UnrealGDK.git`| - -The GDK's [default branch (GitHub documentation)](https://help.github.com/en/articles/setting-the-default-branch) is `release`. This means that, at any point during the development of your game, you can get the latest release of the GDK by running `git pull` inside the `UnrealGDK` directory. When you pull the latest changes, you must also run `git pull` inside the `UnrealEngine` directory, so that your GDK and your Unreal Engine fork remain in sync. - -> **Note:** You need to ensure that the root folder of the Unreal GDK repository is called `UnrealGDK` so its path is: `UnrealGDKThirdPersonShooter\Game\Plugins\UnrealGDK\`. - -
- -### Build dependencies - -In this step, you're going to build the Unreal GDK's dependencies. - -1. Open **File Explorer**, navigate to the root directory of the GDK for Unreal repository (`ThirdPersonShooter\Plugins\UnrealGDK\...`), and double-click `Setup.bat`. If you haven't already signed into your SpatialOS account, the SpatialOS developer website may prompt you to sign in. -1. In **File Explorer**, navigate to the ThirdPersonShooter directory, right-click `ThirdPersonShooter.uproject` and select Generate Visual Studio Project files. -1. In the same directory, double-click `ThirdPersonShooter.sln` to open it with Visual Studio. -1. In the Solution Explorer window, right-click on **ThirdPersonShooter** and select **Build**. -1. When Visual Studio has finished building your project, right-click **ThirdPersonShooter** and select **Set as StartUp Project**. -1. Press F5 on your keyboard or select **Local Windows Debugger** in the Visual Studio toolbar to open your project in the Unreal Editor.
-![Visual Studio toolbar]({{assetRoot}}assets/set-up-template/template-vs-toolbar.png)
-_Image: The Visual Studio toolbar_ -1. In the GDK toolbar, select **Schema** to generate the SpatialOS schema based on your Unreal project. (Schema is a definition of the components and entities your SpatialOS world can have, see the glossary for more details on [schema](https://docs.improbable.io/reference/latest/shared/glossary).)
-![Toolbar]({{assetRoot}}assets/screen-grabs/toolbar/schema-button.png)
-_Image: On the GDK toolbar in the Unreal Editor select **Schema**_
-1. Select [**Snapshot**]({{urlRoot}}/content/generating-a-snapshot) to generate a snapshot (a representation of the state of the SpatialOS world) which will be used to start the deployment.
-![Toolbar]({{assetRoot}}assets/screen-grabs/toolbar/snapshot-button.png)
-_Image: On the GDK toolbar in the Unreal Editor select **Snapshot**_
- -
-### Deploy the project locally - -In this section you’ll run a [local deployment](https://docs.improbable.io/reference/latest/shared/glossary#local-deployment) of the project. As the name suggests, local deployments run on your development machine (you will run a [cloud deployment](https://docs.improbable.io/reference/latest/shared/glossary#cloud-deployment) later in this tutorial). - -1. In the Unreal Editor, on the Unreal toolbar, open the **Play** drop-down menu.
-1. Under **Multiplayer Options**, enter the number of players as **2**. -1. Enter the number of servers as **2**. -1. Ensure the box next to **Run Dedicated Server** is checked.
-![]({{assetRoot}}assets/set-up-template/template-multiplayer-options.png)
-_Image: The Unreal Engine **Play** drop-down menu, with **Multiplayer Options** and **New Editor Window (PIE)** highlighted_
-1. Within the **Play** drop-down menu open **SpatialOS Settings**. -1. Change **Launch configuration file description > Workers > 0 > Rectangle grid row count** to **2**. -1. In the Unreal Editor, in the SpatialOS GDK toolbar, select **Start** (the green play icon). This opens a terminal window and runs the [`spatial local launch`](https://docs.improbable.io/reference/latest/shared/spatial-cli/spatial-local-launch#spatial-local-launch) command, which starts the [SpatialOS Runtime](https://docs.improbable.io/reference/latest/shared/glossary#the-runtime). -1. It's ready when you see `SpatialOS ready. Access the inspector at http://localhost:21000/inspector`. -1. From the Unreal Editor toolbar, select **Play** to run the game. This starts two SpatialOS server-worker instances and two SpatialOS client-worker instances locally, in your Unreal Editor. -
The two server-worker instances are acting as two Unreal servers and the two client-worker instances are acting as two Unreal game clients (as would be used by two game players). -
(You can find out about workers in the [glossary](https://docs.improbable.io/unreal/alpha/content/glossary#workers).) - -Notice that when players shoot each other, their health does not go down. It's not much fun with no skin in the game is it? Let’s fix the health system. - -[Step 2: Replicate health changes]({{urlRoot}}/content/tutorials/multiserver-shooter/tutorial-multiserver-healthchanges) diff --git a/SpatialGDK/Documentation/content/tutorials/tutorial-porting-guide.md b/SpatialGDK/Documentation/content/tutorials/tutorial-porting-guide.md deleted file mode 100644 index 9a16420972..0000000000 --- a/SpatialGDK/Documentation/content/tutorials/tutorial-porting-guide.md +++ /dev/null @@ -1,227 +0,0 @@ -<%(TOC)%> -# Get started: Port your own Unreal project to the GDK - -<%(Callout type="warn" message="The GDK's porting guide is currently in alpha as we improve its stability. We do not recommend attempting to port your Unreal game now. If you need to port your game, please contact us via our [forums](https://forums.improbable.io/), or [Discord](https://discord.gg/vAT7RSU) so we can best support you.")%> - -This guide shows you how to port your own Unreal project to SpatialOS using the GDK for Unreal. By the end of this guide, your game will run on a single server-worker and you will be ready to start adding multiserver logic to take advantage of the distributed architecture of SpatialOS. - -## Before you start - -Before porting your project: - -* If you haven't done this already, follow our Get Started guide before porting your game: - * [Get started: 1 - Dependencies]({{urlRoot}}/content/get-started/dependencies) - * [Get started: 2 - Get and build the GDK’s Unreal Engine Fork]({{urlRoot}}/content/get-started/build-unreal-fork) - * [Get started: 3 - Set up the SpatialOS GDK Starter Template]({{urlRoot}}/content/get-started/gdk-template) - -* Open a terminal window and run the command `spatial update` to ensure your [spatial CLI]({{urlRoot}}/content/glossary#spatialos-command-line-tool-cli) installation is up to date. - -### Terms used in this guide -* `` - The directory containing your project's `.uproject` file and `Source` directory. -* `` - The directory containing your ``. -* `` - The name of your project's `.uproject` file (for example, `\\TP_SpatialGDK.uproject`). - - - - -> **TIP: Reference project** -

-> As you port your own Unreal project to SpatialOS, you could use our pre-ported [Unreal Shooter Game](https://docs.unrealengine.com/en-us/Resources/SampleGames/ShooterGame) as a reference. You should already have this project as it is included in the `Samples` directory of [the SpatialOS Unreal Engine fork](https://github.com/improbableio/UnrealEngine) which you downloaded as part of the _Get Started_ steps. -

-> (If you want to see the game running, there's a [video on youtube](https://www.youtube.com/watch?v=xojgH7hJgQs&feature=youtu.be) to check out.) - - -## Port your game to the GDK - -### 1. Set up the project structure - - -1. Create a new empty directory to represent your `` and move your `` directory inside of it. - - Your project structure should be: `\\\.uproject`
- - For example: - `\MyProject\Game\TP_SpatialGDK.uproject` - -2. Your project needs some extra files and folders to run with the GDK. Copy these files from the template project that you set up earlier in the [Before you start](#before-you-start) section. - - To do this: either in a terminal window or your file manager, navigate to the root of the `StarterTemplate` repository and copy all of the files and directories below to your ``: - - ``` cpp - \TP_SpatialGDK\spatial\ - \TP_SpatialGDK\LaunchClient.bat - \TP_SpatialGDK\LaunchServer.bat - \TP_SpatialGDK\ProjectPaths.bat - ``` - Your project's directory structure should now resemble: - - ``` cpp - \\\ - \\spatial\ - \\LaunchClient.bat - \\LaunchServer.bat - \\ProjectPaths.bat - etc... - ``` - - **Note**: You must place the `spatial` directory in the directory above your ``. -

- -1. You need to configure the GDK helper scripts to work with your project. - -Follow this step to set up your project paths: - - * Open **`\\ProjectPaths.bat`** in a text editor. - - * In `set PROJECT_PATH=Game`, replace `Game` with your `` folder name. - * In `set GAME_NAME= TP_SpatialGDK `, replace `TP_SpatialGDK ` with the name of your game's `.uproject` (`` [terms used in this guide](#terms-used-in-this-guide)). - - **Note**: The helper scripts `LaunchClient.bat` and `LaunchServer.bat` will not work if you do not follow this step correctly. - -### 2. Clone the GDK - -Now you need to clone the SpatialOS GDK for Unreal into your project. To do this: - -1. In **File Explorer**, navigate to the `` directory and create a `Plugins` folder in this directory. -2. In a Git Bash terminal window, navigate to `\Plugins` and clone the [GDK for Unreal](https://github.com/spatialos/UnrealGDK) repository by running either: - * (HTTPS) `git clone https://github.com/spatialos/UnrealGDK.git` - * (SSH) `git clone git@github.com:spatialos/UnrealGDK.git`

-**Note:** You need to ensure that the root directory of the GDK for Unreal repository is called `UnrealGDK` so the file path is: `\Plugins\UnrealGDK\...`
-3. Run `Setup.bat` which is in the root directory of the GDK repository (this should be `\\Plugins\UnrealGDK\`). To do this either: - - In a terminal window, navigate to the root directory of the GDK and run: `Setup.bat` or - - In your file manager, double-click the `Setup.bat` file. - - - **Note**:`Setup.bat` will automatically open the SpatialOS authorization page in your default browser. You may be prompted to sign into your SpatialOS account if you have not already. - -### 3. Add the SpatialGDK module to your project - -1. In **File Explorer**, navigate to `\\\Source\\`. -2. Open the `.build.cs` file in a code editor and add add `"SpatialGDK"` to `PublicDependencyModuleNames`. - - For example: - - ``` csharp - PublicDependencyModuleNames.AddRange(` - new string[] { - "Core", - "CoreUObject", - "Engine", - "OnlineSubsystem", - "OnlineSubsystemUtils", - "AssetRegistry", - "AIModule", - "GameplayTasks", - "SpatialGDK", - } - ); - ``` - -### 3. Build your project -Set up your Unreal project to work with the GDK for Unreal fork of the Unreal Engine, which you cloned and installed in the [Before you start](#before-you-start) section. To do this: - -1. In **File Explorer**, navigate to `\`. -1. Right-click your `.uproject` file and select **Switch Unreal Engine version**. -1. Select the path to the Unreal Engine fork you cloned earlier. -1. In **File Explorer**, right-click ``.uproject and select **Generate Visual Studio Project files**. This automatically generates a Visual Studio solution file for your project called `` -1. In the same directory, double-click ``.sln to open it with Visual Studio. -1. On the Visual Studio toolbar, set your Solution configuration to **Development Editor**.
- ![Visual studio toolbar]({{assetRoot}}assets/screen-grabs/porting-solution-config.png)
- _Image: The Visual Studio toolbar, with the Development Editor Solution configuration highlighted in red._ -1. In the Solution Explorer window, right-click on **``** and select **Build**. - - -### 5. Modify Unreal classes for GDK compatibility -You must modify your `GameInstance` class to work properly with the GDK. - -1. Make your `GameInstance` inherit from `SpatialGameInstance`.
- - > If you have not made a `GameInstance` for your game and are still using the default `GameInstance`, you must either create a Blueprint or a native `GameInstance` class now. Remember to configure your `Project Settings` to use this new `GameInstance` by default, under **Project Settings > Project Maps and Modes > Game Instance > Game Instance Class**.
- -* If your game's `GameInstance` is a C++ class, locate its header file and add the following `#include`: - `"SpatialGameInstance.h"` - - For example: - - ``` cpp - #include "CoreMinimal.h" - #include "SpatialGameInstance.h" - #include "YourProjectGameInstance.generated.h" - ``` - Then, under `UCLASS()`, change the parent class from `UGameInstance` to `USpatialGameInstance`: - - For example: - - ```cpp - UCLASS() - class YOURPROJECT_API UYourProjectGameInstance : public USpatialGameInstance - { - GENERATED_BODY() - }; - ``` - -* If your `GameInstance` is a Blueprint class, you need to open and edit it in the Blueprint Editor: - * From the Blueprint Editor toolbar, navigate to the **Class Settings**. In **Class Options** set the **Parent Class** to `SpatialGameInstance`. - - ![spatial game instance reparent]({{assetRoot}}assets/screen-grabs/spatial-game-instance-reparent.png)
_Image: The Blueprint class settings screen_
- -### 6. Generate schema and a snapshot -You need to generate [schema]({{urlRoot}}/content/spatialos-concepts/schema) and generate a [snapshot]({{urlRoot}}/content/spatialos-concepts/generating-a-snapshot) before you start your deployment. To do this: - -1. In the Unreal Editor, on the [GDK toolbar]({{urlRoot}}/content/toolbars), select **Schema** to run the [Schema Generator]({{urlRoot}}/content/glossary#schema-generation). -1. On the same toolbar, select **Snapshot**, which will generate a snapshot for the map currently open in the editor. - - ![Toolbar]({{assetRoot}}assets/screen-grabs/toolbar/toolbars-basic.png)
_Image: The GDK for Unreal toolbar_
- -### 7. Launch your game -1. Switch your game project to use the SpatialOS networking. To do this: - -* In the Unreal Editor, from the toolbar, open the **Play** drop-down menu and check two checkboxes: - * Check the box for **Run Dedicated Server** - * Check the box for **Spatial Networking** - - ![Toolbar]({{assetRoot}}assets/screen-grabs/toolbar/toolbar-checkboxes.png)
- _Image: The Unreal Engine launch settings drop-down menu_

- - > You can increase the number of servers that you launch by changing the **Number of servers** value. Leave this value at 1 for now. This is because there is currently no multiserver logic in your code. After you have completed this guide you can start building multiserver game logic. - -1. On the [GDK toolbar]({{urlRoot}}/content/toolbars), select **Start**. This builds your [worker configuration]({{urlRoot}}/content/glossary#worker-configuration) file and launches your game in a local deployment.
![Toolbar]({{assetRoot}}assets/screen-grabs/toolbar/start-button.png)
_Image: On the GDK toolbar in the Unreal Editor select **Start**_

-Selecting **Start** opens a terminal window and runs two SpatialOS command line interface ([CLI]({{urlRoot}}/content/glossary#spatialos-command-line-tool-cli) commands: `spatial build build-config` and `spatial local launch`. Your deployment has started when you see `SpatialOS ready` in the terminal window.

-1. On the main Unreal toolbar, select **Play**.
![Toolbar]({{assetRoot}}assets/screen-grabs/toolbar/play-button.png)
_Image: On the Unreal Engine toolbar **Play**_

-1. From the SpatialOS [GDK toolbar]({{urlRoot}}/content/toolbars) select **Inspector**, which will open a local [SpatialOS inspector](https://docs.improbable.io/reference/latest/shared/operate/inspector) in your default web browser. Here you can see the entities and their components present in your deployment.
![Toolbar]({{assetRoot}}assets/screen-grabs/toolbar/inspector-button.png)
_Image: On the GDK toolbar in the Unreal Editor select **Inspector**_
- - -**For running a local deployment with managed workers or a cloud deployment take a look at the [glossary section for deployments]({{urlRoot}}/content/glossary#deployment)** - -You have now ported your Unreal game to run on SpatialOS. Move around and look at the changes reflected in the inspector. - -If you have encountered any problems please check out our [troubleshooting]({{urlRoot}}/content/troubleshooting) and [known-issues]({{urlRoot}}/known-issues). - -#### Logs -You can find Spatial log files for your local deployments in `\spatial\logs\`. - - -* `spatial_.log` contains all of the logs printed to your terminal during the local deployment. -* There are also timestamped folders here which contain additional logs: - 1. `\spatial\logs\workers\` contain managed worker logs which are the workers started by SpatialOS, specified in your [launch configuration]({{urlRoot}}/content/glossary#launch-configuration). - 1. `\spatial\logs\runtime.log` contains the logs printed by the SpatialOS Runtime. These are the services required for SpatialOS to run a local deployment. - -If you require additional debugging logs you can run `spatial local launch` with the flag `--log_level=debug`. - -#### How to modify the default behavior -You can modify some of the GDK settings from the Unreal Editor toolbar at **Edit** > **Project Settings** >**SpatialOS Unreal GDK** > **Toolbar**. -You can change: - -* the snapshot file's filename and location -* the launch configuration - -## Next steps - -If you haven't already, check out the Multiserver shooter tutorial tutorial to learn how to implement [cross-server interactions]({{urlRoot}}/content/tutorials/multiserver-shooter/tutorial-multiserver-intro). -Also check out the documentation on [cross-server RPCs]({{urlRoot}}/content/cross-server-rpcs), [handover]({{urlRoot}}/content/handover-between-server-workers) and [Singleton Actors]({{urlRoot}}/content/singleton-actors). - - -
- ------- -_2019-04-11 Added ShooterGame as a reference project with partial editorial review._ diff --git a/SpatialGDK/Documentation/content/upgrading.md b/SpatialGDK/Documentation/content/upgrading.md deleted file mode 100644 index b4387ccb7c..0000000000 --- a/SpatialGDK/Documentation/content/upgrading.md +++ /dev/null @@ -1,47 +0,0 @@ -<%(TOC)%> -# Keep your GDK up to date - -To use the SpatialOS GDK for Unreal, you need software from two git repositories:
- -* [The SpatialOS Unreal Engine fork](https://github.com/improbableio/UnrealEngine) -* [The GDK](https://github.com/spatialos/UnrealGDK)
-You download both of these as part of the [Get started]({{urlRoot}}/content/get-started/introduction) steps.
-To ensure you benefit from the most up-to-date functionality, always develop your game on the latest version of the software by regularly updating it. Whenever you update your GDK software, you **must** also update your SpatialOS Unreal Engine fork software. If you don't, you might get errors from them being out of synch. - -We recommend that you update your version of the GDK and SpatialOS Unreal Engine fork every week. To do this, follow the steps below. - -## Step 1: Ensure you're on the release branches. - -If you followed our [Get started]({{urlRoot}}/content/get-started/introduction) guide, you have these repositories cloned on your computer.
- -* Your `UnrealEngine` repository should have the branch ending with `-SpatialOSUnrealGDK-release`checked out.
-* Your `UnrealGDK` repository should have the `release` branch checked out.
- -You can find out which branch you have checked out by following the instructions below:
- -1. In a terminal of your choice, change directory to the root of the repository.
-1. Run `git status`. -This should return `On *-SpatialOSUnrealGDK-release` in your `UnrealEngine` repository and `On release` in your `UnrealGDK` repository.
-If it returns a different branch, run `git checkout ` to check out the branch that you want. - -## Step 2: Update your Unreal Engine fork and GDK. - -Before you begin, read the release notes on the releases page of the [`UnrealGDK` GitHub](https://github.com/spatialos/UnrealGDK/releases) so you understand the changes that you're about to download. - -To update your Unreal Engine fork and GDK to the latest version, complete the following steps: - -1. In a terminal, change directory to the root of `UnrealEngine`. -1. Run `git pull` to update your Unreal Engine. -1. In a terminal, change directory to the root of `UnrealGDK`. -1. Run `git pull` to update your GDK. -1. Open **File Explorer**, navigate to the root directory of the Unreal GDK repository, and then double-click **`Setup.bat`**. You might be prompted to sign into your SpatialOS account if you have not signed in yet. -1. In **File Explorer**, navigate to the `` directory that contains your project's `.uproject` file.
-Right-click on your `.uproject` file and select **Generate Visual Studio project files**. - -You are now on the latest GDK and the latest SpatialOS Unreal Engine fork. - -Be sure to join the community on our forums or on Discord. We announce GDK versions there. - ------ - -_2019-04-15 Page added with editorial review_ diff --git a/SpatialGDK/Documentation/contributions/unreal-gdk-coding-standards.md b/SpatialGDK/Documentation/contributions/unreal-gdk-coding-standards.md deleted file mode 100644 index ad21bad938..0000000000 --- a/SpatialGDK/Documentation/contributions/unreal-gdk-coding-standards.md +++ /dev/null @@ -1,43 +0,0 @@ - -<%(TOC)%> -# GDK for Unreal C++ coding standards - -> In general, we follow the [Coding Standard](https://docs.unrealengine.com/en-us/Programming/Development/CodingStandard) (Unreal documentation) set out by Epic. This page highlights some additions to Epic's guidelines. - -* Avoid assignments in if statements unless the variable is declared in the statement. - - It's good practice to limit the lifetime of a variable using the scope. For instance if a variable is only used within an if statement, then its lifetime is cleanly managed using the following approach: - - if (APawn* Pawn = Cast(Actor)) - { - //do something with the Pawn variable - } - - This is safe as the compiler will generate a C2143 compiler error if a comparison is accidently added: - - if (APawn* Pawn == Cast(Actor)) - { - // will generate a C2143 - } - - However, if the variable is declared earlier in the outside scope and cannot be contained within the scope of the statement, then we should avoid using assignments in the statement: - - For example, prefer: - - APawn* Pawn = Cast(Actor); - if (Pawn != nullptr) - { - //do something with the Pawn variable - } - - to: - - APawn* Pawn; - - if(Pawn = Cast(Actor)) //NOOOO - { - - } - -* Wrap native C++ classes/structs in the improbable namespace - For any object that is not tagged with USTRUCT or UCLASS, we should use the namespace `improbable` to avoid name collision. The classes that are wrapped in namespaces should not use the U,F or A prefixes to make it easy to distiguish these from the Unreal classes. diff --git a/SpatialGDK/Documentation/index.md b/SpatialGDK/Documentation/index.md deleted file mode 100644 index 70e3152007..0000000000 --- a/SpatialGDK/Documentation/index.md +++ /dev/null @@ -1,38 +0,0 @@ -<%(TOC)%> -# Welcome to the SpatialOS GDK for Unreal Alpha -<%(Callout type="warn" message=" The SpatialOS GDK for Unreal is in alpha. Its API may change as we learn from feedback. Although not fully ready in terms of performance, stability and documentation, this is a great time to get involved with the GDK and shape it with us. We are committed to improving the GDK rapidly, aiming for a more stable release in Q2 2019: see [Development roadmap](https://github.com/spatialos/UnrealGDK/projects/1). If you are interested in developing with the GDK before then, please contact us via our [forums](https://forums.improbable.io/), or [Discord](https://discord.gg/vAT7RSU).")%> - - - -The SpatialOS Game Development Kit (GDK) for Unreal is a plugin which allows you to host your game and combine multiple dedicated server instances across one seamless game world, whilst using the Unreal Engine networking API. - -The GDK offers:
- -* **Multiserver support:** leveraging our cloud platform, the GDK enables you to use more than one Unreal game server in a single instance so that games can have more players, Actors, and gameplay systems than previously possible.
-* **An Unreal-native experience:** keeping traditional workflows and networking APIs that Unreal Engine developers are familiar with, the GDK introduces new native-feeling concepts that turn a single-server engine into a distributed one. This enables the GDK to retain the functionality of the networking features which Unreal offers out of the box, including transform synchronization, character movement, and map travel.
-* **An easy way to get started:** we have made sure it’s easy to [get started]({{urlRoot}}/content/get-started/introduction) with the GDK by including a Starter Template which you can use as a tour of SpatialOS and a base for your own game, as well as a guide to porting your current multiplayer Unreal game to run on SpatialOS. - -## What next? - -#### Want to get going with SpatialOS? - -* **[Get started]({{urlRoot}}/content/get-started/introduction) with the SpatialOS GDK for Unreal.** If you’re an Unreal game developer and you're ready to get started with SpatialOS, the _Get started_ guide takes you through setting up the SpatialOS Unreal Engine fork and the GDK. (Note that the GDK is aimed at users comfortable with programming in Unreal.) - -#### Want to find out more? - -* Check out our **blogs** on **SpatialOS games** currently in development and the **game design** opportunities and challenges associated with working with SpatialOS. -
-
-* We’d love to hear your game ideas and answer any questions you have about making games on SpatialOS.
-**Join the community on our forums, or on Discord.** -
-
-* If you aren’t already familiar with SpatialOS, you can find out about the concepts which enable it to support game worlds with more persistence, scale, and complexity than previously possible. -
**Read the [SpatialOS concept docs](https://docs.improbable.io/reference/latest/shared/concepts/spatialos)** on the SpatialOS documentation website (5 minute read). -
-
- -
- ------- -_2019-03-25 Page updated with editorial review_ \ No newline at end of file diff --git a/SpatialGDK/Documentation/known-issues.md b/SpatialGDK/Documentation/known-issues.md deleted file mode 100644 index a4d7ddbd77..0000000000 --- a/SpatialGDK/Documentation/known-issues.md +++ /dev/null @@ -1,13 +0,0 @@ -<%(TOC)%> -# Known issues - -For the current state of support of Unreal features, including any workarounds for features which are not fully supported yet, see the [Unreal features support page]({{urlRoot}}/unreal-features-support). - -For all other known issues, please visit [this board](https://github.com/spatialos/UnrealGDK/projects/2) on Github. Feel free to contribute and subscribe to any issue, and raise new ones from [this page](https://github.com/spatialos/UnrealGDK/issues). - -
- ------------- -*2019-04-25 Page updated with full editorial review
-2019-04-25 Removed out-dated known issues from this page and added links to current known issues.* -
diff --git a/SpatialGDK/Documentation/recommended-use.md b/SpatialGDK/Documentation/recommended-use.md deleted file mode 100644 index fa04ea5bff..0000000000 --- a/SpatialGDK/Documentation/recommended-use.md +++ /dev/null @@ -1,8 +0,0 @@ -<%(TOC)%> -# Recommended use - -We are releasing the GDK in [alpha](https://docs.improbable.io/reference/latest/shared/release-policy#maturity-stages) so we can react to feedback and iterate on development quickly. To facilitate this, during our alpha stage we don't have a formal deprecation cycle for APIs and workflows. This means that everything and anything can change. In addition, documentation is limited and some aspects of the GDK are not optimized and stability on a single server is significantly better than multi-server deployments. - -Given these considerations, for now we recommend using the GDK in projects in the evaluation or prototype stage. This ensures that your project's requirements are in line with the GDK's timeline. - -Although the GDK is not fully ready in terms of performance and stability yet, this is a great time to get involved and shape it with us. We are committed to improving the GDK rapidly, aiming for a more stable release in Q2 2019 ([development roadmap](https://github.com/spatialos/UnrealGDK/projects/1)). See the [full feature list](https://docs.improbable.io/unreal/latest/features) on the documentation website. diff --git a/SpatialGDK/Documentation/spatialos-gdkforunreal-header.png b/SpatialGDK/Documentation/spatialos-gdkforunreal-header.png new file mode 100644 index 0000000000..2b59bf914b Binary files /dev/null and b/SpatialGDK/Documentation/spatialos-gdkforunreal-header.png differ diff --git a/SpatialGDK/Documentation/toc.md b/SpatialGDK/Documentation/toc.md deleted file mode 100644 index fd8167813a..0000000000 --- a/SpatialGDK/Documentation/toc.md +++ /dev/null @@ -1,58 +0,0 @@ --

SpatialOS GDK for Unreal

- - [Welcome]({{urlRoot}}/index) - - Get started - - [Introduction]({{urlRoot}}/content/get-started/introduction) - - [1: Get the dependencies]({{urlRoot}}/content/get-started/dependencies) - - [2: Get and build the GDK’s Unreal Engine Fork]({{urlRoot}}/content/get-started/build-unreal-fork) - - [3: Set up the SpatialOS GDK Starter Template]({{urlRoot}}/content/get-started/gdk-template) - - Technical overview - - [Principles of the GDK for Unreal]({{urlRoot}}/content/technical-overview/gdk-principles) - - [How the GDK fits into your game stack]({{urlRoot}}/content/technical-overview/how-the-gdk-fits-in) - - [GDK concepts]({{urlRoot}}/content/technical-overview/gdk-concepts) - - Tutorials - - Multiserver Shooter tutorial - - [Introduction]({{urlRoot}}/content/tutorials/multiserver-shooter/tutorial-multiserver-intro) - - [Step 1: Set up]({{urlRoot}}/content/tutorials/multiserver-shooter/tutorial-multiserver-setup) - - [Step 2: Replicate health changes]({{urlRoot}}/content/tutorials/multiserver-shooter/tutorial-multiserver-healthchanges) - - [Step 3: Test your changes locally]({{urlRoot}}/content/tutorials/multiserver-shooter/tutorial-multiserver-localtest) - - [Step 4: Test your changes in the cloud]({{urlRoot}}/content/tutorials/multiserver-shooter/tutorial-multiserver-cloudtest) - - [Port your own project to the GDK]({{urlRoot}}/content/tutorials/tutorial-porting-guide) - - [Unreal features support]({{urlRoot}}/unreal-features-support) - - [Known issues]({{urlRoot}}/known-issues) --

Concepts and terminology

- - SpatialOS concepts - - [Summary]({{urlRoot}}/content/spatialos-concepts/concepts) - - [Schema]({{urlRoot}}/content/spatialos-concepts/schema) - - [Snapshots]({{urlRoot}}/content/spatialos-concepts/generating-a-snapshot) - - [Glossary]({{urlRoot}}/content/glossary) - - [Toolbar]({{urlRoot}}/content/toolbars) --

Reference

- - [Spatial Type]({{urlRoot}}/content/spatial-type) - - [Dynamic Typebindings]({{urlRoot}}/content/dynamic-typebindings) - - [Unreal networking authority]({{urlRoot}}/content/authority) - - [Singleton Actors]({{urlRoot}}/content/singleton-actors) - - [Actor handover between server-workers]({{urlRoot}}/content/handover-between-server-workers) - - [Map travel]({{urlRoot}}/content/map-travel) - - [Cross-server RPCs]({{urlRoot}}/content/cross-server-rpcs) - - [Helper scripts]({{urlRoot}}/content/helper-scripts) - - [Directory structure]({{urlRoot}}/content/directory-structure) - - [Gameplay Ability System]({{urlRoot}}/content/ability-system) - - [Non-Unreal server-worker types]({{urlRoot}}/content/non-unreal-server-worker-types) --

Workflows

- - [Local development workflow]({{urlRoot}}/content/local-dev-workflow) - - [Cloud development workflow]({{urlRoot}}/content/cloud-dev-workflow) - - [Helper scripts]({{urlRoot}}/content/helper-scripts) - - [Directory structure]({{urlRoot}}/content/directory-structure) - - [Troubleshooting]({{urlRoot}}/content/troubleshooting) - - [Keeping your GDK up to date]({{urlRoot}}/content/upgrading) --

Get involved

- - Contributing to the GDK - - [Coding standards]({{urlRoot}}/contributions/unreal-gdk-coding-standards) - - Visit our GitHub: - - [Issue log](https://github.com/spatialos/unrealgdk/issues) - - [Contribution policy](https://github.com/spatialos/UnrealGDK/blob/master/CONTRIBUTING.md) - - GDK community - - [Discord](https://discordapp.com/channels/311273633307951114/339471548647866368) - - [Forums](https://forums.improbable.io/) - - [Mailing list](http://go.pardot.com/l/169082/2018-06-15/27ld2t) - - [Development roadmap](https://github.com/spatialos/UnrealGDK/projects/1) diff --git a/SpatialGDK/Documentation/unreal-features-support.md b/SpatialGDK/Documentation/unreal-features-support.md deleted file mode 100644 index 7c0bea8809..0000000000 --- a/SpatialGDK/Documentation/unreal-features-support.md +++ /dev/null @@ -1,388 +0,0 @@ -# Unreal features support - -The aim of the GDK is to seamlessly support all native Unreal Engine features, making it easy to create and port any Unreal game code to run on SpatialOS, both in single server and multiserver configurations. - -The following tables show the state of support of Unreal Engine features on the GDK, along with any caveats or workaround you should be aware of. The Unreal Engine features support for multiserver configurations of the GDK will be available in Q3 2019. - - - -**Legend** - - - - - - - - - - - - - - - - -
Fully supported, available now
Supported with caveats or workarounds
Q2 - Q3 2019
Planned for post Q3 2019
- -## Single-Server Support - -Support of Unreal features with the GDK in a single server-worker configuration: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Feature Area (Links are to Unreal documentation)Feature (Links are to Unreal documentation)Support LevelNotes & Caveats
Gameplay FrameworkGameMode
GameState
PlayerState
PlayerController
Character / Pawn
Level Blueprints
Ability SystemSupported with workarounds described here. Replicated ability components not supported.
Property ReplicationData Property Replication (C++ classes)Using fixed arrays instead of TArrays is significantly faster.
Data Property Replication (Blueprint classes)
Dynamic Object Reference Replication (C++ classes)
Dynamic Object Reference Replication (Blueprint classes)
Stably Named Object Reference Replication
Conditional ReplicationReplication condition active overrides (DOREPLIFETIME_ACTIVE_OVERRIDE as defined in Unreal documentation) do not work.
Delta Serialization
Fast TArray SerializationSupported, but slower than native UE.
RepNotify Callbacks
Actor ReplicationClient and server RPCs (C++)Ordering of reliable RPCs is not respected.
Client and Server RPCs (Blueprint)Ordering of reliable RPCs is not respected.
Multicast RPCsRPCs cannot be reliable. (This is due to the distributed systems nature of SpatialOS.)
Actor Roles
Static Subobject Replication
Dynamic Component Replication
Startup Actors
Multiplayer Gameplay FeaturesSplit Screen
Replay System
Character Movement
Vehicle Movement
Online Beacons
Timers
Online Subsystem Abstraction
OptimizationReplication GraphWe will present a different system for the same purpose instead.
Net Dormancy
Net Update Frequency
DebuggingGameplay DebuggerCurrently unsupported due to NetDeltaSerialize dependency.
Cheat Manager
Network ProfilerWe may not support this tool fully but will have equivalent functionality in SpatialOS metrics.
World BuildingWorld Composition / Streaming (Level Streaming)
World Origin Shifting
AIBehavior Trees
Blackboard
Environment Query System
TravelServer Travel
Client Travel
Seamless Travel
MiscPhysics Simulation
Tracing
Skeletal Mesh Animation System
Sequence Editor
UE4 4.22 SupportExpected before the end of June 2019.
PlatformsPC
Xbox OnePlease see annoucement here.
PS4Please see annoucement here.
Mobile
- -## Multiserver Support - -The table for multiserver support is coming soon. - -
- ------------- -*2019-04-25 Page added with full editorial review* -
diff --git a/SpatialGDK/Extras/core-sdk.version b/SpatialGDK/Extras/core-sdk.version index cf51d24272..c90a2e8b7d 100644 --- a/SpatialGDK/Extras/core-sdk.version +++ b/SpatialGDK/Extras/core-sdk.version @@ -1 +1 @@ -13.6.2 \ No newline at end of file +13.8.1 \ No newline at end of file diff --git a/SpatialGDK/Extras/fastbuild/install.ps1 b/SpatialGDK/Extras/fastbuild/install.ps1 index 4047fe4abe..c24d97a7f8 100644 --- a/SpatialGDK/Extras/fastbuild/install.ps1 +++ b/SpatialGDK/Extras/fastbuild/install.ps1 @@ -6,7 +6,7 @@ Import-Module BitsTransfer Add-Type -AssemblyName System.IO.Compression.FileSystem $fileshare="\\lonv-file-01" -$version="v0.96" +$version="v0.98" $rootPath=[System.Io.Path]::GetFullPath("$env:HOMEDRIVE:\tools\fastbuild") If (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { diff --git a/SpatialGDK/Extras/fastbuild/uninstall-0.96.ps1 b/SpatialGDK/Extras/fastbuild/uninstall-0.96.ps1 new file mode 100644 index 0000000000..dc85d09c68 --- /dev/null +++ b/SpatialGDK/Extras/fastbuild/uninstall-0.96.ps1 @@ -0,0 +1,57 @@ +param ( + [switch]$service=$false +) + +$rootPath=[System.IO.Path]::GetFullPath("$env:HOMEDRIVE:\tools\fastbuild") +$version="v0.96" + +If (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + Write-Host "Please run from an Adminstrator command prompt." + exit 1 +} + +Write-Host "Removing from firewall..." +Remove-NetFirewallRule -Group "FASTBuild" ` + -ErrorAction SilentlyContinue + +if ($service) { + $serviceName="FASTBuildWorker-$version" + + if (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) { + Write-Host "Stopping $serviceName service..." + Start-Process nssm.exe "stop",$serviceName -Wait -NoNewWindow + + Write-Host "Removing $serviceName service..." + Start-Process nssm.exe "remove",$serviceName,"confirm" -Wait -NoNewWindow + } +} else { + Write-Host "Removing from startup group..." + Remove-ItemProperty ` + -Path HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\ ` + -Name "FBuildWorker" ` + -ErrorAction SilentlyContinue + + Write-Host "Stopping process..." + Stop-Process -Name "FBuildWorker.exe" -ErrorAction SilentlyContinue + Stop-Process -Name "FBuildWorker.exe.copy" -ErrorAction SilentlyContinue +} + +# Manually remove marker file from the brokerage, since it's not possible to cleanly exit FBuildWorker.exe +$brokerageFile = [System.IO.Path]::Combine($env:FASTBUILD_BROKERAGE_PATH, "main", "17", [System.Net.Dns]::GetHostName()) +if (Test-Path $brokerageFile) { + Write-Host "Removing from brokerage..." + [System.IO.File]::Delete($brokerageFile) +} + +Write-Host "Cleaning environment..." +[System.Environment]::SetEnvironmentVariable("FASTBUILD_EXE_PATH", $null, [System.EnvironmentVariableTarget]::Machine) +[System.Environment]::SetEnvironmentVariable("FASTBUILD_CACHE_PATH", $null, [System.EnvironmentVariableTarget]::Machine) +[System.Environment]::SetEnvironmentVariable("FASTBUILD_BROKERAGE_PATH", $null, [System.EnvironmentVariableTarget]::Machine) +[System.Environment]::SetEnvironmentVariable("FASTBUILD_CACHE_MODE", $null, [System.EnvironmentVariableTarget]::Machine) + +if (Test-Path $rootPath) { + Write-Host "Removing $rootPath..." + [System.IO.Directory]::Delete($rootPath, $true) +} + +Write-Host "Finished!" -ForegroundColor Green diff --git a/SpatialGDK/Extras/fastbuild/uninstall.ps1 b/SpatialGDK/Extras/fastbuild/uninstall.ps1 index dc85d09c68..c9211a6d2c 100644 --- a/SpatialGDK/Extras/fastbuild/uninstall.ps1 +++ b/SpatialGDK/Extras/fastbuild/uninstall.ps1 @@ -3,7 +3,9 @@ param ( ) $rootPath=[System.IO.Path]::GetFullPath("$env:HOMEDRIVE:\tools\fastbuild") -$version="v0.96" +# NOTE: When updating '$version', ensure you also update the '$brokerageFile' path to be correct. +# This will involve replacing brokerage number after "main". e.g. "19.windows" -> "20.windows". Check your brokerage for the correct version. +$version="v0.98" If (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Host "Please run from an Adminstrator command prompt." @@ -37,7 +39,7 @@ if ($service) { } # Manually remove marker file from the brokerage, since it's not possible to cleanly exit FBuildWorker.exe -$brokerageFile = [System.IO.Path]::Combine($env:FASTBUILD_BROKERAGE_PATH, "main", "17", [System.Net.Dns]::GetHostName()) +$brokerageFile = [System.IO.Path]::Combine($env:FASTBUILD_BROKERAGE_PATH, "main", "19.windows", [System.Net.Dns]::GetHostName()) if (Test-Path $brokerageFile) { Write-Host "Removing from brokerage..." [System.IO.File]::Delete($brokerageFile) diff --git a/SpatialGDK/Extras/internal-documentation/release-process.md b/SpatialGDK/Extras/internal-documentation/release-process.md index 71c9aecd8e..3e6788c625 100644 --- a/SpatialGDK/Extras/internal-documentation/release-process.md +++ b/SpatialGDK/Extras/internal-documentation/release-process.md @@ -7,6 +7,9 @@ This document outlines the process for releasing a version of the GDK for Unreal ## 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`). ## Pre-Validation @@ -21,7 +24,7 @@ This document outlines the process for releasing a version of the GDK for Unreal - Look at the previous release versions in the changelog to see how this should be done. 1. Commit your changes to `CHANGELOG.md`. 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. +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). @@ -30,7 +33,7 @@ This document outlines the process for releasing a version of the GDK for Unreal 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. Announce the branch and the commit hash it uses in the #unreal-gdk-release channel. +1. Announce the branch and the commit hash it uses in the `#unreal-gdk-release` channel. ### Create the `UnrealGDKThirdPersonShooter` release candidate 1. `git clone` the [UnrealGDKThirdPersonShooter](https://github.com/spatialos/UnrealGDKThirdPersonShooter). @@ -41,8 +44,8 @@ This document outlines the process for releasing a version of the GDK for Unreal 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 `UnrealGDKTestSuite` release candidate -1. `git clone` the [UnrealGDKThirdPersonShooter](https://github.com/spatialos/UnrealGDKTestSuite). +### 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. @@ -52,6 +55,8 @@ This document outlines the process for releasing a version of the GDK for Unreal ### Serve docs locally It is vital that you test using the docs for the release version that you are about to publish, not with the currently live docs that relate to the previous version. +1. cd `UnrealGDK` +1. git checkout `docs-release` 1. `improbadoc serve ` ## Build your release candidate engine @@ -65,38 +70,17 @@ It is vital that you test using the docs for the release version that you are ab If at any point in the below validation steps you encounter a blocker, you must fix that defect prior to releasing. -There are two ways to do this, you must decide on the most appropriate one. -* Delete the existing release branches and branch new ones from the HEAD of `master` (in the case of the GDK and tutorials) and from the HEAD of `4.xx-SpatialOSUnrealGDK` (in `improbableio/UnrealEngine`). -* `git cherry-[commit-hash]` the change into your release candidate branch. Be sure to cherry-pick any changes that your cherry-pick depends on. GDK changes often depend on engine changes, for example. +The workflow for this is: -The workflow for creating new release branches is: - -1. Raise a bug ticket detailing the blocker. -1. `git checkout master` +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. Make a commit, push, open a PR into `master`. -1. When the PR is merged, `git checkout master` and `git pull`. -1. Branch off of master using `git checkout -b x.y.z-rc-[n+1]` in order to create a new release candidate branch, and re-test the defect to ensure you fixed it. +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. - -The workflow for cherry-picking the fix is: - -1. Raise a bug ticket detailing the blocker. -1. `git checkout master` -1. `git pull` -1. `git checkout -b bugfix/UNR-xxx` -1. Fix the defect. -1. Make a commit, push, open a PR into `master`. -1. When the PR is merged, `git checkout x.y.z-rc`. -1. `git cherry-[commit-hash]` where `[commit-hash]` is the hash of `bugfix/UNR-xxx` merging into `master`. -1. `git push` -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. - -* Raise a bug ticket detailing the blocker. +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 (Multiserver Shooter tutorial) 1. Follow these steps: http://localhost:8080/reference/1.0/content/get-started/tutorial, bearing in mind the following caveats: @@ -107,6 +91,7 @@ The workflow for cherry-picking the fix is: * [Deploy the project locally (again)](http://localhost:8080/reference/1.0/content/get-started/tutorial#deploy-the-project-locally-again) * [Enable cross server RPCs](http://localhost:8080/reference/1.0/content/get-started/tutorial#enable-cross-server-rpcs) * [Deploy the project locally (last time)](http://localhost:8080/reference/1.0/content/get-started/tutorial#deploy-the-project-locally-last-time) + 2. Launch a local SpatialOS deployment, then a standalone server-worker, and then connect two standalone clients to it. To do this: * In your file browser, click `UnrealGDKThirdPersonShooter\LaunchSpatial.bat` in order to run it. * In your file browser, click `UnrealGDKThirdPersonShooter\LaunchServer.bat` in order to run it. @@ -114,33 +99,31 @@ The workflow for cherry-picking the fix is: * Run the same script again in order to launch the second client * Run and shoot eachother with the clients as a smoke test. * Open the `UE4 Console` and enter the command `open 127.0.0.1`. The desired effect is that the client disconnect and then re-connects to the map. If you can continue to play after executing the command then you've succesfully tested client travel. -3. On a seperate Windows PC, launch a local SpatialOS deployment, then a standalone server-worker, and then on your original Windows PC, connect two standalone clients to it. To do this: + +3. Launch a local SpatialOS deployment, then connect two machines as clients using your local network. To do this: * Ensure that both machines are on the same network. -* On your the machine you're going to run the server on, follow the setup steps listed with caveats in step 1. -* Still on your server machine, run `UnrealGDKThirdPersonShooter\LaunchSpatial.bat` +* On your own machine, in your terminal, `cd` to ``. +* Build out a windows client by running: +`Game\Plugins\UnrealGDK\SpatialGDK\Build\Scripts\BuildWorker.bat ThirdPersonShooter Win64 Development ThirdPersonShooter.uproject` +* Send the client you just built to the other machine you'll be using to connect. You can find it at: `\spatial\build\assembly\worker\UnrealClient@Windows.zip` +* Still on your server machine, discover your local IP address by runing `ipconfig`. It's the one entitled `IPv4 Address`. +* Still in your server machine, in a terminal window, `cd` to `\spatial\` and run the following command: `spatial local launch default_launch.json --runtime_ip=` * Still on your server machine, run `UnrealGDKThirdPersonShooter\LaunchServer.bat`. -* Still on your server machine, discover your local IP address by following these [steps](https://lifehacker.com/5833108/how-to-find-your-local-and-external-ip-address). -* On the machine you're going to run your clients on, open `UnrealGDKThirdPersonShooter\LaunchClient.bat` in your code editor of choice and: - * Change `127.0.0.1`to the local IP of your server machine. - * Add `-OverrideSpatialNetworking` after the ip address - * Add `-NetDriverOverrides=/Script/SpatialGDK.SpatialNetDriver` after that - * Add `+useExternalIpForBridge true` after that - * Your final script shoudl look somethisn like: `@echo off -call "%~dp0ProjectPaths.bat" -"%UNREAL_HOME%\Engine\Binaries\Win64\UE4Editor.exe" "%~dp0%PROJECT_PATH%\%GAME_NAME%.uproject" 172.16.120.76 -game -log -OverrideSpatialNetworking -NoLogToSpatial -windowed -ResX=1280 -ResY=720 +workerType UnrealClient -NetDriverOverrides=/Script/SpatialGDK.SpatialNetDriver +useExternalIpForBridge true` -* Save your changes. -* In your file browser, click `UnrealGDKThirdPersonShooter\LaunchClient.bat` in order to run it. -* Run the same script again in order to launch the second client +* On the machine you're going to run your clients on, unzip `UnrealClient@Windows.zip`. +* On the machine you're going to run your clients on, in a terminal window, `cd` to the unzipped `UnrealClient@Windows` direcory and run the following command: `_ThirdPersonShooter.exe -workerType UnrealClient -useExternalIpForBridge true` +* Repeat the above step in order to launch the second client * Run and shoot eachother with the clients as a smoke test. -* You can now turn off your server machine. +* You can now turn off the machine that's running the client, and return to your own machine. ## Validation (GDK Starter Template) -1. Follow these steps: http://localhost:8080/reference/1.0/content/get-started/gdk-template, bearing in mind the following caveats: -* When you clone `UnrealGDKThirdPersonShooter`, be sure to checkout the release candidate branch, so you're working with the release version. +1. Follow these steps: http://localhost:8080/reference/1.0/content/get-started/gdk-template, bearing in mind the following caveat: * When you clone the GDK into the `Plugins` folder, be sure to checkout the release candidate branch, so you're working with the release version. -## Validation (UnrealGDKTestSuite) -1. Follow these steps: https://github.com/spatialos/UnrealGDKTestSuite/blob/release/README.md. All tests must pass. +## Validation (UnrealGDKExampleProject) +1. Follow these steps: http://localhost:8080/reference/1.0/content/get-started/example-project/exampleproject-intro. All tests must pass. + +## Validation (Playtest) +1. Follow these steps: https://brevi.link/unreal-release-playtests. All tests must pass. ## Validation (Docs) 1. Upload docs to docs-testing using Improbadoc. @@ -149,27 +132,54 @@ call "%~dp0ProjectPaths.bat" ## Release -All of the above tests must have passed and there must be no outstanding blocking issues before you start this, the release phase. +All of the above tests **must** have passed and there must be no outstanding blocking issues before you start this, the release phase. + +If you want to soak test this release in `staging-` before releasing to `master`, only execute the `staging-` steps. -1. In `UnrealGDK` merge `x.y.z-rc` into `release`. +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. + +1. In `UnrealGDK`, create `staging-x.y.z-rc` from `x.y.z-rc` (select `x.y.z-rc` in the branch dropdown, then type the name of the new branch to create. If there's already a branch with that name, delete it first). +1. In `UnrealGDK`, merge `x.y.z-rc` into `release` using GitHub PR (use `Update branch` button to merge `release` into `x.y.z-rc`). 1. Use the GitHub Release UI to tag the commit you just made to as `x.y.z`.
-1. Copy the latest release notes from `CHANGELOG.md` into a GDoc and ask for a quick review of them from Tech Writers. -1. Once approved, enter them in the release description -1. In `improbableio/UnrealEngine` merge `4.xx-SpatialOSUnrealGDK-x.y.z-rc` into `4.xx-SpatialOSUnrealGDK-release`. +Copy the latest release notes from `CHANGELOG.md` and paste them into the release description field. +1. In `improbableio/UnrealEngine`, create `staging-4.xx-SpatialOSUnrealGDK-x.y.z-rc` from `4.xx-SpatialOSUnrealGDK-x.y.z-rc`. +1. In `improbableio/UnrealEngine`, merge `4.xx-SpatialOSUnrealGDK-x.y.z-rc` into `4.xx-SpatialOSUnrealGDK-release` using GitHub PR (use `Update branch` button to merge `4.xx-SpatialOSUnrealGDK-release` into `4.xx-SpatialOSUnrealGDK-x.y.z-rc`). 1. Use the GitHub Release UI to tag the commit you just made as `4.xx-SpatialOSUnrealGDK-x.y.z`.
-1. In `UnrealGDKThirdPersonShooter` merge `x.y.z-rc` into `release`, and tag that commit as `x.y.z`. +1. In `UnrealGDKThirdPersonShooter`, create `staging-x.y.z-rc` from `x.y.z-rc`. +1. In `UnrealGDKThirdPersonShooter`, merge `x.y.z-rc` into `release` using GitHub PR (use `Update branch` button to merge `release` into `x.y.z-rc`). 1. Use the GitHub Release UI to tag the commit you just made to as `x.y.z`. 1. In `UnrealGDKThirdPersonShooter`, `git rebase` `release` into `tutorial`. 1. In `UnrealGDKThirdPersonShooter`, `git rebase` `release` into `tutorial-complete`. -1. In `UnrealGDKTestSuite` merge `x.y.z-rc` into `release`, and tag that commit as `x.y.z`. +1. In `UnrealGDKExampleProject`, create `staging-x.y.z-rc` from `x.y.z-rc`. +1. In `UnrealGDKExampleProject`, merge `x.y.z-rc` into `release` using GitHub PR (use `Update branch` button to merge `release` into `x.y.z-rc`). 1. Use the GitHub Release UI to tag the commit you just made to as `x.y.z`. 1. Publish the docs to live using Improbadoc commands listed [here](https://improbableio.atlassian.net/wiki/spaces/GBU/pages/327485360/Publishing+GDK+Docs). +1. Update the [roadmap](https://github.com/spatialos/UnrealGDK/projects/1), moving the release from **Planned** to **Released**, and linking to the release. 1. Announce the release in: * Forums -* Discord (#unreal, do not @here) -* Slack (#releases) -* Email (spatialos-announce@) +* Discord (`#unreal`, do not `@here`) +* Slack (`#releases`) +* Email (`spatialos-announce@`) + +Congratulations, you've done the release! + +## Clean up + +There are potentially changes that were merged into the release candidate branches that aren't merged into the corresponding development (master) branches yet. Because there are some race conditions involved, a GDK engineer should be in charge of doing this. + +Indirect-merge from `A` into `B` (`staging-x.y.z-rc` into `master`, except `improbableio/UnrealEngine` repo where this is `staging-4.xx-SpatialOSUnrealGDK-x.y.z-rc` into `4.xx-SpatialOSUnrealGDK`) is defined as: +1. Make sure there is no `merge-A-into-B` branch created already, and delete it if it exists. +1. Create branch `merge-A-into-B` from `B` (`git checkout B`, `git checkout -b merge-A-into-B`). +1. Merge `A` into `merge-A-into-B` (`git merge A`, `git push origin merge-A-into-B`). +1. Merge `merge-A-into-B` into `B` using GitHub PR. Make sure you use `Create a merge commit` option. + +Use the above definition to perform the following: +1. `UnrealGDK` repo: indirect-merge `staging-x.y.z-rc` into `master`. +1. `improbableio/UnrealEngine` repo: indirect-merge `staging-4.xx-SpatialOSUnrealGDK-x.y.z-rc` into `4.xx-SpatialOSUnrealGDK`. +1. `UnrealGDKThirdPersonShooter` repo: (skip this step if `staging-x.y.z-rc` has no new commits compared to `master`) indirect-merge `staging-x.y.z-rc` into `master`. +1. `UnrealGDKExampleProject` repo: (skip this step if `staging-x.y.z-rc` has no new commits compared to `master`) indirect-merge `staging-x.y.z-rc` into `master`. +1. Delete the `staging-` branches. ## Appendix diff --git a/SpatialGDK/Extras/schema/core_types.schema b/SpatialGDK/Extras/schema/core_types.schema index a6f82736b0..d3fc0ceecb 100644 --- a/SpatialGDK/Extras/schema/core_types.schema +++ b/SpatialGDK/Extras/schema/core_types.schema @@ -1,6 +1,9 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved package unreal; +type Void { +} + type UnrealObjectRef { EntityId entity = 1; uint32 offset = 2; diff --git a/SpatialGDK/Extras/schema/debug_metrics.schema b/SpatialGDK/Extras/schema/debug_metrics.schema new file mode 100644 index 0000000000..e4256f04bf --- /dev/null +++ b/SpatialGDK/Extras/schema/debug_metrics.schema @@ -0,0 +1,16 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +package unreal; + +import "unreal/gdk/core_types.schema"; + +type ModifySettingPayload { + string setting_name = 1; + float setting_value = 2; +} + +component DebugMetrics { + id = 9984; + command Void start_rpc_metrics(Void); + command Void stop_rpc_metrics(Void); + command Void modify_spatial_settings(ModifySettingPayload); +} diff --git a/SpatialGDK/Extras/schema/heartbeat.schema b/SpatialGDK/Extras/schema/heartbeat.schema index 472737b80d..ae33dc0940 100644 --- a/SpatialGDK/Extras/schema/heartbeat.schema +++ b/SpatialGDK/Extras/schema/heartbeat.schema @@ -7,4 +7,5 @@ type HeartbeatEvent { component Heartbeat { id = 9991; event HeartbeatEvent heartbeat; + bool client_has_quit = 1; } diff --git a/SpatialGDK/Extras/schema/relevant.schema b/SpatialGDK/Extras/schema/relevant.schema new file mode 100644 index 0000000000..fcc912c428 --- /dev/null +++ b/SpatialGDK/Extras/schema/relevant.schema @@ -0,0 +1,6 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +package unreal; + +component AlwaysRelevant { + id = 9983; +} diff --git a/SpatialGDK/Extras/schema/rpc_components.schema b/SpatialGDK/Extras/schema/rpc_components.schema index c8a42b996c..1eca1acd30 100644 --- a/SpatialGDK/Extras/schema/rpc_components.schema +++ b/SpatialGDK/Extras/schema/rpc_components.schema @@ -1,28 +1,48 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved package unreal; +import "unreal/gdk/core_types.schema"; + type UnrealRPCPayload { uint32 offset = 1; uint32 rpc_index = 2; bytes rpc_payload = 3; } -type Void { +type UnrealPackedRPCPayload { + uint32 offset = 1; + uint32 rpc_index = 2; + bytes rpc_payload = 3; + EntityId entity = 4; } component UnrealClientRPCEndpoint { id = 9990; - event UnrealRPCPayload unreliable_client_to_server_rpc; - command Void reliable_server_to_client_rpc(UnrealRPCPayload); + // Set to true when authority is gained, indicating that RPCs can be received + bool ready = 1; + event UnrealRPCPayload client_to_server_rpc_event; + event UnrealPackedRPCPayload packed_client_to_server_rpc; } component UnrealServerRPCEndPoint { id = 9989; - event UnrealRPCPayload unreliable_server_to_client_rpc; - command Void reliable_client_to_server_rpc(UnrealRPCPayload); + // Set to true when authority is gained, indicating that RPCs can be received + bool ready = 1; + event UnrealRPCPayload server_to_client_rpc_event; + event UnrealPackedRPCPayload packed_server_to_client_rpc; + command Void server_to_server_rpc_command(UnrealRPCPayload); } component UnrealMulticastRPCEndpoint { id = 9987; event UnrealRPCPayload unreliable_multicast_rpc; } + +// Component that contains a list of RPCs to be executed +// as a part of entity creation request +component RPCsOnEntityCreation { + id = 9985; + list rpcs = 1; + command Void clear_rpcs(Void); +} + diff --git a/SpatialGDK/Extras/schema/spawner.schema b/SpatialGDK/Extras/schema/spawner.schema index f67481a070..7f671d1335 100644 --- a/SpatialGDK/Extras/schema/spawner.schema +++ b/SpatialGDK/Extras/schema/spawner.schema @@ -5,6 +5,7 @@ type SpawnPlayerRequest { string url = 1; bytes unique_id = 2; string online_platform_name = 3; + bool simulated = 4; } type SpawnPlayerResponse { } diff --git a/SpatialGDK/Extras/spot.version b/SpatialGDK/Extras/spot.version new file mode 100644 index 0000000000..5729794af5 --- /dev/null +++ b/SpatialGDK/Extras/spot.version @@ -0,0 +1 @@ +20190626.145947.9ed060f1af \ No newline at end of file diff --git a/SpatialGDK/Extras/templates/WorkerJsonTemplate.json b/SpatialGDK/Extras/templates/WorkerJsonTemplate.json new file mode 100644 index 0000000000..10ec85e060 --- /dev/null +++ b/SpatialGDK/Extras/templates/WorkerJsonTemplate.json @@ -0,0 +1,100 @@ +{ + "build": { + "tasks": [ + { + "name": "codegen", + "description": "required by spatial worker build build-config.", + "steps": [{"name": "No-op", "command": "echo", "arguments": ["No-op."]}] + }, + { + "name": "build", + "description": "required by spatial worker build build-config.", + "steps": [{"name": "No-op", "command": "echo", "arguments": ["No-op."]}] + }, + { + "name": "clean", + "description": "required by spatial worker build build-config.", + "steps": [{"name": "No-op", "command": "echo", "arguments": ["No-op."]}] + } + ] + }, + "bridge": { + "worker_attribute_set": { + "attributes": [ + "{{WorkerTypeName}}" + ] + }, + "entity_interest": { + "range_entity_interest": { + "radius": 50 + } + }, + "streaming_query": [ + { + "global_component_streaming_query": { + "component_name": "unreal.SingletonManager" + } + }, + { + "global_component_streaming_query": { + "component_name": "unreal.Singleton" + } + } + ], + "component_delivery": { + "default": "RELIABLE_ORDERED", + "checkout_all_initially": true + } + }, + "managed": { + "windows": { + "artifact_name": "UnrealEditor@Windows.zip", + "command": "StartEditor.bat", + "arguments": [ + "-server", + "-stdout", + "-nowrite", + "-unattended", + "-nologtimes", + "-nopause", + "-messaging", + "-SaveToUserDir", + "+appName", + "${IMPROBABLE_PROJECT_NAME}", + "+receptionistHost", + "${IMPROBABLE_RECEPTIONIST_HOST}", + "+receptionistPort", + "${IMPROBABLE_RECEPTIONIST_PORT}", + "+workerType", + "${IMPROBABLE_WORKER_NAME}", + "+workerId", + "${IMPROBABLE_WORKER_ID}", + "+linkProtocol", + "Tcp", + "-abslog=${IMPROBABLE_LOG_FILE}", + "-NoVerifyGC" + ] + }, + "linux": { + "artifact_name": "UnrealWorker@Linux.zip", + "command": "StartWorker.sh", + "arguments": [ + "${IMPROBABLE_WORKER_ID}", + "${IMPROBABLE_LOG_FILE}", + "+appName", + "${IMPROBABLE_PROJECT_NAME}", + "+receptionistHost", + "${IMPROBABLE_RECEPTIONIST_HOST}", + "+receptionistPort", + "${IMPROBABLE_RECEPTIONIST_PORT}", + "+workerType", + "${IMPROBABLE_WORKER_NAME}", + "+workerId", + "${IMPROBABLE_WORKER_ID}", + "+linkProtocol", + "Tcp", + "-NoVerifyGC" + ] + } + } +} diff --git a/SpatialGDK/Resources/Cloud.png b/SpatialGDK/Resources/Cloud.png new file mode 100644 index 0000000000..7e69932be7 Binary files /dev/null and b/SpatialGDK/Resources/Cloud.png differ diff --git a/SpatialGDK/Resources/Cloud@0.5x.png b/SpatialGDK/Resources/Cloud@0.5x.png new file mode 100644 index 0000000000..8913079aa8 Binary files /dev/null and b/SpatialGDK/Resources/Cloud@0.5x.png differ diff --git a/SpatialGDK/Resources/Icon128.png b/SpatialGDK/Resources/Icon128.png index c00899b32b..a17d80c10f 100644 Binary files a/SpatialGDK/Resources/Icon128.png and b/SpatialGDK/Resources/Icon128.png differ diff --git a/SpatialGDK/Resources/Inspector.png b/SpatialGDK/Resources/Inspector.png index be94b59975..91abd3067b 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 a61cf569c0..4cd0722bcb 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 index 0d592c9aea..b367ea269f 100644 Binary files a/SpatialGDK/Resources/Launch.png and b/SpatialGDK/Resources/Launch.png differ diff --git a/SpatialGDK/Resources/Launch@0.5x.png b/SpatialGDK/Resources/Launch@0.5x.png index 66ff36c18f..46ecd5831e 100644 Binary files a/SpatialGDK/Resources/Launch@0.5x.png and b/SpatialGDK/Resources/Launch@0.5x.png differ diff --git a/SpatialGDK/Resources/SPATIALOS_LOGO_WHITE.png b/SpatialGDK/Resources/SPATIALOS_LOGO_WHITE.png new file mode 100644 index 0000000000..28114637ab Binary files /dev/null and b/SpatialGDK/Resources/SPATIALOS_LOGO_WHITE.png differ diff --git a/SpatialGDK/Resources/Schema.png b/SpatialGDK/Resources/Schema.png index d4baec0294..9ed71d0901 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 369695c26f..e83b8fd399 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 051c1c0904..9541498f0b 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 2f08485474..ecd3b87047 100644 Binary files a/SpatialGDK/Resources/Snapshot@0.5x.png and b/SpatialGDK/Resources/Snapshot@0.5x.png differ diff --git a/SpatialGDK/Resources/Stop.png b/SpatialGDK/Resources/Stop.png index 22bdd5e564..dfdf5fb6ef 100644 Binary files a/SpatialGDK/Resources/Stop.png and b/SpatialGDK/Resources/Stop.png differ diff --git a/SpatialGDK/Resources/Stop@0.5x.png b/SpatialGDK/Resources/Stop@0.5x.png index 563e855bdf..06f0327559 100644 Binary files a/SpatialGDK/Resources/Stop@0.5x.png and b/SpatialGDK/Resources/Stop@0.5x.png differ diff --git a/SpatialGDK/Resources/Upload.png b/SpatialGDK/Resources/Upload.png new file mode 100644 index 0000000000..7e69932be7 Binary files /dev/null and b/SpatialGDK/Resources/Upload.png differ diff --git a/SpatialGDK/Resources/Upload@0.5x .png b/SpatialGDK/Resources/Upload@0.5x .png new file mode 100644 index 0000000000..9a0391f2a1 Binary files /dev/null and b/SpatialGDK/Resources/Upload@0.5x .png differ diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/ActorInterestComponent.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/ActorInterestComponent.cpp new file mode 100644 index 0000000000..e301e39e7c --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/Components/ActorInterestComponent.cpp @@ -0,0 +1,39 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "EngineClasses/Components/ActorInterestComponent.h" + +#include "Schema/Interest.h" +#include "Interop/SpatialClassInfoManager.h" + +void UActorInterestComponent::CreateQueries(const USpatialClassInfoManager& ClassInfoManager, const SpatialGDK::QueryConstraint& AdditionalConstraints, TArray& OutQueries) const +{ + for (const auto& QueryData : Queries) + { + if (!QueryData.Constraint) + { + continue; + } + + SpatialGDK::Query NewQuery{}; + // Avoid creating an unnecessary AND constraint if there are no AdditionalConstraints to consider. + if (AdditionalConstraints.IsValid()) + { + SpatialGDK::QueryConstraint ComponentConstraints; + QueryData.Constraint->CreateConstraint(ClassInfoManager, ComponentConstraints); + + NewQuery.Constraint.AndConstraint.Add(ComponentConstraints); + NewQuery.Constraint.AndConstraint.Add(AdditionalConstraints); + } + else + { + QueryData.Constraint->CreateConstraint(ClassInfoManager, NewQuery.Constraint); + } + NewQuery.Frequency = QueryData.Frequency; + + if (NewQuery.Constraint.IsValid()) + { + OutQueries.Push(NewQuery); + } + } + +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp index d5ac46d6f3..3a3615740b 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialActorChannel.cpp @@ -36,7 +36,11 @@ namespace // 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. +#if ENGINE_MINOR_VERSION <= 20 void UpdateChangelistHistory(TSharedPtr& RepState) +#else +void UpdateChangelistHistory(TUniquePtr& RepState) +#endif { check(RepState->HistoryEnd >= RepState->HistoryStart); @@ -72,8 +76,8 @@ USpatialActorChannel::USpatialActorChannel(const FObjectInitializer& ObjectIniti , bCreatedEntity(false) , bCreatingNewEntity(false) , EntityId(SpatialConstants::INVALID_ENTITY_ID) - , bFirstTick(true) , bInterestDirty(false) + , bIsListening(false) , bNetOwned(false) , NetDriver(nullptr) , LastPositionSinceUpdate(FVector::ZeroVector) @@ -81,6 +85,7 @@ USpatialActorChannel::USpatialActorChannel(const FObjectInitializer& ObjectIniti { } +#if ENGINE_MINOR_VERSION <= 20 void USpatialActorChannel::Init(UNetConnection* InConnection, int32 ChannelIndex, bool bOpenedLocally) { Super::Init(InConnection, ChannelIndex, bOpenedLocally); @@ -90,6 +95,17 @@ void USpatialActorChannel::Init(UNetConnection* InConnection, int32 ChannelIndex Sender = NetDriver->Sender; Receiver = NetDriver->Receiver; } +#else +void USpatialActorChannel::Init(UNetConnection* InConnection, int32 ChannelIndex, EChannelCreateFlags CreateFlag) +{ + Super::Init(InConnection, ChannelIndex, CreateFlag); + + NetDriver = Cast(Connection->Driver); + check(NetDriver); + Sender = NetDriver->Sender; + Receiver = NetDriver->Receiver; +} +#endif void USpatialActorChannel::DeleteEntityIfAuthoritative() { @@ -109,6 +125,10 @@ void USpatialActorChannel::DeleteEntityIfAuthoritative() if (Actor->GetTearOff()) { 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); } else { @@ -124,19 +144,19 @@ bool USpatialActorChannel::IsSingletonEntity() return NetDriver->GlobalStateManager->IsSingletonEntity(EntityId); } +#if ENGINE_MINOR_VERSION <= 20 bool USpatialActorChannel::CleanUp(const bool bForDestroy) +#else +bool USpatialActorChannel::CleanUp(const bool bForDestroy, EChannelCloseReason CloseReason) +#endif { #if WITH_EDITOR if (NetDriver != nullptr) { const bool bDeleteDynamicEntities = GetDefault()->GetDeleteDynamicEntities(); - UWorld* World = NetDriver->GetWorld(); - const bool bPIEShutdown = World != nullptr && World->WorldType == EWorldType::PIE && World->bIsTearingDown; - if (bDeleteDynamicEntities && NetDriver->IsServer() && - (bPIEShutdown || GIsRequestingExit) && NetDriver->GetActorChannelByEntityId(EntityId) != nullptr) { // If we're a server worker, and the entity hasn't already been cleaned up, delete it on shutdown. @@ -145,14 +165,29 @@ bool USpatialActorChannel::CleanUp(const bool bForDestroy) } #endif + // Must cleanup actor and subobjects before UActorChannel::Cleanup as it will clear CreateSubObjects + Receiver->CleanupDeletedEntity(EntityId); + +#if ENGINE_MINOR_VERSION <= 20 return UActorChannel::CleanUp(bForDestroy); +#else + return UActorChannel::CleanUp(bForDestroy, CloseReason); +#endif } +#if ENGINE_MINOR_VERSION <= 20 int64 USpatialActorChannel::Close() { DeleteEntityIfAuthoritative(); return Super::Close(); } +#else +int64 USpatialActorChannel::Close(EChannelCloseReason Reason) +{ + DeleteEntityIfAuthoritative(); + return Super::Close(Reason); +} +#endif bool USpatialActorChannel::IsDynamicArrayHandle(UObject* Object, uint16 Handle) { @@ -187,12 +222,25 @@ void USpatialActorChannel::UpdateShadowData() } } +void USpatialActorChannel::UpdateSpatialPositionWithFrequencyCheck() +{ + // Check that there has been a sufficient amount of time since the last update. + if ((NetDriver->Time - TimeWhenPositionLastUpdated) >= (1.0f / GetDefault()->PositionUpdateFrequency)) + { + UpdateSpatialPosition(); + } +} + FRepChangeState USpatialActorChannel::CreateInitialRepChangeState(TWeakObjectPtr Object) { checkf(Object != nullptr, TEXT("Attempted to create initial rep change state on an object which is null.")); checkf(!Object->IsPendingKill(), TEXT("Attempted to create initial rep change state on an object which is pending kill. This will fail to create a RepLayout: "), *Object->GetName()); +#if ENGINE_MINOR_VERSION <= 20 FObjectReplicator& Replicator = FindOrCreateReplicator(Object).Get(); +#else + FObjectReplicator& Replicator = FindOrCreateReplicator(Object.Get()).Get(); +#endif TArray InitialRepChanged; @@ -311,13 +359,24 @@ int64 USpatialActorChannel::ReplicateActor() // Update SpatialOS position. if (!bCreatingNewEntity) { - UpdateSpatialPosition(); + if (GetDefault()->bBatchSpatialPositionUpdates) + { + Sender->RegisterChannelForPositionUpdate(this); + } + else + { + UpdateSpatialPositionWithFrequencyCheck(); + } } // Update the replicated property change list. FRepChangelistState* ChangelistState = ActorReplicator->ChangelistMgr->GetRepChangelistState(); bool bWroteSomethingImportant = false; +#if ENGINE_MINOR_VERSION <= 20 ActorReplicator->ChangelistMgr->Update(Actor, Connection->Driver->ReplicationFrame, ActorReplicator->RepState->LastCompareIndex, RepFlags, bForceCompareProperties); +#else + ActorReplicator->ChangelistMgr->Update(ActorReplicator->RepState.Get(), Actor, Connection->Driver->ReplicationFrame, RepFlags, bForceCompareProperties); +#endif const int32 PossibleNewHistoryIndex = ActorReplicator->RepState->HistoryEnd % FRepState::MAX_CHANGE_HISTORY; FRepChangedHistory& PossibleNewHistoryItem = ActorReplicator->RepState->ChangeHistory[PossibleNewHistoryIndex]; @@ -356,6 +415,10 @@ int64 USpatialActorChannel::ReplicateActor() { if (bCreatingNewEntity) { + // Need to try replicating all subobjects before entity creation to make sure their respective FObjectReplicator exists + // so we know what subobjects are relevant for replication when creating the entity. + Actor->ReplicateSubobjects(this, &Bunch, &RepFlags); + Sender->SendCreateEntityRequest(this); // Since we've tried to create this Actor in Spatial, we no longer have authority over the actor since it hasn't been delegated to us. @@ -397,7 +460,7 @@ int64 USpatialActorChannel::ReplicateActor() for (auto& SubobjectInfoPair : GetHandoverSubobjects()) { UObject* Subobject = SubobjectInfoPair.Key; - FClassInfo& SubobjectInfo = *SubobjectInfoPair.Value; + const FClassInfo& SubobjectInfo = *SubobjectInfoPair.Value; // Handover shadow data should already exist for this object. If it doesn't, it must have // started replicating after SetChannelActor was called on the owning actor. @@ -414,9 +477,27 @@ int64 USpatialActorChannel::ReplicateActor() Sender->SendComponentUpdates(Subobject, SubobjectInfo, this, nullptr, &SubobjectHandoverChangeState); } } - } - // TODO: Handle deleted subobjects - see DataChannel.cpp:2542 - UNR:581 + // Look for deleted subobjects + for (auto RepComp = ReplicationMap.CreateIterator(); RepComp; ++RepComp) + { +#if ENGINE_MINOR_VERSION <= 20 + if (!RepComp.Key().IsValid()) +#else + if (!RepComp.Value()->GetWeakObjectPtr().IsValid()) +#endif + { + FUnrealObjectRef ObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromNetGUID(RepComp.Value().Get().ObjectNetGUID); + if (ObjectRef.IsValid()) + { + Sender->SendRemoveComponent(EntityId, NetDriver->ClassInfoManager->GetClassInfoByComponentId(ObjectRef.Offset)); + } + + RepComp.Value()->CleanUp(); + RepComp.RemoveCurrent(); + } + } + } // If we evaluated everything, mark LastUpdateTime, even if nothing changed. LastUpdateTime = Connection->Driver->Time; @@ -430,13 +511,113 @@ int64 USpatialActorChannel::ReplicateActor() return (bWroteSomethingImportant) ? 1 : 0; // TODO: return number of bits written (UNR-664) } -bool USpatialActorChannel::ReplicateSubobject(UObject* Object, const FClassInfo& Info, const FReplicationFlags& RepFlags) +void USpatialActorChannel::DynamicallyAttachSubobject(UObject* Object) +{ + // Find out if this is a dynamic subobject or a subobject that is already attached but is now replicated + FUnrealObjectRef ObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromObject(Object); + + const FClassInfo* Info = nullptr; + + // Subobject that's a part of the CDO by default does not need to be created. + if (ObjectRef.IsValid()) + { + Info = &NetDriver->ClassInfoManager->GetOrCreateClassInfoByObject(Object); + } + else + { + Info = TryResolveNewDynamicSubobjectAndGetClassInfo(Object); + + if (Info == nullptr) + { + // This is a failure but there is already a log inside TryResolveNewDynamicSubbojectAndGetClassInfo + return; + } + } + + check(Info != nullptr); + + // Check to see if we already have authority over the subobject to be added + if (NetDriver->StaticComponentView->HasAuthority(EntityId, Info->SchemaComponents[SCHEMA_Data])) + { + Sender->SendAddComponent(this, Object, *Info); + } + else + { + // If we don't, modify the entity ACL to gain authority. + PendingDynamicSubobjects.Add(TWeakObjectPtr(Object)); + Sender->GainAuthorityThenAddComponent(this, Object, Info); + } +} + +const FClassInfo* USpatialActorChannel::TryResolveNewDynamicSubobjectAndGetClassInfo(UObject* Object) +{ + const FClassInfo* Info = nullptr; + + const FClassInfo& SubobjectInfo = NetDriver->ClassInfoManager->GetOrCreateClassInfoByClass(Object->GetClass()); + + // Find the first ClassInfo relating to a dynamic subobject + // which has not been used on this entity. + for (const auto& DynamicSubobjectInfo : SubobjectInfo.DynamicSubobjectInfo) + { + if (!NetDriver->PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(EntityId, DynamicSubobjectInfo->SchemaComponents[SCHEMA_Data])).IsValid()) + { + Info = &DynamicSubobjectInfo.Get(); + break; + } + } + + // If all ClassInfos are used up, we error. + if (Info == nullptr) + { + UE_LOG(LogSpatialActorChannel, Error, TEXT("Too many dynamic subobjects of type %s attached to Actor %s! Please increase" + " the max number of dynamically attached subobjects per class in the SpatialOS runtime settings."), *Object->GetClass()->GetName(), *Actor->GetName()); + return Info; + } + + NetDriver->PackageMap->ResolveSubobject(Object, FUnrealObjectRef(EntityId, Info->SchemaComponents[SCHEMA_Data])); + + return Info; +} + +bool USpatialActorChannel::ReplicateSubobject(UObject* Object, const FReplicationFlags& RepFlags) { SCOPE_CYCLE_COUNTER(STAT_SpatialActorChannelReplicateSubobject); + bool bCreatedReplicator = false; + +#if ENGINE_MINOR_VERSION <= 20 + bCreatedReplicator = !ReplicationMap.Contains(Object); FObjectReplicator& Replicator = FindOrCreateReplicator(Object).Get(); +#else + FObjectReplicator& Replicator = FindOrCreateReplicator(Object, &bCreatedReplicator).Get(); +#endif + + // If we're creating an entity, don't try replicating + if (bCreatingNewEntity) + { + return false; + } + + // New subobject that hasn't been replicated before + if (bCreatedReplicator) + { + // Attach to to the entity + DynamicallyAttachSubobject(Object); + return false; + } + + if (PendingDynamicSubobjects.Contains(Object)) + { + // Still waiting on subobject to be attached so don't replicate + return false; + } + FRepChangelistState* ChangelistState = Replicator.ChangelistMgr->GetRepChangelistState(); +#if ENGINE_MINOR_VERSION <= 20 Replicator.ChangelistMgr->Update(Object, Replicator.Connection->Driver->ReplicationFrame, Replicator.RepState->LastCompareIndex, RepFlags, bForceCompareProperties); +#else + Replicator.ChangelistMgr->Update(Replicator.RepState.Get(), Object, Replicator.Connection->Driver->ReplicationFrame, RepFlags, bForceCompareProperties); +#endif const int32 PossibleNewHistoryIndex = Replicator.RepState->HistoryEnd % FRepState::MAX_CHANGE_HISTORY; FRepChangedHistory& PossibleNewHistoryItem = Replicator.RepState->ChangeHistory[PossibleNewHistoryIndex]; @@ -464,7 +645,17 @@ bool USpatialActorChannel::ReplicateSubobject(UObject* Object, const FClassInfo& if (RepChanged.Num() > 0) { FRepChangeState RepChangeState = { RepChanged, GetObjectRepLayout(Object) }; + + FUnrealObjectRef ObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromObject(Object); + if (!ObjectRef.IsValid()) + { + UE_LOG(LogSpatialActorChannel, Verbose, TEXT("Attempted to replicate an invalid ObjectRef. This may be a dynamic component that couldn't attach: %s"), *Object->GetName()); + return false; + } + + const FClassInfo& Info = NetDriver->ClassInfoManager->GetOrCreateClassInfoByObject(Object); Sender->SendComponentUpdates(Object, Info, this, &RepChangeState, nullptr); + Replicator.RepState->HistoryEnd++; } @@ -477,26 +668,18 @@ bool USpatialActorChannel::ReplicateSubobject(UObject* Object, const FClassInfo& bool USpatialActorChannel::ReplicateSubobject(UObject* Obj, FOutBunch& Bunch, const FReplicationFlags& RepFlags) { // Intentionally don't call Super::ReplicateSubobject() but rather call our custom version instead. - - if (!NetDriver->PackageMap->GetUnrealObjectRefFromObject(Obj).IsValid()) - { - // Not supported for Spatial replication - return false; - } - - const FClassInfo& SubobjectInfo = NetDriver->ClassInfoManager->GetOrCreateClassInfoByObject(Obj); - return ReplicateSubobject(Obj, SubobjectInfo, RepFlags); + return ReplicateSubobject(Obj, RepFlags); } -TMap USpatialActorChannel::GetHandoverSubobjects() +TMap USpatialActorChannel::GetHandoverSubobjects() { const FClassInfo& Info = NetDriver->ClassInfoManager->GetOrCreateClassInfoByClass(Actor->GetClass()); - TMap FoundSubobjects; + TMap FoundSubobjects; for (auto& SubobjectInfoPair : Info.SubobjectInfo) { - FClassInfo& SubobjectInfo = SubobjectInfoPair.Value.Get(); + const FClassInfo& SubobjectInfo = SubobjectInfoPair.Value.Get(); if (SubobjectInfo.HandoverProperties.Num() == 0) { @@ -593,7 +776,7 @@ void USpatialActorChannel::SetChannelActor(AActor* InActor) } else { - UE_LOG(LogSpatialActorChannel, Log, TEXT("Opened channel for actor %s with existing entity ID %lld."), *InActor->GetName(), EntityId); + UE_LOG(LogSpatialActorChannel, Verbose, TEXT("Opened channel for actor %s with existing entity ID %lld."), *InActor->GetName(), EntityId); if (PackageMap->IsEntityIdPendingCreation(EntityId)) { @@ -621,6 +804,8 @@ void USpatialActorChannel::SetChannelActor(AActor* InActor) check(!HandoverShadowDataMap.Contains(Subobject)); InitializeHandoverShadowData(HandoverShadowDataMap.Add(Subobject, MakeShared>()).Get(), Subobject); } + + SavedOwnerWorkerAttribute = SpatialGDK::GetOwnerWorkerAttribute(InActor); } bool USpatialActorChannel::TryResolveActor() @@ -663,7 +848,13 @@ void USpatialActorChannel::PostReceiveSpatialUpdate(UObject* TargetObject, const FObjectReplicator& Replicator = FindOrCreateReplicator(TargetObject).Get(); TargetObject->PostNetReceive(); + +#if ENGINE_MINOR_VERSION <= 20 Replicator.RepNotifies = RepNotifies; +#else + Replicator.RepState->RepNotifies = RepNotifies; +#endif + Replicator.CallRepNotifies(false); if (!TargetObject->IsPendingKill()) @@ -706,19 +897,26 @@ void USpatialActorChannel::UpdateSpatialPosition() { SCOPE_CYCLE_COUNTER(STAT_SpatialActorChannelUpdateSpatialPosition); + // Additional check to validate Actor is still present + if (Actor == nullptr || Actor->IsPendingKill()) + { + return; + } + // When we update an Actor's position, we want to update the position of all the children of this Actor. // If this Actor is a PlayerController, we want to update all of its children and its possessed Pawn. // That means if this Actor has an Owner or has a NetConnection and is NOT a PlayerController // we want to defer updating position until we reach the highest parent. - if ((Actor->GetOwner() != nullptr || Actor->GetNetConnection() != nullptr) && !Actor->IsA()) - { - return; - } + AActor* ActorOwner = Actor->GetOwner(); - // Check that there has been a sufficient amount of time since the last update. - if ((NetDriver->Time - TimeWhenPositionLastUpdated) < (1.0f / GetDefault()->PositionUpdateFrequency)) + if ((ActorOwner != nullptr || Actor->GetNetConnection() != nullptr) && !Actor->IsA()) { - return; + // 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. + if (!(Actor->GetNetConnection() == nullptr && ActorOwner != nullptr && !ActorOwner->GetIsReplicated())) + { + return; + } } // Check that the Actor has moved sufficiently far to be updated @@ -818,41 +1016,22 @@ void USpatialActorChannel::ServerProcessOwnershipChange() FString NewOwnerWorkerAttribute = SpatialGDK::GetOwnerWorkerAttribute(Actor); - if (bFirstTick || SavedOwnerWorkerAttribute != NewOwnerWorkerAttribute) + if (SavedOwnerWorkerAttribute != NewOwnerWorkerAttribute) { - bool bSuccess = Sender->UpdateEntityACLs(GetEntityId(), NewOwnerWorkerAttribute); + bool bSuccess = Sender->UpdateEntityACLs(EntityId, NewOwnerWorkerAttribute); if (bSuccess) { - bFirstTick = false; SavedOwnerWorkerAttribute = NewOwnerWorkerAttribute; } } } -void USpatialActorChannel::ClientProcessOwnershipChange() -{ - bool bOldNetOwned = bNetOwned; - bNetOwned = IsOwnedByWorker(); - - if (bFirstTick || bOldNetOwned != bNetOwned) - { - Sender->SendComponentInterest(Actor, GetEntityId(), bNetOwned); - bFirstTick = false; - } -} - -void USpatialActorChannel::ProcessOwnershipChange() +void USpatialActorChannel::ClientProcessOwnershipChange(bool bNewNetOwned) { - if (Actor != nullptr && !Actor->IsPendingKill()) + if (bNewNetOwned != bNetOwned) { - if (NetDriver->IsServer()) - { - ServerProcessOwnershipChange(); - } - else - { - ClientProcessOwnershipChange(); - } + bNetOwned = bNewNetOwned; + Sender->SendComponentInterestForActor(this, GetEntityId(), bNetOwned); } } diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialFastArrayNetSerialize.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialFastArrayNetSerialize.cpp index 623897273b..69280a96eb 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialFastArrayNetSerialize.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialFastArrayNetSerialize.cpp @@ -60,7 +60,12 @@ void SpatialFastArrayNetSerializeCB::NetSerializeStruct(UScriptStruct* Struct, F { bHasUnmapped = true; } - checkf(bSuccess, TEXT("NetSerialize on %s failed."), *Struct->GetStructCPPName()); + + // Check the success of the serialization and print a warning if it failed. This is how native handles failed serialization. + if (!bSuccess) + { + UE_LOG(LogSpatialNetSerialize, Warning, TEXT("SpatialFastArrayNetSerialize: NetSerialize %s failed."), *Struct->GetFullName()); + } } else { diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp index 5153437420..27da8068c3 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialGameInstance.cpp @@ -13,6 +13,7 @@ #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPendingNetGame.h" #include "Interop/Connection/SpatialWorkerConnection.h" +#include "Utils/SpatialMetrics.h" DEFINE_LOG_CATEGORY(LogSpatialGameInstance); @@ -114,9 +115,30 @@ void USpatialGameInstance::StartGameInstance() Super::StartGameInstance(); } +bool USpatialGameInstance::ProcessConsoleExec(const TCHAR* Cmd, FOutputDevice& Ar, UObject* Executor) +{ + if (!Super::ProcessConsoleExec(Cmd, Ar, Executor)) + { + if (GetWorld() == nullptr) + { + return false; + } + + USpatialNetDriver* NetDriver = Cast(GetWorld()->GetNetDriver()); + if (NetDriver == nullptr || NetDriver->SpatialMetrics == nullptr) + { + return false; + } + + return NetDriver->SpatialMetrics->ProcessConsoleExec(Cmd, Ar, Executor); + } + return true; +} + void USpatialGameInstance::HandleOnConnected() { UE_LOG(LogSpatialGameInstance, Log, TEXT("Succesfully connected to SpatialOS")); + SpatialWorkerId = SpatialConnection->GetWorkerId(); OnConnected.Broadcast(); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitWriter.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitWriter.cpp index 3e4e50106b..abb24668fa 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitWriter.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetBitWriter.cpp @@ -10,6 +10,8 @@ #include "SpatialConstants.h" #include "Utils/EntityPool.h" +DEFINE_LOG_CATEGORY(LogSpatialNetSerialize); + FSpatialNetBitWriter::FSpatialNetBitWriter(USpatialPackageMapClient* InPackageMap, TSet>& InUnresolvedObjects) : FNetBitWriter(InPackageMap, 0) , UnresolvedObjects(InUnresolvedObjects) diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetConnection.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetConnection.cpp index e6002fa8af..781ad3c3e4 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetConnection.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetConnection.cpp @@ -69,19 +69,6 @@ bool USpatialNetConnection::ClientHasInitializedLevelFor(const AActor* TestActor //Intentionally does not call Super:: } -void USpatialNetConnection::Tick() -{ - // Since we're not receiving actual Unreal packets, Unreal may time out the connection. Timeouts are handled by SpatialOS, so we're setting these values here to keep Unreal happy. - // Note that in the case of InternalAck (UnrealWorker) the engine does this (and more) in Super. - if (!InternalAck) - { - LastReceiveTime = Driver->Time; - LastReceiveRealtime = FPlatformTime::Seconds(); - LastGoodPacketRealtime = FPlatformTime::Seconds(); - } - Super::Tick(); -} - int32 USpatialNetConnection::IsNetReady(bool Saturate) { // TODO: UNR-664 - Currently we do not report the number of bits sent when replicating, this means channel saturation cannot be checked properly. @@ -115,6 +102,31 @@ void USpatialNetConnection::UpdateActorInterest(AActor* Actor) } } +void USpatialNetConnection::ClientNotifyClientHasQuit() +{ + if (PlayerControllerEntity != SpatialConstants::INVALID_ENTITY_ID) + { + if (!Cast(Driver)->StaticComponentView->HasAuthority(PlayerControllerEntity, SpatialConstants::HEARTBEAT_COMPONENT_ID)) + { + UE_LOG(LogSpatialNetConnection, Warning, TEXT("Quit the game but no authority over Heartbeat component: NetConnection %s, PlayerController entity %lld"), *GetName(), PlayerControllerEntity); + return; + } + + Worker_ComponentUpdate Update = {}; + Update.component_id = SpatialConstants::HEARTBEAT_COMPONENT_ID; + Update.schema_type = Schema_CreateComponentUpdate(SpatialConstants::HEARTBEAT_COMPONENT_ID); + Schema_Object* ComponentObject = Schema_GetComponentUpdateFields(Update.schema_type); + + Schema_AddBool(ComponentObject, SpatialConstants::HEARTBEAT_CLIENT_HAS_QUIT_ID, true); + + Cast(Driver)->Connection->SendComponentUpdate(PlayerControllerEntity, &Update); + } + else + { + UE_LOG(LogSpatialNetConnection, Warning, TEXT("Quitting before Heartbeat component has been initialized: NetConnection %s"), *GetName()); + } +} + void USpatialNetConnection::InitHeartbeat(FTimerManager* InTimerManager, Worker_EntityId InPlayerControllerEntity) { checkf(PlayerControllerEntity == SpatialConstants::INVALID_ENTITY_ID, TEXT("InitHeartbeat: PlayerControllerEntity already set: %lld. New entity: %lld"), PlayerControllerEntity, InPlayerControllerEntity); @@ -124,26 +136,6 @@ void USpatialNetConnection::InitHeartbeat(FTimerManager* InTimerManager, Worker_ if (Driver->IsServer()) { SetHeartbeatTimeoutTimer(); - - // Set up heartbeat event callback - TWeakObjectPtr ConnectionPtr = this; - Cast(Driver)->Receiver->AddHeartbeatDelegate(PlayerControllerEntity, HeartbeatDelegate::CreateLambda([ConnectionPtr](Worker_ComponentUpdateOp& Op) - { - if (ConnectionPtr.IsValid()) - { - Schema_Object* EventsObject = Schema_GetComponentUpdateEvents(Op.update.schema_type); - uint32 EventCount = Schema_GetObjectCount(EventsObject, SpatialConstants::HEARTBEAT_EVENT_ID); - if (EventCount > 0) - { - if (EventCount > 1) - { - UE_LOG(LogSpatialNetConnection, Log, TEXT("Received multiple heartbeat events in a single component update, entity %lld."), ConnectionPtr->PlayerControllerEntity); - } - - ConnectionPtr->OnHeartbeat(); - } - } - })); } else { @@ -153,28 +145,34 @@ void USpatialNetConnection::InitHeartbeat(FTimerManager* InTimerManager, Worker_ void USpatialNetConnection::SetHeartbeatTimeoutTimer() { - TimerManager->SetTimer(HeartbeatTimer, [this]() + TimerManager->SetTimer(HeartbeatTimer, [WeakThis = TWeakObjectPtr(this)]() { - // This client timed out. Disconnect it and trigger OnDisconnected logic. - CleanUp(); + if (USpatialNetConnection* Connection = WeakThis.Get()) + { + // This client timed out. Disconnect it and trigger OnDisconnected logic. + Connection->CleanUp(); + } }, GetDefault()->HeartbeatTimeoutSeconds, false); } void USpatialNetConnection::SetHeartbeatEventTimer() { - TimerManager->SetTimer(HeartbeatTimer, [this]() + TimerManager->SetTimer(HeartbeatTimer, [WeakThis = TWeakObjectPtr(this)]() { - Worker_ComponentUpdate ComponentUpdate = {}; + if (USpatialNetConnection* Connection = WeakThis.Get()) + { + Worker_ComponentUpdate ComponentUpdate = {}; - ComponentUpdate.component_id = SpatialConstants::HEARTBEAT_COMPONENT_ID; - ComponentUpdate.schema_type = Schema_CreateComponentUpdate(SpatialConstants::HEARTBEAT_COMPONENT_ID); - Schema_Object* EventsObject = Schema_GetComponentUpdateEvents(ComponentUpdate.schema_type); - Schema_AddObject(EventsObject, SpatialConstants::HEARTBEAT_EVENT_ID); + ComponentUpdate.component_id = SpatialConstants::HEARTBEAT_COMPONENT_ID; + ComponentUpdate.schema_type = Schema_CreateComponentUpdate(SpatialConstants::HEARTBEAT_COMPONENT_ID); + Schema_Object* EventsObject = Schema_GetComponentUpdateEvents(ComponentUpdate.schema_type); + Schema_AddObject(EventsObject, SpatialConstants::HEARTBEAT_EVENT_ID); - USpatialWorkerConnection* Connection = Cast(Driver)->Connection; - if (Connection->IsConnected()) - { - Connection->SendComponentUpdate(PlayerControllerEntity, &ComponentUpdate); + USpatialWorkerConnection* WorkerConnection = Cast(Connection->Driver)->Connection; + if (WorkerConnection->IsConnected()) + { + WorkerConnection->SendComponentUpdate(Connection->PlayerControllerEntity, &ComponentUpdate); + } } }, GetDefault()->HeartbeatIntervalSeconds, true, 0.0f); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp index 27cfc1f67f..cc2b88b315 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialNetDriver.cpp @@ -2,12 +2,12 @@ #include "EngineClasses/SpatialNetDriver.h" -#include "EngineGlobals.h" #include "Engine/ActorChannel.h" #include "Engine/ChildConnection.h" #include "Engine/Engine.h" #include "Engine/LocalPlayer.h" #include "Engine/NetworkObjectList.h" +#include "EngineGlobals.h" #include "GameFramework/GameModeBase.h" #include "GameFramework/GameNetworkManager.h" #include "Net/DataReplication.h" @@ -15,30 +15,48 @@ #include "SocketSubsystem.h" #include "UObject/UObjectIterator.h" +#include "EngineClasses/SpatialActorChannel.h" +#include "EngineClasses/SpatialGameInstance.h" +#include "EngineClasses/SpatialNetConnection.h" +#include "EngineClasses/SpatialPackageMapClient.h" +#include "EngineClasses/SpatialPendingNetGame.h" #include "Interop/Connection/SpatialWorkerConnection.h" #include "Interop/GlobalStateManager.h" #include "Interop/SnapshotManager.h" #include "Interop/SpatialClassInfoManager.h" +#include "Interop/SpatialDispatcher.h" #include "Interop/SpatialPlayerSpawner.h" #include "Interop/SpatialReceiver.h" #include "Interop/SpatialSender.h" -#include "Interop/SpatialDispatcher.h" -#include "EngineClasses/SpatialActorChannel.h" -#include "EngineClasses/SpatialGameInstance.h" -#include "EngineClasses/SpatialNetConnection.h" -#include "EngineClasses/SpatialPackageMapClient.h" -#include "EngineClasses/SpatialPendingNetGame.h" #include "SpatialConstants.h" #include "SpatialGDKSettings.h" -#include "Utils/EngineVersionCheck.h" +#include "Utils/ActorGroupManager.h" #include "Utils/EntityPool.h" +#include "Utils/InterestFactory.h" +#include "Utils/OpUtils.h" #include "Utils/SpatialMetrics.h" +#include "Utils/SpatialMetricsDisplay.h" + +#if WITH_EDITOR +#include "SpatialGDKServicesModule.h" +#endif DEFINE_LOG_CATEGORY(LogSpatialOSNetDriver); DECLARE_CYCLE_STAT(TEXT("ServerReplicateActors"), STAT_SpatialServerReplicateActors, STATGROUP_SpatialNet); DEFINE_STAT(STAT_SpatialConsiderList); +USpatialNetDriver::USpatialNetDriver(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , bAuthoritativeDestruction(true) + , bConnectAsClient(false) + , bPersistSpatialConnection(true) + , bWaitingForAcceptingPlayersToSpawn(false) + , NextRPCIndex(0) + , TimeWhenPositionLastUpdated(0.f) +{ +} + bool USpatialNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, const FURL& URL, bool bReuseAddressAndPort, FString& Error) { if (!Super::InitBase(bInitAsClient, InNotify, URL, bReuseAddressAndPort, Error)) @@ -51,13 +69,26 @@ bool USpatialNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, c bConnectAsClient = bInitAsClient; bAuthoritativeDestruction = true; + bIsReadyToStart = bInitAsClient; FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &USpatialNetDriver::OnMapLoaded); FWorldDelegates::LevelAddedToWorld.AddUObject(this, &USpatialNetDriver::OnLevelAddedToWorld); // Make absolutely sure that the actor channel that we are using is our Spatial actor channel +#if ENGINE_MINOR_VERSION <= 20 ChannelClasses[CHTYPE_Actor] = USpatialActorChannel::StaticClass(); +#else + // Copied from what the Engine does with UActorChannel + FChannelDefinition SpatialChannelDefinition{}; + SpatialChannelDefinition.ChannelName = NAME_Actor; + SpatialChannelDefinition.ClassName = FName(*USpatialActorChannel::StaticClass()->GetPathName()); + SpatialChannelDefinition.ChannelClass = USpatialActorChannel::StaticClass(); + SpatialChannelDefinition.bServerOpen = true; + + ChannelDefinitions[CHTYPE_Actor] = SpatialChannelDefinition; + ChannelDefinitionMap[NAME_Actor] = SpatialChannelDefinition; +#endif // Extract the snapshot to load (if any) from the map URL so that once we are connected to a deployment we can load that snapshot into the Spatial deployment. SnapshotToLoad = URL.GetOption(*SpatialConstants::SnapshotURLOption, TEXT("")); @@ -74,28 +105,65 @@ bool USpatialNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, c bPersistSpatialConnection = true; } + // Initialize ActorGroupManager as it is a depdency of ClassInfoManager (see below) + ActorGroupManager = NewObject(); + ActorGroupManager->Init(); + // Initialize ClassInfoManager here because it needs to load SchemaDatabase. // We shouldn't do that in CreateAndInitializeCoreClasses because it is called // from OnConnectedToSpatialOS callback which could be executed with the async // loading thread suspended (e.g. when resuming rendering thread), in which // case we'll crash upon trying to load SchemaDatabase. ClassInfoManager = NewObject(); - ClassInfoManager->Init(this); - InitiateConnectionToSpatialOS(URL); + // If it fails to load, don't attempt to connect to spatial. + if (!ClassInfoManager->TryInit(this, ActorGroupManager)) + { + Error = TEXT("Failed to load Spatial SchemaDatabase! Make sure that schema has been generated for your project"); + return false; + } - return true; -} + if (!bInitAsClient) + { + GatherClientInterestDistances(); + } -void USpatialNetDriver::PostInitProperties() -{ - Super::PostInitProperties(); +#if WITH_EDITOR + PlayInEditorID = GPlayInEditorID; - if (!HasAnyFlags(RF_ClassDefaultObject)) + // If we're launching in PIE then ensure there is a deployment running before connecting. + if (FSpatialGDKServicesModule* GDKServices = FModuleManager::GetModulePtr("SpatialGDKServices")) { - // GuidCache will be allocated as an FNetGUIDCache above. To avoid an engine code change, we re-do it with the Spatial equivalent. - GuidCache = MakeShareable(new FSpatialNetGUIDCache(this)); + FLocalDeploymentManager* LocalDeploymentManager = GDKServices->GetLocalDeploymentManager(); + + // Wait for a running local deployment before connecting. If the deployment has already started then just connect. + if (LocalDeploymentManager->ShouldWaitForDeployment()) + { + UE_LOG(LogSpatialOSNetDriver, Display, TEXT("Waiting for local SpatialOS deployment to start before connecting...")); + SpatialDeploymentStartHandle = LocalDeploymentManager->OnDeploymentStart.AddLambda([WeakThis = TWeakObjectPtr(this), URL] + { + if (!WeakThis.IsValid()) + { + return; + } + UE_LOG(LogSpatialOSNetDriver, Display, TEXT("Local deployment started, connecting with URL: %s"), *URL.ToString()); + + WeakThis.Get()->InitiateConnectionToSpatialOS(URL); + if (FSpatialGDKServicesModule* GDKServices = FModuleManager::GetModulePtr("SpatialGDKServices")) + { + GDKServices->GetLocalDeploymentManager()->OnDeploymentStart.Remove(WeakThis.Get()->SpatialDeploymentStartHandle); + } + }); + + return true; + } } + +#endif + + InitiateConnectionToSpatialOS(URL); + + return true; } void USpatialNetDriver::InitiateConnectionToSpatialOS(const FURL& URL) @@ -123,7 +191,10 @@ void USpatialNetDriver::InitiateConnectionToSpatialOS(const FURL& URL) if (!bPersistSpatialConnection) { // Destroy the old connection - GameInstance->GetSpatialWorkerConnection()->DestroyConnection(); + if (USpatialWorkerConnection* OldConnection = GameInstance->GetSpatialWorkerConnection()) + { + OldConnection->DestroyConnection(); + } // Create a new SpatialWorkerConnection in the SpatialGameInstance. GameInstance->CreateNewSpatialWorkerConnection(); @@ -137,14 +208,14 @@ void USpatialNetDriver::InitiateConnectionToSpatialOS(const FURL& URL) Connection->LocatorConfig.PlayerIdentityToken = URL.GetOption(TEXT("playeridentity="), TEXT("")); Connection->LocatorConfig.LoginToken = URL.GetOption(TEXT("login="), TEXT("")); Connection->LocatorConfig.UseExternalIp = true; - Connection->LocatorConfig.WorkerType = GameInstance->GetSpatialWorkerType(); + Connection->LocatorConfig.WorkerType = GameInstance->GetSpatialWorkerType().ToString(); } else // Using Receptionist { - Connection->ReceptionistConfig.WorkerType = GameInstance->GetSpatialWorkerType(); + Connection->ReceptionistConfig.WorkerType = GameInstance->GetSpatialWorkerType().ToString(); // Check for overrides in the travel URL. - if (!URL.Host.IsEmpty()) + if (!URL.Host.IsEmpty() && URL.Host.Compare(SpatialConstants::LOCAL_HOST) != 0) { Connection->ReceptionistConfig.ReceptionistHost = URL.Host; } @@ -187,6 +258,7 @@ void USpatialNetDriver::OnConnectedToSpatialOS() if (IsServer()) { + Sender->CreateServerWorkerEntity(); HandleOngoingServerTravel(); } } @@ -220,10 +292,18 @@ void USpatialNetDriver::CreateAndInitializeCoreClasses() SnapshotManager = NewObject(); SpatialMetrics = NewObject(); +#if !UE_BUILD_SHIPPING + // If metrics display is enabled, spawn a singleton actor to replicate the information to each client + if (IsServer() && GetDefault()->bEnableMetricsDisplay) + { + SpatialMetricsDisplay = GetWorld()->SpawnActor(); + } +#endif + PackageMap = Cast(GetSpatialOSNetConnection()->PackageMap); PackageMap->Init(this); Dispatcher->Init(this); - Sender->Init(this); + Sender->Init(this, &TimerManager); Receiver->Init(this, &TimerManager); GlobalStateManager->Init(this, &TimerManager); SnapshotManager->Init(this); @@ -369,11 +449,12 @@ void USpatialNetDriver::OnAcceptingPlayersChanged(bool bAcceptingPlayers) // Extract map name and options FWorldContext& WorldContext = GEngine->GetWorldContextFromPendingNetGameNetDriverChecked(this); + FURL LastURL = WorldContext.PendingNetGame->URL; - FURL RedirectURL = FURL(&WorldContext.LastURL, *GlobalStateManager->DeploymentMapURL, (ETravelType)WorldContext.TravelType); - RedirectURL.Host = WorldContext.LastURL.Host; - RedirectURL.Port = WorldContext.LastURL.Port; - RedirectURL.Op.Append(WorldContext.LastURL.Op); + FURL RedirectURL = FURL(&LastURL, *GlobalStateManager->DeploymentMapURL, (ETravelType)WorldContext.TravelType); + RedirectURL.Host = LastURL.Host; + RedirectURL.Port = LastURL.Port; + RedirectURL.Op.Append(LastURL.Op); RedirectURL.AddOption(*SpatialConstants::ClientsStayConnectedURLOption); WorldContext.PendingNetGame->bSuccessfullyConnected = true; @@ -477,6 +558,36 @@ void USpatialNetDriver::SpatialProcessServerTravel(const FString& URL, bool bAbs #endif // WITH_SERVER_CODE } +void USpatialNetDriver::BeginDestroy() +{ + Super::BeginDestroy(); + + // If we are still connected, cleanup our corresponding worker entity if it exists. + if (Connection != nullptr && WorkerEntityId != SpatialConstants::INVALID_ENTITY_ID) + { + Connection->SendDeleteEntityRequest(WorkerEntityId); + } + +#if WITH_EDITOR + // Ensure our OnDeploymentStart delegate is removed when the net driver is shut down. + if (FSpatialGDKServicesModule* GDKServices = FModuleManager::GetModulePtr("SpatialGDKServices")) + { + GDKServices->GetLocalDeploymentManager()->OnDeploymentStart.Remove(SpatialDeploymentStartHandle); + } +#endif +} + +void USpatialNetDriver::PostInitProperties() +{ + Super::PostInitProperties(); + + if (!HasAnyFlags(RF_ClassDefaultObject)) + { + // GuidCache will be allocated as an FNetGUIDCache above. To avoid an engine code change, we re-do it with the Spatial equivalent. + GuidCache = MakeShareable(new FSpatialNetGUIDCache(this)); + } +} + bool USpatialNetDriver::IsLevelInitializedForActor(const AActor* InActor, const UNetConnection* InConnection) const { //In our case, the connection is not specific to a client. Thus, it's not relevant whether the level is initialized. @@ -509,7 +620,11 @@ void USpatialNetDriver::NotifyActorDestroyed(AActor* ThisActor, bool IsSeamlessT check(Channel->OpenedLocally); Channel->bClearRecentActorRefs = false; // TODO: UNR-952 - Add code here for cleaning up actor channels from our maps. +#if ENGINE_MINOR_VERSION <= 20 Channel->Close(); +#else + Channel->Close(EChannelCloseReason::Destroyed); +#endif } // Remove it from any dormancy lists @@ -524,8 +639,27 @@ void USpatialNetDriver::NotifyActorDestroyed(AActor* ThisActor, bool IsSeamlessT RenamedStartupActors.Remove(ThisActor->GetFName()); } +void USpatialNetDriver::Shutdown() +{ + if (!IsServer()) + { + // Notify the server that we're disconnecting so it can clean up our actors. + if (USpatialNetConnection* SpatialNetConnection = Cast(ServerConnection)) + { + SpatialNetConnection->ClientNotifyClientHasQuit(); + } + } + + Super::Shutdown(); +} + void USpatialNetDriver::OnOwnerUpdated(AActor* Actor) { + if (!IsServer()) + { + return; + } + // If PackageMap doesn't exist, we haven't connected yet, which means // we don't need to update the interest at this point if (PackageMap == nullptr) @@ -546,6 +680,8 @@ void USpatialNetDriver::OnOwnerUpdated(AActor* Actor) } Channel->MarkInterestDirty(); + + Channel->ServerProcessOwnershipChange(); } //SpatialGDK: Functions in the ifdef block below are modified versions of the UNetDriver:: implementations. @@ -756,8 +892,11 @@ void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConne // This deletion entry is for an actor in a streaming level the connection doesn't have loaded, so skip it continue; } - +#if ENGINE_MINOR_VERSION <= 20 UActorChannel* Channel = (UActorChannel*)InConnection->CreateChannel(CHTYPE_Actor, 1); +#else + UActorChannel* Channel = (UActorChannel*)InConnection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::OpenedLocally); +#endif if (Channel) { UE_LOG(LogNetTraffic, Log, TEXT("Server replicate actor creating destroy channel for NetGUID <%s,%s> Priority: %d"), *PriorityActors[j]->DestructionInfo->NetGUID.ToString(), *PriorityActors[j]->DestructionInfo->PathName, PriorityActors[j]->Priority); @@ -839,24 +978,11 @@ void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConne continue; } - // If we're a singleton, and don't have a channel, defer to GSM - if (Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)) - { - Channel = GlobalStateManager->AddSingleton(Actor); - } - else + Channel = CreateSpatialActorChannel(Actor, Cast(InConnection)); + if ((Channel == nullptr) && (Actor->NetUpdateFrequency < 1.0f)) { - // Create a new channel for this actor. - Channel = (USpatialActorChannel*)InConnection->CreateChannel(CHTYPE_Actor, 1); - if (Channel) - { - Channel->SetChannelActor(Actor); - } - else if (Actor->NetUpdateFrequency < 1.0f) - { - UE_LOG(LogNetTraffic, Log, TEXT("Unable to replicate %s"), *Actor->GetName()); - PriorityActors[j]->ActorInfo->NextUpdateTime = Actor->GetWorld()->TimeSeconds + 0.2f * FMath::FRand(); - } + UE_LOG(LogNetTraffic, Log, TEXT("Unable to replicate %s"), *Actor->GetName()); + PriorityActors[j]->ActorInfo->NextUpdateTime = Actor->GetWorld()->TimeSeconds + 0.2f * FMath::FRand(); } } @@ -912,7 +1038,11 @@ void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConne { 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. +#if ENGINE_MINOR_VERSION <= 20 Channel->Close(); +#else + Channel->Close(Actor->GetTearOff() ? EChannelCloseReason::TearOff : EChannelCloseReason::Relevancy); +#endif } } } @@ -921,6 +1051,42 @@ void USpatialNetDriver::ServerReplicateActors_ProcessPrioritizedActors(UNetConne // SpatialGDK - Here Unreal would return the position of the last replicated actor in PriorityActors before the channel became saturated. // In Spatial we use ActorReplicationRateLimit and EntityCreationRateLimit to limit replication so this return value is not relevant. } + +void USpatialNetDriver::ProcessRPC(AActor* Actor, UObject* SubObject, UFunction* Function, void* Parameters) +{ + // The RPC might have been called by an actor directly, or by a subobject on that actor + UObject* CallingObject = SubObject != nullptr ? SubObject : Actor; + + if (IsServer()) + { + // Creating channel to ensure that object will be resolvable + GetOrCreateSpatialActorChannel(CallingObject); + } + + int ReliableRPCIndex = 0; + if (GetDefault()->bCheckRPCOrder) + { + if (Function->HasAnyFunctionFlags(FUNC_NetReliable) && !Function->HasAnyFunctionFlags(FUNC_NetMulticast)) + { + ReliableRPCIndex = GetNextReliableRPCId(Actor, FunctionFlagsToRPCSchemaType(Function->FunctionFlags), CallingObject); + } + } + + TSet> UnresolvedObjects; + RPCPayload Payload = Sender->CreateRPCPayloadFromParams(CallingObject, Function, ReliableRPCIndex, Parameters, UnresolvedObjects); + + if (UnresolvedObjects.Num() == 0) + { + FUnrealObjectRef ObjectRef = PackageMap->GetUnrealObjectRefFromObject(CallingObject); + FPendingRPCParamsPtr RPCParams = MakeUnique(ObjectRef, MoveTemp(Payload), ReliableRPCIndex); + Sender->ProcessRPC(MoveTemp(RPCParams)); + } + else + { + UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("The object %s is unresolved because of failure to create a SpatialActorChannel; RPC %s will be dropped."), *UnresolvedObjects.CreateIterator()->Get()->GetName(), *Function->GetName()); + } +} + #endif // SpatialGDK: This is a modified and simplified version of UNetDriver::ServerReplicateActors. @@ -1033,6 +1199,10 @@ int32 USpatialNetDriver::ServerReplicateActors(float DeltaSeconds) DebugRelevantActors = false; } +#if !UE_BUILD_SHIPPING + ConsiderListSize = FinalSortedCount; +#endif + return Updated; #else return 0; @@ -1048,6 +1218,13 @@ void USpatialNetDriver::TickDispatch(float DeltaTime) { TArray OpLists = Connection->GetOpList(); + // Servers will queue ops at startup until we've extracted necessary information from the op stream + if (!bIsReadyToStart) + { + HandleStartupOpQueueing(OpLists); + return; + } + for (Worker_OpList* OpList : OpLists) { Dispatcher->ProcessOps(OpList); @@ -1076,10 +1253,10 @@ void USpatialNetDriver::ProcessRemoteFunction( return; } - USpatialNetConnection* NetConnection = ServerConnection ? Cast(ServerConnection) : GetSpatialOSNetConnection(); + USpatialNetConnection* NetConnection = GetSpatialOSNetConnection(); if (NetConnection == nullptr) { - UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Attempted to call ProcessRemoteFunction before connection was established")); + UE_LOG(LogSpatialOSNetDriver, Error, TEXT("Attempted to call ProcessRemoteFunction but no SpatialOSNetConnection existed. Has this worker established a connection?")); return; } @@ -1094,6 +1271,15 @@ void USpatialNetDriver::ProcessRemoteFunction( return; } + // The RPC might have been called by an actor directly, or by a subobject on that actor + UObject* CallingObject = SubObject ? SubObject : Actor; + + if (CallingObject->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_NotSpatialType)) + { + UE_LOG(LogSpatialOSNetDriver, Verbose, TEXT("Trying to call RPC %s on object %s (class %s) that isn't supported by Spatial. This RPC will be dropped."), *Function->GetName(), *CallingObject->GetName(), *CallingObject->GetClass()->GetName()); + return; + } + // Copied from UNetDriver::ProcessRemoteFunctionForChannel to copy pass-by-ref // parameters from OutParms into Parameters's memory. if (Stack == nullptr) @@ -1127,21 +1313,9 @@ void USpatialNetDriver::ProcessRemoteFunction( } } - // The RPC might have been called by an actor directly, or by a subobject on that actor - UObject* CallingObject = SubObject ? SubObject : Actor; - if (Function->FunctionFlags & FUNC_Net) { - TSharedRef RPCParams = MakeShared(CallingObject, Function, Parameters, NextRPCIndex++); - -#if !UE_BUILD_SHIPPING - if (Function->HasAnyFunctionFlags(FUNC_NetReliable) && !Function->HasAnyFunctionFlags(FUNC_NetMulticast)) - { - RPCParams->ReliableRPCIndex = GetNextReliableRPCId(Actor, FunctionFlagsToRPCSchemaType(Function->FunctionFlags), CallingObject); - } -#endif // !UE_BUILD_SHIPPING - - Sender->SendRPC(RPCParams); + ProcessRPC(Actor, SubObject, Function, Parameters); } } @@ -1176,9 +1350,27 @@ void USpatialNetDriver::TickFlush(float DeltaTime) UE_LOG(LogNetTraffic, Verbose, TEXT("%s replicated %d actors"), *GetDescription(), Updated); } LastUpdateCount = Updated; + + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + + if (SpatialGDKSettings->bBatchSpatialPositionUpdates && Sender != nullptr) + { + if ((Time - TimeWhenPositionLastUpdated) >= (1.0f / SpatialGDKSettings->PositionUpdateFrequency)) + { + TimeWhenPositionLastUpdated = Time; + + Sender->ProcessPositionUpdates(); + } + } + #endif // WITH_SERVER_CODE } + if (GetDefault()->bPackRPCs && Sender != nullptr) + { + Sender->FlushPackedRPCs(); + } + // Tick the timer manager { TimerManager.Tick(DeltaTime); @@ -1193,10 +1385,14 @@ USpatialNetConnection * USpatialNetDriver::GetSpatialOSNetConnection() const { return Cast(ServerConnection); } - else + else if (ClientConnections.Num() > 0) { return Cast(ClientConnections[0]); } + else + { + return nullptr; + } } USpatialNetConnection* USpatialNetDriver::AcceptNewPlayer(const FURL& InUrl, FUniqueNetIdRepl UniqueId, FName OnlinePlatformName, bool bExistingPlayer) @@ -1208,8 +1404,10 @@ USpatialNetConnection* USpatialNetDriver::AcceptNewPlayer(const FURL& InUrl, FUn // We create a "dummy" connection that corresponds to this player. This connection won't transmit any data. // We may not need to keep it in the future, but for now it looks like path of least resistance is to have one UPlayer (UConnection) per player. + // We use an internal counter to give each client a unique IP address for Unreal's internal bookkeeping. ISocketSubsystem* SocketSubsystem = GetSocketSubsystem(); - TSharedRef FromAddr = SocketSubsystem->CreateInternetAddr(); + TSharedRef FromAddr = SocketSubsystem->CreateInternetAddr(UniqueClientIpAddressCounter); + UniqueClientIpAddressCounter++; SpatialConnection->InitRemoteConnection(this, nullptr, InUrl, *FromAddr, USOCK_Open); Notify->NotifyAcceptedConnection(SpatialConnection); @@ -1464,11 +1662,57 @@ TMap& USpatialNetDriver::GetEntityTo return EntityToActorChannel; } +USpatialActorChannel* USpatialNetDriver::GetOrCreateSpatialActorChannel(UObject* TargetObject) +{ + check(TargetObject); + USpatialActorChannel* Channel = GetActorChannelByEntityId(PackageMap->GetEntityIdFromObject(TargetObject)); + if (Channel == nullptr) + { + AActor* TargetActor = Cast(TargetObject); + if (TargetActor == nullptr) + { + TargetActor = Cast(TargetObject->GetOuter()); + } + check(TargetActor); + Channel = CreateSpatialActorChannel(TargetActor, GetSpatialOSNetConnection()); + } + return Channel; +} + USpatialActorChannel* USpatialNetDriver::GetActorChannelByEntityId(Worker_EntityId EntityId) const { return EntityToActorChannel.FindRef(EntityId); } +USpatialActorChannel* USpatialNetDriver::CreateSpatialActorChannel(AActor* Actor, USpatialNetConnection* InConnection) +{ + if (InConnection == nullptr) + { + return nullptr; + } + + USpatialActorChannel* Channel = nullptr; + + if (Actor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)) + { + Channel = GlobalStateManager->AddSingleton(Actor); + } + else + { +#if ENGINE_MINOR_VERSION <= 20 + Channel = static_cast(InConnection->CreateChannel(CHTYPE_Actor, 1)); +#else + Channel = static_cast(InConnection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::OpenedLocally)); +#endif + if (Channel != nullptr) + { + Channel->SetChannelActor(Actor); + } + } + + return Channel; +} + void USpatialNetDriver::WipeWorld(const USpatialNetDriver::PostWorldWipeDelegate& LoadSnapshotAfterWorldWipe) { if (Cast(GetWorld()->GetGameInstance())->bResponsibleForSnapshotLoading) @@ -1477,7 +1721,6 @@ void USpatialNetDriver::WipeWorld(const USpatialNetDriver::PostWorldWipeDelegate } } -#if !UE_BUILD_SHIPPING uint32 USpatialNetDriver::GetNextReliableRPCId(AActor* Actor, ESchemaComponentType RPCType, UObject* TargetObject) { if (!ReliableRPCIdMap.Contains(Actor)) @@ -1510,8 +1753,6 @@ uint32 USpatialNetDriver::GetNextReliableRPCId(AActor* Actor, ESchemaComponentTy void USpatialNetDriver::OnReceivedReliableRPC(AActor* Actor, ESchemaComponentType RPCType, FString WorkerId, uint32 RPCId, UObject* TargetObject, UFunction* Function) { - check(!WorkerId.IsEmpty()); - if (!ReliableRPCIdMap.Contains(Actor)) { ReliableRPCIdMap.Add(Actor); @@ -1541,17 +1782,17 @@ void USpatialNetDriver::OnReceivedReliableRPC(AActor* Actor, ESchemaComponentTyp { if (RPCId < RPCIdEntry->RPCId) { - UE_LOG(LogSpatialOSNetDriver, Verbose, TEXT("Actor %s: Reliable %s RPC received out of order! Previously received RPC: %s, target %s, index %d. Now received: %s, target %s, index %d. Sender: %s"), + UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Actor %s: Reliable %s RPC received out of order! Previously received RPC: %s, target %s, index %d. Now received: %s, target %s, index %d. Sender: %s"), *Actor->GetName(), *RPCSchemaTypeToString(RPCType), *RPCIdEntry->LastRPCName, *RPCIdEntry->LastRPCTarget, RPCIdEntry->RPCId, *Function->GetName(), *TargetObject->GetName(), RPCId, *WorkerId); } else if (RPCId == RPCIdEntry->RPCId) { - UE_LOG(LogSpatialOSNetDriver, Verbose, TEXT("Actor %s: Reliable %s RPC index duplicated! Previously received RPC: %s, target %s, index %d. Now received: %s, target %s, index %d. Sender: %s"), + UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Actor %s: Reliable %s RPC index duplicated! Previously received RPC: %s, target %s, index %d. Now received: %s, target %s, index %d. Sender: %s"), *Actor->GetName(), *RPCSchemaTypeToString(RPCType), *RPCIdEntry->LastRPCName, *RPCIdEntry->LastRPCTarget, RPCIdEntry->RPCId, *Function->GetName(), *TargetObject->GetName(), RPCId, *WorkerId); } else { - UE_LOG(LogSpatialOSNetDriver, Verbose, TEXT("Actor %s: One or more reliable %s RPCs skipped! Previously received RPC: %s, target %s, index %d. Now received: %s, target %s, index %d. Sender: %s"), + UE_LOG(LogSpatialOSNetDriver, Warning, TEXT("Actor %s: One or more reliable %s RPCs skipped! Previously received RPC: %s, target %s, index %d. Now received: %s, target %s, index %d. Sender: %s"), *Actor->GetName(), *RPCSchemaTypeToString(RPCType), *RPCIdEntry->LastRPCName, *RPCIdEntry->LastRPCTarget, RPCIdEntry->RPCId, *Function->GetName(), *TargetObject->GetName(), RPCId, *WorkerId); } } @@ -1582,8 +1823,6 @@ void USpatialNetDriver::OnRPCAuthorityGained(AActor* Actor, ESchemaComponentType } } -#endif // !UE_BUILD_SHIPPING - void USpatialNetDriver::DelayedSendDeleteEntityRequest(Worker_EntityId EntityId, float Delay) { FTimerHandle RetryTimer; @@ -1592,3 +1831,108 @@ void USpatialNetDriver::DelayedSendDeleteEntityRequest(Worker_EntityId EntityId, Sender->SendDeleteEntityRequest(EntityId); }, Delay, false); } + +void USpatialNetDriver::HandleStartupOpQueueing(const TArray& InOpLists) +{ + if (InOpLists.Num() == 0) + { + return; + } + + QueuedStartupOpLists.Append(InOpLists); + bIsReadyToStart = FindAndDispatchStartupOps(InOpLists); + + if (!bIsReadyToStart) + { + return; + } + + // We've found and dispatched all ops we need for startup, trigger BeginPlay() + // on the GSM and process the queued ops. Note that FindAndDispatchStartupOps() + // will have notified the Dispatcher to skip the startup ops that we've + // processed already. + GlobalStateManager->TriggerBeginPlay(); + + for (Worker_OpList* OpList : QueuedStartupOpLists) + { + Dispatcher->ProcessOps(OpList); + Worker_OpList_Destroy(OpList); + } + + // Sanity check that the dispatcher encountered, skipped, and removed + // all Ops we asked it to skip + check(Dispatcher->GetNumOpsToSkip() == 0); + + QueuedStartupOpLists.Empty(); +} + +bool USpatialNetDriver::FindAndDispatchStartupOps(const TArray& InOpLists) +{ + TArray FoundOps; + + // Search for entity id reservation response and process it. The entity id reservation + // can fail to reserve entity ids. In that case, the EntityPool will not be marked ready, + // a new query will be sent, and we will process the new response here when it arrives. + if (!EntityPool->IsReady()) + { + Worker_Op* EntityIdReservationResponseOp = nullptr; + FindFirstOpOfType(InOpLists, WORKER_OP_TYPE_RESERVE_ENTITY_IDS_RESPONSE, &EntityIdReservationResponseOp); + + if (EntityIdReservationResponseOp != nullptr) + { + FoundOps.Add(EntityIdReservationResponseOp); + } + } + + // Search for StartupActorManager ops we need and process them + if (!GlobalStateManager->IsReadyToCallBeginPlay()) + { + Worker_Op* AddComponentOp = nullptr; + FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_ADD_COMPONENT, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID, &AddComponentOp); + + Worker_Op* AuthorityChangedOp = nullptr; + FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_AUTHORITY_CHANGE, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID, &AuthorityChangedOp); + + Worker_Op* ComponentUpdateOp = nullptr; + FindFirstOpOfTypeForComponent(InOpLists, WORKER_OP_TYPE_COMPONENT_UPDATE, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID, &ComponentUpdateOp); + + if (AddComponentOp != nullptr) + { + FoundOps.Add(AddComponentOp); + } + + if (AuthorityChangedOp != nullptr) + { + FoundOps.Add(AuthorityChangedOp); + } + + if (ComponentUpdateOp != nullptr) + { + FoundOps.Add(ComponentUpdateOp); + } + } + + // For each Op we've found, make a Worker_OpList that just contains that Op, + // and pass it to the dispatcher for processing. This allows us to avoid copying + // the Ops around and dealing with memory that is / should be managed by the Worker SDK. + // The Op remains owned by the original OpList. Finally, notify the dispatcher to skip + // these Ops when they are encountered later when we process the queued ops. + for (Worker_Op* Op : FoundOps) + { + Worker_OpList SingleOpList; + SingleOpList.op_count = 1; + SingleOpList.ops = Op; + + Dispatcher->ProcessOps(&SingleOpList); + Dispatcher->MarkOpToSkip(Op); + } + + if (EntityPool->IsReady() && + GlobalStateManager->IsReadyToCallBeginPlay()) + { + // Return whether or not we are ready to start + return true; + } + + return false; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp index 4690cb01b4..57b7a33430 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/EngineClasses/SpatialPackageMapClient.cpp @@ -135,6 +135,17 @@ FNetworkGUID USpatialPackageMapClient::ResolveEntityActor(AActor* Actor, Worker_ return NetGUID; } +void USpatialPackageMapClient::ResolveSubobject(UObject* Object, const FUnrealObjectRef& ObjectRef) +{ + FSpatialNetGUIDCache* SpatialGuidCache = static_cast(GuidCache.Get()); + FNetworkGUID NetGUID = SpatialGuidCache->GetNetGUIDFromUnrealObjectRef(ObjectRef); + + if (!NetGUID.IsValid()) + { + SpatialGuidCache->AssignNewSubobjectNetGUID(Object, ObjectRef); + } +} + void USpatialPackageMapClient::RemoveEntityActor(Worker_EntityId EntityId) { FSpatialNetGUIDCache* SpatialGuidCache = static_cast(GuidCache.Get()); @@ -145,6 +156,16 @@ void USpatialPackageMapClient::RemoveEntityActor(Worker_EntityId EntityId) } } +void USpatialPackageMapClient::RemoveSubobject(const FUnrealObjectRef& ObjectRef) +{ + FSpatialNetGUIDCache* SpatialGuidCache = static_cast(GuidCache.Get()); + + if (SpatialGuidCache->GetNetGUIDFromUnrealObjectRef(ObjectRef).IsValid()) + { + SpatialGuidCache->RemoveSubobjectNetGUID(ObjectRef); + } +} + void USpatialPackageMapClient::UnregisterActorObjectRefOnly(const FUnrealObjectRef& ObjectRef) { FSpatialNetGUIDCache* SpatialGuidCache = static_cast(GuidCache.Get()); @@ -305,6 +326,11 @@ FNetworkGUID FSpatialNetGUIDCache::AssignNewEntityActorNetGUID(AActor* Actor, Wo // This is the only extra object ref that has to be registered for the subobject. UnrealObjectRefToNetGUID.Emplace(StablyNamedSubobjectRef, SubobjectNetGUID); + + // As the subobject may have be referred to previously in replication flow, it would + // have it's stable name registered as it's UnrealObjectRef inside NetGUIDToUnrealObjectRef. + // Update the map to point to the entity id version. + NetGUIDToUnrealObjectRef.Emplace(SubobjectNetGUID, EntityIdSubobjectRef); } RegisterObjectRef(SubobjectNetGUID, EntityIdSubobjectRef); @@ -324,6 +350,14 @@ FNetworkGUID FSpatialNetGUIDCache::AssignNewEntityActorNetGUID(AActor* Actor, Wo return NetGUID; } +void FSpatialNetGUIDCache::AssignNewSubobjectNetGUID(UObject* Subobject, const FUnrealObjectRef& SubobjectRef) +{ + FNetworkGUID SubobjectNetGUID = GetOrAssignNetGUID_SpatialGDK(Subobject); + RegisterObjectRef(SubobjectNetGUID, SubobjectRef); + + Cast(Driver)->Receiver->ResolvePendingOperations(Subobject, SubobjectRef); +} + // Recursively assign netguids to the outer chain of a UObject. Then associate them with their Spatial representation (FUnrealObjectRef) // This is required in order to be able to refer to a non-replicated stably named UObject. // Dynamically spawned actors and references to their subobjects do not go through this codepath. @@ -371,29 +405,57 @@ void FSpatialNetGUIDCache::RemoveEntityNetGUID(Worker_EntityId EntityId) SpatialGDK::UnrealMetadata* UnrealMetadata = SpatialNetDriver->StaticComponentView->GetComponentData(EntityId); - // There are times when the Editor is quitting out of PIE that UnrealMetadata is nullptr. - // Due to GetNativeEntityClass using LoadObject, if we are shutting down and garbage collecting, this will crash the editor. - // In this case, just return since everything will be cleaned up anyways. - if (UnrealMetadata == nullptr || (IsInGameThread() && IsGarbageCollecting())) + // If UnrealMetadata is nullptr (can happen if the editor is closing down) just return. + if (UnrealMetadata == nullptr) { return; } - const FClassInfo& Info = SpatialNetDriver->ClassInfoManager->GetOrCreateClassInfoByClass(UnrealMetadata->GetNativeEntityClass()); + // Due to UnrealMetadata::GetNativeEntityClass using LoadObject, if we are shutting down and garbage collecting, + // calling LoadObject will crash the editor. In this case, just return since everything will be cleaned up anyways. + if (IsInGameThread() && IsGarbageCollecting()) + { + return; + } SpatialGDK::TSchemaOption& StablyNamedRefOption = UnrealMetadata->StablyNamedRef; - for (auto& SubobjectInfoPair : Info.SubobjectInfo) + if (UnrealMetadata->NativeClass.IsStale()) + { + UE_LOG(LogSpatialPackageMap, Warning, TEXT("Attempting to remove stale object from package map - %s"), *UnrealMetadata->ClassPath); + } + else { - FUnrealObjectRef SubobjectRef(EntityId, SubobjectInfoPair.Key); - if (FNetworkGUID* SubobjectNetGUID = UnrealObjectRefToNetGUID.Find(SubobjectRef)) + const FClassInfo& Info = SpatialNetDriver->ClassInfoManager->GetOrCreateClassInfoByClass(UnrealMetadata->GetNativeEntityClass()); + + for (auto& SubobjectInfoPair : Info.SubobjectInfo) { - NetGUIDToUnrealObjectRef.Remove(*SubobjectNetGUID); - UnrealObjectRefToNetGUID.Remove(SubobjectRef); + FUnrealObjectRef SubobjectRef(EntityId, SubobjectInfoPair.Key); + if (FNetworkGUID* SubobjectNetGUID = UnrealObjectRefToNetGUID.Find(SubobjectRef)) + { + NetGUIDToUnrealObjectRef.Remove(*SubobjectNetGUID); + UnrealObjectRefToNetGUID.Remove(SubobjectRef); - if (StablyNamedRefOption.IsSet()) + if (StablyNamedRefOption.IsSet()) + { + UnrealObjectRefToNetGUID.Remove(FUnrealObjectRef(0, 0, SubobjectInfoPair.Value->SubobjectName.ToString(), StablyNamedRefOption.GetValue())); + } + } + } + } + + // Remove dynamically attached subobjects + if (USpatialActorChannel* Channel = SpatialNetDriver->GetActorChannelByEntityId(EntityId)) + { + for (UObject* DynamicSubobject : Channel->CreateSubObjects) + { + if (FNetworkGUID* SubobjectNetGUID = NetGUIDLookup.Find(DynamicSubobject)) { - UnrealObjectRefToNetGUID.Remove(FUnrealObjectRef(0, 0, SubobjectInfoPair.Value->SubobjectName.ToString(), StablyNamedRefOption.GetValue())); + if (FUnrealObjectRef* SubobjectRef = NetGUIDToUnrealObjectRef.Find(*SubobjectNetGUID)) + { + UnrealObjectRefToNetGUID.Remove(*SubobjectRef); + NetGUIDToUnrealObjectRef.Remove(*SubobjectNetGUID); + } } } } @@ -412,6 +474,53 @@ void FSpatialNetGUIDCache::RemoveEntityNetGUID(Worker_EntityId EntityId) } } +void FSpatialNetGUIDCache::RemoveSubobjectNetGUID(const FUnrealObjectRef& SubobjectRef) +{ + if (!UnrealObjectRefToNetGUID.Contains(SubobjectRef)) + { + return; + } + + USpatialNetDriver* SpatialNetDriver = Cast(Driver); + SpatialGDK::UnrealMetadata* UnrealMetadata = SpatialNetDriver->StaticComponentView->GetComponentData(SubobjectRef.Entity); + + // If UnrealMetadata is nullptr (can happen if the editor is closing down) just return. + if (UnrealMetadata == nullptr) + { + return; + } + + // Due to UnrealMetadata::GetNativeEntityClass using LoadObject, if we are shutting down and garbage collecting, + // calling LoadObject will crash the editor. In this case, just return since everything will be cleaned up anyways. + if (IsInGameThread() && IsGarbageCollecting()) + { + return; + } + + if (UnrealMetadata->NativeClass.IsStale()) + { + UE_LOG(LogSpatialPackageMap, Warning, TEXT("Attempting to remove stale subobject from package map - %s"), *UnrealMetadata->ClassPath); + } + else + { + const FClassInfo& Info = SpatialNetDriver->ClassInfoManager->GetOrCreateClassInfoByClass(UnrealMetadata->GetNativeEntityClass()); + + // Part of the CDO + if (const TSharedRef* SubobjectInfoPtr = Info.SubobjectInfo.Find(SubobjectRef.Offset)) + { + SpatialGDK::TSchemaOption& StablyNamedRefOption = UnrealMetadata->StablyNamedRef; + + if (StablyNamedRefOption.IsSet()) + { + UnrealObjectRefToNetGUID.Remove(FUnrealObjectRef(0, 0, SubobjectInfoPtr->Get().SubobjectName.ToString(), StablyNamedRefOption.GetValue())); + } + } + } + FNetworkGUID SubobjectNetGUID = UnrealObjectRefToNetGUID[SubobjectRef]; + NetGUIDToUnrealObjectRef.Remove(SubobjectNetGUID); + UnrealObjectRefToNetGUID.Remove(SubobjectRef); +} + FNetworkGUID FSpatialNetGUIDCache::GetNetGUIDFromUnrealObjectRef(const FUnrealObjectRef& ObjectRef) { return GetNetGUIDFromUnrealObjectRefInternal(ObjectRef); @@ -530,7 +639,7 @@ FNetworkGUID FSpatialNetGUIDCache::GetOrAssignNetGUID_SpatialGDK(UObject* Object CacheObject.bIgnoreWhenMissing = true; RegisterNetGUID_Internal(NetGUID, CacheObject); - UE_LOG(LogSpatialPackageMap, Log, TEXT("%s: NetGUID for object %s was not found in the cache. Generated new NetGUID %s."), + UE_LOG(LogSpatialPackageMap, Verbose, TEXT("%s: NetGUID for object %s was not found in the cache. Generated new NetGUID %s."), *Cast(Driver)->Connection->GetWorkerId(), *Object->GetPathName(), *NetGUID.ToString()); @@ -546,8 +655,12 @@ void FSpatialNetGUIDCache::RegisterObjectRef(FNetworkGUID NetGUID, const FUnreal FUnrealObjectRef RemappedObjectRef = ObjectRef; NetworkRemapObjectRefPaths(RemappedObjectRef, false /*bIsReading*/); - checkSlow(!NetGUIDToUnrealObjectRef.Contains(NetGUID) || (NetGUIDToUnrealObjectRef.Contains(NetGUID) && NetGUIDToUnrealObjectRef.FindChecked(NetGUID) == RemappedObjectRef)); - checkSlow(!UnrealObjectRefToNetGUID.Contains(RemappedObjectRef) || (UnrealObjectRefToNetGUID.Contains(RemappedObjectRef) && UnrealObjectRefToNetGUID.FindChecked(RemappedObjectRef) == NetGUID)); + checkfSlow(!NetGUIDToUnrealObjectRef.Contains(NetGUID) || (NetGUIDToUnrealObjectRef.Contains(NetGUID) && NetGUIDToUnrealObjectRef.FindChecked(NetGUID) == RemappedObjectRef), + TEXT("NetGUID to UnrealObjectRef mismatch - NetGUID: %s ObjRef in map: %s ObjRef expected: %s"), *NetGUID.ToString(), + *NetGUIDToUnrealObjectRef.FindChecked(NetGUID).ToString(), *RemappedObjectRef.ToString()); + checkfSlow(!UnrealObjectRefToNetGUID.Contains(RemappedObjectRef) || (UnrealObjectRefToNetGUID.Contains(RemappedObjectRef) && UnrealObjectRefToNetGUID.FindChecked(RemappedObjectRef) == NetGUID), + TEXT("UnrealObjectRef to NetGUID mismatch - UnrealObjectRef: %s NetGUID in map: %s NetGUID expected: %s"), *NetGUID.ToString(), + *UnrealObjectRefToNetGUID.FindChecked(RemappedObjectRef).ToString(), *RemappedObjectRef.ToString()); NetGUIDToUnrealObjectRef.Emplace(NetGUID, RemappedObjectRef); UnrealObjectRefToNetGUID.Emplace(RemappedObjectRef, NetGUID); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/EditorWorkerController.h b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/EditorWorkerController.h deleted file mode 100644 index 2fe0e1322c..0000000000 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/EditorWorkerController.h +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Improbable Worlds Ltd, All Rights Reserved -#pragma once - -#if WITH_EDITOR - -#include "Editor.h" -#include "Modules/ModuleManager.h" -#include "Settings/LevelEditorPlaySettings.h" -#include "SpatialGDKEditorToolbar.h" - -namespace SpatialGDK -{ - -struct EditorWorkerController -{ - void OnPrePIEEnded(bool bValue) - { - LastPIEEndTime = FDateTime::Now().ToUnixTimestamp(); - FEditorDelegates::PrePIEEnded.Remove(PIEEndHandle); - } - - void OnSpatialShutdown() - { - LastPIEEndTime = 0; // Reset PIE end time to ensure replace-a-worker isn't called - FSpatialGDKEditorToolbarModule& Toolbar = FModuleManager::GetModuleChecked("SpatialGDKEditorToolbar"); - Toolbar.OnSpatialShutdown.Remove(SpatialShutdownHandle); - FEditorDelegates::PrePIEEnded.Remove(PIEEndHandle); - } - - void InitWorkers(const FString& WorkerType) - { - ReplaceProcesses.Empty(); - - // Only issue the worker replace request if there's a chance the load balancer hasn't acknowledged - // that the previous session's workers have disconnected. There's no hard `heartbeat` time for this as - // it's dependent on multiple factors (fabric load etc.), so this value was landed on after significant - // trial and error. - const int64 WorkerReplaceThresholdSeconds = 8; - - int64 SecondsSinceLastSession = FDateTime::Now().ToUnixTimestamp() - LastPIEEndTime; - UE_LOG(LogSpatialWorkerConnection, Verbose, TEXT("Seconds since last session - %d"), SecondsSinceLastSession); - - PIEEndHandle = FEditorDelegates::PrePIEEnded.AddRaw(this, &EditorWorkerController::OnPrePIEEnded); - - FSpatialGDKEditorToolbarModule& Toolbar = FModuleManager::GetModuleChecked("SpatialGDKEditorToolbar"); - SpatialShutdownHandle = Toolbar.OnSpatialShutdown.AddRaw(this, &EditorWorkerController::OnSpatialShutdown); - - int32 PlayNumberOfServers; - GetDefault()->GetPlayNumberOfServers(PlayNumberOfServers); - - WorkerIds.SetNum(PlayNumberOfServers); - for (int i = 0; i < PlayNumberOfServers; ++i) - { - FString NewWorkerId = WorkerType + FGuid::NewGuid().ToString(); - - if (!WorkerIds[i].IsEmpty() && SecondsSinceLastSession < WorkerReplaceThresholdSeconds) - { - ReplaceProcesses.Add(ReplaceWorker(WorkerIds[i], NewWorkerId)); - } - - WorkerIds[i] = NewWorkerId; - } - } - - FProcHandle ReplaceWorker(const FString& OldWorker, const FString& NewWorker) - { - const FString CmdExecutable = TEXT("spatial.exe"); - - const FString CmdArgs = FString::Printf( - TEXT("local worker replace " - "--existing_worker_id %s " - "--replacing_worker_id %s"), *OldWorker, *NewWorker); - uint32 ProcessID = 0; - FProcHandle ProcHandle = FPlatformProcess::CreateProc( - *(CmdExecutable), *CmdArgs, false, true, true, &ProcessID, 2 /*PriorityModifier*/, - nullptr, nullptr, nullptr); - - return ProcHandle; - } - - void BlockUntilWorkerReady(int32 WorkerIdx) - { - if (WorkerIdx < ReplaceProcesses.Num()) - { - while (FPlatformProcess::IsProcRunning(ReplaceProcesses[WorkerIdx])) - { - FPlatformProcess::Sleep(0.1f); - } - } - } - - TArray WorkerIds; - TArray ReplaceProcesses; - int64 LastPIEEndTime = 0; // Unix epoch time in seconds - FDelegateHandle PIEEndHandle; - FDelegateHandle SpatialShutdownHandle; -}; - -} // namespace SpatialGDK - -#endif diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp index 8b47987766..6ff57ffc17 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/Connection/SpatialWorkerConnection.cpp @@ -1,6 +1,9 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "Interop/Connection/SpatialWorkerConnection.h" +#if WITH_EDITOR +#include "Interop/Connection/EditorWorkerController.h" +#endif #include "EngineClasses/SpatialGameInstance.h" #include "EngineClasses/SpatialNetDriver.h" @@ -15,18 +18,10 @@ #include "SpatialGDKSettings.h" #include "Utils/ErrorCodeRemapping.h" -#if WITH_EDITOR -#include "EditorWorkerController.h" -#endif - DEFINE_LOG_CATEGORY(LogSpatialWorkerConnection); using namespace SpatialGDK; -#if WITH_EDITOR -static EditorWorkerController WorkerController; -#endif - void USpatialWorkerConnection::Init(USpatialGameInstance* InGameInstance) { GameInstance = InGameInstance; @@ -81,6 +76,15 @@ void USpatialWorkerConnection::Connect(bool bInitAsClient) return; } + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); + if (SpatialGDKSettings->bUseDevelopmentAuthenticationFlow && bInitAsClient) + { + LocatorConfig.WorkerType = SpatialConstants::DefaultClientWorkerType.ToString(); + LocatorConfig.UseExternalIp = true; + StartDevelopmentAuth(SpatialGDKSettings->DevelopmentAuthenticationToken); + return; + } + switch (GetConnectionType()) { case SpatialConnectionType::Receptionist: @@ -92,26 +96,93 @@ void USpatialWorkerConnection::Connect(bool bInitAsClient) } } -void USpatialWorkerConnection::ConnectToReceptionist(bool bConnectAsClient) +void USpatialWorkerConnection::OnLoginTokens(void* UserData, const Worker_Alpha_LoginTokensResponse* LoginTokens) { - if (ReceptionistConfig.WorkerType.IsEmpty()) + if (LoginTokens->status.code != WORKER_CONNECTION_STATUS_CODE_SUCCESS) { - ReceptionistConfig.WorkerType = bConnectAsClient ? SpatialConstants::ClientWorkerType : SpatialConstants::ServerWorkerType; - UE_LOG(LogSpatialWorkerConnection, Warning, TEXT("No worker type specified through commandline, defaulting to %s"), *ReceptionistConfig.WorkerType); + UE_LOG(LogSpatialWorkerConnection, Error, TEXT("Failed to get login token, StatusCode: %d, Error: %s"), LoginTokens->status.code, UTF8_TO_TCHAR(LoginTokens->status.detail)); + return; } -#if WITH_EDITOR - const bool bSingleThreadedServer = !bConnectAsClient && (GPlayInEditorID > 0); - const int32 FirstServerEditorID = 1; - if (bSingleThreadedServer) + if (LoginTokens->login_token_count == 0) + { + UE_LOG(LogSpatialWorkerConnection, Warning, TEXT("No deployment found to connect to. Did you add the 'dev_login' tag to the deployment you want to connect to?")); + return; + } + + UE_LOG(LogSpatialWorkerConnection, Verbose, TEXT("Successfully received LoginTokens, Count: %d"), LoginTokens->login_token_count); + USpatialWorkerConnection* Connection = static_cast(UserData); + const FString& DeploymentToConnect = GetDefault()->DevelopmentDeploymentToConnect; + // If not set, use the first deployment. It can change every query if you have multiple items available, because the order is not guaranteed. + if (DeploymentToConnect.IsEmpty()) + { + Connection->LocatorConfig.LoginToken = FString(LoginTokens->login_tokens[0].login_token); + } + else { - if (GPlayInEditorID == FirstServerEditorID) + for (uint32 i = 0; i < LoginTokens->login_token_count; i++) { - WorkerController.InitWorkers(ReceptionistConfig.WorkerType); + FString DeploymentName = FString(LoginTokens->login_tokens[i].deployment_name); + if (DeploymentToConnect.Compare(DeploymentName) == 0) + { + Connection->LocatorConfig.LoginToken = FString(LoginTokens->login_tokens[i].login_token); + break; + } } + } + Connection->ConnectToLocator(); +} + +void USpatialWorkerConnection::OnPlayerIdentityToken(void* UserData, const Worker_Alpha_PlayerIdentityTokenResponse* PIToken) +{ + if (PIToken->status.code != WORKER_CONNECTION_STATUS_CODE_SUCCESS) + { + UE_LOG(LogSpatialWorkerConnection, Error, TEXT("Failed to get PlayerIdentityToken, StatusCode: %d, Error: %s"), PIToken->status.code, UTF8_TO_TCHAR(PIToken->status.detail)); + return; + } + + UE_LOG(LogSpatialWorkerConnection, Log, TEXT("Successfully received PIToken: %s"), UTF8_TO_TCHAR(PIToken->player_identity_token)); + USpatialWorkerConnection* Connection = static_cast(UserData); + Connection->LocatorConfig.PlayerIdentityToken = UTF8_TO_TCHAR(PIToken->player_identity_token); + Worker_Alpha_LoginTokensRequest LTParams{}; + LTParams.player_identity_token = PIToken->player_identity_token; + FTCHARToUTF8 WorkerType(*Connection->LocatorConfig.WorkerType); + LTParams.worker_type = WorkerType.Get(); + LTParams.use_insecure_connection = false; + + if (Worker_Alpha_LoginTokensResponseFuture* LTFuture = Worker_Alpha_CreateDevelopmentLoginTokensAsync(TCHAR_TO_UTF8(*Connection->LocatorConfig.LocatorHost), SpatialConstants::LOCATOR_PORT, <Params)) + { + Worker_Alpha_LoginTokensResponseFuture_Get(LTFuture, nullptr, Connection, &USpatialWorkerConnection::OnLoginTokens); + } +} - ReceptionistConfig.WorkerId = WorkerController.WorkerIds[GPlayInEditorID - 1]; +void USpatialWorkerConnection::StartDevelopmentAuth(FString DevAuthToken) +{ + Worker_Alpha_PlayerIdentityTokenRequest PITParams{}; + FTCHARToUTF8 DAToken(*DevAuthToken); + FTCHARToUTF8 PlayerId(*SpatialConstants::DEVELOPMENT_AUTH_PLAYER_ID); + PITParams.development_authentication_token = DAToken.Get(); + PITParams.player_id = PlayerId.Get(); + PITParams.display_name = ""; + PITParams.metadata = ""; + PITParams.use_insecure_connection = false; + + if (Worker_Alpha_PlayerIdentityTokenResponseFuture* PITFuture = Worker_Alpha_CreateDevelopmentPlayerIdentityTokenAsync(TCHAR_TO_UTF8(*LocatorConfig.LocatorHost), SpatialConstants::LOCATOR_PORT, &PITParams)) + { + Worker_Alpha_PlayerIdentityTokenResponseFuture_Get(PITFuture, nullptr, this, &USpatialWorkerConnection::OnPlayerIdentityToken); } +} + +void USpatialWorkerConnection::ConnectToReceptionist(bool bConnectAsClient) +{ + if (ReceptionistConfig.WorkerType.IsEmpty()) + { + ReceptionistConfig.WorkerType = bConnectAsClient ? SpatialConstants::DefaultClientWorkerType.ToString() : SpatialConstants::DefaultServerWorkerType.ToString(); + UE_LOG(LogSpatialWorkerConnection, Warning, TEXT("No worker type specified through commandline, defaulting to %s"), *ReceptionistConfig.WorkerType); + } + +#if WITH_EDITOR + SpatialGDKServices::InitWorkers(bConnectAsClient, GetSpatialNetDriverChecked()->PlayInEditorID, ReceptionistConfig.WorkerId); #endif if (ReceptionistConfig.WorkerId.IsEmpty()) @@ -144,14 +215,9 @@ void USpatialWorkerConnection::ConnectToReceptionist(bool bConnectAsClient) ConnectionParams.network.connection_type = ReceptionistConfig.LinkProtocol; ConnectionParams.network.use_external_ip = ReceptionistConfig.UseExternalIp; ConnectionParams.network.tcp.multiplex_level = ReceptionistConfig.TcpMultiplexLevel; - // end TODO -#if WITH_EDITOR - if (bSingleThreadedServer) - { - WorkerController.BlockUntilWorkerReady(GPlayInEditorID - 1); - } -#endif + ConnectionParams.enable_dynamic_components = true; + // end TODO Worker_ConnectionFuture* ConnectionFuture = Worker_ConnectAsync( TCHAR_TO_UTF8(*ReceptionistConfig.ReceptionistHost), ReceptionistConfig.ReceptionistPort, @@ -164,7 +230,7 @@ void USpatialWorkerConnection::ConnectToLocator() { if (LocatorConfig.WorkerType.IsEmpty()) { - LocatorConfig.WorkerType = SpatialConstants::ClientWorkerType; + LocatorConfig.WorkerType = SpatialConstants::DefaultClientWorkerType.ToString(); UE_LOG(LogSpatialWorkerConnection, Warning, TEXT("No worker type specified through commandline, defaulting to %s"), *LocatorConfig.WorkerType); } @@ -199,6 +265,8 @@ void USpatialWorkerConnection::ConnectToLocator() FString ProtocolLogDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectLogDir()) + TEXT("protocol-log-"); ConnectionParams.protocol_logging.log_prefix = TCHAR_TO_UTF8(*ProtocolLogDir); + + ConnectionParams.enable_dynamic_components = true; // end TODO Worker_ConnectionFuture* ConnectionFuture = Worker_Alpha_Locator_ConnectAsync(WorkerLocator, &ConnectionParams); @@ -283,6 +351,16 @@ Worker_RequestId USpatialWorkerConnection::SendDeleteEntityRequest(Worker_Entity return NextRequestId++; } +void USpatialWorkerConnection::SendAddComponent(Worker_EntityId EntityId, Worker_ComponentData* ComponentData) +{ + QueueOutgoingMessage(EntityId, *ComponentData); +} + +void USpatialWorkerConnection::SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) +{ + QueueOutgoingMessage(EntityId, ComponentId); +} + void USpatialWorkerConnection::SendComponentUpdate(Worker_EntityId EntityId, const Worker_ComponentUpdate* ComponentUpdate) { QueueOutgoingMessage(EntityId, *ComponentUpdate); @@ -485,11 +563,33 @@ void USpatialWorkerConnection::ProcessOutgoingMessages() nullptr); break; } + case EOutgoingMessageType::AddComponent: + { + FAddComponent* Message = static_cast(OutgoingMessage.Get()); + + static const Worker_UpdateParameters DisableLoopback{ false /* loopback */ }; + Worker_Connection_SendAddComponent(WorkerConnection, + Message->EntityId, + &Message->Data, + &DisableLoopback); + break; + } + case EOutgoingMessageType::RemoveComponent: + { + FRemoveComponent* Message = static_cast(OutgoingMessage.Get()); + + static const Worker_UpdateParameters DisableLoopback{ false /* loopback */ }; + Worker_Connection_SendRemoveComponent(WorkerConnection, + Message->EntityId, + Message->ComponentId, + &DisableLoopback); + break; + } case EOutgoingMessageType::ComponentUpdate: { FComponentUpdate* Message = static_cast(OutgoingMessage.Get()); - static const Worker_Alpha_UpdateParameters DisableLoopback{ false /* loopback */ }; + static const Worker_UpdateParameters DisableLoopback{ false /* loopback */ }; Worker_Alpha_Connection_SendComponentUpdate(WorkerConnection, Message->EntityId, &Message->Update, diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp index 09ea27ade8..da06e1f0b1 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/GlobalStateManager.cpp @@ -23,6 +23,7 @@ #include "Schema/UnrealMetadata.h" #include "SpatialConstants.h" #include "UObject/UObjectGlobals.h" +#include "Utils/EntityPool.h" DEFINE_LOG_CATEGORY(LogGlobalStateManager); @@ -55,7 +56,6 @@ void UGlobalStateManager::Init(USpatialNetDriver* InNetDriver, FTimerManager* In bAcceptingPlayers = false; bCanBeginPlay = false; - bTriggeredBeginPlay = false; } void UGlobalStateManager::ApplySingletonManagerData(const Worker_ComponentData& Data) @@ -80,7 +80,7 @@ void UGlobalStateManager::ApplyStartupActorManagerData(const Worker_ComponentDat { Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); - bool bCanBeginPlayData = GetBoolFromSchema(ComponentObject, SpatialConstants::STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_ID); + const bool bCanBeginPlayData = GetBoolFromSchema(ComponentObject, SpatialConstants::STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_ID); ApplyCanBeginPlayUpdate(bCanBeginPlayData); } @@ -160,7 +160,7 @@ void UGlobalStateManager::ReceiveShutdownMultiProcessRequest() } } -void UGlobalStateManager::OnShutdownComponentUpdate(Worker_ComponentUpdate& Update) +void UGlobalStateManager::OnShutdownComponentUpdate(const Worker_ComponentUpdate& Update) { Schema_Object* EventsObject = Schema_GetComponentUpdateEvents(Update.schema_type); if (Schema_GetObjectCount(EventsObject, SpatialConstants::SHUTDOWN_ADDITIONAL_SERVERS_EVENT_ID) > 0) @@ -204,20 +204,14 @@ void UGlobalStateManager::ApplyStartupActorManagerUpdate(const Worker_ComponentU if (Schema_GetBoolCount(ComponentObject, SpatialConstants::STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_ID) == 1) { - bool bCanBeginPlayUpdate = GetBoolFromSchema(ComponentObject, SpatialConstants::STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_ID); + const bool bCanBeginPlayUpdate = GetBoolFromSchema(ComponentObject, SpatialConstants::STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_ID); ApplyCanBeginPlayUpdate(bCanBeginPlayUpdate); } } -void UGlobalStateManager::ApplyCanBeginPlayUpdate(bool bCanBeginPlayUpdate) +void UGlobalStateManager::ApplyCanBeginPlayUpdate(const bool bCanBeginPlayUpdate) { bCanBeginPlay = bCanBeginPlayUpdate; - - // For now, this will only be called on non-authoritative workers. - if (bCanBeginPlay) - { - TriggerBeginPlay(); - } } void UGlobalStateManager::LinkExistingSingletonActor(const UClass* SingletonActorClass) @@ -262,7 +256,12 @@ void UGlobalStateManager::LinkExistingSingletonActor(const UClass* SingletonActo // We're now ready to start replicating this actor, create a channel USpatialNetConnection* Connection = Cast(NetDriver->ClientConnections[0]); + +#if ENGINE_MINOR_VERSION <= 20 Channel = Cast(Connection->CreateChannel(CHTYPE_Actor, 1)); +#else + Channel = Cast(Connection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::OpenedLocally)); +#endif if (StaticComponentView->GetAuthority(SingletonEntityId, SpatialConstants::POSITION_COMPONENT_ID) == WORKER_AUTHORITY_AUTHORITATIVE) { @@ -327,7 +326,11 @@ USpatialActorChannel* UGlobalStateManager::AddSingleton(AActor* SingletonActor) { // 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]); +#if ENGINE_MINOR_VERSION <= 20 Channel = Cast(Connection->CreateChannel(CHTYPE_Actor, 1)); +#else + Channel = Cast(Connection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::OpenedLocally)); +#endif // If entity id already exists for this singleton, set the actor to it // Otherwise SetChannelActor will issue a new entity id request @@ -354,6 +357,17 @@ USpatialActorChannel* UGlobalStateManager::AddSingleton(AActor* SingletonActor) return Channel; } +void UGlobalStateManager::RegisterSingletonChannel(AActor* SingletonActor, USpatialActorChannel* SingletonChannel) +{ + TPair& ActorChannelPair = NetDriver->SingletonActorChannels.FindOrAdd(SingletonActor->GetClass()); + + 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 : NetDriver->SingletonActorChannels) @@ -398,11 +412,7 @@ bool UGlobalStateManager::IsSingletonEntity(Worker_EntityId EntityId) const void UGlobalStateManager::SetAcceptingPlayers(bool bInAcceptingPlayers) { - if (!NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID)) - { - UE_LOG(LogGlobalStateManager, Warning, TEXT("Tried to set AcceptingPlayers on the GSM but this worker does not have authority.")); - return; - } + check(NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID)); // Send the component update that we can now accept players. UE_LOG(LogGlobalStateManager, Log, TEXT("Setting accepting players to '%s'"), bInAcceptingPlayers ? TEXT("true") : TEXT("false")); @@ -422,44 +432,74 @@ void UGlobalStateManager::SetAcceptingPlayers(bool bInAcceptingPlayers) NetDriver->Connection->SendComponentUpdate(GlobalStateManagerEntityId, &Update); } -void UGlobalStateManager::SetCanBeginPlay(bool bInCanBeginPlay) +void UGlobalStateManager::SetCanBeginPlay(const bool bInCanBeginPlay) { - if (!NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID)) - { - UE_LOG(LogGlobalStateManager, Warning, TEXT("Tried to set CanBeginPlay on the GSM but this worker does not have authority.")); - return; - } + check(NetDriver->StaticComponentView->HasAuthority(GlobalStateManagerEntityId, SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID)); Worker_ComponentUpdate Update = {}; Update.component_id = SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID; Update.schema_type = Schema_CreateComponentUpdate(SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID); Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update.schema_type); - // Set CanBeginPlay on GSM Schema_AddBool(UpdateObject, SpatialConstants::STARTUP_ACTOR_MANAGER_CAN_BEGIN_PLAY_ID, static_cast(bInCanBeginPlay)); bCanBeginPlay = bInCanBeginPlay; NetDriver->Connection->SendComponentUpdate(GlobalStateManagerEntityId, &Update); } -void UGlobalStateManager::AuthorityChanged(bool bWorkerAuthority, Worker_EntityId CurrentEntityID) +void UGlobalStateManager::AuthorityChanged(const Worker_AuthorityChangeOp& AuthOp) { - UE_LOG(LogGlobalStateManager, Log, TEXT("Authority over the GSM has changed. This worker %s authority."), bWorkerAuthority ? TEXT("now has") : TEXT ("does not have")); + UE_LOG(LogGlobalStateManager, Verbose, TEXT("Authority over the GSM component %d has changed. This worker %s authority."), AuthOp.component_id, + AuthOp.authority == WORKER_AUTHORITY_AUTHORITATIVE ? TEXT("now has") : TEXT ("does not have")); + + if (AuthOp.authority != WORKER_AUTHORITY_AUTHORITATIVE) + { + return; + } - if (bWorkerAuthority) + switch (AuthOp.component_id) { - // Make sure we update our known entity id for the GSM when we receive authority. - GlobalStateManagerEntityId = CurrentEntityID; + case SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID: + { + GlobalStateManagerEntityId = AuthOp.entity_id; + SetAcceptingPlayers(true); + break; + } + case SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID: + { + ExecuteInitialSingletonActorReplication(); + break; + } + case SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID: + { + // We can reach this point with bCanBeginPlay==true if the server + // that was authoritative over the GSM restarts. + if (!bCanBeginPlay) + { + BecomeAuthoritativeOverAllActors(); + SetCanBeginPlay(true); + } - if (!bCanBeginPlay) + break; + } + default: { - SetCanBeginPlay(true); - BecomeAuthoritativeOverAllActors(); - TriggerBeginPlay(); + break; } + } +} - // Start accepting players only AFTER we've triggered BeginPlay - SetAcceptingPlayers(true); +bool UGlobalStateManager::HandlesComponent(const Worker_ComponentId ComponentId) const +{ + 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: + return true; + default: + return false; } } @@ -511,16 +551,10 @@ void UGlobalStateManager::BecomeAuthoritativeOverAllActors() void UGlobalStateManager::TriggerBeginPlay() { - if (bTriggeredBeginPlay) - { - UE_LOG(LogGlobalStateManager, Error, TEXT("Tried to trigger BeginPlay twice! This should never happen")); - return; - } + check(IsReadyToCallBeginPlay()); NetDriver->World->GetWorldSettings()->SetGSMReadyForPlay(); NetDriver->World->GetWorldSettings()->NotifyBeginPlay(); - - bTriggeredBeginPlay = true; } // Queries for the GlobalStateManager in the deployment. @@ -542,7 +576,7 @@ void UGlobalStateManager::QueryGSM(bool bRetryUntilAcceptingPlayers) RequestID = NetDriver->Connection->SendEntityQueryRequest(&GSMQuery); EntityQueryDelegate GSMQueryDelegate; - GSMQueryDelegate.BindLambda([this, bRetryUntilAcceptingPlayers](Worker_EntityQueryResponseOp& Op) + GSMQueryDelegate.BindLambda([this, bRetryUntilAcceptingPlayers](const Worker_EntityQueryResponseOp& Op) { if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) { @@ -578,7 +612,7 @@ void UGlobalStateManager::QueryGSM(bool bRetryUntilAcceptingPlayers) Receiver->AddEntityQueryDelegate(RequestID, GSMQueryDelegate); } -void UGlobalStateManager::ApplyDeploymentMapDataFromQueryResponse(Worker_EntityQueryResponseOp& Op) +void UGlobalStateManager::ApplyDeploymentMapDataFromQueryResponse(const Worker_EntityQueryResponseOp& Op) { for (uint32_t i = 0; i < Op.results[0].component_count; i++) { @@ -590,7 +624,7 @@ void UGlobalStateManager::ApplyDeploymentMapDataFromQueryResponse(Worker_EntityQ } } -bool UGlobalStateManager::GetAcceptingPlayersFromQueryResponse(Worker_EntityQueryResponseOp& Op) +bool UGlobalStateManager::GetAcceptingPlayersFromQueryResponse(const Worker_EntityQueryResponseOp& Op) { checkf(Op.result_count == 1, TEXT("There should never be more than one GSM")); @@ -630,9 +664,12 @@ void UGlobalStateManager::RetryQueryGSM(bool bRetryUntilAcceptingPlayers) UE_LOG(LogGlobalStateManager, Log, TEXT("Retrying query for GSM in %f seconds"), RetryTimerDelay); FTimerHandle RetryTimer; - TimerManager->SetTimer(RetryTimer, [this, bRetryUntilAcceptingPlayers]() + TimerManager->SetTimer(RetryTimer, [WeakThis = TWeakObjectPtr(this), bRetryUntilAcceptingPlayers]() { - QueryGSM(bRetryUntilAcceptingPlayers); + if (UGlobalStateManager* GSM = WeakThis.Get()) + { + GSM->QueryGSM(bRetryUntilAcceptingPlayers); + } }, RetryTimerDelay, false); } diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SnapshotManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SnapshotManager.cpp index dea5bf5d88..fdc7ad67b8 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SnapshotManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SnapshotManager.cpp @@ -47,7 +47,7 @@ void USnapshotManager::WorldWipe(const USpatialNetDriver::PostWorldWipeDelegate& RequestID = NetDriver->Connection->SendEntityQueryRequest(&WorldQuery); EntityQueryDelegate WorldQueryDelegate; - WorldQueryDelegate.BindLambda([this, PostWorldWipeDelegate](Worker_EntityQueryResponseOp& Op) + WorldQueryDelegate.BindLambda([this, PostWorldWipeDelegate](const Worker_EntityQueryResponseOp& Op) { if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) { @@ -162,7 +162,7 @@ void USnapshotManager::LoadSnapshot(const FString& SnapshotName) // Set up reserve IDs delegate ReserveEntityIDsDelegate SpawnEntitiesDelegate; - SpawnEntitiesDelegate.BindLambda([EntitiesToSpawn, this](Worker_ReserveEntityIdsResponseOp& Op) + SpawnEntitiesDelegate.BindLambda([EntitiesToSpawn, this](const Worker_ReserveEntityIdsResponseOp& Op) { UE_LOG(LogSnapshotManager, Log, TEXT("Creating entities in snapshot, number of entities to spawn: %i"), Op.number_of_entity_ids); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp index b2bd6c3831..544be20117 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialClassInfoManager.cpp @@ -7,37 +7,48 @@ #include "Engine/Engine.h" #include "GameFramework/Actor.h" #include "Misc/MessageDialog.h" +#include "Runtime/Launch/Resources/Version.h" #include "UObject/Class.h" #include "UObject/UObjectIterator.h" + #if WITH_EDITOR #include "Kismet/KismetSystemLibrary.h" #endif #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" +#include "Utils/ActorGroupManager.h" #include "Utils/RepLayoutUtils.h" DEFINE_LOG_CATEGORY(LogSpatialClassInfoManager); -void USpatialClassInfoManager::Init(USpatialNetDriver* InNetDriver) +bool USpatialClassInfoManager::TryInit(USpatialNetDriver* InNetDriver, UActorGroupManager* InActorGroupManager) { NetDriver = InNetDriver; + ActorGroupManager = InActorGroupManager; FSoftObjectPath SchemaDatabasePath = FSoftObjectPath(TEXT("/Game/Spatial/SchemaDatabase.SchemaDatabase")); SchemaDatabase = Cast(SchemaDatabasePath.TryLoad()); if (SchemaDatabase == nullptr) { - FMessageDialog::Debugf(FText::FromString(TEXT("SchemaDatabase not found! No classes will be supported for SpatialOS replication."))); - return; + UE_LOG(LogSpatialClassInfoManager, Error, TEXT("SchemaDatabase not found! Please generate schema or turn off SpatialOS networking.")); + QuitGame(); + return false; } + + return true; } FORCEINLINE UClass* ResolveClass(FString& ClassPath) { FSoftClassPath SoftClassPath(ClassPath); UClass* Class = SoftClassPath.ResolveClass(); - checkf(Class, TEXT("Failed to load class at path %s"), *ClassPath); + if (Class == nullptr) + { + UE_LOG(LogSpatialClassInfoManager, Warning, TEXT("Failed to find class at path %s! Attempting to load it."), *ClassPath); + Class = SoftClassPath.TryLoadClass(); + } return Class; } @@ -85,19 +96,13 @@ void USpatialClassInfoManager::CreateClassInfoForClass(UClass* Class) TSharedRef Info = ClassInfoMap.Add(Class, MakeShared()); Info->Class = Class; - + // Note: we have to add Class to ClassInfoMap before quitting, as it is expected to be in there by GetOrCreateClassInfoByClass. Therefore the quitting logic cannot be moved higher up. if (!IsSupportedClass(ClassPath)) { UE_LOG(LogSpatialClassInfoManager, Error, TEXT("Could not find class %s in schema database. Double-check whether replication is enabled for this class, the class is explicitly referenced from the starting scene and schema has been generated."), *ClassPath); UE_LOG(LogSpatialClassInfoManager, Error, TEXT("Disconnecting due to no generated schema for %s."), *ClassPath); -#if WITH_EDITOR - // There is no C++ method to quit the current game, so using the Blueprint's QuitGame() that is calling ConsoleCommand("quit") - // Note: don't use RequestExit() in Editor since it would terminate the Engine loop - UKismetSystemLibrary::QuitGame(NetDriver->GetWorld(), nullptr, EQuitPreference::Quit); -#else - FGenericPlatformMisc::RequestExit(false); -#endif + QuitGame(); return; } @@ -107,7 +112,7 @@ void USpatialClassInfoManager::CreateClassInfoForClass(UClass* Class) { ESchemaComponentType RPCType = GetRPCType(RemoteFunction); checkf(RPCType != SCHEMA_Invalid, TEXT("Could not determine RPCType for RemoteFunction: %s"), *GetPathNameSafe(RemoteFunction)); - + FRPCInfo RPCInfo; RPCInfo.Type = RPCType; @@ -118,11 +123,13 @@ void USpatialClassInfoManager::CreateClassInfoForClass(UClass* Class) Info->RPCInfoMap.Add(RemoteFunction, RPCInfo); } + const bool bEnableHandover = GetDefault()->bEnableHandover; + for (TFieldIterator PropertyIt(Class); PropertyIt; ++PropertyIt) { UProperty* Property = *PropertyIt; - if (Property->PropertyFlags & CPF_Handover) + if (bEnableHandover && (Property->PropertyFlags & CPF_Handover)) { for (int32 ArrayIdx = 0; ArrayIdx < PropertyIt->ArrayDim; ++ArrayIdx) { @@ -149,10 +156,28 @@ void USpatialClassInfoManager::CreateClassInfoForClass(UClass* Class) } } + if (Class->IsChildOf()) + { + FinishConstructingActorClassInfo(ClassPath, Info); + } + else + { + FinishConstructingSubobjectClassInfo(ClassPath, Info); + } +} + +void USpatialClassInfoManager::FinishConstructingActorClassInfo(const FString& ClassPath, TSharedRef& Info) +{ ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { - Worker_ComponentId ComponentId = SchemaDatabase->ClassPathToSchema[ClassPath].SchemaComponents[Type]; - if (ComponentId != 0) + Worker_ComponentId ComponentId = SchemaDatabase->ActorClassPathToSchema[ClassPath].SchemaComponents[Type]; + + if (!GetDefault()->bEnableHandover && Type == SCHEMA_Handover) + { + return; + } + + if (ComponentId != SpatialConstants::INVALID_COMPONENT_ID) { Info->SchemaComponents[Type] = ComponentId; ComponentToClassInfoMap.Add(ComponentId, Info); @@ -161,12 +186,17 @@ void USpatialClassInfoManager::CreateClassInfoForClass(UClass* Class) } }); - for (auto& SubobjectClassDataPair : SchemaDatabase->ClassPathToSchema[ClassPath].SubobjectData) + for (auto& SubobjectClassDataPair : SchemaDatabase->ActorClassPathToSchema[ClassPath].SubobjectData) { int32 Offset = SubobjectClassDataPair.Key; - FSubobjectSchemaData SubobjectSchemaData = SubobjectClassDataPair.Value; + FActorSpecificSubobjectSchemaData SubobjectSchemaData = SubobjectClassDataPair.Value; UClass* SubobjectClass = ResolveClass(SubobjectSchemaData.ClassPath); + if (SubobjectClass == nullptr) + { + UE_LOG(LogSpatialClassInfoManager, Error, TEXT("Failed to resolve the class for subobject %s (class path: %s) on actor class %s! This subobject will not be able to replicate in Spatial!"), *SubobjectSchemaData.Name.ToString(), *SubobjectSchemaData.ClassPath, *ClassPath); + continue; + } const FClassInfo& SubobjectInfo = GetOrCreateClassInfoByClass(SubobjectClass); @@ -176,6 +206,11 @@ void USpatialClassInfoManager::CreateClassInfoForClass(UClass* Class) ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { + if (!GetDefault()->bEnableHandover && Type == SCHEMA_Handover) + { + return; + } + Worker_ComponentId ComponentId = SubobjectSchemaData.SchemaComponents[Type]; if (ComponentId != 0) { @@ -188,34 +223,71 @@ void USpatialClassInfoManager::CreateClassInfoForClass(UClass* Class) 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()) + } + } } -bool USpatialClassInfoManager::IsSupportedClass(const FString& PathName) const +void USpatialClassInfoManager::FinishConstructingSubobjectClassInfo(const FString& ClassPath, TSharedRef& Info) { - return SchemaDatabase->ClassPathToSchema.Contains(PathName); + for (const auto& DynamicSubobjectData : SchemaDatabase->SubobjectClassPathToSchema[ClassPath].DynamicSubobjectComponents) + { + // Make a copy of the already made FClassInfo for this dynamic subobject + TSharedRef SpecificDynamicSubobjectInfo = MakeShared(Info.Get()); + + int32 Offset = DynamicSubobjectData.SchemaComponents[SCHEMA_Data]; + check(Offset != SpatialConstants::INVALID_COMPONENT_ID); + + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) + { + Worker_ComponentId ComponentId = DynamicSubobjectData.SchemaComponents[Type]; + + if (ComponentId != SpatialConstants::INVALID_COMPONENT_ID) + { + SpecificDynamicSubobjectInfo->SchemaComponents[Type] = ComponentId; + ComponentToClassInfoMap.Add(ComponentId, SpecificDynamicSubobjectInfo); + ComponentToOffsetMap.Add(ComponentId, Offset); + ComponentToCategoryMap.Add(ComponentId, ESchemaComponentType(Type)); + } + }); + + Info->DynamicSubobjectInfo.Add(SpecificDynamicSubobjectInfo); + } } -const FClassInfo& USpatialClassInfoManager::GetOrCreateClassInfoByClass(UClass* Class) +void USpatialClassInfoManager::TryCreateClassInfoForComponentId(Worker_ComponentId ComponentId) { - if (ClassInfoMap.Find(Class) == nullptr) + if (FString* ClassPath = SchemaDatabase->ComponentIdToClassPath.Find(ComponentId)) { - CreateClassInfoForClass(Class); + if (UClass* Class = LoadObject(nullptr, **ClassPath)) + { + CreateClassInfoForClass(Class); + } } - - return ClassInfoMap[Class].Get(); } -const FClassInfo& USpatialClassInfoManager::GetOrCreateClassInfoByClassAndOffset(UClass* Class, uint32 Offset) +bool USpatialClassInfoManager::IsSupportedClass(const FString& PathName) const { - const FClassInfo& Info = GetOrCreateClassInfoByClass(Class); + return SchemaDatabase->ActorClassPathToSchema.Contains(PathName) || SchemaDatabase->SubobjectClassPathToSchema.Contains(PathName); +} - if (Offset == 0) +const FClassInfo& USpatialClassInfoManager::GetOrCreateClassInfoByClass(UClass* Class) +{ + if (!ClassInfoMap.Contains(Class)) { - return Info; + CreateClassInfoForClass(Class); } - - TSharedRef SubobjectInfo = Info.SubobjectInfo.FindChecked(Offset); - return SubobjectInfo.Get(); + + return ClassInfoMap[Class].Get(); } const FClassInfo& USpatialClassInfoManager::GetOrCreateClassInfoByObject(UObject* Object) @@ -226,18 +298,23 @@ const FClassInfo& USpatialClassInfoManager::GetOrCreateClassInfoByObject(UObject } else { - check(Cast(Object->GetOuter())); + check(Object->GetTypedOuter() != nullptr); FUnrealObjectRef ObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromObject(Object); - check(ObjectRef.IsValid()) + check(ObjectRef.IsValid()); - return GetOrCreateClassInfoByClassAndOffset(Object->GetOuter()->GetClass(), ObjectRef.Offset); + return ComponentToClassInfoMap[ObjectRef.Offset].Get(); } } -const FClassInfo& USpatialClassInfoManager::GetClassInfoByComponentId(Worker_ComponentId ComponentId) const +const FClassInfo& USpatialClassInfoManager::GetClassInfoByComponentId(Worker_ComponentId ComponentId) { + if (!ComponentToClassInfoMap.Contains(ComponentId)) + { + TryCreateClassInfoForComponentId(ComponentId); + } + TSharedRef Info = ComponentToClassInfoMap.FindChecked(ComponentId); return Info.Get(); } @@ -262,8 +339,58 @@ UClass* USpatialClassInfoManager::GetClassByComponentId(Worker_ComponentId Compo return nullptr; } +uint32 USpatialClassInfoManager::GetComponentIdForClass(const UClass& Class) const +{ + const FString ClassPath = Class.GetPathName(); + if (const FActorSchemaData* ActorSchemaData = SchemaDatabase->ActorClassPathToSchema.Find(Class.GetPathName())) + { + return ActorSchemaData->SchemaComponents[SCHEMA_Data]; + } + return SpatialConstants::INVALID_COMPONENT_ID; +} + +TArray USpatialClassInfoManager::GetComponentIdsForClassHierarchy(const UClass& BaseClass, const bool bIncludeDerivedTypes /* = true */) const +{ + TArray OutComponentIds; + + check(SchemaDatabase); + if (bIncludeDerivedTypes) + { + for (TObjectIterator It; It; ++It) + { + const UClass* Class = *It; + check(Class); + if (Class->IsChildOf(&BaseClass)) + { + const Worker_ComponentId ComponentId = GetComponentIdForClass(*Class); + if (ComponentId != SpatialConstants::INVALID_COMPONENT_ID) + { + OutComponentIds.Add(ComponentId); + } + } + } + } + else + { + const uint32 ComponentId = GetComponentIdForClass(BaseClass); + if (ComponentId != SpatialConstants::INVALID_COMPONENT_ID) + { + OutComponentIds.Add(ComponentId); + } + + } + + return OutComponentIds; +} + + bool USpatialClassInfoManager::GetOffsetByComponentId(Worker_ComponentId ComponentId, uint32& OutOffset) { + if (!ComponentToOffsetMap.Contains(ComponentId)) + { + TryCreateClassInfoForComponentId(ComponentId); + } + if (uint32* Offset = ComponentToOffsetMap.Find(ComponentId)) { OutOffset = *Offset; @@ -275,6 +402,11 @@ bool USpatialClassInfoManager::GetOffsetByComponentId(Worker_ComponentId Compone ESchemaComponentType USpatialClassInfoManager::GetCategoryByComponentId(Worker_ComponentId ComponentId) { + if (!ComponentToCategoryMap.Contains(ComponentId)) + { + TryCreateClassInfoForComponentId(ComponentId); + } + if (ESchemaComponentType* Category = ComponentToCategoryMap.Find(ComponentId)) { return *Category; @@ -283,7 +415,58 @@ ESchemaComponentType USpatialClassInfoManager::GetCategoryByComponentId(Worker_C return ESchemaComponentType::SCHEMA_Invalid; } +const FRPCInfo& USpatialClassInfoManager::GetRPCInfo(UObject* Object, UFunction* Function) +{ + check(Object != nullptr && Function != nullptr); + + const FClassInfo& Info = GetOrCreateClassInfoByObject(Object); + const FRPCInfo* RPCInfoPtr = Info.RPCInfoMap.Find(Function); + + // We potentially have a parent function and need to find the child function. + // This exists as it's possible in blueprints to explicitly call the parent function. + if (RPCInfoPtr == nullptr) + { + for (auto It = Info.RPCInfoMap.CreateConstIterator(); It; ++It) + { + if (It.Key()->GetName() == Function->GetName()) + { + // Matching child function found. Use this for the remote function call. + RPCInfoPtr = &It.Value(); + break; + } + } + } + check(RPCInfoPtr != nullptr); + return *RPCInfoPtr; +} + +uint32 USpatialClassInfoManager::GetComponentIdFromLevelPath(const FString& LevelPath) +{ + FString CleanLevelPath = UWorld::RemovePIEPrefix(LevelPath); + if (const uint32* ComponentId = SchemaDatabase->LevelPathToComponentId.Find(CleanLevelPath)) + { + return *ComponentId; + } + return SpatialConstants::INVALID_COMPONENT_ID; +} + bool USpatialClassInfoManager::IsSublevelComponent(Worker_ComponentId ComponentId) { return SchemaDatabase->LevelComponentIds.Contains(ComponentId); } + +void USpatialClassInfoManager::QuitGame() +{ +#if WITH_EDITOR + // There is no C++ method to quit the current game, so using the Blueprint's QuitGame() that is calling ConsoleCommand("quit") + // Note: don't use RequestExit() in Editor since it would terminate the Engine loop +#if ENGINE_MINOR_VERSION <= 20 + UKismetSystemLibrary::QuitGame(NetDriver->GetWorld(), nullptr, EQuitPreference::Quit); +#else + UKismetSystemLibrary::QuitGame(NetDriver->GetWorld(), nullptr, EQuitPreference::Quit, false); +#endif + +#else + FGenericPlatformMisc::RequestExit(false); +#endif +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialDispatcher.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialDispatcher.cpp index 06e8710d49..9095d97db5 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialDispatcher.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialDispatcher.cpp @@ -8,6 +8,8 @@ #include "Interop/SpatialStaticComponentView.h" #include "Interop/SpatialWorkerFlags.h" #include "UObject/UObjectIterator.h" +#include "Utils/OpUtils.h" + DEFINE_LOG_CATEGORY(LogSpatialView); @@ -20,12 +22,17 @@ void USpatialDispatcher::Init(USpatialNetDriver* InNetDriver) void USpatialDispatcher::ProcessOps(Worker_OpList* OpList) { - TArray QueuedComponentUpdateOps; - for (size_t i = 0; i < OpList->op_count; ++i) { Worker_Op* Op = &OpList->ops[i]; + if (OpsToSkip.Num() != 0 && + OpsToSkip.Contains(Op)) + { + OpsToSkip.Remove(Op); + continue; + } + if (IsExternalSchemaOp(Op)) { ProcessExternalSchemaOp(Op); @@ -45,6 +52,8 @@ void USpatialDispatcher::ProcessOps(Worker_OpList* OpList) break; case WORKER_OP_TYPE_REMOVE_ENTITY: Receiver->OnRemoveEntity(Op->remove_entity); + StaticComponentView->OnRemoveEntity(Op->remove_entity.entity_id); + Receiver->RemoveComponentOpsForEntity(Op->remove_entity.entity_id); break; // Components @@ -53,10 +62,11 @@ void USpatialDispatcher::ProcessOps(Worker_OpList* OpList) Receiver->OnAddComponent(Op->add_component); break; case WORKER_OP_TYPE_REMOVE_COMPONENT: + Receiver->OnRemoveComponent(Op->remove_component); break; case WORKER_OP_TYPE_COMPONENT_UPDATE: - QueuedComponentUpdateOps.Add(Op); StaticComponentView->OnComponentUpdate(Op->component_update); + Receiver->OnComponentUpdate(Op->component_update); break; // Commands @@ -69,7 +79,6 @@ void USpatialDispatcher::ProcessOps(Worker_OpList* OpList) // Authority Change case WORKER_OP_TYPE_AUTHORITY_CHANGE: - StaticComponentView->OnAuthorityChange(Op->authority_change); Receiver->OnAuthorityChange(Op->authority_change); break; @@ -104,29 +113,19 @@ void USpatialDispatcher::ProcessOps(Worker_OpList* OpList) } } - for (Worker_Op* Op : QueuedComponentUpdateOps) - { - Receiver->OnComponentUpdate(Op->component_update); - } - + Receiver->FlushRemoveComponentOps(); Receiver->FlushRetryRPCs(); - - // Check every channel for net ownership changes (determines ACL and component interest) - for (auto& EntityChannelPair : NetDriver->GetEntityToActorChannelMap()) - { - EntityChannelPair.Value->ProcessOwnershipChange(); - } } bool USpatialDispatcher::IsExternalSchemaOp(Worker_Op* Op) const { - Worker_ComponentId ComponentId = GetComponentId(Op); + Worker_ComponentId ComponentId = SpatialGDK::GetComponentId(Op); return SpatialConstants::MIN_EXTERNAL_SCHEMA_ID <= ComponentId && ComponentId <= SpatialConstants::MAX_EXTERNAL_SCHEMA_ID; } void USpatialDispatcher::ProcessExternalSchemaOp(Worker_Op* Op) { - Worker_ComponentId ComponentId = GetComponentId(Op); + Worker_ComponentId ComponentId = SpatialGDK::GetComponentId(Op); check(ComponentId != SpatialConstants::INVALID_COMPONENT_ID); switch (Op->op_type) @@ -149,27 +148,6 @@ void USpatialDispatcher::ProcessExternalSchemaOp(Worker_Op* Op) } } -Worker_ComponentId USpatialDispatcher::GetComponentId(Worker_Op* Op) const -{ - switch (Op->op_type) - { - case WORKER_OP_TYPE_ADD_COMPONENT: - return Op->add_component.data.component_id; - case WORKER_OP_TYPE_REMOVE_COMPONENT: - return Op->remove_component.component_id; - case WORKER_OP_TYPE_COMPONENT_UPDATE: - return Op->component_update.update.component_id; - case WORKER_OP_TYPE_AUTHORITY_CHANGE: - return Op->authority_change.component_id; - case WORKER_OP_TYPE_COMMAND_REQUEST: - return Op->command_request.request.component_id; - case WORKER_OP_TYPE_COMMAND_RESPONSE: - return Op->command_response.response.component_id; - default: - return SpatialConstants::INVALID_COMPONENT_ID; - } -} - USpatialDispatcher::FCallbackId USpatialDispatcher::OnAddComponent(Worker_ComponentId ComponentId, const TFunction& Callback) { return AddGenericOpCallback(ComponentId, WORKER_OP_TYPE_ADD_COMPONENT, [Callback](const Worker_Op* Op) @@ -290,3 +268,13 @@ void USpatialDispatcher::RunCallbacks(Worker_ComponentId ComponentId, const Work CallbackData.Callback(Op); } } + +void USpatialDispatcher::MarkOpToSkip(const Worker_Op* Op) +{ + OpsToSkip.Add(Op); +} + +int USpatialDispatcher::GetNumOpsToSkip() const +{ + return OpsToSkip.Num(); +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialInterestConstraints.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialInterestConstraints.cpp new file mode 100644 index 0000000000..3668c0902e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialInterestConstraints.cpp @@ -0,0 +1,129 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Interop/SpatialInterestConstraints.h" + +#include "Interop/SpatialClassInfoManager.h" +#include "Schema/Interest.h" +#include "Schema/StandardLibrary.h" +#include "SpatialConstants.h" +#include "UObject/UObjectIterator.h" + +void UOrConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const +{ + for (const UAbstractQueryConstraint* ConstraintData : Constraints) + { + if (ConstraintData != nullptr) + { + SpatialGDK::QueryConstraint NewConstraint; + ConstraintData->CreateConstraint(ClassInfoManager, NewConstraint); + if (NewConstraint.IsValid()) + { + OutConstraint.OrConstraint.Add(NewConstraint); + } + } + } +} + +void UAndConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const +{ + for (const UAbstractQueryConstraint* ConstraintData : Constraints) + { + if (ConstraintData != nullptr) + { + SpatialGDK::QueryConstraint NewConstraint; + ConstraintData->CreateConstraint(ClassInfoManager, NewConstraint); + if (NewConstraint.IsValid()) + { + OutConstraint.AndConstraint.Add(NewConstraint); + } + } + } +} + +void USphereConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const +{ + OutConstraint.SphereConstraint = SpatialGDK::SphereConstraint{ SpatialGDK::Coordinates::FromFVector(Center), static_cast(Radius) / 100.0 }; +} + +void UCylinderConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const +{ + OutConstraint.CylinderConstraint = SpatialGDK::CylinderConstraint{ SpatialGDK::Coordinates::FromFVector(Center), static_cast(Radius) / 100.0 }; +} + +void UBoxConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const +{ + OutConstraint.BoxConstraint = SpatialGDK::BoxConstraint{ SpatialGDK::Coordinates::FromFVector(Center), SpatialGDK::Coordinates::FromFVector(EdgeLengths) }; +} + +void URelativeSphereConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const +{ + OutConstraint.RelativeSphereConstraint = SpatialGDK::RelativeSphereConstraint{ static_cast(Radius) / 100.0 }; +} + +void URelativeCylinderConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const +{ + OutConstraint.RelativeCylinderConstraint = SpatialGDK::RelativeCylinderConstraint{ static_cast(Radius) / 100.0 }; +} + +void URelativeBoxConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const +{ + OutConstraint.RelativeBoxConstraint = SpatialGDK::RelativeBoxConstraint{ SpatialGDK::Coordinates::FromFVector(EdgeLengths) }; +} + +void UCheckoutRadiusConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const +{ + if (!ActorClass.Get()) + { + return; + } + + SpatialGDK::QueryConstraint RadiusConstraint; + RadiusConstraint.RelativeCylinderConstraint = SpatialGDK::RelativeCylinderConstraint{ static_cast(Radius) / 100.0 }; + + TArray ComponentIds = ClassInfoManager.GetComponentIdsForClassHierarchy(*ActorClass.Get()); + SpatialGDK::QueryConstraint ActorClassConstraints; + for (Worker_ComponentId ComponentId : ComponentIds) + { + SpatialGDK::QueryConstraint ComponentTypeConstraint; + ComponentTypeConstraint.ComponentConstraint = ComponentId; + ActorClassConstraints.OrConstraint.Add(ComponentTypeConstraint); + } + + if (RadiusConstraint.IsValid() && ActorClassConstraints.IsValid()) + { + OutConstraint.AndConstraint.Add(RadiusConstraint); + OutConstraint.AndConstraint.Add(ActorClassConstraints); + } +} + +void UActorClassConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const +{ + if (!ActorClass.Get()) + { + return; + } + + TArray ComponentIds = ClassInfoManager.GetComponentIdsForClassHierarchy(*ActorClass.Get(), bIncludeDerivedClasses); + for (Worker_ComponentId ComponentId : ComponentIds) + { + SpatialGDK::QueryConstraint ComponentTypeConstraint; + ComponentTypeConstraint.ComponentConstraint = ComponentId; + OutConstraint.OrConstraint.Add(ComponentTypeConstraint); + } +} + +void UComponentClassConstraint::CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const +{ + if (!ComponentClass.Get()) + { + return; + } + + TArray ComponentIds = ClassInfoManager.GetComponentIdsForClassHierarchy(*ComponentClass.Get(), bIncludeDerivedClasses); + for (Worker_ComponentId ComponentId : ComponentIds) + { + SpatialGDK::QueryConstraint ComponentTypeConstraint; + ComponentTypeConstraint.ComponentConstraint = ComponentId; + OutConstraint.OrConstraint.Add(ComponentTypeConstraint); + } +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp index fca12523ef..757935c069 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialPlayerSpawner.cpp @@ -4,6 +4,7 @@ #include "Engine/Engine.h" #include "Engine/LocalPlayer.h" +#include "Kismet/GameplayStatics.h" #include "TimerManager.h" #include "EngineClasses/SpatialNetDriver.h" @@ -46,9 +47,14 @@ void USpatialPlayerSpawner::ReceivePlayerSpawnRequest(Schema_Object* Payload, co UniqueIdReader << UniqueId; FName OnlinePlatformName = FName(*GetStringFromSchema(Payload, 3)); + bool bSimulatedPlayer = Schema_GetBool(Payload, 4); URLString.Append(TEXT("?workerAttribute=")).Append(Attributes); - + if (bSimulatedPlayer) + { + URLString += TEXT("?simulatedPlayer=1"); + } + NetDriver->AcceptNewPlayer(FURL(nullptr, *URLString, TRAVEL_Absolute), UniqueId, OnlinePlatformName, false); } @@ -76,7 +82,7 @@ void USpatialPlayerSpawner::SendPlayerSpawnRequest() RequestID = NetDriver->Connection->SendEntityQueryRequest(&SpatialSpawnerQuery); EntityQueryDelegate SpatialSpawnerQueryDelegate; - SpatialSpawnerQueryDelegate.BindLambda([this, RequestID](Worker_EntityQueryResponseOp& Op) + SpatialSpawnerQueryDelegate.BindLambda([this, RequestID](const Worker_EntityQueryResponseOp& Op) { if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) { @@ -107,6 +113,9 @@ void USpatialPlayerSpawner::SendPlayerSpawnRequest() UniqueIdWriter << UniqueId; AddBytesToSchema(RequestObject, 2, UniqueIdWriter); AddStringToSchema(RequestObject, 3, OnlinePlatformName.ToString()); + UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(NetDriver); + bool bSimulatedPlayer = GameInstance ? GameInstance->IsSimulatedPlayer() : false; + Schema_AddBool(RequestObject, 4, bSimulatedPlayer); NetDriver->Connection->SendCommandRequest(Op.results[0].entity_id, &CommandRequest, 1); } @@ -130,15 +139,18 @@ void USpatialPlayerSpawner::ReceivePlayerSpawnResponse(const Worker_CommandRespo UTF8_TO_TCHAR(Op.message)); FTimerHandle RetryTimer; - TimerManager->SetTimer(RetryTimer, [this]() + TimerManager->SetTimer(RetryTimer, [WeakThis = TWeakObjectPtr(this)]() { - SendPlayerSpawnRequest(); + if (USpatialPlayerSpawner* Spawner = WeakThis.Get()) + { + Spawner->SendPlayerSpawnRequest(); + } }, SpatialConstants::GetCommandRetryWaitTimeSeconds(NumberOfAttempts), false); } else { UE_LOG(LogSpatialPlayerSpawner, Error, TEXT("Player spawn request failed too many times. (%u attempts)"), - SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS) + SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS); } } @@ -163,6 +175,12 @@ void USpatialPlayerSpawner::ObtainPlayerParams(FURL& LoginURL, FUniqueNetIdRepl& { LoginURL.AddOption(*FString::Printf(TEXT("%s"), *GameUrlOptions)); } + // Pull in options from the current world URL (to preserve options added to a travel URL) + const TArray& LastURLOptions = WorldContext->LastURL.Op; + for (const FString& Op : LastURLOptions) + { + LoginURL.AddOption(*Op); + } // Send the player unique Id at login OutUniqueId = LocalPlayer->GetPreferredUniqueNetId(); diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp index 1d5644a587..2cddad8fb3 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialReceiver.cpp @@ -5,10 +5,11 @@ #include "Engine/Engine.h" #include "Engine/World.h" #include "GameFramework/PlayerController.h" +#include "Kismet/GameplayStatics.h" #include "TimerManager.h" -#include "EngineClasses/SpatialFastArrayNetSerialize.h" #include "EngineClasses/SpatialActorChannel.h" +#include "EngineClasses/SpatialFastArrayNetSerialize.h" #include "EngineClasses/SpatialGameInstance.h" #include "EngineClasses/SpatialNetConnection.h" #include "EngineClasses/SpatialPackageMapClient.h" @@ -16,13 +17,17 @@ #include "Interop/GlobalStateManager.h" #include "Interop/SpatialPlayerSpawner.h" #include "Interop/SpatialSender.h" +#include "Schema/ClientRPCEndpoint.h" #include "Schema/DynamicComponent.h" +#include "Schema/RPCPayload.h" +#include "Schema/ServerRPCEndpoint.h" #include "Schema/SpawnData.h" #include "Schema/UnrealMetadata.h" #include "SpatialConstants.h" #include "Utils/ComponentReader.h" -#include "Utils/RepLayoutUtils.h" #include "Utils/ErrorCodeRemapping.h" +#include "Utils/RepLayoutUtils.h" +#include "Utils/SpatialMetrics.h" DEFINE_LOG_CATEGORY(LogSpatialReceiver); @@ -82,7 +87,7 @@ void USpatialReceiver::LeaveCriticalSection() ProcessQueuedResolvedObjects(); } -void USpatialReceiver::OnAddEntity(Worker_AddEntityOp& Op) +void USpatialReceiver::OnAddEntity(const Worker_AddEntityOp& Op) { UE_LOG(LogSpatialReceiver, Verbose, TEXT("AddEntity: %lld"), Op.entity_id); @@ -91,7 +96,7 @@ void USpatialReceiver::OnAddEntity(Worker_AddEntityOp& Op) PendingAddEntities.Emplace(Op.entity_id); } -void USpatialReceiver::OnAddComponent(Worker_AddComponentOp& Op) +void USpatialReceiver::OnAddComponent(const Worker_AddComponentOp& Op) { UE_LOG(LogSpatialReceiver, Verbose, TEXT("AddComponent component ID: %u entity ID: %lld"), Op.data.component_id, Op.entity_id); @@ -110,9 +115,10 @@ void USpatialReceiver::OnAddComponent(Worker_AddComponentOp& Op) case SpatialConstants::NOT_STREAMED_COMPONENT_ID: case SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID: case SpatialConstants::HEARTBEAT_COMPONENT_ID: - case SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID: - case SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID: case SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID: + case SpatialConstants::RPCS_ON_ENTITY_CREATION_ID: + case SpatialConstants::DEBUG_METRICS_COMPONENT_ID: + case SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID: // Ignore static spatial components as they are managed by the SpatialStaticComponentView. return; case SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID: @@ -125,6 +131,11 @@ void USpatialReceiver::OnAddComponent(Worker_AddComponentOp& Op) case SpatialConstants::STARTUP_ACTOR_MANAGER_COMPONENT_ID: GlobalStateManager->ApplyStartupActorManagerData(Op.data); return; + case SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID: + case SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID: + Schema_Object* FieldsObject = Schema_GetComponentDataFields(Op.data.schema_type); + RegisterListeningEntityIfReady(Op.entity_id, FieldsObject); + return; } if (ClassInfoManager->IsSublevelComponent(Op.data.component_id)) @@ -132,36 +143,80 @@ void USpatialReceiver::OnAddComponent(Worker_AddComponentOp& Op) return; } - // If a client gains ownership over something it had already checked out, it will - // add component interest on the owner only data components, which will trigger an - // AddComponentOp, but it is not guaranteed to be inside a critical section. - if (!NetDriver->IsServer()) + if (bInCriticalSection) + { + PendingAddComponents.Emplace(Op.entity_id, Op.data.component_id, MakeUnique(Op.data)); + } + else + { + HandleIndividualAddComponent(Op); + } +} + +void USpatialReceiver::OnRemoveEntity(const Worker_RemoveEntityOp& Op) +{ + RemoveActor(Op.entity_id); +} + +void USpatialReceiver::OnRemoveComponent(const Worker_RemoveComponentOp& Op) +{ + // We are queuing here because if an Actor is removed from your view, remove component ops will be + // generated and sent first, and then the RemoveEntityOp will be sent. In this case, we only want + // to delete the Actor and not delete the subobjects that the RemoveComponent relate to. + // So we queue RemoveComponentOps then process the RemoveEntityOps normally, and then apply the + // RemoveComponentOps in ProcessRemoveComponent. Any RemoveComponentOps that relate to delete entities + // will be dropped in ProcessRemoveComponent. + QueuedRemoveComponentOps.Add(Op); +} + +void USpatialReceiver::FlushRemoveComponentOps() +{ + for (const auto& Op : QueuedRemoveComponentOps) + { + ProcessRemoveComponent(Op); + } + + QueuedRemoveComponentOps.Empty(); +} + +void USpatialReceiver::RemoveComponentOpsForEntity(Worker_EntityId EntityId) +{ + for (auto& RemoveComponentOp : QueuedRemoveComponentOps) { - if (USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(Op.entity_id)) + if (RemoveComponentOp.entity_id == EntityId) { - if (ClassInfoManager->GetCategoryByComponentId(Op.data.component_id) == SCHEMA_OwnerOnly) - { - // We received owner only data, and we have the entity checked out already, - // so this happened as a result of adding component interest. Apply the data - // immediately instead of queuing it up (since there will be no AddEntityOp). - ApplyComponentData(Op.entity_id, Op.data, Channel); - return; - } + // Zero component op to prevent array resize + RemoveComponentOp = Worker_RemoveComponentOp{}; } } +} - if (!bInCriticalSection) +void USpatialReceiver::ProcessRemoveComponent(const Worker_RemoveComponentOp& Op) +{ + if (!StaticComponentView->HasComponent(Op.entity_id, Op.component_id)) { - UE_LOG(LogSpatialReceiver, Warning, TEXT("Received a dynamically added component, these are currently unsupported - component ID: %u entity ID: %lld"), - Op.data.component_id, Op.entity_id); return; } - PendingAddComponents.Emplace(Op.entity_id, Op.data.component_id, MakeUnique(Op.data)); -} -void USpatialReceiver::OnRemoveEntity(Worker_RemoveEntityOp& Op) -{ - RemoveActor(Op.entity_id); + if (AActor* Actor = Cast(PackageMap->GetObjectFromEntityId(Op.entity_id).Get())) + { + if (UObject* Object = PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(Op.entity_id, Op.component_id)).Get()) + { + if (USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(Op.entity_id)) + { + Channel->CreateSubObjects.Remove(Object); + + Actor->OnSubobjectDestroyFromReplication(Object); + + Object->PreDestroyFromReplication(); + Object->MarkPendingKill(); + + PackageMap->RemoveSubobject(FUnrealObjectRef(Op.entity_id, Op.component_id)); + } + } + } + + StaticComponentView->OnRemoveComponent(Op); } void USpatialReceiver::UpdateShadowData(Worker_EntityId EntityId) @@ -170,7 +225,7 @@ void USpatialReceiver::UpdateShadowData(Worker_EntityId EntityId) ActorChannel->UpdateShadowData(); } -void USpatialReceiver::OnAuthorityChange(Worker_AuthorityChangeOp& Op) +void USpatialReceiver::OnAuthorityChange(const Worker_AuthorityChangeOp& Op) { if (bInCriticalSection) { @@ -181,7 +236,7 @@ void USpatialReceiver::OnAuthorityChange(Worker_AuthorityChangeOp& Op) HandleActorAuthority(Op); } -void USpatialReceiver::HandlePlayerLifecycleAuthority(Worker_AuthorityChangeOp& Op, APlayerController* PlayerController) +void USpatialReceiver::HandlePlayerLifecycleAuthority(const Worker_AuthorityChangeOp& Op, APlayerController* PlayerController) { // Server initializes heartbeat logic based on its authority over the position component, // client does the same for heartbeat component @@ -192,6 +247,10 @@ void USpatialReceiver::HandlePlayerLifecycleAuthority(Worker_AuthorityChangeOp& { if (USpatialNetConnection* Connection = Cast(PlayerController->GetNetConnection())) { + if (NetDriver->IsServer()) + { + AuthorityPlayerControllerConnectionMap.Add(Op.entity_id, Connection); + } Connection->InitHeartbeat(TimerManager, Op.entity_id); } } @@ -199,7 +258,7 @@ void USpatialReceiver::HandlePlayerLifecycleAuthority(Worker_AuthorityChangeOp& { if (NetDriver->IsServer()) { - HeartbeatDelegates.Remove(Op.entity_id); + AuthorityPlayerControllerConnectionMap.Remove(Op.entity_id); } if (USpatialNetConnection* Connection = Cast(PlayerController->GetNetConnection())) { @@ -209,18 +268,13 @@ void USpatialReceiver::HandlePlayerLifecycleAuthority(Worker_AuthorityChangeOp& } } -void USpatialReceiver::HandleActorAuthority(Worker_AuthorityChangeOp& Op) +void USpatialReceiver::HandleActorAuthority(const Worker_AuthorityChangeOp& Op) { - if (Op.component_id == SpatialConstants::DEPLOYMENT_MAP_COMPONENT_ID) - { - GlobalStateManager->AuthorityChanged(Op.authority == WORKER_AUTHORITY_AUTHORITATIVE, Op.entity_id); - return; - } + StaticComponentView->OnAuthorityChange(Op); - if (Op.component_id == SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID - && Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) + if (GlobalStateManager->HandlesComponent(Op.component_id)) { - GlobalStateManager->ExecuteInitialSingletonActorReplication(); + GlobalStateManager->AuthorityChanged(Op); return; } @@ -230,6 +284,21 @@ void USpatialReceiver::HandleActorAuthority(Worker_AuthorityChangeOp& Op) return; } + if (Op.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID + && Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) + { + check(!NetDriver->IsServer()); + if (RPCsOnEntityCreation* QueuedRPCs = StaticComponentView->GetComponentData(Op.entity_id)) + { + if (QueuedRPCs->HasRPCPayloadData()) + { + ProcessQueuedActorRPCsOnEntityCreation(Actor, *QueuedRPCs); + } + + Sender->SendRequestToClearRPCsOnEntityCreation(Op.entity_id); + } + } + if (APlayerController* PlayerController = Cast(Actor)) { HandlePlayerLifecycleAuthority(Op, PlayerController); @@ -293,21 +362,56 @@ void USpatialReceiver::HandleActorAuthority(Worker_AuthorityChangeOp& Op) Actor->OnAuthorityLost(); } } + + // Subobject Delegation + TPair EntityComponentPair = MakeTuple(static_cast(Op.entity_id), Op.component_id); + if (TSharedRef* PendingSubobjectAttachmentPtr = PendingEntitySubobjectDelegations.Find(EntityComponentPair)) + { + FPendingSubobjectAttachment& PendingSubobjectAttachment = PendingSubobjectAttachmentPtr->Get(); + + PendingSubobjectAttachment.PendingAuthorityDelegations.Remove(Op.component_id); + + if (PendingSubobjectAttachment.PendingAuthorityDelegations.Num() == 0) + { + if (UObject* Object = PendingSubobjectAttachment.Subobject.Get()) + { + Sender->SendAddComponent(PendingSubobjectAttachment.Channel, Object, *PendingSubobjectAttachment.Info); + } + } + + PendingEntitySubobjectDelegations.Remove(EntityComponentPair); + } } else { - // Check to see if we became authoritative over the UnrealClientRPCEndpoint component over this entity - // If we did, our local role should be ROLE_AutonomousProxy. Otherwise ROLE_SimulatedProxy - const FClassInfo& Info = ClassInfoManager->GetOrCreateClassInfoByClass(Actor->GetClass()); - - if ((Actor->IsA() || Actor->IsA()) && Op.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID) + if (Op.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID) { - Actor->Role = (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) ? ROLE_AutonomousProxy : ROLE_SimulatedProxy; + if (USpatialActorChannel* ActorChannel = NetDriver->GetActorChannelByEntityId(Op.entity_id)) + { + ActorChannel->ClientProcessOwnershipChange(Op.authority == WORKER_AUTHORITY_AUTHORITATIVE); + } + + // If we are a Pawn or PlayerController, our local role should be ROLE_AutonomousProxy. Otherwise ROLE_SimulatedProxy + if ((Actor->IsA() || Actor->IsA()) && Op.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID) + { + Actor->Role = (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) ? ROLE_AutonomousProxy : ROLE_SimulatedProxy; + } } } -#if !UE_BUILD_SHIPPING if (Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) + { + if (Op.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID) + { + Sender->SendClientEndpointReadyUpdate(Op.entity_id); + } + if (Op.component_id == SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID) + { + Sender->SendServerEndpointReadyUpdate(Op.entity_id); + } + } + + if (GetDefault()->bCheckRPCOrder && Op.authority == WORKER_AUTHORITY_AUTHORITATIVE) { ESchemaComponentType ComponentType = ClassInfoManager->GetCategoryByComponentId(Op.component_id); if (Op.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID || @@ -318,25 +422,10 @@ void USpatialReceiver::HandleActorAuthority(Worker_AuthorityChangeOp& Op) NetDriver->OnRPCAuthorityGained(Actor, ComponentType); } } -#endif // !UE_BUILD_SHIPPING } -void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) +bool USpatialReceiver::IsReceivedEntityTornOff(Worker_EntityId EntityId) { - checkf(NetDriver, TEXT("We should have a NetDriver whilst processing ops.")); - checkf(NetDriver->GetWorld(), TEXT("We should have a World whilst processing ops.")); - - SpawnData* SpawnDataComp = StaticComponentView->GetComponentData(EntityId); - UnrealMetadata* UnrealMetadataComp = StaticComponentView->GetComponentData(EntityId); - - if (UnrealMetadataComp == nullptr) - { - // Not an Unreal entity - return; - } - - // If the received actor is torn off, don't bother receiving it. - // (This is only needed due to the delay between tearoff and deleting the entity. See https://improbableio.atlassian.net/browse/UNR-841) // Check the pending add components, to find the root component for the received entity. for (PendingAddComponentWrapper& PendingAddComponent : PendingAddComponents) { @@ -345,19 +434,33 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) { continue; } - uint32 Offset = 0; - if (!ClassInfoManager->GetOffsetByComponentId(PendingAddComponent.ComponentId, Offset) || Offset != 0) + + UClass* Class = ClassInfoManager->GetClassByComponentId(PendingAddComponent.ComponentId); + if (!Class->IsChildOf()) { continue; } Worker_ComponentData* ComponentData = PendingAddComponent.Data->ComponentData; Schema_Object* ComponentObject = Schema_GetComponentDataFields(ComponentData->schema_type); - if (Schema_GetBool(ComponentObject, SpatialConstants::ACTOR_TEAROFF_ID)) - { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("The received actor with entity id %lld was already torn off. The actor will not be spawned."), EntityId); - return; - } + return Schema_GetBool(ComponentObject, SpatialConstants::ACTOR_TEAROFF_ID); + } + + return false; +} + +void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) +{ + checkf(NetDriver, TEXT("We should have a NetDriver whilst processing ops.")); + checkf(NetDriver->GetWorld(), TEXT("We should have a World whilst processing ops.")); + + SpawnData* SpawnDataComp = StaticComponentView->GetComponentData(EntityId); + UnrealMetadata* UnrealMetadataComp = StaticComponentView->GetComponentData(EntityId); + + if (UnrealMetadataComp == nullptr) + { + // Not an Unreal entity + return; } if (AActor* EntityActor = Cast(PackageMap->GetObjectFromEntityId(EntityId))) @@ -383,13 +486,32 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) } else { + UClass* Class = UnrealMetadataComp->GetNativeEntityClass(); + if (Class == nullptr) + { + UE_LOG(LogSpatialReceiver, Warning, TEXT("The received actor with entity id %lld couldn't be loaded. The actor (%s) will not be spawned."), + EntityId, *UnrealMetadataComp->ClassPath); + return; + } + + // Make sure ClassInfo exists + ClassInfoManager->GetOrCreateClassInfoByClass(Class); + + // If the received actor is torn off, don't bother spawning it. + // (This is only needed due to the delay between tearoff and deleting the entity. See https://improbableio.atlassian.net/browse/UNR-841) + if (IsReceivedEntityTornOff(EntityId)) + { + UE_LOG(LogSpatialReceiver, Verbose, TEXT("The received actor with entity id %lld was already torn off. The actor will not be spawned."), EntityId); + return; + } + EntityActor = TryGetOrCreateActor(UnrealMetadataComp, SpawnDataComp); if (EntityActor == nullptr) { // This could be nullptr if: // a stably named actor could not be found - // the Actor is a singleton + // 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; } @@ -405,8 +527,19 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) } } + if (Connection == nullptr) + { + UE_LOG(LogSpatialReceiver, Error, TEXT("Unable to find SpatialOSNetConnection! Has this worker been disconnected from SpatialOS due to a timeout?")); + return; + } + // Set up actor channel. +#if ENGINE_MINOR_VERSION <= 20 USpatialActorChannel* Channel = Cast(Connection->CreateChannel(CHTYPE_Actor, NetDriver->IsServer())); +#else + USpatialActorChannel* Channel = Cast(Connection->CreateChannelByName(NAME_Actor, NetDriver->IsServer() ? EChannelCreateFlags::OpenedLocally : EChannelCreateFlags::None)); +#endif + if (!Channel) { UE_LOG(LogSpatialReceiver, Warning, TEXT("Failed to create an actor channel when receiving entity %lld. The actor will not be spawned."), EntityId); @@ -430,14 +563,14 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) if (PendingAddComponent.EntityId == EntityId) { - ApplyComponentData(EntityId, *PendingAddComponent.Data->ComponentData, Channel); + ApplyComponentDataOnActorCreation(EntityId, *PendingAddComponent.Data->ComponentData, Channel); } } if (!NetDriver->IsServer()) { // Update interest on the entity's components after receiving initial component data (so Role and RemoteRole are properly set). - Sender->SendComponentInterest(EntityActor, EntityId, Channel->IsOwnedByWorker()); + Sender->SendComponentInterestForActor(Channel, EntityId, Channel->IsOwnedByWorker()); // 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. @@ -462,6 +595,11 @@ void USpatialReceiver::ReceiveActor(Worker_EntityId EntityId) EntityActor->DispatchBeginPlay(); } + if (EntityActor->GetClass()->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)) + { + GlobalStateManager->RegisterSingletonChannel(EntityActor, Channel); + } + EntityActor->UpdateOverlaps(); } } @@ -479,7 +617,24 @@ void USpatialReceiver::RemoveActor(Worker_EntityId EntityId) AActor* Actor = Cast(WeakActor.Get()); - UE_LOG(LogSpatialReceiver, Log, TEXT("Worker %s Remove Actor: %s %lld"), *NetDriver->Connection->GetWorkerId(), Actor && !Actor->IsPendingKill() ? *Actor->GetName() : TEXT("nullptr"), EntityId); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Worker %s Remove Actor: %s %lld"), *NetDriver->Connection->GetWorkerId(), Actor && !Actor->IsPendingKill() ? *Actor->GetName() : TEXT("nullptr"), EntityId); + + // Cleanup pending add components if any exist. + if (USpatialActorChannel* ActorChannel = NetDriver->GetActorChannelByEntityId(EntityId)) + { + // If we have any pending subobjects on the channel + if (ActorChannel->PendingDynamicSubobjects.Num() > 0) + { + // Then iterate through all pending subobjects and remove entries relating to this entity. + for (const auto& Pair : PendingDynamicSubobjectComponents) + { + if (Pair.Key.Key == EntityId) + { + PendingDynamicSubobjectComponents.Remove(Pair.Key); + } + } + } + } // Actor already deleted (this worker was most likely authoritative over it and deleted it earlier). if (Actor == nullptr || Actor->IsPendingKill()) @@ -487,8 +642,11 @@ void USpatialReceiver::RemoveActor(Worker_EntityId EntityId) if (USpatialActorChannel* ActorChannel = NetDriver->GetActorChannelByEntityId(EntityId)) { UE_LOG(LogSpatialReceiver, Warning, TEXT("RemoveActor: actor for entity %lld was already deleted (likely on the authoritative worker) but still has an open actor channel."), EntityId); +#if ENGINE_MINOR_VERSION <= 20 ActorChannel->ConditionalCleanUp(); - CleanupDeletedEntity(EntityId); +#else + ActorChannel->ConditionalCleanUp(false, EChannelCloseReason::Destroyed); +#endif } return; } @@ -498,8 +656,11 @@ void USpatialReceiver::RemoveActor(Worker_EntityId EntityId) { if (USpatialActorChannel* ActorChannel = NetDriver->GetActorChannelByEntityId(EntityId)) { +#if ENGINE_MINOR_VERSION <= 20 ActorChannel->ConditionalCleanUp(); - CleanupDeletedEntity(EntityId); +#else + ActorChannel->ConditionalCleanUp(false, EChannelCloseReason::TearOff); +#endif } return; } @@ -509,6 +670,10 @@ void USpatialReceiver::RemoveActor(Worker_EntityId EntityId) if (Actor->IsFullNameStableForNetworking()) { QueryForStartupActor(Actor, EntityId); + + // We can't call CleanupDeletedEntity here as we need the NetDriver to maintain the EntityId + // to Actor Channel mapping for the DestoryActor to function correctly + PackageMap->RemoveEntityActor(EntityId); return; } @@ -557,7 +722,7 @@ void USpatialReceiver::QueryForStartupActor(AActor* Actor, Worker_EntityId Entit EntityQueryDelegate StartupActorDelegate; TWeakObjectPtr WeakActor(Actor); - StartupActorDelegate.BindLambda([this, WeakActor, EntityId](Worker_EntityQueryResponseOp& Op) + StartupActorDelegate.BindLambda([this, WeakActor, EntityId](const Worker_EntityQueryResponseOp& Op) { if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) { @@ -592,7 +757,12 @@ void USpatialReceiver::DestroyActor(AActor* Actor, Worker_EntityId EntityId) // Clean up the actor channel. For clients, this will also call destroy on the actor. if (USpatialActorChannel* ActorChannel = NetDriver->GetActorChannelByEntityId(EntityId)) { + +#if ENGINE_MINOR_VERSION <= 20 ActorChannel->ConditionalCleanUp(); +#else + ActorChannel->ConditionalCleanUp(false, EChannelCloseReason::Destroyed); +#endif } else { @@ -613,9 +783,7 @@ void USpatialReceiver::DestroyActor(AActor* Actor, Worker_EntityId EntityId) } NetDriver->StopIgnoringAuthoritativeDestruction(); - CleanupDeletedEntity(EntityId); - - StaticComponentView->OnRemoveEntity(EntityId); + check(PackageMap->GetObjectFromEntityId(EntityId) == nullptr); } void USpatialReceiver::CleanupDeletedEntity(Worker_EntityId EntityId) @@ -660,9 +828,7 @@ AActor* USpatialReceiver::CreateActor(UnrealMetadata* UnrealMetadataComp, SpawnD // Initial Singleton Actor replication is handled with GlobalStateManager::LinkExistingSingletonActors if (NetDriver->IsServer() && ActorClass->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)) { - // If GSM doesn't know of this entity id, queue up data for that entity id, and resolve it when the actor is created - UNR-734 - // If the GSM does know of this entity id, we could just create the actor instead - UNR-735 - return nullptr; + return FindSingletonActor(ActorClass); } // If we're checking out a player controller, spawn it via "USpatialNetDriver::AcceptNewPlayer" @@ -684,7 +850,7 @@ AActor* USpatialReceiver::CreateActor(UnrealMetadata* UnrealMetadataComp, SpawnD FActorSpawnParameters SpawnInfo; SpawnInfo.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; - SpawnInfo.bRemoteOwned = !NetDriver->IsServer(); + SpawnInfo.bRemoteOwned = true; SpawnInfo.bNoFail = true; FVector SpawnLocation = FRepMovement::RebaseOntoLocalOrigin(SpawnDataComp->Location, NetDriver->GetWorld()->OriginLocation); @@ -725,7 +891,7 @@ FTransform USpatialReceiver::GetRelativeSpawnTransform(UClass* ActorClass, FTran return NewTransform; } -void USpatialReceiver::ApplyComponentData(Worker_EntityId EntityId, Worker_ComponentData& Data, USpatialActorChannel* Channel) +void USpatialReceiver::ApplyComponentDataOnActorCreation(Worker_EntityId EntityId, const Worker_ComponentData& Data, USpatialActorChannel* Channel) { uint32 Offset = 0; bool bFoundOffset = ClassInfoManager->GetOffsetByComponentId(Data.component_id, Offset); @@ -738,10 +904,115 @@ void USpatialReceiver::ApplyComponentData(Worker_EntityId EntityId, Worker_Compo TWeakObjectPtr TargetObject = PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(EntityId, Offset)); if (!TargetObject.IsValid()) { - UE_LOG(LogSpatialReceiver, Warning, TEXT("EntityId %lld, ComponentId %d, Offset %d - Could not find target object with given offset for Actor %s!"), EntityId, Data.component_id, Offset, *Channel->GetActor()->GetName()); + // If we can't find this subobject, it's a dynamically attached object. Create it now. + TargetObject = NewObject(Channel->GetActor(), ClassInfoManager->GetClassByComponentId(Data.component_id)); + + Channel->GetActor()->OnSubobjectCreatedFromReplication(TargetObject.Get()); + + PackageMap->ResolveSubobject(TargetObject.Get(), FUnrealObjectRef(EntityId, Offset)); + + Channel->CreateSubObjects.Add(TargetObject.Get()); + } + + ApplyComponentData(TargetObject.Get(), Channel, Data); +} + +void USpatialReceiver::HandleIndividualAddComponent(const Worker_AddComponentOp& Op) +{ + uint32 Offset = 0; + bool bFoundOffset = ClassInfoManager->GetOffsetByComponentId(Op.data.component_id, 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); + return; + } + + // Object already exists, we can apply data directly. + if (UObject* Object = PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(Op.entity_id, Offset)).Get()) + { + ApplyComponentData(Object, NetDriver->GetActorChannelByEntityId(Op.entity_id), Op.data); 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))); + + const FClassInfo& Info = ClassInfoManager->GetClassInfoByComponentId(Op.data.component_id); + + bool bReadyToCreate = true; + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) + { + Worker_ComponentId ComponentId = Info.SchemaComponents[Type]; + + if (ComponentId == SpatialConstants::INVALID_COMPONENT_ID) + { + return; + } + + if (!PendingDynamicSubobjectComponents.Contains(MakeTuple(static_cast(Op.entity_id), ComponentId))) + { + bReadyToCreate = false; + } + }); + + if (bReadyToCreate) + { + AttachDynamicSubobject(Op.entity_id, Info); + } +} + +void USpatialReceiver::AttachDynamicSubobject(Worker_EntityId EntityId, const FClassInfo& Info) +{ + AActor* Actor = Cast(PackageMap->GetObjectFromEntityId(EntityId).Get()); + + if (Actor == nullptr) + { + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Tried to dynamically attach subobject of type %s to entity %lld but couldn't find Actor!"), *Info.Class->GetName(), EntityId); + return; + } + + USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(EntityId); + if (Channel == nullptr) + { + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Tried to dynamically attach subobject of type %s to entity %lld but couldn't find Channel!"), *Info.Class->GetName(), EntityId); + return; + } + + UObject* Subobject = NewObject(Actor, Info.Class.Get()); + + Actor->OnSubobjectCreatedFromReplication(Subobject); + + PackageMap->ResolveSubobject(Subobject, FUnrealObjectRef(EntityId, Info.SchemaComponents[SCHEMA_Data])); + + Channel->CreateSubObjects.Add(Subobject); + + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) + { + Worker_ComponentId ComponentId = Info.SchemaComponents[Type]; + + if (ComponentId == SpatialConstants::INVALID_COMPONENT_ID) + { + return; + } + + TPair EntityComponentPair = MakeTuple(static_cast(EntityId), ComponentId); + + PendingAddComponentWrapper& AddComponent = PendingDynamicSubobjectComponents[EntityComponentPair]; + ApplyComponentData(Subobject, NetDriver->GetActorChannelByEntityId(EntityId), *AddComponent.Data->ComponentData); + PendingDynamicSubobjectComponents.Remove(EntityComponentPair); + }); + + // If on a client, we need to set up the proper component interest for the new subobject. + if (!NetDriver->IsServer()) + { + Sender->SendComponentInterestForSubobject(Info, EntityId, Channel->IsOwnedByWorker()); + } +} + +void USpatialReceiver::ApplyComponentData(UObject* TargetObject, USpatialActorChannel* Channel, const Worker_ComponentData& Data) +{ UClass* Class = ClassInfoManager->GetClassByComponentId(Data.component_id); checkf(Class, TEXT("Component %d isn't hand-written and not present in ComponentToClassMap."), Data.component_id); @@ -765,7 +1036,7 @@ void USpatialReceiver::ApplyComponentData(Worker_EntityId EntityId, Worker_Compo TSet UnresolvedRefs; ComponentReader Reader(NetDriver, ObjectReferencesMap, UnresolvedRefs); - Reader.ApplyComponentData(Data, TargetObject.Get(), Channel, /* bIsHandover */ false); + Reader.ApplyComponentData(Data, TargetObject, Channel, /* bIsHandover */ false); QueueIncomingRepUpdates(ChannelObjectPair, ObjectReferencesMap, UnresolvedRefs); } @@ -775,18 +1046,25 @@ void USpatialReceiver::ApplyComponentData(Worker_EntityId EntityId, Worker_Compo TSet UnresolvedRefs; ComponentReader Reader(NetDriver, ObjectReferencesMap, UnresolvedRefs); - Reader.ApplyComponentData(Data, TargetObject.Get(), Channel, /* bIsHandover */ true); + Reader.ApplyComponentData(Data, TargetObject, Channel, /* bIsHandover */ true); QueueIncomingRepUpdates(ChannelObjectPair, ObjectReferencesMap, UnresolvedRefs); } else { - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Entity: %d Component: %d - Skipping because RPC components don't have actual data."), EntityId, Data.component_id); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Entity: %d Component: %d - Skipping because RPC components don't have actual data."), Channel->GetEntityId(), Data.component_id); } } -void USpatialReceiver::OnComponentUpdate(Worker_ComponentUpdateOp& Op) +void USpatialReceiver::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) { + if (Op.update.component_id == SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID || + Op.update.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID) + { + Schema_Object* FieldsObject = Schema_GetComponentUpdateFields(Op.update.schema_type); + RegisterListeningEntityIfReady(Op.entity_id, FieldsObject); + } + if (StaticComponentView->GetAuthority(Op.entity_id, Op.update.component_id) == WORKER_AUTHORITY_AUTHORITATIVE) { UE_LOG(LogSpatialReceiver, Verbose, TEXT("Entity: %d Component: %d - Skipping update because this was short circuited"), Op.entity_id, Op.update.component_id); @@ -805,6 +1083,9 @@ void USpatialReceiver::OnComponentUpdate(Worker_ComponentUpdateOp& Op) case SpatialConstants::SINGLETON_COMPONENT_ID: case SpatialConstants::UNREAL_METADATA_COMPONENT_ID: case SpatialConstants::NOT_STREAMED_COMPONENT_ID: + case SpatialConstants::RPCS_ON_ENTITY_CREATION_ID: + case SpatialConstants::DEBUG_METRICS_COMPONENT_ID: + case SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID: UE_LOG(LogSpatialReceiver, Verbose, TEXT("Entity: %d Component: %d - Skipping because this is hand-written Spatial component"), Op.entity_id, Op.update.component_id); return; case SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID: @@ -813,10 +1094,7 @@ void USpatialReceiver::OnComponentUpdate(Worker_ComponentUpdateOp& Op) #endif // WITH_EDITOR return; case SpatialConstants::HEARTBEAT_COMPONENT_ID: - if (HeartbeatDelegate* UpdateDelegate = HeartbeatDelegates.Find(Op.entity_id)) - { - UpdateDelegate->ExecuteIfBound(Op); - } + OnHeartbeatComponentUpdate(Op); return; case SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID: GlobalStateManager->ApplySingletonManagerUpdate(Op.update); @@ -831,7 +1109,7 @@ void USpatialReceiver::OnComponentUpdate(Worker_ComponentUpdateOp& Op) case SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID: case SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID: case SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID: - HandleUnreliableRPC(Op); + HandleRPC(Op); return; } @@ -896,59 +1174,93 @@ void USpatialReceiver::OnComponentUpdate(Worker_ComponentUpdateOp& Op) } } -void USpatialReceiver::HandleUnreliableRPC(Worker_ComponentUpdateOp& Op) +void USpatialReceiver::HandleRPC(const Worker_ComponentUpdateOp& Op) { Worker_EntityId EntityId = Op.entity_id; + // If the update is to the client rpc endpoint, then the handler should have authority over the server rpc endpoint component and vice versa + // Ideally these events are never delivered to workers which are not able to handle them with clever interest management + const Worker_ComponentId RPCEndpointComponentId = Op.update.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID + ? SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID : SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID; + // Multicast RPCs should be executed by whoever receives them. if (Op.update.component_id != SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID) { - // If the update is to the client rpc endpoint, then the handler should have authority over the server rpc endpoint component and vice versa - // Ideally these events are never delivered to workers which are not able to handle them with clever interest management - const Worker_ComponentId RPCEndpointComponentId = Op.update.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID - ? SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID : SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID; - if (StaticComponentView->GetAuthority(Op.entity_id, RPCEndpointComponentId) != WORKER_AUTHORITY_AUTHORITATIVE) { return; } } - Schema_Object* EventsObject = Schema_GetComponentUpdateEvents(Op.update.schema_type); + // Always process unpacked RPCs since some cannot be packed. + ProcessRPCEventField(EntityId, Op, RPCEndpointComponentId, /* bPacked */ false); - uint32 EventCount = Schema_GetObjectCount(EventsObject, SpatialConstants::UNREAL_RPC_ENDPOINT_EVENT_ID); + if (GetDefault()->bPackRPCs) + { + // Only process packed RPCs if packing is enabled + ProcessRPCEventField(EntityId, Op, RPCEndpointComponentId, /* bPacked */ true); + } +} + +void USpatialReceiver::ProcessRPCEventField(Worker_EntityId EntityId, const Worker_ComponentUpdateOp& Op, Worker_ComponentId RPCEndpointComponentId, bool bPacked) +{ + Schema_Object* EventsObject = Schema_GetComponentUpdateEvents(Op.update.schema_type); + const Schema_FieldId EventId = bPacked ? SpatialConstants::UNREAL_RPC_ENDPOINT_PACKED_EVENT_ID : SpatialConstants::UNREAL_RPC_ENDPOINT_EVENT_ID; + uint32 EventCount = Schema_GetObjectCount(EventsObject, EventId); for (uint32 i = 0; i < EventCount; i++) { - Schema_Object* EventData = Schema_IndexObject(EventsObject, SpatialConstants::UNREAL_RPC_ENDPOINT_EVENT_ID, i); - - uint32 Offset = Schema_GetUint32(EventData, SpatialConstants::UNREAL_RPC_PAYLOAD_OFFSET_ID); - uint32 Index = Schema_GetUint32(EventData, SpatialConstants::UNREAL_RPC_PAYLOAD_RPC_INDEX_ID); - TArray PayloadData = GetBytesFromSchema(EventData, SpatialConstants::UNREAL_RPC_PAYLOAD_RPC_PAYLOAD_ID); - int64 CountBits = PayloadData.Num() * 8; + Schema_Object* EventData = Schema_IndexObject(EventsObject, EventId, i); - FUnrealObjectRef ObjectRef(EntityId, Offset); + RPCPayload Payload(EventData); - UObject* TargetObject = PackageMap->GetObjectFromUnrealObjectRef(ObjectRef).Get(); + FUnrealObjectRef ObjectRef(EntityId, Payload.Offset); - if (!TargetObject) + if (bPacked) { - UE_LOG(LogSpatialReceiver, Warning, TEXT("HandleUnreliableRPC: Could not find target object: %s, skipping rpc at index: %d"), *ObjectRef.ToString(), Index); - continue; + // When packing unreliable RPCs into one update, they also always go through the PlayerController. + // This means we need to retrieve the actual target Entity ID from the payload. + if (Op.update.component_id == SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID || + Op.update.component_id == SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID) + { + ObjectRef.Entity = Schema_GetEntityId(EventData, SpatialConstants::UNREAL_PACKED_RPC_PAYLOAD_ENTITY_ID); + + + // In a zoned multiworker scenario we might not have gained authority over the current entity in this bundle in time + // before processing so don't ApplyRPCs to an entity that we don't have authority over. + if (StaticComponentView->GetAuthority(ObjectRef.Entity, RPCEndpointComponentId) != WORKER_AUTHORITY_AUTHORITATIVE) + { + continue; + } + } } - const FClassInfo& ClassInfo = ClassInfoManager->GetOrCreateClassInfoByObject(TargetObject); + FPendingRPCParamsPtr Params = MakeUnique(ObjectRef, MoveTemp(Payload)); + if (UObject* TargetObject = PackageMap->GetObjectFromUnrealObjectRef(ObjectRef).Get()) + { + const FClassInfo& ClassInfo = ClassInfoManager->GetOrCreateClassInfoByObject(TargetObject); + UFunction* Function = ClassInfo.RPCs[Payload.Index]; + const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); + + if (!IncomingRPCs.ObjectHasRPCsQueuedOfType(ObjectRef.Entity, RPCInfo.Type)) + { + // Apply if possible, queue otherwise + if (ApplyRPC(*Params)) + { + continue; + } + } + } - UFunction* Function = ClassInfo.RPCs[Index]; - ApplyRPC(TargetObject, Function, PayloadData, CountBits, FString()); + QueueIncomingRPC(MoveTemp(Params)); } } -void USpatialReceiver::OnCommandRequest(Worker_CommandRequestOp& Op) +void USpatialReceiver::OnCommandRequest(const Worker_CommandRequestOp& Op) { Schema_FieldId CommandIndex = Schema_GetCommandRequestCommandIndex(Op.request.schema_type); - if (Op.request.component_id == SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID && CommandIndex == 1) + if (Op.request.component_id == SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID && CommandIndex == SpatialConstants::PLAYER_SPAWNER_SPAWN_PLAYER_COMMAND_ID) { Schema_Object* Payload = Schema_GetCommandRequestObject(Op.request.schema_type); @@ -959,6 +1271,12 @@ void USpatialReceiver::OnCommandRequest(Worker_CommandRequestOp& Op) NetDriver->PlayerSpawner->ReceivePlayerSpawnRequest(Payload, Op.caller_attribute_set.attributes[1], Op.request_id); return; } + else if (Op.request.component_id == SpatialConstants::RPCS_ON_ENTITY_CREATION_ID && CommandIndex == SpatialConstants::CLEAR_RPCS_ON_ENTITY_CREATION) + { + Sender->ClearRPCsOnEntityCreation(Op.entity_id); + Sender->SendEmptyCommandResponse(Op.request.component_id, CommandIndex, Op.request_id); + return; + } #if WITH_EDITOR else if (Op.request.component_id == SpatialConstants::GSM_SHUTDOWN_COMPONENT_ID && CommandIndex == SpatialConstants::SHUTDOWN_MULTI_PROCESS_REQUEST_ID) { @@ -966,38 +1284,70 @@ void USpatialReceiver::OnCommandRequest(Worker_CommandRequestOp& Op) return; } #endif // WITH_EDITOR +#if !UE_BUILD_SHIPPING + else if (Op.request.component_id == SpatialConstants::DEBUG_METRICS_COMPONENT_ID) + { + switch (CommandIndex) + { + case SpatialConstants::DEBUG_METRICS_START_RPC_METRICS_ID: + NetDriver->SpatialMetrics->OnStartRPCMetricsCommand(); + break; + case SpatialConstants::DEBUG_METRICS_STOP_RPC_METRICS_ID: + NetDriver->SpatialMetrics->OnStopRPCMetricsCommand(); + break; + case SpatialConstants::DEBUG_METRICS_MODIFY_SETTINGS_ID: + { + Schema_Object* Payload = Schema_GetCommandRequestObject(Op.request.schema_type); + NetDriver->SpatialMetrics->OnModifySettingCommand(Payload); + break; + } + default: + UE_LOG(LogSpatialReceiver, Error, TEXT("Unknown command index for DebugMetrics component: %d, entity: %lld"), CommandIndex, Op.entity_id); + break; + } - Worker_CommandResponse Response = {}; - Response.component_id = Op.request.component_id; - Response.schema_type = Schema_CreateCommandResponse(Op.request.component_id, CommandIndex); + Sender->SendEmptyCommandResponse(Op.request.component_id, CommandIndex, Op.request_id); + return; + } +#endif // !UE_BUILD_SHIPPING Schema_Object* RequestObject = Schema_GetCommandRequestObject(Op.request.schema_type); - uint32 Offset = 0; - Offset = Schema_GetUint32(RequestObject, SpatialConstants::UNREAL_RPC_PAYLOAD_OFFSET_ID); - - UObject* TargetObject = PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(Op.entity_id, Offset)).Get(); + RPCPayload Payload(RequestObject); + FUnrealObjectRef ObjectRef = FUnrealObjectRef(Op.entity_id, Payload.Offset); + UObject* TargetObject = PackageMap->GetObjectFromUnrealObjectRef(ObjectRef).Get(); if (TargetObject == nullptr) { UE_LOG(LogSpatialReceiver, Warning, TEXT("No target object found for EntityId %d"), Op.entity_id); - Sender->SendCommandResponse(Op.request_id, Response); + Sender->SendEmptyCommandResponse(Op.request.component_id, CommandIndex, Op.request_id); return; } const FClassInfo& Info = ClassInfoManager->GetOrCreateClassInfoByObject(TargetObject); - - uint32 Index = Schema_GetUint32(RequestObject, SpatialConstants::UNREAL_RPC_PAYLOAD_RPC_INDEX_ID); - - UFunction* Function = Info.RPCs[Index]; + UFunction* Function = Info.RPCs[Payload.Index]; + const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); UE_LOG(LogSpatialReceiver, Verbose, TEXT("Received command request (entity: %lld, component: %d, function: %s)"), Op.entity_id, Op.request.component_id, *Function->GetName()); - ReceiveRPCCommandRequest(Op.request, TargetObject, Function, UTF8_TO_TCHAR(Op.caller_worker_id)); - Sender->SendCommandResponse(Op.request_id, Response); + bool bAppliedRPC = false; + if (!IncomingRPCs.ObjectHasRPCsQueuedOfType(ObjectRef.Entity, RPCInfo.Type)) + { + if (ApplyRPC(TargetObject, Function, Payload, FString())) + { + bAppliedRPC = true; + } + } + + if (!bAppliedRPC) + { + QueueIncomingRPC(MakeUnique(ObjectRef, MoveTemp(Payload))); + } + + Sender->SendEmptyCommandResponse(Op.request.component_id, CommandIndex, Op.request_id); } -void USpatialReceiver::OnCommandResponse(Worker_CommandResponseOp& Op) +void USpatialReceiver::OnCommandResponse(const Worker_CommandResponseOp& Op) { if (Op.response.component_id == SpatialConstants::PLAYER_SPAWNER_COMPONENT_ID) { @@ -1013,20 +1363,34 @@ void USpatialReceiver::FlushRetryRPCs() Sender->FlushRetryRPCs(); } -void USpatialReceiver::ReceiveCommandResponse(Worker_CommandResponseOp& Op) +void USpatialReceiver::ReceiveCommandResponse(const Worker_CommandResponseOp& Op) { - TSharedRef* ReliableRPCPtr = PendingReliableRPCs.Find(Op.request_id); + TSharedRef* ReliableRPCPtr = PendingReliableRPCs.Find(Op.request_id); if (ReliableRPCPtr == nullptr) { - // We received a response for an unreliable RPC, ignore. + // We received a response for some other command, ignore. return; } - TSharedRef ReliableRPC = *ReliableRPCPtr; + TSharedRef ReliableRPC = *ReliableRPCPtr; PendingReliableRPCs.Remove(Op.request_id); if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) { - if (ReliableRPC->Attempts < SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS) + bool bCanRetry = false; + + // Only attempt to retry if the error code indicates it makes sense too + if ((Op.status_code == WORKER_STATUS_CODE_TIMEOUT || Op.status_code == WORKER_STATUS_CODE_NOT_FOUND) + && (ReliableRPC->Attempts < SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS)) + { + bCanRetry = true; + } + // Don't apply the retry limit on auth lost, as it should eventually succeed + else if (Op.status_code == WORKER_STATUS_CODE_AUTHORITY_LOST) + { + bCanRetry = true; + } + + if (bCanRetry) { float WaitTime = SpatialConstants::GetCommandRetryWaitTimeSeconds(ReliableRPC->Attempts); UE_LOG(LogSpatialReceiver, Log, TEXT("%s: retrying in %f seconds. Error code: %d Message: %s"), @@ -1041,9 +1405,12 @@ void USpatialReceiver::ReceiveCommandResponse(Worker_CommandResponseOp& Op) // Queue retry FTimerHandle RetryTimer; - TimerManager->SetTimer(RetryTimer, [this, ReliableRPC]() + TimerManager->SetTimer(RetryTimer, [WeakSender = TWeakObjectPtr(Sender), ReliableRPC]() { - Sender->EnqueueRetryRPC(ReliableRPC); + if (USpatialSender* SpatialSender = WeakSender.Get()) + { + SpatialSender->EnqueueRetryRPC(ReliableRPC); + } }, WaitTime, false); } else @@ -1072,7 +1439,11 @@ void USpatialReceiver::ApplyComponentUpdate(const Worker_ComponentUpdate& Compon // Check if bTearOff has been set to true if (Schema_GetBool(ComponentObject, SpatialConstants::ACTOR_TEAROFF_ID)) { +#if ENGINE_MINOR_VERSION <= 20 Channel->ConditionalCleanUp(); +#else + Channel->ConditionalCleanUp(false, EChannelCloseReason::TearOff); +#endif CleanupDeletedEntity(Channel->GetEntityId()); } } @@ -1080,65 +1451,67 @@ void USpatialReceiver::ApplyComponentUpdate(const Worker_ComponentUpdate& Compon QueueIncomingRepUpdates(ChannelObjectPair, ObjectReferencesMap, UnresolvedRefs); } -void USpatialReceiver::ReceiveMulticastUpdate(const Worker_ComponentUpdate& ComponentUpdate, UObject* TargetObject, const TArray& RPCArray) +void USpatialReceiver::RegisterListeningEntityIfReady(Worker_EntityId EntityId, Schema_Object* Object) { - Schema_Object* EventsObject = Schema_GetComponentUpdateEvents(ComponentUpdate.schema_type); - - for (Schema_FieldId EventIndex = 1; (int)EventIndex <= RPCArray.Num(); EventIndex++) + if (Schema_GetBoolCount(Object, SpatialConstants::UNREAL_RPC_ENDPOINT_READY_ID) > 0) { - UFunction* Function = RPCArray[EventIndex - 1]; - for (uint32 i = 0; i < Schema_GetObjectCount(EventsObject, EventIndex); i++) + bool bReady = GetBoolFromSchema(Object, SpatialConstants::UNREAL_RPC_ENDPOINT_READY_ID); + if (bReady) { - Schema_Object* EventData = Schema_IndexObject(EventsObject, EventIndex, i); - - TArray PayloadData = GetBytesFromSchema(EventData, 1); - // A bit hacky, we should probably include the number of bits with the data instead. - int64 CountBits = PayloadData.Num() * 8; - - ApplyRPC(TargetObject, Function, PayloadData, CountBits, FString()); + if (USpatialActorChannel* Channel = NetDriver->GetActorChannelByEntityId(EntityId)) + { + Channel->StartListening(); + if (UObject* TargetObject = Channel->GetActor()) + { + Sender->SendOutgoingRPCs(); + } + } } } } -void USpatialReceiver::ApplyRPC(UObject* TargetObject, UFunction* Function, TArray& PayloadData, int64 CountBits, const FString& SenderWorkerId) +bool USpatialReceiver::ApplyRPC(UObject* TargetObject, UFunction* Function, const RPCPayload& Payload, const FString& SenderWorkerId) { + bool bApplied = false; + uint8* Parms = (uint8*)FMemory_Alloca(Function->ParmsSize); FMemory::Memzero(Parms, Function->ParmsSize); TSet UnresolvedRefs; - FSpatialNetBitReader PayloadReader(PackageMap, PayloadData.GetData(), CountBits, UnresolvedRefs); + RPCPayload PayloadCopy = Payload; + FSpatialNetBitReader PayloadReader(PackageMap, PayloadCopy.PayloadData.GetData(), PayloadCopy.CountDataBits(), UnresolvedRefs); -#if !UE_BUILD_SHIPPING int ReliableRPCId = 0; - if (Function->HasAnyFunctionFlags(FUNC_NetReliable) && !Function->HasAnyFunctionFlags(FUNC_NetMulticast)) + if (GetDefault()->bCheckRPCOrder) { - PayloadReader << ReliableRPCId; + if (Function->HasAnyFunctionFlags(FUNC_NetReliable) && !Function->HasAnyFunctionFlags(FUNC_NetMulticast)) + { + PayloadReader << ReliableRPCId; + } } -#endif // !UE_BUILD_SHIPPING TSharedPtr RepLayout = NetDriver->GetFunctionRepLayout(Function); RepLayout_ReceivePropertiesForRPC(*RepLayout, PayloadReader, Parms); if (UnresolvedRefs.Num() == 0) { -#if !UE_BUILD_SHIPPING - if (Function->HasAnyFunctionFlags(FUNC_NetReliable) && !Function->HasAnyFunctionFlags(FUNC_NetMulticast)) + if (GetDefault()->bCheckRPCOrder) { - AActor* Actor = Cast(TargetObject); - if (Actor == nullptr) + if (Function->HasAnyFunctionFlags(FUNC_NetReliable) && !Function->HasAnyFunctionFlags(FUNC_NetMulticast)) { - Actor = Cast(TargetObject->GetOuter()); - check(Actor); + AActor* Actor = Cast(TargetObject); + if (Actor == nullptr) + { + Actor = Cast(TargetObject->GetOuter()); + check(Actor); + } + NetDriver->OnReceivedReliableRPC(Actor, FunctionFlagsToRPCSchemaType(Function->FunctionFlags), SenderWorkerId, ReliableRPCId, TargetObject, Function); } - NetDriver->OnReceivedReliableRPC(Actor, FunctionFlagsToRPCSchemaType(Function->FunctionFlags), SenderWorkerId, ReliableRPCId, TargetObject, Function); } -#endif // !UE_BUILD_SHIPPING + TargetObject->ProcessEvent(Function, Parms); - } - else - { - QueueIncomingRPC(UnresolvedRefs, TargetObject, Function, PayloadData, CountBits, SenderWorkerId); + bApplied = true; } // Destroy the parameters. @@ -1147,9 +1520,28 @@ void USpatialReceiver::ApplyRPC(UObject* TargetObject, UFunction* Function, TArr { It->DestroyValue_InContainer(Parms); } + return bApplied; +} + +bool USpatialReceiver::ApplyRPC(const FPendingRPCParams& Params) +{ + TWeakObjectPtr TargetObjectWeakPtr = PackageMap->GetObjectFromUnrealObjectRef(Params.ObjectRef); + if (!TargetObjectWeakPtr.IsValid()) + { + return false; + } + + const FClassInfo& ClassInfo = ClassInfoManager->GetOrCreateClassInfoByObject(TargetObjectWeakPtr.Get()); + UFunction* Function = ClassInfo.RPCs[Params.Payload.Index]; + if (Function == nullptr) + { + return false; + } + + return ApplyRPC(TargetObjectWeakPtr.Get(), Function, Params.Payload, FString{}); } -void USpatialReceiver::OnReserveEntityIdsResponse(Worker_ReserveEntityIdsResponseOp& Op) +void USpatialReceiver::OnReserveEntityIdsResponse(const Worker_ReserveEntityIdsResponseOp& Op) { if (Op.status_code == WORKER_STATUS_CODE_SUCCESS) { @@ -1170,7 +1562,7 @@ void USpatialReceiver::OnReserveEntityIdsResponse(Worker_ReserveEntityIdsRespons } } -void USpatialReceiver::OnCreateEntityResponse(Worker_CreateEntityResponseOp& Op) +void USpatialReceiver::OnCreateEntityResponse(const Worker_CreateEntityResponseOp& Op) { if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) { @@ -1181,6 +1573,11 @@ void USpatialReceiver::OnCreateEntityResponse(Worker_CreateEntityResponseOp& Op) UE_LOG(LogSpatialReceiver, Verbose, TEXT("Create entity request succeeded: request id: %d, entity id: %lld, message: %s"), Op.request_id, Op.entity_id, UTF8_TO_TCHAR(Op.message)); } + if (CreateEntityDelegate* Delegate = CreateEntityDelegates.Find(Op.request_id)) + { + Delegate->ExecuteIfBound(Op); + } + TWeakObjectPtr Channel = PopPendingActorRequest(Op.request_id); // It's possible for the ActorChannel to have been closed by the time we receive a response. Actor validity is checked within the channel. @@ -1194,24 +1591,21 @@ void USpatialReceiver::OnCreateEntityResponse(Worker_CreateEntityResponseOp& Op) } } -void USpatialReceiver::OnEntityQueryResponse(Worker_EntityQueryResponseOp& Op) +void USpatialReceiver::OnEntityQueryResponse(const Worker_EntityQueryResponseOp& Op) { - if (Op.status_code == WORKER_STATUS_CODE_SUCCESS) + if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) { - auto RequestDelegate = EntityQueryDelegates.Find(Op.request_id); - if (RequestDelegate) - { - UE_LOG(LogSpatialReceiver, Log, TEXT("Executing EntityQueryResponse with delegate, request id: %d, number of entities: %d, message: %s"), Op.request_id, Op.result_count, UTF8_TO_TCHAR(Op.message)); - RequestDelegate->ExecuteIfBound(Op); - } - else - { - UE_LOG(LogSpatialReceiver, Warning, TEXT("Recieved EntityQueryResponse but with no delegate set, request id: %d, number of entities: %d, message: %s"), Op.request_id, Op.result_count, UTF8_TO_TCHAR(Op.message)); - } + UE_LOG(LogSpatialReceiver, Error, TEXT("EntityQuery failed: request id: %d, message: %s"), Op.request_id, UTF8_TO_TCHAR(Op.message)); + } + + if (EntityQueryDelegate* RequestDelegate = EntityQueryDelegates.Find(Op.request_id)) + { + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Executing EntityQueryResponse with delegate, request id: %d, number of entities: %d, message: %s"), Op.request_id, Op.result_count, UTF8_TO_TCHAR(Op.message)); + RequestDelegate->ExecuteIfBound(Op); } else { - UE_LOG(LogSpatialReceiver, Error, TEXT("EntityQuery failed: request id: %d, message: %s"), Op.request_id, UTF8_TO_TCHAR(Op.message)); + UE_LOG(LogSpatialReceiver, Warning, TEXT("Recieved EntityQueryResponse but with no delegate set, request id: %d, number of entities: %d, message: %s"), Op.request_id, Op.result_count, UTF8_TO_TCHAR(Op.message)); } } @@ -1220,9 +1614,9 @@ void USpatialReceiver::AddPendingActorRequest(Worker_RequestId RequestId, USpati PendingActorRequests.Add(RequestId, Channel); } -void USpatialReceiver::AddPendingReliableRPC(Worker_RequestId RequestId, TSharedRef Params) +void USpatialReceiver::AddPendingReliableRPC(Worker_RequestId RequestId, TSharedRef ReliableRPC) { - PendingReliableRPCs.Add(RequestId, Params); + PendingReliableRPCs.Add(RequestId, ReliableRPC); } void USpatialReceiver::AddEntityQueryDelegate(Worker_RequestId RequestId, EntityQueryDelegate Delegate) @@ -1235,9 +1629,9 @@ void USpatialReceiver::AddReserveEntityIdsDelegate(Worker_RequestId RequestId, R ReserveEntityIDsDelegates.Add(RequestId, Delegate); } -void USpatialReceiver::AddHeartbeatDelegate(Worker_EntityId EntityId, HeartbeatDelegate Delegate) +void USpatialReceiver::AddCreateEntityDelegate(Worker_RequestId RequestId, const CreateEntityDelegate& Delegate) { - HeartbeatDelegates.Add(EntityId, Delegate); + CreateEntityDelegates.Add(RequestId, Delegate); } TWeakObjectPtr USpatialReceiver::PopPendingActorRequest(Worker_RequestId RequestId) @@ -1252,6 +1646,20 @@ 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::ProcessQueuedResolvedObjects() { for (TPair& It : ResolvedObjectQueue) @@ -1261,6 +1669,29 @@ void USpatialReceiver::ProcessQueuedResolvedObjects() ResolvedObjectQueue.Empty(); } +void USpatialReceiver::ProcessQueuedActorRPCsOnEntityCreation(AActor* Actor, RPCsOnEntityCreation& QueuedRPCs) +{ + const FClassInfo& Info = ClassInfoManager->GetOrCreateClassInfoByClass(Actor->GetClass()); + + for (auto& RPC : QueuedRPCs.RPCs) + { + UFunction* Function = Info.RPCs[RPC.Index]; + const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(Actor, Function); + const FUnrealObjectRef ObjectRef = PackageMap->GetUnrealObjectRefFromObject(Actor); + check(ObjectRef != FUnrealObjectRef::UNRESOLVED_OBJECT_REF); + + if (!IncomingRPCs.ObjectHasRPCsQueuedOfType(ObjectRef.Entity, RPCInfo.Type)) + { + if (ApplyRPC(Actor, Function, RPC, FString())) + { + continue; + } + } + + QueueIncomingRPC(MakeUnique(ObjectRef, MoveTemp(RPC))); + } +} + void USpatialReceiver::ResolvePendingOperations(UObject* Object, const FUnrealObjectRef& ObjectRef) { if (bInCriticalSection) @@ -1295,18 +1726,22 @@ void USpatialReceiver::QueueIncomingRepUpdates(FChannelObjectPair ChannelObjectP } } -void USpatialReceiver::QueueIncomingRPC(const TSet& UnresolvedRefs, UObject* TargetObject, UFunction* Function, const TArray& PayloadData, int64 CountBits, const FString& SenderWorkerId) +void USpatialReceiver::QueueIncomingRPC(FPendingRPCParamsPtr Params) { - TSharedPtr IncomingRPC = MakeShared(UnresolvedRefs, TargetObject, Function, PayloadData, CountBits); -#if !UE_BUILD_SHIPPING - IncomingRPC->SenderWorkerId = SenderWorkerId; -#endif // !UE_BUILD_SHIPPING - - for (const FUnrealObjectRef& UnresolvedRef : UnresolvedRefs) + TWeakObjectPtr TargetObjectWeakPtr = PackageMap->GetObjectFromUnrealObjectRef(Params->ObjectRef); + if (!TargetObjectWeakPtr.IsValid()) { - FIncomingRPCArray& IncomingRPCArray = IncomingRPCMap.FindOrAdd(UnresolvedRef); - IncomingRPCArray.Add(IncomingRPC); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("The object has been deleted, dropping the RPC")); + return; } + + UObject* TargetObject = TargetObjectWeakPtr.Get(); + const FClassInfo& ClassInfo = ClassInfoManager->GetOrCreateClassInfoByObject(TargetObject); + UFunction* Function = ClassInfo.RPCs[Params->Payload.Index]; + const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); + ESchemaComponentType Type = RPCInfo.Type; + + IncomingRPCs.QueueRPC(MoveTemp(Params), Type); } void USpatialReceiver::ResolvePendingOperations_Internal(UObject* Object, const FUnrealObjectRef& ObjectRef) @@ -1316,8 +1751,8 @@ void USpatialReceiver::ResolvePendingOperations_Internal(UObject* Object, const Sender->ResolveOutgoingOperations(Object, /* bIsHandover */ false); Sender->ResolveOutgoingOperations(Object, /* bIsHandover */ true); ResolveIncomingOperations(Object, ObjectRef); - Sender->ResolveOutgoingRPCs(Object); - ResolveIncomingRPCs(Object, ObjectRef); + // TODO: UNR-1650 We're trying to resolve all queues, which introduces more overhead. + ResolveIncomingRPCs(); } void USpatialReceiver::ResolveIncomingOperations(UObject* Object, const FUnrealObjectRef& ObjectRef) @@ -1357,13 +1792,13 @@ void USpatialReceiver::ResolveIncomingOperations(UObject* Object, const FUnrealO FRepLayout& RepLayout = DependentChannel->GetObjectRepLayout(ReplicatingObject); FRepStateStaticBuffer& ShadowData = DependentChannel->GetObjectStaticBuffer(ReplicatingObject); - ResolveObjectReferences(RepLayout, ReplicatingObject, *UnresolvedRefs, ShadowData.GetData(), (uint8*)ReplicatingObject, ShadowData.Num(), RepNotifies, bSomeObjectsWereMapped, bStillHasUnresolved); + ResolveObjectReferences(RepLayout, ReplicatingObject, *UnresolvedRefs, ShadowData.GetData(), (uint8*)ReplicatingObject, ReplicatingObject->GetClass()->GetPropertiesSize(), RepNotifies, bSomeObjectsWereMapped, bStillHasUnresolved); if (bSomeObjectsWereMapped) { DependentChannel->RemoveRepNotifiesWithUnresolvedObjs(RepNotifies, RepLayout, *UnresolvedRefs, ReplicatingObject); - UE_LOG(LogSpatialReceiver, Log, TEXT("Resolved for target object %s"), *ReplicatingObject->GetName()); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Resolved for target object %s"), *ReplicatingObject->GetName()); DependentChannel->PostReceiveSpatialUpdate(ReplicatingObject, RepNotifies); } @@ -1376,36 +1811,11 @@ void USpatialReceiver::ResolveIncomingOperations(UObject* Object, const FUnrealO IncomingRefsMap.Remove(ObjectRef); } -void USpatialReceiver::ResolveIncomingRPCs(UObject* Object, const FUnrealObjectRef& ObjectRef) +void USpatialReceiver::ResolveIncomingRPCs() { - FIncomingRPCArray* IncomingRPCArray = IncomingRPCMap.Find(ObjectRef); - if (!IncomingRPCArray) - { - return; - } - - UE_LOG(LogSpatialReceiver, Verbose, TEXT("Resolving incoming RPCs depending on object ref %s, resolved object: %s"), *ObjectRef.ToString(), *Object->GetName()); - - for (const TSharedPtr& IncomingRPC : *IncomingRPCArray) - { - if (!IncomingRPC->TargetObject.IsValid()) - { - // The target object has been destroyed before this RPC was resolved - continue; - } - - IncomingRPC->UnresolvedRefs.Remove(ObjectRef); - if (IncomingRPC->UnresolvedRefs.Num() == 0) - { - FString SenderWorkerId; -#if !UE_BUILD_SHIPPING - SenderWorkerId = IncomingRPC->SenderWorkerId; -#endif // !UE_BUILD_SHIPPING - ApplyRPC(IncomingRPC->TargetObject.Get(), IncomingRPC->Function, IncomingRPC->PayloadData, IncomingRPC->CountBits, SenderWorkerId); - } - } - - IncomingRPCMap.Remove(ObjectRef); + FProcessRPCDelegate Delegate; + Delegate.BindUObject(this, &USpatialReceiver::ApplyRPC); + IncomingRPCs.ProcessRPCs(Delegate); } void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* ReplicatedObject, FObjectReferencesMap& ObjectReferencesMap, uint8* RESTRICT StoredData, uint8* RESTRICT Data, int32 MaxAbsOffset, TArray& RepNotifies, bool& bOutSomeObjectsWereMapped, bool& bOutStillHasUnresolved) @@ -1422,23 +1832,35 @@ void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* R } FObjectReferences& ObjectReferences = It.Value(); + UProperty* Property = ObjectReferences.Property; + // ParentIndex is -1 for handover properties + bool bIsHandover = ObjectReferences.ParentIndex == -1; FRepParentCmd* Parent = ObjectReferences.ParentIndex >= 0 ? &RepLayout.Parents[ObjectReferences.ParentIndex] : nullptr; +#if ENGINE_MINOR_VERSION <= 20 + int32 StoredDataOffset = AbsOffset; +#else + int32 StoredDataOffset = ObjectReferences.ShadowOffset; +#endif + if (ObjectReferences.Array) { check(Property->IsA()); - Property->CopySingleValue(StoredData + AbsOffset, Data + AbsOffset); + if (!bIsHandover) + { + Property->CopySingleValue(StoredData + StoredDataOffset, Data + AbsOffset); + } - FScriptArray* StoredArray = (FScriptArray*)(StoredData + AbsOffset); + FScriptArray* StoredArray = bIsHandover ? nullptr : (FScriptArray*)(StoredData + StoredDataOffset); FScriptArray* Array = (FScriptArray*)(Data + AbsOffset); int32 NewMaxOffset = Array->Num() * Property->ElementSize; bool bArrayHasUnresolved = false; - ResolveObjectReferences(RepLayout, ReplicatedObject, *ObjectReferences.Array, (uint8*)StoredArray->GetData(), (uint8*)Array->GetData(), NewMaxOffset, RepNotifies, bOutSomeObjectsWereMapped, bArrayHasUnresolved); + ResolveObjectReferences(RepLayout, ReplicatedObject, *ObjectReferences.Array, bIsHandover ? nullptr : (uint8*)StoredArray->GetData(), (uint8*)Array->GetData(), NewMaxOffset, RepNotifies, bOutSomeObjectsWereMapped, bArrayHasUnresolved); if (!bArrayHasUnresolved) { It.RemoveCurrent(); @@ -1463,7 +1885,7 @@ void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* R UObject* Object = PackageMap->GetObjectFromNetGUID(NetGUID, true); check(Object); - UE_LOG(LogSpatialReceiver, Log, TEXT("ResolveObjectReferences: Resolved object ref: Offset: %d, Object ref: %s, PropName: %s, ObjName: %s"), AbsOffset, *ObjectRef.ToString(), *Property->GetNameCPP(), *Object->GetName()); + UE_LOG(LogSpatialReceiver, Verbose, TEXT("ResolveObjectReferences: Resolved object ref: Offset: %d, Object ref: %s, PropName: %s, ObjName: %s"), AbsOffset, *ObjectRef.ToString(), *Property->GetNameCPP(), *Object->GetName()); UnresolvedIt.RemoveCurrent(); bResolvedSomeRefs = true; @@ -1485,7 +1907,7 @@ void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* R if (Parent && Parent->Property->HasAnyPropertyFlags(CPF_RepNotify)) { - Property->CopySingleValue(StoredData + AbsOffset, Data + AbsOffset); + Property->CopySingleValue(StoredData + StoredDataOffset, Data + AbsOffset); } if (ObjectReferences.bSingleProp) @@ -1520,7 +1942,7 @@ void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* R if (Parent && Parent->Property->HasAnyPropertyFlags(CPF_RepNotify)) { - if (Parent->RepNotifyCondition == REPNOTIFY_Always || !Property->Identical(StoredData + AbsOffset, Data + AbsOffset)) + if (Parent->RepNotifyCondition == REPNOTIFY_Always || !Property->Identical(StoredData + StoredDataOffset, Data + AbsOffset)) { RepNotifies.AddUnique(Parent->Property); } @@ -1538,13 +1960,49 @@ void USpatialReceiver::ResolveObjectReferences(FRepLayout& RepLayout, UObject* R } } -void USpatialReceiver::ReceiveRPCCommandRequest(const Worker_CommandRequest& CommandRequest, UObject* TargetObject, UFunction* Function, const FString& SenderWorkerId) +void USpatialReceiver::OnHeartbeatComponentUpdate(const Worker_ComponentUpdateOp& Op) { - Schema_Object* RequestObject = Schema_GetCommandRequestObject(CommandRequest.schema_type); + if (!NetDriver->IsServer()) + { + // Clients can ignore Heartbeat component updates. + return; + } - TArray PayloadData = GetBytesFromSchema(RequestObject, SpatialConstants::UNREAL_RPC_PAYLOAD_RPC_PAYLOAD_ID); - // A bit hacky, we should probably include the number of bits with the data instead. - int64 CountBits = PayloadData.Num() * 8; + TWeakObjectPtr* ConnectionPtr = AuthorityPlayerControllerConnectionMap.Find(Op.entity_id); + if (ConnectionPtr == nullptr) + { + // Heartbeat component update on a PlayerController that this server does not have authority over. + // TODO: Disable component interest for Heartbeat components this server doesn't care about - UNR-986 + return; + } - ApplyRPC(TargetObject, Function, PayloadData, CountBits, SenderWorkerId); -} + if (!ConnectionPtr->IsValid()) + { + UE_LOG(LogSpatialReceiver, Warning, TEXT("Received heartbeat component update after NetConnection has been cleaned up. PlayerController entity: %lld"), Op.entity_id); + AuthorityPlayerControllerConnectionMap.Remove(Op.entity_id); + return; + } + + USpatialNetConnection* NetConnection = ConnectionPtr->Get(); + + Schema_Object* EventsObject = Schema_GetComponentUpdateEvents(Op.update.schema_type); + uint32 EventCount = Schema_GetObjectCount(EventsObject, SpatialConstants::HEARTBEAT_EVENT_ID); + if (EventCount > 0) + { + if (EventCount > 1) + { + UE_LOG(LogSpatialReceiver, Verbose, TEXT("Received multiple heartbeat events in a single component update, entity %lld."), Op.entity_id); + } + + NetConnection->OnHeartbeat(); + } + + Schema_Object* FieldsObject = Schema_GetComponentUpdateFields(Op.update.schema_type); + if (Schema_GetBoolCount(FieldsObject, SpatialConstants::HEARTBEAT_CLIENT_HAS_QUIT_ID) > 0 && + GetBoolFromSchema(FieldsObject, SpatialConstants::HEARTBEAT_CLIENT_HAS_QUIT_ID)) + { + // Client has disconnected, let's clean up their connection. + NetConnection->CleanUp(); + AuthorityPlayerControllerConnectionMap.Remove(Op.entity_id); + } +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp index 9d56c04a57..645ed83f40 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialSender.cpp @@ -11,19 +11,25 @@ #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" #include "Interop/Connection/SpatialWorkerConnection.h" -#include "Interop/SpatialReceiver.h" #include "Interop/SpatialDispatcher.h" +#include "Interop/SpatialReceiver.h" +#include "Schema/AlwaysRelevant.h" +#include "Schema/ClientRPCEndpoint.h" #include "Schema/Heartbeat.h" #include "Schema/Interest.h" +#include "Schema/RPCPayload.h" +#include "Schema/ServerRPCEndpoint.h" #include "Schema/Singleton.h" #include "Schema/SpawnData.h" #include "Schema/StandardLibrary.h" #include "Schema/UnrealMetadata.h" #include "SpatialConstants.h" +#include "Utils/ActorGroupManager.h" #include "Utils/ComponentFactory.h" #include "Utils/InterestFactory.h" #include "Utils/RepLayoutUtils.h" #include "Utils/SpatialActorUtils.h" +#include "Utils/SpatialMetrics.h" DEFINE_LOG_CATEGORY(LogSpatialSender); @@ -33,30 +39,26 @@ DECLARE_CYCLE_STAT(TEXT("SendComponentUpdates"), STAT_SpatialSenderSendComponent DECLARE_CYCLE_STAT(TEXT("ResetOutgoingUpdate"), STAT_SpatialSenderResetOutgoingUpdate, STATGROUP_SpatialNet); DECLARE_CYCLE_STAT(TEXT("QueueOutgoingUpdate"), STAT_SpatialSenderQueueOutgoingUpdate, STATGROUP_SpatialNet); -FPendingRPCParams::FPendingRPCParams(UObject* InTargetObject, UFunction* InFunction, void* InParameters, int InRetryIndex) +FReliableRPCForRetry::FReliableRPCForRetry(UObject* InTargetObject, UFunction* InFunction, Worker_ComponentId InComponentId, Schema_FieldId InRPCIndex, const TArray& InPayload, int InRetryIndex) : TargetObject(InTargetObject) , Function(InFunction) - , Attempts(0) + , ComponentId(InComponentId) + , RPCIndex(InRPCIndex) + , Payload(InPayload) + , Attempts(1) , RetryIndex(InRetryIndex) { - Parameters.SetNumZeroed(Function->ParmsSize); - - for (TFieldIterator It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It) - { - It->InitializeValue_InContainer(Parameters.GetData()); - It->CopyCompleteValue_InContainer(Parameters.GetData(), InParameters); - } } -FPendingRPCParams::~FPendingRPCParams() +FPendingRPC::FPendingRPC(FPendingRPC&& Other) + : Offset(Other.Offset) + , Index(Other.Index) + , Data(MoveTemp(Other.Data)) + , Entity(Other.Entity) { - for (TFieldIterator It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It) - { - It->DestroyValue_InContainer(Parameters.GetData()); - } } -void USpatialSender::Init(USpatialNetDriver* InNetDriver) +void USpatialSender::Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimerManager) { NetDriver = InNetDriver; StaticComponentView = InNetDriver->StaticComponentView; @@ -64,6 +66,8 @@ void USpatialSender::Init(USpatialNetDriver* InNetDriver) Receiver = InNetDriver->Receiver; PackageMap = InNetDriver->PackageMap; ClassInfoManager = InNetDriver->ClassInfoManager; + ActorGroupManager = InNetDriver->ActorGroupManager; + TimerManager = InTimerManager; } Worker_RequestId USpatialSender::CreateEntity(USpatialActorChannel* Channel) @@ -73,46 +77,68 @@ Worker_RequestId USpatialSender::CreateEntity(USpatialActorChannel* Channel) FString ClientWorkerAttribute = GetOwnerWorkerAttribute(Actor); - WorkerAttributeSet ServerAttribute = { SpatialConstants::ServerWorkerType }; - WorkerAttributeSet ClientAttribute = { SpatialConstants::ClientWorkerType }; - WorkerAttributeSet OwningClientAttribute = { ClientWorkerAttribute }; + WorkerRequirementSet AnyServerRequirementSet; + WorkerRequirementSet AnyServerOrClientRequirementSet = { SpatialConstants::UnrealClientAttributeSet }; - WorkerRequirementSet ServersOnly = { ServerAttribute }; - WorkerRequirementSet ClientsOnly = { ClientAttribute }; - WorkerRequirementSet OwningClientOnly = { OwningClientAttribute }; + WorkerAttributeSet OwningClientAttributeSet = { ClientWorkerAttribute }; + + WorkerRequirementSet AnyServerOrOwningClientRequirementSet = { OwningClientAttributeSet }; + WorkerRequirementSet OwningClientOnlyRequirementSet = { OwningClientAttributeSet }; + + for (const FName& WorkerType : GetDefault()->ServerWorkerTypes) + { + WorkerAttributeSet ServerWorkerAttributeSet = { WorkerType.ToString() }; - WorkerRequirementSet AnyUnrealServerOrClient = { ServerAttribute, ClientAttribute }; - WorkerRequirementSet AnyUnrealServerOrOwningClient = { ServerAttribute, OwningClientAttribute }; + AnyServerRequirementSet.Add(ServerWorkerAttributeSet); + AnyServerOrClientRequirementSet.Add(ServerWorkerAttributeSet); + AnyServerOrOwningClientRequirementSet.Add(ServerWorkerAttributeSet); + } WorkerRequirementSet ReadAcl; if (Class->HasAnySpatialClassFlags(SPATIALCLASS_ServerOnly)) { - ReadAcl = ServersOnly; + ReadAcl = AnyServerRequirementSet; } else if (Actor->IsA()) { - ReadAcl = AnyUnrealServerOrOwningClient; + ReadAcl = AnyServerOrOwningClientRequirementSet; } else { - ReadAcl = AnyUnrealServerOrClient; + ReadAcl = AnyServerOrClientRequirementSet; } const FClassInfo& Info = ClassInfoManager->GetOrCreateClassInfoByClass(Class); + const WorkerAttributeSet WorkerAttribute{ Info.WorkerType.ToString() }; + const WorkerRequirementSet AuthoritativeWorkerRequirementSet = { WorkerAttribute }; + WriteAclMap ComponentWriteAcl; - ComponentWriteAcl.Add(SpatialConstants::POSITION_COMPONENT_ID, ServersOnly); - ComponentWriteAcl.Add(SpatialConstants::INTEREST_COMPONENT_ID, ServersOnly); - ComponentWriteAcl.Add(SpatialConstants::SPAWN_DATA_COMPONENT_ID, ServersOnly); - ComponentWriteAcl.Add(SpatialConstants::ENTITY_ACL_COMPONENT_ID, ServersOnly); - ComponentWriteAcl.Add(SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID, ServersOnly); - ComponentWriteAcl.Add(SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID, ServersOnly); - ComponentWriteAcl.Add(SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID, OwningClientOnly); + ComponentWriteAcl.Add(SpatialConstants::POSITION_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + ComponentWriteAcl.Add(SpatialConstants::INTEREST_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + ComponentWriteAcl.Add(SpatialConstants::SPAWN_DATA_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + ComponentWriteAcl.Add(SpatialConstants::ENTITY_ACL_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + ComponentWriteAcl.Add(SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + ComponentWriteAcl.Add(SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + ComponentWriteAcl.Add(SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID, OwningClientOnlyRequirementSet); + + // If there are pending RPCs, add this component. + if (OutgoingOnCreateEntityRPCs.Contains(Actor)) + { + ComponentWriteAcl.Add(SpatialConstants::RPCS_ON_ENTITY_CREATION_ID, AuthoritativeWorkerRequirementSet); + } + + // If Actor is a PlayerController, add the heartbeat component. if (Actor->IsA()) { - ComponentWriteAcl.Add(SpatialConstants::HEARTBEAT_COMPONENT_ID, OwningClientOnly); +#if !UE_BUILD_SHIPPING + ComponentWriteAcl.Add(SpatialConstants::DEBUG_METRICS_COMPONENT_ID, AuthoritativeWorkerRequirementSet); +#endif // !UE_BUILD_SHIPPING + ComponentWriteAcl.Add(SpatialConstants::HEARTBEAT_COMPONENT_ID, OwningClientOnlyRequirementSet); } + ComponentWriteAcl.Add(SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID, AuthoritativeWorkerRequirementSet); + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) { Worker_ComponentId ComponentId = Info.SchemaComponents[Type]; @@ -121,7 +147,7 @@ Worker_RequestId USpatialSender::CreateEntity(USpatialActorChannel* Channel) return; } - ComponentWriteAcl.Add(ComponentId, ServersOnly); + ComponentWriteAcl.Add(ComponentId, AuthoritativeWorkerRequirementSet); }); for (auto& SubobjectInfoPair : Info.SubobjectInfo) @@ -143,7 +169,7 @@ Worker_RequestId USpatialSender::CreateEntity(USpatialActorChannel* Channel) return; } - ComponentWriteAcl.Add(ComponentId, ServersOnly); + ComponentWriteAcl.Add(ComponentId, AuthoritativeWorkerRequirementSet); }); } @@ -174,21 +200,37 @@ Worker_RequestId USpatialSender::CreateEntity(USpatialActorChannel* Channel) TArray ComponentDatas; ComponentDatas.Add(Position(Coordinates::FromFVector(Channel->GetActorSpatialPosition(Actor))).CreatePositionData()); ComponentDatas.Add(Metadata(Class->GetName()).CreateMetadataData()); - ComponentDatas.Add(EntityAcl(ReadAcl, ComponentWriteAcl).CreateEntityAclData()); ComponentDatas.Add(Persistence().CreatePersistenceData()); ComponentDatas.Add(SpawnData(Actor).CreateSpawnDataData()); ComponentDatas.Add(UnrealMetadata(StablyNamedObjectRef, ClientWorkerAttribute, Class->GetPathName(), bNetStartup).CreateUnrealMetadataData()); + if (RPCsOnEntityCreation* QueuedRPCs = OutgoingOnCreateEntityRPCs.Find(Actor)) + { + if (QueuedRPCs->HasRPCPayloadData()) + { + ComponentDatas.Add(QueuedRPCs->CreateRPCPayloadData()); + } + OutgoingOnCreateEntityRPCs.Remove(Actor); + } + if (Class->HasAnySpatialClassFlags(SPATIALCLASS_Singleton)) { ComponentDatas.Add(Singleton().CreateSingletonData()); } + if (Actor->bAlwaysRelevant) + { + ComponentDatas.Add(AlwaysRelevant().CreateData()); + } + // If the Actor was loaded rather than dynamically spawned, associate it with its owning sublevel. ComponentDatas.Add(CreateLevelComponentData(Actor)); if (Actor->IsA()) { +#if !UE_BUILD_SHIPPING + ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::DEBUG_METRICS_COMPONENT_ID)); +#endif // !UE_BUILD_SHIPPING ComponentDatas.Add(Heartbeat().CreateHeartbeatData()); } @@ -215,42 +257,114 @@ Worker_RequestId USpatialSender::CreateEntity(USpatialActorChannel* Channel) InterestFactory InterestDataFactory(Actor, Info, NetDriver); ComponentDatas.Add(InterestDataFactory.CreateInterestData()); - ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID)); - ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID)); + ComponentDatas.Add(ClientRPCEndpoint().CreateRPCEndpointData()); + ComponentDatas.Add(ServerRPCEndpoint().CreateRPCEndpointData()); ComponentDatas.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID)); + // Only add subobjects which are replicating + for (auto RepSubobject = Channel->ReplicationMap.CreateIterator(); RepSubobject; ++RepSubobject) + { +#if ENGINE_MINOR_VERSION <= 20 + if (UObject* Subobject = RepSubobject.Key().Get()) +#else + if (UObject* Subobject = RepSubobject.Value()->GetWeakObjectPtr().Get()) +#endif + { + if (Subobject == Actor) + { + // Actor's replicator is also contained in ReplicationMap. + continue; + } + + // If this object is not in the PackageMap, it has been dynamically created. + if (!PackageMap->GetUnrealObjectRefFromObject(Subobject).IsValid()) + { + const FClassInfo* SubobjectInfo = Channel->TryResolveNewDynamicSubobjectAndGetClassInfo(Subobject); + + if (SubobjectInfo == nullptr) + { + // This is a failure but there is already a log inside TryResolveNewDynamicSubbojectAndGetClassInfo + continue; + } + + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) + { + if (SubobjectInfo->SchemaComponents[Type] != SpatialConstants::INVALID_COMPONENT_ID) + { + ComponentWriteAcl.Add(SubobjectInfo->SchemaComponents[Type], AuthoritativeWorkerRequirementSet); + } + }); + } + + const FClassInfo& SubobjectInfo = ClassInfoManager->GetOrCreateClassInfoByObject(Subobject); + + FRepChangeState SubobjectRepChanges = Channel->CreateInitialRepChangeState(Subobject); + FHandoverChangeState SubobjectHandoverChanges = Channel->CreateInitialHandoverChangeState(SubobjectInfo); + + // Reset unresolved objects so they can be filled again by DataFactory + UnresolvedObjectsMap.Empty(); + HandoverUnresolvedObjectsMap.Empty(); + + TArray ActorSubobjectDatas = DataFactory.CreateComponentDatas(Subobject, SubobjectInfo, SubobjectRepChanges, SubobjectHandoverChanges); + ComponentDatas.Append(ActorSubobjectDatas); + + for (auto& HandleUnresolvedObjectsPair : UnresolvedObjectsMap) + { + QueueOutgoingUpdate(Channel, Subobject, HandleUnresolvedObjectsPair.Key, HandleUnresolvedObjectsPair.Value, /* bIsHandover */ false); + } + + for (auto& HandleUnresolvedObjectsPair : HandoverUnresolvedObjectsMap) + { + QueueOutgoingUpdate(Channel, Subobject, HandleUnresolvedObjectsPair.Key, HandleUnresolvedObjectsPair.Value, /* bIsHandover */ true); + } + } + } + + // Or if the subobject has handover properties, add it as well. + // NOTE: this is only for subobjects that are a part of the CDO. + // NOT dynamic subobjects which have been added before entity creation. for (auto& SubobjectInfoPair : Info.SubobjectInfo) { - FClassInfo& SubobjectInfo = SubobjectInfoPair.Value.Get(); + const FClassInfo& SubobjectInfo = SubobjectInfoPair.Value.Get(); - TWeakObjectPtr Subobject = PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(Channel->GetEntityId(), SubobjectInfoPair.Key)); - if (!Subobject.IsValid()) + // Static subobjects aren't guaranteed to exist on actor instances, check they are present before adding write acls + TWeakObjectPtr WeakSubobject = PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(Channel->GetEntityId(), SubobjectInfoPair.Key)); + if (!WeakSubobject.IsValid()) + { + continue; + } + + UObject* Subobject = WeakSubobject.Get(); + + if (SubobjectInfo.SchemaComponents[SCHEMA_Handover] == SpatialConstants::INVALID_COMPONENT_ID) + { + continue; + } + + // If it contains it, we've already created handover data for it. + if (Channel->ReplicationMap.Contains(Subobject)) { - UE_LOG(LogSpatialSender, Warning, TEXT("Tried to generate initial replication state for an invalid sub-object (class %s, sub-object %s, actor %s). Object may have been deleted or is PendingKill."), *SubobjectInfo.Class->GetName(), *SubobjectInfo.SubobjectName.ToString(), *Actor->GetName()); continue; } - FRepChangeState SubobjectRepChanges = Channel->CreateInitialRepChangeState(Subobject); FHandoverChangeState SubobjectHandoverChanges = Channel->CreateInitialHandoverChangeState(SubobjectInfo); // Reset unresolved objects so they can be filled again by DataFactory - UnresolvedObjectsMap.Empty(); HandoverUnresolvedObjectsMap.Empty(); - TArray ActorSubobjectDatas = DataFactory.CreateComponentDatas(Subobject.Get(), SubobjectInfo, SubobjectRepChanges, SubobjectHandoverChanges); - ComponentDatas.Append(ActorSubobjectDatas); + Worker_ComponentData SubobjectHandoverData = DataFactory.CreateHandoverComponentData(SubobjectInfo.SchemaComponents[SCHEMA_Handover], Subobject, SubobjectInfo, SubobjectHandoverChanges); + ComponentDatas.Add(SubobjectHandoverData); - for (auto& HandleUnresolvedObjectsPair : UnresolvedObjectsMap) - { - QueueOutgoingUpdate(Channel, Subobject.Get(), HandleUnresolvedObjectsPair.Key, HandleUnresolvedObjectsPair.Value, /* bIsHandover */ false); - } + ComponentWriteAcl.Add(SubobjectInfo.SchemaComponents[SCHEMA_Handover], AuthoritativeWorkerRequirementSet); for (auto& HandleUnresolvedObjectsPair : HandoverUnresolvedObjectsMap) { - QueueOutgoingUpdate(Channel, Subobject.Get(), HandleUnresolvedObjectsPair.Key, HandleUnresolvedObjectsPair.Value, /* bIsHandover */ true); + QueueOutgoingUpdate(Channel, Subobject, HandleUnresolvedObjectsPair.Key, HandleUnresolvedObjectsPair.Value, /* bIsHandover */ true); } } + ComponentDatas.Add(EntityAcl(ReadAcl, ComponentWriteAcl).CreateEntityAclData()); + Worker_EntityId EntityId = Channel->GetEntityId(); Worker_RequestId CreateEntityRequestId = Connection->SendCreateEntityRequest(MoveTemp(ComponentDatas), &EntityId); PendingActorRequests.Add(CreateEntityRequestId, Channel); @@ -263,7 +377,7 @@ Worker_ComponentData USpatialSender::CreateLevelComponentData(AActor* Actor) UWorld* ActorWorld = Actor->GetTypedOuter(); if (ActorWorld != NetDriver->World) { - const uint32 ComponentId = ClassInfoManager->SchemaDatabase->GetComponentIdFromLevelPath(ActorWorld->GetOuter()->GetPathName()); + const uint32 ComponentId = ClassInfoManager->GetComponentIdFromLevelPath(ActorWorld->GetOuter()->GetPathName()); if (ComponentId != SpatialConstants::INVALID_COMPONENT_ID) { return ComponentFactory::CreateEmptyComponentData(ComponentId); @@ -278,6 +392,157 @@ Worker_ComponentData USpatialSender::CreateLevelComponentData(AActor* Actor) return ComponentFactory::CreateEmptyComponentData(SpatialConstants::NOT_STREAMED_COMPONENT_ID); } +void USpatialSender::SendAddComponent(USpatialActorChannel* Channel, UObject* Subobject, const FClassInfo& SubobjectInfo) +{ + FRepChangeState SubobjectRepChanges = Channel->CreateInitialRepChangeState(Subobject); + FHandoverChangeState SubobjectHandoverChanges = Channel->CreateInitialHandoverChangeState(SubobjectInfo); + + FUnresolvedObjectsMap UnresolvedObjectsMap; + FUnresolvedObjectsMap HandoverUnresolvedObjectsMap; + ComponentFactory DataFactory(UnresolvedObjectsMap, HandoverUnresolvedObjectsMap, false, NetDriver); + + TArray SubobjectDatas = DataFactory.CreateComponentDatas(Subobject, SubobjectInfo, SubobjectRepChanges, SubobjectHandoverChanges); + + for (auto& HandleUnresolvedObjectsPair : UnresolvedObjectsMap) + { + QueueOutgoingUpdate(Channel, Subobject, HandleUnresolvedObjectsPair.Key, HandleUnresolvedObjectsPair.Value, /* bIsHandover */ false); + } + + for (auto& HandleUnresolvedObjectsPair : HandoverUnresolvedObjectsMap) + { + QueueOutgoingUpdate(Channel, Subobject, HandleUnresolvedObjectsPair.Key, HandleUnresolvedObjectsPair.Value, /* bIsHandover */ true); + } + + for (Worker_ComponentData& ComponentData : SubobjectDatas) + { + Connection->SendAddComponent(Channel->GetEntityId(), &ComponentData); + } + + Channel->PendingDynamicSubobjects.Remove(TWeakObjectPtr(Subobject)); +} + +void USpatialSender::GainAuthorityThenAddComponent(USpatialActorChannel* Channel, UObject* Object, const FClassInfo* Info) +{ + const FClassInfo& ActorInfo = ClassInfoManager->GetOrCreateClassInfoByClass(Channel->Actor->GetClass()); + const WorkerAttributeSet WorkerAttribute{ ActorInfo.WorkerType.ToString() }; + const WorkerRequirementSet AuthoritativeWorkerRequirementSet = { WorkerAttribute }; + + EntityAcl* EntityACL = StaticComponentView->GetComponentData(Channel->GetEntityId()); + + TSharedRef PendingSubobjectAttachment = MakeShared(); + PendingSubobjectAttachment->Subobject = Object; + PendingSubobjectAttachment->Channel = Channel; + PendingSubobjectAttachment->Info = Info; + + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) + { + Worker_ComponentId ComponentId = Info->SchemaComponents[Type]; + if (ComponentId != SpatialConstants::INVALID_COMPONENT_ID) + { + // For each valid ComponentId, we need to wait for its authority delegation before + // adding the subobject. + PendingSubobjectAttachment->PendingAuthorityDelegations.Add(ComponentId); + Receiver->PendingEntitySubobjectDelegations.Add( + MakeTuple(static_cast(Channel->GetEntityId()), ComponentId), + PendingSubobjectAttachment); + + EntityACL->ComponentWriteAcl.Add(Info->SchemaComponents[Type], AuthoritativeWorkerRequirementSet); + } + }); + + Worker_ComponentUpdate Update = EntityACL->CreateEntityAclUpdate(); + Connection->SendComponentUpdate(Channel->GetEntityId(), &Update); +} + +void USpatialSender::SendRemoveComponent(Worker_EntityId EntityId, const FClassInfo& Info) +{ + for (Worker_ComponentId SubobjectComponentId : Info.SchemaComponents) + { + if (SubobjectComponentId != SpatialConstants::INVALID_COMPONENT_ID) + { + NetDriver->Connection->SendRemoveComponent(EntityId, SubobjectComponentId); + } + } + + PackageMap->RemoveSubobject(FUnrealObjectRef(EntityId, Info.SchemaComponents[SCHEMA_Data])); +} + +// Creates an entity authoritative on this server worker, ensuring it will be able to receive updates for the GSM. +void USpatialSender::CreateServerWorkerEntity(int AttemptCounter) +{ + const WorkerRequirementSet WorkerIdPermission{ { FString::Format(TEXT("workerId:{0}"), { Connection->GetWorkerId() }) } }; + + WriteAclMap ComponentWriteAcl; + ComponentWriteAcl.Add(SpatialConstants::POSITION_COMPONENT_ID, WorkerIdPermission); + ComponentWriteAcl.Add(SpatialConstants::METADATA_COMPONENT_ID, WorkerIdPermission); + ComponentWriteAcl.Add(SpatialConstants::ENTITY_ACL_COMPONENT_ID, WorkerIdPermission); + ComponentWriteAcl.Add(SpatialConstants::INTEREST_COMPONENT_ID, WorkerIdPermission); + + QueryConstraint Constraint; + // Ensure server worker receives the GSM entity + Constraint.EntityIdConstraint = SpatialConstants::INITIAL_GLOBAL_STATE_MANAGER_ENTITY_ID; + + Query Query; + Query.Constraint = Constraint; + Query.FullSnapshotResult = true; + + ComponentInterest Queries; + Queries.Queries.Add(Query); + + Interest Interest; + Interest.ComponentInterestMap.Add(SpatialConstants::POSITION_COMPONENT_ID, Queries); + + TArray Components; + Components.Add(Position().CreatePositionData()); + Components.Add(Metadata(FString::Format(TEXT("WorkerEntity:{0}"), { Connection->GetWorkerId() })).CreateMetadataData()); + Components.Add(EntityAcl(WorkerIdPermission, ComponentWriteAcl).CreateEntityAclData()); + Components.Add(Interest.CreateInterestData()); + + Worker_RequestId RequestId = Connection->SendCreateEntityRequest(MoveTemp(Components), nullptr); + + CreateEntityDelegate OnCreateWorkerEntityResponse; + OnCreateWorkerEntityResponse.BindLambda([WeakSender = TWeakObjectPtr(this), AttemptCounter](const Worker_CreateEntityResponseOp& Op) + { + if (!WeakSender.IsValid()) + { + return; + } + USpatialSender* Sender = WeakSender.Get(); + + if (Op.status_code == WORKER_STATUS_CODE_SUCCESS) + { + Sender->NetDriver->WorkerEntityId = Op.entity_id; + return; + } + + if (Op.status_code != WORKER_STATUS_CODE_TIMEOUT) + { + UE_LOG(LogSpatialSender, Error, TEXT("Worker entity creation request failed: \"%s\""), + UTF8_TO_TCHAR(Op.message)); + return; + } + + if (AttemptCounter == SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS) + { + UE_LOG(LogSpatialSender, Error, TEXT("Worker entity creation request timed out too many times. (%u attempts)"), + SpatialConstants::MAX_NUMBER_COMMAND_ATTEMPTS); + return; + } + + UE_LOG(LogSpatialSender, Warning, TEXT("Worker entity creation request timed out and will retry.")); + FTimerHandle RetryTimer; + Sender->TimerManager->SetTimer(RetryTimer, [WeakSender, AttemptCounter]() + { + if (USpatialSender* Sender = WeakSender.Get()) + { + Sender->CreateServerWorkerEntity(AttemptCounter + 1); + } + }, SpatialConstants::GetCommandRetryWaitTimeSeconds(AttemptCounter), false); + }); + + Receiver->AddCreateEntityDelegate(RequestId, OnCreateWorkerEntityResponse); +} + void USpatialSender::SendComponentUpdates(UObject* Object, const FClassInfo& Info, USpatialActorChannel* Channel, const FRepChangeState* RepChanges, const FHandoverChangeState* HandoverChanges) { SCOPE_CYCLE_COUNTER(STAT_SpatialSenderSendComponentUpdates); @@ -351,6 +616,43 @@ void USpatialSender::ProcessUpdatesQueuedUntilAuthority(Worker_EntityId EntityId } } +void USpatialSender::FlushPackedRPCs() +{ + if (RPCsToPack.Num() == 0) + { + return; + } + + // TODO: This could be further optimized for the case when there's only 1 RPC to be sent during this frame + // by sending it directly to the corresponding entity, without including the EntityId in the payload - UNR-1563. + for (const auto& It : RPCsToPack) + { + Worker_EntityId PlayerControllerEntityId = It.Key; + const TArray& PendingRPCArray = It.Value; + + Worker_ComponentUpdate ComponentUpdate = {}; + + Worker_ComponentId ComponentId = NetDriver->IsServer() ? SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID : SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID; + ComponentUpdate.component_id = ComponentId; + ComponentUpdate.schema_type = Schema_CreateComponentUpdate(ComponentId); + Schema_Object* EventsObject = Schema_GetComponentUpdateEvents(ComponentUpdate.schema_type); + + for (const FPendingRPC& RPC : PendingRPCArray) + { + Schema_Object* EventData = Schema_AddObject(EventsObject, SpatialConstants::UNREAL_RPC_ENDPOINT_PACKED_EVENT_ID); + + Schema_AddUint32(EventData, SpatialConstants::UNREAL_RPC_PAYLOAD_OFFSET_ID, RPC.Offset); + Schema_AddUint32(EventData, SpatialConstants::UNREAL_RPC_PAYLOAD_RPC_INDEX_ID, RPC.Index); + SpatialGDK::AddBytesToSchema(EventData, SpatialConstants::UNREAL_RPC_PAYLOAD_RPC_PAYLOAD_ID, RPC.Data.GetData(), RPC.Data.Num()); + Schema_AddEntityId(EventData, SpatialConstants::UNREAL_PACKED_RPC_PAYLOAD_ENTITY_ID, RPC.Entity); + } + + Connection->SendComponentUpdate(PlayerControllerEntityId, &ComponentUpdate); + } + + RPCsToPack.Empty(); +} + void FillComponentInterests(const FClassInfo& Info, bool bNetOwned, TArray& ComponentInterest) { if (Info.SchemaComponents[SCHEMA_OwnerOnly] != SpatialConstants::INVALID_COMPONENT_ID) @@ -366,16 +668,24 @@ void FillComponentInterests(const FClassInfo& Info, bool bNetOwned, TArray USpatialSender::CreateComponentInterest(AActor* Actor, bool bIsNetOwned) +TArray USpatialSender::CreateComponentInterestForActor(USpatialActorChannel* Channel, bool bIsNetOwned) { TArray ComponentInterest; - const FClassInfo& ActorInfo = ClassInfoManager->GetOrCreateClassInfoByClass(Actor->GetClass()); + const FClassInfo& ActorInfo = ClassInfoManager->GetOrCreateClassInfoByClass(Channel->Actor->GetClass()); FillComponentInterests(ActorInfo, bIsNetOwned, ComponentInterest); + // Statically attached subobjects for (auto& SubobjectInfoPair : ActorInfo.SubobjectInfo) { - FClassInfo& SubobjectInfo = SubobjectInfoPair.Value.Get(); + 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); } @@ -385,11 +695,39 @@ TArray USpatialSender::CreateComponentInterest(AActor* return ComponentInterest; } -void USpatialSender::SendComponentInterest(AActor* Actor, Worker_EntityId EntityId, bool bNetOwned) +RPCPayload USpatialSender::CreateRPCPayloadFromParams(UObject* TargetObject, UFunction* Function, int ReliableRPCIndex, void* Params, TSet>& UnresolvedObjects) +{ + const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); + + FUnrealObjectRef TargetObjectRef(PackageMap->GetUnrealObjectRefFromNetGUID(PackageMap->GetNetGUIDFromObject(TargetObject))); + if (TargetObjectRef == FUnrealObjectRef::UNRESOLVED_OBJECT_REF) + { + UnresolvedObjects.Add(TargetObject); + } + + FSpatialNetBitWriter PayloadWriter = PackRPCDataToSpatialNetBitWriter(Function, Params, ReliableRPCIndex, UnresolvedObjects); + if (UnresolvedObjects.Num() > 0) + { + UE_LOG(LogSpatialSender, Warning, TEXT("Some RPC parameters for %s were not resolved."), *Function->GetName()); + } + + return RPCPayload(TargetObjectRef.Offset, RPCInfo.Index, TArray(PayloadWriter.GetData(), PayloadWriter.GetNumBytes())); +} + +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::SendComponentInterestForSubobject(const FClassInfo& Info, Worker_EntityId EntityId, bool bNetOwned) { - check(!NetDriver->IsServer()); + checkf(!NetDriver->IsServer(), TEXT("Tried to set ComponentInterest on a server-worker. This should never happen!")); - NetDriver->Connection->SendComponentInterest(EntityId, CreateComponentInterest(Actor, bNetOwned)); + TArray ComponentInterest; + FillComponentInterests(Info, bNetOwned, ComponentInterest); + NetDriver->Connection->SendComponentInterest(EntityId, MoveTemp(ComponentInterest)); } void USpatialSender::SendPositionUpdate(Worker_EntityId EntityId, const FVector& Location) @@ -406,147 +744,248 @@ void USpatialSender::SendPositionUpdate(Worker_EntityId EntityId, const FVector& Connection->SendComponentUpdate(EntityId, &Update); } -void USpatialSender::SendRPC(TSharedRef Params) +bool USpatialSender::SendRPC(const FPendingRPCParams& Params) { - if (!Params->TargetObject.IsValid()) + TWeakObjectPtr TargetObjectWeakPtr = PackageMap->GetObjectFromUnrealObjectRef(Params.ObjectRef); + if (!TargetObjectWeakPtr.IsValid()) { // Target object was destroyed before the RPC could be (re)sent - return; + return false; } + UObject* TargetObject = TargetObjectWeakPtr.Get(); + + USpatialActorChannel* Channel = NetDriver->GetOrCreateSpatialActorChannel(TargetObject); - UObject* TargetObject = Params->TargetObject.Get(); - if (PackageMap->GetUnrealObjectRefFromObject(TargetObject) == FUnrealObjectRef::UNRESOLVED_OBJECT_REF) + if (!Channel) { - UE_LOG(LogSpatialSender, Verbose, TEXT("Trying to send RPC %s on unresolved Actor %s."), *Params->Function->GetName(), *TargetObject->GetName()); - QueueOutgoingRPC(TargetObject, Params); - return; + UE_LOG(LogSpatialSender, Warning, TEXT("Failed to create an Actor Channel for %s."), *TargetObject->GetName()); + return false; } - const FClassInfo& Info = ClassInfoManager->GetOrCreateClassInfoByObject(TargetObject); - const FRPCInfo* RPCInfo = Info.RPCInfoMap.Find(Params->Function); + const FClassInfo& ClassInfo = ClassInfoManager->GetOrCreateClassInfoByObject(TargetObject); + UFunction* Function = ClassInfo.RPCs[Params.Payload.Index]; + + const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); - // We potentially have a parent function and need to find the child function. - // This exists as it's possible in blueprints to explicitly call the parent function. - if (RPCInfo == nullptr) + if (Channel->bCreatingNewEntity) { - for (auto It = Info.RPCInfoMap.CreateConstIterator(); It; ++It) + if (Function->HasAnyFunctionFlags(FUNC_NetClient | FUNC_NetMulticast)) { - if (It.Key()->GetName() == Params->Function->GetName()) + if (Function->HasAnyFunctionFlags(FUNC_NetMulticast)) { - // Matching child function found. Use this for the remote function call. - RPCInfo = &It.Value(); - break; + // TODO: UNR-1437 - Add Support for Multicast RPCs on Entity Creation + UE_LOG(LogSpatialSender, Warning, TEXT("NetMulticast RPC %s triggered on Object %s too close to initial creation."), *Function->GetName(), *TargetObject->GetName()); } + check(NetDriver->IsServer()); + + OutgoingOnCreateEntityRPCs.FindOrAdd(TargetObject).RPCs.Add(Params.Payload); +#if !UE_BUILD_SHIPPING + NetDriver->SpatialMetrics->TrackSentRPC(Function, RPCInfo.Type, Params.Payload.PayloadData.Num()); +#endif // !UE_BUILD_SHIPPING + return true; + } + else + { + UE_LOG(LogSpatialSender, Warning, TEXT("CrossServer RPC %s triggered on Object %s too close to initial creation."), *Function->GetName(), *TargetObject->GetName()); } } - check(RPCInfo); - Worker_EntityId EntityId = SpatialConstants::INVALID_ENTITY_ID; - const UObject* UnresolvedObject = nullptr; - switch (RPCInfo->Type) + switch (RPCInfo.Type) { - case SCHEMA_ClientReliableRPC: - case SCHEMA_ServerReliableRPC: case SCHEMA_CrossServerRPC: { - int ReliableRPCIndex = 0; -#if !UE_BUILD_SHIPPING - ReliableRPCIndex = Params->ReliableRPCIndex; -#endif // !UE_BUILD_SHIPPING - - Worker_ComponentId ComponentId = RPCInfo->Type == SCHEMA_ClientReliableRPC ? SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID : SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID; + Worker_ComponentId ComponentId = SchemaComponentTypeToWorkerComponentId(RPCInfo.Type); - Worker_CommandRequest CommandRequest = CreateRPCCommandRequest(TargetObject, Params->Function, Params->Parameters.GetData(), ComponentId, RPCInfo->Index, EntityId, UnresolvedObject, ReliableRPCIndex); + const UObject* UnresolvedObject = nullptr; + Worker_CommandRequest CommandRequest = CreateRPCCommandRequest(TargetObject, Params.Payload, ComponentId, RPCInfo.Index, EntityId, UnresolvedObject); - if (!UnresolvedObject) + if (UnresolvedObject) { - check(EntityId != SpatialConstants::INVALID_ENTITY_ID); - Worker_RequestId RequestId = Connection->SendCommandRequest(EntityId, &CommandRequest, RPCInfo->Index + 1); + return false; + } - if (Params->Function->HasAnyFunctionFlags(FUNC_NetReliable)) - { - UE_LOG(LogSpatialSender, Verbose, TEXT("Sending reliable command request (entity: %lld, component: %d, function: %s, attempt: %d)"), - EntityId, CommandRequest.component_id, *Params->Function->GetName(), Params->Attempts+1); - // The number of attempts is used to determine the delay in case the command times out and we need to resend it. - Params->Attempts++; - Receiver->AddPendingReliableRPC(RequestId, Params); - } - else - { - UE_LOG(LogSpatialSender, Verbose, TEXT("Sending unreliable command request (entity: %lld, component: %d, function: %s)"), - EntityId, CommandRequest.component_id, *Params->Function->GetName()); - } + check(EntityId != SpatialConstants::INVALID_ENTITY_ID); + Worker_RequestId RequestId = Connection->SendCommandRequest(EntityId, &CommandRequest, SpatialConstants::UNREAL_RPC_ENDPOINT_COMMAND_ID); + +#if !UE_BUILD_SHIPPING + NetDriver->SpatialMetrics->TrackSentRPC(Function, RPCInfo.Type, Params.Payload.PayloadData.Num()); +#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, Params.Payload.PayloadData, 0)); } - break; + else + { + UE_LOG(LogSpatialSender, Verbose, TEXT("Sending unreliable command request (entity: %lld, component: %d, function: %s)"), + EntityId, CommandRequest.component_id, *Function->GetName()); + } + + return true; } case SCHEMA_NetMulticastRPC: + case SCHEMA_ClientReliableRPC: + case SCHEMA_ServerReliableRPC: case SCHEMA_ClientUnreliableRPC: case SCHEMA_ServerUnreliableRPC: { - Worker_ComponentId ComponentId; - if (RPCInfo->Type == SCHEMA_ClientUnreliableRPC) + FUnrealObjectRef TargetObjectRef = PackageMap->GetUnrealObjectRefFromObject(TargetObject); + if (TargetObjectRef == FUnrealObjectRef::UNRESOLVED_OBJECT_REF) { - ComponentId = SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID; + return false; } - else if (RPCInfo->Type == SCHEMA_ServerUnreliableRPC) + + if (RPCInfo.Type != SCHEMA_NetMulticastRPC && !Channel->IsListening()) { - ComponentId = SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID; + // 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 false; } - else + + EntityId = TargetObjectRef.Entity; + check(EntityId != SpatialConstants::INVALID_ENTITY_ID); + + Worker_ComponentId ComponentId = SchemaComponentTypeToWorkerComponentId(RPCInfo.Type); + + bool bCanPackRPC = GetDefault()->bPackRPCs; + if (bCanPackRPC && RPCInfo.Type == SCHEMA_NetMulticastRPC) { - ComponentId = SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID; + bCanPackRPC = false; } - Worker_ComponentUpdate ComponentUpdate = CreateUnreliableRPCUpdate(TargetObject, Params->Function, Params->Parameters.GetData(), ComponentId, RPCInfo->Index, EntityId, UnresolvedObject); + if (bCanPackRPC && GetDefault()->bEnableOffloading) + { + if (const AActor* TargetActor = Cast(PackageMap->GetObjectFromEntityId(TargetObjectRef.Entity).Get())) + { + if (const UNetConnection* OwningConnection = TargetActor->GetNetConnection()) + { + if (const AActor* ConnectionOwner = OwningConnection->OwningActor) + { + if (!ActorGroupManager->IsSameWorkerType(TargetActor, ConnectionOwner)) + { + UE_LOG(LogSpatialSender, Verbose, TEXT("RPC %s Cannot be packed as TargetActor (%s) and Connection Owner (%s) are on different worker types."), + *Function->GetName(), + *TargetActor->GetName(), + *ConnectionOwner->GetName() + ) + bCanPackRPC = false; + } + } + } + } + } - if (!UnresolvedObject) + if (bCanPackRPC) + { + const UObject* UnresolvedObject = nullptr; + if (AddPendingRPC(TargetObject, Params, ComponentId, RPCInfo.Index, UnresolvedObject)) + { +#if !UE_BUILD_SHIPPING + NetDriver->SpatialMetrics->TrackSentRPC(Function, RPCInfo.Type, Params.Payload.PayloadData.Num()); +#endif // !UE_BUILD_SHIPPING + return true; + } + else + { + return false; + } + } + else { - check(EntityId != SpatialConstants::INVALID_ENTITY_ID); + if (!NetDriver->StaticComponentView->HasAuthority(EntityId, ComponentId)) + { + return false; + } + + const UObject* UnresolvedParameter = nullptr; + Worker_ComponentUpdate ComponentUpdate = CreateRPCEventUpdate(TargetObject, Params.Payload, ComponentId, RPCInfo.Index, UnresolvedParameter); - if (!NetDriver->StaticComponentView->HasAuthority(EntityId, ComponentUpdate.component_id)) + if (UnresolvedParameter) { - UE_LOG(LogSpatialSender, Warning, TEXT("Trying to send MulticastRPC component update but don't have authority! Update will not be sent. Entity: %lld"), EntityId); - return; + return false; } Connection->SendComponentUpdate(EntityId, &ComponentUpdate); +#if !UE_BUILD_SHIPPING + NetDriver->SpatialMetrics->TrackSentRPC(Function, RPCInfo.Type, Params.Payload.PayloadData.Num()); +#endif // !UE_BUILD_SHIPPING + return true; } - break; } default: checkNoEntry(); - break; - } - - if (UnresolvedObject) - { - QueueOutgoingRPC(UnresolvedObject, Params); + return false; } } -void USpatialSender::EnqueueRetryRPC(TSharedRef Params) +void USpatialSender::EnqueueRetryRPC(TSharedRef RetryRPC) { - RetryRPCs.Add(Params); + RetryRPCs.Add(RetryRPC); } void USpatialSender::FlushRetryRPCs() { // Retried RPCs are sorted by their index. - RetryRPCs.Sort([](const TSharedPtr& A, const TSharedPtr& B) { return A->RetryIndex < B->RetryIndex; }); + RetryRPCs.Sort([](const TSharedRef& A, const TSharedRef& B) { return A->RetryIndex < B->RetryIndex; }); for (auto& RetryRPC : RetryRPCs) { - SendRPC(RetryRPC); + RetryReliableRPC(RetryRPC); } RetryRPCs.Empty(); } -void USpatialSender::SendCreateEntityRequest(USpatialActorChannel* Channel) +void USpatialSender::RetryReliableRPC(TSharedRef RetryRPC) { - UE_LOG(LogSpatialSender, Log, TEXT("Sending create entity request for %s"), *Channel->Actor->GetName()); + if (!RetryRPC->TargetObject.IsValid()) + { + // Target object was destroyed before the RPC could be (re)sent + return; + } + + UObject* TargetObject = RetryRPC->TargetObject.Get(); + FUnrealObjectRef TargetObjectRef = PackageMap->GetUnrealObjectRefFromObject(TargetObject); + if (TargetObjectRef == FUnrealObjectRef::UNRESOLVED_OBJECT_REF) + { + UE_LOG(LogSpatialSender, Warning, TEXT("Actor %s got unresolved (?) before RPC %s could be retried. This RPC will not be sent."), *TargetObject->GetName(), *RetryRPC->Function->GetName()); + return; + } + + Worker_CommandRequest CommandRequest = CreateRetryRPCCommandRequest(*RetryRPC, TargetObjectRef.Offset); + Worker_RequestId RequestId = Connection->SendCommandRequest(TargetObjectRef.Entity, &CommandRequest, SpatialConstants::UNREAL_RPC_ENDPOINT_COMMAND_ID); - FSoftClassPath ActorClassPath(Channel->Actor->GetClass()); + // The number of attempts is used to determine the delay in case the command times out and we need to resend it. + RetryRPC->Attempts++; + UE_LOG(LogSpatialSender, Verbose, TEXT("Sending reliable command request (entity: %lld, component: %d, function: %s, attempt: %d)"), + TargetObjectRef.Entity, RetryRPC->ComponentId, *RetryRPC->Function->GetName(), RetryRPC->Attempts); + Receiver->AddPendingReliableRPC(RequestId, RetryRPC); +} + +void USpatialSender::RegisterChannelForPositionUpdate(USpatialActorChannel* Channel) +{ + ChannelsToUpdatePosition.Add(Channel); +} + +void USpatialSender::ProcessPositionUpdates() +{ + for (auto& Channel : ChannelsToUpdatePosition) + { + if (Channel.IsValid()) + { + Channel->UpdateSpatialPosition(); + } + } + + ChannelsToUpdatePosition.Empty(); +} + +void USpatialSender::SendCreateEntityRequest(USpatialActorChannel* Channel) +{ + UE_LOG(LogSpatialSender, Log, TEXT("Sending create entity request for %s with EntityId %lld"), *Channel->Actor->GetName(), Channel->GetEntityId()); Worker_RequestId RequestId = CreateEntity(Channel); Receiver->AddPendingActorRequest(RequestId, Channel); @@ -557,6 +996,35 @@ void USpatialSender::SendDeleteEntityRequest(Worker_EntityId EntityId) Connection->SendDeleteEntityRequest(EntityId); } +void USpatialSender::SendRequestToClearRPCsOnEntityCreation(Worker_EntityId EntityId) +{ + Worker_CommandRequest CommandRequest = RPCsOnEntityCreation::CreateClearFieldsCommandRequest(); + NetDriver->Connection->SendCommandRequest(EntityId, &CommandRequest, SpatialConstants::CLEAR_RPCS_ON_ENTITY_CREATION); +} + +void USpatialSender::ClearRPCsOnEntityCreation(Worker_EntityId EntityId) +{ + check(NetDriver->IsServer()); + Worker_ComponentUpdate Update = RPCsOnEntityCreation::CreateClearFieldsUpdate(); + NetDriver->Connection->SendComponentUpdate(EntityId, &Update); +} + +void USpatialSender::SendClientEndpointReadyUpdate(Worker_EntityId EntityId) +{ + ClientRPCEndpoint Endpoint; + Endpoint.bReady = true; + Worker_ComponentUpdate Update = Endpoint.CreateRPCEndpointUpdate(); + NetDriver->Connection->SendComponentUpdate(EntityId, &Update); +} + +void USpatialSender::SendServerEndpointReadyUpdate(Worker_EntityId EntityId) +{ + ServerRPCEndpoint Endpoint; + Endpoint.bReady = true; + Worker_ComponentUpdate Update = Endpoint.CreateRPCEndpointUpdate(); + NetDriver->Connection->SendComponentUpdate(EntityId, &Update); +} + void USpatialSender::ResetOutgoingUpdate(USpatialActorChannel* DependentChannel, UObject* ReplicatedObject, int16 Handle, bool bIsHandover) { SCOPE_CYCLE_COUNTER(STAT_SpatialSenderResetOutgoingUpdate); @@ -653,14 +1121,44 @@ void USpatialSender::QueueOutgoingUpdate(USpatialActorChannel* DependentChannel, } } -void USpatialSender::QueueOutgoingRPC(const UObject* UnresolvedObject, TSharedRef Params) +void USpatialSender::QueueOutgoingRPC(FPendingRPCParamsPtr Params) { - check(UnresolvedObject); - UE_LOG(LogSpatialSender, Log, TEXT("Added pending outgoing RPC depending on object: %s, target: %s, function: %s"), *UnresolvedObject->GetName(), *Params->TargetObject->GetName(), *Params->Function->GetName()); - OutgoingRPCs.FindOrAdd(UnresolvedObject).Add(Params); + TWeakObjectPtr TargetObjectWeakPtr = PackageMap->GetObjectFromUnrealObjectRef(Params->ObjectRef); + if (!TargetObjectWeakPtr.IsValid()) + { + // Target object was destroyed before the RPC could be (re)sent + return; + } + UObject* TargetObject = TargetObjectWeakPtr.Get(); + + const FClassInfo& ClassInfo = ClassInfoManager->GetOrCreateClassInfoByObject(TargetObject); + UFunction* Function = ClassInfo.RPCs[Params->Payload.Index]; + + const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject, Function); + + const FUnrealObjectRef& TargetObjectRef = PackageMap->GetUnrealObjectRefFromObject(TargetObject); + OutgoingRPCs.QueueRPC(MoveTemp(Params), RPCInfo.Type); } -Worker_CommandRequest USpatialSender::CreateRPCCommandRequest(UObject* TargetObject, UFunction* Function, void* Parameters, Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, Worker_EntityId& OutEntityId, const UObject*& OutUnresolvedObject, int ReliableRPCId) +FSpatialNetBitWriter USpatialSender::PackRPCDataToSpatialNetBitWriter(UFunction* Function, void* Parameters, int ReliableRPCId, TSet>& UnresolvedObjects) const +{ + FSpatialNetBitWriter PayloadWriter(PackageMap, UnresolvedObjects); + + if (GetDefault()->bCheckRPCOrder) + { + if (Function->HasAnyFunctionFlags(FUNC_NetReliable) && !Function->HasAnyFunctionFlags(FUNC_NetMulticast)) + { + PayloadWriter << ReliableRPCId; + } + } + + TSharedPtr RepLayout = NetDriver->GetFunctionRepLayout(Function); + RepLayout_SendPropertiesForRPC(*RepLayout, PayloadWriter, Parameters); + + return PayloadWriter; +} + +Worker_CommandRequest USpatialSender::CreateRPCCommandRequest(UObject* TargetObject, const RPCPayload& Payload, Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, Worker_EntityId& OutEntityId, const UObject*& OutUnresolvedObject) { Worker_CommandRequest CommandRequest = {}; CommandRequest.component_id = ComponentId; @@ -677,35 +1175,24 @@ Worker_CommandRequest USpatialSender::CreateRPCCommandRequest(UObject* TargetObj OutEntityId = TargetObjectRef.Entity; - TSet> UnresolvedObjects; - FSpatialNetBitWriter PayloadWriter(PackageMap, UnresolvedObjects); + RPCPayload::WriteToSchemaObject(RequestObject, TargetObjectRef.Offset, CommandIndex, Payload.PayloadData.GetData(), Payload.PayloadData.Num()); -#if !UE_BUILD_SHIPPING - if (Function->HasAnyFunctionFlags(FUNC_NetReliable) && !Function->HasAnyFunctionFlags(FUNC_NetMulticast)) - { - PayloadWriter << ReliableRPCId; - } -#endif // !UE_BUILD_SHIPPING + return CommandRequest; +} - TSharedPtr RepLayout = NetDriver->GetFunctionRepLayout(Function); - RepLayout_SendPropertiesForRPC(*RepLayout, PayloadWriter, Parameters); +Worker_CommandRequest USpatialSender::CreateRetryRPCCommandRequest(const FReliableRPCForRetry& RPC, uint32 TargetObjectOffset) +{ + Worker_CommandRequest CommandRequest = {}; + CommandRequest.component_id = RPC.ComponentId; + CommandRequest.schema_type = Schema_CreateCommandRequest(RPC.ComponentId, SpatialConstants::UNREAL_RPC_ENDPOINT_COMMAND_ID); + Schema_Object* RequestObject = Schema_GetCommandRequestObject(CommandRequest.schema_type); - for (TWeakObjectPtr Object : UnresolvedObjects) - { - if (Object.IsValid()) - { - // Take the first unresolved object - OutUnresolvedObject = Object.Get(); - Schema_DestroyCommandRequest(CommandRequest.schema_type); - return CommandRequest; - } - } + RPCPayload::WriteToSchemaObject(RequestObject, TargetObjectOffset, RPC.RPCIndex, RPC.Payload.GetData(), RPC.Payload.Num()); - WriteRpcPayload(RequestObject, TargetObjectRef.Offset, CommandIndex, PayloadWriter); return CommandRequest; } -Worker_ComponentUpdate USpatialSender::CreateUnreliableRPCUpdate(UObject* TargetObject, UFunction* Function, void* Parameters, Worker_ComponentId ComponentId, Schema_FieldId EventIndex, Worker_EntityId& OutEntityId, const UObject*& OutUnresolvedObject) +Worker_ComponentUpdate USpatialSender::CreateRPCEventUpdate(UObject* TargetObject, const RPCPayload& Payload, Worker_ComponentId ComponentId, Schema_FieldId EventIndex, const UObject*& OutUnresolvedObject) { Worker_ComponentUpdate ComponentUpdate = {}; @@ -722,35 +1209,64 @@ Worker_ComponentUpdate USpatialSender::CreateUnreliableRPCUpdate(UObject* Target return ComponentUpdate; } - OutEntityId = TargetObjectRef.Entity; + RPCPayload::WriteToSchemaObject(EventData, Payload.Offset, Payload.Index, Payload.PayloadData.GetData(), Payload.PayloadData.Num()); - TSet> UnresolvedObjects; - FSpatialNetBitWriter PayloadWriter(PackageMap, UnresolvedObjects); + return ComponentUpdate; +} - TSharedPtr RepLayout = NetDriver->GetFunctionRepLayout(Function); - RepLayout_SendPropertiesForRPC(*RepLayout, PayloadWriter, Parameters); +bool USpatialSender::AddPendingRPC(UObject* TargetObject, const FPendingRPCParams& Parameters, Worker_ComponentId ComponentId, Schema_FieldId RPCIndex, const UObject*& OutUnresolvedObject) +{ + FUnrealObjectRef TargetObjectRef(PackageMap->GetUnrealObjectRefFromNetGUID(PackageMap->GetNetGUIDFromObject(TargetObject))); + if (TargetObjectRef == FUnrealObjectRef::UNRESOLVED_OBJECT_REF) + { + OutUnresolvedObject = TargetObject; + return false; + } + + const FClassInfo& ClassInfo = ClassInfoManager->GetOrCreateClassInfoByObject(TargetObject); + UFunction* Function = ClassInfo.RPCs[Parameters.Payload.Index]; - for (TWeakObjectPtr Object : UnresolvedObjects) + AActor* TargetActor = Cast(PackageMap->GetObjectFromEntityId(TargetObjectRef.Entity).Get()); + check(TargetActor != nullptr); + UNetConnection* OwningConnection = TargetActor->GetNetConnection(); + if (OwningConnection == nullptr) { - if (Object.IsValid()) - { - // Take the first unresolved object - OutUnresolvedObject = Object.Get(); - Schema_DestroyComponentUpdate(ComponentUpdate.schema_type); - return ComponentUpdate; - } + UE_LOG(LogSpatialSender, Warning, TEXT("AddPendingRPC: No connection for object %s (RPC %s, actor %s, entity %lld)"), + *TargetObject->GetName(), *Function->GetName(), *TargetActor->GetName(), TargetObjectRef.Entity); + return false; } - WriteRpcPayload(EventData, TargetObjectRef.Offset, EventIndex, PayloadWriter); + APlayerController* Controller = Cast(OwningConnection->OwningActor); + if (Controller == nullptr) + { + UE_LOG(LogSpatialSender, Warning, TEXT("AddPendingRPC: Connection's owner is not a player controller for object %s (RPC %s, actor %s, entity %lld): connection owner %s"), + *TargetObject->GetName(), *Function->GetName(), *TargetActor->GetName(), TargetObjectRef.Entity, *OwningConnection->OwningActor->GetName()); + return false; + } - return ComponentUpdate; -} + USpatialActorChannel* ControllerChannel = NetDriver->GetOrCreateSpatialActorChannel(Controller); + if (ControllerChannel == nullptr || !ControllerChannel->IsListening()) + { + return false; + } -void USpatialSender::WriteRpcPayload(Schema_Object* Object, uint32 Offset, Schema_FieldId Index, FSpatialNetBitWriter& PayloadWriter) -{ - Schema_AddUint32(Object, SpatialConstants::UNREAL_RPC_PAYLOAD_OFFSET_ID, Offset); - Schema_AddUint32(Object, SpatialConstants::UNREAL_RPC_PAYLOAD_RPC_INDEX_ID, Index); - AddBytesToSchema(Object, SpatialConstants::UNREAL_RPC_PAYLOAD_RPC_PAYLOAD_ID, PayloadWriter); + FUnrealObjectRef ControllerObjectRef = PackageMap->GetUnrealObjectRefFromObject(Controller); + if (ControllerObjectRef == FUnrealObjectRef::UNRESOLVED_OBJECT_REF) + { + OutUnresolvedObject = Controller; + return false; + } + + TSet> UnresolvedObjects; + + FPendingRPC RPC; + RPC.Offset = TargetObjectRef.Offset; + RPC.Index = RPCIndex; + RPC.Data.SetNumUninitialized(Parameters.Payload.PayloadData.Num()); + FMemory::Memcpy(RPC.Data.GetData(), Parameters.Payload.PayloadData.GetData(), Parameters.Payload.PayloadData.Num()); + RPC.Entity = TargetObjectRef.Entity; + RPCsToPack.FindOrAdd(ControllerObjectRef.Entity).Emplace(MoveTemp(RPC)); + return true; } void USpatialSender::SendCommandResponse(Worker_RequestId request_id, Worker_CommandResponse& Response) @@ -758,6 +1274,15 @@ void USpatialSender::SendCommandResponse(Worker_RequestId request_id, Worker_Com Connection->SendCommandResponse(request_id, &Response); } +void USpatialSender::SendEmptyCommandResponse(Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, Worker_RequestId RequestId) +{ + Worker_CommandResponse Response = {}; + Response.component_id = ComponentId; + Response.schema_type = Schema_CreateCommandResponse(ComponentId, CommandIndex); + + Connection->SendCommandResponse(RequestId, &Response); +} + void USpatialSender::ResolveOutgoingOperations(UObject* Object, bool bIsHandover) { // Choose the correct container based on whether it's handover or not @@ -831,26 +1356,11 @@ void USpatialSender::ResolveOutgoingOperations(UObject* Object, bool bIsHandover ObjectToUnresolved.Remove(Object); } -void USpatialSender::ResolveOutgoingRPCs(UObject* Object) +void USpatialSender::SendOutgoingRPCs() { - TArray>* RPCList = OutgoingRPCs.Find(Object); - if (RPCList) - { - for (TSharedRef& RPCParams : *RPCList) - { - if (!RPCParams->TargetObject.IsValid()) - { - // The target object was destroyed before we could send the RPC. - continue; - } - - // We can guarantee that SendRPC won't populate OutgoingRPCs[Object] whilst we're iterating through it, - // because Object has been resolved when we call ResolveOutgoingRPCs. - UE_LOG(LogSpatialSender, Verbose, TEXT("Resolving outgoing RPC depending on object: %s, target: %s, function: %s"), *Object->GetName(), *RPCParams->TargetObject->GetName(), *RPCParams->Function->GetName()); - SendRPC(RPCParams); - } - OutgoingRPCs.Remove(Object); - } + FProcessRPCDelegate Delegate; + Delegate.BindUObject(this, &USpatialSender::SendRPC); + OutgoingRPCs.ProcessRPCs(Delegate); } // Authority over the ClientRPC Schema component is dictated by the owning connection of a client. @@ -888,3 +1398,31 @@ void USpatialSender::UpdateInterestComponent(AActor* Actor) Worker_EntityId EntityId = PackageMap->GetEntityIdFromObject(Actor); Connection->SendComponentUpdate(EntityId, &Update); } + +void USpatialSender::ProcessRPC(FPendingRPCParamsPtr Params) +{ + TWeakObjectPtr TargetObject = PackageMap->GetObjectFromUnrealObjectRef(Params->ObjectRef); + if (!TargetObject.IsValid()) + { + // Target object was destroyed before the RPC could be (re)sent + return; + } + const FClassInfo& ClassInfo = ClassInfoManager->GetOrCreateClassInfoByObject(TargetObject.Get()); + UFunction* Function = ClassInfo.RPCs[Params->Payload.Index]; + const FRPCInfo& RPCInfo = ClassInfoManager->GetRPCInfo(TargetObject.Get(), Function); + + bool bRPCProcessed = false; + if (!OutgoingRPCs.ObjectHasRPCsQueuedOfType(Params->ObjectRef.Entity, RPCInfo.Type)) + { + if (SendRPC(*Params)) + { + bRPCProcessed = true; + } + } + if (!bRPCProcessed) + { + QueueOutgoingRPC(MoveTemp(Params)); + } + // Try to send all pending RPCs unconditionally + SendOutgoingRPCs(); +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp index 0499b91be3..91adba2fde 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialStaticComponentView.cpp @@ -5,6 +5,7 @@ #include "Schema/Component.h" #include "Schema/Heartbeat.h" #include "Schema/Interest.h" +#include "Schema/RPCPayload.h" #include "Schema/Singleton.h" #include "Schema/SpawnData.h" @@ -27,6 +28,16 @@ bool USpatialStaticComponentView::HasAuthority(Worker_EntityId EntityId, Worker_ return GetAuthority(EntityId, ComponentId) == WORKER_AUTHORITY_AUTHORITATIVE; } +bool USpatialStaticComponentView::HasComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId) +{ + if (auto* EntityComponentStorage = EntityComponentMap.Find(EntityId)) + { + return EntityComponentStorage->Contains(ComponentId); + } + + return false; +} + void USpatialStaticComponentView::OnAddComponent(const Worker_AddComponentOp& Op) { TUniquePtr Data; @@ -59,16 +70,33 @@ void USpatialStaticComponentView::OnAddComponent(const Worker_AddComponentOp& Op case SpatialConstants::HEARTBEAT_COMPONENT_ID: Data = MakeUnique>(Op.data); break; + case SpatialConstants::RPCS_ON_ENTITY_CREATION_ID: + Data = MakeUnique>(Op.data); + break; default: - return; + // Component is not hand written, but we still want to know the existence of it on this entity. + Data = nullptr; } - EntityComponentMap.FindOrAdd(Op.entity_id).FindOrAdd(Op.data.component_id) = std::move(Data); } +void USpatialStaticComponentView::OnRemoveComponent(const Worker_RemoveComponentOp& Op) +{ + if (auto* ComponentMap = EntityComponentMap.Find(Op.entity_id)) + { + ComponentMap->Remove(Op.component_id); + } + + if (auto* AuthorityMap = EntityComponentAuthorityMap.Find(Op.entity_id)) + { + AuthorityMap->Remove(Op.component_id); + } +} + void USpatialStaticComponentView::OnRemoveEntity(Worker_EntityId EntityId) { EntityComponentMap.Remove(EntityId); + EntityComponentAuthorityMap.Remove(EntityId); } void USpatialStaticComponentView::OnComponentUpdate(const Worker_ComponentUpdateOp& Op) diff --git a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialWorkerFlags.cpp b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialWorkerFlags.cpp index cfe3695024..c12623d63e 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialWorkerFlags.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Interop/SpatialWorkerFlags.cpp @@ -4,6 +4,7 @@ #include TMap USpatialWorkerFlags::WorkerFlags; +FOnWorkerFlagsUpdated USpatialWorkerFlags::OnWorkerFlagsUpdated; bool USpatialWorkerFlags::GetWorkerFlag(const FString& Name, FString& OutValue) { @@ -25,9 +26,24 @@ void USpatialWorkerFlags::ApplyWorkerFlagUpdate(const Worker_FlagUpdateOp& Op) FString NewValue = FString(UTF8_TO_TCHAR(Op.value)); FString& ValueFlag = WorkerFlags.FindOrAdd(NewName); ValueFlag = NewValue; + OnWorkerFlagsUpdated.Broadcast(NewName, NewValue); } else { WorkerFlags.Remove(NewName); } } +FOnWorkerFlagsUpdated& USpatialWorkerFlags::GetOnWorkerFlagsUpdated() +{ + return OnWorkerFlagsUpdated; +} + +void USpatialWorkerFlags::BindToOnWorkerFlagsUpdated(const FOnWorkerFlagsUpdatedBP& InDelegate) +{ + OnWorkerFlagsUpdated.Add(InDelegate); +} + +void USpatialWorkerFlags::UnbindFromOnWorkerFlagsUpdated(const FOnWorkerFlagsUpdatedBP& InDelegate) +{ + OnWorkerFlagsUpdated.Remove(InDelegate); +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/SimulatedPlayers/SimPlayerBPFunctionLibrary.cpp b/SpatialGDK/Source/SpatialGDK/Private/SimulatedPlayers/SimPlayerBPFunctionLibrary.cpp new file mode 100644 index 0000000000..60c223200e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/SimulatedPlayers/SimPlayerBPFunctionLibrary.cpp @@ -0,0 +1,20 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SimulatedPlayers/SimPlayerBPFunctionLibrary.h" + +#include "Engine/GameInstance.h" +#include "Kismet/GameplayStatics.h" + +DEFINE_LOG_CATEGORY_STATIC(LogSimulatedPlayer, Log, All); + +bool USimPlayerBPFunctionLibrary::IsSimulatedPlayer(const UObject* WorldContextObject) +{ + UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(WorldContextObject); + if (GameInstance == nullptr) + { + UE_LOG(LogSimulatedPlayer, Warning, TEXT("Cannot check if player is simulated: cannot get GameInstance. Defaulting to false.")); + return false; + } + + return GameInstance->IsSimulatedPlayer(); +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp b/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp index 6d317df392..a2f546901d 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/SpatialGDKSettings.cpp @@ -1,12 +1,13 @@ // 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 "Modules/ModuleManager.h" -#include "ISettingsModule.h" +#include "Settings/LevelEditorPlaySettings.h" #endif USpatialGDKSettings::USpatialGDKSettings(const FObjectInitializer& ObjectInitializer) @@ -19,12 +20,26 @@ USpatialGDKSettings::USpatialGDKSettings(const FObjectInitializer& ObjectInitial , ActorReplicationRateLimit(0) , EntityCreationRateLimit(0) , OpsUpdateRate(1000.0f) + , bEnableHandover(true) + , MaxNetCullDistanceSquared(900000000.0f) // Set to twice the default Actor NetCullDistanceSquared (300m) , bUsingQBI(true) , PositionUpdateFrequency(1.0f) , PositionDistanceThreshold(100.0f) // 1m (100cm) , bEnableMetrics(true) + , bEnableMetricsDisplay(false) , MetricsReportRate(2.0f) + , bUseFrameTimeAsLoad(false) + , bCheckRPCOrder(false) + , bBatchSpatialPositionUpdates(true) + , MaxDynamicallyAttachedSubobjectsPerClass(3) + , bEnableServerQBI(bUsingQBI) + , bPackRPCs(true) + , bUseDevelopmentAuthenticationFlow(false) + , DefaultWorkerType(FWorkerType(SpatialConstants::DefaultServerWorkerType)) + , bEnableOffloading(false) + , ServerWorkerTypes({ SpatialConstants::DefaultServerWorkerType }) { + DefaultReceptionistHost = SpatialConstants::LOCAL_HOST; } void USpatialGDKSettings::PostInitProperties() @@ -34,29 +49,35 @@ void USpatialGDKSettings::PostInitProperties() // Check any command line overrides for using QBI (after reading the config value): const TCHAR* CommandLine = FCommandLine::Get(); FParse::Bool(CommandLine, TEXT("useQBI"), bUsingQBI); + +#if WITH_EDITOR + ULevelEditorPlaySettings* PlayInSettings = GetMutableDefault(); + PlayInSettings->bEnableOffloading = bEnableOffloading; + PlayInSettings->DefaultWorkerType = DefaultWorkerType.WorkerTypeName; +#endif } #if WITH_EDITOR -// Add a pop-up to warn users to update their config upon changing the using QBI property. -void USpatialGDKSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +void USpatialGDKSettings::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) { - if (PropertyChangedEvent.Property == nullptr) + Super::PostEditChangeProperty(PropertyChangedEvent); + + // Use MemberProperty here so we report the correct member name for nested changes + const FName Name = (PropertyChangedEvent.MemberProperty != nullptr) ? PropertyChangedEvent.MemberProperty->GetFName() : NAME_None; + + if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, bEnableOffloading)) { - return; + GetMutableDefault()->bEnableOffloading = bEnableOffloading; } - const FName PropertyName = PropertyChangedEvent.Property->GetFName(); - - if (PropertyName == GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, bUsingQBI)) + else if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKSettings, DefaultWorkerType)) { - const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, - FText::FromString(FString::Printf(TEXT("You must set the value of the \"enable_chunk_interest\" Legacy flag to \"%s\" in your launch configuration file for this to work.\n\nIf you are using an auto-generated launch config, you can set this value from within Unreal Editor by going to Edit > Project Settings > SpatialOS GDK for Unreal > Settings.\n\nDo you want to configure your launch config settings now?"), - bUsingQBI ? TEXT("false") : TEXT("true")))); - - if (Result == EAppReturnType::Yes) - { - FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Editor Settings"); - } + GetMutableDefault()->DefaultWorkerType = DefaultWorkerType.WorkerTypeName; + } + else 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!")))); } - Super::PostEditChangeProperty(PropertyChangedEvent); } #endif diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/ActorGroupManager.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/ActorGroupManager.cpp new file mode 100644 index 0000000000..1bdacb3aad --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/ActorGroupManager.cpp @@ -0,0 +1,87 @@ +#include "Utils/ActorGroupManager.h" +#include "SpatialGDKSettings.h" + +void UActorGroupManager::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 UActorGroupManager::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)) + { + if (FoundClass != Class) + { + ClassPathToActorGroup.Add(TSoftClassPtr(Class), *ActorGroup); + } + return *ActorGroup; + } + + 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 UActorGroupManager::GetWorkerTypeForClass(const TSubclassOf Class) +{ + const FName ActorGroup = GetActorGroupForClass(Class); + + if (const FName* WorkerType = ActorGroupToWorkerType.Find(ActorGroup)) + { + return *WorkerType; + } + + return DefaultWorkerType; +} + +FName UActorGroupManager::GetWorkerTypeForActorGroup(const FName& ActorGroup) const +{ + if (const FName* WorkerType = ActorGroupToWorkerType.Find(ActorGroup)) + { + return *WorkerType; + } + + return DefaultWorkerType; +} + +bool UActorGroupManager::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/ComponentFactory.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp index 15ff5ec954..47805a77bb 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentFactory.cpp @@ -58,9 +58,10 @@ bool ComponentFactory::FillSchemaObject(Schema_Object* ComponentObject, UObject* { FSpatialNetBitWriter ValueDataWriter(PackageMap, UnresolvedObjects); - FSpatialNetDeltaSerializeInfo::DeltaSerializeWrite(NetDriver, ValueDataWriter, Object, Parent.ArrayIndex, Parent.Property, NetDeltaStruct); - - AddBytesToSchema(ComponentObject, HandleIterator.Handle, ValueDataWriter); + if (FSpatialNetDeltaSerializeInfo::DeltaSerializeWrite(NetDriver, ValueDataWriter, Object, Parent.ArrayIndex, Parent.Property, NetDeltaStruct) || bIsInitialData) + { + AddBytesToSchema(ComponentObject, HandleIterator.Handle, ValueDataWriter); + } bProcessedFastArrayProperty = true; } @@ -152,7 +153,13 @@ void ComponentFactory::AddProperty(Schema_Object* Object, Schema_FieldId FieldId { bHasUnmapped = true; } - checkf(bSuccess, TEXT("NetSerialize on %s failed."), *Struct->GetStructCPPName()); + + // Check the success of the serialization and print a warning if it failed. This is how native handles failed serialization. + if (!bSuccess) + { + UE_LOG(LogSpatialNetSerialize, Warning, TEXT("AddProperty: NetSerialize %s failed."), *Struct->GetFullName()); + return; + } } else { @@ -303,9 +310,9 @@ void ComponentFactory::AddProperty(Schema_Object* Object, Schema_FieldId FieldId AddProperty(Object, FieldId, EnumProperty->GetUnderlyingProperty(), Data, UnresolvedObjects, ClearedIds); } } - else if (Property->IsA() || Property->IsA()) + else if (Property->IsA() || Property->IsA() || Property->IsA()) { - // Delegates can be set to replicate, but won't serialize across the network. + // These properties can be set to replicate, but won't serialize across the network. } else { diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp index a1654559d4..deb2b26128 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/ComponentReader.cpp @@ -90,7 +90,11 @@ void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject* { FObjectReplicator& Replicator = Channel->PreReceiveSpatialUpdate(Object); - TSharedPtr RepState = Replicator.RepState; +#if ENGINE_MINOR_VERSION <= 20 + TSharedPtr& RepState = Replicator.RepState; +#else + TUniquePtr& RepState = Replicator.RepState; +#endif TArray& Cmds = Replicator.RepLayout->Cmds; TArray& BaseHandleToCmdIndex = Replicator.RepLayout->BaseHandleToCmdIndex; TArray& Parents = Replicator.RepLayout->Parents; @@ -107,9 +111,14 @@ void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject* { // FieldId is the same as rep handle check(FieldId > 0 && (int)FieldId - 1 < BaseHandleToCmdIndex.Num()); - const FRepLayoutCmd& Cmd = Cmds[BaseHandleToCmdIndex[FieldId - 1].CmdIndex]; + int32 CmdIndex = BaseHandleToCmdIndex[FieldId - 1].CmdIndex; + const FRepLayoutCmd& Cmd = Cmds[CmdIndex]; const FRepParentCmd& Parent = Parents[Cmd.ParentIndex]; - +#if ENGINE_MINOR_VERSION <= 20 + int32 ShadowOffset = 0; +#else + int32 ShadowOffset = Cmd.ShadowOffset; +#endif if (NetDriver->IsServer() || ConditionMap.IsRelevant(Parent.Condition)) { // This swaps Role/RemoteRole as we write it @@ -129,11 +138,14 @@ void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject* TSet NewUnresolvedRefs; FSpatialNetBitReader ValueDataReader(PackageMap, ValueData.GetData(), CountBits, NewUnresolvedRefs); - FSpatialNetDeltaSerializeInfo::DeltaSerializeRead(NetDriver, ValueDataReader, Object, Parent.ArrayIndex, Parent.Property, NetDeltaStruct); + if (ValueData.Num() > 0) + { + FSpatialNetDeltaSerializeInfo::DeltaSerializeRead(NetDriver, ValueDataReader, Object, Parent.ArrayIndex, Parent.Property, NetDeltaStruct); + } if (NewUnresolvedRefs.Num() > 0) { - RootObjectReferencesMap.Add(SwappedCmd.Offset, FObjectReferences(ValueData, CountBits, NewUnresolvedRefs, Cmd.ParentIndex, ArrayProperty, /* bFastArrayProp */ true)); + RootObjectReferencesMap.Add(SwappedCmd.Offset, FObjectReferences(ValueData, CountBits, NewUnresolvedRefs, ShadowOffset, Cmd.ParentIndex, ArrayProperty, /* bFastArrayProp */ true)); UnresolvedRefs.Append(NewUnresolvedRefs); } else if (RootObjectReferencesMap.Find(FieldId)) @@ -143,12 +155,12 @@ void ComponentReader::ApplySchemaObject(Schema_Object* ComponentObject, UObject* } else { - ApplyArray(ComponentObject, FieldId, RootObjectReferencesMap, ArrayProperty, Data, SwappedCmd.Offset, Cmd.ParentIndex); + ApplyArray(ComponentObject, FieldId, RootObjectReferencesMap, ArrayProperty, Data, SwappedCmd.Offset, ShadowOffset, Cmd.ParentIndex); } } else { - ApplyProperty(ComponentObject, FieldId, RootObjectReferencesMap, 0, Cmd.Property, Data, SwappedCmd.Offset, Cmd.ParentIndex); + ApplyProperty(ComponentObject, FieldId, RootObjectReferencesMap, 0, Cmd.Property, Data, SwappedCmd.Offset, ShadowOffset, Cmd.ParentIndex); } if (Cmd.Property->GetFName() == NAME_RemoteRole) @@ -165,7 +177,11 @@ 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 <= 20 bool bIsIdentical = Cmd.Property->Identical(RepState->StaticBuffer.GetData() + SwappedCmd.Offset, Data); +#else + bool bIsIdentical = Cmd.Property->Identical(RepState->StaticBuffer.GetData() + SwappedCmd.ShadowOffset, Data); +#endif // Only call RepNotify for REPNOTIFY_Always if we are not applying initial data. if (bIsInitialData) @@ -208,18 +224,18 @@ void ComponentReader::ApplyHandoverSchemaObject(Schema_Object* ComponentObject, if (UArrayProperty* ArrayProperty = Cast(PropertyInfo.Property)) { - ApplyArray(ComponentObject, FieldId, RootObjectReferencesMap, ArrayProperty, Data, PropertyInfo.Offset, -1); + ApplyArray(ComponentObject, FieldId, RootObjectReferencesMap, ArrayProperty, Data, PropertyInfo.Offset, -1, -1); } else { - ApplyProperty(ComponentObject, FieldId, RootObjectReferencesMap, 0, PropertyInfo.Property, Data, PropertyInfo.Offset, -1); + ApplyProperty(ComponentObject, FieldId, RootObjectReferencesMap, 0, PropertyInfo.Property, Data, PropertyInfo.Offset, -1, -1); } } Channel->PostReceiveSpatialUpdate(Object, TArray()); } -void ComponentReader::ApplyProperty(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, uint32 Index, UProperty* Property, uint8* Data, int32 Offset, int32 ParentIndex) +void ComponentReader::ApplyProperty(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, uint32 Index, UProperty* Property, uint8* Data, int32 Offset, int32 ShadowOffset, int32 ParentIndex) { if (UStructProperty* StructProperty = Cast(Property)) { @@ -234,7 +250,7 @@ void ComponentReader::ApplyProperty(Schema_Object* Object, Schema_FieldId FieldI if (bHasUnmapped) { - InObjectReferencesMap.Add(Offset, FObjectReferences(ValueData, CountBits, NewUnresolvedRefs, ParentIndex, Property)); + InObjectReferencesMap.Add(Offset, FObjectReferences(ValueData, CountBits, NewUnresolvedRefs, ShadowOffset, ParentIndex, Property)); UnresolvedRefs.Append(NewUnresolvedRefs); } else if (InObjectReferencesMap.Find(Offset)) @@ -321,7 +337,7 @@ void ComponentReader::ApplyProperty(Schema_Object* Object, Schema_FieldId FieldI } else { - InObjectReferencesMap.Add(Offset, FObjectReferences(ObjectRef, ParentIndex, Property)); + InObjectReferencesMap.Add(Offset, FObjectReferences(ObjectRef, ShadowOffset, ParentIndex, Property)); UnresolvedRefs.Add(ObjectRef); bUnresolved = true; } @@ -352,7 +368,7 @@ void ComponentReader::ApplyProperty(Schema_Object* Object, Schema_FieldId FieldI } else { - ApplyProperty(Object, FieldId, InObjectReferencesMap, Index, EnumProperty->GetUnderlyingProperty(), Data, Offset, ParentIndex); + ApplyProperty(Object, FieldId, InObjectReferencesMap, Index, EnumProperty->GetUnderlyingProperty(), Data, Offset, ShadowOffset, ParentIndex); } } else @@ -361,7 +377,7 @@ void ComponentReader::ApplyProperty(Schema_Object* Object, Schema_FieldId FieldI } } -void ComponentReader::ApplyArray(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, UArrayProperty* Property, uint8* Data, int32 Offset, int32 ParentIndex) +void ComponentReader::ApplyArray(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, UArrayProperty* Property, uint8* Data, int32 Offset, int32 ShadowOffset, int32 ParentIndex) { FObjectReferencesMap* ArrayObjectReferences; bool bNewArrayMap = false; @@ -385,7 +401,7 @@ void ComponentReader::ApplyArray(Schema_Object* Object, Schema_FieldId FieldId, for (int i = 0; i < Count; i++) { int32 ElementOffset = i * Property->Inner->ElementSize; - ApplyProperty(Object, FieldId, *ArrayObjectReferences, i, Property->Inner, ArrayHelper.GetRawPtr(i), ElementOffset, ParentIndex); + ApplyProperty(Object, FieldId, *ArrayObjectReferences, i, Property->Inner, ArrayHelper.GetRawPtr(i), ElementOffset, ElementOffset, ParentIndex); } if (ArrayObjectReferences->Num() > 0) @@ -393,7 +409,7 @@ void ComponentReader::ApplyArray(Schema_Object* Object, Schema_FieldId FieldId, if (bNewArrayMap) { // FObjectReferences takes ownership over ArrayObjectReferences - InObjectReferencesMap.Add(Offset, FObjectReferences(ArrayObjectReferences, ParentIndex, Property)); + InObjectReferencesMap.Add(Offset, FObjectReferences(ArrayObjectReferences, ShadowOffset, ParentIndex, Property)); } } else diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityPool.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityPool.cpp index 4ea5597293..5ee732f981 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityPool.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/EntityPool.cpp @@ -22,13 +22,13 @@ void UEntityPool::Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimerMan void UEntityPool::ReserveEntityIDs(int32 EntitiesToReserve) { - UE_LOG(LogSpatialEntityPool, Verbose, TEXT("Sending bulk entity ID Reservation Request")); + UE_LOG(LogSpatialEntityPool, Verbose, TEXT("Sending bulk entity ID Reservation Request for %d IDs"), EntitiesToReserve); checkf(!bIsAwaitingResponse, TEXT("Trying to reserve Entity IDs while another reserve request is in flight")); // Set up reserve IDs delegate ReserveEntityIDsDelegate CacheEntityIDsDelegate; - CacheEntityIDsDelegate.BindLambda([EntitiesToReserve, this](Worker_ReserveEntityIdsResponseOp& Op) + CacheEntityIDsDelegate.BindLambda([EntitiesToReserve, this](const Worker_ReserveEntityIdsResponseOp& Op) { if (Op.status_code != WORKER_STATUS_CODE_SUCCESS) { @@ -64,20 +64,21 @@ void UEntityPool::ReserveEntityIDs(int32 EntitiesToReserve) ReservedEntityIDRanges.Add(NewEntityRange); - TWeakObjectPtr WeakEntityPoolPtr = this; FTimerHandle ExpirationTimer; - TimerManager->SetTimer(ExpirationTimer, [WeakEntityPoolPtr, ExpiringEntityRangeId = NewEntityRange.EntityRangeId]() + TWeakObjectPtr WeakThis(this); + TimerManager->SetTimer(ExpirationTimer, [WeakThis, ExpiringEntityRangeId = NewEntityRange.EntityRangeId]() { - if (!WeakEntityPoolPtr.IsValid()) + if (UEntityPool* Pool = WeakThis.Get()) { - return; + Pool->OnEntityRangeExpired(ExpiringEntityRangeId); } - - WeakEntityPoolPtr->OnEntityRangeExpired(ExpiringEntityRangeId); }, SpatialConstants::ENTITY_RANGE_EXPIRATION_INTERVAL_SECONDS, false); - bIsReady = true; bIsAwaitingResponse = false; + if (!bIsReady) + { + bIsReady = true; + } }); // Reserve the Entity IDs diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp index c75253f00d..f666b01dbe 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/InterestFactory.cpp @@ -5,65 +5,160 @@ #include "Engine/World.h" #include "Engine/Classes/GameFramework/Actor.h" #include "GameFramework/PlayerController.h" +#include "UObject/UObjectIterator.h" +#include "EngineClasses/Components/ActorInterestComponent.h" #include "EngineClasses/SpatialNetConnection.h" #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" #include "SpatialGDKSettings.h" #include "SpatialConstants.h" +#include "UObject/UObjectIterator.h" DEFINE_LOG_CATEGORY(LogInterestFactory); +namespace +{ +static TMap ClientInterestDistancesSquared; +} + namespace SpatialGDK { +void GatherClientInterestDistances() +{ + ClientInterestDistancesSquared.Empty(); + + const AActor* DefaultActor = Cast(AActor::StaticClass()->GetDefaultObject()); + const float DefaultDistanceSquared = DefaultActor->NetCullDistanceSquared; + const float MaxDistanceSquared = GetDefault()->MaxNetCullDistanceSquared; + + // Gather ClientInterestDistance settings, and add any larger than the default radius to a list for processing. + TMap DiscoveredInterestDistancesSquared; + for (TObjectIterator It; It; ++It) + { + if (It->HasAnySpatialClassFlags(SPATIALCLASS_ServerOnly | SPATIALCLASS_NotSpatialType)) + { + continue; + } + if (It->HasAnyClassFlags(CLASS_NewerVersionExists)) + { + // This skips classes generated for hot reload etc (i.e. REINST_, SKEL_, TRASHCLASS_) + continue; + } + if (!It->IsChildOf()) + { + continue; + } + + const AActor* IteratedDefaultActor = Cast(It->GetDefaultObject()); + if (IteratedDefaultActor->NetCullDistanceSquared > DefaultDistanceSquared) + { + float ActorNetCullDistanceSquared = IteratedDefaultActor->NetCullDistanceSquared; + + if (MaxDistanceSquared != 0.f && IteratedDefaultActor->NetCullDistanceSquared > MaxDistanceSquared) + { + UE_LOG(LogInterestFactory, Warning, TEXT("NetCullDistanceSquared for %s too large, clamping from %f to %f"), + *It->GetName(), ActorNetCullDistanceSquared, MaxDistanceSquared); + + ActorNetCullDistanceSquared = MaxDistanceSquared; + } + + DiscoveredInterestDistancesSquared.Add(*It, ActorNetCullDistanceSquared); + } + } + + // Sort the map for iteration so that parent classes are seen before derived classes. This lets us skip + // derived classes that have a smaller interest distance than a parent class. + DiscoveredInterestDistancesSquared.KeySort([](const UClass& LHS, const UClass& RHS) { + return + LHS.IsChildOf(&RHS) ? -1 : + RHS.IsChildOf(&LHS) ? 1 : + 0; + }); + + // If an actor's interest distance is smaller than that of a parent class, there's no need to add interest for that actor. + // Can't do inline removal since the sorted order is only guaranteed when the map isn't changed. + for (const auto& ActorInterestDistance : DiscoveredInterestDistancesSquared) + { + bool bShouldAdd = true; + for (auto& OptimizedInterestDistance : ClientInterestDistancesSquared) + { + if (ActorInterestDistance.Key->IsChildOf(OptimizedInterestDistance.Key) && ActorInterestDistance.Value <= OptimizedInterestDistance.Value) + { + // No need to add this interest distance since it's captured in the optimized map already. + bShouldAdd = false; + break; + } + } + if (bShouldAdd) + { + ClientInterestDistancesSquared.Add(ActorInterestDistance.Key, ActorInterestDistance.Value); + } + } +} InterestFactory::InterestFactory(AActor* InActor, const FClassInfo& InInfo, USpatialNetDriver* InNetDriver) : Actor(InActor) , Info(InInfo) , NetDriver(InNetDriver) , PackageMap(InNetDriver->PackageMap) -{} +{ +} -Worker_ComponentData InterestFactory::CreateInterestData() +Worker_ComponentData InterestFactory::CreateInterestData() const { return CreateInterest().CreateInterestData(); } -Worker_ComponentUpdate InterestFactory::CreateInterestUpdate() +Worker_ComponentUpdate InterestFactory::CreateInterestUpdate() const { return CreateInterest().CreateInterestUpdate(); } -Interest InterestFactory::CreateInterest() +Interest InterestFactory::CreateInterest() const { if (!GetDefault()->bUsingQBI) { return Interest{}; } - if (Actor->GetNetConnection() != nullptr) + if (GetDefault()->bEnableServerQBI) { - return CreatePlayerOwnedActorInterest(); + if (Actor->GetNetConnection() != nullptr) + { + return CreatePlayerOwnedActorInterest(); + } + else + { + return CreateActorInterest(); + } } else { - return CreateActorInterest(); + if (Actor->IsA(APlayerController::StaticClass())) + { + return CreatePlayerOwnedActorInterest(); + } + else + { + return Interest{}; + } } } -Interest InterestFactory::CreateActorInterest() +Interest InterestFactory::CreateActorInterest() const { Interest NewInterest; - QueryConstraint DefinedConstraints = CreateDefinedConstraints(); + QueryConstraint SystemConstraints = CreateSystemDefinedConstraints(); - if (!DefinedConstraints.IsValid()) + if (!SystemConstraints.IsValid()) { return NewInterest; } Query NewQuery; - NewQuery.Constraint = DefinedConstraints; + NewQuery.Constraint = SystemConstraints; // TODO: Make result type handle components certain workers shouldn't see // e.g. Handover, OwnerOnly, etc. NewQuery.FullSnapshotResult = true; @@ -77,13 +172,13 @@ Interest InterestFactory::CreateActorInterest() return NewInterest; } -Interest InterestFactory::CreatePlayerOwnedActorInterest() +Interest InterestFactory::CreatePlayerOwnedActorInterest() const { - QueryConstraint DefinedConstraints = CreateDefinedConstraints(); + QueryConstraint SystemConstraints = CreateSystemDefinedConstraints(); // Servers only need the defined constraints Query ServerQuery; - ServerQuery.Constraint = DefinedConstraints; + ServerQuery.Constraint = SystemConstraints; ServerQuery.FullSnapshotResult = true; ComponentInterest ServerComponentInterest; @@ -94,9 +189,9 @@ Interest InterestFactory::CreatePlayerOwnedActorInterest() QueryConstraint ClientConstraint; - if (DefinedConstraints.IsValid()) + if (SystemConstraints.IsValid()) { - ClientConstraint.AndConstraint.Add(DefinedConstraints); + ClientConstraint.AndConstraint.Add(SystemConstraints); } if (LevelConstraints.IsValid()) @@ -111,9 +206,11 @@ Interest InterestFactory::CreatePlayerOwnedActorInterest() ComponentInterest ClientComponentInterest; ClientComponentInterest.Queries.Add(ClientQuery); + AddUserDefinedQueries(LevelConstraints, ClientComponentInterest.Queries); + Interest NewInterest; // Server Interest - if (DefinedConstraints.IsValid()) + if (SystemConstraints.IsValid() && GetDefault()->bEnableServerQBI) { NewInterest.ComponentInterestMap.Add(SpatialConstants::POSITION_COMPONENT_ID, ServerComponentInterest); } @@ -126,30 +223,28 @@ Interest InterestFactory::CreatePlayerOwnedActorInterest() return NewInterest; } -QueryConstraint InterestFactory::CreateDefinedConstraints() +void InterestFactory::AddUserDefinedQueries(const QueryConstraint& LevelConstraints, TArray& OutQueries) const { - QueryConstraint SystemDefinedConstraints = CreateSystemDefinedConstraints(); - QueryConstraint UserDefinedConstraints = CreateUserDefinedConstraints(); + check(Actor); + check(NetDriver != nullptr && NetDriver->ClassInfoManager); - QueryConstraint DefinedConstraints; - - if (SystemDefinedConstraints.IsValid()) + TArray ActorInterestComponents; + Actor->GetComponents(ActorInterestComponents); + if (ActorInterestComponents.Num() == 1) { - DefinedConstraints.OrConstraint.Add(SystemDefinedConstraints); + ActorInterestComponents[0]->CreateQueries(*NetDriver->ClassInfoManager, LevelConstraints, OutQueries); } - - if (UserDefinedConstraints.IsValid()) + else if (ActorInterestComponents.Num() > 1) { - DefinedConstraints.OrConstraint.Add(UserDefinedConstraints); + UE_LOG(LogInterestFactory, Error, TEXT("%s has more than one ActorInterestQueryComponent"), *Actor->GetPathName()); } - - return DefinedConstraints; } -QueryConstraint InterestFactory::CreateSystemDefinedConstraints() +QueryConstraint InterestFactory::CreateSystemDefinedConstraints() const { - QueryConstraint CheckoutRadiusConstraint = CreateCheckoutRadiusConstraint(); + QueryConstraint CheckoutRadiusConstraint = CreateCheckoutRadiusConstraints(); QueryConstraint AlwaysInterestedConstraint = CreateAlwaysInterestedConstraint(); + QueryConstraint AlwaysRelevantConstraint = CreateAlwaysRelevantConstraint(); QueryConstraint SystemDefinedConstraints; @@ -163,25 +258,75 @@ QueryConstraint InterestFactory::CreateSystemDefinedConstraints() SystemDefinedConstraints.OrConstraint.Add(AlwaysInterestedConstraint); } + if (AlwaysRelevantConstraint.IsValid()) + { + SystemDefinedConstraints.OrConstraint.Add(AlwaysRelevantConstraint); + } + return SystemDefinedConstraints; } -QueryConstraint InterestFactory::CreateUserDefinedConstraints() +QueryConstraint InterestFactory::CreateCheckoutRadiusConstraints() const { - return QueryConstraint{}; -} + // If the actor has a component to specify interest and that indicates that we shouldn't generate + // constraints based on NetCullDistanceSquared, abort. There is a check elsewhere to ensure that + // there is at most one ActorInterestQueryComponent. + TArray ActorInterestComponents; + Actor->GetComponents(ActorInterestComponents); + if (ActorInterestComponents.Num() == 1) + { + const UActorInterestComponent* ActorInterest = ActorInterestComponents[0]; + check(ActorInterest); + if (!ActorInterest->bUseNetCullDistanceSquaredForCheckoutRadius) + { + return QueryConstraint{}; + } + } -QueryConstraint InterestFactory::CreateCheckoutRadiusConstraint() -{ - QueryConstraint CheckoutRadiusConstraint; + // Checkout Radius constraints are defined by the NetCullDistanceSquared property on actors. + // - Checkout radius is a RelativeCylinder constraint on the player controller. + // - NetCullDistanceSquared on AActor is used to define the default checkout radius with no other constraints. + // - NetCullDistanceSquared on other actor types is used to define additional constraints if needed. + // - If a subtype defines a radius smaller than a parent type, then its requirements are already captured. + // - If a subtype defines a radius larger than all parent types, then it needs an additional constraint. + // - Other than the default from AActor, all radius constraints also include Component constraints to + // capture specific types, including all derived types of that actor. + + const AActor* DefaultActor = Cast(AActor::StaticClass()->GetDefaultObject()); + const float DefaultDistanceSquared = DefaultActor->NetCullDistanceSquared; + + QueryConstraint CheckoutRadiusConstraints; + + // Use AActor's ClientInterestDistance for the default radius (all actors in that radius will be checked out) + const float DefaultCheckoutRadiusMeters = FMath::Sqrt(DefaultDistanceSquared / (100.0f * 100.0f)); + QueryConstraint DefaultCheckoutRadiusConstraint; + DefaultCheckoutRadiusConstraint.RelativeCylinderConstraint = RelativeCylinderConstraint{ DefaultCheckoutRadiusMeters }; + CheckoutRadiusConstraints.OrConstraint.Add(DefaultCheckoutRadiusConstraint); - float CheckoutRadius = Actor->CheckoutRadius / 100.0f; // Convert to meters - CheckoutRadiusConstraint.RelativeCylinderConstraint = RelativeCylinderConstraint{ CheckoutRadius }; + // For every interest distance that we still want, add a constraint with the distance for the actor type and all of its derived types. + for (const auto& InterestDistanceSquared: ClientInterestDistancesSquared) + { + QueryConstraint CheckoutRadiusConstraint; + + QueryConstraint RadiusConstraint; + const float CheckoutRadiusMeters = FMath::Sqrt(InterestDistanceSquared.Value / (100.0f * 100.0f)); + RadiusConstraint.RelativeCylinderConstraint = RelativeCylinderConstraint{ CheckoutRadiusMeters }; + CheckoutRadiusConstraint.AndConstraint.Add(RadiusConstraint); + + QueryConstraint ActorTypeConstraint; + check(InterestDistanceSquared.Key); + AddTypeHierarchyToConstraint(*InterestDistanceSquared.Key, ActorTypeConstraint); + if (ActorTypeConstraint.IsValid()) + { + CheckoutRadiusConstraint.AndConstraint.Add(ActorTypeConstraint); + CheckoutRadiusConstraints.OrConstraint.Add(CheckoutRadiusConstraint); + } + } - return CheckoutRadiusConstraint; + return CheckoutRadiusConstraints; } -QueryConstraint InterestFactory::CreateAlwaysInterestedConstraint() +QueryConstraint InterestFactory::CreateAlwaysInterestedConstraint() const { QueryConstraint AlwaysInterestedConstraint; @@ -209,7 +354,28 @@ QueryConstraint InterestFactory::CreateAlwaysInterestedConstraint() return AlwaysInterestedConstraint; } -void InterestFactory::AddObjectToConstraint(UObjectPropertyBase* Property, uint8* Data, QueryConstraint& OutConstraint) + +QueryConstraint InterestFactory::CreateAlwaysRelevantConstraint() const +{ + QueryConstraint AlwaysRelevantConstraint; + + Worker_ComponentId ComponentIds[] = { + SpatialConstants::SINGLETON_COMPONENT_ID, + SpatialConstants::SINGLETON_MANAGER_COMPONENT_ID, + SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID + }; + + for (Worker_ComponentId ComponentId : ComponentIds) + { + QueryConstraint Constraint; + Constraint.ComponentConstraint = ComponentId; + AlwaysRelevantConstraint.OrConstraint.Add(Constraint); + } + + return AlwaysRelevantConstraint; +} + +void InterestFactory::AddObjectToConstraint(UObjectPropertyBase* Property, uint8* Data, QueryConstraint& OutConstraint) const { UObject* ObjectOfInterest = Property->GetObjectPropertyValue(Data); @@ -230,7 +396,19 @@ void InterestFactory::AddObjectToConstraint(UObjectPropertyBase* Property, uint8 OutConstraint.OrConstraint.Add(EntityIdConstraint); } -QueryConstraint InterestFactory::CreateLevelConstraints() +void InterestFactory::AddTypeHierarchyToConstraint(const UClass& BaseType, QueryConstraint& OutConstraint) const +{ + check(NetDriver && NetDriver->ClassInfoManager); + TArray ComponentIds = NetDriver->ClassInfoManager->GetComponentIdsForClassHierarchy(BaseType); + for (Worker_ComponentId ComponentId : ComponentIds) + { + QueryConstraint ComponentTypeConstraint; + ComponentTypeConstraint.ComponentConstraint = ComponentId; + OutConstraint.OrConstraint.Add(ComponentTypeConstraint); + } +} + +QueryConstraint InterestFactory::CreateLevelConstraints() const { QueryConstraint LevelConstraint; @@ -248,7 +426,7 @@ QueryConstraint InterestFactory::CreateLevelConstraints() // Create component constraints for every loaded sublevel for (const auto& LevelPath : LoadedLevels) { - const uint32 ComponentId = NetDriver->ClassInfoManager->SchemaDatabase->GetComponentIdFromLevelPath(LevelPath.ToString()); + const uint32 ComponentId = NetDriver->ClassInfoManager->GetComponentIdFromLevelPath(LevelPath.ToString()); if (ComponentId != SpatialConstants::INVALID_COMPONENT_ID) { QueryConstraint SpecificLevelConstraint; diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/OpUtils.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/OpUtils.cpp new file mode 100644 index 0000000000..618fda548c --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/OpUtils.cpp @@ -0,0 +1,63 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Utils/OpUtils.h" +#include "SpatialConstants.h" + +namespace SpatialGDK +{ +void FindFirstOpOfType(const TArray& InOpLists, const Worker_OpType InOpType, Worker_Op** OutOp) +{ + 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) + { + *OutOp = Op; + return; + } + } + } +} + +void FindFirstOpOfTypeForComponent(const TArray& InOpLists, const Worker_OpType InOpType, const Worker_ComponentId InComponentId, Worker_Op** OutOp) +{ + 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) && + GetComponentId(Op) == InComponentId) + { + *OutOp = Op; + return; + } + } + } +} + +Worker_ComponentId GetComponentId(const Worker_Op* Op) +{ + switch (Op->op_type) + { + case WORKER_OP_TYPE_ADD_COMPONENT: + return Op->add_component.data.component_id; + case WORKER_OP_TYPE_REMOVE_COMPONENT: + return Op->remove_component.component_id; + case WORKER_OP_TYPE_COMPONENT_UPDATE: + return Op->component_update.update.component_id; + case WORKER_OP_TYPE_AUTHORITY_CHANGE: + return Op->authority_change.component_id; + case WORKER_OP_TYPE_COMMAND_REQUEST: + return Op->command_request.request.component_id; + case WORKER_OP_TYPE_COMMAND_RESPONSE: + return Op->command_response.response.component_id; + default: + return SpatialConstants::INVALID_COMPONENT_ID; + } +} +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp new file mode 100644 index 0000000000..d969286a62 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/RPCContainer.cpp @@ -0,0 +1,73 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Utils/RPCContainer.h" + +#include "Schema/UnrealObjectRef.h" + +using namespace SpatialGDK; + +FPendingRPCParams::FPendingRPCParams(const FUnrealObjectRef& InTargetObjectRef, SpatialGDK::RPCPayload&& InPayload, int InReliableRPCIndex /* = 0 */) + : ReliableRPCIndex(InReliableRPCIndex) + , ObjectRef(InTargetObjectRef) + , Payload(MoveTemp(InPayload)) +{ +} + +void FRPCContainer::QueueRPC(FPendingRPCParamsPtr Params, ESchemaComponentType Type) +{ + FArrayOfParams& ArrayOfParams = QueuedRPCs.FindOrAdd(Type).FindOrAdd(Params->ObjectRef.Entity); + ArrayOfParams.Push(MoveTemp(Params)); +} + +void FRPCContainer::ProcessRPCs(const FProcessRPCDelegate& FunctionToApply, FArrayOfParams& RPCList) +{ + // TODO: UNR-1651 Find a way to drop queued RPCs + int NumProcessedParams = 0; + for (auto& Params : RPCList) + { + if (ApplyFunction(FunctionToApply, *Params)) + { + NumProcessedParams++; + } + else + { + break; + } + } + RPCList.RemoveAt(0, NumProcessedParams); +} + +void FRPCContainer::ProcessRPCs(const FProcessRPCDelegate& FunctionToApply) +{ + for (auto& RPCs : QueuedRPCs) + { + FRPCMap& MapOfQueues = RPCs.Value; + for(auto It = MapOfQueues.CreateIterator(); It; ++It) + { + FArrayOfParams& RPCList = It.Value(); + ProcessRPCs(FunctionToApply, RPCList); + if (RPCList.Num() == 0) + { + It.RemoveCurrent(); + } + } + } +} + +bool FRPCContainer::ObjectHasRPCsQueuedOfType(const Worker_EntityId& EntityId, ESchemaComponentType Type) const +{ + if(const FRPCMap* MapOfQueues = QueuedRPCs.Find(Type)) + { + if(const FArrayOfParams* RPCList = MapOfQueues->Find(EntityId)) + { + return (RPCList->Num() > 0); + } + } + + return false; +} + +bool FRPCContainer::ApplyFunction(const FProcessRPCDelegate& FunctionToApply, const FPendingRPCParams& Params) +{ + return FunctionToApply.Execute(Params); +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp index 0c26be7a66..156938b078 100644 --- a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetrics.cpp @@ -4,10 +4,16 @@ #include "Engine/Engine.h" #include "EngineGlobals.h" +#include "GameFramework/PlayerController.h" +#include "EngineClasses/SpatialNetConnection.h" #include "EngineClasses/SpatialNetDriver.h" +#include "EngineClasses/SpatialPackageMapClient.h" #include "Interop/Connection/SpatialWorkerConnection.h" #include "SpatialGDKSettings.h" +#include "Utils/SchemaUtils.h" + +DEFINE_LOG_CATEGORY(LogSpatialMetrics); void USpatialMetrics::Init(USpatialNetDriver* InNetDriver) { @@ -15,6 +21,9 @@ void USpatialMetrics::Init(USpatialNetDriver* InNetDriver) TimeBetweenMetricsReports = GetDefault()->MetricsReportRate; FramesSinceLastReport = 0; TimeOfLastReport = 0.0f; + + bRPCTrackingEnabled = false; + RPCTrackingStartTime = 0.0f; } void USpatialMetrics::TickMetrics() @@ -60,3 +69,223 @@ double USpatialMetrics::CalculateLoad() const return AverageFrameTime / TargetFrameTime; } + +void USpatialMetrics::SpatialStartRPCMetrics() +{ + if (bRPCTrackingEnabled) + { + UE_LOG(LogSpatialMetrics, Log, TEXT("Already recording RPC metrics")); + return; + } + + UE_LOG(LogSpatialMetrics, Log, TEXT("Recording RPC metrics")); + + bRPCTrackingEnabled = true; + RPCTrackingStartTime = FPlatformTime::Seconds(); + + // If RPC tracking is activated on a client, send a command to the server to start tracking. + if (!NetDriver->IsServer()) + { + FUnrealObjectRef PCObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromObject(Cast(NetDriver->GetSpatialOSNetConnection()->OwningActor)); + Worker_EntityId ControllerEntityId = PCObjectRef.Entity; + + if (ControllerEntityId != SpatialConstants::INVALID_ENTITY_ID) + { + Worker_CommandRequest Request = {}; + Request.component_id = SpatialConstants::DEBUG_METRICS_COMPONENT_ID; + Request.schema_type = Schema_CreateCommandRequest(SpatialConstants::DEBUG_METRICS_COMPONENT_ID, SpatialConstants::DEBUG_METRICS_START_RPC_METRICS_ID); + NetDriver->Connection->SendCommandRequest(ControllerEntityId, &Request, SpatialConstants::DEBUG_METRICS_START_RPC_METRICS_ID); + } + else + { + UE_LOG(LogSpatialMetrics, Warning, TEXT("SpatialStartRPCMetrics: Could not resolve local PlayerController entity! RPC metrics will not start on the server.")); + } + } +} + +void USpatialMetrics::OnStartRPCMetricsCommand() +{ + SpatialStartRPCMetrics(); +} + +void USpatialMetrics::SpatialStopRPCMetrics() +{ + if (!bRPCTrackingEnabled) + { + UE_LOG(LogSpatialMetrics, Log, TEXT("Could not stop recording RPC metrics. RPC metrics not yet started.")); + return; + } + + // Display recorded sent RPCs. + const double TrackRPCInterval = FPlatformTime::Seconds() - RPCTrackingStartTime; + UE_LOG(LogSpatialMetrics, Log, TEXT("Recorded %d unique RPCs over the last %.3f seconds:"), RecentRPCs.Num(), TrackRPCInterval); + + if (RecentRPCs.Num() > 0) + { + // NICELY log sent RPCs. + TArray RecentRPCArray; + RecentRPCs.GenerateValueArray(RecentRPCArray); + + // Show the most frequently called RPCs at the top. + RecentRPCArray.Sort([](const RPCStat& A, const RPCStat& B) + { + if (A.Type != B.Type) + { + return static_cast(A.Type) < static_cast(B.Type); + } + return A.Calls > B.Calls; + }); + + int MaxRPCNameLen = 0; + for (RPCStat& Stat : RecentRPCArray) + { + MaxRPCNameLen = FMath::Max(MaxRPCNameLen, Stat.Name.Len()); + } + + int TotalCalls = 0; + int TotalPayload = 0; + + UE_LOG(LogSpatialMetrics, Log, TEXT("---------------------------")); + UE_LOG(LogSpatialMetrics, Log, TEXT("Recently sent RPCs - %s:"), NetDriver->IsServer() ? TEXT("Server") : TEXT("Client")); + UE_LOG(LogSpatialMetrics, Log, TEXT("RPC Type | %s | # of calls | Calls/sec | Total payload | Avg. payload | Payload/sec"), *FString(TEXT("RPC Name")).RightPad(MaxRPCNameLen)); + + FString SeparatorLine = FString::Printf(TEXT("-------------------+-%s-+------------+------------+---------------+--------------+------------"), *FString::ChrN(MaxRPCNameLen, '-')); + + ESchemaComponentType PrevType = SCHEMA_Invalid; + for (RPCStat& Stat : RecentRPCArray) + { + FString RPCTypeField; + if (Stat.Type != PrevType) + { + RPCTypeField = RPCSchemaTypeToString(Stat.Type); + PrevType = Stat.Type; + UE_LOG(LogSpatialMetrics, Log, TEXT("%s"), *SeparatorLine); + } + UE_LOG(LogSpatialMetrics, Log, TEXT("%s | %s | %10d | %10.4f | %13d | %12.4f | %11.4f"), *RPCTypeField.RightPad(18), *Stat.Name.RightPad(MaxRPCNameLen), Stat.Calls, Stat.Calls / TrackRPCInterval, Stat.TotalPayload, (float)Stat.TotalPayload / Stat.Calls, Stat.TotalPayload / TrackRPCInterval); + TotalCalls += Stat.Calls; + TotalPayload += Stat.TotalPayload; + } + UE_LOG(LogSpatialMetrics, Log, TEXT("%s"), *SeparatorLine); + UE_LOG(LogSpatialMetrics, Log, TEXT("Total | %s | %10d | %10.4f | %13d | %12.4f | %11.4f"), *FString::ChrN(MaxRPCNameLen, ' '), TotalCalls, TotalCalls / TrackRPCInterval, TotalPayload, (float)TotalPayload / TotalCalls, TotalPayload / TrackRPCInterval); + + RecentRPCs.Empty(); + } + + bRPCTrackingEnabled = false; + + // If RPC tracking is stopped on a client, send a command to the server to stop tracking. + if (!NetDriver->IsServer()) + { + FUnrealObjectRef PCObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromObject(Cast(NetDriver->GetSpatialOSNetConnection()->OwningActor)); + Worker_EntityId ControllerEntityId = PCObjectRef.Entity; + + if (ControllerEntityId != SpatialConstants::INVALID_ENTITY_ID) + { + Worker_CommandRequest Request = {}; + Request.component_id = SpatialConstants::DEBUG_METRICS_COMPONENT_ID; + Request.schema_type = Schema_CreateCommandRequest(SpatialConstants::DEBUG_METRICS_COMPONENT_ID, SpatialConstants::DEBUG_METRICS_STOP_RPC_METRICS_ID); + NetDriver->Connection->SendCommandRequest(ControllerEntityId, &Request, SpatialConstants::DEBUG_METRICS_STOP_RPC_METRICS_ID); + } + else + { + UE_LOG(LogSpatialMetrics, Warning, TEXT("SpatialStopRPCMetrics: Could not resolve local PlayerController entity! RPC metrics will not stop on the server.")); + } + } +} + +void USpatialMetrics::OnStopRPCMetricsCommand() +{ + SpatialStopRPCMetrics(); +} + +void USpatialMetrics::SpatialModifySetting(const FString& Name, float Value) +{ + if (!NetDriver->IsServer()) + { + FUnrealObjectRef PCObjectRef = NetDriver->PackageMap->GetUnrealObjectRefFromObject(Cast(NetDriver->GetSpatialOSNetConnection()->OwningActor)); + Worker_EntityId ControllerEntityId = PCObjectRef.Entity; + + if (ControllerEntityId != SpatialConstants::INVALID_ENTITY_ID) + { + Worker_CommandRequest Request = {}; + Request.component_id = SpatialConstants::DEBUG_METRICS_COMPONENT_ID; + Request.schema_type = Schema_CreateCommandRequest(SpatialConstants::DEBUG_METRICS_COMPONENT_ID, SpatialConstants::DEBUG_METRICS_MODIFY_SETTINGS_ID); + + Schema_Object* RequestObject = Schema_GetCommandRequestObject(Request.schema_type); + SpatialGDK::AddStringToSchema(RequestObject, SpatialConstants::MODIFY_SETTING_PAYLOAD_NAME_ID, Name); + Schema_AddFloat(RequestObject, SpatialConstants::MODIFY_SETTING_PAYLOAD_VALUE_ID, Value); + + NetDriver->Connection->SendCommandRequest(ControllerEntityId, &Request, SpatialConstants::DEBUG_METRICS_MODIFY_SETTINGS_ID); + } + else + { + UE_LOG(LogSpatialMetrics, Warning, TEXT("SpatialModifySetting: Could not resolve local PlayerController entity! Setting will not be sent to server.")); + } + } + else + { + bool bKnownSetting = true; + if (Name == TEXT("ActorReplicationRateLimit")) + { + GetMutableDefault()->ActorReplicationRateLimit = static_cast(Value); + } + else if (Name == TEXT("EntityCreationRateLimit")) + { + GetMutableDefault()->EntityCreationRateLimit = static_cast(Value); + } + else if (Name == TEXT("PositionUpdateFrequency")) + { + GetMutableDefault()->PositionUpdateFrequency = Value; + } + else if (Name == TEXT("PositionDistanceThreshold")) + { + GetMutableDefault()->PositionDistanceThreshold = Value; + } + else + { + bKnownSetting = false; + } + + if (bKnownSetting) + { + UE_LOG(LogSpatialMetrics, Log, TEXT("SpatialModifySetting: Spatial GDK setting %s set to %f"), *Name, Value); + } + else + { + UE_LOG(LogSpatialMetrics, Warning, TEXT("SpatialModifySetting: Invalid setting %s"), *Name); + } + } +} + +void USpatialMetrics::OnModifySettingCommand(Schema_Object* CommandPayload) +{ + FString Name = SpatialGDK::GetStringFromSchema(CommandPayload, SpatialConstants::MODIFY_SETTING_PAYLOAD_NAME_ID); + float Value = Schema_GetFloat(CommandPayload, SpatialConstants::MODIFY_SETTING_PAYLOAD_VALUE_ID); + + SpatialModifySetting(Name, Value); +} + +void USpatialMetrics::TrackSentRPC(UFunction* Function, ESchemaComponentType RPCType, int PayloadSize) +{ + if (!bRPCTrackingEnabled) + { + return; + } + + FString FunctionName = FString::Printf(TEXT("%s::%s"), *Function->GetOuter()->GetName(), *Function->GetName()); + + if (RecentRPCs.Find(FunctionName) == nullptr) + { + RPCStat Stat; + Stat.Name = FunctionName; + Stat.Type = RPCType; + Stat.Calls = 0; + Stat.TotalPayload = 0; + + RecentRPCs.Add(FunctionName, Stat); + } + + RPCStat& Stat = RecentRPCs[FunctionName]; + Stat.Calls++; + Stat.TotalPayload += PayloadSize; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetricsDisplay.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetricsDisplay.cpp new file mode 100644 index 0000000000..9928cd2237 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialMetricsDisplay.cpp @@ -0,0 +1,258 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Utils/SpatialMetricsDisplay.h" + +#include "Debug/DebugDrawService.h" +#include "Engine/Canvas.h" +#include "Engine/Engine.h" +#include "EngineClasses/SpatialNetDriver.h" +#include "Interop/Connection/SpatialWorkerConnection.h" +#include "Net/UnrealNetwork.h" +#include "Utils/SpatialMetrics.h" + +#if USE_SERVER_PERF_COUNTERS +#include "Net/PerfCountersHelpers.h" +#endif + +ASpatialMetricsDisplay::ASpatialMetricsDisplay(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.bStartWithTickEnabled = true; + + bReplicates = true; + bAlwaysRelevant = true; + + NetUpdateFrequency = 1.f; + +#if USE_SERVER_PERF_COUNTERS + IPerfCountersModule& PerformanceModule = IPerfCountersModule::Get(); + if (PerformanceModule.GetPerformanceCounters() == nullptr) + { + PerformanceModule.CreatePerformanceCounters(); + } +#endif +} + +void ASpatialMetricsDisplay::BeginPlay() +{ + Super::BeginPlay(); + + WorkerStats.Reserve(PreallocatedWorkerCount); + WorkerStatsLastUpdateTime.Reserve(PreallocatedWorkerCount); + + if (!GetWorld()->IsServer() && GetDefault()->bEnableMetricsDisplay) + { + ToggleStatDisplay(); + } +} + +void ASpatialMetricsDisplay::Destroyed() +{ + Super::Destroyed(); + + if (DrawDebugDelegateHandle.IsValid()) + { + UDebugDrawService::Unregister(DrawDebugDelegateHandle); + } +} + +bool ASpatialMetricsDisplay::ServerUpdateWorkerStats_Validate(const float Time, const FWorkerStats& OneWorkerStats) +{ + return true; +} + +void ASpatialMetricsDisplay::ServerUpdateWorkerStats_Implementation(const float Time, const FWorkerStats& OneWorkerStats) +{ + int32 StatsIndex = WorkerStats.Find(OneWorkerStats); + + if (StatsIndex == INDEX_NONE) + { + StatsIndex = WorkerStats.AddDefaulted(); + } + + WorkerStats[StatsIndex] = OneWorkerStats; + + float& WorkerUpdateTime = WorkerStatsLastUpdateTime.FindOrAdd(OneWorkerStats.WorkerName); + WorkerUpdateTime = Time; +} + +void ASpatialMetricsDisplay::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ASpatialMetricsDisplay, WorkerStats); +} + +void ASpatialMetricsDisplay::DrawDebug(class UCanvas* Canvas, APlayerController* Controller) +{ + enum StatColumns + { + StatColumn_Worker, + StatColumn_AverageFrameTime, + StatColumn_MovementCorrections, + StatColumn_ReplicationLimit, + StatColumn_Last + }; + + const uint32 StatDisplayStartX = 25; + const uint32 StatDisplayStartY = 80; + + const FString StatColumnTitles[StatColumn_Last] = { TEXT("Worker"), TEXT("Frame"), TEXT("Movement Corrections"), TEXT("Replication Limit") }; + const uint32 StatColumnOffsets[StatColumn_Last] = { 0, 160, 80, 160 }; + const uint32 StatRowOffset = 20; + + const FString StatSectionTitle = TEXT("Spatial Metrics Display"); + + if (GetWorld()->IsServer()) + { + return; + } + + FFontRenderInfo FontRenderInfo = Canvas->CreateFontRenderInfo(false, true); + + UFont* RenderFont = GEngine->GetSmallFont(); + + uint32 DrawX = StatDisplayStartX; + uint32 DrawY = StatDisplayStartY; + + Canvas->SetDrawColor(FColor::Green); + Canvas->DrawText(RenderFont, StatSectionTitle, DrawX, DrawY, 1.0f, 1.0f, FontRenderInfo); + DrawY += StatRowOffset; + + Canvas->SetDrawColor(FColor::White); + for (int i = 0; i < StatColumn_Last; ++i) + { + DrawX += StatColumnOffsets[i]; + Canvas->DrawText(RenderFont, StatColumnTitles[i], DrawX, DrawY, 1.0f, 1.0f, FontRenderInfo); + } + + DrawY += StatRowOffset; + + for (const FWorkerStats& OneWorkerStats : WorkerStats) + { + DrawX = StatDisplayStartX + StatColumnOffsets[StatColumn_Worker]; + Canvas->DrawText(RenderFont, FString::Printf(TEXT("%s"), *OneWorkerStats.WorkerName), DrawX, DrawY, 1.0f, 1.0f, FontRenderInfo); + + DrawX += StatColumnOffsets[StatColumn_AverageFrameTime]; + Canvas->DrawText(RenderFont, FString::Printf(TEXT("%.2f ms"), 1000.f / OneWorkerStats.AverageFPS), DrawX, DrawY, 1.0f, 1.0f, FontRenderInfo); + + DrawX += StatColumnOffsets[StatColumn_MovementCorrections]; + Canvas->DrawText(RenderFont, FString::Printf(TEXT("%.4f"), OneWorkerStats.ServerMovementCorrections), DrawX, DrawY, 1.0f, 1.0f, FontRenderInfo); + + DrawX += StatColumnOffsets[StatColumn_ReplicationLimit]; + Canvas->DrawText(RenderFont, FString::Printf(TEXT("%d:%d"), OneWorkerStats.ServerConsiderListSize, OneWorkerStats.ServerReplicationLimit), DrawX, DrawY, 1.0f, 1.0f, FontRenderInfo); + + DrawY += StatRowOffset; + } +} + +void ASpatialMetricsDisplay::ToggleStatDisplay() +{ + if (DrawDebugDelegateHandle.IsValid()) + { + UDebugDrawService::Unregister(DrawDebugDelegateHandle); + DrawDebugDelegateHandle.Reset(); + } + else + { + DrawDebugDelegateHandle = UDebugDrawService::Register(TEXT("Game"), FDebugDrawDelegate::CreateUObject(this, &ASpatialMetricsDisplay::DrawDebug)); + } +} + +void ASpatialMetricsDisplay::Tick(float DeltaSeconds) +{ + Super::Tick(DeltaSeconds); + +#if !UE_BUILD_SHIPPING + + if (!GetWorld()->IsServer() || !HasActorBegunPlay()) + { + return; + } + + USpatialNetDriver* SpatialNetDriver = Cast(GetWorld()->GetNetDriver()); + + if (SpatialNetDriver == nullptr || + SpatialNetDriver->Connection == nullptr || + SpatialNetDriver->SpatialMetrics == nullptr) + { + return; + } + + // Cleanup stats entries for workers that have not reported stats for awhile + if (Role == ROLE_Authority) + { + const float CurrentTime = SpatialNetDriver->Time; + TArray WorkerStatsToRemove; + + for (const FWorkerStats& OneWorkerStats : WorkerStats) + { + if (ShouldRemoveStats(SpatialNetDriver->Time, OneWorkerStats)) + { + WorkerStatsToRemove.Add(OneWorkerStats); + } + } + + for (const FWorkerStats& OneWorkerStats : WorkerStatsToRemove) + { + WorkerStatsLastUpdateTime.Remove(OneWorkerStats.WorkerName); + WorkerStats.Remove(OneWorkerStats); + } + } + + const USpatialMetrics& Metrics = *SpatialNetDriver->SpatialMetrics; + + FWorkerStats Stats{}; + Stats.WorkerName = SpatialNetDriver->Connection->GetWorkerId().Left(WorkerNameMaxLength).ToLower(); + Stats.AverageFPS = Metrics.GetAverageFPS(); + Stats.ServerConsiderListSize = SpatialNetDriver->GetConsiderListSize(); + Stats.ServerReplicationLimit = GetDefault()->ActorReplicationRateLimit; + +#if USE_SERVER_PERF_COUNTERS + float MovementCorrectionsPerSecond = 0.f; + int32 NumServerMoveCorrections = 0; + float WorldTime = GetWorld()->GetTimeSeconds(); + NumServerMoveCorrections = PerfCountersGet(TEXT("NumServerMoveCorrections"), NumServerMoveCorrections); + MovementCorrectionRecord OldestRecord; + if (MovementCorrectionRecords.Peek(OldestRecord)) + { + const float WorldTimeDelta = WorldTime - OldestRecord.Time; + const int32 CorrectionsDelta = NumServerMoveCorrections - OldestRecord.MovementCorrections; + if (WorldTimeDelta > 0.f && CorrectionsDelta > 0) + { + MovementCorrectionsPerSecond = CorrectionsDelta / WorldTimeDelta; + } + + // Store the most recent 30 seconds of game time worth of measurements + if (WorldTimeDelta > 30.f) + { + MovementCorrectionRecords.Pop(); + } + } + Stats.ServerMovementCorrections = MovementCorrectionsPerSecond; + + // Don't store a measurement if time hasn't progressed + if (DeltaSeconds > 0.f) + { + MovementCorrectionRecords.Enqueue({ NumServerMoveCorrections, WorldTime }); + } +#endif // USE_SERVER_PERF_COUNTERS + + ServerUpdateWorkerStats(SpatialNetDriver->Time, Stats); + +#endif // !UE_BUILD_SHIPPING +} + +bool ASpatialMetricsDisplay::ShouldRemoveStats(const float CurrentTime, const FWorkerStats& OneWorkerStats) const +{ + const float* LastUpdateTime = WorkerStatsLastUpdateTime.Find(OneWorkerStats.WorkerName); + + if (LastUpdateTime == nullptr) + { + return true; + } + + const float TimeSinceUpdate = CurrentTime - *LastUpdateTime; + return TimeSinceUpdate > DropStatsIfNoUpdateForTime; +} diff --git a/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp new file mode 100644 index 0000000000..83c744d5da --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Private/Utils/SpatialStatics.cpp @@ -0,0 +1,110 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "Utils/SpatialStatics.h" + +#include "Engine/World.h" +#include "EngineClasses/SpatialNetDriver.h" +#include "GeneralProjectSettings.h" +#include "SpatialConstants.h" +#include "SpatialGDKSettings.h" +#include "Utils/ActorGroupManager.h" + +bool USpatialStatics::IsSpatialNetworkingEnabled() +{ + return GetDefault()->bSpatialNetworking; +} + +UActorGroupManager* USpatialStatics::GetActorGroupManager(const UObject* WorldContext) +{ + if (const UWorld* World = WorldContext->GetWorld()) + { + if (const USpatialNetDriver* SpatialNetDriver = Cast(World->GetNetDriver())) + { + return SpatialNetDriver->ActorGroupManager; + } + } + return nullptr; +} + +FName USpatialStatics::GetCurrentWorkerType(const UObject* WorldContext) +{ + if (const UWorld* World = WorldContext->GetWorld()) + { + if (const UGameInstance* GameInstance = World->GetGameInstance()) + { + return GameInstance->GetSpatialWorkerType(); + } + } + + return NAME_None; +} + +bool USpatialStatics::IsSpatialOffloadingEnabled() +{ + return IsSpatialNetworkingEnabled() && GetDefault()->bEnableOffloading; +} + +bool USpatialStatics::IsActorGroupOwnerForActor(const AActor* Actor) +{ + if (Actor == nullptr) + { + return false; + } + + return IsActorGroupOwnerForClass(Actor, Actor->GetClass()); +} + +bool USpatialStatics::IsActorGroupOwnerForClass(const UObject* WorldContextObject, const TSubclassOf ActorClass) +{ + if (UActorGroupManager* ActorGroupManager = GetActorGroupManager(WorldContextObject)) + { + const FName ClassWorkerType = ActorGroupManager->GetWorkerTypeForClass(ActorClass); + const FName CurrentWorkerType = GetCurrentWorkerType(WorldContextObject); + return ClassWorkerType == CurrentWorkerType; + } + + if (const UWorld* World = WorldContextObject->GetWorld()) + { + return World->GetNetMode() != NM_Client; + } + + return false; +} + +bool USpatialStatics::IsActorGroupOwner(const UObject* WorldContextObject, const FName ActorGroup) +{ + if (UActorGroupManager* ActorGroupManager = GetActorGroupManager(WorldContextObject)) + { + const FName ActorGroupWorkerType = ActorGroupManager->GetWorkerTypeForActorGroup(ActorGroup); + const FName CurrentWorkerType = GetCurrentWorkerType(WorldContextObject); + return ActorGroupWorkerType == CurrentWorkerType; + } + + if (const UWorld* World = WorldContextObject->GetWorld()) + { + return World->GetNetMode() != NM_Client; + } + + return false; +} + +FName USpatialStatics::GetActorGroupForActor(const AActor* Actor) +{ + if (UActorGroupManager* ActorGroupManager = GetActorGroupManager(Actor)) + { + UClass* ActorClass = Actor->GetClass(); + return ActorGroupManager->GetActorGroupForClass(ActorClass); + } + + return SpatialConstants::DefaultActorGroup; +} + +FName USpatialStatics::GetActorGroupForClass(const UObject* WorldContextObject, const TSubclassOf ActorClass) +{ + if (UActorGroupManager* ActorGroupManager = GetActorGroupManager(WorldContextObject)) + { + return ActorGroupManager->GetActorGroupForClass(ActorClass); + } + + return SpatialConstants::DefaultActorGroup; +} diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/ActorInterestComponent.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/ActorInterestComponent.h new file mode 100644 index 0000000000..d18ee7abd0 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/Components/ActorInterestComponent.h @@ -0,0 +1,43 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "Interop/SpatialInterestConstraints.h" + +#include "ActorInterestComponent.generated.h" + +namespace SpatialGDK +{ +struct Query; +} +class USpatialClassInfoManager; + +/** + * Creates a set of SpatialOS Queries for describing interest that this actor has in other entities. + */ +UCLASS(ClassGroup=(SpatialGDK), NotSpatialType, Meta=(BlueprintSpawnableComponent)) +class SPATIALGDK_API UActorInterestComponent final : public UActorComponent +{ + GENERATED_BODY() + +public: + UActorInterestComponent() = default; + ~UActorInterestComponent() = default; + + void CreateQueries(const USpatialClassInfoManager& ClassInfoManager, const SpatialGDK::QueryConstraint& AdditionalConstraints, TArray& OutQueries) const; + + /** + * Whether to use NetCullDistanceSquared to generate constraints relative to the Actor that this component is attached to. + */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Interest") + bool bUseNetCullDistanceSquaredForCheckoutRadius = true; + + /** + * The Queries associated with this component. + */ + UPROPERTY(BlueprintReadonly, EditDefaultsOnly, Category = "Interest") + TArray Queries; + +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h index 53b29b4ae3..a2051e3848 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialActorChannel.h @@ -8,6 +8,7 @@ #include "Interop/Connection/SpatialWorkerConnection.h" #include "Interop/SpatialClassInfoManager.h" #include "Interop/SpatialStaticComponentView.h" +#include "Runtime/Launch/Resources/Version.h" #include "Schema/StandardLibrary.h" #include "SpatialCommonTypes.h" #include "Utils/RepDataUtils.h" @@ -70,15 +71,19 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel FORCEINLINE bool IsOwnedByWorker() const { const TArray& WorkerAttributes = NetDriver->Connection->GetWorkerAttributes(); - if (const WorkerRequirementSet* WorkerRequirementsSet = NetDriver->StaticComponentView->GetComponentData(EntityId)->ComponentWriteAcl.Find(SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID)) + + if (const SpatialGDK::EntityAcl* EntityACL = NetDriver->StaticComponentView->GetComponentData(EntityId)) { - for (const WorkerAttributeSet& AttributeSet : *WorkerRequirementsSet) + if (const WorkerRequirementSet* WorkerRequirementsSet = EntityACL->ComponentWriteAcl.Find(SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID)) { - for (const FString& Attribute : AttributeSet) + for (const WorkerAttributeSet& AttributeSet : *WorkerRequirementsSet) { - if (WorkerAttributes.Contains(Attribute)) + for (const FString& Attribute : AttributeSet) { - return true; + if (WorkerAttributes.Contains(Attribute)) + { + return true; + } } } } @@ -105,17 +110,22 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel } // UChannel interface +#if ENGINE_MINOR_VERSION <= 20 virtual void Init(UNetConnection * InConnection, int32 ChannelIndex, bool bOpenedLocally) override; virtual int64 Close() override; +#else + virtual void Init(UNetConnection * InConnection, int32 ChannelIndex, EChannelCreateFlags CreateFlag) override; + virtual int64 Close(EChannelCloseReason Reason) override; +#endif virtual int64 ReplicateActor() override; virtual void SetChannelActor(AActor* InActor) override; bool TryResolveActor(); - bool ReplicateSubobject(UObject* Obj, const FClassInfo& Info, const FReplicationFlags& RepFlags); + bool ReplicateSubobject(UObject* Obj, const FReplicationFlags& RepFlags); virtual bool ReplicateSubobject(UObject* Obj, FOutBunch& Bunch, const FReplicationFlags& RepFlags) override; - TMap GetHandoverSubobjects(); + TMap GetHandoverSubobjects(); FRepChangeState CreateInitialRepChangeState(TWeakObjectPtr Object); FHandoverChangeState CreateInitialHandoverChangeState(const FClassInfo& ClassInfo); @@ -123,7 +133,6 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel // For an object that is replicated by this channel (i.e. this channel's actor or its component), find out whether a given handle is an array. bool IsDynamicArrayHandle(UObject* Object, uint16 Handle); - void ProcessOwnershipChange(); FObjectReplicator& PreReceiveSpatialUpdate(UObject* TargetObject); void PostReceiveSpatialUpdate(UObject* TargetObject, const TArray& RepNotifies); @@ -134,37 +143,51 @@ class SPATIALGDK_API USpatialActorChannel : public UActorChannel void RemoveRepNotifiesWithUnresolvedObjs(TArray& RepNotifies, const FRepLayout& RepLayout, const FObjectReferencesMap& RefMap, UObject* Object); void UpdateShadowData(); + void UpdateSpatialPositionWithFrequencyCheck(); + void UpdateSpatialPosition(); + + void ServerProcessOwnershipChange(); + void ClientProcessOwnershipChange(bool bNewNetOwned); FORCEINLINE void MarkInterestDirty() { bInterestDirty = true; } FORCEINLINE bool GetInterestDirty() const { return bInterestDirty; } - // If this actor channel is responsible for creating a new entity, this will be set to true once the entity is created. - bool bCreatedEntity; - - // If this actor channel is responsible for creating a new entity, this will be set to true during initial replication. - bool bCreatingNewEntity; + FORCEINLINE void StartListening() { bIsListening = true; } + FORCEINLINE bool IsListening() { return bIsListening; } + const FClassInfo* TryResolveNewDynamicSubobjectAndGetClassInfo(UObject* Object); protected: // UChannel Interface +#if ENGINE_MINOR_VERSION <= 20 virtual bool CleanUp(const bool bForDestroy) override; +#else + virtual bool CleanUp(const bool bForDestroy, EChannelCloseReason CloseReason) override; +#endif private: - void ServerProcessOwnershipChange(); - void ClientProcessOwnershipChange(); + void DynamicallyAttachSubobject(UObject* Object); void DeleteEntityIfAuthoritative(); bool IsSingletonEntity(); - void UpdateSpatialPosition(); void SendPositionUpdate(AActor* InActor, Worker_EntityId InEntityId, const FVector& NewPosition); void InitializeHandoverShadowData(TArray& ShadowData, UObject* Object); FHandoverChangeState GetHandoverChangeList(TArray& ShadowData, UObject* Object); +public: + // If this actor channel is responsible for creating a new entity, this will be set to true once the entity is created. + bool bCreatedEntity; + + // If this actor channel is responsible for creating a new entity, this will be set to true during initial replication. + bool bCreatingNewEntity; + + TSet> PendingDynamicSubobjects; + private: Worker_EntityId EntityId; - bool bFirstTick; bool bInterestDirty; + bool bIsListening; // Used on the client to track gaining/losing ownership. bool bNetOwned; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialGameInstance.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialGameInstance.h index fb8b8ca098..d8e3047c10 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialGameInstance.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialGameInstance.h @@ -25,6 +25,10 @@ class SPATIALGDK_API USpatialGameInstance : public UGameInstance #endif virtual void StartGameInstance() override; + //~ Begin UObject Interface + virtual bool ProcessConsoleExec(const TCHAR* Cmd, FOutputDevice& Ar, UObject* Executor) override; + //~ End UObject Interface + // bResponsibleForSnapshotLoading exists to have persistent knowledge if this worker has authority over the GSM during ServerTravel. bool bResponsibleForSnapshotLoading = false; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetBitWriter.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetBitWriter.h index e193ab3d78..64d66080ea 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetBitWriter.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetBitWriter.h @@ -6,6 +6,8 @@ #include "UObject/CoreNet.h" #include "Schema/UnrealObjectRef.h" +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialNetSerialize, All, All); + class USpatialPackageMapClient; class SPATIALGDK_API FSpatialNetBitWriter : public FNetBitWriter diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetConnection.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetConnection.h index 2e42e068ae..845ce938ff 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetConnection.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetConnection.h @@ -21,6 +21,7 @@ class SPATIALGDK_API USpatialNetConnection : public UIpConnection public: USpatialNetConnection(const FObjectInitializer& ObjectInitializer); + // Begin NetConnection Interface virtual void BeginDestroy() override; virtual void InitBase(UNetDriver* InDriver, class FSocket* InSocket, const FURL& InURL, EConnectionState InState, int32 InMaxPacket = 0, int32 InPacketOverhead = 0) override; @@ -30,17 +31,19 @@ class SPATIALGDK_API USpatialNetConnection : public UIpConnection virtual void LowLevelSend(void* Data, int32 CountBits, FOutPacketTraits& Traits) override; #endif virtual bool ClientHasInitializedLevelFor(const AActor* TestActor) const override; - virtual void Tick() override; virtual int32 IsNetReady(bool Saturate) override; /** Called by PlayerController to tell connection about client level visibility change */ virtual void UpdateLevelVisibility(const FName& PackageName, bool bIsVisible) override; + virtual bool IsReplayConnection() const override { return false; } + // These functions don't make a lot of sense in a SpatialOS implementation. virtual FString LowLevelGetRemoteAddress(bool bAppendPort = false) override { return TEXT(""); } virtual FString LowLevelDescribe() override { return TEXT(""); } virtual FString RemoteAddressToString() override { return TEXT(""); } /////// + // End NetConnection Interface void InitHeartbeat(class FTimerManager* InTimerManager, Worker_EntityId InPlayerControllerEntity); void SetHeartbeatTimeoutTimer(); @@ -51,6 +54,8 @@ class SPATIALGDK_API USpatialNetConnection : public UIpConnection void OnHeartbeat(); void UpdateActorInterest(AActor* Actor); + void ClientNotifyClientHasQuit(); + UPROPERTY() bool bReliableSpatialConnection; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h index a164435e79..b6f8407f18 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialNetDriver.h @@ -26,12 +26,14 @@ class USpatialWorkerConnection; class USpatialDispatcher; class USpatialSender; class USpatialReceiver; +class UActorGroupManager; class USpatialClassInfoManager; class UGlobalStateManager; class USpatialPlayerSpawner; class USpatialStaticComponentView; class USnapshotManager; class USpatialMetrics; +class ASpatialMetricsDisplay; class UEntityPool; @@ -46,7 +48,11 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver GENERATED_BODY() public: + + USpatialNetDriver(const FObjectInitializer& ObjectInitializer); + // Begin UObject Interface + virtual void BeginDestroy() override; virtual void PostInitProperties() override; // End UObject Interface @@ -62,6 +68,7 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver virtual void TickFlush(float DeltaTime) override; virtual bool IsLevelInitializedForActor(const AActor* InActor, const UNetConnection* InConnection) const override; virtual void NotifyActorDestroyed(AActor* Actor, bool IsSeamlessTravel = false) override; + virtual void Shutdown() override; // End UNetDriver interface. virtual void OnOwnerUpdated(AActor* Actor); @@ -89,7 +96,9 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver void RemoveActorChannel(Worker_EntityId EntityId); TMap& GetEntityToActorChannelMap(); + USpatialActorChannel* GetOrCreateSpatialActorChannel(UObject* TargetObject); USpatialActorChannel* GetActorChannelByEntityId(Worker_EntityId EntityId) const; + USpatialActorChannel* CreateSpatialActorChannel(AActor* Actor, USpatialNetConnection* InConnection); DECLARE_DELEGATE(PostWorldWipeDelegate); @@ -104,6 +113,8 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver UPROPERTY() USpatialReceiver* Receiver; UPROPERTY() + UActorGroupManager* ActorGroupManager; + UPROPERTY() USpatialClassInfoManager* ClassInfoManager; UPROPERTY() UGlobalStateManager* GlobalStateManager; @@ -119,6 +130,10 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver UEntityPool* EntityPool; UPROPERTY() USpatialMetrics* SpatialMetrics; + UPROPERTY() + ASpatialMetricsDisplay* SpatialMetricsDisplay; + + Worker_EntityId WorkerEntityId = SpatialConstants::INVALID_ENTITY_ID; TMap> SingletonActorChannels; @@ -127,6 +142,9 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver void StopIgnoringAuthoritativeDestruction() { bAuthoritativeDestruction = true; } #if !UE_BUILD_SHIPPING + int32 GetConsiderListSize() const { return ConsiderListSize; } +#endif + uint32 GetNextReliableRPCId(AActor* Actor, ESchemaComponentType RPCType, UObject* TargetObject); void OnReceivedReliableRPC(AActor* Actor, ESchemaComponentType RPCType, FString WorkerId, uint32 RPCId, UObject* TargetObject, UFunction* Function); void OnRPCAuthorityGained(AActor* Actor, ESchemaComponentType RPCType); @@ -144,14 +162,19 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver using FRPCTypeToReliableRPCIdMap = TMap; // Per actor, maps from RPC type to the reliable RPC index used to detect if reliable RPCs go out of order. TMap, FRPCTypeToReliableRPCIdMap> ReliableRPCIdMap; -#endif // !UE_BUILD_SHIPPING void DelayedSendDeleteEntityRequest(Worker_EntityId EntityId, float Delay); +#if WITH_EDITOR + // We store the PlayInEditorID associated with this NetDriver to handle replace a worker initialization when in the editor. + int32 PlayInEditorID; +#endif + private: TUniquePtr SpatialOutputDevice; TMap EntityToActorChannel; + TArray QueuedStartupOpLists; FTimerManager TimerManager; @@ -159,11 +182,12 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver bool bConnectAsClient; bool bPersistSpatialConnection; bool bWaitingForAcceptingPlayersToSpawn; + bool bIsReadyToStart; + FString SnapshotToLoad; void InitiateConnectionToSpatialOS(const FURL& URL); - void InitializeSpatialOutputDevice(); void CreateAndInitializeCoreClasses(); @@ -173,6 +197,9 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver void HandleOngoingServerTravel(); + void HandleStartupOpQueueing(const TArray& InOpLists); + bool FindAndDispatchStartupOps(const TArray& InOpLists); + UFUNCTION() void OnMapLoaded(UWorld* LoadedWorld); @@ -189,6 +216,8 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver void ServerReplicateActors_ProcessPrioritizedActors(UNetConnection* Connection, const TArray& ConnectionViewers, FActorPriority** PriorityActors, const int32 FinalSortedCount, int32& OutUpdated); #endif + void ProcessRPC(AActor* Actor, UObject* SubObject, UFunction* Function, void* Parameters); + friend USpatialNetConnection; friend USpatialWorkerConnection; @@ -196,4 +225,17 @@ class SPATIALGDK_API USpatialNetDriver : public UIpNetDriver // The SpatialSender uses these indexes to retry any failed reliable RPCs // in the correct order, if needed. int NextRPCIndex; + + float TimeWhenPositionLastUpdated; + + // Counter for giving each connected client a unique IP address to satisfy Unreal's requirement of + // each client having a unique IP address in the UNetDriver::MappedClientConnections map. + // The GDK does not use this address for any networked purpose, only bookkeeping. + uint32 UniqueClientIpAddressCounter = 0; + + FDelegateHandle SpatialDeploymentStartHandle; + +#if !UE_BUILD_SHIPPING + int32 ConsiderListSize = 0; +#endif }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h index a69d48e258..7396935bb6 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h +++ b/SpatialGDK/Source/SpatialGDK/Public/EngineClasses/SpatialPackageMapClient.h @@ -31,7 +31,10 @@ class SPATIALGDK_API USpatialPackageMapClient : public UPackageMapClient void RemovePendingCreationEntityId(Worker_EntityId EntityId); FNetworkGUID ResolveEntityActor(AActor* Actor, Worker_EntityId EntityId); + void ResolveSubobject(UObject* Object, const FUnrealObjectRef& ObjectRef); + void RemoveEntityActor(Worker_EntityId EntityId); + void RemoveSubobject(const FUnrealObjectRef& ObjectRef); // This function is ONLY used in SpatialReceiver::GetOrCreateActor to undo // the unintended registering of objects when looking them up with static paths. @@ -70,7 +73,10 @@ class SPATIALGDK_API FSpatialNetGUIDCache : public FNetGUIDCache FSpatialNetGUIDCache(class USpatialNetDriver* InDriver); FNetworkGUID AssignNewEntityActorNetGUID(AActor* Actor, Worker_EntityId EntityId); + void AssignNewSubobjectNetGUID(UObject* Subobject, const FUnrealObjectRef& SubobjectRef); + void RemoveEntityNetGUID(Worker_EntityId EntityId); + void RemoveSubobjectNetGUID(const FUnrealObjectRef& SubobjectRef); FNetworkGUID AssignNewStablyNamedObjectNetGUID(UObject* Object); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/ConnectionConfig.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/ConnectionConfig.h index 7923e15cb7..e263bf6baf 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/ConnectionConfig.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/ConnectionConfig.h @@ -7,6 +7,7 @@ #include "Misc/CommandLine.h" #include "Misc/Parse.h" #include "SpatialConstants.h" +#include "SpatialGDKSettings.h" #include @@ -27,7 +28,7 @@ struct FConnectionConfig #if PLATFORM_IOS || PLATFORM_ANDROID // On a mobile platform, you can only be a client worker, and therefore use the external IP. - WorkerType = SpatialConstants::ClientWorkerType; + WorkerType = SpatialConstants::DefaultClientWorkerType.ToString(); UseExternalIp = true; #endif FString LinkProtocolString; @@ -76,7 +77,11 @@ struct FReceptionistConfig : public FConnectionConfig if (!IpV4RegexMatcher.FindNext()) { // If an IP is not specified then use default. - ReceptionistHost = SpatialConstants::LOCAL_HOST; + ReceptionistHost = GetDefault()->DefaultReceptionistHost; + if (ReceptionistHost.Compare(SpatialConstants::LOCAL_HOST) != 0) + { + UseExternalIp = true; + } } } @@ -90,7 +95,7 @@ struct FReceptionistConfig : public FConnectionConfig struct FLocatorConfig : public FConnectionConfig { FLocatorConfig() - : LocatorHost(TEXT("locator.improbable.io")) { + : LocatorHost(SpatialConstants::LOCATOR_HOST) { const TCHAR* CommandLine = FCommandLine::Get(); FParse::Value(CommandLine, TEXT("locatorHost"), LocatorHost); FParse::Value(CommandLine, TEXT("playerIdentityToken"), PlayerIdentityToken); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/OutgoingMessages.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/OutgoingMessages.h index f8cd16a92f..e07c319fc3 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/OutgoingMessages.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/OutgoingMessages.h @@ -21,6 +21,8 @@ enum class EOutgoingMessageType : int32 ReserveEntityIdsRequest, CreateEntityRequest, DeleteEntityRequest, + AddComponent, + RemoveComponent, ComponentUpdate, CommandRequest, CommandResponse, @@ -71,6 +73,30 @@ struct FDeleteEntityRequest : FOutgoingMessage Worker_EntityId EntityId; }; +struct FAddComponent : FOutgoingMessage +{ + FAddComponent(Worker_EntityId InEntityId, const Worker_ComponentData& InData) + : FOutgoingMessage(EOutgoingMessageType::AddComponent) + , EntityId(InEntityId) + , Data(InData) + {} + + Worker_EntityId EntityId; + Worker_ComponentData Data; +}; + +struct FRemoveComponent : FOutgoingMessage +{ + FRemoveComponent(Worker_EntityId InEntityId, Worker_ComponentId InComponentId) + : FOutgoingMessage(EOutgoingMessageType::RemoveComponent) + , EntityId(InEntityId) + , ComponentId(InComponentId) + {} + + Worker_EntityId EntityId; + Worker_ComponentId ComponentId; +}; + struct FComponentUpdate : FOutgoingMessage { FComponentUpdate(Worker_EntityId InEntityId, const Worker_ComponentUpdate& InComponentUpdate) diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h index 74c550be7f..fe831afd79 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/Connection/SpatialWorkerConnection.h @@ -7,6 +7,7 @@ #include "Interop/Connection/ConnectionConfig.h" #include "Interop/Connection/OutgoingMessages.h" +#include "SpatialGDKSettings.h" #include "UObject/WeakObjectPtr.h" #include @@ -46,6 +47,8 @@ class SPATIALGDK_API USpatialWorkerConnection : public UObject, public FRunnable Worker_RequestId SendReserveEntityIdsRequest(uint32_t NumOfEntities); Worker_RequestId SendCreateEntityRequest(TArray&& Components, const Worker_EntityId* EntityId); Worker_RequestId SendDeleteEntityRequest(Worker_EntityId EntityId); + void SendAddComponent(Worker_EntityId EntityId, Worker_ComponentData* ComponentData); + void SendRemoveComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId); void SendComponentUpdate(Worker_EntityId EntityId, const Worker_ComponentUpdate* ComponentUpdate); Worker_RequestId SendCommandRequest(Worker_EntityId EntityId, const Worker_CommandRequest* Request, uint32_t CommandId); void SendCommandResponse(Worker_RequestId RequestId, const Worker_CommandResponse* Response); @@ -86,6 +89,10 @@ class SPATIALGDK_API USpatialWorkerConnection : public UObject, public FRunnable void QueueLatestOpList(); void ProcessOutgoingMessages(); + void StartDevelopmentAuth(FString DevAuthToken); + static void OnPlayerIdentityToken(void* UserData, const Worker_Alpha_PlayerIdentityTokenResponse* PIToken); + static void OnLoginTokens(void* UserData, const Worker_Alpha_LoginTokensResponse* LoginTokens); + template void QueueOutgoingMessage(ArgsType&&... Args); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/GlobalStateManager.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/GlobalStateManager.h index 55f6a1cad6..e3e4ffefa4 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/GlobalStateManager.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/GlobalStateManager.h @@ -44,20 +44,29 @@ class SPATIALGDK_API UGlobalStateManager : public UObject void QueryGSM(bool bRetryUntilAcceptingPlayers); void RetryQueryGSM(bool bRetryUntilAcceptingPlayers); - bool GetAcceptingPlayersFromQueryResponse(Worker_EntityQueryResponseOp& Op); - void ApplyDeploymentMapDataFromQueryResponse(Worker_EntityQueryResponseOp& Op); + bool GetAcceptingPlayersFromQueryResponse(const Worker_EntityQueryResponseOp& Op); + void ApplyDeploymentMapDataFromQueryResponse(const Worker_EntityQueryResponseOp& Op); void SetDeploymentMapURL(const FString& MapURL); void SetAcceptingPlayers(bool bAcceptingPlayers); - void SetCanBeginPlay(bool bInCanBeginPlay); + void SetCanBeginPlay(const bool bInCanBeginPlay); - void AuthorityChanged(bool bWorkerAuthority, Worker_EntityId CurrentEntityID); + void AuthorityChanged(const Worker_AuthorityChangeOp& AuthChangeOp); + bool HandlesComponent(const Worker_ComponentId ComponentId) const; void BeginDestroy() override; bool HasAuthority(); + void TriggerBeginPlay(); + + FORCEINLINE bool IsReadyToCallBeginPlay() const + { + return bCanBeginPlay; + } + USpatialActorChannel* AddSingleton(AActor* SingletonActor); + void RegisterSingletonChannel(AActor* SingletonActor, USpatialActorChannel* SingletonChannel); Worker_EntityId GlobalStateManagerEntityId; @@ -75,16 +84,15 @@ class SPATIALGDK_API UGlobalStateManager : public UObject void OnPrePIEEnded(bool bValue); void ReceiveShutdownMultiProcessRequest(); - void OnShutdownComponentUpdate(Worker_ComponentUpdate& Update); + void OnShutdownComponentUpdate(const Worker_ComponentUpdate& Update); void ReceiveShutdownAdditionalServersEvent(); #endif // WITH_EDITOR private: void LinkExistingSingletonActor(const UClass* SingletonClass); void ApplyAcceptingPlayersUpdate(bool bAcceptingPlayersUpdate); - void ApplyCanBeginPlayUpdate(bool bCanBeginPlayUpdate); + void ApplyCanBeginPlayUpdate(const bool bCanBeginPlayUpdate); void BecomeAuthoritativeOverAllActors(); - void TriggerBeginPlay(); #if WITH_EDITOR void SendShutdownMultiProcessRequest(); @@ -105,6 +113,4 @@ class SPATIALGDK_API UGlobalStateManager : public UObject USpatialReceiver* Receiver; FTimerManager* TimerManager; - - bool bTriggeredBeginPlay; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h index 372c6935f4..5485c38b33 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialClassInfoManager.h @@ -56,19 +56,29 @@ struct FClassInfo TWeakObjectPtr Class; + // Exists for all classes TArray RPCs; TMap RPCInfoMap; - TArray HandoverProperties; TArray InterestProperties; + // For Actors and default Subobjects belonging to Actors Worker_ComponentId SchemaComponents[ESchemaComponentType::SCHEMA_Count] = {}; + // Only for Actors + TMap> SubobjectInfo; + + // Only for default Subobjects belonging to Actors FName SubobjectName; - TMap> SubobjectInfo; + // Only for Subobject classes + TArray> DynamicSubobjectInfo; + + FName ActorGroup; + FName WorkerType; }; +class UActorGroupManager; class USpatialNetDriver; DECLARE_LOG_CATEGORY_EXTERN(LogSpatialClassInfoManager, Log, All) @@ -79,21 +89,27 @@ class SPATIALGDK_API USpatialClassInfoManager : public UObject GENERATED_BODY() public: - void Init(USpatialNetDriver* NetDriver); + + bool TryInit(USpatialNetDriver* NetDriver, UActorGroupManager* ActorGroupManager); // Returns true if the class path corresponds to an Actor or Subobject class path in SchemaDatabase // In PIE, PathName must be NetworkRemapped (bReading = false) bool IsSupportedClass(const FString& PathName) const; const FClassInfo& GetOrCreateClassInfoByClass(UClass* Class); - const FClassInfo& GetOrCreateClassInfoByClassAndOffset(UClass* Class, uint32 Offset); const FClassInfo& GetOrCreateClassInfoByObject(UObject* Object); - const FClassInfo& GetClassInfoByComponentId(Worker_ComponentId ComponentId) const; + const FClassInfo& GetClassInfoByComponentId(Worker_ComponentId ComponentId); UClass* GetClassByComponentId(Worker_ComponentId ComponentId); bool GetOffsetByComponentId(Worker_ComponentId ComponentId, uint32& OutOffset); ESchemaComponentType GetCategoryByComponentId(Worker_ComponentId ComponentId); + Worker_ComponentId GetComponentIdForClass(const UClass& Class) const; + TArray GetComponentIdsForClassHierarchy(const UClass& BaseClass, const bool bIncludeDerivedTypes = true) const; + + const FRPCInfo& GetRPCInfo(UObject* Object, UFunction* Function); + + uint32 GetComponentIdFromLevelPath(const FString& LevelPath); bool IsSublevelComponent(Worker_ComponentId ComponentId); UPROPERTY() @@ -101,11 +117,20 @@ class SPATIALGDK_API USpatialClassInfoManager : public UObject private: void CreateClassInfoForClass(UClass* Class); + void TryCreateClassInfoForComponentId(Worker_ComponentId ComponentId); + + void FinishConstructingActorClassInfo(const FString& ClassPath, TSharedRef& Info); + void FinishConstructingSubobjectClassInfo(const FString& ClassPath, TSharedRef& Info); + + void QuitGame(); private: UPROPERTY() USpatialNetDriver* NetDriver; + UPROPERTY() + UActorGroupManager* ActorGroupManager; + TMap, TSharedRef> ClassInfoMap; TMap> ComponentToClassInfoMap; TMap ComponentToOffsetMap; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialDispatcher.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialDispatcher.h index 723ee05285..de152ca3bf 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialDispatcher.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialDispatcher.h @@ -31,6 +31,10 @@ class SPATIALGDK_API USpatialDispatcher : public UObject void Init(USpatialNetDriver* NetDriver); void ProcessOps(Worker_OpList* OpList); + // The following 2 methods should *only* be used by the Startup OpList Queueing flow + // from the SpatialNetDriver, and should be temporary since an alternative solution will be available via the Worker SDK soon. + void MarkOpToSkip(const Worker_Op* Op); + int GetNumOpsToSkip() const; // Each callback method returns a callback ID which is incremented for each registration. // ComponentId must be in the range 1000 - 2000. @@ -60,7 +64,6 @@ class SPATIALGDK_API USpatialDispatcher : public UObject bool IsExternalSchemaOp(Worker_Op* Op) const; void ProcessExternalSchemaOp(Worker_Op* Op); - Worker_ComponentId GetComponentId(Worker_Op* Op) const; FCallbackId AddGenericOpCallback(Worker_ComponentId ComponentId, Worker_OpType OpType, const TFunction& Callback); void RunCallbacks(Worker_ComponentId ComponentId, const Worker_Op* Op); @@ -80,4 +83,5 @@ class SPATIALGDK_API USpatialDispatcher : public UObject FCallbackId NextCallbackId; TMap ComponentOpTypeToCallbacksMap; TMap CallbackIdToDataMap; + TArray OpsToSkip; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialInterestConstraints.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialInterestConstraints.h new file mode 100644 index 0000000000..47f4a3b1e2 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialInterestConstraints.h @@ -0,0 +1,291 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "Templates/SubclassOf.h" + +#include "SpatialInterestConstraints.generated.h" + +namespace SpatialGDK +{ +struct QueryConstraint; +} +class USpatialClassInfoManager; + +/** + * Query data used to configure Query-based Interest. + */ +USTRUCT(BlueprintType) +struct SPATIALGDK_API FQueryData +{ + GENERATED_BODY() +public: + FQueryData() = default; + ~FQueryData() = default; + + /** + * The root constraint associated with the query generated by this component. + */ + UPROPERTY(BlueprintReadonly, EditDefaultsOnly, Instanced, Category = "Query Data") + class UAbstractQueryConstraint* Constraint; + + /** + * Not currently supported. + * + * Used for frequency-based rate limiting. Represents the maximum frequency + * of updates for this particular query. An empty option represents no + * rate-limiting (ie. updates are received as soon as possible). Frequency + * is measured in Hz. + * + * If set, the time between consecutive updates will be at least + * 1/frequency. This is determined at the time that updates are sent from + * the Runtime and may not necessarily correspond to the time updates are + * received by the worker. + * + * If after an update has been sent, multiple updates are applied to a + * component, they will be merged and sent as a single update after + * 1/frequency of the last sent update. When components with events are + * merged, the resultant component will contain a concatenation of all the + * events. + * + * If multiple queries match the same Entity-Component then the highest of + * all frequencies is used. + */ + UPROPERTY() + float Frequency; +}; + +UCLASS(Abstract, BlueprintInternalUseOnly) +class SPATIALGDK_API UAbstractQueryConstraint : public UObject +{ + GENERATED_BODY() +public: + UAbstractQueryConstraint() = default; + virtual ~UAbstractQueryConstraint() = default; + + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const PURE_VIRTUAL(UAbstractQueryConstraint::CreateConstraint, ); +}; + +/** + * Creates a constraint that is satisfied if any of its inner constraints are satisfied. + */ +UCLASS(BlueprintType, EditInlineNew, DefaultToInstanced) +class SPATIALGDK_API UOrConstraint final : public UAbstractQueryConstraint +{ + GENERATED_BODY() +public: + UOrConstraint() = default; + ~UOrConstraint() = default; + + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + + /** Entities captured by any subconstraints will be included in interest results. */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Instanced, Category = "Or Constraint") + TArray Constraints; +}; + +/** + * Creates a constraint that is satisfied if all of its inner constraints are satisfied. + */ +UCLASS(BlueprintType, EditInlineNew, DefaultToInstanced) +class SPATIALGDK_API UAndConstraint final : public UAbstractQueryConstraint +{ + GENERATED_BODY() +public: + UAndConstraint() = default; + ~UAndConstraint() = default; + + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + + /** Entities captured by all subconstraints will be included in interest results. */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Instanced, Category = "And Constraint") + TArray Constraints; +}; + +/** + * Creates a constraint that includes all entities within a sphere centered on the specified point. + */ +UCLASS(BlueprintType, EditInlineNew, DefaultToInstanced) +class SPATIALGDK_API USphereConstraint final : public UAbstractQueryConstraint +{ + GENERATED_BODY() +public: + USphereConstraint() = default; + ~USphereConstraint() = default; + + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + + /** The location in the world that this constraint is relative to. */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Sphere Constraint") + FVector Center = FVector::ZeroVector; + + /** The size of the sphere represented by this constraint in centimeters. */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Meta = (ClampMin = 0.0), Category = "Sphere Constraint") + float Radius = 0.0f; +}; + +/** + * Creates a constraint that includes all entities within a cylinder centered on the specified point. + */ +UCLASS(BlueprintType, EditInlineNew, DefaultToInstanced) +class SPATIALGDK_API UCylinderConstraint final : public UAbstractQueryConstraint +{ + GENERATED_BODY() +public: + UCylinderConstraint() = default; + ~UCylinderConstraint() = default; + + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + + /** The location in the world that this constraint is relative to. */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Cylinder Constraint") + FVector Center = FVector::ZeroVector; + + /** The size of the cylinder represented by this constraint in centimeters. */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Meta = (ClampMin = 0.0), Category = "Cylinder Constraint") + float Radius = 0.0f; +}; + +/** + * Creates a constraint that includes all entities within a bounding box centered on the specified point. + */ +UCLASS(BlueprintType, EditInlineNew, DefaultToInstanced) +class SPATIALGDK_API UBoxConstraint final : public UAbstractQueryConstraint +{ + GENERATED_BODY() +public: + UBoxConstraint() = default; + ~UBoxConstraint() = default; + + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + + /** The location in the world that this constraint is relative to. */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Box Constraint") + FVector Center = FVector::ZeroVector; + + /** The size of the box represented by this constraint. */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Meta = (ClampMin = 0.0), Category = "Box Constraint") + FVector EdgeLengths = FVector::ZeroVector; +}; + +/** + * Creates a constraint that includes all entities within a sphere centered on the actor. + */ +UCLASS(BlueprintType, EditInlineNew, DefaultToInstanced) +class SPATIALGDK_API URelativeSphereConstraint final : public UAbstractQueryConstraint +{ + GENERATED_BODY() +public: + URelativeSphereConstraint() = default; + ~URelativeSphereConstraint() = default; + + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + + /** The size of the sphere represented by this constraint in centimeters. */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Meta = (ClampMin = 0.0), Category = "Relative Sphere Constraint") + float Radius = 0.0f; +}; + +/** + * Creates a constraint that includes all entities within a cylinder centered on the actor. + */ +UCLASS(BlueprintType, EditInlineNew, DefaultToInstanced) +class SPATIALGDK_API URelativeCylinderConstraint final : public UAbstractQueryConstraint +{ + GENERATED_BODY() +public: + URelativeCylinderConstraint() = default; + ~URelativeCylinderConstraint() = default; + + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + + /** The size of the cylinder represented by this constraint in centimeters. */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Meta = (ClampMin = 0.0), Category = "Relative Cylinder Constraint") + float Radius = 0.0f; +}; + +/** + * Creates a constraint that includes all entities within a bounding box centered on the actor. + */ +UCLASS(BlueprintType, EditInlineNew, DefaultToInstanced) +class SPATIALGDK_API URelativeBoxConstraint final : public UAbstractQueryConstraint +{ + GENERATED_BODY() +public: + URelativeBoxConstraint() = default; + ~URelativeBoxConstraint() = default; + + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + + /** The size of the box represented by this constraint. */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Meta = (ClampMin = 0.0), Category = "Relative Box Constraint") + FVector EdgeLengths = FVector::ZeroVector; +}; + +/** + * Creates a constraint that includes an actor type (including subtypes) and a cylindrical range around the actor with the interest. + */ +UCLASS(BlueprintType, EditInlineNew, DefaultToInstanced) +class SPATIALGDK_API UCheckoutRadiusConstraint final : public UAbstractQueryConstraint +{ + GENERATED_BODY() +public: + UCheckoutRadiusConstraint() = default; + ~UCheckoutRadiusConstraint() = default; + + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + + /** The base type of actor that this constraint will capture. */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Checkout Radius Constraint") + TSubclassOf ActorClass; + + /** The size of the cylinder represented by this constraint in centimeters. */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Meta = (ClampMin = 0.0), Category = "Checkout Radius Constraint") + float Radius = 0.0f; +}; + +/** + * Creates a constraint that includes all actors of a type (optionally including subtypes). + */ +UCLASS(BlueprintType, EditInlineNew, DefaultToInstanced) +class SPATIALGDK_API UActorClassConstraint final : public UAbstractQueryConstraint +{ + GENERATED_BODY() +public: + UActorClassConstraint() = default; + ~UActorClassConstraint() = default; + + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + + /** The base type of actor that this constraint will capture. */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Actor Class Constraint") + TSubclassOf ActorClass; + + /** Whether this constraint should capture derived types. */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Actor Class Constraint") + bool bIncludeDerivedClasses = true; +}; + +/** + * Creates a constraint that includes all components of a type (optionally including subtypes). + */ +UCLASS(BlueprintType, EditInlineNew, DefaultToInstanced) +class SPATIALGDK_API UComponentClassConstraint final : public UAbstractQueryConstraint +{ + GENERATED_BODY() +public: + UComponentClassConstraint() = default; + ~UComponentClassConstraint() = default; + + virtual void CreateConstraint(const USpatialClassInfoManager& ClassInfoManager, SpatialGDK::QueryConstraint& OutConstraint) const override; + + /** The base type of component that this constraint will capture. */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Component Class Constraint") + TSubclassOf ComponentClass; + + /** Whether this constraint should capture derived types. */ + UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Component Class Constraint") + bool bIncludeDerivedClasses = true; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialReceiver.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialReceiver.h index 08c5147a28..f3e3d13e7f 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialReceiver.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialReceiver.h @@ -9,10 +9,12 @@ #include "EngineClasses/SpatialPackageMapClient.h" #include "Interop/SpatialClassInfoManager.h" #include "Schema/DynamicComponent.h" +#include "Schema/RPCPayload.h" #include "Schema/SpawnData.h" #include "Schema/StandardLibrary.h" #include "Schema/UnrealObjectRef.h" #include "SpatialCommonTypes.h" +#include "Utils/RPCContainer.h" #include #include @@ -21,6 +23,7 @@ DECLARE_LOG_CATEGORY_EXTERN(LogSpatialReceiver, Log, All); +class USpatialNetConnection; class USpatialSender; class UGlobalStateManager; @@ -45,23 +48,24 @@ struct FObjectReferences , Buffer(MoveTemp(Other.Buffer)) , NumBufferBits(Other.NumBufferBits) , Array(MoveTemp(Other.Array)) + , ShadowOffset(Other.ShadowOffset) , ParentIndex(Other.ParentIndex) , Property(Other.Property) {} // Single property constructor - FObjectReferences(const FUnrealObjectRef& InUnresolvedRef, int32 InParentIndex, UProperty* InProperty) - : bSingleProp(true), bFastArrayProp(false), ParentIndex(InParentIndex), Property(InProperty) + FObjectReferences(const FUnrealObjectRef& InUnresolvedRef, int32 InCmdIndex, int32 InParentIndex, UProperty* InProperty) + : bSingleProp(true), bFastArrayProp(false), ShadowOffset(InCmdIndex), ParentIndex(InParentIndex), Property(InProperty) { UnresolvedRefs.Add(InUnresolvedRef); } // Struct (memory stream) constructor - FObjectReferences(const TArray& InBuffer, int32 InNumBufferBits, const TSet& InUnresolvedRefs, int32 InParentIndex, UProperty* InProperty, bool InFastArrayProp = false) - : UnresolvedRefs(InUnresolvedRefs), bSingleProp(false), bFastArrayProp(InFastArrayProp), Buffer(InBuffer), NumBufferBits(InNumBufferBits), ParentIndex(InParentIndex), Property(InProperty) {} + FObjectReferences(const TArray& InBuffer, int32 InNumBufferBits, const TSet& InUnresolvedRefs, int32 InCmdIndex, int32 InParentIndex, UProperty* InProperty, bool InFastArrayProp = false) + : UnresolvedRefs(InUnresolvedRefs), bSingleProp(false), bFastArrayProp(InFastArrayProp), Buffer(InBuffer), NumBufferBits(InNumBufferBits), ShadowOffset(InCmdIndex), ParentIndex(InParentIndex), Property(InProperty) {} // Array constructor - FObjectReferences(FObjectReferencesMap* InArray, int32 InParentIndex, UProperty* InProperty) - : bSingleProp(false), bFastArrayProp(false), Array(InArray), ParentIndex(InParentIndex), Property(InProperty) {} + FObjectReferences(FObjectReferencesMap* InArray, int32 InCmdIndex, int32 InParentIndex, UProperty* InProperty) + : bSingleProp(false), bFastArrayProp(false), Array(InArray), ShadowOffset(InCmdIndex), ParentIndex(InParentIndex), Property(InProperty) {} TSet UnresolvedRefs; @@ -71,30 +75,37 @@ struct FObjectReferences int32 NumBufferBits; TUniquePtr Array; + int32 ShadowOffset; int32 ParentIndex; UProperty* Property; }; struct FPendingIncomingRPC { - FPendingIncomingRPC(const TSet& InUnresolvedRefs, UObject* InTargetObject, UFunction* InFunction, const TArray& InPayloadData, int64 InCountBits) - : UnresolvedRefs(InUnresolvedRefs), TargetObject(InTargetObject), Function(InFunction), PayloadData(InPayloadData), CountBits(InCountBits) {} + FPendingIncomingRPC(const TSet& InUnresolvedRefs, UObject* InTargetObject, UFunction* InFunction, const SpatialGDK::RPCPayload& InPayload) + : UnresolvedRefs(InUnresolvedRefs), TargetObject(InTargetObject), Function(InFunction), Payload(InPayload) {} TSet UnresolvedRefs; TWeakObjectPtr TargetObject; UFunction* Function; - TArray PayloadData; - int64 CountBits; -#if !UE_BUILD_SHIPPING + SpatialGDK::RPCPayload Payload; FString SenderWorkerId; -#endif // !UE_BUILD_SHIPPING +}; + +struct FPendingSubobjectAttachment +{ + USpatialActorChannel* Channel; + const FClassInfo* Info; + TWeakObjectPtr Subobject; + + TSet PendingAuthorityDelegations; }; using FIncomingRPCArray = TArray>; -DECLARE_DELEGATE_OneParam(EntityQueryDelegate, Worker_EntityQueryResponseOp&); -DECLARE_DELEGATE_OneParam(ReserveEntityIDsDelegate, Worker_ReserveEntityIdsResponseOp&); -DECLARE_DELEGATE_OneParam(HeartbeatDelegate, Worker_ComponentUpdateOp&); +DECLARE_DELEGATE_OneParam(EntityQueryDelegate, const Worker_EntityQueryResponseOp&); +DECLARE_DELEGATE_OneParam(ReserveEntityIDsDelegate, const Worker_ReserveEntityIdsResponseOp&); +DECLARE_DELEGATE_OneParam(CreateEntityDelegate, const Worker_CreateEntityResponseOp&); UCLASS() class USpatialReceiver : public UObject @@ -106,28 +117,33 @@ class USpatialReceiver : public UObject // Dispatcher Calls void OnCriticalSection(bool InCriticalSection); - void OnAddEntity(Worker_AddEntityOp& Op); - void OnAddComponent(Worker_AddComponentOp& Op); - void OnRemoveEntity(Worker_RemoveEntityOp& Op); - void OnAuthorityChange(Worker_AuthorityChangeOp& Op); + void OnAddEntity(const Worker_AddEntityOp& Op); + void OnAddComponent(const Worker_AddComponentOp& Op); + void OnRemoveEntity(const Worker_RemoveEntityOp& Op); + void OnRemoveComponent(const Worker_RemoveComponentOp& Op); + void FlushRemoveComponentOps(); + void RemoveComponentOpsForEntity(Worker_EntityId EntityId); + void OnAuthorityChange(const Worker_AuthorityChangeOp& Op); + + void OnComponentUpdate(const Worker_ComponentUpdateOp& Op); + void HandleRPC(const Worker_ComponentUpdateOp& Op); - void OnComponentUpdate(Worker_ComponentUpdateOp& Op); - void HandleUnreliableRPC(Worker_ComponentUpdateOp& Op); - void OnCommandRequest(Worker_CommandRequestOp& Op); - void OnCommandResponse(Worker_CommandResponseOp& Op); + void ProcessRPCEventField(Worker_EntityId EntityId, const Worker_ComponentUpdateOp &Op, const Worker_ComponentId RPCEndpointComponentId, bool bPacked); - void OnReserveEntityIdsResponse(Worker_ReserveEntityIdsResponseOp& Op); - void OnCreateEntityResponse(Worker_CreateEntityResponseOp& Op); + void OnCommandRequest(const Worker_CommandRequestOp& Op); + void OnCommandResponse(const Worker_CommandResponseOp& Op); + + void OnReserveEntityIdsResponse(const Worker_ReserveEntityIdsResponseOp& Op); + void OnCreateEntityResponse(const Worker_CreateEntityResponseOp& Op); void AddPendingActorRequest(Worker_RequestId RequestId, USpatialActorChannel* Channel); - void AddPendingReliableRPC(Worker_RequestId RequestId, TSharedRef Params); + void AddPendingReliableRPC(Worker_RequestId RequestId, TSharedRef ReliableRPC); void AddEntityQueryDelegate(Worker_RequestId RequestId, EntityQueryDelegate Delegate); void AddReserveEntityIdsDelegate(Worker_RequestId RequestId, ReserveEntityIDsDelegate Delegate); + void AddCreateEntityDelegate(Worker_RequestId RequestId, const CreateEntityDelegate& Delegate); - void AddHeartbeatDelegate(Worker_EntityId EntityId, HeartbeatDelegate Delegate); - - void OnEntityQueryResponse(Worker_EntityQueryResponseOp& Op); + void OnEntityQueryResponse(const Worker_EntityQueryResponseOp& Op); void CleanupDeletedEntity(Worker_EntityId EntityId); @@ -147,41 +163,58 @@ class USpatialReceiver : public UObject AActor* TryGetOrCreateActor(SpatialGDK::UnrealMetadata* UnrealMetadata, SpatialGDK::SpawnData* SpawnData); AActor* CreateActor(SpatialGDK::UnrealMetadata* UnrealMetadata, SpatialGDK::SpawnData* SpawnData); + void ProcessRemoveComponent(const Worker_RemoveComponentOp& Op); + static FTransform GetRelativeSpawnTransform(UClass* ActorClass, FTransform SpawnTransform); void QueryForStartupActor(AActor* Actor, Worker_EntityId EntityId); - void HandlePlayerLifecycleAuthority(Worker_AuthorityChangeOp& Op, class APlayerController* PlayerController); - void HandleActorAuthority(Worker_AuthorityChangeOp& Op); + void HandlePlayerLifecycleAuthority(const Worker_AuthorityChangeOp& Op, class APlayerController* PlayerController); + void HandleActorAuthority(const Worker_AuthorityChangeOp& Op); + + void ApplyComponentDataOnActorCreation(Worker_EntityId EntityId, const Worker_ComponentData& Data, USpatialActorChannel* Channel); + void ApplyComponentData(UObject* TargetObject, USpatialActorChannel* Channel, const Worker_ComponentData& Data); + // This is called for AddComponentOps not in a critical section, which means they are not a part of the initial entity creation. + void HandleIndividualAddComponent(const Worker_AddComponentOp& Op); + void AttachDynamicSubobject(Worker_EntityId EntityId, const FClassInfo& Info); - void ApplyComponentData(Worker_EntityId EntityId, Worker_ComponentData& Data, USpatialActorChannel* Channel); void ApplyComponentUpdate(const Worker_ComponentUpdate& ComponentUpdate, UObject* TargetObject, USpatialActorChannel* Channel, bool bIsHandover); - void ReceiveRPCCommandRequest(const Worker_CommandRequest& CommandRequest, UObject* TargetObject, UFunction* Function, const FString& SenderWorkerId); - void ReceiveMulticastUpdate(const Worker_ComponentUpdate& ComponentUpdate, UObject* TargetObject, const TArray& RPCArray); - void ApplyRPC(UObject* TargetObject, UFunction* Function, TArray& PayloadData, int64 CountBits, const FString& SenderWorkerId); + void RegisterListeningEntityIfReady(Worker_EntityId EntityId, Schema_Object* Object); + + bool ApplyRPC(const FPendingRPCParams& Params); + bool ApplyRPC(UObject* TargetObject, UFunction* Function, const SpatialGDK::RPCPayload& Payload, const FString& SenderWorkerId); - void ReceiveCommandResponse(Worker_CommandResponseOp& Op); + void ReceiveCommandResponse(const Worker_CommandResponseOp& Op); + + bool IsReceivedEntityTornOff(Worker_EntityId EntityId); void QueueIncomingRepUpdates(FChannelObjectPair ChannelObjectPair, const FObjectReferencesMap& ObjectReferencesMap, const TSet& UnresolvedRefs); - void QueueIncomingRPC(const TSet& UnresolvedRefs, UObject* TargetObject, UFunction* Function, const TArray& PayloadData, int64 CountBits, const FString& SenderWorkerId); + + void QueueIncomingRPC(FPendingRPCParamsPtr Params); void ResolvePendingOperations_Internal(UObject* Object, const FUnrealObjectRef& ObjectRef); void ResolveIncomingOperations(UObject* Object, const FUnrealObjectRef& ObjectRef); - void ResolveIncomingRPCs(UObject* Object, const FUnrealObjectRef& ObjectRef); + + void ResolveIncomingRPCs(); + void ResolveObjectReferences(FRepLayout& RepLayout, UObject* ReplicatedObject, FObjectReferencesMap& ObjectReferencesMap, uint8* RESTRICT StoredData, uint8* RESTRICT Data, int32 MaxAbsOffset, TArray& RepNotifies, bool& bOutSomeObjectsWereMapped, bool& bOutStillHasUnresolved); void ProcessQueuedResolvedObjects(); + void ProcessQueuedActorRPCsOnEntityCreation(AActor* Actor, SpatialGDK::RPCsOnEntityCreation& QueuedRPCs); void UpdateShadowData(Worker_EntityId EntityId); TWeakObjectPtr PopPendingActorRequest(Worker_RequestId RequestId); + AActor* FindSingletonActor(UClass* SingletonClass); + + void OnHeartbeatComponentUpdate(const Worker_ComponentUpdateOp& Op); + public: TMap> IncomingRefsMap; -private: - template - friend T* GetComponentData(USpatialReceiver& Receiver, Worker_EntityId EntityId); + TMap, TSharedRef> PendingEntitySubobjectDelegations; +private: UPROPERTY() USpatialNetDriver* NetDriver; @@ -207,17 +240,25 @@ class USpatialReceiver : public UObject TArray> ResolvedObjectQueue; TMap IncomingRPCMap; + FRPCContainer IncomingRPCs; bool bInCriticalSection; TArray PendingAddEntities; TArray PendingAuthorityChanges; TArray PendingAddComponents; + TArray QueuedRemoveComponentOps; TMap> PendingActorRequests; FReliableRPCMap PendingReliableRPCs; TMap EntityQueryDelegates; TMap ReserveEntityIDsDelegates; + TMap CreateEntityDelegates; + + // This will map PlayerController entities to the corresponding SpatialNetConnection + // for PlayerControllers that this server has authority over. This is used for player + // lifecycle logic (Heartbeat component updates, disconnection logic). + TMap> AuthorityPlayerControllerConnectionMap; - TMap HeartbeatDelegates; + TMap, PendingAddComponentWrapper> PendingDynamicSubobjectComponents; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSender.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSender.h index b5592c26ef..79eeebdab7 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSender.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialSender.h @@ -6,13 +6,18 @@ #include "EngineClasses/SpatialNetBitWriter.h" #include "Interop/SpatialClassInfoManager.h" +#include "Schema/RPCPayload.h" +#include "TimerManager.h" #include "Utils/RepDataUtils.h" +#include "Utils/RPCContainer.h" #include #include #include "SpatialSender.generated.h" +using namespace SpatialGDK; + DECLARE_LOG_CATEGORY_EXTERN(LogSpatialSender, Log, All); class USpatialActorChannel; @@ -22,33 +27,44 @@ class USpatialPackageMapClient; class USpatialReceiver; class USpatialStaticComponentView; class USpatialClassInfoManager; +class UActorGroupManager; class USpatialWorkerConnection; -struct FPendingRPCParams +struct FReliableRPCForRetry { - FPendingRPCParams(UObject* InTargetObject, UFunction* InFunction, void* InParameters, int InRetryIndex); - ~FPendingRPCParams(); + FReliableRPCForRetry(UObject* InTargetObject, UFunction* InFunction, Worker_ComponentId InComponentId, Schema_FieldId InRPCIndex, const TArray& InPayload, int InRetryIndex); TWeakObjectPtr TargetObject; UFunction* Function; - TArray Parameters; + Worker_ComponentId ComponentId; + Schema_FieldId RPCIndex; + TArray Payload; int Attempts; // For reliable RPCs int RetryIndex; // Index for ordering reliable RPCs on subsequent tries -#if !UE_BUILD_SHIPPING - int ReliableRPCIndex; -#endif // !UE_BUILD_SHIPPING +}; + +struct FPendingRPC +{ + FPendingRPC() = default; + FPendingRPC(FPendingRPC&& Other); + + uint32 Offset; + Schema_FieldId Index; + TArray Data; + Schema_EntityId Entity; }; // TODO: Clear TMap entries when USpatialActorChannel gets deleted - UNR:100 // care for actor getting deleted before actor channel using FChannelObjectPair = TPair, TWeakObjectPtr>; -using FOutgoingRPCMap = TMap, TArray>>; +using FRPCsOnEntityCreationMap = TMap, RPCsOnEntityCreation>; using FUnresolvedEntry = TSharedPtr>>; using FHandleToUnresolved = TMap; using FChannelToHandleToUnresolved = TMap; using FOutgoingRepUpdates = TMap, FChannelToHandleToUnresolved>; using FUpdatesQueuedUntilAuthority = TMap>; +using FChannelsToUpdatePosition = TSet>; UCLASS() class SPATIALGDK_API USpatialSender : public UObject @@ -56,27 +72,53 @@ class SPATIALGDK_API USpatialSender : public UObject GENERATED_BODY() public: - void Init(USpatialNetDriver* InNetDriver); + void Init(USpatialNetDriver* InNetDriver, FTimerManager* InTimerManager); // Actor Updates void SendComponentUpdates(UObject* Object, const FClassInfo& Info, USpatialActorChannel* Channel, const FRepChangeState* RepChanges, const FHandoverChangeState* HandoverChanges); - void SendComponentInterest(AActor* Actor, Worker_EntityId EntityId, bool bNetOwned); + 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 SendRPC(TSharedRef Params); + bool SendRPC(const FPendingRPCParams& Params); void SendCommandResponse(Worker_RequestId request_id, Worker_CommandResponse& Response); + void SendEmptyCommandResponse(Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, Worker_RequestId RequestId); + void SendAddComponent(USpatialActorChannel* Channel, UObject* Subobject, const FClassInfo& Info); + void SendRemoveComponent(Worker_EntityId EntityId, const FClassInfo& Info); void SendCreateEntityRequest(USpatialActorChannel* Channel); void SendDeleteEntityRequest(Worker_EntityId EntityId); - void EnqueueRetryRPC(TSharedRef Params); + void SendRequestToClearRPCsOnEntityCreation(Worker_EntityId EntityId); + void ClearRPCsOnEntityCreation(Worker_EntityId EntityId); + + void SendClientEndpointReadyUpdate(Worker_EntityId EntityId); + void SendServerEndpointReadyUpdate(Worker_EntityId EntityId); + + void EnqueueRetryRPC(TSharedRef RetryRPC); void FlushRetryRPCs(); + void RetryReliableRPC(TSharedRef RetryRPC); + + void RegisterChannelForPositionUpdate(USpatialActorChannel* Channel); + void ProcessPositionUpdates(); + void ResolveOutgoingOperations(UObject* Object, bool bIsHandover); - void ResolveOutgoingRPCs(UObject* Object); + void SendOutgoingRPCs(); bool UpdateEntityACLs(Worker_EntityId EntityId, const FString& OwnerWorkerAttribute); void UpdateInterestComponent(AActor* Actor); + void ProcessRPC(FPendingRPCParamsPtr Params); + void QueueOutgoingRPC(FPendingRPCParamsPtr Params); void ProcessUpdatesQueuedUntilAuthority(Worker_EntityId EntityId); + + void FlushPackedRPCs(); + + RPCPayload CreateRPCPayloadFromParams(UObject* TargetObject, UFunction* Function, int ReliableRPCIndex, void* Params, TSet>& UnresolvedObjects); + 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); + private: // Actor Lifecycle Worker_RequestId CreateEntity(USpatialActorChannel* Channel); @@ -85,14 +127,16 @@ class SPATIALGDK_API USpatialSender : public UObject // Queuing void ResetOutgoingUpdate(USpatialActorChannel* DependentChannel, UObject* ReplicatedObject, int16 Handle, bool bIsHandover); void QueueOutgoingUpdate(USpatialActorChannel* DependentChannel, UObject* ReplicatedObject, int16 Handle, const TSet>& UnresolvedObjects, bool bIsHandover); - void QueueOutgoingRPC(const UObject* UnresolvedObject, TSharedRef Params); // RPC Construction - Worker_CommandRequest CreateRPCCommandRequest(UObject* TargetObject, UFunction* Function, void* Parameters, Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, Worker_EntityId& OutEntityId, const UObject*& OutUnresolvedObject, int ReliableRPCIndex); - Worker_ComponentUpdate CreateUnreliableRPCUpdate(UObject* TargetObject, UFunction* Function, void* Parameters, Worker_ComponentId ComponentId, Schema_FieldId EventIndex, Worker_EntityId& OutEntityId, const UObject*& OutUnresolvedObject); - void WriteRpcPayload(Schema_Object* Object, uint32 Offset, Schema_FieldId Index, FSpatialNetBitWriter& PayloadWriter); + FSpatialNetBitWriter PackRPCDataToSpatialNetBitWriter(UFunction* Function, void* Parameters, int ReliableRPCId, TSet>& UnresolvedObjects) const; - TArray CreateComponentInterest(AActor* Actor, bool bIsNetOwned); + Worker_CommandRequest CreateRPCCommandRequest(UObject* TargetObject, const RPCPayload& Payload, Worker_ComponentId ComponentId, Schema_FieldId CommandIndex, Worker_EntityId& OutEntityId, const UObject*& OutUnresolvedObject); + Worker_CommandRequest CreateRetryRPCCommandRequest(const FReliableRPCForRetry& RPC, uint32 TargetObjectOffset); + Worker_ComponentUpdate CreateRPCEventUpdate(UObject* TargetObject, const RPCPayload& Payload, Worker_ComponentId ComponentId, Schema_FieldId EventIndex, const UObject*& OutUnresolvedObject); + bool AddPendingRPC(UObject* TargetObject, const FPendingRPCParams& Parameters, Worker_ComponentId ComponentId, Schema_FieldId RPCIndex, const UObject*& OutUnresolvedObject); + + TArray CreateComponentInterestForActor(USpatialActorChannel* Channel, bool bIsNetOwned); private: UPROPERTY() @@ -113,17 +157,27 @@ class SPATIALGDK_API USpatialSender : public UObject UPROPERTY() USpatialClassInfoManager* ClassInfoManager; + UPROPERTY() + UActorGroupManager* ActorGroupManager; + + FTimerManager* TimerManager; + FChannelToHandleToUnresolved RepPropertyToUnresolved; FOutgoingRepUpdates RepObjectToUnresolved; FChannelToHandleToUnresolved HandoverPropertyToUnresolved; FOutgoingRepUpdates HandoverObjectToUnresolved; - FOutgoingRPCMap OutgoingRPCs; + FRPCContainer OutgoingRPCs; + FRPCsOnEntityCreationMap OutgoingOnCreateEntityRPCs; TMap PendingActorRequests; - TArray> RetryRPCs; + TArray> RetryRPCs; FUpdatesQueuedUntilAuthority UpdatesQueuedUntilAuthorityMap; + + FChannelsToUpdatePosition ChannelsToUpdatePosition; + + TMap> RPCsToPack; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialStaticComponentView.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialStaticComponentView.h index 484a0067fb..06a3bfbe5b 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialStaticComponentView.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialStaticComponentView.h @@ -36,8 +36,10 @@ class SPATIALGDK_API USpatialStaticComponentView : public UObject return nullptr; } + bool HasComponent(Worker_EntityId EntityId, Worker_ComponentId ComponentId); void OnAddComponent(const Worker_AddComponentOp& Op); + void OnRemoveComponent(const Worker_RemoveComponentOp& Op); void OnRemoveEntity(Worker_EntityId EntityId); void OnComponentUpdate(const Worker_ComponentUpdateOp& Op); void OnAuthorityChange(const Worker_AuthorityChangeOp& Op); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialWorkerFlags.h b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialWorkerFlags.h index 6533b2b423..21c714b105 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialWorkerFlags.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Interop/SpatialWorkerFlags.h @@ -5,6 +5,9 @@ #include "Kismet/BlueprintFunctionLibrary.h" #include "SpatialWorkerFlags.generated.h" +DECLARE_DYNAMIC_DELEGATE_TwoParams(FOnWorkerFlagsUpdatedBP, FString, FlagName, FString, FlagValue); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnWorkerFlagsUpdated, FString, FlagName, FString, FlagValue); + UCLASS() class SPATIALGDK_API USpatialWorkerFlags : public UBlueprintFunctionLibrary { @@ -18,7 +21,16 @@ class SPATIALGDK_API USpatialWorkerFlags : public UBlueprintFunctionLibrary */ UFUNCTION(BlueprintCallable, Category="SpatialOS") static bool GetWorkerFlag(const FString& Name, FString& OutValue); + + static FOnWorkerFlagsUpdated& GetOnWorkerFlagsUpdated(); + + UFUNCTION(BlueprintCallable, Category = "SpatialOS") + static void BindToOnWorkerFlagsUpdated(const FOnWorkerFlagsUpdatedBP& InDelegate); + + UFUNCTION(BlueprintCallable, Category = "SpatialOS") + static void UnbindFromOnWorkerFlagsUpdated(const FOnWorkerFlagsUpdatedBP& InDelegate); + static FOnWorkerFlagsUpdated OnWorkerFlagsUpdated; private: static void ApplyWorkerFlagUpdate(const struct Worker_FlagUpdateOp& Op); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/AlwaysRelevant.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/AlwaysRelevant.h new file mode 100644 index 0000000000..1ddf2e765c --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/AlwaysRelevant.h @@ -0,0 +1,30 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Schema/Component.h" +#include "SpatialConstants.h" + +#include +#include + +namespace SpatialGDK +{ + +struct AlwaysRelevant : Component +{ + static const Worker_ComponentId ComponentId = SpatialConstants::ALWAYS_RELEVANT_COMPONENT_ID; + + AlwaysRelevant() = default; + + FORCEINLINE Worker_ComponentData CreateData() + { + Worker_ComponentData Data = {}; + Data.component_id = ComponentId; + Data.schema_type = Schema_CreateComponentData(ComponentId); + + return Data; + } +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/ClientRPCEndpoint.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/ClientRPCEndpoint.h new file mode 100644 index 0000000000..5072ff1bef --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/ClientRPCEndpoint.h @@ -0,0 +1,46 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Schema/Component.h" +#include "SpatialConstants.h" +#include "Utils/SchemaUtils.h" + +#include +#include + +namespace SpatialGDK +{ + +struct ClientRPCEndpoint : Component +{ + static const Worker_ComponentId ComponentId = SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID; + + ClientRPCEndpoint() = default; + + Worker_ComponentData CreateRPCEndpointData() + { + Worker_ComponentData Data{}; + Data.component_id = ComponentId; + Data.schema_type = Schema_CreateComponentData(ComponentId); + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + Schema_AddBool(ComponentObject, SpatialConstants::UNREAL_RPC_ENDPOINT_READY_ID, bReady); + + return Data; + } + + Worker_ComponentUpdate CreateRPCEndpointUpdate() + { + Worker_ComponentUpdate Update{}; + Update.component_id = ComponentId; + Update.schema_type = Schema_CreateComponentUpdate(ComponentId); + Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update.schema_type); + Schema_AddBool(UpdateObject, SpatialConstants::UNREAL_RPC_ENDPOINT_READY_ID, bReady); + + return Update; + } + + bool bReady = false; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/Component.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/Component.h index 02edc38688..39e5d91ea1 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/Component.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/Component.h @@ -3,6 +3,7 @@ #pragma once #include +#include "CoreMinimal.h" namespace SpatialGDK { @@ -25,7 +26,7 @@ class ComponentStorage : public ComponentStorageBase { public: explicit ComponentStorage(const T& data) : data{data} {} - explicit ComponentStorage(T&& data) : data{std::move(data)} {} + explicit ComponentStorage(T&& data) : data{MoveTemp(data)} {} ~ComponentStorage() override {} TUniquePtr Copy() const override diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/Heartbeat.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/Heartbeat.h index 6a9ce87897..bd69e9559c 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/Heartbeat.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/Heartbeat.h @@ -26,6 +26,9 @@ struct Heartbeat : Component Worker_ComponentData Data = {}; Data.component_id = ComponentId; Data.schema_type = Schema_CreateComponentData(ComponentId); + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + + Schema_AddBool(ComponentObject, SpatialConstants::HEARTBEAT_CLIENT_HAS_QUIT_ID, false); return Data; } diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/Interest.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/Interest.h index 03d4207125..8803b8d01d 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/Interest.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/Interest.h @@ -55,7 +55,7 @@ struct QueryConstraint TArray AndConstraint; TArray OrConstraint; - FORCEINLINE bool IsValid() + FORCEINLINE bool IsValid() const { if (SphereConstraint.IsSet()) { diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/RPCPayload.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/RPCPayload.h new file mode 100644 index 0000000000..77d491a512 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/RPCPayload.h @@ -0,0 +1,108 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Schema/Component.h" +#include "SpatialConstants.h" +#include "Utils/SchemaUtils.h" + +#include +#include + +namespace SpatialGDK +{ + +struct RPCPayload +{ + RPCPayload() = delete; + + RPCPayload(uint32 InOffset, uint32 InIndex, TArray&& Data) : Offset(InOffset), Index(InIndex), PayloadData(MoveTemp(Data)) + {} + + RPCPayload(const Schema_Object* RPCObject) + { + Offset = Schema_GetUint32(RPCObject, SpatialConstants::UNREAL_RPC_PAYLOAD_OFFSET_ID); + Index = Schema_GetUint32(RPCObject, SpatialConstants::UNREAL_RPC_PAYLOAD_RPC_INDEX_ID); + PayloadData = SpatialGDK::GetBytesFromSchema(RPCObject, SpatialConstants::UNREAL_RPC_PAYLOAD_RPC_PAYLOAD_ID); + } + + int64 CountDataBits() const + { + return PayloadData.Num() * 8; + } + + static void WriteToSchemaObject(Schema_Object* RPCObject, uint32 Offset, uint32 Index, const uint8* Data, int32 NumElems) + { + Schema_AddUint32(RPCObject, SpatialConstants::UNREAL_RPC_PAYLOAD_OFFSET_ID, Offset); + Schema_AddUint32(RPCObject, SpatialConstants::UNREAL_RPC_PAYLOAD_RPC_INDEX_ID, Index); + AddBytesToSchema(RPCObject, SpatialConstants::UNREAL_RPC_PAYLOAD_RPC_PAYLOAD_ID, Data, sizeof(uint8) * NumElems); + } + + uint32 Offset; + uint32 Index; + TArray PayloadData; +}; + +struct RPCsOnEntityCreation : Component +{ + static const Worker_ComponentId ComponentId = SpatialConstants::RPCS_ON_ENTITY_CREATION_ID; + + RPCsOnEntityCreation() = default; + + bool HasRPCPayloadData() const + { + return RPCs.Num() > 0; + } + + RPCsOnEntityCreation(const Worker_ComponentData& Data) + { + Schema_Object* ComponentsObject = Schema_GetComponentDataFields(Data.schema_type); + + uint32 RPCCount = Schema_GetObjectCount(ComponentsObject, SpatialConstants::UNREAL_RPC_PAYLOAD_OFFSET_ID); + + for (uint32 i = 0; i < RPCCount; i++) + { + Schema_Object* ComponentObject = Schema_IndexObject(ComponentsObject, SpatialConstants::UNREAL_RPC_PAYLOAD_OFFSET_ID, i); + RPCs.Add(RPCPayload(ComponentObject)); + } + } + + Worker_ComponentData CreateRPCPayloadData() const + { + Worker_ComponentData Data = {}; + Data.component_id = ComponentId; + Data.schema_type = Schema_CreateComponentData(ComponentId); + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + + for (const auto& Payload : RPCs) + { + Schema_Object* Obj = Schema_AddObject(ComponentObject, SpatialConstants::UNREAL_RPC_PAYLOAD_OFFSET_ID); + RPCPayload::WriteToSchemaObject(Obj, Payload.Offset, Payload.Index, Payload.PayloadData.GetData(), Payload.PayloadData.Num()); + } + + return Data; + } + + static Worker_ComponentUpdate CreateClearFieldsUpdate() + { + Worker_ComponentUpdate Update = {}; + Update.component_id = ComponentId; + Update.schema_type = Schema_CreateComponentUpdate(ComponentId); + Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update.schema_type); + Schema_AddComponentUpdateClearedField(Update.schema_type, SpatialConstants::UNREAL_RPC_PAYLOAD_OFFSET_ID); + + return Update; + } + + static Worker_CommandRequest CreateClearFieldsCommandRequest() + { + Worker_CommandRequest CommandRequest = {}; + CommandRequest.component_id = ComponentId; + CommandRequest.schema_type = Schema_CreateCommandRequest(ComponentId, SpatialConstants::CLEAR_RPCS_ON_ENTITY_CREATION); + return CommandRequest; + } + + TArray RPCs; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerRPCEndpoint.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerRPCEndpoint.h new file mode 100644 index 0000000000..8c7636cd4d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/ServerRPCEndpoint.h @@ -0,0 +1,46 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Schema/Component.h" +#include "SpatialConstants.h" +#include "Utils/SchemaUtils.h" + +#include +#include + +namespace SpatialGDK +{ + +struct ServerRPCEndpoint : Component +{ + static const Worker_ComponentId ComponentId = SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID; + + ServerRPCEndpoint() = default; + + Worker_ComponentData CreateRPCEndpointData() + { + Worker_ComponentData Data{}; + Data.component_id = ComponentId; + Data.schema_type = Schema_CreateComponentData(ComponentId); + Schema_Object* ComponentObject = Schema_GetComponentDataFields(Data.schema_type); + Schema_AddBool(ComponentObject, SpatialConstants::UNREAL_RPC_ENDPOINT_READY_ID, bReady); + + return Data; + } + + Worker_ComponentUpdate CreateRPCEndpointUpdate() + { + Worker_ComponentUpdate Update{}; + Update.component_id = ComponentId; + Update.schema_type = Schema_CreateComponentUpdate(ComponentId); + Schema_Object* UpdateObject = Schema_GetComponentUpdateFields(Update.schema_type); + Schema_AddBool(UpdateObject, SpatialConstants::UNREAL_RPC_ENDPOINT_READY_ID, bReady); + + return Update; + } + + bool bReady = false; +}; + +} // namespace SpatialGDK diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealMetadata.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealMetadata.h index e09c9085d3..654329d626 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealMetadata.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealMetadata.h @@ -68,18 +68,32 @@ struct UnrealMetadata : Component FORCEINLINE UClass* GetNativeEntityClass() { - if (NativeClass != nullptr) + if (NativeClass.IsValid()) { - return NativeClass; + return NativeClass.Get(); } - if (UClass* Class = LoadObject(nullptr, *ClassPath)) +#if !UE_BUILD_SHIPPING + if (NativeClass.IsStale()) { - if (Class->IsChildOf()) - { - NativeClass = Class; - return Class; - } + UE_LOG(LogSpatialClassInfoManager, Warning, TEXT("UnrealMetadata native class %s unloaded whilst entity in view."), *ClassPath); + } +#endif + UClass* Class = nullptr; + + if (StablyNamedRef.IsSet()) + { + Class = FindObject(nullptr, *ClassPath, false); + } + else + { + Class = LoadObject(nullptr, *ClassPath); + } + + if (Class != nullptr && Class->IsChildOf()) + { + NativeClass = Class; + return Class; } return nullptr; @@ -90,7 +104,7 @@ struct UnrealMetadata : Component FString ClassPath; TSchemaOption bNetStartup; - UClass* NativeClass = nullptr; + TWeakObjectPtr NativeClass; }; FORCEINLINE SubobjectToOffsetMap CreateOffsetMapFromActor(AActor* Actor, const FClassInfo& Info) diff --git a/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h b/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h index d19ba6da26..215ea1505c 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Schema/UnrealObjectRef.h @@ -1,6 +1,7 @@ #pragma once #include "Containers/UnrealString.h" +#include "Templates/TypeHash.h" #include "Utils/SchemaOption.h" diff --git a/SpatialGDK/Source/SpatialGDK/Public/SimulatedPlayers/SimPlayerBPFunctionLibrary.h b/SpatialGDK/Source/SpatialGDK/Public/SimulatedPlayers/SimPlayerBPFunctionLibrary.h new file mode 100644 index 0000000000..ecda5e8b9c --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/SimulatedPlayers/SimPlayerBPFunctionLibrary.h @@ -0,0 +1,22 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Kismet/BlueprintFunctionLibrary.h" + +#include "SimPlayerBPFunctionLibrary.generated.h" + +UCLASS() +class SPATIALGDK_API USimPlayerBPFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** + * Get whether this client is simulated. + * This will return true for clients launched inside simulated player deployments, + * or simulated clients launched from the Editor. + */ + UFUNCTION(BlueprintPure, Category="SpatialOS|SimulatedPlayer", meta = (WorldContext = WorldContextObject)) + static bool IsSimulatedPlayer(const UObject* WorldContextObject); +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialCommonTypes.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialCommonTypes.h index 969a6c9ff0..73ec76f2e5 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialCommonTypes.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialCommonTypes.h @@ -18,4 +18,4 @@ using WriteAclMap = TMap; using FChannelObjectPair = TPair, TWeakObjectPtr>; struct FObjectReferences; using FObjectReferencesMap = TMap; -using FReliableRPCMap = TMap>; +using FReliableRPCMap = TMap>; diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h index cd3d1dc08a..70fe07934e 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialConstants.h @@ -2,10 +2,10 @@ #pragma once -#include "UObject/Script.h" - +#include "Improbable/SpatialEngineConstants.h" #include "Schema/UnrealObjectRef.h" #include "SpatialCommonTypes.h" +#include "UObject/Script.h" #include #include @@ -63,9 +63,13 @@ FORCEINLINE FString RPCSchemaTypeToString(ESchemaComponentType RPCType) switch (RPCType) { case SCHEMA_ClientReliableRPC: - return TEXT("Client"); + return TEXT("Client, Reliable"); + case SCHEMA_ClientUnreliableRPC: + return TEXT("Client, Unreliable"); case SCHEMA_ServerReliableRPC: - return TEXT("Server"); + return TEXT("Server, Reliable"); + case SCHEMA_ServerUnreliableRPC: + return TEXT("Server, Unreliable"); case SCHEMA_NetMulticastRPC: return TEXT("Multicast"); case SCHEMA_CrossServerRPC: @@ -83,8 +87,7 @@ namespace SpatialConstants INVALID_ENTITY_ID = 0, INITIAL_SPAWNER_ENTITY_ID = 1, INITIAL_GLOBAL_STATE_MANAGER_ENTITY_ID = 2, - PLACEHOLDER_ENTITY_ID_FIRST = 3, - PLACEHOLDER_ENTITY_ID_LAST = PLACEHOLDER_ENTITY_ID_FIRST + 35, // 36 placeholder entities. + FIRST_AVAILABLE_ENTITY_ID = 3, }; const Worker_ComponentId INVALID_COMPONENT_ID = 0; @@ -108,6 +111,9 @@ namespace SpatialConstants const Worker_ComponentId SERVER_RPC_ENDPOINT_COMPONENT_ID = 9989; const Worker_ComponentId NETMULTICAST_RPCS_COMPONENT_ID = 9987; const Worker_ComponentId NOT_STREAMED_COMPONENT_ID = 9986; + const Worker_ComponentId RPCS_ON_ENTITY_CREATION_ID = 9985; + const Worker_ComponentId DEBUG_METRICS_COMPONENT_ID = 9984; + const Worker_ComponentId ALWAYS_RELEVANT_COMPONENT_ID = 9983; const Worker_ComponentId STARTING_GENERATED_COMPONENT_ID = 10000; @@ -122,18 +128,36 @@ namespace SpatialConstants const Schema_FieldId ACTOR_TEAROFF_ID = 3; const Schema_FieldId HEARTBEAT_EVENT_ID = 1; + const Schema_FieldId HEARTBEAT_CLIENT_HAS_QUIT_ID = 1; const Schema_FieldId SHUTDOWN_MULTI_PROCESS_REQUEST_ID = 1; const Schema_FieldId SHUTDOWN_ADDITIONAL_SERVERS_EVENT_ID = 1; + const Schema_FieldId CLEAR_RPCS_ON_ENTITY_CREATION = 1; + + // DebugMetrics command IDs + const Schema_FieldId DEBUG_METRICS_START_RPC_METRICS_ID = 1; + const Schema_FieldId DEBUG_METRICS_STOP_RPC_METRICS_ID = 2; + const Schema_FieldId DEBUG_METRICS_MODIFY_SETTINGS_ID = 3; + + // ModifySettingPayload Field IDs + const Schema_FieldId MODIFY_SETTING_PAYLOAD_NAME_ID = 1; + const Schema_FieldId MODIFY_SETTING_PAYLOAD_VALUE_ID = 2; + // UnrealRPCPayload Field IDs - const Schema_FieldId UNREAL_RPC_PAYLOAD_OFFSET_ID = 1; - const Schema_FieldId UNREAL_RPC_PAYLOAD_RPC_INDEX_ID = 2; - const Schema_FieldId UNREAL_RPC_PAYLOAD_RPC_PAYLOAD_ID = 3; + const Schema_FieldId UNREAL_RPC_PAYLOAD_OFFSET_ID = 1; + const Schema_FieldId UNREAL_RPC_PAYLOAD_RPC_INDEX_ID = 2; + const Schema_FieldId UNREAL_RPC_PAYLOAD_RPC_PAYLOAD_ID = 3; + // UnrealPackedRPCPayload additional Field ID + const Schema_FieldId UNREAL_PACKED_RPC_PAYLOAD_ENTITY_ID = 4; // Unreal(Client|Server|Multicast)RPCEndpoint Field IDs - const Schema_FieldId UNREAL_RPC_ENDPOINT_EVENT_ID = 1; - const Schema_FieldId UNREAL_RPC_ENDPOINT_COMMAND_ID = 1; + const Schema_FieldId UNREAL_RPC_ENDPOINT_READY_ID = 1; + const Schema_FieldId UNREAL_RPC_ENDPOINT_EVENT_ID = 1; + const Schema_FieldId UNREAL_RPC_ENDPOINT_PACKED_EVENT_ID = 2; + const Schema_FieldId UNREAL_RPC_ENDPOINT_COMMAND_ID = 1; + + const Schema_FieldId PLAYER_SPAWNER_SPAWN_PLAYER_COMMAND_ID = 1; // Reserved entity IDs expire in 5 minutes, we will refresh them every 3 minutes to be safe. const float ENTITY_RANGE_EXPIRATION_INTERVAL_SECONDS = 180.0f; @@ -141,11 +165,10 @@ namespace SpatialConstants const float FIRST_COMMAND_RETRY_WAIT_SECONDS = 0.2f; const uint32 MAX_NUMBER_COMMAND_ATTEMPTS = 5u; - static const FString ServerWorkerType = TEXT("UnrealWorker"); - static const FString ClientWorkerType = TEXT("UnrealClient"); + static const FName DefaultActorGroup = FName(TEXT("Default")); - const WorkerAttributeSet UnrealServerAttributeSet = TArray{ServerWorkerType}; - const WorkerAttributeSet UnrealClientAttributeSet = TArray{ClientWorkerType}; + const WorkerAttributeSet UnrealServerAttributeSet = TArray{DefaultServerWorkerType.ToString()}; + const WorkerAttributeSet UnrealClientAttributeSet = TArray{DefaultClientWorkerType.ToString()}; const WorkerRequirementSet UnrealServerPermission{ {UnrealServerAttributeSet} }; const WorkerRequirementSet UnrealClientPermission{ {UnrealClientAttributeSet} }; @@ -154,6 +177,10 @@ namespace SpatialConstants static const FString ClientsStayConnectedURLOption = TEXT("clientsStayConnected"); static const FString SnapshotURLOption = TEXT("snapshot="); + static const FString AssemblyPattern = TEXT("^[a-zA-Z0-9_.-]{5,64}$"); + static const FString ProjectPattern = TEXT("^[a-z0-9_]{3,32}$"); + static const FString DeploymentPattern = TEXT("^[a-z0-9_]{2,32}$"); + inline float GetCommandRetryWaitTimeSeconds(uint32 NumAttempts) { // Double the time to wait on each failure. @@ -170,4 +197,37 @@ namespace SpatialConstants const Worker_ComponentId MAX_EXTERNAL_SCHEMA_ID = 2000; const FString SPATIALOS_METRICS_DYNAMIC_FPS = TEXT("Dynamic.FPS"); + + const FString LOCATOR_HOST = TEXT("locator.improbable.io"); + const uint16 LOCATOR_PORT = 444; + + const FString DEVELOPMENT_AUTH_PLAYER_ID = TEXT("Player Id"); +} + +FORCEINLINE Worker_ComponentId SchemaComponentTypeToWorkerComponentId(ESchemaComponentType SchemaType) +{ + switch (SchemaType) + { + case SCHEMA_CrossServerRPC: + { + return SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID; + } + case SCHEMA_NetMulticastRPC: + { + return SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID; + } + case SCHEMA_ClientReliableRPC: + case SCHEMA_ClientUnreliableRPC: + { + return SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID; + } + case SCHEMA_ServerReliableRPC: + case SCHEMA_ServerUnreliableRPC: + { + return SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID; + } + default: + checkNoEntry(); + return SpatialConstants::INVALID_COMPONENT_ID; + } } diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKModule.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKModule.h index dc718e5538..dd46ec78ae 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKModule.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKModule.h @@ -3,11 +3,10 @@ #pragma once #include "CoreMinimal.h" -#include "Developer/Settings/Public/ISettingsContainer.h" -#include "Developer/Settings/Public/ISettingsModule.h" -#include "Developer/Settings/Public/ISettingsSection.h" #include "Modules/ModuleManager.h" +#include "Utils/EngineVersionCheck.h" + #include "SpatialGDKLoader.h" DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKModule, Log, All); diff --git a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h index 9ae0d640d7..f48b5396e2 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h +++ b/SpatialGDK/Source/SpatialGDK/Public/SpatialGDKSettings.h @@ -5,6 +5,7 @@ #include "CoreMinimal.h" #include "Engine/EngineTypes.h" #include "Misc/Paths.h" +#include "Utils/ActorGroupManager.h" #include "SpatialGDKSettings.generated.h" @@ -17,57 +18,84 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject USpatialGDKSettings(const FObjectInitializer& ObjectInitializer); #if WITH_EDITOR - virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + 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 */ + /** + * The number of entity IDs to be reserved when the entity pool is first created. Ensure that the number of entity IDs + * reserved is greater than the number of Actors that you expect the server-worker instances to spawn at game deployment + */ UPROPERTY(EditAnywhere, config, Category = "Entity Pool", meta = (ConfigRestartRequired = false, DisplayName = "Initial Entity ID Reservation Count")) uint32 EntityPoolInitialReservationCount; - /** The minimum number of entity IDs available in the pool before a new batch is reserved */ - UPROPERTY(EditAnywhere, config, Category = "Entity Pool", meta = (ConfigRestartRequired = false, DisplayName = "Pool Refresh Minimum Threshold")) + /** + * Specifies when the SpatialOS Runtime should reserve a new batch of entity IDs: the value is the number of un-used entity + * IDs left in the entity pool which triggers the SpatialOS Runtime to reserve new entity IDs + */ + UPROPERTY(EditAnywhere, config, Category = "Entity Pool", meta = (ConfigRestartRequired = false, DisplayName = "Pool Refresh Threshold")) uint32 EntityPoolRefreshThreshold; - /** The number of entity IDs reserved when the minimum threshold is reached */ + /** + * Specifies the number of new entity IDs the SpatialOS Runtime reserves when `Pool refresh threshold` triggers a new batch. + */ UPROPERTY(EditAnywhere, config, Category = "Entity Pool", meta = (ConfigRestartRequired = false, DisplayName = "Refresh Count")) uint32 EntityPoolRefreshCount; - /** Time between heartbeat events sent from clients to notify the servers they are still connected. */ + /** Specifies the amount of time, in seconds, between heartbeat events sent from a game client to notify the server-worker instances that it's connected. */ UPROPERTY(EditAnywhere, config, Category = "Heartbeat", meta = (ConfigRestartRequired = false, DisplayName = "Heartbeat Interval (seconds)")) float HeartbeatIntervalSeconds; - /** Time that should pass since the last heartbeat event received to decide a 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 = (ConfigRestartRequired = false, DisplayName = "Heartbeat Timeout (seconds)")) float HeartbeatTimeoutSeconds; /** - * Limit the number of actors which are replicated per tick to the number specified. - * This acts as a hard limit to the number of actors per frame but nothing else. It's recommended to set this value to around 100~ (experimentation recommended). - * If set to 0, SpatialOS will replicate every actor per frame (unbounded) and so large worlds will experience slowdown server-side and client-side. - * Use `stat SpatialNet` in editor builds to find the number of calls to 'ReplicateActor' and use this to inform the rate limit setting. + * Specifies the maximum number of Actors replicated per tick. + * Default: `0` per tick (no limit) + * (If you set the value to ` 0`, the SpatialOS Runtime replicates every Actor per tick; this forms a large SpatialOS world, affecting the performance of both game clients and server-worker instances.) + * You can use the `stat Spatial` flag when you run project builds to find the number of calls to `ReplicateActor`, and then use this number for reference. */ - UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (ConfigRestartRequired = false, DisplayName = "Actor Replication Rate Limit")) + UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (ConfigRestartRequired = false, DisplayName = "Maximum Actors replicated per tick")) uint32 ActorReplicationRateLimit; - /** Limits the number of entities which can be created in a game tick. Entity creation is handled seperately to actor replication to ensure creation requests are always handled when under load. **/ - UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (ConfigRestartRequired = false, DisplayName = "Entity Creation Rate Limit")) + /** + * Specifies the maximum number of entities created by the SpatialOS Runtime per tick. + * (The SpatialOS Runtime handles entity creation separately from Actor replication to ensure it can handle entity creation requests under load.) + * Note: if you set the value to 0, there is no limit to the number of entities created per tick. However, too many entities created at the same time might overload the SpatialOS Runtime, which can negatively affect your game. + * Default: `0` per tick (no limit) + */ + UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (ConfigRestartRequired = false, DisplayName = "Maximum entities created per tick")) uint32 EntityCreationRateLimit; - /** Rate at which updates are sent to SpatialOS and processed from SpatialOS.*/ + /** + * Specifies the rate, in number of times per second, at which server-worker instance updates are sent to and received from the SpatialOS Runtime. + * Default:1000/s + */ UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (ConfigRestartRequired = false, DisplayName = "SpatialOS Network Update Rate")) float OpsUpdateRate; + /** Replicate handover properties between servers, required for zoned worker deployments.*/ + UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (ConfigRestartRequired = false)) + bool bEnableHandover; + + /** Maximum NetCullDistanceSquared value used in Spatial networking. Set to 0.0 to disable. This is temporary and will be removed when the runtime issue is resolved.*/ + UPROPERTY(EditAnywhere, config, Category = "Replication", meta = (ConfigRestartRequired = false)) + float MaxNetCullDistanceSquared; + /** Query Based Interest is required for level streaming and the AlwaysInterested UPROPERTY specifier to be supported when using spatial networking, however comes at a performance cost for larger-scale projects.*/ - UPROPERTY(EditAnywhere, config, Category = "Query Based Interest", meta = (ConfigRestartRequired = false, DisplayName = "Query Based Interest Enabled")) + UPROPERTY(config, meta = (ConfigRestartRequired = false)) bool bUsingQBI; /** Frequency for updating an Actor's SpatialOS Position. Updating position should have a low update rate since it is expensive.*/ UPROPERTY(EditAnywhere, config, Category = "SpatialOS Position Updates", meta = (ConfigRestartRequired = false)) float PositionUpdateFrequency; - /** Threshold an Actor needs to move before its SpatialOS Position is updated.*/ + /** Threshold an Actor needs to move, in centimeters, before its SpatialOS Position is updated.*/ UPROPERTY(EditAnywhere, config, Category = "SpatialOS Position Updates", meta = (ConfigRestartRequired = false)) float PositionDistanceThreshold; @@ -75,12 +103,72 @@ class SPATIALGDK_API USpatialGDKSettings : public UObject UPROPERTY(EditAnywhere, config, Category = "Metrics", meta = (ConfigRestartRequired = false)) bool bEnableMetrics; + /** Display server metrics on clients.*/ + UPROPERTY(EditAnywhere, config, Category = "Metrics", meta = (ConfigRestartRequired = false)) + bool bEnableMetricsDisplay; + /** Frequency that metrics are reported to SpatialOS.*/ UPROPERTY(EditAnywhere, config, Category = "Metrics", meta = (ConfigRestartRequired = false), DisplayName = "Metrics Report Rate (seconds)") float MetricsReportRate; - /** Change 'Load' value in inspector to represent worker Frame Time instead of a fraction of target FPS.*/ + /** + * 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", meta = (ConfigRestartRequired = false)) bool bUseFrameTimeAsLoad; -}; + /** Include an order index with reliable RPCs and warn if they are executed out of order.*/ + UPROPERTY(config, meta = (ConfigRestartRequired = false)) + bool bCheckRPCOrder; + + /** Batch entity position updates to be processed on a single frame.*/ + UPROPERTY(config, meta = (ConfigRestartRequired = false)) + bool bBatchSpatialPositionUpdates; + + /** Maximum number of ActorComponents/Subobjects of the same class that can be attached to an Actor.*/ + UPROPERTY(EditAnywhere, config, Category = "Schema Generation", meta = (ConfigRestartRequired = false), DisplayName = "Maximum Dynamically Attached Subobjects Per Class") + uint32 MaxDynamicallyAttachedSubobjectsPerClass; + + /** EXPERIMENTAL - This is a stop-gap until we can better define server interest on system entities. + Disabling this is not supported in any type of multi-server environment*/ + UPROPERTY(config, meta = (ConfigRestartRequired = false)) + bool bEnableServerQBI; + + /** Pack RPCs sent during the same frame into a single update. */ + UPROPERTY(config, meta = (ConfigRestartRequired = false)) + bool bPackRPCs; + + /** The receptionist host to use if no 'receptionistHost' argument is passed to the command line. */ + UPROPERTY(EditAnywhere, config, Category = "Local Connection", meta = (ConfigRestartRequired = false)) + FString DefaultReceptionistHost; + + /** If the Development Authentication Flow is used, the client will try to connect to the cloud rather than local deployment. */ + UPROPERTY(EditAnywhere, config, Category = "Cloud Connection", meta = (ConfigRestartRequired = false)) + bool bUseDevelopmentAuthenticationFlow; + + /** The token created using 'spatial project auth dev-auth-token' */ + UPROPERTY(EditAnywhere, config, Category = "Cloud Connection", meta = (ConfigRestartRequired = false)) + 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", meta = (ConfigRestartRequired = false)) + FString DevelopmentDeploymentToConnect; + + /** 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. */ + 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; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/ActorGroupManager.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/ActorGroupManager.h new file mode 100644 index 0000000000..60bc525b6d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/ActorGroupManager.h @@ -0,0 +1,78 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "SpatialConstants.h" + +#include "ActorGroupManager.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() + { + } +}; + +UCLASS(Config=SpatialGDKSettings) +class SPATIALGDK_API UActorGroupManager : public UObject +{ + GENERATED_BODY() + +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/ComponentFactory.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentFactory.h index 173d016d4a..5bb050d106 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentFactory.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentFactory.h @@ -33,6 +33,8 @@ class SPATIALGDK_API ComponentFactory TArray CreateComponentDatas(UObject* Object, const FClassInfo& Info, const FRepChangeState& RepChangeState, const FHandoverChangeState& HandoverChangeState); TArray CreateComponentUpdates(UObject* Object, const FClassInfo& Info, Worker_EntityId EntityId, const FRepChangeState* RepChangeState, const FHandoverChangeState* HandoverChangeState); + Worker_ComponentData CreateHandoverComponentData(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes); + static Worker_ComponentData CreateEmptyComponentData(Worker_ComponentId ComponentId); private: @@ -41,7 +43,6 @@ class SPATIALGDK_API ComponentFactory bool FillSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FRepChangeState& Changes, ESchemaComponentType PropertyGroup, bool bIsInitialData, TArray* ClearedIds = nullptr); - Worker_ComponentData CreateHandoverComponentData(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes); Worker_ComponentUpdate CreateHandoverComponentUpdate(Worker_ComponentId ComponentId, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes, bool& bWroteSomething); bool FillHandoverSchemaObject(Schema_Object* ComponentObject, UObject* Object, const FClassInfo& Info, const FHandoverChangeState& Changes, bool bIsInitialData, TArray* ClearedIds = nullptr); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentReader.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentReader.h index 0464d3d81b..f0d654f501 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentReader.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/ComponentReader.h @@ -22,8 +22,8 @@ class ComponentReader void ApplySchemaObject(Schema_Object* ComponentObject, UObject* Object, USpatialActorChannel* Channel, bool bIsInitialData, TArray& UpdatedIds); void ApplyHandoverSchemaObject(Schema_Object* ComponentObject, UObject* Object, USpatialActorChannel* Channel, bool bIsInitialData, TArray& UpdatedIds); - void ApplyProperty(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, uint32 Index, UProperty* Property, uint8* Data, int32 Offset, int32 ParentIndex); - void ApplyArray(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, UArrayProperty* Property, uint8* Data, int32 Offset, int32 ParentIndex); + void ApplyProperty(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, uint32 Index, UProperty* Property, uint8* Data, int32 Offset, int32 CmdIndex, int32 ParentIndex); + void ApplyArray(Schema_Object* Object, Schema_FieldId FieldId, FObjectReferencesMap& InObjectReferencesMap, UArrayProperty* Property, uint8* Data, int32 Offset, int32 CmdIndex, int32 ParentIndex); uint32 GetPropertyCount(const Schema_Object* Object, Schema_FieldId Id, UProperty* Property); diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/EngineVersionCheck.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/EngineVersionCheck.h index 29dff94549..9393c86659 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 1 +#define SPATIAL_GDK_VERSION 5 // 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/InterestFactory.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/InterestFactory.h index 97df6ee8b8..a34c98adb0 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/InterestFactory.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/InterestFactory.h @@ -16,40 +16,39 @@ DECLARE_LOG_CATEGORY_EXTERN(LogInterestFactory, Log, All); namespace SpatialGDK { +void GatherClientInterestDistances(); + class SPATIALGDK_API InterestFactory { public: InterestFactory(AActor* InActor, const FClassInfo& InInfo, USpatialNetDriver* InNetDriver); - Worker_ComponentData CreateInterestData(); - Worker_ComponentUpdate CreateInterestUpdate(); + Worker_ComponentData CreateInterestData() const; + Worker_ComponentUpdate CreateInterestUpdate() const; private: - Interest CreateInterest(); + Interest CreateInterest() const; // Only uses Defined Constraint - Interest CreateActorInterest(); + Interest CreateActorInterest() const; // Defined Constraint AND Level Constraint - Interest CreatePlayerOwnedActorInterest(); + Interest CreatePlayerOwnedActorInterest() const; -private: - // System Constraint OR User Constraint - QueryConstraint CreateDefinedConstraints(); + void AddUserDefinedQueries(const QueryConstraint& LevelConstraints, TArray& OutQueries) const; // Checkout Constraint OR AlwaysInterested Constraint - QueryConstraint CreateSystemDefinedConstraints(); - - // TODO: Will be created utilizing user defined structs - QueryConstraint CreateUserDefinedConstraints(); + QueryConstraint CreateSystemDefinedConstraints() const; // System Defined Constraints - QueryConstraint CreateCheckoutRadiusConstraint(); - QueryConstraint CreateAlwaysInterestedConstraint(); + QueryConstraint CreateCheckoutRadiusConstraints() const; + QueryConstraint CreateAlwaysInterestedConstraint() const; + QueryConstraint CreateAlwaysRelevantConstraint() const; // Only checkout entities that are in loaded sublevels - QueryConstraint CreateLevelConstraints(); + QueryConstraint CreateLevelConstraints() const; - void AddObjectToConstraint(UObjectPropertyBase* Property, uint8* Data, QueryConstraint& OutConstraint); + void AddObjectToConstraint(UObjectPropertyBase* Property, uint8* Data, QueryConstraint& OutConstraint) const; + void AddTypeHierarchyToConstraint(const UClass& BaseType, QueryConstraint& OutConstraint) const; AActor* Actor; const FClassInfo& Info; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/OpUtils.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/OpUtils.h new file mode 100644 index 0000000000..bbdcc3b53f --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/OpUtils.h @@ -0,0 +1,14 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" + +#include + +namespace SpatialGDK +{ +void FindFirstOpOfType(const TArray& InOpLists, const Worker_OpType OpType, Worker_Op** OutOp); +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 new file mode 100644 index 0000000000..5444e31f77 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/RPCContainer.h @@ -0,0 +1,41 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Schema/RPCPayload.h" +#include "Schema/UnrealObjectRef.h" +#include "SpatialConstants.h" + +#include "CoreMinimal.h" + +struct FPendingRPCParams; +using FPendingRPCParamsPtr = TUniquePtr; +DECLARE_DELEGATE_RetVal_OneParam(bool, FProcessRPCDelegate, const FPendingRPCParams&) + +struct FPendingRPCParams +{ + FPendingRPCParams(const FUnrealObjectRef& InTargetObjectRef, SpatialGDK::RPCPayload&& InPayload, int InReliableRPCIndex = 0); + + // TODO: UNR-1653 Redesign bCheckRPCOrder Tests functionality + int ReliableRPCIndex; + FUnrealObjectRef ObjectRef; + SpatialGDK::RPCPayload Payload; +}; + +class FRPCContainer +{ +public: + void QueueRPC(FPendingRPCParamsPtr Params, ESchemaComponentType Type); + void ProcessRPCs(const FProcessRPCDelegate& FunctionToApply); + bool ObjectHasRPCsQueuedOfType(const Worker_EntityId& EntityId, ESchemaComponentType Type) const; + +private: + using FArrayOfParams = TArray; + using FRPCMap = TMap; + using RPCContainerType = TMap; + + void ProcessRPCs(const FProcessRPCDelegate& FunctionToApply, FArrayOfParams& RPCList); + static bool ApplyFunction(const FProcessRPCDelegate& FunctionToApply, const FPendingRPCParams& Params); + + RPCContainerType QueuedRPCs; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/RepLayoutUtils.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/RepLayoutUtils.h index 26d8284e83..834c97f9a7 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/RepLayoutUtils.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/RepLayoutUtils.h @@ -6,6 +6,7 @@ #include "Net/RepLayout.h" #include "EngineClasses/SpatialNetBitReader.h" +#include "EngineClasses/SpatialNetBitWriter.h" #include "EngineClasses/SpatialNetDriver.h" #include "EngineClasses/SpatialPackageMapClient.h" @@ -141,7 +142,12 @@ inline void ReadStructProperty(FSpatialNetBitReader& Reader, UStructProperty* Pr { bOutHasUnmapped = true; } - checkf(bSuccess, TEXT("NetSerialize on %s failed."), *Struct->GetStructCPPName()); + + // Check the success of the serialization and print a warning if it failed. This is how native handles failed serialization. + if (!bSuccess) + { + UE_LOG(LogSpatialNetSerialize, Warning, TEXT("ReadStructProperty: NetSerialize %s failed."), *Struct->GetFullName()); + } } else { diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaDatabase.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaDatabase.h index f905771b39..c919b88828 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaDatabase.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaDatabase.h @@ -1,5 +1,6 @@ #pragma once +#include "Containers/StaticArray.h" #include "CoreMinimal.h" #include "Engine/DataAsset.h" #include "Engine/World.h" @@ -7,34 +8,68 @@ #include "SchemaDatabase.generated.h" +// Schema data related to a default Subobject owned by a specific Actor class. USTRUCT() -struct FSubobjectSchemaData +struct FActorSpecificSubobjectSchemaData { GENERATED_USTRUCT_BODY() - UPROPERTY(VisibleAnywhere) + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) FString ClassPath; - UPROPERTY(VisibleAnywhere) + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) FName Name; - UPROPERTY(VisibleAnywhere) + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) uint32 SchemaComponents[SCHEMA_Count] = {}; }; +// Schema data related to an Actor class USTRUCT() -struct FSchemaData +struct FActorSchemaData { GENERATED_USTRUCT_BODY() - UPROPERTY(VisibleAnywhere) + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) FString GeneratedSchemaName; - UPROPERTY(VisibleAnywhere) + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) uint32 SchemaComponents[SCHEMA_Count] = {}; - UPROPERTY(VisibleAnywhere) - TMap SubobjectData; + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) + TMap SubobjectData; +}; + +USTRUCT() +struct FDynamicSubobjectSchemaData +{ + GENERATED_USTRUCT_BODY() + + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) + uint32 SchemaComponents[SCHEMA_Count] = {}; +}; + +// Schema data related to a Subobject class +USTRUCT() +struct FSubobjectSchemaData +{ + GENERATED_USTRUCT_BODY() + + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) + FString GeneratedSchemaName; + + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) + TArray DynamicSubobjectComponents; + + FORCEINLINE Worker_ComponentId GetDynamicSubobjectComponentId(int Idx, ESchemaComponentType ComponentType) const + { + Worker_ComponentId ComponentId = 0; + if (Idx < DynamicSubobjectComponents.Num()) + { + ComponentId = DynamicSubobjectComponents[Idx].SchemaComponents[ComponentType]; + } + return ComponentId; + } }; UCLASS() @@ -46,26 +81,22 @@ class SPATIALGDK_API USchemaDatabase : public UDataAsset USchemaDatabase() : NextAvailableComponentId(SpatialConstants::STARTING_GENERATED_COMPONENT_ID) {} - uint32 GetComponentIdFromLevelPath(const FString& LevelPath) const - { - FString CleanLevelPath = UWorld::RemovePIEPrefix(LevelPath); - if (const uint32* ComponentId = LevelPathToComponentId.Find(CleanLevelPath)) - { - return *ComponentId; - } - return SpatialConstants::INVALID_COMPONENT_ID; - } + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) + TMap ActorClassPathToSchema; - UPROPERTY(VisibleAnywhere) - TMap ClassPathToSchema; + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) + TMap SubobjectClassPathToSchema; - UPROPERTY(VisibleAnywhere) + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) TMap LevelPathToComponentId; - UPROPERTY(VisibleAnywhere) + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) + TMap ComponentIdToClassPath; + + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) TSet LevelComponentIds; - UPROPERTY(VisibleAnywhere) + UPROPERTY(Category = "SpatialGDK", VisibleAnywhere) uint32 NextAvailableComponentId; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaUtils.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaUtils.h index f8b782ee5a..ca842c3223 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaUtils.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SchemaUtils.h @@ -43,12 +43,16 @@ inline bool GetBoolFromSchema(const Schema_Object* Object, Schema_FieldId Id) return !!Schema_GetBool(Object, Id); } +inline void AddBytesToSchema(Schema_Object* Object, Schema_FieldId Id, const uint8* Data, uint32 NumBytes) +{ + uint8* PayloadBuffer = Schema_AllocateBuffer(Object, sizeof(char) * NumBytes); + FMemory::Memcpy(PayloadBuffer, Data, sizeof(char) * NumBytes); + Schema_AddBytes(Object, Id, PayloadBuffer, sizeof(char) * NumBytes); +} + inline void AddBytesToSchema(Schema_Object* Object, Schema_FieldId Id, FBitWriter& Writer) { - uint32 PayloadSize = Writer.GetNumBytes(); - uint8* PayloadBuffer = Schema_AllocateBuffer(Object, sizeof(char) * PayloadSize); - FMemory::Memcpy(PayloadBuffer, Writer.GetData(), sizeof(char) * PayloadSize); - Schema_AddBytes(Object, Id, PayloadBuffer, sizeof(char) * PayloadSize); + AddBytesToSchema(Object, Id, Writer.GetData(), Writer.GetNumBytes()); } inline TArray IndexBytesFromSchema(const Schema_Object* Object, Schema_FieldId Id, uint32 Index) diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h index e2115c81cf..e6fe5a3d55 100644 --- a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetrics.h @@ -4,14 +4,19 @@ #include "CoreMinimal.h" +#include "SpatialConstants.h" + #include #include #include "SpatialMetrics.generated.h" +struct Schema_Object; class USpatialNetDriver; class USpatialWorkerConnection; +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialMetrics, Log, All); + UCLASS() class USpatialMetrics : public UObject { @@ -24,6 +29,23 @@ class USpatialMetrics : public UObject double CalculateLoad() const; + double GetAverageFPS() const { return AverageFPS; } + double GetWorkerLoad() const { return WorkerLoad; } + + UFUNCTION(Exec) + void SpatialStartRPCMetrics(); + void OnStartRPCMetricsCommand(); + + UFUNCTION(Exec) + void SpatialStopRPCMetrics(); + void OnStopRPCMetricsCommand(); + + UFUNCTION(Exec) + void SpatialModifySetting(const FString& Name, float Value); + void OnModifySettingCommand(Schema_Object* CommandPayload); + + void TrackSentRPC(UFunction* Function, ESchemaComponentType RPCType, int PayloadSize); + private: UPROPERTY() USpatialNetDriver* NetDriver; @@ -35,5 +57,20 @@ class USpatialMetrics : public UObject double AverageFPS; double WorkerLoad; + + // 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 + // tracked data upon stopping. Calling these console commands on the client will also start/stop RPC + // tracking on the server. + struct RPCStat + { + ESchemaComponentType Type; + FString Name; + int Calls; + int TotalPayload; + }; + TMap RecentRPCs; + bool bRPCTrackingEnabled; + float RPCTrackingStartTime; }; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetricsDisplay.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetricsDisplay.h new file mode 100644 index 0000000000..b0807329bf --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialMetricsDisplay.h @@ -0,0 +1,75 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "Containers/Queue.h" +#include "GameFramework/Info.h" + +#include "SpatialMetricsDisplay.generated.h" + +USTRUCT() +struct FWorkerStats +{ + GENERATED_BODY() + + UPROPERTY() + FString WorkerName; + UPROPERTY() + float AverageFPS; + UPROPERTY() + float ServerMovementCorrections; // per second + UPROPERTY() + int32 ServerConsiderListSize; + UPROPERTY() + uint32 ServerReplicationLimit; + + bool operator==(const FWorkerStats& other) const + { + return (WorkerName.Equals(other.WorkerName)); + } +}; + +UCLASS(SpatialType=Singleton) +class SPATIALGDK_API ASpatialMetricsDisplay : + public AInfo +{ + GENERATED_UCLASS_BODY() + +public: + + virtual void Tick(float DeltaSeconds) override; + + virtual void BeginPlay() override; + virtual void Destroyed() override; + + UFUNCTION(Category = "SpatialGDK", BlueprintCallable) + void ToggleStatDisplay(); + +private: + + FDelegateHandle DrawDebugDelegateHandle; + + UPROPERTY(Replicated) + TArray WorkerStats; + + TMap WorkerStatsLastUpdateTime; + + const uint32 PreallocatedWorkerCount = 8; + const uint32 WorkerNameMaxLength = 18; + + const uint32 DropStatsIfNoUpdateForTime = 10; // seconds + + UFUNCTION(CrossServer, Unreliable, WithValidation) + virtual void ServerUpdateWorkerStats(const float Time, const FWorkerStats& OneWorkerStats); + + bool ShouldRemoveStats(const float CurrentTime, const FWorkerStats& OneWorkerStats) const; + void DrawDebug(class UCanvas* Canvas, APlayerController* Controller); + + struct MovementCorrectionRecord + { + int32 MovementCorrections; + float Time; + }; + TQueue MovementCorrectionRecords; +}; diff --git a/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialStatics.h b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialStatics.h new file mode 100644 index 0000000000..56a969c94a --- /dev/null +++ b/SpatialGDK/Source/SpatialGDK/Public/Utils/SpatialStatics.h @@ -0,0 +1,62 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Kismet/BlueprintFunctionLibrary.h" +#include "Templates/SubclassOf.h" +#include "SpatialStatics.generated.h" + +class AActor; + +UCLASS() +class SPATIALGDK_API USpatialStatics : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + + /** + * Returns true if SpatialOS Networking is enabled. + */ + UFUNCTION(BlueprintPure, Category = "SpatialOS") + static bool IsSpatialNetworkingEnabled(); + + /** + * Returns true if the current Worker Type owns the Actor Group this Actor belongs to. + * Equivalent to World->GetNetMode() != NM_Client when Spatial Networking is disabled. + */ + UFUNCTION(BlueprintPure, Category = "SpatialOS|Offloading") + static bool IsActorGroupOwnerForActor(const AActor* Actor); + + /** + * Returns true if the current Worker Type owns the Actor Group this Actor Class belongs to. + * Equivalent to World->GetNetMode() != NM_Client when Spatial Networking is disabled. + */ + 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); + +private: + + static bool IsSpatialOffloadingEnabled(); + static class UActorGroupManager* GetActorGroupManager(const UObject* WorldContext); + static FName GetCurrentWorkerType(const UObject* WorldContext); +}; diff --git a/SpatialGDK/Source/SpatialGDK/SpatialGDK.Build.cs b/SpatialGDK/Source/SpatialGDK/SpatialGDK.Build.cs index 4c2da40102..2e8d328ac0 100644 --- a/SpatialGDK/Source/SpatialGDK/SpatialGDK.Build.cs +++ b/SpatialGDK/Source/SpatialGDK/SpatialGDK.Build.cs @@ -33,9 +33,14 @@ public SpatialGDK(ReadOnlyTargetRules Target) : base(Target) if (Target.bBuildEditor) { PublicDependencyModuleNames.Add("UnrealEd"); - PublicDependencyModuleNames.Add("SpatialGDKEditorToolbar"); + PublicDependencyModuleNames.Add("SpatialGDKServices"); } + if (Target.bWithPerfCounters) + { + PublicDependencyModuleNames.Add("PerfCounters"); + } + var WorkerLibraryDir = Path.GetFullPath(Path.Combine(ModuleDirectory, "..", "..", "Binaries", "ThirdParty", "Improbable", Target.Platform.ToString())); string LibPrefix = ""; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.cpp index 2c5b85c11a..975ab957d3 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.cpp @@ -8,6 +8,7 @@ #include "UObject/TextProperty.h" #include "Interop/SpatialClassInfoManager.h" +#include "SpatialGDKSettings.h" #include "Utils/CodeWriter.h" #include "Utils/ComponentIdGenerator.h" #include "Utils/DataTypeUtilities.h" @@ -199,7 +200,7 @@ bool IsReplicatedSubobject(TSharedPtr TypeInfo) return false; } -void GenerateSubobjectSchema(UClass* Class, TSharedPtr TypeInfo, FString SchemaPath) +void GenerateSubobjectSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSharedPtr TypeInfo, FString SchemaPath) { FCodeWriter Writer; @@ -240,7 +241,10 @@ void GenerateSubobjectSchema(UClass* Class, TSharedPtr TypeInfo, FS for (EReplicatedPropertyGroup Group : GetAllReplicatedPropertyGroups()) { - if (RepData[Group].Num() == 0) + // Since it is possible to replicate subobjects which have no replicated properties. + // We need to generate a schema component for every subobject. So if we have no replicated + // properties, we only don't generate a schema component if we are REP_SingleClient + if (RepData[Group].Num() == 0 && Group == REP_SingleClient) { continue; } @@ -259,20 +263,6 @@ void GenerateSubobjectSchema(UClass* Class, TSharedPtr TypeInfo, FS } } - // If this class is an Actor, it MUST have bTearOff at field ID 3. - if (Group == REP_MultiClient && Class->IsChildOf()) - { - TSharedPtr ExpectedReplicatesPropData = RepData[Group].FindRef(SpatialConstants::ACTOR_TEAROFF_ID); - const UProperty* ReplicatesProp = AActor::StaticClass()->FindPropertyByName("bTearOff"); - - if (!(ExpectedReplicatesPropData.IsValid() && ExpectedReplicatesPropData->Property == ReplicatesProp)) - { - UE_LOG(LogSchemaGenerator, Error, TEXT("Did not find Actor->bTearOff at field %d for class %s. Modifying the base Actor class is currently not supported."), - SpatialConstants::ACTOR_TEAROFF_ID, - *Class->GetName()); - } - } - Writer.PrintNewLine(); Writer.Printf("type {0} {", *SchemaReplicatedDataName(Group, Class)); Writer.Indent(); @@ -303,12 +293,95 @@ void GenerateSubobjectSchema(UClass* Class, TSharedPtr TypeInfo, FS Writer.Outdent().Print("}"); } + // Use the max number of dynamically attached subobjects per class to generate + // that many schema components for this subobject. + const uint32 DynamicComponentsPerClass = GetDefault()->MaxDynamicallyAttachedSubobjectsPerClass; + + FSubobjectSchemaData SubobjectSchemaData; + + // Use previously generated component IDs when possible. + const FSubobjectSchemaData* const ExistingSchemaData = SubobjectClassPathToSchema.Find(Class->GetPathName()); + if (ExistingSchemaData != nullptr && !ExistingSchemaData->GeneratedSchemaName.IsEmpty() + && ExistingSchemaData->GeneratedSchemaName != ClassPathToSchemaName[Class->GetPathName()]) + { + UE_LOG(LogSchemaGenerator, Error, TEXT("Saved generated schema name does not match in-memory version for class %s - schema %s : %s"), + *Class->GetPathName(), *ExistingSchemaData->GeneratedSchemaName, *ClassPathToSchemaName[Class->GetPathName()]); + UE_LOG(LogSchemaGenerator, Error, TEXT("Schema generation may have resulted in component name clash, recommend you perform a full schema generation")); + } + + for (uint32 i = 1; i <= DynamicComponentsPerClass; i++) + { + FDynamicSubobjectSchemaData DynamicSubobjectComponents; + + for (EReplicatedPropertyGroup Group : GetAllReplicatedPropertyGroups()) + { + // Since it is possible to replicate subobjects which have no replicated properties. + // We need to generate a schema component for every subobject. So if we have no replicated + // properties, we only don't generate a schema component if we are REP_SingleClient + if (RepData[Group].Num() == 0 && Group == REP_SingleClient) + { + continue; + } + + Writer.PrintNewLine(); + + Worker_ComponentId ComponentId = 0; + if (ExistingSchemaData != nullptr) + { + ComponentId = ExistingSchemaData->GetDynamicSubobjectComponentId(i - 1, PropertyGroupToSchemaComponentType(Group)); + } + + if (ComponentId == 0) + { + ComponentId = IdGenerator.Next(); + } + FString ComponentName = SchemaReplicatedDataName(Group, Class) + TEXT("Dynamic") + FString::FromInt(i); + + Writer.Printf("component {0} {", *ComponentName); + Writer.Indent(); + Writer.Printf("id = {0};", ComponentId); + Writer.Printf("data {0};", *SchemaReplicatedDataName(Group, Class)); + Writer.Outdent().Print("}"); + + DynamicSubobjectComponents.SchemaComponents[PropertyGroupToSchemaComponentType(Group)] = ComponentId; + } + + if (HandoverData.Num() > 0) + { + Writer.PrintNewLine(); + + Worker_ComponentId ComponentId = 0; + if (ExistingSchemaData != nullptr) + { + ComponentId = ExistingSchemaData->GetDynamicSubobjectComponentId(i - 1, SCHEMA_Handover); + } + + if (ComponentId == 0) + { + ComponentId = IdGenerator.Next(); + } + FString ComponentName = SchemaHandoverDataName(Class) + TEXT("Dynamic") + FString::FromInt(i); + + Writer.Printf("component {0} {", *ComponentName); + Writer.Indent(); + Writer.Printf("id = {0};", ComponentId); + Writer.Printf("data {0};", *SchemaHandoverDataName(Class)); + Writer.Outdent().Print("}"); + + DynamicSubobjectComponents.SchemaComponents[SCHEMA_Handover] = ComponentId; + } + + SubobjectSchemaData.DynamicSubobjectComponents.Add(MoveTemp(DynamicSubobjectComponents)); + } + Writer.WriteToFile(FString::Printf(TEXT("%s%s.schema"), *SchemaPath, *ClassPathToSchemaName[Class->GetPathName()])); + SubobjectSchemaData.GeneratedSchemaName = ClassPathToSchemaName[Class->GetPathName()]; + SubobjectClassPathToSchema.Add(Class->GetPathName(), SubobjectSchemaData); } void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSharedPtr TypeInfo, FString SchemaPath) { - const FSchemaData* const SchemaData = ClassPathToSchemaData.Find(Class->GetPathName()); + const FActorSchemaData* const SchemaData = ActorClassPathToSchema.Find(Class->GetPathName()); FCodeWriter Writer; @@ -322,7 +395,7 @@ void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSha Writer.PrintNewLine(); Writer.Printf("import \"unreal/gdk/core_types.schema\";"); - FSchemaData ActorSchemaData; + FActorSchemaData ActorSchemaData; ActorSchemaData.GeneratedSchemaName = ClassPathToSchemaName[Class->GetPathName()]; FUnrealFlatRepData RepData = GetFlatRepData(TypeInfo); @@ -335,6 +408,20 @@ void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSha continue; } + // If this class is an Actor, it MUST have bTearOff at field ID 3. + if (Group == REP_MultiClient && Class->IsChildOf()) + { + TSharedPtr ExpectedReplicatesPropData = RepData[Group].FindRef(SpatialConstants::ACTOR_TEAROFF_ID); + const UProperty* ReplicatesProp = AActor::StaticClass()->FindPropertyByName("bTearOff"); + + if (!(ExpectedReplicatesPropData.IsValid() && ExpectedReplicatesPropData->Property == ReplicatesProp)) + { + UE_LOG(LogSchemaGenerator, Error, TEXT("Did not find Actor->bTearOff at field %d for class %s. Modifying the base Actor class is currently not supported."), + SpatialConstants::ACTOR_TEAROFF_ID, + *Class->GetName()); + } + } + Worker_ComponentId ComponentId = 0; if (SchemaData != nullptr && SchemaData->SchemaComponents[PropertyGroupToSchemaComponentType(Group)] != 0) { @@ -398,34 +485,34 @@ void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSha Writer.Outdent().Print("}"); } - GenerateSubobjectSchemaForActor(IdGenerator, Class, TypeInfo, SchemaPath, ActorSchemaData); + GenerateSubobjectSchemaForActor(IdGenerator, Class, TypeInfo, SchemaPath, ActorSchemaData, ActorClassPathToSchema.Find(Class->GetPathName())); - ClassPathToSchemaData.Add(Class->GetPathName(), ActorSchemaData); + ActorClassPathToSchema.Add(Class->GetPathName(), ActorSchemaData); Writer.WriteToFile(FString::Printf(TEXT("%s%s.schema"), *SchemaPath, *ClassPathToSchemaName[Class->GetPathName()])); } -FSubobjectSchemaData GenerateSubobjectSpecificSchema(FCodeWriter& Writer, FComponentIdGenerator& IdGenerator, FString PropertyName, TSharedPtr& TypeInfo, UClass* ComponentClass, UClass* ActorClass, int MapIndex) +FActorSpecificSubobjectSchemaData GenerateSchemaForStaticallyAttachedSubobject(FCodeWriter& Writer, FComponentIdGenerator& IdGenerator, FString PropertyName, TSharedPtr& TypeInfo, UClass* ComponentClass, UClass* ActorClass, int MapIndex, const FActorSpecificSubobjectSchemaData* ExistingSchemaData) { - const FSchemaData* const SchemaData = ClassPathToSchemaData.Find(ActorClass->GetPathName()); - const FSubobjectSchemaData* const SubobjectSchemaData = SchemaData ? SchemaData->SubobjectData.Find(MapIndex) : nullptr; - FUnrealFlatRepData RepData = GetFlatRepData(TypeInfo); - FSubobjectSchemaData SubobjectData; + FActorSpecificSubobjectSchemaData SubobjectData; SubobjectData.ClassPath = ComponentClass->GetPathName(); for (EReplicatedPropertyGroup Group : GetAllReplicatedPropertyGroups()) { - if (RepData[Group].Num() == 0) + // Since it is possible to replicate subobjects which have no replicated properties. + // We need to generate a schema component for every subobject. So if we have no replicated + // properties, we only don't generate a schema component if we are REP_SingleClient + if (RepData[Group].Num() == 0 && Group == REP_SingleClient) { continue; } Worker_ComponentId ComponentId = 0; - if (SubobjectSchemaData != nullptr && SubobjectSchemaData->SchemaComponents[PropertyGroupToSchemaComponentType(Group)] != 0) + if (ExistingSchemaData != nullptr && ExistingSchemaData->SchemaComponents[PropertyGroupToSchemaComponentType(Group)] != 0) { - ComponentId = SubobjectSchemaData->SchemaComponents[PropertyGroupToSchemaComponentType(Group)]; + ComponentId = ExistingSchemaData->SchemaComponents[PropertyGroupToSchemaComponentType(Group)]; } else { @@ -438,7 +525,7 @@ FSubobjectSchemaData GenerateSubobjectSpecificSchema(FCodeWriter& Writer, FCompo Writer.Printf("component {0} {", *ComponentName); Writer.Indent(); Writer.Printf("id = {0};", ComponentId); - Writer.Printf("data {0};", *SchemaReplicatedDataName(Group, ComponentClass)); + Writer.Printf("data unreal.generated.{0};", *SchemaReplicatedDataName(Group, ComponentClass)); Writer.Outdent().Print("}"); SubobjectData.SchemaComponents[PropertyGroupToSchemaComponentType(Group)] = ComponentId; @@ -448,9 +535,9 @@ FSubobjectSchemaData GenerateSubobjectSpecificSchema(FCodeWriter& Writer, FCompo if (HandoverData.Num() > 0) { Worker_ComponentId ComponentId = 0; - if (SubobjectSchemaData != nullptr && SubobjectSchemaData->SchemaComponents[ESchemaComponentType::SCHEMA_Handover] != 0) + if (ExistingSchemaData != nullptr && ExistingSchemaData->SchemaComponents[ESchemaComponentType::SCHEMA_Handover] != 0) { - ComponentId = SubobjectSchemaData->SchemaComponents[ESchemaComponentType::SCHEMA_Handover]; + ComponentId = ExistingSchemaData->SchemaComponents[ESchemaComponentType::SCHEMA_Handover]; } else { @@ -463,7 +550,7 @@ FSubobjectSchemaData GenerateSubobjectSpecificSchema(FCodeWriter& Writer, FCompo Writer.Printf("component {0} {", *(PropertyName + TEXT("Handover"))); Writer.Indent(); Writer.Printf("id = {0};", ComponentId); - Writer.Printf("data {0};", *SchemaHandoverDataName(ComponentClass)); + Writer.Printf("data unreal.generated.{0};", *SchemaHandoverDataName(ComponentClass)); Writer.Outdent().Print("}"); SubobjectData.SchemaComponents[ESchemaComponentType::SCHEMA_Handover] = ComponentId; @@ -472,7 +559,7 @@ FSubobjectSchemaData GenerateSubobjectSpecificSchema(FCodeWriter& Writer, FCompo return SubobjectData; } -void GenerateSubobjectSchemaForActor(FComponentIdGenerator& IdGenerator, UClass* ActorClass, TSharedPtr TypeInfo, FString SchemaPath, FSchemaData& ActorSchemaData) +void GenerateSubobjectSchemaForActor(FComponentIdGenerator& IdGenerator, UClass* ActorClass, TSharedPtr TypeInfo, FString SchemaPath, FActorSchemaData& ActorSchemaData, const FActorSchemaData* ExistingSchemaData) { FCodeWriter Writer; @@ -484,7 +571,7 @@ void GenerateSubobjectSchemaForActor(FComponentIdGenerator& IdGenerator, UClass* Writer.PrintNewLine(); - GenerateActorIncludes(Writer, TypeInfo); + GenerateSubobjectSchemaForActorIncludes(Writer, TypeInfo); FSubobjectMap Subobjects = GetAllSubobjects(TypeInfo); @@ -492,25 +579,38 @@ void GenerateSubobjectSchemaForActor(FComponentIdGenerator& IdGenerator, UClass* for (auto& It : Subobjects) { - uint32 Offset = It.Key; TSharedPtr& SubobjectTypeInfo = It.Value; UClass* SubobjectClass = Cast(SubobjectTypeInfo->Type); - FSubobjectSchemaData SubobjectData; + FActorSpecificSubobjectSchemaData SubobjectData; - if (IsReplicatedSubobject(SubobjectTypeInfo) && SchemaGeneratedClasses.Contains(SubobjectClass)) + if (SchemaGeneratedClasses.Contains(SubobjectClass)) { bHasComponents = true; - SubobjectData = GenerateSubobjectSpecificSchema(Writer, IdGenerator, UnrealNameToSchemaComponentName(SubobjectTypeInfo->Name.ToString()), SubobjectTypeInfo, SubobjectClass, ActorClass, Offset); + + const FActorSpecificSubobjectSchemaData* ExistingSubobjectSchemaData = nullptr; + if (ExistingSchemaData != nullptr) + { + for (auto& SubobjectIt : ExistingSchemaData->SubobjectData) + { + if (SubobjectIt.Value.Name == SubobjectTypeInfo->Name) + { + ExistingSubobjectSchemaData = &SubobjectIt.Value; + break; + } + } + } + SubobjectData = GenerateSchemaForStaticallyAttachedSubobject(Writer, IdGenerator, UnrealNameToSchemaComponentName(SubobjectTypeInfo->Name.ToString()), SubobjectTypeInfo, SubobjectClass, ActorClass, 0, ExistingSubobjectSchemaData); } else { - SubobjectData.ClassPath = SubobjectClass->GetPathName(); + continue; } SubobjectData.Name = SubobjectTypeInfo->Name; - ActorSchemaData.SubobjectData.Add(Offset, SubobjectData); - ClassPathToSchemaData.Add(SubobjectClass->GetPathName(), FSchemaData()); + uint32 SubobjectOffset = SubobjectData.SchemaComponents[SCHEMA_Data]; + check(SubobjectOffset != 0); + ActorSchemaData.SubobjectData.Add(SubobjectOffset, SubobjectData); } if (bHasComponents) @@ -519,12 +619,10 @@ void GenerateSubobjectSchemaForActor(FComponentIdGenerator& IdGenerator, UClass* } } -void GenerateActorIncludes(FCodeWriter& Writer, TSharedPtr& TypeInfo) +void GenerateSubobjectSchemaForActorIncludes(FCodeWriter& Writer, TSharedPtr& TypeInfo) { TSet AlreadyImported; - bool bImportCoreTypes = false; - for (auto& PropertyPair : TypeInfo->Properties) { UProperty* Property = PropertyPair.Key; @@ -536,11 +634,8 @@ void GenerateActorIncludes(FCodeWriter& Writer, TSharedPtr& TypeInf { UObject* Value = PropertyTypeInfo->Object; - if (Value != nullptr && !Value->IsEditorOnly() && IsReplicatedSubobject(PropertyTypeInfo)) + if (Value != nullptr && !Value->IsEditorOnly()) { - // Only include core types if a subobject has any RPCs - bImportCoreTypes |= PropertyTypeInfo->RPCs.Num() > 0; - UClass* Class = Value->GetClass(); if (!AlreadyImported.Contains(Class) && SchemaGeneratedClasses.Contains(Class)) { @@ -550,9 +645,4 @@ void GenerateActorIncludes(FCodeWriter& Writer, TSharedPtr& TypeInf } } } - - if (bImportCoreTypes) - { - Writer.Printf("import \"unreal/gdk/core_types.schema\";"); - } } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.h b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.h index 156b0728fc..f260f2dbad 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SchemaGenerator.h @@ -11,12 +11,19 @@ class FCodeWriter; struct FComponentIdGenerator; extern TArray SchemaGeneratedClasses; -extern TMap ClassPathToSchemaData; +extern TMap ActorClassPathToSchema; +extern TMap SubobjectClassPathToSchema; extern TMap LevelPathToComponentId; -// Generates a schema file, given an output code writer, component ID, Unreal type and type info. +// Generates schema for an Actor void GenerateActorSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSharedPtr TypeInfo, FString SchemaPath); -void GenerateSubobjectSchema(UClass* Class, TSharedPtr TypeInfo, FString SchemaPath); -void GenerateSubobjectSchemaForActor(FComponentIdGenerator& IdGenerator, UClass* ActorClass, TSharedPtr TypeInfo, FString SchemaPath, FSchemaData& ActorSchemaData); -FSubobjectSchemaData GenerateSubobjectSpecificSchema(FCodeWriter& Writer, FComponentIdGenerator& IdGenerator, FString PropertyName, TSharedPtr& TypeInfo, UClass* ComponentClass); -void GenerateActorIncludes(FCodeWriter& Writer, TSharedPtr& TypeInfo); +// Generates schema for a Subobject class - the schema type and the dynamic schema components +void GenerateSubobjectSchema(FComponentIdGenerator& IdGenerator, UClass* Class, TSharedPtr TypeInfo, FString SchemaPath); +// Generates schema for all statically attached subobjects on an Actor. +void GenerateSubobjectSchemaForActor(FComponentIdGenerator& IdGenerator, UClass* ActorClass, TSharedPtr TypeInfo, + FString SchemaPath, FActorSchemaData& ActorSchemaData, const FActorSchemaData* ExistingSchemaData); +// Generates schema for a statically attached subobject on an Actor - called by GenerateSubobjectSchemaForActor. +FActorSpecificSubobjectSchemaData GenerateSchemaForStaticallyAttachedSubobject(FCodeWriter& Writer, FComponentIdGenerator& IdGenerator, + FString PropertyName, TSharedPtr& TypeInfo, UClass* ComponentClass, const FActorSpecificSubobjectSchemaData* ExistingSchemaData); +// Output the includes required by this schema file. +void GenerateSubobjectSchemaForActorIncludes(FCodeWriter& Writer, TSharedPtr& TypeInfo); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp index b6c06afcaf..267822d2e7 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/SpatialGDKEditorSchemaGenerator.cpp @@ -21,24 +21,27 @@ #include "Templates/SharedPointer.h" #include "UObject/UObjectIterator.h" -#include "TypeStructure.h" +#include "Engine/WorldComposition.h" +#include "Interop/SpatialClassInfoManager.h" +#include "Misc/ScopedSlowTask.h" #include "SchemaGenerator.h" +#include "Settings/ProjectPackagingSettings.h" #include "SpatialConstants.h" #include "SpatialGDKEditorSettings.h" +#include "SpatialGDKServicesModule.h" +#include "TypeStructure.h" +#include "UObject/StrongObjectPtr.h" #include "Utils/CodeWriter.h" #include "Utils/ComponentIdGenerator.h" #include "Utils/DataTypeUtilities.h" #include "Utils/SchemaDatabase.h" -#include "Engine/WorldComposition.h" -#include "Misc/ScopedSlowTask.h" -#include "UObject/StrongObjectPtr.h" -#include "Settings/ProjectPackagingSettings.h" DEFINE_LOG_CATEGORY(LogSpatialGDKSchemaGenerator); #define LOCTEXT_NAMESPACE "SpatialGDKSchemaGenerator" TArray SchemaGeneratedClasses; -TMap ClassPathToSchemaData; +TMap ActorClassPathToSchema; +TMap SubobjectClassPathToSchema; uint32 NextAvailableComponentId; // LevelStreaming @@ -74,7 +77,7 @@ void GenerateCompleteSchemaFromClass(FString SchemaPath, FComponentIdGenerator& } else { - GenerateSubobjectSchema(Class, TypeInfo, SchemaPath + TEXT("Subobjects/")); + GenerateSubobjectSchema(IdGenerator, Class, TypeInfo, SchemaPath + TEXT("Subobjects/")); } } @@ -262,7 +265,6 @@ bool ValidateIdentifierNames(TArray>& TypeInfos) }// :: - void GenerateSchemaFromClasses(const TArray>& TypeInfos, const FString& CombinedSchemaPath, FComponentIdGenerator& IdGenerator) { // Generate the actual schema. @@ -348,7 +350,7 @@ void GenerateSchemaForSublevels(const FString& SchemaPath, FComponentIdGenerator } } - Writer.WriteToFile(FString::Printf(TEXT("%slevel_streaming.schema"), *SchemaPath)); + Writer.WriteToFile(FString::Printf(TEXT("%sSublevels/sublevels.schema"), *SchemaPath)); } FString GenerateIntermediateDirectory() @@ -360,6 +362,42 @@ FString GenerateIntermediateDirectory() return AbsoluteCombinedIntermediatePath; } +TMap CreateComponentIdToClassPathMap() +{ + TMap ComponentIdToClassPath; + + for (const auto& ActorSchemaData : ActorClassPathToSchema) + { + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) + { + ComponentIdToClassPath.Add(ActorSchemaData.Value.SchemaComponents[Type], ActorSchemaData.Key); + }); + + for (const auto& SubobjectSchemaData : ActorSchemaData.Value.SubobjectData) + { + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) + { + ComponentIdToClassPath.Add(SubobjectSchemaData.Value.SchemaComponents[Type], SubobjectSchemaData.Value.ClassPath); + }); + } + } + + for (const auto& SubobjectSchemaData : SubobjectClassPathToSchema) + { + for (const auto& DynamicSubobjectData : SubobjectSchemaData.Value.DynamicSubobjectComponents) + { + ForAllSchemaComponentTypes([&](ESchemaComponentType Type) + { + ComponentIdToClassPath.Add(DynamicSubobjectData.SchemaComponents[Type], SubobjectSchemaData.Key); + }); + } + } + + ComponentIdToClassPath.Remove(SpatialConstants::INVALID_COMPONENT_ID); + + return ComponentIdToClassPath; +} + void SaveSchemaDatabase() { FString PackagePath = TEXT("/Game/Spatial/SchemaDatabase"); @@ -367,8 +405,10 @@ void SaveSchemaDatabase() USchemaDatabase* SchemaDatabase = NewObject(Package, USchemaDatabase::StaticClass(), FName("SchemaDatabase"), EObjectFlags::RF_Public | EObjectFlags::RF_Standalone); SchemaDatabase->NextAvailableComponentId = NextAvailableComponentId; - SchemaDatabase->ClassPathToSchema = ClassPathToSchemaData; + SchemaDatabase->ActorClassPathToSchema = ActorClassPathToSchema; + SchemaDatabase->SubobjectClassPathToSchema = SubobjectClassPathToSchema; SchemaDatabase->LevelPathToComponentId = LevelPathToComponentId; + SchemaDatabase->ComponentIdToClassPath = CreateComponentIdToClassPathMap(); SchemaDatabase->LevelComponentIds = LevelComponentIds; FAssetRegistryModule::AssetCreated(SchemaDatabase); @@ -404,47 +444,16 @@ TArray GetAllSupportedClasses() continue; } - UClass* SupportedClass = nullptr; - for (TFieldIterator PropertyIt(*ClassIt); PropertyIt && SupportedClass == nullptr; ++PropertyIt) - { - if (PropertyIt->HasAnyPropertyFlags(CPF_Net | CPF_Handover)) - { - SupportedClass = *ClassIt; - } - } - - for (TFieldIterator FunctionIt(*ClassIt); FunctionIt && SupportedClass == nullptr; ++FunctionIt) - { - if (FunctionIt->HasAnyFunctionFlags(FUNC_NetFuncFlags)) - { - SupportedClass = *ClassIt; - } - } - - // Check for replicated GameplayAbilities and print a warning if we find one. The UnrealGDK does not currently support this. - if (ClassIt->IsChildOf(UGameplayAbility::StaticClass())) - { - UClass* AbilityClass = *ClassIt; - UGameplayAbility* GameplayAbility = Cast(AbilityClass->GetDefaultObject()); + UClass* SupportedClass = *ClassIt; - if (GameplayAbility->GetReplicationPolicy() == EGameplayAbilityReplicationPolicy::ReplicateYes) - { - UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Replicated GameplayAbility found when generating schema. This is not currently supported and will cause undefined behaviour. Please set the 'ReplicationPolicy' to 'NotReplicated'. Ability: %s"), *GameplayAbility->GetName()); - } - } - - // No replicated/handover properties found - if (SupportedClass == nullptr) - { - continue; - } - - // Ensure we don't process skeleton, reinitialized or classes that have since been hot reloaded + // Ensure we don't process transient generated classes for BP if (SupportedClass->GetName().StartsWith(TEXT("SKEL_"), ESearchCase::CaseSensitive) || SupportedClass->GetName().StartsWith(TEXT("REINST_"), ESearchCase::CaseSensitive) || SupportedClass->GetName().StartsWith(TEXT("TRASHCLASS_"), ESearchCase::CaseSensitive) || SupportedClass->GetName().StartsWith(TEXT("HOTRELOADED_"), ESearchCase::CaseSensitive) - || SupportedClass->GetName().StartsWith(TEXT("PROTO_BP_"), ESearchCase::CaseSensitive)) + || SupportedClass->GetName().StartsWith(TEXT("PROTO_BP_"), ESearchCase::CaseSensitive) + || SupportedClass->GetName().StartsWith(TEXT("PLACEHOLDER-CLASS_"), ESearchCase::CaseSensitive) + || SupportedClass->GetName().StartsWith(TEXT("ORPHANED_DATA_ONLY_"), ESearchCase::CaseSensitive)) { continue; } @@ -458,13 +467,52 @@ TArray GetAllSupportedClasses() { continue; } - + Classes.Add(SupportedClass); } return Classes.Array(); } +void CopyWellKnownSchemaFiles() +{ + FString PluginDir = GetDefault()->GetGDKPluginDirectory(); + + FString GDKSchemaDir = FPaths::Combine(PluginDir, TEXT("SpatialGDK/Extras/schema")); + FString GDKSchemaCopyDir = FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("schema/unreal/gdk")); + + FString CoreSDKSchemaDir = FPaths::Combine(PluginDir, TEXT("SpatialGDK/Binaries/ThirdParty/Improbable/Programs/schema")); + FString CoreSDKSchemaCopyDir = FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("build/dependencies/schema/standard_library")); + + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + + if (!PlatformFile.DirectoryExists(*GDKSchemaCopyDir)) + { + if (!PlatformFile.CreateDirectoryTree(*GDKSchemaCopyDir)) + { + UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Could not create gdk schema directory '%s'! Please make sure the parent directory is writeable."), *GDKSchemaCopyDir); + } + } + + if (!PlatformFile.DirectoryExists(*CoreSDKSchemaCopyDir)) + { + if (!PlatformFile.CreateDirectoryTree(*CoreSDKSchemaCopyDir)) + { + UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Could not create standard library schema directory '%s'! Please make sure the parent directory is writeable."), *GDKSchemaCopyDir); + } + } + + if (!PlatformFile.CopyDirectoryTree(*GDKSchemaCopyDir, *GDKSchemaDir, true /*bOverwriteExisting*/)) + { + UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Could not copy gdk schema to '%s'! Please make sure the directory is writeable."), *GDKSchemaCopyDir); + } + + if (!PlatformFile.CopyDirectoryTree(*CoreSDKSchemaCopyDir, *CoreSDKSchemaDir, true /*bOverwriteExisting*/)) + { + UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("Could not copy standard library schema to '%s'! Please make sure the directory is writeable."), *CoreSDKSchemaCopyDir); + } +} + void DeleteGeneratedSchemaFiles() { const FString SchemaOutputPath = GetDefault()->GetGeneratedSchemaOutputFolder(); @@ -481,7 +529,8 @@ void DeleteGeneratedSchemaFiles() void ClearGeneratedSchema() { - ClassPathToSchemaData.Empty(); + ActorClassPathToSchema.Empty(); + SubobjectClassPathToSchema.Empty(); LevelComponentIds.Empty(); LevelPathToComponentId.Empty(); NextAvailableComponentId = SpatialConstants::STARTING_GENERATED_COMPONENT_ID; @@ -514,17 +563,17 @@ bool TryLoadExistingSchemaDatabase() return false; } - ClassPathToSchemaData = SchemaDatabase->ClassPathToSchema; + ActorClassPathToSchema = SchemaDatabase->ActorClassPathToSchema; + SubobjectClassPathToSchema = SchemaDatabase->SubobjectClassPathToSchema; LevelComponentIds = SchemaDatabase->LevelComponentIds; LevelPathToComponentId = SchemaDatabase->LevelPathToComponentId; NextAvailableComponentId = SchemaDatabase->NextAvailableComponentId; // Component Id generation was updated to be non-destructive, if we detect an old schema database, delete it. - if (ClassPathToSchemaData.Num() > 0 && NextAvailableComponentId == SpatialConstants::STARTING_GENERATED_COMPONENT_ID) + if (ActorClassPathToSchema.Num() > 0 && NextAvailableComponentId == SpatialConstants::STARTING_GENERATED_COMPONENT_ID) { UE_LOG(LogSpatialGDKSchemaGenerator, Warning, TEXT("Detected an old schema database, it'll be reset.")); - ClassPathToSchemaData.Empty(); - DeleteGeneratedSchemaFiles(); + ClearGeneratedSchema(); } } else @@ -536,29 +585,84 @@ bool TryLoadExistingSchemaDatabase() return true; } +SPATIALGDKEDITOR_API bool GeneratedSchemaFolderExists() +{ + const FString SchemaOutputPath = GetDefault()->GetGeneratedSchemaOutputFolder(); + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + return PlatformFile.DirectoryExists(*SchemaOutputPath); +} + +void ResolveClassPathToSchemaName(const FString& ClassPath, const FString& SchemaName) +{ + if (SchemaName.IsEmpty()) + { + return; + } + + ClassPathToSchemaName.Add(ClassPath, SchemaName); + SchemaNameToClassPath.Add(SchemaName, ClassPath); + FSoftObjectPath ObjPath = FSoftObjectPath(ClassPath); + FString DesiredSchemaName = UnrealNameToSchemaName(ObjPath.GetAssetName()); + + if (DesiredSchemaName != SchemaName) + { + AddPotentialNameCollision(DesiredSchemaName, ClassPath, SchemaName); + } + AddPotentialNameCollision(SchemaName, ClassPath, SchemaName); +} + void ResetUsedNames() { ClassPathToSchemaName.Empty(); SchemaNameToClassPath.Empty(); PotentialSchemaNameCollisions.Empty(); - for (const TPair& Entry : ClassPathToSchemaData) + for (const TPair& Entry : ActorClassPathToSchema) { - if (Entry.Value.GeneratedSchemaName.IsEmpty()) - { - // Ignore Subobject entries with empty names. - continue; - } - ClassPathToSchemaName.Add(Entry.Key, Entry.Value.GeneratedSchemaName); - SchemaNameToClassPath.Add(Entry.Value.GeneratedSchemaName, Entry.Key); - FSoftObjectPath ObjPath = FSoftObjectPath(Entry.Key); - FString DesiredSchemaName = UnrealNameToSchemaName(ObjPath.GetAssetName()); + ResolveClassPathToSchemaName(Entry.Key, Entry.Value.GeneratedSchemaName); + } - if (DesiredSchemaName != Entry.Value.GeneratedSchemaName) - { - AddPotentialNameCollision(DesiredSchemaName, Entry.Key, Entry.Value.GeneratedSchemaName); - } - AddPotentialNameCollision(Entry.Value.GeneratedSchemaName, Entry.Key, Entry.Value.GeneratedSchemaName); + for (const TPair< FString, FSubobjectSchemaData>& Entry : SubobjectClassPathToSchema) + { + ResolveClassPathToSchemaName(Entry.Key, Entry.Value.GeneratedSchemaName); + } +} + +void RunSchemaCompiler() +{ + FString PluginDir = GetDefault()->GetGDKPluginDirectory(); + + // Get the schema_compiler path and arguments + FString SchemaCompilerExe = FPaths::Combine(PluginDir, TEXT("SpatialGDK/Binaries/ThirdParty/Improbable/Programs/schema_compiler.exe")); + + FString SchemaDir = FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("schema")); + FString CoreSDKSchemaDir = FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("build/dependencies/schema/standard_library")); + FString SchemaDescriptorDir = FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("build/assembly/schema")); + FString SchemaDescriptorOutput = FPaths::Combine(SchemaDescriptorDir, TEXT("schema.descriptor")); + + // The schema_compiler cannot create folders. + if (!FPaths::DirectoryExists(SchemaDescriptorDir)) + { + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + PlatformFile.CreateDirectoryTree(*SchemaDescriptorDir); + } + + FString SchemaCompilerArgs = FString::Printf(TEXT("--schema_path=\"%s\" --schema_path=\"%s\" --descriptor_set_out=\"%s\" --load_all_schema_on_schema_path"), *SchemaDir, *CoreSDKSchemaDir, *SchemaDescriptorOutput); + + UE_LOG(LogSpatialGDKSchemaGenerator, Log, TEXT("Starting '%s' with `%s` arguments."), *SchemaCompilerExe, *SchemaCompilerArgs); + + int32 ExitCode = 1; + FString SchemaCompilerOut; + FString SchemaCompilerErr; + FPlatformProcess::ExecProcess(*SchemaCompilerExe, *SchemaCompilerArgs, &ExitCode, &SchemaCompilerOut, &SchemaCompilerErr); + + if (ExitCode == 0) + { + UE_LOG(LogSpatialGDKSchemaGenerator, Log, TEXT("schema_compiler successfully generated schema descriptor: %s"), *SchemaCompilerOut); + } + else + { + UE_LOG(LogSpatialGDKSchemaGenerator, Error, TEXT("schema_compiler failed to generate schema descriptor: %s"), *SchemaCompilerErr); } } @@ -603,6 +707,7 @@ bool SpatialGDKGenerateSchema() GenerateSchemaForSublevels(SchemaOutputPath, IdGenerator); NextAvailableComponentId = IdGenerator.Peek(); SaveSchemaDatabase(); + RunSchemaCompiler(); return true; } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.cpp index 9a49a6411b..0dec3be453 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SchemaGenerator/TypeStructure.cpp @@ -418,7 +418,7 @@ TSharedPtr CreateUnrealTypeInfo(UStruct* Type, uint32 ParentChecksu } // Jump over invalid replicated property types - if (Cmd.Property->IsA() || Cmd.Property->IsA()) + if (Cmd.Property->IsA() || Cmd.Property->IsA() || Cmd.Property->IsA()) { continue; } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp index afe37aa5fa..29a2085be6 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SnapshotGenerator/SpatialGDKEditorSnapshotGenerator.cpp @@ -3,16 +3,17 @@ #include "SpatialGDKEditorSnapshotGenerator.h" #include "Engine/LevelScriptActor.h" -#include "Schema/Interest.h" -#include "Schema/StandardLibrary.h" -#include "Schema/SpawnData.h" -#include "Schema/UnrealMetadata.h" #include "EngineClasses/SpatialActorChannel.h" #include "EngineClasses/SpatialNetConnection.h" #include "EngineClasses/SpatialNetDriver.h" #include "Interop/SpatialClassInfoManager.h" +#include "Schema/Interest.h" +#include "Schema/SpawnData.h" +#include "Schema/StandardLibrary.h" +#include "Schema/UnrealMetadata.h" #include "SpatialConstants.h" #include "SpatialGDKEditorSettings.h" +#include "SpatialGDKSettings.h" #include "Utils/ComponentFactory.h" #include "Utils/RepDataUtils.h" #include "Utils/RepLayoutUtils.h" @@ -134,292 +135,22 @@ bool CreateGlobalStateManager(Worker_SnapshotOutputStream* OutputStream) Components.Add(CreateDeploymentData()); Components.Add(CreateGSMShutdownData()); Components.Add(CreateStartupActorManagerData()); - Components.Add(EntityAcl(SpatialConstants::UnrealServerPermission, ComponentWriteAcl).CreateEntityAclData()); - - GSM.component_count = Components.Num(); - GSM.components = Components.GetData(); - - return Worker_SnapshotOutputStream_WriteEntity(OutputStream, &GSM) != 0; -} - -bool CreatePlaceholders(Worker_SnapshotOutputStream* OutputStream) -{ - // Set up grid of "placeholder" entities to allow workers to be authoritative over _something_. - int PlaceholderCount = SpatialConstants::PLACEHOLDER_ENTITY_ID_LAST - SpatialConstants::PLACEHOLDER_ENTITY_ID_FIRST + 1; - int PlaceholderCountAxis = static_cast(sqrt(PlaceholderCount)); - checkf(PlaceholderCountAxis * PlaceholderCountAxis == PlaceholderCount, TEXT("The number of placeholders must be a square number.")); - checkf(PlaceholderCountAxis % 2 == 0, TEXT("The number of placeholders on each axis must be even.")); - const float CHUNK_SIZE = 5.0f; // in SpatialOS coordinates. - int PlaceholderEntityIdCounter = SpatialConstants::PLACEHOLDER_ENTITY_ID_FIRST; - for (int x = -PlaceholderCountAxis / 2; x < PlaceholderCountAxis / 2; x++) - { - for (int y = -PlaceholderCountAxis / 2; y < PlaceholderCountAxis / 2; y++) - { - const Coordinates PlaceholderPosition{ x * CHUNK_SIZE + CHUNK_SIZE * 0.5f, 0, y * CHUNK_SIZE + CHUNK_SIZE * 0.5f }; - - Worker_Entity Placeholder; - Placeholder.entity_id = PlaceholderEntityIdCounter; - - TArray Components; - - WriteAclMap ComponentWriteAcl; - ComponentWriteAcl.Add(SpatialConstants::POSITION_COMPONENT_ID, SpatialConstants::UnrealServerPermission); - ComponentWriteAcl.Add(SpatialConstants::METADATA_COMPONENT_ID, SpatialConstants::UnrealServerPermission); - ComponentWriteAcl.Add(SpatialConstants::PERSISTENCE_COMPONENT_ID, SpatialConstants::UnrealServerPermission); - ComponentWriteAcl.Add(SpatialConstants::ENTITY_ACL_COMPONENT_ID, SpatialConstants::UnrealServerPermission); - - Components.Add(Position(PlaceholderPosition).CreatePositionData()); - Components.Add(Metadata(TEXT("Placeholder")).CreateMetadataData()); - Components.Add(Persistence().CreatePersistenceData()); - Components.Add(EntityAcl(SpatialConstants::UnrealServerPermission, ComponentWriteAcl).CreateEntityAclData()); - - Placeholder.component_count = Components.Num(); - Placeholder.components = Components.GetData(); - - if (Worker_SnapshotOutputStream_WriteEntity(OutputStream, &Placeholder) == 0) - { - return false; - } - - PlaceholderEntityIdCounter++; - } - } - // Sanity check. - check(PlaceholderEntityIdCounter == SpatialConstants::PLACEHOLDER_ENTITY_ID_LAST + 1); - - return true; -} - -// This function is not in use. -// Set up classes needed for Startup Actor creation -void SetupStartupActorCreation(USpatialNetDriver*& NetDriver, USpatialNetConnection*& NetConnection, USpatialPackageMapClient*& PackageMap, USpatialClassInfoManager*& ClassInfoManager, UWorld* World) -{ - NetDriver = NewObject(); - NetDriver->ChannelClasses[CHTYPE_Actor] = USpatialActorChannel::StaticClass(); - NetDriver->GuidCache = MakeShareable(new FSpatialNetGUIDCache(NetDriver)); - NetDriver->World = World; - ClassInfoManager = NewObject(); - ClassInfoManager->Init(NetDriver); + const USpatialGDKSettings* SpatialGDKSettings = GetDefault(); - NetDriver->ClassInfoManager = ClassInfoManager; - - NetConnection = NewObject(); - NetConnection->Driver = NetDriver; - NetConnection->State = USOCK_Closed; - - PackageMap = NewObject(); - PackageMap->Initialize(NetConnection, NetDriver->GuidCache); - - NetConnection->PackageMap = PackageMap; - NetDriver->PackageMap = PackageMap; -} - -// This function is not in use. -void CleanupNetDriverAndConnection(USpatialNetDriver* NetDriver, USpatialNetConnection* NetConnection) -{ - // On clean up of the NetDriver due to garbage collection, either the ServerConnection or ClientConnections need to be not nullptr. - // However if the ServerConnection is set on creation, using the FObjectReplicator to create the initial state of the actor, - // the editor will crash. Therefore we set the ServerConnection after we are done using the NetDriver. - NetDriver->ServerConnection = NetConnection; -} - -// This function is not in use. -TArray CreateStartupActorData(USpatialActorChannel* Channel, AActor* Actor, USpatialClassInfoManager* ClassInfoManager, USpatialNetDriver* NetDriver) -{ - const FClassInfo& Info = ClassInfoManager->GetOrCreateClassInfoByClass(Actor->GetClass()); - - // This ensures that the Actor has prepared it's replicated fields before replicating. For instance, the simulate physics on a UPrimitiveComponent - // will be queried and set the Actor's ReplicatedMovement.bRepPhysics field. These fields are then serialized correctly within the snapshot. We are - // modifying the editor's instance of the actor here, but in theory those fields should be inferred or transient anyway, so it shouldn't be a problem. - Actor->CallPreReplication(NetDriver); - - FRepChangeState InitialRepChanges = Channel->CreateInitialRepChangeState(Actor); - FHandoverChangeState InitialHandoverChanges = Channel->CreateInitialHandoverChangeState(Info); - - // Created just to satisfy the ComponentFactory constructor - FUnresolvedObjectsMap UnresolvedObjectsMap; - FUnresolvedObjectsMap HandoverUnresolvedObjectsMap; - ComponentFactory DataFactory(UnresolvedObjectsMap, HandoverUnresolvedObjectsMap, false, NetDriver); - - // Create component data from initial state of Actor (which is the state the Actor is in before running the level) - TArray ComponentData = DataFactory.CreateComponentDatas(Actor, Info, InitialRepChanges, InitialHandoverChanges); - - // Add Actor RPCs to entity - ComponentData.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::CLIENT_RPC_ENDPOINT_COMPONENT_ID)); - ComponentData.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::SERVER_RPC_ENDPOINT_COMPONENT_ID)); - ComponentData.Add(ComponentFactory::CreateEmptyComponentData(SpatialConstants::NETMULTICAST_RPCS_COMPONENT_ID)); - - // Visit each supported subobject and create component data for initial state of each subobject - for (auto& SubobjectInfoPair : Info.SubobjectInfo) + WorkerRequirementSet ReadACL; + for (const FName& WorkerType : SpatialGDKSettings->ServerWorkerTypes) { - uint32 Offset = SubobjectInfoPair.Key; - FClassInfo& SubobjectInfo = SubobjectInfoPair.Value.Get(); - - TWeakObjectPtr Subobject = NetDriver->PackageMap->GetObjectFromUnrealObjectRef(FUnrealObjectRef(Channel->GetEntityId(), Offset)); - if (Subobject.IsValid()) - { - FRepChangeState SubobjectRepChanges = Channel->CreateInitialRepChangeState(Subobject); - FHandoverChangeState SubobjectHandoverChanges = Channel->CreateInitialHandoverChangeState(SubobjectInfo); - - // Create component data for initial state of subobject - ComponentData.Append(DataFactory.CreateComponentDatas(Subobject.Get(), SubobjectInfo, SubobjectRepChanges, SubobjectHandoverChanges)); - } + const WorkerAttributeSet WorkerTypeAttributeSet{ { WorkerType.ToString() } }; + ReadACL.Add(WorkerTypeAttributeSet); } - return ComponentData; -} - -// This function is not in use. -bool CreateStartupActor(Worker_SnapshotOutputStream* OutputStream, AActor* Actor, Worker_EntityId EntityId, USpatialNetConnection* NetConnection, USpatialClassInfoManager* ClassInfoManager) -{ - Worker_Entity Entity; - Entity.entity_id = EntityId; - - UClass* ActorClass = Actor->GetClass(); + Components.Add(EntityAcl(ReadACL, ComponentWriteAcl).CreateEntityAclData()); - const FClassInfo& ActorInfo = ClassInfoManager->GetOrCreateClassInfoByClass(ActorClass); - - WriteAclMap ComponentWriteAcl; - - ComponentWriteAcl.Add(SpatialConstants::POSITION_COMPONENT_ID, SpatialConstants::UnrealServerPermission); - ComponentWriteAcl.Add(SpatialConstants::INTEREST_COMPONENT_ID, SpatialConstants::UnrealServerPermission); - ComponentWriteAcl.Add(SpatialConstants::SPAWN_DATA_COMPONENT_ID, SpatialConstants::UnrealServerPermission); - ComponentWriteAcl.Add(SpatialConstants::ENTITY_ACL_COMPONENT_ID, SpatialConstants::UnrealServerPermission); - - ForAllSchemaComponentTypes([&](ESchemaComponentType Type) - { - Worker_ComponentId ComponentId = ActorInfo.SchemaComponents[Type]; - - if (ComponentId == SpatialConstants::INVALID_COMPONENT_ID) - { - return; - } - - if (Type == SCHEMA_ClientReliableRPC) - { - // No write attribute for RPC_Client since a Startup Actor will have no owner on level start - return; - } - - ComponentWriteAcl.Add(ComponentId, SpatialConstants::UnrealServerPermission); - }); - - - for (auto& SubobjectInfoPair : ActorInfo.SubobjectInfo) - { - FClassInfo& SubobjectInfo = SubobjectInfoPair.Value.Get(); - - // Static subobjects aren't guaranteed to exist on actor instances, check they are present before adding write acls - UObject* Subobject = Actor->GetDefaultSubobjectByName(SubobjectInfo.SubobjectName); - if (Subobject == nullptr || Subobject->IsPendingKill()) - { - continue; - } - - ForAllSchemaComponentTypes([&](ESchemaComponentType Type) - { - Worker_ComponentId ComponentId = SubobjectInfo.SchemaComponents[Type]; - if (ComponentId == SpatialConstants::INVALID_COMPONENT_ID) - { - return; - } - - if (Type == SCHEMA_ClientReliableRPC) - { - // No write attribute for RPC_Client since a Startup Actor will have no owner on level start - return; - } - - ComponentWriteAcl.Add(ComponentId, SpatialConstants::UnrealServerPermission); - }); - } - - USpatialActorChannel* Channel = Cast(NetConnection->CreateChannel(CHTYPE_Actor, 1)); - Channel->SetEntityId(EntityId); - - TArray Components; - Components.Add(Position(Coordinates::FromFVector(Channel->GetActorSpatialPosition(Actor))).CreatePositionData()); - Components.Add(Metadata(ActorClass->GetName()).CreateMetadataData()); - Components.Add(EntityAcl(SpatialConstants::ClientOrServerPermission, ComponentWriteAcl).CreateEntityAclData()); - Components.Add(Persistence().CreatePersistenceData()); - Components.Add(SpawnData(Actor).CreateSpawnDataData()); - Components.Add(UnrealMetadata({}, {}, ActorClass->GetPathName(), Actor->bNetLoadOnClient).CreateUnrealMetadataData()); - Components.Add(Interest().CreateInterestData()); - - Components.Append(CreateStartupActorData(Channel, Actor, ClassInfoManager, Cast(NetConnection->Driver))); - - Entity.component_count = Components.Num(); - Entity.components = Components.GetData(); - - return Worker_SnapshotOutputStream_WriteEntity(OutputStream, &Entity) != 0; -} - -// This function is not in use. -bool ProcessSupportedActors(const TSet& Actors, USpatialClassInfoManager* ClassInfoManager, TFunction Process) -{ - Worker_EntityId CurrentEntityId = SpatialConstants::PLACEHOLDER_ENTITY_ID_LAST + 1; - - for (AActor* Actor : Actors) - { - UClass* ActorClass = Actor->GetClass(); - - // If Actor is critical to the level, skip - if (ActorClass->IsChildOf()) - { - continue; - } - - if (Actor->IsEditorOnly() || Actor->IsPendingKill() || !ClassInfoManager->IsSupportedClass(ActorClass->GetPathName()) || !Actor->GetIsReplicated()) - { - continue; - } - - if (!Process(Actor, CurrentEntityId)) - { - return false; - } - - CurrentEntityId++; - } - - return true; -} - -// This function is not in use. -bool CreateStartupActors(Worker_SnapshotOutputStream* OutputStream, UWorld* World) -{ - USpatialNetDriver* NetDriver = nullptr; - USpatialNetConnection* NetConnection = nullptr; - USpatialPackageMapClient* PackageMap = nullptr; - USpatialClassInfoManager* ClassInfoManager = nullptr; - - SetupStartupActorCreation(NetDriver, NetConnection, PackageMap, ClassInfoManager, World); - - // Create set of world actors (World actor iterator returns same actor multiple times in some circumstances) - TSet WorldActors; - for (TActorIterator It(World); It; ++It) - { - WorldActors.Add(*It); - } - - bool bSuccess = true; - - // Need to add all actors in the world to the package map so they have assigned UnrealObjRefs for the ComponentFactory to use - bSuccess &= ProcessSupportedActors(WorldActors, ClassInfoManager, [&PackageMap, &ClassInfoManager](AActor* Actor, Worker_EntityId EntityId) - { - PackageMap->ResolveEntityActor(Actor, EntityId); - return true; - }); - - bSuccess &= ProcessSupportedActors(WorldActors, ClassInfoManager, [&NetConnection, &OutputStream, &ClassInfoManager](AActor* Actor, Worker_EntityId EntityId) - { - return CreateStartupActor(OutputStream, Actor, EntityId, NetConnection, ClassInfoManager); - }); - - CleanupNetDriverAndConnection(NetDriver, NetConnection); + GSM.component_count = Components.Num(); + GSM.components = Components.GetData(); - return bSuccess; + return Worker_SnapshotOutputStream_WriteEntity(OutputStream, &GSM) != 0; } bool ValidateAndCreateSnapshotGenerationPath(FString& SavePath) @@ -443,17 +174,15 @@ bool ValidateAndCreateSnapshotGenerationPath(FString& SavePath) return true; } - -bool RunUserSnapshotGenerationOverrides(Worker_SnapshotOutputStream* OutputStream) +bool RunUserSnapshotGenerationOverrides(Worker_SnapshotOutputStream* OutputStream, Worker_EntityId& NextAvailableEntityID) { - Worker_EntityId NextEntityId = SpatialConstants::PLACEHOLDER_ENTITY_ID_LAST + 1; for (TObjectIterator SnapshotGenerationClass; SnapshotGenerationClass; ++SnapshotGenerationClass) { if (SnapshotGenerationClass->IsChildOf(USnapshotGenerationTemplate::StaticClass()) && *SnapshotGenerationClass != USnapshotGenerationTemplate::StaticClass()) { UE_LOG(LogSpatialGDKSnapshot, Log, TEXT("Found user snapshot generation class: %s"), *SnapshotGenerationClass->GetName()); USnapshotGenerationTemplate *SnapshotGenerationObj = NewObject(GetTransientPackage(), *SnapshotGenerationClass); - if (!SnapshotGenerationObj->WriteToSnapshotOutput(OutputStream, NextEntityId)) + if (!SnapshotGenerationObj->WriteToSnapshotOutput(OutputStream, NextAvailableEntityID)) { UE_LOG(LogSpatialGDKSnapshot, Error, TEXT("Failure returned in user snapshot generation override method from class: %s"), *SnapshotGenerationClass->GetName()); return false; @@ -477,17 +206,8 @@ bool FillSnapshot(Worker_SnapshotOutputStream* OutputStream, UWorld* World) return false; } - const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); - if (SpatialGDKSettings->bGeneratePlaceholderEntitiesInSnapshot) - { - if (!CreatePlaceholders(OutputStream)) - { - UE_LOG(LogSpatialGDKSnapshot, Error, TEXT("Error generating Placeholders in snapshot: %s"), UTF8_TO_TCHAR(Worker_SnapshotOutputStream_GetError(OutputStream))); - return false; - } - } - - if (!RunUserSnapshotGenerationOverrides(OutputStream)) + Worker_EntityId NextAvailableEntityID = SpatialConstants::FIRST_AVAILABLE_ENTITY_ID; + if (!RunUserSnapshotGenerationOverrides(OutputStream, NextAvailableEntityID)) { UE_LOG(LogSpatialGDKSnapshot, Error, TEXT("Error running user defined snapshot generation overrides in snapshot: %s"), UTF8_TO_TCHAR(Worker_SnapshotOutputStream_GetError(OutputStream))); return false; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp index 122abc6d72..0fcee3970e 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditor.cpp @@ -3,6 +3,7 @@ #include "SpatialGDKEditor.h" #include "Async/Async.h" +#include "SpatialGDKEditorCloudLauncher.h" #include "SpatialGDKEditorSchemaGenerator.h" #include "SpatialGDKEditorSnapshotGenerator.h" @@ -10,8 +11,10 @@ #include "FileHelpers.h" #include "AssetRegistryModule.h" +#include "AssetDataTagMap.h" #include "GeneralProjectSettings.h" #include "Misc/ScopedSlowTask.h" +#include "SpatialGDKEditorSettings.h" #include "UObject/StrongObjectPtr.h" #include "Settings/ProjectPackagingSettings.h" @@ -83,15 +86,15 @@ bool FSpatialGDKEditor::GenerateSchema(bool bFullScan) UEditorEngine::ResolveDirtyBlueprints(bPromptForCompilation, ErroredBlueprints); } - Progress.EnterProgressFrame(bFullScan ? 10.f : 100.f); - bool bResult = SpatialGDKGenerateSchema(); - if (bFullScan) { - Progress.EnterProgressFrame(10.f); - LoadedAssets.Empty(); - CollectGarbage(RF_NoFlags, true); + // 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. + CopyWellKnownSchemaFiles(); + DeleteGeneratedSchemaFiles(); } + + Progress.EnterProgressFrame(bFullScan ? 10.f : 100.f); + bool bResult = 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) @@ -103,98 +106,28 @@ bool FSpatialGDKEditor::GenerateSchema(bool bFullScan) } } + if (bFullScan) + { + Progress.EnterProgressFrame(10.f); + LoadedAssets.Empty(); + CollectGarbage(RF_NoFlags, true); + } + GetMutableDefault()->bSpatialNetworking = bCachedSpatialNetworking; 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; } -//bool FSpatialGDKEditor::SaveAllAssets() -//{ - // Set up the save package dialog - //FPackagesDialogModule& PackagesDialogModule = FModuleManager::LoadModuleChecked(TEXT("PackagesDialog")); - //PackagesDialogModule.CreatePackagesDialog(NSLOCTEXT("PackagesDialogModule", "PackagesDialogTitle", "Save Content"), NSLOCTEXT("PackagesDialogModule", "PackagesDialogMessage", "Select content to save.")); - //PackagesDialogModule.AddButton(DRT_Save, NSLOCTEXT("PackagesDialogModule", "SaveSelectedButton", "Save Selected"), NSLOCTEXT("PackagesDialogModule", "SaveSelectedButtonTip", "Attempt to save the selected content")); - //PackagesDialogModule.AddButton(DRT_DontSave, NSLOCTEXT("PackagesDialogModule", "DontSaveSelectedButton", "Don't Save"), NSLOCTEXT("PackagesDialogModule", "DontSaveSelectedButtonTip", "Do not save any content")); - //PackagesDialogModule.AddButton(DRT_Cancel, NSLOCTEXT("PackagesDialogModule", "CancelButton", "Cancel"), NSLOCTEXT("PackagesDialogModule", "CancelButtonTip", "Do not save any content and cancel the current operation")); - - //TArray AddPackageItemsChecked; - //TArray AddPackageItemsUnchecked; - //for (TArray::TConstIterator PkgIter(InPackages); PkgIter; ++PkgIter) - //{ - // UPackage* CurPackage = *PkgIter; - // check(CurPackage); - - // // If the caller set bCheckDirty to true, only consider dirty packages - // if (!bCheckDirty || (bCheckDirty && CurPackage->IsDirty())) - // { - // // Never save the transient package - // if (CurPackage != GetTransientPackage()) - // { - // // Never save compiled in packages - // if (CurPackage->HasAnyPackageFlags(PKG_CompiledIn) == false) - // { - // if (UncheckedPackages.Contains(MakeWeakObjectPtr(CurPackage))) - // { - // AddPackageItemsUnchecked.Add(CurPackage); - // } - // else - // { - // AddPackageItemsChecked.Add(CurPackage); - // } - // } - // else - // { - // UE_LOG(LogFileHelpers, Warning, TEXT("PromptForCheckoutAndSave attempted to open the save dialog with a compiled in package: %s"), *CurPackage->GetName()); - // } - // } - // else - // { - // UE_LOG(LogFileHelpers, Warning, TEXT("PromptForCheckoutAndSave attempted to open the save dialog with the transient package")); - // } - // } - //} - - //if (AddPackageItemsUnchecked.Num() > 0 || AddPackageItemsChecked.Num() > 0) - //{ - // for (auto Iter = AddPackageItemsChecked.CreateIterator(); Iter; ++Iter) - // { - // PackagesDialogModule.AddPackageItem(*Iter, (*Iter)->GetName(), ECheckBoxState::Checked); - // } - // for (auto Iter = AddPackageItemsUnchecked.CreateIterator(); Iter; ++Iter) - // { - // PackagesDialogModule.AddPackageItem(*Iter, (*Iter)->GetName(), ECheckBoxState::Unchecked); - // } - - // // If valid packages were added to the dialog, display it to the user - // const EDialogReturnType UserResponse = PackagesDialogModule.ShowPackagesDialog(PackagesNotSavedDuringSaveAll); - - // // If the user has responded yes, they want to save the packages they have checked - // if (UserResponse == DRT_Save) - // { - // PackagesDialogModule.GetResults(FilteredPackages, ECheckBoxState::Checked); - - // TArray UncheckedPackagesRaw; - // PackagesDialogModule.GetResults(UncheckedPackagesRaw, ECheckBoxState::Unchecked); - // UncheckedPackages.Empty(); - // for (UPackage* Package : UncheckedPackagesRaw) - // { - // UncheckedPackages.Add(MakeWeakObjectPtr(Package)); - // } - // } - // // If the user has responded they don't wish to save, set the response type accordingly - // else if (UserResponse == DRT_DontSave) - // { - // ReturnResponse = PR_Declined; - // } - // // If the user has cancelled from the dialog, set the response type accordingly - // else - // { - // ReturnResponse = PR_Cancelled; - // } - //} -//} - bool FSpatialGDKEditor::LoadPotentialAssets(TArray>& OutAssets) { FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); @@ -242,8 +175,21 @@ bool FSpatialGDKEditor::LoadPotentialAssets(TArray>& O return false; } Progress.EnterProgressFrame(1, FText::FromString(FString::Printf(TEXT("Loading %s"), *Data.AssetName.ToString()))); - if (auto GeneratedClassPathPtr = Data.TagsAndValues.Find("GeneratedClass")) + + const FString* GeneratedClassPathPtr = nullptr; + +#if ENGINE_MINOR_VERSION <= 20 + GeneratedClassPathPtr = Data.TagsAndValues.Find("GeneratedClass"); +#else + FAssetDataTagMapSharedView::FFindTagResult GeneratedClassFindTagResult = Data.TagsAndValues.FindTag("GeneratedClass"); + if (GeneratedClassFindTagResult.IsSet()) { + GeneratedClassPathPtr = &GeneratedClassFindTagResult.GetValue(); + } +#endif + + if (GeneratedClassPathPtr != nullptr) + { const FString ClassObjectPath = FPackageName::ExportTextPathToObjectPath(*GeneratedClassPathPtr); const FString ClassName = FPackageName::ObjectPathToObjectName(ClassObjectPath); FSoftObjectPath SoftPath = FSoftObjectPath(ClassObjectPath); @@ -268,6 +214,43 @@ void FSpatialGDKEditor::GenerateSnapshot(UWorld* World, FString SnapshotFilename } } +void FSpatialGDKEditor::LaunchCloudDeployment(FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback) +{ + LaunchCloudResult = Async(EAsyncExecution::Thread, SpatialGDKCloudLaunch, + [this, SuccessCallback, FailureCallback] + { + if (!LaunchCloudResult.IsReady() || LaunchCloudResult.Get() != true) + { + FailureCallback.ExecuteIfBound(); + } + else + { + SuccessCallback.ExecuteIfBound(); + } + }); +} + +void FSpatialGDKEditor::StopCloudDeployment(FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback) +{ + StopCloudResult = Async(EAsyncExecution::Thread, SpatialGDKCloudStop, + [this, SuccessCallback, FailureCallback] + { + if (!StopCloudResult.IsReady() || StopCloudResult.Get() != true) + { + FailureCallback.ExecuteIfBound(); + } + else + { + SuccessCallback.ExecuteIfBound(); + } + }); +} + +bool FSpatialGDKEditor::FullScanRequired() +{ + return !GeneratedSchemaFolderExists(); +} + void FSpatialGDKEditor::RemoveEditorAssetLoadedCallback() { if (OnAssetLoadedHandle.IsValid()) diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCloudLauncher.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCloudLauncher.cpp new file mode 100644 index 0000000000..87ab7fc1b6 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorCloudLauncher.cpp @@ -0,0 +1,60 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialGDKEditorCloudLauncher.h" + +#include "Interfaces/IPluginManager.h" +#include "SpatialGDKEditorSettings.h" + +bool SpatialGDKCloudLaunch() +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + + const FString CmdExecutable = TEXT("cmd.exe"); + + FString LauncherCmdArguments = FString::Printf( + TEXT("/c cmd.exe /c DeploymentLauncher.exe create %s %s %s \"%s\" \"%s\" %s"), + *SpatialGDKSettings->GetProjectName(), + *SpatialGDKSettings->GetAssemblyName(), + *SpatialGDKSettings->GetPrimaryDeploymentName(), + *SpatialGDKSettings->GetPrimaryLanchConfigPath(), + *SpatialGDKSettings->GetSnapshotPath(), + *SpatialGDKSettings->GetPrimaryRegionCode().ToString() + ); + + if (SpatialGDKSettings->IsSimulatedPlayersEnabled()) + { + LauncherCmdArguments = FString::Printf( + TEXT("%s %s \"%s\" %s %s"), + *LauncherCmdArguments, + *SpatialGDKSettings->GetSimulatedPlayerDeploymentName(), + *SpatialGDKSettings->GetSimulatedPlayerLaunchConfigPath(), + *SpatialGDKSettings->GetSimulatedPlayerRegionCode().ToString(), + *FString::FromInt(SpatialGDKSettings->GetNumberOfSimulatedPlayer()) + ); + } + + LauncherCmdArguments = FString::Printf( + TEXT("%s ^& pause"), + *LauncherCmdArguments + ); + + FProcHandle DeploymentLauncherProcHandle = FPlatformProcess::CreateProc( + *CmdExecutable, *LauncherCmdArguments, true, false, false, nullptr, 0, + *SpatialGDKSettings->GetDeploymentLauncherPath(), nullptr, nullptr); + + return DeploymentLauncherProcHandle.IsValid(); +} + +bool SpatialGDKCloudStop() +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + + const FString CmdExecutable = TEXT("cmd.exe"); + const FString LauncherCmdArguments = TEXT("/c DeploymentLauncher.exe stop"); + + FProcHandle DeploymentLauncherProcHandle = FPlatformProcess::CreateProc( + *CmdExecutable, *LauncherCmdArguments, true, false, false, nullptr, 0, + *SpatialGDKSettings->GetDeploymentLauncherPath(), nullptr, nullptr); + + return DeploymentLauncherProcHandle.IsValid(); +} diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorModule.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorModule.cpp index e649b7a0c6..5c17008101 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorModule.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorModule.cpp @@ -8,6 +8,8 @@ #include "ISettingsModule.h" #include "ISettingsContainer.h" #include "ISettingsSection.h" +#include "PropertyEditor/Public/PropertyEditorModule.h" +#include "WorkerTypeCustomization.h" #define LOCTEXT_NAMESPACE "FSpatialGDKEditorModule" @@ -53,6 +55,9 @@ void FSpatialGDKEditorModule::RegisterSettings() RuntimeSettingsSection->OnModified().BindRaw(this, &FSpatialGDKEditorModule::HandleRuntimeSettingsSaved); } } + + FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); + PropertyModule.RegisterCustomPropertyTypeLayout("WorkerType", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FWorkerTypeCustomization::MakeInstance)); } void FSpatialGDKEditorModule::UnregisterSettings() @@ -62,6 +67,8 @@ void FSpatialGDKEditorModule::UnregisterSettings() SettingsModule->UnregisterSettings("Project", "SpatialGDKEditor", "Editor Settings"); SettingsModule->UnregisterSettings("Project", "SpatialGDKEditor", "Runtime Settings"); } + FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); + PropertyModule.UnregisterCustomPropertyTypeLayout("FWorkerAssociation"); } bool FSpatialGDKEditorModule::HandleEditorSettingsSaved() diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorSettings.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorSettings.cpp index c6c77c7979..dc3b465adc 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorSettings.cpp +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/SpatialGDKEditorSettings.cpp @@ -1,19 +1,36 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved + #include "SpatialGDKEditorSettings.h" + +#include "Dom/JsonObject.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" + USpatialGDKEditorSettings::USpatialGDKEditorSettings(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) + , bShowSpatialServiceButton(false) , bDeleteDynamicEntities(true) , bGenerateDefaultLaunchConfig(true) , bStopSpatialOnExit(false) - , bGeneratePlaceholderEntitiesInSnapshot(true) + , bAutoStartLocalDeployment(true) + , PrimaryDeploymentRegionCode(ERegionCode::US) + , SimulatedPlayerLaunchConfigPath(FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectDir() / + TEXT("Plugins/UnrealGDK/SpatialGDK/Build/Programs/Improbable.Unreal.Scripts/WorkerCoordinator/SpatialConfig/cloud_launch_sim_player_deployment.json")))) + , SimulatedPlayerDeploymentRegionCode(ERegionCode::US) { - SpatialOSDirectory.Path = GetSpatialOSDirectory(); SpatialOSLaunchConfig.FilePath = GetSpatialOSLaunchConfig(); - SpatialOSSnapshotPath.Path = GetSpatialOSSnapshotFolderPath(); SpatialOSSnapshotFile = GetSpatialOSSnapshotFile(); - GeneratedSchemaOutputFolder.Path = GetGeneratedSchemaOutputFolder(); + ProjectName = GetProjectNameFromSpatial(); } void USpatialGDKEditorSettings::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) @@ -31,6 +48,11 @@ void USpatialGDKEditorSettings::PostEditChangeProperty(struct FPropertyChangedEv PlayInSettings->PostEditChange(); PlayInSettings->SaveConfig(); } + else if (Name == GET_MEMBER_NAME_CHECKED(USpatialGDKEditorSettings, LaunchConfigDesc)) + { + SetRuntimeWorkerTypes(); + SetLevelEditorPlaySettingsWorkerTypes(); + } } void USpatialGDKEditorSettings::PostInitProperties() @@ -39,7 +61,172 @@ void USpatialGDKEditorSettings::PostInitProperties() ULevelEditorPlaySettings* PlayInSettings = GetMutableDefault(); PlayInSettings->SetDeleteDynamicEntities(bDeleteDynamicEntities); - PlayInSettings->PostEditChange(); PlayInSettings->SaveConfig(); + + SetRuntimeWorkerTypes(); + SetLevelEditorPlaySettingsWorkerTypes(); +} + +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->SaveConfig(CPF_Config, *RuntimeSettings->GetDefaultConfigFilename()); + } +} + +void USpatialGDKEditorSettings::SetLevelEditorPlaySettingsWorkerTypes() +{ + ULevelEditorPlaySettings* PlayInSettings = GetMutableDefault(); + + PlayInSettings->WorkerTypesToLaunch.Empty(LaunchConfigDesc.ServerWorkers.Num()); + for (const FWorkerTypeLaunchSection& WorkerLaunch : LaunchConfigDesc.ServerWorkers) + { + PlayInSettings->WorkerTypesToLaunch.Add(WorkerLaunch.WorkerTypeName, WorkerLaunch.NumEditorInstances); + } +} + +FString USpatialGDKEditorSettings::GetProjectNameFromSpatial() const +{ + FString FileContents; + const FString SpatialOSFile = FSpatialGDKServicesModule::GetSpatialOSDirectory().Append(TEXT("/spatialos.json")); + + if (!FFileHelper::LoadFileToString(FileContents, *SpatialOSFile)) + { + return TEXT(""); + } + + TSharedRef> Reader = TJsonReaderFactory<>::Create(FileContents); + TSharedPtr JsonObject; + + if (FJsonSerializer::Deserialize(Reader, JsonObject)) + { + return JsonObject->GetStringField("name"); + } + + return FString(); +} + +bool USpatialGDKEditorSettings::IsAssemblyNameValid(const FString& Name) +{ + const FRegexPattern AssemblyPatternRegex(SpatialConstants::AssemblyPattern); + FRegexMatcher RegMatcher(AssemblyPatternRegex, Name); + + return RegMatcher.FindNext(); +} + +bool USpatialGDKEditorSettings::IsProjectNameValid(const FString& Name) +{ + const FRegexPattern ProjectPatternRegex(SpatialConstants::ProjectPattern); + FRegexMatcher RegMatcher(ProjectPatternRegex, Name); + + return RegMatcher.FindNext(); +} + +bool USpatialGDKEditorSettings::IsDeploymentNameValid(const FString& Name) +{ + const FRegexPattern DeploymentPatternRegex(SpatialConstants::DeploymentPattern); + FRegexMatcher RegMatcher(DeploymentPatternRegex, Name); + + return RegMatcher.FindNext(); +} + +bool USpatialGDKEditorSettings::IsRegionCodeValid(const ERegionCode::Type RegionCode) +{ + UEnum* pEnum = FindObject(ANY_PACKAGE, TEXT("ERegionCode"), true); + + return pEnum != nullptr && pEnum->IsValidEnumValue(RegionCode); +} + +void USpatialGDKEditorSettings::SetPrimaryDeploymentName(const FString& Name) +{ + PrimaryDeploymentName = Name; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetAssemblyName(const FString& Name) +{ + AssemblyName = Name; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetProjectName(const FString& Name) +{ + ProjectName = Name; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetPrimaryLaunchConfigPath(const FString& Path) +{ + PrimaryLaunchConfigPath.FilePath = FPaths::ConvertRelativePathToFull(Path); + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetSnapshotPath(const FString& Path) +{ + SnapshotPath.FilePath = FPaths::ConvertRelativePathToFull(Path); + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetPrimaryRegionCode(const ERegionCode::Type RegionCode) +{ + PrimaryDeploymentRegionCode = RegionCode; +} + +void USpatialGDKEditorSettings::SetSimulatedPlayerRegionCode(const ERegionCode::Type RegionCode) +{ + SimulatedPlayerDeploymentRegionCode = RegionCode; +} + +void USpatialGDKEditorSettings::SetSimulatedPlayersEnabledState(bool IsEnabled) +{ + bSimulatedPlayersIsEnabled = IsEnabled; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetSimulatedPlayerDeploymentName(const FString& Name) +{ + SimulatedPlayerDeploymentName = Name; + SaveConfig(); +} + +void USpatialGDKEditorSettings::SetNumberOfSimulatedPlayers(uint32 Number) +{ + NumberOfSimulatedPlayers = Number; + SaveConfig(); +} + +bool USpatialGDKEditorSettings::IsDeploymentConfigurationValid() const +{ + bool result = IsAssemblyNameValid(AssemblyName) && + IsDeploymentNameValid(PrimaryDeploymentName) && + IsProjectNameValid(ProjectName) && + !SnapshotPath.FilePath.IsEmpty() && + !PrimaryLaunchConfigPath.FilePath.IsEmpty() && + IsRegionCodeValid(PrimaryDeploymentRegionCode); + + if (IsSimulatedPlayersEnabled()) + { + result = result && + IsDeploymentNameValid(SimulatedPlayerDeploymentName) && + !SimulatedPlayerLaunchConfigPath.IsEmpty() && + IsRegionCodeValid(SimulatedPlayerDeploymentRegionCode); + } + + return result; } diff --git a/SpatialGDK/Source/SpatialGDKEditor/Private/WorkerTypeCustomization.cpp b/SpatialGDK/Source/SpatialGDKEditor/Private/WorkerTypeCustomization.cpp new file mode 100644 index 0000000000..15c3f3a0c8 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Private/WorkerTypeCustomization.cpp @@ -0,0 +1,78 @@ +#include "WorkerTypeCustomization.h" + +#include "SpatialGDKSettings.h" + +#include "PropertyCustomizationHelpers.h" +#include "PropertyHandle.h" +#include "Widgets/SToolTip.h" + +TSharedRef FWorkerTypeCustomization::MakeInstance() +{ + return MakeShared(); +} + +void FWorkerTypeCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + TSharedPtr WorkerTypeNameProperty = StructPropertyHandle->GetChildHandle("WorkerTypeName"); + + if (WorkerTypeNameProperty->IsValidHandle()) + { + HeaderRow.NameContent() + [ + StructPropertyHandle->CreatePropertyNameWidget() + ] + .ValueContent() + [ + PropertyCustomizationHelpers::MakePropertyComboBox(WorkerTypeNameProperty, + FOnGetPropertyComboBoxStrings::CreateStatic(&FWorkerTypeCustomization::OnGetStrings), + FOnGetPropertyComboBoxValue::CreateStatic(&FWorkerTypeCustomization::OnGetValue, WorkerTypeNameProperty), + FOnPropertyComboBoxValueSelected::CreateStatic(&FWorkerTypeCustomization::OnValueSelected, WorkerTypeNameProperty)) + ]; + } +} + +void FWorkerTypeCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ +} + +void FWorkerTypeCustomization::OnGetStrings(TArray>& OutComboBoxStrings, TArray>& OutToolTips, TArray& OutRestrictedItems) +{ + 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); + } + } +} + +FString FWorkerTypeCustomization::OnGetValue(TSharedPtr WorkerTypeNameHandle) +{ + if (!WorkerTypeNameHandle->IsValidHandle()) + { + return FString(); + } + + FString WorkerTypeValue; + + if (const USpatialGDKSettings* Settings = GetDefault()) + { + WorkerTypeNameHandle->GetValue(WorkerTypeValue); + const FName WorkerTypeName = FName(*WorkerTypeValue); + + return Settings->ServerWorkerTypes.Contains(WorkerTypeName) ? WorkerTypeValue : TEXT("INVALID"); + } + + return WorkerTypeValue; +} + +void FWorkerTypeCustomization::OnValueSelected(const FString& SelectedValue, TSharedPtr WorkerTypeNameHandle) +{ + if (WorkerTypeNameHandle->IsValidHandle()) + { + const FName NewValue = FName(*SelectedValue); + WorkerTypeNameHandle->SetValue(NewValue); + } +} diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditor.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditor.h index 4382b97199..778ce24d32 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditor.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditor.h @@ -19,12 +19,17 @@ class SPATIALGDKEDITOR_API FSpatialGDKEditor bool GenerateSchema(bool bFullScan); void GenerateSnapshot(UWorld* World, FString SnapshotFilename, FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback, FSpatialGDKEditorErrorHandler ErrorCallback); + void LaunchCloudDeployment(FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback); + void StopCloudDeployment(FSimpleDelegate SuccessCallback, FSimpleDelegate FailureCallback); bool IsSchemaGeneratorRunning() { return bSchemaGeneratorRunning; } + bool FullScanRequired(); private: bool bSchemaGeneratorRunning; TFuture SchemaGeneratorResult; + TFuture LaunchCloudResult; + TFuture StopCloudResult; bool LoadPotentialAssets(TArray>& OutAssets); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorCloudLauncher.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorCloudLauncher.h new file mode 100644 index 0000000000..68ac81dcae --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorCloudLauncher.h @@ -0,0 +1,9 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Logging/LogMacros.h" + +SPATIALGDKEDITOR_API bool SpatialGDKCloudLaunch(); + +SPATIALGDKEDITOR_API bool SpatialGDKCloudStop(); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorModule.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorModule.h index aa94d4cd4b..67b3b177c1 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorModule.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorModule.h @@ -19,4 +19,5 @@ class FSpatialGDKEditorModule : public IModuleInterface void UnregisterSettings(); bool HandleEditorSettingsSaved(); bool HandleRuntimeSettingsSaved(); + bool HandleCloudLauncherSettingsSaved(); }; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSchemaGenerator.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSchemaGenerator.h index dc33cca811..0ce11e4d41 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSchemaGenerator.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSchemaGenerator.h @@ -10,4 +10,10 @@ SPATIALGDKEDITOR_API bool SpatialGDKGenerateSchema(); SPATIALGDKEDITOR_API void ClearGeneratedSchema(); +SPATIALGDKEDITOR_API void DeleteGeneratedSchemaFiles(); + +SPATIALGDKEDITOR_API void CopyWellKnownSchemaFiles(); + SPATIALGDKEDITOR_API bool TryLoadExistingSchemaDatabase(); + +SPATIALGDKEDITOR_API bool GeneratedSchemaFolderExists(); diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h index 5064d8a016..35de44d74e 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/SpatialGDKEditorSettings.h @@ -4,8 +4,9 @@ #include "CoreMinimal.h" #include "Engine/EngineTypes.h" #include "Misc/Paths.h" - #include "SpatialConstants.h" +#include "UObject/Package.h" +#include "SpatialGDKServicesModule.h" #include "SpatialGDKEditorSettings.generated.h" @@ -26,27 +27,27 @@ struct FWorldLaunchSection } /** The size of the simulation, in meters, for the auto-generated launch configuration file. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Simulation dimensions in meters")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Simulation dimensions in meters")) FIntPoint Dimensions; /** The size of the grid squares that the world is divided into, in “world units” (an arbitrary unit that worker instances can interpret as they choose). */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Chunk edge length in meters")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Chunk edge length in meters")) int32 ChunkEdgeLengthMeters; /** The time in seconds between streaming query updates. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Streaming query interval in seconds")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Streaming query interval in seconds")) int32 StreamingQueryIntervalSeconds; /** The frequency in seconds to write snapshots of the simulated world. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Snapshot write period in seconds")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Snapshot write period in seconds")) int32 SnapshotWritePeriodSeconds; /** Legacy non-worker flag configurations. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false)) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false)) TMap LegacyFlags; /** Legacy JVM configurations. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Legacy Java parameters")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Legacy Java parameters")) TMap LegacyJavaParams; }; @@ -65,23 +66,23 @@ struct FWorkerPermissionsSection } /** Gives all permissions to a worker instance. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "All")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "All")) bool bAllPermissions; /** Enables a worker instance to create new entities. */ - UPROPERTY(EditAnywhere, config, meta = (EditCondition = "!bAllPermissions", ConfigRestartRequired = false, DisplayName = "Allow entity creation")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "!bAllPermissions", ConfigRestartRequired = false, DisplayName = "Allow entity creation")) bool bAllowEntityCreation; /** Enables a worker instance to delete entities. */ - UPROPERTY(EditAnywhere, config, meta = (EditCondition = "!bAllPermissions", ConfigRestartRequired = false, DisplayName = "Allow entity deletion")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "!bAllPermissions", ConfigRestartRequired = false, DisplayName = "Allow entity deletion")) bool bAllowEntityDeletion; /** Controls which components can be returned from entity queries that the worker instance performs. If an entity query specifies other components to be returned, the query will fail. */ - UPROPERTY(EditAnywhere, config, meta = (EditCondition = "!bAllPermissions", ConfigRestartRequired = false, DisplayName = "Allow entity query")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "!bAllPermissions", ConfigRestartRequired = false, DisplayName = "Allow entity query")) bool bAllowEntityQuery; /** Specifies which components can be returned in the query result. */ - UPROPERTY(EditAnywhere, config, meta = (EditCondition = "!bAllPermissions", ConfigRestartRequired = false, DisplayName = "Component queries")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "!bAllPermissions", ConfigRestartRequired = false, DisplayName = "Component queries")) TArray Components; }; @@ -97,11 +98,11 @@ struct FLoginRateLimitSection } /** The duration for which worker connection requests will be limited. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false)) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false)) FString Duration; /** The connection request limit for the duration. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false, ClampMin = "1", UIMin = "1")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, ClampMin = "1", UIMin = "1")) int32 RequestsPerDuration; }; @@ -118,44 +119,49 @@ struct FWorkerTypeLaunchSection , LoginRateLimit() , Columns(1) , Rows(1) + , NumEditorInstances(1) , bManualWorkerConnectionOnly(true) { } /** The name of the worker type, defined in the filename of its spatialos..worker.json file. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false)) - FString WorkerTypeName; + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false)) + FName WorkerTypeName; /** Defines the worker instance's permissions. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false)) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false)) FWorkerPermissionsSection WorkerPermissions; /** Defines the maximum number of worker instances that can connect. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Max connection capacity limit (0 = unlimited capacity)", ClampMin = "0", UIMin = "0")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Max connection capacity limit (0 = unlimited capacity)", ClampMin = "0", UIMin = "0")) int32 MaxConnectionCapacityLimit; /** Enable connection rate limiting. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Login rate limit enabled")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Login rate limit enabled")) bool bLoginRateLimitEnabled; /** Login rate limiting configuration. */ - UPROPERTY(EditAnywhere, config, meta = (EditCondition = "bLoginRateLimitEnabled", ConfigRestartRequired = false)) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (EditCondition = "bLoginRateLimitEnabled", ConfigRestartRequired = false)) FLoginRateLimitSection LoginRateLimit; /** Number of columns in the rectangle grid load balancing config. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Rectangle grid column count", ClampMin = "1", UIMin = "1")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Rectangle grid column count", ClampMin = "1", UIMin = "1")) int32 Columns; /** Number of rows in the rectangle grid load balancing config. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Rectangle grid row count", ClampMin = "1", UIMin = "1")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Rectangle grid row count", ClampMin = "1", UIMin = "1")) int32 Rows; + /** Number of instances to launch when playing in editor. */ + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Instances to launch in editor", ClampMin = "0", UIMin = "0")) + int32 NumEditorInstances; + /** Flags defined for a worker instance. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Flags")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Flags")) TMap Flags; /** Determines if the worker instance is launched manually or by SpatialOS. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Manual worker connection only")) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false, DisplayName = "Manual worker connection only")) bool bManualWorkerConnectionOnly; }; @@ -165,31 +171,45 @@ struct FSpatialLaunchConfigDescription GENERATED_BODY() FSpatialLaunchConfigDescription() - : Template(TEXT("small")) + : Template(TEXT("w2_r0500_e5")) , World() { FWorkerTypeLaunchSection UnrealWorkerDefaultSetting; - UnrealWorkerDefaultSetting.WorkerTypeName = SpatialConstants::ServerWorkerType; + UnrealWorkerDefaultSetting.WorkerTypeName = SpatialConstants::DefaultServerWorkerType; UnrealWorkerDefaultSetting.Rows = 1; UnrealWorkerDefaultSetting.Columns = 1; UnrealWorkerDefaultSetting.bManualWorkerConnectionOnly = true; - Workers.Add(UnrealWorkerDefaultSetting); + ServerWorkers.Add(UnrealWorkerDefaultSetting); } /** Deployment template. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false)) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false)) FString Template; /** Configuration for the simulated world. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false)) + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false)) FWorldLaunchSection World; /** Worker-specific configuration parameters. */ - UPROPERTY(EditAnywhere, config, meta = (ConfigRestartRequired = false)) - TArray Workers; + UPROPERTY(Category = "SpatialGDK", EditAnywhere, config, meta = (ConfigRestartRequired = false)) + TArray ServerWorkers; }; +/** +* Enumerates available Region Codes +*/ +UENUM() +namespace ERegionCode +{ + enum Type + { + US = 1, + EU, + AP, + }; +} + UCLASS(config = SpatialGDKEditorSettings, defaultconfig) class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject { @@ -202,60 +222,104 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject virtual void PostInitProperties() override; private: - /** Path to the directory containing the SpatialOS-related files. */ - UPROPERTY(EditAnywhere, config, Category = "General", meta = (ConfigRestartRequired = false, DisplayName = "SpatialOS directory")) - FDirectoryPath SpatialOSDirectory; + + /** Set WorkerTypes in runtime settings. */ + void SetRuntimeWorkerTypes(); + + /** Set WorkerTypesToLaunch in level editor play settings. */ + void SetLevelEditorPlaySettingsWorkerTypes(); public: - /** If checked, all dynamically spawned entities will be deleted when server workers disconnect. */ + /** If checked, show the Spatial service button on the GDK toolbar which can be used to turn the Spatial service on and off. */ + UPROPERTY(EditAnywhere, config, Category = "General", meta = (ConfigRestartRequired = false, DisplayName = "Show Spatial service button")) + bool bShowSpatialServiceButton; + + /** Select to delete all a server-worker instance’s dynamically-spawned entities when the server-worker instance shuts down. If NOT selected, a new server-worker instance has all of these entities from the former server-worker instance’s session. */ UPROPERTY(EditAnywhere, config, Category = "Play in editor settings", meta = (ConfigRestartRequired = false, DisplayName = "Delete dynamically spawned entities")) bool bDeleteDynamicEntities; - /** If checked, a launch configuration will be generated by default when launching spatial through the toolbar. */ - UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (ConfigRestartRequired = false, DisplayName = "Generate default launch config")) + /** Select the check box for the GDK to auto-generate a launch configuration file for your game when you launch a deployment session. If NOT selected, you must specify a launch configuration `.json` file. */ + UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (ConfigRestartRequired = false, DisplayName = "Auto-generate launch configuration file")) bool bGenerateDefaultLaunchConfig; private: - /** Launch configuration file used for `spatial local launch`. */ - UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (EditCondition = "!bGenerateDefaultLaunchConfig", ConfigRestartRequired = false, DisplayName = "Launch configuration")) + /** If you are not using auto-generate launch configuration file, specify a launch configuration `.json` file and location here. */ + UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (EditCondition = "!bGenerateDefaultLaunchConfig", ConfigRestartRequired = false, DisplayName = "Launch configuration file path")) FFilePath SpatialOSLaunchConfig; public: - /** Stop `spatial local launch` when shutting down editor. */ - UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (ConfigRestartRequired = false, DisplayName = "Stop on exit")) + /** Select the check box to stop your game’s local deployment when you shut down Unreal Editor. */ + UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (ConfigRestartRequired = false, DisplayName = "Stop local deployment on exit")) bool bStopSpatialOnExit; -private: - /** Path to your SpatialOS snapshot. */ - UPROPERTY(EditAnywhere, config, Category = "Snapshots", meta = (ConfigRestartRequired = false, DisplayName = "Snapshot path")) - FDirectoryPath SpatialOSSnapshotPath; + /** Start a local SpatialOS deployment when clicking 'Play'. */ + UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (ConfigRestartRequired = false, DisplayName = "Auto-start local deployment")) + bool bAutoStartLocalDeployment; +private: /** Name of your SpatialOS snapshot file. */ UPROPERTY(EditAnywhere, config, Category = "Snapshots", meta = (ConfigRestartRequired = false, DisplayName = "Snapshot file name")) FString SpatialOSSnapshotFile; - /** If checked, the GDK creates a launch configuration file by default when you launch a local deployment through the toolbar. */ - UPROPERTY(EditAnywhere, config, Category = "Schema", meta = (ConfigRestartRequired = false, DisplayName = "Output path for the generated schemas")) - FDirectoryPath GeneratedSchemaOutputFolder; - - /** Command line flags passed in to `spatial local launch`.*/ + /** Add flags to the `spatial local launch` command; they alter the deployment’s behavior. Select the trash icon to remove all the flags.*/ UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (ConfigRestartRequired = false, DisplayName = "Command line flags for local launch")) TArray SpatialOSCommandLineLaunchFlags; +private: + UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (ConfigRestartRequired = false, DisplayName = "SpatialOS project")) + FString ProjectName; + + UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (ConfigRestartRequired = false, DisplayName = "Assembly name")) + FString AssemblyName; + + UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (ConfigRestartRequired = false, DisplayName = "Deployment name")) + FString PrimaryDeploymentName; + + UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (ConfigRestartRequired = false, DisplayName = "Cloud launch configuration path")) + FFilePath PrimaryLaunchConfigPath; + + UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (ConfigRestartRequired = false, DisplayName = "Snapshot path")) + FFilePath SnapshotPath; + + UPROPERTY(EditAnywhere, config, Category = "Cloud", meta = (ConfigRestartRequired = false, DisplayName = "Region")) + TEnumAsByte PrimaryDeploymentRegionCode; + + const FString SimulatedPlayerLaunchConfigPath; + + UPROPERTY(EditAnywhere, config, Category = "Simulated Players", meta = (EditCondition = "bSimulatedPlayersIsEnabled", ConfigRestartRequired = false, DisplayName = "Region")) + TEnumAsByte SimulatedPlayerDeploymentRegionCode; + + UPROPERTY(EditAnywhere, config, Category = "Simulated Players", meta = (ConfigRestartRequired = false, DisplayName = "Include simulated players")) + bool bSimulatedPlayersIsEnabled; + + UPROPERTY(EditAnywhere, config, Category = "Simulated Players", meta = (EditCondition = "bSimulatedPlayersIsEnabled", ConfigRestartRequired = false, DisplayName = "Deployment name")) + FString SimulatedPlayerDeploymentName; + + UPROPERTY(EditAnywhere, config, Category = "Simulated Players", meta = (EditCondition = "bSimulatedPlayersIsEnabled", ConfigRestartRequired = false, DisplayName = "Number of simulated players")) + uint32 NumberOfSimulatedPlayers; + + static bool IsAssemblyNameValid(const FString& Name); + static bool IsProjectNameValid(const FString& Name); + static bool IsDeploymentNameValid(const FString& Name); + static bool IsRegionCodeValid(const ERegionCode::Type RegionCode); + public: - /** Auto-generated launch configuration file description. */ - UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (EditCondition = "bGenerateDefaultLaunchConfig", ConfigRestartRequired = false, DisplayName = "Launch configuration file description")) + /** If you have selected **Auto-generate launch configuration file**, you can change the default options in the file from the drop-down menu. */ + UPROPERTY(EditAnywhere, config, Category = "Launch", meta = (EditCondition = "bGenerateDefaultLaunchConfig", ConfigRestartRequired = false, DisplayName = "Launch configuration file options")) FSpatialLaunchConfigDescription LaunchConfigDesc; - /** If checked, placeholder entities are added to the snapshot on generation. */ - UPROPERTY(EditAnywhere, config, Category = "Snapshots", meta = (ConfigRestartRequired = false, DisplayName = "Generate placeholder entities in snapshot")) - bool bGeneratePlaceholderEntitiesInSnapshot; - - FORCEINLINE FString GetSpatialOSDirectory() const + FORCEINLINE FString GetGDKPluginDirectory() const { - return SpatialOSDirectory.Path.IsEmpty() - ? FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectDir(), TEXT("/../spatial/"))) - : SpatialOSDirectory.Path; + // Get the correct plugin directory. + FString PluginDir = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectPluginsDir(), TEXT("UnrealGDK"))); + + if (!FPaths::DirectoryExists(PluginDir)) + { + // If the Project Plugin doesn't exist then use the Engine Plugin. + PluginDir = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::EnginePluginsDir(), TEXT("UnrealGDK"))); + } + + return PluginDir; } FORCEINLINE FString GetSpatialOSLaunchConfig() const @@ -274,16 +338,12 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject FORCEINLINE FString GetSpatialOSSnapshotFolderPath() const { - return SpatialOSSnapshotPath.Path.IsEmpty() - ? FPaths::ConvertRelativePathToFull(FPaths::Combine(GetSpatialOSDirectory(), TEXT("../spatial/snapshots/"))) - : SpatialOSSnapshotPath.Path; + return FPaths::ConvertRelativePathToFull(FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("snapshots"))); } FORCEINLINE FString GetGeneratedSchemaOutputFolder() const { - return GeneratedSchemaOutputFolder.Path.IsEmpty() - ? FPaths::ConvertRelativePathToFull(FPaths::Combine(GetSpatialOSDirectory(), FString(TEXT("schema/unreal/generated/")))) - : GeneratedSchemaOutputFolder.Path; + return FPaths::ConvertRelativePathToFull(FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), FString(TEXT("schema/unreal/generated/")))); } FORCEINLINE FString GetSpatialOSCommandLineLaunchFlags() const @@ -298,4 +358,98 @@ class SPATIALGDKEDITOR_API USpatialGDKEditorSettings : public UObject return CommandLineLaunchFlags; } + + FString GetProjectNameFromSpatial() const; + + void SetPrimaryDeploymentName(const FString& Name); + FORCEINLINE FString GetPrimaryDeploymentName() const + { + return PrimaryDeploymentName; + } + + void SetAssemblyName(const FString& Name); + FORCEINLINE FString GetAssemblyName() const + { + return AssemblyName; + } + + void SetProjectName(const FString& Name); + FORCEINLINE FString GetProjectName() const + { + return ProjectName; + } + + void SetPrimaryLaunchConfigPath(const FString& Path); + FORCEINLINE FString GetPrimaryLanchConfigPath() const + { + const USpatialGDKEditorSettings* SpatialEditorSettings = GetDefault(); + return PrimaryLaunchConfigPath.FilePath.IsEmpty() + ? SpatialEditorSettings->GetSpatialOSLaunchConfig() + : PrimaryLaunchConfigPath.FilePath; + } + + void SetSnapshotPath(const FString& Path); + FORCEINLINE FString GetSnapshotPath() const + { + const USpatialGDKEditorSettings* SpatialEditorSettings = GetDefault(); + return SnapshotPath.FilePath.IsEmpty() + ? FPaths::Combine(SpatialEditorSettings->GetSpatialOSSnapshotFolderPath(), SpatialEditorSettings->GetSpatialOSSnapshotFile()) + : SnapshotPath.FilePath; + } + + void SetPrimaryRegionCode(const ERegionCode::Type RegionCode); + FORCEINLINE FText GetPrimaryRegionCode() const + { + if (!IsRegionCodeValid(PrimaryDeploymentRegionCode)) + { + return FText::FromString(TEXT("Invalid")); + } + + UEnum* Region = FindObject(ANY_PACKAGE, TEXT("ERegionCode"), true); + + return Region->GetDisplayNameTextByValue(static_cast(PrimaryDeploymentRegionCode.GetValue())); + } + + void SetSimulatedPlayerRegionCode(const ERegionCode::Type RegionCode); + FORCEINLINE FText GetSimulatedPlayerRegionCode() const + { + if (!IsRegionCodeValid(SimulatedPlayerDeploymentRegionCode)) + { + return FText::FromString(TEXT("Invalid")); + } + + UEnum* Region = FindObject(ANY_PACKAGE, TEXT("ERegionCode"), true); + + return Region->GetDisplayNameTextByValue(static_cast(SimulatedPlayerDeploymentRegionCode.GetValue())); + } + + void SetSimulatedPlayersEnabledState(bool IsEnabled); + FORCEINLINE bool IsSimulatedPlayersEnabled() const + { + return bSimulatedPlayersIsEnabled; + } + + void SetSimulatedPlayerDeploymentName(const FString& Name); + FORCEINLINE FString GetSimulatedPlayerDeploymentName() const + { + return SimulatedPlayerDeploymentName; + } + + FORCEINLINE FString GetSimulatedPlayerLaunchConfigPath() const + { + return SimulatedPlayerLaunchConfigPath; + } + + void SetNumberOfSimulatedPlayers(uint32 Number); + FORCEINLINE uint32 GetNumberOfSimulatedPlayer() const + { + return NumberOfSimulatedPlayers; + } + + FORCEINLINE FString GetDeploymentLauncherPath() const + { + return FPaths::ConvertRelativePathToFull(FPaths::Combine(GetGDKPluginDirectory() / TEXT("SpatialGDK/Binaries/ThirdParty/Improbable/Programs/DeploymentLauncher"))); + } + + bool IsDeploymentConfigurationValid() const; }; diff --git a/SpatialGDK/Source/SpatialGDKEditor/Public/WorkerTypeCustomization.h b/SpatialGDK/Source/SpatialGDKEditor/Public/WorkerTypeCustomization.h new file mode 100644 index 0000000000..062f6457ec --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditor/Public/WorkerTypeCustomization.h @@ -0,0 +1,21 @@ +#pragma once + +#include "CoreMinimal.h" +#include "IPropertyTypeCustomization.h" +#include "Utils/ActorGroupManager.h" + +class FWorkerTypeCustomization : public IPropertyTypeCustomization +{ +public: + + static TSharedRef MakeInstance(); + + /** IPropertyTypeCustomization interface */ + virtual void CustomizeHeader(TSharedRef StructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + virtual void CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + +private: + static void OnGetStrings(TArray>& OutComboBoxStrings, TArray>& OutToolTips, TArray& OutRestrictedItems); + static FString OnGetValue(TSharedPtr WorkerTypeNameHandle); + static void OnValueSelected(const FString& SelectedValue, TSharedPtr WorkerTypeNameHandle); +}; diff --git a/SpatialGDK/Source/SpatialGDKEditor/SpatialGDKEditor.Build.cs b/SpatialGDK/Source/SpatialGDKEditor/SpatialGDKEditor.Build.cs index 8bce040c84..0adefffc8e 100644 --- a/SpatialGDK/Source/SpatialGDKEditor/SpatialGDKEditor.Build.cs +++ b/SpatialGDK/Source/SpatialGDKEditor/SpatialGDKEditor.Build.cs @@ -13,9 +13,15 @@ public SpatialGDKEditor(ReadOnlyTargetRules Target) : base(Target) new string[] { "Core", "CoreUObject", + "EditorStyle", "Engine", "EngineSettings", + "Json", + "PropertyEditor", + "Slate", + "SlateCore", "SpatialGDK", + "SpatialGDKServices", "UnrealEd", "GameplayAbilities" }); diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp index 5f368c56a9..d982ff5f42 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbar.cpp @@ -1,23 +1,33 @@ // Copyright (c) Improbable Worlds Ltd, All Rights Reserved #include "SpatialGDKEditorToolbar.h" + #include "Async/Async.h" #include "Editor.h" +#include "Editor/EditorEngine.h" #include "EditorStyleSet.h" +#include "Framework/Application/SlateApplication.h" #include "Framework/MultiBox/MultiBoxBuilder.h" +#include "Framework/Notifications/NotificationManager.h" #include "ISettingsContainer.h" #include "ISettingsModule.h" #include "ISettingsSection.h" +#include "LevelEditor.h" +#include "Misc/FileHelper.h" #include "Misc/MessageDialog.h" -#include "Framework/Notifications/NotificationManager.h" -#include "Widgets/Notifications/SNotificationList.h" +#include "Serialization/JsonWriter.h" #include "SpatialGDKEditorToolbarCommands.h" #include "SpatialGDKEditorToolbarStyle.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/Layout/SBox.h" +#include "Widgets/Notifications/SNotificationList.h" #include "SpatialConstants.h" #include "SpatialGDKEditor.h" -#include "SpatialGDKSettings.h" #include "SpatialGDKEditorSettings.h" +#include "SpatialGDKServicesModule.h" +#include "SpatialGDKSettings.h" +#include "SpatialGDKSimulatedPlayerDeployment.h" #include "Editor/EditorEngine.h" #include "HAL/FileManager.h" @@ -27,15 +37,13 @@ #include "GeneralProjectSettings.h" #include "LevelEditor.h" #include "Misc/FileHelper.h" -#include "Serialization/JsonWriter.h" DEFINE_LOG_CATEGORY(LogSpatialGDKEditorToolbar); #define LOCTEXT_NAMESPACE "FSpatialGDKEditorToolbarModule" FSpatialGDKEditorToolbarModule::FSpatialGDKEditorToolbarModule() -: bStopSpatialOnExit(false), -SpatialOSStackProcessID(0) +: bStopSpatialOnExit(false) { } @@ -50,8 +58,6 @@ void FSpatialGDKEditorToolbarModule::StartupModule() MapActions(PluginCommands); SetupToolbar(PluginCommands); - CheckForRunningStack(); - // load sounds ExecutionStartSound = LoadObject(nullptr, TEXT("/Engine/EditorSounds/Notifications/CompileStart_Cue.CompileStart_Cue")); ExecutionStartSound->AddToRoot(); @@ -63,6 +69,19 @@ void FSpatialGDKEditorToolbarModule::StartupModule() OnPropertyChangedDelegateHandle = FCoreUObjectDelegates::OnObjectPropertyChanged.AddRaw(this, &FSpatialGDKEditorToolbarModule::OnPropertyChanged); bStopSpatialOnExit = GetDefault()->bStopSpatialOnExit; + + FSpatialGDKServicesModule& GDKServices = FModuleManager::GetModuleChecked("SpatialGDKServices"); + LocalDeploymentManager = GDKServices.GetLocalDeploymentManager(); + LocalDeploymentManager->SetAutoDeploy(GetDefault()->bAutoStartLocalDeployment); + + // Bind the play button delegate to starting a local spatial deployment. + if (!UEditorEngine::TryStartSpatialDeployment.IsBound() && GetDefault()->bAutoStartLocalDeployment) + { + UEditorEngine::TryStartSpatialDeployment.BindLambda([this] + { + VerifyAndStartDeployment(); + }); + } } void FSpatialGDKEditorToolbarModule::ShutdownModule() @@ -104,16 +123,12 @@ void FSpatialGDKEditorToolbarModule::PreUnloadCallback() { if (bStopSpatialOnExit) { - StopRunningStack(); + LocalDeploymentManager->TryStopLocalDeployment(); } } void FSpatialGDKEditorToolbarModule::Tick(float DeltaTime) { - if (SpatialOSStackProcessID != 0 && !FPlatformProcess::IsApplicationRunning(SpatialOSStackProcessID)) - { - CleanupSpatialProcess(); - } } bool FSpatialGDKEditorToolbarModule::CanExecuteSchemaGenerator() const @@ -144,30 +159,48 @@ void FSpatialGDKEditorToolbarModule::MapActions(TSharedPtr FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::CanExecuteSnapshotGenerator)); InPluginCommands->MapAction( - FSpatialGDKEditorToolbarCommands::Get().StartSpatialOSStackAction, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartSpatialOSButtonClicked), - FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartSpatialOSStackCanExecute), + FSpatialGDKEditorToolbarCommands::Get().StartSpatialDeployment, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartSpatialDeploymentButtonClicked), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartSpatialDeploymentCanExecute), FIsActionChecked(), - FIsActionButtonVisible::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartSpatialOSStackCanExecute)); + FIsActionButtonVisible::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartSpatialDeploymentIsVisible)); InPluginCommands->MapAction( - FSpatialGDKEditorToolbarCommands::Get().StopSpatialOSStackAction, - FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialOSButtonClicked), - FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialOSStackCanExecute), + FSpatialGDKEditorToolbarCommands::Get().StopSpatialDeployment, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialDeploymentButtonClicked), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialDeploymentCanExecute), FIsActionChecked(), - FIsActionButtonVisible::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialOSStackCanExecute)); + FIsActionButtonVisible::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialDeploymentIsVisible)); InPluginCommands->MapAction( FSpatialGDKEditorToolbarCommands::Get().LaunchInspectorWebPageAction, FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::LaunchInspectorWebpageButtonClicked), FCanExecuteAction()); + + InPluginCommands->MapAction( + FSpatialGDKEditorToolbarCommands::Get().OpenSimulatedPlayerConfigurationWindowAction, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::ShowSimulatedPlayerDeploymentDialog), + FCanExecuteAction()); + + InPluginCommands->MapAction( + FSpatialGDKEditorToolbarCommands::Get().StartSpatialService, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartSpatialServiceButtonClicked), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartSpatialServiceCanExecute), + FIsActionChecked(), + FIsActionButtonVisible::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StartSpatialServiceIsVisible)); + + InPluginCommands->MapAction( + FSpatialGDKEditorToolbarCommands::Get().StopSpatialService, + FExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialServiceButtonClicked), + FCanExecuteAction::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialServiceCanExecute), + FIsActionChecked(), + FIsActionButtonVisible::CreateRaw(this, &FSpatialGDKEditorToolbarModule::StopSpatialServiceIsVisible)); } void FSpatialGDKEditorToolbarModule::SetupToolbar(TSharedPtr InPluginCommands) { FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); - { TSharedPtr MenuExtender = MakeShareable(new FExtender()); MenuExtender->AddMenuExtension( @@ -194,9 +227,12 @@ void FSpatialGDKEditorToolbarModule::AddMenuExtension(FMenuBuilder& Builder) { Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSchema); Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSnapshot); - Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StartSpatialOSStackAction); - Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StopSpatialOSStackAction); + Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StartSpatialDeployment); + Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StopSpatialDeployment); Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().LaunchInspectorWebPageAction); + Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().OpenSimulatedPlayerConfigurationWindowAction); + Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StartSpatialService); + Builder.AddMenuEntry(FSpatialGDKEditorToolbarCommands::Get().StopSpatialService); } Builder.EndSection(); } @@ -214,9 +250,12 @@ void FSpatialGDKEditorToolbarModule::AddToolbarExtension(FToolBarBuilder& Builde true ); Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().CreateSpatialGDKSnapshot); - Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StartSpatialOSStackAction); - Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StopSpatialOSStackAction); + Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StartSpatialDeployment); + Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StopSpatialDeployment); Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().LaunchInspectorWebPageAction); + Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().OpenSimulatedPlayerConfigurationWindowAction); + Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StartSpatialService); + Builder.AddToolBarButton(FSpatialGDKEditorToolbarCommands::Get().StopSpatialService); } TSharedRef FSpatialGDKEditorToolbarModule::CreateGenerateSchemaMenuContent() @@ -233,48 +272,41 @@ TSharedRef FSpatialGDKEditorToolbarModule::CreateGenerateSchemaMenuCont void FSpatialGDKEditorToolbarModule::CreateSnapshotButtonClicked() { - ShowTaskStartNotification("Started snapshot generation"); + OnShowTaskStartNotification("Started snapshot generation"); const USpatialGDKEditorSettings* Settings = GetDefault(); SpatialGDKEditorInstance->GenerateSnapshot( GEditor->GetEditorWorldContext().World(), Settings->GetSpatialOSSnapshotFile(), - FSimpleDelegate::CreateLambda([this]() { ShowSuccessNotification("Snapshot successfully generated!"); }), - FSimpleDelegate::CreateLambda([this]() { ShowFailedNotification("Snapshot generation failed!"); }), + FSimpleDelegate::CreateLambda([this]() { OnShowSuccessNotification("Snapshot successfully generated!"); }), + FSimpleDelegate::CreateLambda([this]() { OnShowFailedNotification("Snapshot generation failed!"); }), FSpatialGDKEditorErrorHandler::CreateLambda([](FString ErrorText) { FMessageDialog::Debugf(FText::FromString(ErrorText)); })); } void FSpatialGDKEditorToolbarModule::SchemaGenerateButtonClicked() { - ShowTaskStartNotification("Generating Schema (Incremental)"); - - if (SpatialGDKEditorInstance->GenerateSchema(false)) - { - ShowSuccessNotification("Incremental Schema Generation Completed!"); - } - else - { - ShowFailedNotification("Incremental Schema Generation Failed"); - } + GenerateSchema(false); } void FSpatialGDKEditorToolbarModule::SchemaGenerateFullButtonClicked() { - ShowTaskStartNotification("Generating Schema (Full)"); + GenerateSchema(true); +} - if (SpatialGDKEditorInstance->GenerateSchema(true)) - { - ShowSuccessNotification("Full Schema Generation Completed!"); - } - else +void FSpatialGDKEditorToolbarModule::OnShowTaskStartNotification(const FString& NotificationText) +{ + AsyncTask(ENamedThreads::GameThread, [NotificationText] { - ShowFailedNotification("Full Schema Generation Failed"); - } + if (FSpatialGDKEditorToolbarModule* Module = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) + { + Module->ShowTaskStartNotification(NotificationText); + } + }); } - void FSpatialGDKEditorToolbarModule::ShowTaskStartNotification(const FString& NotificationText) { + // If a task notification already exists then expire it. if (TaskNotificationPtr.IsValid()) { TaskNotificationPtr.Pin()->ExpireAndFadeout(); @@ -286,7 +318,7 @@ void FSpatialGDKEditorToolbarModule::ShowTaskStartNotification(const FString& No } FNotificationInfo Info(FText::AsCultureInvariant(NotificationText)); - Info.Image = FEditorStyle::GetBrush(TEXT("LevelEditor.RecompileGameCode")); + Info.Image = FSpatialGDKEditorToolbarStyle::Get().GetBrush(TEXT("SpatialGDKEditorToolbar.SpatialOSLogo")); Info.ExpireDuration = 5.0f; Info.bFireAndForget = false; @@ -298,40 +330,64 @@ void FSpatialGDKEditorToolbarModule::ShowTaskStartNotification(const FString& No } } +void FSpatialGDKEditorToolbarModule::OnShowSuccessNotification(const FString& NotificationText) +{ + AsyncTask(ENamedThreads::GameThread, [NotificationText] + { + if (FSpatialGDKEditorToolbarModule* Module = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) + { + Module->ShowSuccessNotification(NotificationText); + } + }); +} + void FSpatialGDKEditorToolbarModule::ShowSuccessNotification(const FString& NotificationText) { - AsyncTask(ENamedThreads::GameThread, [this, NotificationText]{ - TSharedPtr Notification = TaskNotificationPtr.Pin(); + TSharedPtr Notification = TaskNotificationPtr.Pin(); + if (Notification.IsValid()) + { Notification->SetFadeInDuration(0.1f); Notification->SetFadeOutDuration(0.5f); - Notification->SetExpireDuration(7.5f); + Notification->SetExpireDuration(5.0f); Notification->SetText(FText::AsCultureInvariant(NotificationText)); Notification->SetCompletionState(SNotificationItem::CS_Success); Notification->ExpireAndFadeout(); - TaskNotificationPtr.Reset(); if (GEditor && ExecutionSuccessSound) { GEditor->PlayEditorSound(ExecutionSuccessSound); } + } +} + +void FSpatialGDKEditorToolbarModule::OnShowFailedNotification(const FString& NotificationText) +{ + AsyncTask(ENamedThreads::GameThread, [NotificationText] + { + if (FSpatialGDKEditorToolbarModule* Module = FModuleManager::GetModulePtr("SpatialGDKEditorToolbar")) + { + Module->ShowFailedNotification(NotificationText); + } }); } void FSpatialGDKEditorToolbarModule::ShowFailedNotification(const FString& NotificationText) { - AsyncTask(ENamedThreads::GameThread, [this, NotificationText]{ - TSharedPtr Notification = TaskNotificationPtr.Pin(); + 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(SNotificationItem::CS_Fail); - Notification->SetExpireDuration(5.0f); - Notification->ExpireAndFadeout(); if (GEditor && ExecutionFailSound) { GEditor->PlayEditorSound(ExecutionFailSound); } - }); + } } bool FSpatialGDKEditorToolbarModule::ValidateGeneratedLaunchConfig() const @@ -344,7 +400,7 @@ bool FSpatialGDKEditorToolbarModule::ValidateGeneratedLaunchConfig() const { if (SpatialGDKRuntimeSettings->bUsingQBI && (*EnableChunkInterest == TEXT("true"))) { - const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(FString::Printf(TEXT("The legacy flag \"enable_chunk_interest\" is set to true in the generated launch configuration. This flag needs to be set to false when QBI is enabled.\n\nDo you want to configure your launch config settings now?")))); + const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(TEXT("The legacy flag \"enable_chunk_interest\" is set to true in the generated launch configuration. This flag needs to be set to false when QBI is enabled.\n\nDo you want to configure your launch config settings now?"))); if (Result == EAppReturnType::Yes) { @@ -355,7 +411,7 @@ bool FSpatialGDKEditorToolbarModule::ValidateGeneratedLaunchConfig() const } else if (!SpatialGDKRuntimeSettings->bUsingQBI && (*EnableChunkInterest == TEXT("false"))) { - const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(FString::Printf(TEXT("The legacy flag \"enable_chunk_interest\" is set to false in the generated launch configuration. This flag needs to be set to true when QBI is disabled.\n\nDo you want to configure your launch config settings now?")))); + const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(TEXT("The legacy flag \"enable_chunk_interest\" is set to false in the generated launch configuration. This flag needs to be set to true when QBI is disabled.\n\nDo you want to configure your launch config settings now?"))); if (Result == EAppReturnType::Yes) { @@ -367,7 +423,7 @@ bool FSpatialGDKEditorToolbarModule::ValidateGeneratedLaunchConfig() const } else { - const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(FString::Printf(TEXT("The legacy flag \"enable_chunk_interest\" is not specified in the generated launch configuration.\n\nDo you want to configure your launch config settings now?")))); + const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(TEXT("The legacy flag \"enable_chunk_interest\" is not specified in the generated launch configuration.\n\nDo you want to configure your launch config settings now?"))); if (Result == EAppReturnType::Yes) { @@ -377,11 +433,104 @@ bool FSpatialGDKEditorToolbarModule::ValidateGeneratedLaunchConfig() const return false; } + const ULevelEditorPlaySettings* LevelEditorPlaySettings = GetDefault(); + if (!SpatialGDKRuntimeSettings->bEnableHandover && LevelEditorPlaySettings->GetTotalPIEServerWorkerCount() > 1) + { + const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(TEXT("Property handover is disabled and multiple launch servers are 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 (!SpatialGDKRuntimeSettings->ServerWorkerTypes.Contains(SpatialGDKRuntimeSettings->DefaultWorkerType.WorkerTypeName)) + { + const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(TEXT("Default Worker Type is invalid, please choose a valid worker type as the default.\n\nDo you want to configure your project settings now?"))); + + if (Result == EAppReturnType::Yes) + { + FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Runtime Settings"); + } + + return false; + } + + if (SpatialGDKRuntimeSettings->bEnableOffloading) + { + for (const TPair& ActorGroup : SpatialGDKRuntimeSettings->ActorGroups) + { + if (!SpatialGDKRuntimeSettings->ServerWorkerTypes.Contains(ActorGroup.Value.OwningWorkerType.WorkerTypeName)) + { + const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(FString::Printf(TEXT("Actor Group '%s' has an invalid Owning Worker Type, please choose a valid worker type.\n\nDo you want to configure your project settings now?"), *ActorGroup.Key.ToString()))); + + if (Result == EAppReturnType::Yes) + { + FModuleManager::LoadModuleChecked("Settings").ShowViewer("Project", "SpatialGDKEditor", "Runtime Settings"); + } + + return false; + } + } + } + return true; } -void FSpatialGDKEditorToolbarModule::StartSpatialOSButtonClicked() +void FSpatialGDKEditorToolbarModule::StartSpatialServiceButtonClicked() { + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] + { + FDateTime StartTime = FDateTime::Now(); + OnShowTaskStartNotification(TEXT("Starting spatial service...")); + + if (!LocalDeploymentManager->TryStartSpatialService()) + { + OnShowFailedNotification(TEXT("Spatial service failed to start")); + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Could not start spatial service.")); + return; + } + + FTimespan Span = FDateTime::Now() - StartTime; + + OnShowSuccessNotification(TEXT("Spatial service started!")); + UE_LOG(LogSpatialGDKEditorToolbar, Log, TEXT("Spatial service started in %f seconds."), Span.GetTotalSeconds()); + }); +} + +void FSpatialGDKEditorToolbarModule::StopSpatialServiceButtonClicked() +{ + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] + { + FDateTime StartTime = FDateTime::Now(); + OnShowTaskStartNotification(TEXT("Stopping spatial service...")); + + if (!LocalDeploymentManager->TryStopSpatialService()) + { + OnShowFailedNotification(TEXT("Spatial service failed to stop")); + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Could not stop spatial service.")); + return; + } + + FTimespan Span = FDateTime::Now() - StartTime; + + OnShowSuccessNotification(TEXT("Spatial service stopped!")); + UE_LOG(LogSpatialGDKEditorToolbar, Log, TEXT("Spatial service stopped in %f secoonds."), Span.GetTotalSeconds()); + }); +} + +void FSpatialGDKEditorToolbarModule::VerifyAndStartDeployment() +{ + // Don't try and start a local deployment if spatial networking is disabled. + if (!GetDefault()->bSpatialNetworking) + { + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Attempted to start a local deployment but spatial networking is disabled.")); + return; + } + + // Get the latest launch config. const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); FString LaunchConfig; @@ -392,6 +541,11 @@ void FSpatialGDKEditorToolbarModule::StartSpatialOSButtonClicked() return; } + if (!GenerateDefaultWorkerJson()) + { + return; + } + LaunchConfig = FPaths::Combine(FPaths::ConvertRelativePathToFull(FPaths::ProjectIntermediateDir()), TEXT("Improbable/DefaultLaunchConfig.json")); GenerateDefaultLaunchConfig(LaunchConfig); } @@ -400,60 +554,60 @@ void FSpatialGDKEditorToolbarModule::StartSpatialOSButtonClicked() LaunchConfig = SpatialGDKSettings->GetSpatialOSLaunchConfig(); } - const FString ExecuteAbsolutePath = SpatialGDKSettings->GetSpatialOSDirectory(); - const FString CmdExecutable = TEXT("cmd.exe"); - - const FString SpatialCmdArgument = FString::Printf( - TEXT("/c cmd.exe /c spatial.exe worker build build-config ^& spatial.exe local launch \"%s\" %s ^& pause"), *LaunchConfig, *SpatialGDKSettings->GetSpatialOSCommandLineLaunchFlags()); - - UE_LOG(LogSpatialGDKEditorToolbar, Log, TEXT("Starting cmd.exe with `%s` arguments."), *SpatialCmdArgument); - // Temporary workaround: To get spatial.exe to properly show a window we have to call cmd.exe to - // execute it. We currently can't use pipes to capture output as it doesn't work properly with current - // spatial.exe. - SpatialOSStackProcHandle = FPlatformProcess::CreateProc( - *(CmdExecutable), *SpatialCmdArgument, true, false, false, &SpatialOSStackProcessID, 0, - *ExecuteAbsolutePath, nullptr, nullptr); - - FNotificationInfo Info(SpatialOSStackProcHandle.IsValid() == true - ? FText::FromString(TEXT("SpatialOS Starting...")) - : FText::FromString(TEXT("Failed to start SpatialOS"))); - Info.ExpireDuration = 3.0f; - Info.bUseSuccessFailIcons = true; - TSharedPtr NotificationItem = FSlateNotificationManager::Get().AddNotification(Info); + const FString LaunchFlags = SpatialGDKSettings->GetSpatialOSCommandLineLaunchFlags(); - if (!SpatialOSStackProcHandle.IsValid()) + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, LaunchConfig, LaunchFlags] { - NotificationItem->SetCompletionState(SNotificationItem::CS_Fail); - const FString SpatialLogPath = - SpatialGDKSettings->GetSpatialOSDirectory() + FString(TEXT("/logs/spatial.log")); - UE_LOG(LogSpatialGDKEditorToolbar, Error, - TEXT("Failed to start SpatialOS, please refer to log file `%s` for more information."), - *SpatialLogPath); - } - else - { - NotificationItem->SetCompletionState(SNotificationItem::CS_Success); - } + // If the last local deployment is still stopping then wait until it's finished. + while (LocalDeploymentManager->IsDeploymentStopping()) + { + FPlatformProcess::Sleep(0.1f); + } - NotificationItem->ExpireAndFadeout(); + // If schema or worker configurations have been changed then we must restart the deployment. + if (LocalDeploymentManager->IsRedeployRequired() && LocalDeploymentManager->IsLocalDeploymentRunning()) + { + UE_LOG(LogSpatialGDKEditorToolbar, Display, TEXT("Local deployment must restart.")); + OnShowTaskStartNotification(TEXT("Local deployment restarting.")); + LocalDeploymentManager->TryStopLocalDeployment(); + } + else if (LocalDeploymentManager->IsLocalDeploymentRunning()) + { + // A good local deployment is already running. + return; + } + + OnShowTaskStartNotification(TEXT("Starting local deployment...")); + if (LocalDeploymentManager->TryStartLocalDeployment(LaunchConfig, LaunchFlags)) + { + OnShowSuccessNotification(TEXT("Local deployment started!")); + } + else + { + OnShowFailedNotification(TEXT("Local deployment failed to start")); + } + }); } -void FSpatialGDKEditorToolbarModule::StopSpatialOSButtonClicked() +void FSpatialGDKEditorToolbarModule::StartSpatialDeploymentButtonClicked() { - StopRunningStack(); + VerifyAndStartDeployment(); } -void FSpatialGDKEditorToolbarModule::StopRunningStack() +void FSpatialGDKEditorToolbarModule::StopSpatialDeploymentButtonClicked() { - if (SpatialOSStackProcHandle.IsValid()) + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] { - if (FPlatformProcess::IsProcRunning(SpatialOSStackProcHandle)) + OnShowTaskStartNotification(TEXT("Stopping local deployment...")); + if (LocalDeploymentManager->TryStopLocalDeployment()) { - FPlatformProcess::TerminateProc(SpatialOSStackProcHandle, true); + OnShowSuccessNotification(TEXT("Successfully stopped local deployment")); } - - CleanupSpatialProcess(); - } + else + { + OnShowFailedNotification(TEXT("Failed to stop local deployment!")); + } + }); } void FSpatialGDKEditorToolbarModule::LaunchInspectorWebpageButtonClicked() @@ -471,41 +625,55 @@ void FSpatialGDKEditorToolbarModule::LaunchInspectorWebpageButtonClicked() } } -bool FSpatialGDKEditorToolbarModule::StartSpatialOSStackCanExecute() const +bool FSpatialGDKEditorToolbarModule::StartSpatialDeploymentIsVisible() const { - return !SpatialOSStackProcHandle.IsValid() && !FPlatformProcess::IsApplicationRunning(SpatialOSStackProcessID); + if (LocalDeploymentManager->IsSpatialServiceRunning()) + { + return !LocalDeploymentManager->IsLocalDeploymentRunning(); + } + else + { + return true; + } } -bool FSpatialGDKEditorToolbarModule::StopSpatialOSStackCanExecute() const +bool FSpatialGDKEditorToolbarModule::StartSpatialDeploymentCanExecute() const { - return SpatialOSStackProcHandle.IsValid(); + return !LocalDeploymentManager->IsDeploymentStarting() && GetDefault()->bSpatialNetworking; } -void FSpatialGDKEditorToolbarModule::CheckForRunningStack() +bool FSpatialGDKEditorToolbarModule::StopSpatialDeploymentIsVisible() const { - FPlatformProcess::FProcEnumerator ProcEnumerator; - do - { - FPlatformProcess::FProcEnumInfo Proc = ProcEnumerator.GetCurrent(); - const FString ProcName = Proc.GetName(); - if (ProcName.Compare(TEXT("spatial.exe"), ESearchCase::IgnoreCase) == 0) - { - uint32 ProcPID = Proc.GetPID(); - SpatialOSStackProcHandle = FPlatformProcess::OpenProcess(ProcPID); - if (SpatialOSStackProcHandle.IsValid()) - { - SpatialOSStackProcessID = ProcPID; - } - } - } while (ProcEnumerator.MoveNext() && !SpatialOSStackProcHandle.IsValid()); + return LocalDeploymentManager->IsSpatialServiceRunning() && LocalDeploymentManager->IsLocalDeploymentRunning(); } -void FSpatialGDKEditorToolbarModule::CleanupSpatialProcess() +bool FSpatialGDKEditorToolbarModule::StopSpatialDeploymentCanExecute() const { - FPlatformProcess::CloseProc(SpatialOSStackProcHandle); - SpatialOSStackProcessID = 0; + return !LocalDeploymentManager->IsDeploymentStopping(); +} - OnSpatialShutdown.Broadcast(); +bool FSpatialGDKEditorToolbarModule::StartSpatialServiceIsVisible() const +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + + return SpatialGDKSettings->bShowSpatialServiceButton && !LocalDeploymentManager->IsSpatialServiceRunning(); +} + +bool FSpatialGDKEditorToolbarModule::StartSpatialServiceCanExecute() const +{ + return !LocalDeploymentManager->IsServiceStarting(); +} + +bool FSpatialGDKEditorToolbarModule::StopSpatialServiceIsVisible() const +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + + return SpatialGDKSettings->bShowSpatialServiceButton && LocalDeploymentManager->IsSpatialServiceRunning(); +} + +bool FSpatialGDKEditorToolbarModule::StopSpatialServiceCanExecute() const +{ + return !LocalDeploymentManager->IsServiceStopping(); } /** @@ -525,17 +693,61 @@ void FSpatialGDKEditorToolbarModule::OnPropertyChanged(UObject* ObjectBeingModif { bStopSpatialOnExit = Settings->bStopSpatialOnExit; } + 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(); + } + } } } +void FSpatialGDKEditorToolbarModule::ShowSimulatedPlayerDeploymentDialog() +{ + // 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) + ] + ); + + TSharedPtr RootWindow = FGlobalTabmanager::Get()->GetRootWindow(); + + FSlateApplication::Get().AddModalWindow(SimulatedPlayerDeploymentWindowPtr.ToSharedRef(), RootWindow); +} + bool FSpatialGDKEditorToolbarModule::GenerateDefaultLaunchConfig(const FString& LaunchConfigPath) const { - if (const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault()) + if (const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault()) { FString Text; TSharedRef< TJsonWriter<> > Writer = TJsonWriterFactory<>::Create(&Text); - const FSpatialLaunchConfigDescription& LaunchConfigDescription = SpatialGDKSettings->LaunchConfigDesc; + const FSpatialLaunchConfigDescription& LaunchConfigDescription = SpatialGDKEditorSettings->LaunchConfigDesc; // Populate json file for launch config Writer->WriteObjectStart(); // Start of json @@ -565,20 +777,20 @@ bool FSpatialGDKEditorToolbarModule::GenerateDefaultLaunchConfig(const FString& Writer->WriteObjectEnd(); // World section end Writer->WriteObjectStart(TEXT("load_balancing")); // Load balancing section begin Writer->WriteArrayStart("layer_configurations"); - for (const FWorkerTypeLaunchSection& Worker : LaunchConfigDescription.Workers) + 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.Workers) + for (const FWorkerTypeLaunchSection& Worker : LaunchConfigDescription.ServerWorkers) { WriteWorkerSection(Writer, Worker); } // Write the client worker section FWorkerTypeLaunchSection ClientWorker; - ClientWorker.WorkerTypeName = SpatialConstants::ClientWorkerType; + ClientWorker.WorkerTypeName = SpatialConstants::DefaultClientWorkerType; ClientWorker.WorkerPermissions.bAllPermissions = true; ClientWorker.bLoginRateLimitEnabled = false; WriteWorkerSection(Writer, ClientWorker); @@ -599,6 +811,96 @@ bool FSpatialGDKEditorToolbarModule::GenerateDefaultLaunchConfig(const FString& return false; } +bool FSpatialGDKEditorToolbarModule::GenerateDefaultWorkerJson() +{ + if (const USpatialGDKEditorSettings* SpatialGDKEditorSettings = GetDefault()) + { + const FString WorkerJsonDir = FSpatialGDKServicesModule::GetSpatialOSDirectory(TEXT("workers/unreal")); + const FString TemplateWorkerJsonPath = FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(TEXT("SpatialGDK/Extras/templates/WorkerJsonTemplate.json")); + + const FSpatialLaunchConfigDescription& LaunchConfigDescription = SpatialGDKEditorSettings->LaunchConfigDesc; + for (const FWorkerTypeLaunchSection& Worker : LaunchConfigDescription.ServerWorkers) + { + FString JsonPath = FPaths::Combine(WorkerJsonDir, FString::Printf(TEXT("spatialos.%s.worker.json"), *Worker.WorkerTypeName.ToString())); + if (!FPaths::FileExists(JsonPath)) + { + UE_LOG(LogSpatialGDKEditorToolbar, Verbose, TEXT("Could not find worker json at %s"), *JsonPath); + FString Contents; + if (FFileHelper::LoadFileToString(Contents, *TemplateWorkerJsonPath)) + { + Contents.ReplaceInline(TEXT("{{WorkerTypeName}}"), *Worker.WorkerTypeName.ToString()); + if (FFileHelper::SaveStringToFile(Contents, *JsonPath)) + { + LocalDeploymentManager->SetRedeployRequired(); + UE_LOG(LogSpatialGDKEditorToolbar, Verbose, TEXT("Wrote default worker json to %s"), *JsonPath) + } + else + { + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Failed to write default worker json to %s"), *JsonPath) + } + } + else + { + UE_LOG(LogSpatialGDKEditorToolbar, Error, TEXT("Failed to read default worker json template at %s"), *TemplateWorkerJsonPath) + } + } + else + { + UE_LOG(LogSpatialGDKEditorToolbar, Verbose, TEXT("Found worker json at %s"), *JsonPath) + } + } + + return true; + } + + return false; +} + +void FSpatialGDKEditorToolbarModule::GenerateSchema(bool bFullScan) +{ + LocalDeploymentManager->SetRedeployRequired(); + + if (SpatialGDKEditorInstance->FullScanRequired()) + { + OnShowTaskStartNotification("Initial Schema Generation"); + + if (SpatialGDKEditorInstance->GenerateSchema(true)) + { + OnShowSuccessNotification("Initial Schema Generation completed!"); + } + else + { + OnShowFailedNotification("Initial Schema Generation failed"); + } + } + else if (bFullScan) + { + OnShowTaskStartNotification("Generating Schema (Full)"); + + if (SpatialGDKEditorInstance->GenerateSchema(true)) + { + OnShowSuccessNotification("Full Schema Generation completed!"); + } + else + { + OnShowFailedNotification("Full Schema Generation failed"); + } + } + else + { + OnShowTaskStartNotification("Generating Schema (Incremental)"); + + if (SpatialGDKEditorInstance->GenerateSchema(false)) + { + OnShowSuccessNotification("Incremental Schema Generation completed!"); + } + else + { + OnShowFailedNotification("Incremental Schema Generation failed"); + } + } +} + bool FSpatialGDKEditorToolbarModule::WriteFlagSection(TSharedRef< TJsonWriter<> > Writer, const FString& Key, const FString& Value) const { Writer->WriteObjectStart(); @@ -612,7 +914,7 @@ bool FSpatialGDKEditorToolbarModule::WriteFlagSection(TSharedRef< TJsonWriter<> bool FSpatialGDKEditorToolbarModule::WriteWorkerSection(TSharedRef< TJsonWriter<> > Writer, const FWorkerTypeLaunchSection& Worker) const { Writer->WriteObjectStart(); - Writer->WriteValue(TEXT("worker_type"), *Worker.WorkerTypeName); + Writer->WriteValue(TEXT("worker_type"), *Worker.WorkerTypeName.ToString()); Writer->WriteArrayStart(TEXT("flags")); for (const auto& Flag : Worker.Flags) { @@ -664,10 +966,10 @@ bool FSpatialGDKEditorToolbarModule::WriteWorkerSection(TSharedRef< TJsonWriter< return true; } -bool FSpatialGDKEditorToolbarModule::WriteLoadbalancingSection(TSharedRef< TJsonWriter<> > Writer, const FString& WorkerType, const int32 Columns, const int32 Rows, const bool ManualWorkerConnectionOnly) const +bool FSpatialGDKEditorToolbarModule::WriteLoadbalancingSection(TSharedRef< TJsonWriter<> > Writer, const FName& WorkerType, const int32 Columns, const int32 Rows, const bool ManualWorkerConnectionOnly) const { Writer->WriteObjectStart(); - Writer->WriteValue(TEXT("layer"), WorkerType); + Writer->WriteValue(TEXT("layer"), *WorkerType.ToString()); Writer->WriteObjectStart("rectangle_grid"); Writer->WriteValue(TEXT("cols"), Columns); Writer->WriteValue(TEXT("rows"), Rows); diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp index 78be21ad97..cdc97ade7a 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarCommands.cpp @@ -9,9 +9,12 @@ 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(CreateSpatialGDKSnapshot, "Snapshot", "Creates SpatialOS Unreal GDK snapshot.", EUserInterfaceActionType::Button, FInputGesture()); - UI_COMMAND(StartSpatialOSStackAction, "Start", "Starts a local instance of SpatialOS.", EUserInterfaceActionType::Button, FInputGesture()); - UI_COMMAND(StopSpatialOSStackAction, "Stop", "Stops SpatialOS.", 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(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(StartSpatialService, "Start Service", "Starts the Spatial service daemon.", EUserInterfaceActionType::Button, FInputGesture()); + UI_COMMAND(StopSpatialService, "Stop Service", "Stops the Spatial service daemon.", EUserInterfaceActionType::Button, FInputGesture()); } #undef LOCTEXT_NAMESPACE diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarStyle.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarStyle.cpp index ca489f2256..253823939b 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarStyle.cpp +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKEditorToolbarStyle.cpp @@ -39,6 +39,7 @@ namespace const FVector2D Icon16x16(16.0f, 16.0f); const FVector2D Icon20x20(20.0f, 20.0f); const FVector2D Icon40x40(40.0f, 40.0f); +const FVector2D Icon100x22(100.0f, 22.0f); } TSharedRef FSpatialGDKEditorToolbarStyle::Create() @@ -60,16 +61,16 @@ TSharedRef FSpatialGDKEditorToolbarStyle::Create() Style->Set("SpatialGDKEditorToolbar.CreateSpatialGDKSchema.Small", new IMAGE_BRUSH(TEXT("Schema@0.5x"), Icon20x20)); - Style->Set("SpatialGDKEditorToolbar.StartSpatialOSStackAction", + Style->Set("SpatialGDKEditorToolbar.StartSpatialDeployment", new IMAGE_BRUSH(TEXT("Launch"), Icon40x40)); - Style->Set("SpatialGDKEditorToolbar.StartSpatialOSStackAction.Small", + Style->Set("SpatialGDKEditorToolbar.StartSpatialDeployment.Small", new IMAGE_BRUSH(TEXT("Launch@0.5x"), Icon20x20)); - Style->Set("SpatialGDKEditorToolbar.StopSpatialOSStackAction", + Style->Set("SpatialGDKEditorToolbar.StopSpatialDeployment", new IMAGE_BRUSH(TEXT("Stop"), Icon40x40)); - Style->Set("SpatialGDKEditorToolbar.StopSpatialOSStackAction.Small", + Style->Set("SpatialGDKEditorToolbar.StopSpatialDeployment.Small", new IMAGE_BRUSH(TEXT("Stop@0.5x"), Icon20x20)); Style->Set("SpatialGDKEditorToolbar.LaunchInspectorWebPageAction", @@ -78,6 +79,27 @@ TSharedRef FSpatialGDKEditorToolbarStyle::Create() Style->Set("SpatialGDKEditorToolbar.LaunchInspectorWebPageAction.Small", new IMAGE_BRUSH(TEXT("Inspector@0.5x"), Icon20x20)); + Style->Set("SpatialGDKEditorToolbar.OpenSimulatedPlayerConfigurationWindowAction", + new IMAGE_BRUSH(TEXT("Cloud"), Icon40x40)); + + Style->Set("SpatialGDKEditorToolbar.OpenSimulatedPlayerConfigurationWindowAction.Small", + new IMAGE_BRUSH(TEXT("Cloud@0.5x"), Icon20x20)); + + Style->Set("SpatialGDKEditorToolbar.StartSpatialService", + new IMAGE_BRUSH(TEXT("Launch"), Icon40x40)); + + Style->Set("SpatialGDKEditorToolbar.StartSpatialService.Small", + new IMAGE_BRUSH(TEXT("Launch@0.5x"), Icon20x20)); + + Style->Set("SpatialGDKEditorToolbar.StopSpatialService", + new IMAGE_BRUSH(TEXT("Stop"), Icon40x40)); + + Style->Set("SpatialGDKEditorToolbar.StopSpatialService.Small", + new IMAGE_BRUSH(TEXT("Stop@0.5x"), Icon20x20)); + + Style->Set("SpatialGDKEditorToolbar.SpatialOSLogo", + new IMAGE_BRUSH(TEXT("SPATIALOS_LOGO_WHITE"), Icon100x22)); + return Style; } diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKSimulatedPlayerDeployment.cpp b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKSimulatedPlayerDeployment.cpp new file mode 100644 index 0000000000..2d8cac5986 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Private/SpatialGDKSimulatedPlayerDeployment.cpp @@ -0,0 +1,605 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialGDKSimulatedPlayerDeployment.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 "Templates/SharedPointer.h" +#include "SpatialGDKEditorSettings.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" + +void SSpatialGDKSimulatedPlayerDeployment::Construct(const FArguments& InArgs) +{ + const USpatialGDKEditorSettings* SpatialGDKSettings = GetDefault(); + + 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(SpatialGDKSettings->GetProjectName())) + .ToolTipText(FText::FromString(FString(TEXT("The name of the SpatialOS project.")))) + .OnTextCommitted(this, &SSpatialGDKSimulatedPlayerDeployment::OnProjectNameCommited) + .OnTextChanged(this, &SSpatialGDKSimulatedPlayerDeployment::OnProjectNameCommited, ETextCommit::Default) + ] + ] + // 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) + ] + ] + // 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(FSpatialGDKServicesModule::GetSpatialOSDirectory()) + .BrowseTitle(FText::FromString(FString(TEXT("File picker...")))) + .FilePath_UObject(SpatialGDKSettings, &USpatialGDKEditorSettings::GetPrimaryLanchConfigPath) + .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::OnProjectNameCommited(const FText& InText, ETextCommit::Type InCommitType) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetProjectName(InText.ToString()); +} + +void SSpatialGDKSimulatedPlayerDeployment::OnPrimaryDeploymentNameCommited(const FText& InText, ETextCommit::Type InCommitType) +{ + USpatialGDKEditorSettings* SpatialGDKSettings = GetMutableDefault(); + SpatialGDKSettings->SetPrimaryDeploymentName(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(); + + if (!SpatialGDKSettings->IsDeploymentConfigurationValid()) { + FNotificationInfo Info(FText::FromString(TEXT("Deployment configuration is not valid."))); + Info.bUseSuccessFailIcons = true; + Info.ExpireDuration = 3.0f; + + TSharedPtr NotificationItem = FSlateNotificationManager::Get().AddNotification(Info); + NotificationItem->SetCompletionState(SNotificationItem::CS_Fail); + + return FReply::Handled(); + } + + if (TSharedPtr SpatialGDKEditorSharedPtr = SpatialGDKEditorPtr.Pin()) { + FNotificationInfo Info(FText::FromString(TEXT("Starting simulated player deployment..."))); + Info.bUseSuccessFailIcons = true; + Info.bFireAndForget = false; + + TSharedPtr NotificationItem = FSlateNotificationManager::Get().AddNotification(Info); + + NotificationItem->SetCompletionState(SNotificationItem::CS_Pending); + + SpatialGDKEditorSharedPtr->LaunchCloudDeployment( + FSimpleDelegate::CreateLambda([NotificationItem]() { + NotificationItem->SetText(FText::FromString(TEXT("Successfully initiated launching of the cloud deployment."))); + NotificationItem->SetCompletionState(SNotificationItem::CS_Success); + NotificationItem->SetExpireDuration(7.5f); + NotificationItem->ExpireAndFadeout(); + }), + FSimpleDelegate::CreateLambda([NotificationItem]() { + NotificationItem->SetText(FText::FromString(TEXT("Failed to launch the DeploymentLauncher script properly."))); + NotificationItem->SetCompletionState(SNotificationItem::CS_Fail); + NotificationItem->SetExpireDuration(7.5f); + NotificationItem->ExpireAndFadeout(); + }) + ); + return FReply::Handled(); + } + + 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); + + 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()) { + FNotificationInfo Info(FText::FromString(TEXT("Stopping cloud deployment ..."))); + Info.bUseSuccessFailIcons = true; + Info.bFireAndForget = false; + + TSharedPtr NotificationItem = FSlateNotificationManager::Get().AddNotification(Info); + + NotificationItem->SetCompletionState(SNotificationItem::CS_Pending); + + SpatialGDKEditorSharedPtr->StopCloudDeployment( + FSimpleDelegate::CreateLambda([NotificationItem]() { + NotificationItem->SetText(FText::FromString(TEXT("Successfully launched the stop cloud deployments command."))); + NotificationItem->SetCompletionState(SNotificationItem::CS_Success); + }), + FSimpleDelegate::CreateLambda([NotificationItem]() { + NotificationItem->SetText(FText::FromString(TEXT("Failed to launch the DeploymentLauncher script properly."))); + NotificationItem->SetCompletionState(SNotificationItem::CS_Fail); + }) + ); + } + return FReply::Handled(); +} + +void SSpatialGDKSimulatedPlayerDeployment::OnCloudDocumentationClicked() +{ + FString WebError; + FPlatformProcess::LaunchURL(TEXT("https://docs.improbable.io/unreal/latest/content/cloud-deployment-workflow#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; +} diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h index 0986bafacf..b9732a7690 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbar.h @@ -3,6 +3,7 @@ #include "Async/Future.h" #include "CoreMinimal.h" +#include "LocalDeploymentManager.h" #include "Modules/ModuleManager.h" #include "Serialization/JsonWriter.h" #include "Templates/SharedPointer.h" @@ -10,11 +11,13 @@ #include "UObject/UnrealType.h" #include "Widgets/Notifications/SNotificationList.h" -class FToolBarBuilder; class FMenuBuilder; +class FSpatialGDKEditor; +class FToolBarBuilder; class FUICommandList; +class SSpatialGDKSimulatedPlayerDeployment; +class SWindow; class USoundBase; -class FSpatialGDKEditor; struct FWorkerTypeLaunchSection; @@ -41,18 +44,31 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable RETURN_QUICK_DECLARE_CYCLE_STAT(FSpatialGDKEditorToolbarModule, STATGROUP_Tickables); } - FSimpleMulticastDelegate OnSpatialShutdown; - private: void MapActions(TSharedPtr PluginCommands); void SetupToolbar(TSharedPtr PluginCommands); void AddToolbarExtension(FToolBarBuilder& Builder); void AddMenuExtension(FMenuBuilder& Builder); - void StartSpatialOSButtonClicked(); - void StopSpatialOSButtonClicked(); - bool StartSpatialOSStackCanExecute() const; - bool StopSpatialOSStackCanExecute() const; + void VerifyAndStartDeployment(); + + void StartSpatialDeploymentButtonClicked(); + void StopSpatialDeploymentButtonClicked(); + + void StartSpatialServiceButtonClicked(); + void StopSpatialServiceButtonClicked(); + + bool StartSpatialDeploymentIsVisible() const; + bool StartSpatialDeploymentCanExecute() const; + + bool StopSpatialDeploymentIsVisible() const; + bool StopSpatialDeploymentCanExecute() const; + + bool StartSpatialServiceIsVisible() const; + bool StartSpatialServiceCanExecute() const; + + bool StopSpatialServiceIsVisible() const; + bool StopSpatialServiceCanExecute() const; void LaunchInspectorWebpageButtonClicked(); void CreateSnapshotButtonClicked(); @@ -60,34 +76,38 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable void SchemaGenerateFullButtonClicked(); void OnPropertyChanged(UObject* ObjectBeingModified, FPropertyChangedEvent& PropertyChangedEvent); + void ShowSimulatedPlayerDeploymentDialog(); + private: bool CanExecuteSchemaGenerator() const; bool CanExecuteSnapshotGenerator() const; - void StopRunningStack(); - void CheckForRunningStack(); - void CleanupSpatialProcess(); TSharedRef CreateGenerateSchemaMenuContent(); + void OnShowTaskStartNotification(const FString& NotificationText); void ShowTaskStartNotification(const FString& NotificationText); + + void OnShowSuccessNotification(const FString& NotificationText); void ShowSuccessNotification(const FString& NotificationText); + + void OnShowFailedNotification(const FString& NotificationText); void ShowFailedNotification(const FString& NotificationText); bool ValidateGeneratedLaunchConfig() const; bool GenerateDefaultLaunchConfig(const FString& LaunchConfigPath) const; + bool GenerateDefaultWorkerJson(); + + void GenerateSchema(bool bFullScan); bool WriteFlagSection(TSharedRef< TJsonWriter<> > Writer, const FString& Key, const FString& Value) const; bool WriteWorkerSection(TSharedRef< TJsonWriter<> > Writer, const FWorkerTypeLaunchSection& FWorkerTypeLaunchSection) const; - bool WriteLoadbalancingSection(TSharedRef< TJsonWriter<> > Writer, const FString& WorkerType, const int32 Columns, const int32 Rows, const bool bManualWorkerConnectionOnly) const; + bool WriteLoadbalancingSection(TSharedRef< TJsonWriter<> > Writer, const FName& WorkerType, const int32 Columns, const int32 Rows, const bool bManualWorkerConnectionOnly) const; static void ShowCompileLog(); TSharedPtr PluginCommands; FDelegateHandle OnPropertyChangedDelegateHandle; - FProcHandle SpatialOSStackProcHandle; bool bStopSpatialOnExit; - - uint32 SpatialOSStackProcessID; TWeakPtr TaskNotificationPtr; @@ -98,4 +118,9 @@ class FSpatialGDKEditorToolbarModule : public IModuleInterface, public FTickable TFuture SchemaGeneratorResult; TSharedPtr SpatialGDKEditorInstance; + + TSharedPtr SimulatedPlayerDeploymentWindowPtr; + TSharedPtr SimulatedPlayerDeploymentConfigPtr; + + FLocalDeploymentManager* LocalDeploymentManager; }; diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h index 2cdd588c7b..a2b6e4b597 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKEditorToolbarCommands.h @@ -23,7 +23,12 @@ class FSpatialGDKEditorToolbarCommands : public TCommands CreateSpatialGDKSchema; TSharedPtr CreateSpatialGDKSchemaFull; TSharedPtr CreateSpatialGDKSnapshot; - TSharedPtr StartSpatialOSStackAction; - TSharedPtr StopSpatialOSStackAction; + TSharedPtr StartSpatialDeployment; + TSharedPtr StopSpatialDeployment; TSharedPtr LaunchInspectorWebPageAction; + + TSharedPtr OpenSimulatedPlayerConfigurationWindowAction; + + TSharedPtr StartSpatialService; + TSharedPtr StopSpatialService; }; diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKSimulatedPlayerDeployment.h b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKSimulatedPlayerDeployment.h new file mode 100644 index 0000000000..2f7cad9f10 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/Public/SpatialGDKSimulatedPlayerDeployment.h @@ -0,0 +1,95 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "CoreMinimal.h" +#include "Input/Reply.h" +#include "Layout/Visibility.h" +#include "SpatialGDKEditor.h" +#include "SpatialGDKEditorSettings.h" +#include "Templates/SharedPointer.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/Layout/SBorder.h" +#include "Widgets/SCompoundWidget.h" + +class SWindow; + +enum class ECheckBoxState : uint8; + +class SSpatialGDKSimulatedPlayerDeployment : public SCompoundWidget +{ +public: + + SLATE_BEGIN_ARGS(SSpatialGDKSimulatedPlayerDeployment) {} + + /** A reference to the parent window */ + SLATE_ARGUMENT(TSharedPtr, ParentWindow) + SLATE_ARGUMENT(TSharedPtr, SpatialGDKEditor) + + SLATE_END_ARGS() + +public: + + void Construct(const FArguments& InArgs); + +private: + /** The parent window of this widget */ + TWeakPtr ParentWindowPtr; + + /** Pointer to the SpatialGDK editor */ + TWeakPtr SpatialGDKEditorPtr; + + /** Delegate to commit assembly name */ + void OnDeploymentAssemblyCommited(const FText& InText, ETextCommit::Type InCommitType); + + /** Delegate to commit project name */ + void OnProjectNameCommited(const FText& InText, ETextCommit::Type InCommitType); + + /** Delegate to commit primary deployment name */ + void OnPrimaryDeploymentNameCommited(const FText& InText, ETextCommit::Type InCommitType); + + /** Delegate called when the user has picked a path for the snapshot file */ + void OnSnapshotPathPicked(const FString& PickedPath); + + /** Delegate called when the user has picked a path for the primary launch configuration file */ + void OnPrimaryLaunchConfigPathPicked(const FString& PickedPath); + + /** Delegate called to populate the region codes for the primary deployment */ + TSharedRef OnGetPrimaryDeploymentRegionCode(); + + /** Delegate called to populate the region codes for the simulated player deployment */ + TSharedRef OnGetSimulatedPlayerDeploymentRegionCode(); + + /** Delegate called when the user selects a region code from the dropdown for the primary deployment */ + void OnPrimaryDeploymentRegionCodePicked(const int64 RegionCodeEnumValue); + + /** Delegate called when the user selects a region code from the dropdown for the simulated player deployment */ + void OnSimulatedPlayerDeploymentRegionCodePicked(const int64 RegionCodeEnumValue); + + /** Delegate to commit simulated player deployment name */ + void OnSimulatedPlayerDeploymentNameCommited(const FText& InText, ETextCommit::Type InCommitType); + + /** 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(); + + /** Delegate called when the user clicks the 'Stop Deployment' button */ + FReply OnStopClicked(); + + /** Delegate called when the user clicks the cloud deployment documentation */ + void OnCloudDocumentationClicked(); + + /** Delegate called when the user either clicks the simulated players checkbox */ + void OnCheckedSimulatedPlayers(ECheckBoxState NewCheckedState); + + ECheckBoxState IsSimulatedPlayersEnabled() const; + + /** Delegate to determine the 'Launch Deployment' button enabled state */ + bool IsDeploymentConfigurationValid() const; +}; diff --git a/SpatialGDK/Source/SpatialGDKEditorToolbar/SpatialGDKEditorToolbar.Build.cs b/SpatialGDK/Source/SpatialGDKEditorToolbar/SpatialGDKEditorToolbar.Build.cs index 52f62c83a8..b243f29def 100644 --- a/SpatialGDK/Source/SpatialGDKEditorToolbar/SpatialGDKEditorToolbar.Build.cs +++ b/SpatialGDK/Source/SpatialGDKEditorToolbar/SpatialGDKEditorToolbar.Build.cs @@ -16,6 +16,8 @@ public SpatialGDKEditorToolbar(ReadOnlyTargetRules Target) : base(Target) { "Core", "CoreUObject", + "DesktopPlatform", + "DesktopWidgets", "Engine", "EngineSettings", "InputCore", @@ -27,6 +29,7 @@ public SpatialGDKEditorToolbar(ReadOnlyTargetRules Target) : base(Target) "MessageLog", "SpatialGDK", "SpatialGDKEditor", + "SpatialGDKServices", "UnrealEd" } ); diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/Interop/Connection/EditorWorkerController.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/Interop/Connection/EditorWorkerController.cpp new file mode 100644 index 0000000000..a0ae695e2d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKServices/Private/Interop/Connection/EditorWorkerController.cpp @@ -0,0 +1,133 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +#include "Interop/Connection/EditorWorkerController.h" + +#include "SpatialGDKServicesPrivate.h" + +#include "Editor.h" + +#if WITH_EDITOR +namespace +{ +struct EditorWorkerController +{ + // Only issue the worker replace request if there's a chance the load balancer hasn't acknowledged + // that the previous session's workers have disconnected. There's no hard `heartbeat` time for this as + // it's dependent on multiple factors (fabric load etc.), so this value was landed on after significant + // trial and error. + const int64 WorkerReplaceThresholdSeconds = 8; + + const FString ServicePort = TEXT("9876"); + + void OnPrePIEEnded(bool bValue) + { + LastPIEEndTime = FDateTime::Now().ToUnixTimestamp(); + FEditorDelegates::PrePIEEnded.Remove(PIEEndHandle); + bHasInitialized = false; + } + + void OnSpatialShutdown() + { + LastPIEEndTime = 0; // Reset PIE end time to ensure replace-a-worker isn't called + FEditorDelegates::PrePIEEnded.Remove(PIEEndHandle); + } + + void InitWorkers() + { + int64 SecondsSinceLastSession = FDateTime::Now().ToUnixTimestamp() - LastPIEEndTime; + UE_LOG(LogSpatialGDKServices, Verbose, TEXT("Seconds since last session - %d"), SecondsSinceLastSession); + + PIEEndHandle = FEditorDelegates::PrePIEEnded.AddRaw(this, &EditorWorkerController::OnPrePIEEnded); + + const ULevelEditorPlaySettings* LevelEditorPlaySettings = GetDefault(); + const int32 WorkerCount = LevelEditorPlaySettings->GetTotalPIEServerWorkerCount(); + WorkerIds.SetNum(WorkerCount); + ReplaceProcesses.SetNum(WorkerCount); + + int32 WorkerIdIndex = 0; + for (const TPair& WorkerType : LevelEditorPlaySettings->GetPIEServerWorkers()) + { + for (int i = 0; i < WorkerType.Value; ++i) + { + const FString NewWorkerId = WorkerType.Key.ToString() + FGuid::NewGuid().ToString(); + + if (!WorkerIds[WorkerIdIndex].IsEmpty() && SecondsSinceLastSession < WorkerReplaceThresholdSeconds) + { + ReplaceProcesses.Add(ReplaceWorker(WorkerIds[WorkerIdIndex], NewWorkerId)); + } + + WorkerIds[WorkerIdIndex] = NewWorkerId; + WorkerIdIndex++; + } + } + + bHasInitialized = true; + } + + FProcHandle ReplaceWorker(const FString& OldWorker, const FString& NewWorker) + { + const FString CmdExecutable = TEXT("spatial.exe"); + + const FString CmdArgs = FString::Printf( + TEXT("local worker replace " + "--local_service_grpc_port %s " + "--existing_worker_id %s " + "--replacing_worker_id %s"), *ServicePort, *OldWorker, *NewWorker); + uint32 ProcessID = 0; + FProcHandle ProcHandle = FPlatformProcess::CreateProc( + *(CmdExecutable), *CmdArgs, false, true, true, &ProcessID, 2 /*PriorityModifier*/, + nullptr, nullptr, nullptr); + + return ProcHandle; + } + + void BlockUntilWorkerReady(int32 WorkerIdx) + { + if (WorkerIdx < ReplaceProcesses.Num()) + { + while (FPlatformProcess::IsProcRunning(ReplaceProcesses[WorkerIdx])) + { + // Only block until the worker connection will have timed out. + if ((FDateTime::Now().ToUnixTimestamp() - LastPIEEndTime) < WorkerReplaceThresholdSeconds) + { + FPlatformProcess::Sleep(0.1f); + } + } + } + } + + TArray WorkerIds; + TArray ReplaceProcesses; + int64 LastPIEEndTime = 0; // Unix epoch time in seconds + FDelegateHandle PIEEndHandle; + FDelegateHandle SpatialShutdownHandle; + bool bHasInitialized = false; +}; + +static EditorWorkerController WorkerController; +} // end namespace + +namespace SpatialGDKServices +{ +void InitWorkers(bool bConnectAsClient, int32 PlayInEditorID, FString& OutWorkerId) +{ + const bool bSingleThreadedServer = !bConnectAsClient && (PlayInEditorID > 0); + const int32 FirstServerEditorID = 1; + if (bSingleThreadedServer) + { + if (!WorkerController.bHasInitialized) + { + WorkerController.InitWorkers(); + } + + WorkerController.BlockUntilWorkerReady(PlayInEditorID - 1); + OutWorkerId = WorkerController.WorkerIds[PlayInEditorID - 1]; + } +} + +void OnSpatialShutdown() +{ + WorkerController.OnSpatialShutdown(); +} + +} // namespace SpatialGDKServices +#endif // WITH_EDITOR diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp new file mode 100644 index 0000000000..52b4d642f9 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKServices/Private/LocalDeploymentManager.cpp @@ -0,0 +1,611 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "LocalDeploymentManager.h" + +#include "AssetRegistryModule.h" +#include "Async/Async.h" +#include "DirectoryWatcherModule.h" +#include "Editor.h" +#include "FileCache.h" +#include "GeneralProjectSettings.h" +#include "HAL/FileManager.h" +#include "HAL/PlatformFilemanager.h" +#include "Interop/Connection/EditorWorkerController.h" +#include "Misc/FileHelper.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonSerializer.h" +#include "Serialization/JsonWriter.h" +#include "SpatialGDKServicesModule.h" + +DEFINE_LOG_CATEGORY(LogSpatialDeploymentManager); + +static const FString SpatialExe(TEXT("spatial.exe")); +static const FString SpatialServiceVersion(TEXT("20190716.094149.1b6d448edd")); + +FLocalDeploymentManager::FLocalDeploymentManager() + : bLocalDeploymentRunning(false) + , bSpatialServiceRunning(false) + , bSpatialServiceInProjectDirectory(false) + , bStartingDeployment(false) + , bStoppingDeployment(false) + , bStartingSpatialService(false) + , bStoppingSpatialService(false) +{ + // Get the project name from the spatialos.json. + ProjectName = GetProjectName(); + +#if PLATFORM_WINDOWS + // Don't kick off background processes when running commandlets + if (IsRunningCommandlet() == false) + { + // Ensure the worker.jsons are up to date. + WorkerBuildConfigAsync(); + + // Watch the worker config directory for changes. + StartUpWorkerConfigDirectoryWatcher(); + + // Restart the spatial service so it is guaranteed to be running in the current project. + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] + { + TryStopSpatialService(); + TryStartSpatialService(); + + // Ensure we have an up to date state of the spatial service and local deployment. + RefreshServiceStatus(); + }); + } +#endif +} + +const FString FLocalDeploymentManager::GetSpotExe() +{ + return FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(TEXT("SpatialGDK/Binaries/ThirdParty/Improbable/Programs/spot.exe")); +} + +void FLocalDeploymentManager::StartUpWorkerConfigDirectoryWatcher() +{ + FDirectoryWatcherModule& DirectoryWatcherModule = FModuleManager::LoadModuleChecked(TEXT("DirectoryWatcher")); + if (IDirectoryWatcher* DirectoryWatcher = DirectoryWatcherModule.Get()) + { + // Watch the worker config directory for changes. + const FString SpatialDirectory = FSpatialGDKServicesModule::GetSpatialOSDirectory(); + FString WorkerConfigDirectory = FPaths::Combine(SpatialDirectory, TEXT("workers")); + + if (FPaths::DirectoryExists(WorkerConfigDirectory)) + { + WorkerConfigDirectoryChangedDelegate = IDirectoryWatcher::FDirectoryChanged::CreateRaw(this, &FLocalDeploymentManager::OnWorkerConfigDirectoryChanged); + DirectoryWatcher->RegisterDirectoryChangedCallback_Handle(WorkerConfigDirectory, WorkerConfigDirectoryChangedDelegate, WorkerConfigDirectoryChangedDelegateHandle); + } + else + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Worker config directory does not exist! Please ensure you have your worker configurations at %s"), *WorkerConfigDirectory); + } + } +} + +void FLocalDeploymentManager::OnWorkerConfigDirectoryChanged(const TArray& FileChanges) +{ + UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Worker config files updated. Regenerating worker descriptors ('spatial worker build build-config').")); + WorkerBuildConfigAsync(); +} + +FString FLocalDeploymentManager::GetProjectName() +{ + const FString SpatialDirectory = FSpatialGDKServicesModule::GetSpatialOSDirectory(); + + FString SpatialFileName = TEXT("spatialos.json"); + FString SpatialFileResult; + FFileHelper::LoadFileToString(SpatialFileResult, *FPaths::Combine(SpatialDirectory, SpatialFileName)); + + TSharedPtr JsonParsedSpatialFile; + if (ParseJson(SpatialFileResult, JsonParsedSpatialFile)) + { + if (JsonParsedSpatialFile->TryGetStringField(TEXT("name"), ProjectName)) + { + return ProjectName; + } + else + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("'name' does not exist in spatialos.json. Can't read project name.")); + } + } + else + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Json parsing of spatialos.json failed. Can't get project name.")); + } + + ProjectName.Empty(); + return ProjectName; +} + +void FLocalDeploymentManager::WorkerBuildConfigAsync() +{ + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] + { + FString BuildConfigArgs = TEXT("worker build build-config"); + FString WorkerBuildConfigResult; + int32 ExitCode; + ExecuteAndReadOutput(SpatialExe, BuildConfigArgs, FSpatialGDKServicesModule::GetSpatialOSDirectory(), WorkerBuildConfigResult, ExitCode); + + if (ExitCode == ExitCodeSuccess) + { + UE_LOG(LogSpatialDeploymentManager, Display, TEXT("Building worker configurations succeeded!")); + } + else + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Building worker configurations failed. Please ensure your .worker.json files are correct. Result: %s"), *WorkerBuildConfigResult); + } + }); +} + +bool FLocalDeploymentManager::ParseJson(const FString& RawJsonString, TSharedPtr& JsonParsed) +{ + TSharedRef> JsonReader = TJsonReaderFactory::Create(RawJsonString); + return FJsonSerializer::Deserialize(JsonReader, JsonParsed); +} + +// ExecuteAndReadOutput exists so that a spatial command window does not spawn when using 'spatial.exe'. It does not however allow reading from StdErr. +// For other processes which do not spawn cmd windows, use ExecProcess instead. +void FLocalDeploymentManager::ExecuteAndReadOutput(const FString& Executable, const FString& Arguments, const FString& DirectoryToRun, FString& OutResult, int32& ExitCode) +{ +#if PLATFORM_WINDOWS + UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Executing '%s' with arguments '%s' in directory '%s'"), *Executable, *Arguments, *DirectoryToRun); + + void* ReadPipe = nullptr; + void* WritePipe = nullptr; + ensure(FPlatformProcess::CreatePipe(ReadPipe, WritePipe)); + + FProcHandle ProcHandle = FPlatformProcess::CreateProc(*Executable, *Arguments, false, true, true, nullptr, 1 /*PriorityModifer*/, *DirectoryToRun, WritePipe); + + if (ProcHandle.IsValid()) + { + for (bool bProcessFinished = false; !bProcessFinished; ) + { + bProcessFinished = FPlatformProcess::GetProcReturnCode(ProcHandle, &ExitCode); + + OutResult = OutResult.Append(FPlatformProcess::ReadPipe(ReadPipe)); + FPlatformProcess::Sleep(0.01f); + } + + FPlatformProcess::CloseProc(ProcHandle); + } + else + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Execution failed. '%s' with arguments '%s' in directory '%s' "), *Executable, *Arguments, *DirectoryToRun); + } + + FPlatformProcess::ClosePipe(0, ReadPipe); + FPlatformProcess::ClosePipe(0, WritePipe); +#else + ExitCode = 1; +#endif +} + +void FLocalDeploymentManager::RefreshServiceStatus() +{ + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this] + { + GetServiceStatus(); + GetLocalDeploymentStatus(); + + // Timers must be started on the game thread. + AsyncTask(ENamedThreads::GameThread, [this] + { + // It's possible that GEditor won't exist when shutting down. + if (GEditor != nullptr) + { + // Start checking for the service status. + FTimerHandle RefreshTimer; + GEditor->GetTimerManager()->SetTimer(RefreshTimer, [this]() + { + RefreshServiceStatus(); + }, RefreshFrequency, false); + } + }); + }); +} + +bool FLocalDeploymentManager::TryStartLocalDeployment(FString LaunchConfig, FString LaunchArgs) +{ + bRedeployRequired = false; + + if (bStoppingDeployment) + { + UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Local deployment is in the process of stopping. New deployment will start when previous one has stopped.")); + while (bStoppingDeployment) + { + FPlatformProcess::Sleep(0.1f); + } + } + + if (bLocalDeploymentRunning) + { + UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Tried to start a local deployment but one is already running.")); + return false; + } + + LocalRunningDeploymentID.Empty(); + + bStartingDeployment = true; + + // If the service is not running then start it. + if (!bSpatialServiceRunning) + { + TryStartSpatialService(); + } + + FString SpotCreateArgs = FString::Printf(TEXT("alpha deployment create --launch-config=\"%s\" --name=localdeployment --project-name=%s --json %s"), *LaunchConfig, *ProjectName, *LaunchArgs); + + FDateTime SpotCreateStart = FDateTime::Now(); + + FString SpotCreateResult; + FString StdErr; + int32 ExitCode; + FPlatformProcess::ExecProcess(*GetSpotExe(), *SpotCreateArgs, &ExitCode, &SpotCreateResult, &StdErr); + bStartingDeployment = false; + + if (ExitCode != ExitCodeSuccess) + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Creation of local deployment failed. Result: %s - Error: %s"), *SpotCreateResult, *StdErr); + return false; + } + + bool bSuccess = false; + + TSharedPtr SpotJsonResult; + bool bParsingSuccess = ParseJson(SpotCreateResult, SpotJsonResult); + if (!bParsingSuccess) + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Json parsing of spot create result failed. Result: %s"), *SpotCreateResult); + } + + const TSharedPtr* SpotJsonContent = nullptr; + if (bParsingSuccess && !SpotJsonResult->TryGetObjectField(TEXT("content"), SpotJsonContent)) + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("'content' does not exist in Json result from 'spot create': %s"), *SpotCreateResult); + bParsingSuccess = false; + } + + FString DeploymentStatus; + if (bParsingSuccess && SpotJsonContent->Get()->TryGetStringField(TEXT("status"), DeploymentStatus)) + { + if (DeploymentStatus == TEXT("RUNNING")) + { + FString DeploymentID = SpotJsonContent->Get()->GetStringField(TEXT("id")); + LocalRunningDeploymentID = DeploymentID; + bLocalDeploymentRunning = true; + + FDateTime SpotCreateEnd = FDateTime::Now(); + FTimespan Span = SpotCreateEnd - SpotCreateStart; + + OnDeploymentStart.Broadcast(); + + UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Successfully created local deployment in %f seconds."), Span.GetTotalSeconds()); + bSuccess = true; + } + else + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Local deployment creation failed. Deployment status: %s"), *DeploymentStatus); + } + } + else + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("'status' does not exist in Json result from 'spot create': %s"), *SpotCreateResult); + } + + return bSuccess; +} + +bool FLocalDeploymentManager::TryStopLocalDeployment() +{ + if (!bLocalDeploymentRunning || LocalRunningDeploymentID.IsEmpty()) + { + UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Tried to stop local deployment but no active deployment exists.")); + return false; + } + + bStoppingDeployment = true; + + FString SpotDeleteArgs = FString::Printf(TEXT("alpha deployment delete --id=%s --json"), *LocalRunningDeploymentID); + + FString SpotDeleteResult; + FString StdErr; + int32 ExitCode; + FPlatformProcess::ExecProcess(*GetSpotExe(), *SpotDeleteArgs, &ExitCode, &SpotDeleteResult, &StdErr); + bStoppingDeployment = false; + + if (ExitCode != ExitCodeSuccess) + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Failed to stop local deployment! Result: %s - Error: %s"), *SpotDeleteResult, *StdErr); + } + + bool bSuccess = false; + + TSharedPtr SpotJsonResult; + bool bPasingSuccess = ParseJson(SpotDeleteResult, SpotJsonResult); + if (!bPasingSuccess) + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Json parsing of spot delete result failed. Result: %s"), *SpotDeleteResult); + } + + const TSharedPtr* SpotJsonContent = nullptr; + if (bPasingSuccess && !SpotJsonResult->TryGetObjectField(TEXT("content"), SpotJsonContent)) + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("'content' does not exist in Json result from 'spot delete': %s"), *SpotDeleteResult); + bPasingSuccess = false; + } + + FString DeploymentStatus; + if (bPasingSuccess && SpotJsonContent->Get()->TryGetStringField(TEXT("status"), DeploymentStatus)) + { + if (DeploymentStatus == TEXT("STOPPED")) + { + UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Successfully stopped local deplyoment")); + LocalRunningDeploymentID.Empty(); + bLocalDeploymentRunning = false; + bSuccess = true; + } + else + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Stopping local deployment failed. Deployment status: %s"), *DeploymentStatus); + } + } + else + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("'status' does not exist in Json result from 'spot delete': %s"), *SpotDeleteResult); + } + + return bSuccess; +} + +bool FLocalDeploymentManager::TryStartSpatialService() +{ + if (bSpatialServiceRunning) + { + UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Tried to start spatial service but it is already running.")); + return false; + } + + bStartingSpatialService = true; + + FString SpatialServiceStartArgs = FString::Printf(TEXT("service start --version=%s"), *SpatialServiceVersion); + FString ServiceStartResult; + int32 ExitCode; + ExecuteAndReadOutput(SpatialExe, SpatialServiceStartArgs, FSpatialGDKServicesModule::GetSpatialOSDirectory(), ServiceStartResult, ExitCode); + + bStartingSpatialService = false; + + if (ExitCode != ExitCodeSuccess) + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Spatial service failed to start! %s"), *ServiceStartResult); + return false; + } + + if (ServiceStartResult.Contains(TEXT("RUNNING"))) + { + UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Spatial service started!")); + bSpatialServiceRunning = true; + return true; + } + else + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Spatial service failed to start! %s"), *ServiceStartResult); + bSpatialServiceRunning = false; + bLocalDeploymentRunning = false; + return false; + } +} + +bool FLocalDeploymentManager::TryStopSpatialService() +{ + bStoppingSpatialService = true; + + FString SpatialServiceStartArgs = TEXT("service stop"); + FString ServiceStopResult; + int32 ExitCode; + ExecuteAndReadOutput(SpatialExe, SpatialServiceStartArgs, FSpatialGDKServicesModule::GetSpatialOSDirectory(), ServiceStopResult, ExitCode); + bStoppingSpatialService = false; + + if (ExitCode == ExitCodeSuccess) + { + UE_LOG(LogSpatialDeploymentManager, Log, TEXT("Spatial service stopped!")); + bSpatialServiceRunning = false; + bSpatialServiceInProjectDirectory = true; + bLocalDeploymentRunning = false; + return true; + } + else + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Spatial service failed to stop! %s"), *ServiceStopResult); + } + + return false; +} + +bool FLocalDeploymentManager::GetLocalDeploymentStatus() +{ + if (!bSpatialServiceRunning) + { + bLocalDeploymentRunning = false; + return bLocalDeploymentRunning; + } + + FString SpotListArgs = FString::Printf(TEXT("alpha deployment list --project-name=%s --json --view BASIC --status-filter NOT_STOPPED_DEPLOYMENTS"), *ProjectName); + + FString SpotListResult; + FString StdErr; + int32 ExitCode; + FPlatformProcess::ExecProcess(*GetSpotExe(), *SpotListArgs, &ExitCode, &SpotListResult, &StdErr); + + if (ExitCode != ExitCodeSuccess) + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Failed to check local deployment status. Result: %s - Error: %s"), *SpotListResult, *StdErr); + return false; + } + + TSharedPtr SpotJsonResult; + bool bPasingSuccess = ParseJson(SpotListResult, SpotJsonResult); + if (!bPasingSuccess) + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Json parsing of spot list result failed. Result: %s"), *SpotListResult); + } + + const TSharedPtr* SpotJsonContent = nullptr; + if (bPasingSuccess && SpotJsonResult->TryGetObjectField(TEXT("content"), SpotJsonContent)) + { + const TArray>* JsonDeployments; + if (!SpotJsonContent->Get()->TryGetArrayField(TEXT("deployments"), JsonDeployments)) + { + UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("No local deployments running.")); + return false; + } + + for (TSharedPtr JsonDeployment : *JsonDeployments) + { + FString DeploymentStatus; + if (JsonDeployment->AsObject()->TryGetStringField(TEXT("status"), DeploymentStatus)) + { + if (DeploymentStatus == TEXT("RUNNING")) + { + FString DeploymentId = JsonDeployment->AsObject()->GetStringField(TEXT("id")); + + UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Running deployment found: %s"), *DeploymentId); + + LocalRunningDeploymentID = DeploymentId; + bLocalDeploymentRunning = true; + return true; + } + } + } + } + else + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Json parsing of spot list result failed. Can't check deployment status.")); + } + + LocalRunningDeploymentID.Empty(); + bLocalDeploymentRunning = false; + return false; +} + +bool FLocalDeploymentManager::GetServiceStatus() +{ + FString SpatialServiceStatusArgs = TEXT("service status"); + FString ServiceStatusResult; + int32 ExitCode; + ExecuteAndReadOutput(SpatialExe, SpatialServiceStatusArgs, FSpatialGDKServicesModule::GetSpatialOSDirectory(), ServiceStatusResult, ExitCode); + + if (ExitCode != ExitCodeSuccess) + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Failed to check spatial service status: %s"), *ServiceStatusResult); + } + + if (ServiceStatusResult.Contains(TEXT("Local API service is not running."))) + { + UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Spatial service not running.")); + bSpatialServiceInProjectDirectory = true; + bSpatialServiceRunning = false; + bLocalDeploymentRunning = false; + return false; + } + else if (ServiceStatusResult.Contains(TEXT("Local API service is running"))) + { + bSpatialServiceInProjectDirectory = IsServiceInCorrectDirectory(ServiceStatusResult); + bSpatialServiceRunning = true; + return true; + } + + return false; +} + +bool FLocalDeploymentManager::IsServiceInCorrectDirectory(const FString& ServiceStatusResult) +{ + // Get the project file path and ensure it matches the one for the currently running project. + FString SpatialServiceProjectPath; + if (ServiceStatusResult.Split(TEXT("project file path: "), nullptr, &SpatialServiceProjectPath)) + { + // Remove the trailing '" cli-version' that comes with non-interactive 'spatial' calls. + SpatialServiceProjectPath.Split(TEXT("\" cli-version"), &SpatialServiceProjectPath, nullptr); + + FString CurrentProjectSpatialPath = FPaths::Combine(FSpatialGDKServicesModule::GetSpatialOSDirectory(), TEXT("spatialos.json")); + FPaths::NormalizeDirectoryName(SpatialServiceProjectPath); + FPaths::RemoveDuplicateSlashes(SpatialServiceProjectPath); + + UE_LOG(LogSpatialDeploymentManager, Verbose, TEXT("Spatial service running at path: %s "), *SpatialServiceProjectPath); + + if (CurrentProjectSpatialPath.Equals(SpatialServiceProjectPath, ESearchCase::IgnoreCase)) + { + return true; + } + else if (SpatialServiceProjectPath.Contains(TEXT("not available"))) + { + UE_LOG(LogSpatialDeploymentManager, Error, TEXT("Spatial service has hit an erroneous state! Please run 'spatial service stop'.")); + return false; + } + 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); + return false; + } + } + + return false; +} + +bool FLocalDeploymentManager::IsLocalDeploymentRunning() const +{ + return bLocalDeploymentRunning; +} + +bool FLocalDeploymentManager::IsSpatialServiceRunning() const +{ + return bSpatialServiceRunning; +} + +bool FLocalDeploymentManager::IsDeploymentStarting() const +{ + return bStartingDeployment; +} + +bool FLocalDeploymentManager::IsDeploymentStopping() const +{ + return bStoppingDeployment; +} + +bool FLocalDeploymentManager::IsServiceStarting() const +{ + return bStartingSpatialService; +} + +bool FLocalDeploymentManager::IsServiceStopping() const +{ + return bStoppingSpatialService; +} + +bool FLocalDeploymentManager::IsRedeployRequired() const +{ + return bRedeployRequired; +} + +void FLocalDeploymentManager::SetRedeployRequired() +{ + bRedeployRequired = true; +} + +bool FLocalDeploymentManager::ShouldWaitForDeployment() const +{ + if (bAutoDeploy) + { + return !IsLocalDeploymentRunning() || IsDeploymentStopping() || IsDeploymentStarting(); + } + else + { + return false; + } +} + +void FLocalDeploymentManager::SetAutoDeploy(bool bInAutoDeploy) +{ + bAutoDeploy = bInAutoDeploy; +} diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/SpatialGDKServicesModule.cpp b/SpatialGDK/Source/SpatialGDKServices/Private/SpatialGDKServicesModule.cpp new file mode 100644 index 0000000000..c1478c869e --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKServices/Private/SpatialGDKServicesModule.cpp @@ -0,0 +1,45 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#include "SpatialGDKServicesModule.h" + +#include "SpatialGDKServicesPrivate.h" + +#define LOCTEXT_NAMESPACE "FSpatialGDKServicesModule" + +DEFINE_LOG_CATEGORY(LogSpatialGDKServices); + +IMPLEMENT_MODULE(FSpatialGDKServicesModule, SpatialGDKServices); + +void FSpatialGDKServicesModule::StartupModule() +{ +} + +void FSpatialGDKServicesModule::ShutdownModule() +{ +} + +FLocalDeploymentManager* FSpatialGDKServicesModule::GetLocalDeploymentManager() +{ + return &LocalDeploymentManager; +} + +FString FSpatialGDKServicesModule::GetSpatialOSDirectory(const FString& AppendPath) +{ + return FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectDir(), TEXT("/../spatial/"), AppendPath)); +} + +FString FSpatialGDKServicesModule::GetSpatialGDKPluginDirectory(const FString& AppendPath) +{ + FString PluginDir = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectPluginsDir(), TEXT("UnrealGDK"))); + + if (!FPaths::DirectoryExists(PluginDir)) + { + // If the Project Plugin doesn't exist then use the Engine Plugin. + PluginDir = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::EnginePluginsDir(), TEXT("UnrealGDK"))); + ensure(FPaths::DirectoryExists(PluginDir)); + } + + return FPaths::ConvertRelativePathToFull(FPaths::Combine(PluginDir, AppendPath)); +} + +#undef LOCTEXT_NAMESPACE diff --git a/SpatialGDK/Source/SpatialGDKServices/Private/SpatialGDKServicesPrivate.h b/SpatialGDK/Source/SpatialGDKServices/Private/SpatialGDKServicesPrivate.h new file mode 100644 index 0000000000..1f1cae403f --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKServices/Private/SpatialGDKServicesPrivate.h @@ -0,0 +1,7 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +#pragma once + +#include "Logging/LogMacros.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialGDKServices, Log, All); diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/Interop/Connection/EditorWorkerController.h b/SpatialGDK/Source/SpatialGDKServices/Public/Interop/Connection/EditorWorkerController.h new file mode 100644 index 0000000000..a21b84cc77 --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKServices/Public/Interop/Connection/EditorWorkerController.h @@ -0,0 +1,14 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +#pragma once + +#if WITH_EDITOR + +#include "CoreMinimal.h" + +namespace SpatialGDKServices +{ +void SPATIALGDKSERVICES_API InitWorkers(bool bConnectAsClient, int32 PlayInEditorID, FString& OutWorkerId); +void SPATIALGDKSERVICES_API OnSpatialShutdown(); +} // namespace SpatialGDKServices + +#endif // WITH_EDITOR diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/LocalDeploymentManager.h b/SpatialGDK/Source/SpatialGDKServices/Public/LocalDeploymentManager.h new file mode 100644 index 0000000000..936838b2da --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKServices/Public/LocalDeploymentManager.h @@ -0,0 +1,86 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +#pragma once + +#include "Async/Future.h" +#include "CoreMinimal.h" +#include "FileCache.h" +#include "Modules/ModuleManager.h" +#include "Templates/SharedPointer.h" +#include "TimerManager.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogSpatialDeploymentManager, Log, All); + +class FJsonObject; + +class FLocalDeploymentManager +{ +public: + FLocalDeploymentManager(); + + void SPATIALGDKSERVICES_API RefreshServiceStatus(); + + bool SPATIALGDKSERVICES_API TryStartLocalDeployment(FString LaunchConfig, FString LaunchArgs); + bool SPATIALGDKSERVICES_API TryStopLocalDeployment(); + + bool SPATIALGDKSERVICES_API TryStartSpatialService(); + bool SPATIALGDKSERVICES_API TryStopSpatialService(); + + bool SPATIALGDKSERVICES_API GetLocalDeploymentStatus(); + bool SPATIALGDKSERVICES_API GetServiceStatus(); + + bool SPATIALGDKSERVICES_API IsLocalDeploymentRunning() const; + bool SPATIALGDKSERVICES_API IsSpatialServiceRunning() const; + + bool SPATIALGDKSERVICES_API IsDeploymentStarting() const; + bool SPATIALGDKSERVICES_API IsDeploymentStopping() const; + + bool SPATIALGDKSERVICES_API IsServiceStarting() const; + bool SPATIALGDKSERVICES_API IsServiceStopping() const; + + bool SPATIALGDKSERVICES_API IsRedeployRequired() const; + void SPATIALGDKSERVICES_API SetRedeployRequired(); + + // Helper function to inform a client or server whether it should wait for a local deployment to become active. + bool SPATIALGDKSERVICES_API ShouldWaitForDeployment() const; + + void SPATIALGDKSERVICES_API SetAutoDeploy(bool bAutoDeploy); + + // TODO: Refactor these into Utils + FString GetProjectName(); + void WorkerBuildConfigAsync(); + bool ParseJson(const FString& RawJsonString, TSharedPtr& JsonParsed); + void ExecuteAndReadOutput(const FString& Executable, const FString& Arguments, const FString& DirectoryToRun, FString& OutResult, int32& ExitCode); + const FString GetSpotExe(); + + FSimpleMulticastDelegate OnSpatialShutdown; + FSimpleMulticastDelegate OnDeploymentStart; + + FDelegateHandle WorkerConfigDirectoryChangedDelegateHandle; + IDirectoryWatcher::FDirectoryChanged WorkerConfigDirectoryChangedDelegate; + +private: + void StartUpWorkerConfigDirectoryWatcher(); + void OnWorkerConfigDirectoryChanged(const TArray& FileChanges); + bool IsServiceInCorrectDirectory(const FString& ServiceStatusResult); + + static const int32 ExitCodeSuccess = 0; + + // This is the frequency at which check the 'spatial service status' to ensure we have the correct state as the user can change spatial service outside of the editor. + static const int32 RefreshFrequency = 3; + + bool bLocalDeploymentRunning; + bool bSpatialServiceRunning; + bool bSpatialServiceInProjectDirectory; + + bool bStartingDeployment; + bool bStoppingDeployment; + + bool bStartingSpatialService; + bool bStoppingSpatialService; + + FString LocalRunningDeploymentID; + FString ProjectName; + + bool bRedeployRequired = false; + bool bAutoDeploy = false; +}; diff --git a/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesModule.h b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesModule.h new file mode 100644 index 0000000000..bc35e103fe --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKServices/Public/SpatialGDKServicesModule.h @@ -0,0 +1,26 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved +#pragma once + +#include "LocalDeploymentManager.h" +#include "Modules/ModuleInterface.h" +#include "Modules/ModuleManager.h" + +class SPATIALGDKSERVICES_API FSpatialGDKServicesModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + virtual bool SupportsDynamicReloading() override + { + return true; + } + + FLocalDeploymentManager* GetLocalDeploymentManager(); + + static FString GetSpatialOSDirectory(const FString& AppendPath = TEXT("")); + static FString GetSpatialGDKPluginDirectory(const FString& AppendPath = TEXT("")); + +private: + FLocalDeploymentManager LocalDeploymentManager; +}; diff --git a/SpatialGDK/Source/SpatialGDKServices/SpatialGDKServices.Build.cs b/SpatialGDK/Source/SpatialGDKServices/SpatialGDKServices.Build.cs new file mode 100644 index 0000000000..e97ffa405d --- /dev/null +++ b/SpatialGDK/Source/SpatialGDKServices/SpatialGDKServices.Build.cs @@ -0,0 +1,23 @@ +// Copyright (c) Improbable Worlds Ltd, All Rights Reserved + +using UnrealBuildTool; + +public class SpatialGDKServices : ModuleRules +{ + public SpatialGDKServices(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + bFasterWithoutUnity = true; + + PrivateDependencyModuleNames.AddRange( + new string[] { + "Core", + "CoreUObject", + "Engine", + "EngineSettings", + "UnrealEd", + "Json", + "JsonUtilities" + }); + } +} diff --git a/SpatialGDK/SpatialGDK.uplugin b/SpatialGDK/SpatialGDK.uplugin index b52036c23e..b513e17a11 100644 --- a/SpatialGDK/SpatialGDK.uplugin +++ b/SpatialGDK/SpatialGDK.uplugin @@ -1,7 +1,7 @@ { "FileVersion": 3, - "Version": 1, - "VersionName": "1.0", + "Version": 3, + "VersionName": "0.6.0", "FriendlyName": "SpatialOS GDK for Unreal", "Description": "The SpatialOS Game Development Kit (GDK) for Unreal Engine allows you to host your game and combine multiple dedicated server instances across one seamless game world whilst using the Unreal Engine networking API.", "Category": "SpatialOS", @@ -11,14 +11,14 @@ "MarketplaceURL": "", "SupportURL": "https://forums.improbable.io/", "EnabledByDefault": true, - "CanContainContent": true, + "CanContainContent": false, "IsBetaVersion": false, "Installed": true, "Modules": [ { "Name": "SpatialGDK", "Type": "Runtime", - "LoadingPhase": "Default", + "LoadingPhase": "PreDefault", "WhitelistPlatforms": [ "Win64", "Linux", "Mac", "XboxOne", "PS4", "IOS" ] }, { @@ -38,6 +38,12 @@ "Type": "Editor", "LoadingPhase": "Default", "WhitelistPlatforms": [ "Win64" ] + }, + { + "Name": "SpatialGDKServices", + "Type": "Editor", + "LoadingPhase": "PreDefault", + "WhitelistPlatforms": [ "Win64" ] } ], "Plugins": [ diff --git a/ci/get-engine.ps1 b/ci/get-engine.ps1 index 100f5d284b..13a5ed751a 100644 --- a/ci/get-engine.ps1 +++ b/ci/get-engine.ps1 @@ -13,10 +13,11 @@ pushd "$($gdk_home)" } popd - ## Create an UnrealEngine directory if it doesn't already exist - New-Item -Name "UnrealEngine" -ItemType Directory -Force - pushd "UnrealEngine" + ## Create an UnrealEngine-Cache directory if it doesn't already exist + New-Item -Name "UnrealEngine-Cache" -ItemType Directory -Force + + pushd "UnrealEngine-Cache" Start-Event "download-unreal-engine" "get-unreal-engine" $engine_gcs_path = "gs://$($gcs_publish_bucket)/$($unreal_version).zip" @@ -24,6 +25,7 @@ pushd "$($gdk_home)" $gsu_proc = Start-Process -Wait -PassThru -NoNewWindow "gsutil" -ArgumentList @(` "cp", ` + "-n", ` # noclobber "$($engine_gcs_path)", ` "$($unreal_version).zip" ` ) @@ -37,7 +39,9 @@ pushd "$($gdk_home)" Write-Log "Unzipping Unreal Engine" $zip_proc = Start-Process -Wait -PassThru -NoNewWindow "7z" -ArgumentList @(` "x", ` - "$($unreal_version).zip" ` + "$($unreal_version).zip", ` + "-o$($unreal_version)", ` + "-aos" ` # skip existing files ) Finish-Event "unzip-unreal-engine" "get-unreal-engine" if ($zip_proc.ExitCode -ne 0) { @@ -47,12 +51,15 @@ pushd "$($gdk_home)" popd $unreal_path = "$($gdk_home)\UnrealEngine" - Write-Log "Setting UNREAL_HOME environment variable to $($unreal_path)" - [Environment]::SetEnvironmentVariable("UNREAL_HOME", "$($unreal_path)", "Machine") + + ## Create an UnrealEngine symlink to the correct directory + Remove-Item $unreal_path -ErrorAction ignore -Recurse -Force + cmd /c mklink /J $unreal_path "UnrealEngine-Cache\$($unreal_version)" $clang_path = "$($gdk_home)\UnrealEngine\ClangToolchain" Write-Log "Setting LINUX_MULTIARCH_ROOT environment variable to $($clang_path)" [Environment]::SetEnvironmentVariable("LINUX_MULTIARCH_ROOT", "$($clang_path)", "Machine") + $Env:LINUX_MULTIARCH_ROOT = "$($clang_path)" Start-Event "installing-unreal-engine-prerequisites" "get-unreal-engine" # This runs an opaque exe downloaded in the previous step that does *some stuff* that UE needs to occur. diff --git a/ci/setup-and-build-gdk.ps1 b/ci/setup-and-build-gdk.ps1 index e7520b1736..cca20a9ea6 100644 --- a/ci/setup-and-build-gdk.ps1 +++ b/ci/setup-and-build-gdk.ps1 @@ -1,7 +1,7 @@ param( [string] $gdk_home = (get-item "$($PSScriptRoot)").parent.FullName, ## The root of the UnrealGDK repo [string] $gcs_publish_bucket = "io-internal-infra-unreal-artifacts-production/UnrealEngine", - [string] $msbuild_exe = "${env:ProgramFiles(x86)}\MSBuild\14.0\bin\MSBuild.exe", + [string] $msbuild_exe = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\MSBuild.exe", [string] $target_platform = "Win64" ) diff --git a/ci/setup-gdk.ps1 b/ci/setup-gdk.ps1 index 4360a21a3b..4614e92bda 100644 --- a/ci/setup-gdk.ps1 +++ b/ci/setup-gdk.ps1 @@ -72,6 +72,28 @@ pushd "$($gdk_home)" "$($core_sdk_dir)\worker_sdk\c-dynamic-x86_64-gcc_libstdcpp-linux.zip" ` ) Finish-Event "download-worker-sdk-x86_64-gcc_libstdcpp-linux" "download-spatial-packages" + + Start-Event "download-worker-sdk-core-dynamic-x86_64-linux" "download-spatial-packages" + Start-Process -Wait -PassThru -NoNewWindow -FilePath "spatial" -ArgumentList @(` + "package", ` + "retrieve", ` + "worker_sdk", ` + "core-dynamic-x86_64-linux", ` + "$($pinned_core_sdk_version)", ` + "$($core_sdk_dir)\worker_sdk\core-dynamic-x86_64-linux.zip" ` + ) + Finish-Event "download-worker-sdk-core-dynamic-x86_64-linux" "download-spatial-packages" + + Start-Event "download-worker-sdk-csharp" "download-spatial-packages" + Start-Process -Wait -PassThru -NoNewWindow -FilePath "spatial" -ArgumentList @(` + "package", ` + "retrieve", ` + "worker_sdk", ` + "csharp", ` + "$($pinned_core_sdk_version)", ` + "$($core_sdk_dir)\worker_sdk\csharp.zip" ` + ) + Finish-Event "download-worker-sdk-csharp" "download-spatial-packages" Finish-Event "download-spatial-packages" "setup-gdk" Start-Event "extract-spatial-packages" "setup-gdk" @@ -80,6 +102,8 @@ pushd "$($gdk_home)" Expand-Archive -Path "$($core_sdk_dir)\worker_sdk\c-dynamic-x86-msvc_md-win32.zip" -DestinationPath "$($binaries_dir)\Win32\" -Force Expand-Archive -Path "$($core_sdk_dir)\worker_sdk\c-dynamic-x86_64-msvc_md-win32.zip" -DestinationPath "$($binaries_dir)\Win64\" -Force Expand-Archive -Path "$($core_sdk_dir)\worker_sdk\c-dynamic-x86_64-gcc_libstdcpp-linux.zip" -DestinationPath "$($binaries_dir)\Linux\" -Force + Expand-Archive -Path "$($core_sdk_dir)\worker_sdk\core-dynamic-x86_64-linux.zip" -DestinationPath "$($binaries_dir)\Programs\worker_sdk\core\" -Force + Expand-Archive -Path "$($core_sdk_dir)\worker_sdk\csharp.zip" -DestinationPath "$($binaries_dir)\Programs\worker_sdk\csharp\" -Force Finish-Event "extract-spatial-packages" "setup-gdk" # Copy from binaries_dir @@ -89,7 +113,8 @@ pushd "$($gdk_home)" $msbuild_proc = Start-Process -PassThru -NoNewWindow -FilePath "$($msbuild_exe)" -ArgumentList @(` "/nologo", ` "SpatialGDK\Build\Programs\Improbable.Unreal.Scripts\Improbable.Unreal.Scripts.sln", ` - "/property:Configuration=Release" ` + "/property:Configuration=Release",` + "/restore" ` ) # Note: holding on to a handle solves an intermittent issue when waiting on the process id diff --git a/ci/unreal-engine.version b/ci/unreal-engine.version index 139c53df2a..ed116c4273 100644 --- a/ci/unreal-engine.version +++ b/ci/unreal-engine.version @@ -1 +1 @@ -UnrealEngine-24edbad2f25c822bb8ec62846ce80bc1e41d1bdf \ No newline at end of file +UnrealEngine-2ce5d80f1df2ecffa37516c9cc38ffd816da68f5 \ No newline at end of file