Forráskód Böngészése

Merge pull request #386 from aws-lumberyard-dev/mbalfour/gitflow_20230424_mps2

Merge stabilization to development
Mike Balfour 2 éve
szülő
commit
2bf7b1052e

BIN
Documentation/Media/2305_screenshot.png


BIN
Documentation/Media/wwise_installer_options.png


BIN
Documentation/Media/wwise_installer_version_selection.png


+ 123 - 0
Documentation/PackedAssetBuilds.md

@@ -0,0 +1,123 @@
+# Packaged MultiplayerSample Builds
+To make relocatable client and server builds for the MultiplayerSample, we recommend making packaged builds. These package builds will contain the Game or Server Launcher and the bundled assets needed to run the launcher outside of the developer environment.
+
+You can make both release packaged builds or profile packaged builds. For more information about creating release builds, see the O3DE documentation on [Creating a Project Game Release Layout for Windows](https://www.o3de.org/docs/user-guide/packaging/windows-release-builds/).
+
+The guide below covers how to make profile packaged builds which are very useful for early sharing and play testing.
+
+## Windows Profile PAK Setup
+
+### Install WWise
+Go to https://www.audiokinetic.com/download/, create a login, log in, and download the installer. WWise is needed to process and package audio assets in the project.
+
+![WWise installer options](Media/wwise_installer_options.png)
+
+Inside the installer select the version to use.  Install version **2021.1.11.7933**, select both Authoring and SDK, Microsoft platform.
+
+![WWise version selection](Media/wwise_installer_version_selection.png)
+
+> REBOOT (or logout / login). Otherwise, the environment settings won't get picked up for any builds in Visual Studio. They will only apply to command-line builds, and only for any command-line windows that have been opened after the installer finishes.
+
+### Build profile build and process assets
+
+Build and run MPS as per the [README.md](../README.md) and ensure all assets are built.
+
+### Test the profile build
+
+* Open the game in editor
+    * load `NewStarBase` level
+    * Verify that game can launch and connect to local server from editor
+* Validate local game launcher can connect to local server
+
+### Build AssetBuilder
+
+You will need to build the [AssetBundler](https://www.o3de.org/docs/user-guide/packaging/asset-bundler/overview/) tool if not built.
+
+For example:
+```shell
+cmake --build build\windows --target AssetBundler --config profile -- /m /nologo
+```
+
+### Build monolithic game
+
+Build a second version of the executables as monolithic pak builds.
+
+``` shell
+# Create build files for a monolithic build that also disables all user/project registry settings overrides
+cmake -B build\windows_mono -S . -G "Visual Studio 16" -DLY_3RDPARTY_PATH=c:\your\path\to\3rdParty -DLY_MONOLITHIC_GAME=1 -DALLOW_SETTINGS_REGISTRY_DEVELOPMENT_OVERRIDES=0
+
+# Build the profile versions of all the executables
+cmake --build build\windows_mono --target MultiplayerSample.GameLauncher MultiplayerSample.ServerLauncher MultiplayerSample.UnifiedLauncher --config profile -- /m /nologo
+```
+
+The outputs in windows_mono\bin\profile can be copied and run anywhere, once the pak files are put in the proper location.
+
+
+### Bundle Content
+
+Run the AssetBundler
+
+```
+build\windows\bin\profile\AssetBundler.exe --project-path="c:\your\path\to\o3de-multiplayersample"
+```
+
+Follow steps for "Create a bundle for game assets", "Create a bundle for engine assets" and "Add bundles to the release game layout" from https://www.o3de.org/docs/user-guide/packaging/asset-bundler/bundle-assets-for-release/
+
+* The "default seed lists" choice should choose all but 4 seed lists to make the `engine_pc.pak`
+* The other [seed lists](https://github.com/o3de/o3de-multiplayersample/tree/development/AssetBundling/SeedLists) should all get selected to make the `game_pc.pak`.
+
+> It's important to make sure that the bootstrap.game.profile.setreg file has been added to one of the seed files. (also add debug if you want to support debug builds)
+
+### Create the Launcher Zip file
+
+Use the following .bat file or equivalent copy steps to create a directory with the launchers in it:
+```shell
+rem Use this by calling 'make_release D:\my\output\dir' to make a release directory
+mkdir %1
+mkdir %1\Cache
+mkdir %1\Cache\pc
+mkdir %1\Gems
+mkdir %1\Gems\AWSCore
+ 
+rem Copy the pak files
+copy c:\your\path\to\o3de-multiplayersample\AssetBundling\Bundles\*.pak %1\Cache\pc
+ 
+rem Copy the executables and DLLs
+copy c:\your\path\to\o3de-multiplayersample\build\windows_mono\bin\profile\*.* %1
+ 
+rem Copy the AWSCore files
+copy c:\your\path\to\o3de-multiplayersample\build\windows_mono\bin\profile\Gems\AWSCore\*.* %1\Gems\AWSCore
+ 
+rem Copy the launch_client / launch_server files
+copy c:\your\path\to\o3de-multiplayersample\launch_*.* %1
+ 
+rem Copy the PIX DLLs if PIX is enabled in the build
+:: copy c:\your\path\to\3rdParty\WinPixEventRuntime\bin\x64 %1
+```
+
+Note: The script above will copy everything from the profile directory. You can either remove files you don't require such as .pdb files to save space, or modify the script to copy just whats needed.
+
+It's recommended during testing that you include source information for your build, such as the commit IDs for o3de, o3de-multiplayersample, and o3de-multiplayersample-assets.
+You can add this as .txt file in the build folder.
+
+Zip up the directory before running it to make sure the zip is "pure" without any logs or artifacts.
+
+### Run the final build to verify it
+
+```shell
+MultiplayerSample.ServerLauncher.exe --console-command-file=launch_server.cfg --rhi=null -NullRenderer -bg_ConnectToAssetProcessor=0 -sys_PakPriority=2 -sv_terminateOnPlayerExit=false
+MultiplayerSample.GameLauncher.exe --console-command-file=launch_client.cfg -bg_ConnectToAssetProcessor=0 -sys_PakPriority=2
+```
+
+After running, check the output logs to verify there aren't any crashes, missing assets, etc. If any assets are missing, go back to the AssetBundler step, add the assets, and repeat.
+
+## Linux profile packaged builds
+
+Instructions for Linux are similar to Windows instructions above. All examples are Ubuntu 22.04 which is the primary Linux platform for O3DE. See https://www.o3de.org/docs/welcome-guide/requirements/ for more details.
+
+## Install WWise
+See instructions above but install Wwise for Linux Ubuntu
+
+### Build profile build and process assets
+
+Build and run MPS as per the [README_LINUX.md](../README_LINUX.md) and ensure all assets are built.

+ 29 - 0
Documentation/ReleaseNotes.md

@@ -0,0 +1,29 @@
+# Release Notes for MultiplayerSample
+
+## O3DE 2305 Release
+
+![2305 Screenshot](Media/2305_screenshot.png)
+
+2305 is the base version of the game. It is expected to be functional and support up to 10 players. 
+
+### Testing Notes
+
+| Platform         | In Editor    | Profile PAKs | Release + PAKs |
+|------------------|--------------|--------------|----------------|
+| Windows          | yes - stable | yes - stable | no             |
+| Ubuntu           | yes -stable  | no           | no             |
+| Mobile Platforms | n/a          | no           | no             |
+
+MPS was tested using both LAN hosted servers and using AWS EC2 for remote server testing.
+
+### Known Issues
+
+There are some notable known issues:
+
+* On Linux there's a problem with some actor motionsets appearing to have root motion applied: https://github.com/o3de/o3de-multiplayersample/issues/315
+* For the ServerLauncher there are serialization warnings in the log relating to PopcornFX which are harmless, as PopcornFX is ignored on the Server by design.
+* The game has fixed support for 10 players: https://github.com/o3de/o3de-multiplayersample/issues/351. More than 10 players can join the server, but they will not be able to play the game (nor show correctly in UX).
+* Opening ImGUI can block player input: https://github.com/o3de/o3de-multiplayersample/issues/353
+
+For the complete and up-to-date list of issues, please see [MPS issues](https://github.com/o3de/o3de-multiplayersample/issues?q=is%3Aissue+is%3Aopen+label%3Akind%2Fbug) and [MPS SampleAssets issue](https://github.com/o3de/o3de-multiplayersample-assets/issues).
+

+ 47 - 0
Documentation/SettingsScreen.md

@@ -0,0 +1,47 @@
+# Settings screen
+
+The settings screen contains a set of controls for modifying various options in O3DE. This screen demonstrates how to expose settings from O3DE to the user and how to persist those settings to a file. 
+
+## User settings
+### Graphics api
+
+O3DE supports running with multiple different graphics APIs, depending on the platform. The full set of APIs currently supported are the following:
+
+* "null" - A null renderer, nothing is drawn to the screen
+* "dx12" - The DirectX 12 API (Windows only)
+* "vulkan" - The Vulkan API (all platforms)
+* "metal" - The Metal API (iOS / Mac platforms only)
+
+The different APIs use different OS-level and driver-level code, which can result in performance differences based on a user's specific operating system, graphics card, and graphics drivers.
+
+Normally the API is selected through a command-line argument, such as `-rhi="null"`. The setting on the settings screen provides the ability to specify the default API to use on subsequent runs of the launcher.
+
+The implementation of this setting is more complex than the other settings due to how early the setting needs to be applied. The user setting needs to be read in after enough of the engine has been initialized to be able to load in settings from project-specific code, but before the renderer itself has been initialized; the API choice cannot be changed once the renderer is initialized. This timing occurs during the construction of the project's Module class. This setting is implemented by modifying a settings registry key that controls what priority the renderer should use to select an API.
+
+### Texture quality
+
+The Texture Quality setting controls the "mip bias", which is the highest mip level to load for each streamed texture. A value of `0` is the highest-resolution mipmap, `1` is the second-highest mipmap, and so on. This value exposes the tradeoff between quality and memory - the highest-resolution textures look the best but use the most VRAM. The user can choose to lower the quality and lower the VRAM usage, which can help with performance.
+
+In O3DE, the mip bias is exposed through a [CVAR](https://www.o3de.org/docs/user-guide/appendix/cvars/), a settings registry key, and an API. The settings screen uses the API to directly modify the setting.
+
+### Audio volume
+
+The Audio Volume setting controls the master volume for the entire audio system, from `0` (min) to `100` (max). The audio system has data-driven volume channels; each project's audio setup may have a different set of more granular volume controls, such as for music, sound effects, voice, and ambient noise. This setting implements a master volume control, but any of the more granular controls would be implemented in a similar way.
+
+Volume controls for the audio system are exposed through an API, and not through any CVAR or settings registry keys. The settings screen uses the API directly to modify the value.
+
+### Fullscreen
+
+The Fullscreen setting controls whether the launcher is running in a window on the desktop or in fullscreen mode.
+
+The fullscreen/windowed mode selection is controlled by the `r_fullscreen` CVAR. The settings screen demonstrates how to set the cvar from a user setting.
+
+## Modifying and extending the settings screen
+
+See the following source files for the settings screen implementation:
+* [https://github.com/o3de/o3de-multiplayersample/blob/development/Gem/Code/Source/UserSettings/MultiplayerSampleUserSettings.cpp](https://github.com/o3de/o3de-multiplayersample/blob/development/Gem/Code/Source/UserSettings/MultiplayerSampleUserSettings.cpp)
+* [https://github.com/o3de/o3de-multiplayersample/blob/development/Gem/Code/Source/Components/UI/UiSettingsComponent.cpp](https://github.com/o3de/o3de-multiplayersample/blob/development/Gem/Code/Source/Components/UI/UiSettingsComponent.cpp)
+
+The `MultiplayerSampleUserSettings.cpp` file contains the backend logic for loading and saving the user settings and applying the settings to the engine. The default values for each setting can be found here as well. More settings could be exposed and implemented here.
+
+The `UiSettingsComponent.cpp` file contains the UX logic for turning the setting values into user-friendly names and toggle controls. The names and specific choices of which values to expose can be found here. For example, the Master Volume control exposes the volume in increments of 10, but it could be modified to expose more volume values to increment it by 5 or by 1. Similarly, the Graphics API control could be modified to only expose APIs that exist for that platform.

+ 214 - 119
Gem/Code/Source/Components/UI/UiSettingsComponent.cpp

@@ -6,30 +6,16 @@
  */
 
 #include <Atom/RHI/Factory.h>
-#include <AzCore/Console/IConsole.h>
 #include <AzCore/Interface/Interface.h>
 #include <AzCore/Serialization/EditContext.h>
-#include <IAudioSystem.h>
 #include <LyShine/Bus/UiButtonBus.h>
 #include <LyShine/Bus/UiElementBus.h>
 #include <LyShine/Bus/UiTextBus.h>
 #include <Source/Components/UI/UiSettingsComponent.h>
+#include <Source/UserSettings/MultiplayerSampleUserSettings.h>
 
 namespace MultiplayerSample
 {
-    void MpsSettings::Reflect(AZ::ReflectContext* context)
-    {
-        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
-        {
-            serializeContext->Class<MpsSettings>()
-                ->Version(0)
-                ->Field("GraphicsApi", &MpsSettings::m_atomApiType)
-                ->Field("MasterVolume", &MpsSettings::m_masterVolume)
-                ->Field("TextureQuality", &MpsSettings::m_streamingImageMipBias)
-                ;
-        }
-    }
-
     void UiToggle::Reflect(AZ::ReflectContext* context)
     {
         if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
@@ -41,7 +27,6 @@ namespace MultiplayerSample
                 ->Field("RightButton", &UiToggle::m_rightButtonEntity)
                 ;
 
-
             if (AZ::EditContext* editContext = serializeContext->GetEditContext())
             {
                 editContext->Class<UiToggle>("Ui Toggle", "Manages the user settings")
@@ -56,7 +41,6 @@ namespace MultiplayerSample
 
     void UiSettingsComponent::Reflect(AZ::ReflectContext* context)
     {
-        MpsSettings::Reflect(context);
         UiToggle::Reflect(context);
 
         if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
@@ -66,6 +50,8 @@ namespace MultiplayerSample
                 ->Field("GraphicsApi", &UiSettingsComponent::m_graphicsApiToggle)
                 ->Field("TextureQuality", &UiSettingsComponent::m_textureQualityToggle)
                 ->Field("MasterVolume", &UiSettingsComponent::m_masterVolumeToggle)
+                ->Field("Fullscreen", &UiSettingsComponent::m_fullscreenToggle)
+                ->Field("Resolution", &UiSettingsComponent::m_resolutionToggle)
                 ;
 
             if (AZ::EditContext* editContext = serializeContext->GetEditContext())
@@ -76,155 +62,264 @@ namespace MultiplayerSample
                     ->DataElement(AZ::Edit::UIHandlers::Default, &UiSettingsComponent::m_graphicsApiToggle, "Graphics Api", "The Graphics Api toggle elements.")
                     ->DataElement(AZ::Edit::UIHandlers::Default, &UiSettingsComponent::m_textureQualityToggle, "Texture Quality", "The Texture Quality toggle elements.")
                     ->DataElement(AZ::Edit::UIHandlers::Default, &UiSettingsComponent::m_masterVolumeToggle, "Master Volume", "The Master Volume toggle elements.")
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &UiSettingsComponent::m_fullscreenToggle, "Fullscreen", "The Fullscreen toggle elements.")
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &UiSettingsComponent::m_resolutionToggle, "Resolution", "The Resolution toggle elements.")
                     ;
             }
         }
     }
 
-    void UiSettingsComponent::Activate()
+    AzFramework::NativeWindowHandle UiSettingsComponent::GetWindowHandle()
     {
-        // Initialize our user settings 
+        AzFramework::NativeWindowHandle windowHandle = nullptr;
+        AzFramework::WindowSystemRequestBus::BroadcastResult(
+            windowHandle,
+            &AzFramework::WindowSystemRequestBus::Events::GetDefaultWindowHandle);
 
-        // Initialize the current streaming image mip bias setting.
-        if (AZ::IConsole* console = AZ::Interface<AZ::IConsole>::Get(); console)
-        {
-            int16_t mipBias = 0;
-            console->GetCvarValue("r_streamingImageMipBias", mipBias);
-            m_settings.m_streamingImageMipBias = aznumeric_cast<uint8_t>(mipBias);
-        }
+        return windowHandle;
+    }
 
-        // Initialize the graphics API type
-        m_settings.m_atomApiType = AZ::RHI::Factory::Get().GetAPIUniqueIndex();
+    void UiSettingsComponent::Activate()
+    {
+        // Listen for window notifications so that we can detect fullscreen/windowed changes.
+        AzFramework::WindowNotificationBus::Handler::BusConnect(GetWindowHandle());
 
-        // There's currently no way to initialize the master volume, this doesn't seem to be fetchable anywhere.
+        // Loads and applies the current user settings when this component activates.
+        // The user settings should *already* be loaded and applied at Launcher startup, but connecting to the server
+        // and switching levels can cause some engine settings to reset themselves, so this will reapply the desired
+        // user settings again.
+        MultiplayerSampleUserSettingsRequestBus::Broadcast(&MultiplayerSampleUserSettingsRequestBus::Events::Load);
 
         // Initialize the toggles to the current values
-        OnGraphicsApiToggle(ToggleDirection::None);
-        OnTextureQualityToggle(ToggleDirection::None);
-        OnMasterVolumeToggle(ToggleDirection::None);
-
-        // Start listening for button presses
-        UiButtonBus::Event(m_graphicsApiToggle.m_leftButtonEntity, &UiButtonInterface::SetOnClickCallback,
-            [this]([[maybe_unused]] AZ::EntityId buttonEntityId, [[maybe_unused]] AZ::Vector2 position) 
-            { 
-                OnGraphicsApiToggle(ToggleDirection::Left); 
-            });
-        UiButtonBus::Event(m_graphicsApiToggle.m_rightButtonEntity, &UiButtonInterface::SetOnClickCallback,
-            [this]([[maybe_unused]] AZ::EntityId buttonEntityId, [[maybe_unused]] AZ::Vector2 position)
-            {
-                OnGraphicsApiToggle(ToggleDirection::Right);
-            });
-        UiButtonBus::Event(m_textureQualityToggle.m_leftButtonEntity, &UiButtonInterface::SetOnClickCallback,
-            [this]([[maybe_unused]] AZ::EntityId buttonEntityId, [[maybe_unused]] AZ::Vector2 position)
-            {
-                OnTextureQualityToggle(ToggleDirection::Left);
-            });
-        UiButtonBus::Event(m_textureQualityToggle.m_rightButtonEntity, &UiButtonInterface::SetOnClickCallback,
-            [this]([[maybe_unused]] AZ::EntityId buttonEntityId, [[maybe_unused]] AZ::Vector2 position)
-            {
-                OnTextureQualityToggle(ToggleDirection::Right);
-            });
-        UiButtonBus::Event(m_masterVolumeToggle.m_leftButtonEntity, &UiButtonInterface::SetOnClickCallback,
-            [this]([[maybe_unused]] AZ::EntityId buttonEntityId, [[maybe_unused]] AZ::Vector2 position)
+        InitializeToggle(m_graphicsApiToggle, OnGraphicsApiToggle);
+        InitializeToggle(m_textureQualityToggle, OnTextureQualityToggle);
+        InitializeToggle(m_masterVolumeToggle, OnMasterVolumeToggle);
+        InitializeToggle(m_fullscreenToggle, OnFullscreenToggle);
+        InitializeToggle(m_resolutionToggle, OnResolutionToggle);
+    }
+
+    void UiSettingsComponent::Deactivate()
+    {
+        AzFramework::WindowNotificationBus::Handler::BusDisconnect();
+    }
+
+    void UiSettingsComponent::InitializeToggle(UiToggle& toggle, AZStd::function<void(UiToggle&, ToggleDirection)> toggleUpdateFn)
+    {
+        toggleUpdateFn(toggle, ToggleDirection::None);
+
+        UiButtonBus::Event(toggle.m_leftButtonEntity, &UiButtonInterface::SetOnClickCallback,
+            [&toggle, toggleUpdateFn]([[maybe_unused]] AZ::EntityId buttonEntityId, [[maybe_unused]] AZ::Vector2 position)
             {
-                OnMasterVolumeToggle(ToggleDirection::Left);
+                toggleUpdateFn(toggle, ToggleDirection::Left);
             });
-        UiButtonBus::Event(m_masterVolumeToggle.m_rightButtonEntity, &UiButtonInterface::SetOnClickCallback,
-            [this]([[maybe_unused]] AZ::EntityId buttonEntityId, [[maybe_unused]] AZ::Vector2 position)
+        UiButtonBus::Event(toggle.m_rightButtonEntity, &UiButtonInterface::SetOnClickCallback,
+            [&toggle, toggleUpdateFn]([[maybe_unused]] AZ::EntityId buttonEntityId, [[maybe_unused]] AZ::Vector2 position)
             {
-                OnMasterVolumeToggle(ToggleDirection::Right);
+                toggleUpdateFn(toggle, ToggleDirection::Right);
             });
     }
 
-    void UiSettingsComponent::Deactivate()
+    template<typename ValueType>
+    uint32_t UiSettingsComponent::GetRotatedIndex(
+        const AZStd::vector<AZStd::pair<ValueType, AZStd::string>>& valuesToLabels,
+        const ValueType& value, ToggleDirection toggleDirection)
     {
+        const size_t totalValues = valuesToLabels.size();
+
+        uint32_t curIndex = 0;
+
+        // Loop through and look for the correct value
+        for (size_t index = 0; index < totalValues; index++)
+        {
+            if (value == valuesToLabels[index].first)
+            {
+                curIndex = aznumeric_cast<uint32_t>(index);
+                break;
+            }
+        }
+
+        switch (toggleDirection)
+        {
+        case ToggleDirection::Left:
+            return aznumeric_cast<uint32_t>((curIndex + (totalValues - 1)) % totalValues);
+        case ToggleDirection::Right:
+            return aznumeric_cast<uint32_t>((curIndex + 1) % totalValues);
+        default:
+            return curIndex;
+        }
     }
 
-    void UiSettingsComponent::OnGraphicsApiToggle(ToggleDirection toggleDirection)
+    void UiSettingsComponent::OnGraphicsApiToggle(UiToggle& toggle, ToggleDirection toggleDirection)
     {
         // This list is expected to match the values in AZ::RHI::ApiIndex.
-        const char* labels[] =
+        const AZStd::vector<AZStd::pair<AZStd::string, AZStd::string>> valuesToLabels =
         {
-            "Null",
-            "DirectX 12",
-            "Vulkan",
-            "Metal"
+            { "null", "Null" },
+            { "dx12", "DirectX 12" },
+            { "vulkan", "Vulkan" },
+            { "metal", "Metal" }
         };
 
-        const size_t NumLabels = AZ_ARRAY_SIZE(labels);
+        // Get the current api selection.
+        AZStd::string graphicsApi;
+        MultiplayerSampleUserSettingsRequestBus::BroadcastResult(
+            graphicsApi, &MultiplayerSampleUserSettingsRequestBus::Events::GetGraphicsApi);
 
-        if (toggleDirection != ToggleDirection::None)
+        // If there isn't anything stored in the user settings yet, default to the currently-loaded api.
+        if (graphicsApi.empty())
         {
-            m_settings.m_atomApiType = (toggleDirection == ToggleDirection::Right)
-                ? (m_settings.m_atomApiType + 1) % NumLabels
-                : (m_settings.m_atomApiType + (NumLabels - 1)) % NumLabels
-                ;
+            graphicsApi = AZ::RHI::Factory::Get().GetName().GetStringView();
         }
 
-        UiTextBus::Event(m_graphicsApiToggle.m_labelEntity, &UiTextInterface::SetText, labels[m_settings.m_atomApiType]);
+        // Rotate the index based on toggle direction.
+        uint32_t graphicsApiIndex = GetRotatedIndex(valuesToLabels, graphicsApi, toggleDirection);
+
+        UiTextBus::Event(toggle.m_labelEntity, &UiTextInterface::SetText, valuesToLabels[graphicsApiIndex].second);
+
+        MultiplayerSampleUserSettingsRequestBus::Broadcast(
+            &MultiplayerSampleUserSettingsRequestBus::Events::SetGraphicsApi, valuesToLabels[graphicsApiIndex].first);
+
+        MultiplayerSampleUserSettingsRequestBus::Broadcast(&MultiplayerSampleUserSettingsRequestBus::Events::Save);
     }
 
-    void UiSettingsComponent::OnTextureQualityToggle(ToggleDirection toggleDirection)
+    void UiSettingsComponent::OnTextureQualityToggle(UiToggle& toggle, ToggleDirection toggleDirection)
     {
-        const char* labels[] =
+        const AZStd::vector<AZStd::pair<int16_t, AZStd::string>> valuesToLabels =
         {
-            "Ultra (4K)",
-            "High (2K)",
-            "Medium (1K)",
-            "Low (512)",
-            "Very Low (256)",
-            "Extremely Low (128)",
-            "Rock Bottom (64)"
+            { aznumeric_cast<int16_t>(6), "Rock Bottom (64)" },
+            { aznumeric_cast<int16_t>(5), "Extremely Low (128)" },
+            { aznumeric_cast<int16_t>(4), "Very Low (256)" },
+            { aznumeric_cast<int16_t>(3), "Low (512)" },
+            { aznumeric_cast<int16_t>(2), "Medium (1K)" },
+            { aznumeric_cast<int16_t>(1), "High (2K)" },
+            { aznumeric_cast<int16_t>(0), "Ultra (4K)" },
         };
 
-        const size_t NumLabels = AZ_ARRAY_SIZE(labels);
+        // Get the current texture quality value.
+        int16_t textureQuality = 0;
+        MultiplayerSampleUserSettingsRequestBus::BroadcastResult(
+            textureQuality, &MultiplayerSampleUserSettingsRequestBus::Events::GetTextureQuality);
 
-        if (toggleDirection != ToggleDirection::None)
-        {
-            // As we go from left to right on our settings, we want our textureQuality number to go from 6 down to 0
-            // because smaller mip bias numbers mean higher-resolution textures.
-            m_settings.m_streamingImageMipBias = (toggleDirection == ToggleDirection::Right)
-                ? (m_settings.m_streamingImageMipBias + (NumLabels - 1)) % NumLabels
-                : (m_settings.m_streamingImageMipBias + 1) % NumLabels
-                ;
-        }
+        // Rotate the index based on toggle direction.
+        uint32_t textureQualityIndex = GetRotatedIndex(valuesToLabels, textureQuality, toggleDirection);
+
+        UiTextBus::Event(toggle.m_labelEntity, &UiTextInterface::SetText, valuesToLabels[textureQualityIndex].second);
 
-        AZ::IConsole* console = AZ::Interface<AZ::IConsole>::Get();
-        if (console)
+        MultiplayerSampleUserSettingsRequestBus::Broadcast(
+            &MultiplayerSampleUserSettingsRequestBus::Events::SetTextureQuality, valuesToLabels[textureQualityIndex].first);
+
+        MultiplayerSampleUserSettingsRequestBus::Broadcast(&MultiplayerSampleUserSettingsRequestBus::Events::Save);
+    }
+
+    void UiSettingsComponent::OnMasterVolumeToggle(UiToggle& toggle, ToggleDirection toggleDirection)
+    {
+        const AZStd::vector<AZStd::pair<uint8_t, AZStd::string>> valuesToLabels =
         {
-            AZ::CVarFixedString commandString = AZ::CVarFixedString::format("r_streamingImageMipBias %" PRId16, m_settings.m_streamingImageMipBias);
-            console->PerformCommand(commandString.c_str());
-        }
+            { aznumeric_cast<uint8_t>(0), "0 (off)" },
+            { aznumeric_cast<uint8_t>(10), "10" },
+            { aznumeric_cast<uint8_t>(20), "20" },
+            { aznumeric_cast<uint8_t>(30), "30" },
+            { aznumeric_cast<uint8_t>(40), "40" },
+            { aznumeric_cast<uint8_t>(50), "50" },
+            { aznumeric_cast<uint8_t>(60), "60" },
+            { aznumeric_cast<uint8_t>(70), "70" },
+            { aznumeric_cast<uint8_t>(80), "80" },
+            { aznumeric_cast<uint8_t>(90), "90" },
+            { aznumeric_cast<uint8_t>(100), "100 (max)" },
+        };
+
+        // Get the current master volume value.
+        uint8_t masterVolume = 0;
+        MultiplayerSampleUserSettingsRequestBus::BroadcastResult(
+            masterVolume, &MultiplayerSampleUserSettingsRequestBus::Events::GetMasterVolume);
+
+        // Make sure our master volume is a multiple of 10.
+        masterVolume = (masterVolume / 10) * 10;
+
+        // Rotate the index based on toggle direction.
+        uint32_t masterVolumeIndex = GetRotatedIndex(valuesToLabels, masterVolume, toggleDirection);
+
+        UiTextBus::Event(toggle.m_labelEntity, &UiTextInterface::SetText, valuesToLabels[masterVolumeIndex].second);
 
-        UiTextBus::Event(m_textureQualityToggle.m_labelEntity, &UiTextInterface::SetText, labels[m_settings.m_streamingImageMipBias]);
+        MultiplayerSampleUserSettingsRequestBus::Broadcast(
+            &MultiplayerSampleUserSettingsRequestBus::Events::SetMasterVolume, valuesToLabels[masterVolumeIndex].first);
+
+        MultiplayerSampleUserSettingsRequestBus::Broadcast(&MultiplayerSampleUserSettingsRequestBus::Events::Save);
     }
 
-    void UiSettingsComponent::OnMasterVolumeToggle(ToggleDirection toggleDirection)
+    void UiSettingsComponent::OnFullscreenToggle(UiToggle& toggle, ToggleDirection toggleDirection)
     {
-        if (toggleDirection != ToggleDirection::None)
+        const AZStd::vector<AZStd::pair<bool, AZStd::string>> valuesToLabels =
         {
-            m_settings.m_masterVolume = (toggleDirection == ToggleDirection::Right)
-                ? (m_settings.m_masterVolume + 10) % 110
-                : (m_settings.m_masterVolume + 100) % 110
-                ;
-        }
+            { false, "Windowed" },
+            { true, "Fullscreen" },
+        };
+
+        // Get the current fullscreen state. Unlike the other settings, we'll get this from the current window state so that we
+        // handle things like Alt-enter that can change our windowing state regardless of what our user settings thinks.
 
-        auto audioSystem = AZ::Interface<Audio::IAudioSystem>::Get();
-        if (audioSystem)
+        // Start by defaulting to the user setting.
+        bool fullscreen = false;
+        MultiplayerSampleUserSettingsRequestBus::BroadcastResult(
+            fullscreen, &MultiplayerSampleUserSettingsRequestBus::Events::GetFullscreen);
+
+        // Next, try to get the current state from the window. If it fails to get the state, we'll auto-default to the
+        // user setting value that we fetched above.
+        AzFramework::WindowRequestBus::EventResult(fullscreen,
+            GetWindowHandle(), &AzFramework::WindowRequestBus::Events::GetFullScreenState);
+
+        // Rotate the index based on toggle direction.
+        uint32_t fullscreenIndex = GetRotatedIndex(valuesToLabels, fullscreen, toggleDirection);
+
+        UiTextBus::Event(toggle.m_labelEntity, &UiTextInterface::SetText, valuesToLabels[fullscreenIndex].second);
+
+        MultiplayerSampleUserSettingsRequestBus::Broadcast(
+            &MultiplayerSampleUserSettingsRequestBus::Events::SetFullscreen, valuesToLabels[fullscreenIndex].first);
+
+        MultiplayerSampleUserSettingsRequestBus::Broadcast(&MultiplayerSampleUserSettingsRequestBus::Events::Save);
+    }
+
+    void UiSettingsComponent::OnResolutionToggle(UiToggle& toggle, ToggleDirection toggleDirection)
+    {
+        const AZStd::vector<AZStd::pair<AZStd::pair<uint32_t, uint32_t>, AZStd::string>> valuesToLabels =
         {
-            Audio::TAudioObjectID rtpcId = audioSystem->GetAudioRtpcID("Volume_Master");
+            { {1280, 720}, "1280 x 720" },
+            { {1920, 1080}, "1920 x 1080" },
+            { {1920, 1200}, "1920 x 1200" },
+            { {2560, 1440}, "2560 x 1440" },
+            { {2560, 1600}, "2560 x 1600" },
+            { {3840, 2160}, "3840 x 2160" },
+            { {3840, 2400}, "3840 x 2400" },
+        };
 
-            if (rtpcId != INVALID_AUDIO_CONTROL_ID)
-            {
-                Audio::ObjectRequest::SetParameterValue setParameter;
-                setParameter.m_audioObjectId = INVALID_AUDIO_OBJECT_ID;
-                setParameter.m_parameterId = rtpcId;
-                setParameter.m_value = m_settings.m_masterVolume / 100.0f;
-                AZ::Interface<Audio::IAudioSystem>::Get()->PushRequest(AZStd::move(setParameter));
-            }
-        }
+        // Get the current resolution value.
+        AZStd::pair<uint32_t, uint32_t> resolution = { 1920, 1080 };
+        MultiplayerSampleUserSettingsRequestBus::BroadcastResult(
+            resolution, &MultiplayerSampleUserSettingsRequestBus::Events::GetResolution);
+
+        // Rotate the index based on toggle direction.
+        uint32_t resolutionIndex = GetRotatedIndex(valuesToLabels, resolution, toggleDirection);
+
+        UiTextBus::Event(toggle.m_labelEntity, &UiTextInterface::SetText, valuesToLabels[resolutionIndex].second);
+
+        MultiplayerSampleUserSettingsRequestBus::Broadcast(
+            &MultiplayerSampleUserSettingsRequestBus::Events::SetResolution, valuesToLabels[resolutionIndex].first);
+
+        MultiplayerSampleUserSettingsRequestBus::Broadcast(&MultiplayerSampleUserSettingsRequestBus::Events::Save);
+    }
 
-        UiTextBus::Event(m_masterVolumeToggle.m_labelEntity, &UiTextInterface::SetText, AZStd::string::format("%d", m_settings.m_masterVolume));
+    void UiSettingsComponent::OnWindowResized([[maybe_unused]] uint32_t width, [[maybe_unused]] uint32_t height)
+    {
+        // Refresh the windowed / fullscreen setting. There is no direct notification for fullscreen changes,
+        // so we detect it indirectly by listening for OnWindowResized and OnRefreshRateChanged messages.
+        OnFullscreenToggle(m_fullscreenToggle, ToggleDirection::None);
+    }
+
+    void UiSettingsComponent::OnRefreshRateChanged([[maybe_unused]] uint32_t refreshRate)
+    {
+        // Refresh the windowed / fullscreen setting. There is no direct notification for fullscreen changes,
+        // so we detect it indirectly by listening for OnWindowResized and OnRefreshRateChanged messages.
+        OnFullscreenToggle(m_fullscreenToggle, ToggleDirection::None);
     }
 
 }

+ 26 - 22
Gem/Code/Source/Components/UI/UiSettingsComponent.h

@@ -9,26 +9,10 @@
 
 
 #include <AzCore/Component/Component.h>
+#include <AzFramework/Windowing/WindowBus.h>
 
 namespace MultiplayerSample
 {
-    //! These are all of the user settings that MPS supports.
-    struct MpsSettings
-    {
-        AZ_TYPE_INFO(MpsSettings, "{1E545ABF-6650-41D8-AC69-9C50BB5561F0}");
-        static void Reflect(AZ::ReflectContext* context);
-
-        //! The API type that Atom should use at startup. (This value comes from AZ::RHI::APIIndex)
-        uint32_t m_atomApiType = 0;
-
-        //! The master audio volume (0 - 100). 0 is silent, 100 is max volume.
-        uint8_t m_masterVolume = 100;
-
-        //! The streaming image texture mip bias (0 - N). This affects the max mipmap level that will be loaded for streaming images.
-        //! This doesn't affect other types of images like UI or VFX.
-        uint8_t m_streamingImageMipBias = 1;
-    };
-
     struct UiToggle
     {
         AZ_TYPE_INFO(UiToggle, "{60AD7DDE-1730-41D8-BB82-630FF8008370}");
@@ -39,8 +23,13 @@ namespace MultiplayerSample
         AZ::EntityId m_rightButtonEntity;
     };
 
+    // This component is attached to the UI Settings screen and handles the logic for changing the user setting values
+    // when the UI toggles are toggled. It activates on level load, not on settings screen navigation as one might think.
+    // On activation, it reapplies all of the MPS user settings while initializing the toggles to help ensure that the
+    // previously-applied settings weren't overridden by the server or by level loads.
     class UiSettingsComponent
         : public AZ::Component
+        , public AzFramework::WindowNotificationBus::Handler
     {
     public:
         AZ_COMPONENT(UiSettingsComponent, "{6F0F5495-E766-444C-808E-4EB91AD891D6}");
@@ -50,6 +39,12 @@ namespace MultiplayerSample
         void Activate() override;
         void Deactivate() override;
     private:
+        // WindowNotificationBus overrides
+        void OnWindowResized(uint32_t width, uint32_t height) override;
+        void OnRefreshRateChanged([[maybe_unused]] uint32_t refreshRate) override;
+
+        static AzFramework::NativeWindowHandle GetWindowHandle();
+
         enum class ToggleDirection
         {
             None,
@@ -57,14 +52,23 @@ namespace MultiplayerSample
             Right
         };
 
-        void OnGraphicsApiToggle(ToggleDirection toggleDirection);
-        void OnTextureQualityToggle(ToggleDirection toggleDirection);
-        void OnMasterVolumeToggle(ToggleDirection toggleDirection);
+        void InitializeToggle(UiToggle& toggle, AZStd::function<void(UiToggle&, ToggleDirection)> toggleUpdateFn);
+
+        static void OnGraphicsApiToggle(UiToggle& toggle, ToggleDirection toggleDirection);
+        static void OnTextureQualityToggle(UiToggle& toggle, ToggleDirection toggleDirection);
+        static void OnMasterVolumeToggle(UiToggle& toggle, ToggleDirection toggleDirection);
+        static void OnFullscreenToggle(UiToggle& toggle, ToggleDirection toggleDirection);
+        static void OnResolutionToggle(UiToggle& toggle, ToggleDirection toggleDirection);
+
+        template<typename ValueType>
+        static uint32_t GetRotatedIndex(
+            const AZStd::vector<AZStd::pair<ValueType, AZStd::string>>& valuesToLabels,
+            const ValueType& value, ToggleDirection toggleDirection);
 
         UiToggle m_graphicsApiToggle;
         UiToggle m_textureQualityToggle;
         UiToggle m_masterVolumeToggle;
-
-        MpsSettings m_settings;
+        UiToggle m_fullscreenToggle;
+        UiToggle m_resolutionToggle;
     };
 }

+ 9 - 1
Gem/Code/Source/MultiplayerSampleModule.cpp

@@ -16,7 +16,7 @@
 #include <Components/BackgroundMusicComponent.h>
 #include <Components/ScriptableDecalComponent.h>
 #include <Source/AutoGen/AutoComponentTypes.h>
-#include "MultiplayerSampleSystemComponent.h"
+#include <MultiplayerSampleSystemComponent.h>
 
 #if AZ_TRAIT_CLIENT
 #   include <Components/UI/HUDComponent.h>
@@ -24,6 +24,7 @@
 #   include <Components/UI/UiRestBetweenRoundsComponent.h>
 #   include <Components/UI/UiSettingsComponent.h>
 #   include <Components/UI/UiStartMenuComponent.h>
+    #include <UserSettings/MultiplayerSampleUserSettings.h>
 #endif
 
 namespace MultiplayerSample
@@ -70,6 +71,13 @@ namespace MultiplayerSample
                 azrtti_typeid<MultiplayerSampleSystemComponent>(),
             };
         }
+
+#if AZ_TRAIT_CLIENT
+        // This needs to be created as a part of the MultiplayerSampleModule, not during any sort of System Component activation.
+        // It will affect registry keys that get read by System Components as a part of their activation and we can't guarantee
+        // that those other core System Components will get started after our game-specific one.
+        MultiplayerSampleUserSettings m_userSettings;
+#endif
     };
 }
 

+ 313 - 0
Gem/Code/Source/UserSettings/MultiplayerSampleUserSettings.cpp

@@ -0,0 +1,313 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <Atom/RPI.Public/Image/ImageSystem.h>
+#include <Atom/RPI.Public/Image/StreamingImage.h>
+#include <Atom/RPI.Public/Image/StreamingImagePool.h>
+
+#include <AzCore/Console/IConsole.h>
+#include <AzCore/IO/GenericStreams.h>
+#include <AzCore/Settings/SettingsRegistry.h>
+#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
+#include <AzCore/Utils/Utils.h>
+#include <AzFramework/FileFunc/FileFunc.h>
+#include <AzFramework/Windowing/WindowBus.h>
+#include <IAudioSystem.h>
+#include <UserSettings/MultiplayerSampleUserSettings.h>
+
+namespace MultiplayerSample
+{
+    MultiplayerSampleUserSettings::MultiplayerSampleUserSettings()
+        : m_graphicsApiKey(BaseRegistryKey + FixedString("/ApiName"))
+        , m_masterVolumeKey(BaseRegistryKey + FixedString("/MasterVolume"))
+        , m_textureQualityKey(BaseRegistryKey + FixedString("/TextureQuality"))
+        , m_fullscreenKey(BaseRegistryKey + FixedString("/Fullscreen"))
+        , m_resolutionWidthKey(BaseRegistryKey + FixedString("/Resolution/Width"))
+        , m_resolutionHeightKey(BaseRegistryKey + FixedString("/Resolution/Height"))
+    {
+        MultiplayerSampleUserSettingsRequestBus::Handler::BusConnect();
+
+        // Create a full path including filename for the user settings file.
+        m_userSettingsPath = AZ::Utils::GetProjectUserPath();
+        m_userSettingsPath /= "Registry";
+        m_userSettingsPath /= "MultiplayerSampleUserSettings.setreg";
+
+        // Load all of our settings keys, create default values if they don't exist and initialize the engine settings as appropriate.
+        Load();
+    }
+
+    MultiplayerSampleUserSettings::~MultiplayerSampleUserSettings()
+    {
+        MultiplayerSampleUserSettingsRequestBus::Handler::BusDisconnect();
+
+        // Always auto-save the user settings on destruction.
+        Save();
+    }
+
+    void MultiplayerSampleUserSettings::Load()
+    {
+        if (auto* registry = AZ::SettingsRegistry::Get(); registry != nullptr)
+        {
+            // Read the setreg file from a loose file into a string in memory. This isn't technically a "cfg" file,
+            // but the method does the exact set of steps needed here to read a loose file into memory, so even though
+            // it has a slightly misleading name, it keeps us from duplicating the code.
+            AZ::Outcome<AZStd::string, AZStd::string> userSettings = 
+                AzFramework::FileFunc::GetCfgFileContents(AZStd::string(m_userSettingsPath.FixedMaxPathString()));
+
+            if (userSettings.IsSuccess())
+            {
+                // Merge the user settings file under the base "/O3DE/MultiplayerSample/User/Settings" key.
+                // This will ensure that it cannot overwrite any other engine settings.
+                // MergeSettings() is used here instead of MergeSettingsFile() because MergeSettingsFile() uses
+                // FileIOBase to read in the file, which will attempt to read it from a PAK file in PAK file builds.
+                // Our settings file will always be a loose file, so we instead read it into a buffer beforehand and then
+                // apply it here from the in-memory buffer.
+                [[maybe_unused]] auto mergeSuccess = registry->MergeSettings(userSettings.GetValue(),
+                    AZ::SettingsRegistryInterface::Format::JsonMergePatch, BaseRegistryKey);
+
+                AZ_Error("UserSettings", mergeSuccess, "Failed to merge user settings into the O3DE registry.");
+            }
+
+            // Get the current settings values or the defaults if the keys don't exist.
+            AZStd::string apiName = GetGraphicsApi();
+            uint8_t masterVolume = GetMasterVolume();
+            int16_t textureQuality = GetTextureQuality();
+            bool fullscreen = GetFullscreen();
+            AZStd::pair<uint32_t, uint32_t> resolution = GetResolution();
+
+            // Set the settings values, which will notify the engine as well as write the keys back into the registry.
+            SetGraphicsApi(apiName);
+            SetMasterVolume(masterVolume);
+            SetTextureQuality(textureQuality);
+            SetFullscreen(fullscreen);
+            SetResolution(resolution);
+        }
+    }
+
+    AZStd::string MultiplayerSampleUserSettings::GetGraphicsApi()
+    {
+        // Default to an empty string, which will just use the default API.
+        AZStd::string apiName;
+
+        if (auto* registry = AZ::SettingsRegistry::Get(); registry != nullptr)
+        {
+            registry->Get(apiName, m_graphicsApiKey.c_str());
+        }
+
+        return apiName;
+    }
+
+    void MultiplayerSampleUserSettings::SetGraphicsApi(const AZStd::string& apiName)
+    {
+        if (auto* registry = AZ::SettingsRegistry::Get(); registry != nullptr)
+        {
+            // Set the requested api name as the highest (and only) user priority in the registry.
+            // Atom will select this api at startup as long as it exists and nothing was passed in via command-line.
+            // If the passed-in apiName is empty, just let Atom use its standard default priorities for api selection.
+            // If the passed-in apiName doesn't match one supported by Atom on this platform, Atom will ignore it and use
+            // its standard default priorities as well.
+            if (!apiName.empty())
+            {
+                AZStd::vector<AZStd::string> factoriesPriority;
+                factoriesPriority.emplace_back(apiName);
+                registry->SetObject("/O3DE/Atom/RHI/FactoryManager/factoriesPriority", factoriesPriority);
+            }
+
+            registry->Set(m_graphicsApiKey.c_str(), apiName);
+        }
+    }
+
+    uint8_t MultiplayerSampleUserSettings::GetMasterVolume()
+    {
+        // Default to full volume (100)
+        AZ::u64 masterVolume = 100;
+
+        if (auto* registry = AZ::SettingsRegistry::Get(); registry != nullptr)
+        {
+            registry->Get(masterVolume, m_masterVolumeKey.c_str());
+        }
+
+        // Make sure any hand-edited registry values stay within a valid range.
+        return AZStd::clamp(aznumeric_cast<uint8_t>(masterVolume), aznumeric_cast<uint8_t>(0), aznumeric_cast<uint8_t>(100));
+    }
+
+    void MultiplayerSampleUserSettings::SetMasterVolume(uint8_t masterVolume)
+    {
+        if (auto* registry = AZ::SettingsRegistry::Get(); registry != nullptr)
+        {
+            // Send a request to the audio system to change the master volume.
+            auto audioSystem = AZ::Interface<Audio::IAudioSystem>::Get();
+            if (audioSystem)
+            {
+                Audio::TAudioObjectID rtpcId = audioSystem->GetAudioRtpcID("Volume_Master");
+
+                if (rtpcId != INVALID_AUDIO_CONTROL_ID)
+                {
+                    Audio::ObjectRequest::SetParameterValue setParameter;
+                    setParameter.m_audioObjectId = INVALID_AUDIO_OBJECT_ID;
+                    setParameter.m_parameterId = rtpcId;
+                    // Master volume in the audio system is expected to be 0.0 (min) - 1.0 (max), but we're using 0 - 100 as integers,
+                    // so convert it from 0 - 100 to the 0 - 1 range.
+                    setParameter.m_value = masterVolume / 100.0f;
+                    AZ::Interface<Audio::IAudioSystem>::Get()->PushRequest(AZStd::move(setParameter));
+                }
+            }
+
+            registry->Set(m_masterVolumeKey.c_str(), aznumeric_cast<AZ::u64>(masterVolume));
+        }
+    }
+
+    int16_t MultiplayerSampleUserSettings::GetTextureQuality()
+    {
+        AZ::s64 textureQuality = 1;
+
+        if (auto* registry = AZ::SettingsRegistry::Get(); registry != nullptr)
+        {
+            registry->Get(textureQuality, m_textureQualityKey.c_str());
+        }
+
+        return AZStd::clamp(aznumeric_cast<int16_t>(textureQuality), aznumeric_cast<int16_t>(0), aznumeric_cast<int16_t>(10));
+    }
+
+    void MultiplayerSampleUserSettings::SetTextureQuality(int16_t textureQuality)
+    {
+        if (auto* registry = AZ::SettingsRegistry::Get(); registry != nullptr)
+        {
+            if (auto* imageSystem = AZ::RPI::ImageSystemInterface::Get())
+            {
+                AZ::Data::Instance<AZ::RPI::StreamingImagePool> pool = imageSystem->GetSystemStreamingPool();
+                pool->SetMipBias(textureQuality);
+            }
+
+            registry->Set(m_textureQualityKey.c_str(), aznumeric_cast<AZ::s64>(textureQuality));
+        }
+    }
+
+    bool MultiplayerSampleUserSettings::GetFullscreen()
+    {
+        bool fullscreen = false;
+
+        if (auto* registry = AZ::SettingsRegistry::Get(); registry != nullptr)
+        {
+            registry->Get(fullscreen, m_fullscreenKey.c_str());
+        }
+
+        return fullscreen;
+    }
+
+    void MultiplayerSampleUserSettings::SetFullscreen(bool fullscreen)
+    {
+        if (auto* registry = AZ::SettingsRegistry::Get(); registry != nullptr)
+        {
+            if (AZ::IConsole* console = AZ::Interface<AZ::IConsole>::Get(); console)
+            {
+                // Change the fullscreen state if we haven't created the window yet.
+                AZ::CVarFixedString commandString = AZ::CVarFixedString::format("r_fullscreen %u", fullscreen ? 1 : 0);
+                console->PerformCommand(commandString.c_str());
+
+                // Change the fullscreen state if the window already exists
+                AzFramework::NativeWindowHandle windowHandle = nullptr;
+                AzFramework::WindowSystemRequestBus::BroadcastResult(
+                    windowHandle,
+                    &AzFramework::WindowSystemRequestBus::Events::GetDefaultWindowHandle);
+
+                AzFramework::WindowRequestBus::Event(
+                    windowHandle,
+                    &AzFramework::WindowRequestBus::Events::SetFullScreenState, fullscreen);
+            }
+
+            registry->Set(m_fullscreenKey.c_str(), fullscreen);
+        }
+    }
+
+    AZStd::pair<uint32_t, uint32_t> MultiplayerSampleUserSettings::GetResolution()
+    {
+        AZ::u64 width = 1920;
+        AZ::u64 height = 1080;
+
+        if (auto* registry = AZ::SettingsRegistry::Get(); registry != nullptr)
+        {
+            registry->Get(width, m_resolutionWidthKey.c_str());
+            registry->Get(height, m_resolutionHeightKey.c_str());
+        }
+
+        return { aznumeric_cast<uint32_t>(width), aznumeric_cast<uint32_t>(height) };
+    }
+
+    void MultiplayerSampleUserSettings::SetResolution(AZStd::pair<uint32_t, uint32_t> resolution)
+    {
+        if (auto* registry = AZ::SettingsRegistry::Get(); registry != nullptr)
+        {
+            if (AZ::IConsole* console = AZ::Interface<AZ::IConsole>::Get(); console)
+            {
+                // This will technically change the window resolution to whatever is requrested, but it should 
+                // ideally take into account the current DPI scaling and what the maximum resolution of the monitor is.
+                
+                // Change the resolution if the window doesn't exist yet.
+                AZ::CVarFixedString commandString = AZ::CVarFixedString::format("r_width %u", resolution.first);
+                console->PerformCommand(commandString.c_str());
+
+                commandString = AZ::CVarFixedString::format("r_height %u", resolution.second);
+                console->PerformCommand(commandString.c_str());
+
+                // Change the resolution if the window already exists.
+                AzFramework::NativeWindowHandle windowHandle = nullptr;
+                AzFramework::WindowSystemRequestBus::BroadcastResult(
+                    windowHandle,
+                    &AzFramework::WindowSystemRequestBus::Events::GetDefaultWindowHandle);
+
+                bool fullscreen = false;
+                AzFramework::WindowRequestBus::EventResult(
+                    fullscreen, windowHandle,
+                    &AzFramework::WindowRequestBus::Events::GetFullScreenState);
+
+                // Don't resize if we're in fullscreen mode.
+                if (!fullscreen)
+                {
+                    AzFramework::WindowRequestBus::Event(
+                        windowHandle,
+                        &AzFramework::WindowRequestBus::Events::ResizeClientArea,
+                        AzFramework::WindowSize(resolution.first, resolution.second), AzFramework::WindowPosOptions());
+                }
+            }
+
+            registry->Set(m_resolutionWidthKey.c_str(), aznumeric_cast<AZ::u64>(resolution.first));
+            registry->Set(m_resolutionHeightKey.c_str(), aznumeric_cast<AZ::u64>(resolution.second));
+        }
+    }
+
+    void MultiplayerSampleUserSettings::Save()
+    {
+        AZ::IO::FixedMaxPath userSettingsSavePath = m_userSettingsPath;
+        userSettingsSavePath.ReplaceExtension("setreg.tmp");
+
+        constexpr AZ::IO::OpenMode openMode = AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath;
+
+        // Write to a temporary file and then move the file to the final location
+        if (AZ::IO::SystemFileStream userSettingsStream(userSettingsSavePath.c_str(), openMode); userSettingsStream.IsOpen())
+        {
+            auto settingsRegistry = AZ::SettingsRegistry::Get();
+
+            // Remove the .tmp extension from the user settings path
+            // This results in the final path where the settings will actually be saved
+            userSettingsSavePath.ReplaceExtension();
+            AZ::SettingsRegistryMergeUtils::DumperSettings dumperSettings;
+            dumperSettings.m_prettifyOutput = true;
+            if (AZ::SettingsRegistryMergeUtils::DumpSettingsRegistryToStream(
+                *settingsRegistry, BaseRegistryKey, userSettingsStream, dumperSettings))
+            {
+                // Use SystemFile::Rename to move the file to the final destination
+                userSettingsStream.Close();
+                bool renameSuccess = AZ::IO::SystemFile::Rename(userSettingsStream.GetFilename(), userSettingsSavePath.c_str(), true);
+                AZ_Error("UserSettings", renameSuccess, 
+                    "Renaming '%s' to '%s' failed.", userSettingsStream.GetFilename(), userSettingsSavePath.c_str());
+            }
+        }
+    }
+
+} // namespace MultiplayerSample

+ 109 - 0
Gem/Code/Source/UserSettings/MultiplayerSampleUserSettings.h

@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+#include <AzCore/EBus/EBus.h>
+#include <AzCore/IO/Path/Path.h>
+
+namespace MultiplayerSample
+{
+    // This provides a way to get/set every user setting that MultiplayerSample supports, and to save the user settings file.
+    // Getting the values pulls them out of the saved user settings data, and setting the values both sets them in the user
+    // settings and communicates the change to the appropriate part of the game engine to make the change take effect.
+    class MultiplayerSampleUserSettingsRequests
+        : public AZ::EBusTraits
+    {
+    public:
+        static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
+
+        virtual ~MultiplayerSampleUserSettingsRequests() = default;
+
+        // Load the user settings and refresh the game engine based on the settings. They automatically get loaded and applied
+        // on launcher startup, but this might need to be called to refresh the settings after connecting to the server and loading
+        // the level in case any engine systems get reset by server cvars and level data.
+        virtual void Load() = 0;
+
+        // Save the user settings file out to disk.
+        virtual void Save() = 0;
+
+        // Change the default graphics API between dx12/vulkan/metal/null on the next restart of the game.
+        virtual AZStd::string GetGraphicsApi() = 0;
+        virtual void SetGraphicsApi(const AZStd::string& apiName) = 0;
+
+        // Change the master volume from 0 - 100.
+        virtual uint8_t GetMasterVolume() = 0;
+        virtual void SetMasterVolume(uint8_t masterVolume) = 0;
+
+        // Change the texture quality. 0 = highest quality (highest mipmap), N = lowest quality (lowest mipmap).
+        // There's no well-defined value for lowest quality so we'll just arbitrarily cap it at 6 (64x64 if mip 0 is 4096x4096). 
+        // Anything lower doesn't really provide any benefit.
+        virtual int16_t GetTextureQuality() = 0;
+        virtual void SetTextureQuality(int16_t textureQuality) = 0;
+
+        // Change between fullscreen and windowed.
+        virtual bool GetFullscreen() = 0;
+        virtual void SetFullscreen(bool fullscreen) = 0;
+
+        // Change the rendering resolution (width, height)
+        virtual AZStd::pair<uint32_t, uint32_t> GetResolution() = 0;
+        virtual void SetResolution(AZStd::pair<uint32_t, uint32_t> resolution) = 0;
+    };
+
+    using MultiplayerSampleUserSettingsRequestBus = AZ::EBus<MultiplayerSampleUserSettingsRequests>;
+
+    // This implements the bus provided above. The user settings get auto-loaded at construction and auto-saved at destruction,
+    // though saves can also be triggered at other times as well. Because one of the settings is the default graphics API, these
+    // settings need to be loaded before system components are initialized because the Atom system components load the graphics
+    // API. All of the other settings are changeable at any time and would have allowed this class to get created later in the
+    // boot process.
+    class MultiplayerSampleUserSettings : public MultiplayerSampleUserSettingsRequestBus::Handler
+    {
+    public:
+        MultiplayerSampleUserSettings();
+        ~MultiplayerSampleUserSettings() override;
+
+        void Load() override;
+        void Save() override;
+
+        AZStd::string GetGraphicsApi() override;
+        void SetGraphicsApi(const AZStd::string& apiName) override;
+
+        uint8_t GetMasterVolume() override;
+        void SetMasterVolume(uint8_t masterVolume) override;
+
+        int16_t GetTextureQuality() override;
+        void SetTextureQuality(int16_t textureQuality) override;
+
+        bool GetFullscreen() override;
+        void SetFullscreen(bool fullscreen) override;
+
+
+        AZStd::pair<uint32_t, uint32_t> GetResolution() override;
+        void SetResolution(AZStd::pair<uint32_t, uint32_t> resolution) override;
+
+    private:
+        using FixedString = AZStd::fixed_string<256>;
+
+        // The base registry key that all our user settings will live underneath.
+        // We keep them separate from the rest of the registry hierarchy to ensure that users can't
+        // edit their settings file by hand to overwrite any other registry keys that weren't intentionally exposed.
+        static inline constexpr FixedString BaseRegistryKey = "/O3DE/MultiplayerSample/User/Settings";
+
+        // These keep track of the specific registry keys used for each setting.
+        const FixedString m_graphicsApiKey;
+        const FixedString m_textureQualityKey;
+        const FixedString m_masterVolumeKey;
+        const FixedString m_fullscreenKey;
+        const FixedString m_resolutionWidthKey;
+        const FixedString m_resolutionHeightKey;
+
+        // The path to the user settings file.
+        AZ::IO::FixedMaxPath m_userSettingsPath;
+    };
+
+} // namespace MultiplayerSample

+ 3 - 0
Gem/Code/multiplayersample_shared_files.cmake

@@ -6,5 +6,8 @@
 #
 
 set(FILES
+
     Source/MultiplayerSampleModule.cpp
+    Source/UserSettings/MultiplayerSampleUserSettings.h
+    Source/UserSettings/MultiplayerSampleUserSettings.cpp
 )

+ 22 - 67
Levels/GameplayTest/GameplayTest.prefab

@@ -120,14 +120,14 @@
                     "Entity_[15464591867054]",
                     "Entity_[15473181801646]",
                     "Entity_[15481771736238]",
-                    "Entity_[873004712589]",
                     "Instance_[1238918361381]/ContainerEntity",
                     "Instance_[990233405078]/ContainerEntity",
                     "Instance_[44514490248300]/ContainerEntity",
                     "Instance_[2090718165100]/ContainerEntity",
                     "Entity_[58675766534237]",
                     "Entity_[65620728651869]",
-                    "Instance_[6302654760100]/ContainerEntity"
+                    "Instance_[6302654760100]/ContainerEntity",
+                    "Instance_[10391347535934]/ContainerEntity"
                 ]
             }
         }
@@ -11842,17 +11842,6 @@
                         "$type": "MultiplayerSample::MatchPlayerCoinsComponent"
                     }
                 },
-                "Component_[3709937876037180985]": {
-                    "$type": "GenericComponentWrapper",
-                    "Id": 3709937876037180985,
-                    "m_template": {
-                        "$type": "UiCanvasAssetRefComponent",
-                        "CanvasAssetRef": {
-                            "AssetPath": "uicanvases/basichud.uicanvas"
-                        },
-                        "IsAutoLoad": true
-                    }
-                },
                 "Component_[5143246854913296527]": {
                     "$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
                     "Id": 5143246854913296527,
@@ -12040,60 +12029,6 @@
                     "Id": 9914289529455341679
                 }
             }
-        },
-        "Entity_[873004712589]": {
-            "Id": "Entity_[873004712589]",
-            "Name": "In Game Menu",
-            "Components": {
-                "Component_[10973769579758943127]": {
-                    "$type": "EditorInspectorComponent",
-                    "Id": 10973769579758943127
-                },
-                "Component_[13892978883605229067]": {
-                    "$type": "EditorPendingCompositionComponent",
-                    "Id": 13892978883605229067
-                },
-                "Component_[16221779430861293146]": {
-                    "$type": "EditorDisabledCompositionComponent",
-                    "Id": 16221779430861293146
-                },
-                "Component_[16377069407396552235]": {
-                    "$type": "EditorVisibilityComponent",
-                    "Id": 16377069407396552235
-                },
-                "Component_[199753144438342893]": {
-                    "$type": "EditorEntitySortComponent",
-                    "Id": 199753144438342893
-                },
-                "Component_[2280755623652026535]": {
-                    "$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
-                    "Id": 2280755623652026535,
-                    "Parent Entity": "Entity_[356758116574]"
-                },
-                "Component_[4368122420119951993]": {
-                    "$type": "EditorEntityIconComponent",
-                    "Id": 4368122420119951993
-                },
-                "Component_[438967016699456639]": {
-                    "$type": "EditorLockComponent",
-                    "Id": 438967016699456639
-                },
-                "Component_[5604360131636598685]": {
-                    "$type": "EditorOnlyEntityComponent",
-                    "Id": 5604360131636598685
-                },
-                "Component_[9355518200202427665]": {
-                    "$type": "GenericComponentWrapper",
-                    "Id": 9355518200202427665,
-                    "m_template": {
-                        "$type": "UiCanvasAssetRefComponent",
-                        "CanvasAssetRef": {
-                            "AssetPath": "uicanvases/ingamemenu.uicanvas"
-                        },
-                        "IsAutoLoad": true
-                    }
-                }
-            }
         }
     },
     "Instances": {
@@ -12122,6 +12057,26 @@
                 }
             ]
         },
+        "Instance_[10391347535934]": {
+            "Source": "Prefabs/UI.prefab",
+            "Patches": [
+                {
+                    "op": "replace",
+                    "path": "/ContainerEntity/Components/Component_[11968026641926863763]/Parent Entity",
+                    "value": "../Entity_[356758116574]"
+                },
+                {
+                    "op": "replace",
+                    "path": "/ContainerEntity/Components/Component_[11968026641926863763]/Transform Data/Translate/1",
+                    "value": 12.499994277954102
+                },
+                {
+                    "op": "replace",
+                    "path": "/ContainerEntity/Components/Component_[11968026641926863763]/Transform Data/Translate/2",
+                    "value": 4.017059803009033
+                }
+            ]
+        },
         "Instance_[1154437282147]": {
             "Source": "Prefabs/Diamond_Gem.prefab",
             "Patches": [

+ 37 - 11
README.md

@@ -20,9 +20,10 @@ Game features:
 * Jump pads to boost players high into the air
 * A configurable number of rounds (default: 3 rounds)
 * Configurable gem spawning patterns per round to drive player exploration
-* Support for 1 to 15 players
+* Support for 1 to 10 players
 * Rich sounds and visual effects support
-* Teleporters to aid player exploration
+* Teleporters to aid player exploration and to demonstrate moving players.
+* [User Settings screen](Documentation/SettingsScreen.md)
 * Many points of extensibility
 
 > A player can win the whole game early by reaching a score of 400. See the [Gameplay Configuration](Documentation/GamplayConfiguration.md) docs.
@@ -73,7 +74,7 @@ These instructions use the following installation paths. Be sure to substitute y
    Cloning into 'o3de-multiplayersample'...
    ```
 
-3. Clone the assets. In this example the assets are cloned beside the muliplayersample project.
+3. Clone the assets. In this example the assets are cloned beside the multiplayersample project.
 
    ```shell
    git clone https://github.com/o3de/o3de-multiplayersample-assets.git
@@ -117,6 +118,28 @@ These instructions use the following installation paths. Be sure to substitute y
    echo o3de-multiplayersample-assets > C:/o3de/.git/info/exclude
    ```
 
+### Step 1a. Ensure your branches match
+
+Before building the project, ensure that o3de, o3de-multiplayersample and o3de-multiplayersample-assets are all cloned from the same named branches. For example, if you are using the **development** branch of o3de-multiplayersample, then it must be matched with the **development** branches of o3de, and o3de-multiplayersample-assets. Ensure that you update the submodules in o3de-multiplayersample-assets when switching branches in that repository.
+
+If you're using a release or installer version of O3DE, then you must checkout versions of the sample repositories that match the release. O3DE uses standard Git [tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging) to identify the release-compatible version of each repository. 
+
+For each O3DE release, repositories that have been updated to match the release should have a matching tag for the release. You can see all defined tags using the [Tags](https://github.com/o3de/o3de-multiplayersample/tags] view in each repository.
+
+Branches can be checked out using standard Git commands, for example, `git checkout tags/<tag> -b <local branch name>`.
+
+### Step 1b. Verify you have the LFS files.
+
+Verify that you have all the files from the LFS endpoint. For each cloned repository, run:
+
+```
+git lfs pull
+```
+
+If using your own fork, complete LFS setup by updating the [LFS Url](https://www.o3de.org/docs/welcome-guide/setup/setup-from-github/#fork-and-clone). 
+
+If you have problems with working with LFS, see the troubleshooting guide: https://github.com/o3de/o3de/wiki/Git-LFS-Troubleshooting.
+
 ## Step 2. Register the engine, the project, and the Gems
 
 ### Option #1 - Use the CLI
@@ -149,7 +172,7 @@ If you've already built the O3DE engine, use the O3DE project manager to open an
 
 1. Run `o3de.exe`. If you used the engine build instructions from the [Getting Started](https://www.o3de.org/docs/welcome-guide/) guide, `o3de.exe` can be found at `C:/o3de/build/windows/bin/profile/o3de.exe`.
 
-1. (Optional) If MultiplayerSample is not in the **My Projects** view, then click the **New Project...** drop down and select **Open Existing Project**. Select the o3de-mulitplayersample project. See the [Project Manager User Guide](https://www.o3de.org/docs/user-guide/project-config/project-manager/#projects) for details.
+1. (Optional) If MultiplayerSample is not in the **My Projects** view, then click the **New Project...** drop down and select **Open Existing Project**. Select the o3de-multiplayersample project. See the [Project Manager User Guide](https://www.o3de.org/docs/user-guide/project-config/project-manager/#projects) for details.
 
 1. You can choose **Build** in Project Manager to build the project, and skip the following **Step 3. Configure and build** steps.
 
@@ -279,9 +302,9 @@ When debugging set `net_UdpTimeoutConnections` to false. This prevents connectio
 
 This project ships with several levels, the ones of note are:
 
-1. `NewStarBase` - The main game level. Also the default level.
-2. `GamePlayTest` - Everything needed for gameplay, but in a tiny, fast-loading level. All game objects (Gems, HUD, and so on) are included.
-3. `StartMenu` - An example menu to join, host, and connect to servers.
+1. `NewStarBase` - The main game level (the default level for gameplay).
+2. `StartMenu` - An example menu to join, host, and connect to servers.
+3. `GamePlayTest` - Everything needed for gameplay, but in a tiny, fast-loading level. All game objects (Gems, HUD, and so on) are included.
 4. `MultiplayerScriptingSample` - An example of scripts for Multiplayer.
 
 Other levels in the project are used for testing or performance evaluation purposes and are considered experimental.
@@ -296,10 +319,13 @@ You can contribute by [reporting issues and making feature requests](https://git
 
 ## Documentation
 
-| Link                                                            | Description                       |
-|-----------------------------------------------------------------|-----------------------------------|
-| [README_LINUX](README_LINUX.md)                                 | Linux specific setup instructions |
-| [Gameplay Configuration](Documentation/GamplayConfiguration.md) | How to adjust gameplay settings   |
+| Link                                                            | Description                                                               |
+|-----------------------------------------------------------------|---------------------------------------------------------------------------|
+| [README_LINUX](README_LINUX.md)                                 | Linux specific setup instructions                                         |
+| [Release Notes](Documentation/ReleaseNotes.md)                  | Release notes and known issues per major release                          |
+| [Gameplay Configuration](Documentation/GamplayConfiguration.md) | How to adjust gameplay settings                                           |
+| [SettingsScreen](Documentation/SettingsScreen.md)               | How to use and extend the settings screen                                 |
+| [Packaging MPS](Documentation/PackedAssetBuilds.md)             | How to build and package MPS for distribution or running servers remotely |
 
 ## O3DE Useful Links
 

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 273 - 301
UICanvases/Settings.uicanvas


+ 239 - 63
scriptcanvas/ClientDisconnect.scriptcanvas

@@ -5,7 +5,7 @@
     "ClassData": {
         "m_scriptCanvas": {
             "Id": {
-                "id": 63723511289631
+                "id": 41296274672356
             },
             "Name": "Script Canvas Graph",
             "Components": {
@@ -16,7 +16,7 @@
                         "m_nodes": [
                             {
                                 "Id": {
-                                    "id": 63744986126111
+                                    "id": 41317749508836
                                 },
                                 "Name": "SC-Node(ExecuteConsoleCommand)",
                                 "Components": {
@@ -100,7 +100,7 @@
                             },
                             {
                                 "Id": {
-                                    "id": 63740691158815
+                                    "id": 41313454541540
                                 },
                                 "Name": "EBusEventHandler",
                                 "Components": {
@@ -341,7 +341,88 @@
                             },
                             {
                                 "Id": {
-                                    "id": 63736396191519
+                                    "id": 47163199998692
+                                },
+                                "Name": "SC-Node(ExecuteConsoleCommand)",
+                                "Components": {
+                                    "Component_[17382034980001092472]": {
+                                        "$type": "{E42861BD-1956-45AE-8DD7-CCFC1E3E5ACF} Method",
+                                        "Id": 17382034980001092472,
+                                        "Slots": [
+                                            {
+                                                "id": {
+                                                    "m_id": "{365E1DAE-26DC-42E8-9DC2-E4AE1A04B08F}"
+                                                },
+                                                "contracts": [
+                                                    {
+                                                        "$type": "SlotTypeContract"
+                                                    }
+                                                ],
+                                                "slotName": "String",
+                                                "Descriptor": {
+                                                    "ConnectionType": 1,
+                                                    "SlotType": 2
+                                                },
+                                                "DataType": 1
+                                            },
+                                            {
+                                                "id": {
+                                                    "m_id": "{E061744B-A882-4069-B606-F3F0036C2941}"
+                                                },
+                                                "contracts": [
+                                                    {
+                                                        "$type": "SlotTypeContract"
+                                                    }
+                                                ],
+                                                "slotName": "In",
+                                                "Descriptor": {
+                                                    "ConnectionType": 1,
+                                                    "SlotType": 1
+                                                }
+                                            },
+                                            {
+                                                "id": {
+                                                    "m_id": "{C7F2B1D8-93A2-4B0B-98B2-BFDE5E7E34B1}"
+                                                },
+                                                "contracts": [
+                                                    {
+                                                        "$type": "SlotTypeContract"
+                                                    }
+                                                ],
+                                                "slotName": "Out",
+                                                "Descriptor": {
+                                                    "ConnectionType": 2,
+                                                    "SlotType": 1
+                                                }
+                                            }
+                                        ],
+                                        "Datums": [
+                                            {
+                                                "isOverloadedStorage": false,
+                                                "scriptCanvasType": {
+                                                    "m_type": 5
+                                                },
+                                                "isNullPointer": false,
+                                                "$type": "{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9} AZStd::string",
+                                                "value": "disconnect",
+                                                "label": "Command"
+                                            }
+                                        ],
+                                        "methodType": 0,
+                                        "methodName": "ExecuteConsoleCommand",
+                                        "className": "ConsoleRequestBus",
+                                        "inputSlots": [
+                                            {
+                                                "m_id": "{365E1DAE-26DC-42E8-9DC2-E4AE1A04B08F}"
+                                            }
+                                        ],
+                                        "prettyClassName": "ConsoleRequestBus"
+                                    }
+                                }
+                            },
+                            {
+                                "Id": {
+                                    "id": 41309159574244
                                 },
                                 "Name": "SC-Node(EqualTo)",
                                 "Components": {
@@ -490,7 +571,7 @@
                             },
                             {
                                 "Id": {
-                                    "id": 63732101224223
+                                    "id": 41304864606948
                                 },
                                 "Name": "SC-Node(TimeDelayNodeableNode)",
                                 "Components": {
@@ -631,7 +712,7 @@
                             },
                             {
                                 "Id": {
-                                    "id": 63727806256927
+                                    "id": 41300569639652
                                 },
                                 "Name": "ReceiveScriptEvent",
                                 "Components": {
@@ -929,35 +1010,7 @@
                         "m_connections": [
                             {
                                 "Id": {
-                                    "id": 63749281093407
-                                },
-                                "Name": "srcEndpoint=(TimeDelay: Done), destEndpoint=(ExecuteConsoleCommand: In)",
-                                "Components": {
-                                    "Component_[18339910822672896418]": {
-                                        "$type": "{64CA5016-E803-4AC4-9A36-BDA2C890C6EB} Connection",
-                                        "Id": 18339910822672896418,
-                                        "sourceEndpoint": {
-                                            "nodeId": {
-                                                "id": 63732101224223
-                                            },
-                                            "slotId": {
-                                                "m_id": "{5BB44F12-9009-4193-961B-8E2F6CC538FB}"
-                                            }
-                                        },
-                                        "targetEndpoint": {
-                                            "nodeId": {
-                                                "id": 63744986126111
-                                            },
-                                            "slotId": {
-                                                "m_id": "{8C46EFC2-676D-4B14-A5F0-B58404C64CD0}"
-                                            }
-                                        }
-                                    }
-                                }
-                            },
-                            {
-                                "Id": {
-                                    "id": 63753576060703
+                                    "id": 41326339443428
                                 },
                                 "Name": "srcEndpoint=(EntityBus Handler: ExecutionSlot:OnEntityActivated), destEndpoint=(Receive Script Event: Connect)",
                                 "Components": {
@@ -966,7 +1019,7 @@
                                         "Id": 9151944094509384703,
                                         "sourceEndpoint": {
                                             "nodeId": {
-                                                "id": 63740691158815
+                                                "id": 41313454541540
                                             },
                                             "slotId": {
                                                 "m_id": "{ECD88DEB-EE0D-4ECC-8583-48E97A5113B2}"
@@ -974,7 +1027,7 @@
                                         },
                                         "targetEndpoint": {
                                             "nodeId": {
-                                                "id": 63727806256927
+                                                "id": 41300569639652
                                             },
                                             "slotId": {
                                                 "m_id": "{739ED44A-BACE-4B23-9F76-DED2F3A7457C}"
@@ -985,7 +1038,7 @@
                             },
                             {
                                 "Id": {
-                                    "id": 63757871027999
+                                    "id": 41330634410724
                                 },
                                 "Name": "srcEndpoint=(Receive Script Event: ScreenToShow), destEndpoint=(Equal To (==): Value A)",
                                 "Components": {
@@ -994,7 +1047,7 @@
                                         "Id": 17600078873613684267,
                                         "sourceEndpoint": {
                                             "nodeId": {
-                                                "id": 63727806256927
+                                                "id": 41300569639652
                                             },
                                             "slotId": {
                                                 "m_id": "{25FC9880-C3C6-4432-AA4F-1C894497F22D}"
@@ -1002,7 +1055,7 @@
                                         },
                                         "targetEndpoint": {
                                             "nodeId": {
-                                                "id": 63736396191519
+                                                "id": 41309159574244
                                             },
                                             "slotId": {
                                                 "m_id": "{DBE1F264-1D99-4604-8070-B4ADFC8A2A1C}"
@@ -1013,7 +1066,7 @@
                             },
                             {
                                 "Id": {
-                                    "id": 63762165995295
+                                    "id": 41334929378020
                                 },
                                 "Name": "srcEndpoint=(Equal To (==): True), destEndpoint=(TimeDelay: Start)",
                                 "Components": {
@@ -1022,7 +1075,7 @@
                                         "Id": 10150836379398510596,
                                         "sourceEndpoint": {
                                             "nodeId": {
-                                                "id": 63736396191519
+                                                "id": 41309159574244
                                             },
                                             "slotId": {
                                                 "m_id": "{4C285E52-9795-47E5-90A5-70936454D2AF}"
@@ -1030,7 +1083,7 @@
                                         },
                                         "targetEndpoint": {
                                             "nodeId": {
-                                                "id": 63732101224223
+                                                "id": 41304864606948
                                             },
                                             "slotId": {
                                                 "m_id": "{89844691-7E99-4E4B-A2CC-7920469B3351}"
@@ -1041,7 +1094,7 @@
                             },
                             {
                                 "Id": {
-                                    "id": 71514581964575
+                                    "id": 41339224345316
                                 },
                                 "Name": "srcEndpoint=(Receive Script Event: ExecutionSlot:SetActiveScreen), destEndpoint=(Equal To (==): In)",
                                 "Components": {
@@ -1050,7 +1103,7 @@
                                         "Id": 5974153072275692349,
                                         "sourceEndpoint": {
                                             "nodeId": {
-                                                "id": 63727806256927
+                                                "id": 41300569639652
                                             },
                                             "slotId": {
                                                 "m_id": "{5C46F887-23F6-4A14-9ADD-03B5E0543328}"
@@ -1058,7 +1111,7 @@
                                         },
                                         "targetEndpoint": {
                                             "nodeId": {
-                                                "id": 63736396191519
+                                                "id": 41309159574244
                                             },
                                             "slotId": {
                                                 "m_id": "{DC73B288-02D4-4667-A0E5-3ED395DF8DC2}"
@@ -1066,12 +1119,68 @@
                                         }
                                     }
                                 }
+                            },
+                            {
+                                "Id": {
+                                    "id": 52205491604196
+                                },
+                                "Name": "srcEndpoint=(TimeDelay: Done), destEndpoint=(ExecuteConsoleCommand: In)",
+                                "Components": {
+                                    "Component_[15414995311809178445]": {
+                                        "$type": "{64CA5016-E803-4AC4-9A36-BDA2C890C6EB} Connection",
+                                        "Id": 15414995311809178445,
+                                        "sourceEndpoint": {
+                                            "nodeId": {
+                                                "id": 41304864606948
+                                            },
+                                            "slotId": {
+                                                "m_id": "{5BB44F12-9009-4193-961B-8E2F6CC538FB}"
+                                            }
+                                        },
+                                        "targetEndpoint": {
+                                            "nodeId": {
+                                                "id": 47163199998692
+                                            },
+                                            "slotId": {
+                                                "m_id": "{E061744B-A882-4069-B606-F3F0036C2941}"
+                                            }
+                                        }
+                                    }
+                                }
+                            },
+                            {
+                                "Id": {
+                                    "id": 53103139769060
+                                },
+                                "Name": "srcEndpoint=(ExecuteConsoleCommand: Out), destEndpoint=(ExecuteConsoleCommand: In)",
+                                "Components": {
+                                    "Component_[13185958598790311973]": {
+                                        "$type": "{64CA5016-E803-4AC4-9A36-BDA2C890C6EB} Connection",
+                                        "Id": 13185958598790311973,
+                                        "sourceEndpoint": {
+                                            "nodeId": {
+                                                "id": 47163199998692
+                                            },
+                                            "slotId": {
+                                                "m_id": "{C7F2B1D8-93A2-4B0B-98B2-BFDE5E7E34B1}"
+                                            }
+                                        },
+                                        "targetEndpoint": {
+                                            "nodeId": {
+                                                "id": 41317749508836
+                                            },
+                                            "slotId": {
+                                                "m_id": "{8C46EFC2-676D-4B14-A5F0-B58404C64CD0}"
+                                            }
+                                        }
+                                    }
+                                }
                             }
                         ],
                         "m_scriptEventAssets": [
                             [
                                 {
-                                    "id": 63727806256927
+                                    "id": 41300569639652
                                 },
                                 {}
                             ]
@@ -1086,13 +1195,49 @@
                     "GraphCanvasData": [
                         {
                             "Key": {
-                                "id": 63723511289631
+                                "id": 41296274672356
                             },
                             "Value": {
                                 "ComponentData": {
                                     "{5F84B500-8C45-40D1-8EFC-A5306B241444}": {
                                         "$type": "SceneComponentSaveData",
                                         "Constructs": [
+                                            {
+                                                "Type": 1,
+                                                "DataContainer": {
+                                                    "ComponentData": {
+                                                        "{24CB38BB-1705-4EC5-8F63-B574571B4DCD}": {
+                                                            "$type": "NodeSaveData"
+                                                        },
+                                                        "{524D8380-AC09-444E-870E-9CEF2535B4A2}": {
+                                                            "$type": "CommentNodeTextSaveData",
+                                                            "Comment": "The \"disconnect\" is required here because once a client has connected to a server for the first time, it won't be able to initiate a level load without calling a client-side disconnect to reset the state.",
+                                                            "BackgroundColor": [
+                                                                0.9800000190734863,
+                                                                0.9700000286102295,
+                                                                0.6499999761581421
+                                                            ],
+                                                            "FontSettings": {
+                                                                "PixelSize": 16
+                                                            }
+                                                        },
+                                                        "{7CC444B1-F9B3-41B5-841B-0C4F2179F111}": {
+                                                            "$type": "GeometrySaveData",
+                                                            "Position": [
+                                                                960.0,
+                                                                220.0
+                                                            ]
+                                                        },
+                                                        "{B0B99C8A-03AF-4CF6-A926-F65C874C3D97}": {
+                                                            "$type": "StylingComponentSaveData"
+                                                        },
+                                                        "{B1F49A35-8408-40DA-B79E-F1E3B64322CE}": {
+                                                            "$type": "PersistentIdComponentSaveData",
+                                                            "PersistentId": "{587F771B-FC38-4C05-8A31-6002D71FF144}"
+                                                        }
+                                                    }
+                                                }
+                                            },
                                             {
                                                 "Type": 3,
                                                 "DataContainer": {
@@ -1114,23 +1259,23 @@
                                                         },
                                                         "{6F4811ED-BD83-4A2A-8831-58EEA4020D57}": {
                                                             "$type": "NodeGroupFrameComponentSaveData",
-                                                            "DisplayHeight": 520.0,
-                                                            "DisplayWidth": 2260.0,
+                                                            "DisplayHeight": 547.0,
+                                                            "DisplayWidth": 2400.0,
                                                             "PersistentGroupedId": [
-                                                                "{6FDF698F-ACB9-4747-AA27-E23A3DA8FEC3}",
-                                                                "{1E80C9AA-C1A2-4313-9310-1EE8E5127DC8}",
-                                                                "{4B7B33DD-6E65-4A51-A4B7-F2E91BA9DAB4}",
+                                                                "{89868DAB-1712-4C78-AE33-0014BDC142EA}",
                                                                 "{282BB238-27B1-482D-A39E-A42B99C55EA9}",
-                                                                "{E008D29B-1194-4DD3-83CD-71A9BD9442C5}",
+                                                                "{4B7B33DD-6E65-4A51-A4B7-F2E91BA9DAB4}",
+                                                                "{C21F9316-8419-4C35-BAFE-536829F5C78A}",
+                                                                "{1E80C9AA-C1A2-4313-9310-1EE8E5127DC8}",
                                                                 "{496A1DBA-E33D-4AA7-AA49-C533AFA7E519}",
-                                                                "{C21F9316-8419-4C35-BAFE-536829F5C78A}"
+                                                                "{587F771B-FC38-4C05-8A31-6002D71FF144}"
                                                             ]
                                                         },
                                                         "{7CC444B1-F9B3-41B5-841B-0C4F2179F111}": {
                                                             "$type": "GeometrySaveData",
                                                             "Position": [
                                                                 -800.0,
-                                                                180.0
+                                                                160.0
                                                             ]
                                                         },
                                                         "{B0B99C8A-03AF-4CF6-A926-F65C874C3D97}": {
@@ -1155,7 +1300,7 @@
                         },
                         {
                             "Key": {
-                                "id": 63727806256927
+                                "id": 41300569639652
                             },
                             "Value": {
                                 "ComponentData": {
@@ -1198,7 +1343,7 @@
                         },
                         {
                             "Key": {
-                                "id": 63732101224223
+                                "id": 41304864606948
                             },
                             "Value": {
                                 "ComponentData": {
@@ -1228,7 +1373,7 @@
                         },
                         {
                             "Key": {
-                                "id": 63736396191519
+                                "id": 41309159574244
                             },
                             "Value": {
                                 "ComponentData": {
@@ -1258,7 +1403,7 @@
                         },
                         {
                             "Key": {
-                                "id": 63740691158815
+                                "id": 41313454541540
                             },
                             "Value": {
                                 "ComponentData": {
@@ -1292,7 +1437,7 @@
                         },
                         {
                             "Key": {
-                                "id": 63744986126111
+                                "id": 41317749508836
                             },
                             "Value": {
                                 "ComponentData": {
@@ -1306,7 +1451,7 @@
                                     "{7CC444B1-F9B3-41B5-841B-0C4F2179F111}": {
                                         "$type": "GeometrySaveData",
                                         "Position": [
-                                            1000.0,
+                                            1300.0,
                                             360.0
                                         ]
                                     },
@@ -1320,6 +1465,37 @@
                                     }
                                 }
                             }
+                        },
+                        {
+                            "Key": {
+                                "id": 47163199998692
+                            },
+                            "Value": {
+                                "ComponentData": {
+                                    "{24CB38BB-1705-4EC5-8F63-B574571B4DCD}": {
+                                        "$type": "NodeSaveData"
+                                    },
+                                    "{328FF15C-C302-458F-A43D-E1794DE0904E}": {
+                                        "$type": "GeneralNodeTitleComponentSaveData",
+                                        "PaletteOverride": "MethodNodeTitlePalette"
+                                    },
+                                    "{7CC444B1-F9B3-41B5-841B-0C4F2179F111}": {
+                                        "$type": "GeometrySaveData",
+                                        "Position": [
+                                            960.0,
+                                            360.0
+                                        ]
+                                    },
+                                    "{B0B99C8A-03AF-4CF6-A926-F65C874C3D97}": {
+                                        "$type": "StylingComponentSaveData",
+                                        "SubStyle": ".method"
+                                    },
+                                    "{B1F49A35-8408-40DA-B79E-F1E3B64322CE}": {
+                                        "$type": "PersistentIdComponentSaveData",
+                                        "PersistentId": "{89868DAB-1712-4C78-AE33-0014BDC142EA}"
+                                    }
+                                }
+                            }
                         }
                     ],
                     "StatisticsHelper": {
@@ -1342,7 +1518,7 @@
                             },
                             {
                                 "Key": 13774516393157610292,
-                                "Value": 1
+                                "Value": 2
                             }
                         ]
                     }

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott